quantui 0.5.1__tar.gz → 0.6.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 (71) hide show
  1. {quantui-0.5.1 → quantui-0.6.0}/CHANGELOG.md +82 -1
  2. {quantui-0.5.1/quantui.egg-info → quantui-0.6.0}/PKG-INFO +2 -2
  3. {quantui-0.5.1 → quantui-0.6.0}/README.md +1 -1
  4. {quantui-0.5.1 → quantui-0.6.0}/pyproject.toml +1 -1
  5. {quantui-0.5.1 → quantui-0.6.0}/quantui/__init__.py +1 -1
  6. {quantui-0.5.1 → quantui-0.6.0}/quantui/app.py +120 -40
  7. {quantui-0.5.1 → quantui-0.6.0}/quantui/app_builders.py +209 -14
  8. {quantui-0.5.1 → quantui-0.6.0}/quantui/app_exports.py +124 -0
  9. {quantui-0.5.1 → quantui-0.6.0}/quantui/app_formatters.py +3 -1
  10. {quantui-0.5.1 → quantui-0.6.0}/quantui/app_visualization.py +412 -122
  11. {quantui-0.5.1 → quantui-0.6.0}/quantui/live_log.py +3 -1
  12. {quantui-0.5.1 → quantui-0.6.0}/quantui/orbital_visualization.py +456 -47
  13. quantui-0.6.0/quantui/theme.py +125 -0
  14. {quantui-0.5.1 → quantui-0.6.0}/quantui/user_settings.py +20 -0
  15. {quantui-0.5.1 → quantui-0.6.0}/quantui/visualization_py3dmol.py +6 -1
  16. {quantui-0.5.1 → quantui-0.6.0}/quantui/viz_assets.py +21 -6
  17. {quantui-0.5.1 → quantui-0.6.0}/quantui/viz_backend_router.py +34 -8
  18. {quantui-0.5.1 → quantui-0.6.0/quantui.egg-info}/PKG-INFO +2 -2
  19. {quantui-0.5.1 → quantui-0.6.0}/quantui.egg-info/SOURCES.txt +1 -0
  20. {quantui-0.5.1 → quantui-0.6.0}/LICENSE +0 -0
  21. {quantui-0.5.1 → quantui-0.6.0}/MANIFEST.in +0 -0
  22. {quantui-0.5.1 → quantui-0.6.0}/SECURITY.md +0 -0
  23. {quantui-0.5.1 → quantui-0.6.0}/quantui/analytics.py +0 -0
  24. {quantui-0.5.1 → quantui-0.6.0}/quantui/app_analysis.py +0 -0
  25. {quantui-0.5.1 → quantui-0.6.0}/quantui/app_history.py +0 -0
  26. {quantui-0.5.1 → quantui-0.6.0}/quantui/app_runflow.py +0 -0
  27. {quantui-0.5.1 → quantui-0.6.0}/quantui/ase_bridge.py +0 -0
  28. {quantui-0.5.1 → quantui-0.6.0}/quantui/benchmarks.py +0 -0
  29. {quantui-0.5.1 → quantui-0.6.0}/quantui/c_stderr.py +0 -0
  30. {quantui-0.5.1 → quantui-0.6.0}/quantui/cactus.py +0 -0
  31. {quantui-0.5.1 → quantui-0.6.0}/quantui/calc_log.py +0 -0
  32. {quantui-0.5.1 → quantui-0.6.0}/quantui/calculator.py +0 -0
  33. {quantui-0.5.1 → quantui-0.6.0}/quantui/cancellation.py +0 -0
  34. {quantui-0.5.1 → quantui-0.6.0}/quantui/cli.py +0 -0
  35. {quantui-0.5.1 → quantui-0.6.0}/quantui/comparison.py +0 -0
  36. {quantui-0.5.1 → quantui-0.6.0}/quantui/config.py +0 -0
  37. {quantui-0.5.1 → quantui-0.6.0}/quantui/data/js/3Dmol-min.js +0 -0
  38. {quantui-0.5.1 → quantui-0.6.0}/quantui/data/js/3Dmol-min.js.LICENSE.txt +0 -0
  39. {quantui-0.5.1 → quantui-0.6.0}/quantui/data/library/library.sqlite +0 -0
  40. {quantui-0.5.1 → quantui-0.6.0}/quantui/data/manifests/bulk_qm9.json +0 -0
  41. {quantui-0.5.1 → quantui-0.6.0}/quantui/data/manifests/curated.json +0 -0
  42. {quantui-0.5.1 → quantui-0.6.0}/quantui/data/manifests/presets.json +0 -0
  43. {quantui-0.5.1 → quantui-0.6.0}/quantui/descriptor_cards.py +0 -0
  44. {quantui-0.5.1 → quantui-0.6.0}/quantui/freq_calc.py +0 -0
  45. {quantui-0.5.1 → quantui-0.6.0}/quantui/freq_ir_workers.py +0 -0
  46. {quantui-0.5.1 → quantui-0.6.0}/quantui/gpu_offload.py +0 -0
  47. {quantui-0.5.1 → quantui-0.6.0}/quantui/help_content.py +0 -0
  48. {quantui-0.5.1 → quantui-0.6.0}/quantui/ir_plot.py +0 -0
  49. {quantui-0.5.1 → quantui-0.6.0}/quantui/issue_tracker.py +0 -0
  50. {quantui-0.5.1 → quantui-0.6.0}/quantui/log_utils.py +0 -0
  51. {quantui-0.5.1 → quantui-0.6.0}/quantui/molecule.py +0 -0
  52. {quantui-0.5.1 → quantui-0.6.0}/quantui/molecule_library.py +0 -0
  53. {quantui-0.5.1 → quantui-0.6.0}/quantui/nmr_calc.py +0 -0
  54. {quantui-0.5.1 → quantui-0.6.0}/quantui/optimizer.py +0 -0
  55. {quantui-0.5.1 → quantui-0.6.0}/quantui/pes_scan.py +0 -0
  56. {quantui-0.5.1 → quantui-0.6.0}/quantui/preopt.py +0 -0
  57. {quantui-0.5.1 → quantui-0.6.0}/quantui/progress.py +0 -0
  58. {quantui-0.5.1 → quantui-0.6.0}/quantui/pubchem.py +0 -0
  59. {quantui-0.5.1 → quantui-0.6.0}/quantui/reorganization_energy.py +0 -0
  60. {quantui-0.5.1 → quantui-0.6.0}/quantui/results_storage.py +0 -0
  61. {quantui-0.5.1 → quantui-0.6.0}/quantui/security.py +0 -0
  62. {quantui-0.5.1 → quantui-0.6.0}/quantui/session_calc.py +0 -0
  63. {quantui-0.5.1 → quantui-0.6.0}/quantui/structure_providers.py +0 -0
  64. {quantui-0.5.1 → quantui-0.6.0}/quantui/tddft_calc.py +0 -0
  65. {quantui-0.5.1 → quantui-0.6.0}/quantui/utils.py +0 -0
  66. {quantui-0.5.1 → quantui-0.6.0}/quantui/vib_cache.py +0 -0
  67. {quantui-0.5.1 → quantui-0.6.0}/quantui.egg-info/dependency_links.txt +0 -0
  68. {quantui-0.5.1 → quantui-0.6.0}/quantui.egg-info/entry_points.txt +0 -0
  69. {quantui-0.5.1 → quantui-0.6.0}/quantui.egg-info/requires.txt +0 -0
  70. {quantui-0.5.1 → quantui-0.6.0}/quantui.egg-info/top_level.txt +0 -0
  71. {quantui-0.5.1 → quantui-0.6.0}/setup.cfg +0 -0
