Release Notes

0.2.0

Prelude

This release grows the control this package gives over circuit synthesis. An Evolution gate now takes an explicit synthesis method, a new synthesis module supplies fermionic product formulas of first and higher order, and the FermionicTrotterization pass selects and applies one across every evolution in a circuit at once. Alongside it, the Jordan-Wigner mapper gained direct implementations for all four operator representations, and new adapters let a mapper’s output be simplified or emitted group by group. The C API caught up considerably. The edge-vertex and transfer-vertex representations, their mappers and their algebra are now reachable from C, as are the operator support, sector conservation, in-place and scaled arithmetic, and group ordering. Both APIs also gained access to the electronic integrals parsed by an FCIDump, which previously could only be reached by converting the whole data structure into an operator. Additionally, this release draws a sharper boundary around what this package is for: fermionic operators, the mappers that convert them, and the mapper-agnostic circuit and its transpilation. Simulation is ffsim’s concern, so the native FCI kernel and the linear-algebra utilities that duplicated it have been removed in favor of delegating to ffsim, and the UCC and UCJ gates now wrap ffsim’s operators instead of reimplementing their parameterizations. Simulation consequently requires the ffsim extra and, through ffsim’s dependency on PySCF, is unavailable on Windows; building operators, mapping them and transpiling the resulting circuits remain supported everywhere. Since this is still a pre-1.0 release, all of this was done by removal rather than deprecation: the upgrade notes below pair every removed API with its replacement, and are worth reading in full before upgrading.

Python API Features

  • 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.

  • The sampling weights of QDriftTrotterization can now be supplied through its new keyword-only weights argument, instead of being derived from the evolved operator on every call. Leaving it at None (the default) behaves exactly as before, so existing circuits are sampled identically.

    This is a performance option: the default path reduces one value per operator term down to one per group on every call to run(), which is repeated work when generating an ensemble from a single Hamiltonian, even though the result is identical every time. Derive it once with group_coeff_means() instead, alongside group_order(), which hoists the group lookup out of the same loop:

    hamiltonian = group_order(hamiltonian)
    weights = group_coeff_means(hamiltonian)
    pm.optimization = FermionicPassManager(
        [QDriftTrotterization(num_groups, weights=weights)]
    )
    

    The entries must be non-negative, since a weight is the magnitude \(h_j\) of the qDRIFT decomposition \(H = \sum_j h_j H_j\), in which a coefficient’s sign belongs to \(H_j\) and is read off the evolved operator directly.

    Two constraints come with a supplied array. Its scale is not free: the sum of the weights also sets the shared evolution time, so multiplying every entry by gamma samples identically but evolves for gamma * t. And because the array describes the terms (or groups) of one specific operator, its length is validated against every Evolution gate it is applied to, and a circuit holding more than one such gate is rejected; leaving weights unset keeps such a circuit supported, since each gate then derives its own.

  • FCIDump now exposes the electronic integrals it parsed, which previously were reachable only by converting the whole data structure into a FermionOperator. The new methods return read-only copies as NumPy arrays, in the same flattened layout the electronic-integral constructors consume: get_one_body_tril_a(), get_one_body_tril_b(), get_two_body_tril_aa(), get_two_body_tril_ab() and get_two_body_tril_bb(). The beta-spin arrays return None for a spin-restricted file; the new is_unrestricted attribute reports on all three at once. The new constant attribute gives the constant (nuclear-repulsion) energy, or None when the file carries none:

    fcidump = FCIDump.from_file("molecule.fcidump")
    one_body = fcidump.get_one_body_tril_a()
    two_body = fcidump.get_two_body_tril_aa()
    

    Note that get_two_body_tril_ab() is packed differently from the other two-body arrays: it is only 4-fold symmetric, so it holds the full (npair, npair) matrix, whose row pair indexes the alpha-spin and whose column pair the beta-spin species. Refer to the class documentation for the index formula of every array. Expanding these into dense (norb, norb) or (norb,) * 4 tensors, as required by sample-based diagonalization tooling for example, is left to the caller.

  • 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 Trotterizes every Evolution gate in a circuit with one fermion-to-fermion synthesis method:

    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 expands each selected gate into the factors its method emits, so no separate expansion step is needed. Pass apply=False to only select the method and leave the expansion to something else, such as Qiskit’s Decompose; note that a gate which is never expanded reaches the fermion-to-qubit stage whole, where it is mapped without Evolution.synthesis ever being read.

  • The qiskit_fermions.operators.terms.grouping module now covers the analysis of an existing grouping alongside the assignment of group indices. Group indices carry no intrinsic meaning, so none of the new functions reports whether a grouping is “correct”; each answers one narrow question about a grouping, so that an assumption a downstream consumer makes can be checked up front rather than being paid for on every call:

    • groups_are_hermitian() reports, for each group, whether the operator formed by its terms is Hermitian. This is the property a randomized product formula relies on when it samples whole groups, since only a Hermitian group has a unitary time evolution.

    • groups_have_uniform_coeffs() reports, for each group, whether its coefficients are numerically equal (by magnitude, by default).

    See Group operator terms: use the operator structure for a worked example.

  • Added group_order(), which returns a copy of an operator with its terms ordered by group index. Each group becomes one contiguous run of terms and the group indices become non-decreasing; the sort is stable, so terms within a group keep their relative order. An operator tracking no group indices has nothing to order by and is returned as an unchanged copy.

    Group indices only say which terms belong together, so this changes an operator’s term layout, not its value. The layout is what makes group lookup cheap: split_out_groups() has to scan every term to find the requested groups in general, but on a group-ordered operator it binary-searches the group boundaries instead, so a lookup costs what the requested groups cost rather than what the held terms cost. Ordering once up front therefore pays off across repeated lookups:

    from qiskit_fermions.operators.terms.ordering import group_order
    
    hamil = group_order(hamil)
    # every subsequent hamil.split_out_groups(group_indices=...) is now a binary search
    

    This is worth doing before repeatedly transpiling with QDriftTrotterization, which looks up its sampled groups on every call. See Generate SqDRIFT circuits for a worked example.

  • 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))
    
  • QDriftTrotterization now records how many draws its filter_trivial mode discarded. When at least one Evolution gate was actually filtered, the returned circuit’s metadata carries filter_trivial.discarded and filter_trivial.emitted, each holding one count per filtered gate in circuit order. Their ratio estimates the acceptance probability, which is the factor by which the filtering inflated the coefficients of the terms it retained. Neither field is present when no gate was filtered, so read them with .get().

