Quantum Phase Estimation on a Grover Operator
This notebook demonstrates the capability of Classiq's QPE function, focusing on a case where it is applied on a Grover Operator. This is the core of the Amplitude Estimation algorithm.
The demonstration done for a toy model, where the Grover operator is constructed for a "good state" on 5 qubits \(|\psi\rangle_{\rm good}\propto (|2\rangle+|3\rangle+|5\rangle+|7\rangle+|11\rangle+|13\rangle\)).
1. Building a model with Classiq
from classiq import (
CArray,
CInt,
H,
Output,
QArray,
QBit,
QNum,
U,
X,
Z,
allocate,
allocate_num,
control,
create_model,
hadamard_transform,
invert,
qfunc,
qpe,
repeat,
synthesize,
within_apply,
)
import numpy as np
NUM_QUBITS = 5
ints_to_flip = [2, 3, 5, 7, 11, 13]
precisions = [l for l in range(1, 6)]
print("precisions:", precisions)
precisions: [1, 2, 3, 4, 5]
transpilation_options = {"classiq": "custom", "qiskit": 3}
1.1 Constructing the Relevant Quantum Functions
# qfunc for specific state preparation
@qfunc
def my_state_preparation(states: CArray[CInt], x: QNum, ind: QBit):
hadamard_transform(x)
repeat(states.len, lambda i: control(x == states[i], lambda: X(ind)))
# qfunc for grover
import numpy as np
@qfunc
def my_grover(x: QNum, ind: QBit):
Z(ind) # oracle for the good state
within_apply(
lambda: invert(lambda: my_state_preparation(ints_to_flip, x, ind)),
lambda: within_apply(
lambda: (X(ind), H(ind)), lambda: control(x == 0, lambda: X(ind))
), # zero oracle 1-|0><0|
)
U(0, 0, 0, np.pi, ind)
from classiq import CustomHardwareSettings, Preferences
preferences = Preferences(
custom_hardware_settings=CustomHardwareSettings(basis_gates=["cx", "u"]),
transpilation_option=transpilation_options["classiq"],
)
1.2 Synthesizing with two Different Optimization Scenarios
from classiq import (
Constraints,
OptimizationParameter,
QuantumProgram,
set_constraints,
set_preferences,
synthesize,
)
classiq_depths_ae_opt_width = []
classiq_cx_counts_ae_opt_width = []
classiq_widths_ae_opt_width = []
classiq_cx_counts_ae_opt_depth_max_width = []
classiq_depths_ae_opt_depth_max_width = []
classiq_widths_ae_opt_depth_max_width = []
for precision in precisions:
@qfunc
def main():
ind = QBit("ind")
allocate(1, ind)
x = QNum("x")
allocate_num(NUM_QUBITS - 1, False, 0, x)
phase = QNum("phase")
allocate_num(precision, False, precision, phase)
qpe(lambda: my_grover(x, ind), phase)
qmod = create_model(main)
qprog = synthesize(qmod)
# width optimization
constraints = Constraints(
optimization_parameter=OptimizationParameter.WIDTH,
)
qmod = set_constraints(qmod, constraints=constraints)
qmod = set_preferences(qmod, preferences=preferences)
quantum_program = synthesize(qmod)
circuit = QuantumProgram.from_qprog(quantum_program)
classiq_widths_ae_opt_width.append(circuit.data.width)
classiq_depths_ae_opt_width.append(circuit.transpiled_circuit.depth)
classiq_cx_counts_ae_opt_width.append(circuit.transpiled_circuit.count_ops["cx"])
# Depth Optimization with a Constrained Width
constraints = Constraints(
optimization_parameter=OptimizationParameter.DEPTH,
max_width=15 + 3 * (precision - 1), # setting some bound
)
qmod = set_constraints(qmod, constraints=constraints)
qmod = set_preferences(qmod, preferences=preferences)
quantum_program = synthesize(qmod)
circuit = QuantumProgram.from_qprog(quantum_program)
classiq_widths_ae_opt_depth_max_width.append(circuit.data.width)
classiq_depths_ae_opt_depth_max_width.append(circuit.transpiled_circuit.depth)
classiq_cx_counts_ae_opt_depth_max_width.append(
circuit.transpiled_circuit.count_ops["cx"]
)
print("classiq depths:", classiq_depths_ae_opt_width)
print("classiq cx_counts:", classiq_cx_counts_ae_opt_width)
print("classiq widths:", classiq_widths_ae_opt_width)
print("classiq depths:", classiq_depths_ae_opt_depth_max_width)
print("classiq cx_counts:", classiq_cx_counts_ae_opt_depth_max_width)
print("classiq widths:", classiq_widths_ae_opt_depth_max_width)
classiq depths: [866, 2596, 6056, 12976, 26816]
classiq cx_counts: [513, 1544, 3600, 7713, 15929]
classiq widths: [6, 7, 8, 9, 10]
classiq depths: [426, 1272, 2926, 6196, 13015]
classiq cx_counts: [241, 722, 1664, 3523, 7384]
classiq widths: [15, 18, 21, 24, 27]
2. Comparing to Qiskit Implementation
The qiskit data was generated using qiskit version 1.0.0. To run the qiskit code uncomment the commented cells below.
qiskit_depths_ae = [3094, 9262, 21596, 46254, 95560]
qiskit_cx_counts_ae = [1712, 5130, 11958, 25604, 52884]
qiskit_widths_ae = [6, 7, 8, 9, 10]
# from importlib.metadata import version
# try:
# import qiskit
# if version('qiskit') != "1.0.0":
# !pip uninstall qiskit -y
# !pip install qiskit==1.0.0
# except ImportError:
# !pip install qiskit==1.0.0
# import numpy as np
# from qiskit import QuantumCircuit, QuantumRegister, transpile
# from qiskit.circuit.library import GroverOperator, PhaseEstimation, XGate
# # building the state preparation circuit, the last qubit is the indicator of good/bad state
# state_preparation = QuantumCircuit(NUM_QUBITS)
# for q in range(NUM_QUBITS - 1):
# state_preparation.h(q)
# for states in states_to_flip:
# mcx = XGate().control(num_ctrl_qubits=NUM_QUBITS - 1, ctrl_state=states)
# state_preparation.append(mcx, [k for k in range(NUM_QUBITS)])
# circuit = QuantumCircuit(NUM_QUBITS)
# circuit.compose(state_preparation, [k for k in range(NUM_QUBITS)], inplace=True)
# # building oracle
# oracle = QuantumCircuit(NUM_QUBITS)
# oracle.z(4) # good state = last qubit is |1>
# # building the grover operator
# grover_op = GroverOperator(oracle, state_preparation=circuit, insert_barriers=True)
# qiskit_depths_ae = []
# qiskit_cx_counts_ae = []
# qiskit_widths_ae = []
# for precision in precisions:
# qpe_qc = PhaseEstimation(precision, circuit)
# transpiled_cir = transpile(
# qpe_qc,
# basis_gates=["u", "cx"],
# optimization_level=transpilation_options["qiskit"],
# )
# qiskit_depths_ae.append(transpiled_cir.depth())
# qiskit_cx_counts_ae.append(transpiled_cir.count_ops()["cx"])
# qiskit_widths_ae.append(transpiled_cir.width())
3. Plotting the Data
import matplotlib.pyplot as plt
classiq_color = "#119DA4"
classiq_color_1 = "#F43764"
qiskit_color = "#bb8bff"
plt.rcParams["font.family"] = "serif"
plt.rc("savefig", dpi=300)
plt.rcParams["axes.linewidth"] = 1
plt.rcParams["xtick.major.size"] = 5
plt.rcParams["xtick.minor.size"] = 5
plt.rcParams["ytick.major.size"] = 5
plt.rcParams["ytick.minor.size"] = 5
(qiskit1,) = plt.semilogy(
precisions,
qiskit_depths_ae,
"-s",
label="qiskit",
markerfacecolor=qiskit_color,
markeredgecolor="k",
markersize=6,
markeredgewidth=1.5,
color=qiskit_color,
)
(classiq1,) = plt.semilogy(
precisions,
classiq_depths_ae_opt_width,
"-D",
label="classiq width opt.",
markerfacecolor=classiq_color,
markeredgecolor="k",
markersize=6.5,
markeredgewidth=1.5,
color=classiq_color,
)
(classiq2,) = plt.semilogy(
precisions,
classiq_depths_ae_opt_depth_max_width,
"-o",
label="classiq depth opt.",
markerfacecolor=classiq_color_1,
markeredgecolor="k",
markersize=7,
markeredgewidth=1.5,
linewidth=1.5,
color=classiq_color_1,
)
first_legend = plt.legend(
handles=[qiskit1, classiq1, classiq2], fontsize=16, loc="upper left"
)
plt.ylabel("Depth", fontsize=16)
plt.xlabel("QPE-precision", fontsize=16)
plt.yticks(fontsize=16)
plt.xticks(fontsize=16)
plt.axis(ymin=3e2, ymax=5e5)
plt.xticks(precisions)
for x, y, num_qubits in zip(
precisions, classiq_depths_ae_opt_width, classiq_widths_ae_opt_width
):
plt.text(x * 0.94, y * 1.2, str(num_qubits), fontsize=16, color=classiq_color)
for x, y, num_qubits in zip(
precisions,
classiq_depths_ae_opt_depth_max_width,
classiq_widths_ae_opt_depth_max_width,
):
plt.text(x * 0.96, y * 1.25, str(num_qubits), fontsize=16, color=classiq_color_1)
for x, y, num_qubits in zip(precisions, qiskit_depths_ae, qiskit_widths_ae):
plt.text(x * 0.98, y * 1.2, str(num_qubits), fontsize=16, color=qiskit_color)
plt.text(4.8, 5e2, "(b)", fontsize=16)
Text(4.8, 500.0, '(b)')