Release Notes

Upcoming release (327/merge)

New Features

  • Added a C API for the edge-vertex and transfer-vertex operator representations, which were previously available only from Python. This includes two new opaque structs, QfEdgeVertexOperator and QfTransferVertexOperator, each one with an equivalent set of functions to the existing operator structs.

    Both operators store two parallel index arrays, left_indices and right_indices, in place of the single modes array the Majorana operator uses. Because a generator is always identified by exactly one (left, right) pair, the two arrays necessarily have the same length and the constructors therefore take a single num_indices argument covering both.

  • Added qf_edge_op_canonical_order and qf_transfer_op_canonical_order, along with the commutator, anti-commutator and double-commutator functions for both new operator types (qf_edge_op_commutator and friends).

  • How an Evolution gate gets decomposed in fermionic space is now configurable through its new keyword-only synthesis argument, mirroring the synthesis argument of Qiskit’s PauliEvolutionGate. The available methods live in the new qiskit_fermions.circuit.library.synthesis module, which provides the FermionicEvolutionSynthesis interface and its first-order FermionicLieTrotter implementation.

    This fermion-to-fermion step is optional: an Evolution gate can be handed straight to the fermion-to-qubit stage regardless of how many terms its operator holds. Decomposing it beforehand is a choice, taken to obtain cheaper factors or to expose structure (such as mutually commuting groups) that the later stage can exploit. Because both sides of the rewrite stay in fermionic space, that structure survives it, which is what these methods can exploit and Qiskit’s EvolutionSynthesis cannot.

    The default is unchanged: leaving synthesis at None uses FermionicLieTrotter, which reproduces exactly the decomposition that Evolution performed before, so existing circuits synthesize identically.

  • Improved the numerical conditioning of state-vector simulation for Evolution and OrbitalRotation gates by supplying the exact fixed-sector operator trace to SciPy’s matrix exponential routine.

  • Added FermionicSuzukiTrotter, a higher-order product formula for decomposing an Evolution gate in fermionic space. Where FermionicLieTrotter applies each factor once, this composes them symmetrically to cancel lower-order error terms, and its reps argument divides the evolution into several shorter steps:

    Evolution(num_modes, operator, time=1.0, synthesis=FermionicSuzukiTrotter(order=2, reps=4))
    

    Because it operates on the fermionic operator, it splits it along its groups where they are assigned – a partition that is no longer available to a product formula applied after the fermion-to-qubit mapping. On a six-mode Fermi-Hubbard chain grouped into three flow sets, order=2 with reps=4 reached a Trotter error roughly two times lower than a second-order qubit-side formula, at slightly fewer two-qubit gates.

    FermionicLieTrotter is the first-order member of this family and is now implemented as such, which also gives it the reps argument it previously lacked. The two are interchangeable at equal reps.

    Note that a higher order buys accuracy with depth: an order-k formula emits roughly 5**((k-2)/2) times as many factors as an order-2 one. Note also that the accuracy of a decomposed circuit is governed by the weaker of the fermionic and the fermion-to-qubit formula, so raising the order here while the latter stays first-order buys little.

  • Added FermionicTrotterization, a transpiler pass that selects the fermion-to-fermion synthesis method of every Evolution gate in a circuit:

    pm.optimization = FermionicPassManager(
        [FermionicTrotterization(FermionicSuzukiTrotter(order=2, reps=4))]
    )
    

    Choosing a synthesis method per gate means threading it through everything that constructs an Evolution – including UCC and UCJ, which build their own internally. This pass makes the choice once for a whole pipeline instead. An optional filter predicate restricts it to a subset of the gates, which is worth using when a circuit mixes evolutions that benefit from a higher order with ones that do not, such as the all-commuting diagonal-Coulomb operators of a UCJ.

    The pass selects a method rather than expanding the evolution immediately, so each Evolution stays a single node for the passes that follow – notably RelabelModes, which needs whole operators to build its mode-relabeling model. This is the opposite choice from QDriftTrotterization, whose random one-shot sampling cannot be deferred.

  • jordan_wigner() now dispatches on all four operator types rather than only FermionOperator, delegating to whichever direct implementation matches the operator it is given. Passing one of the other three operator types previously raised a TypeError.

  • The is_hermitian() method is now part of the OperatorTrait protocol. Every operator class already provided it, so it can now be called on any value typed as an OperatorTrait rather than only on a concrete operator class.

    Note that the strength of this check varies by operator type, which the protocol documents as its weakest guarantee: a True result is always reliable, while a False result is conservative for operator types whose normal form is not a genuine canonical form. See is_hermitian() for the one such case.

  • Added two adapters that wrap a mapper function to control the Pauli term order it produces, which MapperFnEvolutionSynthesis now preserves through synthesis.

    simplify() simplifies the mapped operator, merging duplicate Pauli terms and pinning a canonical term order.

    group_wise() maps an operator one groups entry at a time and sums the results. The operator is unchanged, but the terms of each group come out adjacent rather than interleaved, which lets those acting on disjoint qubits share a circuit layer:

    MapperFnEvolutionSynthesis(group_wise(jordan_wigner))
    

