OpenQASM with LabOne Q¶
This is a tutorial on how to translate OpenQASM 3.0 programs into LabOne Q Experiments.
The tutorial assumes that the reader has a basic understanding of OpenQASM, laboneq
DSL objects, QuantumElement
, and QuantumOperations
.
Quickstart¶
This is a quickstart on introducing the core objects needed for turning OpenQASM programs into laboneq
Experiment
s.
We define a program with 2 qubits and a single-qubit X gate that sequentially acts on the qubits.
program = """
OPENQASM 3;
qubit q0;
qubit q1;
x q0;
x q1;
"""
Qubits¶
We will define our DeviceSetup
and three Transmon
qubits q0
, q1
, and q2
by using the helper function generate_device_setup_qubits()
.
These qubits are used for the rest of the examples in this notebook.
from laboneq.contrib.example_helpers.generate_device_setup import (
generate_device_setup_qubits,
)
# Select the number of qubits
number_of_qubits = 3
# Generate the device setup and the qubit objects using a helper function
device_setup, qubits = generate_device_setup_qubits(
number_qubits=number_of_qubits,
pqsc=[{"serial": "DEV10001"}],
hdawg=[
{
"serial": "DEV8001",
"zsync": 0,
"number_of_channels": 8,
"options": None,
}
],
shfqa=[
{
"serial": "DEV12001",
"zsync": 1,
"readout_multiplex": 6,
"options": None,
}
],
shfsg=[
{
"serial": "DEV12002",
"zsync": 2,
"number_of_channels": 8,
"options": None,
}
],
include_flux_lines=True,
server_host="localhost",
setup_name=f"my_{number_of_qubits}_tunable_qubit_setup",
)
QuantumOperations¶
Quantum operations map to OpenQASM gates and operations that act on qubits.
from laboneq import simple
from laboneq.dsl import quantum
class TransmonOperations(quantum.QuantumOperations):
QUBIT_TYPES = simple.Transmon
@quantum.quantum_operation
def x(self, qubit):
# Implement x gate
...
Initialize a QPU
with selected qubits and QuantumOperations
.
qpu = quantum.QPU(qubits=qubits, quantum_operations=TransmonOperations())
OpenQASM transpiler¶
laboneq
provides an OpenQASMTranspiler
, which converts OpenQASM programs into target QPU
compatible Experiments
.
from laboneq import openqasm3
Initializing OpenQASMTranspiler
with chosen QPU
.
This transpiler and attached QPU
are used in the rest of the tutorial.
transpiler = openqasm3.OpenQASMTranspiler(qpu)
Now we'll use OpenQASMTranspiler.experiment()
which generates an Experiment
from given OpenQASM program.
We map the qubits used within the OpenQASM program to the qubits provided by the QPU
in the qubit_map
argument.
exp = transpiler.experiment(
program=program,
qubit_map={"q0": "q0", "q1": "q1"},
options=openqasm3.SingleProgramOptions(
count=2,
averaging_mode=simple.AveragingMode.CYCLIC,
acquisition_type=simple.AcquisitionType.RAW,
reset_oscillator_phase=False,
),
)
The generated Experiment
can be then compiled via Session
and executed.
Qubit mapping¶
OpenQASM defined qubits can be mapped into LabOneQ Qubits via the qubit_map
argument.
Qubit register¶
As mentioned above, qubits defined in the OpenQASM program can be mapped to LabOne Q Qubits via the qubit_map
argument. Either individual qubits or qubit registers may be supplied in the mapping. Individual qubits may be OpenQASM "logical" qubits or hardware qubits. The different possibilities are demonstrated below.
Qubit registers must be a list of qubits matching the size defined in the program.
program = """
OPENQASM 3;
qubit[2] qregister;
"""
exp = transpiler.experiment(
program=program,
qubit_map={"qregister": ["q1", "q2"]},
)
Logical and hardware qubits¶
program = """
OPENQASM 3;
qubit q0;
x q0;
x $2;
"""
exp = transpiler.experiment(
program=program,
qubit_map={"q0": "q0", "$2": "q1"},
)
Gates and operations¶
The OpenQASM gates are implemented via QuantumOperations
.
By default the name of the gates/operations are mapped to the identically named operations defined in QuantumOperations
.
The qubits on which the gate is applied to, is always supplied first to the quantum operation. The qubit arguments are in the same order as defined in the program.
Fixed gates¶
Fixed gates are always supplied by the mapped qubits.
"""
OPENQASM 3;
qubit q0;
x q0;
cnot q0, q1;
"""
class TransmonOperations(quantum.QuantumOperations):
QUBIT_TYPES = simple.Transmon
@quantum.quantum_operation
def x(self, qubit): ...
@quantum.quantum_operation
def cnot(self, control, target): ...
Broadcasting¶
When one of the inputs of the gate is a qubit register, the gate is broadcasted to all qubits in the register.
In the example below, the X gate is applied to all qubits in the register simultaneously.
We then apply the cnot gate two times, one with the pair q0
and qregister[0]
, and the other with the pair q0
and qregister[1]
.
"""
OPENQASM 3;
qubit q0;
qubit[2] qregister;
x qregister;
cnot q0, qregister;
"""
Parametrized gates¶
Parametrized gates are called on QuantumOperations
in the following way.
The qubits on which the gate is applied are always supplied first to the quantum operation. The rest of the variables are passed in as arguments.
"""
OPENQASM 3;
qubit q0;
x(pi/2) q0;
"""
class TransmonOperations(quantum.QuantumOperations):
QUBIT_TYPES = simple.Transmon
@quantum.quantum_operation
def x(self, qubit, angle): # q0, pi/2
...
Inputs¶
Input mapping can be done via the inputs
argument.
program = """
OPENQASM 3;
input bool a;
"""
exp = transpiler.experiment(program=program, qubit_map={}, inputs={"a": False})
Delay instruction¶
Delays support SI units of time. Backend-dependent units are not supported.
The following example demonstrates how to insert a delay of 100 ns on the qubit q
and then a delay of 100 ns on the qubit register qregister
between two x
gates.
program = """
OPENQASM 3;
qubit q;
qubit[2] qregister;
x q;
delay[100ns] q;
delay[100ns] qregister;
x q;
"""
exp = transpiler.experiment(
program=program, qubit_map={"q": "q0", "qregister": ["q1", "q2"]}
)
Barrier instruction¶
Barrier instruction is supported by default and is not required to be implemented in QuantumOperations
.
The default implementation will reserve all the signal lines on each qubit in barrier
statement.
The barrier instruction is supported via QuantumOperations.barrier(*qubits)
.
It is suggested for barrier
to take arbitrary number of qubits, but this might change in the future when broadcasting
is fully supported.
program = """
OPENQASM 3;
qubit q0;
qubit q1;
qubit[2] qregister;
barrier; // all qubits
barrier q0; // single qubit
barrier q0, q1; // Multiple qubits
barrier qregister; // apply barrier to all qubits in qregister
"""
class TransmonOperations(quantum.QuantumOperations):
QUBIT_TYPES = simple.Transmon
@quantum.quantum_operation
def barrier(self, *qubits):
# QuantumOperations will reserve all qubit signal lines by default
...
Measurement¶
The measure
statement is supported via QuantumOperations
by implementing a measure(qubit, handle)
method.
The handle
is the name of the target bit
defined in the program.
It is up to the user to ensure the handles are unique across the produced Experiment
.
program = """
OPENQASM 3;
qubit q0;
qubit[2] qregister;
bit b;
bit[2] c;
b = measure q0;
c = measure qregister;
"""
class TransmonOperations(quantum.QuantumOperations):
QUBIT_TYPES = simple.Transmon
@quantum.quantum_operation
def measure(self, qubit, handle):
print(f"Measurement is done with {qubit.uid} associated with handle {handle}")
transpiler_meas = openqasm3.OpenQASMTranspiler(
quantum.QPU(qubits=qubits, quantum_operations=TransmonOperations())
)
exp = transpiler_meas.experiment(
program, qubit_map={"q0": "q0", "qregister": ["q1", "q2"]}
)
Extern function calls¶
Extern function calls can be mapped to Python callable
s via the externs
argument.
program = """
OPENQASM 3;
defcalgrammar "openpulse";
extern elongate(duration, float[64]) -> duration;
duration x = elongate(10ns, 3);
qubit q;
delay[x] q;
"""
def elongate(duration, multiple):
return duration * multiple
exp = transpiler.experiment(
program=program, qubit_map={"q": "q0"}, externs={"elongate": elongate}
)
Pragmas¶
laboneq
has specific pragma syntax to define experiment values, zi.<value>
.
Currently supported pragmas:
zi.acquisition_type <AcquisitionType>
: Sets the experiment real time loop acquisition type
program = """
OPENQASM 3;
pragma zi.acquisition_type raw
"""
exp = transpiler.experiment(
program=program,
qubit_map={},
)
print(exp.sections[0].acquisition_type)
Openpulse grammar¶
Ports¶
The port mapping from program defined ports to LabOne Q Qubit signals can be done via externs
argument.
program = """
OPENQASM 3;
include "stdgates.inc";
defcalgrammar "openpulse";
const int frequency = 4.5e9;
cal {
extern port drive;
frame frame0 = newframe(drive, 6.1e9, 0.0);
set_frequency(frame0, frequency);
}
"""
exp = transpiler.experiment(
program=program,
qubit_map={"q0": "q0"},
externs={"drive": openqasm3.port("q0", "drive")},
)
Setting the frequency¶
program = """
OPENQASM 3;
include "stdgates.inc";
defcalgrammar "openpulse";
const int frequency = 4.5e9;
cal {
extern port q0drive;
frame frame0 = newframe(q0drive, 6.1e9, 0.0);
set_frequency(frame0, frequency);
}
"""
exp = transpiler.experiment(
program=program,
qubit_map={"q0": "q0"},
externs={"q0drive": openqasm3.port("q0", "drive")},
)
Playing a waveform¶
play()
is supported with waveform
as an argument or as an implicit input.
When waveform is used as an implicit input, they must be defined in inputs
argument.
waveform
declared type must either be a laboneq
Pulse
or a list of samples.
Input waveform as an implicit argument
program = """
OPENQASM 3;
include "stdgates.inc";
defcalgrammar "openpulse";
const int frequency = 4.5e9;
cal {
extern port drive;
frame frame0 = newframe(drive, 6.1e9, 0.0);
play(frame0, wf0);
}
"""
exp = transpiler.experiment(
program=program,
qubit_map={"q0": "q0"},
externs={"drive": openqasm3.port("q0", "drive")},
inputs={
"wf0": simple.pulse_library.gaussian_square(
uid="q0_readout", length=2e-7, amplitude=0.5
)
},
)
Input waveform
from an extern
program = """
OPENQASM 3;
defcalgrammar "openpulse";
cal {
extern constant_pulse(complex[float[64]], duration) -> waveform;
}
cal {
waveform two_ten_ns = constant_pulse(0.7, 10ns);
extern port drive;
frame frame0 = newframe(drive, 6.1e9, 0.0);
play(frame0, two_ten_ns);
}
"""
def constant_pulse(amplitude, duration):
return simple.pulse_library.const(amplitude=amplitude, length=duration)
exp = transpiler.experiment(
program=program,
qubit_map={"q0": "q0"},
externs={"constant_pulse": constant_pulse, "drive": openqasm3.port("q0", "drive")},
)
Experiment settings¶
A set of LabOne Q specific Experiment
settings can be supplied to the transpiler.
program = """
OPENQASM 3;
qubit q0;
qubit q1;
x q0;
x q1;
"""
exp = transpiler.experiment(
program=program,
qubit_map={"q0": "q0", "q1": "q1"},
options=openqasm3.SingleProgramOptions(
count=2,
averaging_mode=simple.AveragingMode.CYCLIC,
acquisition_type=simple.AcquisitionType.RAW,
reset_oscillator_phase=False,
),
)
Combine multiple QASM programs¶
Multiple OpenQASM programs can be combined into a single Experiment
by using OpenQASMTranspiler.batch_experiment()
.
The mapping arguments are similar to single programs and are shared among the programs.
The generated experiment content outside of programs can be controlled via MultiProgramOptions
Add a measurement¶
By default the multi program experiment adds a measurement for all used qubits.
This can be controlled via MultiProgramOptions.add_measurement
flag.
When measurement is used, the QuantumOperations
associated with qubits must have measure
operation defined.
The measure is added after the OpenQASM programs.
The handle
is the name of the target qubit
defined in the program.
class TransmonOperations(quantum.QuantumOperations):
QUBIT_TYPES = simple.Transmon
@quantum.quantum_operation
def x(self, qubit): ...
@quantum.quantum_operation
def measure(self, qubit, handle):
# measure is called with qubit and handle name
...
qpu = quantum.QPU(qubits=qubits, quantum_operations=TransmonOperations())
transpiler = openqasm3.OpenQASMTranspiler(qpu)
program_0 = """
OPENQASM 3;
qubit q1;
x q1;
"""
program_1 = """
OPENQASM 3;
qubit q1;
qubit q2;
x q1;
x q2;
"""
exp = transpiler.batch_experiment(
programs=[program_0, program_1],
qubit_map={"q1": "q0", "q2": "q1"},
options=openqasm3.MultiProgramOptions(
add_measurement=True,
),
)
Add a qubit reset¶
When MultiProgramOptions.add_reset
is set to True
(default: False
), an reset operation is added
for each qubit used in the Experiment
.
Reset expects an reset()
operation to be defined in QuantumOperations
.
Reset is added before the OpenQASM programs are executed.
class TransmonOperations(quantum.QuantumOperations):
QUBIT_TYPES = simple.Transmon
@quantum.quantum_operation
def x(self, qubit): ...
@quantum.quantum_operation
def reset(self, qubit): ...
qpu = quantum.QPU(qubits=qubits, quantum_operations=TransmonOperations())
transpiler = openqasm3.OpenQASMTranspiler(qpu)
exp = transpiler.batch_experiment(
programs=[program_0, program_1],
qubit_map={"q1": "q0", "q2": "q1"},
options=openqasm3.MultiProgramOptions(add_reset=True, add_measurement=False),
)