Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
images and JupyterHub profiles can omit it (default off); instructors enable
cluster testing with `QUANTUI_ENABLE_SLURM=1` without any extra student
command.
- **CPU teaching SIF defaults** — `quantui.def` sets `QUANTUI_FREQ_PARALLEL=1`
so NCShare classroom sessions parallelize IR-intensity displacements on
multi-core nodes. The Settings checkbox reflects the deployment value and is
locked while the variable is set; use `QUANTUI_FREQ_PARALLEL=0` to force off
site-wide.

### Fixed

Expand Down
7 changes: 7 additions & 0 deletions apptainer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ every layer (CUDA base image, all-pip install, a pinned PyPI release rather than
the working tree). Merging them would ship a multi-GB CUDA stack to students who
will never have a GPU. See [GPU image](#gpu-image) below.

**CPU image environment defaults** (set in `quantui.def` `%environment`):

| Variable | CPU `quantui.sif` | GPU `quantui-gpu.sif` |
| --- | --- | --- |
| `QUANTUI_FREQ_PARALLEL` | `1` — parallel CPU workers for IR-intensity displacements on frequency calcs (≥4 cores, ≥2 atoms). Overrides the Settings checkbox while set. | unset (off unless you set it) |
| `QUANTUI_ENABLE_SLURM` | unset (class stays in-kernel) | unset |

The compiled `.sif` image is **not** committed to git — it is too large (~4–5 GB).
Build it locally (see below) or download the latest release asset from the
[GitHub Releases page](https://github.com/The-Schultz-Lab/QuantUI/releases).
Expand Down
6 changes: 6 additions & 0 deletions apptainer/quantui.def
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ FROM: condaforge/miniforge3:26.1.1-3
# across kernel restarts. The home dir is bind-mounted by Apptainer.
export QUANTUI_RESULTS_DIR="${HOME}/.quantui/results"

# Parallel CPU workers for IR-intensity finite-difference SCFs (6N displacements)
# on multi-core hosts. Reference SCF + Hessian stay in-kernel. Students can still
# see the preference in Settings; when this variable is set it overrides the
# saved checkbox (set QUANTUI_FREQ_PARALLEL=0 to force off site-wide).
export QUANTUI_FREQ_PARALLEL=1

%files
# Source paths are relative to where you run `apptainer build` (repo root).
# Run the build command from the repo root, not from apptainer/.
Expand Down
15 changes: 13 additions & 2 deletions quantui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,10 @@
SUPPORTED_BASIS_SETS,
SUPPORTED_METHODS,
)
from quantui.freq_ir_workers import (
freq_parallel_env_configured,
freq_parallel_opt_in,
)
from quantui.help_content import HELP_TOPICS
from quantui.molecule import Molecule, parse_xyz_input
from quantui.progress import StepProgress
Expand Down Expand Up @@ -1964,7 +1968,12 @@ def _build_status_panel(self) -> None:
vib_framerate_fps=self._user_settings.viz.vib_framerate_fps,
gpu_enabled=self._user_settings.compute.gpu_enabled,
density_fit_enabled=self._user_settings.compute.density_fit,
freq_parallel_enabled=self._user_settings.compute.freq_parallel,
freq_parallel_enabled=(
freq_parallel_opt_in()
if freq_parallel_env_configured()
else self._user_settings.compute.freq_parallel
),
freq_parallel_env_locked=freq_parallel_env_configured(),
execution_backend=self._user_settings.compute.execution_backend,
slurm_available=is_slurm_available(),
)
Expand Down Expand Up @@ -3748,10 +3757,12 @@ def _on_density_fit_enabled_changed(self, change) -> None:
def _on_freq_parallel_enabled_changed(self, change) -> None:
"""Persist the parallel IR finite-difference preference.

The freq_calc driver reads this via :func:`freq_ir_workers._freq_parallel_opt_in`
The freq_calc driver reads this via :func:`freq_ir_workers.freq_parallel_opt_in`
on each run (unless ``QUANTUI_FREQ_PARALLEL`` overrides in the environment).
Refresh the time estimate because parallel divides the IR term in the model.
"""
if freq_parallel_env_configured():
return
new_val = bool(change["new"])
if new_val == self._user_settings.compute.freq_parallel:
return
Expand Down
20 changes: 18 additions & 2 deletions quantui/app_builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ def build_status_panel(
gpu_enabled: bool = True,
density_fit_enabled: bool = False,
freq_parallel_enabled: bool = False,
freq_parallel_env_locked: bool = False,
execution_backend: str = "local",
slurm_available: bool = False,
) -> None:
Expand Down Expand Up @@ -327,8 +328,17 @@ def _render_status(gpu_state: Any) -> str:
value=freq_parallel_enabled,
description="Parallelize IR intensity displacements (CPU)",
indent=False,
disabled=freq_parallel_env_locked,
layout=layout_fn(width="320px", margin="2px 0 0 0"),
)
if freq_parallel_env_locked:
fp_env_note = widgets.HTML(
f'<div style="font-size:11px;color:{_theme.css.TEXT_SUBTLE};margin:2px 0 0 0">'
"Controlled by deployment (<code>QUANTUI_FREQ_PARALLEL</code> in the "
"container or session environment).</div>"
)
else:
fp_env_note = None

