chemparseplot 1.9.7__tar.gz → 1.9.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/PKG-INFO +20 -1
  2. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/_version.py +2 -2
  3. chemparseplot-1.9.9/chemparseplot/api.py +143 -0
  4. chemparseplot-1.9.9/chemparseplot/parse/__init__.py +20 -0
  5. chemparseplot-1.9.9/chemparseplot/parse/grammar/__init__.py +48 -0
  6. chemparseplot-1.9.9/chemparseplot/parse/grammar/_deps.py +68 -0
  7. chemparseplot-1.9.9/chemparseplot/parse/grammar/orca_text.py +235 -0
  8. chemparseplot-1.9.9/chemparseplot/parse/grammar/xyz.py +181 -0
  9. chemparseplot-1.9.9/chemparseplot/parse/orca/__init__.py +32 -0
  10. chemparseplot-1.9.9/chemparseplot/parse/orca/_opi.py +67 -0
  11. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/parse/orca/neb/__init__.py +8 -4
  12. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/parse/orca/neb/opi_parser.py +79 -30
  13. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/plot/neb.py +118 -24
  14. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/plot/optimization.py +16 -0
  15. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/pyproject.toml +16 -1
  16. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/readme.md +12 -0
  17. chemparseplot-1.9.9/tests/fixtures/h2mol.xyz +4 -0
  18. chemparseplot-1.9.9/tests/fixtures/orca_energy_coords_snippet.out +14 -0
  19. chemparseplot-1.9.9/tests/fixtures_geomscan_snippet.txt +9 -0
  20. {chemparseplot-1.9.7/chemparseplot/parse/orca → chemparseplot-1.9.9/tests/parse/grammar}/__init__.py +0 -4
  21. chemparseplot-1.9.9/tests/parse/grammar/test_grammar_deps.py +38 -0
  22. chemparseplot-1.9.9/tests/parse/grammar/test_orca_text_grammar.py +57 -0
  23. chemparseplot-1.9.9/tests/parse/grammar/test_xyz_grammar.py +61 -0
  24. chemparseplot-1.9.9/tests/parse/test_orca_public_api.py +168 -0
  25. chemparseplot-1.9.9/tests/plot/test_surface_auto_thin.py +114 -0
  26. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/test_coverage_batch.py +12 -0
  27. chemparseplot-1.9.9/tests/test_public_api.py +72 -0
  28. chemparseplot-1.9.7/chemparseplot/parse/__init__.py +0 -11
  29. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/.gitignore +0 -0
  30. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/LICENSE +0 -0
  31. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/__init__.py +0 -0
  32. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/parse/chemgp_hdf5.py +0 -0
  33. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/parse/chemgp_jsonl.py +0 -0
  34. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/parse/converter.py +0 -0
  35. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/parse/eon/__init__.py +0 -0
  36. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/parse/eon/_trajectory_common.py +0 -0
  37. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/parse/eon/con_io.py +0 -0
  38. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/parse/eon/dimer_trajectory.py +0 -0
  39. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/parse/eon/gprd.py +0 -0
  40. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/parse/eon/min_trajectory.py +0 -0
  41. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/parse/eon/minimization.py +0 -0
  42. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/parse/eon/neb.py +0 -0
  43. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/parse/eon/saddle_search.py +0 -0
  44. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/parse/eon/stitch.py +0 -0
  45. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/parse/file_.py +0 -0
  46. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/parse/neb_utils.py +0 -0
  47. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/parse/orca/geomscan.py +0 -0
  48. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/parse/orca/neb/interp.py +0 -0
  49. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/parse/patterns.py +0 -0
  50. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/parse/plumed.py +0 -0
  51. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/parse/projection.py +0 -0
  52. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/parse/sella/saddle_search.py +0 -0
  53. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/parse/trajectory/__init__.py +0 -0
  54. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/parse/trajectory/hdf5.py +0 -0
  55. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/parse/trajectory/neb.py +0 -0
  56. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/parse/types.py +0 -0
  57. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/plot/__init__.py +0 -0
  58. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/plot/chemgp.py +0 -0
  59. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/plot/geomscan.py +0 -0
  60. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/plot/plumed.py +0 -0
  61. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/plot/structs.py +0 -0
  62. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/plot/theme.py +0 -0
  63. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/units.py +0 -0
  64. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/chemparseplot/util.py +0 -0
  65. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/__init__.py +0 -0
  66. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/_optional_imports.py +0 -0
  67. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/conftest.py +0 -0
  68. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/parse/__init__.py +0 -0
  69. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/parse/_con_fixtures.py +0 -0
  70. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/parse/orca/test_geomscan.py +0 -0
  71. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/parse/orca/test_interp.py +0 -0
  72. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/parse/test_chemgp_hdf5.py +0 -0
  73. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/parse/test_chemgp_jsonl_types.py +0 -0
  74. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/parse/test_con_io.py +0 -0
  75. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/parse/test_converter.py +0 -0
  76. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/parse/test_dimer_trajectory.py +0 -0
  77. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/parse/test_eon_neb_types.py +0 -0
  78. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/parse/test_eon_trajectory_common.py +0 -0
  79. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/parse/test_min_trajectory.py +0 -0
  80. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/parse/test_minimization_result.py +0 -0
  81. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/parse/test_neb_stitch.py +0 -0
  82. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/parse/test_neb_utils.py +0 -0
  83. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/parse/test_parser_types.py +0 -0
  84. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/parse/test_patterns.py +0 -0
  85. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/parse/test_plumed.py +0 -0
  86. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/parse/test_projection.py +0 -0
  87. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/parse/test_readcon_boundary.py +0 -0
  88. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/parse/test_saddle_results.py +0 -0
  89. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/parse/test_sella_saddle.py +0 -0
  90. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/parse/test_trajectory_hdf5.py +0 -0
  91. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/parse/test_trajectory_neb.py +0 -0
  92. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/plot/__init__.py +0 -0
  93. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/plot/test_chemgp_utils.py +0 -0
  94. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/plot/test_neb_helpers.py +0 -0
  95. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/plot/test_neb_renderers.py +0 -0
  96. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/plot/test_optimization.py +0 -0
  97. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/plot/test_orca_neb_payload.py +0 -0
  98. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/plot/test_projection_refactor.py +0 -0
  99. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/plot/test_strip_rendering.py +0 -0
  100. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/plot/test_structs_units.py +0 -0
  101. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/tests/test_full_coverage.py +0 -0
  102. {chemparseplot-1.9.7 → chemparseplot-1.9.9}/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.7
