High-level Algorithm Design with Qmod
Workshop Part I - Language Concepts
In this workshop we will learn to use high-level quantum programming language concepts to design quantum algorithms. We will use the Qmod language to model the functionality, and the Classiq platform to synthesize it into gate-level descriptions, visualize the circuits, and execute them. We will focus on high-level quantum types and expressions in different evaluation modes.
Part I is a walk through Qmod's structure and constructs, as well as some of its unique high-level concepts. We will look closely at these constructs using small examples. In Part II we will combine some of these concepts in a complete quantum algorithm.
There are 5 code exercises in this notebook, split into two sections, A and B. In each exercise follow the instructions - complete the code snippet where indicated by a TODO comment, execute the code, and try to understand the results. Solutions are provided at the end of the notebook. Don't continue to the next exercise before you completed the previous one and compared your code and results against the solution.
Section A (15 minutes)
Warmup: A First Qmod Program
Let's start with a simple example to demonstrate the structure of Qmod code, as well as its synthesis and execution flow. We prepare and sample a Bell state - one of the most fundamental quantum states.
Further reading: Quantum Functions
from classiq import *
@qfunc # This decorator declares a quantum function
def create_bell_state(pair: QArray[QBit, 2]):
H(pair[0])
CX(pair[0], pair[1])
@qfunc # Function 'main' is the entry point of our quantum program
def main(res: Output[QArray[QBit, 2]]):
allocate(res)
create_bell_state(res)
We can now compile with the SDK function synthesize. We get back an executable description called quantum program which we then execute on any simulation or quantum hardware. To manage the execution flow we use ExecutionSession. In our case, a simple sampling (using the default number of shots) of the quantum program will suffice.
qprog = synthesize(main)
show(qprog) # Visualize the quantum program for analysis
# Execute and print the results
with ExecutionSession(qprog) as es:
res = es.sample()
display(res.dataframe)
Exercise 1: GHZ State
Based on the Bell state example above, prepare a GHZ state with 3 qubits. The GHZ state creates maximum entanglement between three qubits: (|000⟩ + |111⟩)/√2.
from classiq import *
@qfunc
def create_ghz_state(qubits: QArray[QBit, 3]):
# TODO: Apply GHZ logic
pass
@qfunc
def main(res: Output[QArray[QBit, 3]]):
allocate(res)
create_ghz_state(res)
# TODO: Synthesize the model, show, execute and print results
# Hint: Follow the same pattern as the demonstration above
Exercise 2: GHZ with Numeric Variables
Define a main function that calls the create_ghz_state (as implemented in Exercise 1) with a signed quantum number and outputs the results.
Synthesize it, analyze the quantum program, execute it and print the results. What do you expect the resulting value of x to be?
Further reading: Quantum Types
from classiq import *
@qfunc
def main(x: Output[QNum[3, SIGNED, 0]]):
allocate(x)
# TODO: Create GHZ state with QNum (gives 0 and -1)
# Hint: Is it any different from the previous main implementation?
pass
qprog = synthesize(main)
show(qprog) # Visualize the quantum program for analysis
# Execute and print the results
with ExecutionSession(qprog) as es:
res = es.sample()
display(res.dataframe)
Section B (30 minutes)
Exercise 3: Arithmetic Expressions with Automatic Type Inference
Part A: Numeric type inference
Create and execute a quantum program that assigns a quantum arithmetic expression to a numeric variable:
-
Declare quantum numeric variables
aandbas unsigned integers of size of 2. -
Apply
hadamard_transformonaandb(to put them in uniform superposition of all possible states). -
Assign the value of
3*a + btoc. -
Synthesize, show, execute and print results. Inspect the printouts - what numeric attributes were inferred for variable
c? Why?
Further reading: Numeric Assignment
from classiq import *
@qfunc
def main(a: Output[QNum[2]], b: Output[QNum[2]], c: Output[QNum]):
allocate(a)
allocate(b)
# TODO: Put a and b in equal superposition
# TODO: Assign the value of 3*a + b to c
allocate(1, c) # Placeholder - replace with actual assignment
# Print out c's inferred size in qubits
print(f"The size of c is {c.size}")
qprog = synthesize(main)
show(qprog) # Visualize the quantum program for analysis
# Execute and print the results
with ExecutionSession(qprog) as es:
res = es.sample()
display(res.dataframe)
c must be an unsigned integer of size 4 qubits or more.
Part B: Numeric type inference with fixed-point fractions
Repeat Part A, only this time declaring a with 1 fraction digit and b with 2 fraction digits. How did the numeric attributes of c change? What are the corresponding sampled values for c in the result?
from classiq import *
@qfunc
def main(
a: Output[QNum[2, UNSIGNED, 1]], b: Output[QNum[2, UNSIGNED, 2]], c: Output[QNum]
):
allocate(a)
allocate(b)
# TODO: Put a and b in equal superposition
# TODO: Assign the value of 3*a + b to c
# Hint: Is it any different from the previous main implementation?
allocate(1, c) # Placeholder - replace with actual assignment
# Print out the numeric attributes of the inferred type
print("Numeric attributes of c:")
print(
f"size={c.size}, is_signed={c.is_signed}, fraction_digits={c.fraction_digits}"
)
qprog = synthesize(main)
show(qprog) # Visualize the quantum program for analysis
# Execute and print the results
with ExecutionSession(qprog) as es:
res = es.sample()
display(res.dataframe)
c should be of size 5, unsigend, and with 2 fraction digits. This is the minimal type that covers the expression's domain.
Exercise 4: Conditional Operations
Define a main function that initializes a quantum variable x (a 3-qubit signed number with 2 binary fraction digits) in an equal superposition of all states, then conditionally flips a single-qubit variable named flag when the value of x is less than 0.5. Inspect the execution results - how is flag entangled with x?
Further reading: Control Statement
from classiq import *
@qfunc
def main(x: Output[QNum[3, SIGNED, 2]], flag: Output[QBit]):
allocate(x)
hadamard_transform(x)
allocate(flag)
# TODO : Flip the state of flag if x < 0.5
qprog = synthesize(main)
show(qprog) # Visualize the quantum program for analysis
# Execute and print the results
with ExecutionSession(qprog) as es:
res = es.sample()
display(res.dataframe)
x is evenly distributed across the 8 values, and flag is flipped in 6 out of the 8 cases.
Exercise 5: Grover Algorithm using Quantum Struct and Constant Phase
The Grover search algorithm uses two kinds of conditional phase flips - one reflecting about the "good" states, and the other reflecting about the zero state. This is easy to express using fixed \(\pi\) phase rotation under the required control condition. It is also convenient to encapsulate the problem variables in a quantum struct, so they can be passed around between functional units.
Create a quantum program that finds assignments for \(a, b\) and \(c \in \{0, 1, 2, 3\}\) that satisfy the equation \(3a + b + 2c = 9\).
-
Declare the variables as fields of a quantum struct
-
Define the phase oracle in terms of the problem condition
-
Define the zero-reflection in the diffuser
Further reading: Phase Statement
from classiq import *
from classiq.qmod.symbolic import pi
class MyProblemVars(QStruct):
# TODO: Declare the problem variables as quantum numeric fields
dummy: QBit # Placeholder - replace with actual fields
@qfunc
def phase_oracle(v: MyProblemVars):
# TODO: Apply a phase flip if the state of v satisfies the equation (use field access in the form `v.a` etc.)
pass
@qfunc
def zero_reflection(state: QNum):
# TODO: Apply a phase flip if state is |0>
pass
@qfunc
def grover_operator(v: MyProblemVars):
phase_oracle(v)
hadamard_transform(v)
zero_reflection(v)
hadamard_transform(v)
@qfunc
def main(v: Output[MyProblemVars]):
allocate(v)
hadamard_transform(v)
for i in range(2):
grover_operator(v)
qprog = synthesize(main)
show(qprog) # Visualize the quantum program for analysis
# Execute and print the results
with ExecutionSession(qprog) as es:
res = es.sample()
display(res.dataframe)
a, b, and c that satisfy the equation. They should be sampled with approximately equal probabilities.
Exercise 6: Phase Arithmetic
Part A
Compute the expression \(x * y\) in the phase of their respective states. Use a coefficient to distribute all possible states over the \(2\pi\) phase rotation.
To actually view the phases of the states, simulate the program using state-vector simulation. Expand the quantum program visualization down to the gate-level implementation. How is the phase statement synthesized? Inspect the resulting phases of the different states in the printout. Do they match the phase expression over x and y?
Further reading: Phase Statement
import numpy as np
from classiq import *
from classiq.execution import ClassiqBackendPreferences, ExecutionPreferences
@qfunc
def main(x: Output[QNum[2]], y: Output[QNum[2]]):
allocate(x)
allocate(y)
# TODO: put x and y in uniform superposition
# TODO: Encode x * y into the phase with a normalization coefficient to 2pi
# Synthesize the model and show
qprog = synthesize(main)
show(qprog)
# Specify execution preferences for state vector simulation
preferences = ExecutionPreferences(
num_shots=1,
backend_preferences=ClassiqBackendPreferences(backend_name="simulator_statevector"),
)
# Execute and print results:
with ExecutionSession(qprog, preferences) as es:
res = es.sample()
display(res.dataframe)
x). The steps are a 1/8 of a full \(2\pi\) rotation
Bonus: Fourier Arithmetic
Create a quantum program that computes \(y = x^2\) by computing \(x^2\) in the Fourier basis. Then transform the result back to the computational basis. Inspect the execution results to validate the correctness of your algorithm.
import numpy as np
from classiq import *
@qfunc
def main(x: Output[QNum[3]], y: Output[QNum[4]]):
allocate(x)
hadamard_transform(x)
allocate(y)
# TODO: Use within_apply and function qft() to transform into and out of the Fourier basis
# Synthesize the model and show
qprog = synthesize(main)
# show(qprog)
# Synthesize the model, show, execute and print results
with ExecutionSession(qprog) as es:
res = es.sample()
display(res.dataframe)
Solutions
Solution 1: GHZ State
from classiq import *
@qfunc
def create_ghz_state(qubits: QArray[QBit, 3]):
# Apply GHZ logic
H(qubits[0])
CX(qubits[0], qubits[1])
CX(qubits[1], qubits[2])
@qfunc
def main(res: Output[QArray[QBit, 3]]):
allocate(res)
create_ghz_state(res)
# Synthesize the model, show, execute and print results
qprog = synthesize(main)
show(qprog)
with ExecutionSession(qprog) as es:
res = es.sample()
print("Ex.1 Results:")
display(res.dataframe)
Solution 2: GHZ with Quantum Numbers
from classiq import *
@qfunc
def main(x: Output[QNum[3, SIGNED, 0]]):
# Create GHZ state with QNum (gives 0 and -1)
allocate(x)
create_ghz_state(x)
# Synthesize the model, show, execute and print results
qprog = synthesize(main)
show(qprog)
with ExecutionSession(qprog) as es:
res = es.sample()
print("Ex.2 Results:")
display(res.dataframe)
Solution 3: Arithmetic Expressions with Automatic Type Inference
Part A: Numeric type inference
from classiq import *
@qfunc
def main(a: Output[QNum[2]], b: Output[QNum[2]], c: Output[QNum]):
allocate(a)
allocate(b)
# Put a and b in equal superposition
hadamard_transform(a)
hadamard_transform(b)
# Assign the value of 3*a + b to c
c |= 3 * a + b
# Print out c's inferred size in qubits
print(f"The size of c is {c.size}")
# Synthesize the model, show, execute and print results
qprog = synthesize(main)
show(qprog)
with ExecutionSession(qprog) as es:
res = es.sample()
print("Ex.3A Results:")
display(res.dataframe)
Part B: Numeric type inference with fixed-point fractions
from classiq import *
@qfunc
def main(
a: Output[QNum[2, UNSIGNED, 1]], b: Output[QNum[2, UNSIGNED, 2]], c: Output[QNum]
):
# Allocate a and b, then put in superposition
allocate(a)
allocate(b)
hadamard_transform(a)
hadamard_transform(b)
# Assign the value of 3*a + b to c
c |= 3 * a + b
# Print out the numeric attributes of the inferred type
print("Numeric attributes of c:")
print(
f"size: {c.size}, is_signed: {c.is_signed}, fraction_digits: {c.fraction_digits}"
)
# Synthesize the model, show, execute and print results
qprog = synthesize(main)
show(qprog)
with ExecutionSession(qprog) as es:
res = es.sample()
print("Ex.3B Results:")
display(res.dataframe)
Solution 4: Conditional Operations
from classiq import *
@qfunc
def main(x: Output[QNum[3, SIGNED, 2]], flag: Output[QBit]):
allocate(x)
hadamard_transform(x)
allocate(flag)
# Flip the state of flag if x < 0.5
control(x < 0.5, lambda: X(flag)) # Flip the state of flag if x < 0.5
# Synthesize the model, show, execute and print results
qprog = synthesize(main)
show(qprog)
with ExecutionSession(qprog) as es:
res = es.sample()
print("Ex.4 Results:")
display(res.dataframe)
Solution 5: Grover Algorithm using Quantum Struct and Constant Phase
from classiq import *
from classiq.qmod.symbolic import pi
class MyProblemVars(QStruct):
# Declare the problem variables as quantum numeric fields
a: QNum[2]
b: QNum[2]
c: QNum[2]
@qfunc
def phase_oracle(v: MyProblemVars):
# Apply a phase flip if the state of v satisfies the equation (use field access in the form 'v.a' etc.)
control(3 * v.a + v.b + 2 * v.c == 9, lambda: phase(pi))
@qfunc
def zero_reflection(state: QNum):
# Apply a phase flip if state is |0>
control(state == 0, lambda: phase(pi))
@qfunc
def grover_operator(v: MyProblemVars):
phase_oracle(v)
hadamard_transform(v)
zero_reflection(v)
hadamard_transform(v)
@qfunc
def main(v: Output[MyProblemVars]):
allocate(v)
hadamard_transform(v)
for i in range(2):
grover_operator(v)
# Synthesize the model, show, execute and print results
qprog = synthesize(main)
show(qprog)
with ExecutionSession(qprog) as es:
res = es.sample()
print("Ex.5 Results:")
display(res.dataframe)
Solution 6: Phase Arithmetic
import numpy as np
from classiq import *
from classiq.execution import ClassiqBackendPreferences, ExecutionPreferences
@qfunc
def main(x: Output[QNum[2]], y: Output[QNum[2]]):
allocate(x)
allocate(y)
# Put x and y in uniform superposition
hadamard_transform(x)
hadamard_transform(y)
# Encode x * y into the phase with a normalization coefficient to 2pi
phase(x * y, np.pi / 8)
# Synthesize the model and show
qprog = synthesize(main)
show(qprog)
# Specify execution preferences for state vector simulation
preferences = ExecutionPreferences(
num_shots=1,
backend_preferences=ClassiqBackendPreferences(backend_name="simulator_statevector"),
)
# Execute and print results
with ExecutionSession(qprog, preferences) as es:
res = es.sample()
print("Ex.6A Results:")
display(res.dataframe)
Bonus
import numpy as np
from classiq import *
@qfunc
def main(x: Output[QNum[3]], y: Output[QNum[4]]):
allocate(x)
hadamard_transform(x)
allocate(y)
# Use within_apply and function qft() to transform into and out of the Fourier basis
within_apply(
lambda: qft(y),
# Evaluates y += x**2 in the Fourier basis
lambda: phase(y * (x**2), 2 * np.pi / (2**y.size)),
)
# Synthesize the model, show, execute and print results
qprog = synthesize(main)
# show(qprog)
with ExecutionSession(qprog) as es:
res = es.sample()
print("Ex.6B Results:")
display(res.dataframe)