A C++ core with a nanobind Python frontend for working with Pauli strings and
qubit Hamiltonians. Coefficients can be numeric (std::complex<double>) or
fully symbolic (SymEngine).
See arXiv:2601.02233 for the algorithmic background.
Status. Functional core with a large test suite. The Python API surface is stable. PyPI wheels and Sphinx docs are not published yet.
- Binary symplectic representation — multiplication, commutators, and hashing are O(n / 64) over the qubit count.
- Two coefficient backends — numeric (
complex<double>) and symbolic (SymEngine::Expression); cross-type arithmetic is handled automatically and type-promotes the way you expect (any symbolic term → the whole Hamiltonian becomes symbolic). - Compaction invariant — every operation that returns a
QubitHamiltonianmerges duplicate-operator terms and drops zero-coefficient terms. You never need to callsimplify()for correctness. - Tequila-compatible API —
qubits,n_qubits,is_hermitian,is_antihermitian,dagger,conjugate,transpose,simplify(threshold),split,map_qubits,power/__pow__,to_matrix,paulistrings,count_measurements,is_all_z. - OpenFermion bridge — the
QubitHamiltonianfactory accepts anopenfermion.QubitOperatordirectly;to_openfermionandfrom_openfermionhelpers are available.
import pauliengine as pe
# A single Pauli string: dict-of-operators style.
p1 = pe.PauliString(1.0, {0: "Z", 1: "X"})
# Symbolic coefficient — anything coercible to a SymEngine Expression.
p2 = pe.PauliString("a", {1: "X"})
# OpenFermion-style: PauliString((coeff, [(Pauli, qubit), ...]))
p3 = pe.PauliString((1.0, [("X", 0), ("Y", 2)]))
# Build a Hamiltonian from a list of PauliStrings (or (coeff, dict) tuples).
H = pe.QubitHamiltonian([p1, p2, p3])
print(H.qubits()) # [0, 1, 2]
print(H.is_hermitian()) # True
print(H.dagger()) # for Hermitian H this is just HTests are in tests/:
pytest tests/pe.PauliString accepts several input shapes:
pe.PauliString(1.0, {0: "Z", 1: "X"}) # dict input
pe.PauliString(1.0, "X0 Y1 Z2") # space-separated string input
pe.PauliString((1.0, [("X", 0), ("Y", 2)])) # OpenFermion-style (coeff, list)
pe.PauliString("a", {0: "X"}) # symbolic coefficientpe.QubitHamiltonian accepts:
pe.QubitHamiltonian([ps1, ps2, ...]) # list of PauliStrings
pe.QubitHamiltonian([(1.0, {0: "X"}), ("a", {1: "Z"})]) # list of tuples
pe.QubitHamiltonian(openfermion_qubit_operator) # see OpenFermion bridge
pe.QubitHamiltonian.zero() # empty Hamiltonian
pe.QubitHamiltonian.unit() # identity (single term, coeff 1, no ops)If any term in the list is symbolic, the resulting Hamiltonian is symbolic.
# PauliString * PauliString, with the right factors of i from Pauli algebra.
p4 = pe.PauliString(1.0, {0: "X"}) * pe.PauliString(1.0, {0: "Y"}) # -> 1j * Z(0)
# Scalar multiplication on both sides; +, -, unary -, and addition between
# PauliStrings (returns a QubitHamiltonian).
H1 = p1 + p2 - p3
H2 = 0.5 * H1 + (-H1) * 2j
H3 = H1 ** 3 # integer powers
c = H1.commutator(H2) # commutator (also available on PauliString)Cross-type multiplication (numeric × symbolic) is supported and promotes the result to symbolic.
H.size() # number of Pauli-string terms (also len(H))
H.qubits() # sorted list of qubits with non-identity operators
H.n_qubits() # len(H.qubits())
H.is_all_z()
H.is_hermitian() # True iff every coefficient is real
H.is_antihermitian() # True iff every coefficient is purely imaginary
H.count_measurements() # 1 if all-Z, else len(H)For a single PauliString:
ps.size() # number of non-identity Pauli ops (also len(ps))
ps.count_y() # number of Y operators (used by conjugate/transpose)
ps.naked() # same operator with coefficient 1
ps.key_openfermion() # OpenFermion-style key
ps.get_pauli_at_index(q) # "I" / "X" / "Y" / "Z"H.dagger() # complex-conjugate each coefficient
H.conjugate() # complex conjugation (flips a sign per Y operator)
H.transpose() # transpose (flips a sign per Y operator, no conjugation)
H.simplify(1e-10) # drop terms with |coefficient| <= threshold
H.split() # -> (hermitian, anti_hermitian) pair (numeric coeffs only)
H.map_qubits({0: 5, 1: 2})
H.power(3) # also via H ** 3
split()andto_matrix()require coefficients that evaluate to a complex number — callH.subs({...})first on symbolic Hamiltonians.
import numpy as np
M = np.array(H.to_matrix()) # 2**n x 2**n, ignores unused qubits
M_full = np.array(H.to_matrix(ignore_unused_qubits=False)) # absolute qubit indicesAny string coefficient (or SymEngine::Expression from C++) makes the term
symbolic. Symbolic PauliStrings and Hamiltonians support every arithmetic
operation plus:
H = pe.QubitHamiltonian([("a", {0: "X"}), ("b", {1: "Z"})])
dH = H.diff("a") # symbolic derivative
H2 = H.subs({"a": 2.0}) # substitute and evaluatediff is also available on PauliString. Mixing symbolic and numeric inputs
is fine: the factory scans every element and uses the symbolic builder if
needed.
from openfermion import QubitOperator
qop = 1.5 * QubitOperator("X0 Y1") + 0.5j * QubitOperator("Z2")
# Factory accepts QubitOperator directly:
H = pe.QubitHamiltonian(qop)
# Or use the explicit helpers:
H = pe.from_openfermion(qop)
qop_back = pe.QubitHamiltonian.to_openfermion(H)
assert qop == qop_backopenfermion is an optional dependency — from_openfermion /
to_openfermion import it lazily and raise ImportError with a helpful
message if it is missing.
The library is a header-only template under include/pauliengine/. Both
PauliString<Coeff> and QubitHamiltonian<Coeff> work for
Coeff = std::complex<double> and Coeff = SymEngine::Expression. Every
operation exposed in Python is available in C++ with the same name.
- A C++20 compiler (MSVC 19.3+, GCC 11+, or Clang 14+)
- CMake 3.20+
- Python 3.11–3.13
- Conan 2 (to pull in SymEngine)
- nanobind (build-time)
pip install conan
conan profile detect
conan install . --output-folder=build --build=missing
pip install .The CMake build picks up the Conan toolchain from build/conan_toolchain.cmake.
For an editable / development install use pip install -e . instead of
pip install ..
If you are on Windows and have not built SymEngine before, the Conan step will build it from source on first install — that takes a few minutes. Subsequent builds use the cached artifact.
Performance note. PauliEngine can be built without SymEngine, but symbolic coefficients are unavailable in that mode and the runtime cost of certain numeric paths increases.
Conan does not currently ship a prebuilt SymEngine binary for macOS. Build it from source once before the main install step:
conan install --build=symengine/0.14.0Subsequent installs pick up the cached artifact, so this only needs to be done the first time.
pip install pytest
pytest tests/If you use PauliEngine in academic work, please cite arXiv:2601.02233.