C API Features

  • Added qf_ferm_op_conserves_sector(), which checks that every term of a QfFermionOperator conserves the particle number within each block of modes. It complements the existing qf_ferm_op_conserves_particle_number(), which only checks the total. For a spin-orbital layout of norb spatial orbitals, passing the two blocks {norb, norb} requires the alpha and beta sectors to be conserved separately, i.e. strict conservation of both the particle number and the z-component of the spin:

    uint32_t spin_blocks[2] = {norb, norb};
    bool conserves = qf_ferm_op_conserves_sector(op, spin_blocks, 2);
    

    Passing NULL with a block count of 0 treats all modes as a single block, which is equivalent to calling qf_ferm_op_conserves_particle_number().

  • Added the operator support to the C API, which was previously available only from Python via get_support. Each of the four operator representations gained a pair of functions: qf_ferm_op_num_support() and qf_ferm_op_get_support(), plus the qf_maj_op, qf_edge_op and qf_transfer_op equivalents.

    Unlike the other getters, these do not hand out a pointer into the operator, because the support is computed on demand rather than stored. Call the num_support function first to size the output buffer, then pass that buffer to get_support:

    uint32_t num_support = qf_ferm_op_num_support(op);
    uint32_t *support = malloc(num_support * sizeof(uint32_t));
    qf_ferm_op_get_support(op, support);
    

    The C functions write the mode indices in ascending order, whereas the Python method returns an unordered set.

  • Added in-place operator arithmetic to the C API, which avoids allocating a result operator when the left operand can be overwritten. Each of the four operator representations gained add_inplace, scaled_add_inplace and mul_inplace, for example qf_ferm_op_add_inplace(), qf_ferm_op_scaled_add_inplace() and qf_ferm_op_mul_inplace():

    // Accumulate `factor * right` into `left` without building an intermediate.
    QkComplex64 factor = {-1.0, 0.0};
    qf_ferm_op_scaled_add_inplace(left, right, &factor);
    

    Mind that the three differ in their treatment of group indices. The two additions append the terms of the right operand and therefore reset the groups attribute to NULL, just as qf_ferm_op_add() does. mul_inplace only scales the coefficients, which leaves the number of terms unchanged, and so it preserves the grouping.

  • 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 scaled_add to the C API for all four operator representations: qf_ferm_op_scaled_add(), qf_maj_op_scaled_add(), qf_edge_op_scaled_add() and qf_transfer_op_scaled_add(). Each returns left + factor * right, folding the scaling into the addition so that no scaled copy of the right operand is built along the way.

    This is also how the C API spells subtraction, since a factor of -1 negates the appended coefficients:

    QkComplex64 minus_one = {-1.0, 0.0};
    QfFermionOperator *difference = qf_ferm_op_scaled_add(left, right, &minus_one);
    

    As for qf_ferm_op_add(), the terms of the right operand are appended rather than combined with those of the left, so the result tracks no group indices. Call the corresponding simplify function to collect equal terms.

