chemphase 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
chemphase/__init__.py ADDED
@@ -0,0 +1,35 @@
1
+ """chemphase — 相图与化学势热图生成器
2
+
3
+ 一键从 Materials Project API 或本地 VASP 计算结果生成:
4
+ - 二元/三元成分相图(含 Hull 连线、标签自动避让)
5
+ - 化学势热图
6
+ - 结构对比报告
7
+ """
8
+
9
+ __version__ = "0.1.0"
10
+
11
+
12
+ def __getattr__(name):
13
+ """惰性导入:仅在实际使用子模块时才导入"""
14
+ if name == "PhaseDiagramConfig":
15
+ from chemphase.core import PhaseDiagramConfig
16
+ return PhaseDiagramConfig
17
+ if name == "generate_binary_composition_diagram":
18
+ from chemphase.diagrams import generate_binary_composition_diagram
19
+ return generate_binary_composition_diagram
20
+ if name == "generate_ternary_composition_diagram":
21
+ from chemphase.diagrams import generate_ternary_composition_diagram
22
+ return generate_ternary_composition_diagram
23
+ if name == "main":
24
+ from chemphase.cli import main
25
+ return main
26
+ raise AttributeError(f"module 'chemphase' has no attribute '{name}'")
27
+
28
+
29
+ __all__ = [
30
+ "__version__",
31
+ "PhaseDiagramConfig",
32
+ "generate_binary_composition_diagram",
33
+ "generate_ternary_composition_diagram",
34
+ "main",
35
+ ]
chemphase/__main__.py ADDED
@@ -0,0 +1,9 @@
1
+ """chemphase 命令行入口
2
+
3
+ 支持 python -m chemphase 调用
4
+ """
5
+
6
+ from chemphase.cli import main
7
+
8
+ if __name__ == "__main__":
9
+ main()
chemphase/cli.py ADDED
@@ -0,0 +1,205 @@
1
+ """命令行接口 (CLI)
2
+
3
+ 安装后通过终端命令 `chemphase` 或 `python -m chemphase` 调用
4
+ """
5
+
6
+ import sys
7
+ import argparse
8
+ from pathlib import Path
9
+ from itertools import combinations
10
+
11
+ from chemphase.core import (
12
+ PhaseDiagramConfig, validate_elements, ensure_api_key,
13
+ scan_local_phases, build_local_entries, structure_comparison_report,
14
+ get_entries_from_mp, get_system_name, get_all_element_combinations,
15
+ print_banner, DEFAULT_ELEMENTS,
16
+ )
17
+ from chemphase.diagrams import (
18
+ generate_binary_composition_diagram,
19
+ generate_ternary_composition_diagram,
20
+ )
21
+
22
+
23
+ def run_local_mode(config: PhaseDiagramConfig, elements, output_root: str):
24
+ """本地数据模式:读取 VASP 计算结果生成相图"""
25
+ print(f"\n扫描本地目录: {config.local_dir}")
26
+ local_phases = scan_local_phases(config.local_dir, elements, config.eah_threshold)
27
+ print(f"找到 {len(local_phases)} 个相关相")
28
+
29
+ if not local_phases:
30
+ print("没有找到有效的相数据")
31
+ return
32
+
33
+ # 结构对比
34
+ if config.compare_structure:
35
+ print("\n" + "=" * 60)
36
+ print(" 结构对比分析")
37
+ print("=" * 60)
38
+ report = structure_comparison_report(local_phases, config)
39
+ print(f" 总共对比: {report['total_compared']}")
40
+ print(f" 结构匹配: {report['matching']}")
41
+ print(f" 结构不同: {report['different']}")
42
+ print(f" MP未收录: {report['not_found_in_mp']}")
43
+
44
+ import json
45
+ report_file = Path(output_root) / "structure_comparison_report.json"
46
+ report_file.parent.mkdir(parents=True, exist_ok=True)
47
+ with open(report_file, 'w') as f:
48
+ json.dump(report, f, indent=2, sort_keys=True)
49
+ print(f" 报告已保存: {report_file}")
50
+
51
+ entries = build_local_entries(local_phases)
52
+ print(f"\n构建了 {len(entries)} 个PhaseDiagramEntry")
53
+
54
+ output_dir = Path(output_root)
55
+ output_dir.mkdir(parents=True, exist_ok=True)
56
+
57
+ binary_combos = list(combinations(elements, 2))
58
+ ternary_combos = list(combinations(elements, 3))
59
+
60
+ # 二元相图
61
+ print("\n" + "=" * 60)
62
+ print(" 生成二元相图")
63
+ print("=" * 60)
64
+ binary_dir = output_dir / "binary"
65
+ binary_dir.mkdir(parents=True, exist_ok=True)
66
+ for e1, e2 in binary_combos:
67
+ sub_entries = [e for e in entries if set(e.composition.as_dict().keys()) <= {e1, e2}]
68
+ if len(sub_entries) >= 2:
69
+ generate_binary_composition_diagram(sub_entries, (e1, e2), binary_dir, "本地数据")
70
+
71
+ # 三元相图
72
+ print("\n" + "=" * 60)
73
+ print(" 生成三元相图")
74
+ print("=" * 60)
75
+ ternary_dir = output_dir / "ternary"
76
+ ternary_dir.mkdir(parents=True, exist_ok=True)
77
+ for e1, e2, e3 in ternary_combos:
78
+ sub_entries = [e for e in entries if set(e.composition.as_dict().keys()) <= {e1, e2, e3}]
79
+ if len(sub_entries) >= 3:
80
+ generate_ternary_composition_diagram(sub_entries, (e1, e2, e3), ternary_dir, "本地数据")
81
+
82
+ print("\n" + "=" * 60)
83
+ print(" 本地数据模式完成")
84
+ print("=" * 60)
85
+ print(f"输出目录: {output_dir}")
86
+
87
+
88
+ def run_api_mode(config: PhaseDiagramConfig, elements, output_root: str):
89
+ """API 下载模式:从 Materials Project 下载数据生成相图"""
90
+ print(f"\n从Materials Project下载 {get_system_name(elements)} 体系数据")
91
+
92
+ output_dir = Path(output_root)
93
+ output_dir.mkdir(parents=True, exist_ok=True)
94
+
95
+ binary_combos = list(combinations(elements, 2))
96
+ ternary_combos = list(combinations(elements, 3))
97
+
98
+ # 二元相图
99
+ print("\n" + "=" * 60)
100
+ print(" 生成二元相图")
101
+ print("=" * 60)
102
+ binary_dir = output_dir / "binary"
103
+ binary_dir.mkdir(parents=True, exist_ok=True)
104
+ for e1, e2 in binary_combos:
105
+ try:
106
+ entries = get_entries_from_mp(config, [e1, e2])
107
+ if len(entries) >= 2:
108
+ from pymatgen.analysis.phase_diagram import PDEntry
109
+ pd_entries = [PDEntry(composition=e.composition, energy=e.energy, name=e.name)
110
+ for e in entries]
111
+ generate_binary_composition_diagram(pd_entries, (e1, e2), binary_dir, "MP数据")
112
+ except Exception as e:
113
+ print(f" - {e1}-{e2}: {e}")
114
+
115
+ # 三元相图
116
+ print("\n" + "=" * 60)
117
+ print(" 生成三元相图")
118
+ print("=" * 60)
119
+ ternary_dir = output_dir / "ternary"
120
+ ternary_dir.mkdir(parents=True, exist_ok=True)
121
+ for e1, e2, e3 in ternary_combos:
122
+ try:
123
+ entries = get_entries_from_mp(config, [e1, e2, e3])
124
+ if len(entries) >= 3:
125
+ from pymatgen.analysis.phase_diagram import PDEntry
126
+ pd_entries = [PDEntry(composition=e.composition, energy=e.energy, name=e.name)
127
+ for e in entries]
128
+ generate_ternary_composition_diagram(pd_entries, (e1, e2, e3), ternary_dir, "MP数据")
129
+ except Exception as e:
130
+ print(f" - {e1}-{e2}-{e3}: {e}")
131
+
132
+ print("\n" + "=" * 60)
133
+ print(" API模式完成")
134
+ print("=" * 60)
135
+ print(f"输出目录: {output_dir}")
136
+
137
+
138
+ def main():
139
+ """主入口函数"""
140
+ parser = argparse.ArgumentParser(
141
+ prog="chemphase",
142
+ description="相图与化学势热图统一生成器 v5.0",
143
+ formatter_class=argparse.RawDescriptionHelpFormatter,
144
+ epilog="""
145
+ 使用示例:
146
+ # API模式 - 下载CuAgOSe数据(默认)
147
+ chemphase
148
+
149
+ # 指定API模式的元素
150
+ chemphase --elements Li O Co
151
+
152
+ # 本地模式 - 读取VASP计算结果
153
+ chemphase --local /path/to/calculations --elements Cu Ag O Se
154
+
155
+ # 混合模式 - 本地数据为主
156
+ chemphase --local /path/to/data --elements Cu Ag O Se
157
+
158
+ # 启用结构对比
159
+ chemphase --local /path/to/data --elements Cu Ag O Se --compare-structure
160
+ """
161
+ )
162
+ parser.add_argument('--local', type=str, help='本地VASP计算结果目录')
163
+ parser.add_argument('--elements', nargs='+', help='目标元素列表(如 Cu Ag O Se)')
164
+ parser.add_argument('--output', default='phase_diagrams_output', help='输出目录 (默认: phase_diagrams_output)')
165
+ parser.add_argument('--eah', type=float, default=0.05, help='Energy above hull 阈值 (默认: 0.05 eV/atom)')
166
+ parser.add_argument('--compare-structure', action='store_true', help='启用本地结构 vs MP数据库对比')
167
+ parser.add_argument('--debug', action='store_true', help='调试模式')
168
+
169
+ args = parser.parse_args()
170
+
171
+ print_banner()
172
+
173
+ config = PhaseDiagramConfig(
174
+ eah_threshold=args.eah,
175
+ output_root=args.output,
176
+ local_dir=Path(args.local) if args.local else None,
177
+ compare_structure=args.compare_structure,
178
+ debug=args.debug
179
+ )
180
+
181
+ if args.local:
182
+ if not args.elements:
183
+ print("错误: 本地模式需要指定 --elements")
184
+ sys.exit(1)
185
+ ok, valid, errors = validate_elements(args.elements)
186
+ if not ok:
187
+ print("错误: " + ", ".join(errors))
188
+ sys.exit(1)
189
+ run_local_mode(config, valid, args.output)
190
+ else:
191
+ ensure_api_key(config)
192
+ elements = args.elements if args.elements else DEFAULT_ELEMENTS
193
+ ok, valid, errors = validate_elements(elements)
194
+ if not ok:
195
+ print("错误: " + ", ".join(errors))
196
+ sys.exit(1)
197
+ run_api_mode(config, valid, args.output)
198
+
199
+ print("\n" + "=" * 60)
200
+ print(" 执行完成")
201
+ print("=" * 60)
202
+
203
+
204
+ if __name__ == "__main__":
205
+ main()
chemphase/core.py ADDED
@@ -0,0 +1,369 @@
1
+ """核心模块:配置管理、VASP 数据解析、Materials Project API、结构对比"""
2
+
3
+ import os
4
+ import re
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Dict, List, Optional, Tuple, Any
9
+ from itertools import combinations
10
+
11
+ # ============================================================
12
+ # 元素验证
13
+ # ============================================================
14
+ VALID_ELEMENTS = {
15
+ 'H', 'He', 'Li', 'Be', 'B', 'C', 'N', 'O', 'F', 'Ne',
16
+ 'Na', 'Mg', 'Al', 'Si', 'P', 'S', 'Cl', 'Ar', 'K', 'Ca',
17
+ 'Sc', 'Ti', 'V', 'Cr', 'Mn', 'Fe', 'Co', 'Ni', 'Cu', 'Zn',
18
+ 'Ga', 'Ge', 'As', 'Se', 'Br', 'Kr', 'Rb', 'Sr', 'Y', 'Zr',
19
+ 'Nb', 'Mo', 'Tc', 'Ru', 'Rh', 'Pd', 'Ag', 'Cd', 'In', 'Sn',
20
+ 'Sb', 'Te', 'I', 'Xe', 'Cs', 'Ba', 'La', 'Ce', 'Pr', 'Nd',
21
+ 'Pm', 'Sm', 'Eu', 'Gd', 'Tb', 'Dy', 'Ho', 'Er', 'Tm', 'Yb',
22
+ 'Lu', 'Hf', 'Ta', 'W', 'Re', 'Os', 'Ir', 'Pt', 'Au', 'Hg',
23
+ 'Tl', 'Pb', 'Bi', 'Po', 'At', 'Rn', 'Fr', 'Ra', 'Ac', 'Th',
24
+ 'Pa', 'U', 'Np', 'Pu', 'Am', 'Cm', 'Bk', 'Cf', 'Es', 'Fm',
25
+ 'Md', 'No', 'Lr', 'Rf', 'Db', 'Sg', 'Bh', 'Hs', 'Mt', 'Ds',
26
+ 'Rg', 'Cn', 'Nh', 'Fl', 'Mc', 'Lv', 'Ts', 'Og'
27
+ }
28
+
29
+ EAH_THRESHOLD = 0.05 # 能量 above hull 阈值 (eV/atom)
30
+ OUTPUT_ROOT = "phase_diagrams_output"
31
+ UNIFIED_PHASES_DIR = "unified_phases"
32
+ DEFAULT_ELEMENTS = ["Cu", "Ag", "O", "Se"]
33
+ STRUCTURE_TOLERANCE = 0.2 # 结构匹配容忍度
34
+
35
+
36
+ def validate_elements(elements: List[str]) -> Tuple[bool, List[str], List[str]]:
37
+ """验证元素符号是否有效"""
38
+ errors = []
39
+ valid = []
40
+ for elem in elements:
41
+ elem = elem.strip().capitalize()
42
+ if elem in VALID_ELEMENTS:
43
+ valid.append(elem)
44
+ else:
45
+ errors.append(f"'{elem}' 不是有效元素符号")
46
+ return len(errors) == 0, valid, errors
47
+
48
+
49
+ # ============================================================
50
+ # 配置类
51
+ # ============================================================
52
+ class PhaseDiagramConfig:
53
+ """相图生成配置"""
54
+
55
+ def __init__(self, api_key: str = "", eah_threshold: float = EAH_THRESHOLD,
56
+ output_root: str = OUTPUT_ROOT, unified_phases_dir: str = UNIFIED_PHASES_DIR,
57
+ debug: bool = False, local_dir: Optional[Path] = None,
58
+ compare_structure: bool = False):
59
+ self.api_key = api_key or os.environ.get("MATERIALS_PROJECT_API_KEY", "")
60
+ self.eah_threshold = eah_threshold
61
+ self.output_root = output_root
62
+ self.unified_phases_dir = unified_phases_dir
63
+ self.debug = debug
64
+ self.local_dir = local_dir
65
+ self.compare_structure = compare_structure
66
+
67
+ @property
68
+ def database_file(self) -> Path:
69
+ return Path(self.unified_phases_dir) / "phases_database.json"
70
+
71
+
72
+ def ensure_api_key(config: PhaseDiagramConfig) -> str:
73
+ """确保 API 密钥可用,优先从环境变量读取"""
74
+ if not config.api_key:
75
+ print("\n" + "=" * 60)
76
+ print(" 请设置 Materials Project API 密钥")
77
+ print("=" * 60)
78
+ print(" 获取方式: https://materialsproject.org/api")
79
+ print(" 环境变量: export MATERIALS_PROJECT_API_KEY=你的密钥")
80
+ print()
81
+ key = input(" 请输入API密钥: ").strip()
82
+ if not key:
83
+ print(" 未输入密钥,退出")
84
+ sys.exit(0)
85
+ config.api_key = key
86
+ print(" ⚠ 密钥仅本次有效,建议设置环境变量 MATERIALS_PROJECT_API_KEY")
87
+ return config.api_key
88
+
89
+
90
+ # ============================================================
91
+ # 本地 VASP 数据解析
92
+ # ============================================================
93
+
94
+ def parse_energy_from_vasprun(vasprun_path: str, debug: bool = False) -> Optional[float]:
95
+ """从 vasprun.xml 解析最终能量"""
96
+ try:
97
+ from pymatgen.io.vasp.outputs import Vasprun
98
+ vr = Vasprun(vasprun_path)
99
+ return vr.final_energy
100
+ except Exception as e:
101
+ if debug:
102
+ print(f" vasprun.xml解析失败: {e}")
103
+ return None
104
+
105
+
106
+ def parse_energy_from_outcar(outcar_path: str, debug: bool = False) -> Optional[float]:
107
+ """从 OUTCAR 解析最终能量"""
108
+ try:
109
+ with open(outcar_path, 'r') as f:
110
+ content = f.read()
111
+ matches = re.findall(r'energy\(sigma->0\)\s*=\s*([-\d.]+)', content)
112
+ if matches:
113
+ return float(matches[-1])
114
+ except Exception as e:
115
+ if debug:
116
+ print(f" OUTCAR解析失败: {e}")
117
+ return None
118
+
119
+
120
+ def parse_structure_info(poscar_path: str, debug: bool = False) -> Tuple[Optional[str], Optional[int]]:
121
+ """从 POSCAR 解析化学式和原子数"""
122
+ try:
123
+ from pymatgen.core import Structure
124
+ structure = Structure.from_file(poscar_path)
125
+ return structure.composition.reduced_formula, structure.num_atoms
126
+ except Exception as e:
127
+ if debug:
128
+ print(f" POSCAR解析失败: {e}")
129
+ return None, None
130
+
131
+
132
+ def parse_phase_directory(phase_dir: Path, eah_threshold: float = EAH_THRESHOLD) -> Optional[Dict]:
133
+ """解析单个 VASP 计算目录"""
134
+ dir_name = phase_dir.name
135
+ name_match = re.match(r'(.+?)_EaH_([\d.]+)(_stable)?', dir_name)
136
+ if not name_match:
137
+ return None
138
+
139
+ formula_from_name = name_match.group(1)
140
+ eah = float(name_match.group(2))
141
+ is_stable = name_match.group(3) == '_stable' or eah < eah_threshold
142
+
143
+ poscar = phase_dir / "POSCAR"
144
+ vasprun = phase_dir / "vasprun.xml"
145
+ outcar = phase_dir / "OUTCAR"
146
+
147
+ if not poscar.exists():
148
+ return None
149
+
150
+ formula, num_atoms = parse_structure_info(str(poscar))
151
+ if not formula:
152
+ formula = formula_from_name
153
+
154
+ energy = None
155
+ if vasprun.exists():
156
+ energy = parse_energy_from_vasprun(str(vasprun))
157
+ elif outcar.exists():
158
+ energy = parse_energy_from_outcar(str(outcar))
159
+
160
+ if energy is None:
161
+ return None
162
+
163
+ energy_per_atom = energy / num_atoms if num_atoms else None
164
+ from pymatgen.core import Composition
165
+ elements = list(set(Composition(formula).as_dict().keys()))
166
+
167
+ return {
168
+ 'name': dir_name,
169
+ 'formula': formula,
170
+ 'energy': energy,
171
+ 'energy_per_atom': energy_per_atom,
172
+ 'num_atoms': num_atoms,
173
+ 'eah': eah,
174
+ 'is_stable': is_stable,
175
+ 'elements': elements,
176
+ 'path': str(phase_dir)
177
+ }
178
+
179
+
180
+ def scan_local_phases(local_dir: Path, target_elements: Optional[List[str]] = None,
181
+ eah_threshold: float = EAH_THRESHOLD) -> List[Dict]:
182
+ """扫描本地目录中的所有 VASP 计算"""
183
+ if not local_dir.exists():
184
+ print(f"目录不存在: {local_dir}")
185
+ return []
186
+
187
+ phases = []
188
+ target_set = set(target_elements) if target_elements else None
189
+
190
+ for phase_dir in local_dir.iterdir():
191
+ if not phase_dir.is_dir():
192
+ continue
193
+ phase_data = parse_phase_directory(phase_dir, eah_threshold)
194
+ if phase_data is None:
195
+ continue
196
+ if target_set:
197
+ phase_elements = set(phase_data['elements'])
198
+ if not phase_elements <= target_set:
199
+ continue
200
+ phases.append(phase_data)
201
+
202
+ return phases
203
+
204
+
205
+ def build_local_entries(phases: List[Dict]) -> List:
206
+ """从本地相数据构建 pymatgen PDEntry 列表"""
207
+ from pymatgen.core import Structure
208
+ from pymatgen.analysis.phase_diagram import PDEntry
209
+
210
+ entries = []
211
+ for phase in phases:
212
+ try:
213
+ structure = Structure.from_file(phase['path'] + "/POSCAR")
214
+ entry = PDEntry(
215
+ composition=structure.composition,
216
+ energy=phase['energy'],
217
+ name=phase['name']
218
+ )
219
+ entries.append(entry)
220
+ except Exception as e:
221
+ print(f" 构建Entry失败 {phase['name']}: {e}")
222
+ return entries
223
+
224
+
225
+ # ============================================================
226
+ # 结构对比功能
227
+ # ============================================================
228
+
229
+ def compare_structures(local_structure, mp_structure,
230
+ tolerance: float = STRUCTURE_TOLERANCE) -> Dict:
231
+ """比较两个晶体结构的相似度"""
232
+ from pymatgen.analysis.structure_matcher import StructureMatcher
233
+ matcher = StructureMatcher(ltol=tolerance, stol=tolerance, angle_tol=10)
234
+ try:
235
+ is_same = matcher.fit(local_structure, mp_structure)
236
+ return {
237
+ 'is_same': is_same,
238
+ 'local_formula': local_structure.composition.reduced_formula,
239
+ 'mp_formula': mp_structure.composition.reduced_formula,
240
+ 'local_sites': len(local_structure.sites),
241
+ 'mp_sites': len(mp_structure.sites),
242
+ 'local_volume': local_structure.volume,
243
+ 'mp_volume': mp_structure.volume,
244
+ 'volume_diff_percent': abs(local_structure.volume - mp_structure.volume) / mp_structure.volume * 100
245
+ }
246
+ except Exception as e:
247
+ return {
248
+ 'is_same': False,
249
+ 'error': str(e),
250
+ 'local_formula': local_structure.composition.reduced_formula,
251
+ 'mp_formula': mp_structure.composition.reduced_formula
252
+ }
253
+
254
+
255
+ def structure_comparison_report(local_phases: List[Dict], config: PhaseDiagramConfig) -> Dict:
256
+ """生成结构对比报告"""
257
+ from pymatgen.ext.matproj import MPRester
258
+ from pymatgen.core import Structure
259
+
260
+ report = {
261
+ 'total_compared': 0,
262
+ 'matching': 0,
263
+ 'different': 0,
264
+ 'not_found_in_mp': 0,
265
+ 'details': []
266
+ }
267
+
268
+ if not config.api_key:
269
+ print(" ! 无API密钥,跳过结构对比")
270
+ return report
271
+
272
+ try:
273
+ mpr = MPRester(api_key=config.api_key)
274
+ except Exception as e:
275
+ print(f" ! 无法连接Materials Project: {e}")
276
+ return report
277
+
278
+ for phase in local_phases:
279
+ formula = phase['formula']
280
+ report['total_compared'] += 1
281
+
282
+ try:
283
+ mp_entries = mpr.get_entries(formula, inc_structure=True)
284
+ if not mp_entries:
285
+ report['not_found_in_mp'] += 1
286
+ report['details'].append({
287
+ 'formula': formula,
288
+ 'status': 'not_found_in_mp',
289
+ 'message': f'在MP数据库中未找到 {formula}'
290
+ })
291
+ continue
292
+
293
+ local_structure = Structure.from_file(phase['path'] + "/POSCAR")
294
+ mp_entry = mp_entries[0]
295
+ mp_structure = mp_entry.structure
296
+
297
+ comparison = compare_structures(local_structure, mp_structure)
298
+ comparison['formula'] = formula
299
+ comparison['local_path'] = phase['path']
300
+
301
+ if comparison['is_same']:
302
+ report['matching'] += 1
303
+ comparison['status'] = 'matching'
304
+ else:
305
+ report['different'] += 1
306
+ comparison['status'] = 'different'
307
+
308
+ report['details'].append(comparison)
309
+
310
+ except Exception as e:
311
+ report['not_found_in_mp'] += 1
312
+ report['details'].append({
313
+ 'formula': formula,
314
+ 'status': 'error',
315
+ 'message': str(e)
316
+ })
317
+
318
+ return report
319
+
320
+
321
+ # ============================================================
322
+ # 工具函数
323
+ # ============================================================
324
+
325
+ def get_all_element_combinations(elements: List[str], min_size: int = 2) -> List:
326
+ """获取元素的所有组合(从 min_size 到全部)"""
327
+ result = []
328
+ for r in range(min_size, len(elements) + 1):
329
+ for combo in combinations(elements, r):
330
+ result.append(combo)
331
+ return result
332
+
333
+
334
+ def load_database(config: PhaseDiagramConfig) -> Dict:
335
+ """加载本地相数据库"""
336
+ Path(config.unified_phases_dir).mkdir(parents=True, exist_ok=True)
337
+ if config.database_file.exists():
338
+ with open(config.database_file, 'r') as f:
339
+ return json.load(f)
340
+ return {"phases": {}, "download_history": [], "element_systems": {}}
341
+
342
+
343
+ def save_database(config: PhaseDiagramConfig, db: Dict):
344
+ """保存本地相数据库"""
345
+ Path(config.unified_phases_dir).mkdir(parents=True, exist_ok=True)
346
+ with open(config.database_file, 'w') as f:
347
+ json.dump(db, f, indent=2, sort_keys=True)
348
+
349
+
350
+ def get_entries_from_mp(config: PhaseDiagramConfig, elements: List[str]):
351
+ """从 Materials Project 下载条目"""
352
+ from pymatgen.ext.matproj import MPRester
353
+ mpr = MPRester(api_key=config.api_key)
354
+ return mpr.get_entries_in_chemsys(elements)
355
+
356
+
357
+ def get_system_name(elements: List[str]) -> str:
358
+ """获取体系名称(排序后的元素用下划线连接)"""
359
+ return "_".join(sorted(elements))
360
+
361
+
362
+ def print_banner():
363
+ """打印启动横幅"""
364
+ print("""
365
+ ╔═══════════════════════════════════════════════════════════════════════════╗
366
+ ║ 相图与化学势热图统一生成器 v5.0 (chemphase) ║
367
+ ║ Phase Diagram & Chemical Potential Heatmap Generator ║
368
+ ╚═══════════════════════════════════════════════════════════════════════════╝
369
+ """)
chemphase/diagrams.py ADDED
@@ -0,0 +1,280 @@
1
+ """相图生成模块:二元/三元成分相图 + Hull 连线"""
2
+
3
+ import numpy as np
4
+ import matplotlib
5
+ matplotlib.use('Agg')
6
+ import matplotlib.pyplot as plt
7
+ from scipy.spatial import Delaunay
8
+ from pathlib import Path
9
+ from typing import List, Tuple
10
+
11
+ import plotly.graph_objects as go
12
+ from pymatgen.analysis.phase_diagram import PhaseDiagram
13
+
14
+ from chemphase.plotting import (
15
+ coord_to_cartesian, allocate_colors, calculate_label_positions,
16
+ )
17
+
18
+ # ============================================================
19
+ # 绘图参数
20
+ # ============================================================
21
+ MARKER_SIZE = 25
22
+ MARKER_LINE_WIDTH = 2
23
+ TITLE_FONT_SIZE = 20
24
+ LABEL_BACKGROUND = 'rgba(255,255,255,0.9)'
25
+
26
+ BINARY_FIG_WIDTH = 12
27
+ BINARY_FIG_HEIGHT = 10
28
+ BINARY_LABEL_FONT_SIZE = 14
29
+ BINARY_XLABEL_FONT_SIZE = 14
30
+ BINARY_YLABEL_FONT_SIZE = 14
31
+ BINARY_HULL_LINE_COLOR = "gray"
32
+ BINARY_HULL_LINE_WIDTH = 1.5
33
+ BINARY_DPI = 150
34
+
35
+ TERNARY_FIG_WIDTH = 1000
36
+ TERNARY_FIG_HEIGHT = 900
37
+ TERNARY_LABEL_FONT_SIZE = 14
38
+ TERNARY_TITLE_FONT_SIZE = 20
39
+ TERNARY_HULL_LINE_COLOR = "gray"
40
+ TERNARY_HULL_LINE_WIDTH = 1.5
41
+ TERNARY_SHOW_HULL = True
42
+ TERNARY_LABEL_MARGIN = 0.12
43
+
44
+
45
+ # ============================================================
46
+ # 二元成分相图
47
+ # ============================================================
48
+
49
+ def generate_binary_composition_diagram(entries, elements: Tuple[str, str],
50
+ output_dir: Path, title_suffix: str = "") -> bool:
51
+ """生成二元成分相图(ΔE vs 成分,含 Hull 连线)
52
+
53
+ Parameters
54
+ ----------
55
+ entries : list of pymatgen PDEntry
56
+ 热力学条目列表
57
+ elements : tuple of (str, str)
58
+ 两种元素符号
59
+ output_dir : Path
60
+ 输出目录
61
+ title_suffix : str
62
+ 标题后缀(如 "MP数据" 或 "本地数据")
63
+
64
+ Returns
65
+ -------
66
+ bool : 是否成功生成
67
+ """
68
+ try:
69
+ pd = PhaseDiagram(entries)
70
+ phases = []
71
+ base_energy = None
72
+
73
+ for entry in pd.stable_entries:
74
+ comp_dict = entry.composition.as_dict()
75
+ formula = entry.composition.reduced_formula
76
+ total = sum(comp_dict.values())
77
+ x = comp_dict.get(elements[0], 0) / total if total > 0 else 0.5
78
+ e_per_atom = pd.get_hull_energy(entry.composition) / entry.composition.num_atoms
79
+
80
+ if base_energy is None or e_per_atom < base_energy:
81
+ base_energy = e_per_atom
82
+
83
+ phases.append({
84
+ 'formula': formula, 'x': x, 'e_per_atom': e_per_atom,
85
+ 'comp_dict': comp_dict
86
+ })
87
+
88
+ for phase in phases:
89
+ phase['delta_e'] = phase['e_per_atom'] - base_energy
90
+
91
+ phases = allocate_colors(phases)
92
+ fig, ax = plt.subplots(figsize=(BINARY_FIG_WIDTH, BINARY_FIG_HEIGHT))
93
+
94
+ ax.set_xlim(-0.05, 1.05)
95
+ y_max = max(p['delta_e'] for p in phases) if phases else 0.5
96
+ y_min = min(min(p['delta_e'] for p in phases), -0.05) if phases else -0.1
97
+ ax.set_ylim(y_min - 0.15, y_max + 0.5)
98
+ ax.set_xlabel(f'Composition (x in {elements[0]}$_{{1-x}}${elements[1]}$_x$)',
99
+ fontsize=BINARY_XLABEL_FONT_SIZE)
100
+ ax.set_ylabel('ΔE (eV/atom)', fontsize=BINARY_YLABEL_FONT_SIZE)
101
+ ax.spines['top'].set_visible(False)
102
+ ax.spines['right'].set_visible(False)
103
+
104
+ # Hull 连线(Delaunay 三角剖分)
105
+ if len(phases) >= 3:
106
+ points = np.array([[p['x'], p['delta_e']] for p in phases])
107
+ try:
108
+ tri = Delaunay(points)
109
+ for simplex in tri.simplices:
110
+ for i in range(3):
111
+ ax.plot([points[simplex[i], 0], points[simplex[(i + 1) % 3], 0]],
112
+ [points[simplex[i], 1], points[simplex[(i + 1) % 3], 1]],
113
+ '-', color=BINARY_HULL_LINE_COLOR, linewidth=BINARY_HULL_LINE_WIDTH)
114
+ except Exception:
115
+ pass
116
+
117
+ # 标签避让
118
+ occupied = []
119
+ sorted_phases = sorted(phases, key=lambda p: p['delta_e'])
120
+ for phase in sorted_phases:
121
+ best_y_offset = 0.05
122
+ for y_offset_test in np.arange(0.02, 0.25, 0.02):
123
+ overlaps = any(abs(ey - (phase['delta_e'] + y_offset_test)) < 0.05
124
+ for (ex, ey, ef) in occupied)
125
+ if not overlaps:
126
+ best_y_offset = y_offset_test
127
+ break
128
+ phase['label_y_offset'] = best_y_offset
129
+ occupied.append((phase['x'], phase['delta_e'] + best_y_offset, phase['formula']))
130
+
131
+ for phase in phases:
132
+ ax.plot(phase['x'], phase['delta_e'], 'o', markersize=MARKER_SIZE / 3,
133
+ color=phase['color'])
134
+ ax.annotate(phase['formula'],
135
+ (phase['x'], phase['delta_e'] + phase['label_y_offset']),
136
+ xytext=(0, 5), textcoords='offset points',
137
+ fontsize=BINARY_LABEL_FONT_SIZE,
138
+ color=phase['color'], fontweight='bold', ha='center',
139
+ bbox=dict(boxstyle='round,pad=0.3', facecolor='white',
140
+ alpha=0.8, edgecolor=phase['color']))
141
+
142
+ system_str = f"{elements[0]}-{elements[1]}"
143
+ suffix = f" ({title_suffix})" if title_suffix else ""
144
+ ax.set_title(f'{system_str}{suffix}', fontsize=TITLE_FONT_SIZE, fontweight='bold')
145
+ plt.tight_layout()
146
+
147
+ filename = f"binary_{elements[0]}-{elements[1]}_phase.png"
148
+ filepath = output_dir / filename
149
+ plt.savefig(str(filepath), dpi=BINARY_DPI, bbox_inches='tight')
150
+ plt.close('all')
151
+ print(f" + {filename}")
152
+ return True
153
+ except Exception as e:
154
+ print(f" - {elements[0]}-{elements[1]}: {e}")
155
+ return False
156
+
157
+
158
+ # ============================================================
159
+ # 三元成分相图
160
+ # ============================================================
161
+
162
+ def draw_triangle_boundary(fig, elems: Tuple[str, str, str]):
163
+ """绘制三元相图的三角形边界"""
164
+ triangle_x = [0, 1, 0.5, 0]
165
+ triangle_y = [0, 0, np.sqrt(3) / 2, 0]
166
+ fig.add_trace(go.Scatter(
167
+ x=triangle_x, y=triangle_y, mode='lines',
168
+ line=dict(color='black', width=4), name='边界', hoverinfo='skip'
169
+ ))
170
+ fig.add_trace(go.Scatter(
171
+ x=[0, 1, 0.5], y=[-0.12, -0.12, np.sqrt(3) / 2 + 0.14],
172
+ mode='text', text=[f"<b>{elems[0]}</b>", f"<b>{elems[1]}</b>", f"<b>{elems[2]}</b>"],
173
+ textfont=dict(size=24, color='black'), name='元素标签', hoverinfo='skip'
174
+ ))
175
+
176
+
177
+ def draw_hull_lines(fig, phases: List[dict]):
178
+ """绘制三元相图的 Hull 连线(Delaunay 三角剖分)"""
179
+ if not TERNARY_SHOW_HULL:
180
+ return
181
+ points = np.array([[p['x'], p['y']] for p in phases])
182
+ if len(points) < 3:
183
+ return
184
+ tri = Delaunay(points)
185
+ hull_x, hull_y = [], []
186
+ for simplex in tri.simplices:
187
+ for i in range(3):
188
+ hull_x.extend([points[simplex[i], 0], points[simplex[(i + 1) % 3], 0], np.nan])
189
+ hull_y.extend([points[simplex[i], 1], points[simplex[(i + 1) % 3], 1], np.nan])
190
+ fig.add_trace(go.Scatter(
191
+ x=hull_x, y=hull_y, mode='lines',
192
+ line=dict(color=TERNARY_HULL_LINE_COLOR, width=TERNARY_HULL_LINE_WIDTH),
193
+ name='Hull连线', hoverinfo='skip'
194
+ ))
195
+
196
+
197
+ def draw_phases(fig, phases: List[dict]):
198
+ """在三元相图上绘制相点和标签"""
199
+ for phase in phases:
200
+ fig.add_trace(go.Scatter(
201
+ x=[phase['x']], y=[phase['y']], mode='markers',
202
+ marker=dict(size=MARKER_SIZE, color=phase['color'],
203
+ line=dict(color='white', width=MARKER_LINE_WIDTH)),
204
+ name=phase['formula'],
205
+ hovertemplate=f"<b>{phase['formula']}</b><br>E = {phase['e_per_atom']:.4f} eV/atom",
206
+ showlegend=False
207
+ ))
208
+ fig.add_annotation(
209
+ x=phase['label_x'], y=phase['label_y'],
210
+ text=f"<b>{phase['formula']}</b>", showarrow=False,
211
+ font=dict(size=TERNARY_LABEL_FONT_SIZE, color=phase['color']),
212
+ bgcolor=LABEL_BACKGROUND, bordercolor=phase['color'],
213
+ borderwidth=1, borderpad=3, xref='x', yref='y'
214
+ )
215
+
216
+
217
+ def generate_ternary_composition_diagram(entries, elements: Tuple[str, str, str],
218
+ output_dir: Path, title_suffix: str = "") -> bool:
219
+ """生成三元成分相图(Gibbs 三角图,含 Hull 连线和自动避让标签)
220
+
221
+ Parameters
222
+ ----------
223
+ entries : list of pymatgen PDEntry
224
+ 热力学条目列表
225
+ elements : tuple of (str, str, str)
226
+ 三种元素符号
227
+ output_dir : Path
228
+ 输出目录
229
+ title_suffix : str
230
+ 标题后缀
231
+
232
+ Returns
233
+ -------
234
+ bool : 是否成功生成
235
+ """
236
+ try:
237
+ pd = PhaseDiagram(entries)
238
+ phases = []
239
+
240
+ for entry in pd.stable_entries:
241
+ comp_dict = entry.composition.as_dict()
242
+ formula = entry.composition.reduced_formula
243
+ x, y = coord_to_cartesian(comp_dict, elements)
244
+ e_per_atom = pd.get_hull_energy(entry.composition) / entry.composition.num_atoms
245
+ phases.append({
246
+ 'formula': formula, 'x': x, 'y': y, 'e_per_atom': e_per_atom,
247
+ 'comp_dict': comp_dict
248
+ })
249
+
250
+ phases = allocate_colors(phases)
251
+ phases = calculate_label_positions(phases, TERNARY_LABEL_MARGIN)
252
+
253
+ fig = go.Figure()
254
+ draw_triangle_boundary(fig, elements)
255
+ draw_hull_lines(fig, phases)
256
+ draw_phases(fig, phases)
257
+
258
+ system_str = "-".join(elements)
259
+ suffix = f" ({title_suffix})" if title_suffix else ""
260
+ fig.update_layout(
261
+ title=dict(text=f"<b>{system_str}{suffix}</b><br><sup>{len(phases)} phases</sup>",
262
+ font=dict(size=TERNARY_TITLE_FONT_SIZE), x=0.5, xanchor='center'),
263
+ xaxis=dict(range=[-0.2, 1.2], showgrid=False, zeroline=False, showticklabels=False),
264
+ yaxis=dict(range=[-0.25, 1.1], showgrid=False, zeroline=False, showticklabels=False,
265
+ scaleanchor='x', scaleratio=1),
266
+ plot_bgcolor='white', width=TERNARY_FIG_WIDTH, height=TERNARY_FIG_HEIGHT
267
+ )
268
+
269
+ filename = f"ternary_{elements[0]}-{elements[1]}-{elements[2]}_phase.png"
270
+ filepath = output_dir / filename
271
+ try:
272
+ fig.write_image(str(filepath), scale=2)
273
+ except Exception:
274
+ fig.write_html(str(filepath).replace('.png', '.html'))
275
+ plt.close('all')
276
+ print(f" + {filename}")
277
+ return True
278
+ except Exception as e:
279
+ print(f" - {elements[0]}-{elements[1]}-{elements[2]}: {e}")
280
+ return False
chemphase/plotting.py ADDED
@@ -0,0 +1,94 @@
1
+ """绘图辅助模块:坐标变换、标签定位、颜色分配、核心绘图函数"""
2
+
3
+ import numpy as np
4
+ import matplotlib
5
+ matplotlib.use('Agg')
6
+ import matplotlib.pyplot as plt
7
+ from scipy.spatial import Delaunay
8
+ from typing import Dict, List, Tuple
9
+
10
+ # ============================================================
11
+ # 全局绘图参数
12
+ # ============================================================
13
+ COLOR_PALETTE = [
14
+ "#FF0000", "#FF8800", "#FFDD00", "#00FF00", "#00FFCC", "#00BBFF",
15
+ "#0066FF", "#8800FF", "#FF00AA", "#FF0044", "#AAFF00", "#00FF88",
16
+ "#00DDFF", "#4400FF", "#DD00FF", "#4400FF", "#FF3333", "#FF9933",
17
+ "#FFEE33", "#33FF33", "#33FFCC", "#33CCFF", "#3388FF", "#9933FF",
18
+ "#FF33AA", "#FF3388", "#99FF33", "#33FF99", "#33EEFF", "#5533FF",
19
+ "#EE33FF", "#FF5533", "#FF6666", "#FFAA66", "#FFFF66", "#66FF66",
20
+ "#66FFCC", "#66DDFF", "#66AAFF", "#AA66FF", "#FF66CC", "#AAFF66",
21
+ "#66FFAA", "#66EEFF", "#6644FF", "#FF66FF", "#FF6644", "#FFAAAA",
22
+ "#FFCCAA", "#FFFFAA", "#AAFFAA", "#AAFFCC", "#AAEEFF", "#AACCFF",
23
+ "#CCAAFF", "#FFAAEE", "#CCFFAA", "#CC0000", "#CC6600", "#CCCC00",
24
+ "#00CC00", "#00CCCC", "#0099CC", "#0033CC", "#6600CC", "#CC0099",
25
+ "#CC0033", "#99CC00", "#00CC66",
26
+ ]
27
+
28
+ # ============================================================
29
+ # 三元相图坐标变换
30
+ # ============================================================
31
+
32
+ def coord_to_cartesian(comp_dict: Dict, elems: Tuple[str, str, str]) -> Tuple[float, float]:
33
+ """将三元成分坐标转换为笛卡尔坐标(等边三角形底边朝下)"""
34
+ total = sum(comp_dict.values())
35
+ if total == 0:
36
+ return 0.5, 0.5
37
+ fracs = {e: comp_dict.get(e, 0) / total for e in elems}
38
+ # elems[0] 在左下角, elems[1] 在右下角, elems[2] 在顶部
39
+ x = fracs.get(elems[1], 0) + 0.5 * fracs.get(elems[2], 0)
40
+ y = (np.sqrt(3) / 2) * fracs.get(elems[2], 0)
41
+ return x, y
42
+
43
+
44
+ # ============================================================
45
+ # 标签自动避让算法
46
+ # ============================================================
47
+
48
+ def find_best_label_position(px: float, py: float, occupied: List[Tuple[float, float, str]],
49
+ margin: float = 0.12) -> Tuple[str, float, float]:
50
+ """在三元相图中为标签找到最佳位置(避免与其他标签重叠)"""
51
+ positions = [
52
+ ('top left', -0.12, 0.10), ('top right', 0.12, 0.10),
53
+ ('bottom left', -0.12, -0.08), ('bottom right', 0.12, -0.08),
54
+ ('middle left', -0.15, 0), ('middle right', 0.15, 0),
55
+ ('top center', 0, 0.12), ('bottom center', 0, -0.10),
56
+ ]
57
+ best_pos, best_x, best_y = 'top left', px - 0.12, py + 0.10
58
+ best_dist = -1
59
+ for pos_name, ox, oy in positions:
60
+ test_x, test_y = px + ox, py + oy
61
+ if test_y < -0.05 or test_y > 0.95:
62
+ continue
63
+ if test_x < -0.05 or test_x > 1.05:
64
+ continue
65
+ min_dist = min(((test_x - pox) ** 2 + (test_y - poy) ** 2) ** 0.5
66
+ for (pox, poy, _) in occupied) if occupied else 999
67
+ if min_dist > best_dist:
68
+ best_dist = min_dist
69
+ best_pos, best_x, best_y = pos_name, test_x, test_y
70
+ return best_pos, best_x, best_y
71
+
72
+
73
+ def calculate_label_positions(phases: List[Dict], margin: float = 0.12) -> List[Dict]:
74
+ """为所有相计算最佳标签位置"""
75
+ occupied = []
76
+ sorted_phases = sorted(phases, key=lambda p: p['y'])
77
+ for phase in sorted_phases:
78
+ pos, lx, ly = find_best_label_position(phase['x'], phase['y'], occupied, margin)
79
+ phase['label_pos'] = pos
80
+ phase['label_x'] = lx
81
+ phase['label_y'] = ly
82
+ occupied.append((lx, ly, phase['formula']))
83
+ return phases
84
+
85
+
86
+ # ============================================================
87
+ # 颜色分配
88
+ # ============================================================
89
+
90
+ def allocate_colors(phases: List[Dict]) -> List[Dict]:
91
+ """为每个相分配颜色(循环使用调色板)"""
92
+ for i, phase in enumerate(phases):
93
+ phase['color'] = COLOR_PALETTE[i % len(COLOR_PALETTE)]
94
+ return phases
chemphase/py.typed ADDED
File without changes
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.4
2
+ Name: chemphase
3
+ Version: 0.1.0
4
+ Summary: 相图与化学势热图统一生成器 — 一键生成二元/三元成分相图、化学势热图,支持 Materials Project API 和本地 VASP 数据
5
+ Author: Phase Diagram Generator Team
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/user/chemphase
8
+ Project-URL: Repository, https://github.com/user/chemphase
9
+ Project-URL: Issues, https://github.com/user/chemphase/issues
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Topic :: Scientific/Engineering :: Physics
17
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: pymatgen>=2023.0.0
22
+ Requires-Dist: doped>=3.2.0
23
+ Requires-Dist: matplotlib>=3.5.0
24
+ Requires-Dist: numpy>=1.21.0
25
+ Requires-Dist: scipy>=1.7.0
26
+ Requires-Dist: plotly>=5.0.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=8.0; extra == "dev"
29
+ Requires-Dist: pytest-cov; extra == "dev"
30
+ Requires-Dist: ruff; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # chemphase
34
+
35
+ **相图与化学势热图统一生成器 v5.0**
36
+
37
+ [![Python](https://img.shields.io/badge/Python-3.10%2B-blue)](https://python.org)
38
+ [![License](https://img.shields.io/badge/License-MIT-green)](LICENSE)
39
+
40
+ 一键从 Materials Project API 或本地 VASP 计算结果生成高质量相图。
41
+
42
+ ## 功能
43
+
44
+ - **API 模式**:从 Materials Project 数据库下载热力学数据
45
+ - **本地模式**:读取本地 VASP 计算目录 (POSCAR + vasprun.xml/OUTCAR)
46
+ - **混合模式**:本地数据 + API 补充
47
+ - **二元成分相图**:ΔE vs 成分,含 Hull 连线、自动标签避让
48
+ - **三元成分相图**:Gibbs 三角图 (Plotly 交互式),含 Hull 三角剖分
49
+ - **结构对比**:检测本地计算与 MP 数据库的晶体结构差异
50
+
51
+ ## 安装
52
+
53
+ ```bash
54
+ pip install chemphase
55
+ ```
56
+
57
+ ## 快速开始
58
+
59
+ ### API 模式(默认)
60
+
61
+ ```bash
62
+ # 使用默认元素 Cu-Ag-O-Se
63
+ chemphase
64
+
65
+ # 指定元素
66
+ chemphase --elements Li O Co
67
+ ```
68
+
69
+ ### 本地 VASP 数据模式
70
+
71
+ ```bash
72
+ chemphase --local /path/to/vasp/calculations --elements Cu Ag O Se
73
+ ```
74
+
75
+ ### 结构对比
76
+
77
+ ```bash
78
+ chemphase --local /path/to/vasp/calculations --elements Cu Ag O Se --compare-structure
79
+ ```
80
+
81
+ ## 依赖
82
+
83
+ - [pymatgen](https://pymatgen.org) — 材料分析库
84
+ - [doped](https://github.com/SMTG-Bham/doped) — 缺陷计算工具
85
+ - [matplotlib](https://matplotlib.org) — 二维绘图
86
+ - [plotly](https://plotly.com) — 交互式三元图
87
+
88
+ ## API 密钥
89
+
90
+ API 模式需要 Materials Project API 密钥。设置环境变量:
91
+
92
+ ```bash
93
+ export MATERIALS_PROJECT_API_KEY=你的密钥
94
+ ```
95
+
96
+ 获取密钥:https://materialsproject.org/api
97
+
98
+ ## 命令行参数
99
+
100
+ | 参数 | 说明 | 默认值 |
101
+ |------|------|--------|
102
+ | `--elements` | 元素列表 | `Cu Ag O Se` |
103
+ | `--local` | 本地VASP目录 | — |
104
+ | `--output` | 输出目录 | `phase_diagrams_output` |
105
+ | `--eah` | E above hull 阈值 | `0.05` eV/atom |
106
+ | `--compare-structure` | 启用结构对比 | `False` |
107
+ | `--debug` | 调试模式 | `False` |
108
+
109
+ ## 许可证
110
+
111
+ MIT License — 详见 [LICENSE](LICENSE)
@@ -0,0 +1,13 @@
1
+ chemphase/__init__.py,sha256=p9VbMzSHeJ7-DtW7BD37OIvtHvgSQN92qH-G_Jr2-lQ,1150
2
+ chemphase/__main__.py,sha256=4rL1MjgAJg4D9r8hlsuw07V9i3s1Tq9fhiJg_3z1Z0U,138
3
+ chemphase/cli.py,sha256=wqu0ICrxFyBA-gy41ji-F77GwJitCaqL55PjHwN2MMA,7493
4
+ chemphase/core.py,sha256=tloHRlBPta45vvA4rgGGrPy_HQzx55LCyG31Trep2YE,13331
5
+ chemphase/diagrams.py,sha256=ebpLwlQb6x5BE-oVh6Gh8yBnQ7pLMgHLBB9D41JLC64,10742
6
+ chemphase/plotting.py,sha256=vJYvCnj9t5uC7bUE2EZ4bYX7Pp-bqZ-8_pca_lsYI1Y,4088
7
+ chemphase/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ chemphase-0.1.0.dist-info/licenses/LICENSE,sha256=1SF_moV1CtyB-g2v-Yo3V1_l5jVV0SN7CGjT_yWD0O0,1084
9
+ chemphase-0.1.0.dist-info/METADATA,sha256=vSAioALKtA95jqeZ7C6KTaowvOsTEmew7PotYynDxys,3334
10
+ chemphase-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
11
+ chemphase-0.1.0.dist-info/entry_points.txt,sha256=6Z2apB54PumucA2xQ9DuE0R4u0i7yYBZKjC6NC1JCBw,49
12
+ chemphase-0.1.0.dist-info/top_level.txt,sha256=sqMrf27Atz_bzynj8ZzufVEXl_JtlldAdfy1ImMtSuc,10
13
+ chemphase-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ chemphase = chemphase.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Phase Diagram Generator Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ chemphase