Skip to main content

Quantum Circuit Compilation

Quantum Circuit Compilation is a technique that aims to reduce the depth of certain quantum circuits by approximating them by fixed depth variational circuits. A well-known example of this is quantum assisted quantum compiling (QAQC). This is available in QURI Algo through our compilation module, which will be introduced through this notebook.

Overview

In this notebook we will go through

  • Some background of QAQC
  • How to use the Hilbert-Schmidt cost function
  • How to use the QURI Algo implementation of QAQC

QAQC

The objective of QAQC is to approximate a known quantum circuit which is understood to be too deep by a low-depth variational circuit V(θ)V(\theta). Ideally the depth of V(θ)V(\theta) is O(Nq)\mathcal{O}(N_q) or O(logNq)\mathcal{O}(\log N_q), which is achievable with either brick-wall or tensor-network inspired ansatze. A cost-function is needed, that when minimized ensures that UN(t)=eiϕV(θ)U_N(t) = e^{i\phi} V(\theta) for some global phase ϕ\phi.

We can use the Hilbert-Schmidt (HS) test for this which is

CHST(U,V)=114NqTr[UV]2. C_\textup{HST}(U,V) = 1 - \frac{1}{4^{N_q}} |\textup{Tr}[U^\dagger V]|^2.

The advantage of this is that it can be evaluated on quantum hardware by sampling a 2Nq2N_q qubit wavefunction. This has implications for QPE where multi-qubit controlled time-evolution is performed several times. Although the 2Nq2N_q requirement likely makes the optimization untractable using classical computing, running the optimization using a quantum device requires little additional overhead compared to QPE and has the potential to reduce the run-time of QPE dramatically.

To summarize it briefly, the estimation is done on a 2Nq2 N_q qubit quantum register consisting of NqN_q-qubit subsystems AA and BB. The system has initially been prepared as a product of Bell states defined by pairs of qubits belonging to AA and BB. Denoting by AjA_j and BjB_j the jj'th qubit on subsystem AA and BB respectively. We write

ΦAj,Bj=12(00+11)Aj,Bj. \ket{\Phi}_{A_j,B_j} = \frac{1}{\sqrt{2}}(\ket{00} + \ket{11})_{A_j, B_j}.

Then the initial state is

ΦA,B=j=1NqΦAj,Bj. \ket{\Phi}_{A,B} = \bigotimes_{j=1}^{N_q} \ket{\Phi}_{A_j,B_j}.

Then UU and VV^* are applied to the AA and BB subsystem respectively. The resulting state density matrix is

ρAB(U,V)=(UAVB)ΦA,BΦA,B(UAVB). \rho_{AB}(U,V) = (U_A\otimes V_B^*)\ket{\Phi}_{A,B}\bra{\Phi}_{A,B}(U_A\otimes V_B^*)^\dagger.

Then measurements are conducted in the basis of pair-wise Bell states ΦAj,Bj\ket{\Phi}_{A_j,B_j}. Resultingly, this gives us the HS test because

14NqTr[UV]2=Tr[Π1Π2ΠNqρAB(U,V)]. \frac{1}{4^{N_q}} |\textup{Tr}[U^\dagger V]|^2 = \textup{Tr}[\Pi_1\Pi_2\ldots\Pi_{N_q}\rho_{AB}(U,V)].

Here the Πi\Pi_i operators are

Πi=ΦAj,BjΦAj,Bj. \Pi_i = \ket{\Phi}_{A_j,B_j}\bra{\Phi}_{A_j,B_j}.

Another variant is the local HS (LHS) test, which is defined as

CLHST(U,V)=11NqjNqCLHST(j)(U,V) C_\textup{LHST}(U,V) = 1 - \frac{1}{{N_q}} \sum_j^{N_q} C^{(j)}_\textup{LHST}(U,V)

with

CLHST(j)(U,V)=1Tr[ΠjρAB(U,V)]. C^{(j)}_\textup{LHST}(U,V) = 1 - \textup{Tr}[\Pi_j\rho_{AB}(U,V)].

We provide the hybrid cost-function

Cα(U,V)=αCHST(U,V)+(1α)CLHST(U,V) C_\alpha(U,V) = \alpha C_\textup{HST}(U,V) + (1-\alpha) C_\textup{LHST}(U,V)

When running either the HS or LHS tests the measurements required are the same. For this reason, whenever a series of measurements are performed that result in the HS test, those same measurements if averaged over single Bell pairs could be used to simultaneously compute the LHS test. Therefore the above cost-function is no more expensive than either of its parts. There are certain situations where the LHS results in an optimization landscape that is easier for gradient based optimizers to navigate. This is because the inner product between UU and some randomly chosen VV is exponentially suppressed due to the scaling of the Hilbert space. The LHS test avoids this issue by weighting the local cost of each pair of qubits equally. So for large systems it is worthwhile to use α<1\alpha < 1 .

Cost function

In this section we will show how to use the HS test. Although the objective of this notebook is to use it for circuit compilation, it is a rather versatile tool, and so when introducing it we will use it to investigate the Trotter time-evolution circuit. By the end of this section we will have experimented with several different Trotter steps and picked one that is faithful to the exact dynamics of our system.

