# Spaces, Operators, and States¶

The use of quantum computers for tackling problems in quantum chemistry involves a wide variety of quantum mechanical structures. Many of these are shared with quantum chemistry problems studied with classical computers. For example, the electronic Hamiltonian may be considered a second-quantized operator acting on a fermionic Hilbert space. However, quantum computing approaches often require objects and formalisms that are atypical to “conventional” quantum chemistry – for instance, fermionic operators and states must be mapped to operators acting on and states within a qubit Hilbert space.

InQuanto provides options for representing each of these objects.
Broadly, three core types of objects are used - InQuanto operator and
state classes represent operators and states within a given Hilbert
space. Space objects (chiefly `FermionSpace`

and `QubitSpace`

) can
be used to describe specific Hilbert spaces and then generate these
states and operators. The relationship between these objects is depicted in Fig. 24.

We first discuss fermionic
and qubit spaces and how operators and states can
be mapped between the two. An option for
parafermionic [10] spaces is also provided by the `ParaFermionSpace`

object. This mapping is
described in the Iterative Qubit-Excitation Based VQE algorithm section. We also consider
integral operators - classes for the
efficient manipulation of molecular orbital integral tensors. Finally,
we consider orbital transformers and
optimization – several InQuanto classes for the convenient
manipulation of molecular orbitals bases.

## Fermionic Spaces¶

Given a system of \(N\) electrons described by an orthonormal basis of \(Q\) molecular spin-orbitals, or momentum space modes, where \(Q \geq N\), one can construct a fermionic Fock space [5], or “fermion space”. Each basis vector of this abstract vector space corresponds to an occupation number (ON) vector

where an occupation number \(\eta_P = 1\) (\(0\)) if spin-orbital \(P\) is occupied (unoccupied). Each ON vector represents a canonically ordered \(N\)-electron Slater determinant. A fermion space is an abstract linear vector space equipped with the usual properties of an inner product and resolution of identity, for a given set of spin-orbitals or modes. An arbitrary vector in this space corresponds to a linear combination of ON vectors

if the spin-orbitals or modes used to construct the ON vectors are orthonormal, such that the ON vectors obey the relation

This is a useful feature of fermion spaces: a well defined but zero overlap between ON vectors with different values of \(N = \sum_{P=0}^{Q-1} \eta_P\) and \(N' = \sum_{P=0}^{Q-1} \eta'_P\) allows for a consistent treatment of systems with different numbers of electrons. Thus the ON vectors form an orthonormal basis in the \(2^Q\)-dimensional fermion space, which in principle can be decomposed into a direct sum of fermionic subspaces \(\mathcal{F}(Q, N)\), where each subspace represents a different number of electrons distributed over the \(Q\) spin-orbitals.

The dimension of each \(\mathcal{F}(Q, N)\) subspace is equal to the number of terms in the FCI expansion \(\binom{Q}{N}\), such that the exact (FCI) wavefunction for \(N\) electrons (in a given basis) can be expressed as a vector in \(\mathcal{F}(Q, N)\).

Anticommuting creation and annihilation operators act on the fermion space as linear maps between vectors within the space. From these
operators, one- and two-body interactions can be defined, along with the fermionic ON vectors, and thus the entire fermionic
problem can be described once the \(N\)-electron subspace is specified. In InQuanto, this logic is followed in the
`FermionSpace`

class which represents the fermionic Fock space \(\mathcal{F}(Q)\), and contains related
utilities for the construction of fermionic interaction operators (represented by the `FermionOperator`

class) and
ON vectors (represented by the `FermionState`

class). In the next subsection
`FermionSpace`

is discussed, while `FermionOperator`

and `FermionState`

are covered in later sections.

### The `FermionSpace`

Class¶

This class provides a range of tools related to the fermionic Fock space. These tools include the definition of the
space itself, and operations on this space related to the generation of fermionic operators and for specification of
occupations. The `FermionSpace`

class represents a fermionic Hilbert space, maintaining consistent indexing of spin-orbitals. It incorporates the fermionic anti-commutation algebra in the logic of the class, allowing for the generation of various fermionic quantum operators acting on a given Hilbert space.

Various fermionic quantum operators can be constructed from the methods in this class.
These include the number operators, spin operators, one- and two-body operators in various
forms, and the anti-Hermitian operators needed for unitary coupled cluster ansatz generation. For example,
the UCC terms for minimal basis H_{2} (with 4 spin-orbitals) are defined as follows:

```
from inquanto.spaces import FermionSpace
from inquanto.states import FermionState
# create a FermionSpace object for, say, H2
space = FermionSpace(4)
state = FermionState([1, 1, 0, 0])
# with the state and space objects we can construct operators using native functions, for example:
singles = space.construct_single_ucc_operators(state)
doubles = space.construct_double_ucc_operators(state)
uccsd_excitations = singles + doubles
print("number of uccsd excitations for minimal basis h2:", len(uccsd_excitations))
```

```
number of uccsd excitations for minimal basis h2: 3
```

In this case, the occupations specified by `state`

are needed to construct the UCC operators, though this is not always required depending on the operators being generated. The operators are returned as `FermionOperator`

objects
(`FermionOperator`

and `FermionState`

are discussed in more detail later). Several types of operators can be generated by the various methods of `FermionSpace`

, including various forms of number operators, excitation operators and symmetry operators. An exhaustive list of these operators can be found in `the API reference`

.

Once the space and
occupations are defined, information relevant to the ON vector can be displayed using the `print_state()`

method:

```
from inquanto.spaces import FermionSpace
from inquanto.states import FermionState
# create a FermionSpace object for, say, H2
space = FermionSpace(4)
state = FermionState([1, 1, 0, 0])
print("fermionic fock state:")
space.print_state(state)
```

```
fermionic fock state:
0 0a : 1
1 0b : 1
2 1a : 0
3 1b : 0
```

The left-most column refers to spin-orbital indexes, the next column shows the spatial orbital indexes with the
alpha (`a`

) and beta (`b`

) spin labels, while the right-most column shows the occupation of each spin-orbital.

Typically, operators generated using the `FermionSpace`

methods will preserve particle number and spin conservation symmetries. In addition to these, `FermionSpace`

can also optionally handle point group symmetry information. By passing in point group symmetries when
instantiating the `FermionSpace`

class object, we can generate only those excitations that are non-zero by
symmetry. Below, the single excitations of H_{2} are symmetry forbidden, leaving only one double.

```
from inquanto.spaces import FermionSpace
from inquanto.states import FermionState
from inquanto.symmetry import PointGroup
# create a FermionSpace object for, say, H2
space = FermionSpace(4)
state = FermionState([1, 1, 0, 0])
# we can also pass symmetry information to remove redundant excitations
space = FermionSpace(
4, point_group=PointGroup("D2h"), orb_irreps=["Ag", "Ag", "B1u", "B1u"]
)
singles = space.construct_single_ucc_operators(state)
doubles = space.construct_double_ucc_operators(state)
uccsd_excitations = singles + doubles
print("number of allowed uccsd excitations for minimal basis h2:", len(uccsd_excitations))
```

```
number of allowed uccsd excitations for minimal basis h2: 1
```

This potentially reduces the size of the problem; for example, the number of parameters in variational algorithms like VQE. When a `FermionSpace`

object is generated from a driver, it will typically include point group information if the driver utilized this information in performing precursor classical computation. The `FermionSpace`

classes can be used to generate explicit operator representations of the symmetries of the system with the `symmetry_operators_z2()`

method - usage of this is detailed in the symmetry section.

### Fermion Operators & States¶

In second quantization, operators acting on fermionic states are represented by linear combinations and tensor products of creation and annihilation operators. These obey the anticommutation relations

