qarp.operators

Operators. Public depth: flat for the operator, mapping and grouping types, plus seven qualified namespaces. Function-first fermionic surface — models, integrals, ucc, onv (bare ucc_singles / active_space_integrals do not carry their domain). Interop and aliases — functions (raw C++ transforms: a bare jordan_wigner would sit beside the JordanWigner Mapping class), compat (openfermion MSB interop, §1) and pyscf (the pyscf recipes). All other submodules are private.

class qarp.operators.BravyiKitaev(n_qubits: int | None = None)[source]

Bases: Mapping

A class for mapping FermionOperators to qubit operators using the Bravyi-Kitaev mapping.

The BK transform of an operator depends on the register width: the update, parity and remainder sets of orbital j change with n_qubits. A list is therefore mapped on one width — n_qubits when given, else the largest count_qubits over the list — so its members compose (e.g. UCC generators against a Hamiltonian). A single operator with n_qubits=None keeps the per-operator inference.

Parameters:

n_qubits – register width for every encode_operator call, or None to infer it (per call, as described above).

encode_operator(op: list[FermionOperator] | FermionOperator)[source]

Perform a Bravyi-Kitaev mapping of the provided operator(s).

Parameters:

op – A list of, or single, FermionOperator object.

Returns:

The result of the mapping as a list of, or single, QubitOperator.

encode_state(onv: list[int]) list[int][source]

Perform a state mapping of a provided onv to the Bravyi-Kitaev basis.

Parameters:

onv – An occupation-number vector (abab list).

Returns:

A list of 0s and 1s representing the initial qubit state in the Bravyi-Kitaev basis.

class qarp.operators.FermionOperator(*args, **kwargs)

Bases: object

openfermion-compatible FermionOperator backed by a packed ladder-sequence C++ core.

coefficient(self, term: object) object

Fast single-term coefficient lookup (qarpx extra, not openfermion).

compress(self, abs_tol: float = 1e-08) None
property constant

(self) -> complex

count_qubits(self) int
free_symbols(self) list[str]

Sorted names of the free symbols across all coefficients.

get_operators(self) list[qarpx.FermionOperator]
hermitian_conjugated(self) qarpx.FermionOperator
identity = <nanobind.nb_func object>
is_symbolic(self) bool
isclose(self, other: qarpx.FermionOperator, tol: float = 1e-08) bool
sparse_matrix(n_qubits=None)

qarpx-LSB scipy.sparse.csc_matrix of the Jordan-Wigner image (qubit q ↔ bit q).

The only matrix API (conventions §1) — directly contractable with qarpx statevectors / unitaries. openfermion’s MSB layout, for interop only, is qarp.operators.compat.get_sparse_operator().

substitute(self, values: object) qarpx.FermionOperator

Substitute named symbols with numeric values; demotes to the numeric backend when no free symbols remain (qarpx extra).

property terms

Term dictionary (read-only view; assign a full dict to replace).

zero = <nanobind.nb_func object>
class qarp.operators.FullyCommuting[source]

Bases: GroupingStrategy

Greedy first-fit under general multi-qubit commutation.

Uses qx.commutes (two Paulis commute iff they disagree on an even number of qubits where both are non-identity) — a strict superset of QWC, so each group holds more terms and fewer measurement circuits are needed. Diagonalisation needs an entangling Clifford (diagonalise_group()).

group(terms: Sequence[Dict[int, str]], n_qubits: int) List[List[int]][source]

Return groups as lists of indices into terms.

qubit_wise: ClassVar[bool] = False
class qarp.operators.GroupingStrategy[source]

Bases: ABC

Partition Pauli terms into groups measurable/exponentiable together.

