How this package relates to ffsim¶
Important
The concepts in this guide are only available in the Python API.
ffsim and this package divide the work rather than overlap, along a single line:
ffsim owns |
|
|---|---|
The high-level ansatz operators ( |
The fermionic circuit ( |
Fast simulation in a compact fixed-particle-number space |
The mapper framework and the operator data structures the circuit is built from |
The practical consequence runs both ways. The ansatz math lives in ffsim, so UCJ and
UCC take an ffsim operator directly as their input. Conversely, ffsim’s own Qiskit gates
are specific to the Jordan-Wigner transformation, so lowering one of its ansatz operators through a
different encoding is what bringing it here buys you.
Simulation is where the two meet, and this guide explains how. ffsim is this package’s simulation
backend, reached through its own protocols rather than a wrapper: the SKQD guide calls ffsim.apply_unitary(), ffsim.linear_operator(), and
ffsim.sample_state_vector() directly on FermionicCircuitobjects and
FermionOperatorobjects, with no conversion step.
Plugging into ffsim’s simulation interface¶
ffsim is a high-performance simulator for fermionic quantum circuits that exploits
particle number and spin-Z conservation to represent state vectors more compactly than a
generic \(2^n\)-dimensional qubit statevector. It defines protocols that any object
can implement to participate in its simulation machinery (see qiskit_fermions.protocols
for this package’s own protocols, which follow the same design):
ffsim.SupportsApplyUnitary, by the method_apply_unitary_(vec, norb, nelec, copy), applying the object as a unitary to a fixed-particle-number state vectorffsim.SupportsLinearOperator, by the method_linear_operator_(norb, nelec), returning ascipy.sparse.linalg.LinearOperatorview of the object on that same sectorffsim.SupportsTrace, by the method_trace_(norb, nelec), returning the object’s trace on that sector.
This package implements that interface: every fermionic gate in
qiskit_fermions.circuit.library provides _apply_unitary_, and FermionOperator
provides _linear_operator_ and _trace_. That is what makes this package’s operators and
circuits compatible with ffsim, so its tools work on them natively, with no conversion step.
ffsim.apply_unitary()
can simulate a FermionicCircuit end to end, ffsim.linear_operator() can diagonalize a
FermionOperator, and sampling utilities like ffsim.sample_state_vector() (used in the
SKQD guide to turn a simulated statevector into measurement counts)
work without modification.
What _linear_operator_ returns is an ordinary SciPy
LinearOperator, so scipy.sparse.linalg.eigsh() and friends work
on it too.
>>> import ffsim
>>> import numpy as np
>>>
>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import Evolution
>>> from qiskit_fermions.operators import FermionOperator, ann, cre
>>>
>>> norb, nelec = 2, (1, 1)
>>> hamiltonian = FermionOperator.from_terms([
... ([cre(0), ann(1)], 0.5),
... ([cre(1), ann(0)], 0.5),
... ])
>>>
>>> circuit = FermionicCircuit(2 * norb)
>>> circuit.append(Evolution(2 * norb, hamiltonian, time=1.0), circuit.modes)
>>>
>>> reference = ffsim.hartree_fock_state(norb, nelec)
>>> state = ffsim.apply_unitary(reference, circuit, norb=norb, nelec=nelec) # native ffsim call
Coupling through ffsim’s protocols is what buys this: its actively developed ecosystem of simulation and sampling utilities applies to this package’s circuits and operators unchanged, and users already working with ffsim can mix in this package’s gates and operators without learning another simulation API.
ffsim is an optional dependency¶
ffsim is declared as an optional extra (pip install "qiskit-fermions[ffsim]", or transitively by
[all]), guarded at runtime by HAS_FFSIM (as was shown
above). Installing it unlocks simulation: SupportsLinearOperator and
SupportsTrace are implemented by converting this package’s FermionOperator into
an ffsim.FermionOperator, so without ffsim they raise
MissingOptionalLibraryError. Everything that does not simulate (building
operators, mapping them, and transpiling the resulting circuits) needs none of it.
ffsim transitively depends on PySCF, which does not support Windows, so
on Windows the extra resolves to nothing (a silent no-op through a sys_platform marker) rather
than an install failure. Windows users who want to simulate can do so through the Windows
Subsystem for Linux in the meantime.
Fermionic simulation lives in a fixed particle-number sector¶
Both _apply_unitary_ and _linear_operator_ take an nelec argument and represent the state
vector over the fixed-particle-number determinant basis for that (norb, nelec) sector,
mirroring ffsim’s (and, transitively, PySCF’s) FCI space setup, rather than the full
\(2^{\text{num\_modes}}\)-dimensional space a general qubit simulator would use. This is a much
smaller space (its size is a product of binomial coefficients rather than a power of two), but it
comes with a hard restriction. Only operators and gates that preserve particle number (and, in
the spinful case, each spin species’ particle number individually) can be represented in it. An
operator whose action would move amplitude to a different particle number has nowhere to put it.
ffsim resolves this by rejecting such an operator outright: converting one to a linear operator
raises a ValueError naming which conservation law fails. That is the right default, because
applying \(\exp(-i t H)\) for a Hamiltonian \(H\) with a non-particle-conserving term would
otherwise turn a would-be unitary into a non-unitary, physically-meaningless map.
Evolutionsurfaces that rejection from the operator it evolves.OrbitalRotationchecks that its (embedded) rotation matrix is block-diagonal across the alpha/beta split in the spinful case, and raises aValueErrorif it mixes spin sectors.
>>> from qiskit_fermions.circuit.library import Evolution
>>>
>>> non_conserving = FermionOperator.from_terms([([cre(0), cre(1)], 1.0)]) # creates 2 particles
>>> gate = Evolution(2 * norb, non_conserving, time=1.0)
>>>
>>> try:
... gate._apply_unitary_(reference, norb, nelec, copy=True)
... except ValueError as exc:
... print("rejected:", exc)
rejected: The given FermionOperator could not be converted to a LinearOperator because it does not conserve particle number and the z component of spin. Conserves particle number: False Conserves spin z: False
Non-particle-preserving simulation is therefore out of reach in fermionic space, by construction of
the fixed-sector representation, and that is the price of the compact FCI space which makes
fermionic simulation tractable in the first place. If your algorithm needs particle-number-violating
operators (for example, a qubit-native error channel, or an operator built for a mapped Hamiltonian
that does not conserve particle numbers term by term), transpile to qubits first and simulate the
resulting QuantumCircuit with a qubit-level simulator instead. Any
transpilation route works for this; the
fermionic circuit and transpilation guides go into more detail, but
generate_preset_jw_pass_manager() is a reasonable default
choice.
The block-spin convention for spinful systems¶
nelec is typed int | tuple[int, int], and its type, not a separate flag, is what selects
one of the supported mode layouts:
An int selects the spinless interpretation. The
norbmodes are treated directly as spinless orbitals, andnelecis the total particle count.A pair
(n_alpha, n_beta)selects the spinful interpretation of2 * norbmodes under a fixed block-spin convention. Modes0 .. norbare the alpha (spin-up) orbitals and modesnorb .. 2 * norbare the beta (spin-down) orbitals. Each spin species is conserved independently; an alpha-only term can move an electron between alpha modes but never into a beta mode, and vice versa.
This dispatch happens at every ffsim-protocol entry point in this package (gate constructors,
_apply_unitary_placed_ implementations, and the native Rust kernel’s sector compilation),
and it is the convention used throughout the LUCJ and
SKQD guides (for example,
InitializeModes.from_hartree_fock() fills alpha occupations into modes 0 .. n_alpha and
beta occupations into modes norb .. norb + n_beta). It is worth contrasting this with the
qiskit_fermions.operators module and FermionicCircuit in general, which use
generic mode indices with no inherent spin semantics (see the fermionic circuit guide). The block-spin meaning is imposed only when a spinful nelec
is supplied to a simulation call, not baked into the operator or circuit representation itself.
>>> from qiskit_fermions.circuit.library import InitializeModes
>>>
>>> norb, nelec = 3, (2, 1)
>>> init = InitializeModes.from_hartree_fock(norb, nelec)
>>> print([bool(occ) for occ in init.occupation]) # alpha modes 0,1; beta mode 3 (= norb + 0)
[True, True, False, True, False, False]
Next steps¶
Walk through the SKQD guide to see
ffsim.apply_unitary()andffsim.sample_state_vector()used together to sample circuits built from this package’s gates, and to evaluate a Hamiltonian expectation value throughffsim.linear_operator()(itself backed by the same_linear_operator_protocol method described here).Walk through the LUCJ guide for the other half of the story: taking an ffsim ansatz operator through this package’s transpilation pipeline and onto a device coupling map.
Read the fermionic circuit guide for the generic, spin-agnostic mode indexing used outside of simulation calls.
- Read the transpilation guide for how to leave fermionic space
and simulate on qubits, which is required for non-particle-conserving operators.
Browse
qiskit_fermions.protocolsfor the full catalog of protocols this package defines, including the conversion protocols (SupportsFermionOperator,SupportsMajoranaOperator) that are unrelated to ffsim.Read ffsim’s own guides for the workflows it owns, in particular building a UCJ ansatz and optimizing one variationally. The parameter vectors those guides drive are what
UCJandUCCaccept, through the operators they build.