chemparseplot 1.9.12__tar.gz → 1.9.14__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 (103) hide show
  1. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/PKG-INFO +2 -2
  2. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/_version.py +2 -2
  3. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/eon/__init__.py +2 -2
  4. chemparseplot-1.9.14/chemparseplot/parse/eon/frame_series.py +191 -0
  5. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/plot/neb.py +2 -0
  6. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/pyproject.toml +1 -1
  7. chemparseplot-1.9.14/tests/parse/test_frame_series.py +121 -0
  8. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/plot/test_surface_auto_thin.py +9 -0
  9. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/.gitignore +0 -0
  10. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/LICENSE +0 -0
  11. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/__init__.py +0 -0
  12. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/api.py +0 -0
  13. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/__init__.py +0 -0
  14. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/chemgp_hdf5.py +0 -0
  15. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/chemgp_jsonl.py +0 -0
  16. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/converter.py +0 -0
  17. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/eon/_trajectory_common.py +0 -0
  18. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/eon/con_io.py +0 -0
  19. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/eon/dimer_trajectory.py +0 -0
  20. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/eon/gprd.py +0 -0
  21. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/eon/min_trajectory.py +0 -0
  22. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/eon/minimization.py +0 -0
  23. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/eon/neb.py +0 -0
  24. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/eon/saddle_search.py +0 -0
  25. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/eon/stitch.py +0 -0
  26. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/file_.py +0 -0
  27. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/grammar/__init__.py +0 -0
  28. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/grammar/_deps.py +0 -0
  29. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/grammar/orca_text.py +0 -0
  30. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/grammar/xyz.py +0 -0
  31. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/neb_utils.py +0 -0
  32. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/orca/__init__.py +0 -0
  33. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/orca/_opi.py +0 -0
  34. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/orca/geomscan.py +0 -0
  35. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/orca/neb/__init__.py +0 -0
  36. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/orca/neb/interp.py +0 -0
  37. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/orca/neb/opi_parser.py +0 -0
  38. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/patterns.py +0 -0
  39. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/plumed.py +0 -0
  40. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/projection.py +0 -0
  41. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/sella/saddle_search.py +0 -0
  42. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/trajectory/__init__.py +0 -0
  43. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/trajectory/hdf5.py +0 -0
  44. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/trajectory/neb.py +0 -0
  45. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/parse/types.py +0 -0
  46. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/plot/__init__.py +0 -0
  47. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/plot/chemgp.py +0 -0
  48. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/plot/geomscan.py +0 -0
  49. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/plot/optimization.py +0 -0
  50. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/plot/plumed.py +0 -0
  51. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/plot/structs.py +0 -0
  52. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/plot/theme.py +0 -0
  53. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/units.py +0 -0
  54. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/chemparseplot/util.py +0 -0
  55. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/readme.md +0 -0
  56. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/__init__.py +0 -0
  57. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/_optional_imports.py +0 -0
  58. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/conftest.py +0 -0
  59. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/fixtures/h2mol.xyz +0 -0
  60. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/fixtures/orca_energy_coords_snippet.out +0 -0
  61. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/fixtures_geomscan_snippet.txt +0 -0
  62. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/__init__.py +0 -0
  63. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/_con_fixtures.py +0 -0
  64. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/grammar/__init__.py +0 -0
  65. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/grammar/test_grammar_deps.py +0 -0
  66. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/grammar/test_orca_text_grammar.py +0 -0
  67. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/grammar/test_xyz_grammar.py +0 -0
  68. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/orca/test_geomscan.py +0 -0
  69. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/orca/test_interp.py +0 -0
  70. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/test_chemgp_hdf5.py +0 -0
  71. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/test_chemgp_jsonl_types.py +0 -0
  72. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/test_con_io.py +0 -0
  73. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/test_converter.py +0 -0
  74. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/test_dimer_trajectory.py +0 -0
  75. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/test_eon_neb_types.py +0 -0
  76. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/test_eon_trajectory_common.py +0 -0
  77. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/test_min_trajectory.py +0 -0
  78. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/test_minimization_result.py +0 -0
  79. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/test_neb_stitch.py +0 -0
  80. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/test_neb_utils.py +0 -0
  81. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/test_orca_public_api.py +0 -0
  82. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/test_parser_types.py +0 -0
  83. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/test_patterns.py +0 -0
  84. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/test_plumed.py +0 -0
  85. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/test_projection.py +0 -0
  86. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/test_readcon_boundary.py +0 -0
  87. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/test_saddle_results.py +0 -0
  88. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/test_sella_saddle.py +0 -0
  89. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/test_trajectory_hdf5.py +0 -0
  90. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/parse/test_trajectory_neb.py +0 -0
  91. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/plot/__init__.py +0 -0
  92. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/plot/test_chemgp_utils.py +0 -0
  93. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/plot/test_neb_helpers.py +0 -0
  94. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/plot/test_neb_renderers.py +0 -0
  95. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/plot/test_optimization.py +0 -0
  96. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/plot/test_orca_neb_payload.py +0 -0
  97. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/plot/test_projection_refactor.py +0 -0
  98. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/plot/test_strip_rendering.py +0 -0
  99. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/plot/test_structs_units.py +0 -0
  100. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/test_coverage_batch.py +0 -0
  101. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/test_full_coverage.py +0 -0
  102. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/test_public_api.py +0 -0
  103. {chemparseplot-1.9.12 → chemparseplot-1.9.14}/tests/tutorials/test_chemparseplot.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chemparseplot