3
+ Version: 1.9.9
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
@@ -25,6 +25,8 @@ Requires-Dist: ase>=3.22; extra == 'all'
25
25
  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
+ Requires-Dist: orca-pi>=0.1.0; extra == 'all'
29
+ Requires-Dist: parsimonious>=0.10.0; extra == 'all'
28
30
  Requires-Dist: polars>=0.20; extra == 'all'
29
31
  Requires-Dist: readcon>=0.13.1; extra == 'all'
30
32
  Requires-Dist: xyzrender>=0.1.2; extra == 'all'
@@ -39,6 +41,8 @@ Requires-Dist: sphinx-sitemap>=2.5.1; extra == 'doc'
39
41
  Requires-Dist: sphinx-togglebutton>=0.3.2; extra == 'doc'
40
42
  Requires-Dist: sphinx>=7.2.6; extra == 'doc'
41
43
  Requires-Dist: sphinxcontrib-apidoc>=0.4; extra == 'doc'
44
+ Provides-Extra: grammar
45
+ Requires-Dist: parsimonious>=0.10.0; extra == 'grammar'
42
46
  Provides-Extra: lint
43
47
  Requires-Dist: ruff>=0.1.6; extra == 'lint'
44
48
  Provides-Extra: neb
@@ -46,6 +50,8 @@ Requires-Dist: ase>=3.22; extra == 'neb'
46
50
  Requires-Dist: h5py>=3.0; extra == 'neb'
47
51
  Requires-Dist: polars>=0.20; extra == 'neb'
48
52
  Requires-Dist: readcon>=0.13.1; extra == 'neb'
53
+ Provides-Extra: opi
54
+ Requires-Dist: orca-pi>=0.1.0; extra == 'opi'
49
55
  Provides-Extra: plot
50
56
  Requires-Dist: cmcrameri>=1.7; extra == 'plot'
51
57
  Requires-Dist: matplotlib>=3.8.2; extra == 'plot'
@@ -58,6 +64,7 @@ Provides-Extra: test
58
64
  Requires-Dist: ase>=3.22; extra == 'test'
