本文脚本集成了从 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()
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()