exec_backend_label = widgets.HTML(
f'<div style="font-size:12px;color:{_theme.css.TEXT_SLATE_DARK};margin-top:12px;'
Expand Down Expand Up @@ -360,6 +370,13 @@ def _render_status(gpu_state: Any) -> str:
"Use the Cluster Jobs tab to monitor and cancel runs.</div>"
)

fp_settings_rows: list[Any] = [
fp_toggle_label,
app.freq_parallel_enabled_cb,
]
if fp_env_note is not None:
fp_settings_rows.append(fp_env_note)

settings_box = widgets.VBox(
[
settings_html,
Expand All @@ -370,8 +387,7 @@ def _render_status(gpu_state: Any) -> str:
app.gpu_enabled_cb,
df_toggle_label,
app.density_fit_enabled_cb,
fp_toggle_label,
app.freq_parallel_enabled_cb,
*fp_settings_rows,
exec_backend_label,
app.execution_backend_dd,
exec_backend_note,
Expand Down
10 changes: 10 additions & 0 deletions quantui/freq_ir_workers.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,16 @@ def run_displaced_scf(coords_bohr_flat) -> Any:
return np.array(mf.dip_moment(verbose=0))


def freq_parallel_opt_in() -> bool:
"""Return whether parallel IR displacements are enabled for the next run."""
return _freq_parallel_opt_in()


def freq_parallel_env_configured() -> bool:
"""Return True when ``QUANTUI_FREQ_PARALLEL`` is set in the environment."""
return os.environ.get("QUANTUI_FREQ_PARALLEL") is not None


def _freq_parallel_opt_in() -> bool:
"""Whether parallel IR displacements are opted in.

Expand Down
39 changes: 39 additions & 0 deletions tests/test_cpu_container_def.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""CPU Apptainer image deployment defaults (quantui.def).

Lightweight text assertions — no Apptainer build required.
"""

from __future__ import annotations

import re
from pathlib import Path

import pytest

REPO = Path(__file__).resolve().parent.parent
CPU_DEF = REPO / "apptainer" / "quantui.def"
GPU_DEF = REPO / "apptainer" / "quantui-gpu.def"


@pytest.fixture(scope="module")
def cpu_def_text() -> str:
return CPU_DEF.read_text(encoding="utf-8")


def _section(text: str, name: str) -> str:
m = re.search(rf"^{re.escape(name)}\s*$", text, re.M)
assert m is not None, f"no {name} section"
rest = text[m.end() :]
nxt = re.search(r"^%[a-z]+\s*$", rest, re.M)
return rest[: nxt.start()] if nxt else rest


class TestCpuTeachingImageDefaults:
def test_cpu_image_enables_ir_parallel_by_default(self, cpu_def_text):
env = _section(cpu_def_text, "%environment")
assert re.search(r"export\s+QUANTUI_FREQ_PARALLEL=1\b", env)

def test_gpu_image_does_not_inherit_cpu_ir_parallel_default(self):
gpu_text = GPU_DEF.read_text(encoding="utf-8")
env = _section(gpu_text, "%environment")
assert "QUANTUI_FREQ_PARALLEL" not in env
14 changes: 14 additions & 0 deletions tests/test_freq_ir_workers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

from quantui.freq_ir_workers import (
_truthy,
freq_parallel_env_configured,
freq_parallel_opt_in,
parallel_enabled_for_run,
pick_worker_count,
threads_per_worker,
Expand Down Expand Up @@ -138,3 +140,15 @@ def test_recognised_falsy(self, value):
def test_whitespace_stripped(self):
assert _truthy(" 1 ") is True
assert _truthy("\ttrue\n") is True


class TestEnvConfiguredHelpers:
def test_freq_parallel_env_configured(self, monkeypatch):
monkeypatch.delenv("QUANTUI_FREQ_PARALLEL", raising=False)
assert freq_parallel_env_configured() is False
monkeypatch.setenv("QUANTUI_FREQ_PARALLEL", "0")
assert freq_parallel_env_configured() is True

def test_freq_parallel_opt_in_public_alias(self, monkeypatch):
monkeypatch.setenv("QUANTUI_FREQ_PARALLEL", "yes")
assert freq_parallel_opt_in() is True