@@ -7,6 +7,85 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.0] - 2026-08-04
11
+
12
+ ### Added
13
+
14
+ - **Save orbital isosurfaces as images.** A **Save PNG** button sits under the
15
+ viewer and captures exactly what you are looking at, camera and all. Options
16
+ for the filename, the DPI written into the file (72–600; this sets the
17
+ *print* size, not the pixel count), and a transparent background for dropping
18
+ a figure onto a slide — the transparency applies only to the saved file, so
19
+ the viewer on screen is unchanged.
20
+ - **Tunable isosurface resolution** — Coarse/Medium/Fine/Very fine grids,
21
+ remembered between launches.
22
+ - **Live isosurface controls**: isovalue, opacity and a colour-scheme dropdown
23
+ (GaussView orange/blue, Avogadro/Jmol red/blue, journal yellow/blue, and
24
+ more). All three update the existing viewer without recomputing anything, so
25
+ the orientation you rotated into is preserved.
26
+ - **The isovalue is explained in chemical terms.** Beside the slider, QuantUI
27
+ reports what fraction of the orbital's density the surface actually encloses
28
+ — "encloses 98.4% of the density" — because an amplitude threshold is rarely
29
+ the question anyone has.
30
+ - **A Cancel button** for isosurface generation. The underlying PySCF call
31
+ cannot be interrupted, so the computation finishes in the background, but its
32
+ result is discarded and the controls come back immediately.
33
+ - **The molecule is shown before you generate anything**, so the panel is never
34
+ empty — and you can orient the structure first, since that view carries into
35
+ the isosurface.
36
+
37
+ ### Changed
38
+
39
+ - **Orbital isosurfaces are py3Dmol only.** The Plotly renderer is no longer
40
+ selectable for them: it downsamples the volume by construction, and it cannot
41
+ carry the new image export. Plotly remains available for the molecule viewers.
42
+ - **Exported animations now match what you saw.** Vibrational modes and
43
+ optimization trajectories both export from py3Dmol — the renderer that drew
44
+ them on screen — instead of being rebuilt in Plotly. Exported files are also
45
+ now complete HTML documents declaring UTF-8, so labels no longer risk
46
+ mojibake when opened from disk.
47
+ - **For geometry optimizations, the Orbital Isosurface panel opens by default**
48
+ on the Analysis tab.
49
+ - **Isosurface viewers are framed** like the other 3-D viewers, and generating a
50
+ new one dims the current one in place rather than collapsing the panel.
51
+
52
+ ### Fixed
53
+
54
+ - **3-D backgrounds follow the theme.** The isosurface, vibrational and
55
+ trajectory viewers, and the Analysis-tab molecule viewer, kept their old
56
+ background when switching Light/Dark until something happened to redraw them.
57
+ - **The page no longer jumps** when generating an isosurface.
58
+ - **Isosurface changes now apply correctly.** Adjusting the isovalue, opacity or
59
+ colours previously layered a new surface on top of the old one, so raising the
60
+ isovalue appeared to do nothing, lowering opacity did nothing, and switching
61
+ palettes made the surface progressively brighter.
62
+ - Very large orbital grids are dramatically lighter in the browser — the cube
63
+ data was being embedded three times per render.
64
+
65
+ ## [0.5.2] - 2026-08-03
66
+
67
+ ### Fixed
68
+
69
+ - **Dark mode now has visible panel edges.** Borders, cards, and dividers were
70
+ effectively invisible in Dark mode — measured at 1.14:1 against the panel
71
+ background, well under the 3:1 that WCAG asks of a UI component. Dark mode is
72
+ a whole-page colour inversion rather than a separate palette, so a light grey
73
+ border inverts to a near-black one on a near-black page and disappears. The
74
+ borders are now a mid-tone that stays visible in *both* modes (3.20:1 light,
75
+ 4.30:1 dark). Text contrast was measured too and was never the problem — it
76
+ is 7.24:1 in light mode and slightly *better* inverted — so text colours are
77
+ deliberately unchanged.
78
+
79
+ ### Added
80
+
81
+ - **Every 3-D viewer now has a border around it**, so it is clear where the
82
+ view ends and where the page can be scrolled without dragging the structure.
83
+ This covers the molecule preview, the results and analysis structure views,
84
+ the geometry-optimization trajectory, the vibrational-mode animation, and the
85
+ classical pre-optimization preview. The frame hugs the plot itself rather
86
+ than spanning the window width, and looks the same whichever rendering
87
+ backend (py3Dmol or plotlymol3d) is active.
88
+
10
89
  ## [0.5.1] - 2026-07-31
