diff --git a/src/qibolab/_core/instruments/qm/components/configs.py b/src/qibolab/_core/instruments/qm/components/configs.py index 0ffbccf7e..4a9a9067f 100644 --- a/src/qibolab/_core/instruments/qm/components/configs.py +++ b/src/qibolab/_core/instruments/qm/components/configs.py @@ -2,9 +2,18 @@ from pydantic import Field -from qibolab._core.components import AcquisitionConfig, DcConfig +from qibolab._core.components import AcquisitionConfig, DcConfig, OscillatorConfig -__all__ = ["OpxOutputConfig", "QmAcquisitionConfig", "QmConfigs"] +__all__ = [ + "OpxOutputConfig", + "QmAcquisitionConfig", + "QmConfigs", + "OctaveOscillatorConfig", +] + +OctaveOutputModes = Literal[ + "always_on", "always_off", "triggered", "triggered_reversed" +] class OpxOutputConfig(DcConfig): @@ -25,6 +34,15 @@ class OpxOutputConfig(DcConfig): for more details. Changing the filters affects the calibration of single shot discrimination (threshold and angle). """ + output_mode: Literal["direct", "amplified"] = "direct" + + +class OctaveOscillatorConfig(OscillatorConfig): + """Oscillator confing that allows switching the output mode.""" + + kind: Literal["octave-oscillator"] = "octave-oscillator" + + output_mode: OctaveOutputModes = "triggered" class QmAcquisitionConfig(AcquisitionConfig): @@ -41,4 +59,4 @@ class QmAcquisitionConfig(AcquisitionConfig): """Constant voltage to be applied on the input.""" -QmConfigs = Union[OpxOutputConfig, QmAcquisitionConfig] +QmConfigs = Union[OpxOutputConfig, OctaveOscillatorConfig, QmAcquisitionConfig] diff --git a/src/qibolab/_core/instruments/qm/config/__init__.py b/src/qibolab/_core/instruments/qm/config/__init__.py index c8d587388..d4567a43e 100644 --- a/src/qibolab/_core/instruments/qm/config/__init__.py +++ b/src/qibolab/_core/instruments/qm/config/__init__.py @@ -1,2 +1,3 @@ from .config import Configuration +from .devices import ControllerId, ModuleTypes from .pulses import SAMPLING_RATE, operation diff --git a/src/qibolab/_core/instruments/qm/config/config.py b/src/qibolab/_core/instruments/qm/config/config.py index 25bb7e49c..2c1131aed 100644 --- a/src/qibolab/_core/instruments/qm/config/config.py +++ b/src/qibolab/_core/instruments/qm/config/config.py @@ -12,7 +12,17 @@ from qibolab._core.pulses import Pulse, Readout from ..components import OpxOutputConfig, QmAcquisitionConfig -from .devices import AnalogOutput, Controller, Octave, OctaveInput, OctaveOutput +from .devices import ( + AnalogOutput, + Controller, + ControllerId, + Controllers, + FemAnalogOutput, + ModuleTypes, + Octave, + OctaveInput, + OctaveOutput, +) from .elements import AcquireOctaveElement, DcElement, Element, RfOctaveElement from .pulses import ( QmAcquisition, @@ -42,7 +52,7 @@ class Configuration: """ version: int = 1 - controllers: dict[str, Controller] = field(default_factory=dict) + controllers: Controllers = field(default_factory=Controllers) octaves: dict[str, Octave] = field(default_factory=dict) elements: dict[str, Element] = field(default_factory=dict) pulses: dict[str, Union[QmPulse, QmAcquisition]] = field(default_factory=dict) @@ -53,20 +63,27 @@ class Configuration: integration_weights: dict = field(default_factory=dict) mixers: dict = field(default_factory=dict) - def add_controller(self, device: str): + def add_controller(self, device: ControllerId, modules: dict[str, ModuleTypes]): if device not in self.controllers: - self.controllers[device] = Controller() + self.controllers[device] = Controller(type=modules[device]) - def add_octave(self, device: str, connectivity: str): + def add_octave( + self, device: str, connectivity: ControllerId, modules: dict[str, ModuleTypes] + ): if device not in self.octaves: - self.add_controller(connectivity) + self.add_controller(connectivity, modules) self.octaves[device] = Octave(connectivity) def configure_dc_line( self, id: ChannelId, channel: DcChannel, config: OpxOutputConfig ): controller = self.controllers[channel.device] - controller.analog_outputs[channel.port] = AnalogOutput.from_config(config) + if controller.type == "opx1": + controller.analog_outputs[channel.port] = AnalogOutput.from_config(config) + else: + controller.analog_outputs[channel.port] = FemAnalogOutput.from_config( + config + ) self.elements[id] = DcElement.from_channel(channel) def configure_iq_line( @@ -116,7 +133,11 @@ def configure_acquire_line( ) def register_waveforms( - self, pulse: Pulse, element: Optional[str] = None, dc: bool = False + self, + pulse: Pulse, + max_voltage: float, + element: Optional[str] = None, + dc: bool = False, ): if dc: qmpulse = QmPulse.from_dc_pulse(pulse) @@ -125,7 +146,7 @@ def register_waveforms( qmpulse = QmPulse.from_pulse(pulse) else: qmpulse = QmAcquisition.from_pulse(pulse, element) - waveforms = waveforms_from_pulse(pulse) + waveforms = waveforms_from_pulse(pulse, max_voltage) if dc: self.waveforms[qmpulse.waveforms["single"]] = waveforms["I"] else: @@ -133,26 +154,30 @@ def register_waveforms( self.waveforms[getattr(qmpulse.waveforms, mode)] = waveforms[mode] return qmpulse - def register_iq_pulse(self, element: str, pulse: Pulse): + def register_iq_pulse(self, element: str, pulse: Pulse, max_voltage: float): op = operation(pulse) if op not in self.pulses: - self.pulses[op] = self.register_waveforms(pulse) + self.pulses[op] = self.register_waveforms(pulse, max_voltage) self.elements[element].operations[op] = op return op - def register_dc_pulse(self, element: str, pulse: Pulse): + def register_dc_pulse(self, element: str, pulse: Pulse, max_voltage: float): op = operation(pulse) if op not in self.pulses: - self.pulses[op] = self.register_waveforms(pulse, dc=True) + self.pulses[op] = self.register_waveforms(pulse, max_voltage, dc=True) self.elements[element].operations[op] = op return op - def register_acquisition_pulse(self, element: str, readout: Readout): + def register_acquisition_pulse( + self, element: str, readout: Readout, max_voltage: float + ): """Registers pulse, waveforms and integration weights in QM config.""" op = operation(readout) acquisition = f"{op}_{element}" if acquisition not in self.pulses: - self.pulses[acquisition] = self.register_waveforms(readout.probe, element) + self.pulses[acquisition] = self.register_waveforms( + readout.probe, max_voltage, element + ) self.elements[element].operations[op] = acquisition return op diff --git a/src/qibolab/_core/instruments/qm/config/devices.py b/src/qibolab/_core/instruments/qm/config/devices.py index 9cc8665d0..35250b511 100644 --- a/src/qibolab/_core/instruments/qm/config/devices.py +++ b/src/qibolab/_core/instruments/qm/config/devices.py @@ -1,14 +1,25 @@ from dataclasses import dataclass, field -from typing import Any, Generic, TypeVar +from typing import Generic, Literal, TypeVar, Union from qibolab._core.components import OscillatorConfig -from ..components import OpxOutputConfig, QmAcquisitionConfig +from ..components import OctaveOscillatorConfig, OpxOutputConfig, QmAcquisitionConfig +from ..components.configs import OctaveOutputModes -__all__ = ["AnalogOutput", "OctaveOutput", "OctaveInput", "Controller", "Octave"] +__all__ = [ + "AnalogOutput", + "FemAnalogOutput", + "ModuleTypes", + "OctaveOutput", + "OctaveInput", + "Controller", + "Octave", + "ControllerId", + "Controllers", +] -DEFAULT_INPUTS = {"1": {}, "2": {}} +DEFAULT_INPUTS = {"1": {"offset": 0}, "2": {"offset": 0}} """Default controller config section. Inputs are always registered to avoid issues with automatic mixer @@ -25,7 +36,7 @@ class PortDict(Generic[V], dict[str, V]): in the QUA config. """ - def __setitem__(self, key: Any, value: V): + def __setitem__(self, key: Union[str, int], value: V): super().__setitem__(str(key), value) @@ -39,6 +50,17 @@ def from_config(cls, config: OpxOutputConfig): return cls(offset=config.offset, filter=config.filter) +@dataclass(frozen=True) +class FemAnalogOutput(AnalogOutput): + output_mode: Literal["direct", "amplified"] = "direct" + + @classmethod + def from_config(cls, config: OpxOutputConfig): + return cls( + offset=config.offset, filter=config.filter, output_mode=config.output_mode + ) + + @dataclass(frozen=True) class AnalogInput: offset: float = 0.0 @@ -53,24 +75,32 @@ def from_config(cls, config: QmAcquisitionConfig): class OctaveOutput: LO_frequency: int gain: int = 0 - LO_source: str = "internal" - output_mode: str = "triggered" + LO_source: Literal["internal", "external"] = "internal" + output_mode: OctaveOutputModes = "triggered" @classmethod - def from_config(cls, config: OscillatorConfig): - return cls(LO_frequency=config.frequency, gain=config.power) + def from_config(cls, config: Union[OscillatorConfig, OctaveOscillatorConfig]): + kwargs = dict(LO_frequency=config.frequency, gain=config.power) + if isinstance(config, OctaveOscillatorConfig): + kwargs["output_mode"] = config.output_mode + return cls(**kwargs) @dataclass(frozen=True) class OctaveInput: LO_frequency: int - LO_source: str = "internal" - IF_mode_I: str = "direct" - IF_mode_Q: str = "direct" + LO_source: Literal["internal", "external"] = "internal" + IF_mode_I: Literal["direct", "envelop", "mixer"] = "direct" + IF_mode_Q: Literal["direct", "envelop", "mixer"] = "direct" + + +ModuleTypes = Literal["opx1", "LF", "MW"] @dataclass class Controller: + type: ModuleTypes = "opx1" + """https://docs.quantum-machines.co/latest/docs/Introduction/config/?h=opx10#controllers""" analog_outputs: PortDict[dict[str, AnalogOutput]] = field(default_factory=PortDict) digital_outputs: PortDict[dict[str, dict]] = field(default_factory=PortDict) analog_inputs: PortDict[dict[str, AnalogInput]] = field( @@ -90,8 +120,66 @@ def add_octave_input(self, port: int, config: QmAcquisitionConfig): ) +@dataclass +class Opx1000: + type: Literal["opx1000"] = "opx1000" + fems: dict[str, Controller] = field(default_factory=PortDict) + + @dataclass class Octave: - connectivity: str + connectivity: Union[str, tuple[str, int]] RF_outputs: PortDict[dict[str, OctaveOutput]] = field(default_factory=PortDict) RF_inputs: PortDict[dict[str, OctaveInput]] = field(default_factory=PortDict) + + def __post_init__(self): + if "/" in self.connectivity: + con, fem = self.connectivity.split("/") + self.connectivity = (con, int(fem)) + + +ControllerId = Union[str, tuple[str, int]] + + +def process_controller_id(id: ControllerId): + """Convert controller identifier depending on cluster type. + + For OPX+ clusters ``id`` is just the controller name (eg. 'con1'). + For OPX1000 clusters ``id`` has the format + '{controller_name}/{fem_number}' (eg. 'con1/4'). + In that case ``id`` may also be a ``tuple`` + `(controller_name, fem_number)` + """ + if isinstance(id, tuple): + con, fem = id + return con, str(fem) + if "/" in id: + return id.split("/") + return id, None + + +class Controllers(dict[str, Union[Controller, Opx1000]]): + """Dictionary of controllers compatible with OPX+ and OPX1000.""" + + def __contains__(self, key: ControllerId) -> bool: + con, fem = process_controller_id(key) + contains = super().__contains__(con) + if fem is None: + return contains + return contains and fem in self[con].fems + + def __getitem__(self, key: ControllerId) -> Controller: + con, fem = process_controller_id(key) + value = super().__getitem__(con) + if fem is None: + return value + return value.fems[fem] + + def __setitem__(self, key: ControllerId, value: Controller): + con, fem = process_controller_id(key) + if fem is None: + super().__setitem__(key, value) + else: + if con not in self: + super().__setitem__(con, Opx1000()) + self[con].fems[fem] = value diff --git a/src/qibolab/_core/instruments/qm/config/elements.py b/src/qibolab/_core/instruments/qm/config/elements.py index 1be37b276..f3820da2e 100644 --- a/src/qibolab/_core/instruments/qm/config/elements.py +++ b/src/qibolab/_core/instruments/qm/config/elements.py @@ -1,36 +1,22 @@ from dataclasses import dataclass, field from typing import Union -import numpy as np +from typing_extensions import TypedDict from qibolab._core.components import Channel __all__ = ["DcElement", "RfOctaveElement", "AcquireOctaveElement", "Element"] -def iq_imbalance(g, phi): - """Create the correction matrix for the mixer imbalance. - - Mixer imbalance is caused by the gain and phase imbalances. - - More information here: - https://docs.qualang.io/libs/examples/mixer-calibration/#non-ideal-mixer - - Args: - g (float): relative gain imbalance between the I & Q ports (unit-less). - Set to 0 for no gain imbalance. - phi (float): relative phase imbalance between the I & Q ports (radians). - Set to 0 for no phase imbalance. - """ - c = np.cos(phi) - s = np.sin(phi) - N = 1 / ((1 - g**2) * (2 * c**2 - 1)) - return [float(N * x) for x in [(1 - g) * c, (1 + g) * s, (1 - g) * s, (1 + g) * c]] +InOutType = Union[tuple[str, int], tuple[str, int, int]] +OctavePort = TypedDict("OpxPlusPort", {"port": tuple[str, int]}) +Port = TypedDict("Port", {"port": InOutType}) +ConnectivityType = Union[str, tuple[str, int]] @dataclass(frozen=True) class OutputSwitch: - port: tuple[str, int] + port: InOutType delay: int = 57 buffer: int = 18 """Default calibration parameters for digital pulses. @@ -41,19 +27,33 @@ class OutputSwitch: """ -def _to_port(channel: Channel) -> dict[str, tuple[str, int]]: - """Convert a channel to the port dictionary required for the QUA config.""" - return {"port": (channel.device, channel.port)} +def _to_port(channel: Channel) -> Port: + """Convert a channel to the port dictionary required for the QUA config. + + The following syntax is assumed for ``channel.device``: + * For OPX+ clusters: string with the device name (eg. 'con1') + * For OPX1000 clusters: string of '{device_name}/{fem_number}' + """ + if "/" not in channel.device: + port = (channel.device, channel.port) + else: + con, fem = channel.device.split("/") + port = (con, int(fem), channel.port) + return {"port": port} -def output_switch(opx: str, port: int): +def output_switch(connectivity: ConnectivityType, port: int): """Create output switch section.""" - return {"output_switch": OutputSwitch((opx, 2 * port - 1))} + if isinstance(connectivity, tuple): + args = connectivity + (2 * port - 1,) + else: + args = (connectivity, 2 * port - 1) + return {"output_switch": OutputSwitch(args)} @dataclass class DcElement: - singleInput: dict[str, tuple[str, int]] + singleInput: Port intermediate_frequency: int = 0 operations: dict[str, str] = field(default_factory=dict) @@ -62,16 +62,22 @@ def from_channel(cls, channel: Channel): return cls(_to_port(channel)) +DigitalInputs = TypedDict("digitalInputs", {"output_switch": OutputSwitch}) + + @dataclass class RfOctaveElement: - RF_inputs: dict[str, tuple[str, int]] - digitalInputs: dict[str, OutputSwitch] + RF_inputs: OctavePort + digitalInputs: DigitalInputs intermediate_frequency: int operations: dict[str, str] = field(default_factory=dict) @classmethod def from_channel( - cls, channel: Channel, connectivity: str, intermediate_frequency: int + cls, + channel: Channel, + connectivity: ConnectivityType, + intermediate_frequency: int, ): return cls( _to_port(channel), @@ -82,9 +88,9 @@ def from_channel( @dataclass class AcquireOctaveElement: - RF_inputs: dict[str, tuple[str, int]] - RF_outputs: dict[str, tuple[str, int]] - digitalInputs: dict[str, OutputSwitch] + RF_inputs: OctavePort + RF_outputs: OctavePort + digitalInputs: DigitalInputs intermediate_frequency: int time_of_flight: int = 24 smearing: int = 0 @@ -95,7 +101,7 @@ def from_channel( cls, probe_channel: Channel, acquire_channel: Channel, - connectivity: str, + connectivity: ConnectivityType, intermediate_frequency: int, time_of_flight: int, smearing: int, diff --git a/src/qibolab/_core/instruments/qm/config/pulses.py b/src/qibolab/_core/instruments/qm/config/pulses.py index 983dc4d2b..beb803ba4 100644 --- a/src/qibolab/_core/instruments/qm/config/pulses.py +++ b/src/qibolab/_core/instruments/qm/config/pulses.py @@ -8,8 +8,7 @@ SAMPLING_RATE = 1 """Sampling rate of Quantum Machines OPX+ in GSps.""" -MAX_VOLTAGE_OUTPUT = 0.5 -"""Maximum output of Quantum Machines OPX+ in Volts.""" + __all__ = [ "operation", @@ -42,9 +41,9 @@ class ConstantWaveform: type: str = "constant" @classmethod - def from_pulse(cls, pulse: Pulse) -> dict[str, "Waveform"]: + def from_pulse(cls, pulse: Pulse, max_voltage: float) -> dict[str, "Waveform"]: phase = wrap_phase(pulse.relative_phase) - voltage_amp = pulse.amplitude * MAX_VOLTAGE_OUTPUT + voltage_amp = pulse.amplitude * max_voltage return { "I": cls(voltage_amp * np.cos(phase)), "Q": cls(voltage_amp * np.sin(phase)), @@ -57,8 +56,8 @@ class ArbitraryWaveform: type: str = "arbitrary" @classmethod - def from_pulse(cls, pulse: Pulse) -> dict[str, "Waveform"]: - original_waveforms = pulse.envelopes(SAMPLING_RATE) * MAX_VOLTAGE_OUTPUT + def from_pulse(cls, pulse: Pulse, max_voltage: float) -> dict[str, "Waveform"]: + original_waveforms = pulse.envelopes(SAMPLING_RATE) * max_voltage rotated_waveforms = rotate(original_waveforms, pulse.relative_phase) new_duration = baked_duration(pulse.duration) pad_len = new_duration - int(pulse.duration) @@ -72,7 +71,7 @@ def from_pulse(cls, pulse: Pulse) -> dict[str, "Waveform"]: Waveform = Union[ConstantWaveform, ArbitraryWaveform] -def waveforms_from_pulse(pulse: Pulse) -> dict[str, Waveform]: +def waveforms_from_pulse(pulse: Pulse, max_voltage: float) -> dict[str, Waveform]: """Register QM waveforms for a given pulse.""" needs_baking = pulse.duration < 16 or pulse.duration % 4 != 0 wvtype = ( @@ -80,7 +79,7 @@ def waveforms_from_pulse(pulse: Pulse) -> dict[str, Waveform]: if isinstance(pulse.envelope, Rectangular) and not needs_baking else ArbitraryWaveform ) - return wvtype.from_pulse(pulse) + return wvtype.from_pulse(pulse, max_voltage) @dataclass(frozen=True) diff --git a/src/qibolab/_core/instruments/qm/controller.py b/src/qibolab/_core/instruments/qm/controller.py index f8efb7a21..805196556 100644 --- a/src/qibolab/_core/instruments/qm/controller.py +++ b/src/qibolab/_core/instruments/qm/controller.py @@ -27,10 +27,10 @@ from qibolab._core.pulses import Acquisition, Align, Delay, Pulse, Readout from qibolab._core.sequence import PulseSequence from qibolab._core.sweeper import ParallelSweepers, Parameter, Sweeper -from qibolab._core.unrolling import Bounds, unroll_sequences +from qibolab._core.unrolling import unroll_sequences from .components import OpxOutputConfig, QmAcquisitionConfig -from .config import SAMPLING_RATE, Configuration +from .config import SAMPLING_RATE, Configuration, ControllerId, ModuleTypes from .program import ExecutionArguments, create_acquisition, program from .program.sweepers import find_lo_frequencies, sweeper_amplitude @@ -39,11 +39,18 @@ __all__ = ["QmController", "Octave"] -BOUNDS = Bounds( - waveforms=int(4e4), - readout=30, - instructions=int(1e6), -) +MAX_VOLTAGE = 0.5 +"""Maximum output of Quantum Machines OPX+ in Volts.""" +MAX_VOLTAGE_AMPLIFIED = 2.5 +"""Maximum output of Quantum Machines OPX1000 FEMs in amplified mode in +Volts.""" + + +def channel_max_voltage(config: Config): + """Find maximum voltage of a channel from the associated ``config``.""" + if hasattr(config, "output_mode") and config.output_mode == "amplified": + return MAX_VOLTAGE_AMPLIFIED + return MAX_VOLTAGE @dataclass(frozen=True) @@ -54,7 +61,7 @@ class Octave: """Name of the device.""" port: int """Network port of the Octave in the cluster configuration.""" - connectivity: str + connectivity: ControllerId """OPXplus that acts as the waveform generator for the Octave.""" @@ -133,9 +140,15 @@ class QmController(Controller): """ octaves: dict[str, Octave] = Field(default_factory=dict) - """Dictionary containing the - :class:`qibolab.instruments.qm.controller.Octave` instruments being - used.""" + """Dictionary containing the Octaves used.""" + fems: dict[str, ModuleTypes] = Field( + default_factory=lambda: defaultdict(lambda: "opx1") + ) + """Dictionary containing the FEM types (for OPX1000 clusters). + + Defaults to 'opx1' type to maintain original behavior for OPX+ clusters + where ``fems`` are not given. + """ bounds: str = "qm/bounds" """Maximum bounds used for batching in sequence unrolling.""" @@ -223,15 +236,14 @@ def disconnect(self): self._reset_temporary_calibration() if self.manager is not None: self.manager.close_all_quantum_machines() - self.manager.close() self.manager = None def configure_device(self, device: str): """Add device in the ``config``.""" if "octave" in device: - self.config.add_octave(device, self.octaves[device].connectivity) + self.config.add_octave(device, self.octaves[device].connectivity, self.fems) else: - self.config.add_controller(device) + self.config.add_controller(device, self.fems) def configure_channel( self, channel: ChannelId, configs: dict[str, Config] @@ -293,20 +305,23 @@ def configure_channels( probe_map[probe] = id return probe_map - def register_pulse(self, channel: ChannelId, pulse: Union[Pulse, Readout]) -> str: + def register_pulse( + self, channel: ChannelId, config: Config, pulse: Union[Pulse, Readout] + ) -> str: """Add pulse in the QM ``config``. And return corresponding operation. """ ch = self.channels[channel] + max_voltage = channel_max_voltage(config) if isinstance(ch, DcChannel): assert isinstance(pulse, Pulse) - return self.config.register_dc_pulse(channel, pulse) + return self.config.register_dc_pulse(channel, pulse, max_voltage) if isinstance(ch, IqChannel): assert isinstance(pulse, Pulse) - return self.config.register_iq_pulse(channel, pulse) + return self.config.register_iq_pulse(channel, pulse, max_voltage) assert isinstance(pulse, Readout) - return self.config.register_acquisition_pulse(channel, pulse) + return self.config.register_acquisition_pulse(channel, pulse, max_voltage) def register_pulses(self, configs: dict[str, Config], sequence: PulseSequence): """Adds all pulses except measurements of a given sequence in the QM @@ -321,14 +336,13 @@ def register_pulses(self, configs: dict[str, Config], sequence: PulseSequence): f"Quantum Machines cannot play pulse with duration {pulse.duration}. " "Only integer duration in ns is supported." ) - if isinstance(pulse, Pulse): - self.register_pulse(id, pulse) + self.register_pulse(id, configs[id], pulse) elif isinstance(pulse, Readout): - self.register_pulse(id, pulse) + self.register_pulse(id, configs[id], pulse) def register_duration_sweeper_pulses( - self, args: ExecutionArguments, sweeper: Sweeper + self, args: ExecutionArguments, configs: dict[str, Config], sweeper: Sweeper ): """Register pulse with many different durations. @@ -345,11 +359,11 @@ def register_duration_sweeper_pulses( ) for value in sweeper.values.astype(int): sweep_pulse = original_pulse.model_copy(update={"duration": value}) - sweep_op = self.register_pulse(ids[0], sweep_pulse) + sweep_op = self.register_pulse(ids[0], configs[ids[0]], sweep_pulse) params.duration_ops.append((value, sweep_op)) def register_amplitude_sweeper_pulses( - self, args: ExecutionArguments, sweeper: Sweeper + self, args: ExecutionArguments, configs: dict[str, Config], sweeper: Sweeper ): """Register pulse with different amplitude. @@ -362,7 +376,9 @@ def register_amplitude_sweeper_pulses( ids = args.sequence.pulse_channels(pulse.id) params = args.parameters[pulse.id] params.amplitude_pulse = sweep_pulse - params.amplitude_op = self.register_pulse(ids[0], sweep_pulse) + params.amplitude_op = self.register_pulse( + ids[0], configs[ids[0]], sweep_pulse + ) def register_acquisitions( self, @@ -385,7 +401,11 @@ def register_acquisitions( "Quantum Machines does not support acquisition with different duration than probe." ) - op = self.config.register_acquisition_pulse(channel_id, readout) + probe_id = self.channels[channel_id].probe + max_voltage = channel_max_voltage(configs[probe_id]) + op = self.config.register_acquisition_pulse( + channel_id, readout, max_voltage + ) acq_config = configs[channel_id] assert isinstance(acq_config, QmAcquisitionConfig) @@ -421,10 +441,11 @@ def preprocess_sweeps( for sweeper in find_sweepers(sweepers, Parameter.offset): for id in sweeper.channels: args.parameters[id].element = id + args.parameters[id].max_offset = channel_max_voltage(configs[id]) for sweeper in find_sweepers(sweepers, Parameter.amplitude): - self.register_amplitude_sweeper_pulses(args, sweeper) + self.register_amplitude_sweeper_pulses(args, configs, sweeper) for sweeper in find_sweepers(sweepers, Parameter.duration): - self.register_duration_sweeper_pulses(args, sweeper) + self.register_duration_sweeper_pulses(args, configs, sweeper) def execute_program(self, program): """Executes an arbitrary program written in QUA language.""" @@ -460,9 +481,11 @@ def play( # register DC elements so that all qubits are # sweetspot even when they are not used + offsets = [] for id, channel in self.channels.items(): if isinstance(channel, DcChannel): self.configure_channel(id, configs) + offsets.append((id, configs[id].offset)) probe_map = self.configure_channels(configs, sequence.channels) self.register_pulses(configs, sequence) @@ -470,7 +493,7 @@ def play( args = ExecutionArguments(sequence, acquisitions, options.relaxation_time) self.preprocess_sweeps(sweepers, configs, args, probe_map) - experiment = program(args, options, sweepers) + experiment = program(args, options, sweepers, offsets) if self.script_file_name is not None: script_config = ( diff --git a/src/qibolab/_core/instruments/qm/program/acquisition.py b/src/qibolab/_core/instruments/qm/program/acquisition.py index 9aea9d079..b7e21afe2 100644 --- a/src/qibolab/_core/instruments/qm/program/acquisition.py +++ b/src/qibolab/_core/instruments/qm/program/acquisition.py @@ -56,7 +56,8 @@ class Acquisition(ABC): @property def name(self): """Identifier to download results from the instruments.""" - return f"{self.operation}_{self.element}" + # FIXME: QUA 1.2.1a2 and OPX1000 don't like `/` character in stream processing ``save`` + return f"{self.operation}_{self.element}".replace("/", "|") @property def npulses(self): diff --git a/src/qibolab/_core/instruments/qm/program/arguments.py b/src/qibolab/_core/instruments/qm/program/arguments.py index 4f93f70df..efebe2e95 100644 --- a/src/qibolab/_core/instruments/qm/program/arguments.py +++ b/src/qibolab/_core/instruments/qm/program/arguments.py @@ -27,6 +27,7 @@ class Parameters: element: Optional[str] = None lo_frequency: Optional[int] = None + max_offset: float = 0.5 @dataclass diff --git a/src/qibolab/_core/instruments/qm/program/instructions.py b/src/qibolab/_core/instruments/qm/program/instructions.py index f1dc1de50..700a89d7a 100644 --- a/src/qibolab/_core/instruments/qm/program/instructions.py +++ b/src/qibolab/_core/instruments/qm/program/instructions.py @@ -5,6 +5,7 @@ from qualang_tools.loops import from_array from qibolab._core.execution_parameters import AcquisitionType, ExecutionParameters +from qibolab._core.identifier import ChannelId from qibolab._core.pulses import Align, Delay, Pulse, Readout, VirtualZ from qibolab._core.sweeper import ParallelSweepers, Parameter, Sweeper @@ -170,9 +171,14 @@ def program( args: ExecutionArguments, options: ExecutionParameters, sweepers: list[ParallelSweepers], + offsets: list[tuple[ChannelId, float]], ): """QUA program implementing the required experiment.""" with qua.program() as experiment: + # FIXME: force offset setting due to a bug in QUA 1.2.1a2 and OPX1000 + for channel_id, offset in offsets: + qua.set_dc_offset(channel_id, "single", offset) + n = declare(int) # declare acquisition variables for acquisition in args.acquisitions.values(): diff --git a/src/qibolab/_core/instruments/qm/program/sweepers.py b/src/qibolab/_core/instruments/qm/program/sweepers.py index 64fb0680f..305cd7709 100644 --- a/src/qibolab/_core/instruments/qm/program/sweepers.py +++ b/src/qibolab/_core/instruments/qm/program/sweepers.py @@ -9,8 +9,6 @@ from .arguments import ExecutionArguments, Parameters -MAX_OFFSET = 0.5 -"""Maximum voltage supported by Quantum Machines OPX+ instrument in volts.""" MAX_AMPLITUDE_FACTOR = 1.99 """Maximum multiplication factor for ``qua.amp`` used when sweeping amplitude. @@ -105,10 +103,10 @@ def _duration_interpolated(variable: _Variable, parameters: Parameters): def _offset(variable: _Variable, parameters: Parameters): - with qua.if_(variable >= MAX_OFFSET): - qua.set_dc_offset(parameters.element, "single", MAX_OFFSET) - with qua.elif_(variable <= -MAX_OFFSET): - qua.set_dc_offset(parameters.element, "single", -MAX_OFFSET) + with qua.if_(variable >= parameters.max_offset): + qua.set_dc_offset(parameters.element, "single", parameters.max_offset) + with qua.elif_(variable <= -parameters.max_offset): + qua.set_dc_offset(parameters.element, "single", -parameters.max_offset) with qua.else_(): qua.set_dc_offset(parameters.element, "single", variable)