diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69d4c7a..c15a8fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,7 +72,7 @@ jobs: # Single line: PowerShell does not support backslash line continuation. run: pytest -m "not network" --ignore=tests/test_session_calc.py --ignore=tests/test_optimizer.py --ignore=tests/test_preopt.py --no-cov --no-header -q - # ── Windows: real PyFock Phase-1 calculation ─────────────────────────────── + # ── Windows: real PyFock calculation ─────────────────────────────────────── test-windows-pyfock: name: PyFock integration (windows-latest, Python 3.11) runs-on: windows-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bc7307..84fb7db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht ### Added +- **PyFock geometry and analysis phase** — native-Windows PBE/def2 geometry + optimization now uses PyFock's ASE calculator with analytical density-fitted + gradients. Single-point and final optimized-geometry results retain orbital + arrays, Mulliken charges, dipoles, and engine-native cube generation. + - **PyFock Phase-1 backend (M-PYFOCK PYF.2–PYF.6)** — normalized engine dispatch, native-Windows PBE/def2 single points for neutral closed-shell molecules, capability-driven calculation menus, engine provenance in logs diff --git a/README.md b/README.md index 76cdde0..6782cb3 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,8 @@ Real output from QuantUI, straight from the app: - **In-session calculations** — RHF, UHF, 9 DFT functionals, MP2, CCSD, CCSD(T), NMR shielding, TD-DFT UV-Vis, and 1D PES scans via PySCF, running in your Python kernel. Optional PyFock 0.1.x adds native-Windows PBE - single points for neutral, closed-shell molecules with def2-SVP/def2-TZVP; + single points and geometry optimizations for neutral, closed-shell molecules + with def2-SVP/def2-TZVP, including orbital and population analysis; engine capabilities automatically gate the setup menus - **Implicit solvent** — PCM solvation (Water, Ethanol, THF, DMSO, Acetonitrile) via a single checkbox @@ -127,7 +128,7 @@ Real output from QuantUI, straight from the app: | --- | --- | --- | | Linux / macOS | Full | PySCF installs natively | | WSL (Windows) | Full | Use an Ubuntu WSL environment | -| Windows (native) | Partial | PyFock runs guarded PBE single points; use WSL/Apptainer for the full PySCF feature set | +| Windows (native) | Partial | PyFock runs guarded PBE single points and geometry optimizations; use WSL/Apptainer for the full PySCF feature set | ### Windows users: native PyFock or the full container @@ -140,11 +141,12 @@ python -m pip install "quantui[pyfock,ase,app]" ``` In **System Settings → Quantum engine**, select PyFock (or leave Automatic -selected when PySCF is absent). Phase 1 intentionally supports only neutral, -closed-shell PBE single points with def2-SVP or def2-TZVP. Density fitting is -always enabled. Hybrids, charged/open-shell systems, geometry optimization, -solvent, checkpoint warm starts, GPU, Mulliken/dipole analysis, and orbital -export remain PySCF-only. +selected when PySCF is absent). The validated subset supports neutral, +closed-shell PBE single points and geometry optimizations with def2-SVP or +def2-TZVP. Density fitting is always enabled, analytical gradients drive +optimization, and orbital, Mulliken, dipole, and cube analysis are retained. +Hybrids, charged/open-shell systems, solvent, checkpoint warm starts, and GPU +remain PySCF-only. PySCF does not install on Windows natively. For the complete feature set, the [`apptainer/quantui.def`](https://github.com/The-Schultz-Lab/QuantUI/blob/main/apptainer/quantui.def) container bundles diff --git a/docs/installation.md b/docs/installation.md index deaf7a6..acf3d0b 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -27,7 +27,7 @@ pip install "quantui[pyscf,ase,app]" ## Windows -For native Windows and the guarded PyFock single-point subset, use Python 3.11: +For native Windows and the guarded PyFock subset, use Python 3.11: ```powershell py -3.11 -m venv .venv @@ -35,8 +35,9 @@ py -3.11 -m venv .venv python -m pip install "quantui[pyfock,ase,app]" ``` -Automatic engine selection uses PyFock when PySCF is absent. Phase 1 supports -neutral, closed-shell PBE/def2-SVP or PBE/def2-TZVP single points. For all +Automatic engine selection uses PyFock when PySCF is absent. The validated +subset supports neutral, closed-shell PBE/def2-SVP or PBE/def2-TZVP single +points and geometry optimizations. For all other methods and workflows, use WSL or the Apptainer container described in [Platform Support](platforms.md). @@ -45,7 +46,7 @@ other methods and workflows, use WSL or the Apptainer container described in | Extra | What it adds | | --- | --- | | `pyscf` | Canonical PySCF backend (full calculation and analysis feature set) | -| `pyfock` | PyFock Phase-1 backend (native-Windows PBE single points) | +| `pyfock` | PyFock backend (native-Windows PBE single points and geometry optimization) | | `ase` | ASE bridge for trajectory export and structure I/O | | `app` | Voilà, JupyterLab, and notebook launcher dependencies | | `xtb` | GFN-FF metal pre-optimization via xtb | diff --git a/docs/platforms.md b/docs/platforms.md index 8a577af..f743f5e 100644 --- a/docs/platforms.md +++ b/docs/platforms.md @@ -7,13 +7,13 @@ PyFock subset or move to WSL/Apptainer for the full feature set. | --- | --- | --- | | **Linux / macOS** | :material-check-circle:{ .green } Full | PySCF installs natively via conda or pip | | **WSL (Windows)** | :material-check-circle:{ .green } Full | Ubuntu WSL environment — follows the Linux path exactly | -| **Windows native** | :material-alert-circle:{ .yellow } Partial | PyFock: neutral closed-shell PBE/def2 single points; WSL/Apptainer: full PySCF feature set | +| **Windows native** | :material-alert-circle:{ .yellow } Partial | PyFock: neutral closed-shell PBE/def2 single points and geometry optimizations; WSL/Apptainer: full PySCF feature set | !!! info "Windows users" Install `quantui[pyfock,ase,app]` under Python 3.11 for native PBE single - points. QuantUI's engine picker hides unsupported controls. For hybrids, - ions, radicals, optimizations, spectra, solvent, checkpointing, GPU, and - orbital analysis, use [WSL 2](https://learn.microsoft.com/en-us/windows/wsl/install) + points and geometry optimizations. QuantUI's engine picker hides unsupported + controls. For hybrids, ions, radicals, spectra, solvent, checkpointing, and + GPU, use [WSL 2](https://learn.microsoft.com/en-us/windows/wsl/install) with Ubuntu or the [Apptainer container](https://github.com/The-Schultz-Lab/QuantUI/blob/main/apptainer/README.md) which bundles the complete environment in a single file. diff --git a/pyproject.toml b/pyproject.toml index ec7df6a..bb5d169 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -131,6 +131,9 @@ pyscf = [ # extra supplies and caps that runtime dependency explicitly. pyfock = [ "pyfock>=0.1.7,<0.2", + # PyFock's ASE calculator is the geometry-optimization path used by + # QuantUI; include ASE here so the documented backend extra is complete. + "ase>=3.22.0,<4", "scipy>=1.10,<2", # 0.68.0rc1 can be selected by PyFock's permissive floor and brought a # prerelease llvmlite that crashed at import on Linux/Python 3.12. diff --git a/quantui/app.py b/quantui/app.py index a6be266..f513eeb 100644 --- a/quantui/app.py +++ b/quantui/app.py @@ -1770,6 +1770,7 @@ def __init__(self) -> None: self._last_orb_mo_occ: Any = None self._last_orb_mol_atom: Any = None self._last_orb_mol_basis: Any = None + self._last_orb_engine_id: str = "pyscf" # Mulliken Populations panel state (table + Plotly bar chart). self._last_mulliken_symbols: Any = None self._last_mulliken_charges: Any = None @@ -4076,9 +4077,7 @@ def _apply_quantum_engine_capabilities(self) -> None: self.execution_backend_dd.value = "local" self.solvent_cb.value = False self.solvent_cb.disabled = True - self.solvent_cb.description = ( - "Implicit solvent — unavailable in PyFock Phase 1" - ) + self.solvent_cb.description = "Implicit solvent — unavailable in PyFock" self._freq_preopt_cb.value = False self._freq_preopt_cb.disabled = True self._freq_preopt_cb.layout.display = "none" @@ -5791,7 +5790,7 @@ def _on_scf_converged() -> None: def _run_required_final_single_point(target_mol, reason: str): """Run a required post-optimization single point on target geometry.""" - from quantui import run_in_session + from quantui.engines import EnginePreference, EngineRequest, run_calc _solvent = self.solvent_dd.value if self.solvent_cb.value else None self.run_status.value = ( @@ -5801,13 +5800,28 @@ def _run_required_final_single_point(target_mol, reason: str): f"\n-- Required single-point ({reason}) " "on optimized geometry --------------------------------\n" ) - sp_result = run_in_session( - molecule=target_mol, - method=self.method_dd.value, - basis=self.basis_dd.value, - progress_stream=log, # type: ignore[arg-type] - solvent=_solvent, + sp_result = run_calc( + EngineRequest( + request_id=_uuid.uuid4().hex, + calc_type="single_point", + method=self.method_dd.value, + basis=self.basis_dd.value, + charge=int(target_mol.charge), + multiplicity=int(target_mol.multiplicity), + molecule={ + "atoms": list(target_mol.atoms), + "coordinates": [list(c) for c in target_mol.coordinates], + }, + options={"verbose": 4, "scf_rescue": True}, + progress_stream=log, # type: ignore[arg-type] + solvent=_solvent, + ), + preferred=cast( + EnginePreference, + self._user_settings.compute.quantum_engine, + ), ) + sp_result = sp_result.to_session_result() if not bool(getattr(sp_result, "converged", False)): raise RuntimeError( "Required post-optimization single-point did not converge." @@ -5961,7 +5975,7 @@ def _run_required_final_single_point(target_mol, reason: str): ) self.run_status.value = "Optimizing geometry..." - from quantui import optimize_geometry + from quantui.engines import EnginePreference, EngineRequest, run_calc # History-based expected step count → "step k/~N" + a floor # for the live progress fraction. None on cold history. @@ -5973,19 +5987,35 @@ def _run_required_final_single_point(target_mol, reason: str): ) log.opt_fmax_target = float(self.fmax_fi.value) log.opt_fmax0 = None - result = optimize_geometry( - molecule=calc_mol, - method=self.method_dd.value, - basis=self.basis_dd.value, - fmax=self.fmax_fi.value, - steps=self.max_steps_si.value, - progress_stream=log, # type: ignore[arg-type] - expected_steps=( - int(round(_expected_steps)) if _expected_steps else None + result = run_calc( + EngineRequest( + request_id=_uuid.uuid4().hex, + calc_type="geometry_opt", + method=self.method_dd.value, + basis=self.basis_dd.value, + charge=int(calc_mol.charge), + multiplicity=int(calc_mol.multiplicity), + molecule={ + "atoms": list(calc_mol.atoms), + "coordinates": [list(c) for c in calc_mol.coordinates], + }, + options={ + "fmax": self.fmax_fi.value, + "steps": self.max_steps_si.value, + "expected_steps": ( + int(round(_expected_steps)) if _expected_steps else None + ), + "resume": _resume, + "scf_rescue": True, + }, + progress_stream=log, # type: ignore[arg-type] + checkpoint=_ckpt, ), - checkpoint=_ckpt, - resume=_resume, - ) + preferred=cast( + EnginePreference, + self._user_settings.compute.quantum_engine, + ), + ).to_session_result() _sp_result = _run_required_final_single_point( result.molecule, "after geometry optimisation", @@ -6017,6 +6047,19 @@ def _run_required_final_single_point(target_mol, reason: str): "pyscf_mol_basis", result.pyscf_mol_basis, ) + result.atom_symbols = getattr( + _sp_result, "atom_symbols", result.atom_symbols + ) + result.mulliken_charges = getattr( + _sp_result, "mulliken_charges", result.mulliken_charges + ) + result.dipole_moment_debye = getattr( + _sp_result, "dipole_moment_debye", result.dipole_moment_debye + ) + result.dipole_vector_debye = getattr( + _sp_result, "dipole_vector_debye", result.dipole_vector_debye + ) + result.engine_id = getattr(_sp_result, "engine_id", result.engine_id) result_html = self._format_opt_result(result) save_spectra, save_type = {}, "geometry_opt" elif ct == "Frequency": @@ -6961,6 +7004,18 @@ def _begin_run_checkpoint(self, log_stream: Optional[Any] = None) -> Optional[An the reason a calculation doesn't start. """ try: + from quantui.engines import EnginePreference, resolve_engine + + caps = resolve_engine( + cast( + EnginePreference, + self._user_settings.compute.quantum_engine, + ) + ).capabilities() + if not caps.supports_checkpoint_warm_start: + self._checkpoint_resumable = False + return None + from quantui.app_runflow import checkpoint_identity from quantui.checkpoint import Checkpoint diff --git a/quantui/app_analysis.py b/quantui/app_analysis.py index 56ce282..f50a99e 100644 --- a/quantui/app_analysis.py +++ b/quantui/app_analysis.py @@ -234,6 +234,7 @@ def apply_analysis_context(app: Any, ctx: Any) -> None: app._last_orb_mo_coeff = None app._last_orb_mo_occ = None app._last_orb_method = None + app._last_orb_engine_id = "pyscf" # Mulliken state consumed by the Populations panel — reset so a context # without charges cannot leak the prior calc's chart into this one. app._last_mulliken_symbols = None diff --git a/quantui/app_visualization.py b/quantui/app_visualization.py index dfaaf7d..59fafbf 100644 --- a/quantui/app_visualization.py +++ b/quantui/app_visualization.py @@ -1297,6 +1297,7 @@ def show_orbital_diagram(app: Any, result: Any) -> bool: app._last_orb_mo_occ = mo_occ app._last_orb_mol_atom = getattr(result, "pyscf_mol_atom", None) app._last_orb_mol_basis = getattr(result, "pyscf_mol_basis", None) + app._last_orb_engine_id = str(getattr(result, "engine_id", "pyscf") or "pyscf") # AUDIT additional-concerns — snapshot the method that actually # produced this mo_coeff, from the result object itself, rather than # reading the live Method dropdown at cube-generation time (below). @@ -1580,6 +1581,7 @@ def _show_range_err() -> None: from quantui.orbital_visualization import ( generate_cube_from_arrays, + generate_pyfock_cube_from_arrays, infer_charge_and_spin, plot_cube_isosurface, render_orbital_isosurface_py3dmol, @@ -1641,7 +1643,14 @@ def _show_range_err() -> None: # the dropdown shows later — immutable result provenance instead # of a mutable, disconnectable UI control. _method_for_provenance = str(getattr(app, "_last_orb_method", "") or "") - generate_cube_from_arrays( + # The PySCF and PyFock generators have intentionally different + # optional keyword sets; both accept this shared call shape. + _cube_generator: Any = ( + generate_pyfock_cube_from_arrays + if getattr(app, "_last_orb_engine_id", "pyscf") == "pyfock" + else generate_cube_from_arrays + ) + _cube_generator( mol_atom, mol_basis, mo_coeff, diff --git a/quantui/engines/pyfock_engine.py b/quantui/engines/pyfock_engine.py index f32d292..c683dcc 100644 --- a/quantui/engines/pyfock_engine.py +++ b/quantui/engines/pyfock_engine.py @@ -1,11 +1,11 @@ -"""PyFock adapter for the guarded Phase-1 single-point DFT subset.""" +"""PyFock adapter for the guarded neutral, closed-shell DFT subset.""" from __future__ import annotations import importlib.util import sys from contextlib import redirect_stderr, redirect_stdout -from typing import Optional +from typing import Any, Optional from .base import ( EngineCapabilities, @@ -44,7 +44,7 @@ def _pyfock_version() -> str: class PyfockEngine: - """Pure-Python DFT engine for native Windows and teaching single points.""" + """Pure-Python DFT engine for native Windows and teaching workflows.""" @property def engine_id(self) -> str: @@ -54,17 +54,18 @@ def capabilities(self) -> EngineCapabilities: return EngineCapabilities( engine_id="pyfock", display_name="PyFock", - supported_calc_types=("single_point",), + supported_calc_types=("single_point", "geometry_opt"), supported_methods=_PYFOCK_METHODS, supported_basis_sets=_PYFOCK_BASES, supports_solvent=False, supports_checkpoint_warm_start=False, supports_gpu=False, supports_post_hf=False, - supports_orbital_export=False, + supports_orbital_export=True, platform_notes=( - "Phase-1 neutral, closed-shell PBE single points. Density fitting " - "is always on; hybrids, solvent, checkpoints, and GPU are gated off. " + "Neutral, closed-shell PBE single points and geometry optimizations. " + "Density fitting and analytical gradients are used; hybrids, " + "solvent, checkpoints, and GPU are gated off. " "Install with pip install quantui[pyfock]." ), recommended_auxbasis=_AUX_BASIS, @@ -72,8 +73,13 @@ def capabilities(self) -> EngineCapabilities: ) def run(self, request: EngineRequest) -> EngineResult: - """Run one neutral, closed-shell PBE single point through PyFock.""" + """Run one validated PyFock calculation.""" self._validate_request(request) + if request.calc_type == "geometry_opt": + return self._run_geometry_opt(request) + return self._run_single_point(request) + + def _run_single_point(self, request: EngineRequest) -> EngineResult: stream = request.progress_stream or sys.stdout atoms, coordinates = _molecule_arrays(request.molecule) @@ -94,7 +100,7 @@ def run(self, request: EngineRequest) -> EngineResult: "quantui[pyfock] extra and restart the kernel." ), ) from exc - print("\n-- PyFock Phase-1 single point -----------------------------") + print("\n-- PyFock single point -------------------------------------") print( f"Engine: PyFock {_pyfock_version() or 'unknown'} | " f"{request.method}/{request.basis} | density fitting: on" @@ -126,7 +132,7 @@ def run(self, request: EngineRequest) -> EngineResult: # functions). PyFock defaults to Cartesian 6d/10f, which shifts # even the water reference outside the milestone parity bound. dft.sao = True - energy, _density = dft.scf() + energy, density = dft.scf() except EngineUnavailableError: raise except Exception as exc: # noqa: BLE001 — normalize third-party failures @@ -153,10 +159,7 @@ def run(self, request: EngineRequest) -> EngineResult: getattr(dft, "mo_energies", None), getattr(dft, "mo_occupations", None), ) - warnings = [ - "PyFock Phase 1 uses density fitting with def2-universal-jfit.", - "Orbital export and Mulliken/dipole analysis are not available yet.", - ] + warnings = ["PyFock uses density fitting with def2-universal-jfit."] if not bool(getattr(dft, "converged", False)): warnings.append("PyFock reached its iteration limit without convergence.") @@ -173,21 +176,60 @@ def run(self, request: EngineRequest) -> EngineResult: homo_lumo_gap_ev=gap_ev, warnings=warnings, ) - result.native_result = result.to_session_result() + native = result.to_session_result() + _attach_analysis(native, mol, basis, dft, density, atoms, coordinates, warnings) + result.native_result = native return result + def _run_geometry_opt(self, request: EngineRequest) -> EngineResult: + from quantui.molecule import Molecule + from quantui.optimizer import optimize_geometry + + atoms, coordinates = _molecule_arrays(request.molecule) + molecule = Molecule( + atoms=list(atoms), + coordinates=[list(c) for c in coordinates], + charge=request.charge, + multiplicity=request.multiplicity, + ) + native = optimize_geometry( + molecule=molecule, + method=request.method, + basis=request.basis, + fmax=_positive_float(request.options.get("fmax"), default=0.05), + steps=_positive_int(request.options.get("steps"), default=200), + progress_stream=request.progress_stream, + expected_steps=request.options.get("expected_steps"), + engine_id=self.engine_id, + ncores=_positive_int(request.options.get("ncores"), default=1), + ) + return EngineResult( + request_id=request.request_id, + engine_id=self.engine_id, + status="success", + converged=native.converged, + energy_hartree=native.energy_hartree, + n_iterations=native.n_steps, + method=native.method, + basis=native.basis, + formula=native.formula, + native_result=native, + ) + def _validate_request(self, request: EngineRequest) -> None: caps = self.capabilities() if request.calc_type not in caps.supported_calc_types: raise UnsupportedCapabilityError( - f"PyFock does not support {request.calc_type!r} in Phase 1.", - user_message="PyFock Phase 1 supports Single Point calculations only.", + f"PyFock does not support {request.calc_type!r}.", + user_message=( + "PyFock supports Single Point and Geometry Opt calculations." + ), ) if request.method.upper() not in caps.supported_methods: raise UnsupportedCapabilityError( f"PyFock method {request.method!r} is not validated.", user_message=( - "PyFock Phase 1 currently supports PBE only. Hybrid " + "PyFock currently supports PBE only. Hybrid " "functionals require exact exchange that upstream PyFock " "does not yet provide." ), @@ -195,30 +237,28 @@ def _validate_request(self, request: EngineRequest) -> None: if request.basis not in (caps.supported_basis_sets or ()): raise UnsupportedCapabilityError( f"PyFock basis {request.basis!r} is not validated.", - user_message=( - "PyFock Phase 1 currently supports def2-SVP and def2-TZVP." - ), + user_message=("PyFock currently supports def2-SVP and def2-TZVP."), ) if request.charge != 0: raise UnsupportedCapabilityError( - "PyFock charged molecules are not enabled in Phase 1.", + "PyFock charged molecules are not enabled.", user_message=( - "PyFock Phase 1 is limited to neutral molecules while its " + "PyFock is limited to neutral molecules while its " "charge convention is validated. Select PySCF for ions." ), ) if request.multiplicity != 1: raise UnsupportedCapabilityError( - "PyFock open-shell calculations are not enabled in Phase 1.", + "PyFock open-shell calculations are not enabled.", user_message=( - "PyFock Phase 1 is limited to closed-shell singlets. " + "PyFock is limited to closed-shell singlets. " "Select PySCF for radicals or other spin states." ), ) if request.solvent: raise UnsupportedCapabilityError( - "PyFock solvent models are not enabled in Phase 1.", - user_message="Implicit solvent is not available with PyFock Phase 1.", + "PyFock solvent models are not enabled.", + user_message="Implicit solvent is not available with PyFock.", ) _molecule_arrays(request.molecule) @@ -229,6 +269,58 @@ def _load_pyfock_api(): return Mol, Basis, DFT +def _attach_analysis( + result: Any, + mol: Any, + basis: Any, + dft: Any, + density: Any, + atoms: list[str], + coordinates: list[list[float]], + warnings: list[str], +) -> None: + """Attach portable orbital, Mulliken, and dipole fields when available.""" + try: + import numpy as np + + result.mo_energy_hartree = np.asarray(dft.mo_energies, dtype=float) + result.mo_occ = np.asarray(dft.mo_occupations, dtype=float) + result.mo_coeff = np.asarray(dft.mo_coefficients, dtype=float) + result.pyscf_mol_atom = [ + (symbol, [float(x), float(y), float(z)]) + for symbol, (x, y, z) in zip(atoms, coordinates) + ] + result.pyscf_mol_basis = result.basis + except Exception as exc: # noqa: BLE001 - analysis is additive + warnings.append(f"PyFock orbital extraction was unavailable: {exc}") + + try: + import numpy as np + from pyfock import Integrals + + dmat = np.asarray(density, dtype=float) + overlap = np.asarray(Integrals.overlap_mat_symm(basis), dtype=float) + populations = np.diag(dmat @ overlap) + gross: Any = np.zeros(len(atoms), dtype=float) + for ao_index, atom_index in enumerate(basis.bfs_atoms): + gross[int(atom_index)] += populations[ao_index] + result.atom_symbols = list(atoms) + result.mulliken_charges = [ + float(charge - population) + for charge, population in zip(mol.Zcharges, gross) + ] + + dipole_matrix = Integrals.dipole_moment_mat_symm(basis) + dipole_debye = ( + np.asarray(mol.get_dipole_moment(dipole_matrix, dmat), dtype=float) + * 2.541746473 + ) + result.dipole_vector_debye = [float(v) for v in dipole_debye] + result.dipole_moment_debye = float(np.linalg.norm(dipole_debye)) + except Exception as exc: # noqa: BLE001 - analysis is additive + warnings.append(f"PyFock population analysis was unavailable: {exc}") + + def _molecule_arrays(molecule: dict): atoms = molecule.get("atoms") coordinates = molecule.get("coordinates") diff --git a/quantui/engines/pyscf_engine.py b/quantui/engines/pyscf_engine.py index 35ccb6b..afc9694 100644 --- a/quantui/engines/pyscf_engine.py +++ b/quantui/engines/pyscf_engine.py @@ -68,7 +68,9 @@ def capabilities(self) -> EngineCapabilities: ) def run(self, request: EngineRequest) -> EngineResult: - """Adapt the existing, feature-complete PySCF single-point path.""" + """Adapt the established PySCF paths to the shared engine contract.""" + if request.calc_type == "geometry_opt": + return self._run_geometry_opt(request) if request.calc_type != "single_point": raise UnsupportedCapabilityError( f"Shared engine dispatch does not yet own {request.calc_type!r}.", @@ -113,6 +115,42 @@ def run(self, request: EngineRequest) -> EngineResult: native_result=native, ) + def _run_geometry_opt(self, request: EngineRequest) -> EngineResult: + from quantui.molecule import Molecule + from quantui.optimizer import optimize_geometry + + molecule = Molecule( + atoms=list(request.molecule["atoms"]), + coordinates=[list(c) for c in request.molecule["coordinates"]], + charge=request.charge, + multiplicity=request.multiplicity, + ) + native = optimize_geometry( + molecule=molecule, + method=request.method, + basis=request.basis, + fmax=float(request.options.get("fmax", 0.05)), + steps=int(request.options.get("steps", 200)), + progress_stream=request.progress_stream, + expected_steps=request.options.get("expected_steps"), + checkpoint=request.checkpoint, + resume=bool(request.options.get("resume", False)), + scf_rescue=bool(request.options.get("scf_rescue", True)), + engine_id=self.engine_id, + ) + return EngineResult( + request_id=request.request_id, + engine_id=self.engine_id, + status="success", + converged=native.converged, + energy_hartree=native.energy_hartree, + n_iterations=native.n_steps, + method=native.method, + basis=native.basis, + formula=native.formula, + native_result=native, + ) + def build_pyscf_engine() -> Optional[QuantumEngine]: if not _AVAILABLE: diff --git a/quantui/help_content.py b/quantui/help_content.py index d80f7ac..6096e4a 100644 --- a/quantui/help_content.py +++ b/quantui/help_content.py @@ -44,8 +44,8 @@ "History to reload a previous run" "" "
Platform note: PySCF's full feature set requires Linux, " - "macOS, or WSL. Native Windows can use the optional PyFock Phase-1 " - "subset for neutral closed-shell PBE single points.
" + "macOS, or WSL. Native Windows can use the optional PyFock subset " + "for neutral closed-shell PBE single points and geometry optimization." "Each dropdown in the Calculate tab has a ? button for " "context-sensitive help on that specific option.
" "While a calculation runs, see Live run progress in the " @@ -60,9 +60,11 @@ "uses PyFock, which is useful on native Windows.
" "PySCF is QuantUI's canonical engine and provides the full " "calculation, solvent, checkpoint, GPU, and analysis feature set.
" - "PyFock Phase 1 is deliberately narrower: neutral, " - "closed-shell PBE single points with def2-SVP or def2-TZVP. Density " - "fitting is always on. Unsupported methods and controls disappear " + "
PyFock is deliberately narrower: neutral, closed-shell " + "PBE single points and geometry optimizations with def2-SVP or " + "def2-TZVP. Density fitting is always on, analytical gradients drive " + "geometry optimization, and orbital/population analysis is retained. " + "Unsupported methods and controls disappear " "from the setup menus instead of failing deep inside a calculation.
" "The selected engine is recorded in the live log, result card, "
"and saved result.json.