Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Against other tools

Manual: Truncation, Engine and propagation

Against PauliPropagation.jl, the other mature Pauli-propagation engine, the comparison is term for term: same circuits, same truncation, parity-gated timing. Against state-vector, stabilizer and MPO methods the tools do not overlap, and this page records where each applies.

Which method fits which problem

your problemreach for
one observable, non-Clifford circuit, more qubits than a dense method holdsPauli propagation — this engine
any observable, exactly, at ~26–30 qubits or fewera state-vector simulator
a Clifford circuit, at any qubit countstim
a low-operator-entanglement operator you need a bond-dimension bound foran MPO/tensor-network method
storing and algebraically manipulating a Pauli operator, without propagationqiskit.SparsePauliOp, openfermion.QubitOperator
a second Pauli-propagation engine to cross-check againstPauliPropagation.jl

The sections below give the measured basis for each row. Everything under Methodology is the provenance behind the cross-engine numbers rather than part of choosing a method.

vs state-vector simulation

Not a competitor but a complementary oracle, and this suite uses it as one everywhere it reaches. Every exact reference on this site is a dense statevector (usually qiskit Aer); where two exact routes were affordable the reference is both of them, required to agree.

state-vectorPauli propagation
object carried2ⁿ amplitudesthe Pauli strings the observable spreads over
cost driverqubit countcircuit depth and operator spreading; n enters only through the channel count
resultany observable, exactlyone observable, to a truncation error you must measure
ceiling here~26–30 qubits (the 30-qubit cone reference cost ~150 s and 16.1 GiB)127 qubits routinely; 2.3 × 10⁸ terms in a single sum measured

Two measured illustrations of where the boundary sits:

  • Benchmark B needed an exact reference for a weight-10 observable whose causal cone is 30 qubits. Untruncated Pauli propagation over that cone exceeded a 26 GiB address-space cap at 4.3 × 10⁸ terms; the statevector over the same cone took ~150 s and does not care about depth.
  • The same benchmark’s weight-17 observable has a 59-qubit cone: 2^59 amplitudes rules out any dense method, and untruncated propagation is far past the wall above. Neither method reaches it, which is why those references are self-converged and reported as not converged.

A state-vector simulator gives the answer; Pauli propagation gives the answer plus a truncation error to bound. That asymmetry is why every page on this site carries a convergence panel.

vs stabilizer (stim) simulation

Also an oracle, not a competitor. At a Clifford point stim gives the exact ±1 integer in under 0.1 s at any qubit count, and Benchmark A exists to be scored against it. Benchmark B reproduces those integers bit-exactly at every one of eight cutoffs, for three observables at both Clifford endpoints.

  • stim: Clifford circuits, exactly, at enormous scale. Where a circuit is Clifford it is strictly the better tool.
  • Pauli propagation: non-Clifford circuits, where the tableau method has nothing to say. The kicked-Ising kick angle separates the two: at θ_h ∈ {0, π/2} the circuit is Clifford and stim answers; at the hard interior angles the operator spreads over millions of Pauli strings.

A stabilizer simulator cannot serve as a noisy oracle: a tableau simulation samples one Pauli error rather than averaging over them, which is why Showcase B2 carries a hand-rolled Kraus density-matrix reference instead.

vs tensor-network / MPO methods

No measured head-to-head, and this site does not claim one. What it has is a cost-model comparison on the same operators: Showcase B6 computes the Pauli-spectrum entropy (the quantity governing truncation error for this engine) alongside the operator entanglement across a bipartition (the quantity governing MPO bond dimension), and finds them saying different things about the same operator: S_2 grows steadily with depth while S_op saturates around 1.3 nats from depth 5 on. A TDVP baseline at large n is a named limitation of Benchmark D, not silently approximated.

vs qiskit.SparsePauliOp / openfermion.QubitOperator

Different scope: these are Pauli-operator containers with algebraic manipulation, not propagation engines with truncation — no crossover concept applies. The committed comparison (baseline_comparison/README.md) benchmarks construction from string terms and one-layer Clifford conjugation, seeded inputs, n_terms ∈ {100, 1000, 10 000}; medians in µs, ratio = library / paulistrings:

constructpaulistringsqiskitopenfermionqiskit ratioopenfermion ratio
100 terms99.91 053.7982.410.5×9.8×
1 000683.79 693.810 570.314.2×15.5×
10 0003 070.096 566.8106 078.931.5×34.6×
conjugate by a Clifford layerpaulistringsqiskitratio
100 terms8.92 133.8240×
1 00071.04 978.470×
10 0001 057.332 642.031×

