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

B5 — Hybrid depth reduction

Manual: Incremental propagation, Truncation

Split a circuit near the end, back-propagate the observable through the tail classically, and hand a QPU the shorter front circuit plus an evolved observable. The composed expectation is exactly the full-circuit one — so the trade is QPU depth against classical term count, and this page measures both sides of it.

As the tail depth k grows the residual front circuit shrinks linearly in Trotter steps (186 → 0 gates) while the weight-capped evolved observable grows roughly exponentially (1 → 12 413 terms).

The idea

Split a circuit at layer k from the end:

|0...0> --[ front circuit, depth L-k ]--[ tail circuit, depth k ]--> measure O

Back-propagate O through the tail classically — direction="heisenberg" on the last k layers, with the same engine this repository ships for everything else — to get O' = U_tail† O U_tail. Composing the halves, ⟨psi_front| O' |psi_front⟩ (with |psi_front⟩ = U_front |0...0⟩) equals ⟨ψ|O|ψ⟩ for the full circuit exactly, since Heisenberg conjugation composes. The QPU only runs the shorter front circuit; O' is computed once, off the QPU.

The artifact is a schema-v1 task file — the residual front circuit plus the evolved observable, ready for a QPU-side runner. task_exact.json and task_truncated.json in the showcase directory are exactly that, and they are the same schema the cross-engine comparison drives both engines from.

Running it

source .venv/bin/activate
python examples/b5_operator_backpropagation/run_b5.py

Key calls — an excerpt from the actual example script, verified by pytest examples/tests/test_showcase_b5.py, not standalone:

tail_evolved = observable.propagate(tail_circuit, policy, direction="heisenberg")
psio.save(npz_path, tail_evolved)                 # round-trip through disk
loaded = interop.load_task(task_path)
composed = loaded.observable.propagate(loaded.circuit, loaded.truncation,
                                        direction=loaded.direction)

This regenerates every artifact in the directory — both task JSONs, the .npz, the CSV, both SVG figures — and the whole sweep runs in about 3 seconds on a laptop-class machine; this page is about depth reduction, not speed. The CI-visible correctness gate is numpy-only and runs well under a second: pytest examples/tests/test_showcase_b5.py.

Validation

8-qubit, 4-layer seeded hardware_efficient_ansatz, observable Z_4, tail depth k = 1 (front: 3 layers / 69 gates; tail: 1 layer / 23 gates):

checkvaluegap
full-circuit Heisenberg expectation−0.175584682492551—
composed value, task file read back off disk−0.1755846824925510.0
qiskit-Aer statevector cross-check−0.1755846824925511.665e-16

The round-trip gap is exactly 0.0, well inside the 1e-12 bound, and it is a genuine round trip through disk: the evolved observable is saved to .npz via paulistrings.io and read back before being embedded in the task file.

Sweeping min_abs_coeff against the exact reference above:

min_abs_coefftermsvalue|gap|
1e-100.00000000001.756e-01
3e-282−0.20483090932.925e-02
1e-21598−0.23883207966.325e-02
3e-37389−0.19263152771.705e-02
1e-318011−0.18704526981.146e-02
1e-445654−0.17582576282.411e-04
1e-664465−0.17558483081.483e-07
1e-864786−0.17558468401.531e-09

Truncation is applied after every channel, not after every Python call (see Truncation), so splitting a circuit and truncating separately on each half agrees exactly, for any split point, with truncating the full circuit in one shot — a test pins it. The table above is therefore independent of where k is chosen. The gap is not monotone in the cutoff (2.9e-2 at 3e-2, then 6.3e-2 at 1e-2) — dropped terms carry signs, and a truncated Pauli sum has no variational bound.

The actual trade-off: depth against term count

16-qubit heavy-hex kicked-Ising sublattice, 6 Trotter steps, θ_h = 0.6 (a generic, non-Clifford kick), observable Z_8, truncated at weight <= 6 throughout — the weight cap is what makes the classical half tractable in practice. Sweeping the tail depth k from 0 (nothing back-propagated) to 6 (front empty):

k (tail steps)front layersfront gatesevolved-observable terms
061861
151552
2412414
3393132
42621118
51314608
60012413

The residual circuit shrinks linearly in Trotter steps while the weight-capped evolved-observable term count grows roughly exponentially before the cap bites — the classic operator-spreading-vs-light-cone shape, and exactly the cost paid classically for every layer moved off the QPU.

Numbers: every value on this page comes from examples/b5_operator_backpropagation/run_b5.py, committed alongside its outputs — task_exact.json, task_truncated.json, evolved_observable_exact.npz, depth_vs_terms.csv, depth_vs_terms.svg, and convergence_panel.svg — in the showcase directory.