chemparseplot 1.8.0__tar.gz → 1.9.0__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.8.0 → chemparseplot-1.9.0}/PKG-INFO +4 -4
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/_version.py +2 -2
- chemparseplot-1.9.0/chemparseplot/__init__.py +28 -0
- {chemparseplot-1.8.0/chemparseplot → chemparseplot-1.9.0/chemparseplot/parse/eon}/__init__.py +3 -2
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/parse/eon/_trajectory_common.py +7 -6
- chemparseplot-1.9.0/chemparseplot/parse/eon/con_io.py +136 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/parse/eon/dimer_trajectory.py +3 -1
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/parse/eon/min_trajectory.py +3 -1
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/parse/eon/stitch.py +29 -28
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/plot/optimization.py +158 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/plot/structs.py +24 -3
- chemparseplot-1.9.0/chemparseplot/units.py +129 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/util.py +2 -1
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/pyproject.toml +4 -2
- chemparseplot-1.9.0/tests/parse/__init__.py +0 -0
- chemparseplot-1.9.0/tests/parse/_con_fixtures.py +89 -0
- chemparseplot-1.9.0/tests/parse/test_con_io.py +237 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/parse/test_dimer_trajectory.py +3 -3
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/parse/test_eon_trajectory_common.py +73 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/parse/test_min_trajectory.py +3 -3
- chemparseplot-1.9.0/tests/parse/test_neb_stitch.py +167 -0
- chemparseplot-1.9.0/tests/parse/test_readcon_boundary.py +78 -0
- chemparseplot-1.9.0/tests/plot/__init__.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/plot/test_optimization.py +43 -0
- chemparseplot-1.9.0/tests/plot/test_structs_units.py +48 -0
- chemparseplot-1.8.0/chemparseplot/parse/eon/__init__.py +0 -1
- chemparseplot-1.8.0/chemparseplot/units.py +0 -23
- chemparseplot-1.8.0/tests/parse/test_neb_stitch.py +0 -70
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/.gitignore +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/LICENSE +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/parse/__init__.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/parse/chemgp_hdf5.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/parse/chemgp_jsonl.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/parse/converter.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/parse/eon/gprd.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/parse/eon/minimization.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/parse/eon/neb.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/parse/eon/saddle_search.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/parse/file_.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/parse/neb_utils.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/parse/orca/__init__.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/parse/orca/geomscan.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/parse/orca/neb/__init__.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/parse/orca/neb/interp.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/parse/orca/neb/opi_parser.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/parse/patterns.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/parse/plumed.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/parse/projection.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/parse/sella/saddle_search.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/parse/trajectory/__init__.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/parse/trajectory/hdf5.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/parse/trajectory/neb.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/parse/types.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/plot/__init__.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/plot/chemgp.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/plot/geomscan.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/plot/neb.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/plot/plumed.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/chemparseplot/plot/theme.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/readme.md +0 -0
- {chemparseplot-1.8.0/tests/plot → chemparseplot-1.9.0/tests}/__init__.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/_optional_imports.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/conftest.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/parse/orca/test_geomscan.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/parse/orca/test_interp.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/parse/test_chemgp_hdf5.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/parse/test_chemgp_jsonl_types.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/parse/test_converter.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/parse/test_eon_neb_types.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/parse/test_minimization_result.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/parse/test_neb_utils.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/parse/test_parser_types.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/parse/test_patterns.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/parse/test_plumed.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/parse/test_projection.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/parse/test_saddle_results.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/parse/test_sella_saddle.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/parse/test_trajectory_hdf5.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/parse/test_trajectory_neb.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/plot/test_chemgp_utils.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/plot/test_neb_helpers.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/plot/test_neb_renderers.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/plot/test_orca_neb_payload.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/plot/test_projection_refactor.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/plot/test_strip_rendering.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/test_coverage_batch.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/test_full_coverage.py +0 -0
- {chemparseplot-1.8.0 → chemparseplot-1.9.0}/tests/tutorials/test_chemparseplot.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chemparseplot
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.0
|
|
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
|
|
@@ -26,7 +26,7 @@ Requires-Dist: cmcrameri>=1.7; extra == 'all'
|
|
|
26
26
|
Requires-Dist: h5py>=3.0; extra == 'all'
|
|
27
27
|
Requires-Dist: matplotlib>=3.8.2; extra == 'all'
|
|
28
28
|
Requires-Dist: polars>=0.20; extra == 'all'
|
|
29
|
-
Requires-Dist: readcon>=0.
|
|
29
|
+
Requires-Dist: readcon>=0.13.1; extra == 'all'
|
|
30
30
|
Requires-Dist: xyzrender>=0.1.2; extra == 'all'
|
|
31
31
|
Provides-Extra: doc
|
|
32
32
|
Requires-Dist: mdit-py-plugins>=0.3.4; extra == 'doc'
|
|
@@ -45,7 +45,7 @@ Provides-Extra: neb
|
|
|
45
45
|
Requires-Dist: ase>=3.22; extra == 'neb'
|
|
46
46
|
Requires-Dist: h5py>=3.0; extra == 'neb'
|
|
47
47
|
Requires-Dist: polars>=0.20; extra == 'neb'
|
|
48
|
-
Requires-Dist: readcon>=0.
|
|
48
|
+
Requires-Dist: readcon>=0.13.1; extra == 'neb'
|
|
49
49
|
Provides-Extra: plot
|
|
50
50
|
Requires-Dist: cmcrameri>=1.7; extra == 'plot'
|
|
51
51
|
Requires-Dist: matplotlib>=3.8.2; extra == 'plot'
|
|
@@ -61,7 +61,7 @@ Requires-Dist: matplotlib>=3.8.2; extra == 'test'
|
|
|
61
61
|
Requires-Dist: polars>=0.20; extra == 'test'
|
|
62
62
|
Requires-Dist: pytest-cov>=4.1.0; extra == 'test'
|
|
63
63
|
Requires-Dist: pytest>=7.4.3; extra == 'test'
|
|
64
|
-
Requires-Dist: readcon>=0.
|
|
64
|
+
Requires-Dist: readcon>=0.13.1; extra == 'test'
|
|
65
65
|
Requires-Dist: rgpycrumbs; extra == 'test'
|
|
66
66
|
Requires-Dist: scipy>=1.11; extra == 'test'
|
|
67
67
|
Provides-Extra: xyzrender
|
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (1,
|
|
21
|
+
__version__ = version = '1.9.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 9, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2023-present Rohit Goswami <rog32@hi.is>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
"""chemparseplot public surface with deferred heavy submodules.
|
|
5
|
+
|
|
6
|
+
``parse`` and ``units`` remain attributes; they load via importlib on first
|
|
7
|
+
access so a bare ``import chemparseplot`` does not construct the pint registry
|
|
8
|
+
or pull the full parse package graph until needed.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
|
+
|
|
16
|
+
__all__ = ["parse", "units"]
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from chemparseplot import parse as parse
|
|
20
|
+
from chemparseplot import units as units
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def __getattr__(name: str) -> Any:
|
|
24
|
+
if name == "parse":
|
|
25
|
+
return importlib.import_module("chemparseplot.parse")
|
|
26
|
+
if name == "units":
|
|
27
|
+
return importlib.import_module("chemparseplot.units")
|
|
28
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
{chemparseplot-1.8.0/chemparseplot → chemparseplot-1.9.0/chemparseplot/parse/eon}/__init__.py
RENAMED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# SPDX-FileCopyrightText: 2023-present Rohit Goswami <rog32@hi.is>
|
|
2
2
|
#
|
|
3
3
|
# SPDX-License-Identifier: MIT
|
|
4
|
+
"""eOn trajectory parsers."""
|
|
4
5
|
|
|
5
|
-
from chemparseplot import
|
|
6
|
+
from chemparseplot.parse.eon import con_io
|
|
6
7
|
|
|
7
|
-
__all__ = ["
|
|
8
|
+
__all__ = ["con_io"]
|
|
@@ -8,9 +8,10 @@ from pathlib import Path
|
|
|
8
8
|
from typing import Any, TypeVar
|
|
9
9
|
|
|
10
10
|
import polars as pl
|
|
11
|
-
import readcon
|
|
12
11
|
from ase import Atoms
|
|
13
12
|
|
|
13
|
+
from chemparseplot.parse.eon import con_io
|
|
14
|
+
|
|
14
15
|
log = logging.getLogger(__name__)
|
|
15
16
|
|
|
16
17
|
T = TypeVar("T")
|
|
@@ -67,7 +68,7 @@ def require_dat_file(job_dir: Path, name: str) -> Path:
|
|
|
67
68
|
def read_first_structure(path: Path) -> Atoms:
|
|
68
69
|
"""Read the first structure from an eOn CON file."""
|
|
69
70
|
|
|
70
|
-
return
|
|
71
|
+
return con_io.read_first_atoms(path)
|
|
71
72
|
|
|
72
73
|
|
|
73
74
|
def read_optional_first(job_dir: Path, names: Sequence[str]) -> Atoms | None:
|
|
@@ -87,14 +88,14 @@ def load_movie_and_table(
|
|
|
87
88
|
dat_name: str,
|
|
88
89
|
parse_dat: Callable[[Path], pl.DataFrame],
|
|
89
90
|
metadata_columns: Sequence[str],
|
|
90
|
-
build_table_from_metadata: Callable[[Sequence[
|
|
91
|
+
build_table_from_metadata: Callable[[Sequence[object]], pl.DataFrame],
|
|
91
92
|
log_label: str,
|
|
92
93
|
) -> tuple[list[Atoms], pl.DataFrame]:
|
|
93
94
|
"""Load the shared movie/data payload for an eOn trajectory parser."""
|
|
94
95
|
|
|
95
96
|
movie_file = resolve_movie_file(job_dir, movie_stem)
|
|
96
97
|
log.info("Loading %s trajectory from %s", log_label, job_dir)
|
|
97
|
-
frames =
|
|
98
|
+
frames = con_io.read_con_frames(movie_file)
|
|
98
99
|
atoms_list = [frame.to_ase() for frame in frames]
|
|
99
100
|
|
|
100
101
|
dat_df = build_table_from_metadata(frames)
|
|
@@ -119,7 +120,7 @@ def load_movie_and_table(
|
|
|
119
120
|
return atoms_list, dat_df
|
|
120
121
|
|
|
121
122
|
|
|
122
|
-
def metadata_value(frame:
|
|
123
|
+
def metadata_value(frame: object, key: str) -> Any:
|
|
123
124
|
"""Return a typed metadata value from a readcon frame."""
|
|
124
125
|
|
|
125
126
|
if key == "frame_index":
|
|
@@ -130,7 +131,7 @@ def metadata_value(frame: readcon.ConFrame, key: str) -> Any:
|
|
|
130
131
|
|
|
131
132
|
|
|
132
133
|
def frame_rows_to_table(
|
|
133
|
-
frames: Sequence[
|
|
134
|
+
frames: Sequence[object],
|
|
134
135
|
columns: Sequence[str],
|
|
135
136
|
*,
|
|
136
137
|
allow_leading_incomplete: bool = False,
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2023-present Rohit Goswami <rog32@hi.is>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
"""Public readcon-backed CON/convel helpers for eOn parsers.
|
|
5
|
+
|
|
6
|
+
All ``.con`` / ``.convel`` I/O in chemparseplot should go through this module so
|
|
7
|
+
metadata-native energies (``readcon>=0.7``) stay on one code path. ``readcon``
|
|
8
|
+
remains an optional extra (``chemparseplot[neb]``); import errors surface only
|
|
9
|
+
when these helpers are called.
|
|
10
|
+
|
|
11
|
+
```{versionadded} 1.8.1
|
|
12
|
+
```
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from collections.abc import Sequence
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"CON_SUFFIXES",
|
|
23
|
+
"frame_with_energy",
|
|
24
|
+
"is_con_path",
|
|
25
|
+
"read_con_as_ase",
|
|
26
|
+
"read_con_frames",
|
|
27
|
+
"read_first_atoms",
|
|
28
|
+
"write_atoms_as_con",
|
|
29
|
+
"write_con_frames",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
CON_SUFFIXES = (".con", ".convel")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def is_con_path(path: str | Path) -> bool:
|
|
36
|
+
"""Return True when *path* looks like an eOn CON/convel file."""
|
|
37
|
+
return Path(path).suffix.lower() in CON_SUFFIXES
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _readcon():
|
|
41
|
+
try:
|
|
42
|
+
import readcon
|
|
43
|
+
except ImportError as exc: # pragma: no cover - exercised when extra missing
|
|
44
|
+
msg = (
|
|
45
|
+
"readcon is required for CON I/O. Install with: "
|
|
46
|
+
"pip install 'chemparseplot[neb]' (readcon>=0.7.0)"
|
|
47
|
+
)
|
|
48
|
+
raise ImportError(msg) from exc
|
|
49
|
+
return readcon
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def read_con_frames(path: str | Path) -> list[Any]:
|
|
53
|
+
"""Read all frames from a CON/convel file as ``readcon.ConFrame`` objects."""
|
|
54
|
+
readcon = _readcon()
|
|
55
|
+
return list(readcon.read_con(str(path)))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def read_con_as_ase(path: str | Path) -> list:
|
|
59
|
+
"""Read all frames from a CON/convel file as ASE ``Atoms`` objects."""
|
|
60
|
+
readcon = _readcon()
|
|
61
|
+
return list(readcon.read_con_as_ase(str(path)))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def read_first_atoms(path: str | Path):
|
|
65
|
+
"""Read the first frame of a CON file as an ASE ``Atoms`` object."""
|
|
66
|
+
return read_con_as_ase(path)[0]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def frame_with_energy(frame: Any, energy: float) -> Any:
|
|
70
|
+
"""Return a ``ConFrame`` copy with per-frame total ``energy`` set (eV).
|
|
71
|
+
|
|
72
|
+
Uses ``ConFrame.set_energy`` when available (readcon>=0.7) so metadata stays
|
|
73
|
+
schema-valid; falls back to reconstructing the frame with string metadata.
|
|
74
|
+
"""
|
|
75
|
+
readcon = _readcon()
|
|
76
|
+
ConFrame = readcon.ConFrame
|
|
77
|
+
metadata = frame.metadata
|
|
78
|
+
if hasattr(metadata, "items"):
|
|
79
|
+
metadata_dict = {str(k): v for k, v in metadata.items()}
|
|
80
|
+
else:
|
|
81
|
+
metadata_dict = dict(metadata or {})
|
|
82
|
+
clone = ConFrame(
|
|
83
|
+
frame.cell,
|
|
84
|
+
frame.angles,
|
|
85
|
+
frame.atoms,
|
|
86
|
+
frame.prebox_header,
|
|
87
|
+
frame.postbox_header,
|
|
88
|
+
metadata_dict,
|
|
89
|
+
)
|
|
90
|
+
value = float(energy)
|
|
91
|
+
setter = getattr(clone, "set_energy", None)
|
|
92
|
+
if callable(setter):
|
|
93
|
+
setter(value)
|
|
94
|
+
return clone
|
|
95
|
+
scalar = getattr(clone, "set_scalar_metadata", None)
|
|
96
|
+
if callable(scalar):
|
|
97
|
+
scalar("energy", value)
|
|
98
|
+
return clone
|
|
99
|
+
metadata_dict["energy"] = str(value)
|
|
100
|
+
return ConFrame(
|
|
101
|
+
frame.cell,
|
|
102
|
+
frame.angles,
|
|
103
|
+
frame.atoms,
|
|
104
|
+
frame.prebox_header,
|
|
105
|
+
frame.postbox_header,
|
|
106
|
+
metadata_dict,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def write_con_frames(path: str | Path, frames: Sequence[Any]) -> Path:
|
|
111
|
+
"""Write ``ConFrame`` objects to *path* via ``readcon.write_con``."""
|
|
112
|
+
readcon = _readcon()
|
|
113
|
+
out = Path(path)
|
|
114
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
115
|
+
readcon.write_con(str(out), list(frames))
|
|
116
|
+
return out
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def write_atoms_as_con(
|
|
120
|
+
path: str | Path,
|
|
121
|
+
atoms_list: Sequence[Any],
|
|
122
|
+
*,
|
|
123
|
+
energies: Sequence[float | None] | None = None,
|
|
124
|
+
) -> Path:
|
|
125
|
+
"""Convert ASE ``Atoms`` to ``ConFrame`` and write a CON file.
|
|
126
|
+
|
|
127
|
+
Optional *energies* (eV) are attached per frame with :func:`frame_with_energy`.
|
|
128
|
+
"""
|
|
129
|
+
readcon = _readcon()
|
|
130
|
+
frames = []
|
|
131
|
+
for idx, atoms in enumerate(atoms_list):
|
|
132
|
+
frame = readcon.ConFrame.from_ase(atoms)
|
|
133
|
+
if energies is not None and idx < len(energies) and energies[idx] is not None:
|
|
134
|
+
frame = frame_with_energy(frame, float(energies[idx]))
|
|
135
|
+
frames.append(frame)
|
|
136
|
+
return write_con_frames(path, frames)
|
|
@@ -16,6 +16,8 @@ from pathlib import Path
|
|
|
16
16
|
import numpy as np
|
|
17
17
|
import polars as pl
|
|
18
18
|
import readcon
|
|
19
|
+
|
|
20
|
+
from chemparseplot.parse.eon import con_io
|
|
19
21
|
from ase import Atoms
|
|
20
22
|
|
|
21
23
|
from ._trajectory_common import (
|
|
@@ -95,7 +97,7 @@ def parse_climb_con(path: Path) -> list[Atoms]:
|
|
|
95
97
|
List of ASE Atoms objects, one per iteration.
|
|
96
98
|
"""
|
|
97
99
|
# eOn .con files may not have a .con extension for movie files
|
|
98
|
-
return
|
|
100
|
+
return con_io.read_con_as_ase(path)
|
|
99
101
|
|
|
100
102
|
|
|
101
103
|
def _load_mode_dat(path: Path) -> np.ndarray | None:
|
|
@@ -15,6 +15,8 @@ from pathlib import Path
|
|
|
15
15
|
|
|
16
16
|
import polars as pl
|
|
17
17
|
import readcon
|
|
18
|
+
|
|
19
|
+
from chemparseplot.parse.eon import con_io
|
|
18
20
|
from ase import Atoms
|
|
19
21
|
|
|
20
22
|
from ._trajectory_common import (
|
|
@@ -80,7 +82,7 @@ def parse_min_con(path: Path) -> list[Atoms]:
|
|
|
80
82
|
list[Atoms]
|
|
81
83
|
List of ASE Atoms objects, one per iteration.
|
|
82
84
|
"""
|
|
83
|
-
return
|
|
85
|
+
return con_io.read_con_as_ase(path)
|
|
84
86
|
|
|
85
87
|
|
|
86
88
|
def table_from_min_metadata(frames: list[readcon.ConFrame]) -> pl.DataFrame:
|
|
@@ -37,19 +37,10 @@ log = logging.getLogger(__name__)
|
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
def _frame_with_energy(frame, energy: float):
|
|
40
|
-
"""Return a ConFrame copy with ``energy`` set
|
|
41
|
-
from
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
metadata["energy"] = str(float(energy))
|
|
45
|
-
return ConFrame(
|
|
46
|
-
frame.cell,
|
|
47
|
-
frame.angles,
|
|
48
|
-
frame.atoms,
|
|
49
|
-
frame.prebox_header,
|
|
50
|
-
frame.postbox_header,
|
|
51
|
-
metadata,
|
|
52
|
-
)
|
|
40
|
+
"""Return a ConFrame copy with ``energy`` set via :mod:`con_io`."""
|
|
41
|
+
from chemparseplot.parse.eon.con_io import frame_with_energy
|
|
42
|
+
|
|
43
|
+
return frame_with_energy(frame, energy)
|
|
53
44
|
|
|
54
45
|
|
|
55
46
|
@dataclass(frozen=True, slots=True)
|
|
@@ -90,11 +81,20 @@ def _cartesian_rmsd(atoms_a, atoms_b) -> float:
|
|
|
90
81
|
|
|
91
82
|
|
|
92
83
|
def _cumulative_rmsd(atoms_list) -> list[float]:
|
|
93
|
-
"""Cumulative consecutive-frame RMSD, starting at 0 for the first frame.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
84
|
+
"""Cumulative consecutive-frame RMSD, starting at 0 for the first frame.
|
|
85
|
+
|
|
86
|
+
Stacks positions once and uses vectorized pairwise steps instead of a
|
|
87
|
+
Python loop over ASE frames (hot path for multi-segment stitch bands).
|
|
88
|
+
"""
|
|
89
|
+
if not atoms_list:
|
|
90
|
+
return []
|
|
91
|
+
if len(atoms_list) == 1:
|
|
92
|
+
return [0.0]
|
|
93
|
+
# Materialize positions in one array: (n_frames, n_atoms, 3)
|
|
94
|
+
pos = np.stack([np.asarray(a.get_positions(), dtype=float) for a in atoms_list])
|
|
95
|
+
deltas = pos[1:] - pos[:-1]
|
|
96
|
+
step = np.sqrt(np.mean(np.sum(deltas * deltas, axis=2), axis=1))
|
|
97
|
+
return [0.0, *np.cumsum(step).tolist()]
|
|
98
98
|
|
|
99
99
|
|
|
100
100
|
def stitch_neb_segments(
|
|
@@ -129,7 +129,11 @@ def stitch_neb_segments(
|
|
|
129
129
|
StitchSummary
|
|
130
130
|
Boundary indices, per-segment barriers, and the overall highest point.
|
|
131
131
|
"""
|
|
132
|
-
from
|
|
132
|
+
from chemparseplot.parse.eon.con_io import (
|
|
133
|
+
read_con_frames,
|
|
134
|
+
write_atoms_as_con,
|
|
135
|
+
write_con_frames,
|
|
136
|
+
)
|
|
133
137
|
|
|
134
138
|
saddle_overrides = saddle_overrides or {}
|
|
135
139
|
out_dir = Path(out_dir)
|
|
@@ -144,7 +148,7 @@ def stitch_neb_segments(
|
|
|
144
148
|
sp_best: tuple[float, object] | None = None # (referenced energy, saddle atoms)
|
|
145
149
|
|
|
146
150
|
for seg_idx, (label, con_path, start, end) in enumerate(segments):
|
|
147
|
-
frames =
|
|
151
|
+
frames = read_con_frames(con_path)
|
|
148
152
|
seg_frames = list(frames[start:end])
|
|
149
153
|
if not seg_frames:
|
|
150
154
|
msg = f"Segment '{label}' ({con_path}) sliced to zero frames."
|
|
@@ -181,7 +185,7 @@ def stitch_neb_segments(
|
|
|
181
185
|
if label in saddle_overrides:
|
|
182
186
|
sad_path, sad_abs = saddle_overrides[label]
|
|
183
187
|
peak_energy = float(sad_abs) + shift - ref
|
|
184
|
-
sad_atoms =
|
|
188
|
+
sad_atoms = read_con_frames(sad_path)[0].to_ase()
|
|
185
189
|
if sp_best is None or peak_energy > sp_best[0]:
|
|
186
190
|
sp_best = (peak_energy, sad_atoms)
|
|
187
191
|
else:
|
|
@@ -199,8 +203,8 @@ def stitch_neb_segments(
|
|
|
199
203
|
)
|
|
200
204
|
|
|
201
205
|
# --- Write the combined band ---
|
|
202
|
-
|
|
203
|
-
|
|
206
|
+
write_con_frames(out_dir / "neb.con", out_frames)
|
|
207
|
+
write_con_frames(out_dir / "neb_path_000.con", out_frames)
|
|
204
208
|
|
|
205
209
|
atoms_list = [f.to_ase() for f in out_frames]
|
|
206
210
|
rxn_coord = _cumulative_rmsd(atoms_list)
|
|
@@ -214,14 +218,11 @@ def stitch_neb_segments(
|
|
|
214
218
|
|
|
215
219
|
# --- Saddle overlay (sp.con) ---
|
|
216
220
|
if sp_best is not None:
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
sp_frame = _frame_with_energy(ConFrame.from_ase(sp_best[1]), float(sp_best[0]))
|
|
220
|
-
write_con(str(out_dir / "sp.con"), [sp_frame])
|
|
221
|
+
write_atoms_as_con(out_dir / "sp.con", [sp_best[1]], energies=[float(sp_best[0])])
|
|
221
222
|
else:
|
|
222
223
|
# Fallback: the global band maximum so the landscape overlay still works.
|
|
223
224
|
peak_idx = int(np.argmax(energies))
|
|
224
|
-
|
|
225
|
+
write_con_frames(out_dir / "sp.con", [out_frames[peak_idx]])
|
|
225
226
|
|
|
226
227
|
highest_index = int(np.argmax(energies))
|
|
227
228
|
summary = StitchSummary(
|
|
@@ -14,6 +14,7 @@ The key semantic difference from NEB plots:
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
import logging
|
|
17
|
+
from collections.abc import Sequence
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
from typing import Any
|
|
19
20
|
|
|
@@ -298,6 +299,163 @@ def plot_single_ended_convergence(trajs, labels, output: Path, dpi: int) -> None
|
|
|
298
299
|
save_standard_figure(fig, output, dpi=dpi)
|
|
299
300
|
|
|
300
301
|
|
|
302
|
+
def render_single_ended_landscape(
|
|
303
|
+
*,
|
|
304
|
+
atoms_list: Sequence[Any],
|
|
305
|
+
energies_eV: np.ndarray,
|
|
306
|
+
ref_a: Any,
|
|
307
|
+
ref_b: Any,
|
|
308
|
+
overlay_atom_lists: Sequence[Sequence[Any]] | None = None,
|
|
309
|
+
overlay_labels: Sequence[str] | None = None,
|
|
310
|
+
ira_instance: Any | None = None,
|
|
311
|
+
ira_kmax: float = 14.0,
|
|
312
|
+
project_path: bool = True,
|
|
313
|
+
surface_type: str = "grad_matern",
|
|
314
|
+
energy_unit: str = "eV",
|
|
315
|
+
energy_cap: float | None = None,
|
|
316
|
+
energy_cap_window: float | None = None,
|
|
317
|
+
cmap: str = "viridis",
|
|
318
|
+
output: Path,
|
|
319
|
+
dpi: int = 150,
|
|
320
|
+
theme: Any | None = None,
|
|
321
|
+
plot_structures: str = "none",
|
|
322
|
+
strip_structs: Sequence[Any] | None = None,
|
|
323
|
+
strip_labels: Sequence[str] | None = None,
|
|
324
|
+
endpoint_start_label: str = "Init",
|
|
325
|
+
endpoint_end_label: str = "Min",
|
|
326
|
+
endpoint_boxed: bool = True,
|
|
327
|
+
annotate_overlay_starts: bool = False,
|
|
328
|
+
overlay_start_label: str = "R",
|
|
329
|
+
strip_renderer: str = "xyzrender",
|
|
330
|
+
xyzrender_config: str = "paton",
|
|
331
|
+
strip_spacing: float = 1.5,
|
|
332
|
+
strip_zoom: float | None = None,
|
|
333
|
+
strip_dividers: bool = False,
|
|
334
|
+
rotation: str = "auto",
|
|
335
|
+
perspective_tilt: float = 0.0,
|
|
336
|
+
) -> None:
|
|
337
|
+
"""Full single-ended landscape pipeline shared by min/saddle CLIs.
|
|
338
|
+
|
|
339
|
+
Coordinates use :func:`~chemparseplot.parse.neb_utils.calculate_landscape_coords`
|
|
340
|
+
and synthetic gradients; the surface/path overlay uses
|
|
341
|
+
:func:`plot_optimization_landscape`. Energies are passed in **eV** and
|
|
342
|
+
converted once for display (avoids double conversion when ``energy_unit``
|
|
343
|
+
is not eV).
|
|
344
|
+
|
|
345
|
+
```{versionadded} 1.8.1
|
|
346
|
+
```
|
|
347
|
+
"""
|
|
348
|
+
from chemparseplot.parse.neb_utils import (
|
|
349
|
+
calculate_landscape_coords,
|
|
350
|
+
compute_synthetic_gradients,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
has_strip = plot_structures == "endpoints" and strip_structs is not None
|
|
354
|
+
fig, ax, ax_strip = create_landscape_axes(dpi=dpi, has_strip=has_strip, theme=theme)
|
|
355
|
+
|
|
356
|
+
rmsd_a, rmsd_b = calculate_landscape_coords(
|
|
357
|
+
atoms_list,
|
|
358
|
+
ira_instance,
|
|
359
|
+
ira_kmax,
|
|
360
|
+
ref_a=ref_a,
|
|
361
|
+
ref_b=ref_b,
|
|
362
|
+
)
|
|
363
|
+
energies = np.asarray(energies_eV, dtype=float)
|
|
364
|
+
n = min(len(rmsd_a), len(energies))
|
|
365
|
+
rmsd_a, rmsd_b, energies = rmsd_a[:n], rmsd_b[:n], energies[:n]
|
|
366
|
+
display_e = convert_energy(energies, energy_unit)
|
|
367
|
+
cap = energy_cap
|
|
368
|
+
if cap is None and energy_cap_window is not None:
|
|
369
|
+
cap = float(np.min(display_e)) + energy_cap_window
|
|
370
|
+
if cap is not None:
|
|
371
|
+
# Cap in display units, map back to eV for the surface routine which
|
|
372
|
+
# converts from eV once.
|
|
373
|
+
factor = float(convert_energy(np.array([1.0]), energy_unit)[0])
|
|
374
|
+
energies = np.minimum(energies, cap / factor if factor else cap)
|
|
375
|
+
display_e = convert_energy(energies, energy_unit)
|
|
376
|
+
|
|
377
|
+
f_para = -np.gradient(energies)
|
|
378
|
+
grad_a, grad_b = compute_synthetic_gradients(rmsd_a, rmsd_b, f_para)
|
|
379
|
+
|
|
380
|
+
plot_optimization_landscape(
|
|
381
|
+
ax,
|
|
382
|
+
rmsd_a,
|
|
383
|
+
rmsd_b,
|
|
384
|
+
grad_a,
|
|
385
|
+
grad_b,
|
|
386
|
+
energies,
|
|
387
|
+
project_path=project_path,
|
|
388
|
+
method=surface_type,
|
|
389
|
+
cmap=cmap,
|
|
390
|
+
label_mode="optimization",
|
|
391
|
+
energy_unit=energy_unit,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
basis = None
|
|
395
|
+
if project_path:
|
|
396
|
+
_, _, basis = project_landscape_path(rmsd_a, rmsd_b, project_path=True)
|
|
397
|
+
|
|
398
|
+
overlays = overlay_atom_lists or ()
|
|
399
|
+
labels = list(overlay_labels or ())
|
|
400
|
+
for idx, atoms_ov in enumerate(overlays):
|
|
401
|
+
lbl = labels[idx] if idx < len(labels) else f"path{idx}"
|
|
402
|
+
ra, rb = calculate_landscape_coords(
|
|
403
|
+
atoms_ov,
|
|
404
|
+
ira_instance,
|
|
405
|
+
ira_kmax,
|
|
406
|
+
ref_a=ref_a,
|
|
407
|
+
ref_b=ref_b,
|
|
408
|
+
)
|
|
409
|
+
m = min(len(ra), len(atoms_ov))
|
|
410
|
+
ra, rb = ra[:m], rb[:m]
|
|
411
|
+
px, py, _ = project_landscape_path(ra, rb, project_path=project_path, basis=basis)
|
|
412
|
+
color = OVERLAY_COLORS[idx % len(OVERLAY_COLORS)]
|
|
413
|
+
if len(overlays) > 1:
|
|
414
|
+
ax.plot(
|
|
415
|
+
px,
|
|
416
|
+
py,
|
|
417
|
+
"o-",
|
|
418
|
+
color=color,
|
|
419
|
+
markersize=3,
|
|
420
|
+
linewidth=1.5,
|
|
421
|
+
alpha=0.8,
|
|
422
|
+
zorder=55,
|
|
423
|
+
label=lbl,
|
|
424
|
+
)
|
|
425
|
+
if annotate_overlay_starts and len(px):
|
|
426
|
+
annotate_endpoint(ax, float(px[0]), float(py[0]), overlay_start_label, boxed=False)
|
|
427
|
+
|
|
428
|
+
plot_x, plot_y, _ = project_landscape_path(
|
|
429
|
+
rmsd_a, rmsd_b, project_path=project_path, basis=basis
|
|
430
|
+
)
|
|
431
|
+
annotate_endpoint(
|
|
432
|
+
ax, float(plot_x[0]), float(plot_y[0]), endpoint_start_label, boxed=endpoint_boxed
|
|
433
|
+
)
|
|
434
|
+
annotate_endpoint(
|
|
435
|
+
ax, float(plot_x[-1]), float(plot_y[-1]), endpoint_end_label, boxed=endpoint_boxed
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
if overlays and len(overlays) > 1:
|
|
439
|
+
ax.legend(frameon=True, framealpha=0.9, loc="best")
|
|
440
|
+
|
|
441
|
+
if has_strip and ax_strip is not None and strip_structs is not None:
|
|
442
|
+
render_endpoint_strip(
|
|
443
|
+
ax_strip,
|
|
444
|
+
list(strip_structs),
|
|
445
|
+
list(strip_labels or []),
|
|
446
|
+
strip_zoom=strip_zoom,
|
|
447
|
+
rotation=rotation,
|
|
448
|
+
theme=theme,
|
|
449
|
+
strip_renderer=strip_renderer,
|
|
450
|
+
strip_spacing=strip_spacing,
|
|
451
|
+
strip_dividers=strip_dividers,
|
|
452
|
+
perspective_tilt=perspective_tilt,
|
|
453
|
+
xyzrender_config=xyzrender_config,
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
save_landscape_figure(fig, output, dpi=dpi, has_strip=has_strip, ax=ax, ax_strip=ax_strip)
|
|
457
|
+
|
|
458
|
+
|
|
301
459
|
def plot_optimization_landscape(
|
|
302
460
|
ax,
|
|
303
461
|
rmsd_a: np.ndarray,
|
|
@@ -44,10 +44,31 @@ def axis_label(label: str, unit: str) -> str:
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
def convert_energy(values: Any, unit: str, *, source_unit: str = "eV") -> np.ndarray:
|
|
47
|
-
"""Convert energy-like values between supported presentation units.
|
|
47
|
+
"""Convert energy-like values between supported presentation units.
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
Delegates to :func:`chemparseplot.units.convert_energy_magnitude` (pint-backed).
|
|
50
|
+
Accepts plain arrays or pint ``Quantity`` instances (converted via ``.to``).
|
|
51
|
+
"""
|
|
52
|
+
from chemparseplot.units import convert_energy_magnitude, normalize_energy_unit
|
|
53
|
+
|
|
54
|
+
# Pint Quantity with a compatible unit → magnitude in target presentation unit.
|
|
55
|
+
to_method = getattr(values, "to", None)
|
|
56
|
+
magnitude = getattr(values, "magnitude", None)
|
|
57
|
+
units = getattr(values, "units", None)
|
|
58
|
+
if callable(to_method) and magnitude is not None and units is not None:
|
|
59
|
+
try:
|
|
60
|
+
# Prefer chemical presentation units when already tagged.
|
|
61
|
+
unit_s = str(units)
|
|
62
|
+
if "chem_" in unit_s or "chem_eV" in unit_s:
|
|
63
|
+
return convert_energy_magnitude(magnitude, unit, source_unit=source_unit)
|
|
64
|
+
# Generic energy Quantity: convert to eV then to presentation unit.
|
|
65
|
+
in_eV = np.asarray(values.to("eV").magnitude, dtype=float)
|
|
66
|
+
return convert_energy_magnitude(in_eV, unit, source_unit="eV")
|
|
67
|
+
except Exception:
|
|
68
|
+
# Fall through to array path.
|
|
69
|
+
values = magnitude
|
|
70
|
+
|
|
71
|
+
return convert_energy_magnitude(values, unit, source_unit=source_unit)
|
|
51
72
|
|
|
52
73
|
|
|
53
74
|
def convert_energy_curvature(
|