本文脚本集成了从 Materials Project 拉取数据、自动下载所有竞争相的 POSCAR 文件、到生成带标签避让的交互式/静态三元相图的完整流程。 一行命令即可得到三元化合物体系相图。 示例:Li-P-S 三元体系 【依赖安装】 运行本脚本前,请确保安装以下 Python 库: pip install numpy matplotlib scipy plotly pymatgen 【API 密钥配置】 本脚本需要 Materials Project API 密钥才能获取数据。 【使用方法】 # 基本使用(默认 Li-P-S 体系) python super_unified_script.py # 自定义化学体系 python super_unified_script.py --system Li P S # 自定义图片尺寸和字体 python super_unified_script.py --fig-width 1800 --fig-height 1600 --title-size 28 # 使用 Matplotlib 静态图 python super_unified_script.py --matplotlib 【输出文件】 - phase_diagram_output.html # 交互式 HTML(Plotly) - phase_diagram_output.png # 静态图片 - phase/ # 竞争相结构文件夹 运行示例 $ python super_unified_script-.py 2026-05-0519:45:38,113 - INFO - ============================================================ 2026-05-0519:45:38,113 - INFO - Li-P-S 三元相图绘制开始 2026-05-0519:45:38,114 - INFO - ============================================================ 2026-05-0519:45:38,114 - INFO - 化学体系: ['Li', 'P', 'S'] 2026-05-0519:45:38,114 - INFO - 图片尺寸: 900 x 800 2026-05-0519:45:38,114 - INFO - 使用 Plotly 绘制交互式相图... 2026-05-0519:45:38,114 - INFO - 连接 Materials Project... 2026-05-0519:45:38,650 - INFO - 获取 ['Li', 'P', 'S'] 体系数据... Retrieving ThermoDoc documents: 100%|████████████████████████████████████████████████████████████████████████████████████████████████| 94/94 [00:00<00:00, 1288446.33it/s] 2026-05-0519:45:40,974 - INFO - 从 MP 获取到 94 个相 2026-05-0519:45:40,975 - INFO - 开始下载 94 个竞争相到 phase_Li_P_S/ 2026-05-0519:45:40,979 - INFO - 下载成功: Li_EaH_0.000 2026-05-0519:45:40,980 - INFO - 跳过(已存在): Li_EaH_0.000 2026-05-0519:45:40,980 - INFO - 跳过(已存在): Li_EaH_0.000 2026-05-0519:45:40,980 - INFO - 跳过(已存在): Li_EaH_0.000 2026-05-0519:45:40,980 - INFO - 跳过(已存在): Li_EaH_0.000 2026-05-0519:45:40,981 - INFO - 跳过(已存在): Li_EaH_0.000 2026-05-0519:45:40,981 - INFO - 跳过(已存在): Li_EaH_0.000 2026-05-0519:45:40,981 - INFO - 跳过(已存在): Li_EaH_0.000 2026-05-0519:45:40,981 - INFO - 跳过(已存在): Li_EaH_0.000 2026-05-0519:45:40,984 - INFO - 下载成功: LiP_EaH_0.000 2026-05-0519:45:40,990 - INFO - 下载成功: LiP7_EaH_0.000 2026-05-0519:45:40,995 - INFO - 下载成功: Li3P7_EaH_0.000 2026-05-0519:45:40,998 - INFO - 下载成功: LiP3_EaH_0.000 2026-05-0519:45:41,001 - INFO - 下载成功: Li3P_EaH_0.000 2026-05-0519:45:41,005 - INFO - 下载成功: LiP5_EaH_0.000 2026-05-0519:45:41,006 - INFO - 跳过(已存在): LiP5_EaH_0.000 2026-05-0519:45:41,010 - INFO - 下载成功: Li2S_EaH_0.000 2026-05-0519:45:41,013 - INFO - 下载成功: LiS4_EaH_0.000 2026-05-0519:45:41,016 - INFO - 下载成功: LiS_EaH_0.000 2026-05-0519:45:41,016 - INFO - 跳过(已存在): LiS_EaH_0.000 2026-05-0519:45:41,017 - INFO - 跳过(已存在): Li2S_EaH_0.000 2026-05-0519:45:41,017 - INFO - 跳过(已存在): Li2S_EaH_0.000 2026-05-0519:45:41,017 - INFO - 跳过(已存在): Li2S_EaH_0.000 2026-05-0519:45:41,017 - INFO - 跳过(已存在): LiS_EaH_0.000 2026-05-0519:45:41,017 - INFO - 跳过(已存在): Li2S_EaH_0.000 2026-05-0519:45:41,020 - INFO - 下载成功: P_EaH_0.000 2026-05-0519:45:41,020 - INFO - 跳过(已存在): P_EaH_0.000 2026-05-0519:45:41,020 - INFO - 跳过(已存在): P_EaH_0.000 2026-05-0519:45:41,020 - INFO - 跳过(已存在): P_EaH_0.000 2026-05-0519:45:41,021 - INFO - 跳过(已存在): P_EaH_0.000 2026-05-0519:45:41,021 - INFO - 跳过(已存在): P_EaH_0.000 2026-05-0519:45:41,021 - INFO - 跳过(已存在): P_EaH_0.000 2026-05-0519:45:41,021 - INFO - 跳过(已存在): P_EaH_0.000 2026-05-0519:45:41,021 - INFO - 跳过(已存在): P_EaH_0.000 2026-05-0519:45:41,021 - INFO - 跳过(已存在): P_EaH_0.000 2026-05-0519:45:41,021 - INFO - 跳过(已存在): P_EaH_0.000 2026-05-0519:45:41,021 - INFO - 跳过(已存在): P_EaH_0.000 2026-05-0519:45:41,022 - INFO - 跳过(已存在): P_EaH_0.000 2026-05-0519:45:41,022 - INFO - 跳过(已存在): P_EaH_0.000 2026-05-0519:45:41,025 - INFO - 下载成功: Li3PS4_EaH_0.000 2026-05-0519:45:41,028 - INFO - 下载成功: Li2PS3_EaH_0.000 2026-05-0519:45:41,033 - INFO - 下载成功: Li7P3S11_EaH_0.000 2026-05-0519:45:41,039 - INFO - 下载成功: Li7PS6_EaH_0.000 2026-05-0519:45:41,048 - INFO - 下载成功: Li48P16S61_EaH_0.000 2026-05-0519:45:41,048 - INFO - 跳过(已存在): Li3PS4_EaH_0.000 2026-05-0519:45:41,049 - INFO - 跳过(已存在): Li2PS3_EaH_0.000 2026-05-0519:45:41,049 - INFO - 跳过(已存在): Li3PS4_EaH_0.000 2026-05-0519:45:41,053 - INFO - 下载成功: P2S3_EaH_0.000 2026-05-0519:45:41,057 - INFO - 下载成功: P4S5_EaH_0.000 2026-05-0519:45:41,057 - INFO - 跳过(已存在): P2S3_EaH_0.000 2026-05-0519:45:41,062 - INFO - 下载成功: P2S7_EaH_0.000 2026-05-0519:45:41,068 - INFO - 下载成功: P4S7_EaH_0.000 2026-05-0519:45:41,068 - INFO - 跳过(已存在): P2S3_EaH_0.000 2026-05-0519:45:41,074 - INFO - 下载成功: P4S9_EaH_0.000 2026-05-0519:45:41,077 - INFO - 下载成功: P2S_EaH_0.000 2026-05-0519:45:41,081 - INFO - 下载成功: PS_EaH_0.000 2026-05-0519:45:41,082 - INFO - 跳过(已存在): P2S7_EaH_0.000 2026-05-0519:45:41,086 - INFO - 下载成功: P2S5_EaH_0.000 2026-05-0519:45:41,087 - INFO - 跳过(已存在): P4S9_EaH_0.000 2026-05-0519:45:41,087 - INFO - 跳过(已存在): P2S3_EaH_0.000 2026-05-0519:45:41,088 - INFO - 跳过(已存在): P4S5_EaH_0.000 2026-05-0519:45:41,094 - INFO - 下载成功: P4S3_EaH_0.000 2026-05-0519:45:41,094 - INFO - 跳过(已存在): P4S3_EaH_0.000 2026-05-0519:45:41,099 - INFO - 下载成功: S_EaH_0.000 2026-05-0519:45:41,100 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,100 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,100 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,101 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,101 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,101 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,101 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,101 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,101 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,102 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,102 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,102 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,102 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,102 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,102 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,103 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,103 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,103 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,103 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,103 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,103 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,104 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,104 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,104 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,104 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,104 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,104 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,105 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,105 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,105 - INFO - 跳过(已存在): S_EaH_0.000 2026-05-0519:45:41,105 - INFO - 下载完成:26/94 个相 2026-05-0519:45:41,105 - INFO - 创建相图对象... 2026-05-0519:45:41,126 - INFO - 稳定相数量: 15 2026-05-0519:45:47,299 - INFO - 保存 HTML: phase_diagram_output.html 2026-05-0519:45:47,510 - INFO - 保存 PNG: phase_diagram_output.png 2026-05-0519:45:52,641 - INFO - ====================================================================== 2026-05-0519:45:52,642 - INFO - 相图数据统计: 2026-05-0519:45:52,642 - INFO - ====================================================================== 2026-05-0519:45:52,642 - INFO - Li3P (0.250, 0.000) E=-3.4816 eV 2026-05-0519:45:52,642 - INFO - LiP7 (0.875, 0.000) E=-5.1305 eV 2026-05-0519:45:52,642 - INFO - P (1.000, 0.000) E=-5.4133 eV 2026-05-0519:45:52,642 - INFO - Li3P7 (0.700, 0.000) E=-4.7216 eV 2026-05-0519:45:52,642 - INFO - LiP (0.500, 0.000) E=-4.1844 eV 2026-05-0519:45:52,642 - INFO - Li (0.000, 0.000) E=-1.9089 eV 2026-05-0519:45:52,643 - INFO - Li2S (0.167, 0.289) E=-4.1552 eV 2026-05-0519:45:52,643 - INFO - P4S3 (0.786, 0.371) E=-5.2280 eV 2026-05-0519:45:52,643 - INFO - Li3PS4 (0.375, 0.433) E=-4.6433 eV 2026-05-0519:45:52,643 - INFO - P4S7 (0.682, 0.551) E=-5.0994 eV 2026-05-0519:45:52,643 - INFO - P4S9 (0.654, 0.600) E=-5.0466 eV 2026-05-0519:45:52,643 - INFO - P2S5 (0.643, 0.619) E=-5.0206 eV 2026-05-0519:45:52,643 - INFO - P2S7 (0.611, 0.674) E=-4.9322 eV 2026-05-0519:45:52,643 - INFO - LiS4 (0.400, 0.693) E=-4.3039 eV 2026-05-0519:45:52,643 - INFO - S (0.500, 0.866) E=-4.1364 eV 2026-05-0519:45:52,644 - INFO - 完成! 输出: phase_diagram_output.html, phase_diagram_output.png 2026-05-0519:45:52,644 - INFO - 脚本执行成功! 如果需要更为严格或不同泛函/计算参数的相图,可直接对所下载下来的相文件进行计算,然后再通过Doped的代码读取并储存不同化合物的能量,然后修改本文代码的文件读入,再重新绘图。 绘制图片图例如下 图片 脚本代码 #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ ============================================================================ 三元相图计算与可视化 - 超级统一脚本 ============================================================================ 【项目介绍】 本脚本用于绘制 Li-P-S 三元体系的相图,从 Materials Project 获取数据, 自动下载所有竞争相结构,并生成交互式/静态相图。 【核心功能】 1. 从 Materials Project 获取 Li-P-S 体系相图数据 2. 自动下载所有竞争相的 POSCAR 结构文件到 phase/ 文件夹 3. 绘制三元相图 (Plotly 交互式 / Matplotlib 静态) 4. Delaunay 三角剖分 Hull 连线 5. 标签自动避让算法(防止标签重叠) 6. 112 色颜色方案(支持 100+ 数据点) 【使用示例】 # 1. 基本使用(默认 Li-P-S 体系) python super_unified_script.py # 2. 自定义化学体系 python super_unified_script.py --system Li P S # 3. 交互式配置所有参数 python super_unified_script.py --config # 4. 自定义图片尺寸和字体 python super_unified_script.py --fig-width 1800 --fig-height 1600 --title-size 28 # 5. 使用 Matplotlib 静态图 python super_unified_script.py --matplotlib # 6. 关闭 Hull 连线 python super_unified_script.py --no-hull # 7. 查看所有参数 python super_unified_script.py --help 【输出文件】 - phase_diagram_output.html # 交互式 HTML(Plotly) - phase_diagram_output.png # 静态图片 - phase/ # 竞争相结构文件夹 【日期】2026-05-05 【版本】3.0 (Li-P-S 专用公开版) ============================================================================ 【依赖安装】 运行本脚本前,请确保安装以下 Python 库: # 核心依赖 pip install numpy matplotlib scipy plotly # Materials Project 相关(重要!) pip install pymatgen # 详细安装命令: pip install numpy matplotlib scipy plotly pymatgen # 如果遇到安装问题,尝试: pip install --upgrade pip pip install numpy matplotlib scipy plotly pymatgen 【API 密钥配置】 本脚本需要 Materials Project API 密钥才能获取数据。 获取方式: 1. 访问 https://next-gen.materialsproject.org/dashboard 2. 注册/登录账号 3. 在 Dashboard 页面复制 API Token 4. 将下方的 API_KEY 替换为你的密钥 注意: - API 密钥是免费的,但有使用限制 - 请勿与他人大规模共享你的 API 密钥 - 每个用户有默认的速率限制 ============================================================================ """ # ============================================================================ # 【第一部分】用户可配置参数(所有参数集中在此区域) # ============================================================================ # ---------- 1.1 API 密钥配置 ---------- # 【重要】请将下方的 API_KEY 替换为你自己的 Materials Project API 密钥 # 获取地址:https://next-gen.materialsproject.org/dashboard API_KEY = "你的API密钥在这里" # <-- 替换这里! # ---------- 1.2 化学体系配置 ---------- # 默认化学体系元素(三个元素构成三元相图) SYSTEM_ELEMENTS = ["Li", "P", "S"] # ---------- 1.3 显示控制 ---------- # 不稳定相显示阈值 (eV/atom) # -1 = 仅显示稳定相(能量在 convex hull 上) # 0.05 = 显示 0.05 eV/atom 内的不稳定相 # 0.1 = 显示 0.1 eV/atom 内的不稳定相 SHOW_UNSTABLE = -1 # ---------- 1.4 图片输出配置 ---------- # 输出文件名前缀(不含扩展名) OUTPUT_PREFIX = "phase_diagram_output" # 相结构下载目录(所有竞争相的 POSCAR 文件会下载到这里) # 文件夹名称包含元素集合,防止不同任务数据混合 PHASE_FOLDER = "phase" # 图片尺寸(像素) FIG_WIDTH = 900 FIG_HEIGHT = 800 # 图片分辨率(DPI) OUTPUT_DPI = 150 # ---------- 1.5 字体大小配置 ---------- # 标题字体大小 TITLE_FONT_SIZE = 28 # 元素标签字体大小(三角形三个顶点的 Li, P, S) ELEMENT_FONT_SIZE = 36 # 数据点标签字体大小(各化合物名称) LABEL_FONT_SIZE = 16 # ---------- 1.6 标记样式配置 ---------- # 数据点大小(像素) MARKER_SIZE = 25 # 数据点边框宽度 MARKER_LINE_WIDTH = 2 # ---------- 1.7 颜色方案(112 色) ---------- # 从色轮均匀分布的 112 种颜色,支持 100+ 数据点 # 颜色格式:16 进制 (RRGGBB) COLOR_PALETTE = [ # 第1组:基础色轮 16色 "#FF0000", "#FF8800", "#FFDD00", "#00FF00", "#00FFCC", "#00BBFF", "#0066FF", "#8800FF", "#FF00AA", "#FF0044", "#AAFF00", "#00FF88", "#00DDFF", "#4400FF", "#DD00FF", "#FF4400", # 第2组:偏移30度 16色 "#FF3333", "#FF9933", "#FFEE33", "#33FF33", "#33FFCC", "#33CCFF", "#3388FF", "#9933FF", "#FF33AA", "#FF3388", "#99FF33", "#33FF99", "#33EEFF", "#5533FF", "#EE33FF", "#FF5533", # 第3组:偏移60度 16色 "#FF6666", "#FFAA66", "#FFFF66", "#66FF66", "#66FFCC", "#66DDFF", "#66AAFF", "#AA66FF", "#FF66CC", "#FF6666", "#AAFF66", "#66FFAA", "#66EEFF", "#6644FF", "#FF66FF", "#FF6644", # 第4组:浅色调 16色 "#FFAAAA", "#FFCCAA", "#FFFFAA", "#AAFFAA", "#AAFFCC", "#AAEEFF", "#AACCFF", "#CCAAFF", "#FFAAEE", "#FFAAAA", "#CCFFAA", "#AAFFCC", "#AAEEFF", "#AAAFFF", "#FFAAFF", "#FFCCAA", # 第5组:深色调 16色 "#CC0000", "#CC6600", "#CCCC00", "#00CC00", "#00CCCC", "#0099CC", "#0033CC", "#6600CC", "#CC0099", "#CC0033", "#99CC00", "#00CC66", "#0099CC", "#3300CC", "#CC00CC", "#CC3300", # 第6组:浅色调216色 "#FFDDDD", "#FFEEDD", "#FFFFDD", "#DDFFDD", "#DDFFEE", "#DDEEFF", "#DDCCFF", "#EEDDFF", "#FFDDFF", "#FFDDCC", "#EEFFDD", "#DDFFEE", "#DDEEFF", "#CCDDFF", "#FFDDFF", "#FFEECC", # 第7组:暗色调 8色 "#880000", "#884400", "#888800", "#008800", "#008888", "#004488", "#440088", "#880044", # 第8组:亮色调 8色 "#FFBBBB", "#FFDDAA", "#FFFFBB", "#BBFFBB", "#BBFFDD", "#BBDDFF", "#BBCCFF", "#DDBBFF", ] # ---------- 1.8 Hull 连线配置 ---------- # 是否显示 Hull 连线(数据点之间的三角剖分连线) SHOW_HULL_LINES = True # Hull 连线颜色 HULL_LINE_COLOR = "gray" # Hull 连线宽度(像素) HULL_LINE_WIDTH = 1.5 # ---------- 1.9 图例与坐标轴配置 ---------- # 是否显示图例 SHOW_LEGEND = False # ---------- 1.10 标签样式配置 ---------- # 标签最小间距(用于避让算法) LABEL_MARGIN = 0.12 # 标签背景颜色(白色半透明) LABEL_BACKGROUND = 'rgba(255,255,255,0.9)' # 标签边框宽度 LABEL_BORDER_WIDTH = 1 # ---------- 1.11 绘图引擎配置 ---------- # True = 使用 Plotly(生成交互式 HTML) # False = 使用 Matplotlib(生成静态 PNG) # 默认使用 Matplotlib,因为用户更喜欢其绘图风格 USE_PLOTLY = False # ============================================================================ # 【第二部分】导入必要的库 # ============================================================================ import argparse # 命令行参数解析库 import logging # 日志记录库 import sys # 系统操作库 import os # 文件路径操作库 from pathlib import Path # 面向对象的路径操作库 from datetime import datetime # 日期时间处理库 import numpy as np # 数值计算库(用于坐标转换和三角剖分) import matplotlib # matplotlib 绑图主库 matplotlib.use('Agg') # 使用非 GUI 后端(仅生成文件,不显示窗口) import matplotlib.pyplot as plt # matplotlib 绑图模块 from pymatgen.ext.matproj import MPRester # Materials Project API 接口 from pymatgen.analysis.phase_diagram import PhaseDiagram # 相图分析类 from scipy.spatial import Delaunay # Delaunay 三角剖分算法 import plotly.graph_objects as go # Plotly 交互式绑图对象 # ============================================================================ # 【第三部分】辅助函数 # ============================================================================ def setup_logging(): """ 配置日志记录器 功能说明: 日志同时输出到文件和控制台 日志文件名按时间戳自动生成,格式:log_YYYYMMDD_HHMMSS.log 返回: logging.Logger: 配置好的日志记录器 """ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") # 获取当前时间戳 log_file = f"log_{timestamp}.log" # 日志文件名 log_format = '%(asctime)s - %(levelname)s - %(message)s' # 日志格式 logging.basicConfig( level=logging.INFO, # 记录 INFO 级别及以上的日志 format=log_format, # 日志格式字符串 handlers=[ logging.FileHandler(log_file, encoding='utf-8'), # 输出到文件(支持中文) logging.StreamHandler(sys.stdout) # 输出到控制台 ] ) return logging.getLogger(__name__) # 返回日志记录器实例 def coord_to_cartesian(comp_dict, elems=None): """ 将化合物组成转换为三元相图笛卡尔坐标 物理背景: 三元相图使用等边三角形坐标系: - 顶点 A (元素1,如 Li): (0, 0) - 顶点 B (元素2,如 P): (1, 0) - 顶点 C (元素3,如 S): (0.5, √3/2) 坐标转换公式: 对于化合物 A_x B_y C_z: 1. 计算总原子数:total = x + y + z 2. 计算摩尔分数:f_A = x/total, f_B = y/total, f_C = z/total 3. 转换为笛卡尔坐标: X = f_B + 0.5 × f_C Y = (√3/2) × f_C 参数: comp_dict: 元素计数字典,格式 {元素符号: 原子数} 例如:{"Li": 2, "P": 1, "S": 4} elems: 元素顺序列表,默认为 ["Li", "P", "S"] 返回: tuple: (x, y) 笛卡尔坐标,范围 [0, 1] 计算示例: Li3PS4(磷硫化锂): - 组成:Li=3, P=1, S=4,总原子数=8 - f_Li = 3/8 = 0.375 - f_P = 1/8 = 0.125 - f_S = 4/8 = 0.5 - X = 0.125 + 0.5×0.5 = 0.375 - Y = 0.866 × 0.5 = 0.433 """ if elems is None: elems = SYSTEM_ELEMENTS total = sum(comp_dict.values()) # 计算总原子数 if total == 0: return0.5, 0.5 # 处理空组成情况 # 计算各元素的摩尔分数 fracs = {e: comp_dict.get(e, 0) / total for e in elems} # 转换为笛卡尔坐标 x = fracs.get(elems[1], 0) + 0.5 * fracs.get(elems[2], 0) y = (np.sqrt(3) / 2) * fracs.get(elems[2], 0) return x, y def find_best_label_position(px, py, occupied, margin=0.12): """ 标签自动避让算法 算法原理: 1. 定义 8 个候选标签位置(相对于数据点的偏移量) 2. 检查每个位置是否在三角形边界内(不会被裁剪) 3. 计算到最近已占用位置的距离 4. 选择距离已占用位置最远的有效位置 参数: px: 数据点的 x 坐标 py: 数据点的 y 坐标 occupied: 已占用位置列表,格式 [(x, y, label_text), ...] margin: 最小间距阈值 返回: tuple: (位置名称, 标签x坐标, 标签y坐标) 位置名称说明: 'top left': 左上方偏移 'top right': 右上方偏移 'bottom left': 左下方偏移 'bottom right': 右下方偏移 'middle left': 正左方偏移 'middle right': 正右方偏移 'top center': 正上方偏移 'bottom center': 正下方偏移 """ # 8 个候选位置(名称,水平偏移,垂直偏移) positions = [ ('top left', -0.12, 0.10), ('top right', 0.12, 0.10), ('bottom left', -0.12, -0.08), ('bottom right', 0.12, -0.08), ('middle left', -0.15, 0), ('middle right', 0.15, 0), ('top center', 0, 0.12), ('bottom center', 0, -0.10), ] # 初始化最佳位置 best_pos, best_x, best_y = 'top left', px - 0.12, py + 0.10 best_dist = -1 # 遍历所有候选位置 for pos_name, ox, oy in positions: test_x, test_y = px + ox, py + oy # 边界检查:确保标签在三角形内(不会被裁剪) if test_y < -0.05or test_y > 0.95: continue if test_x < -0.05or test_x > 1.05: continue # 计算到最近已占用位置的距离 ifnot occupied: min_dist = 999 else: min_dist = min( ((test_x - pox)**2 + (test_y - poy)**2)**0.5 for (pox, poy, _) in occupied ) # 选择距离已占用位置最远的有效位置 if min_dist > best_dist: best_dist = min_dist best_pos, best_x, best_y = pos_name, test_x, test_y return best_pos, best_x, best_y def allocate_colors(phases): """ 为每个相分配唯一颜色 原理说明: 使用模运算循环使用颜色池中的颜色 颜色按色轮角度均匀分布,确保相邻颜色有足够区分度 参数: phases: 相数据列表 返回: list: 每个相增加 'color' 字段 """ for i, phase in enumerate(phases): phase['color'] = COLOR_PALETTE[i % len(COLOR_PALETTE)] return phases def calculate_label_positions(phases): """ 计算每个相的标签位置 算法流程: 1. 按 Y 坐标排序(从下到上),下方先放置标签 2. 遍历每个相,调用 find_best_label_position 3. 将放置好的位置加入已占用列表 参数: phases: 相数据列表 返回: list: 每个相增加 'label_pos', 'label_x', 'label_y' 字段 """ occupied = [] # 已占用位置列表 sorted_phases = sorted(phases, key=lambda p: p['y']) # 按 Y 坐标排序 for phase in sorted_phases: pos, lx, ly = find_best_label_position( phase['x'], phase['y'], occupied, margin=LABEL_MARGIN ) phase['label_pos'] = pos phase['label_x'] = lx phase['label_y'] = ly occupied.append((lx, ly, phase['formula'])) return phases def download_phases(entries, phase_folder, logger): """ 下载所有竞争相的 POSCAR 结构文件 功能说明: 将 Materials Project 获取的所有相的结构文件保存到本地文件夹 每个相保存在单独的子文件夹中 参数: entries: pymatgen 的 ComputedStructureEntry 列表 phase_folder: 下载目录路径 logger: 日志记录器 返回: int: 成功下载的相数量 """ phase_path = Path(phase_folder) phase_path.mkdir(exist_ok=True) # 创建主文件夹 logger.info(f"开始下载 {len(entries)} 个竞争相到 {phase_folder}/") downloaded = 0 for entry in entries: try: # 生成安全的文件夹名称(处理特殊字符) safe_name = entry.name.replace(" ", "_").replace("/", "_").replace("(", "").replace(")", "") # 获取能量 above hull eah = entry.data.get("energy_above_hull", 0) eah_str = f"{eah:.3f}" # 创建子文件夹名称:化学式_EaH_能量 folder_name = f"{safe_name}_EaH_{eah_str}" phase_dir = phase_path / folder_name # 如果文件夹已存在且包含 POSCAR,跳过 if phase_dir.exists() and (phase_dir / "POSCAR").exists(): logger.info(f" 跳过(已存在): {folder_name}") continue phase_dir.mkdir(exist_ok=True) # 保存 POSCAR 文件 entry.structure.to(filename=str(phase_dir / "POSCAR")) downloaded += 1 logger.info(f" 下载成功: {folder_name}") except Exception as e: logger.warning(f" 下载失败: {entry.name} - {str(e)}") logger.info(f"下载完成:{downloaded}/{len(entries)} 个相") return downloaded def get_phase_data(system, api_key, logger, download=True): """ 从 Materials Project 获取相图数据 功能说明: 1. 连接 Materials Project REST API 2. 获取指定化学体系的所有条目 3. 可选:下载所有竞争相的 POSCAR 文件 4. 创建 pymatgen PhaseDiagram 对象 5. 提取稳定相数据 参数: system: 元素列表,如 ["Li", "P", "S"] api_key: Materials Project API 密钥 logger: 日志记录器 download: 是否下载竞争相结构文件 返回: tuple: (PhaseDiagram对象, 稳定相列表) 稳定相列表格式: [{'formula': str, 'x': float, 'y': float, 'e_per_atom': float}, ...] """ logger.info("连接 Materials Project...") # 创建 API 连接 mpr = MPRester(api_key=api_key) logger.info(f"获取 {system} 体系数据...") entries = mpr.get_entries_in_chemsys(system) logger.info(f"从 MP 获取到 {len(entries)} 个相") # 可选:下载所有相的结构文件 # 文件夹名称包含元素集合,防止不同任务数据混合 if download: phase_folder = f"phase_{'_'.join(system)}" download_phases(entries, phase_folder, logger) # 创建相图对象 logger.info("创建相图对象...") pd = PhaseDiagram(entries) logger.info(f"稳定相数量: {len(pd.stable_entries)}") # 提取稳定相数据 phases = [] for entry in pd.stable_entries: comp_dict = entry.composition.as_dict() formula = entry.composition.reduced_formula x, y = coord_to_cartesian(comp_dict, system) e_per_atom = pd.get_hull_energy(entry.composition) / entry.composition.num_atoms phases.append({ 'formula': formula, 'x': x, 'y': y, 'e_per_atom': e_per_atom, 'comp_dict': comp_dict }) # 按 Y 坐标排序(从下到上) phases.sort(key=lambda p: p['y']) return pd, phases # ============================================================================ # 【第四部分】Plotly 绘图函数 # ============================================================================ def draw_triangle_boundary_plotly(fig, elems): """ 绘制三元相图的三角形边界和元素标签(Plotly 版本) 参数: fig: Plotly Figure 对象 elems: 元素列表,如 ["Li", "P", "S"] """ # 等边三角形的三个顶点坐标 triangle_x = [0, 1, 0.5, 0] # 第四个点是闭合三角形 triangle_y = [0, 0, np.sqrt(3)/2, 0] # 绘制三角形边界线 fig.add_trace(go.Scatter( x=triangle_x, y=triangle_y, mode='lines', # 只画线,不画点 line=dict(color='black', width=4), # 黑色线,宽4像素 name='边界', hoverinfo='skip' # 悬停时不显示信息 )) # 绘制元素标签 # Li 在左下角 (0, -0.12) # P 在右下角 (1, -0.12) # S 在顶部 (0.5, √3/2 + 0.14) fig.add_trace(go.Scatter( x=[0, 1, 0.5], y=[-0.12, -0.12, np.sqrt(3)/2 + 0.14], mode='text', # 只显示文本 text=[f"{elems[0]}", f"{elems[1]}", f"{elems[2]}"], textfont=dict(size=ELEMENT_FONT_SIZE, color='black'), name='元素标签', hoverinfo='skip' )) def draw_hull_lines_plotly(fig, phases): """ 绘制 Delaunay 三角剖分连线(Plotly 版本) 物理意义: Delaunay 三角剖分将所有数据点连接成三角形网格 这些连线表示相邻相之间的能量关系 密集区域表示可能的反应路径 技术说明: Delaunay 三角剖分的特点: - 任意三角形的外接圆内不包含其他点 - 三角形尽可能接近等边 - 适合展示点之间的邻接关系 参数: fig: Plotly Figure 对象 phases: 相数据列表 """ ifnot SHOW_HULL_LINES: return points = np.array([[p['x'], p['y']] for p in phases]) if len(points) < 3: return tri = Delaunay(points) hull_x, hull_y = [], [] # 遍历每个三角形 for simplex in tri.simplices: for i in range(3): hull_x.extend([points[simplex[i], 0], points[simplex[(i+1) % 3], 0], np.nan]) hull_y.extend([points[simplex[i], 1], points[simplex[(i+1) % 3], 1], np.nan]) fig.add_trace(go.Scatter( x=hull_x, y=hull_y, mode='lines', line=dict(color=HULL_LINE_COLOR, width=HULL_LINE_WIDTH), name='Hull连线', hoverinfo='skip' )) def draw_phases_plotly(fig, phases): """ 绘制所有相的数据点和标签(Plotly 版本) 参数: fig: Plotly Figure 对象 phases: 相数据列表 """ for phase in phases: color = phase['color'] # 绘制数据点 fig.add_trace(go.Scatter( x=[phase['x']], y=[phase['y']], mode='markers', marker=dict( size=MARKER_SIZE, # 点的大小 color=color, # 点的颜色 line=dict(color='white', width=MARKER_LINE_WIDTH) # 白色边框 ), name=phase['formula'], hovertemplate=f"{phase['formula']}
E = {phase['e_per_atom']:.4f} eV/atom", showlegend=SHOW_LEGEND )) # 添加标签 fig.add_annotation( x=phase['label_x'], y=phase['label_y'], text=f"{phase['formula']}", showarrow=False, # 不显示箭头 font=dict(size=LABEL_FONT_SIZE, color=color), bgcolor=LABEL_BACKGROUND, bordercolor=color, borderwidth=LABEL_BORDER_WIDTH, borderpad=3, xref='x', yref='y' ) def plot_ternary_plotly(system, api_key, output_prefix, logger, download=True): """ 使用 Plotly 绘制交互式三元相图 Plotly 特点: - 生成交互式 HTML,可放大缩小 - 支持悬停查看详情 - 适合网页展示和分享 参数: system: 元素列表 api_key: MP API 密钥 output_prefix: 输出文件前缀 logger: 日志记录器 download: 是否下载竞争相 """ logger.info("使用 Plotly 绘制交互式相图...") # 获取相图数据 pd, phases = get_phase_data(system, api_key, logger, download=download) # 分配颜色 phases = allocate_colors(phases) # 计算标签位置 phases = calculate_label_positions(phases) # 创建 Figure 对象 fig = go.Figure() # 绘制三角形边界 draw_triangle_boundary_plotly(fig, system) # 绘制 Hull 连线 draw_hull_lines_plotly(fig, phases) # 绘制数据点和标签 draw_phases_plotly(fig, phases) # 生成标题字符串 system_str = "-".join(system) unstable_str = "stable only"if SHOW_UNSTABLE < 0else f"unstable<{SHOW_UNSTABLE}" # 设置布局 fig.update_layout( title=dict( text=f"{system_str} 三元相图
{unstable_str} | {len(phases)} phases", font=dict(size=TITLE_FONT_SIZE), x=0.5, xanchor='center' ), xaxis=dict( range=[-0.2, 1.2], showgrid=False, zeroline=False, showticklabels=False ), yaxis=dict( range=[-0.25, 1.1], showgrid=False, zeroline=False, showticklabels=False, scaleanchor='x', # X 和 Y 轴等比例 scaleratio=1 ), showlegend=SHOW_LEGEND, plot_bgcolor='white', width=FIG_WIDTH, height=FIG_HEIGHT, margin=dict(l=100, r=50, t=120, b=100) ) # 保存文件 html_file = f"{output_prefix}.html" png_file = f"{output_prefix}.png" logger.info(f"保存 HTML: {html_file}") fig.write_html(html_file) logger.info(f"保存 PNG: {png_file}") fig.write_image(png_file, scale=2) # 输出统计信息 logger.info("=" * 70) logger.info("相图数据统计:") logger.info("=" * 70) for phase in phases: logger.info(f" {phase['formula']:<12} ({phase['x']:.3f}, {phase['y']:.3f}) E={phase['e_per_atom']:.4f} eV") logger.info(f"\n完成! 输出: {html_file}, {png_file}") # ============================================================================ # 【第五部分】Matplotlib 绘图函数 # ============================================================================ def draw_triangle_boundary_mpl(ax, elems): """ 绘制三元相图的三角形边界和元素标签(Matplotlib 版本) 参数: ax: matplotlib Axes 对象 elems: 元素列表 """ triangle_x, triangle_y = [0, 1, 0.5, 0], [0, 0, np.sqrt(3)/2, 0] ax.plot(triangle_x, triangle_y, 'k-', linewidth=2) ax.text(0, -0.08, elems[0], fontsize=ELEMENT_FONT_SIZE, ha='center', fontweight='bold') ax.text(1, -0.08, elems[1], fontsize=ELEMENT_FONT_SIZE, ha='center', fontweight='bold') ax.text(0.5, np.sqrt(3)/2 + 0.1, elems[2], fontsize=ELEMENT_FONT_SIZE, ha='center', fontweight='bold') def draw_hull_lines_mpl(ax, phases): """ 绘制 Delaunay 三角剖分连线(Matplotlib 版本) 参数: ax: matplotlib Axes 对象 phases: 相数据列表 """ ifnot SHOW_HULL_LINES: return points = np.array([[p['x'], p['y']] for p in phases]) if len(points) < 3: return tri = Delaunay(points) for simplex in tri.simplices: for i in range(3): ax.plot( [points[simplex[i], 0], points[simplex[(i+1) % 3], 0]], [points[simplex[i], 1], points[simplex[(i+1) % 3], 1]], color=HULL_LINE_COLOR, linewidth=HULL_LINE_WIDTH ) def draw_phases_mpl(ax, phases): """ 绘制数据点和标签(Matplotlib 版本) 参数: ax: matplotlib Axes 对象 phases: 相数据列表 """ for phase in phases: ax.plot(phase['x'], phase['y'], 'o', markersize=MARKER_SIZE/3, color=phase['color']) ax.annotate( phase['formula'], (phase['label_x'], phase['label_y']), fontsize=LABEL_FONT_SIZE, color=phase['color'], fontweight='bold', bbox=dict(boxstyle='round,pad=0.3', facecolor='white', alpha=0.8, edgecolor=phase['color']) ) def plot_ternary_matplotlib(system, api_key, output_prefix, logger, download=True): """ 使用 Matplotlib 绘制静态三元相图 Matplotlib 特点: - 生成静态图片 (PNG, PDF, SVG) - 适合论文发表 - 可精确控制每个元素 参数: system: 元素列表 api_key: MP API 密钥 output_prefix: 输出文件前缀 logger: 日志记录器 download: 是否下载竞争相 """ logger.info("使用 Matplotlib 绘制静态相图...") # 获取相图数据 pd, phases = get_phase_data(system, api_key, logger, download=download) # 分配颜色 phases = allocate_colors(phases) # 计算标签位置 phases = calculate_label_positions(phases) # 创建图形 fig, ax = plt.subplots(1, 1, figsize=(FIG_WIDTH/100, FIG_HEIGHT/100)) # 绘制三角形边界 draw_triangle_boundary_mpl(ax, system) # 绘制 Hull 连线 draw_hull_lines_mpl(ax, phases) # 绘制数据点 draw_phases_mpl(ax, phases) # 设置坐标轴 ax.set_xlim(-0.15, 1.15) ax.set_ylim(-0.2, 1.05) ax.set_aspect('equal') ax.axis('off') # 设置标题 system_str = "-".join(system) ax.set_title(f"{system_str} Ternary Phase Diagram", fontsize=TITLE_FONT_SIZE, fontweight='bold') plt.tight_layout() # 保存 png_file = f"{output_prefix}.png" logger.info(f"保存 PNG: {png_file}") plt.savefig(png_file, dpi=OUTPUT_DPI, bbox_inches='tight', facecolor='white') plt.close() logger.info(f"\n完成! 输出: {png_file}") # ============================================================================ # 【第六部分】交互式配置向导 # ============================================================================ def interactive_config(): """ 交互式配置向导 功能说明: 通过命令行交互,让用户逐步设置各种参数 每一步都有默认值和建议值 返回: dict: 用户配置的参数字典 """ print("\n" + "=" * 60) print(" Li-P-S 三元相图绘制 - 交互式配置向导") print("=" * 60) config = {} # 配置项 1:化学体系 print("\n【1/6】化学体系设置") print(f" 默认: {SYSTEM_ELEMENTS}") elem1 = input(f" 元素1 (默认 {SYSTEM_ELEMENTS[0]}): ").strip() or SYSTEM_ELEMENTS[0] elem2 = input(f" 元素2 (默认 {SYSTEM_ELEMENTS[1]}): ").strip() or SYSTEM_ELEMENTS[1] elem3 = input(f" 元素3 (默认 {SYSTEM_ELEMENTS[2]}): ").strip() or SYSTEM_ELEMENTS[2] config['system'] = [elem1, elem2, elem3] # 配置项 2:图片尺寸 print("\n【2/6】图片尺寸设置") width = input(f" 宽度像素 (默认 {FIG_WIDTH}): ").strip() config['fig_width'] = int(width) if width else FIG_WIDTH height = input(f" 高度像素 (默认 {FIG_HEIGHT}): ").strip() config['fig_height'] = int(height) if height else FIG_HEIGHT # 配置项 3:字体大小 print("\n【3/6】字体大小设置") title = input(f" 标题字体 (默认 {TITLE_FONT_SIZE}): ").strip() config['title_size'] = int(title) if title else TITLE_FONT_SIZE label = input(f" 标签字体 (默认 {LABEL_FONT_SIZE}): ").strip() config['label_size'] = int(label) if label else LABEL_FONT_SIZE # 配置项 4:显示选项 print("\n【4/6】显示选项") hull = input(" 显示 Hull 连线? (Y/n): ").strip().lower() config['show_hull'] = (hull != 'n') legend = input(" 显示图例? (y/N): ").strip().lower() config['show_legend'] = (legend == 'y') marker = input(f" 标记大小 (默认 {MARKER_SIZE}): ").strip() config['marker_size'] = int(marker) if marker else MARKER_SIZE # 配置项 5:绘图引擎 print("\n【5/6】绘图引擎") engine = input(" 使用 Plotly (交互式 HTML)? (Y/n): ").strip().lower() config['use_plotly'] = (engine != 'n') # 配置项 6:输出设置 print("\n【6/6】输出设置") prefix = input(f" 文件前缀 (默认 {OUTPUT_PREFIX}): ").strip() config['output_prefix'] = prefix or OUTPUT_PREFIX print("\n" + "=" * 60) print("配置完成!") print("=" * 60) return config # ============================================================================ # 【第七部分】主函数 # ============================================================================ def main(): """ 主函数入口 功能流程: 1. 解析命令行参数 2. 设置日志 3. 处理交互式配置(如有) 4. 执行绘图 5. 输出结果 """ # 在函数开头声明所有将使用的全局变量 global FIG_WIDTH, FIG_HEIGHT, TITLE_FONT_SIZE, LABEL_FONT_SIZE global MARKER_SIZE, SHOW_HULL_LINES, SHOW_LEGEND, USE_PLOTLY global SYSTEM_ELEMENTS, PHASE_FOLDER parser = argparse.ArgumentParser( description=""" ============================================================================ Li-P-S 三元相图绘制脚本 v3.0 ============================================================================ 功能: - 从 Materials Project 获取 Li-P-S 体系相图数据 - 自动下载所有竞争相结构到 phase_元素集合/ 文件夹 - 绘制三元相图(默认 Matplotlib 静态模式) - Delaunay 三角剖分 Hull 连线 - 标签自动避让算法 - 112 色颜色方案 示例: python super_unified_script.py # 使用默认参数 python super_unified_script.py --config # 交互式配置 python super_unified_script.py --system Li P S # 自定义体系 python super_unified_script.py --plotly # 使用 Plotly 交互式 ============================================================================ """, formatter_class=argparse.RawDescriptionHelpFormatter ) # 添加命令行参数 parser.add_argument("--system", nargs="+", default=SYSTEM_ELEMENTS, help="化学体系元素,如: Li P S") parser.add_argument("--api-key", default=API_KEY, help="Materials Project API 密钥") parser.add_argument("--output", "-o", default=OUTPUT_PREFIX, help="输出文件前缀") parser.add_argument("--config", action="store_true", help="启用交互式配置向导") parser.add_argument("--fig-width", type=int, default=FIG_WIDTH, help="图片宽度 (像素)") parser.add_argument("--fig-height", type=int, default=FIG_HEIGHT, help="图片高度 (像素)") parser.add_argument("--title-size", type=int, default=TITLE_FONT_SIZE, help="标题字体大小") parser.add_argument("--label-size", type=int, default=LABEL_FONT_SIZE, help="标签字体大小") parser.add_argument("--marker-size", type=int, default=MARKER_SIZE, help="标记大小") parser.add_argument("--no-hull", action="store_true", help="不显示 Hull 连线") parser.add_argument("--legend", action="store_true", help="显示图例") parser.add_argument("--matplotlib", action="store_true", help="使用 Matplotlib(已废弃,默认即Matplotlib)") parser.add_argument("--plotly", action="store_true", help="使用 Plotly 交互式模式") parser.add_argument("--no-download", action="store_true", help="不下载竞争相结构文件") args = parser.parse_args() logger = setup_logging() logger.info("=" * 60) logger.info("Li-P-S 三元相图绘制开始") logger.info("=" * 60) logger.info(f"化学体系: {args.system}") logger.info(f"图片尺寸: {args.fig_width} x {args.fig_height}") # 交互式配置模式 if args.config: config = interactive_config() args.system = config.get('system', args.system) args.fig_width = config.get('fig_width', args.fig_width) args.fig_height = config.get('fig_height', args.fig_height) args.title_size = config.get('title_size', args.title_size) args.label_size = config.get('label_size', args.label_size) args.marker_size = config.get('marker_size', args.marker_size) args.output = config.get('output_prefix', args.output) args.show_hull = config.get('show_hull', True) args.show_legend = config.get('show_legend', False) args.use_plotly = config.get('use_plotly', True) # 更新全局变量 FIG_WIDTH = args.fig_width FIG_HEIGHT = args.fig_height TITLE_FONT_SIZE = args.title_size LABEL_FONT_SIZE = args.label_size MARKER_SIZE = args.marker_size SHOW_HULL_LINES = not args.no_hull SHOW_LEGEND = args.legend # 如果指定了 --plotly,则使用 Plotly;否则默认使用 Matplotlib USE_PLOTLY = args.plotly if args.plotly elsenot args.matplotlib SYSTEM_ELEMENTS = args.system # 执行绘图 try: if USE_PLOTLY: plot_ternary_plotly(args.system, args.api_key, args.output, logger, download=not args.no_download) else: plot_ternary_matplotlib(args.system, args.api_key, args.output, logger, download=not args.no_download) logger.info("脚本执行成功!") except Exception as e: logger.error(f"执行失败: {e}") import traceback logger.error(traceback.format_exc()) sys.exit(1) # ============================================================================ # 程序入口 # ============================================================================ if __name__ == "__main__": main()