openfermion has no equivalent conjugation operation and is not in the second group. PauliStrings.jl, the library that inspired this one, is excluded for the same reason PauliPropagation.jl is driven by subprocess: no PyJulia wiring anywhere.

vs PauliPropagation.jl

The comparison baseline is subprocess-driven and out of CI, pinned to PauliPropagation.jl 0.8.2 on julia 1.12.6 in a committed Project.toml/Manifest.toml. There is no PyJulia or juliacall anywhere: the entry points are a Julia script that reads a task JSON and emits a result JSON, and a subprocess wrapper that skips cleanly when no julia is on PATH.

Performance vs tracked-set size

There is no single ratio: the ranking changes sign, and where it changes sign depends on the workload by an order of magnitude.

The source is a dedicated head-to-head study (jl_performance/README.md): single-threaded core versus core, five interleaved abba pairs per configuration, accepted on direction consistency, never on a difference of two independently-noisy means. Every configuration passes a per-layer term-count parity gate before any timing is reported. ratio > 1 means this engine is faster.

Ranking crossover

workloadchannelscrossover (peak terms)
kicked-Ising, 127 q, 5 Trotter steps1 3552.73 × 10³
XXZ chain, n = 100, 6 Trotter steps1 7822.00 × 10⁴
Haar SU(4) brickwork, n = 36, depth 6105none on the swept range: faster at every sign-consistent point

The crossover spans 7× across these three workloads and the SU(4) matrix-gate path has none at all, so no single global crossover is quoted anywhere on this site.

Above the crossover

workloadpeak termsratio
kicked-Ising6.37 × 10⁵2.146
kicked-Ising2.15 × 10⁶1.610
XXZ2.66 × 10⁶2.023, still rising
Haar SU(4)2.30 × 10⁶2.921, still rising

Memory, from the same study: process floors are 37.8 MB against Julia’s 0.601 GiB, a factor of 16; at the largest SU(4) configuration peak RSS is 0.239 GiB against 1.625 GiB, or 95 floor-subtracted bytes per peak term against 479. Both engines sample their own /proc/self/status, never a driver-side getrusage(RUSAGE_CHILDREN).

Below the crossover

Below the crossover jl’s hash-map backend is faster, by up to 3.6× at 68 terms: a hash-map insert per term costs little at 10² terms, while the bucketed per-layer pipeline costs nearly the same whatever the term count.

Methodology

How the PauliPropagation.jl comparison is made, and what had to be pinned before any number above could be quoted.

Parity discipline

Term-count parity blocks timing: no cross-engine wall time is reported for a configuration whose evolved Pauli sums diverge term-for-term at matched truncation, so every timed comparison runs the parity check first, untimed.

Four properties make the check strong:

  1. One description, two engines. Both sides are driven from the same schema-v1 task JSON, built from the same recorded gate list; unknown keys, gate names and fields are hard errors on both sides.
  2. Per applied layer, not just the final count: a divergence that cancels by the end is exactly the truncation-schedule bug the check exists to catch.
  3. Term by term, not just the contracted expectation. Every gate name in the schema vocabulary (plus reversed-qubit variants of cnot and unitary_2q) gets a single-gate task compared coefficient by coefficient to 1e-12.
  4. Along a sweep, not at one point: the parity legs use three cutoffs.

The results, all from committed benchmark READMEs:

whereconfigurationcomparedresult
Benchmark B127 q, 5 steps, 3 observables × 3 cutoffs12 195 per-layer counts9/9 pass, every count identical; 8 of 9 expectations agree to the last bit
Benchmark C127 q, 20 steps, 3 dyadic cutoffs16 260 per-layer counts3/3 pass, final and peak counts exact, expectations ≤ 5.6e-17
Benchmark DXXZ chain, 4 configurations171–702 layers each4/4 pass, expectations ≤ 1.7e-16
Benchmark EHaar SU(4) brickwork, unitary_2q gatesall layers at two sizesexact, and the expectation to 1e-12
parity gate itself6 q, 57 gates, both directions, with and without truncation57 layers per row, 5 rowsall identical, expectations ≤ 5.6e-17

The truncated rows of the last entry are non-vacuous: the same circuit with no policy keeps 3 881 terms, so the 1e-4 row exercises coefficient truncation and the max_weight=4 row exercises weight truncation.

Convention agreement

Both engines are Hermitian-Y: a real coefficient multiplies the literal Pauli string, Y carries no phase of its own, and the coefficient type stays real under every gate in the vocabulary. This was verified by hand-derived probe: S X S† = +Y and S† X S = −Y come out identically on both sides, and a cross-engine test encodes the sign.

Index conventions map cleanly: jl is 1-based with the leftmost Pauli character qubit 1, this repository 0-based with the leftmost character qubit 0. Observable keys map verbatim and gate indices map with a +1. Direction maps exactly: "heisenberg" ↔ heisenberg=true (jl’s default), "forward" ↔ heisenberg=false.

