pymdkit 1.2.6__tar.gz → 1.2.8__tar.gz

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.
Files changed (36) hide show
  1. {pymdkit-1.2.6/src/pymdkit.egg-info → pymdkit-1.2.8}/PKG-INFO +1 -1
  2. {pymdkit-1.2.6 → pymdkit-1.2.8}/pyproject.toml +1 -1
  3. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit/__init__.py +1 -1
  4. pymdkit-1.2.8/src/pymdkit/commands/stable_entry.py +239 -0
  5. {pymdkit-1.2.6 → pymdkit-1.2.8/src/pymdkit.egg-info}/PKG-INFO +1 -1
  6. pymdkit-1.2.6/src/pymdkit/commands/stable_entry.py +0 -119
  7. {pymdkit-1.2.6 → pymdkit-1.2.8}/LICENSE +0 -0
  8. {pymdkit-1.2.6 → pymdkit-1.2.8}/README.md +0 -0
  9. {pymdkit-1.2.6 → pymdkit-1.2.8}/setup.cfg +0 -0
  10. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit/commands/__init__.py +0 -0
  11. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit/commands/_fileio.py +0 -0
  12. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit/commands/_vasp_output.py +0 -0
  13. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit/commands/_vaspset.py +0 -0
  14. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit/commands/add_groups.py +0 -0
  15. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit/commands/compute_ehull.py +0 -0
  16. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit/commands/compute_msd_all_groups.py +0 -0
  17. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit/commands/compute_rmsd.py +0 -0
  18. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit/commands/electrostatic_energy.py +0 -0
  19. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit/commands/final_energy.py +0 -0
  20. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit/commands/gather_contcar.py +0 -0
  21. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit/commands/nep_rmse.py +0 -0
  22. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit/commands/perturb.py +0 -0
  23. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit/commands/stru2xyz.py +0 -0
  24. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit/commands/submit_vasp.py +0 -0
  25. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit/commands/substitute.py +0 -0
  26. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit/commands/supercell.py +0 -0
  27. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit/commands/symmetrize.py +0 -0
  28. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit/commands/vasp2xyz.py +0 -0
  29. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit/commands/vasp_relax.py +0 -0
  30. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit/commands/vasp_static.py +0 -0
  31. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit/pymdkit_main.py +0 -0
  32. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit.egg-info/SOURCES.txt +0 -0
  33. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit.egg-info/dependency_links.txt +0 -0
  34. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit.egg-info/entry_points.txt +0 -0
  35. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit.egg-info/requires.txt +0 -0
  36. {pymdkit-1.2.6 → pymdkit-1.2.8}/src/pymdkit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pymdkit
3
- Version: 1.2.6
3
+ Version: 1.2.8
4
4
  Summary: A unified command-line toolkit for atomistic / MD structure workflows.
5
5
  Author-email: Yueda Wang <ydwang0608@ustc.edu.cn>
6
6
  License-Expression: GPL-3.0-or-later
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pymdkit"
7
- version = "1.2.6"
7
+ version = "1.2.8"
8
8
  description = "A unified command-line toolkit for atomistic / MD structure workflows."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  """pymdkit -- a unified CLI for atomistic / MD structure workflows."""
2
2
 
