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 [48]. 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

(97)\[\begin{split}G_1(\theta) = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & \cos(\theta) & -\sin(\theta) & 0 \\ 0 & \sin(\theta) & \cos(\theta) & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}\end{split}\]

which corresponds to a rotation between the \(|10\rangle\) and \(|01\rangle\) basis states. This has a straight-forward generalization to \(n\) qubits [48]. 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)

(98)\[\begin{split}G_2(\theta)|1100\rangle = \cos(\theta)|1100\rangle + \sin(\theta)|0011\rangle \\ G_2(\theta)|0011\rangle = \cos(\theta)|0011\rangle - \sin(\theta)|1100\rangle\end{split}\]

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\) [48, 49] 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 [48] 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 [50]. Currently, pytket utilizes a linear depth scaling approach for decomposing multicontrols [51] 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 [52] 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.