chemparseplot 1.9.6__tar.gz → 1.9.8__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 (88) hide show
  1. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/PKG-INFO +13 -1
  2. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/_version.py +2 -2
  3. chemparseplot-1.9.8/chemparseplot/api.py +91 -0
  4. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/plot/optimization.py +113 -39
  5. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/readme.md +12 -0
  6. chemparseplot-1.9.8/tests/fixtures_geomscan_snippet.txt +9 -0
  7. chemparseplot-1.9.8/tests/test_public_api.py +72 -0
  8. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/.gitignore +0 -0
  9. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/LICENSE +0 -0
  10. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/__init__.py +0 -0
  11. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/__init__.py +0 -0
  12. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/chemgp_hdf5.py +0 -0
  13. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/chemgp_jsonl.py +0 -0
  14. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/converter.py +0 -0
  15. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/eon/__init__.py +0 -0
  16. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/eon/_trajectory_common.py +0 -0
  17. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/eon/con_io.py +0 -0
  18. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/eon/dimer_trajectory.py +0 -0
  19. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/eon/gprd.py +0 -0
  20. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/eon/min_trajectory.py +0 -0
  21. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/eon/minimization.py +0 -0
  22. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/eon/neb.py +0 -0
  23. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/eon/saddle_search.py +0 -0
  24. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/eon/stitch.py +0 -0
  25. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/file_.py +0 -0
  26. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/neb_utils.py +0 -0
  27. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/orca/__init__.py +0 -0
  28. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/orca/geomscan.py +0 -0
  29. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/orca/neb/__init__.py +0 -0
  30. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/orca/neb/interp.py +0 -0
  31. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/orca/neb/opi_parser.py +0 -0
  32. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/patterns.py +0 -0
  33. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/plumed.py +0 -0
  34. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/projection.py +0 -0
  35. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/sella/saddle_search.py +0 -0
  36. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/trajectory/__init__.py +0 -0
  37. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/trajectory/hdf5.py +0 -0
  38. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/trajectory/neb.py +0 -0
  39. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/parse/types.py +0 -0
  40. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/plot/__init__.py +0 -0
  41. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/plot/chemgp.py +0 -0
  42. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/plot/geomscan.py +0 -0
  43. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/plot/neb.py +0 -0
  44. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/plot/plumed.py +0 -0
  45. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/plot/structs.py +0 -0
  46. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/plot/theme.py +0 -0
  47. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/units.py +0 -0
  48. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/chemparseplot/util.py +0 -0
  49. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/pyproject.toml +0 -0
  50. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/__init__.py +0 -0
  51. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/_optional_imports.py +0 -0
  52. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/conftest.py +0 -0
  53. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/parse/__init__.py +0 -0
  54. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/parse/_con_fixtures.py +0 -0
  55. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/parse/orca/test_geomscan.py +0 -0
  56. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/parse/orca/test_interp.py +0 -0
  57. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/parse/test_chemgp_hdf5.py +0 -0
  58. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/parse/test_chemgp_jsonl_types.py +0 -0
  59. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/parse/test_con_io.py +0 -0
  60. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/parse/test_converter.py +0 -0
  61. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/parse/test_dimer_trajectory.py +0 -0
  62. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/parse/test_eon_neb_types.py +0 -0
  63. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/parse/test_eon_trajectory_common.py +0 -0
  64. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/parse/test_min_trajectory.py +0 -0
  65. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/parse/test_minimization_result.py +0 -0
  66. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/parse/test_neb_stitch.py +0 -0
  67. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/parse/test_neb_utils.py +0 -0
  68. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/parse/test_parser_types.py +0 -0
  69. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/parse/test_patterns.py +0 -0
  70. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/parse/test_plumed.py +0 -0
  71. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/parse/test_projection.py +0 -0
  72. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/parse/test_readcon_boundary.py +0 -0
  73. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/parse/test_saddle_results.py +0 -0
  74. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/parse/test_sella_saddle.py +0 -0
  75. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/parse/test_trajectory_hdf5.py +0 -0
  76. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/parse/test_trajectory_neb.py +0 -0
  77. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/plot/__init__.py +0 -0
  78. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/plot/test_chemgp_utils.py +0 -0
  79. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/plot/test_neb_helpers.py +0 -0
  80. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/plot/test_neb_renderers.py +0 -0
  81. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/plot/test_optimization.py +0 -0
  82. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/plot/test_orca_neb_payload.py +0 -0
  83. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/plot/test_projection_refactor.py +0 -0
  84. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/plot/test_strip_rendering.py +0 -0
  85. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/plot/test_structs_units.py +0 -0
  86. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/test_coverage_batch.py +0 -0
  87. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/tests/test_full_coverage.py +0 -0
  88. {chemparseplot-1.9.6 → chemparseplot-1.9.8}/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.6
