Usage¶
Pauli propagation works in the Heisenberg picture: rather than simulating the statevector, it evolves each observable backwards through the circuit gate by gate, producing a closed-form trigonometric polynomial in the parameters. The workflow has four steps.
Step 0: Imports¶
import pennylane as qml # To define the circuit
from pprop import Propagator # For Pauli Propagation
Step 1: Define the ansatz¶
The ansatz must be a plain Python function that accepts a parameter array,
applies PennyLane gates, and returns a list of qml.expval(...) calls,
one per observable. Observables can be single Pauli words or arbitrary linear
combinations thereof.
Note
qml.Barrier() is a PennyLane no-op used only for circuit drawing.
It is automatically ignored by the propagator.
def ansatz(params: list[float]):
qml.RX(params[0], wires=0)
qml.RX(params[1], wires=1)
qml.RY(params[2], wires=0)
qml.RY(params[3], wires=1)
qml.Hadamard(wires=2)
qml.Barrier()
qml.CNOT(wires=[0, 1])
qml.CNOT(wires=[1, 2])
qml.Barrier()
qml.RY(params[4], wires=0)
qml.RY(params[5], wires=1)
qml.RY(params[6], wires=2)
return [
qml.expval(qml.PauliZ(0)), # ⟨Z₀⟩
qml.expval(qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliX(2)), # ⟨X₀X₁X₂⟩
qml.expval(qml.PauliY(2)), # ⟨Y₂⟩
qml.expval(-qml.PauliX(0) @ qml.PauliX(1) @ qml.PauliX(2)
+ 13 * qml.PauliZ(2)), # ⟨-X₀X₁X₂ + 13Z₂⟩
]
Step 2: Create the Propagator¶
Propagator wraps the ansatz and accepts two optional truncation cutoffs:
k1: Pauli weight cutoff, discard evolved Pauli words with more thank1non-identity single-qubit factors. Useful for large systems where high-weight terms contribute negligibly.k2: Frequency cutoff, discard terms whose total number of trigonometric factors exceedsk2. Controls expression complexity.
Setting both to None performs exact propagation with no approximation.
prop = Propagator(
ansatz,
k1=None, # Pauli weight cutoff (None = exact)
k2=None, # Frequency cutoff (None = exact)
)
>>> prop
Propagator
Number of qubits : 3
Trainable parameters : 7
Step 3: Propagate¶
.propagate() evolves each observable backwards through the circuit. Each
line of output shows the initial Pauli word (or linear combination) being
propagated.
prop.propagate()
Propagating (1.0000)*Z0
Propagating (1.0000)*X0 X1 X2
Propagating (1.0000)*Y2
Propagating (-1.0000)*X0 X1 X2 + (13.0000)*Z2
Step 4: Evaluate¶
Once propagated, calling prop(params) returns all expectation values as a
NumPy array of shape (num_observables,).
random_params = qml.numpy.arange(prop.num_params)
prop_output = prop(random_params)
>>> prop_output
[ 0.32448207 -0.5280619 0. 4.16046337]
Gradients are available via .eval_and_grad(), which returns
(values, jacobian) with shapes (num_obs,) and (num_obs, num_params)
respectively:
vals, grads = prop.eval_and_grad(random_params)
Inspecting the symbolic expression¶
.expression(idx) reconstructs the closed-form SymPy expression for
observable idx.
prop.expression(0) # ⟨Z₀⟩
-1.0*sin(θ₂)*sin(θ₃)*sin(θ₄)*cos(θ₀)*cos(θ₁) + 1.0*cos(θ₀)*cos(θ₂)*cos(θ₄)