Resources to compute the overlap between quantum states through random measurements.

Random unitary evolution

Performing random measurements on a state is equivalent to evolving it through random unitaries and, then, performing projective measurements on a state vector of a basis of choice. In this implementation, we take our reference state vector to be the $|0\rangle$ of the computational basis.

However, the first step is being able to randomly evolve states, for which we need to properly sample unitary operators from the Haar metric.

random_unitary_circuits[source]

random_unitary_circuits(n_rnd:int, n_qubits:int, d:Optional[int]=2, local:bool=True)

Samples n_rnd unitary random circuits with acting on n_qubits.

local indicates whether the random unitaries act at a local (local=True) or global (local=False) level. Global unitaries are much more costly to implement both at a numerical and experimental level. In terms of simulation, they require the explicit computation of $N\times N$ matrices, where $N$ is the size of the Hilbert space. In contrast, the operator resulting from the combination of local unitaries is expressed as the lazy computation of their tensor product, which is a list of n_qubits elements of size $d\times d$, where d is the size of the local Hilbert space.

n_qubits = 2
loc = random_unitary_circuits(1, n_qubits)[0]
glob = random_unitary_circuits(1, n_qubits, local=False)[0]
print(loc)
print(glob)
TensoredOp([
  Operator([[ 0.07099579+0.32903812j,  0.75676771+0.56035358j],
            [ 0.94076065+0.04077892j, -0.26224586+0.21102985j]],
           input_dims=(2,), output_dims=(2,)),
  Operator([[-0.30514745-0.1707145j ,  0.93310157+0.08404199j],
            [ 0.91508156+0.20091623j,  0.34239631-0.07087433j]],
           input_dims=(2,), output_dims=(2,))
])
Operator([[-0.34752919+0.56203462j,  0.08680883-0.41629036j,
           -0.03508419+0.18996298j, -0.21437868+0.54702107j],
          [ 0.37249939-0.0753213j ,  0.02126428+0.10429014j,
           -0.22450534-0.66745055j, -0.3635745 +0.46493333j],
          [ 0.01154624-0.41187321j,  0.44426806-0.09316649j,
            0.65389342+0.00971268j,  0.18810724+0.40139479j],
          [-0.08891875-0.4911538j , -0.70610247-0.32125248j,
            0.11540848+0.16062449j, -0.2948111 +0.15180454j]],
         input_dims=(2, 2), output_dims=(2, 2))

In terms of actual execution on a quantum hardware, the performance of local unitaries is straightforward, whereas global unitaries must be decomposed into a series of local operations.

loc.to_circuit_op().primitive.draw('mpl')
glob.to_circuit_op().primitive.draw('mpl')

Overlap through randomized measurements

The overlap between quantum states through random measurements can be performed with either local or global unitaries setting local accordingly. The method mainly consists on sampling a set of random unitaries $U$ and, then, evolving and measuring the states of interest. This way, the probability $P(s)$ of measuring a given state $|s\rangle$ after a random unitary evolution $U$ of our initial state $\rho$ is $P(s) = \text{Tr}\left[U\rho U^\dagger|s\rangle\langle s|\right]$ and its expected value over all possible random measurements is $\langle P(s)\rangle=1/N$.

The expectation of the probability $P(s)$ over all possible random measurements is $\langle P(s)\rangle=1/N$. Measuring higher moments of $P(s)$, i. e. $\langle P(s)^n\rangle$, we can infer the value of $\text{Tr}\left[\rho^n\right]$ [1]. In this application case, we are mostly interested in computing the second moment $\langle P(s)^2\rangle$, from which we can infer $\text{Tr}\left[\rho^2\right]$. This is not restricted to purities, as we can compute the expectation $\langle P_0(s)P_1(s)\rangle$ from statistics obtained measuring $\rho_0, \ \rho_1$ over the same set of random unitaries $U$.

With global unitaries, the computation is rather straightforward. We can choose any vector $s$ of the computational basis to compute the statstics from. For convenience we have taken the zero state as our observable ~Zero. Then, $$\text{Tr}\left[\rho_0\rho_1\right]=N(N+1)\langle P_0(s)P_1(s)\rangle - 1.$$ The main issue, as discussed above, is the execution of such global operators in the quantum circuits.

