Execute Custom Quantum Program: Tutorial¶
This notebook guides you through executing a custom quantum program using qoolqit.
A QuantumProgram combines a Register and a Drive and serves as the main interface for compilation and execution.
Here we will define a register via the DataGraph class of qoolqit and a custom waveform sublassing the Waveform class.
1. Create the Register¶
from qoolqit import DataGraph, Register
#Define a register from a DataGraph
graph=DataGraph.hexagonal(1,1)
reg=Register.from_graph(graph)
#Define a register from positions
reg=Register.from_coordinates(list(graph.coords.values()))
Remember it is possible to use prefedined embedders from graphs or define custom ones. Refer to the documentation for this.
2. Create a custom Drive¶
Let us define a profile following the function $f(t) = \Omega_{\mathrm{max}} \cdot \sin^2( \frac{π}{2} \cdot \sin(π t) )$.
import math
from qoolqit.waveforms import Waveform
class SmoothPulse(Waveform):
"""f(t) = omega_max * sin( (π/2) * sin(π t) )^2, for 0 ≤ t ≤ duration"""
def __init__(self, duration: float , omega_max: float) -> None:
super().__init__(duration, omega_max=omega_max)
def function(self, t: float) -> float:
return self.omega_max * math.sin(0.5 * math.pi * math.sin(math.pi * t/self.duration)) ** 2
3. Combine Register and Drive into a Quantum Program¶
⚠️ Remember
qoolqit always uses dimensionless units ( documentation ):
- Energies are normalized by the maximum drive amplitude $\Omega_\text{max}=1$.
- Time is measured in units of $1/\Omega_\text{max}$ (Rabi oscillations).
- Space is measured in units of $r_B=(C_6/\Omega_\text{max})^{1/6}$.
3.a Details about units of measure¶
Why normalization?¶
All quantum devices have a hardware limit for the maximum drive strength they can apply $\Omega_\text{max}$.
To make programs device-independent, we take this maximum value as our unit of energy and measure every amplitude as a function of it:$$ \bar{\Omega} = \frac{\Omega}{\Omega_\text{max}} $$ For example:
- If the physical maximum is $\Omega_\text{max}=4\pi \,\text{rad/µs}$, then $\bar{\Omega}_\text{max}=1$.
- A drive of $\Omega=2\pi \,\text{rad/µs}$ becomes $\bar{\Omega}=0.5$.
- A drive of $\Omega=\pi \,\text{rad/µs}$ becomes $\bar{\Omega}=0.25$.
This makes all quantum programs dimensionless and therefore comparable across devices.
What happens to other units?¶
Normalizing the drive amplitude automatically sets the scale for time and space as well:
Energy units:
$$ \bar{E} = \frac{E}{\Omega_\text{max}} $$ So also the detuning is measured in units of $\Omega_\text{max}$.Time units:
At zero detuning, the Rabi frequency is equal to the drive,
$$ \Omega_R = \Omega_{\text{max}}, $$
so the natural unit of time is
$$ t_R = \frac{1}{\Omega_\text{max}}. $$
All times are expressed in multiples of the Rabi period at maximum drive $T=\frac{2 \pi}{\Omega_{\text{max}}}=2 \pi t_R$.
$$ \bar{t} = \frac{t}{t_R}= \frac{2 \pi t}{T} $$ So, in a time $\bar{t}=2 \pi$ one gets a full Rabi period.Space units:
Interactions follow $E(r)=C_6/r^6$.
The characteristic length is defined by setting $E(r_B)=\Omega_\text{max}$:
$$ r_B = \left(\frac{C_6}{\Omega_\text{max}}\right)^{1/6}. $$
Distances are expressed relative to $r_B$, i.e. the Rydberg blockade radius at maximum drive.
$$ \bar{r} = \frac{r}{r_B}. $$
Summary¶
- Energy → normalized by $\Omega_\text{max}$
- Time → measured in $1/\Omega_\text{max}$ (Rabi oscillations)
- Space → measured in $r_B=(C_6 / \Omega_\text{max})^{1/6}$ (interaction length)
3.b Definition of drive and program¶
from qoolqit import Drive, QuantumProgram, Ramp
T1 = 2*math.pi #Duration
amp_1 = SmoothPulse(T1,omega_max=1) #amplitude drive
det_1 = Ramp(T1,-1,1) #detuning drive
drive_1=Drive(amplitude=amp_1,detuning=det_1) #definine the drive
program = QuantumProgram(reg, drive_1) #define the quantum program
program.register.draw() #draw the register
program.draw() #draw the drive
4. Compile to a device¶
To run the QuantumProgram we have to compile it to switch back to physical units that are compatible with a given device.
The now compiled sequence and the compiled register are measured in Pulser units. In the compilation it is also possible to select CompilerProfiles to force specific compilation constraints [ documentation ]
from qoolqit import AnalogDevice
device=AnalogDevice() #Call the device
program.compile_to(device) #Compile to a device
program.compiled_sequence.draw()
program.compiled_sequence.register.draw()
5. Execute the compile quantum program¶
import numpy as np
from qoolqit import BackendName, ResultType
# Number of samples
runs = 1000
evaluation_times = np.linspace(0, 1, num=10).tolist()
# Simulate with EMUMPS backend and output bitstring counts
emumps_bitstrings = program.run(
backend_name=BackendName.EMUMPS,
result_type=ResultType.BITSTRINGS,
evaluation_times=evaluation_times,
runs=runs)
print("Length of the output list:")
print(len(emumps_bitstrings))
print("Final state bitstrings:")
print(emumps_bitstrings[-1])
Length of the output list: 10 Final state bitstrings: Counter({'000000': 133, '000001': 68, '100000': 67, '000010': 67, '000100': 64, '010100': 63, '001010': 60, '010000': 58, '001000': 56, '000101': 54, '010001': 50, '101000': 48, '100010': 40, '010101': 35, '101010': 33, '001001': 13, '000110': 12, '110000': 11, '000011': 10, '100100': 9, '001011': 7, '011000': 7, '010010': 4, '110100': 4, '101101': 3, '110001': 2, '101100': 2, '001100': 2, '100110': 2, '011010': 2, '100001': 2, '111100': 1, '111000': 1, '001101': 1, '111001': 1, '110011': 1, '010011': 1, '011100': 1, '110010': 1, '000111': 1, '011001': 1, '100101': 1, '100011': 1})
6. Plot bitstring distribution¶
import matplotlib.pyplot as plt
def plot_distribution(counter):
counter = dict(sorted(counter.items(), key=lambda item: item[1], reverse=True))
fig, ax = plt.subplots(1, 1, figsize=(12, 6))
ax.set_xlabel("Bitstrings")
ax.set_ylabel("Counts")
ax.set_xticks(range(len(counter)))
ax.set_xticklabels(counter.keys(), rotation=90)
ax.bar(counter.keys(), counter.values(), width=0.5)
return fig
fig = plot_distribution(emumps_bitstrings[-1])
Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting. Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting.