# 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,
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):

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)')