quantui 0.5.2__tar.gz → 0.6.1__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 (71) hide show
  1. {quantui-0.5.2 → quantui-0.6.1}/CHANGELOG.md +98 -1
  2. {quantui-0.5.2/quantui.egg-info → quantui-0.6.1}/PKG-INFO +1 -1
  3. {quantui-0.5.2 → quantui-0.6.1}/pyproject.toml +1 -1
  4. {quantui-0.5.2 → quantui-0.6.1}/quantui/__init__.py +1 -1
  5. {quantui-0.5.2 → quantui-0.6.1}/quantui/app.py +122 -4
  6. {quantui-0.5.2 → quantui-0.6.1}/quantui/app_analysis.py +104 -0
  7. {quantui-0.5.2 → quantui-0.6.1}/quantui/app_builders.py +231 -1
  8. {quantui-0.5.2 → quantui-0.6.1}/quantui/app_exports.py +124 -0
  9. {quantui-0.5.2 → quantui-0.6.1}/quantui/app_formatters.py +260 -23
  10. {quantui-0.5.2 → quantui-0.6.1}/quantui/app_runflow.py +16 -0
  11. {quantui-0.5.2 → quantui-0.6.1}/quantui/app_visualization.py +579 -106
  12. {quantui-0.5.2 → quantui-0.6.1}/quantui/orbital_visualization.py +456 -47
  13. {quantui-0.5.2 → quantui-0.6.1}/quantui/reorganization_energy.py +93 -0
  14. {quantui-0.5.2 → quantui-0.6.1}/quantui/results_storage.py +67 -0
  15. {quantui-0.5.2 → quantui-0.6.1}/quantui/user_settings.py +20 -0
  16. {quantui-0.5.2 → quantui-0.6.1}/quantui/viz_assets.py +21 -6
  17. {quantui-0.5.2 → quantui-0.6.1}/quantui/viz_backend_router.py +34 -8
  18. {quantui-0.5.2 → quantui-0.6.1/quantui.egg-info}/PKG-INFO +1 -1
  19. {quantui-0.5.2 → quantui-0.6.1}/LICENSE +0 -0
  20. {quantui-0.5.2 → quantui-0.6.1}/MANIFEST.in +0 -0
  21. {quantui-0.5.2 → quantui-0.6.1}/README.md +0 -0
  22. {quantui-0.5.2 → quantui-0.6.1}/SECURITY.md +0 -0
  23. {quantui-0.5.2 → quantui-0.6.1}/quantui/analytics.py +0 -0
  24. {quantui-0.5.2 → quantui-0.6.1}/quantui/app_history.py +0 -0
  25. {quantui-0.5.2 → quantui-0.6.1}/quantui/ase_bridge.py +0 -0
  26. {quantui-0.5.2 → quantui-0.6.1}/quantui/benchmarks.py +0 -0
  27. {quantui-0.5.2 → quantui-0.6.1}/quantui/c_stderr.py +0 -0
  28. {quantui-0.5.2 → quantui-0.6.1}/quantui/cactus.py +0 -0
  29. {quantui-0.5.2 → quantui-0.6.1}/quantui/calc_log.py +0 -0
  30. {quantui-0.5.2 → quantui-0.6.1}/quantui/calculator.py +0 -0
  31. {quantui-0.5.2 → quantui-0.6.1}/quantui/cancellation.py +0 -0
  32. {quantui-0.5.2 → quantui-0.6.1}/quantui/cli.py +0 -0
  33. {quantui-0.5.2 → quantui-0.6.1}/quantui/comparison.py +0 -0
  34. {quantui-0.5.2 → quantui-0.6.1}/quantui/config.py +0 -0
  35. {quantui-0.5.2 → quantui-0.6.1}/quantui/data/js/3Dmol-min.js +0 -0
  36. {quantui-0.5.2 → quantui-0.6.1}/quantui/data/js/3Dmol-min.js.LICENSE.txt +0 -0
  37. {quantui-0.5.2 → quantui-0.6.1}/quantui/data/library/library.sqlite +0 -0
  38. {quantui-0.5.2 → quantui-0.6.1}/quantui/data/manifests/bulk_qm9.json +0 -0
  39. {quantui-0.5.2 → quantui-0.6.1}/quantui/data/manifests/curated.json +0 -0
  40. {quantui-0.5.2 → quantui-0.6.1}/quantui/data/manifests/presets.json +0 -0
  41. {quantui-0.5.2 → quantui-0.6.1}/quantui/descriptor_cards.py +0 -0
  42. {quantui-0.5.2 → quantui-0.6.1}/quantui/freq_calc.py +0 -0
  43. {quantui-0.5.2 → quantui-0.6.1}/quantui/freq_ir_workers.py +0 -0
  44. {quantui-0.5.2 → quantui-0.6.1}/quantui/gpu_offload.py +0 -0
  45. {quantui-0.5.2 → quantui-0.6.1}/quantui/help_content.py +0 -0
  46. {quantui-0.5.2 → quantui-0.6.1}/quantui/ir_plot.py +0 -0
  47. {quantui-0.5.2 → quantui-0.6.1}/quantui/issue_tracker.py +0 -0
  48. {quantui-0.5.2 → quantui-0.6.1}/quantui/live_log.py +0 -0
  49. {quantui-0.5.2 → quantui-0.6.1}/quantui/log_utils.py +0 -0
  50. {quantui-0.5.2 → quantui-0.6.1}/quantui/molecule.py +0 -0
  51. {quantui-0.5.2 → quantui-0.6.1}/quantui/molecule_library.py +0 -0
  52. {quantui-0.5.2 → quantui-0.6.1}/quantui/nmr_calc.py +0 -0
  53. {quantui-0.5.2 → quantui-0.6.1}/quantui/optimizer.py +0 -0
  54. {quantui-0.5.2 → quantui-0.6.1}/quantui/pes_scan.py +0 -0
  55. {quantui-0.5.2 → quantui-0.6.1}/quantui/preopt.py +0 -0
  56. {quantui-0.5.2 → quantui-0.6.1}/quantui/progress.py +0 -0
  57. {quantui-0.5.2 → quantui-0.6.1}/quantui/pubchem.py +0 -0
  58. {quantui-0.5.2 → quantui-0.6.1}/quantui/security.py +0 -0
  59. {quantui-0.5.2 → quantui-0.6.1}/quantui/session_calc.py +0 -0
  60. {quantui-0.5.2 → quantui-0.6.1}/quantui/structure_providers.py +0 -0
  61. {quantui-0.5.2 → quantui-0.6.1}/quantui/tddft_calc.py +0 -0
  62. {quantui-0.5.2 → quantui-0.6.1}/quantui/theme.py +0 -0
  63. {quantui-0.5.2 → quantui-0.6.1}/quantui/utils.py +0 -0
  64. {quantui-0.5.2 → quantui-0.6.1}/quantui/vib_cache.py +0 -0
  65. {quantui-0.5.2 → quantui-0.6.1}/quantui/visualization_py3dmol.py +0 -0
  66. {quantui-0.5.2 → quantui-0.6.1}/quantui.egg-info/SOURCES.txt +0 -0
  67. {quantui-0.5.2 → quantui-0.6.1}/quantui.egg-info/dependency_links.txt +0 -0
  68. {quantui-0.5.2 → quantui-0.6.1}/quantui.egg-info/entry_points.txt +0 -0
  69. {quantui-0.5.2 → quantui-0.6.1}/quantui.egg-info/requires.txt +0 -0
  70. {quantui-0.5.2 → quantui-0.6.1}/quantui.egg-info/top_level.txt +0 -0
  71. {quantui-0.5.2 → quantui-0.6.1}/setup.cfg +0 -0