3
- __version__ = "1.2.6"
3
+ __version__ = "1.2.8"
@@ -0,0 +1,239 @@
1
+ """Download stable Materials Project structures for a chemical system."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import re
8
+ from pathlib import Path
9
+
10
+ COMMAND = "stable-entry"
11
+ HELP = "Download stable Materials Project structures for a chemical system."
12
+
13
+
14
+ def safe_formula(formula):
15
+ return re.sub(r"[^A-Za-z0-9_.-]+", "", str(formula))
16
+
17
+
18
+ def is_public_mpid(text):
19
+ return bool(re.fullmatch(r"mp-\d+", str(text or "")))
20
+
21
+
22
+ def mpid_text(value):
23
+ if value is None:
24
+ return ""
25
+ if isinstance(value, dict):
26
+ for key in ("material_id", "task_id", "entry_id", "id", "identifier"):
27
+ if value.get(key):
28
+ return mpid_text(value[key])
29
+ return "-".join(f"{k}-{v}" for k, v in sorted(value.items()))
30
+ for attr in ("string", "identifier"):
31
+ text = getattr(value, attr, None)
32
+ if text:
33
+ return str(text)
34
+ text = str(value)
35
+ match = re.search(r"mp-\d+", text)
36
+ if match:
37
+ return match.group(0)
38
+ match = re.search(r"identifier=['\"]?(mp-[A-Za-z0-9]+)", text)
39
+ if match:
40
+ return match.group(1)
41
+ return text
42
+
43
+
44
+ def entry_id_text(entry):
45
+ data = getattr(entry, "data", {}) or {}
46
+ for key in ("material_id", "task_id", "entry_id", "id"):
47
+ text = mpid_text(data.get(key))
48
+ if is_public_mpid(text):
49
+ return text
50
+ for key in ("material_id", "task_id", "entry_id", "id"):
51
+ text = mpid_text(data.get(key))
52
+ if text:
53
+ return text
54
+ entry_id = getattr(entry, "entry_id", None)
55
+ text = mpid_text(entry_id)
56
+ if text:
57
+ return text
58
+ return "unknown"
59
+
60
+
61
+ def entry_sort_key(entry):
62
+ formula = str(entry.composition.reduced_formula)
63
+ return (len(entry.composition.elements), formula, entry_id_text(entry))
64
+
65
+
66
+ def add_arguments(parser: argparse.ArgumentParser) -> None:
67
+ parser.add_argument("-s", nargs="+", required=True,
68
+ help="Chemical system elements, e.g. -s Li La Ta Cl.")
69
+ parser.add_argument("-mp-api-key", default=None,
70
+ help="Materials Project API key.")
71
+
72
+
73
+ def doc_get(doc, name, default=None):
74
+ if isinstance(doc, dict):
75
+ return doc.get(name, default)
76
+ return getattr(doc, name, default)
77
+
78
+
79
+ def resolve_public_material_ids(entries, api_key=None):
80
+ missing = [entry for entry in entries if not is_public_mpid(entry_id_text(entry))]
81
+ if not missing:
82
+ return
83
+ try:
84
+ from mp_api.client import MPRester as MPApiRester
85
+ from pymatgen.analysis.structure_matcher import StructureMatcher
86
+ except ModuleNotFoundError:
87
+ return
88
+
89
+ matcher = StructureMatcher()
90
+ cache = {}
91
+ with MPApiRester(api_key) as mpr:
92
+ for entry in missing:
93
+ formula = str(entry.composition.reduced_formula)
94
+ if formula not in cache:
95
+ cache[formula] = mpr.materials.summary.search(
96
+ formula=formula,
97
+ is_stable=True,
98
+ fields=["material_id", "formula_pretty", "structure"],
99
+ )
100
+ for doc in cache[formula]:
101
+ structure = doc_get(doc, "structure")
102
+ material_id = mpid_text(doc_get(doc, "material_id"))
103
+ if not structure or not is_public_mpid(material_id):
104
+ continue
105
+ if str(structure.composition.reduced_formula) != formula:
106
+ continue
107
+ if matcher.fit(entry.structure, structure):
108
+ data = getattr(entry, "data", None)
109
+ if data is None:
110
+ entry.data = {}
111
+ data = entry.data
112
+ data["material_id"] = material_id
113
+ break
114
+
115
+
116
+ def fetch_stable_entries(elements, api_key=None):
117
+ """Fetch entries and keep only entries stable on the full MP phase diagram."""
118
+ from pymatgen.analysis.phase_diagram import PhaseDiagram
119
+ from pymatgen.ext.matproj import MPRester
120
+
121
+ with MPRester(api_key) as mpr:
122
+ try:
123
+ entries = mpr.get_entries_in_chemsys(
124
+ elements,
125
+ inc_structure=True,
126
+ property_data=["material_id"],
127
+ additional_criteria={"thermo_types": ["GGA_GGA+U"]},
128
+ )
129
+ except (TypeError, KeyError):
130
+ entries = mpr.get_entries_in_chemsys(
131
+ elements,
132
+ inc_structure=True,
133
+ additional_criteria={"thermo_types": ["GGA_GGA+U"]},
134
+ )
135
+ phase_diagram = PhaseDiagram(entries)
136
+ stable_entries = list(phase_diagram.stable_entries)
137
+ resolve_public_material_ids(stable_entries, api_key)
138
+ print(f"Fetched {len(entries)} MP thermo entries; {len(stable_entries)} are stable on the full phase diagram.")
139
+ by_mpid = {}
140
+ for entry in stable_entries:
141
+ by_mpid[entry_id_text(entry)] = entry
142
+ return list(by_mpid.values())
143
+
144
+
145
+ def api_key_value(api_key=None):
146
+ if api_key:
147
+ return api_key
148
+ try:
149
+ from pymatgen.ext.matproj import MPRester
150
+ return getattr(MPRester(), "api_key", None)
151
+ except Exception:
152
+ return None
153
+
154
+
155
+ def extract_cif_text(text):
156
+ stripped = text.lstrip()
157
+ if not stripped.startswith(("{", "[")):
158
+ if "_symmetry_space_group_name_H-M" in text or stripped.startswith(("data_", "#")):
159
+ return text
160
+ try:
161
+ payload = json.loads(text)
162
+ except json.JSONDecodeError:
163
+ return None
164
+ candidates = []
165
+ if isinstance(payload, dict):
166
+ candidates.extend(payload.get("response", []) or [])
167
+ candidates.extend(payload.get("data", []) or [])
168
+ candidates.append(payload)
169
+ for item in candidates:
170
+ if isinstance(item, dict):
171
+ cif = item.get("cif") or item.get("cif_symmetrized") or item.get("symmetrized_cif")
172
+ if cif:
173
+ return cif
174
+ return None
175
+
176
+
177
+ def download_mp_symmetrized_cif(material_id, path, api_key=None):
178
+ """Download the Materials Project CIF (Symmetrized) file for material_id."""
179
+ key = api_key_value(api_key)
180
+ headers = {"Accept": "text/plain", "User-Agent": "pymdkit"}
181
+ if key:
182
+ headers["X-API-KEY"] = key
183
+ quoted = quote(material_id, safe="")
184
+ urls = [
185
+ f"https://materialsproject.org/materials/{quoted}/cif?download=true&symmetrized=true",
186
+ f"https://materialsproject.org/materials/{quoted}/cif?symmetrized=true",
187
+ f"https://www.materialsproject.org/rest/v2/materials/{quoted}/cif",
188
+ ]
189
+ errors = []
190
+ for url in urls:
191
+ try:
192
+ req = Request(url, headers=headers)
193
+ with urlopen(req, timeout=60) as response:
194
+ text = response.read().decode("utf-8")
195
+ cif_text = extract_cif_text(text)
196
+ if cif_text:
197
+ Path(path).write_text(cif_text, encoding="utf-8")
198
+ return
199
+ errors.append(f"{url}: response did not contain CIF text")
200
+ except Exception as exc: # noqa: BLE001 - try alternate MP endpoints
201
+ errors.append(f"{url}: {type(exc).__name__}: {exc}")
202
+ raise RuntimeError("Could not download MP CIF (Symmetrized): " + " | ".join(errors))
203
+
204
+
205
+ def run(args) -> int:
206
+
207
+ elements = list(dict.fromkeys(args.s))
208
+ label = "-".join(elements)
209
+ out_dir = Path(f"{label}-stable-entries")
210
+ out_dir.mkdir(parents=True, exist_ok=True)
211
+
212
+ try:
213
+ entries = fetch_stable_entries(elements, args.mp_api_key)
214
+ except ModuleNotFoundError as exc:
215
+ if exc.name == "emmet":
216
+ raise SystemExit(
217
+ "Error: stable-entry requires emmet models from mp-api. "
218
+ "Reinstall pymdkit or run `pip install mp-api`."
219
+ ) from exc
220
+ raise
221
+
222
+ entries = sorted(entries, key=entry_sort_key)
223
+ written = 0
224
+ for entry in entries:
225
+ mpid = safe_formula(entry_id_text(entry).split(" ")[0])
226
+ formula = safe_formula(entry.composition.reduced_formula)
227
+ path = out_dir / f"{mpid}-{formula}.cif"
228
+ download_mp_symmetrized_cif(mpid, path, args.mp_api_key)
229
+ print(f"{mpid}\t{entry.composition.reduced_formula}\t-> {path}")
230
+ written += 1
231
+
232
+ print(f"Wrote {written} stable entr{'y' if written == 1 else 'ies'} to {out_dir}")
233
+ return 0 if written else 1
234
+
235
+
236
+ if __name__ == "__main__":
237
+ _parser = argparse.ArgumentParser(description=__doc__)
238
+ add_arguments(_parser)
239
+ raise SystemExit(run(_parser.parse_args()))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pymdkit
3
- Version: 1.2.6
3
+ Version: 1.2.8
4
4
  Summary: A unified command-line toolkit for atomistic / MD structure workflows.
5
5
  Author-email: Yueda Wang <ydwang0608@ustc.edu.cn>
6
6
  License-Expression: GPL-3.0-or-later
@@ -1,119 +0,0 @@
1
- """Download stable Materials Project structures for a chemical system."""
2
-
3
- from __future__ import annotations
4
-
5
- import argparse
6
- import re
7
- from pathlib import Path
8
-
9
- COMMAND = "stable-entry"
10
- HELP = "Download stable Materials Project structures for a chemical system."
11
-
12
-
13
- def safe_formula(formula):
14
- return re.sub(r"[^A-Za-z0-9_.-]+", "", str(formula))
15
-
16
-
17
- def mpid_text(value):
18
- if value is None:
19
- return ""
20
- if isinstance(value, dict):
21
- for key in ("material_id", "task_id", "entry_id", "id", "identifier"):
22
- if value.get(key):
23
- return mpid_text(value[key])
24
- return "-".join(f"{k}-{v}" for k, v in sorted(value.items()))
25
- for attr in ("string", "identifier"):
26
- text = getattr(value, attr, None)
27
- if text:
28
- return str(text)
29
- text = str(value)
30
- match = re.search(r"mp-\d+", text)
31
- if match:
32
- return match.group(0)
33
- match = re.search(r"identifier=['\"]?(mp-[A-Za-z0-9]+)", text)
34
- if match:
35
- return match.group(1)
36
- return text
37
-
38
-
39
- def entry_id_text(entry):
40
- data = getattr(entry, "data", {}) or {}
41
- for key in ("material_id", "task_id", "entry_id", "id"):
42
- text = mpid_text(data.get(key))
43
- if text:
44
- return text
45
- entry_id = getattr(entry, "entry_id", None)
46
- text = mpid_text(entry_id)
47
- if text:
48
- return text
49
- return "unknown"
50
-
51
-
52
- def entry_sort_key(entry):
53
- formula = str(entry.composition.reduced_formula)
54
- return (len(entry.composition.elements), formula, entry_id_text(entry))
55
-
56
-
57
- def add_arguments(parser: argparse.ArgumentParser) -> None:
58
- parser.add_argument("-s", nargs="+", required=True,
59
- help="Chemical system elements, e.g. -s Li La Ta Cl.")
60
- parser.add_argument("-mp-api-key", default=None,
61
- help="Materials Project API key.")
62
-
63
-
64
- def fetch_stable_entries(elements, api_key=None):
65
- """Fetch entries and keep only entries stable on the full MP phase diagram."""
66
- from pymatgen.analysis.phase_diagram import PhaseDiagram
67
- from pymatgen.ext.matproj import MPRester
68
-
69
- with MPRester(api_key) as mpr:
70
- entries = mpr.get_entries_in_chemsys(
71
- elements,
72
- inc_structure=True,
73
- additional_criteria={"thermo_types": ["GGA_GGA+U"]},
74
- )
75
- phase_diagram = PhaseDiagram(entries)
76
- stable_entries = list(phase_diagram.stable_entries)
77
- print(f"Fetched {len(entries)} MP thermo entries; {len(stable_entries)} are stable on the full phase diagram.")
78
- by_mpid = {}
79
- for entry in stable_entries:
80
- by_mpid[entry_id_text(entry)] = entry
81
- return list(by_mpid.values())
82
-
83
-
84
- def run(args) -> int:
85
- from pymatgen.io.cif import CifWriter
86
-
87
- elements = list(dict.fromkeys(args.s))
88
- label = "-".join(elements)
89
- out_dir = Path(f"{label}-stable-entries")
90
- out_dir.mkdir(parents=True, exist_ok=True)
91
-
92
- try:
93
- entries = fetch_stable_entries(elements, args.mp_api_key)
94
- except ModuleNotFoundError as exc:
95
- if exc.name == "emmet":
96
- raise SystemExit(
97
- "Error: stable-entry requires emmet models from mp-api. "
98
- "Reinstall pymdkit or run `pip install mp-api`."
99
- ) from exc
100
- raise
101
-
102
- entries = sorted(entries, key=entry_sort_key)
103
- written = 0
104
- for entry in entries:
105
- mpid = safe_formula(entry_id_text(entry).split(" ")[0])
106
- formula = safe_formula(entry.composition.reduced_formula)
107
- path = out_dir / f"{mpid}-{formula}.cif"
108
- CifWriter(entry.structure).write_file(str(path))
109
- print(f"{mpid}\t{entry.composition.reduced_formula}\t-> {path}")
110
- written += 1
111
-
112
- print(f"Wrote {written} stable entr{'y' if written == 1 else 'ies'} to {out_dir}")
113
- return 0 if written else 1
114
-
115
-
116
- if __name__ == "__main__":
117
- _parser = argparse.ArgumentParser(description=__doc__)
118
- add_arguments(_parser)
119
- raise SystemExit(run(_parser.parse_args()))
File without changes
File without changes
File without changes