On the other hand, local unitaries are much easier to implement in a quantum computer. Nevertheless, the posterior analysis requires the estimation of the statistics over all the states in the computational basis, rather than just a single one. Then, as derived in [2] the overlap between two quantum states may be obtained as $$\text{Tr}\left[\rho_0\rho_1\right]=N\sum_{s,s'}(-d)^{-D[s,s']}\langle P_0(s)P_1(s')\rangle,$$ where the sum goes over all pairs of states $s,s'$ in the computational basis, $d$ is the size of the local Hilbert space and $D[s,s']$ is the Hamming distance between states $s,s'$.

Hence, both approaches present a tradeoff between overloading the quantum (global unitaries) or the classical (local unitaries) hardware. See [3] for further detail. In terms of the API, the behaviour of the function is exactly the same besides the local optional argument .

randomized_measurement_overlap[source]

randomized_measurement_overlap(state0:Union[StateFn, QuantumCircuit], state1:Union[StateFn, ListOp, NoneType]=None, param_dict:Optional[Dict[ParameterExpression, List[float]]]=None, n_rnd:Optional[int]=None, local:bool=True, expectation:Optional[ExpectationBase]=None, backend:Union[Backend, QuantumInstance, NoneType]=None)

Overlap computation between states using randomized measurements.

Overlap estimators, such as randomized_measurement_overlap, are mainly intended to work with StateFn and ListOp objects. However, we provide compatibility with state0 in QuantumCircuit with the idea to cover quick purity estimations straight from the circuits.

The bread-and-butter calls and applications of these methods are all detailed in the basic usage.

In order to illustrate its behaviour, we first need to create a parameterized quantum circuit.

theta0, theta1 = Parameter('θ0'), Parameter('θ1')
qc = QuantumCircuit(2)
qc.h(0)
qc.rx(theta0, 0)
qc.rx(theta1, 1)
qc.draw('mpl')

As previously mentioned, these overlap computers operate with StateFn so we will call CircuitStateFn on the circuit to create our state. If a Backend or QuantumInstance is not provided, everything is computed analytically with operator flow. Otherwise, the resulting observables are internally converted with a CircuitSampler built from the backend. Providing an Expectation converts the observable prior to the CircuitSampler.

state = CircuitStateFn(qc)
expectation = PauliExpectation()
backend = BasicAer.get_backend('qasm_simulator')
qi = QuantumInstance(backend, shots=1000)

Now we can, for instance, compare the effect of using local or global unitaries. Let's first define a set of parameters to evaluate the overlap across their respective states.

param_dict = {theta0: [0, 0.1, 0.1], 
              theta1: [0, 0.1, 1.5]}
overlap = randomized_measurement_overlap(state, param_dict=param_dict, n_rnd=500, 
                                         local=False, expectation=expectation, backend=qi)
overlap
array([[0.95660219, 0.94857493, 0.46670327],
       [0.94857493, 0.95362864, 0.51531025],
       [0.46670327, 0.51531025, 1.00551041]])
overlap = randomized_measurement_overlap(state, param_dict=param_dict, n_rnd=500,
                                         local=True, expectation=expectation, backend=qi)
overlap
array([[0.96025352, 0.95692673, 0.54417865],
       [0.95692673, 0.96440688, 0.5944077 ],
       [0.54417865, 0.5944077 , 0.99312741]])

In terms of the result, we do not appreciate any substantial difference between both approaches. The execution performance, however, depends strongly on the backends, as previously discussed.

state0 = state.bind_parameters({theta0: 0., theta1: 0.5})
state1 = state.bind_parameters({theta0: [0.2, 0.5], theta1: [1.5, 0.5]})

overlap = randomized_measurement_overlap(state0, state1, n_rnd=200)
overlap
array([0.76451921, 0.97574972])
purity = randomized_measurement_overlap(state0)
purity

Overlap across backends

A major advantage of the random measurement protocol is that it is device independent, so long as we find the right way to define the unitary transformations across platforms. This allows us to perform the random measurements independently in separate devices and then infer the overlap between the states from the classical statistics.

device_independent_overlap[source]

device_independent_overlap(state0:StateFn, backends:Union[QuantumInstance, Backend, Iterable[Union[QuantumInstance, Backend]]], state1:Union[StateFn, ListOp, List[StateFn], NoneType]=None, n_rnd:Optional[int]=None, local:bool=True)

Computes overlap between states through randomized measurements using different backends.

We can perform a wide range of operations with device_independent_overlap. It has a similar behaviour to that of the other overlap computation functions with the main difference being that it does not accept parameter-dependent states.

The most basic functionality is the calculation of the purity of a state in various backends. This is done by providing a single state0 input and a collection of backends.

qc = QuantumCircuit(1)
qc.h(0)
qc.draw('mpl')
state = CircuitStateFn(qc)
backends = [QasmSimulator(), QasmSimulator.from_backend(FakeVigo())]
purities = device_independent_overlap(state, backends)
purities
array([[0.99763748, 0.92217925],
       [0.92217925, 0.85981494]])

The outputs follow the same phylosophy as when randomized_measurement_overlap is provided with a param_dict: return the overlap between everything. Hence, the diagonal always contains the purity of each state in its backend.

As an optional input, it accepts a set of states state1 with which the overlaps are computed. In this case, it distributes the provided backends in a rather intuitive way following two main principles:

  • If there is no backend for state0 it loads a StatevectorSimulator.
  • state0 always takes the first backend.

Therefore, we identify the following cases as function of the amount of provided backends:

  • A single backend: state0 is simulated and all states state1 run in the backend.
  • Two backends: state0 runs on the first, all state1 run on the second.
  • As many backends as state1: state0 is simulated and state1 run each on the backends.
  • state0 + state1 backends: state0 runs on the first, state1 states run on the rest.