FermionOperator
class FermionOperator(coeffs, actions, modes, boundaries)
Bases: object
A spin-less fermionic operator.
Definition
This operator is defined by a linear combination of products of fermionic creation and annihilation operators acting on spin-less fermionic modes. That is to say, the individual terms fulfill the following anti-commutation relations: [1]
where and do not distinguish the spin species of the fermionic modes they are indexing.
This makes the definition of the entire operator the following:
where and is the (complex) coefficient making up the linear combination of products. The index can take any value between 0 and the number of fermionic modes acted upon by the operator minus 1.
Implementation
This class stores the terms and coefficients in multiple sparse vectors, akin to the compressed sparse row format commonly used for sparse matrices. More concretely, a single operator contains 4 arrays:
coeffs | A vector of complex coefficients consisting of two 64-bit floating point numbers. |
actions | A vector of booleans storing the nature of the second-quantization actions. |
modes | A vector of 32-bit integers storing the fermionic mode indices acted upon. |
boundaries | A vector of integers indicating the boundaries in actions and modes. |
Entries in actions indicate creation (annihilation) operators by True (False). Fermionic modes indexed by modes are considered spinless.
You can access read-only copies of these internal arrays via their respective methods: get_coeffs(), get_actions(), get_modes(), and get_boundaries().
This data structure allows for very efficient construction and manipulation of operators. However, it implies that duplicate terms might be contained in an operator at any moment. These must be resolved manually through the use of simplify().
Construction
An operator can be constructed directly by providing the arrays outlined above:
>>> from qiskit_fermions.operators import FermionOperator
>>> coeffs = [1.0, 2.0, -3.0, 4.0j, -0.5j]
>>> actions = [True, False, False, True, True, True, False, False]
>>> modes = [0, 0, 0, 1, 0, 1, 2, 3]
>>> boundaries = [0, 0, 1, 2, 4, 8]
>>> op = FermionOperator(coeffs, actions, modes, boundaries)
>>> print(format(op))
1.000000e0 +0.000000e0j * ()
-3.000000e0 +0.000000e0j * (-0)
0.000000e0 +4.000000e0j * (-0 +1)
2.000000e0 +0.000000e0j * (+0)
-0.000000e0-5.000000e-1j * (+0 +1 -2 -3)
For convenience, it is possible to construct an operator from a Python dictionary like so:
>>> from qiskit_fermions.operators import cre, ann
>>> op = FermionOperator.from_dict(
... {
... (): 1.0,
... (cre(0),): 2.0,
... (ann(0),): -3.0,
... (ann(0), cre(1)): 4.0j,
... (cre(0), cre(1), ann(2), ann(3)): -0.5j,
... }
... )
>>> print(format(op))
1.000000e0 +0.000000e0j * ()
-3.000000e0 +0.000000e0j * (-0)
0.000000e0 +4.000000e0j * (-0 +1)
2.000000e0 +0.000000e0j * (+0)
-0.000000e0-5.000000e-1j * (+0 +1 -2 -3)
In this example, we have leveraged cre() and ann() for creating the creation and annihilation operators at the specified modes.
In addition, the following construction and quick helper methods are available:
zero() | Constructs the additive identity operator. |
one() | Constructs the multiplicative identity operator. |
from_terms(terms) | Constructs a new operator from an iterator of terms (see also iter_terms()). |
from_terms_with_groups(terms) | Constructs a new operator from an iterator of terms with groups (see also iter_terms_with_groups()). |
Formatting
In the examples above, the constructed operators have been printed using the output from format(), which results in a human-readable form of the operator.
>>> print(format(op))
1.000000e0 +0.000000e0j * ()
-3.000000e0 +0.000000e0j * (-0)
0.000000e0 +4.000000e0j * (-0 +1)
2.000000e0 +0.000000e0j * (+0)
-0.000000e0-5.000000e-1j * (+0 +1 -2 -3)
The printing order of format(op) gets explicitly sorted before printing. As such, it does not reflect the order of the terms inside the operator.
An alternative form can be obtained from the repr() function, which results in a Python-interpretable representation. In other words, this output can readily be copied and pasted into a Python shell:
>>> print(repr(op))
FermionOperator.from_dict({...})
Finally, for large operators both of these outputs might be very long and undesirable. Then, a very simple form with minimal information can be obtained from the str() function:
>>> print(str(op))
<FermionOperator with 5 terms>
Iteration
Since the underlying data structure is implemented in Rust and has a non-trivial layout, it cannot be iterated over directly:
>>> list(iter(op))
Traceback (most recent call last):
...
TypeError: 'qiskit_fermions.operators.fermion_operator.FermionOperator' object is not iterable
Instead, this class provides custom iterators to fulfill this purpose:
>>> list(sorted(op.iter_terms()))
[([], (1+0j)), ([(False, 0)], (-3+0j)), ([(False, 0), (True, 1)], 4j), ([(True, 0)], (2+0j)), ([(True, 0), (True, 1), (False, 2), (False, 3)], (-0-0.5j))]
For more relevant implementation details.
The table below lists all available iterators:
iter_terms() | An iterator over the operator's terms. |
iter_terms_with_groups() | An iterator over the operator's terms with their associated group index. |
Arithmetics
The following arithmetic operations are supported:
Addition/Subtraction
>>> op = FermionOperator.one()
>>> (op + op).simplify()
FermionOperator.from_dict({(): 2+0j})
>>> (op - op).simplify()
FermionOperator.from_dict({})
>>> op += op
>>> op.simplify()
FermionOperator.from_dict({(): 2+0j})
>>> op -= op
>>> op.simplify()
FermionOperator.from_dict({})
Scalar Multiplication/Divison
>>> op = FermionOperator.one()
>>> (2 * op).simplify()
FermionOperator.from_dict({(): 2+0j})
>>> (op / 2).simplify()
FermionOperator.from_dict({(): 0.5+0j})
>>> op *= 2
>>> op.simplify()
FermionOperator.from_dict({(): 2+0j})
>>> op /= 2
>>> op.simplify()
FermionOperator.from_dict({(): 1+0j})
Operator Composition
Operator composition corresponds to left-multiplication: c = a & b corresponds to . In other words, the composition of two operators returns a resulting operator that performs “first a and then b”.
>>> op1 = FermionOperator.from_dict({(): 2.0, (cre(0),): 3.0})
>>> op2 = FermionOperator.from_dict({(): 1.5, (ann(1),): 4.0})
>>> comp = (op1 & op2).simplify()
>>> print(format(comp))
3.000000e0 +0.000000e0j * ()
8.000000e0 +0.000000e0j * (-1)
1.200000e1 +0.000000e0j * (-1 +0)
4.500000e0 +0.000000e0j * (+0)
>>> op2 &= op1
>>> print(format(op2.simplify()))
3.000000e0 +0.000000e0j * ()
8.000000e0 +0.000000e0j * (-1)
4.500000e0 +0.000000e0j * (+0)
1.200000e1 +0.000000e0j * (+0 -1)
>>> squared = (op1 ** 2).simplify()
>>> print(format(squared))
4.000000e0 +0.000000e0j * ()
1.200000e1 +0.000000e0j * (+0)
9.000000e0 +0.000000e0j * (+0 +0)
For convenience, the right-multiplication is implemented by c = a @ b (resulting in ).
>>> (op1 @ op2).equiv(op2 & op1)
True
Other Operations
In addition to the magic methods that correspond to the arithmetic operations outlined above, the following methods are available:
adjoint() | Returns the Hermitian conjugate (or adjoint) of this operator. |
ichop([atol]) | Removes terms whose coefficient magnitude lies below the provided threshold. |
simplify([atol]) | Returns an equivalent but simplified operator. |
normal_ordered([sandwich]) | Returns an equivalent operator with normal ordered terms. |
relabel_modes(permutation) | Returns a new operator with relabeled modes. |
Properties
Finally, various methods exist to check certain properties of an operator:
is_hermitian([atol]) | Returns whether this operator is Hermitian. |
max_rank() | Returns the maximum rank of the terms in this operator. |
conserves_particle_number() | Returns whether this operator is particle-number conserving. |
[1]
https://en.wikipedia.org/wiki/Second_quantization#Fermion_creation_and_annihilation_operators
Attributes
groups
An optional vector of group indices for each term.
For more information refer to the grouping module.
Methods
adjoint
adjoint()
Returns the Hermitian conjugate (or adjoint) of this operator.
This affects the terms and coefficients as follows:
- the actions in each term reverse their order and flip between creation and annihilation
- the coefficients are complex conjugated
>>> from qiskit_fermions.operators import FermionOperator
>>> op = FermionOperator.from_dict({(): -1.0j, ((True, 0), (False, 1)): 1.0})
>>> adj = op.adjoint()
>>> print(format(adj))
-0.000000e0 +1.000000e0j * ()
1.000000e0 -0.000000e0j * (+1 -0)
conserves_particle_number
conserves_particle_number()
Returns whether this operator is particle-number conserving.
>>> from qiskit_fermions.operators import FermionOperator
>>> op = FermionOperator.from_dict({((True, 0), (False, 1)): 1})
>>> op.conserves_particle_number()
True
>>> op = FermionOperator.from_dict({((True, 0),): 1})
>>> op.conserves_particle_number()
False
Returns
Whether this operator is particle-number conserving.
conserves_sector
conserves_sector(block_sizes)
Returns whether every term conserves particle number within each mode block.
block_sizes partitions the mode range into consecutive, non-overlapping blocks: block b spans modes [start_b, start_b + block_sizes[b]) where start_b is the sum of the preceding block sizes. A term conserves the sector if and only if, in every block, its number of creation operators equals its number of annihilation operators. A term acting on a mode beyond the final block does not conserve the sector.
An empty block_sizes treats all modes as a single block, making this equivalent to conserves_particle_number(). A single block [norb] checks conservation for a spinless FCI sector, while two equal blocks [norb, norb] check that the alpha modes [0, norb) and beta modes [norb, 2 * norb) are each conserved – i.e. conservation of both particle number and the z-component of spin.
>>> from qiskit_fermions.operators import FermionOperator
>>> op = FermionOperator.from_dict({((True, 0), (False, 2)): 1})
>>> op.conserves_sector([4]) # one spinless block of 4 orbitals
True
>>> op.conserves_sector([2, 2]) # moves a particle from the alpha block to the beta block
False
Parameters
block_sizes – the sizes of the consecutive mode blocks that each must be conserved.
Returns
Whether every term conserves particle number within each mode block.
equiv
equiv(other, atol=1e-08)
Checks this operator for equivalence with another operator.
Equivalence in this context means approximate equality up to the specified absolute tolerance. To be more precise, this method returns True, when all the absolute values of the coefficients in the difference other - self are below the specified threshold atol.
This is the mathematical comparison you almost always want. It differs from the == operator, which tests exact equality of the stored terms (their coefficients, actions, modes, and internal term boundaries) with no tolerance and no simplification. Two mathematically equal operators can therefore compare unequal under == if they are stored differently – for example an unsimplified a + a versus 2 * a, or terms held in a different order. Use equiv to compare operators up to numerical tolerance.
>>> from qiskit_fermions.operators import FermionOperator
>>> op = FermionOperator.from_dict({(): 1e-7})
>>> zero = FermionOperator.zero()
>>> op.equiv(zero)
False
>>> op.equiv(zero, 1e-6)
True
>>> op.equiv(zero, 1e-9)
False
Parameters
- other – the other operator to compare with.
- atol – the absolute tolerance for the comparison. This value defaults to
1e-8.
from_1body_tril_spin
classmethod from_1body_tril_spin(one_body_a, one_body_b, norb)
Constructs an operator from separate spin-species triangular 1-body integrals.
The resulting operator is defined by
where () are the integral coefficients stored in one_body_a (one_body_b, resp.), and are the indices expanded from the triangular index which indexes the arrays, and is the number of orbitals, norb.
The resulting operator acts on spin-less fermionic modes in block-spin ordering: modes are the -spin orbitals and modes are the -spin orbitals (so orbital of the -spin species is mode ).
>>> import numpy as np
>>> from qiskit_fermions.operators import FermionOperator
>>> one_body_a = np.array([1.0, 2.0, 3.0])
>>> one_body_b = np.array([-1.0, -2.0, -3.0])
>>> op = FermionOperator.from_1body_tril_spin(one_body_a, one_body_b, norb=2)
>>> print(format(op))
1.000000e0 +0.000000e0j * (+0 -0)
2.000000e0 +0.000000e0j * (+0 -1)
2.000000e0 +0.000000e0j * (+1 -0)
3.000000e0 +0.000000e0j * (+1 -1)
-1.000000e0 +0.000000e0j * (+2 -2)
-2.000000e0 +0.000000e0j * (+2 -3)
-2.000000e0 +0.000000e0j * (+3 -2)
-3.000000e0 +0.000000e0j * (+3 -3)
Parameters
- one_body_a – a 1-dimensional array of length storing the 1-body electronic integral coefficients of the -spin species, as a flattened triangular matrix.
- one_body_b – a 1-dimensional array of length storing the 1-body electronic integral coefficients of the -spin species, as a flattened triangular matrix.
- norb – the number of orbitals, .
Returns
The 1-body component of the electronic structure Hamiltonian as defined above.
from_1body_tril_spin_sym
classmethod from_1body_tril_spin_sym(one_body_a, norb)
Constructs an operator from spin-symmetric triangular 1-body integrals.
The resulting operator is defined by
where are the integral coefficients stored in one_body_a, and are the indices expanded from the triangular index which indexes the array, and is the number of orbitals, norb.
The resulting operator acts on spin-less fermionic modes in block-spin ordering: modes are the -spin orbitals and modes are the -spin orbitals (so orbital of the -spin species is mode ).
>>> import numpy as np
>>> from qiskit_fermions.operators import FermionOperator
>>> one_body_a = np.array([1.0, 2.0, 3.0])
>>> op = FermionOperator.from_1body_tril_spin_sym(one_body_a, norb=2)
>>> print(format(op))
1.000000e0 +0.000000e0j * (+0 -0)
2.000000e0 +0.000000e0j * (+0 -1)
2.000000e0 +0.000000e0j * (+1 -0)
3.000000e0 +0.000000e0j * (+1 -1)
1.000000e0 +0.000000e0j * (+2 -2)
2.000000e0 +0.000000e0j * (+2 -3)
2.000000e0 +0.000000e0j * (+3 -2)
3.000000e0 +0.000000e0j * (+3 -3)
Parameters
- one_body_a – a 1-dimensional array of length storing the 1-body electronic integral coefficients of the -spin species, as a flattened triangular matrix.
- norb – the number of orbitals, .
Returns
The 1-body component of the electronic structure Hamiltonian as defined above.
from_2body_tril_spin
classmethod from_2body_tril_spin(two_body_aa, two_body_ab, two_body_bb, norb)
Constructs an operator from separate spin-species triangular 2-body integrals.
The resulting operator is defined by
where (, ) are the integral coefficients stored in two_body_aa (two_body_ab, two_body_bb, resp.), is the running index of the array, () generates the unique permutations of the 4-index (see below), and is the number of orbitals, norb.
The two-body coefficients are expected in chemist ordering, , i.e. the two index pairs and each label a charge density. The factor of is the conventional two-body prefactor.
The resulting operator acts on spin-less fermionic modes in block-spin ordering: modes are the -spin orbitals and modes are the -spin orbitals (so orbital of the -spin species is mode ).
two_body_aa and two_body_bb are a S8-fold symmetric arrays. That means, they are the flattened lower-triangular data of matrices of shape (npair, npair), where npair = (norb * (norb + 1) // 2. These in turn are the lower-triangular data of the 4-dimensional arrays of shape (norb, norb, norb, norb). Therefore, above expands the flattened index into all index permutations that index these 4-dimensional arrays.
However, two_body_ab is only S4-fold symmetric. Thus, it contains the full data of the (npair, npair) matrix (but still in flattened form). performs the corresponding index expansion. (In the definition above, we reused the index as an abuse of notation.)
>>> import numpy as np
>>> from qiskit_fermions.operators import FermionOperator
>>> two_body_aa = np.arange(1, 7, dtype=float)
>>> two_body_ab = np.arange(11, 20, dtype=float)
>>> two_body_bb = np.arange(-1, -7, -1, dtype=float)
>>> op = FermionOperator.from_2body_tril_spin(two_body_aa, two_body_ab, two_body_bb, norb=2)
>>> len(op)
64
Parameters
- two_body_aa – a 1-dimensional array of the S8-fold symmetric 2-body electronic integral coefficients of the -spin species, as a flattened array.
- two_body_ab – a 1-dimensional array of the S4-fold symmetric 2-body electronic integral coefficients of the -spin species, as a flattened array.
- two_body_bb – a 1-dimensional array of the S8-fold symmetric 2-body electronic integral coefficients of the -spin species, as a flattened array.
- norb – the number of orbitals, .
Returns
The 2-body component of the electronic structure Hamiltonian as defined above.
from_2body_tril_spin_sym
classmethod from_2body_tril_spin_sym(two_body_aa, norb)
Constructs an operator from spin-symmetric triangular 2-body integrals.
The resulting operator is defined by
where are the integral coefficients stored in two_body_aa, is the running index of the array, generates the unique permutations of the 4-index (see below), and is the number of orbitals, norb.
The two-body coefficients are expected in chemist ordering, , i.e. the two index pairs and each label a charge density. The factor of is the conventional two-body prefactor.
The resulting operator acts on spin-less fermionic modes in block-spin ordering: modes are the -spin orbitals and modes are the -spin orbitals (so orbital of the -spin species is mode ).
two_body_aa is an S8-fold symmetric array. That means, it is the flattened lower-triangular data of a matrix of shape (npair, npair), where npair = (norb * (norb + 1) // 2. This in turn is the lower-triangular data of the 4-dimensional array of shape (norb, norb, norb, norb). Therefore, above expands the flattened index into all index permutations that index this 4-dimensional array.
>>> import numpy as np
>>> from qiskit_fermions.operators import FermionOperator
>>> two_body_aa = np.arange(1, 7, dtype=float)
>>> op = FermionOperator.from_2body_tril_spin_sym(two_body_aa, norb=2)
>>> len(op)
64
Parameters
- two_body_aa – a 1-dimensional array of the S8-fold symmetric 2-body electronic integral coefficients of the -spin species, as a flattened array.
- norb – the number of orbitals, .
Returns
The 2-body component of the electronic structure Hamiltonian as defined above.
from_dict
classmethod from_dict(data)
Constructs a new operator from a dictionary.
>>> from qiskit_fermions.operators import FermionOperator
>>> op = FermionOperator.from_dict(
... {
... (): 1.0-1.0j,
... ((True, 0), (False, 1)): 2.0,
... }
... )
>>> print(format(op))
1.000000e0 -1.000000e0j * ()
2.000000e0 +0.000000e0j * (+0 -1)
Parameters
data – a dictionary mapping tuples of terms to complex coefficients. Each key is a tuple of (bool, int) pairs. You may use cre() and ann() to simplify their construction.
Returns
A new operator.
from_fcidump
classmethod from_fcidump(fcidump)
Constructs a FermionOperator from an FCIDump data structure.
Assuming you have an FCIDump file called molecule.fcidump, you can construct the second-quantized operator like so:
from qiskit_fermions.operators import FermionOperator
from qiskit_fermions.operators.library import FCIDump
fcidump = FCIDump.from_file("molecule.fcidump")
operator = FermionOperator.from_fcidump(fcidump)Parameters
fcidump – the FCIDump data structure.
Returns
The constructed operator. When the FCIDump provides a constant (e.g. nuclear-repulsion) energy, it is included as the identity term ().
from_terms
classmethod from_terms(terms)
Constructs a new operator from an iterator of terms (see also iter_terms()).
>>> from qiskit_fermions.operators import FermionOperator
>>> op = FermionOperator.from_dict({(): 2.0, ((True, 0),): 1.0, ((False, 1),): -1.0j})
>>> op.equiv(FermionOperator.from_terms(op.iter_terms()))
True
Parameters
terms – an iterator of terms as produced by iter_terms().
Returns
A new operator.
from_terms_with_groups
classmethod from_terms_with_groups(terms)
Constructs a new operator from an iterator of terms with groups (see also iter_terms_with_groups()).
>>> from qiskit_fermions.operators import FermionOperator
>>> op = FermionOperator(
... [2.0, 1.0, -1.0],
... [True, False, True, False],
... [0, 1, 1, 0],
... [0, 0, 2, 4],
... )
>>> op.groups = [0, 1, 1]
>>> reconstructed = FermionOperator.from_terms_with_groups(op.iter_terms_with_groups())
>>> op.equiv(reconstructed) and op.groups == reconstructed.groups
True
Parameters
terms – an iterator of terms as produced by iter_terms_with_groups().
Returns
A new operator.
get_actions
get_actions()
Returns a read-only list of the operator’s actions.
This method returns a copy of the internal data.
The explanation of the internal data structure, here.
>>> from qiskit_fermions.operators import FermionOperator
>>> op = FermionOperator.one()
>>> op += FermionOperator.from_dict({((True, 0), (False, 1)): 1.0})
>>> op.get_actions()
[True, False]
Returns
A list of the operator’s actions.
get_boundaries
get_boundaries()
Returns a read-only list of the indices indicating the boundaries between operator terms.
This method returns a copy of the internal data.
The explanation of the internal data structure, here.
>>> from qiskit_fermions.operators import FermionOperator
>>> op = FermionOperator.one()
>>> op += FermionOperator.from_dict({((True, 0), (False, 1)): 1.0})
>>> op.get_boundaries()
[0, 0, 2]
Returns
A list of the operator’s terms boundaries.
get_coeffs
get_coeffs()
Returns a read-only list of the operator’s coefficients.
This method returns a copy of the internal data.
The explanation of the internal data structure, here.
>>> from qiskit_fermions.operators import FermionOperator
>>> op = FermionOperator.one()
>>> op += -1j * FermionOperator.one()
>>> op.get_coeffs()
[(1+0j), -1j]
Returns
A list of the operator’s coefficients.
get_modes
get_modes()
Returns a read-only list of the operator’s acted-upon mode indices.
This method returns a copy of the internal data.
The explanation of the internal data structure, here.
>>> from qiskit_fermions.operators import FermionOperator
>>> op = FermionOperator.one()
>>> op += FermionOperator.from_dict({((True, 0), (False, 1)): 1.0})
>>> op.get_modes()
[0, 1]
Returns
A list of the operator’s modes.
get_support
get_support()
Returns the set of mode indices which this operator acts upon.
>>> from qiskit_fermions.operators import FermionOperator
>>> op = FermionOperator.from_dict(
... {
... ((True, 0), (False, 4)): 1,
... ((True, 1), (True, 3), (False, 4), (False, 7)): 1,
... }
... )
>>> assert op.get_support() == {0, 1, 3, 4, 7}
Returns
The set of mode indices which this operator acts upon.
group_weights
group_weights()
Returns the mean absolute coefficient magnitude of each group.
The i-th entry is the sum of abs(coeff) over the terms in group i, divided by the number of terms in that group. If groups is None, this function also returns None.
This is the sampling weight of a randomized product formula (e.g. qDRIFT) that draws whole groups rather than individual terms. Computing it natively is considerably cheaper than reducing get_coeffs() and groups in NumPy, because those two accessors each copy one value per ungrouped term out of the operator only for it to be aggregated back down to one value per group, whereas this returns just the num_groups() reduced values.
A group index that no term carries weighs 0.0, which keeps it out of the sample.
>>> from qiskit_fermions.operators import FermionOperator
>>> op = FermionOperator(
... [1.0, 2.0, -1.0, -2.0],
... [True, False, True, False, True, False, True, False],
... [0, 1, 2, 3, 1, 0, 3, 2],
... [0, 2, 4, 6, 8],
... )
>>> print(op.group_weights())
None
>>> op.groups = [0, 1, 0, 1]
>>> op.group_weights()
[1.0, 2.0]
Returns
The mean absolute coefficient magnitude of each group index.
has_groups
has_groups()
Returns whether this operator tracks group indices.
This is equivalent to (but cheaper than) checking op.groups is not None, because it does not copy the group indices out of the operator in order to inspect them.
This returns True even when groups is an empty list, which is the state of a grouped operator that holds no terms.
>>> from qiskit_fermions.operators import FermionOperator
>>> op = FermionOperator(
... [1.0, 2.0, -1.0, -2.0],
... [True, False, True, False, True, False, True, False],
... [0, 1, 2, 3, 1, 0, 3, 2],
... [0, 2, 4, 6, 8],
... )
>>> op.has_groups()
False
>>> op.groups = [0, 1, 0, 1]
>>> op.has_groups()
True
Returns
Whether groups is set on this operator.
ichop
ichop(atol=1e-08)
Removes terms whose coefficient magnitude lies below the provided threshold.
This method modifies the operator in place and returns None.
This method truncates coefficients greedily! If the acted upon operator may contain separate coefficients for duplicate terms consider calling simplify() instead!
>>> from qiskit_fermions.operators import FermionOperator
>>> op = FermionOperator.from_dict({(): 1e-4, ((True, 0),): 1e-6, ((False, 0),): 1e-10})
>>> print(format(op))
1.000000e-4 +0.000000e0j * ()
1.000000e-10 +0.000000e0j * (-0)
1.000000e-6 +0.000000e0j * (+0)
>>> op.ichop()
>>> print(format(op))
1.000000e-4 +0.000000e0j * ()
1.000000e-6 +0.000000e0j * (+0)
>>> op.ichop(1e-5)
>>> print(format(op))
1.000000e-4 +0.000000e0j * ()
Parameters
atol – the absolute tolerance for the cutoff. This value defaults to 1e-8.
is_hermitian
is_hermitian(atol=1e-08)
Returns whether this operator is Hermitian.
This check is implemented using equiv() on the normal_ordered() difference of self and its adjoint() and zero().
>>> from qiskit_fermions.operators import FermionOperator
>>> op = FermionOperator.from_dict({
... ((True, 0), (False, 1)): 1.00001j,
... ((True, 1), (False, 0)): -1j,
... })
>>> op.is_hermitian()
False
>>> op.is_hermitian(1e-4)
True
Parameters
atol – The numerical accuracy upto which coefficients are considered equal. This value defaults to 1e-8.
Returns
Whether this operator is Hermitian.
iter_terms
iter_terms()
An iterator over the operator’s terms.
Mutating the iteration items does not affect the underlying operator data.
>>> from qiskit_fermions.operators import FermionOperator
>>> op = FermionOperator.from_dict({(): 2.0, ((True, 0),): 1.0, ((False, 1),): -1.0j})
>>> list(sorted(op.iter_terms()))
[([], (2+0j)), ([(False, 1)], (-0-1j)), ([(True, 0)], (1+0j))]
iter_terms_with_groups
iter_terms_with_groups()
An iterator over the operator’s terms with their associated group index.
Mutating the iteration items does not affect the underlying operator data.
>>> from qiskit_fermions.operators import FermionOperator
>>> op = FermionOperator(
... [2.0, 1.0, -1.0],
... [True, False, True, False],
... [0, 1, 1, 0],
... [0, 0, 2, 4],
... )
>>> op.groups = [0, 1, 1]
>>> list(sorted(op.iter_terms_with_groups()))
[([], (2+0j), 0), ([(True, 0), (False, 1)], (1+0j), 1), ([(True, 1), (False, 0)], (-1+0j), 1)]
max_rank
max_rank()
Returns the maximum rank of the terms in this operator.
The length of the longest term can depend on the operator’s form which means that (for example) operator simplification or normal-ordering can result in a different maximum rank.
>>> from qiskit_fermions.operators import FermionOperator
>>> op = FermionOperator.from_dict({
... ((True, 0), (False, 1), (True, 2), (False, 3)): 1,
... })
>>> op.max_rank()
4
Returns
The maximum rank of this operator.
normal_ordered
normal_ordered(sandwich=None)
Returns an equivalent operator with normal ordered terms.
The normal order of an operator term is defined such that all creation actions appear before all annihilation actions. Within each group, the acted-upon modes are ordered lexicographically. Whether their order is ascending or descending depends upon the value of the sandwich argument:
None(default): both groups are ordered lexicographically descending (e.g.+1 +0 -1 -0)True: larger indices appear towards the middle, i.e. creation actions are lexicographically ascending while annihilation ones are descending (e.g.+0 +1 -1 -0)False: smaller indices appear towards the middle, i.e. creation actions are lexicographically descending while annihilation ones are ascending (e.g.+1 +0 -0 -1)
When a term is being reordered, the anti-commutation relations have to be taken into account, , implying that the number of terms may change.
>>> from qiskit_fermions.operators import FermionOperator
>>> op = FermionOperator.from_dict({((False, 1), (True, 1), (False, 0), (True, 0)): 1})
>>> print(format(op.normal_ordered().simplify()))
1.000000e0 +0.000000e0j * ()
-1.000000e0 +0.000000e0j * (+0 -0)
-1.000000e0 +0.000000e0j * (+1 -1)
-1.000000e0 +0.000000e0j * (+1 +0 -1 -0)
>>> print(format(op.normal_ordered(sandwich=True).simplify()))
1.000000e0 +0.000000e0j * ()
-1.000000e0 +0.000000e0j * (+0 -0)
1.000000e0 +0.000000e0j * (+0 +1 -1 -0)
-1.000000e0 +0.000000e0j * (+1 -1)
>>> print(format(op.normal_ordered(sandwich=False).simplify()))
1.000000e0 +0.000000e0j * ()
-1.000000e0 +0.000000e0j * (+0 -0)
-1.000000e0 +0.000000e0j * (+1 -1)
1.000000e0 +0.000000e0j * (+1 +0 -0 -1)
Returns
An equivalent but normal-ordered operator.
num_groups
num_groups()
Returns the number of groups.
If groups is None, this function also returns None. Otherwise, it will return the number of groups which is defined to be the largest occurring group index plus 1 (which may therefore be used as the index for the next group).
>>> from qiskit_fermions.operators import FermionOperator
>>> op = FermionOperator(
... [1.0, 2.0, -1.0, -2.0],
... [True, False, True, False, True, False, True, False],
... [0, 1, 2, 3, 1, 0, 3, 2],
... [0, 2, 4, 6, 8],
... )
>>> op.groups = [0, 1, 0, 1]
>>> op.num_groups()
2
Returns
The largest group index in groups plus 1.
one
classmethod one()
Constructs the multiplicative identity operator.
Composing the operator that is constructed by this method with another one has no effect.
>>> from qiskit_fermions.operators import FermionOperator
>>> op = FermionOperator.from_dict({(): 2.0})
>>> one = FermionOperator.one()
>>> op & one == op
True
relabel_modes
relabel_modes(permutation)
Returns a new operator with relabeled modes.
>>> from qiskit_fermions.operators import FermionOperator
>>> op = FermionOperator.from_dict({
... ((True, 0), (False, 1)): 1,
... ((True, 0), (False, 1), (True, 2), (False, 3)): 1,
... })
>>> permutation = [5, 6, 4, 3]
>>> relabeled = op.relabel_modes(permutation)
>>> print(format(relabeled))
1.000000e0 +0.000000e0j * (+5 -6)
1.000000e0 +0.000000e0j * (+5 -6 +4 -3)
Parameters
permutation – the index permutation list. Mode i is relabeled to permutation[i], so the list must contain no duplicate entries and must be long enough to index every mode the operator acts upon (its length must exceed the operator’s largest mode index).
Returns
A new operator with its modes relabeled.
Raises
ValueError – if permutation contains duplicate entries, or is too short to relabel some mode the operator acts upon.
simplify
simplify(atol=1e-08)
Returns an equivalent but simplified operator.
The simplification process first sums all coefficients that belong to equal terms and then only retains those whose total coefficient exceeds the specified tolerance (just like ichop()).
When an operator has been arithmetically manipulated or constructed in a way that does not guarantee unique terms, this method should be called before applying any method that filters numerically small coefficients to avoid loss of information. See the example below which showcases how ichop() can truncate terms that sum to a total coefficient magnitude which should not be truncated:
>>> from qiskit_fermions.operators import FermionOperator
>>> coeffs = [1e-5] * int(1e5)
>>> boundaries = [0] + [0] * int(1e5)
>>> op = FermionOperator(coeffs, [], [], boundaries)
>>> canon = op.simplify(1e-4)
>>> assert canon.equiv(op.one(), 1e-6)
>>> op.ichop(1e-4)
>>> assert op.equiv(op.zero(), 1e-6)
Parameters
atol – the absolute tolerance for the cutoff. This value defaults to 1e-8.
Returns
An equivalent but simplified operator.
split_out_groups
split_out_groups(group_indices=None)
Splits this operator into an optional list of new operators based on groups.
If groups is None, this function also returns None. Otherwise, if group_indices is None (the default), it returns a list of one new operator for every group index in groups, in index order. If group_indices is given, only the requested indices are built, in the given order: this avoids the cost of constructing operators for groups that are never used, which is especially beneficial when only a small number of groups out of a much larger total are needed, e.g. when subsampling groups for a randomized product formula. A duplicate index in group_indices is returned once per occurrence.
>>> from qiskit_fermions.operators import FermionOperator
>>> op = FermionOperator(
... [1.0, 2.0, -1.0, -2.0],
... [True, False, True, False, True, False, True, False],
... [0, 1, 2, 3, 1, 0, 3, 2],
... [0, 2, 4, 6, 8],
... )
>>> print(op.split_out_groups())
None
>>> op.groups = [0, 1, 0, 1]
>>> groups = op.split_out_groups()
>>> for g in groups:
... print(list(sorted(g.iter_terms())))
[([(True, 0), (False, 1)], (1+0j)), ([(True, 1), (False, 0)], (-1+0j))]
[([(True, 2), (False, 3)], (2+0j)), ([(True, 3), (False, 2)], (-2+0j))]
>>> groups = op.split_out_groups(group_indices=[1])
>>> for g in groups:
... print(list(sorted(g.iter_terms())))
[([(True, 2), (False, 3)], (2+0j)), ([(True, 3), (False, 2)], (-2+0j))]
Parameters
group_indices – the group indices for which to build operators, in the desired output order. When omitted, every group is built, in index order.
Returns
An optional vector of one new operator for each requested group index.
zero
classmethod zero()
Constructs the additive identity operator.
Adding the operator that is constructed by this method to another one has no effect.
>>> from qiskit_fermions.operators import FermionOperator
>>> op = FermionOperator.from_dict({(): 2.0})
>>> zero = FermionOperator.zero()
>>> op + zero == op
True
Protocol Methods
_anti_commutator_
static _anti_commutator_(op_a, op_b)
_commutator_
static _commutator_(op_a, op_b)
_double_commutator_
static _double_commutator_(op_a, op_b, op_c, sign)
_fci_linear_operator_
_fci_linear_operator_(norb, nelec)
_linear_operator_
_linear_operator_(norb, nelec)
Returns a SciPy LinearOperator for this operator on the (norb, nelec) FCI sector.
This implements the SupportsLinearOperator protocol, so an operator carrying a native FCI kernel can be passed directly to scipy.sparse.linalg.expm_multiply() or to ffsim.linear_operator(). It depends only on the internal _SupportsFciLinearOperator contract – the _fci_linear_operator_ carrier – rather than on any concrete operator type, and wraps that native matrix-vector kernel in a genuine scipy.sparse.linalg.LinearOperator; expm_multiply() requires the adjoint action, so both matvec and rmatvec are supplied.
The native kernel requires a contiguous one-dimensional complex128 vector, whereas SciPy’s machinery may feed a LinearOperator real probe vectors (from its one-norm estimator) or non-contiguous (dim, 1) column slices. The matvec/rmatvec wrappers coerce the input with numpy.ascontiguousarray(v, complex128).reshape(-1); the numpy handles are bound once here (per operator) rather than re-resolved on every matvec inside the expm_multiply() loop.
This is attached to the operator classes as _linear_operator_ at import time (see qiskit_fermions.operators): the native operator classes are compiled types whose instances cannot themselves subclass SciPy’s LinearOperator, so the protocol method is provided in Python.
Parameters
- norb (int) – the number of spatial orbitals.
- nelec (int |tuple[int, int]) – the electron count – an integer for a spinless sector, or an
(n_alpha, n_beta)pair for a spinful one. - self (_SupportsFciLinearOperator)
Returns
A scipy.sparse.linalg.LinearOperator applying this operator on the requested sector.
Return type
_majorana_operator_
_majorana_operator_()
Converts this operator into a MajoranaOperator.
This implements the SupportsMajoranaOperator protocol by delegating to fermion_to_majorana().