Upgrade Notes

  • The qiskit_fermions.linalg.apply_unitary and qiskit_fermions.linalg.linear_operator functions have been removed. They were thin wrappers around the SupportsApplyUnitary and SupportsLinearOperator protocol methods, duplicating ffsim.apply_unitary() and ffsim.linear_operator(). Call ffsim’s functions instead, or, when ffsim is not installed, the protocol method on the object directly:

    # before
    from qiskit_fermions.linalg import apply_unitary, linear_operator
    vec = apply_unitary(vec, gate, norb, nelec)
    linop = linear_operator(operator, norb, nelec)
    
    # after (with ffsim)
    vec = ffsim.apply_unitary(vec, gate, norb=norb, nelec=nelec)
    linop = ffsim.linear_operator(operator, norb=norb, nelec=nelec)
    
    # after (without ffsim)
    vec = gate._apply_unitary_(vec, norb, nelec, copy=True)
    linop = operator._linear_operator_(norb, nelec)
    

    The protocols themselves are unchanged.

  • The simulation optional dependency has been renamed to ffsim, so pip install "qiskit-fermions[simulation]" becomes pip install "qiskit-fermions[ffsim]". The extra names the dependency it installs rather than a capability. Installing qiskit-fermions[all] is unaffected.

  • MapperFnEvolutionSynthesis no longer simplifies the operator returned by its mapper_fn. Simplifying sorts the Pauli terms into a canonical order, and a product formula synthesizes them in the order it receives them, so the previous behavior discarded whatever order the mapper had chosen.

    This changes the synthesized circuit for existing pipelines: the gate count is unaffected, but the order the rotations are emitted in (and hence the two-qubit depth) may differ. The evolution being approximated is unchanged.

    Note also that the term order a mapper produces is not guaranteed to be stable between runs, because the operators of the Rust core do not preserve the order their terms were added in. Wrap the mapper in the new simplify() to restore the previous behavior and pin a canonical order:

    MapperFnEvolutionSynthesis(simplify(jordan_wigner))
    
  • The qiskit_fermions.linalg.fci module has been removed along with the kernel, including its FciLinearOperator class and the slater_determinant_statevector and occupation_axis_mask functions. ffsim provides equivalents: ffsim.slater_determinant() and ffsim.addresses_to_strings() respectively.

  • An operator that does not conserve particle number, or the z-component of spin in the spinful case, is now rejected by ffsim rather than by this package’s own check, so the ValueError that Evolution surfaces when simulating one carries a different message.

  • The UCC gate is now constructed from one of ffsim’s UCCSD operators, which is its only constructor argument. The spin variant and the number of modes are read off that operator, so the variant argument and the UCC.Variant enum are gone:

    # before
    ansatz = UCC("restricted", t1, t2)
    
    # after
    ansatz = UCC(ffsim.UCCSDOpRestrictedReal(t1=t1, t2=t2))
    

    Accordingly, UCC.from_t_amplitudes, UCC.num_parameters, UCC.from_parameters and UCC.to_parameters have been removed, as have the spinless variant and the opt-in antisymmetric parameterization, which have no ffsim equivalent. ffsim’s operators provide n_params(), from_parameters() and to_parameters() with identical conventions. To build an ansatz outside that family (a spinless one, or an antisymmetrized \(t_2\)), construct an Evolution over your own cluster operator directly, which also gives you control over the Trotter ordering of its terms.

    The wrapped operator is available as UCC.uccsd_op, so its amplitudes are reachable as gate.uccsd_op.t1 and gate.uccsd_op.t2; the gate no longer mirrors them as attributes of its own. UCC.cluster_operator() is unchanged. A final_orbital_rotation carried by the ffsim operator is now appended as a closing OrbitalRotation.

    Since ffsim is now the gate’s input type rather than an optional accelerator, constructing a UCC requires the ffsim extra (pip install "qiskit-fermions[ffsim]") and raises MissingOptionalLibraryError without it. ffsim does not support Windows, so the gate is unavailable there; use WSL.

  • The UCJ gate is now constructed from one of ffsim’s UCJ operators, which is its only constructor argument. The spin variant, the number of spatial orbitals and the number of modes are read off that operator, so the variant argument and the UCJ.Variant enum are gone:

    # before
    ansatz = UCJ.from_t_amplitudes(nelec, t2, t1=t1, n_reps=2)
    ansatz = UCJ("balanced", diag_coulomb_mats, orbital_rotations)
    
    # after
    ansatz = UCJ(ffsim.UCJOpSpinBalanced.from_t_amplitudes(t2, t1=t1, n_reps=2))
    ansatz = UCJ(ffsim.UCJOpSpinBalanced(diag_coulomb_mats, orbital_rotations))
    

    Accordingly, UCJ.from_t_amplitudes, UCJ.num_parameters, UCJ.from_parameters and UCJ.to_parameters have been removed: ffsim’s operators provide from_t_amplitudes(), n_params(), from_parameters() and to_parameters() with identical conventions, and additionally offer the compressed (optimize=True) double factorization and from_cisd_vec(), which this package never implemented. The wrapped operator is available as UCJ.ucj_op, so its tensors are reachable as gate.ucj_op.diag_coulomb_mats and so on; the gate no longer mirrors them as attributes of its own.

    Since ffsim is now the gate’s input type rather than an optional accelerator, constructing a UCJ requires the ffsim extra (pip install "qiskit-fermions[ffsim]") and raises MissingOptionalLibraryError without it. ffsim does not support Windows, so the gate is unavailable there; use WSL.

