chemparseplot 1.9.16__tar.gz → 1.10.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.9.16 → chemparseplot-1.10.0}/PKG-INFO +3 -1
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/_version.py +2 -2
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/api.py +1 -2
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/chemgp_hdf5.py +1 -0
- chemparseplot-1.10.0/chemparseplot/parse/eon/__init__.py +22 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/eon/_trajectory_common.py +4 -1
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/eon/dimer_trajectory.py +2 -3
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/eon/frame_series.py +5 -3
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/eon/min_trajectory.py +2 -4
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/eon/saddle_search.py +1 -3
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/grammar/__init__.py +7 -1
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/grammar/orca_text.py +3 -4
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/grammar/xyz.py +11 -8
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/sella/saddle_search.py +4 -3
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/types.py +1 -1
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/plot/__init__.py +8 -1
- chemparseplot-1.10.0/chemparseplot/plot/disconnectivity.py +323 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/plot/geomscan.py +7 -1
- chemparseplot-1.10.0/chemparseplot/plot/landscape.py +315 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/plot/neb.py +13 -18
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/plot/optimization.py +2 -6
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/plot/structs.py +1 -3
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/pyproject.toml +41 -11
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/fixtures/orca_energy_coords_snippet.out +0 -1
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/_con_fixtures.py +5 -6
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/grammar/test_orca_text_grammar.py +2 -2
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/grammar/test_xyz_grammar.py +1 -1
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/test_con_io.py +1 -1
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/test_eon_trajectory_common.py +17 -8
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/test_frame_series.py +3 -1
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/test_min_trajectory.py +1 -2
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/test_neb_stitch.py +6 -4
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/test_orca_public_api.py +9 -4
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/test_readcon_boundary.py +3 -1
- chemparseplot-1.10.0/tests/plot/test_landscape_disconnectivity.py +140 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/plot/test_neb_helpers.py +3 -2
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/plot/test_neb_renderers.py +7 -8
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/plot/test_optimization.py +7 -2
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/plot/test_strip_rendering.py +3 -1
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/test_full_coverage.py +32 -5
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/tutorials/test_chemparseplot.py +3 -2
- chemparseplot-1.9.16/chemparseplot/parse/eon/__init__.py +0 -8
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/.gitignore +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/LICENSE +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/__init__.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/__init__.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/chemgp_jsonl.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/converter.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/eon/con_io.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/eon/gprd.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/eon/minimization.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/eon/neb.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/eon/stitch.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/file_.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/grammar/_deps.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/neb_utils.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/orca/__init__.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/orca/_opi.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/orca/geomscan.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/orca/neb/__init__.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/orca/neb/interp.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/orca/neb/opi_parser.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/patterns.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/plumed.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/projection.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/trajectory/__init__.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/trajectory/hdf5.py +5 -5
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/parse/trajectory/neb.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/plot/chemgp.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/plot/plumed.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/plot/theme.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/units.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/chemparseplot/util.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/readme.md +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/__init__.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/_optional_imports.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/conftest.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/fixtures/h2mol.xyz +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/fixtures_geomscan_snippet.txt +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/__init__.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/grammar/__init__.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/grammar/test_grammar_deps.py +1 -1
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/orca/test_geomscan.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/orca/test_interp.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/test_chemgp_hdf5.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/test_chemgp_jsonl_types.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/test_converter.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/test_dimer_trajectory.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/test_eon_neb_types.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/test_minimization_result.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/test_neb_utils.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/test_parser_types.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/test_patterns.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/test_plumed.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/test_projection.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/test_saddle_results.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/test_sella_saddle.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/test_trajectory_hdf5.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/parse/test_trajectory_neb.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/plot/__init__.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/plot/test_chemgp_utils.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/plot/test_orca_neb_payload.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/plot/test_projection_refactor.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/plot/test_structs_units.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/plot/test_surface_auto_thin.py +0 -0
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/test_coverage_batch.py +2 -2
- {chemparseplot-1.9.16 → chemparseplot-1.10.0}/tests/test_public_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chemparseplot
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.10.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
|
|
@@ -65,6 +65,7 @@ Requires-Dist: wheel>=0.40; extra == 'release'
|
|
|
65
65
|
Provides-Extra: test
|
|
66
66
|
Requires-Dist: ase>=3.22; extra == 'test'
|
|
67
67
|
Requires-Dist: h5py>=3.0; extra == 'test'
|
|
68
|
+
Requires-Dist: jax[cpu]>=0.4; extra == 'test'
|
|
68
69
|
Requires-Dist: matplotlib>=3.8.2; extra == 'test'
|
|
69
70
|
Requires-Dist: parsimonious>=0.10.0; extra == 'test'
|
|
70
71
|
Requires-Dist: polars>=0.20; extra == 'test'
|
|
@@ -73,6 +74,7 @@ Requires-Dist: pytest>=7.4.3; extra == 'test'
|
|
|
73
74
|
Requires-Dist: readcon>=0.13.1; extra == 'test'
|
|
74
75
|
Requires-Dist: rgpycrumbs; extra == 'test'
|
|
75
76
|
Requires-Dist: scipy>=1.11; extra == 'test'
|
|
77
|
+
Requires-Dist: xyzrender>=0.1.3; extra == 'test'
|
|
76
78
|
Provides-Extra: xyzrender
|
|
77
79
|
Requires-Dist: xyzrender>=0.1.2; extra == 'xyzrender'
|
|
78
80
|
Description-Content-Type: text/markdown
|
|
@@ -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.10.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 10, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -101,7 +101,6 @@ def parse_orca_final_energy(path_or_text: str):
|
|
|
101
101
|
return summary.final_energy
|
|
102
102
|
|
|
103
103
|
|
|
104
|
-
|
|
105
104
|
def extract_orca_geomscan_energy(
|
|
106
105
|
data: str, energy_type: str = "Actual Energy"
|
|
107
106
|
) -> tuple[Q_, Q_]:
|
|
@@ -138,6 +137,6 @@ def suite_pins() -> dict[str, str]:
|
|
|
138
137
|
try:
|
|
139
138
|
cfg = load_config()
|
|
140
139
|
pins.update(cfg.merged_package_pins_normalized())
|
|
141
|
-
except Exception:
|
|
140
|
+
except Exception:
|
|
142
141
|
pass
|
|
143
142
|
return pins
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2023-present Rohit Goswami <rog32@hi.is>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
"""eOn trajectory parsers.
|
|
5
|
+
|
|
6
|
+
``con_io`` is always importable (readcon loads on first CON call).
|
|
7
|
+
``frame_series`` needs the ``neb`` extra (readcon + polars + ase) and is
|
|
8
|
+
lazy so bare ``import chemparseplot.parse`` stays light.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from chemparseplot.parse.eon import con_io
|
|
12
|
+
|
|
13
|
+
__all__ = ["con_io", "frame_series"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def __getattr__(name: str):
|
|
17
|
+
if name == "frame_series":
|
|
18
|
+
from chemparseplot.parse.eon import frame_series as _frame_series
|
|
19
|
+
|
|
20
|
+
return _frame_series
|
|
21
|
+
msg = f"module {__name__!r} has no attribute {name!r}"
|
|
22
|
+
raise AttributeError(msg)
|
|
@@ -141,7 +141,10 @@ def frame_rows_to_table(
|
|
|
141
141
|
rows: list[dict[str, Any]] = []
|
|
142
142
|
saw_complete_row = False
|
|
143
143
|
for frame in frames:
|
|
144
|
-
row = {
|
|
144
|
+
row = {
|
|
145
|
+
column: coerce_metadata_value(column, metadata_value(frame, column))
|
|
146
|
+
for column in columns
|
|
147
|
+
}
|
|
145
148
|
if any(value is None for value in row.values()):
|
|
146
149
|
if saw_complete_row or not allow_leading_incomplete:
|
|
147
150
|
return pl.DataFrame()
|
|
@@ -16,11 +16,10 @@ 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
|
|
21
19
|
from ase import Atoms
|
|
22
20
|
|
|
23
|
-
from .
|
|
21
|
+
from chemparseplot.parse.eon import con_io
|
|
22
|
+
from chemparseplot.parse.eon._trajectory_common import (
|
|
24
23
|
frame_rows_to_table,
|
|
25
24
|
load_movie_and_table,
|
|
26
25
|
load_optional_payload,
|
|
@@ -25,12 +25,10 @@ from chemparseplot.parse.eon._trajectory_common import (
|
|
|
25
25
|
metadata_value,
|
|
26
26
|
)
|
|
27
27
|
from chemparseplot.parse.eon.dimer_trajectory import (
|
|
28
|
-
DIMER_METADATA_COLUMNS,
|
|
29
28
|
DimerTrajectoryData,
|
|
30
29
|
table_from_dimer_metadata,
|
|
31
30
|
)
|
|
32
31
|
from chemparseplot.parse.eon.min_trajectory import (
|
|
33
|
-
MIN_METADATA_COLUMNS,
|
|
34
32
|
MinTrajectoryData,
|
|
35
33
|
table_from_min_metadata,
|
|
36
34
|
)
|
|
@@ -121,7 +119,11 @@ def neb_path_arrays(frames: Sequence[Any]) -> dict[str, Any]:
|
|
|
121
119
|
ok = True
|
|
122
120
|
for frame in frames:
|
|
123
121
|
v = metadata_value(frame, key)
|
|
124
|
-
if v is None and key in (
|
|
122
|
+
if v is None and key in (
|
|
123
|
+
"reaction_coordinate",
|
|
124
|
+
"relative_energy",
|
|
125
|
+
"parallel_force",
|
|
126
|
+
):
|
|
125
127
|
# optional NEB stamps
|
|
126
128
|
ok = False
|
|
127
129
|
break
|
|
@@ -15,14 +15,12 @@ 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
|
|
20
18
|
from ase import Atoms
|
|
21
19
|
|
|
22
|
-
from .
|
|
20
|
+
from chemparseplot.parse.eon import con_io
|
|
21
|
+
from chemparseplot.parse.eon._trajectory_common import (
|
|
23
22
|
frame_rows_to_table,
|
|
24
23
|
load_movie_and_table,
|
|
25
|
-
read_first_structure,
|
|
26
24
|
read_optional_first,
|
|
27
25
|
)
|
|
28
26
|
|
|
@@ -369,9 +369,7 @@ def parse_eon_saddle(eresp: Path, rloc: "SpinID") -> "SaddleMeasure":
|
|
|
369
369
|
dimer_trans=meth.trans,
|
|
370
370
|
init_energy=init_energy,
|
|
371
371
|
barrier=(
|
|
372
|
-
results_data.saddle_energy - init_energy
|
|
373
|
-
if init_energy is not None
|
|
374
|
-
else None
|
|
372
|
+
results_data.saddle_energy - init_energy if init_energy is not None else None
|
|
375
373
|
),
|
|
376
374
|
mol_id=getattr(rloc, "mol_id", None),
|
|
377
375
|
spin=getattr(rloc, "spin", None),
|
|
@@ -30,7 +30,13 @@ __all__ = [
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
def __getattr__(name: str):
|
|
33
|
-
if name in (
|
|
33
|
+
if name in (
|
|
34
|
+
"parse_xyz_text",
|
|
35
|
+
"parse_xyz_file",
|
|
36
|
+
"XyzFrame",
|
|
37
|
+
"XyzAtom",
|
|
38
|
+
"write_xyz_text",
|
|
39
|
+
):
|
|
34
40
|
from chemparseplot.parse.grammar import xyz as _xyz
|
|
35
41
|
|
|
36
42
|
return getattr(_xyz, name)
|
|
@@ -151,7 +151,7 @@ def extract_final_energies_hartree(text: str) -> list[float]:
|
|
|
151
151
|
line = m.group(0).strip()
|
|
152
152
|
try:
|
|
153
153
|
tree = g.parse(line)
|
|
154
|
-
except Exception:
|
|
154
|
+
except Exception:
|
|
155
155
|
continue
|
|
156
156
|
v = EnergyVisitor()
|
|
157
157
|
v.visit(tree)
|
|
@@ -192,14 +192,13 @@ def extract_cartesian_angstrom_blocks(text: str) -> list[list[OrcaAtomCoord]]:
|
|
|
192
192
|
for m in _CART_BLOCK_RE.finditer(text):
|
|
193
193
|
chunk = (
|
|
194
194
|
"CARTESIAN COORDINATES (ANGSTROEM)\n"
|
|
195
|
-
"---------------------------------\n"
|
|
196
|
-
+ m.group(1)
|
|
195
|
+
"---------------------------------\n" + m.group(1)
|
|
197
196
|
)
|
|
198
197
|
if not chunk.endswith("\n"):
|
|
199
198
|
chunk += "\n"
|
|
200
199
|
try:
|
|
201
200
|
tree = g.parse(chunk)
|
|
202
|
-
except Exception:
|
|
201
|
+
except Exception:
|
|
203
202
|
continue
|
|
204
203
|
v = CartVisitor()
|
|
205
204
|
v.visit(tree)
|
|
@@ -29,8 +29,8 @@ _XYZ_GRAMMAR_SRC = r"""
|
|
|
29
29
|
meta = natoms newline comment_line newline coord_block ws?
|
|
30
30
|
natoms = ~r"\d+"
|
|
31
31
|
comment_line = ~r"[^\n]*"
|
|
32
|
-
coord_block = (
|
|
33
|
-
|
|
32
|
+
coord_block = (atom_line newline?)+
|
|
33
|
+
atom_line = ws? atype ws float_ ws float_ ws float_ ws?
|
|
34
34
|
atype = ~r"[A-Za-z][a-zA-Z0-9]{0,2}"
|
|
35
35
|
float_ = ~r"[+-]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][+-]?\d+)?"
|
|
36
36
|
newline = ~r"\n"
|
|
@@ -107,26 +107,29 @@ def _make_visitor() -> Any:
|
|
|
107
107
|
self.comment: str = ""
|
|
108
108
|
self.atoms: list[XyzAtom] = []
|
|
109
109
|
|
|
110
|
-
def visit_natoms(self, node: Any,
|
|
110
|
+
def visit_natoms(self, node: Any, _visited_children: Any) -> int:
|
|
111
111
|
self.natoms = int(node.text)
|
|
112
112
|
return self.natoms
|
|
113
113
|
|
|
114
|
-
def visit_comment_line(self, node: Any,
|
|
114
|
+
def visit_comment_line(self, node: Any, _visited_children: Any) -> str:
|
|
115
115
|
self.comment = node.text
|
|
116
116
|
return self.comment
|
|
117
117
|
|
|
118
|
-
def
|
|
118
|
+
def visit_atom_line(self, node: Any, visited_children: Any) -> XyzAtom:
|
|
119
|
+
del node # structure from visited_children
|
|
119
120
|
flat = _flatten(visited_children)
|
|
120
|
-
symbol = next(
|
|
121
|
+
symbol = next(
|
|
122
|
+
x for x in flat if isinstance(x, str) and x.strip() and x[0].isalpha()
|
|
123
|
+
)
|
|
121
124
|
nums = [x for x in flat if isinstance(x, float)]
|
|
122
125
|
atom = XyzAtom(symbol=symbol, x=nums[0], y=nums[1], z=nums[2])
|
|
123
126
|
self.atoms.append(atom)
|
|
124
127
|
return atom
|
|
125
128
|
|
|
126
|
-
def visit_atype(self, node: Any,
|
|
129
|
+
def visit_atype(self, node: Any, _visited_children: Any) -> str:
|
|
127
130
|
return node.text
|
|
128
131
|
|
|
129
|
-
def visit_float_(self, node: Any,
|
|
132
|
+
def visit_float_(self, node: Any, _visited_children: Any) -> float:
|
|
130
133
|
return float(node.text)
|
|
131
134
|
|
|
132
135
|
def generic_visit(self, node: Any, visited_children: Any) -> Any:
|
|
@@ -18,13 +18,14 @@ except ImportError:
|
|
|
18
18
|
|
|
19
19
|
log = logging.getLogger(__name__)
|
|
20
20
|
|
|
21
|
+
|
|
21
22
|
@dataclass(frozen=True, slots=True)
|
|
22
23
|
class SellaLog:
|
|
23
24
|
"""Typed record for a single Sella geometry-step log entry.
|
|
24
25
|
|
|
25
|
-
```{versionadded} 0.0.3
|
|
26
|
-
```
|
|
27
|
-
"""
|
|
26
|
+
```{versionadded} 0.0.3
|
|
27
|
+
```
|
|
28
|
+
"""
|
|
28
29
|
|
|
29
30
|
step_id: int
|
|
30
31
|
time_float: float
|
|
@@ -84,7 +84,7 @@ class OrcaNebResult(DataclassMapping):
|
|
|
84
84
|
|
|
85
85
|
def __post_init__(self) -> None:
|
|
86
86
|
if self.n_images is None:
|
|
87
|
-
object.__setattr__(self, "n_images",
|
|
87
|
+
object.__setattr__(self, "n_images", len(self.energies))
|
|
88
88
|
|
|
89
89
|
@classmethod
|
|
90
90
|
def from_mapping(cls, data: Mapping[str, Any]) -> OrcaNebResult:
|
|
@@ -25,7 +25,14 @@ __all__ = [
|
|
|
25
25
|
def __getattr__(name):
|
|
26
26
|
import importlib
|
|
27
27
|
|
|
28
|
-
lazy_submodules = {
|
|
28
|
+
lazy_submodules = {
|
|
29
|
+
"geomscan",
|
|
30
|
+
"structs",
|
|
31
|
+
"chemgp",
|
|
32
|
+
"optimization",
|
|
33
|
+
"landscape",
|
|
34
|
+
"disconnectivity",
|
|
35
|
+
}
|
|
29
36
|
if name in lazy_submodules:
|
|
30
37
|
return importlib.import_module(f".{name}", __name__)
|
|
31
38
|
if name == "ureg":
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2023-present Rohit Goswami <rog32@hi.is>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
"""Approximate disconnectivity graphs from minima databases.
|
|
5
|
+
|
|
6
|
+
The classic disconnectivity graph needs transition states; a hopping
|
|
7
|
+
campaign records only quenched minima. These helpers draw the superbasin
|
|
8
|
+
merge tree of a descriptor-space k-nearest-neighbour graph instead: at each
|
|
9
|
+
energy level the minima below it split into connected components, and
|
|
10
|
+
components merge as the level rises. That approximation is a rendering of
|
|
11
|
+
the database made for reading the figure; it should never feed a reported
|
|
12
|
+
statistic.
|
|
13
|
+
|
|
14
|
+
Basins can be lumped for display by spectral clustering on the same graph.
|
|
15
|
+
Everything is iterative: with a thousand minima the merge chains grow
|
|
16
|
+
deeper than any recursion limit.
|
|
17
|
+
|
|
18
|
+
```{versionadded} 1.10.0
|
|
19
|
+
```
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
from scipy.sparse import coo_matrix
|
|
26
|
+
from scipy.sparse.csgraph import connected_components, laplacian
|
|
27
|
+
|
|
28
|
+
from chemparseplot.plot.theme import RUHI_COLORS
|
|
29
|
+
|
|
30
|
+
BASIN_PALETTE = [
|
|
31
|
+
RUHI_COLORS[c] for c in ["teal", "sky", "magenta", "coral", "sunshine"]
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def dedup(matrix, energies, tol):
|
|
36
|
+
"""Merge near-identical minima, keeping the lowest-energy representative.
|
|
37
|
+
|
|
38
|
+
Returns the kept row indices, lowest energy first.
|
|
39
|
+
|
|
40
|
+
```{versionadded} 1.10.0
|
|
41
|
+
```
|
|
42
|
+
"""
|
|
43
|
+
order = np.argsort(energies)
|
|
44
|
+
rep_idx = []
|
|
45
|
+
for i in order:
|
|
46
|
+
if any(np.linalg.norm(matrix[i] - matrix[j]) < tol for j in rep_idx):
|
|
47
|
+
continue
|
|
48
|
+
rep_idx.append(i)
|
|
49
|
+
return np.array(rep_idx)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def knn_graph(matrix, k=6):
|
|
53
|
+
"""Symmetric k-nearest-neighbour adjacency in descriptor space.
|
|
54
|
+
|
|
55
|
+
```{versionadded} 1.10.0
|
|
56
|
+
```
|
|
57
|
+
"""
|
|
58
|
+
d = np.linalg.norm(matrix[:, None, :] - matrix[None, :, :], axis=-1)
|
|
59
|
+
np.fill_diagonal(d, np.inf)
|
|
60
|
+
n = len(matrix)
|
|
61
|
+
rows, cols = [], []
|
|
62
|
+
for i in range(n):
|
|
63
|
+
for j in np.argsort(d[i])[:k]:
|
|
64
|
+
rows += [i, j]
|
|
65
|
+
cols += [j, i]
|
|
66
|
+
data = np.ones(len(rows))
|
|
67
|
+
return coo_matrix((data, (rows, cols)), shape=(n, n)).tocsr()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def bridge_components(adjacency, matrix, weight=0.1):
|
|
71
|
+
"""Weakly connect graph components through their closest point pairs.
|
|
72
|
+
|
|
73
|
+
Spectral clustering assumes a connected graph; a k-NN graph over a
|
|
74
|
+
minima database often fragments. One weak edge per component pair
|
|
75
|
+
(closest points in descriptor space) restores the geometry without
|
|
76
|
+
drowning the strong intra-basin structure.
|
|
77
|
+
|
|
78
|
+
```{versionadded} 1.10.0
|
|
79
|
+
```
|
|
80
|
+
"""
|
|
81
|
+
n_comp, labels = connected_components(adjacency, directed=False)
|
|
82
|
+
if n_comp == 1:
|
|
83
|
+
return adjacency
|
|
84
|
+
adjacency = adjacency.tolil()
|
|
85
|
+
for a in range(n_comp):
|
|
86
|
+
for b in range(a + 1, n_comp):
|
|
87
|
+
ia = np.where(labels == a)[0]
|
|
88
|
+
ib = np.where(labels == b)[0]
|
|
89
|
+
d = np.linalg.norm(
|
|
90
|
+
matrix[ia][:, None, :] - matrix[ib][None, :, :], axis=-1
|
|
91
|
+
)
|
|
92
|
+
i, j = np.unravel_index(int(np.argmin(d)), d.shape)
|
|
93
|
+
adjacency[ia[i], ib[j]] = weight
|
|
94
|
+
adjacency[ib[j], ia[i]] = weight
|
|
95
|
+
return adjacency.tocsr()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def spectral_basins(adjacency, n_basins, rng, matrix=None):
|
|
99
|
+
"""Display-only basin assignment by spectral clustering on the graph.
|
|
100
|
+
|
|
101
|
+
With ``matrix`` given, disconnected components are weakly bridged
|
|
102
|
+
through their closest descriptor pairs first.
|
|
103
|
+
|
|
104
|
+
```{versionadded} 1.10.0
|
|
105
|
+
```
|
|
106
|
+
"""
|
|
107
|
+
if matrix is not None:
|
|
108
|
+
adjacency = bridge_components(adjacency, matrix)
|
|
109
|
+
lap = laplacian(adjacency.astype(float), normed=True)
|
|
110
|
+
k = min(n_basins, adjacency.shape[0] - 1)
|
|
111
|
+
# Dense eigendecomposition: minima databases stay around a thousand
|
|
112
|
+
# nodes, and iterative smallest-magnitude solvers are unreliable on
|
|
113
|
+
# the near-singular Laplacians of disconnected graphs.
|
|
114
|
+
_, vecs = np.linalg.eigh(np.asarray(lap.todense()))
|
|
115
|
+
x = vecs[:, :k]
|
|
116
|
+
x = x / (np.linalg.norm(x, axis=1, keepdims=True) + 1e-12)
|
|
117
|
+
# Farthest-point seeding: deterministic given the first pick, and it
|
|
118
|
+
# cannot drop both centers into one embedded cloud.
|
|
119
|
+
first = int(rng.integers(len(x)))
|
|
120
|
+
centers = [x[first]]
|
|
121
|
+
while len(centers) < k:
|
|
122
|
+
d = np.min(
|
|
123
|
+
[np.linalg.norm(x - c, axis=1) for c in centers], axis=0
|
|
124
|
+
)
|
|
125
|
+
centers.append(x[int(np.argmax(d))])
|
|
126
|
+
centers = np.array(centers)
|
|
127
|
+
for _ in range(60):
|
|
128
|
+
assign = np.argmin(
|
|
129
|
+
np.linalg.norm(x[:, None, :] - centers[None, :, :], axis=-1), axis=1
|
|
130
|
+
)
|
|
131
|
+
new = np.array(
|
|
132
|
+
[
|
|
133
|
+
x[assign == c].mean(axis=0) if (assign == c).any() else centers[c]
|
|
134
|
+
for c in range(k)
|
|
135
|
+
]
|
|
136
|
+
)
|
|
137
|
+
if np.allclose(new, centers):
|
|
138
|
+
break
|
|
139
|
+
centers = new
|
|
140
|
+
return assign
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class Node:
|
|
144
|
+
"""A merge-tree node; leaves carry their minimum's index.
|
|
145
|
+
|
|
146
|
+
```{versionadded} 1.10.0
|
|
147
|
+
```
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
__slots__ = ("level", "children", "leaf", "x", "emin", "argmin")
|
|
151
|
+
|
|
152
|
+
def __init__(self, level, children=None, leaf=None):
|
|
153
|
+
self.level = level
|
|
154
|
+
self.children = children or []
|
|
155
|
+
self.leaf = leaf
|
|
156
|
+
self.x = None
|
|
157
|
+
if self.leaf is not None:
|
|
158
|
+
self.emin = level
|
|
159
|
+
self.argmin = leaf
|
|
160
|
+
else:
|
|
161
|
+
self.emin = min(c.emin for c in self.children)
|
|
162
|
+
self.argmin = min(self.children, key=lambda c: c.emin).argmin
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def merge_tree(adjacency, energies, levels):
|
|
166
|
+
"""Superbasin merge tree over an energy-threshold sweep.
|
|
167
|
+
|
|
168
|
+
```{versionadded} 1.10.0
|
|
169
|
+
```
|
|
170
|
+
"""
|
|
171
|
+
n = len(energies)
|
|
172
|
+
leaves = [Node(energies[i], leaf=i) for i in range(n)]
|
|
173
|
+
active = {}
|
|
174
|
+
prev_sets = []
|
|
175
|
+
for lev in levels:
|
|
176
|
+
mask = energies <= lev
|
|
177
|
+
if not mask.any():
|
|
178
|
+
continue
|
|
179
|
+
sub = np.where(mask)[0]
|
|
180
|
+
_, labels = connected_components(
|
|
181
|
+
adjacency[sub][:, sub], directed=False
|
|
182
|
+
)
|
|
183
|
+
comps = {}
|
|
184
|
+
for local, g in enumerate(sub):
|
|
185
|
+
comps.setdefault(labels[local], []).append(g)
|
|
186
|
+
new_sets = [frozenset(v) for v in comps.values()]
|
|
187
|
+
for s in new_sets:
|
|
188
|
+
if s in active:
|
|
189
|
+
continue
|
|
190
|
+
parts = [p for p in prev_sets if p <= s and p in active]
|
|
191
|
+
merged = [active[p] for p in parts]
|
|
192
|
+
covered = frozenset().union(*parts) if parts else frozenset()
|
|
193
|
+
for p in parts:
|
|
194
|
+
del active[p]
|
|
195
|
+
extra = sorted(s - covered, key=lambda i: energies[i])
|
|
196
|
+
if not merged:
|
|
197
|
+
node = leaves[extra[0]]
|
|
198
|
+
for m in extra[1:]:
|
|
199
|
+
node = Node(
|
|
200
|
+
max(energies[m], node.level), children=[node, leaves[m]]
|
|
201
|
+
)
|
|
202
|
+
else:
|
|
203
|
+
node = merged[0] if len(merged) == 1 else Node(lev, children=merged)
|
|
204
|
+
for m in extra:
|
|
205
|
+
node = Node(lev, children=[node, leaves[m]])
|
|
206
|
+
active[s] = node
|
|
207
|
+
prev_sets = new_sets
|
|
208
|
+
roots = list(active.values())
|
|
209
|
+
if len(roots) > 1:
|
|
210
|
+
return Node(levels[-1], children=roots)
|
|
211
|
+
return roots[0]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def iter_nodes(root):
|
|
215
|
+
"""Post-order traversal without recursion.
|
|
216
|
+
|
|
217
|
+
```{versionadded} 1.10.0
|
|
218
|
+
```
|
|
219
|
+
"""
|
|
220
|
+
out, stack = [], [(root, False)]
|
|
221
|
+
while stack:
|
|
222
|
+
node, done = stack.pop()
|
|
223
|
+
if done or node.leaf is not None:
|
|
224
|
+
out.append(node)
|
|
225
|
+
continue
|
|
226
|
+
stack.append((node, True))
|
|
227
|
+
for c in reversed(node.children):
|
|
228
|
+
stack.append((c, False))
|
|
229
|
+
return out
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def layout(root):
|
|
233
|
+
"""Leaf x positions in tree order, children sorted deepest-first.
|
|
234
|
+
|
|
235
|
+
```{versionadded} 1.10.0
|
|
236
|
+
```
|
|
237
|
+
"""
|
|
238
|
+
for node in iter_nodes(root):
|
|
239
|
+
if node.leaf is None:
|
|
240
|
+
node.children.sort(key=lambda c: c.emin)
|
|
241
|
+
x = 0.0
|
|
242
|
+
for node in iter_nodes(root):
|
|
243
|
+
if node.leaf is not None:
|
|
244
|
+
node.x = x
|
|
245
|
+
x += 1.0
|
|
246
|
+
else:
|
|
247
|
+
node.x = float(np.mean([c.x for c in node.children]))
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def paint(root, assign, palette=None):
|
|
251
|
+
"""Colour each subtree by the spectral basin of its deepest minimum.
|
|
252
|
+
|
|
253
|
+
```{versionadded} 1.10.0
|
|
254
|
+
```
|
|
255
|
+
"""
|
|
256
|
+
palette = palette or BASIN_PALETTE
|
|
257
|
+
colors = {}
|
|
258
|
+
for node in iter_nodes(root):
|
|
259
|
+
colors[id(node)] = palette[int(assign[node.argmin]) % len(palette)]
|
|
260
|
+
return colors
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def draw(ax, root, colors, lw=1.1):
|
|
264
|
+
"""Vertical stems to the merge level, horizontal bars at merges.
|
|
265
|
+
|
|
266
|
+
```{versionadded} 1.10.0
|
|
267
|
+
```
|
|
268
|
+
"""
|
|
269
|
+
for node in iter_nodes(root):
|
|
270
|
+
if node.leaf is not None:
|
|
271
|
+
continue
|
|
272
|
+
for c in node.children:
|
|
273
|
+
ax.plot(
|
|
274
|
+
[c.x, c.x],
|
|
275
|
+
[c.level, node.level],
|
|
276
|
+
color=colors.get(id(c), "#444444"),
|
|
277
|
+
lw=lw,
|
|
278
|
+
solid_capstyle="round",
|
|
279
|
+
zorder=2,
|
|
280
|
+
)
|
|
281
|
+
xs = [c.x for c in node.children]
|
|
282
|
+
ax.plot(
|
|
283
|
+
[min(xs), max(xs)],
|
|
284
|
+
[node.level, node.level],
|
|
285
|
+
color="#444444",
|
|
286
|
+
lw=0.9,
|
|
287
|
+
zorder=1,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def plot_disconnectivity(
|
|
292
|
+
ax,
|
|
293
|
+
matrix,
|
|
294
|
+
energies,
|
|
295
|
+
*,
|
|
296
|
+
dedup_tol=0.15,
|
|
297
|
+
knn=6,
|
|
298
|
+
n_levels=48,
|
|
299
|
+
n_basins=5,
|
|
300
|
+
rng=None,
|
|
301
|
+
):
|
|
302
|
+
"""Full pipeline: dedup, graph, merge tree, spectral paint, draw.
|
|
303
|
+
|
|
304
|
+
Returns ``(kept_indices, root)`` so callers can label leaves.
|
|
305
|
+
|
|
306
|
+
```{versionadded} 1.10.0
|
|
307
|
+
```
|
|
308
|
+
"""
|
|
309
|
+
rng = rng or np.random.default_rng(11)
|
|
310
|
+
reps = dedup(matrix, energies, dedup_tol)
|
|
311
|
+
m = matrix[reps]
|
|
312
|
+
en = energies[reps]
|
|
313
|
+
adjacency = knn_graph(m, knn)
|
|
314
|
+
levels = np.linspace(en.min(), en.max(), n_levels)[1:]
|
|
315
|
+
root = merge_tree(adjacency, en, levels)
|
|
316
|
+
layout(root)
|
|
317
|
+
assign = spectral_basins(adjacency, n_basins, rng, matrix=m)
|
|
318
|
+
colors = paint(root, assign)
|
|
319
|
+
draw(ax, root, colors)
|
|
320
|
+
ax.set_xticks([])
|
|
321
|
+
ax.set_ylabel("quenched energy")
|
|
322
|
+
ax.spines["bottom"].set_visible(False)
|
|
323
|
+
return reps, root
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import matplotlib.pyplot as plt
|
|
2
2
|
|
|
3
|
-
from chemparseplot.plot.structs import
|
|
3
|
+
from chemparseplot.plot.structs import (
|
|
4
|
+
AxisUnits,
|
|
5
|
+
BasePlotter,
|
|
6
|
+
EnergyPath,
|
|
7
|
+
axis_label,
|
|
8
|
+
to_magnitude,
|
|
9
|
+
)
|
|
4
10
|
|
|
5
11
|
|
|
6
12
|
def plot_energy_paths(
|