Contract:
  • group() returns an index partition of terms — every index appears in exactly one group; group and within-group order must be deterministic given the input order (circuit identity depends on it).

  • Partitioning strategies must be order-insensitive: permuting terms must permute the partition, not change it. Greedy first-fit is order-sensitive, so built-in strategies route through canonical_term_order(). Without this the same Hamiltonian written two ways gives two circuits — and for symmetry-carrying operators, two different physics (see test_grouping_is_order_insensitive). NoGrouping is exempt: it makes no grouping decision and deliberately preserves input order.

  • Strategies never see coefficients — those stay positionally attached in the consumer, so one strategy serves measurement (real coeffs), Trotter (angles), and imaginary-time paths alike.

  • qubit_wise = True guarantees every group is qubit-wise commuting, i.e. diagonalisable by single-qubit rotations only (group_basis() applies). Consumers that cannot insert an entangling Clifford (circuit cutting) require this and must check it.

abstractmethod group(terms: Sequence[Dict[int, str]], n_qubits: int) List[List[int]][source]

Return groups as lists of indices into terms.

qubit_wise: ClassVar[bool] = False
class qarp.operators.JordanWigner[source]

Bases: Mapping

A class for mapping FermionOperators to qubit operators using the Jordan-Wigner mapping.

encode_operator(op: list[FermionOperator] | FermionOperator)[source]

Perform a Jordan-Wigner mapping of the provided operator(s).

Parameters:

op – A list of, or single, FermionOperator object.

Returns:

The result of the mapping as a list of, or single, QubitOperator.

encode_state(onv: list[int]) list[int][source]

Perform a state mapping of a provided onv to an initial bitrepresentation.

Parameters:

onv – An occupation-number vector (abab list).

Returns:

A list of 0s and 1s representing the initial qubit state.

class qarp.operators.LinearCombinationUnitaries(A: ndarray, *, pad: bool = True, tol: float = 1e-12)[source]

Bases: object

Pauli/LCU decomposition of a matrix.

The decomposition represents a square power-of-two matrix as

A = sum_P c_P P

where P is a tensor product of single-qubit Pauli matrices. If the input matrix is not square power-of-two sized and pad=True, it is decomposed after zero-padding to the next square power-of-two dimension.

A is read in the qarpx LSB basis (row/column bit k ↔ qubit k, conventions §1) and Pauli strings are qubit-ordered: character k acts on qubit k, so "XZ" is Z_1 X_0 = kron(Z, X). This is the same indexing as QubitOperator terms and PauliBlock strings, and to_QubitOperator().sparse_matrix() reproduces A.

adjust_padding(matrix: ndarray | None = None) ndarray[source]

Return a zero-padded square power-of-two copy of matrix.

assert_qubitlike_shape(matrix: ndarray | None = None) bool[source]

Return True when the matrix is square with dimension 2**n.

decomposition(tol: float | None = None) List[Tuple[str, complex]][source]

Return the Pauli decomposition as [(pauli_string, coefficient), ...].

ij_code2_pstr(ns: Tuple[int, int], length: int) str[source]
ij_code2sym_code(i: int, j: int) Tuple[int, int][source]
itpd_core(matrix: ndarray) ndarray[source]

Inverse TPD transform: restore the standard matrix representation.

padded_matrix() ndarray[source]

Return the matrix used for decomposition, padding if requested.

pauli_basis2ppoly(cmat: ndarray, tol: float | None = None) List[Tuple[str, complex]][source]

Transform a Pauli-basis coefficient matrix into Pauli-string terms.

pauli_str_to_matrix(pauli_string: str) ndarray[source]

Convert a qubit-ordered Pauli string such as 'XXYI' to its LSB matrix P_{n-1} P_0.

ppoly2pauli_basis(ppoly: List[Tuple[str, complex]], *, nqubits: int | None = None) ndarray[source]

Convert weighted Pauli strings into the TPD coefficient matrix.

pstr2ij_code(pstr: str) Tuple[int, int][source]
pstr2sym_code(pstr: str, sim_code: dict[str, Tuple[int, int]] | None = None) Tuple[int, int][source]
reconstruct(ppoly: List[Tuple[str, complex]], *, nqubits: int | None = None, crop: bool = False) ndarray[source]

Reconstruct a matrix from a Pauli decomposition.

