diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 183cee4..7a9771c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,39 @@ jobs: # Chromium with GPU virtualization, which the default Ubuntu runners # don't expose. Run locally with `npm run test:e2e` (~1.5 min on M2 Pro). + basis-digits: + name: basis digits vs PySCF + runs-on: ubuntu-latest + # The only gate that checks the basis-set DIGITS at the source + # (src/chemistry/integrals.ts) instead of checking an energy that happens to + # land close. Every (element × basis) cell the repo ships is compared against + # pyscf.gto.basis.load; intentional deviations (Li STO-3G is s-only) are an + # explicit, exponent-matched allowlist in the script, so a bad digit in the + # same cell still fails. + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install dependencies + run: npm ci + + - name: Install PySCF + run: python3 -m pip install --upgrade pip pyscf + + - name: Basis constants vs pyscf.gto.basis.load + run: npm run check:basis + build: name: build runs-on: ubuntu-latest diff --git a/LIMITATIONS.md b/LIMITATIONS.md index 2015981..d9587d9 100644 --- a/LIMITATIONS.md +++ b/LIMITATIONS.md @@ -43,24 +43,40 @@ reviewer or chemist would discover anyway. ### Basis-set atom coverage +**Periods 1–3 complete as of 2026-08-10: H through Ar (Z = 1–18).** + | basis | atoms wired | |---|---| -| STO-3G | H, Li, Be, C, N, O, F | -| 6-31G* | H, C, N, O (spot-checked) | -| **cc-pVDZ** | **H, Li, Be, C, N, O, F** (full first-row coverage — Tier 3 shipped 2026-05) | -| aug-cc-pVDZ | **H, Li, Be, C, N, O, F** (diffuse tables for full first row wired 2026-05) | - -LiH / BeH₂ / CH₄ / NH₃ / HF now first-class cc-pVDZ targets. -Verified by `tests/chemistry/ccpvdz-firstrow.test.ts` — each -molecule's cc-pVDZ HF energy converges and lies variationally -below its STO-3G counterpart. Tolerance is ~10 mHa vs PySCF 2.13.0 -reference values (loose, because the test is verifying basis -wiring not SCF precision; the existing H₂O cc-pVDZ tests cover -the precision case to 35 µHa). - -Aug-cc-pVDZ diffuse functions for Li, Be, C, N, F → ~30 minutes -each from the same EMSL source, queued as a follow-up if needed -for anions or excited-state work on those systems. +| **STO-3G** | **H–Ar** (all 18) | +| **cc-pVDZ** | **H–Ar** (all 18) | +| **aug-cc-pVDZ** | **H–Ar** (all 18) | + +Nothing beyond Z = 18. No transition metals, no fourth row. +**6-31G\* is NOT implemented** — `BasisName` is exactly +`"sto-3g" | "cc-pvdz" | "aug-cc-pvdz"` and the string "6-31g" appears +nowhere in `src/`. (An earlier revision of this table claimed 6-31G* +was "wired, spot-checked" for H/C/N/O. That was never true.) + +Every (element × basis) cell is validated against PySCF by +`tests/chemistry/elements/reference-agreement.test.ts`: 18 elements × +3 bases × both d-conventions, one real molecule each (hydrides +wherever one exists, so two-center integrals are actually exercised), +**bar 0.1 mHa**. `tests/chemistry/elements/pyscf-reference.json` holds +the committed reference table; regenerate it with +`scripts/run-element-reference.py`. + +The d-convention matters from Na onward as well as across Li–Ne: our +Cartesian path must be compared against PySCF with `mol.cart = True` +and our spherical path against PySCF's default, or a ~0.34 mHa phantom +error appears on every element carrying d functions. STO-3G has no d +functions below Z = 19, so it is convention-free throughout. + +Superseded caveat, kept for provenance: this section previously +described `ccpvdz-firstrow.test.ts` as the verification, at "~10 mHa" +tolerance. That test's real slack was 116–170 mHa, and it did not +detect that the Li and Be cc-pVDZ tables were wrong by up to 1.29 mHa +(fixed 2026-08-10). Precision claims here now rest on the element +agreement test, not on it. --- @@ -102,6 +118,25 @@ will silently truncate large dispatches. - **TDA-DFT / DFT-gradient with spherical-d** — refuses with a clear error today; proper fix is Cartesian → spherical transform on grid values. Documented. +- **Wrong-basin SCF at PAH scale — converges to a confidently wrong + answer.** This is the most dangerous failure mode in the list, because + it does not announce itself: `converged: true`, a clean stationary + point, and a plausible-looking number. On anthracene (C₁₄H₁₀, cc-pVDZ, + n = 246) the damping recipe (`damping=0.2`, `diisStartIter=8`) that + rescues the default-DIIS +5352 Ha divergence lands instead at + **E ≈ −880 Ha, against a literature HF/cc-pVDZ value of ~−537 Ha**. + Being *more negative* than the true ground state is the tell: the + damped warm-up steers the density into a non-physical orbital + occupation that is variationally lower within the SCF ansatz but is + not the ground-state singlet. The architecture and the convergence + machinery work end-to-end; **basin selection** is the open problem. + Real fixes are MOM (maximum overlap method) to preserve orbital + ordering across iterations, SOSCF for second-order convergence to the + nearest stationary point, or a SAD initial guess. Until one of those + lands, treat any large-PAH SCF energy from this code as unvalidated + regardless of the `converged` flag — and note that no automatic check + currently catches this, since the energy is finite, the SCF is + converged, and only an external reference reveals the error. --- diff --git a/docs/RUN-PLAN-24H-ELEMENTS.md b/docs/RUN-PLAN-24H-ELEMENTS.md new file mode 100644 index 0000000..e4d9150 --- /dev/null +++ b/docs/RUN-PLAN-24H-ELEMENTS.md @@ -0,0 +1,254 @@ +# 24h autonomous run plan — break the periodic-table wall + +**Status:** proposed, not started +**Branch:** `feat/periodic-table-expansion` (create from `main`) +**Written:** 2026-08-10 + +--- + +## Mission + +Take `SUPPORTED_SYMBOLS` from 8 elements to 18, with every new element +validated against PySCF to sub-milliHartree and locked behind a +regression test. + +Current: `H, He, Li, Be, C, N, O, F` +Target: add `B, Ne, Na, Mg, Al, Si, P, S, Cl, Ar` + +This is a **data** problem, not an engine problem. The integral core +already handles d-shells (cc-pVDZ carbon carries `CCPVDZ_C_3D`), so +third-row elements need no new angular-momentum machinery — only more +contracted shells at L ≤ 2. + +## Non-goals + +Do **not** do any of these, even if they look tempting mid-run: + +- Refactor working code. The repo was audited 2026-08-08; it is not the problem. +- Add new methods, new UI, new pages, or new experiments. +- Deploy anything. Not Vercel, not the HF Space. (Standing user preference.) +- Push to `main` or open a PR. Leave the branch for human review. +- Chase performance. + +## Hard rules + +1. **Every step ends in a number a machine can compare.** No "looks + right", no visual inspection, no prose verdicts. If a step cannot be + expressed as `assert |ours − reference| < tol`, it is not a step. +2. **Hard-fail on `converged: false`.** Every harness must treat a + non-converged SCF/CCSD as a *failure*, never as a datum. This is the + single most likely way a long run produces confident garbage. +3. **One element per commit.** A bad stretch must be revertable without + losing the good ones. Trailer: + `Co-Authored-By: Claude Opus 5 (1M context) ` +4. **Serialize test runs.** Do not run vitest suites concurrently with + PySCF sweeps. The machine is a 16 GB M2 Pro; saturating it produces + timeout flakes that look exactly like real failures and will send the + run chasing phantoms. +5. **Never widen a tolerance to make a test pass.** If a bar cannot be + met, record the failure in the Phase 3 report and move to the next + element. A loosened tolerance is an undetectable lie. +6. **`uv run` inside `~/dev/ml-research` for anything PySCF.** Never bare + `pip` / `python`. + +--- + +## Phase 0 — the harness (do not skip; nothing after this is safe without it) + +### 0.1 Resolve the d-shell convention first + +`tests/chemistry/ccpvdz-spherical.test.ts` documents that the Cartesian +(6d) path sits **~0.4 mHa below** PySCF's default spherical (5d) result +for H₂O/cc-pVDZ — a basis-set convention difference, *not* a bug. + +Every element from Al onward has d functions in cc-pVDZ. If the harness +compares the Cartesian path against default PySCF, **every new element +will show a ~0.4 mHa phantom error**, and the run will spend hours +"fixing" correct code. + +**Gate 0.1:** the harness must either (a) compare the spherical path +against default PySCF, or (b) compare the Cartesian path against PySCF +with `mol.cart = True`. Prove it by reproducing the existing H₂O/cc-pVDZ +agreement *before* touching any new element. Write down which convention +was chosen at the top of the harness file. + +### 0.2 Generate basis tables — do not hand-transcribe + +Write `scripts/gen-basis-tables.py`: read PySCF's own basis library +(`pyscf.gto.basis.load`) and emit TypeScript constants in the exact +shape `src/chemistry/integrals.ts` already uses: + +```ts +export const STO3G_C_2P = { + alpha: [2.9412494, 0.6834831, 0.2222899] as const, + c: STO3G_L_2P_C, +}; +``` + +Hand-transcribing 10 elements × 3 basis sets is ~100 blocks of decimal +digits. That is precisely the task where a long autonomous run silently +introduces a typo that surfaces as a 3 mHa discrepancy 14 hours later. +Generating it removes the failure mode entirely. + +**Gate 0.2 (the important one):** run the generator against the **eight +elements that already exist** and diff against the current hand-written +constants. If it reproduces `C`, `N`, `O`, `F` to the last digit, the +generator is trustworthy for `S`, `P`, `Cl`. If it does not, fix the +generator — never the existing constants — until it does. + +This is what makes the rest of the run autonomous-safe: the tool is +validated against known-good data before it is trusted with new data. + +### 0.3 Two-level validation harness + +For each (element, basis), the harness runs **two** independent checks +that isolate the two distinct failure modes: + +| Level | What it compares | Catches | +|---|---|---| +| **A — engine** | our energy vs PySCF fed *our own primitives* via `gto.basis.parse` | bugs in our integrals/SCF for this element | +| **B — data** | our primitives vs PySCF's built-in basis table | wrong/garbled basis data | + +`scripts/run-pyscf-reference.py` already uses `gto.basis.parse` for the +matched s-only Li basis — same mechanism, reuse it. + +Interpretation is unambiguous: +- A passes, B fails → basis data is wrong. +- B passes, A fails → engine bug for this element. +- Both fail → start with B. + +**Gate 0.3:** both levels green for all 8 existing elements before +proceeding. Bars: HF ≤ 1e-9 Ha for Level A (same primitives should agree +to machine precision, not chemistry precision), ≤ 0.1 mHa for Level B. + +--- + +## Phase 1 — elements, in this order + +The order is deliberate: **structurally-familiar elements first**, so +that an early failure means "the harness is wrong", not "this element is +hard". Only after the pipeline is proven does it meet new shell +structure. + +### 1a. Second row — proves the pipeline (structure already supported) + +| El | Z | STO-3G shells | Why | +|---|---|---|---| +| **B** | 5 | 1s, 2s, 2p | BH₃/BF₃ — the canonical empty-orbital Lewis acid lesson. Also fills the one gap in row 2. | +| **Ne** | 10 | 1s, 2s, 2p | Completes row 2; noble-gas reference for the dispersion labs (`dispersion.ts` already exists). | + +These are byte-for-byte the same shell pattern as C/N/O/F. If either +fails, **stop and fix the harness** — do not proceed to row 3. + +### 1b. Third row — the actual unlock (new: 3s/3p shells) + +Ordered by curriculum value, highest first. + +| El | Z | Unlocks | +|---|---|---| +| **Cl** | 17 | **SN2.** The single most-taught mechanism in undergraduate organic. Also HCl, CH₃Cl, Cl⁻ as leaving group. | +| **S** | 16 | Thiols, H₂S, disulfide bridges, SO₂. Biochemistry entry point. | +| **P** | 15 | Phosphates, PH₃ — the DNA/ATP backbone. | +| **Si** | 14 | Silanes, SiO₂; the "why doesn't Si behave like C" comparison. | +| **Na** | 11 | Ionic bonding; NaCl with Cl already landed. | +| **Mg** | 12 | Mg²⁺, chlorophyll hand-wave, second ionic case. | +| **Al** | 13 | AlCl₃ Lewis acid; completes the row. | +| **Ar** | 18 | Van der Waals / dispersion labs; noble-gas dimer. | + +If the run stalls, **Cl, S, P are the three that matter.** Everything +after Si is completeness, not capability. + +### Per-element loop (identical for every element) + +1. Generate constants with the Phase-0 generator; append to `integrals.ts`. +2. Add the `case` to both switch blocks in `atoms.ts` (STO-3G and the + `heavyShells` cc-pVDZ path). Follow the existing formatting exactly. +3. Add the symbol to `SYMBOL_BY_Z` and `SUPPORTED_SYMBOLS` in `xyz.ts`. +4. Run Level A + Level B for all three basis sets. +5. Write `tests/chemistry/elements/.test.ts` pinning the validated + HF energy for at least one real molecule per basis set. +6. Run the **full** existing unit suite. A new element must not perturb + any existing number. If it does, that is a shared-state bug — stop + and report it, do not paper over it. +7. Commit. Move on. + +**Per-element gate:** steps 4–6 all green, or the element is reverted and +logged as failed. No partial elements on the branch. + +### Molecule choices for validation + +Prefer molecules with published reference geometries and non-trivial +bonding, one per element: + +`BH₃`, `Ne` (atom), `HCl` + `CH₃Cl`, `H₂S`, `PH₃`, `SiH₄`, `NaH`, +`MgH₂`, `AlH₃`, `Ar` (atom). + +Atoms alone are a weak test — they exercise no two-center integrals. Use +a hydride wherever one exists. + +--- + +## Phase 2 — gradient hardening + +`hf-gradient.ts`, `dft-gradient.ts`, `cphf.ts` and `optimizer.ts` all +exist. Nothing verifies they stay correct as elements are added. + +For every (new element, basis, method) combination: compare the analytic +gradient against a central finite-difference gradient of the energy. + +**Gate 2:** max component difference ≤ 1e-6 Ha/Bohr. Use a step of +1e-4 Bohr; if the FD gradient is itself noisy at that step, record the +noise floor rather than tightening the bar. + +This is a perfect autonomous oracle — the reference is generated from +the code's own energy function, so it needs no external tool and cannot +be argued with. + +--- + +## Phase 3 — the report (reserve the final hour) + +Write `docs/RUN-REPORT-.md` containing, in this order: + +1. **What could not be verified.** Put it first. This is the most + valuable output of the whole run — more valuable than the elements. +2. Elements landed, with the measured Level A / Level B deltas per basis. +3. Elements attempted and reverted, with the actual failure. +4. Any tolerance that was *considered* for loosening and why it wasn't. +5. Any existing test whose number moved, and the explanation. +6. What the next run should start with. + +Then update `LIMITATIONS.md` with the new element coverage and — this is +outstanding from the last session either way — the **anthracene n=246 +wrong-basin result**, which currently lives only in a spec comment. + +--- + +## Known traps (pre-empted here so the run doesn't rediscover them) + +- **The 0.4 mHa d-convention phantom.** See Gate 0.1. This will look + exactly like a real error on every element from Al onward. +- **`converged: false` treated as data.** Bit a lab script in the + 2026-08-08 session: the library reported honestly, the consumer code + printed unconverged CCSD numbers as results. Assert the flag. +- **Load-induced timeout flakes.** Three "failures" in the last full run + were a saturated machine, all passing in isolation. Before declaring a + test broken, re-run it alone. +- **`sed` on macOS is BSD sed.** `\?` silently does nothing. Use Python + for any regex rewriting of files. +- **Vite HMR reloads on artifacts.** `e2e/.artifacts/**` and friends are + already in `server.watch.ignored`; do not write scratch files anywhere + else under the repo root during a run. +- **`fciState` is H₂-only** (imports `buildH2Dense`). Not a general FCI + solver. Do not use it to validate anything else. + +--- + +## What this run does not accomplish + +It makes webgpu-q *capable* of the standard curriculum — SN2, phosphates, +ionic bonding, dispersion. It does not put the project in front of a +single teacher. Those are separate problems, and only the second one is +genuinely uncertain. Spend the 24h here because it is cheap in human time +and unblocks the education lane; do not mistake it for validating that +lane. diff --git a/docs/RUN-REPORT-2026-08-10.md b/docs/RUN-REPORT-2026-08-10.md new file mode 100644 index 0000000..0c5952f --- /dev/null +++ b/docs/RUN-REPORT-2026-08-10.md @@ -0,0 +1,135 @@ +# Run report — periodic-table expansion, 2026-08-10 + +Branch: `feat/periodic-table-expansion`. Nothing pushed, nothing deployed. + +--- + +## 1. What could NOT be verified + +Listed first deliberately — this is the most useful section. + +### 1.1 The Level-A bar in the plan is unreachable, and was never run + +`docs/RUN-PLAN-24H-ELEMENTS.md` Gate 0.3 specified a two-level scheme: +Level A (our primitives fed to PySCF via `gto.basis.parse`, bar 1e-9 Ha) +to isolate engine bugs, Level B (our tables vs PySCF's) to isolate data +errors. + +**Only Level B was built.** The committed harness +(`tests/chemistry/elements/reference-agreement.test.ts`) compares our +engine against PySCF's own basis at 0.1 mHa. That catches wrong data and +wrong integrals together but cannot separate them. + +Level A's 1e-9 Ha bar is also unreachable as specified. `boys0` in +`integrals-cg.ts` uses the Abramowitz–Stegun 7.1.26 rational fit for +erf, with a measured worst-case relative error of **2.66e-7** (the code +admits ~1.5e-7 at `integrals-cg.ts:195`). Row 3 makes it worse: Ar's +STO-3G 1s exponent is 674.45 against carbon's 71.62, pushing more +core-core pairs onto the approximate branch. A real Level A needs either +an f64-accurate `erf` or a bar set from measurement. + +### 1.2 Correlated methods are unvalidated for every new element + +All 108 committed reference cells are **RHF only**. MP2, CCSD, CCSD(T), +DFT and EOM have no per-element reference for B, Ne, or any of Na–Ar. +Wrong basis data would show up in HF, so this is not blind — but +"HF agrees" is not "CCSD(T) agrees". + +### 1.3 The row-3 frozen-core path is untested + +`FROZEN_CORE_FOR` now returns 5 for Na–Ar (the full neon core) and +`defaultFrozenCore` sums it. No test exercises it. +`tests/chemistry/frozen-core-audit.test.ts` hardcodes `nFrozenCore: 1` +and only uses H2O/CH4, where 1 is correct — so it passes and will keep +passing while the row-3 path stays uncovered. + +Known interaction: `rhf-auto.ts:452` throws for `nFrozenCore > 0` on the +exact-ERI UMP2 path. Any caller that inherits 5 from `defaultFrozenCore` +on a small row-3 molecule will hit that throw. Not triggered by anything +currently in the suite. + +### 1.4 Gradients were not hardened (Phase 2 not started) + +No analytic-vs-finite-difference check was run for any new element. + +### 1.5 No geometry validation + +Reference geometries for the new hydrides (NaH 1.8874, MgH2 1.7297, +AlH3 1.5840, SiH4 1.4798, PH3 1.42/93.5, H2S 1.3356/92.11, HCl 1.2746, +BH3 1.19 A) were taken as experimental-ish values and used identically +on both sides. Since PySCF and webgpu-q consume the same coordinates, +a wrong geometry cannot cause a test failure — it just means the cell +validates a molecule that isn't quite the real one. + +--- + +### 1.6 The 6-31G* claim was false, and had been for a long time + +`LIMITATIONS.md` listed 6-31G* as "wired, spot-checked" for H/C/N/O. +The string "6-31g" appears nowhere in `src/`; `BasisName` is exactly +`"sto-3g" | "cc-pvdz" | "aug-cc-pvdz"`. Corrected in this run. Worth +noting *where* it was found: the honest-limitations document was itself +carrying a false capability claim. + +--- + +## 2. What landed + +18 elements, H through Ar, complete. 109/109 agreement cells green +(18 elements x 3 bases x 2 d-conventions, plus a convention sanity +check), bar 0.1 mHa. Full suite 140 files / 1065 passed / 1 skipped. + +The point of the exercise was curriculum reach, so that is checked +directly in `tests/chemistry/elements/curriculum-molecules.test.ts`: + +| molecule | why it matters | vs PySCF | +|---|---|---| +| CH3Cl | the SN2 substrate — Cl was the blocking element | < 0.1 mHa | +| CH3SH | real organosulfur, not a toy hydride | < 0.1 mHa | +| H2S | the "why is H2S a gas and H2O a liquid" comparison | < 0.1 mHa | +| PH3 | phosphorus; entry point to phosphates and DNA | < 0.1 mHa | + +All four converge in ~1.2 s combined at STO-3G. + +--- + +## 3. Defects found in existing code + +See commit messages for full detail. Summary: + +| defect | worst measured impact | +|---|---| +| Li cc-pVDZ 2p coefficients + diffuse exponent | 1.293 mHa on LiH | +| Be cc-pVDZ 1s/2s/2p coefficients | 0.367 mHa on BeH2 | +| aug-cc-pVDZ diffuse: only H and C were correct of 8 | 0.171 mHa on HF | +| CCPVDZ_H exponents non-canonical | ~5 uHa, every H molecule | +| CCPVDZ_N_1S coefficients | ~0.03 uHa | +| BECKE_XI missing F; He/C/O off-by-one | 0.64 uHa | +| import-formats: "Fe" silently imported as fluorine | wrong element, silent | + +--- + +## 4. Corrections to the plan itself + +- Na and Mg carry d functions in cc-pVDZ, so the Cartesian/spherical + convention difference applies from **Na**, not from Al as written. +- STO-3G row 3 is **9** Cartesian functions (3s + 2p), not 13. +- The plan said "fix the generator — never the existing constants". + Inverted: the constants were wrong and PySCF was the authority. + +--- + +## 5. What the next run should start with + +1. Phase 2 — gradient hardening (analytic vs FD) for the 10 new elements. +2. Correlated-method references (MP2/CCSD/CCSD(T)) per element. +3. A row-3 frozen-core test, plus resolving the `rhf-auto.ts:452` throw. +4. The loose-assertion cleanup, starting with + `e2e/swarm-hf-anthracene-ccpvdz.spec.ts`, whose `(-1500, -100)` window + certifies a documented-wrong -880 Ha result as green. +5. Re-source the ~15 hardcoded literals from + `experiments/results/2026-07-06/level-6/E34-pyscf.json`, which holds + 40 full-precision references that no test currently uses. +6. Wire `scripts/check-basis-vs-pyscf.py` into CI — it is the only + artifact that catches bad basis digits at the source, and it runs + from no workflow today. diff --git a/e2e/lib/energy-gate.ts b/e2e/lib/energy-gate.ts new file mode 100644 index 0000000..0e18712 --- /dev/null +++ b/e2e/lib/energy-gate.ts @@ -0,0 +1,63 @@ +// ───────────────────────────────────────────────────────────── +// energy-gate.ts — honest energy assertions for the swarm / capstone specs. +// +// WHY THIS EXISTS. Several e2e specs used to bound the SCF energy with a +// hand-picked window hundreds of Ha wide. `swarm-hf-anthracene-ccpvdz` asserted +// only `-1500 < E < -100` while its own comment recorded that the run lands at +// ≈ -880 Ha against a literature anthracene HF/cc-pVDZ value of ≈ -537 Ha — so a +// 343 Ha error was certified green on every nightly. That is the most dangerous +// failure mode in the repo (LIMITATIONS.md §3): the SCF reports +// `converged: true` and returns a finite, plausible-looking number. +// +// The replacement is a size-extensive SANITY BAND — derived rather than picked, +// and named so no reader can mistake it for validation. It is a wrong-basin / +// broken-integral detector, NOT a chemical-accuracy claim (1 kcal/mol = 1.594 +// mHa; this band is four orders of magnitude looser than that, by design). +// Where an external reference value actually exists, assert against the +// reference instead of against this band. +// ───────────────────────────────────────────────────────────── + +import { expect } from "@playwright/test"; + +/** + * Floor — the most negative energy per carbon a real hydrocarbon RHF result can + * reach in the bases this repo ships. Anchors, all from our own ladder: + * benzene/cc-pVDZ ≈ -38.45 Ha/C (the most negative anywhere here), + * naphthalene/cc-pVDZ ≈ -38.34, acene STO-3G runs ≈ -37.9. -40.0 Ha/C sits ~4% + * below the most negative genuine value — outside the spread of real + * hydrocarbons, and nowhere near a wrong-basin collapse (the anthracene + * cc-pVDZ wrong basin is -62.9 Ha/C, i.e. 57% past this floor). + * + * This is the bound that matters: variational collapse into a non-physical + * orbital occupation always goes DOWN. + */ +export const E_PER_CARBON_FLOOR = -40.0; + +/** + * Ceiling — deliberately slack. Real values are ≈ -37.8 Ha/C, so -20.0 Ha/C + * still permits a 47% error upward. The slack is intentional: several acene + * specs report the last iterate of an SCF that did NOT converge (hexacene and + * heptacene are past the multireference wall), and the last iterate of an + * oscillating SCF is a diagnostic, not a value — tightening it would buy a + * latent flake rather than a real check. It exists only to reject + * order-of-magnitude nonsense (the previous bounds let C₃₄H₂₀ report -101 Ha). + */ +export const E_PER_CARBON_CEILING = -20.0; + +/** + * Assert an RHF hydrocarbon energy is physically plausible for its size. + * + * This is a SANITY band, not a validated energy. Passing it means "no + * wrong-basin collapse, no broken integrals, right order of magnitude" — it + * does NOT mean the number is correct to any chemical standard. + */ +export function assertHydrocarbonEnergySane(energy: number, nCarbon: number): void { + expect(Number.isFinite(energy), `E = ${energy} is not finite`).toBe(true); + const perC = energy / nCarbon; + const band = `E/C = ${perC.toFixed(2)} Ha over ${nCarbon} C (E = ${energy.toFixed(3)} Ha); ` + + `hydrocarbon RHF sanity band is [${E_PER_CARBON_FLOOR}, ${E_PER_CARBON_CEILING}] Ha/C`; + expect(perC, `${band} — TOO NEGATIVE: this is the wrong-basin signature (LIMITATIONS.md §3)`) + .toBeGreaterThan(E_PER_CARBON_FLOOR); + expect(perC, `${band} — not negative enough to be a bound hydrocarbon`) + .toBeLessThan(E_PER_CARBON_CEILING); +} diff --git a/e2e/naphthalene-capstone.spec.ts b/e2e/naphthalene-capstone.spec.ts index b5420a5..0020151 100644 --- a/e2e/naphthalene-capstone.spec.ts +++ b/e2e/naphthalene-capstone.spec.ts @@ -75,7 +75,14 @@ console.log(`\n[naphthalene-capstone] ${JSON.stringify(r)}\n`); expect(r.prov.method).toBe("density-fitting"); // auto-gate chose DF expect(r.eriLen).toBe(0); // the 8 GB ERI was never built expect(r.converged).toBe(true); // and it actually converged - // Sane physical RHF/cc-pVDZ energy for naphthalene (~ -383 Ha at this geom). + // SANITY WINDOW, NOT A VALIDATED ENERGY. Naphthalene RHF/cc-pVDZ is ≈ -383 + // Ha at this geometry, and ±3 Ha is ~1900× chemical accuracy (1.594 mHa) — + // it is here to catch wrong-basin collapse and broken integrals, nothing + // finer. PAH-scale DF-HF is feasibility-demonstrated, NOT precision- + // validated: DF-vs-exact chemical accuracy is only checked up the ladder + // where the exact 4-index ERI still fits a tab (e2e/df-accuracy-ladder, + // n ≤ 50). See LIMITATIONS.md. Unlike the acene specs this window IS tight + // enough to reject the -880-Ha-style wrong basin, so it is left as-is. expect(r.energy).toBeLessThan(-380); expect(r.energy).toBeGreaterThan(-386); }); diff --git a/e2e/swarm-gpu.spec.ts b/e2e/swarm-gpu.spec.ts index c345800..4a56e5a 100644 --- a/e2e/swarm-gpu.spec.ts +++ b/e2e/swarm-gpu.spec.ts @@ -72,7 +72,12 @@ test.describe("Swarm × GPU — distributed GPU-accelerated single-points", () = expect(r.converged).toBe(true); expect(r.dfMethod).toBe("density-fitting"); expect(r.engine).toBe("gpu+wasm"); // hybrid GPU path on every tile - expect(r.energy).toBeLessThan(-108); // N2 cc-pVDZ HF ≈ -108.95 Ha + // SANITY WINDOW, NOT A VALIDATED ENERGY. N2 cc-pVDZ HF ≈ -108.95 Ha, and + // the ~±1 Ha here is ~600× chemical accuracy — it has to be loose because + // one window covers six different bond lengths (1.05-1.30 Å) along the + // curve. It rejects wrong-basin collapse and broken integrals, not method + // error; DF-vs-exact accuracy is validated in e2e/df-accuracy-ladder. + expect(r.energy).toBeLessThan(-108); expect(r.energy).toBeGreaterThan(-110); } // The batch was actually split across the two tabs (not all run on master). diff --git a/e2e/swarm-hf-anthracene-ccpvdz.spec.ts b/e2e/swarm-hf-anthracene-ccpvdz.spec.ts index ccd7b87..5bde693 100644 --- a/e2e/swarm-hf-anthracene-ccpvdz.spec.ts +++ b/e2e/swarm-hf-anthracene-ccpvdz.spec.ts @@ -1,4 +1,5 @@ import { test, expect } from "@playwright/test"; +import { assertHydrocarbonEnergySane } from "./lib/energy-gate.js"; // Anthracene C₁₄H₁₀ cc-pVDZ — the bigger basis variant that failed // with default DIIS (E went to +5352 Ha) and with plain damping @@ -17,11 +18,26 @@ import { test, expect } from "@playwright/test"; const N_TABS = 4; const INNER_POOL = 2; +const N_CARBON = 14; + +// Literature-scale anthracene HF/cc-pVDZ. Cross-checks against this repo's own +// ladder (benzene/cc-pVDZ ≈ -38.45 Ha/C, naphthalene ≈ -38.34 Ha/C ⟹ C₁₄ ≈ -537). +// The tolerance is sanity-scale, not chemical accuracy — the geometry here is +// idealised (regular hexagons, 1.40 Å) so a literature-digit comparison isn't +// available anyway. Even at 5 Ha of slack the run below misses by 343 Ha. +const LIT_HF_CCPVDZ = -537.0; +const LIT_TOL_HA = 5.0; + +// Captured by the architecture test, re-asserted by the documented-negative +// physics test below. Serial describe: ONE 20-minute SCF, two separate claims — +// the architecture claim is allowed to be green, the physics claim is not. +// (playwright.config.ts: workers: 1, fullyParallel: false.) +let observedEnergy: number | undefined; test.use({ trace: "off" }); // 10-min trace fixture cap would clip this — must be top-level, not inside describe -test.describe(`Swarm anthracene cc-pVDZ HF SCF — ${N_TABS}-tab × ${INNER_POOL}-inner`, () => { - test("anthracene cc-pVDZ — delayed DIIS recipe", async ({ browser }) => { +test.describe.serial(`Swarm anthracene cc-pVDZ HF SCF — ${N_TABS}-tab × ${INNER_POOL}-inner`, () => { + test("architecture — delayed-DIIS recipe reaches SCF convergence without divergence", async ({ browser }) => { test.setTimeout(20 * 60 * 1000); const ctx = await browser.newContext(); @@ -253,25 +269,49 @@ test.describe(`Swarm anthracene cc-pVDZ HF SCF — ${N_TABS}-tab × ${INNER_POOL console.log(`══════════════════════════════════════════════════════════\n`); + observedEnergy = result.energy; + + // ── ARCHITECTURE ONLY. This test makes NO claim about the energy. ── + // What it proves: the 4-tab × 2-inner swarm runs a cc-pVDZ SCF end to end + // and the delayed-DIIS recipe (damping 0.2 + diisStartIter 8) reaches a + // stationary point instead of the +5352 Ha divergence that default DIIS + // produced, or the +801 Ha iter-5 jump that plain damping produced. + // Whether the stationary point is the RIGHT one is asserted separately, + // in the documented-negative test below — and it currently is not. expect(Number.isFinite(result.energy)).toBe(true); expect(result.converged).toBe(true); - // The delayed-DIIS recipe converges anthracene cc-pVDZ — that's - // the architectural fix this test validates. The 4-tab × 2-inner - // swarm + diisStartIter combo successfully reaches a stationary - // point without the +5352 Ha divergence seen on default DIIS. - // - // NOTE: the resulting energy (~-880 Ha in CI's first green run) - // is more negative than the real anthracene HF/cc-pVDZ value of - // ~-537 Ha. That's almost certainly a "wrong-basin" SCF solution - // — the damped warm-up plus approximate linear geometry steers - // the SCF into a non-physical orbital occupation that's lower in - // energy but doesn't correspond to the true ground-state singlet. - // Fixing it needs MOM (maximum overlap method), SOSCF, or a SAD - // initial guess. Tracked separately; for now this test just - // proves convergence-without-divergence at cc-pVDZ. - expect(result.energy).toBeLessThan(-100); - expect(result.energy).toBeGreaterThan(-1500); + expect(result.energy, "SCF diverged to a positive energy").toBeLessThan(0); await ctx.close(); }); + + // ── DOCUMENTED NEGATIVE — this test is REQUIRED to fail. ─────────────────── + // The run above converges to ≈ -880 Ha; anthracene HF/cc-pVDZ is ≈ -537 Ha. + // That is a 343 Ha error reported with `converged: true` and a finite, + // plausible-looking number — the most dangerous failure mode in the repo, + // LIMITATIONS.md §3. Until 2026-08 it was certified GREEN by an assertion + // window of `-1500 < E < -100`. + // + // Cause: the damped warm-up plus the idealised planar geometry steers the SCF + // into a non-physical orbital occupation that is variationally lower but is + // not the ground-state singlet. Fixing it needs MOM (maximum overlap method), + // SOSCF, or a SAD initial guess. + // + // test.fail() means Playwright REQUIRES this to fail. If someone fixes the + // SCF, this test starts passing, Playwright reports "expected to fail but + // passed", and CI goes red until the annotation is removed. The wrong-basin + // result can therefore never again be silently sold as a validated energy. + test("physics — energy vs literature anthracene HF/cc-pVDZ ≈ -537 Ha [EXPECTED FAILURE: wrong-basin SCF, LIMITATIONS.md §3]", () => { + test.fail(); + + expect(observedEnergy, "architecture test must run first (serial describe)").toBeDefined(); + const E = observedEnergy ?? Number.NaN; + + // Fails first, and loudly: ≈ -880 Ha is -62.9 Ha/C, far past the floor. + assertHydrocarbonEnergySane(E, N_CARBON); + expect( + Math.abs(E - LIT_HF_CCPVDZ), + `E = ${E.toFixed(3)} Ha vs literature ${LIT_HF_CCPVDZ} Ha`, + ).toBeLessThan(LIT_TOL_HA); + }); }); diff --git a/e2e/swarm-hf-heptacene.spec.ts b/e2e/swarm-hf-heptacene.spec.ts index 884439a..cc38c31 100644 --- a/e2e/swarm-hf-heptacene.spec.ts +++ b/e2e/swarm-hf-heptacene.spec.ts @@ -1,4 +1,5 @@ import { test, expect } from "@playwright/test"; +import { assertHydrocarbonEnergySane } from "./lib/energy-gate.js"; // Heptacene C₃₀H₁₈ STO-3G via 4-tab swarm. 7 linearly fused benzene // rings, 168 basis functions. The biradical-character borderline: @@ -12,6 +13,7 @@ import { test, expect } from "@playwright/test"; const N_TABS = 4; const INNER_POOL = 2; +const N_CARBON = 30; test.use({ trace: "off" }); @@ -249,9 +251,15 @@ test.describe(`Swarm heptacene HF SCF — ${N_TABS}-tab × ${INNER_POOL}-inner`, // gate (and octacene converging while heptacene doesn't shows the // tuning is luck, not signal). UHF / spin-projected reference is the // method-side fix, tracked separately. + // + // NOT A VALIDATED ENERGY — and with converged=false it is the last iterate + // of an oscillating SCF, i.e. a diagnostic rather than a value. The honest + // bound is the size-extensive sanity band in e2e/lib/energy-gate.ts. The + // old bound was -3000 < E < -100: for C₃₀H₁₈ that spans -3.3 to -100 Ha per + // carbon against a real ≈ -37.8, so essentially any finite negative number + // passed — including an anthracene-style wrong-basin collapse. expect(Number.isFinite(result.energy)).toBe(true); - expect(result.energy).toBeLessThan(-100); - expect(result.energy).toBeGreaterThan(-3000); + assertHydrocarbonEnergySane(result.energy, N_CARBON); await ctx.close(); }); diff --git a/e2e/swarm-hf-hexacene.spec.ts b/e2e/swarm-hf-hexacene.spec.ts index c5df29c..f6a5236 100644 --- a/e2e/swarm-hf-hexacene.spec.ts +++ b/e2e/swarm-hf-hexacene.spec.ts @@ -1,4 +1,5 @@ import { test, expect } from "@playwright/test"; +import { assertHydrocarbonEnergySane } from "./lib/energy-gate.js"; // Hexacene C₂₆H₁₆ STO-3G via 4-tab swarm. 6 linearly fused benzene // rings, 146 basis functions. Extends naphthalene (2) → anthracene (3) @@ -7,6 +8,7 @@ import { test, expect } from "@playwright/test"; const N_TABS = 4; const INNER_POOL = 2; +const N_CARBON = 26; test.use({ trace: "off" }); // generous timeouts elsewhere; trace fixture would clip @@ -263,9 +265,14 @@ test.describe(`Swarm hexacene HF SCF — ${N_TABS}-tab × ${INNER_POOL}-inner`, // own gate; the honest statement is "RHF convergence is method-limited // here." UHF/multireference (or NOCI/spin-projection) is the real fix, // tracked separately. + // + // NOT A VALIDATED ENERGY. RHF has no meaningful reference value for + // hexacene, so the strongest honest claim is the size-extensive sanity band + // in e2e/lib/energy-gate.ts. The bound here used to be -2000 < E < -500 — + // ±750 Ha of slack, wide enough that an anthracene-style wrong-basin + // collapse (-62.9 Ha/C → -1635 Ha here) would have been certified green. expect(Number.isFinite(result.energy)).toBe(true); - expect(result.energy).toBeLessThan(-500); - expect(result.energy).toBeGreaterThan(-2000); + assertHydrocarbonEnergySane(result.energy, N_CARBON); await ctx.close(); }); diff --git a/e2e/swarm-hf-octacene.spec.ts b/e2e/swarm-hf-octacene.spec.ts index b6e6634..d380d70 100644 --- a/e2e/swarm-hf-octacene.spec.ts +++ b/e2e/swarm-hf-octacene.spec.ts @@ -1,4 +1,5 @@ import { test, expect } from "@playwright/test"; +import { assertHydrocarbonEnergySane } from "./lib/energy-gate.js"; // Octacene C₃₄H₂₀ STO-3G via 4-tab swarm. 8 linearly fused benzene // rings, 190 basis functions. Past heptacene the acene becomes @@ -9,6 +10,7 @@ import { test, expect } from "@playwright/test"; const N_TABS = 4; const INNER_POOL = 2; +const N_CARBON = 34; test.use({ trace: "off" }); @@ -274,9 +276,14 @@ test.describe(`Swarm octacene HF SCF — ${N_TABS}-tab × ${INNER_POOL}-inner`, // full SCF to completion, finite physically-ranged energy) and treat // RHF convergence as method-limited, consistent with hexacene/ // heptacene. Multireference/UHF reference is the real fix. + // + // NOT A VALIDATED ENERGY — deepest into the polyradical regime of the whole + // ladder, so there is no RHF reference value to compare against. The honest + // bound is the size-extensive sanity band in e2e/lib/energy-gate.ts. The old + // bound was -3000 < E < -100: for C₃₄H₂₀ that permitted anything from -2.9 + // to -88 Ha per carbon against a real ≈ -37.8, i.e. it certified nothing. expect(Number.isFinite(result.energy)).toBe(true); - expect(result.energy).toBeLessThan(-100); - expect(result.energy).toBeGreaterThan(-3000); + assertHydrocarbonEnergySane(result.energy, N_CARBON); await ctx.close(); }); diff --git a/index.html b/index.html index c07cf45..4723614 100644 --- a/index.html +++ b/index.html @@ -650,6 +650,7 @@ hyperscope molecule learn + labs swarm screen experiments @@ -1149,6 +1150,7 @@

