[ANN] Paulimorphic.jl (v0.1.0): Pauli-operator algebra, fermion-to-qubit encodings, and more

I’m happy to announce Paulimorphic.jl, a package for constructing, transforming, and analyzing Pauli operators. The Julia ecosystem already has a few well-established packages built on Pauli operators, e.g., PauliStrings.jl, PauliPropagation.jl, and QuantumClifford.jl. Their use cases are mostly operator dynamics, circuit simulation, and error correction. Paulimorphic instead focuses on representational manipulation and structural analysis of Pauli operators themselves: both as observables in their own right and as encodings of other quantum many-body systems. Particularly, Paulimorphic provides fermion-to-qubit encodings (Jordan–Wigner, parity, and Bravyi–Kitaev) alongside graph-theoretic analysis of operator anticommutation structure. In other words, the package is designed for investigating the polymorphic character of Pauli operators, hence Pauli-morphic.

The main design choice for Paulimorphic is: Pauli strings (PauliStr) are stored in a bit-packed symplectic representation that includes their Pauli-group phases, and every PauliSum is kept in a deterministic canonical form (phases absorbed into coefficients, duplicates merged, terms sorted, no exact-zero coefficients) upon construction. This canonical-form representation makes relational operations such as equality checks and hashing, as well as interoperability with other packages, predictable.

As of version 0.1.0, Paulimorphic supports the following main features:

  • Operator algebra: sums, scalar and operator products, Hermitian adjoints, commutation and anticommutation evaluation.
  • Multi-level data manipulation: stamping, shifting, and pasting single-site operators; reframing and truncating Pauli sums.
  • Frustration-graph analysis backed by lightweight graph tools: connected components, BFS, isomorphism detection, line graphs, and root-graph reconstruction.
  • Fermion-to-qubit encodings: in both Majorana and Dirac forms, with validity checkers. Spin-sectored encodings with selectable operator ordering of Molecular electronic Hamiltonians are also supported.

Aside from these features, Paulimorphic can also be used with other Julia packages as a flexible Pauli-operator constructor.

Working with the rest of the ecosystem

Let’s first construct a simple transverse-field Ising chain Hamiltonian

H = -J \sum_{i=1}^{n-1} Z_i Z_{i+1} - h \sum_{i=1}^{n} X_i

as a PauliSum (assigned to H):

using Paulimorphic

n, J, h = 8, 1.0, 0.5

zz = [stamp!(PauliStr(n), i, symZ, 2) for i in 1:n-1]  # ZᵢZᵢ₊₁
xs = [stamp!(PauliStr(n), i, symX)    for i in 1:n  ]  # Xᵢ
H  = PauliSum([zz; xs], [fill(-J, n-1); fill(-h, n)])  # Canonical-form `PauliSum`

Then, handling the constructed objects in relevant packages takes only a few lines.

Measurement grouping with Graphs.jl

getFrustrationInfo returns the anticommutation (frustration) graph as a Pair of vertices and one-based edge index pairs, which can be used to directly construct a Graphs.SimpleGraph. A proper coloring of this graph partitions the Hamiltonian into mutually commuting groups:

using Graphs: SimpleGraph, add_edge!, greedy_color

terms, edges = getFrustrationInfo(H)  # vertices => anticommuting pairs

g = SimpleGraph(length(terms))
for (i, j) in edges
    add_edge!(g, i, j)
end

coloring = greedy_color(g)
groups = [terms[coloring.colors .== c] for c in 1:coloring.num_colors]

Operator dynamics with PauliStrings.jl

Since a canonical PauliSum has already absorbed all phases into its coefficients, each string it holds forms a one-to-one mapping to a PauliStrings.Operator. Therefore, a Lanczos algorithm is immediately available:

import PauliStrings as PS

function to_ps_operator(h::PauliSum)
    op = PS.Operator(countSites(h))
    for (str, c) in collectTerms(h)
        op += c, replace(toString(str, denseString=true), 'I' => '1')
    end
    op
end

O  = PauliSum([stamp!(PauliStr(n), i, symX) for i in 1:n], fill(1.0, n)) # Σᵢ Xᵢ
bs = PS.lanczos(to_ps_operator(H), to_ps_operator(O), 10, 2^12)

Expectation-value estimation with PauliPropagation.jl

Similarly, Paulimorphic.PauliSum can be easily converted to PauliPropagation.PauliSum, which can then be propagated through a parametrized quantum circuit to evaluate its energy expectation value in the Heisenberg picture:

import PauliPropagation as PP

function to_pp_sum(h::PauliSum)
    nq = countSites(h)
    strs = map(collectTerms(h)) do (str, c)
        syms = Symbol.(collect(toString(str, denseString=true)))
        PP.PauliString(nq, syms, 1:nq, real(c))
    end
    PP.PauliSum(strs)
end

circuit = PP.hardwareefficientcircuit(n, 2)
thetas = 0.1 .* randn(PP.countparameters(circuit))
prop = PP.propagate(circuit, to_pp_sum(H), thetas)
energy = PP.overlapwithzero(prop)

Moving forward

This is only the first release of the package, but I would appreciate feedback on the operator-algebra interface and any suggestions for additional functionality. I’m curious about what scenarios others may find structural analyses of Pauli operators useful for. Issues are welcome!

1 Like