If crop=True, the reconstructed padded matrix is cropped back to the original input shape.

sym_code2ij_code(x: int, z: int) Tuple[int, int][source]
sym_code2pstr(ns: Tuple[int, int], length: int) str[source]
to_QubitOperator(tol: float | None = None)[source]

Convert the decomposition to a qarp.operators.QubitOperator.

tpd(matrix: ndarray) ndarray[source]

Tensorized Pauli decomposition core transform.

This returns the coefficient matrix in Pauli-basis indexing. The input is copied, so callers do not need to guard against mutation.

class qarp.operators.Mapping[source]

Bases: ABC

Abstract base class for fermion-to-qubit mappings.

abstractmethod encode_operator(op: list[FermionOperator] | FermionOperator) list[QubitOperator] | QubitOperator[source]

Map the provided FermionOperator(s) to QubitOperator(s).

abstractmethod encode_state(onv: list[int]) list[int][source]

Map an occupation-number vector to the mapped qubit basis state.

class qarp.operators.NoGrouping[source]

Bases: GroupingStrategy

One term per group — the termwise (circuit-per-term) behaviour.

group(terms: Sequence[Dict[int, str]], n_qubits: int) List[List[int]][source]

Return groups as lists of indices into terms.

qubit_wise: ClassVar[bool] = True
class qarp.operators.Parity(n_qubits: int)[source]

Bases: Mapping

A class for mapping FermionOperators to qubit operators using the parity mapping. :param n_qubits: number of qubits in the qubit register.

encode_operator(op: list[FermionOperator] | FermionOperator)[source]

Perform a parity mapping of the provided operator(s).

The transform runs in the qarpx C++ kernel (a verbatim port of the previous hand-rolled Python implementation, term order included).

Parameters:

op – A list of, or single, FermionOperator object.

Returns:

The result of the mapping as a list of, or single, QubitOperator.

encode_state(onv: list[int]) list[int][source]

Perform a state mapping of a provided onv to the parity basis.

Parameters:

onv – An occupation-number vector (abab list).

Returns:

A list of 0s and 1s representing the initial qubit state in the parity basis.

class qarp.operators.QubitOperator(*args, **kwargs)

Bases: object

openfermion-compatible QubitOperator backed by a packed binary-symplectic C++ core (PauliEngine design, arXiv:2601.02233).

coefficient(self, term: object) object

Fast single-term coefficient lookup (qarpx extra, not openfermion).

compress(self, abs_tol: float = 1e-08) None
property constant

(self) -> complex

count_qubits(self) int
free_symbols(self) list[str]

Sorted names of the free symbols across all coefficients.

get_operators(self) list[qarpx.QubitOperator]
hermitian_conjugated(self) qarpx.QubitOperator
identity = <nanobind.nb_func object>
is_symbolic(self) bool
isclose(self, other: qarpx.QubitOperator, tol: float = 1e-08) bool
sparse_matrix(n_qubits=None)

qarpx-LSB scipy.sparse.csc_matrix of the operator (qubit q ↔ bit q).

The only matrix API (conventions §1) — directly contractable with qarpx statevectors / unitaries. openfermion’s MSB layout, for interop only, is qarp.operators.compat.get_sparse_operator().

substitute(self, values: object) qarpx.QubitOperator

Substitute named symbols with numeric values; demotes to the numeric backend when no free symbols remain (qarpx extra).

property terms

Term dictionary (read-only view; assign a full dict to replace).

zero = <nanobind.nb_func object>
class qarp.operators.QubitWiseCommuting[source]

Bases: GroupingStrategy

Greedy first-fit under qubit-wise commutation.

QWC is the special case of commutation where every shared qubit carries the same Pauli, so a group diagonalises with a per-qubit basis change (H for X, Sdg·H for Y) — no entangling Clifford needed.

group(terms: Sequence[Dict[int, str]], n_qubits: int) List[List[int]][source]

Return groups as lists of indices into terms.