That mapping is not fully symmetric in practice: jl 0.8.2 defines no Schrödinger-picture transfer map at all for unitary_1q, unitary_2q, amplitude_damping, pauli_channel or depolarize2, so direction="forward" has no jl counterpart for any circuit using those gates. Every cross-engine comparison on this site is therefore run in the Heisenberg direction, where both sides agree.

Semantic divergences

Established by probes whose expected values are hand-derived in comments, never read back from the library.

The one real divergence: the coefficient boundary

jl truncates on abs(coeff) < min_abs_coeff, so it keeps a coefficient exactly equal to the threshold; this repository’s CoefficientThreshold keeps |c| > eps, so it drops it. Measured on both sides:

coefficient== 0.25?jl at min_abs_coeff = 0.25this engine
0.25true1 term0 terms
0.24999999999999994false0 terms0 terms
0.25000000000000006false1 term1 term

For generic angles the divergence is a measure-zero event and every parity row above passes untouched. It is not measure-zero for dyadic cutoffs at Clifford angles, where coefficients are exact dyadics and can land on the cutoff bit-exactly (Benchmark C hits this). The mitigation, when it bites: perturb the threshold on one side by one ulp and report it, never a coefficient. jl gets nextafter(eps, ∞); no float lies between, so jl’s |c| < eps′ becomes exactly this engine’s |c| <= eps. A test pins the divergence so a version bump cannot change it silently.

The second, narrower divergence: exact zeros

With min_abs_coeff = 0.0, abs(c) < 0 is never true, so jl keeps an exactly-zero coefficient; this engine’s merge kernels drop exact zeros unconditionally. Pinned with amplitude_damping(γ=1), whose X → √(1−γ)·X = 0 is bit-exact: this engine keeps 0 terms, jl keeps 1. Not measure-zero in practice, since Clifford-point angles produce exact cancellations. Mitigation for comparative runs: a strictly positive min_abs_coeff (any eps > 0 kills jl’s zeros too), stated in the results file.

Verified agreements

  • The weight boundary: both engines keep weight == max_weight.
  • When truncation is applied: jl truncates once per gate; this engine once per channel. The two coincide iff one gate object is one channel, which is the suite’s construction rule and structural in the schema. Measured: rz(0.05) on X with min_abs_coeff = 0.1 kills the sin branch immediately, so the second gate sees 1 term, not 2.
  • Noise-channel parameter scales: jl damps by 1 − λ where this repository takes a probability p, so depolarize(p) maps to λ = 4p/3 and dephase(p) to λ = 2p; amplitude_damping(γ) is 1:1. jl has no native general Pauli channel or two-qubit depolarizing gate, so the runner builds each as a single diagonal-PTM gate with the exact dual — one gate, one truncation point. Both verified term by term.
  • Two-qubit matrix ordering for unitary_2q is undocumented upstream; it was pinned against a known CNOT in both qubit orderings.

Noise-channel parity

Noise-channel semantics agree between the engines term by term, including the orientation of amplitude_damping under direction="heisenberg": the Heisenberg map is the unital dual Φ† (Φ†(I) = I), so ⟨Z⟩ for a qubit in |0⟩ — the channel’s fixed point — stays at 1. The shared fixture gives 9 terms on both engines with identical labels and bit-exact coefficients, a test pins the orientation from both sides, and Showcase B2 carries the physics.

Known gaps

  • direction="forward" with unitary_1q, unitary_2q, amplitude_damping, pauli_channel, depolarize2: PauliPropagation.jl 0.8.2 defines no Schrödinger transfer map for those, and the runner rejects such a task up front. Every benchmark in the suite is Heisenberg.
  • Non-computational, non-uniform product states: jl evaluates against |0…0⟩-style states only; this repository’s per-qubit label alphabet is strictly larger, so such a state cannot be compared against jl.
  • Stim-sourced circuits must be expanded into an inline gate list on the Python side; jl has no Stim parser, and the runner makes that a hard error.
  • topn truncation is absent from the interchange schema: jl has no equivalent. Likewise jl’s max_freq / max_sins truncations are excluded.
  • jl’s experimental fused rotation kernel has no parity established, because it truncates during gate application.

Sources for this page: benchmarks/julia/README.md (the probes, the divergences, the gaps) and benchmarks/README.md, plus the per-benchmark READMEs linked inline. Cross-engine performance numbers: jl_performance/README.md (protocol and headline tables, engine 81c568a) and post-optimization/README.md. qiskit/openfermion numbers: baseline_comparison/README.md and its committed results.json.