@@ -7,6 +7,101 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.1] - 2026-08-05
11
+
12
+ ### Fixed
13
+
14
+ - **Reorganization-energy results now survive History.** Reloading one from
15
+ History showed the calculation's headline numbers as missing — λ, the
16
+ four-point energies and the per-channel breakdown were never written to disk,
17
+ so there was nothing to redisplay. Both the live and History result cards now
18
+ render from the same saved data, so anything visible during a run is visible
19
+ after reloading it.
20
+ **Results saved before this cannot be recovered** — λ requires two geometry
21
+ optimizations and four SCF energies. Those results now say so on the card and
22
+ tell you to re-run, rather than appearing silently incomplete.
23
+ - **3-D backgrounds follow the theme** in the new geometry viewers, matching the
24
+ fix applied to the other viewers in 0.6.0.
25
+
26
+ ### Added
27
+
28
+ - **A Geometries panel** for reorganization energy, on the Analysis tab — which
29
+ previously showed nothing at all for this calculation type. Step through the
30
+ distinct geometries behind the four energies, or overlay two of them with
31
+ arrows marking how each atom moved. The arrows can be scaled ×1–×10 for small
32
+ relaxations; the structures always show true computed positions.
33
+ - **Geometry relaxation on the result card** — RMSD and the largest single-atom
34
+ shift between the neutral and ion geometries, which is what λ physically
35
+ measures.
36
+ - **Compare λ across saved results.** Selecting several reorganization-energy
37
+ runs in the Compare tab now shows a λ table with a column per channel, for
38
+ screening candidates by how much they reorganize.
39
+
40
+ ### Changed
41
+
42
+ - The NCShare GPU diagnostic notebook now reports the **CPU affinity mask**
43
+ alongside the core count, and states plainly when CPU timings cannot be
44
+ trusted. A job can be granted 6 cores while still *seeing* 192; whether that
45
+ matters depends on how the scheduler constrains it, and the core count alone
46
+ cannot distinguish the two cases.
47
+ - `apptainer/README.md` records the first verified H200 run — driver, compute
48
+ capability, partition, and the measured CPU/GPU crossover.
49
+
50
+ ## [0.6.0] - 2026-08-04
51
+
52
+ ### Added
53
+
54
+ - **Save orbital isosurfaces as images.** A **Save PNG** button sits under the
55
+ viewer and captures exactly what you are looking at, camera and all. Options
56
+ for the filename, the DPI written into the file (72–600; this sets the
57
+ *print* size, not the pixel count), and a transparent background for dropping
58
+ a figure onto a slide — the transparency applies only to the saved file, so
59
+ the viewer on screen is unchanged.
60
+ - **Tunable isosurface resolution** — Coarse/Medium/Fine/Very fine grids,
61
+ remembered between launches.
62
+ - **Live isosurface controls**: isovalue, opacity and a colour-scheme dropdown
63
+ (GaussView orange/blue, Avogadro/Jmol red/blue, journal yellow/blue, and
64
+ more). All three update the existing viewer without recomputing anything, so
65
+ the orientation you rotated into is preserved.
66
+ - **The isovalue is explained in chemical terms.** Beside the slider, QuantUI
67
+ reports what fraction of the orbital's density the surface actually encloses
68
+ — "encloses 98.4% of the density" — because an amplitude threshold is rarely
69
+ the question anyone has.
70
+ - **A Cancel button** for isosurface generation. The underlying PySCF call
71
+ cannot be interrupted, so the computation finishes in the background, but its
72
+ result is discarded and the controls come back immediately.
73
+ - **The molecule is shown before you generate anything**, so the panel is never
74
+ empty — and you can orient the structure first, since that view carries into
75
+ the isosurface.
76
+
77
+ ### Changed
78
+
79
+ - **Orbital isosurfaces are py3Dmol only.** The Plotly renderer is no longer
80
+ selectable for them: it downsamples the volume by construction, and it cannot
81
+ carry the new image export. Plotly remains available for the molecule viewers.
82
+ - **Exported animations now match what you saw.** Vibrational modes and
83
+ optimization trajectories both export from py3Dmol — the renderer that drew
84
+ them on screen — instead of being rebuilt in Plotly. Exported files are also
85
+ now complete HTML documents declaring UTF-8, so labels no longer risk
86
+ mojibake when opened from disk.
87
+ - **For geometry optimizations, the Orbital Isosurface panel opens by default**
88
+ on the Analysis tab.
89
+ - **Isosurface viewers are framed** like the other 3-D viewers, and generating a
90
+ new one dims the current one in place rather than collapsing the panel.
91
+
92
+ ### Fixed
93
+
94
+ - **3-D backgrounds follow the theme.** The isosurface, vibrational and
95
+ trajectory viewers, and the Analysis-tab molecule viewer, kept their old
96
+ background when switching Light/Dark until something happened to redraw them.
97
+ - **The page no longer jumps** when generating an isosurface.
98
+ - **Isosurface changes now apply correctly.** Adjusting the isovalue, opacity or
99
+ colours previously layered a new surface on top of the old one, so raising the
100
+ isovalue appeared to do nothing, lowering opacity did nothing, and switching
101
+ palettes made the surface progressively brighter.
102
+ - Very large orbital grids are dramatically lighter in the browser — the cube
103
+ data was being embedded three times per render.
104
+
10
105
  ## [0.5.2] - 2026-08-03