11
90
 
12
91
  ### Fixed
@@ -484,7 +563,9 @@ Initial public scaffolding of the QuantUI package: `quantui` package with
484
563
  `calculator.py`, basic notebook launcher, Apptainer container definition,
485
564
  MIT license, and project metadata.
486
565
 
487
- [Unreleased]: https://github.com/The-Schultz-Lab/QuantUI/compare/v0.5.1...HEAD
566
+ [Unreleased]: https://github.com/The-Schultz-Lab/QuantUI/compare/v0.6.0...HEAD
567
+ [0.6.0]: https://github.com/The-Schultz-Lab/QuantUI/compare/v0.5.2...v0.6.0
568
+ [0.5.2]: https://github.com/The-Schultz-Lab/QuantUI/compare/v0.5.1...v0.5.2
488
569
  [0.5.1]: https://github.com/The-Schultz-Lab/QuantUI/compare/v0.5.0...v0.5.1
489
570
  [0.5.0]: https://github.com/The-Schultz-Lab/QuantUI/compare/v0.4.1...v0.5.0
490
571
  [0.4.1]: https://github.com/The-Schultz-Lab/QuantUI/compare/v0.4.0...v0.4.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quantui
3
- Version: 0.5.1
3
+ Version: 0.6.0
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
@@ -91,7 +91,7 @@ Dynamic: license-file
91
91
 
