Solvation (CPCM/COSMO) handover — v0.9.0 shipped, v0.9.1 in progress (2026-05-20)¶
Status of the v0.9.0 implicit-solvation headline feature and the v0.9.1 follow-up work. Read this if you’re picking up the solvation chain.
What’s complete on origin/main¶
The full v0.9.0 CPCM/COSMO stack landed in commits a014833
(feature), cbe713f (tutorial + examples), 4e2abcd (4 rebuild-
caught bug fixes), e4a8193 (tutorial numbers corrected to real
runs). Verified end-to-end against a live C++ build.
Package: python/vibeqc/solvation/¶
Module |
Role |
|---|---|
|
S1a — Bondi vdW radii × 1.20, Lebedev-Laikov tessellation (6–974 pts/sphere; 302 default), Scalmani-Frisch CSC switching (σ = 0.5 bohr). |
|
S1b math — |
|
S1b coupling — |
|
S1c — |
|
S1d — 16 solvent presets + aliases. |
Wiring¶
run_job(..., solvent="water")— top-level kwarg; attachesSolventResultto the returned object.vibeqc.ase.VibeQC(solvent=...)— ASE calculator path; routes forces throughcpcm_gradient.
Energy convention¶
E_tot = E_HF^gas[D^solv] + ½ q·V_tot (standard Cossi-Scalmani).
The in-solvent SCF returns E_SCF^{w/V_q} = E_HF^gas[D] + q·V_elec;
the driver decomposes correctly (driver.run_cpcm_scf).
Tests / docs¶
tests/test_solvation_cpcm.py— 25 tests, all green on a live build.docs/user_guide/solvation.md— user-guide chapter.docs/tutorial/39_solvation_water.md— worked tutorial, numbers verified against real runs (CH₂O/PBE0/def2-SVP).examples/molecular/solvation/— 3 runnable scripts + README.
Verified numbers (CH₂O / PBE0 / def2-SVP, vibe-qc v0.9.0)¶
Single-point hydration:
E_solv = −5.011 kcal/mol,ΔG_solv = −4.122 kcal/mol, 7 macro-iters, 1047 cavity points.Geometry opt:
r(C=O)1.1961 Å (gas) → 1.2027 Å (water),Δr = +0.0066 Å.Dielectric scan: textbook
(ε−1)/εsaturation; water−5.011, conductor limit reached ~95 % by ε ≈ 20.
v0.9.1 — Item 1 LANDED (this chat, 2026-05-20)¶
Item 1: fully analytic dV^elec/dR — DONE ✅¶
The v0.9.0 cpcm_gradient evaluated its electronic-ESP-derivative
piece q·∂V^elec/∂R by finite difference. That last FD piece is now
closed form. All three milestones complete:
Milestone A (C++) ✅ —
compute_external_charge_density_gradientincpp/src/gradient.cpp/cpp/include/vibeqc/gradient.hpp, bound incpp/src/bindings.cpp(ExternalChargeGradientstruct + the function). Drives libint’s nuclear-attraction gradient engine with the cavity points as fractional point charges; returnsatom_grad(n_atoms, 3) +point_grad(n_points, 3) ofE_ext = Σ_i q_i Tr(D·M_i). Translational invariance verified to machine precision (1e-18).Milestone B (Python) ✅ —
gradient._electronic_esp_gradient_analyticconsumes the C++ pieces, routespoint_grad[i]tocavity.point_atom[i], applies the CPCM sign.cpcm_gradientuses it by default;_electronic_esp_gradient_via_fdreachable viacpcm_gradient(..., use_fd_electronic=True).Milestone C ✅ — 2 new tests in
tests/test_solvation_cpcm.py(test_electronic_esp_gradient_analytic_matches_fd,test_cpcm_gradient_analytic_vs_fd_electronic_consistent); 27/27 solvation tests green.docs/user_guide/solvation.md+docs/tutorial/39_solvation_water.mdupdated; CHANGELOG “### Changed — CPCM solvation gradient is now fully closed-form”.
The CPCM gradient is now closed-form end to end. Verified against the FD oracle on H₂O/STO-3G: electronic-ESP kernel ~1e-4 Ha/bohr, full gradient ~5e-3 Ha/bohr (FD truncation dominates).
Sign accounting (the bug that bit the v0.9.0 ship — get it right):
M_iis the positive Coulomb kernel⟨μ|1/|r−s_i||ν⟩.V^elec_i = −Σ_μν D_μν M_i,μν(electron charge −1).E_ext = Σ_i q_i Tr(D·M_i) = −q·V^elec.The C++ function returns
dE_ext/dR;cpcm_gradientneedsq·dV^elec/dR = −dE_ext/dR.libint’s
Operator::nucleargives−Σ q_i ⟨1/r⟩; the gradient engine’s contraction with D yields−dE_ext/dR, so the C++ function negates once to return+dE_ext/dR.
Build note for this worktree. third_party/ is symlinked from
the parent checkout (ln -s ../../../third_party third_party —
already in place; /third_party is gitignored). Per-worktree venv
at .venv/. Rebuild after a C++ change with:
.venv/bin/pip install --no-build-isolation -e .
v0.9.1 — Item 2 LANDED (this chat, 2026-05-20)¶
Item 2: DF / RIJCOSX in CPCM — DONE ✅¶
run_cpcm_scf no longer hard-codes make_direct_jk_builder. The
macro-iteration inner SCF now selects the Fock-build path off the
method options: density_fit → RI-JK, density_fit + cosx →
RIJCOSX, else direct. New helpers _resolve_cpcm_jk_builder +
_ensure_cpcm_aux_basis in driver.py (the latter autodetects
aux_basis from the orbital basis name and fills it in so the
gas-phase bootstrap, the inner SCFs, and the JKBuilder all share one
aux). Verified on H₂O/def2-SVP (DF vs direct agree to 5e-5 Ha);
tests test_cpcm_density_fit_matches_direct,
test_cpcm_rijcosx_converges. Known follow-up: the CPCM analytic
gradient’s gas-phase piece still uses conventional integral
derivatives even under a DF SCF (mismatch within DF fitting error) —
a DF-consistent CPCM gradient is a future refinement.
Item 3: ECP + CPCM — GUARDED, slips to v0.9.x ⚠️¶
Attempted (2026-05-20) and deliberately not shipped — the
combination has a real charge-bookkeeping bug. run_cpcm_scf now
raises NotImplementedError when options.ecp_centers is set,
rather than returning a silently-wrong solvation energy.
The bug. Folding the ECP one-electron operator into the
macro-iteration Hcore is necessary but not sufficient. The cavity
electrostatic potential V_nuc must also use the ECP-reduced
effective nuclear charge Z − n_core, not the bare atomic number.
With bare Z the apparent surface charge sees a spuriously large net
solute charge. Measured on Zn²⁺ / ecp10mdf / 6-31g / CPCM-water: the
Molecule carries charge = 12 (ECP bookkeeping so n_electrons
returns the 18-valence count), V_nuc used bare Z = 30, electrons
= 18 → net charge the cavity saw = +12 instead of the physical +2.
E_solv ∝ q_net², so the result came out (12/2)² = 36× too large:
−22.65 Ha vs a Born estimate of ≈ −0.6 Ha.
What v0.9.x needs. Per-atom effective charge Z_eff = Z − n_core
entering both _nuclear_potential_at_cavity and the gradient’s
_nuclear_esp_gradient_contribution. n_core is robustly per
(element, ECP family) — for ecpNNmdf it is NN, but lanl2dz
varies per element, so it must be queried from libecpint rather than
parsed from the library name.
Guard + test on main: driver.run_cpcm_scf (early
NotImplementedError), test_cpcm_with_ecp_raises_not_implemented.
Item 4+: deferred v0.9.x / v0.10 follow-ups (NOT started)¶
DF-consistent CPCM gradient. The analytic gradient’s gas-phase SCF piece uses conventional 2e integral derivatives even when the SCF was density-fitted. Route
_gas_phase_gradientthrough the DF gradient path when the SCF options carrydensity_fit. Small.External-code parity validation. No CPCM
E_solvhas been compared against ORCA / Gaussian / PySCF CPCM. All current tests are internal-consistency (analytic-vs-FD, DF-vs-direct, dielectric monotonicity). A real parity number is needed before claiming benchmark-grade accuracy.Non-electrostatic terms (SMD-style CDR). Cavitation + dispersion + repulsion. v0.10+ roadmap; substantial.
Periodic CPCM. Out of scope until v1.x (conductor model needs adaptation for the periodic image problem).
Files to consult¶
python/vibeqc/solvation/gradient.py— the gradient module being extended;_electronic_esp_gradient_via_fdis the function to replace,cpcm_gradientthe dispatcher.cpp/src/gradient.cpp::one_electron_gradient_contribution— the nuclear-attraction-gradient block (lines ~189–242) is the direct template for the new C++ function.cpp/src/integrals.cpp::compute_1e_matrix— shows the libint nuclear engine taking avector<pair<double, array<double,3>>>charge list (fractional charges already supported internally).tests/test_solvation_cpcm.py::test_cpcm_gradient_matches_fd_water_in_water— the analytic-vs-FD regression that must stay green.
Standing rules (CLAUDE.md)¶
Rebase-then-push to main; never --force; never --no-verify;
every commit ships green; new native deps need a roadmap
conversation (this work adds none — pure libint). Commit per
milestone, rebase before each push.