11
106
 
12
107
  ### Fixed
@@ -508,7 +603,9 @@ Initial public scaffolding of the QuantUI package: `quantui` package with
508
603
  `calculator.py`, basic notebook launcher, Apptainer container definition,
509
604
  MIT license, and project metadata.
510
605
 
511
- [Unreleased]: https://github.com/The-Schultz-Lab/QuantUI/compare/v0.5.2...HEAD
606
+ [Unreleased]: https://github.com/The-Schultz-Lab/QuantUI/compare/v0.6.1...HEAD
607
+ [0.6.1]: https://github.com/The-Schultz-Lab/QuantUI/compare/v0.6.0...v0.6.1
608
+ [0.6.0]: https://github.com/The-Schultz-Lab/QuantUI/compare/v0.5.2...v0.6.0
512
609
  [0.5.2]: https://github.com/The-Schultz-Lab/QuantUI/compare/v0.5.1...v0.5.2
513
610
  [0.5.1]: https://github.com/The-Schultz-Lab/QuantUI/compare/v0.5.0...v0.5.1
514
611
  [0.5.0]: https://github.com/The-Schultz-Lab/QuantUI/compare/v0.4.1...v0.5.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quantui
3
- Version: 0.5.2
3
+ Version: 0.6.1
4
4
  Summary: An open-source frontend for DFT and post-HF quantum chemistry with PySCF