qubit_wise: ClassVar[bool] = True
qarp.operators.basis_rotation_grouping(integrals_1e: NDArray, integrals_2e: NDArray, tolerance: float = 1e-08) tuple[list[float], list[FermionOperator], list[NDArray]][source]

Group a molecular Hamiltonian by basis-rotated number-operator sets.

Uses the spin-summed excitation operators E_pq = Σ_σ a†_pσ a_qσ:

H = const + Σ_pq h'_pq E_pq + ½ Σ_r λ_r (Σ_p ν_r[p] ñ_p)²

with h'_ps = h_ps ½ Σ_q (pq|qs) absorbing the reordering term and ñ_p = n_2p + n_2p+1 the spin-summed number operator. Each group is therefore diagonal once the spatial rotation is applied to both spin sublattices.

Parameters:
  • integrals_1e – Spatial one-electron matrix, chemists’ MO basis.

  • integrals_2e – Spatial two-electron order-4 tensor, chemists’ (pq|rs).

  • tolerance – Relative factor-discard threshold (see double_factorization()) — higher discards more factors (fewer groups, larger deterministic bias).

Returns:

per-group coefficients c_ℓ (c₀ = 1 for the corrected one-body group, then the descending-|λ| two-electron factors), diagonal number-operator FermionOperator``s ``G_ℓ, and real orthogonal spin-orbital (abab) rotation matrices u_ℓ. Reconstruction contract, pinned by the re-summation test: H = constant + Σ_ℓ c_ℓ · U(u_ℓ) G_ℓ U(u_ℓ)†.

Return type:

(coefficients, groups, rotations)

qarp.operators.diagonal_group_to_masks(operator: FermionOperator, n_qubits: int) tuple[float, dict[int, float]][source]

Expand a number-operator polynomial into Z-parity masks.

n_p = (1 Z_p)/2 — the Jordan-Wigner occupation identity, matching the JW-only rotation circuit — so a diagonal group becomes a constant plus {z_mask: coefficient}. This is the bridge from the fermionic groups to the shot post-processing: run() consumes the masks directly on computational-basis counts.

Raises:

ValueError – for any term that is not (), a†_p a_p, or a†_p a_p a†_q a_q.

qarp.operators.double_factorization(tensor: NDArray, tolerance: float = 1e-08) tuple[NDArray, NDArray, NDArray][source]

Double low-rank factorization T[p,q,r,s] = Σ_r λ_r L_r[p,q] L_r[r,s] with L_r = u_r diag(ν_r) u_rᵀ.

Parameters:
  • tensor – Order-4 tensor, square in every axis and symmetric under (pq)↔(qp), (rs)↔(sr) and (pq)↔(rs). Both eigendecompositions need that symmetry, and neither reports its absence: eigh reads one triangle and returns a wrong answer.

  • tolerance – First-stage factors with |λ| <= tolerance · max|λ| are discarded — a deterministic truncation bias, not noise. The cut is relative because the scale of λ is system-dependent (max|λ| spans ~1–15 across STO-3G molecules), so no absolute threshold is portable. Exact-null factors are always dropped: they are the tensor’s null space, not truncation.

References

PennyLane qchem.factorize; InQuanto’s double-factorization manual page — both document the same two-eigendecomposition construction.

Returns:

surviving eigenvalues sorted by descending |λ| (shape (L,)), their factors’ eigenvalue vectors ((L, n)) and eigenvector matrices ((L, n, n), eigenvectors in columns).

Return type:

(λ, ν, u)

qarp.operators.fermion_operator_from_tensor(tensor: NDArray, threshold: float = 1e-12) FermionOperator[source]

Build the k-body FermionOperator of a 2k-index spin-orbital tensor.

T[p1..pk, q1..qk] contributes T · a†_p1 a†_pk · a_q1 a_qk.

Parameters:
  • tensor – A 2k-index numpy tensor over spin orbitals (k = 1, 2, 3, …).

  • threshold – Entries with absolute value below this are dropped.

Returns:

The corresponding FermionOperator.