Where \(\{\hat{A}, \hat{B}\} = \hat{A}\hat{B} + \hat{B}\hat{A}\). Mathematically, these operators couple ON vectors belonging to different subspaces \(\mathcal{F}(Q, N)\). Thus, a given arrangement of occupations can be used to build an ON vector by applying a product of creation operators to the vacuum state, such that creation operators are only applied to spin-orbitals or modes that should be unoccupied

In InQuanto, vectors in \(\mathcal{F}(Q, N)\) space are represented by `FermionState`

objects. Individual basis states without coefficients are represented by `FermionStateString`

objects. A `FermionStateString`

functions as a dictionary mapping spin-orbital indices represented as integers, to occupation numbers (also represented as integers, of values `0`

or `1`

). For example, for an ON vector of four spin-orbitals, with spin-orbitals indexed `0`

and `1`

occupied, and spin-orbitals indexed `3`

and `4`

unoccupied:

```
from inquanto.states import FermionStateString
on_vector = FermionStateString({0:1, 1:1, 3:0, 4:0})
print(on_vector)
```

```
{0: 1, 1: 1, 3: 0, 4: 0}
```

A simple list of occupation numbers may also be provided to the constructor, as a convenient alternative. In this case, a default register of fermionic modes will be assumed, indexed from \([0,N)\), where \(N\) is the length of the provided list (i.e. the number of spin-orbitals)

```
from inquanto.states import FermionStateString
on_vector = FermionStateString([1,1,0,0])
print(on_vector)
```

```
{0: 1, 1: 1, 2: 0, 3: 0}
```

Note

The use of a dictionary with integer keys – as opposed to a simple `list`

or `tuple`

– allows for the representation of spin-orbitals with discontinuous indices. This may be useful when representing states which are elements of different Hilbert spaces. It also allows for a uniform interface with classes which represent states wherein the modes are represented by more complex objects, as in the case of `QubitState`

below.

The `FermionState`

then represents the state as a dictionary, with
the `FermionStateString`

objects (occupation configurations) as keys, and
numerical quantities (configuration coefficients) as values. Typically, this will be constructed with a `dict`

giving such a mapping:

```
from inquanto.states import FermionState, FermionStateString
on_vector = FermionStateString({0:1, 1:1, 2:0, 3:0})
state = FermionState({on_vector: 1.0})
print(state)
```

```
(1.0, {0: 1, 1: 1, 2: 0, 3: 0})
```

If a simple `FermionState`

representing a single basis state with unit coefficient is required, a convenient
alternative for construction is also provided. A list of integer
occupation values may be directly passed to the constructor, following the logic of the `FermionStateString`

above:

```
state = FermionState([1, 1, 0, 0])
print(state)
```

```
(1.0, {0: 1, 1: 1, 2: 0, 3: 0})
```

A `FermionState`

object can also be generated as a method of `FermionSpace`

, by passing the
occupation list as an argument. This allows for consistent spin-orbital indexing determined by the `FermionSpace`

instance.

```
from inquanto.spaces import FermionSpace
from inquanto.states import FermionState
space = FermionSpace(4)
state = space.generate_occupation_state_from_list([1, 1, 0, 0])
```

`FermionState`

objects may be operated on with standard linear algebraic operations to return
new `FermionState`

objects, and may be iterated over.

```
new_state = 2.0 * state + FermionState([0,0,1,1])
for x in new_state.split():
print(x)
```

```
(2.0, {0: 1, 1: 1, 2: 0, 3: 0})
(1.0, {0: 0, 1: 0, 2: 1, 3: 1})
```

In addition to representing fermionic states, InQuanto has capabilities for representing operators acting within fermionic Hilbert spaces.
Analogously to `FermionStateString`

, the `FermionOperatorString`

represents a single unweighted
string of fermionic creation and annihilation operators. A `FermionOperatorString`

may be created with a `tuple`

of pairs of integers, in which the first integer represents the spin-orbital or mode and the second
integer is 1 (0) for creation (annihilation) operators. For example, consider an excitation operator in which an
electron is annihilated from spin-orbital 2 and created on spin-orbital 3 ( \(\hat{f}^\dagger_3\hat{f}_2\) ):

```
from inquanto.operators import FermionOperator, FermionOperatorString
f_op_str = FermionOperatorString(((3, 1), (2, 0)))
print(f_op_str)
```

```
F3^ F2
```

For convenience, it is also possible to create such an object from string input:

```
f_op_str = FermionOperatorString.from_string("F3^ F2")
print(f_op_str)
```

```
F3^ F2
```

Similarly to `FermionState`

, fermionic operators are then represented by `FermionOperator`

. This stores the coefficients of each sequence of creation/annihilation operators as a dictionary, in which the keys
are `FermionOperatorString`

objects and the values are their coefficients. It may be created through providing the
constructor with such a dictionary. Again, a convenient alternative is provided for the construction of an operator comprised of a single string:

```
f_op_string = FermionOperatorString(((3, 1), (2, 0)))
f_op1 = FermionOperator({f_op_string: 1.0})
f_op2 = FermionOperator(f_op_string, 1.0)
print(f_op1)
print(f_op2)
```

```
(1.0, F3^ F2 )
(1.0, F3^ F2 )
```

Additional construction methods are also available in the reference documentation for `FermionOperator`

. Standard algebraic manipulation is possible for this class:

```
f_op_string = FermionOperatorString(((3, 1), (2, 0)))
f_op_string_conj = FermionOperatorString(((2, 1), (3, 0)))
f_op = FermionOperator({f_op_string: 1.0})
f_op_conj = FermionOperator({f_op_string_conj: 1.0})
f_op_sum = f_op + f_op_conj
print(f_op_sum)
```

```
(1.0, F3^ F2 ), (1.0, F2^ F3 )
```

Various other methods for manipulating `FermionOperator`

instances are available in InQuanto. The
following example ( fermion_operator) summarizes some of the
features of InQuanto’s `FermionOperator`

and `FermionState`

objects and their usage.

```
from inquanto.operators import FermionOperator
from inquanto.operators import FermionOperatorString
# construct a fermion operator
op1 = FermionOperator({FermionOperatorString([(0, 0)]): 1.0})
# now multiply by its adjoint
op = op1 * op1.dagger()
# does it commute with itself? Yes!
print("commutator of op with itself:", op.commutator(op))
print("does op commute with itself?", op.commutes_with(op))
# instantiate from string
op2 = FermionOperator({FermionOperatorString.from_string("F1 F2^"): 3.5})
# is this normal ordered?
print("is op2 normal ordered?", op2.is_normal_ordered())
op2 = op2.normal_ordered()
print("what about now?", op2.is_normal_ordered())
print("is operator number conserving?", op2.is_two_body_number_conserving())
# sum of operators so far:
op3 = op + op2
print("op3 =", op3)
# remove terms with coefficients of absolute value < 3
print("truncated op3 =", op3.truncated(3.0))
# map this fermion operator to a qubit operator
print("JW mapped op3 =", op3.qubit_encode())
# we can apply the operators kets and bras defined in the fermion space to retrieve a new FermionState
from inquanto.states import FermionState
fock_state = FermionState([1, 1, 0, 0])
print("<HF|op3 = ", op3.apply_bra(fock_state))
print("op3|HF> = ", op3.apply_ket(fock_state))
```

```
commutator of op with itself: (0.0, ), (0.0, F0^ F0 )
does op commute with itself? True
is op2 normal ordered? False
what about now? True
is operator number conserving? True
op3 = (1.0, F0 F0^), (-3.5, F2^ F1 )
truncated op3 = (-3.5, F2^ F1 )
JW mapped op3 = (0.5, ), (0.5, Z0), (-0.875j, Y1 X2), (-0.875, X1 X2), (-0.875, Y1 Y2), (0.875j, X1 Y2)
<HF|op3 = (0)
op3|HF> = (-3.5, {0: 1, 1: 0, 2: 1, 3: 0})
```

InQuanto also contains a class for dealing with sets of fermionic operators that are not linearly combined – the `FermionOperatorList`

. Each separate `FermionOperator`

in the `FermionOperatorList`

may be associated with an additional scalar. This is particularly
useful when describing sequences of exponentiated fermionic operators, for example when considering variational ansatzes.

```
from sympy import sympify
from inquanto.operators import FermionOperatorList
fol = FermionOperatorList([(sympify("a"),op1),(sympify("b"),op2)])
print(fol)
```

```
a [(1.0, F0 )],
b [(-3.5, F2^ F1 )]
```

Note

`FermionOperatorList`

are designed to retain ordering for use in cases where operator ordering matters, such as exponentiation. In contrast, `FermionOperator`

is not naturally ordered.

This class is particularly useful for containing Trotter sequences. The `FermionOperator`

class contains various helper methods for generating such sequences.

```
op1 = FermionOperator({FermionOperatorString([(0, 0),(1,1)]): 1.0})
op2 = FermionOperator({FermionOperatorString([(2, 0),(3,1)]): 1.0})
op3 = op1 + op2
op_trotterized = op3.trotterize(trotter_number=2)
print(op_trotterized)
```

```
0.5 [(1.0, F0 F1^)],
0.5 [(1.0, F2 F3^)],
0.5 [(1.0, F0 F1^)],
0.5 [(1.0, F2 F3^)]
```

Additional information on the functionality of these classes is provided in the API reference.

## Qubit spaces, operators and states¶

For the analysis of many quantum algorithms, it is useful to work in a representation above the level of quantum circuits decomposed into some set of quantum gate primitives. Similarly to the FermionSpace class, qubit Hilbert spaces are represented in InQuanto using the `QubitSpace`

class.

```
from inquanto.spaces import QubitSpace
qubit_space = QubitSpace(4)
```

The `QubitSpace`

object represents the \(2^N\) dimensional Hilbert space of an \(N\)-qubit system. For chemistry purposes, operators and states in a qubit Hilbert space are typically generated from fermionic operators and states, and thus the `QubitSpace`

class does not have as many methods for directly generating operators and states as the fermionic equivalent. However, methods for finding symmetry operators of a given qubit operator are included, as discussed in the symmetry section.

### Qubit Operators¶

Conventionally, operators acting upon states of qubits are typically represented as linear combinations of *Pauli strings*

where each Pauli string

is a tensor product of single qubit Pauli \(p_n \in \{I,X,Y,Z\}\) operators and \(N\) is the number of qubits. Any operator acting on a register of qubits can be decomposed in this form. This representation is particularly useful when considering quantum chemistry problems, where operators acting on states of fermions must be mapped to operators acting on states of qubits prior to simulation. In InQuanto, operators of this form are implemented using the `QubitOperator`

class. In chemistry, instances of this will frequently derive from mapping a `FermionOperator`

, however they may also be constructed manually. Akin to the fermion operators discussed above, these comprise a map between `QubitOperatorString`

Pauli strings and numerical or symbolic coefficients.

Whereas `FermionOperatorString`

represents strings of creation and annihilation operators as a collection of pairs of integers (indexing modes) and \(0\) or \(1\) (annihilation/creation), pytket provides us a more sophisticated approach to identifying qubits. As such, a `QubitOperatorString`

contains a map between pytket `qubit`

objects and `Pauli`

objects, indicating which Pauli operator (of \(\{I,X,Y,Z\}\)) is acting on which qubit. Note that this map is not necessarily exhaustive – while it may contain *explicit* identity operators, the operator is assumed to also *implicitly* act with the identity on any qubit not contained within the map.

```
from inquanto.operators import QubitOperatorString
from pytket import Qubit
from pytket.pauli import Pauli
pauli_string = QubitOperatorString({Qubit(0):Pauli.X, Qubit(1):Pauli.X})
print(pauli_string.map)
```

```
{q[0]: <Pauli.X: 1>, q[1]: <Pauli.X: 1>}
```

Much like `FermionOperator`

, a `QubitOperator`

then stores a linear combination of Pauli strings as a map between these `QubitOperatorString`

objects and numerical or symbolic coefficients.

```
from inquanto.operators import QubitOperator
pauli_string = QubitOperatorString({Qubit(0):Pauli.X, Qubit(1):Pauli.X})
op = QubitOperator({pauli_string:1.0})
print(op)
```

```
(1.0, X0 X1)
```

As with their fermionic counterparts, a variety of convenient alternative methods to construct the operator are provided, detailed in the API reference for `QubitOperator`

. Basic linear algebra is supported on these objects, along with various useful tools detailed in the API reference for `QubitOperator`

.

```
op1 = QubitOperator("X0",1.0)
op2 = QubitOperator("Z0",1.0)
op3 = op1 + op2
print('Op1 + Op1:', op3)
print('Op1 conjugated:', op1.dagger())
print('Commutator:', op1.commutator(op2))
print('Sparse matrix form:', op1.to_sparse_matrix(1))
print('Identity:', QubitOperator.identity())
```

```
Op1 + Op1: (1.0, X0), (1.0, Z0)
Op1 conjugated: (1.0, X0)
Commutator: (-2j, Y0)
Sparse matrix form: <Compressed Sparse Column sparse matrix of dtype 'complex128'
with 2 stored elements and shape (2, 2)>
Coords Values
(1, 0) (1+0j)
(0, 1) (1+0j)
Identity: (1.0, )
```

Note

These examples have used an alternative string construction method for the `QubitOperator`

, which is useful for simple examples such as this.

Several helper methods for determining properties of an operator are also available:

```
print('Is operator Hermitian?', op3.is_hermitian())
print('Is operator anti-Hermitian?', op3.is_antihermitian())
print('Is operator unitary?', op3.is_unitary())
print('Is operator unit-norm?', op3.is_unit_norm(order=2))
print('Is operator self-inverse?', op3.is_self_inverse())
```

```
Is operator Hermitian? True
Is operator anti-Hermitian? False
Is operator unitary? False
Is operator unit-norm? False
Is operator self-inverse? False
```

As mentioned above, a `QubitOperator`

consists of a mapping between `QubitOperatorString`

objects and their
corresponding coefficients in the linear combination representing the operator. The terms may be extracted with the
`pauli_strings()`

property, whereas the coefficients may be extracted as a
`list`

with the `coefficients()`

property.

```
print('Operator coefficients:')
print(op3.coefficients)
print('Pauli strings:')
print(op3.pauli_strings)
```

```
Operator coefficients:
[1.0, 1.0]
Pauli strings:
[(Xq[0]), (Zq[0])]
```

Often when considering quantum algorithms, it is useful to describe qubit operators in their symplectic representation. This consists of an \((M\times 2N)\) binary matrix where \(M\) is the number of independent Pauli strings in the operator, and \(N\) is the number of qubits. The leftmost half of this matrix designates whether a Pauli \(X\) is acting on qubit \(n\) in term \(m\), and the rightmost half designates whether a Pauli \(Z\) is acting on the same qubit in the same term. As \(Y = \mathrm{i} XZ\), both leftmost and rightmost entries are \(1\) if a Pauli \(Y\) is present. Note that in this representation, information regarding the coefficients of each term must be stored independently.

```
print(op1.symplectic_representation())
```

```
[[ True False]]
```

InQuanto additionally provides a class for dealing with sets of Pauli operators that are not linearly combined, akin to the `FermionOperatorList`

