本文脚本集成了从 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 获取数据,
自动下载所有竞争相结构,并生成交互式/静态相图。
【核心功能】
-
从 Materials Project 获取 Li-P-S 体系相图数据
-
自动下载所有竞争相的 POSCAR 结构文件到 phase/ 文件夹
-
绘制三元相图 (Plotly 交互式 / Matplotlib 静态)
-
Delaunay 三角剖分 Hull 连线
-
标签自动避让算法(防止标签重叠)
-
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 密钥才能获取数据。
获取方式:
-
注册/登录账号
-
在 Dashboard 页面复制 API Token
-
将下方的 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:
-
计算总原子数:total = x + y + z
-
计算摩尔分数:f_A = x/total, f_B = y/total, f_C = z/total
-
转换为笛卡尔坐标:
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):
"""
标签自动避让算法
算法原理:
-
定义 8 个候选标签位置(相对于数据点的偏移量)
-
检查每个位置是否在三角形边界内(不会被裁剪)
-
计算到最近已占用位置的距离
-
选择距离已占用位置最远的有效位置
参数:
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):
"""
计算每个相的标签位置
算法流程:
-
按 Y 坐标排序(从下到上),下方先放置标签
-
遍历每个相,调用 find_best_label_position
-
将放置好的位置加入已占用列表
参数:
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 获取相图数据
功能说明:
-
连接 Materials Project REST API
-
获取指定化学体系的所有条目
-
可选:下载所有竞争相的 POSCAR 文件
-
创建 pymatgen PhaseDiagram 对象
-
提取稳定相数据
参数:
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"<b>{elems[0]}</b>",
f"<b>{elems[1]}</b>",
f"<b>{elems[2]}</b>"],
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"<b>{phase['formula']}</b><br>E = {phase['e_per_atom']:.4f} eV/atom",
showlegend=SHOW_LEGEND
))
添加标签
fig.add_annotation(
x=phase['label_x'], y=phase['label_y'],
text=f"<b>{phase['formula']}</b>",
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"<b>{system_str} 三元相图</b><br><sup>{unstable_str} | {len(phases)} phases</sup>",
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():
"""
主函数入口
功能流程:
-
解析命令行参数
-
设置日志
-
处理交互式配置(如有)
-
执行绘图
-
输出结果
"""
在函数开头声明所有将使用的全局变量
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()