chemparseplot 1.10.1__tar.gz → 1.11.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.
Files changed (106) hide show
  1. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/PKG-INFO +1 -1
  2. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/_version.py +2 -2
  3. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/plot/disconnectivity.py +6 -16
  4. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/plot/landscape.py +33 -14
  5. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/plot/neb.py +41 -12
  6. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/pyproject.toml +1 -0
  7. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/plot/test_landscape_disconnectivity.py +4 -12
  8. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/plot/test_strip_rendering.py +37 -0
  9. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/.gitignore +0 -0
  10. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/LICENSE +0 -0
  11. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/__init__.py +0 -0
  12. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/api.py +0 -0
  13. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/__init__.py +0 -0
  14. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/chemgp_hdf5.py +0 -0
  15. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/chemgp_jsonl.py +0 -0
  16. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/converter.py +0 -0
  17. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/eon/__init__.py +0 -0
  18. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/eon/_trajectory_common.py +0 -0
  19. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/eon/con_io.py +0 -0
  20. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/eon/dimer_trajectory.py +0 -0
  21. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/eon/frame_series.py +0 -0
  22. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/eon/gprd.py +0 -0
  23. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/eon/min_trajectory.py +0 -0
  24. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/eon/minimization.py +0 -0
  25. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/eon/neb.py +0 -0
  26. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/eon/saddle_search.py +0 -0
  27. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/eon/stitch.py +0 -0
  28. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/file_.py +0 -0
  29. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/grammar/__init__.py +0 -0
  30. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/grammar/_deps.py +0 -0
  31. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/grammar/orca_text.py +0 -0
  32. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/grammar/xyz.py +0 -0
  33. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/neb_utils.py +0 -0
  34. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/orca/__init__.py +0 -0
  35. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/orca/_opi.py +0 -0
  36. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/orca/geomscan.py +0 -0
  37. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/orca/neb/__init__.py +0 -0
  38. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/orca/neb/interp.py +0 -0
  39. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/orca/neb/opi_parser.py +0 -0
  40. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/patterns.py +0 -0
  41. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/plumed.py +0 -0
  42. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/projection.py +0 -0
  43. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/sella/saddle_search.py +0 -0
  44. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/trajectory/__init__.py +0 -0
  45. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/trajectory/hdf5.py +0 -0
  46. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/trajectory/neb.py +0 -0
  47. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/parse/types.py +0 -0
  48. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/plot/__init__.py +0 -0
  49. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/plot/chemgp.py +0 -0
  50. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/plot/geomscan.py +0 -0
  51. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/plot/optimization.py +0 -0
  52. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/plot/plumed.py +0 -0
  53. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/plot/structs.py +0 -0
  54. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/plot/theme.py +0 -0
  55. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/units.py +0 -0
  56. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/chemparseplot/util.py +0 -0
  57. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/readme.md +0 -0
  58. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/__init__.py +0 -0
  59. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/_optional_imports.py +0 -0
  60. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/conftest.py +0 -0
  61. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/fixtures/h2mol.xyz +0 -0
  62. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/fixtures/orca_energy_coords_snippet.out +0 -0
  63. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/fixtures_geomscan_snippet.txt +0 -0
  64. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/__init__.py +0 -0
  65. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/_con_fixtures.py +0 -0
  66. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/grammar/__init__.py +0 -0
  67. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/grammar/test_grammar_deps.py +0 -0
  68. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/grammar/test_orca_text_grammar.py +0 -0
  69. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/grammar/test_xyz_grammar.py +0 -0
  70. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/orca/test_geomscan.py +0 -0
  71. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/orca/test_interp.py +0 -0
  72. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/test_chemgp_hdf5.py +0 -0
  73. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/test_chemgp_jsonl_types.py +0 -0
  74. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/test_con_io.py +0 -0
  75. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/test_converter.py +0 -0
  76. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/test_dimer_trajectory.py +0 -0
  77. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/test_eon_neb_types.py +0 -0
  78. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/test_eon_trajectory_common.py +0 -0
  79. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/test_frame_series.py +0 -0
  80. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/test_min_trajectory.py +0 -0
  81. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/test_minimization_result.py +0 -0
  82. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/test_neb_stitch.py +0 -0
  83. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/test_neb_utils.py +0 -0
  84. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/test_orca_public_api.py +0 -0
  85. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/test_parser_types.py +0 -0
  86. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/test_patterns.py +0 -0
  87. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/test_plumed.py +0 -0
  88. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/test_projection.py +0 -0
  89. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/test_readcon_boundary.py +0 -0
  90. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/test_saddle_results.py +0 -0
  91. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/test_sella_saddle.py +0 -0
  92. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/test_trajectory_hdf5.py +0 -0
  93. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/parse/test_trajectory_neb.py +0 -0
  94. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/plot/__init__.py +0 -0
  95. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/plot/test_chemgp_utils.py +0 -0
  96. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/plot/test_neb_helpers.py +0 -0
  97. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/plot/test_neb_renderers.py +0 -0
  98. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/plot/test_optimization.py +0 -0
  99. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/plot/test_orca_neb_payload.py +0 -0
  100. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/plot/test_projection_refactor.py +0 -0
  101. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/plot/test_structs_units.py +0 -0
  102. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/plot/test_surface_auto_thin.py +0 -0
  103. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/test_coverage_batch.py +0 -0
  104. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/test_full_coverage.py +0 -0
  105. {chemparseplot-1.10.1 → chemparseplot-1.11.0}/tests/test_public_api.py +0 -0
  106. {chemparseplot-1.10.1 → chemparseplot-1.11.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.10.1
3
+ Version: 1.11.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
@@ -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.10.1'
22
- __version_tuple__ = version_tuple = (1, 10, 1)
21
+ __version__ = version = '1.11.0'
22
+ __version_tuple__ = version_tuple = (1, 11, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -27,9 +27,7 @@ from scipy.sparse.csgraph import connected_components, laplacian
27
27
 
28
28
  from chemparseplot.plot.theme import RUHI_COLORS
29
29
 
30
- BASIN_PALETTE = [
31
- RUHI_COLORS[c] for c in ["teal", "sky", "magenta", "coral", "sunshine"]
32
- ]
30
+ BASIN_PALETTE = [RUHI_COLORS[c] for c in ["teal", "sky", "magenta", "coral", "sunshine"]]
33
31
 
34
32
 
35
33
  def dedup(matrix, energies, tol):
@@ -86,9 +84,7 @@ def bridge_components(adjacency, matrix, weight=0.1):
86
84
  for b in range(a + 1, n_comp):
87
85
  ia = np.where(labels == a)[0]
88
86
  ib = np.where(labels == b)[0]
89
- d = np.linalg.norm(
90
- matrix[ia][:, None, :] - matrix[ib][None, :, :], axis=-1
91
- )
87
+ d = np.linalg.norm(matrix[ia][:, None, :] - matrix[ib][None, :, :], axis=-1)
92
88
  i, j = np.unravel_index(int(np.argmin(d)), d.shape)
93
89
  adjacency[ia[i], ib[j]] = weight
94
90
  adjacency[ib[j], ia[i]] = weight
@@ -119,9 +115,7 @@ def spectral_basins(adjacency, n_basins, rng, matrix=None):
119
115
  first = int(rng.integers(len(x)))
120
116
  centers = [x[first]]
121
117
  while len(centers) < k:
122
- d = np.min(
123
- [np.linalg.norm(x - c, axis=1) for c in centers], axis=0
124
- )
118
+ d = np.min([np.linalg.norm(x - c, axis=1) for c in centers], axis=0)
125
119
  centers.append(x[int(np.argmax(d))])
126
120
  centers = np.array(centers)
127
121
  for _ in range(60):
@@ -147,7 +141,7 @@ class Node:
147
141
  ```
148
142
  """
149
143
 
150
- __slots__ = ("level", "children", "leaf", "x", "emin", "argmin")
144
+ __slots__ = ("argmin", "children", "emin", "leaf", "level", "x")
151
145
 
152
146
  def __init__(self, level, children=None, leaf=None):
153
147
  self.level = level
@@ -177,9 +171,7 @@ def merge_tree(adjacency, energies, levels):
177
171
  if not mask.any():
178
172
  continue
179
173
  sub = np.where(mask)[0]
180
- _, labels = connected_components(
181
- adjacency[sub][:, sub], directed=False
182
- )
174
+ _, labels = connected_components(adjacency[sub][:, sub], directed=False)
183
175
  comps = {}
184
176
  for local, g in enumerate(sub):
185
177
  comps.setdefault(labels[local], []).append(g)
@@ -196,9 +188,7 @@ def merge_tree(adjacency, energies, levels):
196
188
  if not merged:
197
189
  node = leaves[extra[0]]
198
190
  for m in extra[1:]:
199
- node = Node(
200
- max(energies[m], node.level), children=[node, leaves[m]]
201
- )
191
+ node = Node(max(energies[m], node.level), children=[node, leaves[m]])
202
192
  else:
203
193
  node = merged[0] if len(merged) == 1 else Node(lev, children=merged)
204
194
  for m in extra:
@@ -28,7 +28,7 @@ import matplotlib as mpl
28
28
  import matplotlib.pyplot as plt
29
29
  import numpy as np
30
30
 
31
- from chemparseplot.plot.theme import RUHI_COLORS, RUHI_THEME
31
+ from chemparseplot.plot.theme import RUHI_THEME
32
32
 
33
33
  ARM_MARKERS = ["o", "^", "s", "P", "X"]
34
34
 
@@ -170,9 +170,16 @@ def run_sketchmap(
170
170
  for line in lm_raw.splitlines()
171
171
  if line.strip() and not line.startswith("#")
172
172
  ]
173
- idx = np.array([int(r[0]) for r in rows])
173
+ idx = [int(r[0]) for r in rows]
174
+ # Guarantee the pinned rows: dimlandmark's -ifirst only seeds the greedy
175
+ # walk, and farthest-point sampling skips anything near an already-kept
176
+ # point, which is exactly where a pinned reference's twin sits.
177
+ selected = [i for i in idx if i >= n_pinned]
178
+ keep = list(range(n_pinned)) + selected
179
+ keep = keep[: max(n_landmark, n_pinned)]
180
+ idx = np.array(keep)
174
181
  (workdir / "landmarks.dat").write_text(
175
- "\n".join(" ".join(r[1:]) for r in rows) + "\n"
182
+ "\n".join(" ".join(f"{v:.8f}" for v in matrix[i]) for i in keep) + "\n"
176
183
  )
177
184
  proj = subprocess.run( # noqa: S603
178
185
  [
@@ -232,14 +239,12 @@ def plot_landscape_surface(ax, coords, energies, cmap=None, levels=20):
232
239
  np.linspace(coords[:, 0].min(), coords[:, 0].max(), 240),
233
240
  np.linspace(coords[:, 1].min(), coords[:, 1].max(), 240),
234
241
  )
235
- surface = griddata(
236
- coords, np.clip(energies, vmin, vmax), (gx, gy), method="linear"
237
- )
242
+ surface = griddata(coords, np.clip(energies, vmin, vmax), (gx, gy), method="linear")
238
243
  lv = np.linspace(vmin, vmax, levels)
239
- cf = ax.contourf(gx, gy, surface, levels=lv, cmap=cmap, vmin=vmin, vmax=vmax,
240
- alpha=0.85)
241
- ax.contour(gx, gy, surface, levels=lv, colors="black", linewidths=0.3,
242
- alpha=0.45)
244
+ cf = ax.contourf(
245
+ gx, gy, surface, levels=lv, cmap=cmap, vmin=vmin, vmax=vmax, alpha=0.85
246
+ )
247
+ ax.contour(gx, gy, surface, levels=lv, colors="black", linewidths=0.3, alpha=0.45)
243
248
  ax.set_facecolor(RUHI_THEME.gridcolor)
244
249
  return cf
245
250
 
@@ -250,8 +255,14 @@ def margin_inset_slots(n):
250
255
  ```{versionadded} 1.10.0
251
256
  ```
252
257
  """
253
- slots = [(-0.26, 0.82), (-0.26, 0.22), (1.30, 0.82), (1.30, 0.22),
254
- (-0.26, 0.52), (1.30, 0.52)]
258
+ slots = [
259
+ (-0.26, 0.82),
260
+ (-0.26, 0.22),
261
+ (1.30, 0.82),
262
+ (1.30, 0.22),
263
+ (-0.26, 0.52),
264
+ (1.30, 0.52),
265
+ ]
255
266
  return slots[:n]
256
267
 
257
268
 
@@ -296,8 +307,16 @@ def place_margin_insets(fig, ax, entries, zoom=0.14, renderer="xyzrender"):
296
307
  renderer=renderer,
297
308
  xyzrender_config="paton",
298
309
  )
299
- ax.scatter([x], [y], marker="o", s=120, facecolors="none",
300
- edgecolors="black", linewidths=1.4, zorder=7)
310
+ ax.scatter(
311
+ [x],
312
+ [y],
313
+ marker="o",
314
+ s=120,
315
+ facecolors="none",
316
+ edgecolors="black",
317
+ linewidths=1.4,
318
+ zorder=7,
319
+ )
301
320
  if entry.get("label"):
302
321
  ax.annotate(
303
322
  entry["label"],
@@ -560,6 +560,7 @@ def plot_structure_strip(
560
560
  max_display_height_px: float | None = None,
561
561
  width_fill_fraction=0.82,
562
562
  prefer_single_row: bool = True,
563
+ label_ax=None,
563
564
  ) -> Any:
564
565
  """Renders a horizontal gallery of atomic structures.
565
566
 
@@ -580,6 +581,10 @@ def plot_structure_strip(
580
581
  If True (default), pack up to 16 structures in one row. If False,
581
582
  honour ``max_cols`` so e.g. 12 images with ``max_cols=6`` become two
582
583
  rows of larger molecules.
584
+ label_ax : matplotlib.axes.Axes or None
585
+ Optional caption-only axis. When supplied, structure images use the
586
+ full height of ``ax`` and labels are placed in ``label_ax`` at matching
587
+ normalized column and row positions.
583
588
 
584
589
  ```{versionadded} 0.1.0
585
590
  ```
@@ -601,6 +606,8 @@ def plot_structure_strip(
601
606
  labels = []
602
607
 
603
608
  ax.axis("off")
609
+ if label_ax is not None:
610
+ label_ax.axis("off")
604
611
  n_plot = len(atoms_list)
605
612
  # Single-row packing is optional: landscape strips with many images often
606
613
  # want max_cols=6 → two rows of larger structures.
@@ -623,13 +630,26 @@ def plot_structure_strip(
623
630
  ax.set_xlim(0, ax_w_px)
624
631
  ax.set_ylim(0, ax_h_px)
625
632
 
633
+ label_w_px = 0
634
+ label_h_px = 0
635
+ if label_ax is not None:
636
+ label_bbox = label_ax.get_window_extent(mpl_renderer)
637
+ label_w_px = max(1, int(round(label_bbox.width)))
638
+ label_h_px = max(1, int(round(label_bbox.height)))
639
+ label_ax.set_xlim(0, label_w_px)
640
+ label_ax.set_ylim(0, label_h_px)
641
+
626
642
  per_col_px = ax_w_px / max(n_cols, 1)
627
643
  # The band must hold the tallest label: multiline labels previously
628
644
  # overflowed a fixed 1.8-line reservation into the structure images.
629
- max_label_lines = max(
630
- (str(lbl).count("\n") + 1 for lbl in labels), default=1
631
- ) if labels else 1
632
- label_band_px = label_fontsize * fig.dpi / 72 * (0.9 + 1.35 * max_label_lines)
645
+ max_label_lines = (
646
+ max((str(lbl).count("\n") + 1 for lbl in labels), default=1) if labels else 1
647
+ )
648
+ label_band_px = (
649
+ 0.0
650
+ if label_ax is not None
651
+ else label_fontsize * fig.dpi / 72 * (0.9 + 1.35 * max_label_lines)
652
+ )
633
653
  top_padding_px = 6.0
634
654
  bottom_padding_px = 6.0
635
655
  row_gap_px = 10.0
@@ -689,16 +709,25 @@ def plot_structure_strip(
689
709
  _alpha_blit_rgba(canvas, resized, x0, y0)
690
710
 
691
711
  if labels and i < len(labels):
692
- # Label under *this* cell (not a shared strip baseline). Multi-row
693
- # layouts previously wrote every label at y=4, so column mates from
694
- # different rows stacked on top of each other.
695
- cell_label_y = max(2.0, slot_bottom_y + 3.0)
696
- ax.text(
697
- x=slot_center_x,
698
- y=cell_label_y,
712
+ text_ax = label_ax if label_ax is not None else ax
713
+ if label_ax is not None:
714
+ label_slot_width = label_w_px / max(n_cols, 1)
715
+ label_row_height = label_h_px / max(n_rows, 1)
716
+ label_x = (col + 0.5) * label_slot_width
717
+ label_y = label_h_px - (row + 0.5) * label_row_height
718
+ label_va = "center"
719
+ else:
720
+ # Each row owns its caption baseline so column mates cannot
721
+ # occupy the same text position.
722
+ label_x = slot_center_x
723
+ label_y = max(2.0, slot_bottom_y + 3.0)
724
+ label_va = "bottom"
725
+ text_ax.text(
726
+ x=label_x,
727
+ y=label_y,
699
728
  s=labels[i],
700
729
  ha="center",
701
- va="bottom",
730
+ va=label_va,
702
731
  fontsize=label_fontsize,
703
732
  color=theme_color,
704
733
  fontweight="bold",
@@ -211,6 +211,7 @@ ban-relative-imports = "all"
211
211
  "PLW2901",
212
212
  ]
213
213
  "chemparseplot/plot/geomscan.py" = ["PLC0415"]
214
+ "chemparseplot/plot/landscape.py" = ["PLC0415"] # optional plotting deps stay lazy
214
215
  "chemparseplot/units.py" = [
215
216
  "F822", # lazy Q_/ureg via __getattr__
216
217
  "EM102",
@@ -48,9 +48,7 @@ def test_cn_matrix_species_restriction():
48
48
  pos = np.zeros((4, 3))
49
49
  pos[:, 0] = [0.0, 1.0, 5.0, 6.0]
50
50
  full = cn_matrix([pos.ravel()], ["O", "H", "O", "H"], cutoff=1.5)
51
- only_o = cn_matrix(
52
- [pos.ravel()], ["O", "H", "O", "H"], cutoff=1.5, cn_species="O"
53
- )
51
+ only_o = cn_matrix([pos.ravel()], ["O", "H", "O", "H"], cutoff=1.5, cn_species="O")
54
52
  assert full.shape[1] == 4
55
53
  assert only_o.shape[1] == 2
56
54
 
@@ -106,9 +104,7 @@ def test_merge_tree_deep_chain_no_recursion():
106
104
 
107
105
  def test_spectral_basins_shapes():
108
106
  rng = np.random.default_rng(7)
109
- m = np.vstack(
110
- [rng.normal(0, 0.1, size=(10, 2)), rng.normal(5, 0.1, size=(10, 2))]
111
- )
107
+ m = np.vstack([rng.normal(0, 0.1, size=(10, 2)), rng.normal(5, 0.1, size=(10, 2))])
112
108
  adjacency = knn_graph(m, k=3)
113
109
  assign = spectral_basins(adjacency, 2, rng, matrix=m)
114
110
  assert assign.shape == (20,)
@@ -119,12 +115,8 @@ def test_spectral_basins_shapes():
119
115
 
120
116
  def test_plot_disconnectivity_end_to_end():
121
117
  rng = np.random.default_rng(9)
122
- m = np.vstack(
123
- [rng.normal(0, 0.2, size=(15, 3)), rng.normal(4, 0.2, size=(15, 3))]
124
- )
125
- es = np.concatenate(
126
- [rng.uniform(-10, -8, size=15), rng.uniform(-9, -7, size=15)]
127
- )
118
+ m = np.vstack([rng.normal(0, 0.2, size=(15, 3)), rng.normal(4, 0.2, size=(15, 3))])
119
+ es = np.concatenate([rng.uniform(-10, -8, size=15), rng.uniform(-9, -7, size=15)])
128
120
  fig, ax = plt.subplots()
129
121
  reps, root = plot_disconnectivity(ax, m, es, dedup_tol=0.01, knn=3)
130
122
  assert len(reps) <= 30
@@ -197,6 +197,43 @@ class TestPlotStructureStrip:
197
197
  assert len(ax.artists) == 0
198
198
  plt.close(fig)
199
199
 
200
+ def test_separate_label_axis_keeps_text_out_of_image_band(
201
+ self, monkeypatch, small_molecules
202
+ ):
203
+ fig = plt.figure(figsize=(6, 2.4))
204
+ grid = fig.add_gridspec(2, 1, height_ratios=(4, 1), hspace=0.08)
205
+ image_ax = fig.add_subplot(grid[0])
206
+ label_ax = fig.add_subplot(grid[1])
207
+
208
+ def _fake_render(*_args, **_kwargs):
209
+ img = np.zeros((60, 80, 4), dtype=np.float32)
210
+ img[10:50, 20:60, :3] = 1.0
211
+ img[10:50, 20:60, 3] = 1.0
212
+ return img
213
+
214
+ labels = ["Cage\n0.00 eV", "Prism\n0.10 eV", "Book\n0.20 eV"]
215
+ monkeypatch.setattr("chemparseplot.plot.neb._render_atoms", _fake_render)
216
+ plot_structure_strip(
217
+ image_ax,
218
+ small_molecules,
219
+ labels,
220
+ label_ax=label_ax,
221
+ )
222
+
223
+ fig.canvas.draw()
224
+ mpl_renderer = fig.canvas.get_renderer()
225
+ image_bbox = image_ax.get_window_extent(mpl_renderer)
226
+ label_bbox = label_ax.get_window_extent(mpl_renderer)
227
+
228
+ assert len(image_ax.texts) == 0
229
+ assert [text.get_text() for text in label_ax.texts] == labels
230
+ for text in label_ax.texts:
231
+ text_bbox = text.get_window_extent(mpl_renderer)
232
+ assert label_bbox.contains(text_bbox.x0, text_bbox.y0)
233
+ assert label_bbox.contains(text_bbox.x1, text_bbox.y1)
234
+ assert text_bbox.y1 <= image_bbox.y0
235
+ plt.close(fig)
236
+
200
237
 
201
238
  class TestPlotStructureInset:
202
239
  def test_basic_inset(self, h2o):
File without changes
File without changes