59
65
  Requires-Dist: h5py>=3.0; extra == 'test'
60
66
  Requires-Dist: matplotlib>=3.8.2; extra == 'test'
67
+ Requires-Dist: parsimonious>=0.10.0; extra == 'test'
61
68
  Requires-Dist: polars>=0.20; extra == 'test'
62
69
  Requires-Dist: pytest-cov>=4.1.0; extra == 'test'
63
70
  Requires-Dist: pytest>=7.4.3; extra == 'test'
@@ -115,6 +122,18 @@ publication-quality plots.
115
122
  ## Installation
116
123
 
117
124
  pip install chemparseplot
125
+
126
+ Suite config (shared with rgpycrumbs — **no** `~/.config/chemparseplot`):
127
+
128
+ # ~/.config/rgpkgs/config.toml or ./rgpkgs.toml
129
+ # [pins] lock = "uv.lock"
130
+ # [pins.packages] …
131
+
132
+ Feature extras (`plot`, `neb`, …) are *transitional* install convenience. Target
133
+ is the same on-demand uv / `ensure_import` design as the rgpycrumbs hub; prefer
134
+ bare install + suite pins for new work.
135
+
136
+ # transitional:
118
137
  # With plotting support
119
138
  pip install "chemparseplot[plot]"
120
139
  # Everything