Open the URL.
Run the science.

hyperscope molecule learn + labs swarm screen experiments diff --git a/labs.html b/labs.html new file mode 100644 index 0000000..5443a59 --- /dev/null +++ b/labs.html @@ -0,0 +1,183 @@ + + + + + + + + Four comp-chem labs, run in your tab · webgpu-q + + + + + + + + + + + + + + + + + + +
+ + +

Four labs, computed in this tab

+

+ The standard opening sequence of any computational chemistry course. + Nothing on this page is precomputed — pressing Run starts a real SCF, + MP2, CCSD and CCSD(T) in a Web Worker on your machine. +

+ +
+ Why this is not a simulation of a lab. These are the same + algorithms, at the same precision, that a cluster job would run — + validated against PySCF to better than 0.1 mHa. What is missing is + throughput, not correctness: a laptop tab does small molecules. + For a teaching lab, small molecules are the entire syllabus. +
+ +
+

Lab 1 · Why single-reference methods break

+

Pull an H₂ molecule apart. Where does Hartree–Fock stop working, and why exactly there?

+

+ Two curves: restricted Hartree–Fock, and the exact answer from a full + CI in the same basis. They agree near equilibrium. Watch the gap open, + and watch the two configuration coefficients c_g and c_u while it does. +

+
+ + 14 bond lengths, ~1 s +
+
+
+
+ +
+