3
- Version: 1.9.12
3
+ Version: 1.9.14
4
4
  Summary: Parsers and plotting tools for computational chemistry
5
5
  Project-URL: Documentation, https://chemparseplot.rgoswami.me
6
6
  Project-URL: Issues, https://github.com/HaoZeke/chemparseplot/issues
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Programming Language :: Python :: Implementation :: CPython
20
- Requires-Python: >=3.10
20
+ Requires-Python: >=3.11
21
21
  Requires-Dist: numpy>=1.26.2
22
22
  Requires-Dist: pint>=0.22
23
23
  Provides-Extra: all
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.9.12'
22
- __version_tuple__ = version_tuple = (1, 9, 12)
21
+ __version__ = version = '1.9.14'
22
+ __version_tuple__ = version_tuple = (1, 9, 14)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -3,6 +3,6 @@
3
3
  # SPDX-License-Identifier: MIT
4
4
  """eOn trajectory parsers."""
5
5
 
6
- from chemparseplot.parse.eon import con_io
6
+ from chemparseplot.parse.eon import con_io, frame_series
7
7
 
8
- __all__ = ["con_io"]
8
+ __all__ = ["con_io", "frame_series"]
@@ -0,0 +1,191 @@
1
+ # SPDX-FileCopyrightText: 2023-present Rohit Goswami <rog32@hi.is>
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ """ConFrame sequence → series / trajectory DTOs for eOn plots.
5
+
6
+ The plot packet is an ordered sequence of ``readcon.ConFrame`` (or duck-typed
7
+ frames with ``energy``, ``metadata``, ``to_ase`` / geometry). ASE and Matter
8
+ are adapters into this wire format; this module does not invent a second schema.
9
+
10
+ ```{versionadded} 1.9.14
11
+ ```
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from collections.abc import Sequence
17
+ from typing import Any
18
+
19
+ import numpy as np
20
+ import polars as pl
21
+ from ase import Atoms
22
+
23
+ from chemparseplot.parse.eon._trajectory_common import (
24
+ frame_rows_to_table,
25
+ metadata_value,
26
+ )
27
+ from chemparseplot.parse.eon.dimer_trajectory import (
28
+ DIMER_METADATA_COLUMNS,
29
+ DimerTrajectoryData,
30
+ table_from_dimer_metadata,
31
+ )
32
+ from chemparseplot.parse.eon.min_trajectory import (
33
+ MIN_METADATA_COLUMNS,
34
+ MinTrajectoryData,
35
+ table_from_min_metadata,
36
+ )
37
+
38
+ __all__ = [
39
+ "atoms_list_from_frames",
40
+ "dimer_trajectory_from_frames",
41
+ "energies_from_frames",
42
+ "min_trajectory_from_frames",
43
+ "neb_path_arrays",
44
+ "series_from_frames",
45
+ ]
46
+
47
+
48
+ def energies_from_frames(frames: Sequence[Any]) -> np.ndarray:
49
+ """Total energy (eV) per frame; raises if any frame lacks energy."""
50
+ if not frames:
51
+ msg = "energies_from_frames: empty frame sequence"
52
+ raise ValueError(msg)
53
+ out = np.empty(len(frames), dtype=float)
54
+ for i, frame in enumerate(frames):
55
+ e = metadata_value(frame, "energy")
56
+ if e is None:
57
+ msg = f"frame {i} has no energy stamp"
58
+ raise ValueError(msg)
59
+ out[i] = float(e)
60
+ return out
61
+
62
+
63
+ def atoms_list_from_frames(frames: Sequence[Any]) -> list[Atoms]:
64
+ """Convert frames to ASE Atoms with energy stored in ``atoms.info``."""
65
+ atoms_list: list[Atoms] = []
66
+ for i, frame in enumerate(frames):
67
+ to_ase = getattr(frame, "to_ase", None)
68
+ if not callable(to_ase):
69
+ msg = f"frame {i} has no to_ase(); need readcon.ConFrame or equivalent"
70
+ raise TypeError(msg)
71
+ atoms = to_ase()
72
+ e = metadata_value(frame, "energy")
73
+ if e is not None:
74
+ atoms.info["energy"] = float(e)
75
+ atoms_list.append(atoms)
76
+ return atoms_list
77
+
78
+
79
+ def series_from_frames(
80
+ frames: Sequence[Any],
81
+ columns: Sequence[str],
82
+ *,
83
+ allow_leading_incomplete: bool = False,
84
+ ) -> pl.DataFrame:
85
+ """Build a polars table of metadata columns from frames (empty if incomplete)."""
86
+ return frame_rows_to_table(
87
+ frames,
88
+ columns,
89
+ allow_leading_incomplete=allow_leading_incomplete,
90
+ )
91
+
92
+
93
+ def neb_path_arrays(frames: Sequence[Any]) -> dict[str, Any]:
94
+ """Extract NEB path series from stamped frames (writePathCon / path_frames).
95
+
96
+ Returns
97
+ -------
98
+ dict
99
+ ``atoms_list``, ``energies``, optional ``reaction_coordinate``,
100
+ ``relative_energy``, ``parallel_force``, ``neb_bead``, ``frame_index``.
101
+ """
102
+ if not frames:
103
+ msg = "neb_path_arrays: empty frame sequence"
104
+ raise ValueError(msg)
105
+ atoms_list = atoms_list_from_frames(frames)
106
+ energies = energies_from_frames(frames)
107
+ result: dict[str, Any] = {
108
+ "atoms_list": atoms_list,
109
+ "energies": energies,
110
+ "n_frames": len(frames),
111
+ }
112
+ for key in (
113
+ "reaction_coordinate",
114
+ "relative_energy",
115
+ "parallel_force",
116
+ "neb_bead",
117
+ "frame_index",
118
+ "lowest_eigenvalue",
119
+ ):
120
+ vals = []
121
+ ok = True
122
+ for frame in frames:
123
+ v = metadata_value(frame, key)
124
+ if v is None and key in ("reaction_coordinate", "relative_energy", "parallel_force"):
125
+ # optional NEB stamps
126
+ ok = False
127
+ break
128
+ vals.append(v)
129
+ if ok and any(v is not None for v in vals):
130
+ result[key] = np.array(
131
+ [np.nan if v is None else float(v) for v in vals],
132
+ dtype=float,
133
+ )
134
+ return result
135
+
136
+
137
+ def min_trajectory_from_frames(frames: Sequence[Any]) -> MinTrajectoryData:
138
+ """Build :class:`MinTrajectoryData` from stamped minimization ConFrames."""
139
+ if not frames:
140
+ msg = "min_trajectory_from_frames: empty frame sequence"
141
+ raise ValueError(msg)
142
+ atoms_list = atoms_list_from_frames(frames)
143
+ dat_df = table_from_min_metadata(list(frames))
144
+ if dat_df.is_empty():
145
+ # Minimal table from energy + sequential iteration
146
+ energies = energies_from_frames(frames)
147
+ dat_df = pl.DataFrame(
148
+ {
149
+ "iteration": list(range(len(frames))),
150
+ "step_size": [0.0] * len(frames),
151
+ "convergence": [0.0] * len(frames),
152
+ "energy": energies.tolist(),
153
+ }
154
+ )
155
+ return MinTrajectoryData(
156
+ atoms_list=atoms_list,
157
+ dat_df=dat_df,
158
+ initial_atoms=atoms_list[0],
159
+ final_atoms=atoms_list[-1],
160
+ )
161
+
162
+
163
+ def dimer_trajectory_from_frames(frames: Sequence[Any]) -> DimerTrajectoryData:
164
+ """Build :class:`DimerTrajectoryData` from stamped climb ConFrames."""
165
+ if not frames:
166
+ msg = "dimer_trajectory_from_frames: empty frame sequence"
167
+ raise ValueError(msg)
168
+ atoms_list = atoms_list_from_frames(frames)
169
+ dat_df = table_from_dimer_metadata(list(frames))
170
+ if dat_df.is_empty():
171
+ energies = energies_from_frames(frames)
172
+ e0 = float(energies[0])
173
+ dat_df = pl.DataFrame(
174
+ {
175
+ "iteration": list(range(len(frames))),
176
+ "step_size": [0.0] * len(frames),
177
+ "delta_e": (energies - e0).tolist(),
178
+ "convergence": [0.0] * len(frames),
179
+ "eigenvalue": [0.0] * len(frames),
180
+ "torque": [0.0] * len(frames),
181
+ "angle": [0.0] * len(frames),
182
+ "rotations": [0] * len(frames),
183
+ }
184
+ )
185
+ return DimerTrajectoryData(
186
+ atoms_list=atoms_list,
187
+ dat_df=dat_df,
188
+ initial_atoms=atoms_list[0],
189
+ saddle_atoms=atoms_list[-1],
190
+ mode_vector=None,
191
+ )
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import io
2
4
  import logging
3
5
  import shutil
@@ -16,7 +16,7 @@ readme = "readme.md"
16
16
  keywords = ["compchem", "parser", "plot"]
17
17
  license = { text = "MIT" }
18
18
  authors = [{ name = "Rohit Goswami", email = "rog32@hi.is" }]
19
- requires-python = ">=3.10"
19
+ requires-python = ">=3.11"
20
20
  classifiers = [
21
21
  "Development Status :: 4 - Beta",
22
22
  "Programming Language :: Python",
@@ -0,0 +1,121 @@
1
+ # SPDX-FileCopyrightText: 2023-present Rohit Goswami <rog32@hi.is>
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ """ConFrame sequence → series used by eOn plot paths."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import numpy as np
9
+ import pytest
10
+
11
+ pytest.importorskip("readcon")
12
+ pytest.importorskip("ase")
13
+ pytest.importorskip("polars")
14
+
15
+ from ase import Atoms
16
+
17
+ from chemparseplot.parse.eon.frame_series import (
18
+ atoms_list_from_frames,
19
+ dimer_trajectory_from_frames,
20
+ energies_from_frames,
21
+ min_trajectory_from_frames,
22
+ neb_path_arrays,
23
+ )
24
+
25
+
26
+ def _h_frame(energy: float, z: float, *, frame_index: int, **scalars):
27
+ import readcon
28
+
29
+ atoms = Atoms("H", positions=[[0.0, 0.0, z]], cell=[10, 10, 10], pbc=False)
30
+ frame = readcon.ConFrame.from_ase(atoms)
31
+ frame.set_energy(float(energy))
32
+ frame.set_frame_index(int(frame_index))
33
+ for key, value in scalars.items():
34
+ frame.set_scalar_metadata(key, float(value) if isinstance(value, float) else value)
35
+ return frame
36
+
37
+
38
+ def test_energies_from_frames_match_stamps():
39
+ frames = [_h_frame(1.0 + 0.1 * i, float(i), frame_index=i) for i in range(4)]
40
+ e = energies_from_frames(frames)
41
+ assert e.shape == (4,)
42
+ assert np.allclose(e, [1.0, 1.1, 1.2, 1.3])
43
+
44
+
45
+ def test_energies_from_frames_missing_raises():
46
+ import readcon
47
+ from ase import Atoms
48
+
49
+ bare = readcon.ConFrame.from_ase(Atoms("H", positions=[[0, 0, 0]]))
50
+ with pytest.raises(ValueError, match="no energy"):
51
+ energies_from_frames([bare])
52
+
53
+
54
+ def test_neb_path_arrays_length_and_stamps():
55
+ frames = []
56
+ for i in range(5):
57
+ fr = _h_frame(
58
+ -10.0 + i,
59
+ 0.1 * i,
60
+ frame_index=i,
61
+ reaction_coordinate=0.2 * i,
62
+ relative_energy=float(i),
63
+ parallel_force=-0.01 * i,
64
+ )
65
+ fr.set_neb_bead(i)
66
+ frames.append(fr)
67
+ path = neb_path_arrays(frames)
68
+ assert path["n_frames"] == 5
69
+ assert len(path["atoms_list"]) == 5
70
+ assert np.allclose(path["energies"], [-10, -9, -8, -7, -6])
71
+ assert np.allclose(path["reaction_coordinate"], [0.0, 0.2, 0.4, 0.6, 0.8])
72
+ assert np.allclose(path["relative_energy"], [0, 1, 2, 3, 4])
73
+ assert path["atoms_list"][0].info["energy"] == pytest.approx(-10.0)
74
+
75
+
76
+ def test_min_trajectory_from_frames_metrics():
77
+ frames = [
78
+ _h_frame(
79
+ -5.0 + 0.5 * i,
80
+ 0.0,
81
+ frame_index=i,
82
+ step_size=0.1 * (i + 1),
83
+ convergence=1.0 / (i + 1),
84
+ )
85
+ for i in range(3)
86
+ ]
87
+ traj = min_trajectory_from_frames(frames)
88
+ assert len(traj.atoms_list) == 3
89
+ assert traj.dat_df.height == 3
90
+ assert "energy" in traj.dat_df.columns
91
+ assert traj.dat_df["energy"].to_list() == pytest.approx([-5.0, -4.5, -4.0])
92
+ assert traj.final_atoms is traj.atoms_list[-1]
93
+
94
+
95
+ def test_dimer_trajectory_from_frames_metrics():
96
+ frames = [
97
+ _h_frame(
98
+ 0.0 + i,
99
+ 0.0,
100
+ frame_index=i,
101
+ step_size=0.05,
102
+ delta_e=float(i),
103
+ convergence=0.1,
104
+ eigenvalue=-1.0,
105
+ torque=0.0,
106
+ angle=0.0,
107
+ rotations=0,
108
+ )
109
+ for i in range(3)
110
+ ]
111
+ # rotations may need int via set_scalar_metadata
112
+ traj = dimer_trajectory_from_frames(frames)
113
+ assert len(traj.atoms_list) == 3
114
+ assert traj.dat_df.height == 3
115
+ assert "delta_e" in traj.dat_df.columns or "energy" in traj.dat_df.columns
116
+
117
+
118
+ def test_atoms_list_from_frames_preserves_energy_info():
119
+ frames = [_h_frame(2.5, 0.0, frame_index=0)]
120
+ atoms = atoms_list_from_frames(frames)
121
+ assert atoms[0].info["energy"] == pytest.approx(2.5)
@@ -185,3 +185,12 @@ class TestSurfaceFitConfig:
185
185
  )
186
186
  assert seen["n"] <= 20
187
187
  plt.close(fig)
188
+
189
+
190
+ def test_surface_fit_config_importable():
191
+ """Regression: class body return annotations need future annotations (Py3.11+)."""
192
+ from chemparseplot.plot.neb import SurfaceFitConfig
193
+
194
+ cfg = SurfaceFitConfig.from_mapping({"auto_thin": True, "max_surface_points": 32})
195
+ assert cfg.auto_thin is True
196
+ assert cfg.max_surface_points == 32
File without changes
File without changes