Multiconfiguration States Using Givens Rotations
In InQuanto, Givens rotations can be used to prepare a quantum circuit corresponding to a linear combination of
determinants, where the latter are selected by the user and represented by the terms (occupation configurations) of
an InQuanto QubitState
. The resulting object can be used as i) an ansatz itself, or ii)
a multiconfigurational generalization of a mean-field single reference, to which another ansatz (that allows for
multireference initial states) can be applied. In both of these cases, InQuanto allows for fixed and variational
configuration coefficients.
The methodology of preparing these circuits is based on proofs that multicontrolled Givens rotations are universal for quantum chemistry [49]. A Givens rotation is a unitary operation that linearly mixes between two configurations. Restricting to real coefficients, a 2-qubit (1-body) Givens unitary can be written in the 2-qubit computational basis as
which corresponds to a rotation between the \(|10\rangle\) and \(|01\rangle\) basis states. This has a straight-forward generalization to \(n\) qubits [49]. For example, if one chooses to mix \(|1100\rangle\) and \(|0011\rangle\), this can be done using a 4-qubit (2-body) Givens rotation \(G_2\) applied to either \(|1100\rangle\) or \(|0011\rangle\) (and where \(G_2\) is a 4 \(\times\) 4 generalization of \(G_1\), with appropriate lexicographical ordering)
Hence the configuration coefficients correspond to elements of the unitary, are tied to the rotation angles
of the corresponding gates that implement the unitary, and must be normalized to unity. In InQuanto,
the gate decompositions previously reported in the literature for particle-number preserving forms of the \(G_1\)
and \(G_2\) [49, 50] are used in the Ansatz classes
MultiConfigurationState
and MultiConfigurationAnsatz
.
The following example shows how to use InQuanto to prepare a circuit in a quantum state corresponding to an equal mixing of 2 occupation configurations:
from inquanto.ansatzes import MultiConfigurationState
from inquanto.states import QubitState, QubitStateString
from pytket.extensions.qiskit import AerStateBackend
from math import sqrt
config1, c_1 = QubitStateString([1, 1, 0, 0]), 1/sqrt(2)
config2, c_2 = QubitStateString([0, 0, 1, 1]), 1/sqrt(2)
qubit_states = QubitState({config1: c_1, config2: c_2})
multi_state = MultiConfigurationState(qubit_states)
multi_state_circuit = AerStateBackend().get_compiled_circuit(multi_state.get_circuit())
print("Circuit depth:", multi_state_circuit.depth())
print("Number of gates:", multi_state_circuit.n_gates)
Circuit depth: 21
Number of gates: 34
Here we have chosen the coefficients of both configurations \(c_1 = c_2 = 1/\sqrt{2}\), but any real values \(c_i\) are accepted as long as \(\sum_i |c_i|^2 = 1\).
When linearly combining more than two determinants, all rotations must end up in the desired Fock space sector.
This conserves spin and particle numbers, and prevents
unwanted configurations from entering the full expansion of the rotated state vector. One way to
accomplish this is to apply Givens rotations in a sequence corresponding to the order of input configurations (i.e.
the order of QubitStateString
objects used to instantiate
QubitState
). A Givens rotation at the \(j^{\text{th}}\) configuration in the sequence is
then controlled on certain qubits only if necessary; the necessity depending on whether it can also rotate any of the \(i<j\)
configurations. In InQuanto, this is implemented in a general way that guarantees the state of the circuit will be
the user-defined linear combination of configurations (with user-defined coefficients for
MultiConfigurationState
). However, the size of the circuits can increase significantly
when these extra controls are present. Consider adding a third configuration \(|1001\rangle\) to the example above,
such that the second Givens rotation to mix \(|1100\rangle\) and \(|1001\rangle\) can also act on the basis
state \(|0011\rangle\). This more than doubles the circuit size since that single excitation (second Givens
rotation) needs to be controlled on a qubit of the first configuration:
from inquanto.ansatzes import MultiConfigurationState
from inquanto.states import QubitState, QubitStateString
from pytket.extensions.qiskit import AerStateBackend
from math import sqrt
config1, c_1 = QubitStateString([1, 1, 0, 0]), 1/sqrt(3)
config2, c_2 = QubitStateString([0, 0, 1, 1]), 1/sqrt(3)
config3, c_3 = QubitStateString([1, 0, 0, 1]), 1/sqrt(3)
qubit_states = QubitState({config1: c_1, config2: c_2, config3: c_3,})
multi_state = MultiConfigurationState(qubit_states)
multi_state_circuit = AerStateBackend().get_compiled_circuit(multi_state.get_circuit())
print("Circuit depth:", multi_state_circuit.depth())
print("Number of gates:", multi_state_circuit.n_gates)
Circuit depth: 51
Number of gates: 76
However, adding a configuration with an associated Givens rotation that can not act on previous configurations (in the sense that the two Givens rotations act on disjoint qubit subspaces) leads to only a linear (with respect to the number of configurations) increase in circuit size. By keeping this in mind, the same state vector as above can be obtained with less gates by changing the order so that \(|1001\rangle\) is “before” \(|0011\rangle\), since no extra controls of the Givens rotations are needed in this in order:
from inquanto.ansatzes import MultiConfigurationState
from inquanto.states import QubitState, QubitStateString
from pytket.extensions.qiskit import AerStateBackend
from math import sqrt
config1, c_1 = QubitStateString([1, 1, 0, 0]), 1/sqrt(3)
config2, c_2 = QubitStateString([1, 0, 0, 1]), 1/sqrt(3)
config3, c_3 = QubitStateString([0, 0, 1, 1]), 1/sqrt(3)
qubit_states = QubitState({config1: c_1, config2: c_2, config3: c_3,})
multi_state = MultiConfigurationState(qubit_states)
multi_state_circuit = AerStateBackend().get_compiled_circuit(multi_state.get_circuit())
print("Circuit depth:", multi_state_circuit.depth())
print("Number of gates:", multi_state_circuit.n_gates)
Circuit depth: 25
Number of gates: 40
Basis states separated by more than two-body rotations (e.g. a rotation corresponding to a triples excitation) can also be linearly combined. For this, a sequence of multicontrolled SWAPs combined with a multicontrolled \(G_1\) rotation [49] is utilized. This multicontrolled “SWAP+rotation ladder” can represent a general chemical excitation to any order. However, again due to the use of extra controls, a large increase in circuit depth is observed when comparing a 2-body mixing to e.g. 3-body mixing. The following example shows this by first preparing \(|\psi \rangle = \frac{1}{\sqrt{2}}|111000\rangle + \frac{1}{\sqrt{2}}|100011\rangle\) (separated by a 2-body excitation), then preparing \(|\psi \rangle = \frac{1}{\sqrt{2}}|111000\rangle + \frac{1}{\sqrt{2}}|000111\rangle\) (separated by a 3-body excitation), and then comparing the size of the corresponding circuits:
from inquanto.ansatzes import MultiConfigurationState
from inquanto.states import QubitState, QubitStateString
from pytket.extensions.qiskit import AerStateBackend
from math import sqrt
backend = AerStateBackend()
config1, c_1 = QubitStateString([1, 1, 1, 0, 0, 0]), 1/sqrt(2)
config2, c_2 = QubitStateString([1, 0, 0, 0, 1, 1]), 1/sqrt(2)
qs_2bodyrot = QubitState({config1: c_1, config2: c_2})
ms_2bodyrot = MultiConfigurationState(qs_2bodyrot)
ms_circ_2bodyrot = backend.get_compiled_circuit(ms_2bodyrot.get_circuit())
print("Circuit depth, configs separated by 2-body excitation:", ms_circ_2bodyrot.depth())
print("Number of gates, configs separated by 2-body excitation:", ms_circ_2bodyrot.n_gates)
config1, c_1 = QubitStateString([1, 1, 1, 0, 0, 0]), 1/sqrt(2)
config2, c_2 = QubitStateString([0, 0, 0, 1, 1, 1]), 1/sqrt(2)
qs_3bodyrot = QubitState({config1: c_1, config2: c_2})
ms_3bodyrot = MultiConfigurationState(qs_3bodyrot)
ms_circ_3bodyrot = backend.get_compiled_circuit(ms_3bodyrot.get_circuit())
print("\nCircuit depth, configs separated by 3-body excitation:", ms_circ_3bodyrot.depth())
print("Number of gates, configs separated by 3-body excitation:", ms_circ_3bodyrot.n_gates)
Circuit depth, configs separated by 2-body excitation: 21
Number of gates, configs separated by 2-body excitation: 35
Circuit depth, configs separated by 3-body excitation: 321
Number of gates, configs separated by 3-body excitation: 347
How much the circuit size increases due to the presence of (multi)controls depends crucially on their decomposition to a particular gate set. The well-known approach based on recursive decomposition of Toffoli gates yields quadratic scaling with respect to the number of control qubits [51]. Currently, pytket utilizes a linear depth scaling approach for decomposing multicontrols [52] during circuit compilation for intermediate (\(6 \le n \le 50\)) numbers of control qubits. However, the user should in general be cautious of large circuits when mixing of a large number of basis configurations and/or mixing configurations that are separated by (\(N>2\))-body rotations.
Symbolic Multiconfiguration Ansatz
In all the examples shown above, the resulting ansatz object corresponds to a multiconfiguration state with fixed
coefficients. In the next example we show how to prepare an ansatz object with variational coefficients. For this we
use the InQuanto ansatz class MultiConfigurationAnsatz
. An instance of this class
can be used in an InQuanto computable which in turn can be plugged into an InQuanto algorithm in the usual ways
(see Algorithms and Computables sections) to optimize the coefficients and
obtain the ground state:
from inquanto.ansatzes import MultiConfigurationAnsatz
from inquanto.states import QubitStateString
qss_ref = QubitStateString([1, 1, 0, 0])
qss_2 = QubitStateString([0, 0, 1, 1])
qss_list = [qss_ref, qss_2]
ansatz = MultiConfigurationAnsatz(qss_list)
# |psi> = a|1100> + b|0011> , b = SQRT(1 - |a|^2) => only 1 parameter in VQE
print("Ansatz parameter info")
print("symbols:", ansatz.state_symbols.symbols)
print("N_parameters:", ansatz.n_symbols)
Ansatz parameter info
symbols: [theta0]
N_parameters: 1
Then, by optimizing the rotation angles of the Givens unitaries via VQE, a quantum circuit version of configuration interaction is achieved in which the determinants are selected a-priori followed by a variational optimization of their coefficients.
Note that MultiConfigurationAnsatz
takes a list of
QubitStateString
objects (representing Slater determinants) as its input argument. This
differs from MultiConfigurationState
which needs a QubitState
object containing occupations states and their coefficients (so that gate angles are calculated for a given set of
coefficients during instantiation of MultiConfigurationState
). On the other hand,
MultiConfigurationAnsatz
only needs occupation states to construct the variational ansatz
(in this case the rotation angles of the object returned by MultiConfigurationAnsatz
are
symbolic). However, the symbols representing these coefficients are related by normalization, hence in this example
there is only 1 variational parameter.
Multiconfiguration states without Givens rotations
In addition to MultiConfigurationState
which uses Givens rotations, a user can also prepare
arbitrary multiconfigurational states with fixed coefficients by utilizing pytket’s
StatePreparationBox via
InQuanto. This can be done using the InQuanto Ansatz class MultiConfigurationStateBox
,
which serves as a wrapper of
StatePreparationBox. The API
is similar to MultiConfigurationState
. The major differences between
MultiConfigurationState
and MultiConfigurationStateBox
is that
the latter does not use Given’s rotations specifically, but rather multiplexed \(R_y\) and \(R_z\) gates which
prepare a circuit from a statevector provided in the computational basis (see [53] for more details on the
circuit synthesis method). This means that, unlike in MultiConfigurationState
, for
MultiConfigurationStateBox
the input QubitState
(which specifies
the chemical occuptations) has to be converted to a computational statevector in numpy.ndarray
format,
before the circuit is built. This is akin to first solving an exponentially scaling classical problem before
preparing the state circuit. Hence, MultiConfigurationStateBox
is considered a tool for exploring
and debugging state preparation schemes, rather than a scalable state preparation method. An example showing its usage
is given below.
from inquanto.ansatzes import MultiConfigurationStateBox
from inquanto.states import QubitState, QubitStateString
from pytket.extensions.qiskit import AerStateBackend
from math import sqrt
config1, c_1 = QubitStateString([1, 1, 0, 0]), 1/sqrt(2)
config2, c_2 = QubitStateString([0, 0, 1, 1]), 1/sqrt(2)
qubit_states = QubitState({config1: c_1, config2: c_2})
multi_state = MultiConfigurationStateBox(qubit_states)
multi_state_circuit = AerStateBackend().get_compiled_circuit(multi_state.get_circuit())
print("Circuit depth:", multi_state_circuit.depth())
print("Number of gates:", multi_state_circuit.n_gates)
Circuit depth: 25
Number of gates: 28
Note that in MultiConfigurationStateBox
coefficients must be fixed and non-symbolic,
hence the use of this class to variationally optimize the configuration coefficients is currently not
supported.