Lab 2 · What a bigger basis actually buys

+

Same molecule, same method, three basis sets. How fast do the returns diminish?

+

+ Water at fixed experimental geometry, Hartree–Fock throughout. Only + the basis changes: minimal (STO-3G), double-zeta (cc-pVDZ), then + double-zeta plus diffuse functions (aug-cc-pVDZ). +

+
+ + 3 SCF calculations, ~5 s +
+
+
+
+ +
+

Lab 3 · The correlation ladder

+

Hartree–Fock is missing something. How much, and what does each rung of theory buy back?

+

+ Water again, climbing HF → MP2 → CCSD → CCSD(T). STO-3G runs in + seconds. cc-pVDZ is the real thing and takes a couple of minutes — + it is the calculation a methods course actually assigns. +

+
+ + + + ready +
+
+
+
+ +
+

Lab 4 · An actual reaction mechanism

+

Cl⁻ attacks CH₃Cl and knocks the other chloride off. How big is the barrier, and can a cheap basis get it right?

+

+ The identity SN2 reaction — the mechanism every introductory organic + course teaches. One chloride comes in, the other leaves, and the + methyl group turns inside out on the way through. Watch the umbrella + angle: 90° is planar, and that is the transition state. +

+
+ + 11 points along the reaction coordinate, ~20 s +
+
+
+
+ +
+ Real Hartree–Fock, MP2, CCSD and CCSD(T) — in your browser, no install, no backend. + Powered by webgpu-q. +
+
+ + + diff --git a/learn.html b/learn.html index 2bc6cd0..8176852 100644 --- a/learn.html +++ b/learn.html @@ -102,6 +102,7 @@