5
5
  Author-email: Jonathan Schultz <nccu-schultz-lab@users.noreply.github.com>
6
6
  License: MIT License
@@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta"
8
8
 
9
9
  [project]
10
10
  name = "quantui"
11
- version = "0.5.2"
11
+ version = "0.6.1"
12
12
  description = "An open-source frontend for DFT and post-HF quantum chemistry with PySCF"
13
13
  readme = "README.md"
14
14
  requires-python = ">=3.9"
@@ -7,7 +7,7 @@ Calculations run locally in the Jupyter session — no cluster or SLURM required
7
7
  PySCF requires Linux/macOS/WSL. Windows users should use the Apptainer container.
8
8
  """
9
9
 
10
- __version__ = "0.5.2"
10
+ __version__ = "0.6.1"
11
11
 
12
12
  import logging
13
13
  from typing import Any
@@ -45,6 +45,9 @@ from quantui.app_analysis import (
45
45
  from quantui.app_analysis import (
46
46
  deactivate_all_ana_panels as _ana_deactivate_all_ana_panels,
47
47
  )
48
+ from quantui.app_analysis import (
49
+ on_reorg_view_changed as _ana_on_reorg_view_changed,
50
+ )
48
51
  from quantui.app_analysis import (
49
52
  pop_energies as _ana_pop_energies,
50
53
  )
@@ -69,6 +72,9 @@ from quantui.app_analysis import (
69
72
  from quantui.app_analysis import (
70
73
  pop_preopt_trajectory as _ana_pop_preopt_trajectory,
71
74
  )
75
+ from quantui.app_analysis import (
76
+ pop_reorg_geometries as _ana_pop_reorg_geometries,
77
+ )
72
78
  from quantui.app_analysis import (
73
79
  pop_uv_vis as _ana_pop_uv_vis,
74
80
  )
@@ -147,6 +153,9 @@ from quantui.app_exports import (
147
153
  from quantui.app_exports import (
148
154
  on_iso_export_cube as _exp_on_iso_export_cube,
149
155
  )
156
+ from quantui.app_exports import (
157
+ on_orb_png_captured as _exp_on_orb_png_captured,
158
+ )
150
159
  from quantui.app_formatters import (
151
160
  format_freq_result as _fmt_freq_result,
152
161
  )
@@ -333,6 +342,12 @@ from quantui.app_visualization import (
333
342
  from quantui.app_visualization import (
334
343
  on_ir_mode_changed as _viz_on_ir_mode_changed,
335
344
  )
345
+ from quantui.app_visualization import (
346
+ on_iso_appearance_changed as _viz_on_iso_appearance_changed,
347
+ )
348
+ from quantui.app_visualization import (
349
+ on_iso_cancel as _viz_on_iso_cancel,
350
+ )
336
351
  from quantui.app_visualization import (
337
352
  on_iso_generate as _viz_on_iso_generate,
338
353
  )
@@ -360,6 +375,9 @@ from quantui.app_visualization import (
360
375
  from quantui.app_visualization import (
361
376
  render_vib_mode as _viz_render_vib_mode,
362
377
  )
378
+ from quantui.app_visualization import (
379
+ rerender_3d_scenes_for_theme as _viz_rerender_3d_scenes_for_theme,
380
+ )
363
381
  from quantui.app_visualization import (
364
382
  show_ir_spectrum as _viz_show_ir_spectrum,
365
383
  )
@@ -1618,6 +1636,7 @@ class QuantUIApp:
1618
1636
  ("IR Spectrum", "_ir_accordion", "Frequency"),
1619
1637
  ("PES Scan", "_pes_scan_accordion", "PES Scan"),
1620
1638
  ("Isosurface", "_iso_accordion", "Single Point (Linux/WSL only)"),
1639
+ ("Geometries", "_reorg_geom_accordion", "Reorganization Energy"),
1621
1640
  ("UV-Vis", "_tddft_accordion", "UV-Vis (TD-DFT)"),
1622
1641
  ("NMR", "_nmr_accordion", "NMR Shielding"),
1623
1642
  ]
@@ -1628,9 +1647,31 @@ class QuantUIApp:
1628
1647
  ("Isosurface", "_pop_isosurface", False),
1629
1648
  ],
1630
1649
  "geometry_opt": [
1631
- ("Trajectory", "_pop_geo_trajectory", True),
1650
+ # ORDER MATTERS: the FIRST entry whose populator returns True and
1651
+ # carries auto_select=True becomes the default panel (see the rules
1652
+ # above). Isosurface therefore leads (requested 2026-08-04); an
1653
+ # earlier attempt put it last, which did nothing because Trajectory
1654
+ # had already claimed the selection.
1655
+ #
1656
+ # Trajectory keeps auto_select=True as the fallback: when a result
1657
+ # has no orbital data, _pop_isosurface returns False, Isosurface
1658
+ # never activates, and Trajectory becomes the default instead.
1659
+ # ORDER IS LOAD-BEARING TWICE OVER.
1660
+ #
1661
+ # 1. Execution: _pop_energies calls show_orbital_diagram, which is
1662
+ # what populates _last_orb_mo_coeff / _mol_atom / _mol_basis —
1663
+ # the very state _pop_isosurface checks. Energies MUST run
1664
+ # first, or Isosurface reports "required data is missing" on a
1665
+ # result that has it. (Putting Isosurface first did exactly
1666
+ # that, 2026-08-04.)
1667
+ # 2. Selection: the FIRST entry with auto_select=True that returns
1668
+ # True becomes the default panel. Energies is False, so
1669
+ # Isosurface is the first candidate and opens by default —
1670
+ # which is the request — while Trajectory keeps True as the
1671
+ # fallback for results with no orbital data.
1632
1672
  ("Energies", "_pop_energies", False),
1633
- ("Isosurface", "_pop_isosurface", False),
1673
+ ("Isosurface", "_pop_isosurface", True),
1674
+ ("Trajectory", "_pop_geo_trajectory", True),
1634
1675
  ],
1635
1676
  "frequency": [
1636
1677
  ("Vibrational", "_pop_vibrational", True),
@@ -1644,6 +1685,16 @@ class QuantUIApp:
1644
1685
  "nmr": [
1645
1686
  ("NMR", "_pop_nmr_shielding", True),
1646
1687
  ],
1688
+ # reorganization_energy had NO entry at all until 2026-08-05, so the
1689
+ # Analysis tab populated nothing for these runs — not a missing panel,
1690
+ # no panels. Order matters twice over: the FIRST auto_select=True that
1691
+ # returns True wins, AND _pop_energies loads the orbital state
1692
+ # _pop_isosurface checks, so Energies must precede Isosurface.
1693
+ "reorganization_energy": [
1694
+ ("Energies", "_pop_energies", False),
1695
+ ("Geometries", "_pop_reorg_geometries", True),
1696
+ ("Isosurface", "_pop_isosurface", False),
1697
+ ],
1647
1698
  "pes_scan": [
1648
1699
  ("PES Scan", "_pop_pes_plot", True),
1649
1700
  ("Trajectory", "_pop_pes_trajectory", False),
@@ -1659,6 +1710,9 @@ class QuantUIApp:
1659
1710
  def _pop_energies(self, ctx: _AnalysisContext) -> bool:
1660
1711
  return _ana_pop_energies(self, ctx)
1661
1712
 
1713
+ def _pop_reorg_geometries(self, ctx: _AnalysisContext) -> bool:
1714
+ return _ana_pop_reorg_geometries(self, ctx)
1715
+
1662
1716
  def _pop_isosurface(self, ctx: _AnalysisContext) -> bool:
1663
1717
  return _ana_pop_isosurface(self, ctx)
1664
1718
 
@@ -1979,6 +2033,35 @@ class QuantUIApp:
1979
2033
  )
1980
2034
  # Cube + bundle exports
1981
2035
  self._iso_export_cube_btn.on_click(self._on_iso_export_cube)
2036
+ self._iso_cancel_btn.on_click(self._safe_cb(self._on_iso_cancel))
2037
+ # Reorg geometry views (REORG.3): both redraw from data already in
2038
+ # memory, so they respond directly rather than behind an Apply button.
2039
+ for _w in (
2040
+ self._reorg_view_toggle,
2041
+ self._reorg_overlay_pair,
2042
+ self._reorg_exaggerate,
2043
+ ):
2044
+ _w.observe(self._safe_cb(self._on_reorg_view_changed), names="value")
2045
+ # PNG capture arrives from the browser, so there is no button to bind
2046
+ # here — the viewer's own Save-PNG button posts into this Textarea and
2047
+ # ipywidgets syncs it back, firing this observer (ORBX.1).
2048
+ self._orb_png_inbox.observe(
2049
+ self._safe_cb(self._on_orb_png_captured), names="value"
2050
+ )
2051
+ # Persist the grid choice so it survives a relaunch (ORBX.2).
2052
+ self._iso_resolution_dd.observe(
2053
+ self._safe_cb(self._on_iso_resolution_changed), names="value"
2054
+ )
2055
+ # Appearance controls redraw from the cube on disk — no cubegen — so
2056
+ # they can respond directly rather than behind an Apply button.
2057
+ for _w in (
2058
+ self._iso_isovalue_slider,
2059
+ self._iso_opacity_slider,
2060
+ self._iso_colors_dd,
2061
+ # NOT _iso_png_transparent: it is an export-only option, applied at
2062
+ # capture time. Observing it here would change the live viewer.
2063
+ ):
2064
+ _w.observe(self._safe_cb(self._on_iso_appearance_changed), names="value")
1982
2065
  self._export_bundle_btn.on_click(self._on_export_bundle)
1983
2066
 
1984
2067
  # ── Files tab ────────────────────────────────────────────────────────
@@ -2746,8 +2829,18 @@ class QuantUIApp:
2746
2829
  _last_pes = getattr(self, "_last_pes_result", None)
2747
2830
  if _last_pes is not None:
2748
2831
  self._show_pes_scan_result(_last_pes)
2749
- # Re-render 3D molecule viewer so scene_bgcolor updates immediately.
2750
- self._refresh_calc_mol_viewer()
2832
+ # Re-render BOTH 3D molecule viewers so scene_bgcolor updates
2833
+ # immediately. This used to call _refresh_calc_mol_viewer directly,
2834
+ # which covered the Calculate tab only — the Analysis-tab viewer kept
2835
+ # its old background until something else happened to redraw it.
2836
+ # _rerender_3d_views already handled both; it just was not reached from
2837
+ # here. Reported 2026-08-04.
2838
+ self._rerender_3d_views()
2839
+ # ...and the isosurface / vibrational viewers, which bake the same
2840
+ # colour into their generated HTML. Without this they keep the old
2841
+ # background until the user regenerates — reported 2026-08-04. Both
2842
+ # re-render from cached inputs; neither re-runs a calculation.
2843
+ _viz_rerender_3d_scenes_for_theme(self)
2751
2844
 
2752
2845
  def _initialize_viz_state_from_preference(self) -> None:
2753
2846
  """Align _viz_backend and the three preference widgets with the
