Protocols for overlaps
The Overlap
of the two quantum states \(\ket{\Psi_0}\) and \(\ket{\Psi_1}\) with a kernel \(\hat{A}\) is, in general, a complex number given by
where \(\hat{A}\) is a QubitOperator
i.e. it is written as a linear combination of Pauli strings \(\hat{A} = \sum_i c_i P_i\), and may be an identity.
InQuanto supports shot-based calculations of overlaps with the HadamardTestOverlap
, SwapFactorizedOverlap
, and ComputeUncomputeFactorizedOverlap
protocols, discussed below.
HadamardTestOverlap
Let \(\ket{\Psi_0} = U_0 \ket{\bar{0}}\) and \(\ket{\Psi_1} = U_1 \ket{\bar{0}}\), where \(U_0\) and \(U_1\) are state preparation unitaries.
The HadamardTestOverlap
protocol uses a “linear combination of unitaries” approach with a single ancilla on which to control the state preparation unitaries [30].
This protocol offers two measurement options: direct=False
, where measurement is performed on the ancilla qubit only, and direct=True
, where measurement is performed on both
the ancilla and state registers [31]. First, we discuss the direct=False
case.
To calculate the real part of the overlap, \(\text{Re}\langle \Psi_0 | \Psi_1 \rangle\), a single circuit is required which takes the form:
This circuit prepares the quantum state:
where the first ket in each term is the ancilla, and \(|\Psi_0 \pm \Psi_1\rangle = (U_0 \pm U_1)|\bar{0}\rangle\) is the state register. Given this state, the real part of the overlap is given by \(\text{Re}\langle \Psi_0 | \Psi_1 \rangle = p(0) - p(1)\), where \(p(b)\) is the probability of measuring the ancilla qubit in the state \(b\). To compute the imaginary part of the overlap, a similar circuit is required with a small modification compared to the circuit above:
and the imaginary part is given equivalently by \(\text{Im}\langle \Psi_0 | \Psi_1 \rangle = p(0) - p(1)\).
For an overlap with a kernel \(\hat{A}=\sum_i c_i P_i\), we may write:
where each Pauli word has been appended to the \(| \Psi_1 \rangle\) state preparation; \(| \Psi_1^i \rangle = P_i U_1 |\bar{0}\rangle\). Each term in this sum is then computed
independently as described above. Thus, with direct=False
, to compute the complex overlap with a kernel of \(N\) terms, \(2N\) circuits are required.
In the direct=True
case, the Pauli words in \(\hat{A}\) are partitioned into simultaneously measurable sets (commuting sets, for example). Measurement circuits for each set
are then appended to the end of the state register, similarly to the Pauli averaging protocol. These circuits take the form:
In this case, measurement of the state register measures \(\langle \Psi_0 + \Psi_1 | P_i | \Psi_0 + \Psi_1 \rangle\) when the ancilla is \(|0\rangle\), and
\(\langle \Psi_0 - \Psi_1 | P_i | \Psi_0 - \Psi_1 \rangle\) when the ancilla is \(|1\rangle\). By taking a linear combination of these outcomes we can retrieve
\(\langle \Psi_0 | P_i | \Psi_1\rangle\). Thus, with direct=True
, to compute the complex overlap with a kernel we require \(2N_p\) circuits, where \(N_p\) is
the number of simultaneously measurable sets of Pauli words in the kernel.
A simple example of using this protocol is given below:
from inquanto.operators import QubitOperator
from inquanto.states import QubitState, FermionState
from inquanto.ansatzes import FermionSpaceAnsatzUCCSD, HardwareEfficientAnsatz
from inquanto.computables import Overlap
from inquanto.protocols import HadamardTestOverlap
from pytket import OpType
from pytket.extensions.qiskit import AerBackend
from pytket.partition import PauliPartitionStrat
bra = HardwareEfficientAnsatz([OpType.Rx, OpType.Ry], QubitState([1, 1, 0, 0]), 2)
ket = FermionSpaceAnsatzUCCSD(4, FermionState([1, 1, 0, 0], 1))
params = (
bra.state_symbols.construct_random().to_dict()
| ket.state_symbols.construct_random().to_dict()
)
kernel = QubitOperator.from_string("(-0.1, Z0), (0.1, Z1), (0.25, X0 X1)")
ovlp = Overlap(bra, ket, kernel)
protocol = HadamardTestOverlap(
AerBackend(),
shots_per_circuit=int(12e3),
direct=True,
pauli_partition_strategy=PauliPartitionStrat.CommutingSets
)
protocol.build_from(params, ovlp)
protocol.run(seed=0)
circs = protocol.get_circuits()
print("Circuit count: ", len(circs))
ovlp.evaluate(protocol.get_evaluator())
Circuit count: 4
(-0.051508333333333337+0.030033333333333332j)
FactorizedOverlap
Protocols of the abstract type FactorizedOverlap
work on pairs of ansatzes that satisfy the following conditions:
Reference preparation can be factorized out from both state preparation unitaries i.e. \(\ket{\Psi_0} = U_0 U_\text{ref}\ket{\bar{0}}\) and \(\ket{\Psi_1} = U_1 U_\text{ref}\ket{\bar{0}}\)
Both ansatzes have the same reference state preparation circuit \(U_\text{ref}\)
Non-reference factors \(U_0\) and \(U_1\) in the state preparation unitaries yield \(\ket{\bar{0}}\) when acting on \(\ket{\bar{0}}\); i.e. \(U_0\ket{\bar{0}} = U_1\ket{\bar{0}} = \ket{\bar{0}}\).
Exploitation of these properties admits more efficient overlap measurement circuits that avoid application of a control to the entire state preparations as is done in HadamardTestOverlap
.
Rather, only the reference parts of the state preparations must be controlled by an ancilla qubit.
The \(U_0\ket{\bar{0}} = U_1\ket{\bar{0}} = \ket{\bar{0}}\) property is enforced by raising a TypeError
upon preparation of the protocol circuits if either of the specified ansatzes is of a type that is not guaranteed to satisfy it.
All ansatz classes derived from FermionSpaceStateExp
and FermionSpaceStateExpChemicallyAware
are compatible with FactorizedOverlap
.
These are:
Note
The chemically aware ansatzes are only supported when both bra and ket state are chemically aware. The reason for this is that the chemically aware ansatz reference circuits are in spatial orbital Jordan–Wigner encoding, and so the reference state preparation circuit is not identical to the spin-orbital Jordan–Wigner encoded reference of the non-chemically aware ansatz, even if both ansatzes share the same fermionic reference state. See here for more details.
Currently, two classes derived from FactorizedOverlap
type are implemented: SwapFactorizedOverlap
and ComputeUncomputeFactorizedOverlap
.
These correspond to the two overlap measurement circuits presented in [32].
SwapFactorizedOverlap
This approach introduces an ancillary state register (lower in figure) to prepare the same linear combinations on the upper state register as in the case of the HadamardTestOverlap
.
Since this final state is identical to that of HadamardTestOverlap
, SwapFactorizedOverlap
is compatible with direct
operator averaging as in the circuit shown below:
A simple example usage of the direct SwapFactorizedOverlap
protocol is given below.
from inquanto.operators import QubitOperator
from inquanto.states import FermionState
from inquanto.ansatzes import FermionSpaceAnsatzkUpCCGSD
from inquanto.computables import Overlap
from inquanto.protocols import SwapFactorizedOverlap
from pytket.extensions.qiskit import AerBackend
from pytket.partition import PauliPartitionStrat
bra = FermionSpaceAnsatzkUpCCGSD(4, FermionState([1, 1, 0, 0], 1), 2)
ket = FermionSpaceAnsatzkUpCCGSD(4, FermionState([1, 1, 0, 0], 1), 2)
bra.symbol_substitution('{}_bra')
ket.symbol_substitution('{}_ket')
params = (
bra.state_symbols.construct_random(seed=1).to_dict()
| ket.state_symbols.construct_random(seed=2).to_dict()
)
kernel = QubitOperator.from_string("(-0.1, Z0), (0.1, Z1), (0.25, X0 X1)")
ovlp = Overlap(bra, ket, kernel)
protocol = SwapFactorizedOverlap(
AerBackend(),
shots_per_circuit=int(12e3),
direct=True,
pauli_partition_strategy=PauliPartitionStrat.CommutingSets
)
protocol.build_from(params, ovlp)
protocol.run(seed=0)
circs = protocol.get_circuits()
print("Circuit count: ", len(circs))
ovlp.evaluate(protocol.get_evaluator())
Circuit count: 4
(-0.040783333333333345+0.0032333333333333337j)
Indirect measurement is also possible with this protocol, for which the required Pauli words are appended to the ket state preparation as shown below.
A simple example usage of the indirect SwapFactorizedOverlap
protocol is given below.
from inquanto.operators import QubitOperator
from inquanto.states import FermionState
from inquanto.ansatzes import FermionSpaceAnsatzkUpCCGSD
from inquanto.computables import Overlap
from inquanto.protocols import SwapFactorizedOverlap
from pytket.extensions.qiskit import AerBackend
bra = FermionSpaceAnsatzkUpCCGSD(4, FermionState([1, 1, 0, 0], 1), 2)
ket = FermionSpaceAnsatzkUpCCGSD(4, FermionState([1, 1, 0, 0], 1), 2)
bra.symbol_substitution('{}_bra')
ket.symbol_substitution('{}_ket')
params = (
bra.state_symbols.construct_random(seed=1).to_dict()
| ket.state_symbols.construct_random(seed=2).to_dict()
)
kernel = QubitOperator.from_string("(-0.1, Z0), (0.1, Z1), (0.25, X0 X1)")
ovlp = Overlap(bra, ket, kernel)
protocol = SwapFactorizedOverlap(AerBackend(), shots_per_circuit=int(12e3), direct=False)
protocol.build_from(params, ovlp)
protocol.run(seed=0)
circs = protocol.get_circuits()
print("Circuit count: ", len(circs))
ovlp.evaluate(protocol.get_evaluator())
Circuit count: 6
(-0.04296666666666668-0.000683333333333333j)
ComputeUncomputeFactorizedOverlap
An alternative protocol in the FactorizedOverlap
family is ComputeUncomputeFactorizedOverlap
, which takes inspiration from the ComputeUncompute
method for overlaps squared,
and prepares the state:
This is accomplished by the following circuit:
Which has the advantage of fewer qubits than SwapFactorizedOverlap
, requiring only a single state register and one ancilla qubit, however it does produce deeper circuits.
Since projection of Pauli words on the final state does not correspond to terms in the matrix element of the kernel, it is not possible to use this protocol in conjunction with the direct
operator averaging scheme described above.
A simple example usage of the direct ComputeUncomputeFactorizedOverlap
protocol is given below.
from inquanto.operators import QubitOperator
from inquanto.states import FermionState
from inquanto.ansatzes import FermionSpaceAnsatzkUpCCGSD
from inquanto.computables import Overlap
from inquanto.protocols import ComputeUncomputeFactorizedOverlap
from pytket.extensions.qiskit import AerBackend
bra = FermionSpaceAnsatzkUpCCGSD(4, FermionState([1, 1, 0, 0], 1), 2)
ket = FermionSpaceAnsatzkUpCCGSD(4, FermionState([1, 1, 0, 0], 1), 2)
bra.symbol_substitution('{}_bra')
ket.symbol_substitution('{}_ket')
params = (
bra.state_symbols.construct_random(seed=1).to_dict()
| ket.state_symbols.construct_random(seed=2).to_dict()
)
kernel = QubitOperator.from_string("(-0.1, Z0), (0.1, Z1), (0.25, X0 X1)")
ovlp = Overlap(bra, ket, kernel)
protocol = ComputeUncomputeFactorizedOverlap(AerBackend(), shots_per_circuit=int(12e3))
protocol.build_from(params, ovlp)
protocol.run(seed=0)
circs = protocol.get_circuits()
print("Circuit count: ", len(circs))
ovlp.evaluate(protocol.get_evaluator())
Circuit count: 6
(-0.04065000000000001+0.004800000000000016j)