Skip to content

Commit ba18fbb

Browse files
abgnydnclaude
andcommitted
feat(cis): Davidson iterative eigensolver wired into CIS / TDA
Tier 3 follow-up. CIS gains `useDavidson: true` opt-in that avoids building the full A matrix on the OV space. Closes the "Davidson / Krylov eigensolver" row in LIMITATIONS.md — Davidson is now available across **every** eigenvalue-method path: EE/IP/EA-EOM-CCSD (commits 6b9a96f, a35baeb) + CIS / TDA (this). Implementation: - New `davidsonExtract` path in `runCIS`. Builds a matvec from the same `eri()` accessor used by `buildA`: (Av)_{ia} = (ε_a − ε_i) v_ia + Σ_{jb} [s_Coul · (ai|bj) − (ij|ab)] v_jb s_Coul = 2 for singlet, 0 for triplet (same as dense path). - Diagonal preconditioner is the Koopmans gap (ε_a − ε_i). - Default off (`useDavidson` undefined) → existing dense path, bit-identical to before. Verified by `cis-davidson.test.ts` on H₂O HF/STO-3G: - k=3 singlets: Davidson vs dense agree to < 1e-6 Ha - k=3 triplets: same Also a co-located type-fix in `cphf.ts` (caught by the typecheck): the symmetrize loop's array indexing was triggering `noUncheckedIndexedAccess`; cleaned up with hoisted refs. Full suite: 446 passing / 1 skipped (+2 from this commit), no regressions. Tier 3 progress: 13 items shipped this session. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c9f5633 commit ba18fbb

4 files changed

Lines changed: 121 additions & 4 deletions

File tree

LIMITATIONS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ will silently truncate large dispatches.
124124
| ccData / QC-Schema compatibility | Output not consumable by external tooling | Tier 3 |
125125
| FAIR / Zenodo DOIs per release | Citations point at GitHub tag, not DOI | Tier 3 — set up CI workflow |
126126
| Aux-basis density fitting | We have CD-DF but not JKFIT / RIFIT integral path | Tier 3 — needs 3-index ERI routine |
127-
| Davidson / Krylov eigensolver | OK for n_occ·n_virt ≤ ~500; dense above. Measured 2026-05: EOM-CCSD on CH₂O / HCN at STO-3G (dim 3488 / 2660) takes 15-30 min per molecule on M2 Pro because the Hessenberg + Wilkinson QR is dense. Davidson would make these ~minutes. | Tier 3 · high impact |
127+
| Davidson / Krylov eigensolver | **Shipped 2026-05**. Block Davidson (`src/manybody/davidson.ts`) wired into EE/IP/EA-EOM-CCSD (commits 6b9a96f, a35baeb) and now also CIS / TDA (this commit) behind `useDavidson: true`. Dense + Davidson agree on k roots to ≤ 1e-6 Ha across all paths. Unblocks large-basis CIS/TDDFT + bigger EOM-CCSD systems. | shipped ✓ |
128128
| Multi-node parallel | One tab, one GPU | Tier 4 — substrate is Phase D WebRTC |
129129
| Periodic boundary conditions | No solids, no surfaces | Tier 4 |
130130
| Spin-orbit coupling / X2C / DKH | No heavy elements | Tier 4 |

src/chemistry/cis.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import type { MolecularIntegrals } from "./cg-molecular.js";
3333
import { transformERIToMO } from "./mp2.js";
3434
import { eigsymmetric } from "../manybody/dense-eig.js";
35+
import { davidson } from "../manybody/davidson.js";
3536