Bug Fixes

  • Fixed the excessive memory consumption of fermion_jordan_wigner() (and therefore of jordan_wigner() when applied to a FermionOperator). The mapper accumulated the mapped terms using an addition that concatenates rather than merges duplicate Pauli terms, so the intermediate observable grew with the number of Pauli terms emitted instead of the number of distinct ones. Mapping a large Hamiltonian could therefore exhaust the available memory even though the simplified result was orders of magnitude smaller. The accumulators are now canonicalized once they have grown by a factor over their previously merged size, which bounds the memory they hold in terms of the operator they actually represent.

    Peak memory still grows with the number of worker threads: the terms are handed to whichever thread is free rather than partitioned by the Pauli strings they produce, so each accumulator ends up holding roughly a full copy of the mapped operator. What changed is the factor each one carries – previously proportional to the number of Pauli terms emitted, now to the number of distinct ones. Reducing the thread count through rayon’s RAYON_NUM_THREADS environment variable therefore lowers the peak, roughly in proportion, and is the control to reach for when memory is tight.

    The mapped operator is unchanged: canonicalization only merges duplicate terms, so the result represents exactly the same observable as before.

  • fermion_jordan_wigner() no longer copies the operator it is given. The Python binding took its argument by value, which duplicated every term buffer of the input FermionOperator purely in order to read it.

  • Fixed a decomposition of the Evolution gate that never terminated. The evolution of a single operator term used to decompose into an identical single-term Evolution, so repeated expansion made no progress. Most visibly this made inverse() raise a RecursionError, since it recurses through the gate’s definition.

    A factor emitted by a FermionicEvolutionSynthesis is now marked Evolution.atomic and is left in place by decompose() instead of being expanded again. Decomposing an Evolution repeatedly therefore reaches a fixed point.

  • Fixed decompose() producing non-unitary factors when applied more than twice to an operator carrying groups. Each group was split further, term by term, but an individual term is generally not Hermitian even when the group containing it is – the conjugate pairs of a UCC cluster generator being the motivating example. The exponential of such a factor is not unitary.

    The fermion-to-qubit stage rejected those factors with a ValueError about complex coefficients, but state-vector simulation applied them without complaint and returned a non-normalized state. Since a group is now atomic, the split that produced them no longer happens.

  • Evolution.inverse() is now implemented directly, returning an Evolution that evolves the same operator for the negated time and preserves the gate’s synthesis method. Previously the inherited implementation recursed through the gate’s definition and returned a plain Gate, discarding both the operator and the synthesis method.

0.1.0

Prelude

This is the first release of Qiskit Fermions, an extension of Qiskit with tools for fermionic systems. It provides operator data structures, a framework and library for converting those operators to qubit form (mappers), and a framework and library for synthesizing the corresponding quantum circuits. Following Qiskit’s design philosophy, the core of this package is written in Rust and exposed through first-party bindings for both Python and C.