– the `QubitOperatorList`

. This provides similar functionality to that of its fermionic counterpart.

```
from sympy import sympify
from inquanto.operators import QubitOperatorList
qol = QubitOperatorList([(sympify("a"),op1),(sympify("b"),op2)])
print(qol)
```

```
a [(1.0, X0)],
b [(1.0, Z0)]
```

Trotterization functionality is also available for the `QubitOperator`

.

```
op1 = QubitOperator("X0",1.0)
op2 = QubitOperator("Z0",1.0)
op3 = op1 + op2
op_trotterized = op3.trotterize(trotter_number=2)
print(op_trotterized)
```

```
0.5 [(1.0, X0)],
0.5 [(1.0, Z0)],
0.5 [(1.0, X0)],
0.5 [(1.0, Z0)]
```

The `API reference`

details these methods, and provides a full breakdown of the other functionality available for the `QubitOperatorList`

classes.

### Qubit States & Expectation Values¶

A register of \(N\) qubits corresponds to a \(\mathcal{C}^{2^{N}}\) Hilbert space. As such, it can be represented with a \(2^N\) dimensional vector of complex numbers. Clearly, generating such a vector is not scalable on a classical computer (otherwise we wouldn’t need quantum computers), but this approach can be practical for small \(N\). For some tasks (for instance, if we are simulating the action of a known Clifford operator), we can guarantee that a given state will have polynomial support. In this case, we can efficiently store the state in a sparse state vector. InQuanto provides an alternative to an explicit sparse state vector representation of states in the form of the `QubitState`

class, instances of which consist of linear combinations of `QubitStateString`

objects.

```
from inquanto.states import QubitState
qubit_state = QubitState([1,1,0,0],1.)
print(qubit_state)
```

```
(1.0, {q[0]: 1, q[1]: 1, q[2]: 0, q[3]: 0})
```

These can be converted to state vector representations:

```
state_vector = qubit_state.to_ndarray()
print(state_vector)
```

```
[[0.+0.j]
[0.+0.j]
[0.+0.j]
[0.+0.j]
[0.+0.j]
[0.+0.j]
[0.+0.j]
[0.+0.j]
[0.+0.j]
[0.+0.j]
[0.+0.j]
[0.+0.j]
[1.+0.j]
[0.+0.j]
[0.+0.j]
[0.+0.j]]
```

Most importantly for the purposes of analyzing quantum algorithms, the `QubitState`

representation allows for performing linear algebra with other `QubitState`

and `QubitOperator`

objects.

```
overlap = qubit_state.vdot(qubit_state)
expectation_value = op1.state_expectation(qubit_state)
print('Overlap:', overlap)
print('Expectation value:', expectation_value)
```

```
Overlap: 1.0
Expectation value: 0
```

`QubitState`

objects are implicitly sparse, in contrast to full dense statevector representations. Performing calculations in this way allows for the analysis of qubit states and operators without the need to either generate full circuit representations, or the expensive generation of full \(2^{2N}\) matrix representations of operators.

## Fermion-to-Qubit Mapping¶

In quantum chemistry, we are most often concerned with the properties of electrons. As electrons are fermions, second-quantized fermionic creation and annihilation operators obey the fermionic anticommutation relations:

This implicitly restricts the occupation number of a given fermionic mode to \(\{0,1\}\). Conversely, qubits can be considered to be *paraparticles*. Like fermionic modes, each qubit is a two-level system. However, the above fermionic anticommutation relations are not obeyed:

As such, in order to use a system of qubits to simulate a system of fermions, the fermionic anticommutation relations must be encoded. An encoding scheme consists of a linear map between states and operators in the fermionic Hilbert space and the states and operators in the qubit Hilbert space. For quantum chemistry purposes, mapping both states and operators is important - for example, in a canonical VQE calculation, both the Hamiltonian operator and the reference state must be mapped to the qubit space.

In InQuanto, fermion-to-qubit mappings are stored in the inquanto.mappings module. Four mappings are included in the current version:

`"Paraparticular" mapping`

– this does not encode fermionic statistics

In order to map an InQuanto `FermionOperator`

to a `QubitOperator`

, the `operator_map()`

method can be used:

```
from inquanto.operators import FermionOperator,FermionOperatorString
from inquanto.mappings import QubitMappingJordanWigner
fermion_operator = FermionOperator(FermionOperatorString([(1, 0)]), 0.5)
qubit_operator = QubitMappingJordanWigner.operator_map(fermion_operator)
print(qubit_operator)
```

```
(0.25, Z0 X1), (0.25j, Z0 Y1)
```

and similarly, to map `FermionState`

objects to `QubitState`

objects, the `state_map()`

method is used:

```
from inquanto.states import FermionState
fermion_state = FermionState([0,0,1,1])
qubit_state = QubitMappingJordanWigner.state_map(fermion_state)
```

Note that both of these methods include an optional argument specifying the register of tket `Qubit`

objects that comprise the target qubit space. Without this argument provided (as above), the mapping will if possible infer that a minimally sized register is to be used, indexed incrementally from 0. For some mappings (for example, the Bravyi-Kitaev mapping), this information cannot be inferred. If these mappings are used, it is necessary to specify the qubit register.

```
from pytket import Qubit
from inquanto.mappings import QubitMappingBravyiKitaev
qubit_operator_bk = QubitMappingBravyiKitaev.operator_map(fermion_operator,[Qubit(i) for i in range(8)])
print(qubit_operator_bk)
```

```
(0.25, Z0 X1 X3 X7), (0.25j, Y1 X3 X7)
```

Finally, when performing state vector simulations one may wish to represent fermionic and qubit states in the form of full dense or sparse vectors of complex numbers. The inquanto.mappings classes support this, and will return in the same representation as the input.

```
from numpy import array
from scipy.sparse import csc_matrix
dense_fermionic_state = array([[1],[0],[0],[0]])
sparse_fermionic_state = csc_matrix(dense_fermionic_state)
dense_qubit_state = QubitMappingJordanWigner.state_map(dense_fermionic_state,qubits=[Qubit(i) for i in range(4)])
sparse_qubit_state = QubitMappingJordanWigner.state_map(sparse_fermionic_state,qubits=[Qubit(i) for i in range(4)])
print(dense_qubit_state)
print(sparse_qubit_state)
```

```
[[1]
[0]
[0]
[0]
[0]
[0]
[0]
[0]
[0]
[0]
[0]
[0]
[0]
[0]
[0]
[0]]
<Compressed Sparse Column sparse matrix of dtype 'int64'
with 1 stored elements and shape (16, 1)>
Coords Values
(0, 0) 1
```

### Custom mappings¶

For advanced usage, it is possible to define custom mapping schemes. The `QubitMapping`

class is a superclass containing the logic of the `operator_map()`

and `state_map()`

methods. Custom mappings can be designed by subclassing this and implementing the `update_set()`

, `parity_set()`

, `rho_set()`

and `state_map_matrix()`

methods. The first three of these methods return sets of qubits according to the formalism of Seeley, Richard and Love [38]. The `state_map_matrix()`

method returns a matrix which transforms a binary column vector representation of the binary index corresponding to a given basis state in the fermionic space, to a similar vector in the qubit space. In the flip/update/parity/rho set formalism, fermionic creation and annihilation operators are mapped according to the following relation:

where \(U(i)\) is the update set, \(P(i)\) is the parity set, \(\rho(i)\) is the rho set for orbital \(i\). As an example, for the Jordan-Wigner mapping:

Comparing this against (77) we see the update and flip set are always empty, whereas the parity set consists of all qubits with index less than \(i\). `state_map_matrix()`

