Skip to content
Open
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
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 51 additions & 16 deletions LIMITATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down Expand Up @@ -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.

---

Expand Down
254 changes: 254 additions & 0 deletions docs/RUN-PLAN-24H-ELEMENTS.md
Original file line number Diff line number Diff line change
@@ -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) <noreply@anthropic.com>`
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/<sym>.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-<date>.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.
Loading
Loading