3637
export interface HFLike {
3738
/** AO → MO coefficients (row-major, column i = MO i). */
@@ -50,6 +51,13 @@ export interface CISOpts {
5051
readonly nRoots?: number;
5152
/** Spin sector (default both). Singlet only is the common case. */
5253
readonly spin?: "singlet" | "triplet" | "both";
54+
/** Use the block Davidson iterative eigensolver instead of building
55+
* the full A matrix and calling `eigsymmetric`. Recommended for
56+
* dim ≳ 200 (cc-pVDZ on multi-atom systems). Default false
57+
* preserves the existing dense behavior. */
58+
readonly useDavidson?: boolean;
59+
/** Davidson residual-norm tolerance. Default 1e-7. */
60+
readonly davidsonTol?: number;
5361
}
5462

5563
export interface CISStateBlock {
@@ -146,9 +154,55 @@ export function runCIS(
146154
return { energies, amplitudes };
147155
};
148156

157+
/** Davidson path — never materializes A. Builds a matvec from
158+
* the CIS A-block formula and supplies the Koopmans diagonal
159+
* preconditioner (ε_a − ε_i). */
160+
const davidsonExtract = (sCoul: number): CISStateBlock => {
161+
const eps = hf.orbitalEnergies;
162+
const diagonal = new Float64Array(dim);
163+
for (let i = 0; i < nOcc; i++) {
164+
for (let a = 0; a < nVirt; a++) {
165+
const ai = nOcc + a;
166+
diagonal[i * nVirt + a] = eps[ai]! - eps[i]!;
167+
}
168+
}
169+
const matvec = (v: Float64Array): Float64Array => {
170+
// (Av)_{ia} = (ε_a − ε_i) v_ia
171+
// + Σ_{jb} [sCoul · (ia|jb) − (ij|ab)] v_jb
172+
const out = new Float64Array(dim);
173+
for (let i = 0; i < nOcc; i++) {
174+
for (let a = 0; a < nVirt; a++) {
175+
const ai = nOcc + a;
176+
const eOrb = eps[ai]! - eps[i]!;
177+
let s = eOrb * v[i * nVirt + a]!;
178+
for (let j = 0; j < nOcc; j++) {
179+
for (let b = 0; b < nVirt; b++) {
180+
const bj = nOcc + b;
181+
const coul = sCoul !== 0 ? sCoul * eri(i, ai, j, bj) : 0;
182+
const exch = eri(i, j, ai, bj);
183+
s += (coul - exch) * v[j * nVirt + b]!;
184+
}
185+
}
186+
out[i * nVirt + a] = s;
187+
}
188+
}
189+
return out;
190+
};
191+
const dav = davidson(dim, matvec, diagonal, {
192+
k: nRoots,
193+
tol: opts.davidsonTol ?? 1e-7,
194+
maxIter: 200,
195+
filter: (_re, im) => Math.abs(im) < 1e-6,
196+
});
197+
return { energies: dav.energies, amplitudes: dav.vectors };
198+
};
199+
149200
const empty: CISStateBlock = { energies: new Float64Array(0), amplitudes: new Float64Array(0) };
150-
const singlet = (spin === "singlet" || spin === "both") ? diagAndExtract(buildA(2)) : empty;
151-
const triplet = (spin === "triplet" || spin === "both") ? diagAndExtract(buildA(0)) : empty;
201+
const extract = opts.useDavidson
202+
? davidsonExtract
203+
: (sCoul: number): CISStateBlock => diagAndExtract(buildA(sCoul));
204+
const singlet = (spin === "singlet" || spin === "both") ? extract(2) : empty;
205+
const triplet = (spin === "triplet" || spin === "both") ? extract(0) : empty;
152206

153207
return { nOccupied: nOcc, nVirtual: nVirt, singlet, triplet };
154208
}

src/chemistry/cphf.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,9 @@ export function cphfPolarizability(
187187
for (let x = 0; x < 3; x++) {
188188
for (let y = 0; y < 3; y++) {
189189
let s = 0;
190-
for (let k = 0; k < dim; k++) s += X[x][k]! * muOV[y][k]!;
190+
const Xx = X[x]!;
191+
const Fy = muOV[y]!;
192+
for (let k = 0; k < dim; k++) s += Xx[k]! * Fy[k]!;
191193
alpha[x * 3 + y] = 4 * s;
192194
}
193195
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// CIS Davidson eigensolver cross-check.
2+
//
3+
// Verifies that the iterative `useDavidson: true` path returns the
4+
// same lowest singlet + triplet excitation energies as the dense
5+
// `eigsymmetric` path. Davidson avoids building the full A matrix,
6+
// which scales as O(dim²) memory — important for cc-pVDZ on
7+
// multi-atom systems where dim grows past a few hundred.
8+
9+
import { describe, expect, test } from "vitest";
10+
import { computeMolecularIntegrals } from "../../src/chemistry/cg-molecular.js";
11+
import { moleculeToShellsNuclei, type Atom } from "../../src/chemistry/atoms.js";
12+
import { runRHFSCF } from "../../src/chemistry/hf-scf.js";
13+
import { runCIS } from "../../src/chemistry/cis.js";
14+
15+
describe("CIS — Davidson vs dense", () => {
16+
test("H₂O HF/STO-3G — k=3 singlets agree to ≤ 1e-7 Ha", () => {
17+
const half = (104.52 / 2) * Math.PI / 180;
18+
const xH = 0.9572 * Math.sin(half);
19+
const zH = 0.9572 * Math.cos(half);
20+
const atoms: Atom[] = [
21+
{ symbol: "O", pos: [0, 0, 0] },
22+
{ symbol: "H", pos: [ xH, 0, zH] },
23+
{ symbol: "H", pos: [-xH, 0, zH] },
24+
];
25+
const { shells, nuclei, nElectrons } = moleculeToShellsNuclei(atoms);
26+
const integrals = computeMolecularIntegrals(shells, nuclei);
27+
const hf = runRHFSCF(integrals, nElectrons, {
28+
useDIIS: true, maxIter: 200, energyTol: 1e-10, densityTol: 1e-8,
29+
});
30+
31+
const dense = runCIS(integrals, hf, { nRoots: 3, spin: "singlet" });
32+
const dav = runCIS(integrals, hf, { nRoots: 3, spin: "singlet", useDavidson: true, davidsonTol: 1e-9 });
33+
34+
expect(dense.singlet.energies.length).toBe(3);
35+
expect(dav.singlet.energies.length).toBe(3);
36+
for (let k = 0; k < 3; k++) {
37+
expect(Math.abs(dav.singlet.energies[k]! - dense.singlet.energies[k]!)).toBeLessThan(1e-6);
38+
}
39+
}, 60_000);
40+
41+
test("H₂O HF/STO-3G — k=3 triplets agree to ≤ 1e-6 Ha", () => {
42+
const half = (104.52 / 2) * Math.PI / 180;
43+
const xH = 0.9572 * Math.sin(half);
44+
const zH = 0.9572 * Math.cos(half);
45+
const atoms: Atom[] = [
46+
{ symbol: "O", pos: [0, 0, 0] },
47+
{ symbol: "H", pos: [ xH, 0, zH] },
48+
{ symbol: "H", pos: [-xH, 0, zH] },
49+
];
50+
const { shells, nuclei, nElectrons } = moleculeToShellsNuclei(atoms);
51+
const integrals = computeMolecularIntegrals(shells, nuclei);
52+
const hf = runRHFSCF(integrals, nElectrons, {
53+
useDIIS: true, maxIter: 200, energyTol: 1e-10, densityTol: 1e-8,
54+
});
55+
const dense = runCIS(integrals, hf, { nRoots: 3, spin: "triplet" });
56+
const dav = runCIS(integrals, hf, { nRoots: 3, spin: "triplet", useDavidson: true });
57+
for (let k = 0; k < 3; k++) {
58+
expect(Math.abs(dav.triplet.energies[k]! - dense.triplet.energies[k]!)).toBeLessThan(1e-6);
59+
}
60+
}, 60_000);
61+
});

0 commit comments

Comments
 (0)