pymdkit 1.2.10__tar.gz → 1.2.12__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 (35) hide show
  1. {pymdkit-1.2.10/src/pymdkit.egg-info → pymdkit-1.2.12}/PKG-INFO +1 -1
  2. {pymdkit-1.2.10 → pymdkit-1.2.12}/pyproject.toml +1 -1
  3. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit/__init__.py +1 -1
  4. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit/commands/stable_entry.py +35 -63
  5. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit/commands/supercell.py +28 -8
  6. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit/commands/vasp2xyz.py +21 -3
  7. {pymdkit-1.2.10 → pymdkit-1.2.12/src/pymdkit.egg-info}/PKG-INFO +1 -1
  8. {pymdkit-1.2.10 → pymdkit-1.2.12}/LICENSE +0 -0
  9. {pymdkit-1.2.10 → pymdkit-1.2.12}/README.md +0 -0
  10. {pymdkit-1.2.10 → pymdkit-1.2.12}/setup.cfg +0 -0
  11. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit/commands/__init__.py +0 -0
  12. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit/commands/_fileio.py +0 -0
  13. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit/commands/_vasp_output.py +0 -0
  14. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit/commands/_vaspset.py +0 -0
  15. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit/commands/add_groups.py +0 -0
  16. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit/commands/compute_ehull.py +0 -0
  17. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit/commands/compute_msd_all_groups.py +0 -0
  18. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit/commands/compute_rmsd.py +0 -0
  19. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit/commands/electrostatic_energy.py +0 -0
  20. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit/commands/final_energy.py +0 -0
  21. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit/commands/gather_contcar.py +0 -0
  22. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit/commands/nep_rmse.py +0 -0
  23. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit/commands/perturb.py +0 -0
  24. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit/commands/stru2xyz.py +0 -0
  25. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit/commands/submit_vasp.py +0 -0
  26. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit/commands/substitute.py +0 -0
  27. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit/commands/symmetrize.py +0 -0
  28. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit/commands/vasp_relax.py +0 -0
  29. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit/commands/vasp_static.py +0 -0
  30. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit/pymdkit_main.py +0 -0
  31. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit.egg-info/SOURCES.txt +0 -0
  32. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit.egg-info/dependency_links.txt +0 -0
  33. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit.egg-info/entry_points.txt +0 -0
  34. {pymdkit-1.2.10 → pymdkit-1.2.12}/src/pymdkit.egg-info/requires.txt +0 -0
  35. {pymdkit-1.2.10 → pymdkit-1.2.12}/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.10
3
+ Version: 1.2.12
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.10"
7
+ version = "1.2.12"
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.10"
3
+ __version__ = "1.2.12"
@@ -3,10 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import argparse
6
- import json
7
6
  import re
8
- from urllib.parse import quote
9
- from urllib.request import Request, urlopen
10
7
  from pathlib import Path
11
8
 
12
9
  COMMAND = "stable-entry"
@@ -152,69 +149,44 @@ def fetch_stable_entries(elements, api_key=None):
152
149
  return list(by_mpid.values())
153
150
 
154
151
 
155
- def api_key_value(api_key=None):
156
- if api_key:
157
- return api_key
158
- try:
159
- from pymatgen.ext.matproj import MPRester
160
- return getattr(MPRester(), "api_key", None)
161
- except Exception:
162
- return None
152
+ def get_api_structure(material_id, api_key=None):
153
+ """Return the MP API structure for a public material_id."""
154
+ from mp_api.client import MPRester as MPApiRester
163
155
 
156
+ with MPApiRester(api_key) as mpr:
157
+ if hasattr(mpr, "get_structure_by_material_id"):
158
+ try:
159
+ return mpr.get_structure_by_material_id(
160
+ material_id,
161
+ conventional_unit_cell=True,
162
+ )
163
+ except TypeError:
164
+ return mpr.get_structure_by_material_id(material_id)
165
+
166
+ docs = mpr.materials.summary.search(
167
+ material_ids=[material_id],
168
+ fields=["material_id", "structure"],
169
+ )
170
+ if not docs:
171
+ raise RuntimeError(f"no MP summary structure found for {material_id}")
172
+ return doc_get(docs[0], "structure")
173
+
174
+
175
+ def write_mp_symmetrized_cif(material_id, fallback_structure, path, api_key=None):
176
+ """Write an MP API structure as a symmetrized CIF."""
177
+ from pymatgen.io.cif import CifWriter
164
178
 
165
- def extract_cif_text(text):
166
- stripped = text.lstrip()
167
- if not stripped.startswith(("{", "[")):
168
- if "_symmetry_space_group_name_H-M" in text or stripped.startswith(("data_", "#")):
169
- return text
170
179
  try:
171
- payload = json.loads(text)
172
- except json.JSONDecodeError:
173
- return None
174
- candidates = []
175
- if isinstance(payload, dict):
176
- candidates.extend(payload.get("response", []) or [])
177
- candidates.extend(payload.get("data", []) or [])
178
- candidates.append(payload)
179
- for item in candidates:
180
- if isinstance(item, dict):
181
- cif = item.get("cif") or item.get("cif_symmetrized") or item.get("symmetrized_cif")
182
- if cif:
183
- return cif
184
- return None
185
-
186
-
187
- def download_mp_symmetrized_cif(material_id, path, api_key=None):
188
- """Download the Materials Project CIF (Symmetrized) file for material_id."""
189
- key = api_key_value(api_key)
190
- headers = {"Accept": "text/plain", "User-Agent": "pymdkit"}
191
- if key:
192
- headers["X-API-KEY"] = key
193
- quoted = quote(material_id, safe="")
194
- urls = [
195
- f"https://api.materialsproject.org/materials/{quoted}/cif?symmetrized=true",
196
- f"https://api.materialsproject.org/materials/{quoted}/cif",
197
- f"https://api.materialsproject.org/materials/core/{quoted}/cif?symmetrized=true",
198
- f"https://api.materialsproject.org/materials/core/{quoted}/cif",
199
- f"https://materialsproject.org/materials/{quoted}/cif?download=true&symmetrized=true",
200
- f"https://materialsproject.org/materials/{quoted}/cif?symmetrized=true",
201
- f"https://www.materialsproject.org/rest/v2/materials/{quoted}/cif",
202
- ]
203
- errors = []
204
- for url in urls:
205
- try:
206
- req = Request(url, headers=headers)
207
- with urlopen(req, timeout=60) as response:
208
- text = response.read().decode("utf-8")
209
- cif_text = extract_cif_text(text)
210
- if cif_text:
211
- Path(path).write_text(cif_text, encoding="utf-8")
212
- return
213
- errors.append(f"{url}: response did not contain CIF text")
214
- except Exception as exc: # noqa: BLE001 - try alternate MP endpoints
215
- errors.append(f"{url}: {type(exc).__name__}: {exc}")
216
- raise RuntimeError("Could not download MP CIF (Symmetrized): " + " | ".join(errors))
180
+ structure = get_api_structure(material_id, api_key)
181
+ except Exception as exc: # noqa: BLE001 - keep command useful if MP API changes
182
+ structure = fallback_structure
183
+ print(f" ! {material_id}: MP API structure fetch failed ({type(exc).__name__}); "
184
+ "using thermo structure fallback.")
217
185
 
186
+ try:
187
+ CifWriter(structure, symprec=0.1, refine_struct=True).write_file(str(path))
188
+ except TypeError:
189
+ CifWriter(structure, symprec=0.1).write_file(str(path))
218
190
 
219
191
  def run(args) -> int:
220
192
 
@@ -239,7 +211,7 @@ def run(args) -> int:
239
211
  mpid = safe_formula(entry_id_text(entry).split(" ")[0])
240
212
  formula = safe_formula(entry.composition.reduced_formula)
241
213
  path = out_dir / f"{mpid}-{formula}.cif"
242
- download_mp_symmetrized_cif(mpid, path, args.mp_api_key)
214
+ write_mp_symmetrized_cif(mpid, entry.structure, path, args.mp_api_key)
243
215
  print(f"{mpid}\t{entry.composition.reduced_formula}\t-> {path}")
244
216
  written += 1
245
217
 
@@ -3,7 +3,8 @@ Build a supercell whose cell lengths stay within a maximum (-max-abc).
3
3
 
4
4
  For each lattice vector the structure is repeated as many times as possible
5
5
  without its length exceeding ``-max-abc`` (in Angstrom):