qarp.operators.group_basis(group_indices: Sequence[int], pauli_dicts: Sequence[Dict[int, str]]) Dict[int, str][source]

Per-qubit basis ('X'/'Y'/'Z') for a QWC group.

By the QWC property every term agrees on each shared qubit, so the union over the group’s terms gives the basis-change recipe. Qubits absent from every term are omitted (no basis change needed).

qarp.operators.orbital_rotation_generator(parameters: NDArray, n: int) NDArray[source]

Real skew-symmetric generator kappa from its n(n-1)/2 free entries.

The (n, n) matrix orbital_rotation_matrix() exponentiates. kappa[p, q] = parameters[k] for (p, q) in np.tril_indices(n, -1) order, kappa[q, p] = -kappa[p, q], zero diagonal — the flat real vector an optimiser drives.

Parameters:
  • parameters – Real vector of length n * (n - 1) // 2.

  • n – Number of orbitals.

Returns:

The real skew-symmetric (n, n) generator.

Raises:

ValueError – wrong parameter count, or complex parameters.

qarp.operators.orbital_rotation_matrix(kappa: NDArray) NDArray[source]

U = exp(-kappa) for an anti-Hermitian generator kappa.

New orbitals are the columns of U, so C_new = C_old @ U and rotate_tensor(U, tensor) expresses tensor in the new basis. Real skew-symmetric kappa gives a real orthogonal U, which may be applied to chemists’-notation integrals; complex anti-Hermitian kappa gives a unitary U that is only valid on creators-first spin-orbital tensors (the caveat of rotate_tensor()). For the JW circuit of a spatial rotation expand first with qarp.operators.integrals.spatial_to_spin_orbital().

Parameters:

kappa – Square anti-Hermitian matrix, kappa == -kappa.conj().T.

Returns:

The (n, n) rotation expm(-kappa); real when kappa is real.

Raises:

ValueErrorkappa is not a square 2-D array, or is not anti-Hermitian (checked with rtol=0, atol=1e-12).

qarp.operators.orbital_rotation_parameters(kappa: NDArray) NDArray[source]

Inverse of orbital_rotation_generator().

The packed parameter vector of a real skew-symmetric generator: strict-lower-triangle entries in np.tril_indices(n, -1) order.

Parameters:

kappa – Real skew-symmetric (n, n) matrix.

Returns:

Real vector of length n * (n - 1) // 2.

Raises:

ValueErrorkappa is not a real square skew-symmetric matrix (checked with rtol=0, atol=1e-12).

class qarp.operators.qDRIFT(H: QubitOperator, samples: int, ratio: float | None = None, verbose: bool = False, seed: int | None = None)[source]

Bases: object

partially_randomized() QubitOperator[source]

Apply partially randomized qDRIFT approximation.

Keeps the most significant terms deterministically and samples the remaining terms randomly according to their weights.

Returns:

Approximate QubitOperator with deterministic + sampled terms

Raises:

ValueError – If ratio was not provided during initialization

qdrift() QubitOperator[source]

Apply fully randomized qDRIFT approximation.

Samples Pauli terms according to their weight distribution and constructs an approximate Hamiltonian.

Returns:

Approximate QubitOperator with sampled terms

Raises:

ValueError – If samples is 0 (at least 1 sample required)

qarp.operators.rotate_tensor(u: NDArray, tensor: NDArray) NDArray[source]

Change of single-particle basis for a 2k-index coefficient tensor.

In the creators-first convention of fermion_operator_from_tensor(), creator slots contract with u.conj() and annihilator slots with u — the rank-2k generalization of \(u^\dagger h u\), valid for complex unitaries. The new modes are the columns of u; a rectangular (n, m) matrix projects into an m-mode basis (truncation).

For real orthogonal u every slot transforms identically, so the same function also rotates chemists’-notation integral tensors (where creator/annihilator slots alternate).

Parameters:
  • u – The (n, m) basis-change matrix; new mode p is column p.

  • tensor – A 2k-index tensor over the n old modes.

Returns:

The tensor over the m new modes.