# Repository: https://gitlab.com/quantify-os/quantify-scheduler
# Licensed according to the LICENCE file on the main branch
"""
A module containing factory functions for measurements on the quantum-device layer.
These factories are used to take a parametrized representation of on a operation
and use that to create an instance of the operation itself.
"""
from __future__ import annotations
from typing import List, Literal
import warnings
import numpy as np
from quantify_scheduler import Operation
from quantify_scheduler.enums import BinMode
from quantify_scheduler.operations.acquisition_library import (
NumericalWeightedIntegrationComplex,
SSBIntegrationComplex,
Trace,
TriggerCount,
)
from quantify_scheduler.operations.pulse_library import SquarePulse, ResetClockPhase
# pylint: disable=too-many-arguments
[docs]def dispersive_measurement(
pulse_amp: float,
pulse_duration: float,
port: str,
clock: str,
acq_duration: float,
acq_delay: float,
acq_channel: int,
acq_index: int,
acq_protocol: Literal[
"SSBIntegrationComplex", "Trace", "NumericalWeightedIntegrationComplex"
]
| None,
pulse_type: Literal["SquarePulse"] = "SquarePulse",
bin_mode: BinMode | None = BinMode.AVERAGE,
acq_protocol_default: Literal[
"SSBIntegrationComplex", "Trace", "NumericalWeightedIntegrationComplex"
] = "SSBIntegrationComplex",
reset_clock_phase: bool = True,
acq_weights_a: List[complex] | np.ndarray | None = None,
acq_weights_b: List[complex] | np.ndarray | None = None,
acq_weights_sampling_rate: float | None = None,
) -> Operation:
"""
Generator function for a standard dispersive measurement.
A dispersive measurement (typically) exists of a pulse being applied to the device
followed by an acquisition protocol to interpret the signal coming back from the
device.
"""
# ensures default argument is used if not specified at gate level.
# ideally, this input would not be accepted, but this is a workaround for #267
if bin_mode is None:
bin_mode = BinMode.AVERAGE
# Note that the funny structure here comes from the fact that the measurement
# is a composite operation. We need to either introduce some level of nesting
# in the structure of arguments (to re-use our custom structure), or just keep
# this as a simple piece of code and accept that different protocols (e.g.,
# using different measurement pulses) would require a different generator function.
if pulse_type == "SquarePulse":
pulse_op = SquarePulse(
amp=pulse_amp,
duration=pulse_duration,
port=port,
clock=clock,
)
else:
# here we need to add support for SoftSquarePulse
raise NotImplementedError(
f'Invalid pulse_type "{pulse_type}" specified as argument to '
+ "dispersive_measurement. Currently dispersive_measurement only"
+ ' allows "SquarePulse". Please correct your device config.'
)
if reset_clock_phase:
device_op = ResetClockPhase(clock=clock)
device_op.add_pulse(pulse_op)
else:
device_op = pulse_op
if acq_protocol is None:
acq_protocol = acq_protocol_default
if acq_protocol == "SSBIntegrationComplex":
# readout pulse
device_op.add_acquisition(
SSBIntegrationComplex(
duration=acq_duration,
t0=acq_delay,
acq_channel=acq_channel,
acq_index=acq_index,
port=port,
clock=clock,
bin_mode=bin_mode,
)
)
elif acq_protocol == "Trace":
device_op.add_acquisition(
Trace(
clock=clock,
duration=acq_duration,
t0=acq_delay,
acq_channel=acq_channel,
acq_index=acq_index,
port=port,
)
)
elif acq_protocol == "NumericalWeightedIntegrationComplex":
if (
acq_weights_a is None
or acq_weights_b is None
or acq_weights_sampling_rate is None
):
raise TypeError(
f"Keyword arguments 'acq_weights_a', 'acq_weights_b' and "
f"'acq_weights_sampling_rate' must not be None when {acq_protocol=} is "
f"selected. These arguments can be specified in the device "
f"configuration."
)
dur_from_weights = len(acq_weights_a) / acq_weights_sampling_rate
if not np.isclose(acq_duration, dur_from_weights):
warnings.warn(
f"The specified weights and sampling rate lead to a weighted "
f"integration duration of {dur_from_weights:0.1e} s, which is "
f"different from the specified default acquisition duration of "
f"{acq_duration:0.1e} s. The default acquisition duration will be "
f"ignored for weighted acquisition.",
UserWarning,
)
device_op.add_acquisition(
NumericalWeightedIntegrationComplex(
weights_a=acq_weights_a,
weights_b=acq_weights_b,
weights_sampling_rate=acq_weights_sampling_rate,
port=port,
clock=clock,
acq_channel=acq_channel,
acq_index=acq_index,
bin_mode=bin_mode,
t0=acq_delay,
)
)
else:
raise ValueError(f'Acquisition protocol "{acq_protocol}" is not supported.')
return device_op
[docs]def optical_measurement(
pulse_amplitudes: List[float],
pulse_durations: List[float],
pulse_ports: List[str],
pulse_clocks: List[str],
acq_duration: float,
acq_delay: float,
acq_port: str,
acq_clock: str,
acq_channel: int,
acq_index: int,
bin_mode: BinMode | None,
acq_protocol: Literal["Trace", "TriggerCount"] | None,
acq_protocol_default: Literal["Trace", "TriggerCount"],
pulse_type: Literal["SquarePulse"],
) -> Operation:
# pylint: disable=too-many-locals
"""Generator function for an optical measurement with multiple excitation pulses.
An optical measurement generates a square pulse in the optical range and uses
either the Trace acquisition to return the output of a photon detector as a
function of time or the TriggerCount acquisition to return the number of photons
that are collected.
All pulses can have different amplitudes, durations, ports and clocks. All pulses
start simultaneously. The acquisition can have an ``acq_delay`` with respect to the
pulses. A negative ``acq_delay`` causes the acquisition to be scheduled at time 0
and the pulses at the positive time ``-acq_delay``.
Parameters
----------
pulse_amplitudes
list of amplitudes of the corresponding pulses
pulse_durations
list of durations of the corresponding pulses
pulse_ports
Port names, where the corresponding pulses are applied
pulse_clocks
Clock names of the corresponding pulses
acq_duration
Duration of the acquisition
acq_delay
Delay between the start of the readout pulse and the start of the acquisition:
acq_delay = t0_pulse - t0_acquisition.
acq_port
Port name of the acquisition
acq_clock
Clock name of the acquisition
acq_channel
Acquisition channel of the device element
acq_index
Acquisition index as defined in the Schedule
bin_mode
Describes what is done when data is written to a register that already
contains a value. Options are "append" which appends the result to the
list. "average" which stores the count value of the new result and the
old register value is not currently implemented. ``None`` internally
resolves to ``BinMode.APPEND``.
acq_protocol
Acquisition protocol. "Trace" returns a time trace of the collected signal.
"TriggerCount" returns the number of times the trigger threshold is surpassed.
acq_protocol_default
Acquisition protocol if ``acq_protocol`` is None
pulse_type
Shape of the pulse to be generated
Returns
-------
:
Operation with the generated pulses and acquisition
Raises
------
ValueError
If first four function arguments do not have the same length.
NotImplementedError
If an unknown ``pulse_type`` or ``acq_protocol`` are used.
"""
# ensures default argument is used if not specified at gate level.
# ideally, this input would not be accepted, but this is a workaround for #267
if bin_mode is None:
bin_mode = BinMode.APPEND
# All lists should be of equal length so this should be ensured
if (
not len(pulse_amplitudes)
== len(pulse_durations)
== len(pulse_ports)
== len(pulse_clocks)
):
raise ValueError(
"For multiple optical excitations, lists must have same length:\n"
+ f"{len(pulse_amplitudes)=},\n"
+ f"{len(pulse_durations)=},\n"
+ f"{len(pulse_ports)=},\n"
+ f"{len(pulse_clocks)=}"
)
# If acq_delay >= 0, the pulse starts at 0 and the acquisition at acq_delay
# If acq_delay < 0, the acquisition starts at 0 and the pulse at -acq_delay (which is positive)
t0_pulse = max(0, -acq_delay)
t0_acquisition = max(0, acq_delay)
# This operation will contain all pulses and the acquisition
device_op = Operation("OpticalMeasurement")
if pulse_type == "SquarePulse":
settings = zip(pulse_amplitudes, pulse_durations, pulse_ports, pulse_clocks)
for amp, dur, port, clock in settings:
device_op.add_pulse(
SquarePulse(
amp=amp,
duration=dur,
port=port,
clock=clock,
t0=t0_pulse,
)
)
else:
raise NotImplementedError(
f"Invalid pulse_type '{pulse_type}' specified as argument to "
f"optical_measurement. Currently, only 'SquarePulse' is accepted. "
f"Please correct your device config."
)
if acq_protocol is None:
acq_protocol = acq_protocol_default
if acq_protocol == "TriggerCount":
device_op.add_acquisition(
TriggerCount(
port=acq_port,
clock=acq_clock,
duration=acq_duration,
t0=t0_acquisition,
acq_channel=acq_channel,
acq_index=acq_index,
bin_mode=bin_mode,
)
)
elif acq_protocol == "Trace":
device_op.add_acquisition(
Trace(
port=acq_port,
clock=acq_clock,
duration=acq_duration,
t0=t0_acquisition,
acq_channel=acq_channel,
acq_index=acq_index,
)
)
else:
raise NotImplementedError(
f"Acquisition protocol '{acq_protocol}' is not supported. "
f"Currently, only 'TriggerCount' and 'Trace' are accepted."
)
return device_op