3
+ Version: 1.9.8
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
@@ -115,6 +115,18 @@ publication-quality plots.
115
115
  ## Installation
116
116
 
117
117
  pip install chemparseplot
118
+
119
+ Suite config (shared with rgpycrumbs — **no** `~/.config/chemparseplot`):
120
+
121
+ # ~/.config/rgpkgs/config.toml or ./rgpkgs.toml
122
+ # [pins] lock = "uv.lock"
123
+ # [pins.packages] …
124
+
125
+ Feature extras (`plot`, `neb`, …) are *transitional* install convenience. Target
126
+ is the same on-demand uv / `ensure_import` design as the rgpycrumbs hub; prefer
127
+ bare install + suite pins for new work.
128
+
129
+ # transitional:
118
130
  # With plotting support
119
131
  pip install "chemparseplot[plot]"
120
132
  # 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.6'
22
- __version_tuple__ = version_tuple = (1, 9, 6)
21
+ __version__ = version = '1.9.8'
22
+ __version_tuple__ = version_tuple = (1, 9, 8)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -0,0 +1,91 @@
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
+ "normalize_energy_unit",
49
+ "suite_pins",
50
+ ]
51
+
52
+
53
+ def extract_orca_geomscan_energy(
54
+ data: str, energy_type: str = "Actual Energy"
55
+ ) -> tuple[Q_, Q_]:
56
+ """Parse ORCA geometry-scan surface energies from output text.
57
+
58
+ In-process library path: text → typed ``(distance, energy)`` pint Quantities
59
+ in Bohr and Hartree (ORCA defaults).
60
+
61
+ Parameters
62
+ ----------
63
+ data:
64
+ Blob of ORCA output containing ``Calculated Surface`` blocks.
65
+ energy_type:
66
+ ``\"Actual Energy\"`` or ``\"SCF energy\"`` (substring match in the block).
67
+
68
+ Returns
69
+ -------
70
+ tuple[Q_, Q_]
71
+ Distances (bohr) and energies (hartree). Empty quantities if no match.
72
+ """
73
+ return extract_energy_data(data, energy_type)
74
+
75
+
76
+ def suite_pins() -> dict[str, str]:
77
+ """Return suite package pins from rgpkgs config + env (soft on old hub).
78
+
79
+ Empty dict if rgpycrumbs is missing or too old.
80
+ """
81
+ try:
82
+ from rgpycrumbs.api import load_config, pins_from_env
83
+ except ImportError:
84
+ return {}
85
+ pins = dict(pins_from_env())
86
+ try:
87
+ cfg = load_config()
88
+ pins.update(cfg.merged_package_pins_normalized())
89
+ except Exception: # noqa: BLE001 — soft fail for consumers
90
+ pass
91
+ return pins
@@ -59,13 +59,15 @@ def create_landscape_axes(
59
59
  dpi: int,
60
60
  has_strip: bool,
61
61
  theme,
62
- base_size: float = 5.37,
63
- strip_height_ratio: float = 0.24,
64
- strip_hspace: float = 0.30,
62
+ base_size: float = 6.0,
63
+ strip_height_ratio: float = 0.38,
64
+ strip_hspace: float = 0.22,
65
65
  ):
66
66
  """Create a landscape figure with an optional structure strip axis."""
67
67
 
68
- fig = plt.figure(figsize=(base_size, base_size + (1.20 if has_strip else 0)), dpi=dpi)
68
+ # Extra bottom room so x-label / ticks are not crushed by the strip.
69
+ fig_h = base_size + (2.15 if has_strip else 0.35)
70
+ fig = plt.figure(figsize=(base_size + 1.1, fig_h), dpi=dpi)
69
71
  if has_strip:
70
72
  gs = GridSpec(
71
73
  2,
@@ -73,6 +75,10 @@ def create_landscape_axes(
73
75
  height_ratios=[1, strip_height_ratio],
74
76
  hspace=strip_hspace,
75
77
  figure=fig,
78
+ left=0.14,
79
+ right=0.86,
80
+ top=0.90,
81
+ bottom=0.08,
76
82
  )
77
83
  ax = fig.add_subplot(gs[0])
78
84
  ax_strip = fig.add_subplot(gs[1])
@@ -102,22 +108,24 @@ def annotate_endpoint(ax, x: float, y: float, label: str, *, boxed: bool):
102
108
  """Annotate an optimization endpoint consistently."""
103
109
 
104
110
  kwargs = {
105
- "fontsize": 10,
111
+ "fontsize": 12,
106
112
  "fontweight": "bold",
113
+ "color": "black",
107
114
  "ha": "center",
108
115
  "va": "bottom",
109
- "zorder": 60,
116
+ "zorder": 120,
110
117
  }
111
118
  if boxed:
112
119
  kwargs.update(
113
120
  {
114
- "xytext": (0, 6),
121
+ "xytext": (0, 8),
115
122
  "textcoords": "offset points",
116
123
  "bbox": {
117
- "boxstyle": "round,pad=0.2",
124
+ "boxstyle": "round,pad=0.25",
118
125
  "facecolor": "white",
119
- "edgecolor": "none",
120
- "alpha": 0.85,
126
+ "edgecolor": "black",
127
+ "linewidth": 0.8,
128
+ "alpha": 0.95,
121
129
  },
122
130
  }
123
131
  )
@@ -160,7 +168,8 @@ def render_endpoint_strip(
160
168
  show_dividers=strip_dividers,
161
169
  perspective_tilt=perspective_tilt,
162
170
  xyzrender_config=xyzrender_config,
163
- width_fill_fraction=0.86,
171
+ width_fill_fraction=0.72,
172
+ prefer_single_row=True,
164
173
  )
165
174
 
166
175
 
@@ -204,15 +213,21 @@ def save_landscape_figure(
204
213
  ax=None,
205
214
  ax_strip=None,
206
215
  ) -> None:
207
- """Save optimization landscapes without tight-layout strip warnings."""
216
+ """Save optimization landscapes; always crop with a pad so labels stay visible."""
208
217
 
209
218
  if has_strip and ax is not None and ax_strip is not None:
210
- enforce_strip_clearance(fig, ax, ax_strip)
211
- if not has_strip:
219
+ enforce_strip_clearance(fig, ax, ax_strip, min_clearance_px=40.0)
220
+ elif not has_strip:
212
221
  fig.tight_layout()
213
- fig.savefig(str(output), dpi=dpi, bbox_inches="tight")
214
- else:
215
- fig.savefig(str(output), dpi=dpi)
222
+ # Always tight-crop: without it strip layouts left large white margins and
223
+ # could hide axis labels depending on the viewer/frame.
224
+ fig.savefig(
225
+ str(output),
226
+ dpi=dpi,
227
+ bbox_inches="tight",
228
+ pad_inches=0.25,
229
+ facecolor=fig.get_facecolor(),
230
+ )
216
231
  plt.close(fig)
217
232
 
218
233
 
@@ -314,6 +329,8 @@ def render_single_ended_landscape(
314
329
  energy_unit: str = "eV",
315
330
  energy_cap: float | None = None,
316
331
  energy_cap_window: float | None = None,
332
+ relative_energy: bool = True,
333
+ title: str | None = None,
317
334
  cmap: str = "viridis",
318
335
  output: Path,
319
336
  dpi: int = 150,
@@ -321,8 +338,8 @@ def render_single_ended_landscape(
321
338
  plot_structures: str = "none",
322
339
  strip_structs: Sequence[Any] | None = None,
323
340
  strip_labels: Sequence[str] | None = None,
324
- endpoint_start_label: str = "Init",
325
- endpoint_end_label: str = "Min",
341
+ endpoint_start_label: str = "initial",
342
+ endpoint_end_label: str = "minimized",
326
343
  endpoint_boxed: bool = True,
327
344
  annotate_overlay_starts: bool = False,
328
345
  overlay_start_label: str = "R",
@@ -345,6 +362,8 @@ def render_single_ended_landscape(
345
362
  ```{versionadded} 1.8.1
346
363
  ```
347
364
  """
365
+ from matplotlib import ticker
366
+
348
367
  from chemparseplot.parse.neb_utils import (
349
368
  calculate_landscape_coords,
350
369
  compute_synthetic_gradients,
@@ -363,33 +382,46 @@ def render_single_ended_landscape(
363
382
  energies = np.asarray(energies_eV, dtype=float)
364
383
  n = min(len(rmsd_a), len(energies))
365
384
  rmsd_a, rmsd_b, energies = rmsd_a[:n], rmsd_b[:n], energies[:n]
366
- display_e = convert_energy(energies, energy_unit)
385
+ # Relative energy keeps colorbars readable (absolute eV often span <0.2 eV
386
+ # and all ticks collapse to the same printed value).
387
+ e_ref = float(np.min(energies)) if relative_energy else 0.0
388
+ energies_plot = energies - e_ref
389
+ display_e = convert_energy(energies_plot, energy_unit)
367
390
  cap = energy_cap
368
391
  if cap is None and energy_cap_window is not None:
369
392
  cap = float(np.min(display_e)) + energy_cap_window
370
393
  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
394
  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)
395
+ energies_plot = np.minimum(
396
+ energies_plot, cap / factor if factor else cap
397
+ )
398
+ display_e = convert_energy(energies_plot, energy_unit)
376
399
 
377
- f_para = -np.gradient(energies)
400
+ f_para = -np.gradient(energies_plot)
378
401
  grad_a, grad_b = compute_synthetic_gradients(rmsd_a, rmsd_b, f_para)
379
402
 
380
- plot_optimization_landscape(
403
+ z_label = (
404
+ f"Relative energy ({energy_unit})"
405
+ if relative_energy
406
+ else energy_axis_label(energy_unit)
407
+ )
408
+ cb = plot_optimization_landscape(
381
409
  ax,
382
410
  rmsd_a,
383
411
  rmsd_b,
384
412
  grad_a,
385
413
  grad_b,
386
- energies,
414
+ energies_plot,
387
415
  project_path=project_path,
388
416
  method=surface_type,
389
417
  cmap=cmap,
390
418
  label_mode="optimization",
391
419
  energy_unit=energy_unit,
420
+ z_label=z_label,
392
421
  )
422
+ if cb is not None:
423
+ cb.ax.yaxis.set_major_formatter(ticker.FormatStrFormatter("%.3f"))
424
+ cb.set_label(z_label, rotation=270, labelpad=18)
393
425
 
394
426
  basis = None
395
427
  if project_path:
@@ -408,7 +440,9 @@ def render_single_ended_landscape(
408
440
  )
409
441
  m = min(len(ra), len(atoms_ov))
410
442
  ra, rb = ra[:m], rb[:m]
411
- px, py, _ = project_landscape_path(ra, rb, project_path=project_path, basis=basis)
443
+ px, py, _ = project_landscape_path(
444
+ ra, rb, project_path=project_path, basis=basis
445
+ )
412
446
  color = OVERLAY_COLORS[idx % len(OVERLAY_COLORS)]
413
447
  if len(overlays) > 1:
414
448
  ax.plot(
@@ -423,37 +457,77 @@ def render_single_ended_landscape(
423
457
  label=lbl,
424
458
  )
425
459
  if annotate_overlay_starts and len(px):
426
- annotate_endpoint(ax, float(px[0]), float(py[0]), overlay_start_label, boxed=False)
460
+ annotate_endpoint(
461
+ ax, float(px[0]), float(py[0]), overlay_start_label, boxed=False
462
+ )
427
463
 
428
464
  plot_x, plot_y, _ = project_landscape_path(
429
465
  rmsd_a, rmsd_b, project_path=project_path, basis=basis
430
466
  )
431
467
  annotate_endpoint(
432
- ax, float(plot_x[0]), float(plot_y[0]), endpoint_start_label, boxed=endpoint_boxed
468
+ ax,
469
+ float(plot_x[0]),
470
+ float(plot_y[0]),
471
+ endpoint_start_label,
472
+ boxed=endpoint_boxed,
433
473
  )
434
474
  annotate_endpoint(
435
- ax, float(plot_x[-1]), float(plot_y[-1]), endpoint_end_label, boxed=endpoint_boxed
475
+ ax,
476
+ float(plot_x[-1]),
477
+ float(plot_y[-1]),
478
+ endpoint_end_label,
479
+ boxed=endpoint_boxed,
436
480
  )
437
481
 
482
+ # True 1:1 Å panel: Δd window matches Δs (same RMSD metric).
483
+ if project_path and len(plot_x) > 1:
484
+ x0, x1 = float(np.min(plot_x)), float(np.max(plot_x))
485
+ y0, y1 = float(np.min(plot_y)), float(np.max(plot_y))
486
+ s_pad = max((x1 - x0) * 0.06, 0.01)
487
+ x0, x1 = x0 - s_pad, x1 + s_pad
488
+ half = max(0.5 * (x1 - x0), abs(y0), abs(y1), 0.02)
489
+ x_mid = 0.5 * (x0 + x1)
490
+ ax.set_xlim(x_mid - half, x_mid + half)
491
+ ax.set_ylim(-half, half)
492
+ ax.set_aspect("equal", adjustable="box")
493
+
494
+ # Re-assert axis labels after surface/path (bold, large enough to read).
495
+ if project_path:
496
+ ax.set_xlabel(_LABELS["optimization"]["x"], fontweight="bold", fontsize=12)
497
+ ax.set_ylabel(_LABELS["optimization"]["y"], fontweight="bold", fontsize=12)
498
+ else:
499
+ ax.set_xlabel(r"RMSD from ref A ($\AA$)", fontweight="bold", fontsize=12)
500
+ ax.set_ylabel(r"RMSD from ref B ($\AA$)", fontweight="bold", fontsize=12)
501
+ ax.tick_params(axis="both", labelsize=10)
502
+ if title:
503
+ ax.set_title(title, fontweight="bold", fontsize=13, pad=10)
504
+
438
505
  if overlays and len(overlays) > 1:
439
- ax.legend(frameon=True, framealpha=0.9, loc="best")
506
+ ax.legend(frameon=True, framealpha=0.95, loc="best")
440
507
 
441
508
  if has_strip and ax_strip is not None and strip_structs is not None:
509
+ zoom = (
510
+ strip_zoom
511
+ if strip_zoom is not None
512
+ else max(0.45, default_strip_zoom(list(strip_structs)) * 1.6)
513
+ )
442
514
  render_endpoint_strip(
443
515
  ax_strip,
444
516
  list(strip_structs),
445
517
  list(strip_labels or []),
446
- strip_zoom=strip_zoom,
518
+ strip_zoom=zoom,
447
519
  rotation=rotation,
448
520
  theme=theme,
449
521
  strip_renderer=strip_renderer,
450
- strip_spacing=strip_spacing,
522
+ strip_spacing=max(strip_spacing, 2.2),
451
523
  strip_dividers=strip_dividers,
452
524
  perspective_tilt=perspective_tilt,
453
525
  xyzrender_config=xyzrender_config,
454
526
  )
455
527
 
456
- save_landscape_figure(fig, output, dpi=dpi, has_strip=has_strip, ax=ax, ax_strip=ax_strip)
528
+ save_landscape_figure(
529
+ fig, output, dpi=dpi, has_strip=has_strip, ax=ax, ax_strip=ax_strip
530
+ )
457
531
 
458
532
 
459
533
  def plot_optimization_landscape(
@@ -533,11 +607,11 @@ def plot_optimization_landscape(
533
607
 
534
608
  labels = _LABELS.get(label_mode, _LABELS["optimization"])
535
609
  if project_path:
536
- ax.set_xlabel(labels["x"])
537
- ax.set_ylabel(labels["y"])
610
+ ax.set_xlabel(labels["x"], fontweight="bold", fontsize=12)
611
+ ax.set_ylabel(labels["y"], fontweight="bold", fontsize=12)
538
612
  else:
539
- ax.set_xlabel(r"RMSD from ref A ($\AA$)")
540
- ax.set_ylabel(r"RMSD from ref B ($\AA$)")
613
+ ax.set_xlabel(r"RMSD from ref A ($\AA$)", fontweight="bold", fontsize=12)
614
+ ax.set_ylabel(r"RMSD from ref B ($\AA$)", fontweight="bold", fontsize=12)
541
615
 
542
616
  return cb
543
617
 
@@ -45,6 +45,18 @@ publication-quality plots.
45
45
  ## Installation
46
46
 
47
47
  pip install chemparseplot
48
+
49
+ Suite config (shared with rgpycrumbs — **no** `~/.config/chemparseplot`):
50
+
51
+ # ~/.config/rgpkgs/config.toml or ./rgpkgs.toml
52
+ # [pins] lock = "uv.lock"
53
+ # [pins.packages] …
54
+
55
+ Feature extras (`plot`, `neb`, …) are *transitional* install convenience. Target
56
+ is the same on-demand uv / `ensure_import` design as the rgpycrumbs hub; prefer
57
+ bare install + suite pins for new work.
58
+
59
+ # transitional:
48
60
  # With plotting support
49
61
  pip install "chemparseplot[plot]"
50
62
  # Everything
@@ -0,0 +1,9 @@
1
+ The Calculated Surface using the 'Actual Energy'
2
+ 7.55890395 -0.74239862
3
+ 7.32930292 -0.74349939
4
+ 7.09970189 -0.74467446
5
+
6
+ The Calculated Surface using the SCF energy
7
+ 7.55890395 -0.74239862
8
+ 7.32930292 -0.74349939
9
+ 7.09970189 -0.74467446
@@ -0,0 +1,72 @@
1
+ # SPDX-FileCopyrightText: 2023-present Rohit Goswami <rog32@hi.is>
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ """Drive shipped chemparseplot.api entry points (parse → typed result)."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+
10
+ import numpy as np
11
+ import pytest
12
+
13
+ from chemparseplot.api import (
14
+ convert_energy_magnitude,
15
+ extract_orca_geomscan_energy,
16
+ normalize_energy_unit,
17
+ suite_pins,
18
+ )
19
+
20
+ # Inline fixture (same content as tests/fixtures_geomscan_snippet.txt)
21
+ _GEOMSCAN_SNIPPET = """
22
+ The Calculated Surface using the 'Actual Energy'
23
+ 7.55890395 -0.74239862
24
+ 7.32930292 -0.74349939
25
+ 7.09970189 -0.74467446
26
+
27
+ The Calculated Surface using the SCF energy
28
+ 7.55890395 -0.74239862
29
+ 7.32930292 -0.74349939
30
+ 7.09970189 -0.74467446
31
+ """
32
+
33
+ FIXTURE_FILE = Path(__file__).parent / "fixtures_geomscan_snippet.txt"
34
+
35
+
36
+ def test_normalize_and_convert_energy_library_path():
37
+ assert normalize_energy_unit("ev") == "eV"
38
+ kcal = convert_energy_magnitude(1.0, "kcal/mol")
39
+ val = float(np.asarray(kcal).reshape(-1)[0])
40
+ assert 20.0 < val < 25.0
41
+ back = float(np.asarray(convert_energy_magnitude(val, "eV", source_unit="kcal/mol")))
42
+ assert abs(back - 1.0) < 1e-6
43
+
44
+
45
+ def test_extract_orca_geomscan_energy_typed_nonempty():
46
+ """Public parse path: ORCA geomscan text → pint Quantities with data."""
47
+ dist, energy = extract_orca_geomscan_energy(_GEOMSCAN_SNIPPET, "Actual Energy")
48
+ assert dist.size == 3
49
+ assert energy.size == 3
50
+ assert str(dist.units) == "bohr"
51
+ assert str(energy.units) == "hartree"
52
+ np.testing.assert_allclose(
53
+ dist.magnitude,
54
+ [7.55890395, 7.32930292, 7.09970189],
55
+ )
56
+ np.testing.assert_allclose(
57
+ energy.magnitude,
58
+ [-0.74239862, -0.74349939, -0.74467446],
59
+ )
60
+
61
+
62
+ def test_extract_orca_geomscan_energy_from_fixture_file():
63
+ text = FIXTURE_FILE.read_text(encoding="utf-8")
64
+ dist, energy = extract_orca_geomscan_energy(text, "SCF energy")
65
+ assert dist.size >= 1
66
+ assert energy.size == dist.size
67
+ assert dist.size == 3
68
+
69
+
70
+ def test_suite_pins_returns_dict():
71
+ pins = suite_pins()
72
+ assert isinstance(pins, dict)
File without changes
File without changes