Release Notes¶
Upcoming release (main)¶
Python API Features¶
Added
HopEvolutionSynthesis, a plugin for Qiskit’sHighLevelSynthesisthat synthesizes thePauliEvolutionGateof a hop between two qubits (the image of a fermionic hop between adjacent modes under the Jordan-Wigner mapping) as a singleXXPlusYYGate. The gate is the exact evolution, native to common basis-gate sets, and cheaper in two-qubit gates than the generic synthesis. Any other gate is left to the next synthesis method.
Added the
F2FSynthesistranspiler pass, the fermion-to-fermion counterpart ofF2QSynthesis. It breaks gates down into circuits of smaller fermionic gates, with the plugin that itsconfignames for each type of gate:pm.optimization = FermionicPassManager( [F2FSynthesis({"Evolution": ("SuzukiTrotter", (), {"order": 2, "reps": 4})})] )
The plugins follow the same scheme as those of the fermion-to-qubit stage. They implement the
F2FSynthesisPluginprotocol, are managed by theF2FSynthesisPluginManager, and are registered under theqiskit_fermions.circuit.synthesisentry-point group as<gate>.<method>. The builtin ones areEvolution.LieTrotterandEvolution.SuzukiTrotter. A plugin instance can be given to the pass directly instead, in which case no entry point is needed.
Added a
min_num_modes()method toFermionOperator,MajoranaOperator,EdgeVertexOperatorandTransferVertexOperator, returning the smallest number of modes that can host the operator (0if it acts on none). For a Majorana operator this counts fermionic modes.
Added the
MergeEvolutionstranspiler pass. It fuses consecutiveEvolutiongates of equivalent operators on the same modes into a single gate with the summed evolution time, which is exact. This removes the repeated factor where, for example, two repetitions of a product formula meet.
FermionicSuzukiTrottergained amerge_adjacentargument, which is enabled by default. It fuses neighboring gates of the same factor, as found at the seams between repetitions and between the sub-sweeps of the higher orders. The product formula is unchanged, but the synthesized circuit has fewer gates. Passmerge_adjacent=Falseto obtain the previous gate sequence.
An
OrbitalRotationis now decomposed in fermionic space, before any encoding is chosen. The newFermionicGivensDecomposition, registered asOrbitalRotation.Givens, factorizes the rotation into atomicEvolutiongates on one or two modes each.
generate_preset_jw_pass_manager()runsF2FSynthesiswith{"OrbitalRotation": "Givens"}last in itsoptimizationstage, after the passes that consume orbital rotations as such.
The qDRIFT protocol is now a fermion-to-fermion synthesis method on an equal footing with the product formulas.
FermionicQDriftis registered asEvolution.QDrift, so it is selected like any other method:F2FSynthesis({"Evolution": ("QDrift", (num_terms,), {"rng": 42})})
It takes the
num_terms,weightsandrngarguments thatQDriftTrotterizationtook.
QDriftTrotterizationnow draws anEvolutiongate with theFermionicQDriftthat the gate carries as itssynthesis, which lets every gate of a circuit hold theweightsof its own operator. The method passed to the pass is now optional and applies to the gates that carry none; a gate with neither is left in place, with aUserWarning. Asynthesisthat is not aFermionicQDriftis ignored by the pass.
A
PrepareSlaterDeterminantis now decomposed in fermionic space, before any encoding is chosen. The newFermionicSlaterGivensDecomposition, registered asPrepareSlaterDeterminant.Givens, turns the gate into anInitializeModesof the reference followed by atomicEvolutiongates.
C API Features¶
Added
qf_ferm_op_min_num_modes(),qf_maj_op_min_num_modes(),qf_edge_op_min_num_modes()andqf_transfer_op_min_num_modes(), which return the smallest number of modes that can host an operator (0if it acts on none). For a Majorana operator this counts fermionic modes.
Python API Upgrade Notes¶
generate_preset_jw_pass_manager()now registers theHopEvolutionSynthesisahead of the methods that itshls_configlists forPauliEvolution, or ahead of the default method if none is given. This is unconditional, in the same way that the preset has always lowered an orbital rotation toXXPlusYYGates: a method that the given configuration lists (including"default") only applies to what is not a hop. The given configuration is extended and not modified. For a hop with a complex amplitude, theXXPlusYYGatereplaces a product-formula approximation, so the circuit is both shorter and exact where it was previously only accurate up to the Trotter error. To opt out, assemble a pipeline from the individual stages instead of using the preset.
The
FermionicEvolutionSynthesisbase class was removed. A synthesis method for anEvolutionis any object with asynthesize(gate)method that returns aFermionicCircuit; it no longer needs to derive from a class. The type of thesynthesisargument is nowF2FSynthesisPlugin.
FermionicSuzukiTrotternow fuses neighboring gates of the same factor by default, so forreps > 1ororder >= 4it emits fewer, longer-evolving gates than before. The result is mathematically identical. Passmerge_adjacent=Falseto keep the previous gate sequence.
A custom
weightsarray of the qDRIFT sampling is no longer rejected up front for a circuit with more than oneEvolutiongate, since the method that holds it is stateless and sees one gate at a time. Its length is still checked against every gate it is given, so a mismatch raises aValueErroras before.
Python API Deprecation Notes¶
The
FermionicTrotterizationtranspiler pass is deprecated in favor ofF2FSynthesis, which acts on any gate that can be broken down and not only on anEvolution. WhereFermionicTrotterization(method, apply=..., filter=...)was used, writeF2FSynthesis({"Evolution": method}, apply=..., filter=...), or name a registered method such as{"Evolution": "LieTrotter"}.
The
OrbitalRotation.GivensDecompositionandPrepareSlaterDeterminant.GivensDecompositionSlaterfermion-to-qubit synthesis plugins (the classesGivensDecompositionOrbitalRotationSynthesisandGivensDecompositionSlaterDeterminantSynthesis) are deprecated. They still work by running the fermionic decomposition ofF2FSynthesisdirectly and mapping the result under Jordan-Wigner, emitting the same gates as before. Instead, configureF2FSynthesiswith{"OrbitalRotation": "Givens"}or{"PrepareSlaterDeterminant": "Givens"}in theoptimizationstage, after which any encoding that can map anEvolutionhandles the gate. A pipeline assembled by hand also needs theHopEvolutionSynthesisin itsqubitstage to obtainXXPlusYYGates, as the Jordan-Wigner preset has. Theminimize_2q_gate_countargument of the Slater plugin, the peeling of disjoint unit-orbitals, was removed: it has no effect any more and warns.
The former signature of
QDriftTrotterization,QDriftTrotterization(num_terms, *, filter_trivial=False, weights=None, rng=None), is deprecated. Passing anintfirst returns a pass that samples by itself. Instead, writeF2FSynthesis({"Evolution": FermionicQDrift(num_terms, weights=..., rng=...)})wherefilter_trivialwasFalse, andQDriftTrotterization(FermionicQDrift(num_terms, weights=..., rng=...))where it wasTrue.
Bug Fixes¶
F2QSynthesis,RelabelModesandQDriftTrotterizationnow visit a circuit’s instructions in circuit order rather than in the order in which the DAG stores them. The two differ after a pass has replaced a node by a sub-circuit, for example whenFermionicTrotterizationexpands severalEvolutiongates. These passes rebuild their output node by node, so the factors of two successive replacements could be interleaved, producing a well-formed circuit that implements a different unitary.
FermionicSuzukiTrotternow raises aValueErroron construction whenorderis< 1. Previously these values were accepted and synthesis then failed with aRecursionError, unless the evolved operator had a single factor.
Other Notes¶
qiskit~=2.6.0is now supported in addition toqiskit~=2.5.0. The Qiskit requirement is nowqiskit>=2.5.0,<2.7.
0.3.0¶
Prelude¶
This release adds a framework for ternary-tree fermion-to-qubit encodings. A ternary tree is described once and compiled into an encoding that all four operator mappers accept, which makes every mapping expressible as such a tree available, including the Jordan-Wigner, parity, Bravyi-Kitaev and Jiang-Kalev-Mruczkiewicz-Neven (JKMN) encodings. The framework is available from both the Python and the C API, and with it this package moves beyond Jordan-Wigner as the only mapper it ships.
The FermionicCircuit also became considerably more complete. It can now be composed with
other circuits, repeated, separated by barriers and measured, all at the fermionic level. A
measurement names the mode it reads, so the sampled bitstrings stay indexed by the original
mode even after the transpiler relabels modes or routes qubits, and the new FermionicMeasure
synthesis plugin is part of the preset Jordan-Wigner pass manager.
Python API Features¶
Added
FermionicCircuit.barrier(), which re-exposesbarrier()on the fermionic layer:circuit.barrier() # every mode circuit.barrier(circuit.register[1], circuit.register[2])
The optimization passes do not fuse gates across a barrier, so this is how you keep
MergeOrbitalRotationsfrom merging consecutiveOrbitalRotationgates, orMergeSlaterDeterminantPreparationfrom folding anInitializeModesinto theOrbitalRotationthat follows it.The fermion-to-qubit synthesis stage maps a barrier onto every qubit of each register it touched, so it constrains the subsequent qubit-level transpilation too. Because a
F2QLayoutrelates whole registers, whose two sides can even differ in size, a barrier is deliberately not narrowed to the qubits encoding the modes it was placed on.
Added
FermionicCircuit.compose(), which composes anotherFermionicCircuitonto an existing one:circuit.compose(other, inplace=True)
The operand’s instructions are inlined rather than wrapped into a single gate. An optional
fargsargument places a narrower circuit onto a subset of this circuit’s modes, mirroringappend(), whilefrontcomposes before rather than after the existing instructions andinplacemodifies the circuit instead of returning a new one.
Added measurement support to
FermionicCircuit. Usemeasure_all()to read every mode into a classical bit of its own (modeiinto biti), ormeasure()together with the newadd_register()to read out only some of them:circuit.measure_all() # adds a "meas" register measured = circuit.measure_all(inplace=False) # or leave the original alone creg = ClassicalRegister(2, "c") # or choose the bits yourself circuit.add_register(creg) circuit.measure([0, 2], creg)
Measure the
FermionicCircuitrather than the transpiledQuantumCircuit: a fermionic measurement names the mode it reads, so every stage that moves modes around (RelabelModes, and the qubit stage’s layout and routing) carries the measurement with it, and the sampled bitstrings stay indexed by the original mode. Measuring after transpilation instead requires undoingfinal_index_layout(), and thepermutationmetadata ifRelabelModesran, by hand.Note
Unlike
measure_all(), no barrier is inserted before the measurements. Callbarrier()first if you want one.A measured circuit can no longer be applied to a state vector, since a measurement is not unitary, so keep an unmeasured circuit (or use
inplace=False) if you also want to simulate it.
Added
FermionicMeasure, the instructionFermionicCircuit.measure()appends, andTrivialOccupationFermionicMeasureSynthesis, which lowers it to a qubit measurement. The plugin is registered under theFermionicMeasure.TrivialOccupationentry point and is part ofgenerate_preset_jw_pass_manager(). LikeTrivialOccupationInitializeModesSynthesis, it assumes an occupation-basis encoding with a 1-to-1 mapping of modes to qubits; another encoding needs its own plugin registered underFermionicMeasure.
Added
FermionicCircuit.repeat(), which returns a new circuit holding a given number of repetitions of an existing one:trotterized = trotter_step.repeat(reps)
The repetitions are flattened rather than wrapped into a single gate, so
count_ops()scales with the number of repetitions anddecompose()decomposes the repeated body rather than peeling off one repetition. This is what lets the optimization stage act across the repetition boundary: a symmetric Trotter step that ends and begins with half-duration orbital rotations leaves two rotations per step once repeated, whichMergeOrbitalRotationsthen fuses into one per step plus one. Writing the step naively and repeating it therefore gives the same circuit as fusing those boundaries by hand.Pass
insert_barriers=Trueto place abarrier()between consecutive repetitions, which keeps that fusion from happening. No barrier is placed after the final repetition.
Added ternary-tree fermion-to-qubit encodings. A
TernaryTreedescribes the encoding and is compiled once into aTernaryTreeEncoding, which the mappersfermion_ternary_tree(),majorana_ternary_tree(),edge_vertex_ternary_tree()andtransfer_vertex_ternary_tree()map through. In the Python API,ternary_tree()dispatches to whichever of the four matches the operator it is given, mirroringjordan_wigner().This simplifies the use of every mapping that can be expressed as a ternary tree, including but not limited to the Jordan-Wigner, parity, Bravyi-Kitaev and Jiang-Kalev-Mruczkiewicz-Neven (JKMN) encodings.
C API Features¶
Added the ternary-tree framework to the C API. Trees are built with
qf_ternary_tree_new()(from a parent-and-label specification, the general interface),qf_ternary_tree_chain()orqf_ternary_tree_breadth_first(), inspected withqf_ternary_tree_num_nodes(),qf_ternary_tree_num_legs()andqf_ternary_tree_max_weight(), and freed withqf_ternary_tree_free(). A tree is compiled into aQfTernaryTreeEncodingwithqf_ternary_tree_encoding_new(), inspected withqf_ternary_tree_encoding_num_modes()andqf_ternary_tree_encoding_max_weight(), and then passed toqf_ferm_op_ternary_tree(),qf_maj_op_ternary_tree(),qf_edge_op_ternary_tree(),qf_transfer_op_ternary_tree()orqf_ternary_tree_encoding_total_parity().Like the Jordan-Wigner mappers, the four operator mappers and
qf_ternary_tree_encoding_total_parity()write the resulting QkObs through an out-parameter and return aQfExitCode. This isQfExitCode_ValueErrorwhen the operator acts on a mode the encoding does not cover, or when the requested qubit count is below the encoding’s mode count, whichever modes the operator acts on. A specification that does not describe a single connected tree is rejected earlier, byqf_ternary_tree_new(), which then writes no tree, so check its exit code before compiling the result.
Python API Upgrade Notes¶
We dropped support for Python 3.10 which has reached end-of-life.
Bug Fixes¶
qf_ferm_op_jordan_wigner(),qf_maj_op_jordan_wigner(),qf_edge_op_jordan_wigner()andqf_transfer_op_jordan_wigner()now returnQfExitCode_NullPointerErrorwhen the operator or the output pointer isNULL, instead of aborting the process.
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
Evolutiongate gets decomposed in fermionic space is now configurable through its new keyword-onlysynthesisargument, mirroring thesynthesisargument of Qiskit’sPauliEvolutionGate. The available methods live in the newqiskit_fermions.circuit.library.synthesismodule, which provides theFermionicEvolutionSynthesisinterface and its first-orderFermionicLieTrotterimplementation.This fermion-to-fermion step is optional: an
Evolutiongate 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 commutinggroups) 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’sEvolutionSynthesiscannot.The default is unchanged: leaving
synthesisatNoneusesFermionicLieTrotter, which reproduces exactly the decomposition thatEvolutionperformed before, so existing circuits synthesize identically.
The sampling weights of
QDriftTrotterizationcan now be supplied through its new keyword-onlyweightsargument, instead of being derived from the evolved operator on every call. Leaving it atNone(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 withgroup_coeff_means()instead, alongsidegroup_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
gammasamples identically but evolves forgamma * t. And because the array describes the terms (or groups) of one specific operator, its length is validated against everyEvolutiongate it is applied to, and a circuit holding more than one such gate is rejected; leavingweightsunset keeps such a circuit supported, since each gate then derives its own.
FCIDumpnow exposes the electronic integrals it parsed, which previously were reachable only by converting the whole data structure into aFermionOperator. 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()andget_two_body_tril_bb(). The beta-spin arrays returnNonefor a spin-restricted file; the newis_unrestrictedattribute reports on all three at once. The newconstantattribute gives the constant (nuclear-repulsion) energy, orNonewhen 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,) * 4tensors, as required by sample-based diagonalization tooling for example, is left to the caller.
Added
FermionicSuzukiTrotter, a higher-order product formula for decomposing anEvolutiongate in fermionic space. WhereFermionicLieTrotterapplies each factor once, this composes them symmetrically to cancel lower-order error terms, and itsrepsargument 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
groupswhere 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=2withreps=4reached a Trotter error roughly two times lower than a second-order qubit-side formula, at slightly fewer two-qubit gates.FermionicLieTrotteris the first-order member of this family and is now implemented as such, which also gives it therepsargument it previously lacked. The two are interchangeable at equalreps.Note that a higher order buys accuracy with depth: an order-
kformula emits roughly5**((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 everyEvolutiongate 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– includingUCCandUCJ, which build their own internally. This pass makes the choice once for a whole pipeline instead. An optionalfilterpredicate 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 aUCJ.The pass expands each selected gate into the factors its method emits, so no separate expansion step is needed. Pass
apply=Falseto only select the method and leave the expansion to something else, such as Qiskit’sDecompose; note that a gate which is never expanded reaches the fermion-to-qubit stage whole, where it is mapped withoutEvolution.synthesisever being read.
The
qiskit_fermions.operators.terms.groupingmodule 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.
Added direct Jordan-Wigner mappers for the three remaining operator data structures:
majorana_jordan_wigner(),edge_vertex_jordan_wigner()andtransfer_vertex_jordan_wigner().
jordan_wigner()now dispatches on all four operator types rather than onlyFermionOperator, delegating to whichever direct implementation matches the operator it is given. Passing one of the other three operator types previously raised aTypeError.
The
is_hermitian()method is now part of theOperatorTraitprotocol. Every operator class already provided it, so it can now be called on any value typed as anOperatorTraitrather 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
Trueresult is always reliable, while aFalseresult is conservative for operator types whose normal form is not a genuine canonical form. Seeis_hermitian()for the one such case.
Added two adapters that wrap a mapper function to control the Pauli term order it produces, which
MapperFnEvolutionSynthesisnow preserves through synthesis.simplify()simplifies the mapped operator, merging duplicate Pauli terms and pinning a canonical term order.group_wise()maps an operator onegroupsentry 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))
QDriftTrotterizationnow records how many draws itsfilter_trivialmode discarded. When at least oneEvolutiongate was actually filtered, the returned circuit’smetadatacarriesfilter_trivial.discardedandfilter_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().
Evolutioncan now be simulated for any operator type of this package, not onlyFermionOperator. An operator of another type is converted through its fermionic image (theSupportsFermionOperatorprotocol) before being simulated, so evolving aMajoranaOperator,EdgeVertexOperatororTransferVertexOperatorno longer raisesNotImplementedError.
FermionOperatornow implementsffsim.SupportsTracethrough a new_trace_()method, soffsim.trace()can compute the trace of an operator on a fixed(norb, nelec)sector. This is what preconditionsscipy.sparse.linalg.expm_multiply()along the evolution path.
C API Features¶
Added
qf_ferm_op_conserves_sector(), which checks that every term of aQfFermionOperatorconserves the particle number within each block of modes. It complements the existingqf_ferm_op_conserves_particle_number(), which only checks the total. For a spin-orbital layout ofnorbspatial 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
NULLwith a block count of0treats all modes as a single block, which is equivalent to callingqf_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()andqf_ferm_op_get_support(), plus theqf_maj_op,qf_edge_opandqf_transfer_opequivalents.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_supportfunction first to size the output buffer, then pass that buffer toget_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_inplaceandmul_inplace, for exampleqf_ferm_op_add_inplace(),qf_ferm_op_scaled_add_inplace()andqf_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
groupsattribute toNULL, just asqf_ferm_op_add()does.mul_inplaceonly 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,
QfEdgeVertexOperatorandQfTransferVertexOperator, each one with an equivalent set of functions to the existing operator structs.Both operators store two parallel index arrays,
left_indicesandright_indices, in place of the singlemodesarray 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 singlenum_indicesargument covering both.
Added
qf_edge_op_canonical_order()andqf_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).
Added C API functions for the edge-vertex and transfer-vertex mappers:
qf_edge_vertex_to_fermion(),qf_edge_vertex_to_majorana(),qf_transfer_vertex_to_fermion(),qf_transfer_vertex_to_majorana(), andqf_transfer_vertex_to_edge_vertex().
Added
scaled_addto the C API for all four operator representations:qf_ferm_op_scaled_add(),qf_maj_op_scaled_add(),qf_edge_op_scaled_add()andqf_transfer_op_scaled_add(). Each returnsleft + 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
-1negates 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 correspondingsimplifyfunction to collect equal terms.
QfFCIDumpnow exposes the electronic integrals it parsed, which previously were reachable only by converting the whole data structure into aQfFermionOperator. The new accessorsqf_fcidump_get_one_body_tril_a(),qf_fcidump_get_one_body_tril_b(),qf_fcidump_get_two_body_tril_aa(),qf_fcidump_get_two_body_tril_ab()andqf_fcidump_get_two_body_tril_bb()borrow the internal buffer, writing a pointer and a length through out-parameters:double *integrals; uint64_t len; qf_fcidump_get_one_body_tril_a(fcidump, &integrals, &len);
The returned pointer must not be freed and stays valid only until the
QfFCIDumpis freed. Guard the three beta-spin getters with the newqf_fcidump_is_unrestricted(), andqf_fcidump_constant()with the newqf_fcidump_has_constant(); calling either without its guard panics. Refer to the header documentation for the index formula of every array; mind in particular thatqf_fcidump_get_two_body_tril_ab()is packed differently from the other two-body arrays, holding the full(npair, npair)matrix because it is only 4-fold symmetric.
Added
qf_ferm_op_groups_are_hermitian()andqf_ferm_op_groups_have_uniform_coeffs(), which report for each group whether the operator formed by its terms is Hermitian and whether its coefficients are numerically equal, along with the equivalent functions for the Majorana, edge-vertex and transfer-vertex operator types.
Added
qf_ferm_op_group_order(), which returns a copy of an operator with its terms ordered by group index, along with the equivalent functions for the Majorana (qf_maj_op_group_order()), edge-vertex (qf_edge_op_group_order()) and transfer-vertex (qf_transfer_op_group_order()) operator types. Each group becomes one contiguous run of terms, which letsqf_ferm_op_split_out_groups()binary-search the group boundaries instead of scanning every term.
Added direct Jordan-Wigner mappers for the three remaining operator data structures, alongside the existing
qf_ferm_op_jordan_wigner():qf_maj_op_jordan_wigner(),qf_edge_op_jordan_wigner()andqf_transfer_op_jordan_wigner().
Python API Upgrade Notes¶
The
qiskit_fermions.linalg.apply_unitaryandqiskit_fermions.linalg.linear_operatorfunctions have been removed. They were thin wrappers around the_apply_unitary_and_linear_operator_protocol methods, duplicatingffsim.apply_unitary()andffsim.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.SupportsApplyUnitaryandqiskit_fermions.protocols.SupportsLinearOperatorprotocols have been removed for the same reason: they restated contracts that ffsim defines. Use ffsim’s own protocols, which they mirrored one-to-one:qiskit_fermions.protocols.SupportsApplyUnitarybecomesffsim.SupportsApplyUnitaryqiskit_fermions.protocols.SupportsLinearOperatorbecomesffsim.SupportsLinearOperator
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.SupportsApplyUnitaryPlacedstays, since it extendsffsim.SupportsApplyUnitarywith 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 raisesOSError, while a file that does not honor the FCIDump format (a missing header namelist, a missingNORBorNELECfield, or a malformedMS2field) raisesValueError. An FCIDump carrying an MO energy value, which is still unsupported, also raisesValueError.
The
group_weightsmethod has been removed from every operator class and from theOperatorTraitprotocol. It is replaced bygroup_coeff_means(), a free function in theqiskit_fermions.operators.terms.groupingmodule, 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
groupsarray whose length differs from the operator’s number of terms now raises aValueErrorinstead of being accepted silently. AssigningNoneto clear the group indices is unaffected.
MapperFnEvolutionSynthesisno longer simplifies the operator returned by itsmapper_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 double factorization utilities have been removed from
qiskit_fermions.linalg:double_factorized_t2,double_factorized_t2_alpha_beta,reconstruct_t2,reconstruct_t2_alpha_betaanddouble_factorized_2body. Thet2variants existed to serveUCJ.from_t_amplitudes, which now lives in ffsim, anddouble_factorized_2bodyhad no caller. Useffsim.linalg.double_factorized_t2(),ffsim.linalg.double_factorized_t2_alpha_beta()andffsim.linalg.double_factorized()instead; the last is a superset of the removed two-body routine, additionally offering an optimized (optimize=True) decomposition.
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 aFermionOperatorinto anffsim.FermionOperatorand delegating toffsim.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.fcimodule has been removed along with the kernel, including itsFciLinearOperatorclass and theslater_determinant_statevectorandoccupation_axis_maskfunctions. ffsim provides equivalents:ffsim.slater_determinant()andffsim.addresses_to_strings()respectively.
OrbitalRotationno longer falls back to a generator-plus-exponential path when ffsim is absent; it always delegates toffsim.apply_orbital_rotation(). The results are unchanged when ffsim is installed.
The
UCCgate 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 thevariantargument and theUCC.Variantenum 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_parametersandUCC.to_parametershave been removed, as have thespinlessvariant and the opt-inantisymmetricparameterization, which have no ffsim equivalent. ffsim’s operators providen_params(),from_parameters()andto_parameters()with identical conventions. To build an ansatz outside that family (a spinless one, or an antisymmetrized \(t_2\)), construct anEvolutionover 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 asgate.uccsd_op.t1andgate.uccsd_op.t2; the gate no longer mirrors them as attributes of its own.UCC.cluster_operator()is unchanged. Afinal_orbital_rotationcarried by the ffsim operator is now appended as a closingOrbitalRotation.Since ffsim is now the gate’s input type rather than an optional accelerator, constructing a
UCCrequires theffsimextra (pip install "qiskit-fermions[ffsim]") and raisesMissingOptionalLibraryErrorwithout it. ffsim does not support Windows, so the gate is unavailable there; use WSL.
The
UCJgate 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 thevariantargument and theUCJ.Variantenum 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_parametersandUCJ.to_parametershave been removed: ffsim’s operators providefrom_t_amplitudes(),n_params(),from_parameters()andto_parameters()with identical conventions, and additionally offer the compressed (optimize=True) double factorization andfrom_cisd_vec(), which this package never implemented. The wrapped operator is available asUCJ.ucj_op, so its tensors are reachable asgate.ucj_op.diag_coulomb_matsand 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
UCJrequires theffsimextra (pip install "qiskit-fermions[ffsim]") and raisesMissingOptionalLibraryErrorwithout 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
ValueErrorinstead 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 aQfExitCodeand takes the parsedQfFCIDumpthrough 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_ValueErrorwhen a file does not honor the FCIDump format (a missing header namelist, a missingNORBorNELECfield, a malformedMS2field, 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 behavior).
qf_ferm_op_group_weightsand its per-type counterparts have been renamed toqf_ferm_op_group_coeff_means()and friends. Their behavior is unchanged.
qf_ferm_op_set_groups()and its per-type counterparts now return aQfExitCoderather thanvoid. Passing a group array whose length differs from the operator’s number of terms returnsQfExitCode_ValueErrorinstead of being accepted silently; passingNULLto clear the group indices is unaffected.
Build System Changes¶
The
simulationoptional dependency has been renamed toffsim, sopip install "qiskit-fermions[simulation]"becomespip install "qiskit-fermions[ffsim]". The extra names the dependency it installs rather than a capability. Installingqiskit-fermions[all]is unaffected.
The
optimizationoptional dependency has been renamed topyomo, sopip install "qiskit-fermions[optimization]"becomespip 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 behindRelabelModesandbuild_excitation_span_minimization_model()but cannot solve it. Choosing a solver is a separate, deliberate step (RelabelModestakes one through itssolverargument and this package does not prescribe which), and naming the extra after Pyomo makes that boundary visible at install time. Installingqiskit-fermions[all]is unaffected.
Bug Fixes¶
Fixed the excessive memory consumption of
fermion_jordan_wigner()and ofqf_ferm_op_jordan_wigner()(and therefore ofjordan_wigner()when applied to aFermionOperator). 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_THREADSenvironment 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_canonicalizein C) if you need every duplicate combined.
Fixed
QDriftTrotterizationdiscarding theEvolution.synthesismethod 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 markedEvolution.atomic.
Fixed
RelabelModesfinding a permutation for the wrong Hamiltonian when a circuit’sEvolutiongates had already been decomposed. AFermionicEvolutionSynthesisnarrows 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
Evolutiongate that never terminated. The evolution of a single operator term used to decompose into an identical single-termEvolution, so repeated expansion made no progress. Most visibly this madeinverse()raise aRecursionError, since it recurses through the gate’s definition.A factor emitted by a
FermionicEvolutionSynthesisis now markedEvolution.atomicand is left in place bydecompose()instead of being expanded again. Decomposing anEvolutionrepeatedly 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 aUCCcluster generator being the motivating example. The exponential of such a factor is not unitary.The fermion-to-qubit stage rejected those factors with a
ValueErrorabout 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 anEvolutionthat evolves the same operator for the negated time and preserves the gate’ssynthesismethod. Previously the inherited implementation recursed through the gate’s definition and returned a plainGate, discarding both the operator and the synthesis method.
Performance Improvements¶
Improved the numerical conditioning of state-vector simulation for
EvolutionandOrbitalRotationgates 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 inputFermionOperatorpurely 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.