Parameterized quantum circuits [Experimental]¶
Variational circuits are a class of circuits which are composed of continuously parameterized gates. These parameters are generally optimized to meet a certain functionality, such as generating a target output quantum state. Such variational circuits are attractive in the NISQ era, where quantum devices are not error-corrected and limited to shallow (i.e., few-gate) circuits.
We use JAX and its ecosystem of associated libraries, such as Optax, for automatically differentiation our circuits and optimzation via a gradient descent algorithm.
import matplotlib.pyplot as plt
import tqdm
from graphiq.circuit import ops
from graphiq.circuit.circuit_dag import CircuitDAG
import graphiq.backends.density_matrix.functions as dmf
from graphiq.backends.density_matrix.compiler import DensityMatrixCompiler
from graphiq.metrics import Infidelity
import graphiq.benchmarks.circuits
As usual, we start with our target state. For this tutorial, choose any of the Bell state, 3- or 4-qubit GHZ state. We will use the infidelity as our metric to minimize. Currently, variational circuits are only supported for the DensityMatrix backend.
circuit = CircuitDAG(n_emitter=target.n_qubits, n_photon=0, n_classical=0)
circuit.add(
ops.ParameterizedOneQubitRotation(
register=0,
reg_type="e",
params=(0.0, 1.0, 1.0), # set the parameters explicitly, if desired
)
)
for i in range(1, target.n_qubits):
circuit.add(
ops.ParameterizedControlledRotationQubit(
control=i - 1, control_type="e", target=i, target_type="e"
)
)
circuit.draw_circuit()
params = circuit.initialize_parameters() # randomly sample the initial parameters
We wrap all the functions for computing the metric loss value into a single function, where the first parameter is our parameter dictionary.
Now, we define our Optax optimizer to use. For this demonstration, we will use the Adagrad algorithm, but many other popular optimizers exist and are already implemented in Optax. These include Adam, SGD, RMSProp, etc.
Here is the main learning loop. We iterate n_step times in our gradient-descent algorithm. Each loss value is stored to an array for plotting the learning curve later.
cost_curve = []
for step in (pbar := tqdm.tqdm(range(n_step), disable=(not progress))):
loss = compute_loss(params, circuit)
grads = jax.grad(compute_loss)(params, circuit)
updates, opt_state = optimizer.update(grads, opt_state)
params = optax.apply_updates(params, updates)
cost_curve.append(loss)
if progress:
pbar.set_description(f"Cost: {loss:.10f}")
else:
print(step, loss, params)
Plot the resulting state to see how the optimization performed.
Now we do it with the GradientDescentSolver.