pymdkit 1.6.1__tar.gz → 1.6.2__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 (57) hide show
  1. {pymdkit-1.6.1 → pymdkit-1.6.2}/PKG-INFO +12 -3
  2. {pymdkit-1.6.1 → pymdkit-1.6.2}/README.md +13 -4
  3. {pymdkit-1.6.1 → pymdkit-1.6.2}/pyproject.toml +1 -1
  4. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/__init__.py +1 -1
  5. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/_symmetry.py +4 -0
  6. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/compute_rmsd.py +3 -0
  7. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/convert.py +30 -0
  8. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/gpumd_group.py +2 -2
  9. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/realloc_occ.py +6 -0
  10. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/substitute.py +5 -1
  11. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/supercell.py +1 -3
  12. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit.egg-info/PKG-INFO +12 -3
  13. {pymdkit-1.6.1 → pymdkit-1.6.2}/tests/test_convert.py +53 -1
  14. {pymdkit-1.6.1 → pymdkit-1.6.2}/tests/test_realloc_occ.py +4 -0
  15. {pymdkit-1.6.1 → pymdkit-1.6.2}/LICENSE +0 -0
  16. {pymdkit-1.6.1 → pymdkit-1.6.2}/setup.cfg +0 -0
  17. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/__init__.py +0 -0
  18. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/_cifio.py +0 -0
  19. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/_extxyz.py +0 -0
  20. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/_fileio.py +0 -0
  21. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/_geometry.py +0 -0
  22. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/_gpumd.py +0 -0
  23. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/_lattice.py +0 -0
  24. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/_oxidation.py +0 -0
  25. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/_precision.py +0 -0
  26. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/_reduction.py +0 -0
  27. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/_supercell.py +0 -0
  28. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/_vasp_output.py +0 -0
  29. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/_vaspset.py +0 -0
  30. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/add_config_type.py +0 -0
  31. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/chemsys_entry.py +0 -0
  32. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/compute_ehull.py +0 -0
  33. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/ewald.py +0 -0
  34. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/gather_fs.py +0 -0
  35. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/gpumd_msd.py +0 -0
  36. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/gpumd_relax.py +0 -0
  37. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/gpumd_thermo.py +0 -0
  38. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/gpumd_trjcat.py +0 -0
  39. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/nep_rmse.py +0 -0
  40. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/pca.py +0 -0
  41. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/perturb.py +0 -0
  42. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/submit_vasp.py +0 -0
  43. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/trj_extract.py +0 -0
  44. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/trj_sparse.py +0 -0
  45. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/umap.py +0 -0
  46. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/vasp2xyz.py +0 -0
  47. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/vasp_fe.py +0 -0
  48. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/vasp_relax.py +0 -0
  49. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/commands/vasp_static.py +0 -0
  50. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit/pymdkit_main.py +0 -0
  51. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit.egg-info/SOURCES.txt +0 -0
  52. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit.egg-info/dependency_links.txt +0 -0
  53. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit.egg-info/entry_points.txt +0 -0
  54. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit.egg-info/requires.txt +0 -0
  55. {pymdkit-1.6.1 → pymdkit-1.6.2}/src/pymdkit.egg-info/top_level.txt +0 -0
  56. {pymdkit-1.6.1 → pymdkit-1.6.2}/tests/test_lattice_symmetry.py +0 -0
  57. {pymdkit-1.6.1 → pymdkit-1.6.2}/tests/test_vaspset.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pymdkit
3
- Version: 1.6.1
3
+ Version: 1.6.2
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
@@ -263,8 +263,9 @@ and `0.875`. Generated CIF occupancy fields use two decimal places.
263
263
  `-max-supercell AxBxC` searches bounded diagonal supercells, smallest volume
264
264
  first, until every multiplicity times occupancy times supercell determinant is
265
265
  an integer. Equal-volume choices favor the most isotropic resulting cell.
266
- All selections, site atom counts, oxidation states, charges, and failures are
267
- recorded in `OUTPUT/realloc-occ.txt`.
266
+ All selections, site atom counts, oxidation states, charges, failures, the
267
+ allowed occupancy grid, and the final summary are recorded in
268
+ `OUTPUT/realloc-occ.txt` for both single-file and folder modes.
268
269
 
269
270
  `gather-fs` recursively scans all job folders below the current directory. For
270
271
  VASP it follows the global `vaspout.h5 > vasprun.xml > OUTCAR` priority and