6
- ``n_i = max(1, floor(max_abc / |a_i|))``.
6
+ `
7
+ _i = max(1, floor(max_abc / |a_i|))``.
7
8
 
8
9
  Single file: supercell -i example.vasp -o out.vasp -max-abc 20
9
10
  Whole folder: supercell -if vasp-opted -of out -max-abc 20
@@ -15,20 +16,28 @@ GPUMD setup: supercell -if extxyz-vasp-opted -max-abc 24 -individual \\
15
16
  temperature, each seeded with model.xyz + the shared GPUMD
16
17
  input files (target_t in run.in replaced by the temperature).
17
18
  With -add-groups, model.xyz is tagged with a GPUMD group index
18
- by element order (Li->0, Y->1, Cl->2).
19
+ by element order (Li->0, Y->1, Cl->2).
20
+ Single GPUMD: supercell -i example.cif -max-abc 24 -temp 500 600
21
+ -> ./model.xyz plus ./500/ and ./600/ directly, without
22
+ creating a structure-name folder.
19
23
  """
20
24
 
21
25
  import argparse
22
26
  import shutil
23
27
  from pathlib import Path
24
28
 
25
- from ase.io import read, write
26
-
27
29
  try:
28
- from . import _fileio, add_groups as _add_groups
30
+ from . import _fileio
29
31
  except ImportError: # running as a standalone script
30
32
  import _fileio
31
- import add_groups as _add_groups
33
+
34
+
35
+ def tag_groups(path, elements):
36
+ try:
37
+ from . import add_groups as _add_groups
38
+ except ImportError: # running as a standalone script
39
+ import add_groups as _add_groups
40
+ _add_groups._tag_one(path, path, elements)
32
41
 
33
42
  COMMAND = "supercell"
34
43
  HELP = "Build a supercell with cell lengths capped at -max-abc Angstrom."
@@ -43,6 +52,8 @@ def repetitions_for(atoms, max_abc):
43
52
 
44
53
 
45
54
  def make_supercell(in_path, out_path, max_abc):
55
+ from ase.io import read, write
56
+
46
57
  atoms = read(str(in_path))
47
58
  reps = repetitions_for(atoms, max_abc)
48
59
  sc = atoms.repeat(reps)
@@ -111,9 +122,18 @@ def add_arguments(parser):
111
122
 
112
123
 
113
124
  def _run_gpumd(args):
114
- """GPUMD setup: ./<name>/model.xyz + per-temperature job folders."""
125
+ """GPUMD setup: model.xyz + per-temperature job folders."""
126
+ if args.input:
127
+ model = Path("model.xyz")
128
+ make_supercell(args.input, model, args.max_abc)
129
+ if args.add_groups:
130
+ tag_groups(model, args.add_groups)
131
+ setup_temp_folders(Path("."), args.temp, args.md_input_folder)
132
+ print(f"Done: set up single structure with temperatures {args.temp}.")
133
+ return 0
134
+
115
135
  if not args.input_folder:
116
- raise SystemExit("Error: -temp requires -if/-input-folder.")
136
+ raise SystemExit("Error: -temp requires -i/-input or -if/-input-folder.")
117
137
  count = 0
118
138
  for f in _fileio.structure_files(args.input_folder):
119
139
  folder = Path(_folder_name(f.stem))
@@ -22,7 +22,8 @@ except ImportError: # running as a standalone script
22
22
  import _vasp_output
23
23
 
24
24
  COMMAND = "vasp2xyz"
25
- HELP = "Collect SCF-converged VASP job folders into one extxyz file."
25
+ HELP = "Collect SCF-converged VASP job folders into one extxyz file."
26
+ KBAR_TO_EV_PER_A3 = 0.0006241509125883258
26
27
 
27
28
 
28
29
  def folder_sort_key(name):
@@ -79,6 +80,23 @@ def strip_calculation_results(atoms):
79
80
  atoms.arrays.pop(key, None)
80
81
 
81
82
 
83
+ def vasp_stress_to_ase(stress):
84
+ """Convert VASP stress in kB to ASE stress in eV/A^3."""
85
+ if stress is None:
86
+ return None
87
+ try:
88
+ import numpy as np
89
+ return -np.asarray(stress, dtype=float) * KBAR_TO_EV_PER_A3
90
+ except ImportError:
91
+ pass
92
+
93
+ def convert(value):
94
+ if isinstance(value, (list, tuple)):
95
+ return [convert(x) for x in value]
96
+ return -float(value) * KBAR_TO_EV_PER_A3
97
+
98
+ return convert(stress)
99
+
82
100
  def atoms_from_output(output):
83
101
  from ase.calculators.singlepoint import SinglePointCalculator
84
102
  from pymatgen.io.ase import AseAtomsAdaptor
@@ -92,9 +110,9 @@ def atoms_from_output(output):
92
110
  if "forces" in step:
93
111
  results["forces"] = step["forces"]
94
112
  if "stress" in step:
95
- results["stress"] = step["stress"]
113
+ results["stress"] = vasp_stress_to_ase(step["stress"])
96
114
  elif "stresses" in step:
97
- results["stress"] = step["stresses"]
115
+ results["stress"] = vasp_stress_to_ase(step["stresses"])
98
116
  atoms.calc = SinglePointCalculator(atoms, **results)
99
117
  return atoms
100
118
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pymdkit
3
- Version: 1.2.10
3
+ Version: 1.2.12
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
File without changes
File without changes
File without changes