PLAN.md — Schwinger-Model Majorization Poset Extension for tessera¶
Hand this file to Claude Code as the working spec. Each phase has a concrete deliverable, a file layout, and an acceptance test. Do not skip acceptance tests; every phase gates the next.
The hypothesis this plan is built to test, plus falsification criteria, limitations, and bibliographic context, are in quantum-methodology.md. The user-facing API documentation for what’s built so far is in quantum.md.
0. Goal and scope¶
Extend tessera with a C++ subsystem that:
Represents the 1+1D Kogut–Susskind Schwinger-model Hamiltonian as a Matrix Product Operator (MPO) on a Jordan–Wigner’d spin chain.
Finds its ground state by DMRG.
Evolves it in real time by TDVP after a local quench creating a \(q\bar{q}\) pair at separation \(d\).
At each time slice, computes Schmidt spectra \(\lambda_{[i,j]}(t)\) for all contiguous-interval cuts \([i,j]\), builds a majorization poset, and compares it against (a) the Lieb–Robinson cone and (b) a causet causal order when the chain is embedded in a causet.
1. Architectural principle — minimize Python/C++ crossings¶
The entire simulation loop stays in C++. Python is a result viewer, not a driver.
C++ side: build MPO -> DMRG -> TDVP loop -> spectra -> poset
|
Py side: export
|
plot, save, interop
Only these cross the barrier:
In (Py → C++): scalar config (N, m, g, Δt, T, cut family, seed).
Out (C++ → Py): final/periodic snapshots of observables and the poset as plain data (
std::vector<double>, adjacency lists).
Do not expose MPS tensors or MPOs to Python. Do not call Python callbacks inside the time loop.
2. Dependencies¶
Purpose |
Library |
Why |
|---|---|---|
MPS/MPO, DMRG, TDVP |
ITensor (C++) v3 |
Mature, header-heavy C++, built-in DMRG+TDVP, handles quantum-number conservation |
Linear algebra (small ops outside ITensor) |
Eigen 3 |
already likely in tessera’s toolchain |
Graph / poset |
Boost.Graph |
Hasse diagram, transitive reduction |
Python bindings |
pybind11 |
already in tessera |
Tests |
Catch2 or doctest |
whichever tessera already uses |
Add ITensor as a git submodule under third_party/itensor and wire
via CMake add_subdirectory. ITensor depends on BLAS/LAPACK — reuse
what tessera already has. Do not pull ITensor through a package manager;
pin a known-good commit.
3. File layout¶
tessera/
include/tessera/quantum/
schwinger_model.hpp # MPO construction
dmrg_runner.hpp # ground-state driver
tdvp_runner.hpp # real-time driver
schmidt.hpp # spectrum extraction
majorization.hpp # partial order, poset
causal_compare.hpp # LR / tessera comparison
src/quantum/
schwinger_model.cpp
dmrg_runner.cpp
tdvp_runner.cpp
schmidt.cpp
majorization.cpp
causal_compare.cpp
tests/quantum/
test_schwinger_spectrum.cpp # Phase 1 acceptance
test_majorization.cpp # Phase 3 acceptance
test_tdvp_string.cpp # Phase 4 acceptance
test_causal_compare.cpp # Phase 5 acceptance
python/tessera/quantum/
__init__.py
_bindings.cpp # pybind11, only exports final data
examples/quantum/
run_schwinger.cpp
qqbar_quench.cpp
4. Physics spec (fix once, reference everywhere)¶
Open-BC staggered-fermion Schwinger model, Jordan–Wigner’d, Gauss-law-eliminated. One qubit per site, \(N\) sites total.
with background field \(L_0\) (keep as a parameter, default 0). Expand
\(L_n^2\); the result is a long-range \(Z_m Z_{m'}\) Hamiltonian whose
MPO bond dimension grows with \(N\). ITensor’s AutoMPO handles this
natively — use it, do not roll a hand-built MPO.
Reference for all numerics: Bañuls, Cichy, Cirac, Jansen, JHEP 11, 158 (2013).
5. Phased implementation¶
Phase 0 — scaffolding (≤ 2 days)¶
Add ITensor submodule; CMake build verifies with ITensor’s own
sample/dmrg.cccompiled against the tessera toolchain.Create the directory layout above with empty stubs.
CI runs on each PR.
Acceptance: make test_itensor_hello returns the ground state
energy of the Heisenberg chain for \(N=20\) to within \(10^{-6}\) of the
ITensor reference value.
Phase 1 — Schwinger MPO (2–3 days)¶
Implement
schwinger_model.hpp: a class that takes \((N, a, m, g, L_0)\) and returns anITensor::MPO.Use
AutoMPOto avoid manually tracking the \(L_n^2\) expansion.Conserve \(U(1)\) total charge; quantum numbers on every index.
Acceptance (test_schwinger_spectrum.cpp):
DMRG ground-state energy per site for \(N=20\), \(a=1\), \(m/g \in \{0.0,
0.125, 0.25\}\) matches Bañuls et al. 2013 Table 1 to \(< 10^{-3}\).
Bond dimension 100 is enough for this check.
Phase 2 — DMRG runner (2 days)¶
dmrg_runner.hpp: thin wrapper aroundITensor::dmrg. Inputs:SchwingerMPO, sweep schedule, max bond dim, noise, Krylov dim. Outputs:MPSground state, converged energy, final bond dim.Expose
computeGroundState(config)through pybind11 returning only \((E_0, \text{bond\_dim}, \text{truncation\_err})\).
Acceptance: re-runs Phase 1 with the wrapper; numerics unchanged.
Phase 3 — Schmidt spectra and majorization poset (3 days)¶
schmidt.hpp: given an MPS in right canonical form and an interval \([i,j]\), return \(\lambda_{[i,j]}\) via SVD at the appropriate bond (ITensor exposes Schmidt values directly after orthogonalization).For contiguous cuts this is \(O(N^2)\) spectra; store as a
std::vector<std::vector<double>>.majorization.hpp:// lambda <_maj mu iff, with both sorted descending, zero-padded, // sum_{i=1..k} lambda_i <= sum_{i=1..k} mu_i for all k, // and total probability equal (trivially 1 here). bool majorizes(const std::vector<double>& mu, const std::vector<double>& lambda, double tol = 1e-12);
Build a DAG whose nodes are intervals
[i,j]and edges represent the majorization relation; apply Boost.Graph transitive reduction to obtain the Hasse diagram.Store as
tessera::Poset(define this type; reuse tessera’s existing causet adjacency structure if the API fits).
Acceptance (test_majorization.cpp):
Product state \(|0\rangle^{\otimes N}\): every spectrum is \((1, 0, \ldots)\); Hasse diagram has no edges.
\(N\)-qubit GHZ: every single-site spectrum is \((\tfrac{1}{2}, \tfrac{1}{2})\); pairwise majorization relations all equal (no strict edges).
Bell state across the center cut of \(N=2\): spectrum \((\tfrac{1}{2}, \tfrac{1}{2})\); compared against product state \((\tfrac{1}{2}, \tfrac{1}{2}) \prec (1,0)\) correctly.
Phase 4 — TDVP with \(q\bar{q}\) quench (4 days)¶
tdvp_runner.hpp: wraps ITensor’s 2-site TDVP. Inputs: MPS, MPO, \(\Delta t\), \(T\), max bond dim, observables callback (pure C++).Initial state: DMRG ground state, then apply \(\sigma^+_{i_0} \sigma^-_{i_0 + d}\) (normalized) to create a \(q\bar{q}\) pair. Gauss’s law requires appropriate string of \(L\) shifts between them — handle via the electric-field string operator; reference Buyens et al. 2014 for the exact construction.
At each recorded timestep, invoke observables callback that records:
\(\langle L_n \rangle_t\) (electric-field profile)
\(\langle Z_n \rangle_t\) (charge density)
\(\lambda_{[i,j]}(t)\) for all contiguous cuts
full majorization poset
Acceptance (test_tdvp_string.cpp):
Heavy-quark limit \(m/g \gg 1\), \(d=4\), \(T = d\cdot a\) at \(a=1\), \(g=1\): \(\langle L_n \rangle_t\) is approximately uniform at value \(\pm 1\) between \(i_0\) and \(i_0 + d\) (flux tube) and zero outside, to within \(0.05\) after \(t = T/2\).
Energy conservation: \(|\Delta E|/|E_0| < 10^{-3}\) over the run.
Phase 5 — causal-order comparison (3 days)¶
causal_compare.hpp: builds three partial orders on the label set \(\{(\text{cut}, t)\}\):\(\prec_\text{maj}\): from Phase 3 Hasse diagram, per time slice.
\(\prec_\text{LR}\): from the Lieb–Robinson velocity \(v_\text{LR}\) extracted by fitting OTOC (\(\langle [\sigma^+_i(t), \sigma^-_j]^\dagger [\sigma^+_i(t), \sigma^-_j]\rangle\)) front propagation.
\(\prec_\text{tessera}\): if the chain is embedded in a causet, inherit from tessera’s existing causal order.
Compute agreement statistics: Kendall-\(\tau\), fraction of discordant pairs, graph edit distance on Hasse diagrams.
Output: a single struct
struct CausalComparisonReport { double tau_maj_lr; double tau_maj_cs; ... }
Acceptance (test_causal_compare.cpp):
On a regular (total-order) causet chain: \(\prec_\text{maj}\) agrees with \(\prec_\text{LR}\) above some \(v_E \leq v_\text{LR}\); both agree with \(\prec_\text{tessera}\) since the causet is trivially totally ordered per time slice.
Agreement rate quoted with uncertainty from bootstrap over Trotter seeds.
Phase 6 — tessera integration (1–2 weeks, optional for v1)¶
Status (as of 2026-04-25): data-extraction layer landed; MPO rebuild on the extracted chain is the remaining piece.
Done:
tessera::Poset::fromSpacetime(Spacetime const&)— inherits a Hasse cover Poset from the timelike-edge subgraph of anytessera::Spacetime. Uses transitive closure + reduction; preserves metric semantics by orienting earliest-time → latest-time and filtering onEdge::getSquaredLength() < 0.tessera::quantum::extractCausetChain(Spacetime const&)— packages the antichain layering, flat lattice ↔ Spacetime ID mapping, adjacent-slice timelike-edge hopping pairs, and the inherited partial-order Poset into a singleCausetChain.Python bindings:
tessera.quantum.Poset.fromSpacetime(st)andtessera.quantum.extractCausetChain(st)work on anytessera.Spacetimeinstance (CDT-built, hand-crafted, or futuretessera.CausalSet).C++ acceptance:
tests/quantum/test_poset_from_spacetime.cpp(7 cases) andtests/quantum/test_causet_chain.cpp(5 cases). Python acceptance:tests/quantum/test_phase6_causet_chain_python.py.
Remaining for v1 close-out (deferred):
Rebuild the Schwinger MPO on the
CausetChainsoH_hopfollows the extracted hopping pairs rather than uniform nearest-neighbour. For trivial chains (one vertex per slice) this is justparams.N = chain.nSitesand the existingbuildSchwingerMporuns as-is.For non-trivial antichains (causet branches), the chain layout produces hopping pairs with stride > 1. Two paths:
Stay on a 1D MPS, accept the long-range hopping cost in the MPO bond dimension.
Switch to ITensor’s tree-tensor-network support; tree TN handles branching antichains natively. Both paths are open work; pick after profiling the stride-1 case on a small CDT-derived chain.
Replace ≼_cs in
computeCausalComparisonwith the inheritedpartialOrderon the cut/time label set, then re-run the Phase 5 invariants (lrVsCs.kendallTau == 1.0should still hold when the chain is totally ordered per slice; it may relax for branching causets, which is the Phase 6 hypothesis).
Phase 7 — MERA / KAK / EML (research, not v1)¶
Do not attempt until Phases 1–5 produce publication-quality numbers. Replace MPS with binary MERA; KAK-decompose disentanglers; EML- parameterize KAK angles for symbolic regression over couplings. Reference: Evenbly–Vidal 2009 for MERA algorithms; Hauru–Van Damme– Haegeman 2021 for Riemannian optimization; Odrzywołek 2026 for EML.
6. Python layer (minimal)¶
python/tessera/quantum/_bindings.cpp exposes exactly:
py::class_<QuantumConfig>(m, "QuantumConfig")
.def(py::init<>())
.def_readwrite("N", &QuantumConfig::N)
.def_readwrite("a", &QuantumConfig::a)
.def_readwrite("m", &QuantumConfig::m)
.def_readwrite("g", &QuantumConfig::g)
.def_readwrite("L0", &QuantumConfig::L0)
.def_readwrite("maxBondDim", &QuantumConfig::maxBondDim)
.def_readwrite("dt", &QuantumConfig::dt)
.def_readwrite("T", &QuantumConfig::T);
py::class_<GroundStateResult>(m, "GroundStateResult")
.def_readonly("energy", &GroundStateResult::energy)
.def_readonly("bondDim", &GroundStateResult::bondDim)
.def_readonly("truncationErr", &GroundStateResult::truncationErr);
py::class_<QuenchResult>(m, "QuenchResult")
.def_readonly("times", &QuenchResult::times)
.def_readonly("lProfile", &QuenchResult::lProfile) // [t][n]
.def_readonly("zProfile", &QuenchResult::zProfile)
.def_readonly("posets", &QuenchResult::posets); // one per t
m.def("computeGroundState", &computeGroundState);
m.def("runQqbarQuench", &runQqbarQuench);
No MPS, MPO, or ITensor type crosses the barrier.
7. Conventions and traps¶
Sign of \((-1)^n\) in \(H_m\): staggered convention, site index starts at \(n=1\). Bañuls et al. use the same; match their sign.
Gauss’s law: after elimination, the long-range \(Z_m Z_{m'}\) term has coefficients that depend on the parity of sites between \(m\) and \(m'\). Verify by independent sum on \(N=4\) before trusting the MPO.
Truncation in TDVP: 2-site TDVP grows bond dim; 1-site TDVP conserves it. Start with 2-site for the quench, switch to 1-site once bond dim saturates your budget.
Schmidt spectrum zeros: pad to equal length before majorization comparison; otherwise partial-sum test is miscounted.
Boost.Graph transitive reduction: not in Boost’s public API directly; implement via DFS over comparability graph if needed.
OTOC computation: expensive. Compute only at the final time and at one or two intermediate times for \(v_\text{LR}\) estimation; do not run every step.
8. First week targets¶
Day 1–2: Phase 0 done, ITensor builds inside tessera’s CMake tree.
Day 3–4: Phase 1 MPO constructed; matches Bañuls 2013 values.
Day 5–6: Phase 2 wrapper; Phase 3 majorization infrastructure on trivial and GHZ test states.
End of week 1: DMRG + majorization pipeline end-to-end on \(N=20\) Schwinger ground state.
9. Open questions to defer¶
Whether to quantum-number-decompose spectra before majorization (majorization within each \(U(1)\) sector vs. global). Default: global for v1; revisit.
Whether to normalize spectra per cut (Schmidt coefficients squared sum to 1 automatically for normalized states; sanity-check this after TDVP truncation).
Whether the “cut family” should include non-contiguous subsets eventually. Default: contiguous only, per the majorization well-definedness argument.
10. Non-goals for v1¶
GPU acceleration of the tensor contractions. ITensor has partial CUDA support; ignore until CPU performance is a bottleneck.
Non-abelian gauge groups (\(SU(2)\), \(SU(3)\)). Schwinger is \(U(1)\); that is the full v1 scope.
2+1D. Chain only for v1.
Replacing ITensor. Do not write a bespoke MPS library.