@@ -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.7'
22
- __version_tuple__ = version_tuple = (1, 9, 7)
21
+ __version__ = version = '1.9.9'
22
+ __version_tuple__ = version_tuple = (1, 9, 9)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -0,0 +1,143 @@
1
+ # SPDX-FileCopyrightText: 2023-present Rohit Goswami <rog32@hi.is>
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ """Stable public library entry points for chemparseplot.
5
+
6
+ Prefer this module for new consumers (cookbooks, wailord, notebooks).
7
+
8
+ **Representative in-process path (parse → typed result):**
9
+
10
+ .. code-block:: python
11
+
12
+ from chemparseplot.api import extract_orca_geomscan_energy
13
+
14
+ dist_bohr, energy_Eh = extract_orca_geomscan_energy(orca_out_text, "Actual Energy")
15
+ # dist_bohr, energy_Eh are pint Quantities (bohr, hartree)
16
+
17
+ Heavy plot stacks still live under ``chemparseplot.plot`` (optional deps /
18
+ transitional extras until suite uv design fully covers them).
19
+
20
+ Suite pins/config: use ``rgpycrumbs.api`` (shared rgpkgs TOML) — this package
21
+ does **not** invent ``~/.config/chemparseplot``.
22
+
23
+ .. versionadded:: 1.9.8
24
+ .. versionchanged:: 1.9.9
25
+ Expose ORCA geomscan energy parse as the stable library path.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from typing import TYPE_CHECKING
31
+
32
+ from chemparseplot.parse.orca.geomscan import extract_energy_data
33
+ from chemparseplot.units import (
34
+ ENERGY_UNITS,
35
+ convert_energy_magnitude,
36
+ energy_quantity,
37
+ normalize_energy_unit,
38
+ )
39
+
40
+ if TYPE_CHECKING:
41
+ from chemparseplot.units import Q_
42
+
43
+ __all__ = [
44
+ "ENERGY_UNITS",
45
+ "convert_energy_magnitude",
46
+ "energy_quantity",
47
+ "extract_orca_geomscan_energy",
48
+ "grammar_available",
49
+ "normalize_energy_unit",
50
+ "parse_orca_final_energy",
51
+ "parse_xyz",
52
+ "suite_pins",
53
+ ]
54
+
55
+
56
+ def grammar_available() -> bool:
57
+ """True if the optional parsimonious grammar track is importable."""
58
+ from chemparseplot.parse.grammar import grammar_available as _ga
59
+
60
+ return _ga()
61
+
62
+
63
+ def _looks_like_path(s: str) -> bool:
64
+ """True for short path-like strings (avoid Path on multi-line blobs)."""
65
+ if "\n" in s or len(s) > 4096:
66
+ return False
67
+ from pathlib import Path
68
+
69
+ try:
70
+ return Path(s).is_file()
71
+ except OSError:
72
+ return False
73
+
74
+
75
+ def parse_xyz(path_or_text: str):
76
+ """Parse XYZ via grammar track (file path if exists, else text).
77
+
78
+ Requires ``chemparseplot[grammar]``.
79
+ """
80
+ from chemparseplot.parse.grammar.xyz import parse_xyz_file, parse_xyz_text
81
+
82
+ if _looks_like_path(path_or_text):
83
+ return parse_xyz_file(path_or_text)
84
+ return parse_xyz_text(path_or_text)
85
+
86
+
87
+ def parse_orca_final_energy(path_or_text: str):
88
+ """Last ``FINAL SINGLE POINT ENERGY`` as a hartree Quantity.
89
+
90
+ Grammar track; requires ``chemparseplot[grammar]``.
91
+ """
92
+ from chemparseplot.parse.grammar.orca_text import (
93
+ parse_orca_text_file,
94
+ parse_orca_text_summary,
95
+ )
96
+
97
+ if _looks_like_path(path_or_text):
98
+ summary = parse_orca_text_file(path_or_text)
99
+ else:
100
+ summary = parse_orca_text_summary(path_or_text)
101
+ return summary.final_energy
102
+
103
+
104
+
105
+ def extract_orca_geomscan_energy(
106
+ data: str, energy_type: str = "Actual Energy"
107
+ ) -> tuple[Q_, Q_]:
108
+ """Parse ORCA geometry-scan surface energies from output text.
109
+
110
+ In-process library path: text → typed ``(distance, energy)`` pint Quantities
111
+ in Bohr and Hartree (ORCA defaults).
112
+
113
+ Parameters
114
+ ----------
115
+ data:
116
+ Blob of ORCA output containing ``Calculated Surface`` blocks.
117
+ energy_type:
118
+ ``\"Actual Energy\"`` or ``\"SCF energy\"`` (substring match in the block).
119
+
120
+ Returns
121
+ -------
122
+ tuple[Q_, Q_]
123
+ Distances (bohr) and energies (hartree). Empty quantities if no match.
124
+ """
125
+ return extract_energy_data(data, energy_type)
126
+
127
+
128
+ def suite_pins() -> dict[str, str]:
129
+ """Return suite package pins from rgpkgs config + env (soft on old hub).
130
+
131
+ Empty dict if rgpycrumbs is missing or too old.
132
+ """
133
+ try:
134
+ from rgpycrumbs.api import load_config, pins_from_env
135
+ except ImportError:
136
+ return {}
137
+ pins = dict(pins_from_env())
138
+ try:
139
+ cfg = load_config()
140
+ pins.update(cfg.merged_package_pins_normalized())
141
+ except Exception: # noqa: BLE001 — soft fail for consumers
142
+ pass
143
+ return pins
@@ -0,0 +1,20 @@
1
+ # SPDX-FileCopyrightText: 2023-present Rohit Goswami <rog32@hi.is>
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+
5
+ from chemparseplot.parse import eon, orca, patterns
6
+
7
+ __all__ = ["eon", "grammar", "orca", "patterns"]
8
+
9
+ # Lazy imports for modules with optional heavy deps (h5py, pandas, parsimonious)
10
+ # Import directly: from chemparseplot.parse.chemgp_hdf5 import read_h5_table
11
+ # Or: from chemparseplot.parse import plumed, projection
12
+ # Grammar track: from chemparseplot.parse.grammar import parse_xyz_text
13
+
14
+
15
+ def __getattr__(name: str):
16
+ if name == "grammar":
17
+ from chemparseplot.parse import grammar as _grammar
18
+
19
+ return _grammar
20
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,48 @@
1
+ # SPDX-FileCopyrightText: 2023-present Rohit Goswami <rog32@hi.is>
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ """Grammar / AST track for text-heavy formats (parsimonious).
5
+
6
+ Optional extra: ``pip install 'chemparseplot[grammar]'``.
7
+
8
+ Prefer structured APIs (OPI) for ORCA products that expose them; use this
9
+ package for XYZ and text-only ORCA sections (energy lines, Cartesian blocks).
10
+
11
+ ```{versionadded} 1.9.9
12
+ ```
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from chemparseplot.parse.grammar._deps import (
18
+ grammar_available,
19
+ reset_grammar_cache,
20
+ )
21
+
22
+ __all__ = [
23
+ "grammar_available",
24
+ "parse_orca_text_file",
25
+ "parse_orca_text_summary",
26
+ "parse_xyz_file",
27
+ "parse_xyz_text",
28
+ "reset_grammar_cache",
29
+ ]
30
+
31
+
32
+ def __getattr__(name: str):
33
+ if name in ("parse_xyz_text", "parse_xyz_file", "XyzFrame", "XyzAtom", "write_xyz_text"):
34
+ from chemparseplot.parse.grammar import xyz as _xyz
35
+
36
+ return getattr(_xyz, name)
37
+ if name in (
38
+ "parse_orca_text_summary",
39
+ "parse_orca_text_file",
40
+ "OrcaTextSummary",
41
+ "OrcaAtomCoord",
42
+ "extract_final_energies_hartree",
43
+ "extract_cartesian_angstrom_blocks",
44
+ ):
45
+ from chemparseplot.parse.grammar import orca_text as _ot
46
+
47
+ return getattr(_ot, name)
48
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,68 @@
1
+ # SPDX-FileCopyrightText: 2023-present Rohit Goswami <rog32@hi.is>
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ """Lazy loading for optional ``parsimonious`` (grammar track).
5
+
6
+ ```{versionadded} 1.9.9
7
+ ```
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import importlib
13
+ import importlib.util
14
+ from typing import Any
15
+
16
+ _grammar_mod: Any | None = None
17
+ _nodes_mod: Any | None = None
18
+ _probe: bool | None = None
19
+
20
+ _INSTALL_HINT = (
21
+ "parsimonious is not installed. "
22
+ "Install with: pip install 'chemparseplot[grammar]' "
23
+ "for grammar/AST text parsers (XYZ, text-heavy ORCA sections)."
24
+ )
25
+
26
+
27
+ def grammar_available() -> bool:
28
+ """Return True if ``parsimonious`` can be imported."""
29
+ global _probe
30
+ if _probe is not None:
31
+ return _probe
32
+ try:
33
+ _probe = importlib.util.find_spec("parsimonious") is not None
34
+ except (ImportError, ModuleNotFoundError, ValueError):
35
+ _probe = False
36
+ return _probe
37
+
38
+
39
+ def get_grammar_class() -> Any:
40
+ """Return ``parsimonious.grammar.Grammar``, importing lazily."""
41
+ global _grammar_mod
42
+ if _grammar_mod is not None:
43
+ return _grammar_mod.Grammar
44
+ try:
45
+ _grammar_mod = importlib.import_module("parsimonious.grammar")
46
+ except ImportError as exc:
47
+ raise ImportError(_INSTALL_HINT) from exc
48
+ return _grammar_mod.Grammar
49
+
50
+
51
+ def get_node_visitor_class() -> Any:
52
+ """Return ``parsimonious.nodes.NodeVisitor``, importing lazily."""
53
+ global _nodes_mod
54
+ if _nodes_mod is not None:
55
+ return _nodes_mod.NodeVisitor
56
+ try:
57
+ _nodes_mod = importlib.import_module("parsimonious.nodes")
58
+ except ImportError as exc:
59
+ raise ImportError(_INSTALL_HINT) from exc
60
+ return _nodes_mod.NodeVisitor
61
+
62
+
63
+ def reset_grammar_cache() -> None:
64
+ """Clear cached parsimonious import state (tests only)."""
65
+ global _grammar_mod, _nodes_mod, _probe
66
+ _grammar_mod = None
67
+ _nodes_mod = None
68
+ _probe = None
@@ -0,0 +1,235 @@
1
+ # SPDX-FileCopyrightText: 2023-present Rohit Goswami <rog32@hi.is>
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ """Grammar-oriented extractors for text-heavy ORCA output sections.
5
+
6
+ Prefer structured backends (OPI) when available. This module covers sections
7
+ that remain text dumps: final single-point energies and Cartesian geometry
8
+ blocks. Foundation for VPT2/IR/populations ports from wailord.
9
+
10
+ ```{versionadded} 1.9.9
11
+ ```
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+ from dataclasses import dataclass
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ from chemparseplot.parse.grammar._deps import (
22
+ get_grammar_class,
23
+ get_node_visitor_class,
24
+ )
25
+ from chemparseplot.units import Q_
26
+
27
+ # Named rules are real expressions (not aliases) so visitors fire reliably.
28
+ _ENERGY_LINE_GRAMMAR_SRC = r"""
29
+ energy_line = label ws value ws?
30
+ label = "FINAL SINGLE POINT ENERGY"
31
+ value = ~r"[+-]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][+-]?\d+)?"
32
+ ws = ~r"[ \t]+"
33
+ """
34
+
35
+ _CART_BLOCK_GRAMMAR_SRC = r"""
36
+ cart_block = header newline dashes newline (atom_line newline)+
37
+ header = "CARTESIAN COORDINATES (ANGSTROEM)"
38
+ dashes = ~r"-{3,}"
39
+ atom_line = ws? atype ws float_ ws float_ ws float_ ws?
40
+ atype = ~r"[A-Za-z][a-zA-Z0-9]{0,2}"
41
+ float_ = ~r"[+-]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][+-]?\d+)?"
42
+ newline = ~r"\n"
43
+ ws = ~r"[ \t]+"
44
+ """
45
+
46
+ _energy_grammar: Any | None = None
47
+ _cart_grammar: Any | None = None
48
+
49
+ _ENERGY_LINE_RE = re.compile(
50
+ r"^.*FINAL SINGLE POINT ENERGY[ \t]+"
51
+ r"([+-]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][+-]?\d+)?).*$",
52
+ re.MULTILINE,
53
+ )
54
+
55
+ _CART_BLOCK_RE = re.compile(
56
+ r"CARTESIAN COORDINATES \(ANGSTROEM\)\n"
57
+ r"-{3,}\n"
58
+ r"((?:[ \t]*[A-Za-z][a-zA-Z0-9]{0,2}[ \t]+"
59
+ r"[+-]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][+-]?\d+)?[ \t]+"
60
+ r"[+-]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][+-]?\d+)?[ \t]+"
61
+ r"[+-]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][+-]?\d+)?[ \t]*\n)+)",
62
+ re.MULTILINE,
63
+ )
64
+
65
+ _N_ATOMS_RE = re.compile(
66
+ r"Number of atoms\s*(?:\.{3,}|\.\.\.)\s*(\d+)",
67
+ )
68
+
69
+
70
+ def _energy_g() -> Any:
71
+ global _energy_grammar
72
+ if _energy_grammar is None:
73
+ Grammar = get_grammar_class()
74
+ _energy_grammar = Grammar(_ENERGY_LINE_GRAMMAR_SRC)
75
+ return _energy_grammar
76
+
77
+
78
+ def _cart_g() -> Any:
79
+ global _cart_grammar
80
+ if _cart_grammar is None:
81
+ Grammar = get_grammar_class()
82
+ _cart_grammar = Grammar(_CART_BLOCK_GRAMMAR_SRC)
83
+ return _cart_grammar
84
+
85
+
86
+ def _flatten(xs: Any) -> list[Any]:
87
+ out: list[Any] = []
88
+ if isinstance(xs, list):
89
+ for x in xs:
90
+ out.extend(_flatten(x))
91
+ elif xs is not None and xs != "":
92
+ out.append(xs)
93
+ return out
94
+
95
+
96
+ @dataclass(frozen=True, slots=True)
97
+ class OrcaAtomCoord:
98
+ """One atom in an ORCA Cartesian (Å) block."""
99
+
100
+ symbol: str
101
+ x: float
102
+ y: float
103
+ z: float
104
+
105
+
106
+ @dataclass(frozen=True, slots=True)
107
+ class OrcaTextSummary:
108
+ """Structured slice of text ORCA output."""
109
+
110
+ energies_hartree: tuple[float, ...]
111
+ last_geometry: tuple[OrcaAtomCoord, ...]
112
+ n_atoms: int | None
113
+
114
+ @property
115
+ def final_energy_hartree(self) -> float | None:
116
+ if not self.energies_hartree:
117
+ return None
118
+ return self.energies_hartree[-1]
119
+
120
+ @property
121
+ def final_energy(self) -> Any:
122
+ """Last final single-point energy as a pint Quantity (hartree)."""
123
+ e = self.final_energy_hartree
124
+ if e is None:
125
+ return Q_([], "hartree")
126
+ return Q_(e, "hartree")
127
+
128
+
129
+ def extract_final_energies_hartree(text: str) -> list[float]:
130
+ """All ``FINAL SINGLE POINT ENERGY`` values in document order.
131
+
132
+ Candidate lines are selected with a line scan; each is validated and
133
+ extracted with a parsimonious line grammar.
134
+ """
135
+ g = _energy_g()
136
+ NodeVisitor = get_node_visitor_class()
137
+
138
+ class EnergyVisitor(NodeVisitor):
139
+ def __init__(self) -> None:
140
+ self.value: float | None = None
141
+
142
+ def visit_value(self, node: Any, visited_children: Any) -> float:
143
+ self.value = float(node.text)
144
+ return self.value
145
+
146
+ def generic_visit(self, node: Any, visited_children: Any) -> Any:
147
+ return visited_children or node.text
148
+
149
+ out: list[float] = []
150
+ for m in _ENERGY_LINE_RE.finditer(text):
151
+ line = m.group(0).strip()
152
+ try:
153
+ tree = g.parse(line)
154
+ except Exception: # noqa: BLE001 — skip non-conforming noise
155
+ continue
156
+ v = EnergyVisitor()
157
+ v.visit(tree)
158
+ if v.value is not None:
159
+ out.append(v.value)
160
+ return out
161
+
162
+
163
+ def extract_cartesian_angstrom_blocks(text: str) -> list[list[OrcaAtomCoord]]:
164
+ """Parse every ``CARTESIAN COORDINATES (ANGSTROEM)`` block via grammar."""
165
+ g = _cart_g()
166
+ NodeVisitor = get_node_visitor_class()
167
+
168
+ class CartVisitor(NodeVisitor):
169
+ def __init__(self) -> None:
170
+ self.atoms: list[OrcaAtomCoord] = []
171
+
172
+ def visit_atom_line(self, node: Any, visited_children: Any) -> OrcaAtomCoord:
173
+ flat = _flatten(visited_children)
174
+ symbol = next(
175
+ x for x in flat if isinstance(x, str) and x.strip() and x[0].isalpha()
176
+ )
177
+ nums = [x for x in flat if isinstance(x, float)]
178
+ atom = OrcaAtomCoord(symbol=symbol, x=nums[0], y=nums[1], z=nums[2])
179
+ self.atoms.append(atom)
180
+ return atom
181
+
182
+ def visit_atype(self, node: Any, visited_children: Any) -> str:
183
+ return node.text
184
+
185
+ def visit_float_(self, node: Any, visited_children: Any) -> float:
186
+ return float(node.text)
187
+
188
+ def generic_visit(self, node: Any, visited_children: Any) -> Any:
189
+ return visited_children or node.text
190
+
191
+ blocks: list[list[OrcaAtomCoord]] = []
192
+ for m in _CART_BLOCK_RE.finditer(text):
193
+ chunk = (
194
+ "CARTESIAN COORDINATES (ANGSTROEM)\n"
195
+ "---------------------------------\n"
196
+ + m.group(1)
197
+ )
198
+ if not chunk.endswith("\n"):
199
+ chunk += "\n"
200
+ try:
201
+ tree = g.parse(chunk)
202
+ except Exception: # noqa: BLE001
203
+ continue
204
+ v = CartVisitor()
205
+ v.visit(tree)
206
+ if v.atoms:
207
+ blocks.append(v.atoms)
208
+ return blocks
209
+
210
+
211
+ def extract_n_atoms(text: str) -> int | None:
212
+ """Best-effort ``Number of atoms`` from ORCA text (last occurrence)."""
213
+ matches = _N_ATOMS_RE.findall(text)
214
+ if not matches:
215
+ return None
216
+ return int(matches[-1])
217
+
218
+
219
+ def parse_orca_text_summary(text: str) -> OrcaTextSummary:
220
+ """Summarize final energies and last Cartesian geometry from ORCA text."""
221
+ energies = extract_final_energies_hartree(text)
222
+ blocks = extract_cartesian_angstrom_blocks(text)
223
+ last_geom: tuple[OrcaAtomCoord, ...] = ()
224
+ if blocks:
225
+ last_geom = tuple(blocks[-1])
226
+ return OrcaTextSummary(
227
+ energies_hartree=tuple(energies),
228
+ last_geometry=last_geom,
229
+ n_atoms=extract_n_atoms(text),
230
+ )
231
+
232
+
233
+ def parse_orca_text_file(path: str | Path) -> OrcaTextSummary:
234
+ """Read an ORCA ``.out`` and return :class:`OrcaTextSummary`."""
235
+ return parse_orca_text_summary(Path(path).read_text())