Getting Started
Install monoprop and run a minimal operator-propagation example.
This page covers the installation and a minimal introduction to monoprop.
For more detailed usage instructions, see the Python API.
Installation
Install monoprop from PyPI using pip:
pip install monopropIf you use uv as package manager:
uv add monopropThese prebuilt wheels run on a single process — they are published without MPI. For a multi-rank / MPI-enabled build, or to build the C++ library and executables, build from source as described in Building from source.
Introduction
monoprop is a classical quantum-circuit simulator based on operator propagation: rather than storing the full quantum state, it expands an operator in the Majorana (or Pauli) basis and propagates it through a circuit, dropping terms that contribute little. Expectation values and gradients are then read off the propagated operator.
The Concepts section explains how the method works — the basis and notation, the propagation algorithm, and the Heisenberg and Schrödinger simulation modes.
Minimal Example
The following example back-propagates the Majorana observable through a single Majorana rotation generated by . Because the generator anticommutes with the observable, the gate splits it into a cosine branch (the original monomial, scaled by ) and a sine branch (the new monomial , scaled by , where is the observable); see Propagation Algorithm:
import numpy as np
from monoprop import MajoranaPropagator, ExpGate, Circuit, MajoranaOperator
observable = MajoranaOperator({(0, 1, 2, 4): 1.0}, num_modes=8)
# A gate generator carries the Hermitian operator, the same convention as an observable:
# M_γ = i·m_4 m_5 is a length-2 monomial, so its coefficient is imaginary. monoprop
# divides out the Hermitian phase i for you when the circuit is ingested.
gate = ExpGate(MajoranaOperator({(4, 5): 1j}, num_modes=8)) # exp(+i θ · M_γ), M_γ = i·m_4 m_5
circuit = Circuit(gates=[gate], parameters=[0.5])
mbs = MajoranaPropagator.from_circuit(circuit, observable, cutoff=16)
result = mbs.evolved_operator()
assert sorted(result.terms) == [(0, 1, 2, 4), (0, 1, 2, 5)] # original (cosine) term plus the new (sine) term
assert np.isclose(result.terms[(0, 1, 2, 4)].real, np.cos(2 * 0.5)) # cosine branchFor qubit problems there is a dedicated simulator, PauliPropagator, which takes
Pauli operators and gates directly. The following example back-propagates the
two-qubit observable under a single qubit rotation
, which likewise splits it into a cosine and a sine branch.
Its result is a PauliOperator whose .terms map each Pauli to its coefficient
(see Notation):
import numpy as np
from monoprop import PauliPropagator, ExpGate, Circuit, PauliOperator, Pauli
observable = PauliOperator({"ZZ": 1.0}, num_qubits=2)
gate = ExpGate(PauliOperator({Pauli("X", 0): 1.0}, num_qubits=2)) # exp(+i θ · X_0)
circuit = Circuit(gates=[gate], parameters=[0.5])
pp = PauliPropagator.from_circuit(circuit, observable, cutoff=16)
result = pp.evolved_operator() # a PauliOperator; .terms maps Pauli -> coefficient
# Back-propagating Z⊗Z through the X_0 rotation splits it into a cosine and a sine branch.
coeffs = {pauli.string: coeff.real for pauli, coeff in result.terms.items()}
assert np.isclose(coeffs["ZZ"], np.cos(2 * 0.5)) # cosine branch (unrotated term)
assert np.isclose(coeffs["YZ"], -np.sin(2 * 0.5)) # sine branch (rotated term)