Python API Upgrade Notes

  • The qiskit_fermions.linalg.apply_unitary and qiskit_fermions.linalg.linear_operator functions have been removed. They were thin wrappers around the _apply_unitary_ and _linear_operator_ 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 methods themselves are unchanged.

  • The qiskit_fermions.protocols.SupportsApplyUnitary and qiskit_fermions.protocols.SupportsLinearOperator protocols have been removed for the same reason: they restated contracts that ffsim defines. Use ffsim’s own protocols, which they mirrored one-to-one:

    The protocol methods (_apply_unitary_ and _linear_operator_) are unchanged, so the objects of this package that implemented them still do; only the local restatement of the interface is gone. SupportsApplyUnitaryPlaced stays, since it extends ffsim.SupportsApplyUnitary with a mode placement and has no ffsim counterpart.

  • from_file() now raises a catchable Python exception when it cannot parse a file, instead of propagating a Rust panic. A path that cannot be opened or read raises OSError, while a file that does not honour the FCIDump format (a missing header namelist, a missing NORB or NELEC field, or a malformed MS2 field) raises ValueError. An FCIDump carrying an MO energy value, which is still unsupported, also raises ValueError.

  • The group_weights method has been removed from every operator class and from the OperatorTrait protocol. It is replaced by group_coeff_means(), a free function in the qiskit_fermions.operators.terms.grouping module, whose behavior is unchanged:

    # before
    weights = op.group_weights()
    
    # after
    from qiskit_fermions.operators.terms.grouping import group_coeff_means
    
    weights = group_coeff_means(op)
    
  • Assigning a groups array whose length differs from the operator’s number of terms now raises a ValueError instead of being accepted silently. Assigning None to clear the group indices 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))
    
  • Simulation now requires ffsim. The native Rust FCI (full configuration interaction) kernel that used to back _linear_operator_ has been removed; the method is now implemented by converting a FermionOperator into an ffsim.FermionOperator and delegating to ffsim.linear_operator().

    Simulation is ffsim’s concern: this package focuses on fermionic mappers and the mapper-agnostic circuit and its transpilation. Because ffsim depends on PySCF, which does not support Windows, simulation is consequently unavailable on Windows; the rest of the package (building operators, mapping them and transpiling the resulting circuits) is unaffected on every platform. Windows users who need to simulate can do so through the Windows Subsystem for Linux.

    Calling a simulation entry point without ffsim installed now raises MissingOptionalLibraryError.

  • 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.

  • 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.

  • Unpickling an operator whose pickled group indices do not number one per term now raises a ValueError instead of producing an operator whose group indices no longer match its terms. This is only reachable for a hand-crafted payload or one written by an incompatible version; pickles written by this version round-trip unchanged.

C API Upgrade Notes

  • qf_fcidump_from_file() now returns a QfExitCode and takes the parsed QfFCIDump through a new out-parameter, matching the convention already used by the mapper functions:

    QfFCIDump *fcidump = NULL;
    QfExitCode exit = qf_fcidump_from_file("molecule.fcidump", &fcidump);
    

    It returns QfExitCode_ValueError when a file does not honour the FCIDump format (a missing header namelist, a missing NORB or NELEC field, a malformed MS2 field, or an MO energy value, which is still unsupported), leaving the out-parameter untouched. This replaces the previous signature, which returned the pointer directly and had no way to report a failure (an unparsable file unwound a Rust panic across the FFI boundary, which is undefined behaviour).

  • qf_ferm_op_group_weights and its per-type counterparts have been renamed to qf_ferm_op_group_coeff_means() and friends. Their behavior is unchanged.

  • qf_ferm_op_set_groups() and its per-type counterparts now return a QfExitCode rather than void. Passing a group array whose length differs from the operator’s number of terms returns QfExitCode_ValueError instead of being accepted silently; passing NULL to clear the group indices is unaffected.

Build System Changes

  • 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.

  • The optimization optional dependency has been renamed to pyomo, so pip install "qiskit-fermions[optimization]" becomes pip install "qiskit-fermions[pyomo]". The extra names the dependency it installs rather than a capability, which the old name over-promised: Pyomo is a modeling language, so it builds the mixed-integer program behind RelabelModes and build_excitation_span_minimization_model() but cannot solve it. Choosing a solver is a separate, deliberate step (RelabelModes takes one through its solver argument and this package does not prescribe which), and naming the extra after Pyomo makes that boundary visible at install time. Installing qiskit-fermions[all] is unaffected.

Bug Fixes

  • Fixed the excessive memory consumption of fermion_jordan_wigner() and of qf_ferm_op_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. It is, however, no longer guaranteed to be fully simplified, and the number of terms it holds may vary with the thread count. Call simplify() (qk_obs_canonicalize in C) if you need every duplicate combined.

  • Fixed QDriftTrotterization discarding the Evolution.synthesis method of the gates it Trotterizes, and emitting sampled gates that could be decomposed into non-unitary factors. The sampled gates now carry over the synthesis method of the gate they replace and are marked Evolution.atomic.

  • Fixed RelabelModes finding a permutation for the wrong Hamiltonian when a circuit’s Evolution gates had already been decomposed. A FermionicEvolutionSynthesis narrows every factor it emits onto that factor’s support, so the factor’s operator carries mode indices local to the gate, while its position in the register is given by the node’s qubit arguments. The automatic optimization read the operator alone, which gathered those local indices: every narrowed two-mode factor became the excitation (0, 1) regardless of which modes it actually coupled, and a long-range coupling dropped out of the model entirely.

    The permutation this returned was valid but optimized for a Hamiltonian that was not the circuit’s, and nothing raised, since local indices are themselves valid mode indices. Circuits whose evolutions reached the pass whole were unaffected. If you relied on the permutation of a decomposed circuit, re-run the pass: it may now return a different (and better) one.

  • 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.

Performance Improvements

  • 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.

  • 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.

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.