for the Jordan-Wigner transformation will always return the identity matrix, since state \(\ket{n}\) in the fermionic space is always mapped to state \(\ket{n}\) in the qubit space.

## Integral Operators¶

We have described above how InQuanto stores and handles the fermionic Hamiltonian via the
`FermionOperator`

class. The `FermionOperator`

class stores
those operators and numeric integral values as items of a dictionary. Such storage is convenient for manipulating
and accessing individual terms of the Hamiltonian, but is not tailored well to linear algebra operations, such as
integral transformation.

The InQuanto `operators`

module provides integral operator classes for storing and manipulating
chemistry integrals (\(h_{ij}\) and \(h_{ijkl}\), typically molecular orbital integrals) as algebraic objects.
The standard integral operators are the `ChemistryRestrictedIntegralOperator`

and
`ChemistryUnrestrictedIntegralOperator`

classes, for spin-restricted and spin-unrestricted formalisms respectively. In the restricted case, one-body
integrals are stored in a 2-dimensional `numpy`

array with shape `(n, n)`

, and two-body integrals
in a 4-dimensional array with shape `(n, n, n, n)`

, where `n`

is the number of spatial orbitals.
In the unrestricted case, the \(\uparrow\) and \(\downarrow\) spin channels have
independent integrals. Hence, two 2-dimensional arrays store the one-body integrals, one for each spin channel, and
four 4-dimensional arrays store all spin-configurations of the two-body integrals
(\(\upuparrows\upuparrows, \downdownarrows\downdownarrows, \upuparrows\downdownarrows\) and \(\downdownarrows\upuparrows\)).

The inquanto-pyscf extension is the primary tool for generating integral operators for a chemical system,
though integral operators may also be instantiated directly with `numpy`

arrays (see the `ChemistryRestrictedIntegralOperator`

and `ChemistryUnrestrictedIntegralOperator`

constructors). Below, we use the Express module
to load in a pre-computed integral operator for LiH:

```
import pandas as pd
from inquanto.express import load_h5
pd.options.display.max_rows = 15
lih_sto3g = load_h5('lih_sto3g.h5', as_tuple = True)
integral_operator = lih_sto3g.hamiltonian_operator
print(integral_operator.df())
```

```
Coefficients Terms
0 1.050650
1 -4.746695 F0^ F0
2 0.109103 F0^ F2
3 0.168024 F0^ F4
4 -0.026783 F0^ F10
... ... ...
1496 0.009754 F11^ F10^ F8 F9
1497 0.009754 F11^ F10^ F8 F9
1498 0.228088 F11^ F10^ F10 F11
1499 0.228088 F11^ F10^ F10 F11
1500 -0.935480 F11^ F11
[1501 rows x 2 columns]
```

where the `df()`

method produces a `pandas`

dataframe
of all integrals and their corresponding fermion operator terms. Integral operators may be converted directly into a qubit Hamiltonian with the
`qubit_encode()`

method, which uses Jordan-Wigner mapping by default:

```
qubit_hamiltonian = integral_operator.qubit_encode()
print("LiH STO-3G JW qubit hamiltonian:\n", qubit_hamiltonian.df())
```

```
LiH STO-3G JW qubit hamiltonian:
Coefficient Term \
0 -4.107196
1 -0.396888 Z11
2 -0.396888 Z10
3 0.114044 Z10 Z11
4 -0.228984 Z9
.. ... ...
626 -0.011161 Z0 X1 Z2 Z3 Z4 Z5 Z6 Z7 Z8 Z9 Z10 X11
627 0.029058 Z0 Y1 Z2 Y3
628 0.034437 Z0 Y1 Z2 Z3 Z4 Y5
629 -0.011161 Z0 Y1 Z2 Z3 Z4 Z5 Z6 Z7 Z8 Z9 Z10 Y11
630 0.414556 Z0 Z1
Coefficient Type
0 <class 'numpy.float64'>
1 <class 'numpy.float64'>
2 <class 'numpy.float64'>
3 <class 'numpy.float64'>
4 <class 'numpy.float64'>
.. ...
626 <class 'numpy.float64'>
627 <class 'numpy.float64'>
628 <class 'numpy.float64'>
629 <class 'numpy.float64'>
630 <class 'numpy.float64'>
[631 rows x 3 columns]
```

Unitary transformations may be applied to the integrals with the `rotate()`

method,
which takes a unitary matrix and transforms in-place:

```
from scipy.stats import ortho_group
u = ortho_group.rvs(lih_sto3g.n_orbital) # Random, real unitary matrix
integral_operator.rotate(u)
print(integral_operator.df())
```

```
Coefficients Terms
0 1.050650
1 -1.380618 F0^ F0
2 0.498548 F0^ F2
3 -0.678413 F0^ F4
4 0.758578 F0^ F6
... ... ...
4460 -0.002527 F11^ F10^ F9 F10
4461 -0.002527 F11^ F10^ F9 F10
4462 0.176896 F11^ F10^ F10 F11
4463 0.176896 F11^ F10^ F10 F11
4464 -1.401626 F11^ F11
[4465 rows x 2 columns]
```

Integral operators support other utility methods including `approx_equal()`

,
for comparing Hamiltonians, `items()`

, which iterates over terms, and
`to_FermionOperator()`

, for converting between operator and integral focused objects.

Alongside integral operators, the `operators`

module also provides a set of classes for managing reduced density matrices (RDMs).
These may be used in combination with integral operators to compute useful properties of the Hamiltonian. For example, with the
`UnrestrictedOneBodyRDM`

we may compute the total mean-field energy and effective potential matrices:

```
from inquanto.operators import UnrestrictedOneBodyRDM
import numpy as np
integral_operator = load_h5('h3_sto3g_m2_u.h5', as_tuple = True).hamiltonian_operator
rdm1 = UnrestrictedOneBodyRDM(rdm1_aa=np.diag([1, 1, 0]), rdm1_bb=np.diag([1, 0, 0]))
print("Mean-field energy:\n", integral_operator.energy(rdm1))
print("\nEffective potential a:\n", integral_operator.effective_potential(rdm1)[0])
print("\nEffective potential b:\n", integral_operator.effective_potential(rdm1)[1])
```

```
Mean-field energy:
-1.5140974066187696
Effective potential a:
[[ 0.996 -0. 0.226]
[-0. 0.924 0. ]
[ 0.226 0. 1.632]]
Effective potential b:
[[ 1.163 -0. 0.069]
[-0. 1.541 -0. ]
[ 0.069 0. 1.712]]
```

Above, RDM is initialized with `numpy`

arrays in the same basis as the integral operator. The `energy()`

method may also be provided with a 2-RDM (see `RestrictedTwoBodyRDM`

and `UnrestrictedTwoBodyRDM`

)
to calculate the total, non-mean-field, energy.

In addition to the basic integral operator classes, InQuanto also includes compact integral operators:
`ChemistryRestrictedIntegralOperatorCompact`

and `ChemistryUnrestrictedIntegralOperatorCompact`

,
which exploit symmetries in the two-body integrals to reduce classical memory requirements. Compact integral operator classes support the same operations
as discussed above, and are most naturally instantiated using the inquanto-pyscf extension.

## Orbital Transformation and Optimization¶

Slater determinants, represented by vectors in Fock space, are functions of molecular orbitals. Typically the molecular orbitals are found by solving some other problem, such as the Hartree-Fock equations, but in some cases orbitals are chosen to be some other set of functions. For instance, they could be localized or optimized in some other way. InQuanto contains tools to help with procedures of this type, and to facilitate the transfer of these methods to quantum algorithms.

The `OrbitalTransformer`

class contains several methods for common practices in molecular orbital manipulation. For
instance, to Gram-Schmidt orthogonalize a set of molecular orbitals in an orthogonal atomic orbital basis,