@@ -3313,6 +3406,31 @@ class QuantUIApp:
3313
3406
  def _on_export_pdb(self, btn) -> None:
3314
3407
  _exp_on_export_pdb(self, btn)
3315
3408
 
3409
+ def _on_reorg_view_changed(self, change) -> None:
3410
+ _ana_on_reorg_view_changed(self, change)
3411
+
3412
+ def _on_iso_cancel(self, btn) -> None:
3413
+ _viz_on_iso_cancel(self, btn)
3414
+
3415
+ def _on_iso_appearance_changed(self, change) -> None:
3416
+ _viz_on_iso_appearance_changed(self, change)
3417
+
3418
+ def _on_orb_png_captured(self, change) -> None:
3419
+ _exp_on_orb_png_captured(self, change)
3420
+
3421
+ def _on_iso_resolution_changed(self, change) -> None:
3422
+ """Persist the isosurface grid choice.
3423
+
3424
+ Saved on change rather than at generate time so the preference sticks
3425
+ even if the user picks a grid and then closes the app without running
3426
+ anything.
3427
+ """
3428
+ new_val = (change or {}).get("new")
3429
+ if not new_val or new_val == self._user_settings.viz.iso_resolution:
3430
+ return
3431
+ self._user_settings.viz.iso_resolution = str(new_val)
3432
+ self._user_settings.save()
3433
+
3316
3434
  def _on_iso_export_cube(self, btn) -> None:
3317
3435
  _exp_on_iso_export_cube(self, btn)
3318
3436
 
@@ -253,6 +253,110 @@ def pop_energies(app: Any, ctx: Any) -> bool:
253
253
  return bool(app._show_orbital_diagram(result))
254
254
 
255
255
 
256
+ def pop_reorg_geometries(app: Any, ctx: Any) -> bool:
257
+ """Populate the Geometries panel for a reorganization-energy result.
258
+
259
+ Works from a live result OR a saved one by reading the same channel payload
260
+ both now carry — the shape REORG.1 introduced. That is deliberate: the
261
+ original bug was two paths reading different things, so this one never had
262
+ the chance to grow a second reader.
263
+
264
+ Returns False when the payload is absent, which is exactly the pre-REORG.1
265
+ case; the panel then stays unavailable rather than showing an empty viewer,
266
+ and the results card explains why.
267
+ """
268
+ channels, neutral = _reorg_payload(app, ctx)
269
+ if not channels or not neutral:
270
+ _set_panel_unavailable_message(
271
+ app,
272
+ "Geometries",
273
+ (
274
+ "Not available for this result: the per-channel geometries were "
275
+ "not saved. Re-run the calculation to enable the comparison."
276
+ ),
277
+ )
278
+ return False
279
+
280
+ from quantui.reorganization_energy import reorg_geometries
281
+
282
+ geoms = reorg_geometries(channels, neutral)
283
+ if len(geoms) < 2:
284
+ return False
285
+ app._reorg_geometries = geoms
286
+ app._reorg_overlay_pair.options = [
287
+ (f"{geoms[0]['label'].split(' — ')[0]} vs {g['label'].split(' — ')[0]}", i)
288
+ for i in range(1, len(geoms))
289
+ for g in [geoms[i]]
290
+ ]
291
+ if app._reorg_overlay_pair.options:
292
+ app._reorg_overlay_pair.value = app._reorg_overlay_pair.options[0][1]
293
+ render_reorg_geometries(app)
294
+ return True
295
+
296
+
297
+ def _reorg_payload(app: Any, ctx: Any) -> tuple[list, dict]:
298
+ """Channel list + neutral geometry, from a live result or a saved one."""
299
+ live = getattr(ctx, "live_result", None)
300
+ if live is not None and getattr(live, "channels", None):
301
+ from quantui.results_storage import _reorg_channels_payload
302
+
303
+ payload = _reorg_channels_payload(live) or []
304
+ neutral = payload[0].get("neutral_geometry") if payload else None
305
+ return payload, neutral or {}
306
+
307
+ result_dir = getattr(ctx, "result_dir", None)
308
+ if result_dir is not None:
309
+ try:
310
+ from quantui import load_result
311
+
312
+ data = load_result(result_dir)
313
+ payload = data.get("reorg_channels") or []
314
+ neutral = payload[0].get("neutral_geometry") if payload else None
315
+ return payload, neutral or {}
316
+ except Exception: # noqa: BLE001 — a missing panel, never a crash
317
+ return [], {}
318
+ return [], {}
319
+
320
+
321
+ def render_reorg_geometries(app: Any) -> None:
322
+ """Draw the current view (stepper or overlay) into the Geometries panel."""
323
+ geoms = getattr(app, "_reorg_geometries", None)
324
+ if not geoms:
325
+ return
326
+ from quantui.app_visualization import (
327
+ build_reorg_geometry_viewer_html,
328
+ build_reorg_overlay_html,
329
+ )
330
+
331
+ bg = app._plotly_theme_colors()["scene_bgcolor"]
332
+ try:
333
+ if app._reorg_view_toggle.value == "overlay":
334
+ idx = int(app._reorg_overlay_pair.value or 1)
335
+ html = build_reorg_overlay_html(
336
+ geoms[0],
337
+ geoms[idx],
338
+ bgcolor=bg,
339
+ exaggerate=float(getattr(app._reorg_exaggerate, "value", 1.0)),
340
+ )
341
+ else:
342
+ html = build_reorg_geometry_viewer_html(geoms, bgcolor=bg)
343
+ app._set_html_output(app._reorg_geom_output, html)
344
+ except Exception as exc: # noqa: BLE001
345
+ app._set_html_output(
346
+ app._reorg_geom_output,
347
+ f'<p style="color:#b91c1c;padding:8px">Geometry view failed: {exc}</p>',
348
+ )
349
+
350
+
351
+ def on_reorg_view_changed(app: Any, change: Any = None) -> None:
352
+ """Toggle between stepper and overlay; the pair picker only applies to one."""
353
+ is_overlay = app._reorg_view_toggle.value == "overlay"
354
+ app._reorg_overlay_pair.layout.display = "" if is_overlay else "none"
355
+ # Only meaningful for the overlay — the stepper shows true positions.
356
+ app._reorg_exaggerate.layout.display = "" if is_overlay else "none"
357
+ render_reorg_geometries(app)
358
+
359
+
256
360
  def pop_isosurface(app: Any, ctx: Any) -> bool:
257
361
  """Populate Isosurface availability from orbital state.
258
362