First it needs to be imported from the cost_functions module. It needs to be initialized with a quantum compilation backend that will take care of the estimation functions for us. This will use an estimator from QURI Parts.

from quri_parts.qulacs.estimator import create_qulacs_vector_estimator

from quri_algo.core.cost_functions import HilbertSchmidtTest

hstest = HilbertSchmidtTest(
create_qulacs_vector_estimator(), alpha=1.0
) # alpha=1.0 is the default.

Let's test it out.

import numpy as np

from quri_parts.circuit import QuantumCircuit

test_0 = QuantumCircuit(2)
test_0.add_RZ_gate(0, np.pi)
test_0.add_RX_gate(1, np.pi)
test_1 = QuantumCircuit(2)
test_1.add_RZ_gate(0, 0.5 * np.pi)
test_1.add_RX_gate(1, 0.5 * np.pi)

result = hstest(
test_0, test_1, alpha=0.7
) # When calling hstest we can supply an alpha that's different from the one we initialized.
print("The result from HS test is", result)
#output
The result from HS test is _Estimate(value=(0.6749999999999999+0j), error=0.0)

We can use a time-evolution circuit factory and generate a Trotterized time evolution based on a simple Hamiltonian. Here we pick the Heisenberg interaction on a spin-1/2 lattice with periodic boundary conditions. We will compute and plot

f(t)=CHST(U(0),U(t))f(t) = C_\textup{HST}(U(0),U(t))

with UU being either the exact time-evolution operator or the one generated by Trotterization.

import numpy as np
import matplotlib.pyplot as plt

from numpy import linspace

from quri_parts.core.operator import Operator, pauli_label
from quri_algo.circuit.time_evolution.trotter_time_evo import (
TrotterTimeEvolutionCircuitFactory,
)
from quri_algo.circuit.time_evolution.exact_unitary import (
ExactUnitaryTimeEvolutionCircuitFactory,
)
from quri_algo.problem.hamiltonian import QubitHamiltonianInput

s = 1 / 2
j = 1.0

heisenberg = Operator(
{
pauli_label("X0 X1"): j * s**2,
pauli_label("X1 X2"): j * s**2,
pauli_label("X2 X3"): j * s**2,
pauli_label("X3 X0"): j * s**2,
pauli_label("Y0 Y1"): j * s**2,
pauli_label("Y1 Y2"): j * s**2,
pauli_label("Y2 Y3"): j * s**2,
pauli_label("Y3 Y0"): j * s**2,
pauli_label("Z0 Z1"): j * s**2,
pauli_label("Z1 Z2"): j * s**2,
pauli_label("Z2 Z3"): j * s**2,
pauli_label("Z3 Z0"): j * s**2,
}
)
heisenberg_input = QubitHamiltonianInput(4, heisenberg)
circuit_factory = TrotterTimeEvolutionCircuitFactory(heisenberg_input, 1)
exact_time_evolution_factory = ExactUnitaryTimeEvolutionCircuitFactory(heisenberg_input)

start_circuit = circuit_factory(0.0)
evo_times = linspace(0, 4 * np.pi, 51)
hs_result_trotter = [
hstest(start_circuit, circuit_factory(time), alpha=0.5).value.real
for time in evo_times
]
hs_result_exact = [
hstest(start_circuit, exact_time_evolution_factory(time), alpha=0.5).value.real
for time in evo_times
]


ax = plt.axes()

ax.plot(evo_times, hs_result_trotter)
ax.plot(evo_times, hs_result_exact)
ax.set_title("Hilbert Schmidt test")
ax.set_xlabel("t")
ax.set_ylabel("C(U(0),U(t))")
ax.set_ybound(0.0, 1.05)
ax.legend(["Trotter", "Exact"])

png

With just a single Trotter step, there seems to be some discrepancy in the time-evolution. From the above result we cannot tell how accurate the Trotterization is for this system, what the result does tell us that the Hilbert Schmidt cost function does not agree entirely with the exact time-evolution. However, interestingly the Trotterized time-evolution operator shows the same periodicity in tt as its exact counterpart. When the Hilbert Schmidt cost function reaches 0 the two input unitaries are equivalent, which shows that the time evolution operator has undergone a full period of time-evolution. This is in fact not too surprising since the Trotterized time-evolution operator is a product of unitaries, each of which is parametrized with respect to tt displaying the same periodicity. To see how accurate the Trotterized time evolution operator itself is, we would need to calculate C(Uexact(t),UTrotter(t))C(U_\textup{exact}(t),U_\textup{Trotter}(t)).

evo_times = linspace(0, 4 * np.pi, 51)

ax = plt.axes()

max_trotter = 5
hs_results_trotter = []
legend = []
for i in range(1, max_trotter + 1):
circuit_factory = TrotterTimeEvolutionCircuitFactory(heisenberg_input, i)
hs_result_trotter = [
hstest(exact_time_evolution_factory(time), circuit_factory(time)).value.real
for time in evo_times
]