```
from inquanto.operators import OrbitalTransformer
import numpy
orbitals = numpy.array([[1, 2], [3, 4]])
ot = OrbitalTransformer()
ot.gram_schmidt(v=orbitals, overlap=None)
```

```
array([[-0.316, -0.949],
[-0.949, 0.316]])
```

or, equivalently, in a non-orthogonal atomic orbital basis,

```
s = numpy.array([[1.0, 0.66314574], [0.66314574, 1.0]])
ot.gram_schmidt(v=orbitals, overlap=s)
```

```
array([[-0.267, -1.309],
[-0.802, 1.068]])
```

One can achieve the same goal of orthonormalization using the `orthonormalize()`

method, which finds the closest orthonormal set by the symmetric transformation.

```
ot.orthonormalize(v=orbitals, overlap=s)
```

```
array([[-0.887, 0.999],
[ 1.336, 0.001]])
```

The `OrbitalTransformer`

object also defines a method for computing the unitary which relates two sets of molecular
orbitals. For example, to find the unitary relating the MO coefficients in the matrix \(X\) with those in matrix \(C\),

```
X = numpy.array([[1, 2], [3, 4]])
C = numpy.array([[2, 1], [4, 3]])
ot = OrbitalTransformer()
my_unitary = ot.compute_unitary(v_init=X, v_final=C)
print(my_unitary)
```

```
[[0. 1.]
[1. 0.]]
```

Similarly, to transform orbitals X to orbitals C, if the unitary is known,

```
new_C = ot.transform(v=X, tu=my_unitary)
print(new_C)
```

```
[[2. 1.]
[4. 3.]]
```

The majority of the time, the unitary is not known, and is the result of some optimization process.
The `OrbitalOptimizer`

class in InQuanto is constructed with a black-box function which finds
the rotational unitary, given some variational criteria. For instance, we can perform a localization which depends on
some function `localize`

:

```
from inquanto.operators import OrbitalOptimizer
oo = OrbitalOptimizer(
v_init=initial_orbitals,
occ=[2, 2, 0, 0],
split_rotation=False,
functional=localize,
minimizer=MinimizerScipy(),
reduce_free_parameters=True
)
final_orbitals, minimising_unitary, final_value = oo.optimize()
```

The `OrbitalOptimizer`

object will try to retain orbital symmetries if `point_group`

and `orb_irreps`

are both passed into the constructor. The function passed to the `functional`

argument must be a function of a 2D
array.

Note

The `OrbitalOptimizer`

is also a `callable`

object, but the callable execution of the optimization returns
only the optimized orbitals. This is for compatibility with the `transf`

functionality in some extensions.

After the optimization, a report can be generated with the `generate_report()`

method.

## Double Factorization¶

An important restriction in near-term chemistry applications of quantum computing is the size of the Hamiltonian operator, particularly the two-body interaction term:

which has \(\mathcal{O}(N^4)\) terms where \(N\) is the number of orbitals. Double factorization is a two-step, tensor decomposition of the two-body integrals \(h_{pqrs}\), which provides a systematic approach for truncating terms in two-body Hamiltonians. Below, we provide an introduction to the essential equations, and an example of using this decomposition strategy with InQuanto.

The first decomposition is given by:

where \(N_\gamma = N^2\) for an exact decomposition. And the second:

for each \(t\), where \(N_\lambda^t = N\) for an exact decomposition.

In InQuanto, the first factorization can be done either by eigenvalue decomposition or a pivoted, incomplete Cholesky decomposition [39, 40]. The second factorization step is always an eigenvalue decomposition, since the fully factorized operator must be expressed in terms of unitary matrices to enable circuit-level rotations as shown below.

Each of the decompositions may be truncated to better control the size and scaling of number of terms in the Hamiltonian. The magnitudes of the factors \(\gamma^t\) and \(\lambda_u^t\) determine the importance of their corresponding terms in the decompositions and so terms can be systematically discarded based on their values.

The sums are truncated by discarding terms in ascending order of the factor magnitudes starting from the smallest such
that the sum of the *discarded* factors does not exceed some threshold [41]:

In practice, the first decomposition can be truncated to \(N_\gamma\sim \mathcal{O}(N)\), and the second decomposition to \(N_\lambda^t < N\), helping to control the scaling of the number of terms in the two-body Hamiltonian.

Inserting the double factorized integrals into the two-body Hamiltonian (79), after some work we arrive at the diagonalized expression [41]:

Here, \(\hat{S}\) is a one-body offset term which comes from rearranging the fermion operators:

and \(\hat{R}(\textbf{U}_t)\) are Fock-space basis rotation operators, corresponding to the single particle basis rotation matrices \(\textbf{U}_t\) (the eigenvector matrices of the second decomposition (81)). These operators are given by the Thouless theorem [42]:

which are implemented at the circuit level with a Givens QR decomposition [43]. See below for an example, and here for more details.

We may similarly diagonalize the one-body integrals and \(\hat{S}\) operator:

where all one-body-like terms have been consolidated into an effective one-body Hamiltonian \(H_1'\). The full Hamiltonian can then be written in this diagonal form as:

In InQuanto, a `ChemistryRestrictedIntegralOperator`

object may be transformed into this
representation, a `DoubleFactorizedHamiltonian`

object, using the
`double_factorize()`

method. We demonstrate this below
for H_{2}O in the STO-3G basis:

```
import numpy as np
from inquanto.express import load_h5
from inquanto.operators import ChemistryRestrictedIntegralOperator
ham = load_h5("h2o_sto3g.h5", as_tuple=True).hamiltonian_operator
df_ham = ham.double_factorize(
tol1=1e-3,
)
gammas = df_ham.two_body.outer_params
lambdas = df_ham.two_body.inner_params
N_gamma = len(gammas)
N_lambda = [len(l) for l in lambdas]
df_ham.n_orb, N_gamma, N_lambda
```

```
(7, 23, [7, 7, 4, 7, 2, 7, 4, 7, 2, 2, 7, 4, 4, 7, 2, 7, 4, 7, 2, 7, 2, 4, 7])
```

where we show the extent of the truncation (recall equation (82)). Default behavior is to consolidate and diagonalize all one-body terms, so
`df_ham`

represents a Hamiltonian of the form (87).

Note

`double_factorize()`

performs the
decomposition on the molecular orbital (MO) integrals. In classical literature, similar methods have been used to
reduce the memory storage requirements of two-body atomic orbital (AO) integrals [44]. In InQuanto, the purpose
of this approach is to truncate the Hamiltonian for quantum simulation, so it should not be expected to reduce
classical memory requirements.

The `items()`

method returns an iterator which generates the
`FermionOperator`

terms (the number operator sums in brackets of (87)) alongside their corresponding
rotation matrices (\(\textbf{W}\) and \(\textbf{U}_t\)). For example, the one-body and the first two-body terms are:

```
terms = list(df_ham.items())
print("One body operator: ", terms[1][0])
print("One body rotation: ", terms[1][1])
print("\nFirst two body operator: ", terms[2][0])
print("First two body rotation: ", terms[2][1])
```

```
One body operator: (-35.15627858132068, F0^ F0 ), (-35.15627858132068, F1^ F1 ), (-8.99140515493988, F2^ F2 ), (-8.99140515493988, F3^ F3 ), (-8.30673410272335, F4^ F4 ), (-8.30673410272335, F5^ F5 ), (-8.057439359193436, F6^ F6 ), (-8.057439359193436, F7^ F7 ), (-8.028943982634141, F8^ F8 ), (-8.028943982634141, F9^ F9 ), (-4.621602713403972, F10^ F10 ), (-4.621602713403972, F11^ F11 ), (-4.593693566394927, F12^ F12 ), (-4.593693566394927, F13^ F13 )
One body rotation: [[-0.999 -0.031 -0. -0.011 0. 0. 0.006]
[ 0.028 -0.897 -0. -0.22 0. -0. -0.382]
[-0. 0. -0.775 0. 0. 0.632 -0. ]
[ 0.011 0.046 -0. -0.908 0. 0. 0.416]
[ 0. -0. -0. -0. -1. -0. 0. ]
[ 0.014 -0.438 -0. 0.355 -0. 0. 0.825]
[-0. 0. -0.632 0. 0. -0.775 0. ]]
First two body operator: (2.046238405452405, F0^ F0 F0^ F0 ), (2.046238405452405, F0^ F0 F1^ F1 ), (0.8165371440369917, F0^ F0 F2^ F2 ), (0.8165371440369917, F0^ F0 F3^ F3 ), (0.8055041237052253, F0^ F0 F4^ F4 ), (0.8055041237052253, F0^ F0 F5^ F5 ), (0.767389123035518, F0^ F0 F6^ F6 ), (0.767389123035518, F0^ F0 F7^ F7 ), (0.7479034551648777, F0^ F0 F8^ F8 ), (0.7479034551648777, F0^ F0 F9^ F9 ), (0.41264550253745813, F0^ F0 F10^ F10 ), (0.41264550253745813, F0^ F0 F11^ F11 ), (0.4035573895111223, F0^ F0 F12^ F12 ), (0.4035573895111223, F0^ F0 F13^ F13 ), (2.046238405452405, F1^ F1 F0^ F0 ), (2.046238405452405, F1^ F1 F1^ F1 ), (0.8165371440369917, F1^ F1 F2^ F2 ), (0.8165371440369917, F1^ F1 F3^ F3 ), (0.8055041237052253, F1^ F1 F4^ F4 ), (0.8055041237052253, F1^ F1 F5^ F5 ), (0.767389123035518, F1^ F1 F6^ F6 ), (0.767389123035518, F1^ F1 F7^ F7 ), (0.7479034551648777, F1^ F1 F8^ F8 ), (0.7479034551648777, F1^ F1 F9^ F9 ), (0.41264550253745813, F1^ F1 F10^ F10 ), (0.41264550253745813, F1^ F1 F11^ F11 ), (0.4035573895111223, F1^ F1 F12^ F12 ), (0.4035573895111223, F1^ F1 F13^ F13 ), (0.8165371440369917, F2^ F2 F0^ F0 ), (0.8165371440369917, F2^ F2 F1^ F1 ), (0.32583344434133926, F2^ F2 F2^ F2 ), (0.32583344434133926, F2^ F2 F3^ F3 ), (0.32143079463649665, F2^ F2 F4^ F4 ), (0.32143079463649665, F2^ F2 F5^ F5 ), (0.30622126982800785, F2^ F2 F6^ F6 ), (0.30622126982800785, F2^ F2 F7^ F7 ), (0.2984456501590826, F2^ F2 F8^ F8 ), (0.2984456501590826, F2^ F2 F9^ F9 ), (0.16466330572421778, F2^ F2 F10^ F10 ), (0.16466330572421778, F2^ F2 F11^ F11 ), (0.16103675769568102, F2^ F2 F12^ F12 ), (0.16103675769568102, F2^ F2 F13^ F13 ), (0.8165371440369917, F3^ F3 F0^ F0 ), (0.8165371440369917, F3^ F3 F1^ F1 ), (0.32583344434133926, F3^ F3 F2^ F2 ), (0.32583344434133926, F3^ F3 F3^ F3 ), (0.32143079463649665, F3^ F3 F4^ F4 ), (0.32143079463649665, F3^ F3 F5^ F5 ), (0.30622126982800785, F3^ F3 F6^ F6 ), (0.30622126982800785, F3^ F3 F7^ F7 ), (0.2984456501590826, F3^ F3 F8^ F8 ), (0.2984456501590826, F3^ F3 F9^ F9 ), (0.16466330572421778, F3^ F3 F10^ F10 ), (0.16466330572421778, F3^ F3 F11^ F11 ), (0.16103675769568102, F3^ F3 F12^ F12 ), (0.16103675769568102, F3^ F3 F13^ F13 ), (0.8055041237052253, F4^ F4 F0^ F0 ), (0.8055041237052253, F4^ F4 F1^ F1 ), (0.32143079463649665, F4^ F4 F2^ F2 ), (0.32143079463649665, F4^ F4 F3^ F3 ), (0.3170876333750909, F4^ F4 F4^ F4 ), (0.3170876333750909, F4^ F4 F5^ F5 ), (0.30208361911523307, F4^ F4 F6^ F6 ), (0.30208361911523307, F4^ F4 F7^ F7 ), (0.2944130633377986, F4^ F4 F8^ F8 ), (0.2944130633377986, F4^ F4 F9^ F9 ), (0.16243838109804687, F4^ F4 F10^ F10 ), (0.16243838109804687, F4^ F4 F11^ F11 ), (0.15886083485519145, F4^ F4 F12^ F12 ), (0.15886083485519145, F4^ F4 F13^ F13 ), (0.8055041237052253, F5^ F5 F0^ F0 ), (0.8055041237052253, F5^ F5 F1^ F1 ), (0.32143079463649665, F5^ F5 F2^ F2 ), (0.32143079463649665, F5^ F5 F3^ F3 ), (0.3170876333750909, F5^ F5 F4^ F4 ), (0.3170876333750909, F5^ F5 F5^ F5 ), (0.30208361911523307, F5^ F5 F6^ F6 ), (0.30208361911523307, F5^ F5 F7^ F7 ), (0.2944130633377986, F5^ F5 F8^ F8 ), (0.2944130633377986, F5^ F5 F9^ F9 ), (0.16243838109804687, F5^ F5 F10^ F10 ), (0.16243838109804687, F5^ F5 F11^ F11 ), (0.15886083485519145, F5^ F5 F12^ F12 ), (0.15886083485519145, F5^ F5 F13^ F13 ), (0.767389123035518, F6^ F6 F0^ F0 ), (0.767389123035518, F6^ F6 F1^ F1 ), (0.30622126982800785, F6^ F6 F2^ F2 ), (0.30622126982800785, F6^ F6 F3^ F3 ), (0.30208361911523307, F6^ F6 F4^ F4 ), (0.30208361911523307, F6^ F6 F5^ F5 ), (0.2877895677180508, F6^ F6 F6^ F6 ), (0.2877895677180508, F6^ F6 F7^ F7 ), (0.2804819687896131, F6^ F6 F8^ F8 ), (0.2804819687896131, F6^ F6 F9^ F9 ), (0.15475209021245984, F6^ F6 F10^ F10 ), (0.15475209021245984, F6^ F6 F11^ F11 ), (0.15134382699799553, F6^ F6 F12^ F12 ), (0.15134382699799553, F6^ F6 F13^ F13 ), (0.767389123035518, F7^ F7 F0^ F0 ), (0.767389123035518, F7^ F7 F1^ F1 ), (0.30622126982800785, F7^ F7 F2^ F2 ), (0.30622126982800785, F7^ F7 F3^ F3 ), (0.30208361911523307, F7^ F7 F4^ F4 ), (0.30208361911523307, F7^ F7 F5^ F5 ), (0.2877895677180508, F7^ F7 F6^ F6 ), (0.2877895677180508, F7^ F7 F7^ F7 ), (0.2804819687896131, F7^ F7 F8^ F8 ), (0.2804819687896131, F7^ F7 F9^ F9 ), (0.15475209021245984, F7^ F7 F10^ F10 ), (0.15475209021245984, F7^ F7 F11^ F11 ), (0.15134382699799553, F7^ F7 F12^ F12 ), (0.15134382699799553, F7^ F7 F13^ F13 ), (0.7479034551648777, F8^ F8 F0^ F0 ), (0.7479034551648777, F8^ F8 F1^ F1 ), (0.2984456501590826, F8^ F8 F2^ F2 ), (0.2984456501590826, F8^ F8 F3^ F3 ), (0.2944130633377986, F8^ F8 F4^ F4 ), (0.2944130633377986, F8^ F8 F5^ F5 ), (0.2804819687896131, F8^ F8 F6^ F6 ), (0.2804819687896131, F8^ F8 F7^ F7 ), (0.2733599255869175, F8^ F8 F8^ F8 ), (0.2733599255869175, F8^ F8 F9^ F9 ), (0.15082260028140723, F8^ F8 F10^ F10 ), (0.15082260028140723, F8^ F8 F11^ F11 ), (0.147500880233922, F8^ F8 F12^ F12 ), (0.147500880233922, F8^ F8 F13^ F13 ), (0.7479034551648777, F9^ F9 F0^ F0 ), (0.7479034551648777, F9^ F9 F1^ F1 ), (0.2984456501590826, F9^ F9 F2^ F2 ), (0.2984456501590826, F9^ F9 F3^ F3 ), (0.2944130633377986, F9^ F9 F4^ F4 ), (0.2944130633377986, F9^ F9 F5^ F5 ), (0.2804819687896131, F9^ F9 F6^ F6 ), (0.2804819687896131, F9^ F9 F7^ F7 ), (0.2733599255869175, F9^ F9 F8^ F8 ), (0.2733599255869175, F9^ F9 F9^ F9 ), (0.15082260028140723, F9^ F9 F10^ F10 ), (0.15082260028140723, F9^ F9 F11^ F11 ), (0.147500880233922, F9^ F9 F12^ F12 ), (0.147500880233922, F9^ F9 F13^ F13 ), (0.41264550253745813, F10^ F10 F0^ F0 ), (0.41264550253745813, F10^ F10 F1^ F1 ), (0.16466330572421778, F10^ F10 F2^ F2 ), (0.16466330572421778, F10^ F10 F3^ F3 ), (0.16243838109804687, F10^ F10 F4^ F4 ), (0.16243838109804687, F10^ F10 F5^ F5 ), (0.15475209021245984, F10^ F10 F6^ F6 ), (0.15475209021245984, F10^ F10 F7^ F7 ), (0.15082260028140723, F10^ F10 F8^ F8 ), (0.15082260028140723, F10^ F10 F9^ F9 ), (0.08321430695009596, F10^ F10 F10^ F10 ), (0.08321430695009596, F10^ F10 F11^ F11 ), (0.08138159334405823, F10^ F10 F12^ F12 ), (0.08138159334405823, F10^ F10 F13^ F13 ), (0.41264550253745813, F11^ F11 F0^ F0 ), (0.41264550253745813, F11^ F11 F1^ F1 ), (0.16466330572421778, F11^ F11 F2^ F2 ), (0.16466330572421778, F11^ F11 F3^ F3 ), (0.16243838109804687, F11^ F11 F4^ F4 ), (0.16243838109804687, F11^ F11 F5^ F5 ), (0.15475209021245984, F11^ F11 F6^ F6 ), (0.15475209021245984, F11^ F11 F7^ F7 ), (0.15082260028140723, F11^ F11 F8^ F8 ), (0.15082260028140723, F11^ F11 F9^ F9 ), (0.08321430695009596, F11^ F11 F10^ F10 ), (0.08321430695009596, F11^ F11 F11^ F11 ), (0.08138159334405823, F11^ F11 F12^ F12 ), (0.08138159334405823, F11^ F11 F13^ F13 ), (0.4035573895111223, F12^ F12 F0^ F0 ), (0.4035573895111223, F12^ F12 F1^ F1 ), (0.16103675769568102, F12^ F12 F2^ F2 ), (0.16103675769568102, F12^ F12 F3^ F3 ), (0.15886083485519145, F12^ F12 F4^ F4 ), (0.15886083485519145, F12^ F12 F5^ F5 ), (0.15134382699799553, F12^ F12 F6^ F6 ), (0.15134382699799553, F12^ F12 F7^ F7 ), (0.147500880233922, F12^ F12 F8^ F8 ), (0.147500880233922, F12^ F12 F9^ F9 ), (0.08138159334405823, F12^ F12 F10^ F10 ), (0.08138159334405823, F12^ F12 F11^ F11 ), (0.07958924346013586, F12^ F12 F12^ F12 ), (0.07958924346013586, F12^ F12 F13^ F13 ), (0.4035573895111223, F13^ F13 F0^ F0 ), (0.4035573895111223, F13^ F13 F1^ F1 ), (0.16103675769568102, F13^ F13 F2^ F2 ), (0.16103675769568102, F13^ F13 F3^ F3 ), (0.15886083485519145, F13^ F13 F4^ F4 ), (0.15886083485519145, F13^ F13 F5^ F5 ), (0.15134382699799553, F13^ F13 F6^ F6 ), (0.15134382699799553, F13^ F13 F7^ F7 ), (0.147500880233922, F13^ F13 F8^ F8 ), (0.147500880233922, F13^ F13 F9^ F9 ), (0.08138159334405823, F13^ F13 F10^ F10 ), (0.08138159334405823, F13^ F13 F11^ F11 ), (0.07958924346013586, F13^ F13 F12^ F12 ), (0.07958924346013586, F13^ F13 F13^ F13 )
First two body rotation: [[-0.991 -0.09 -0. -0. -0.094 0. -0.01 ]
[ 0.109 -0.641 -0. -0. -0.587 -0. 0.483]
[-0. 0. -0.694 0. 0. -0.72 -0. ]
[ 0.044 0.399 0. -0. -0.798 0. -0.449]
[ 0. -0. -0. -1. 0. 0. -0. ]
[ 0.057 -0.65 -0. 0. 0.101 0. -0.751]
[-0. 0. -0.72 0. 0. 0.694 0. ]]
```

Note

The corresponding rotation matrix for the constant energy term is the identity.

Circuit representations of the rotation operators may be computed using the `inquanto.ansatzes.restricted_basis_rotation_to_circuit()`

method,
which uses the `RealRestrictedBasisRotationAnsatz`

to encode the rotations in the Jordan-Wigner picture
(see here for more information). For example, below we construct a computable for the expectation value of a single term in
(87) with a UCCSD ansatz:

```
from inquanto.mappings import QubitMappingJordanWigner
from inquanto.ansatzes import FermionSpaceAnsatzUCCSD, CircuitAnsatz, ComposedAnsatz, restricted_basis_rotation_to_circuit
from inquanto.states import FermionState
from inquanto.computables import ExpectationValue
jw = QubitMappingJordanWigner()
operator, rotation = terms[4] # Selecting the 4th term as an example
ansatz = FermionSpaceAnsatzUCCSD( # |psi>
fermion_space=7*2,
fermion_state=FermionState([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0]),
qubit_mapping=jw
)
ansatz_rotator = CircuitAnsatz(restricted_basis_rotation_to_circuit(rotation.T)) # R(U)^
rotated_ansatz = ComposedAnsatz(ansatz_rotator, ansatz) # R(U)^ |psi>
term_computable = ExpectationValue(rotated_ansatz, jw.operator_map(operator)) # <psi| R(U) X R(U)^ |psi>
```

Note the transpose of the rotation matrix: \(\hat{R}(\textbf{U}_t)^\dagger = \hat{R}(\textbf{U}_t^T)\), which is true for a real-valued rotation matrix.