92
92
  # QuantUI
93
93
 
94
- [![PyPI](https://img.shields.io/pypi/v/quantui)](https://pypi.org/project/quantui/)
94
+ [![PyPI](https://img.shields.io/pypi/v/quantui?v=2)](https://pypi.org/project/quantui/)
95
95
  [![CI](https://github.com/The-Schultz-Lab/QuantUI/actions/workflows/ci.yml/badge.svg)](https://github.com/The-Schultz-Lab/QuantUI/actions/workflows/ci.yml)
96
96
  [![Docs](https://img.shields.io/badge/docs-GitHub%20Pages-blue)](https://the-schultz-lab.github.io/QuantUI/)
97
97
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/The-Schultz-Lab/QuantUI/blob/main/LICENSE)
@@ -1,6 +1,6 @@
1
1
  # QuantUI
2
2
 
3
- [![PyPI](https://img.shields.io/pypi/v/quantui)](https://pypi.org/project/quantui/)
3
+ [![PyPI](https://img.shields.io/pypi/v/quantui?v=2)](https://pypi.org/project/quantui/)
4
4
  [![CI](https://github.com/The-Schultz-Lab/QuantUI/actions/workflows/ci.yml/badge.svg)](https://github.com/The-Schultz-Lab/QuantUI/actions/workflows/ci.yml)
5
5
  [![Docs](https://img.shields.io/badge/docs-GitHub%20Pages-blue)](https://the-schultz-lab.github.io/QuantUI/)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/The-Schultz-Lab/QuantUI/blob/main/LICENSE)
@@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta"
8
8
 
9
9
  [project]
10
10
  name = "quantui"
11
- version = "0.5.1"
11
+ version = "0.6.0"
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.1"
10
+ __version__ = "0.6.0"
11
11
 
12
12
  import logging
13
13
  from typing import Any
@@ -32,6 +32,7 @@ import quantui
32
32
  import quantui.calc_log as _calc_log
33
33
  import quantui.issue_tracker as _issue_tracker
34
34
  from quantui import molecule_library as _ml
35
+ from quantui import theme as _theme
35
36
  from quantui.app_analysis import (
36
37
  activate_ana_panel as _ana_activate_ana_panel,
37
38
  )
@@ -146,6 +147,9 @@ from quantui.app_exports import (
146
147
  from quantui.app_exports import (
147
148
  on_iso_export_cube as _exp_on_iso_export_cube,
148
149
  )
150
+ from quantui.app_exports import (
151
+ on_orb_png_captured as _exp_on_orb_png_captured,
152
+ )
149
153
  from quantui.app_formatters import (
150
154
  format_freq_result as _fmt_freq_result,
151
155
  )
@@ -332,6 +336,12 @@ from quantui.app_visualization import (
332
336
  from quantui.app_visualization import (
333
337
  on_ir_mode_changed as _viz_on_ir_mode_changed,
334
338
  )
339
+ from quantui.app_visualization import (
340
+ on_iso_appearance_changed as _viz_on_iso_appearance_changed,
341
+ )
342
+ from quantui.app_visualization import (
343
+ on_iso_cancel as _viz_on_iso_cancel,
344
+ )
335
345
  from quantui.app_visualization import (
336
346
  on_iso_generate as _viz_on_iso_generate,
337
347
  )
@@ -359,6 +369,9 @@ from quantui.app_visualization import (
359
369
  from quantui.app_visualization import (
360
370
  render_vib_mode as _viz_render_vib_mode,
361
371
  )
372
+ from quantui.app_visualization import (
373
+ rerender_3d_scenes_for_theme as _viz_rerender_3d_scenes_for_theme,
374
+ )
362
375
  from quantui.app_visualization import (
363
376
  show_ir_spectrum as _viz_show_ir_spectrum,
364
377
  )
@@ -450,9 +463,6 @@ try:
450
463
  from quantui.visualization_py3dmol import (
451
464
  VIZ_STYLE_OPTIONS as _VIZ_STYLE_OPTIONS,
452
465
  )
453
- from quantui.visualization_py3dmol import (
454
- display_molecule as _display_molecule,
455
- )
456
466
  from quantui.visualization_py3dmol import (
457
467
  render_molecule_html as _render_molecule_html,
458
468
  )
@@ -460,7 +470,6 @@ try:
460
470
  VISUALIZATION_AVAILABLE = True
461
471
  except ImportError:
462
472
  VISUALIZATION_AVAILABLE = False
463
- _display_molecule = None # type: ignore[assignment]
464
473
  _render_molecule_html = None # type: ignore[assignment]
465
474
  _PLOTLYMOL_VIZ = False
466
475
  _PY3DMOL_VIZ = False
@@ -616,7 +625,7 @@ h3 {
616
625
  color: #64748b !important;
617
626
  margin: 24px 0 10px !important;
618
627
  padding-bottom: 5px !important;
619
- border-bottom: 1px solid #e2e8f0 !important;
628
+ border-bottom: 1px solid __Q_BORDER__ !important;
620
629
  }
621
630
 
622
631
  /* Rounded corners on inputs, dropdowns, and buttons -------------------- */
@@ -632,21 +641,6 @@ h3 {
632
641
  background: transparent !important;
633
642
  }
634
643
 
635
- /* 3D molecule-viewer frames — a subtle bounding box so the viewer
636
- extent reads clearly. The light border inverts to dark automatically under
637
- the global dark-mode invert filter. */
638
- .quantui-viewer-frame {
639
- border: 1px solid #c0ccd8 !important;
640
- border-radius: 6px !important;
641
- overflow: hidden !important;
642
- }
643
- /* Collapse the frame to nothing when the viewer output is empty (e.g. before
644
- a calc runs) so no hollow box shows. Degrades to an always-on border where
645
- :has() is unsupported. */
646
- .quantui-viewer-frame:not(:has(.jp-OutputArea-child)) {
647
- border-color: transparent !important;
648
- }
649
-
650
644
  /* Inline "calculating" spinner — shown next to slow on-demand
651
645
  controls (e.g. orbital-isosurface generation) while work is in flight. */
652
646
  @keyframes quantui-spin { to { transform: rotate(360deg); } }
@@ -654,13 +648,20 @@ h3 {
654
648
  display: inline-block;
655
649
  width: 14px;
656
650
  height: 14px;
657
- border: 2px solid #c0ccd8;
651
+ border: 2px solid __Q_BORDER__;
658
652
  border-top-color: #2563eb;
659
653
  border-radius: 50%;
660
654
  animation: quantui-spin 0.7s linear infinite;
661
655
  vertical-align: middle;
662
656
  }
663
- </style>"""
657
+ </style>""".replace(
658
+ # Sentinel substitution rather than an f-string: this block is dense with
659
+ # CSS braces, every one of which would need doubling. If a second sentinel
660
+ # is ever added, substitute the LONGER name first — "__Q_BORDER__" matches
661
+ # inside "__Q_BORDER_STRONG__" and would leave a dangling "_STRONG__".
662
+ "__Q_BORDER__",
663
+ _theme.BORDER,
664
+ )
664
665
 
665
666
  _LAYOUT_TRAITS: frozenset[str] = frozenset(widgets.Layout.class_trait_names())
666
667
 
@@ -1639,9 +1640,31 @@ class QuantUIApp:
1639
1640
  ("Isosurface", "_pop_isosurface", False),
1640
1641
  ],
1641
1642
  "geometry_opt": [
1642
- ("Trajectory", "_pop_geo_trajectory", True),
1643
+ # ORDER MATTERS: the FIRST entry whose populator returns True and
1644
+ # carries auto_select=True becomes the default panel (see the rules
1645
+ # above). Isosurface therefore leads (requested 2026-08-04); an
1646
+ # earlier attempt put it last, which did nothing because Trajectory
1647
+ # had already claimed the selection.
1648
+ #
1649
+ # Trajectory keeps auto_select=True as the fallback: when a result
1650
+ # has no orbital data, _pop_isosurface returns False, Isosurface
1651
+ # never activates, and Trajectory becomes the default instead.
1652
+ # ORDER IS LOAD-BEARING TWICE OVER.
1653
+ #
1654
+ # 1. Execution: _pop_energies calls show_orbital_diagram, which is
1655
+ # what populates _last_orb_mo_coeff / _mol_atom / _mol_basis —
1656
+ # the very state _pop_isosurface checks. Energies MUST run
1657
+ # first, or Isosurface reports "required data is missing" on a
1658
+ # result that has it. (Putting Isosurface first did exactly
1659
+ # that, 2026-08-04.)
1660
+ # 2. Selection: the FIRST entry with auto_select=True that returns
1661
+ # True becomes the default panel. Energies is False, so
1662
+ # Isosurface is the first candidate and opens by default —
1663
+ # which is the request — while Trajectory keeps True as the
1664
+ # fallback for results with no orbital data.
1643
1665
  ("Energies", "_pop_energies", False),
1644
- ("Isosurface", "_pop_isosurface", False),
1666
+ ("Isosurface", "_pop_isosurface", True),
1667
+ ("Trajectory", "_pop_geo_trajectory", True),
1645
1668
  ],
1646
1669
  "frequency": [
1647
1670
  ("Vibrational", "_pop_vibrational", True),
@@ -1990,6 +2013,27 @@ class QuantUIApp:
1990
2013
  )
1991
2014
  # Cube + bundle exports
1992
2015
  self._iso_export_cube_btn.on_click(self._on_iso_export_cube)
2016
+ self._iso_cancel_btn.on_click(self._safe_cb(self._on_iso_cancel))
2017
+ # PNG capture arrives from the browser, so there is no button to bind
2018
+ # here — the viewer's own Save-PNG button posts into this Textarea and
2019
+ # ipywidgets syncs it back, firing this observer (ORBX.1).
2020
+ self._orb_png_inbox.observe(
2021
+ self._safe_cb(self._on_orb_png_captured), names="value"
2022
+ )
2023
+ # Persist the grid choice so it survives a relaunch (ORBX.2).
2024
+ self._iso_resolution_dd.observe(
2025
+ self._safe_cb(self._on_iso_resolution_changed), names="value"
2026
+ )
2027
+ # Appearance controls redraw from the cube on disk — no cubegen — so
2028
+ # they can respond directly rather than behind an Apply button.
2029
+ for _w in (
2030
+ self._iso_isovalue_slider,
2031
+ self._iso_opacity_slider,
2032
+ self._iso_colors_dd,
2033
+ # NOT _iso_png_transparent: it is an export-only option, applied at
2034
+ # capture time. Observing it here would change the live viewer.
2035
+ ):
2036
+ _w.observe(self._safe_cb(self._on_iso_appearance_changed), names="value")
1993
2037
  self._export_bundle_btn.on_click(self._on_export_bundle)
1994
2038
 
1995
2039
  # ── Files tab ────────────────────────────────────────────────────────
@@ -2383,7 +2427,7 @@ class QuantUIApp:
2383
2427
  body = rows[1:]
2384
2428
  head_html = "".join(
2385
2429
  f'<th style="padding:4px 10px;text-align:left;'
2386
- f"border-bottom:1px solid #cbd5e1;font-size:12px;"
2430
+ f"border-bottom:1px solid {_theme.BORDER};font-size:12px;"
2387
2431
  f'color:#1e293b">{_html.escape(str(c))}</th>'
2388
2432
  for c in header
2389
2433
  )
@@ -2427,7 +2471,7 @@ class QuantUIApp:
2427
2471
  # reach the parent app.
2428
2472
  iframe_html = (
2429
2473
  '<iframe sandbox="allow-scripts" '
2430
- 'style="width:100%;height:400px;border:1px solid #cbd5e1;'
2474
+ f'style="width:100%;height:400px;border:1px solid {_theme.BORDER};'
2431
2475
  'border-radius:4px" '
2432
2476
  f'srcdoc="{_html.escape(raw, quote=True)}"></iframe>'
2433
2477
  )
@@ -2757,8 +2801,18 @@ class QuantUIApp:
2757
2801
  _last_pes = getattr(self, "_last_pes_result", None)
2758
2802
  if _last_pes is not None:
2759
2803
  self._show_pes_scan_result(_last_pes)
2760
- # Re-render 3D molecule viewer so scene_bgcolor updates immediately.
2761
- self._refresh_calc_mol_viewer()
2804
+ # Re-render BOTH 3D molecule viewers so scene_bgcolor updates
2805
+ # immediately. This used to call _refresh_calc_mol_viewer directly,
2806
+ # which covered the Calculate tab only — the Analysis-tab viewer kept
2807
+ # its old background until something else happened to redraw it.
2808
+ # _rerender_3d_views already handled both; it just was not reached from
2809
+ # here. Reported 2026-08-04.
2810
+ self._rerender_3d_views()
2811
+ # ...and the isosurface / vibrational viewers, which bake the same
2812
+ # colour into their generated HTML. Without this they keep the old
2813
+ # background until the user regenerates — reported 2026-08-04. Both
2814
+ # re-render from cached inputs; neither re-runs a calculation.
2815
+ _viz_rerender_3d_scenes_for_theme(self)
2762
2816
 
2763
2817
  def _initialize_viz_state_from_preference(self) -> None:
2764
2818
  """Align _viz_backend and the three preference widgets with the
@@ -2871,7 +2925,7 @@ class QuantUIApp:
2871
2925
  def _rerender_3d_views(self) -> None:
2872
2926
  """Re-render visible 3D molecule viewers using the router to pick a
2873
2927
  backend per task. Updates the "Rendering with: X" label widgets."""
2874
- if _display_molecule is None:
2928
+ if _render_molecule_html is None:
2875
2929
  return
2876
2930
 
2877
2931
  # Calculate-tab molecule preview (MOLECULE_PREVIEW task).
@@ -2884,15 +2938,19 @@ class QuantUIApp:
2884
2938
  if self._analysis_displayed_molecule is not None:
2885
2939
  chosen = self._resolve_backend(VizTask.ANALYSIS_STRUCTURE_VIEW)
2886
2940
  if chosen is not None:
2887
- self._analysis_mol_output.clear_output()
2888
- with self._analysis_mol_output:
2889
- _display_molecule(
2890
- self._analysis_displayed_molecule,
2891
- backend=str(chosen),
2892
- style=self._viz_style,
2893
- lighting=self._viz_lighting,
2894
- bgcolor=self._plotly_theme_colors()["scene_bgcolor"],
2895
- )
2941
+ # Same render path as the first draw (app_visualization.
2942
+ # _show_result_3d), not display(): the fragment it returns
2943
+ # carries the viewer's own border. Going through display()
2944
+ # here would silently drop that border the moment a user
2945
+ # toggled backends on the Analysis tab.
2946
+ html = _render_molecule_html(
2947
+ self._analysis_displayed_molecule,
2948
+ backend=str(chosen),
2949
+ style=self._viz_style,
2950
+ lighting=self._viz_lighting,
2951
+ bgcolor=self._plotly_theme_colors()["scene_bgcolor"],
2952
+ )
2953
+ self._set_html_output(self._analysis_mol_output, html)
2896
2954
  self._update_analysis_backend_label(chosen)
2897
2955
 
2898
2956
  def _update_analysis_backend_label(self, chosen: VizBackend) -> None:
@@ -3320,6 +3378,28 @@ class QuantUIApp:
3320
3378
  def _on_export_pdb(self, btn) -> None:
3321
3379
  _exp_on_export_pdb(self, btn)
3322
3380
 
3381
+ def _on_iso_cancel(self, btn) -> None:
3382
+ _viz_on_iso_cancel(self, btn)
3383
+
3384
+ def _on_iso_appearance_changed(self, change) -> None:
3385
+ _viz_on_iso_appearance_changed(self, change)
3386
+
3387
+ def _on_orb_png_captured(self, change) -> None:
3388
+ _exp_on_orb_png_captured(self, change)
3389
+
3390
+ def _on_iso_resolution_changed(self, change) -> None:
3391
+ """Persist the isosurface grid choice.
3392
+
3393
+ Saved on change rather than at generate time so the preference sticks
3394
+ even if the user picks a grid and then closes the app without running
3395
+ anything.
3396
+ """
3397
+ new_val = (change or {}).get("new")
3398
+ if not new_val or new_val == self._user_settings.viz.iso_resolution:
3399
+ return
3400
+ self._user_settings.viz.iso_resolution = str(new_val)
3401
+ self._user_settings.save()
3402
+
3323
3403
  def _on_iso_export_cube(self, btn) -> None:
3324
3404
  _exp_on_iso_export_cube(self, btn)
3325
3405
 
@@ -5514,7 +5594,7 @@ class QuantUIApp:
5514
5594
  rows.append(f'<div style="{style}">{esc}</div>')
5515
5595
  self._log_output_html.value = (
5516
5596
  '<div style="font-family:monospace;font-size:12px;line-height:1.4;'
5517
- "padding:8px 10px;background:#f8fafc;border:1px solid #e2e8f0;"
5597
+ f"padding:8px 10px;background:#f8fafc;border:1px solid {_theme.BORDER};"
5518
5598
  'border-radius:4px;overflow-x:auto;max-height:550px;overflow-y:auto">'
5519
5599
  + "".join(rows)
5520
5600
  + "</div>"
@@ -5530,7 +5610,7 @@ class QuantUIApp:
5530
5610
  if key and key in HELP_TOPICS:
5531
5611
  entry = HELP_TOPICS[key]
5532
5612
  self.help_content_html.value = (
5533
- f'<div style="border:1px solid #e2e8f0;border-radius:6px;'
5613
+ f'<div style="border:1px solid {_theme.BORDER};border-radius:6px;'
5534
5614
  f'padding:14px 18px;margin:8px 0;background:#f8fafc;max-width:700px">'
5535
5615
  f'<h4 style="margin:0 0 10px;color:#1e293b;font-size:15px;font-weight:700">'
5536
5616
  f'{entry["title"]}</h4>'