ax.plot(evo_times, hs_result_trotter, "--")
legend.append(str(i) + "-step Trotter ")

ax.set_title("Hilbert Schmidt test")
ax.set_xlabel("t")
ax.set_ylabel("C(V(t),U(t))")
ax.set_ybound(0.0, 1.05)
ax.legend(legend)

png

Interestingly there does not seem to be any significant improvement in the range of 1 - 5 Trotter steps. However, for the trend is that a higher number of Trotter steps results in more accurate time-evolution for small tt. Let's go higher

evo_times = linspace(0, 4 * np.pi, 51)

ax = plt.axes()

trotter_steps = [10, 20, 30, 40, 50]
hs_results_trotter = []
legend = []
for i in trotter_steps:
circuit_factory = TrotterTimeEvolutionCircuitFactory(heisenberg_input, i)
hs_result_trotter = [
hstest(exact_time_evolution_factory(time), circuit_factory(time)).value.real
for time in evo_times
]

ax.plot(evo_times, hs_result_trotter, "--")
legend.append(str(i) + "-step Trotter ")

ax.set_title("Hilbert Schmidt test")
ax.set_xlabel("t")
ax.set_ylabel("C(V(t),U(t))")
ax.set_ybound(0.0, 1.05)
ax.legend(legend)

png

We see the same trend repeated again. For nTrotter=50n_\textup{Trotter} = 50, the time-evolution should be rather faithful in the range 0<t2π0 < t \leq 2\pi which makes up a single period of the repeating dynamics. Finally, let's experiment with the circuit compilation itself

Quantum compilation

The QAQC compiler itself needs to be initialized using an optimizer and a cost function. Below we will instantiate one with an ideal estimator.

from quri_parts.algo.optimizer import Adam

from quri_algo.algo.compiler import QAQC

NSHOTS = 10000
ALPHA = 1.0

ideal_estimator = create_qulacs_vector_estimator()
optimizer = Adam()
ideal_cost_fn = HilbertSchmidtTest(ideal_estimator, alpha=ALPHA)

ideal_compiler = QAQC(ideal_cost_fn, optimizer)

Now to run it, we need to provide a circuit factory. A circuit factory is some callable object that based on the call argument returns a QuantumCircuit. We can use the TrotterTimeEvolutionCircuitFactory that we employed earlier. We will also need a variational ansatz, for this we can pick any ansatz in principle. To keep things simple we use the HardwareEfficientReal ansatz

from quri_parts.algo.ansatz import (
HardwareEfficient,
build_entangler_map,
EntanglementPatternType,
)

REPS = 8
time = np.pi / 20

entangler_map = build_entangler_map(
heisenberg_input.n_state_qubit, [EntanglementPatternType.CIRCULAR] * REPS
) # Building a circular entangler map will map to a problem with a perdiodic boundary
ansatz = HardwareEfficient(
heisenberg_input.n_state_qubit, REPS, entangler_map_seq=entangler_map
)
circuit_factory = TrotterTimeEvolutionCircuitFactory(heisenberg_input, 50)

ideal_compiled = ideal_compiler(
circuit_factory, ansatz, evolution_time=time
) # Keyword arguments get passed to the circuit factory, which allows us to pass quite general circuit factories.

print(
"The HS cost of the compiled circuit is:",
hstest(circuit_factory(time), ideal_compiled).value.real,
)
#output
The HS cost of the compiled circuit is: 0.018348033485234394

It seem like we can compile circuits to a rather shallow depth. Comparison of the circuit depths yields

print(
f"The depth of the Trotterized time-evolution with HS cost {hstest(exact_time_evolution_factory(time),circuit_factory(time)).value.real} is {circuit_factory(time).depth}"
)
print(
f"The depth of the compiled time-evolution with HS cost {hstest(exact_time_evolution_factory(time),ideal_compiled).value.real} is {ideal_compiled.depth}"
)
#output
The depth of the Trotterized time-evolution with HS cost 2.245753150109664e-08 is 600
The depth of the compiled time-evolution with HS cost 0.018348027830480215 is 34

So we are able to trade quantum circuit faithfullness for a dramatic reduction in circuit depth. For NISQ or EFTQC devices the above difference in depth easily makes the difference between whether a circuit can be executed on quantum hardware or not.

Summary

In this notebook we

  • Introduced quantum compilation with an emphasis on QAQC and the HS test
  • Showed how to execute the HS test in QURI Algo to evaluate quantum circuits in terms of their faithfulness to exact time-evolution
  • Ran QAQC on a simple example with the spin-1/2 Heisenberg model on 4 spins with a periodic boundary

QAQC is a powerful technique, but it's rather slow, and the optimization itself is difficult. Evaluating the HS cost function is expensive as the number of qubits needed is twice that of the circuits being compared. There are solutions to this however, notably if the dynamics are only accounted for within the Lieb-Robinson bound, in which cases the local dynamics only are taken into account. This results in the local variational quantum compilation algorithm, which we provide in our proprietary version of QURI Algo.

Further study

Now that you have familiarized yourself with QAQC try running larger simulations, or swap out the ideal estimator for a noisy one.