chemparseplot 1.9.13__tar.gz → 1.9.15__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.
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/PKG-INFO +1 -1
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/_version.py +2 -2
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/eon/__init__.py +2 -2
- chemparseplot-1.9.15/chemparseplot/parse/eon/frame_series.py +191 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/plot/neb.py +43 -30
- chemparseplot-1.9.15/tests/parse/test_frame_series.py +121 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/plot/test_neb_renderers.py +19 -12
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/.gitignore +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/LICENSE +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/__init__.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/api.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/__init__.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/chemgp_hdf5.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/chemgp_jsonl.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/converter.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/eon/_trajectory_common.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/eon/con_io.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/eon/dimer_trajectory.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/eon/gprd.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/eon/min_trajectory.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/eon/minimization.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/eon/neb.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/eon/saddle_search.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/eon/stitch.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/file_.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/grammar/__init__.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/grammar/_deps.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/grammar/orca_text.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/grammar/xyz.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/neb_utils.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/orca/__init__.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/orca/_opi.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/orca/geomscan.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/orca/neb/__init__.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/orca/neb/interp.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/orca/neb/opi_parser.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/patterns.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/plumed.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/projection.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/sella/saddle_search.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/trajectory/__init__.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/trajectory/hdf5.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/trajectory/neb.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/parse/types.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/plot/__init__.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/plot/chemgp.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/plot/geomscan.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/plot/optimization.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/plot/plumed.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/plot/structs.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/plot/theme.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/units.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/chemparseplot/util.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/pyproject.toml +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/readme.md +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/__init__.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/_optional_imports.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/conftest.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/fixtures/h2mol.xyz +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/fixtures/orca_energy_coords_snippet.out +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/fixtures_geomscan_snippet.txt +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/__init__.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/_con_fixtures.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/grammar/__init__.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/grammar/test_grammar_deps.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/grammar/test_orca_text_grammar.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/grammar/test_xyz_grammar.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/orca/test_geomscan.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/orca/test_interp.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/test_chemgp_hdf5.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/test_chemgp_jsonl_types.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/test_con_io.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/test_converter.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/test_dimer_trajectory.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/test_eon_neb_types.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/test_eon_trajectory_common.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/test_min_trajectory.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/test_minimization_result.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/test_neb_stitch.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/test_neb_utils.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/test_orca_public_api.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/test_parser_types.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/test_patterns.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/test_plumed.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/test_projection.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/test_readcon_boundary.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/test_saddle_results.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/test_sella_saddle.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/test_trajectory_hdf5.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/parse/test_trajectory_neb.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/plot/__init__.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/plot/test_chemgp_utils.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/plot/test_neb_helpers.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/plot/test_optimization.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/plot/test_orca_neb_payload.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/plot/test_projection_refactor.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/plot/test_strip_rendering.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/plot/test_structs_units.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/plot/test_surface_auto_thin.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/test_coverage_batch.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/test_full_coverage.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/tests/test_public_api.py +0 -0
- {chemparseplot-1.9.13 → chemparseplot-1.9.15}/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.
|
|
3
|
+
Version: 1.9.15
|
|
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
|
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 9,
|
|
21
|
+
__version__ = version = '1.9.15'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 9, 15)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -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
|
+
)
|
|
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|
|
3
3
|
import io
|
|
4
4
|
import logging
|
|
5
5
|
import shutil
|
|
6
|
-
import subprocess
|
|
7
6
|
import tempfile
|
|
8
7
|
from dataclasses import dataclass
|
|
9
8
|
from pathlib import Path
|
|
@@ -198,20 +197,40 @@ def render_structure_to_image(atoms, zoom, rotation): # noqa: ARG001
|
|
|
198
197
|
return img_data
|
|
199
198
|
|
|
200
199
|
|
|
200
|
+
def _import_xyzrender():
|
|
201
|
+
"""Import xyzrender via ensure_import (AUTO_DEPS) or a normal import.
|
|
202
|
+
|
|
203
|
+
Prefer the Python API over a PATH binary. Matches the suite lazy-dep model
|
|
204
|
+
(rgpycrumbs ``ensure_import`` / ``RGPYCRUMBS_AUTO_DEPS``).
|
|
205
|
+
"""
|
|
206
|
+
try:
|
|
207
|
+
from rgpycrumbs._aux import enable_library_auto_deps, ensure_import
|
|
208
|
+
|
|
209
|
+
enable_library_auto_deps()
|
|
210
|
+
return ensure_import("xyzrender")
|
|
211
|
+
except ImportError:
|
|
212
|
+
pass
|
|
213
|
+
try:
|
|
214
|
+
import xyzrender
|
|
215
|
+
except ImportError as exc:
|
|
216
|
+
msg = (
|
|
217
|
+
"xyzrender is required for strip_renderer='xyzrender'. "
|
|
218
|
+
"Install with: pip install 'xyzrender>=0.1.3' "
|
|
219
|
+
"(or set RGPYCRUMBS_AUTO_DEPS=1 so ensure_import can stage it)"
|
|
220
|
+
)
|
|
221
|
+
raise RuntimeError(msg) from exc
|
|
222
|
+
return xyzrender
|
|
223
|
+
|
|
224
|
+
|
|
201
225
|
def _check_xyzrender():
|
|
202
|
-
"""Verify
|
|
226
|
+
"""Verify xyzrender is importable (package or ensure_import cache).
|
|
203
227
|
|
|
204
228
|
Raises
|
|
205
229
|
------
|
|
206
230
|
RuntimeError
|
|
207
|
-
If xyzrender
|
|
231
|
+
If xyzrender cannot be imported, with install instructions.
|
|
208
232
|
"""
|
|
209
|
-
|
|
210
|
-
msg = (
|
|
211
|
-
"xyzrender binary not found on PATH. "
|
|
212
|
-
"Install with: pip install 'xyzrender>=0.1.3'"
|
|
213
|
-
)
|
|
214
|
-
raise RuntimeError(msg)
|
|
233
|
+
_import_xyzrender()
|
|
215
234
|
|
|
216
235
|
|
|
217
236
|
def _apply_perspective_tilt(atoms, tilt_deg=8.0):
|
|
@@ -267,9 +286,8 @@ def _parse_rotation_angles(rotation_str):
|
|
|
267
286
|
def _render_xyzrender(atoms, rotation="auto", canvas_size=400, config="paton"):
|
|
268
287
|
"""Render an ASE Atoms object to a numpy RGBA array via xyzrender.
|
|
269
288
|
|
|
270
|
-
Uses the
|
|
271
|
-
|
|
272
|
-
``flat``, ``tube``, ``wire``, ``skeletal``.
|
|
289
|
+
Uses the **Python API** (``xyzrender.render``), staged through
|
|
290
|
+
``ensure_import`` when ``RGPYCRUMBS_AUTO_DEPS=1`` — not a PATH binary.
|
|
273
291
|
|
|
274
292
|
Parameters
|
|
275
293
|
----------
|
|
@@ -280,15 +298,18 @@ def _render_xyzrender(atoms, rotation="auto", canvas_size=400, config="paton"):
|
|
|
280
298
|
Any ASE-style string (e.g. ``"0x,90y,0z"``) disables auto-orient
|
|
281
299
|
and pre-rotates the atoms.
|
|
282
300
|
canvas_size : int
|
|
283
|
-
Output image width/height in pixels
|
|
301
|
+
Output image width/height in pixels.
|
|
284
302
|
|
|
285
303
|
Returns
|
|
286
304
|
-------
|
|
287
305
|
numpy.ndarray
|
|
288
306
|
RGBA image array with shape ``(H, W, 4)`` and float dtype.
|
|
289
307
|
"""
|
|
308
|
+
import os
|
|
309
|
+
|
|
290
310
|
from ase.io import write as _ase_write
|
|
291
311
|
|
|
312
|
+
xr = _import_xyzrender()
|
|
292
313
|
with tempfile.NamedTemporaryFile(suffix=".xyz", delete=False) as xyz_fh:
|
|
293
314
|
xyz_path = xyz_fh.name
|
|
294
315
|
png_path = xyz_path.rsplit(".", 1)[0] + ".png"
|
|
@@ -299,31 +320,23 @@ def _render_xyzrender(atoms, rotation="auto", canvas_size=400, config="paton"):
|
|
|
299
320
|
|
|
300
321
|
if has_custom_rotation:
|
|
301
322
|
rx, ry, rz = _parse_rotation_angles(rotation)
|
|
302
|
-
# Pre-rotate and disable auto-orient
|
|
303
323
|
atoms.rotate(rx, "x", center="COP")
|
|
304
324
|
atoms.rotate(ry, "y", center="COP")
|
|
305
325
|
atoms.rotate(rz, "z", center="COP")
|
|
306
326
|
|
|
307
327
|
_ase_write(xyz_path, atoms, format="xyz")
|
|
308
|
-
|
|
309
|
-
|
|
328
|
+
# Python API: same knobs as CLI --hy -t -S --config
|
|
329
|
+
xr.render(
|
|
310
330
|
xyz_path,
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
"-t",
|
|
319
|
-
]
|
|
320
|
-
if has_custom_rotation:
|
|
321
|
-
cmd.append("--no-orient")
|
|
322
|
-
subprocess.run(cmd, check=True, capture_output=True) # noqa: S603
|
|
331
|
+
config=config,
|
|
332
|
+
canvas_size=canvas_size,
|
|
333
|
+
hy=True,
|
|
334
|
+
transparent=True,
|
|
335
|
+
orient=not has_custom_rotation,
|
|
336
|
+
output=png_path,
|
|
337
|
+
)
|
|
323
338
|
img_data = plt.imread(png_path)
|
|
324
339
|
finally:
|
|
325
|
-
import os
|
|
326
|
-
|
|
327
340
|
for p in (xyz_path, png_path):
|
|
328
341
|
try:
|
|
329
342
|
os.unlink(p)
|
|
@@ -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)
|
|
@@ -44,26 +44,33 @@ class TestRenderStructureToImage:
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
class TestCheckXyzrender:
|
|
47
|
-
"""
|
|
47
|
+
"""Package importability (ensure_import / site-packages), not PATH binary."""
|
|
48
48
|
|
|
49
|
-
def
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
def test_missing_package_raises(self):
|
|
50
|
+
import builtins
|
|
51
|
+
real_import = builtins.__import__
|
|
52
|
+
|
|
53
|
+
def fake_import(name, *a, **k):
|
|
54
|
+
if name == "xyzrender" or name.startswith("xyzrender."):
|
|
55
|
+
raise ImportError("blocked")
|
|
56
|
+
if name == "rgpycrumbs" or name.startswith("rgpycrumbs."):
|
|
57
|
+
raise ImportError("blocked")
|
|
58
|
+
return real_import(name, *a, **k)
|
|
59
|
+
|
|
60
|
+
with patch.object(builtins, "__import__", side_effect=fake_import):
|
|
61
|
+
with pytest.raises(RuntimeError, match="xyzrender is required"):
|
|
52
62
|
_check_xyzrender()
|
|
53
63
|
|
|
54
|
-
def
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
def test_present_package_passes(self):
|
|
65
|
+
pytest.importorskip("xyzrender")
|
|
66
|
+
_check_xyzrender()
|
|
57
67
|
|
|
58
68
|
|
|
59
69
|
class TestRenderXyzrender:
|
|
60
|
-
"""xyzrender
|
|
70
|
+
"""xyzrender Python API renderer."""
|
|
61
71
|
|
|
62
|
-
@pytest.mark.skipif(
|
|
63
|
-
shutil.which("xyzrender") is None,
|
|
64
|
-
reason="xyzrender not installed",
|
|
65
|
-
)
|
|
66
72
|
def test_produces_image(self, water):
|
|
73
|
+
pytest.importorskip("xyzrender")
|
|
67
74
|
img = _render_xyzrender(water, canvas_size=200)
|
|
68
75
|
assert isinstance(img, np.ndarray)
|
|
69
76
|
assert img.ndim == 3
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|