Source code for qblox_scheduler.operations.hardware_operations.pulse_factories

# Repository: https://gitlab.com/qblox/packages/software/qblox-scheduler
# Licensed according to the LICENSE file on the main branch
#
# Copyright 2020-2025, Quantify Consortium
# Copyright 2025, Qblox B.V.

"""
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

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from qblox_scheduler.operations.variables import Variable
import math
from collections.abc import Sequence
from typing import TYPE_CHECKING

import numpy as np

from qblox_scheduler.backends.qblox import constants, helpers
from qblox_scheduler.operations import pulse_library
from qblox_scheduler.operations.expressions import DType
from qblox_scheduler.operations.loop_domains import linspace
from qblox_scheduler.resources import BasebandClockResource
from qblox_scheduler.schedules.schedule import TimeableSchedule

if TYPE_CHECKING:
    from qblox_scheduler.operations.variables import Variable


[docs] def long_square_pulse( amp: complex | Variable | Sequence[float | Variable], duration: float, port: str, clock: str = BasebandClockResource.IDENTITY, t0: float = 0, reference_magnitude: pulse_library.ReferenceMagnitude | None = None, ) -> TimeableSchedule: """ Create a long square pulse using DC voltage offsets. .. warning:: This function creates a :class:`~qblox_scheduler.schedules.schedule.TimeableSchedule` object, containing a combination of voltage offsets and waveforms. Overlapping Schedules with VoltageOffsets in time 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 TimeableSchedule. By default 0. reference_magnitude : Optional Scaling value and unit for the unitless amplitude. Uses settings in hardware config if not provided. Returns ------- TimeableSchedule A Schedule 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``. """ if duration * 1e9 < constants.MIN_TIME_BETWEEN_OPERATIONS: raise ValueError( f"The duration of a long_square_pulse must be at least " f"{constants.MIN_TIME_BETWEEN_OPERATIONS} ns." f" Duration of offending operation: {duration}." f" Start time: {t0}" ) if isinstance(amp, Sequence): amplitude_path_I = amp[0] amplitude_path_Q = amp[1] elif isinstance(amp, complex): amplitude_path_I = amp.real amplitude_path_Q = amp.imag else: amplitude_path_I = amp amplitude_path_Q = 0 sched = TimeableSchedule(long_square_pulse.__name__) if duration > constants.MIN_TIME_BETWEEN_OPERATIONS * 1e-9: sched.add( pulse_library.VoltageOffset( offset_path_I=amplitude_path_I, offset_path_Q=amplitude_path_Q, port=port, clock=clock, reference_magnitude=reference_magnitude, ), rel_time=t0, ) sched.add( pulse_library.VoltageOffset( offset_path_I=0.0, offset_path_Q=0.0, port=port, clock=clock, reference_magnitude=reference_magnitude, ), rel_time=duration - constants.MIN_TIME_BETWEEN_OPERATIONS * 1e-9, ) sched.add( pulse_library.SquarePulse( amp=amp, duration=constants.MIN_TIME_BETWEEN_OPERATIONS * 1e-9, port=port, clock=clock, reference_magnitude=reference_magnitude, ) ) return sched
[docs] def long_chirp_pulse( amp: float, duration: float, port: str, start_freq: float, end_freq: float, clock: str = BasebandClockResource.IDENTITY, t0: float = 0, part_duration_ns: int = constants.STITCHED_PULSE_PART_DURATION_NS, reference_magnitude: pulse_library.ReferenceMagnitude | None = None, ) -> TimeableSchedule: """ Create a long chirp pulse using SetClockFrequency. 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. start_freq : float Start frequency of the Chirp. Note that this is the frequency at which the waveform is calculated, this may differ from the clock frequency. end_freq : float End frequency of the Chirp. 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 TimeableSchedule. By default 0. part_duration_ns : int, Optional Chunk size in nanoseconds. reference_magnitude : Optional Scaling value and unit for the unitless amplitude. Uses settings in hardware config if not provided. Returns ------- TimeableSchedule A TimeableSchedule object describing a chirp pulse. Raises ------ ValueError When the duration of the pulse is not a multiple of ``grid_time_ns``. """ dur_ns = helpers.to_grid_time(duration) num_whole_parts = int(dur_ns / part_duration_ns) dur_left = dur_ns - num_whole_parts * part_duration_ns chunk_duration_sec = part_duration_ns * 1e-9 schedule = TimeableSchedule("Long_Chirp_Pulse") current_freq = start_freq if num_whole_parts > 0: frequency_step = (end_freq - start_freq) / num_whole_parts phase_shift_rad = np.pi * frequency_step * chunk_duration_sec phase_shift_deg = np.rad2deg(phase_shift_rad) % 360 chirp_pulse = pulse_library.ChirpPulse( amp=amp, duration=chunk_duration_sec, clock=clock, start_freq=0.0, end_freq=frequency_step, port=port, reference_magnitude=reference_magnitude, ) for i in range(num_whole_parts): schedule.add( pulse_library.SetClockFrequency( clock_freq_new=current_freq, clock=clock, ), rel_time=t0 if i == 0 else 0, ) schedule.add(chirp_pulse) schedule.add( pulse_library.ShiftClockPhase( phase_shift=phase_shift_deg, clock=clock, ) ) current_freq += frequency_step if dur_left > 0: # Final chunk is played with waveform again schedule.add( pulse_library.SetClockFrequency( clock_freq_new=start_freq, clock=clock, ), rel_time=t0 if num_whole_parts == 0 else 0, ) schedule.add( pulse_library.ChirpPulse( amp=amp, duration=dur_left * 1e-9, clock=clock, start_freq=current_freq - start_freq, end_freq=end_freq - start_freq, port=port, reference_magnitude=reference_magnitude, ) ) # Reset to the initial clock. schedule.add( pulse_library.SetClockFrequency( clock_freq_new=None, clock=clock, ) ) return schedule
[docs] def staircase_pulse( start_amp: float, final_amp: float, num_steps: int, duration: float, port: str, clock: str = BasebandClockResource.IDENTITY, t0: float = 0, min_operation_time_ns: int = constants.MIN_TIME_BETWEEN_OPERATIONS, reference_magnitude: pulse_library.ReferenceMagnitude | None = None, ) -> TimeableSchedule: """ 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:`~qblox_scheduler.schedules.schedule.TimeableSchedule` object, containing a combination of voltage offsets and waveforms. Overlapping Schedules with VoltageOffsets in time 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 TimeableSchedule. By default 0. min_operation_time_ns : int, Optional Min operation time in ns. The duration of the long_square_pulse must be a multiple of this. By default equal to the min operation time time of Qblox modules. reference_magnitude : Optional Scaling value and unit for the unitless amplitude. Uses settings in hardware config if not provided. Returns ------- TimeableSchedule A Schedule object containing incrementing or decrementing offset instructions. Raises ------ ValueError When the duration of a step is not a multiple of ``grid_time_ns``. """ sched = TimeableSchedule(staircase_pulse.__name__) try: step_duration = helpers.to_grid_time(duration / num_steps, min_operation_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" {min_operation_time_ns} ns." ) from err if num_steps == 0: raise ValueError("Cannot create a staircase pulse with 0 steps.") elif num_steps == 1: return long_square_pulse( amp=final_amp, duration=duration, port=port, clock=clock, t0=t0, reference_magnitude=reference_magnitude, ) amp_step = (final_amp - start_amp) / (num_steps - 1) # The final step is a special case, see below. with sched.loop( linspace(start_amp, final_amp - amp_step, num_steps - 1, dtype=DType.AMPLITUDE), rel_time=t0 ) as amp: sched.add( pulse_library.VoltageOffset( offset_path_I=amp, offset_path_Q=0.0, port=port, clock=clock, reference_magnitude=reference_magnitude, ) ) sched.add(pulse_library.IdlePulse(step_duration)) # 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. sched.add( pulse_library.VoltageOffset( offset_path_I=final_amp, offset_path_Q=0.0, port=port, clock=clock, reference_magnitude=reference_magnitude, ) ) sched.add( pulse_library.VoltageOffset( offset_path_I=0.0, offset_path_Q=0.0, port=port, clock=clock, reference_magnitude=reference_magnitude, ), rel_time=step_duration - min_operation_time_ns * 1e-9, ) sched.add( pulse_library.SquarePulse( amp=final_amp, duration=min_operation_time_ns * 1e-9, port=port, clock=clock, reference_magnitude=reference_magnitude, ) ) return sched
[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, ) -> TimeableSchedule: """ 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:`~qblox_scheduler.schedules.schedule.TimeableSchedule` object, containing a combination of voltage offsets and waveforms. Overlapping Schedules with VoltageOffsets in time 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 the baseband clock. t0 : float, Optional Time in seconds when to start the pulses relative to the start time of the Operation in the TimeableSchedule. By default 0. part_duration_ns : int, Optional Duration of each partial ramp in nanoseconds, by default :class:`~qblox_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 ------- TimeableSchedule A ``TimeableSchedule`` 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 sched = TimeableSchedule(long_ramp_pulse.__name__) if num_whole_parts > 0: with sched.loop( linspace( offset, offset + (num_whole_parts - 1) * amp_part, num_whole_parts, dtype=DType.AMPLITUDE, ), rel_time=t0, ) as offset_part: sched.add( pulse_library.VoltageOffset( offset_path_I=offset_part, offset_path_Q=0.0, port=port, clock=clock, reference_magnitude=reference_magnitude, ) ) sched.add( pulse_library.RampPulse( amp=amp_part, duration=part_duration_ns * 1e-9, port=port, clock=clock, reference_magnitude=reference_magnitude, ) ) last_sample_voltage = offset + num_whole_parts * 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 ): sched.add( pulse_library.VoltageOffset( offset_path_I=0.0, offset_path_Q=0.0, port=port, clock=clock, reference_magnitude=reference_magnitude, ) ) sched.add( pulse_library.RampPulse( amp=amp_left, offset=last_sample_voltage, duration=dur_left, port=port, clock=clock, reference_magnitude=reference_magnitude, ) ) return sched