chemparseplot 1.4.1__tar.gz → 1.5.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.4.1 → chemparseplot-1.5.0}/PKG-INFO +1 -1
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/_version.py +2 -2
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/parse/__init__.py +2 -2
- chemparseplot-1.5.0/chemparseplot/parse/eon/__init__.py +1 -0
- chemparseplot-1.5.0/chemparseplot/parse/eon/dimer_trajectory.py +154 -0
- chemparseplot-1.5.0/chemparseplot/parse/eon/min_trajectory.py +133 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/parse/neb_utils.py +27 -12
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/parse/plumed.py +10 -0
- chemparseplot-1.5.0/chemparseplot/parse/projection.py +149 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/plot/__init__.py +4 -11
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/plot/neb.py +529 -88
- chemparseplot-1.5.0/chemparseplot/plot/optimization.py +251 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/plot/structs.py +1 -1
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/pyproject.toml +13 -12
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/tests/parse/test_chemgp_hdf5.py +6 -4
- chemparseplot-1.5.0/tests/parse/test_dimer_trajectory.py +166 -0
- chemparseplot-1.5.0/tests/parse/test_min_trajectory.py +146 -0
- chemparseplot-1.5.0/tests/parse/test_projection.py +148 -0
- chemparseplot-1.5.0/tests/plot/test_optimization.py +220 -0
- chemparseplot-1.5.0/tests/plot/test_projection_refactor.py +119 -0
- chemparseplot-1.5.0/tests/plot/test_strip_rendering.py +178 -0
- chemparseplot-1.5.0/tests/test_coverage_batch.py +463 -0
- chemparseplot-1.5.0/tests/test_full_coverage.py +1683 -0
- chemparseplot-1.4.1/chemparseplot/scripts/__init__.py +0 -3
- chemparseplot-1.4.1/chemparseplot/scripts/plot_gp.py +0 -732
- chemparseplot-1.4.1/chemparseplot/scripts/plt_neb.py +0 -1151
- chemparseplot-1.4.1/tests/scripts/__init__.py +0 -3
- chemparseplot-1.4.1/tests/scripts/test_plot_gp_cli.py +0 -54
- chemparseplot-1.4.1/tests/scripts/test_plt_neb_cli.py +0 -54
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/.gitignore +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/LICENSE +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/__init__.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/parse/chemgp_hdf5.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/parse/chemgp_jsonl.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/parse/converter.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/parse/eon/gprd.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/parse/eon/minimization.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/parse/eon/neb.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/parse/eon/saddle_search.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/parse/file_.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/parse/orca/__init__.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/parse/orca/geomscan.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/parse/orca/neb/__init__.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/parse/orca/neb/interp.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/parse/orca/neb/opi_parser.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/parse/patterns.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/parse/sella/saddle_search.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/parse/trajectory/__init__.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/parse/trajectory/hdf5.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/parse/trajectory/neb.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/plot/chemgp.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/plot/geomscan.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/plot/plumed.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/plot/theme.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/units.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/chemparseplot/util.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/readme.md +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/tests/conftest.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/tests/parse/orca/test_geomscan.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/tests/parse/orca/test_interp.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/tests/parse/test_converter.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/tests/parse/test_neb_utils.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/tests/parse/test_patterns.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/tests/parse/test_plumed.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/tests/parse/test_trajectory_hdf5.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/tests/parse/test_trajectory_neb.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/tests/plot/__init__.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/tests/plot/test_chemgp_utils.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.0}/tests/plot/test_neb_renderers.py +0 -0
- {chemparseplot-1.4.1 → chemparseplot-1.5.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.5.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
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '1.
|
|
32
|
-
__version_tuple__ = version_tuple = (1,
|
|
31
|
+
__version__ = version = '1.5.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 5, 0)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
#
|
|
3
3
|
# SPDX-License-Identifier: MIT
|
|
4
4
|
|
|
5
|
-
from chemparseplot.parse import orca, patterns
|
|
5
|
+
from chemparseplot.parse import eon, orca, patterns
|
|
6
6
|
|
|
7
7
|
# Lazy imports for modules with optional heavy deps (h5py, pandas)
|
|
8
8
|
# Import directly: from chemparseplot.parse.chemgp_hdf5 import read_h5_table
|
|
9
|
-
# Or: from chemparseplot.parse import plumed
|
|
9
|
+
# Or: from chemparseplot.parse import plumed, projection
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""eOn trajectory parsers."""
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Dimer/saddle search trajectory parser for eOn output.
|
|
2
|
+
|
|
3
|
+
Reads structured per-iteration data from ``climb.dat`` (TSV) and
|
|
4
|
+
concatenated trajectory from ``climb.con`` (movie file), as produced
|
|
5
|
+
by eOn with ``write_movies=true``.
|
|
6
|
+
|
|
7
|
+
.. versionadded:: 1.5.0
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
import polars as pl
|
|
18
|
+
from ase import Atoms
|
|
19
|
+
from ase.io import read as ase_read
|
|
20
|
+
|
|
21
|
+
log = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class DimerTrajectoryData:
|
|
26
|
+
"""Container for a dimer/saddle search trajectory.
|
|
27
|
+
|
|
28
|
+
Attributes
|
|
29
|
+
----------
|
|
30
|
+
atoms_list
|
|
31
|
+
Per-iteration structures from the movie file.
|
|
32
|
+
dat_df
|
|
33
|
+
Polars DataFrame with per-iteration metrics from ``climb.dat``.
|
|
34
|
+
initial_atoms
|
|
35
|
+
Starting structure (from ``reactant.con`` or ``pos.con``).
|
|
36
|
+
saddle_atoms
|
|
37
|
+
Final saddle point structure (from ``saddle.con``), or None.
|
|
38
|
+
mode_vector
|
|
39
|
+
Eigenvector at the saddle (from ``mode.dat``), or None.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
atoms_list: list[Atoms]
|
|
43
|
+
dat_df: pl.DataFrame
|
|
44
|
+
initial_atoms: Atoms
|
|
45
|
+
saddle_atoms: Atoms | None = None
|
|
46
|
+
mode_vector: np.ndarray | None = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def parse_climb_dat(path: Path) -> pl.DataFrame:
|
|
50
|
+
"""Read the structured ``climb.dat`` TSV file.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
path
|
|
55
|
+
Path to the ``climb.dat`` file.
|
|
56
|
+
|
|
57
|
+
Returns
|
|
58
|
+
-------
|
|
59
|
+
pl.DataFrame
|
|
60
|
+
DataFrame with columns matching the TSV header.
|
|
61
|
+
"""
|
|
62
|
+
return pl.read_csv(path, separator="\t")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def parse_climb_con(path: Path) -> list[Atoms]:
|
|
66
|
+
"""Read concatenated structures from the ``climb.con`` movie file.
|
|
67
|
+
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
path
|
|
71
|
+
Path to the ``climb`` or ``climb.con`` file.
|
|
72
|
+
|
|
73
|
+
Returns
|
|
74
|
+
-------
|
|
75
|
+
list[Atoms]
|
|
76
|
+
List of ASE Atoms objects, one per iteration.
|
|
77
|
+
"""
|
|
78
|
+
# eOn .con files may not have a .con extension for movie files
|
|
79
|
+
atoms_list = ase_read(str(path), index=":", format="eon")
|
|
80
|
+
return list(atoms_list)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _find_initial_structure(job_dir: Path) -> Atoms | None:
|
|
84
|
+
"""Locate the initial/reactant structure in the job directory."""
|
|
85
|
+
for name in ("reactant.con", "pos.con"):
|
|
86
|
+
p = job_dir / name
|
|
87
|
+
if p.exists():
|
|
88
|
+
return ase_read(str(p), format="eon")
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _load_mode_dat(path: Path) -> np.ndarray | None:
|
|
93
|
+
"""Load eigenvector from mode.dat (Nx3 whitespace-separated)."""
|
|
94
|
+
if not path.exists():
|
|
95
|
+
return None
|
|
96
|
+
return np.loadtxt(path)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def load_dimer_trajectory(job_dir: Path) -> DimerTrajectoryData:
|
|
100
|
+
"""Load a complete dimer/saddle search trajectory from an eOn job directory.
|
|
101
|
+
|
|
102
|
+
Expects the job to have been run with ``write_movies=true``.
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
job_dir
|
|
107
|
+
Path to the eOn job output directory containing ``climb``,
|
|
108
|
+
``climb.dat``, ``saddle.con``, etc.
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
DimerTrajectoryData
|
|
113
|
+
Combined trajectory data.
|
|
114
|
+
|
|
115
|
+
Raises
|
|
116
|
+
------
|
|
117
|
+
FileNotFoundError
|
|
118
|
+
If required files (``climb``, ``climb.dat``) are missing.
|
|
119
|
+
"""
|
|
120
|
+
# Find the movie file (may be "climb" or "climb.con")
|
|
121
|
+
climb_con = job_dir / "climb"
|
|
122
|
+
if not climb_con.exists():
|
|
123
|
+
climb_con = job_dir / "climb.con"
|
|
124
|
+
if not climb_con.exists():
|
|
125
|
+
msg = f"No climb movie file found in {job_dir}"
|
|
126
|
+
raise FileNotFoundError(msg)
|
|
127
|
+
|
|
128
|
+
climb_dat = job_dir / "climb.dat"
|
|
129
|
+
if not climb_dat.exists():
|
|
130
|
+
msg = f"No climb.dat found in {job_dir} (was write_movies enabled?)"
|
|
131
|
+
raise FileNotFoundError(msg)
|
|
132
|
+
|
|
133
|
+
log.info("Loading dimer trajectory from %s", job_dir)
|
|
134
|
+
atoms_list = parse_climb_con(climb_con)
|
|
135
|
+
dat_df = parse_climb_dat(climb_dat)
|
|
136
|
+
log.info("Loaded %d frames, %d data rows", len(atoms_list), dat_df.height)
|
|
137
|
+
|
|
138
|
+
initial = _find_initial_structure(job_dir)
|
|
139
|
+
if initial is None:
|
|
140
|
+
log.warning("No reactant.con or pos.con found; using first movie frame")
|
|
141
|
+
initial = atoms_list[0]
|
|
142
|
+
|
|
143
|
+
saddle_path = job_dir / "saddle.con"
|
|
144
|
+
saddle = ase_read(str(saddle_path), format="eon") if saddle_path.exists() else None
|
|
145
|
+
|
|
146
|
+
mode = _load_mode_dat(job_dir / "mode.dat")
|
|
147
|
+
|
|
148
|
+
return DimerTrajectoryData(
|
|
149
|
+
atoms_list=atoms_list,
|
|
150
|
+
dat_df=dat_df,
|
|
151
|
+
initial_atoms=initial,
|
|
152
|
+
saddle_atoms=saddle,
|
|
153
|
+
mode_vector=mode,
|
|
154
|
+
)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Minimization trajectory parser for eOn output.
|
|
2
|
+
|
|
3
|
+
Reads structured per-iteration data from the minimization ``.dat`` file
|
|
4
|
+
and concatenated trajectory from the movie ``.con`` file, as produced
|
|
5
|
+
by eOn with ``write_movies=true``.
|
|
6
|
+
|
|
7
|
+
.. versionadded:: 1.5.0
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import polars as pl
|
|
17
|
+
from ase import Atoms
|
|
18
|
+
from ase.io import read as ase_read
|
|
19
|
+
|
|
20
|
+
log = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class MinTrajectoryData:
|
|
25
|
+
"""Container for a minimization trajectory.
|
|
26
|
+
|
|
27
|
+
Attributes
|
|
28
|
+
----------
|
|
29
|
+
atoms_list
|
|
30
|
+
Per-iteration structures from the movie file.
|
|
31
|
+
dat_df
|
|
32
|
+
Polars DataFrame with per-iteration metrics.
|
|
33
|
+
initial_atoms
|
|
34
|
+
Starting structure (first frame).
|
|
35
|
+
final_atoms
|
|
36
|
+
Final minimized structure (from ``min.con`` or last frame).
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
atoms_list: list[Atoms]
|
|
40
|
+
dat_df: pl.DataFrame
|
|
41
|
+
initial_atoms: Atoms
|
|
42
|
+
final_atoms: Atoms
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def parse_min_dat(path: Path) -> pl.DataFrame:
|
|
46
|
+
"""Read the structured minimization TSV data file.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
path
|
|
51
|
+
Path to the minimization ``.dat`` file.
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
pl.DataFrame
|
|
56
|
+
DataFrame with columns: iteration, step_size, convergence, energy.
|
|
57
|
+
"""
|
|
58
|
+
return pl.read_csv(path, separator="\t")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def parse_min_con(path: Path) -> list[Atoms]:
|
|
62
|
+
"""Read concatenated structures from the minimization movie file.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
path
|
|
67
|
+
Path to the movie ``.con`` file.
|
|
68
|
+
|
|
69
|
+
Returns
|
|
70
|
+
-------
|
|
71
|
+
list[Atoms]
|
|
72
|
+
List of ASE Atoms objects, one per iteration.
|
|
73
|
+
"""
|
|
74
|
+
atoms_list = ase_read(str(path), index=":", format="eon")
|
|
75
|
+
return list(atoms_list)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def load_min_trajectory(
|
|
79
|
+
job_dir: Path,
|
|
80
|
+
prefix: str = "min",
|
|
81
|
+
) -> MinTrajectoryData:
|
|
82
|
+
"""Load a complete minimization trajectory from an eOn job directory.
|
|
83
|
+
|
|
84
|
+
Expects the job to have been run with ``write_movies=true``.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
job_dir
|
|
89
|
+
Path to the eOn job output directory.
|
|
90
|
+
prefix
|
|
91
|
+
Movie file prefix (default ``"min"``). The movie file is
|
|
92
|
+
``{prefix}`` and the data file is ``{prefix}.dat``.
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
MinTrajectoryData
|
|
97
|
+
Combined trajectory data.
|
|
98
|
+
|
|
99
|
+
Raises
|
|
100
|
+
------
|
|
101
|
+
FileNotFoundError
|
|
102
|
+
If required files are missing.
|
|
103
|
+
"""
|
|
104
|
+
movie_file = job_dir / prefix
|
|
105
|
+
if not movie_file.exists():
|
|
106
|
+
movie_file = job_dir / f"{prefix}.con"
|
|
107
|
+
if not movie_file.exists():
|
|
108
|
+
msg = f"No minimization movie file ({prefix}) found in {job_dir}"
|
|
109
|
+
raise FileNotFoundError(msg)
|
|
110
|
+
|
|
111
|
+
dat_file = job_dir / f"{prefix}.dat"
|
|
112
|
+
if not dat_file.exists():
|
|
113
|
+
msg = f"No {prefix}.dat found in {job_dir} (was write_movies enabled?)"
|
|
114
|
+
raise FileNotFoundError(msg)
|
|
115
|
+
|
|
116
|
+
log.info("Loading minimization trajectory from %s", job_dir)
|
|
117
|
+
atoms_list = parse_min_con(movie_file)
|
|
118
|
+
dat_df = parse_min_dat(dat_file)
|
|
119
|
+
log.info("Loaded %d frames, %d data rows", len(atoms_list), dat_df.height)
|
|
120
|
+
|
|
121
|
+
# Final structure: prefer explicit min.con, fall back to last movie frame
|
|
122
|
+
min_con = job_dir / "min.con"
|
|
123
|
+
if min_con.exists():
|
|
124
|
+
final = ase_read(str(min_con), format="eon")
|
|
125
|
+
else:
|
|
126
|
+
final = atoms_list[-1]
|
|
127
|
+
|
|
128
|
+
return MinTrajectoryData(
|
|
129
|
+
atoms_list=atoms_list,
|
|
130
|
+
dat_df=dat_df,
|
|
131
|
+
initial_atoms=atoms_list[0],
|
|
132
|
+
final_atoms=final,
|
|
133
|
+
)
|
|
@@ -8,6 +8,8 @@ RMSD landscape coordinate calculation, synthetic 2D gradient projection,
|
|
|
8
8
|
and landscape DataFrame construction.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
11
13
|
import logging
|
|
12
14
|
|
|
13
15
|
import numpy as np
|
|
@@ -18,43 +20,56 @@ log = logging.getLogger(__name__)
|
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
def calculate_landscape_coords(
|
|
21
|
-
atoms_list: list[Atoms],
|
|
23
|
+
atoms_list: list[Atoms],
|
|
24
|
+
ira_instance,
|
|
25
|
+
ira_kmax: float,
|
|
26
|
+
ref_a: Atoms | None = None,
|
|
27
|
+
ref_b: Atoms | None = None,
|
|
22
28
|
) -> tuple[np.ndarray, np.ndarray]:
|
|
23
|
-
"""Calculate 2D landscape coordinates (RMSD-
|
|
29
|
+
"""Calculate 2D landscape coordinates (RMSD-A, RMSD-B) for a path.
|
|
24
30
|
|
|
25
31
|
```{versionadded} 1.2.0
|
|
26
32
|
```
|
|
27
33
|
|
|
28
|
-
|
|
34
|
+
```{versionchanged} 1.5.0
|
|
35
|
+
Added *ref_a* and *ref_b* parameters for explicit reference structures.
|
|
36
|
+
```
|
|
29
37
|
|
|
30
38
|
:param atoms_list: List of ASE Atoms objects representing the path.
|
|
31
39
|
:param ira_instance: An instantiated IRA object (or None).
|
|
32
40
|
:param ira_kmax: kmax factor for IRA.
|
|
33
|
-
:
|
|
41
|
+
:param ref_a: Reference structure A. Defaults to ``atoms_list[0]``.
|
|
42
|
+
:param ref_b: Reference structure B. Defaults to ``atoms_list[-1]``.
|
|
43
|
+
:return: A tuple of (rmsd_a, rmsd_b) arrays.
|
|
34
44
|
"""
|
|
35
45
|
from concurrent.futures import ThreadPoolExecutor
|
|
36
46
|
|
|
37
47
|
from rgpycrumbs.geom.api.alignment import calculate_rmsd_from_ref
|
|
38
48
|
|
|
39
|
-
|
|
49
|
+
if ref_a is None:
|
|
50
|
+
ref_a = atoms_list[0]
|
|
51
|
+
if ref_b is None:
|
|
52
|
+
ref_b = atoms_list[-1]
|
|
53
|
+
|
|
54
|
+
log.info("Calculating landscape coordinates (RMSD-A, RMSD-B)...")
|
|
40
55
|
with ThreadPoolExecutor(max_workers=2) as pool:
|
|
41
|
-
|
|
56
|
+
fut_a = pool.submit(
|
|
42
57
|
calculate_rmsd_from_ref,
|
|
43
58
|
atoms_list,
|
|
44
59
|
ira_instance,
|
|
45
|
-
ref_atom=
|
|
60
|
+
ref_atom=ref_a,
|
|
46
61
|
ira_kmax=ira_kmax,
|
|
47
62
|
)
|
|
48
|
-
|
|
63
|
+
fut_b = pool.submit(
|
|
49
64
|
calculate_rmsd_from_ref,
|
|
50
65
|
atoms_list,
|
|
51
66
|
ira_instance,
|
|
52
|
-
ref_atom=
|
|
67
|
+
ref_atom=ref_b,
|
|
53
68
|
ira_kmax=ira_kmax,
|
|
54
69
|
)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return
|
|
70
|
+
rmsd_a = fut_a.result()
|
|
71
|
+
rmsd_b = fut_b.result()
|
|
72
|
+
return rmsd_a, rmsd_b
|
|
58
73
|
|
|
59
74
|
|
|
60
75
|
def compute_synthetic_gradients(
|
|
@@ -196,6 +196,12 @@ def calculate_fes_from_hills(hills, imin=1, imax=None, xlim=None, ylim=None, npo
|
|
|
196
196
|
|
|
197
197
|
dx = max_cv1 - min_cv1
|
|
198
198
|
dy = max_cv2 - min_cv2
|
|
199
|
+
if dx == 0:
|
|
200
|
+
sigma_x = np.max(hills_data[:, 3])
|
|
201
|
+
dx = 6 * sigma_x
|
|
202
|
+
if dy == 0:
|
|
203
|
+
sigma_y = np.max(hills_data[:, 4])
|
|
204
|
+
dy = 6 * sigma_y
|
|
199
205
|
xlims = [min_cv1 - 0.05 * dx, max_cv1 + 0.05 * dx]
|
|
200
206
|
ylims = [min_cv2 - 0.05 * dy, max_cv2 + 0.05 * dy]
|
|
201
207
|
|
|
@@ -247,6 +253,10 @@ def calculate_fes_from_hills(hills, imin=1, imax=None, xlim=None, ylim=None, npo
|
|
|
247
253
|
# Determine grid boundaries
|
|
248
254
|
min_cv1, max_cv1 = np.min(hills_data[:, 1]), np.max(hills_data[:, 1])
|
|
249
255
|
dx = max_cv1 - min_cv1
|
|
256
|
+
if dx == 0:
|
|
257
|
+
# Single-point CV range: use 3*sigma as padding
|
|
258
|
+
sigma = np.max(hills_data[:, 2])
|
|
259
|
+
dx = 6 * sigma
|
|
250
260
|
xlims = [min_cv1 - 0.05 * dx, max_cv1 + 0.05 * dx]
|
|
251
261
|
|
|
252
262
|
# Override with user-defined or periodic limits
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Reaction valley (s, d) projection utilities.
|
|
2
|
+
|
|
3
|
+
Extracts the 2D RMSD-plane rotation into reusable functions.
|
|
4
|
+
The projection maps ``(rmsd_a, rmsd_b)`` coordinates into
|
|
5
|
+
progress ``s`` (along the path) and deviation ``d`` (perpendicular).
|
|
6
|
+
|
|
7
|
+
For NEB paths, reference A is the reactant and B is the product.
|
|
8
|
+
For single-ended methods, A is the initial structure and B is the
|
|
9
|
+
final (saddle or minimum).
|
|
10
|
+
|
|
11
|
+
Implements the method from :cite:`goswami2026valley`.
|
|
12
|
+
|
|
13
|
+
.. versionadded:: 1.5.0
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class ProjectionBasis:
|
|
25
|
+
"""Orthonormal basis for the (s, d) reaction valley projection.
|
|
26
|
+
|
|
27
|
+
Attributes
|
|
28
|
+
----------
|
|
29
|
+
a_start, b_start
|
|
30
|
+
RMSD values of the first point (origin of the rotated frame).
|
|
31
|
+
u_a, u_b
|
|
32
|
+
Unit vector along the path direction in (a, b) space.
|
|
33
|
+
v_a, v_b
|
|
34
|
+
Unit vector perpendicular to the path (``v = rotate(u, +90deg)``).
|
|
35
|
+
path_norm
|
|
36
|
+
Euclidean length of the path vector in (a, b) space.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
a_start: float
|
|
40
|
+
b_start: float
|
|
41
|
+
u_a: float
|
|
42
|
+
u_b: float
|
|
43
|
+
v_a: float
|
|
44
|
+
v_b: float
|
|
45
|
+
path_norm: float
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def compute_projection_basis(
|
|
49
|
+
rmsd_a: np.ndarray,
|
|
50
|
+
rmsd_b: np.ndarray,
|
|
51
|
+
) -> ProjectionBasis:
|
|
52
|
+
"""Compute the projection basis from first/last points of the arrays.
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
rmsd_a, rmsd_b
|
|
57
|
+
RMSD distance arrays (to reference A and B respectively).
|
|
58
|
+
The first element defines the origin; the last defines the
|
|
59
|
+
path direction.
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
ProjectionBasis
|
|
64
|
+
Frozen dataclass with the orthonormal basis vectors.
|
|
65
|
+
|
|
66
|
+
Raises
|
|
67
|
+
------
|
|
68
|
+
ValueError
|
|
69
|
+
If the path has zero length (first and last points coincide
|
|
70
|
+
in RMSD space).
|
|
71
|
+
"""
|
|
72
|
+
a_start, b_start = float(rmsd_a[0]), float(rmsd_b[0])
|
|
73
|
+
a_end, b_end = float(rmsd_a[-1]), float(rmsd_b[-1])
|
|
74
|
+
|
|
75
|
+
vec_a, vec_b = a_end - a_start, b_end - b_start
|
|
76
|
+
path_norm = np.hypot(vec_a, vec_b)
|
|
77
|
+
|
|
78
|
+
if path_norm < 1e-12: # noqa: PLR2004
|
|
79
|
+
msg = (
|
|
80
|
+
"Path has zero length in RMSD space "
|
|
81
|
+
f"(start=({a_start:.6f}, {b_start:.6f}), "
|
|
82
|
+
f"end=({a_end:.6f}, {b_end:.6f}))"
|
|
83
|
+
)
|
|
84
|
+
raise ValueError(msg)
|
|
85
|
+
|
|
86
|
+
u_a = vec_a / path_norm
|
|
87
|
+
u_b = vec_b / path_norm
|
|
88
|
+
|
|
89
|
+
return ProjectionBasis(
|
|
90
|
+
a_start=a_start,
|
|
91
|
+
b_start=b_start,
|
|
92
|
+
u_a=u_a,
|
|
93
|
+
u_b=u_b,
|
|
94
|
+
v_a=-u_b,
|
|
95
|
+
v_b=u_a,
|
|
96
|
+
path_norm=path_norm,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def project_to_sd(
|
|
101
|
+
rmsd_a: np.ndarray,
|
|
102
|
+
rmsd_b: np.ndarray,
|
|
103
|
+
basis: ProjectionBasis,
|
|
104
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
105
|
+
"""Project (rmsd_a, rmsd_b) into (s, d) reaction valley coordinates.
|
|
106
|
+
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
rmsd_a, rmsd_b
|
|
110
|
+
RMSD arrays to project.
|
|
111
|
+
basis
|
|
112
|
+
Pre-computed projection basis.
|
|
113
|
+
|
|
114
|
+
Returns
|
|
115
|
+
-------
|
|
116
|
+
s, d
|
|
117
|
+
Progress and deviation arrays.
|
|
118
|
+
"""
|
|
119
|
+
da = rmsd_a - basis.a_start
|
|
120
|
+
db = rmsd_b - basis.b_start
|
|
121
|
+
s = da * basis.u_a + db * basis.u_b
|
|
122
|
+
d = da * basis.v_a + db * basis.v_b
|
|
123
|
+
return s, d
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def inverse_sd_to_ab(
|
|
127
|
+
s: np.ndarray,
|
|
128
|
+
d: np.ndarray,
|
|
129
|
+
basis: ProjectionBasis,
|
|
130
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
131
|
+
"""Map (s, d) grid coordinates back to (a, b) RMSD space.
|
|
132
|
+
|
|
133
|
+
Used for evaluating the RBF surface on a projected grid.
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
s, d
|
|
138
|
+
Progress and deviation arrays (can be meshgrid raveled).
|
|
139
|
+
basis
|
|
140
|
+
Pre-computed projection basis.
|
|
141
|
+
|
|
142
|
+
Returns
|
|
143
|
+
-------
|
|
144
|
+
rmsd_a, rmsd_b
|
|
145
|
+
Coordinates in the original RMSD plane.
|
|
146
|
+
"""
|
|
147
|
+
rmsd_a = basis.a_start + s * basis.u_a + d * basis.v_a
|
|
148
|
+
rmsd_b = basis.b_start + s * basis.u_b + d * basis.v_b
|
|
149
|
+
return rmsd_a, rmsd_b
|
|
@@ -14,18 +14,11 @@ from chemparseplot.plot.theme import (
|
|
|
14
14
|
|
|
15
15
|
# Lazy imports for submodules with heavy deps (cmcrameri, pint, etc.)
|
|
16
16
|
def __getattr__(name):
|
|
17
|
-
|
|
18
|
-
from chemparseplot.plot import geomscan as _mod
|
|
17
|
+
import importlib
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
if name
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return _mod
|
|
25
|
-
if name == "chemgp":
|
|
26
|
-
from chemparseplot.plot import chemgp as _mod
|
|
27
|
-
|
|
28
|
-
return _mod
|
|
19
|
+
lazy_submodules = {"geomscan", "structs", "chemgp", "optimization"}
|
|
20
|
+
if name in lazy_submodules:
|
|
21
|
+
return importlib.import_module(f".{name}", __name__)
|
|
29
22
|
if name == "ureg":
|
|
30
23
|
from chemparseplot.units import ureg as _ureg
|
|
31
24
|
|