# Repository: https://gitlab.com/quantify-os/quantify-scheduler
# Licensed according to the LICENCE file on the main branch
"""
Module containing factory functions for pulses on the quantum-device layer.
These factories take a parametrized representation of an operation and create an
instance of the operation itself. The created operations make use of Qblox-specific
hardware features.
"""
from __future__ import annotations
import math
import numpy as np
from quantify_scheduler.backends.qblox import constants, helpers
from quantify_scheduler.backends.qblox.operations.stitched_pulse import (
StitchedPulse,
StitchedPulseBuilder,
)
from quantify_scheduler.operations import pulse_library
from quantify_scheduler.resources import BasebandClockResource
[docs]
def long_square_pulse(
amp: float,
duration: float,
port: str,
clock: str = BasebandClockResource.IDENTITY,
t0: float = 0,
grid_time_ns: int = constants.GRID_TIME,
reference_magnitude: pulse_library.ReferenceMagnitude | None = None,
) -> StitchedPulse:
"""
Create a long square pulse using DC voltage offsets.
.. warning::
This function creates a
:class:`~quantify_scheduler.backends.qblox.operations.stitched_pulse.StitchedPulse`
object, containing a combination of voltage offsets and waveforms. Overlapping
StitchedPulses on the same port and clock may lead to unexpected results.
Parameters
----------
amp : float
Amplitude of the envelope.
duration : float
The pulse duration in seconds.
port : str
Port of the pulse, must be capable of playing a complex waveform.
clock : str, optional
Clock used to modulate the pulse. By default the baseband clock.
t0 : float, optional
Time in seconds when to start the pulses relative to the start time
of the Operation in the Schedule. By default 0.
grid_time_ns : int, optional
Grid time in ns. The duration of the long_square_pulse must be a multiple
of this. By default equal to the grid time of Qblox modules.
reference_magnitude : optional
Scaling value and unit for the unitless amplitude. Uses settings in
hardware config if not provided.
Returns
-------
StitchedPulse
A StitchedPulse object containing an offset instruction with the specified
amplitude.
Raises
------
ValueError
When the duration of the pulse is not a multiple of ``grid_time_ns``.
"""
try:
duration = helpers.to_grid_time(duration, grid_time_ns) * 1e-9
except ValueError as err:
raise ValueError(
f"The duration of a long_square_pulse must be a multiple of "
f"{grid_time_ns} ns."
) from err
pulse = (
StitchedPulseBuilder(
name=long_square_pulse.__name__, port=port, clock=clock, t0=t0
)
.add_voltage_offset(
path_I=np.real(amp),
path_Q=np.imag(amp),
reference_magnitude=reference_magnitude,
)
# The last bit, with duration 'grid time' ns, is replaced by a normal pulse. The
# offset is set back to 0 before this pulse, because the Qblox backend might
# otherwise lengthen the full operation by adding an 'UpdateParameters'
# instruction at the end.
.add_voltage_offset(
path_I=0.0, path_Q=0.0, rel_time=duration - grid_time_ns * 1e-9
)
.add_pulse(
pulse_library.SquarePulse(
amp=amp,
duration=grid_time_ns * 1e-9,
port=port,
clock=clock,
reference_magnitude=reference_magnitude,
)
)
.build()
)
return pulse
[docs]
def staircase_pulse(
start_amp: float,
final_amp: float,
num_steps: int,
duration: float,
port: str,
clock: str = BasebandClockResource.IDENTITY,
t0: float = 0,
grid_time_ns: int = constants.GRID_TIME,
reference_magnitude: pulse_library.ReferenceMagnitude | None = None,
) -> StitchedPulse:
"""
Create a staircase-shaped pulse using DC voltage offsets.
This function generates a real valued staircase pulse, which reaches its final
amplitude in discrete steps. In between it will maintain a plateau.
.. warning::
This function creates a
:class:`~quantify_scheduler.backends.qblox.operations.stitched_pulse.StitchedPulse`
object, containing a combination of voltage offsets and waveforms. Overlapping
StitchedPulses on the same port and clock may lead to unexpected results.
Parameters
----------
start_amp : float
Starting amplitude of the staircase envelope function.
final_amp : float
Final amplitude of the staircase envelope function.
num_steps : int
The number of plateaus.
duration : float
Duration of the pulse in seconds.
port : str
Port of the pulse.
clock : str, optional
Clock used to modulate the pulse. By default the baseband clock.
t0 : float, optional
Time in seconds when to start the pulses relative to the start time
of the Operation in the Schedule. By default 0.
grid_time_ns : int, optional
Grid time in ns. The duration of each step of the staircase must be a multiple
of this. By default equal to the grid time of Qblox modules.
reference_magnitude : optional
Scaling value and unit for the unitless amplitude. Uses settings in
hardware config if not provided.
Returns
-------
StitchedPulse
A StitchedPulse object containing incrementing or decrementing offset
instructions.
Raises
------
ValueError
When the duration of a step is not a multiple of ``grid_time_ns``.
"""
builder = StitchedPulseBuilder(
name=staircase_pulse.__name__, port=port, clock=clock, t0=t0
)
try:
step_duration = helpers.to_grid_time(duration / num_steps, grid_time_ns) * 1e-9
except ValueError as err:
raise ValueError(
f"The duration of each step of the staircase must be a multiple of"
f" {grid_time_ns} ns."
) from err
amps = np.linspace(start_amp, final_amp, num_steps)
# The final step is a special case, see below.
for amp in amps[:-1]:
builder.add_voltage_offset(
path_I=amp,
path_Q=0.0,
duration=step_duration,
min_duration=grid_time_ns * 1e-9,
reference_magnitude=reference_magnitude,
)
# The final step is an offset with the last part (of duration 'grid time' ns)
# replaced by a pulse. The offset is set back to 0 before the pulse, because the
# Qblox backend might otherwise lengthen the full operation by adding an
# 'UpdateParameters' instruction at the end.
builder.add_voltage_offset(
path_I=amps[-1],
path_Q=0.0,
duration=step_duration - grid_time_ns * 1e-9,
min_duration=grid_time_ns * 1e-9,
reference_magnitude=reference_magnitude,
)
builder.add_voltage_offset(path_I=0.0, path_Q=0.0)
builder.add_pulse(
pulse_library.SquarePulse(
amp=amps[-1],
duration=grid_time_ns * 1e-9,
port=port,
clock=clock,
reference_magnitude=reference_magnitude,
)
)
pulse = builder.build()
return pulse
[docs]
def long_ramp_pulse(
amp: float,
duration: float,
port: str,
offset: float = 0,
clock: str = BasebandClockResource.IDENTITY,
t0: float = 0,
part_duration_ns: int = constants.STITCHED_PULSE_PART_DURATION_NS,
reference_magnitude: pulse_library.ReferenceMagnitude | None = None,
) -> StitchedPulse:
"""
Creates a long ramp pulse by stitching together shorter ramps.
This function creates a long ramp pulse by stitching together ramp pulses of the
specified duration ``part_duration_ns``, with DC voltage offset instructions placed
in between.
.. warning::
This function creates a
:class:`~quantify_scheduler.backends.qblox.operations.stitched_pulse.StitchedPulse`
object, containing a combination of voltage offsets and waveforms. Overlapping
StitchedPulses on the same port and clock may lead to unexpected results.
Parameters
----------
amp : float
Amplitude of the ramp envelope function.
duration : float
The pulse duration in seconds.
port : str
Port of the pulse.
offset : float, optional
Starting point of the ramp pulse. By default 0.
clock : str, optional
Clock used to modulate the pulse, by default a BasebandClock is used.
t0 : float, optional
Time in seconds when to start the pulses relative to the start time of the
Operation in the Schedule. By default 0.
part_duration_ns : int, optional
Duration of each partial ramp in nanoseconds, by default
:class:`~quantify_scheduler.backends.qblox.constants.STITCHED_PULSE_PART_DURATION_NS`.
reference_magnitude : optional
Scaling value and unit for the unitless amplitude. Uses settings in
hardware config if not provided.
Returns
-------
StitchedPulse
A ``StitchedPulse`` composed of shorter ramp pulses with varying DC offsets,
forming one long ramp pulse.
"""
dur_ns = helpers.to_grid_time(duration)
num_whole_parts = (dur_ns - 1) // part_duration_ns
amp_part = part_duration_ns / dur_ns * amp
dur_left = (dur_ns - num_whole_parts * part_duration_ns) * 1e-9
amp_left = amp - num_whole_parts * amp_part
builder = StitchedPulseBuilder(
long_ramp_pulse.__name__, port=port, clock=clock, t0=t0
)
last_sample_voltage = offset
for _ in range(num_whole_parts):
# Add an offset for each ramp part, except for the first one if the overall ramp
# offset is 0.
if not (last_sample_voltage == offset and math.isclose(offset, 0.0)):
builder.add_voltage_offset(
path_I=last_sample_voltage,
path_Q=0.0,
reference_magnitude=reference_magnitude,
)
builder.add_pulse(
pulse_library.RampPulse(
amp=amp_part,
duration=part_duration_ns * 1e-9,
port=port,
reference_magnitude=reference_magnitude,
)
)
last_sample_voltage += amp_part
# For the final part, the voltage offset is set to 0, because the Qblox
# backend might otherwise lengthen the full operation by adding an
# 'UpdateParameters' instruction at the end.
# Insert a 0 offset if offsets were inserted above and the last offset is not 0.
if not math.isclose(last_sample_voltage, offset) and not math.isclose(
last_sample_voltage - amp_part, 0.0
):
builder.add_voltage_offset(path_I=0.0, path_Q=0.0)
builder.add_pulse(
pulse_library.RampPulse(
amp=amp_left,
offset=last_sample_voltage,
duration=dur_left,
port=port,
reference_magnitude=reference_magnitude,
)
)
pulse = builder.build()
return pulse