Skip to content

sbd_solver: populate SCIResult.rdm1/rdm2 from SBD's computed RDMs - #36

Merged
Sophia Wen (hfwen0502) merged 1 commit into
mainfrom
sbd-rdm
Sep 17, 2026
Merged

Sophia Wen (hfwen0502) merged 1 commit into
mainfrom
sbd-rdm

Conversation

@hfwen0502

@hfwen0502 Sophia Wen (hfwen0502) commented Sep 17, 2026

Copy link
Copy Markdown
Member

Summary

  • sbd_solver.py's _solve_sci_core calls backend.tpb_diag(...), which
    (per bindings.cpp) always returns results["one_p_rdm"] and
    results["two_p_rdm"] in its dict — populated with real data whenever
    sbd_config["do_rdm"] is set to 1. _solve_sci_core read
    results["energy"]/results["density"] but never read
    one_p_rdm/two_p_rdm — every SCIResult it returned left rdm1/rdm2
    at their dataclass default of None, even when a caller explicitly
    requested RDMs. SBD computed real RDMs and the wrapper threw them away.
  • Adds assemble_rdms(results, norb) (public — used by more than just this
    module now) that reshapes SBD's raw flat arrays into rdm1 (norb,norb)
    / rdm2 (norb,norb,norb,norb) and wires it into solve_sci/
    solve_sci_batch's returned SCIResult.
  • Returns (None, None) when do_rdm was 0 (the default) — no behavior
    change for existing callers that don't request RDMs.

Verification

The reshape/transpose is not a guess — verified by hand against PySCF's
own make_rdm1/make_rdm2 on a fixed subspace, on all three SBD
backends
(cpu, gpu-thrust, gpu-omp-offload):

  • Element-wise match against PySCF's RDMs (not just a scalar energy check).
  • Energy-reconstruction identity E = einsum("pr,pr->",rdm1,hcore) + 0.5*einsum("prqs,prqs->",rdm2,eri) reproduces the solver's own reported
    energy to machine precision (~1e-14) on all three backends.

No automated regression test is included for this specific reshape
formula — an earlier version of this PR had one, but it depended on
pyscf/qiskit-addon-sqd, neither of which is in this project's test
extras (only notebook-dependencies), so it broke CI collection. A future
regression here would also surface immediately and visibly through
run_sbd_diag.py's --rdm output (trace of rdm1, natural orbital
occupations) in #37.

Test plan

  • Manually re-verified through the actual shipped solve_sci path
    (not just the raw bindings) on H2O, on all three backends —
    trace(rdm1) matches the electron count, energy-from-RDMs matches
    the solver's own energy to ~1e-14.
  • do_rdm=0 (default) confirmed unaffected — rdm1/rdm2 stay None.

SBD's C++ layer already computes real one_p_rdm/two_p_rdm whenever
do_rdm=1 is requested (sbdiag.h's do_rdm != 0 branch), and bindings.cpp
already returns them in the results dict -- but _solve_sci_core never
read those two keys, so every SCIResult silently left rdm1/rdm2 at their
None default even when a caller asked for RDMs.

The reshape/transpose in the new _assemble_rdms helper is not a guess:
verified against PySCF's own make_rdm1/make_rdm2 on a small fixed H2O
subspace, on all three SBD backends (cpu, gpu-thrust, gpu-omp-offload) --
both element-wise on the full tensors and via the energy identity
E = einsum("pr,pr->",rdm1,hcore) + 0.5*einsum("prqs,prqs->",rdm2,eri)
that SCIResult.rdm1/rdm2 are contracted with everywhere else in
qiskit-addon-sqd. All three backends agreed to machine precision.

do_rdm=0 (the default, used by both run_sqd_sbd.py and
run_sqd_enlarge_subspace_sbd.py) is unaffected: rdm1/rdm2 stay None,
exactly as before.

This is plumbing only, verified against a hand-built tiny subspace, not a
new driver -- an orbital-optimization driver using these RDMs with
rotate_integrals/optimize_orbitals is a separate follow-on.
@hfwen0502
Sophia Wen (hfwen0502) merged commit fd3a419 into main Sep 17, 2026
11 checks passed
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