@@ -330,6 +331,14 @@ International Tables setting from pymatgen/spglib and correspond to the
330
331
  [Bilbao Crystallographic Server WYCKPOS tables](https://cryst.ehu.es/cryst/get_wp.html);
331
332
  the provenance URL is also recorded inside each generated CIF.
332
333
 
334
+ For structural conversion, supercell creation, and GPUMD grouping, PMK reads
335
+ CIF input with pymatgen rather than ASE. ASE interprets
336
+ `_space_group_crystal_system` as a setting hint only for rhombohedral groups
337
+ and emits a potentially misleading warning for other crystal systems. Using
338
+ pymatgen avoids that ambiguity while PMK continues to use ASE for non-CIF
339
+ formats. A partially occupied CIF must first be ordered with `convert -d2o`
340
+ before it can be represented as an explicit atom list.
341
+
333
342
  CIF `_atom_type_symbol` and `_atom_site_label` values use per-element
334
343
  inequivalent-site identifiers such as `Li1`, `Y1`, `Cl1`, and `Cl2`.
335
344
  The final atom-site loop starts with `_atom_site_label`, followed by
@@ -236,9 +236,10 @@ and `0.875`. Generated CIF occupancy fields use two decimal places.
236
236
  `-max-supercell AxBxC` searches bounded diagonal supercells, smallest volume
237
237
  first, until every multiplicity times occupancy times supercell determinant is
238
238
  an integer. Equal-volume choices favor the most isotropic resulting cell.
239
- All selections, site atom counts, oxidation states, charges, and failures are
240
- recorded in `OUTPUT/realloc-occ.txt`.
241
-
239
+ All selections, site atom counts, oxidation states, charges, failures, the
240
+ allowed occupancy grid, and the final summary are recorded in
241
+ `OUTPUT/realloc-occ.txt` for both single-file and folder modes.
242
+
242
243
  `gather-fs` recursively scans all job folders below the current directory. For
243
244
  VASP it follows the global `vaspout.h5 > vasprun.xml > OUTCAR` priority and
244
245
  requires full convergence before copying the selected `-fs-name` as `.vasp`. The gathered VASP file's first line is replaced with `energy=<final_energy> eV` for later screening.
@@ -301,7 +302,15 @@ atom-site loop. Multiplicity-letter
301
302
  assignments use the conventional
302
303
  International Tables setting from pymatgen/spglib and correspond to the
303
304
  [Bilbao Crystallographic Server WYCKPOS tables](https://cryst.ehu.es/cryst/get_wp.html);
304
- the provenance URL is also recorded inside each generated CIF.
305
+ the provenance URL is also recorded inside each generated CIF.
306
+
307
+ For structural conversion, supercell creation, and GPUMD grouping, PMK reads
308
+ CIF input with pymatgen rather than ASE. ASE interprets
309
+ `_space_group_crystal_system` as a setting hint only for rhombohedral groups
310
+ and emits a potentially misleading warning for other crystal systems. Using
311
+ pymatgen avoids that ambiguity while PMK continues to use ASE for non-CIF
312
+ formats. A partially occupied CIF must first be ordered with `convert -d2o`
313
+ before it can be represented as an explicit atom list.
305
314
 
306
315
  CIF `_atom_type_symbol` and `_atom_site_label` values use per-element
307
316
  inequivalent-site identifiers such as `Li1`, `Y1`, `Cl1`, and `Cl2`.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pymdkit"
7
- version = "1.6.1"
7
+ version = "1.6.2"
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.6.1"
3
+ __version__ = "1.6.2"
@@ -42,6 +42,10 @@ def load_structure(path):
42
42
  try:
43
43
  return Structure.from_file(str(path))
44
44
  except Exception as pymatgen_error:
45
+ if _cifio.is_cif(path):
46
+ raise ValueError(
47
+ f"pymatgen could not read CIF {path}: {pymatgen_error}"
48
+ ) from pymatgen_error
45
49
  try:
46
50
  from ase.io import read
47
51
  from pymatgen.io.ase import AseAtomsAdaptor
@@ -55,6 +55,9 @@ def load_structure(path):
55
55
  try:
56
56
  return Structure.from_file(str(path))
57
57
  except Exception as pymatgen_error:
58
+ if path.suffix.lower() == ".cif":
59
+ print(f" [warn] pymatgen could not read CIF {path}: {pymatgen_error}")
60
+ return None
58
61
  try:
59
62
  from ase.io import read
60
63
  from pymatgen.io.ase import AseAtomsAdaptor
@@ -21,7 +21,32 @@ COMMAND = "convert"
21
21
  HELP = "Convert structures, refine CIF symmetry, or order disorder."
22
22
 
23
23
 
24
+ def _read_cif(path):
25
+ """Read CIF with pymatgen, avoiding ASE's setting-name ambiguity."""
26
+ from pymatgen.core import Structure
27
+ from pymatgen.io.ase import AseAtomsAdaptor
28
+
29
+ structure = Structure.from_file(str(path))
30
+ if not structure.is_ordered:
31
+ raise ValueError(
32
+ "partially occupied CIF cannot be converted to an explicit atom "
33
+ "format; use convert -d2o first"
34
+ )
35
+ return AseAtomsAdaptor.get_atoms(structure)
36
+
37
+
24
38
  def _read(path, all_frames=False):
39
+ path = Path(path)
40
+ if _cifio.is_cif(path):
41
+ if all_frames:
42
+ raise ValueError("CIF input is a structure, not a trajectory")
43
+ try:
44
+ return _read_cif(path)
45
+ except Exception as error:
46
+ raise ValueError(
47
+ f"pymatgen could not read CIF {path}: {error}"
48
+ ) from error
49
+
25
50
  from ase.io import read
26
51
 
27
52
  try:
@@ -234,6 +259,11 @@ def convert_one(
234
259
  angle_tolerance,
235
260
  add_oxidation,
236
261
  )
262
+ elif _cifio.is_cif(input_path) and _cifio.is_cif(output_path):
263
+ structure = _read_structure(input_path)
264
+ _write_structure(
265
+ output_path, structure, refine=False, symmetrize=False
266
+ )
237
267
  else:
238
268
  atoms = _read(input_path, all_frames=trajectory)
239
269
  _write(output_path, atoms, symmetrize=False)
@@ -14,7 +14,7 @@ import argparse
14
14
  from pathlib import Path
15
15
 
16
16
  import numpy as np
17
- from ase.io import read, write
17
+ from ase.io import write
18
18
 
19
19
  try:
20
20
  from . import _cifio, _fileio, convert
@@ -38,7 +38,7 @@ def add_arguments(parser):
38
38
 
39
39
 
40
40
  def _tag_one(in_path, out_path, elements):
41
- atoms = read(str(in_path))
41
+ atoms = convert._read(in_path)
42
42
  groups = []
43
43
  for element in atoms.get_chemical_symbols():
44
44
  if element in elements:
@@ -699,9 +699,15 @@ def run(args):
699
699
 
700
700
  output_root = Path(args.output_folder)
701
701
  output_root.mkdir(parents=True, exist_ok=True)
702
+ occupancy_text = (
703
+ "automatic"
704
+ if allowed_occupancies is None
705
+ else ", ".join(f"{value:.3f}" for value in allowed_occupancies)
706
+ )
702
707
  report = [
703
708
  "PMK realloc-occ summary",
704
709
  f"Target formula: {args.formula}",
710
+ f"Allowed occupancies: {occupancy_text}",
705
711
  f"Maximum supercell: {'x'.join(map(str, max_supercell))}",
706
712
  "",
707
713
  ]
@@ -212,7 +212,11 @@ def read_structure(path):
212
212
  path = Path(path)
213
213
  try:
214
214
  return Structure.from_file(str(path))
215
- except Exception:
215
+ except Exception as pymatgen_error:
216
+ if _cifio.is_cif(path):
217
+ raise ValueError(
218
+ f"pymatgen could not read CIF {path}: {pymatgen_error}"
219
+ ) from pymatgen_error
216
220
  from ase.io import read as ase_read
217
221
  from pymatgen.io.ase import AseAtomsAdaptor
218
222
 
@@ -78,9 +78,7 @@ def make_supercell(
78
78
  max_abc=None,
79
79
  close_abc=None,
80
80
  ):
81
- from ase.io import read
82
-
83
- atoms = read(str(in_path))
81
+ atoms = convert._read(in_path)
84
82
  lengths, _, _ = _lattice.geometry_from_matrix(atoms.cell.array)
85
83
  supercell_matrix = _supercell.resolve_matrix(
86
84
  lengths,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pymdkit
3
- Version: 1.6.1
3
+ Version: 1.6.2
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
@@ -263,8 +263,9 @@ and `0.875`. Generated CIF occupancy fields use two decimal places.
263
263
  `-max-supercell AxBxC` searches bounded diagonal supercells, smallest volume
264
264
  first, until every multiplicity times occupancy times supercell determinant is
265
265
  an integer. Equal-volume choices favor the most isotropic resulting cell.
266
- All selections, site atom counts, oxidation states, charges, and failures are
267
- recorded in `OUTPUT/realloc-occ.txt`.
266
+ All selections, site atom counts, oxidation states, charges, failures, the
267
+ allowed occupancy grid, and the final summary are recorded in
268
+ `OUTPUT/realloc-occ.txt` for both single-file and folder modes.
268
269
 
269
270
  `gather-fs` recursively scans all job folders below the current directory. For
270
271
  VASP it follows the global `vaspout.h5 > vasprun.xml > OUTCAR` priority and
@@ -330,6 +331,14 @@ International Tables setting from pymatgen/spglib and correspond to the
330
331
  [Bilbao Crystallographic Server WYCKPOS tables](https://cryst.ehu.es/cryst/get_wp.html);
331
332
  the provenance URL is also recorded inside each generated CIF.
332
333
 
334
+ For structural conversion, supercell creation, and GPUMD grouping, PMK reads
335
+ CIF input with pymatgen rather than ASE. ASE interprets
336
+ `_space_group_crystal_system` as a setting hint only for rhombohedral groups
337
+ and emits a potentially misleading warning for other crystal systems. Using
338
+ pymatgen avoids that ambiguity while PMK continues to use ASE for non-CIF
339
+ formats. A partially occupied CIF must first be ordered with `convert -d2o`
340
+ before it can be represented as an explicit atom list.
341
+
333
342
  CIF `_atom_type_symbol` and `_atom_site_label` values use per-element
334
343
  inequivalent-site identifiers such as `Li1`, `Y1`, `Cl1`, and `Cl2`.
335
344
  The final atom-site loop starts with `_atom_site_label`, followed by
@@ -9,10 +9,62 @@ from unittest.mock import patch
9
9
  sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
10
10
 
11
11
  from pymdkit import pymdkit_main
12
- from pymdkit.commands import _cifio, _supercell, _symmetry, convert
12
+ from pymdkit.commands import (
13
+ _cifio,
14
+ _supercell,
15
+ _symmetry,
16
+ compute_rmsd,
17
+ convert,
18
+ substitute,
19
+ )
13
20
 
14
21
 
15
22
  class ConvertTests(unittest.TestCase):
23
+ def test_cif_input_uses_pymatgen_reader(self):
24
+ atoms = object()
25
+ with patch.object(convert, "_read_cif", return_value=atoms) as reader:
26
+ self.assertIs(convert._read("input.cif"), atoms)
27
+ reader.assert_called_once_with(Path("input.cif"))
28
+ with self.assertRaisesRegex(ValueError, "not a trajectory"):
29
+ convert._read("input.cif", all_frames=True)
30
+
31
+ def test_failed_cif_parse_never_falls_back_to_ase(self):
32
+ class Structure:
33
+ @classmethod
34
+ def from_file(cls, path):
35
+ raise RuntimeError("invalid CIF")
36
+
37
+ pymatgen = types.ModuleType("pymatgen")
38
+ core = types.ModuleType("pymatgen.core")
39
+ core.Structure = Structure
40
+ pymatgen.core = core
41
+ modules = {"pymatgen": pymatgen, "pymatgen.core": core}
42
+ with patch.dict(sys.modules, modules):
43
+ with self.assertRaisesRegex(ValueError, "pymatgen could not read CIF"):
44
+ _symmetry.load_structure("broken.cif")
45
+ with self.assertRaisesRegex(ValueError, "pymatgen could not read CIF"):
46
+ substitute.read_structure("broken.cif")
47
+ self.assertIsNone(compute_rmsd.load_structure("broken.cif"))
48
+
49
+ def test_cif_to_cif_preserves_disorder_through_pymatgen(self):
50
+ structure = object()
51
+ with patch.object(
52
+ convert, "_read_structure", return_value=structure
53
+ ) as reader, patch.object(
54
+ convert, "_write_structure"
55
+ ) as writer, patch.object(
56
+ convert, "_read"
57
+ ) as ase_reader:
58
+ convert.convert_one("input.cif", "output.cif")
59
+ reader.assert_called_once_with(Path("input.cif"))
60
+ writer.assert_called_once_with(
61
+ Path("output.cif"),
62
+ structure,
63
+ refine=False,
64
+ symmetrize=False,
65
+ )
66
+ ase_reader.assert_not_called()
67
+
16
68
  def test_cli_defaults_to_p1_and_optional_supercell(self):
17
69
  parser = argparse.ArgumentParser()
18
70
  convert.add_arguments(parser)
@@ -176,6 +176,10 @@ class ReallocOccTests(unittest.TestCase):
176
176
  ):
177
177
  self.assertEqual(realloc_occ.run(args), 0)
178
178
  report = (output / "realloc-occ.txt").read_text(encoding="utf-8")
179
+ self.assertIn(
180
+ "Allowed occupancies: 0.250, 0.500, 0.750, 1.000",
181
+ report,
182
+ )
179
183
  self.assertIn("supercell=1x1x1", report)
180
184
  self.assertIn("Li1=6", report)
181
185
  self.assertIn("charge=0.00000000", report)
File without changes
File without changes
File without changes