TransferVertexOperator¶
- class TransferVertexOperator¶
Bases:
objectAn transfer-vertex operator.
Definition¶
This operator is defined in terms of the transfer-vertex (\(T_{jk}\), \(V_j\)) operators:
\[\begin{align} V_j &= -i \gamma_{2j-1} \gamma_{2j} = -(a_j a_j - a_j a^\dagger_j + a^\dagger_j a_j - a^\dagger_j a^\dagger_j) = 1 - 2 a^\dagger_j a_j \, , \nonumber \\ T_{jk} &= \frac{i}{2} V_j E_{jk} = \frac{1}{2} \gamma_{2j} \gamma_{2k-1} \, , \nonumber \\ T_{kj} &= \frac{i}{2} E_{jk} V_k = -\frac{1}{2} \gamma_{2j-1} \gamma_{2k} \nonumber \end{align}\]where \(E_{jk}\) is an edge operator of the
EdgeVertexOperatorand these individual terms fulfill the following mixed fermionic-bosonic commutation relations for \(j \lt k \lt l \lt m\): [1]\[\begin{align} \left\{ T_{jk}, V_k \right\} &= 0 \nonumber \\ \left\{ T_{jk}, T_{lk} \right\} &= 0 \nonumber \\ \left[ V_k, V_l \right] &= 0 \nonumber \\ \left[ T_{jk}, V_l \right] &= 0 \nonumber \\ \left[ T_{jk}, T_{lm} \right] &= 0 \nonumber \\ \left[ T_{jk}, T_{kj} \right] &= 0 \nonumber \\ \left[ T_{jk}, T_{km} \right] &= 0 \nonumber \, . \end{align}\]A simple example can be represented visually like so:
We can abuse the notation a little bit and define \(V_j = T_{jj}\) which reflects how the internal data structure of this operator works. This makes the definition of the entire operator the following:
\[\text{\texttt{EdgeVertexOperator}} = \sum_i c_i \bigotimes_{lr} T_{lr} \, ,\]where \(lr\) indexing the involved operator terms and \(c_i\) is the (complex) coefficient making up the linear combination of products. The indices \(l\) and \(r\) can take any value between 0 and the number of fermionic modes acted upon by the operator minus 1.
We will refer to \(T_{lr}\) as generalized transfer operators.
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:
coeffsA vector of complex coefficients consisting of two 64-bit floating point numbers.
left_indicesA vector of 32-bit integers storing the left fermionic mode indices (\(l\) above).
right_indicesA vector of 32-bit integers storing the right fermionic mode indices (\(r\) above).
boundariesA vector of integers indicating the boundaries in
actionsandmodes.Fermionic modes indexed by
left_indicesandright_indicesare considered spinless.Note
You may access read-only copies of these internal arrays via their respective methods:
get_coeffs(),get_left_indices(),get_right_indices(), andget_boundaries().This data structure allows for very efficient construction and manipulation of operators. However, it implies that duplicate terms may 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 TransferVertexOperator >>> coeffs = [1.0, 2.0, -3.0, 4.0j, -0.5j] >>> left_indices = [0, 3, 0, 2, 3, 0] >>> right_indices = [1, 4, 1, 2, 3, 1] >>> boundaries = [0, 0, 1, 2, 4, 6] >>> op = TransferVertexOperator(coeffs, left_indices, right_indices, boundaries) >>> print(format(op)) 1.000000e0 +0.000000e0j * () 2.000000e0 +0.000000e0j * (T(0,1)) 0.000000e0 +4.000000e0j * (T(0,1) V(2)) -0.000000e0-5.000000e-1j * (V(3) T(0,1)) -3.000000e0 +0.000000e0j * (T(3,4))
For convenience, it is possible to construct an operator from a Python dictionary like so:
>>> op = TransferVertexOperator.from_dict( ... { ... (): 1.0, ... ((0, 1),): 2.0, ... ((3, 4),): -3.0, ... ((0, 1), (2, 2)): 4.0j, ... ((3, 3), (0, 1)): -0.5j, ... } ... ) >>> print(format(op)) 1.000000e0 +0.000000e0j * () 2.000000e0 +0.000000e0j * (T(0,1)) 0.000000e0 +4.000000e0j * (T(0,1) V(2)) -0.000000e0-5.000000e-1j * (V(3) T(0,1)) -3.000000e0 +0.000000e0j * (T(3,4))
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()).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 * () 2.000000e0 +0.000000e0j * (T(0,1)) 0.000000e0 +4.000000e0j * (T(0,1) V(2)) -0.000000e0-5.000000e-1j * (V(3) T(0,1)) -3.000000e0 +0.000000e0j * (T(3,4))
Note
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)) TransferVertexOperator.from_dict({...})
Finally, for large operators both of these outputs may be very long and undesirable. Then, a very simple form with minimal information can be obtained from the
str()function:>>> print(str(op)) <TransferVertexOperator 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.transfer_vertex_operator.TransferVertexOperator' object is not iterable
Instead, this class provides custom iterators to fulfill this purpose:
>>> list(sorted(op.iter_terms())) [([], (1+0j)), ([(0, 1)], (2+0j)), ([(0, 1), (2, 2)], 4j), ([(3, 3), (0, 1)], (-0-0.5j)), ([(3, 4)], (-3+0j))]
See also
iter_terms()For more relevant implementation details.
The table below lists all available iterators:
An iterator over the operator's terms.
Arithmetics¶
The following arithmetic operations are supported:
Addition/Subtraction¶
>>> op = TransferVertexOperator.one() >>> (op + op).simplify() TransferVertexOperator.from_dict({(): 2+0j}) >>> (op - op).simplify() TransferVertexOperator.from_dict({}) >>> op += op >>> op.simplify() TransferVertexOperator.from_dict({(): 2+0j}) >>> op -= op >>> op.simplify() TransferVertexOperator.from_dict({})
Scalar Multiplication/Divison¶
>>> op = TransferVertexOperator.one() >>> (2 * op).simplify() TransferVertexOperator.from_dict({(): 2+0j}) >>> (op / 2).simplify() TransferVertexOperator.from_dict({(): 0.5+0j}) >>> op *= 2 >>> op.simplify() TransferVertexOperator.from_dict({(): 2+0j}) >>> op /= 2 >>> op.simplify() TransferVertexOperator.from_dict({(): 1+0j})
Operator Composition¶
Note
Operator composition corresponds to left-multiplication:
c = a & bcorresponds to \(C = B A\). In other words, the composition of two operators returns a resulting operator that performs “firstaand thenb”.>>> op1 = TransferVertexOperator.from_dict({(): 2.0, ((0, 1),): 3.0}) >>> op2 = TransferVertexOperator.from_dict({(): 1.5, ((2, 2),): 4.0}) >>> comp = (op1 & op2).simplify() >>> print(format(comp)) 3.000000e0 +0.000000e0j * () 4.500000e0 +0.000000e0j * (T(0,1)) 8.000000e0 +0.000000e0j * (V(2)) 1.200000e1 +0.000000e0j * (V(2) T(0,1)) >>> op2 &= op1 >>> print(format(op2.simplify())) 3.000000e0 +0.000000e0j * () 4.500000e0 +0.000000e0j * (T(0,1)) 1.200000e1 +0.000000e0j * (T(0,1) V(2)) 8.000000e0 +0.000000e0j * (V(2)) >>> squared = (op1 ** 2).simplify() >>> print(format(squared)) 4.000000e0 +0.000000e0j * () 1.200000e1 +0.000000e0j * (T(0,1)) 9.000000e0 +0.000000e0j * (T(0,1) T(0,1))
Note
For convenience, the right-multiplication is implemented by
c = a @ b(resulting in \(C = A B\)).>>> (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.
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.
Attributes
- groups¶
An optional vector of group indices for each term.
For more information refer to the
groupingmodule.
Methods
- adjoint()¶
Returns the Hermitian conjugate (or adjoint) of this operator.
Note
All generators of this operator are themselves Hermitian, which means this entire operator is guaranteed to be self-adjoint. Thus, this method simply returns a copy of the original 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 TransferVertexOperator >>> op = TransferVertexOperator.from_dict({(): -1.0j, ((0, 0), (0, 1)): 1.0}) >>> adj = op.adjoint() >>> print(format(adj)) -0.000000e0 +1.000000e0j * () 1.000000e0 -0.000000e0j * (V(0) T(0,1))
- 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 differenceother - selfare below the specified thresholdatol.>>> from qiskit_fermions.operators import TransferVertexOperator >>> op = TransferVertexOperator.from_dict({(): 1e-7}) >>> zero = TransferVertexOperator.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.
- classmethod from_dict(data)¶
Constructs a new operator from a dictionary.
>>> from qiskit_fermions.operators import TransferVertexOperator >>> op = TransferVertexOperator.from_dict( ... { ... (): 1.0-1.0j, ... ((0, 0),): 2.0, ... ((0, 1),): 2.0j, ... } ... ) >>> print(format(op)) 1.000000e0 -1.000000e0j * () 2.000000e0 +0.000000e0j * (V(0)) 0.000000e0 +2.000000e0j * (T(0,1))
- Parameters:
data – a dictionary mapping tuples of terms to complex coefficients. Each key is a tuple of
(int, int)pairs indicating the indices of the generalized transfer operator, \(T_{lr}\) (if \(l = r\) then this corresponds to the vertex operator \(V_l\)).- Returns:
A new operator.
- classmethod from_terms(terms)¶
Constructs a new operator from an iterator of terms (see also
iter_terms()).>>> from qiskit_fermions.operators import TransferVertexOperator >>> op = TransferVertexOperator.from_dict({(): 2.0, ((0, 0),): 1.0, ((0, 1),): -1.0j}) >>> op.equiv(TransferVertexOperator.from_terms(op.iter_terms())) True
- Parameters:
terms – an iterator of terms as produced by
iter_terms().- Returns:
A new operator.
- get_boundaries()¶
Returns a read-only list of the indices indicating the boundaries between operator terms.
Note
This method returns a copy of the internal data.
See also
The explanation of the internal data structure, here.
>>> from qiskit_fermions.operators import TransferVertexOperator >>> op = TransferVertexOperator.one() >>> op += TransferVertexOperator.from_dict({((0, 1),): 1.0}) >>> op.get_boundaries() [0, 0, 1]
- Returns:
A list of the operator’s terms boundaries.
- get_coeffs()¶
Returns a read-only list of the operator’s coefficients.
Note
This method returns a copy of the internal data.
See also
The explanation of the internal data structure, here.
>>> from qiskit_fermions.operators import TransferVertexOperator >>> op = TransferVertexOperator.one() >>> op += -1j * TransferVertexOperator.one() >>> op.get_coeffs() [(1+0j), -1j]
- Returns:
A list of the operator’s coefficients.
- get_left_indices()¶
Returns a read-only list of the left indices of all generalized transfer operator terms.
Note
This method returns a copy of the internal data.
See also
The explanation of the internal data structure, here.
>>> from qiskit_fermions.operators import TransferVertexOperator >>> op = TransferVertexOperator.from_dict({((0, 0),): 1.0}) >>> op += TransferVertexOperator.from_dict({((0, 1),): 1.0}) >>> op.get_left_indices() [0, 0]
- Returns:
A list of the left indices of all generalized transfer operator terms.
- get_right_indices()¶
Returns a read-only list of the right indices of all generalized transfer operator terms.
Note
This method returns a copy of the internal data.
See also
The explanation of the internal data structure, here.
>>> from qiskit_fermions.operators import TransferVertexOperator >>> op = TransferVertexOperator.from_dict({((0, 0),): 1.0}) >>> op += TransferVertexOperator.from_dict({((0, 1),): 1.0}) >>> op.get_right_indices() [0, 1]
- Returns:
A list of the right indices of all generalized transfer operator terms.
- get_support()¶
Returns the set of mode indices which this operator acts upon.
>>> from qiskit_fermions.operators import TransferVertexOperator >>> op = TransferVertexOperator.from_dict( ... { ... ((0, 1), (3, 4)): 1, ... ((7, 7),): 1, ... } ... ) >>> assert op.get_support() == {0, 1, 3, 4, 7}
- Returns:
The set of mode indices which this operator acts upon.
- ichop(atol=1e-08)¶
Removes terms whose coefficient magnitude lies below the provided threshold.
Caution
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 TransferVertexOperator >>> op = TransferVertexOperator.from_dict({(): 1e-4, ((1, 0),): 1e-6, ((0, 1),): 1e-10}) >>> print(format(op)) 1.000000e-4 +0.000000e0j * () 1.000000e-10 +0.000000e0j * (T(0,1)) 1.000000e-6 +0.000000e0j * (T(1,0)) >>> op.ichop() >>> print(format(op)) 1.000000e-4 +0.000000e0j * () 1.000000e-6 +0.000000e0j * (T(1,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(atol=1e-08)¶
Returns whether this operator is Hermitian.
Note
This check is implemented using
equiv()on thenormal_ordered()difference ofselfand itsadjoint()andzero().- 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()¶
An iterator over the operator’s terms.
Warning
Mutating the iteration items does not affect the underlying operator data.
>>> from qiskit_fermions.operators import TransferVertexOperator >>> op = TransferVertexOperator.from_dict({(): 2.0, ((0, 0),): 1.0, ((0, 1),): -1.0j}) >>> list(sorted(op.iter_terms())) [([], (2+0j)), ([(0, 0)], (1+0j)), ([(0, 1)], (-0-1j))]
- normal_ordered()¶
Returns an equivalent operator with normal ordered terms.
The normal order of an operator term is defined such that all vertex operators appear before all transfer operators. Within each group, the acted-upon modes are ordered lexicographically.
Note
When a term is being reordered, the mixed commutation and anti-commutation relations have to be taken into account. See here for the detailed definitions.
>>> from qiskit_fermions.operators import TransferVertexOperator >>> op = TransferVertexOperator.from_dict({((0, 1), (1, 0), (1, 2), (0, 0), (2, 2)): 1}) >>> print(format(op.normal_ordered().simplify())) -1.000000e0 -0.000000e0j * (V(0) V(2) T(0,1) T(1,0) T(1,2))
- Returns:
An equivalent but normal-ordered operator.
- num_groups()¶
Returns the number of groups.
If
groupsisNone, this function also returnsNone. 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 TransferVertexOperator >>> op = TransferVertexOperator( ... [1.0, 2.0, -1.0], ... [0, 1, 2, 3], ... [1, 0, 3, 2], ... [0, 1, 3, 4], ... ) >>> op.groups = [0, 1, 0] >>> op.num_groups() 2
- Returns:
The largest group index in
groupsplus 1.
- 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 TransferVertexOperator >>> op = TransferVertexOperator.from_dict({(): 2.0}) >>> one = TransferVertexOperator.one() >>> op & one == op True
- relabel_modes(permutation)¶
Returns a new operator with relabeled modes.
>>> from qiskit_fermions.operators import TransferVertexOperator >>> op = TransferVertexOperator.from_dict({ ... ((0, 1), (2, 3)): 1, ... ((1, 2), (3, 0)): 1, ... }) >>> permutation = [4, 2, 5, 3] >>> relabeled = op.relabel_modes(permutation) >>> print(format(relabeled)) 1.000000e0 +0.000000e0j * (T(2,5) T(3,4)) 1.000000e0 +0.000000e0j * (T(4,2) T(5,3))
- Parameters:
permutation – the index permutation list.
- Returns:
A new operator with its modes relabeled.
- 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 TransferVertexOperator >>> coeffs = [1e-5] * int(1e5) >>> boundaries = [0] + [0] * int(1e5) >>> op = TransferVertexOperator(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()¶
Splits this operator into an optional list of new operators based on
groups.If
groupsisNone, this function also returnsNone. Otherwise, it will return a list of new operators that contain those terms of this operator with the corresponding group index.>>> from qiskit_fermions.operators import TransferVertexOperator >>> op = TransferVertexOperator( ... [1.0, 2.0, -1.0], ... [0, 1, 2, 3], ... [1, 0, 3, 2], ... [0, 1, 3, 4], ... ) >>> print(op.split_out_groups()) None >>> op.groups = [0, 1, 0] >>> groups = op.split_out_groups() >>> for g in groups: ... print(list(sorted(g.iter_terms()))) [([(0, 1)], (1+0j)), ([(3, 2)], (-1+0j))] [([(1, 0), (2, 3)], (2+0j))]
- Returns:
An optional vector of one new operator for each group index in
groups.
- 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 TransferVertexOperator >>> op = TransferVertexOperator.from_dict({(): 2.0}) >>> zero = TransferVertexOperator.zero() >>> op + zero == op True