ppvm / Pauli propagation & tableau simulation
Sections
  1. § 1 Install
  2. § 2 Stim circuits & sampling
  3. § 3 Generalized Tableau
  4. § 4 Pauli Propagation
  5. § 5 Loss channel details
  6. § 6 Next steps

Pauli Propagation

Overview

PauliSum simulates quantum circuits in the Heisenberg picture. Instead of evolving a state vector forward through the circuit, it propagates observables backward. The observable is represented as a weighted sum of Pauli strings, and each gate is applied analytically by conjugation.

This makes it possible to simulate large, deep circuits — including noisy ones — at a fraction of the cost of full state-vector simulation.

Basic usage

from ppvm import PauliSum

# Observable: Z on each qubit
state = PauliSum.new(n_qubits=3, terms=[f"Z{i}" for i in range(3)])

# Apply gates in reverse circuit order
state.cnot(1, 2)
state.cnot(0, 1)
state.h(0)

# Expectation value with respect to |0...0>
print(state.overlap_with_zero())

Term notation

Terms can be specified as full Pauli strings ("XZI") or in compact notation ("X0Z1" — Pauli + qubit index). Coefficients default to 1.0 but can be set explicitly:

ps = PauliSum.new(4, [("Z0Z1", 0.5), ("X2", 0.3)])

End-to-end: GHZ expectation values

For observable-centric studies — “what is the expectation value of this operator after this circuit?” — the snippet below propagates the all-Z observable backwards through a GHZ-prep circuit and prints its overlap with |0…0⟩:

docs/examples/paulisum_ghz.py tested
from ppvm import PauliSum

state = PauliSum.new(n_qubits=2, terms=["ZZ"])

# Circuit is H(0); CNOT(0, 1) — propagate backwards.
state.cnot(0, 1)
state.h(0)

print(state)  # → 1.000 * IZ
print(state.overlap_with_zero())  # → 1.0

Truncation

Two truncation strategies control the approximation/performance trade-off:

  • Coefficient truncation (min_abs_coeff): drops terms with absolute coefficient below a threshold.
  • Weight truncation (max_pauli_weight): drops terms with more non-identity Paulis than the cutoff.
ps = PauliSum.new(10, "Z0", min_abs_coeff=1e-8, max_pauli_weight=5)

Pauli propagation scales well when the observable has small support and the circuit doesn't grow that support too quickly. Truncation strategies (CoefficientThreshold, MaxPauliWeight, MaxLossWeight, CombinedStrategy) let you bound the working set on the fly.

Simulating loss

To simulate qubit loss, ppvm offers LossyPauliSum. This is a dedicated class that behaves just like a PauliSum, but adds additional methods for the loss.

This separation exists because we need to extend the Pauli basis to account for loss (see § 5 Loss channel details for the full background). This comes at a storage overhead — we now need 3 bits to represent a character in a Pauli string rather than 2.

Here is a small example:

from ppvm import LossyPauliSum

ps = LossyPauliSum.new(n_qubits=1, terms=["Z"])

# Reset at the end of the circuit
ps.reset_loss_channel(0)

# Loss after an X gate
ps.loss_channel(0, 0.1)

# Apply an X gate
ps.x(0)

z_exp = ps.overlap_with_zero()

# This will be -0.8: in 10% of cases we have <Z> = 1 instead of -1.
print(f"<Z>: {z_exp}")

A third truncation strategy is available for lossy simulations:

  • Loss weight truncation (max_loss_weight): drops terms with more than a given number of L operators. Since the contribution of strings with L on many positions scales as $p_L^k$, this effectively controls the branching from loss and reset channels.
ps = LossyPauliSum.new(3, "ZZZ", max_loss_weight=2)
On this page
  1. Overview
  2. Basic usage
  3. Term notation
  4. GHZ example
  5. Truncation
  6. Simulating loss