Skip to content

Bind tpb::SBD::seed, so a random initial vector is actually controllable - #48

Merged
Sophia Wen (hfwen0502) merged 4 commits into
mainfrom
bind-tpb-seed
Sep 30, 2026
Merged

Sophia Wen (hfwen0502) merged 4 commits into
mainfrom
bind-tpb-seed

Conversation

@hfwen0502

@hfwen0502 Sophia Wen (hfwen0502) commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

TPB_SBD exposes init but not seed. Since init = 1 means "random initial vector" — tpb/sbdiag.h:312 passes both to BasisInitVector — every random-start TPB run was pinned to upstream's default 1729, with no way to vary it. That defeats the point of a random start: checking whether a result depends on where the solver began.

GDB_SBD has bound seed all along, so this was an asymmetry between the two config structs rather than a deliberate omission.

The change

Two lines in python/bindings.cpp mirroring GDB's entry, plus --seed on examples/tpb/run_sbd_diag.py — the driver already had --init, so you could ask for a random start without being able to choose which one. Both help strings now say that --init 1 is the mode --seed affects, since neither name conveys that alone.

Verification

Done by hand rather than as a shipped test, to keep the test surface proportionate to a two-line binding.

check result
round-trip config.seed = 4242 → reads back 4242, maps as int
different seeds change the start vector tol=2.71579, -68.5062 vs tol=2.70452, -68.4933
same seed reproduces identical to every digit
converged energy unaffected both → -76.2359466308, matching the pinned reference

The probe is SBD's own Davidson iteration 0.0 line, whose energy is the Rayleigh quotient of the starting vector — a round-trip check alone would pass against a field that stores what you set and is then ignored. I also confirmed the comparison discriminates: forcing both runs to one seed makes the start vectors identical.

Through the driver on a 24-alpha h2o subspace: --seed 1729 starts at tol=1.11868 / -73.3618, --seed 987654321 at tol=1.14711 / -73.2877, both converging to -76.0588897208.

4 passed serial, 3 passed under mpirun -n 2.

Not covered: there is no regression guard. If the binding is later removed, or upstream stops honouring seed, nothing fails — the field would keep storing what you set while being ignored.

Related, not included

initial_adeterminant_bitstring / initial_bdeterminant_bitstring (TPB) and initial_determinant_bitstring (GDB) are also unbound. Those let you name the starting determinant instead of taking whichever sorts first, and both solvers already consume them (tpb/sbdiag.h:335-338, gdb/sbdiag.h:292-305). GDB_SBD.timing_barriers is unbound too. Each is its own change.

Separately worth an upstream issue: max_time is narrower than it looks — GDB's CPU Davidson overloads (gdb/davidson.h:41, :317) do not take it, and gdb/sbdiag.h passes it only at :370 inside the Thrust branch, so it is inert on _core_cpu and _core_gpu_omp_offload.

🤖 Generated with Claude Code

We expose `init`, and `init == 1` means "random start vector"
(tpb/sbdiag.h:312 passes it to BasisInitVector along with `seed`), but `seed` itself
was never bound -- so every random-start TPB run was pinned to upstream's default
1729 with no way to vary it. That defeats the one thing a random start is for:
checking whether a result depends on where the solver began. GDB has bound `seed`
all along, so this was an asymmetry between the two config structs rather than a
decision.

Two lines, mirroring GDB's entry, with the docstring noting it only applies at
init = 1 -- which the field name does not convey.

test/test_config_bindings.py is new, and deliberately does not stop at a
round-trip: a field that stores what you set and is then ignored would pass that.
It compares SBD's own "Davidson iteration 0.0" line, whose energy is the Rayleigh
quotient of the starting vector, across two seeds -- 1729 gives tol=2.71579 where
987654321 gives tol=2.70452 on the full case -- and asserts the same seed
reproduces exactly while both converge to the same eigenvalue.

Verified the assertion discriminates: forcing both runs to one seed makes the
start vectors identical and the test fails, so it catches "bound but ignored"
rather than only "not bound".

Runs on a 24-alpha subspace against the -76.0588897208 anchor rather than the full
275-alpha reference case. A random start needs many more Davidson iterations than
the default one, and the full case took 133 s where this takes 10 s.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The driver already exposed --init, so you could ask for a random starting vector
but not choose which one -- the same gap the binding fix closes, one layer up.
Without this the fix is reachable only by writing Python against the binding.

Also says in --init's help that 1 is the mode --seed affects, since neither name
conveys that on its own.

Verified end to end on a 24-alpha h2o subspace: --seed 1729 starts at tol=1.11868
/ -73.3618 and --seed 987654321 at tol=1.14711 / -73.2877, both converging to
-76.0588897208.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Too much test surface for a two-line config binding. The behaviour was verified by
hand instead and the evidence is in the previous commits and the PR description:
--seed 1729 starts at tol=2.71579 where 987654321 starts at tol=2.70452, the same
seed reproduces exactly, and both converge to -76.2359466308.

What is given up is the regression guard. If someone later removes the binding or
upstream stops honouring `seed`, nothing fails -- the field would keep storing what
you set while being ignored, which is the failure mode the deleted test was built
to catch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Upstream documents init==0 as "the HF solution / start from fermi sea"
(tpb/davidson.h:50), but the code is `W[0] = 1.0` -- unit weight on the first
determinant of the basis, with no search for Hartree-Fock. Identical in GDB
(gdb/davidson.h:18). The two coincide only when the basis is ordered so HF sorts
first, and a subspace of sampled determinants generally is not: that is how a run
can start on a high-order excitation with no coupling to the rest and return a
diagonal element as its energy.

Also notes that only 0 and 1 exist, and that other values start from a zero vector
without erroring -- there is no else branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@hfwen0502
Sophia Wen (hfwen0502) merged commit 551c59b into main Sep 30, 2026
10 checks passed
@hfwen0502
Sophia Wen (hfwen0502) deleted the bind-tpb-seed branch September 30, 2026 21:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant