From 136ebf8c7bdbbdc74f08d3487bff2b6221235b29 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Sat, 30 Dec 2023 01:38:01 +0400 Subject: [PATCH 01/46] feat: introduce QM Octaves (wip) --- doc/source/tutorials/instrument.rst | 2 +- src/qibolab/channels.py | 10 +- src/qibolab/instruments/qm/__init__.py | 4 +- src/qibolab/instruments/qm/config.py | 195 +++++++++++++------------ src/qibolab/instruments/qm/devices.py | 45 ++++++ src/qibolab/instruments/qm/driver.py | 69 +++++++-- src/qibolab/instruments/qm/ports.py | 84 +++++++++++ 7 files changed, 293 insertions(+), 116 deletions(-) create mode 100644 src/qibolab/instruments/qm/devices.py create mode 100644 src/qibolab/instruments/qm/ports.py diff --git a/doc/source/tutorials/instrument.rst b/doc/source/tutorials/instrument.rst index 783470c81..de6a7d09e 100644 --- a/doc/source/tutorials/instrument.rst +++ b/doc/source/tutorials/instrument.rst @@ -202,6 +202,6 @@ the new controller a new port type. See, for example, the already implemented ports: * :class:`qibolab.instruments.rfsoc.driver.RFSoCPort` -* :class:`qibolab.instruments.qm.config.QMPort` +* :class:`qibolab.instruments.qm.ports.QMPort` * :class:`qibolab.instruments.zhinst.ZhPort` * :class:`qibolab.instruments.qblox.port` diff --git a/src/qibolab/channels.py b/src/qibolab/channels.py index 9d934f6d1..93c5a2fb5 100644 --- a/src/qibolab/channels.py +++ b/src/qibolab/channels.py @@ -105,12 +105,12 @@ def power_range(self, value): self.port.power_range = value @property - def filters(self): - return self.port.filters + def filter(self): + return self.port.filter - @filters.setter - def filters(self, value): - self.port.filters = value + @filter.setter + def filter(self, value): + self.port.filter = value @dataclass diff --git a/src/qibolab/instruments/qm/__init__.py b/src/qibolab/instruments/qm/__init__.py index f05ce1ed7..8389e395d 100644 --- a/src/qibolab/instruments/qm/__init__.py +++ b/src/qibolab/instruments/qm/__init__.py @@ -1,3 +1,3 @@ -from .config import QMPort -from .driver import QMOPX +from .devices import Octave, OPXplus +from .driver import QMController from .simulator import QMSim diff --git a/src/qibolab/instruments/qm/config.py b/src/qibolab/instruments/qm/config.py index 4016783c9..8dece48a8 100644 --- a/src/qibolab/instruments/qm/config.py +++ b/src/qibolab/instruments/qm/config.py @@ -1,30 +1,17 @@ import math from dataclasses import dataclass, field -from typing import Dict, Optional, Tuple, Union import numpy as np from qibo.config import raise_error -from qibolab.instruments.port import Port from qibolab.pulses import PulseType, Rectangular -PortId = Tuple[str, int] -"""Type for port definition, for example: ("con1", 2).""" -IQPortId = Union[Tuple[PortId], Tuple[PortId, PortId]] -"""Type for collections of IQ ports.""" +from .ports import OPXIQ, OctaveOutput, OPXOutput SAMPLING_RATE = 1 """Sampling rate of Quantum Machines OPX in GSps.""" -@dataclass -class QMPort(Port): - name: IQPortId - offset: float = 0.0 - gain: int = 0 - filters: Optional[Dict[str, float]] = None - - @dataclass class QMConfig: """Configuration for communicating with the ``QuantumMachinesManager``.""" @@ -40,7 +27,32 @@ class QMConfig: integration_weights: dict = field(default_factory=dict) mixers: dict = field(default_factory=dict) - def register_analog_output_controllers(self, port: QMPort): + def register_opxplus_port(self, port): + if port.device not in self.controllers: + self.controllers[port.device] = {} + + key = "analog_outputs" if isinstance(port, OPXOutput) else "analog_inputs" + data = self.controllers[port.device] + if key in data: + data[key].update(port.serial) + else: + data[key] = port.serial + + def register_octave_port(self, port): + if port.device not in self.controllers: + self.controllers[port.device] = {} + + key = "RF_outputs" if isinstance(port, OctaveOutput) else "RF_inputs" + data = self.controllers[port.device] + if key in data: + data[key].update(port.serial) + else: + data[key] = port.serial + # TODO: Missing ``connectivity`` key here which declares which OPX+ + # is connected to this Octave + # Probably need to move this to ``Octave`` to make it work + + def register_port(self, port): """Register controllers in the ``config``. Args: @@ -48,16 +60,15 @@ def register_analog_output_controllers(self, port: QMPort): Contains information about the controller and port number and some parameters (offset, gain, filter, etc.). """ - for con, port_number in port.name: - if con not in self.controllers: - self.controllers[con] = {"analog_outputs": {}} - self.controllers[con]["analog_outputs"][port_number] = { - "offset": port.offset - } - if port.filters is not None: - self.controllers[con]["analog_outputs"][port_number][ - "filter" - ] = port.filters + # TODO: Update docstring + if isinstance(port, OPXIQ): + self.register_output_port(port.i) + self.register_output_port(port.q) + else: + if isinstance(port, (OPXInput, OPXOutput)): + self.register_opxplus_port(port) + else: + self.register_octave_port(port) @staticmethod def iq_imbalance(g, phi): @@ -91,28 +102,37 @@ def register_drive_element(self, qubit, intermediate_frequency=0): """ if f"drive{qubit.name}" not in self.elements: # register drive controllers - self.register_analog_output_controllers(qubit.drive.port) + self.register_port(qubit.drive.port) # register element - lo_frequency = math.floor(qubit.drive.local_oscillator.frequency) - self.elements[f"drive{qubit.name}"] = { - "mixInputs": { - "I": qubit.drive.port.name[0], - "Q": qubit.drive.port.name[1], - "lo_frequency": lo_frequency, - "mixer": f"mixer_drive{qubit.name}", - }, - "intermediate_frequency": intermediate_frequency, - "operations": {}, - } - drive_g = qubit.mixer_drive_g - drive_phi = qubit.mixer_drive_phi - self.mixers[f"mixer_drive{qubit.name}"] = [ + if isinstance(qubit.drive.port, OPXOutput): + lo_frequency = math.floor(qubit.drive.lo_frequency) + self.elements[f"drive{qubit.name}"] = { + "mixInputs": { + "I": qubit.drive.port.i.pair, + "Q": qubit.drive.port.q.pair, + "lo_frequency": lo_frequency, + "mixer": f"mixer_drive{qubit.name}", + }, + } + drive_g = qubit.mixer_drive_g + drive_phi = qubit.mixer_drive_phi + self.mixers[f"mixer_drive{qubit.name}"] = [ + { + "intermediate_frequency": intermediate_frequency, + "lo_frequency": lo_frequency, + "correction": self.iq_imbalance(drive_g, drive_phi), + } + ] + else: + self.elements[f"drive{qubit.name}"] = { + "RF_inputs": {"port": qubit.drive.port.pair}, + } + self.elements[f"drive{qubit.name}"].update( { "intermediate_frequency": intermediate_frequency, - "lo_frequency": lo_frequency, - "correction": self.iq_imbalance(drive_g, drive_phi), + "operations": {}, } - ] + ) else: self.elements[f"drive{qubit.name}"][ "intermediate_frequency" @@ -134,55 +154,46 @@ def register_readout_element( """ if f"readout{qubit.name}" not in self.elements: # register readout controllers - self.register_analog_output_controllers(qubit.readout.port) + self.register_port(qubit.readout.port) # register feedback controllers - controllers = self.controllers - for con, port_number in qubit.feedback.port.name: - if con not in controllers: - controllers[con] = { - "analog_outputs": {}, - "digital_outputs": { - 1: {}, - }, - "analog_inputs": {}, - } - if "digital_outputs" not in controllers[con]: - controllers[con]["digital_outputs"] = { - 1: {}, + self.register_port(qubit.feedback.port) + # register element + if isinstance(qubit.readout.port, OPXOutput): + lo_frequency = math.floor(qubit.readout.lo_frequency) + self.elements[f"readout{qubit.name}"] = { + "mixInputs": { + "I": qubit.readout.port.i.pair, + "Q": qubit.readout.port.q.pair, + "lo_frequency": lo_frequency, + "mixer": f"mixer_readout{qubit.name}", + }, + "outputs": { + "out1": qubit.feedback.port.i.pair, + "out2": qubit.feedback.port.q.pair, + }, + } + readout_g = qubit.mixer_readout_g + readout_phi = qubit.mixer_readout_phi + self.mixers[f"mixer_readout{qubit.name}"] = [ + { + "intermediate_frequency": intermediate_frequency, + "lo_frequency": lo_frequency, + "correction": self.iq_imbalance(readout_g, readout_phi), } - if "analog_inputs" not in controllers[con]: - controllers[con]["analog_inputs"] = {} - controllers[con]["analog_inputs"][port_number] = { - "offset": 0.0, - "gain_db": qubit.feedback.port.gain, + ] + else: + self.elements[f"readout{qubit.name}"] = { + "RF_inputs": {"port": qubit.readout.port.pair}, + "RF_outputs": {"port": qubit.feedback.port.pair}, } - # register element - lo_frequency = math.floor(qubit.readout.local_oscillator.frequency) - self.elements[f"readout{qubit.name}"] = { - "mixInputs": { - "I": qubit.readout.port.name[0], - "Q": qubit.readout.port.name[1], - "lo_frequency": lo_frequency, - "mixer": f"mixer_readout{qubit.name}", - }, - "intermediate_frequency": intermediate_frequency, - "operations": {}, - "outputs": { - "out1": qubit.feedback.port.name[0], - "out2": qubit.feedback.port.name[1], - }, - "time_of_flight": time_of_flight, - "smearing": smearing, - } - readout_g = qubit.mixer_readout_g - readout_phi = qubit.mixer_readout_phi - self.mixers[f"mixer_readout{qubit.name}"] = [ + self.elements[f"readout{qubit.name}"].update( { "intermediate_frequency": intermediate_frequency, - "lo_frequency": lo_frequency, - "correction": self.iq_imbalance(readout_g, readout_phi), + "operations": {}, + "time_of_flight": time_of_flight, + "smearing": smearing, } - ] + ) else: self.elements[f"readout{qubit.name}"][ "intermediate_frequency" @@ -202,11 +213,11 @@ def register_flux_element(self, qubit, intermediate_frequency=0): """ if f"flux{qubit.name}" not in self.elements: # register controller - self.register_analog_output_controllers(qubit.flux.port) + self.register_port(qubit.flux.port) # register element self.elements[f"flux{qubit.name}"] = { "singleInput": { - "port": qubit.flux.port.name[0], + "port": qubit.flux.port.pair, }, "intermediate_frequency": intermediate_frequency, "operations": {}, @@ -219,18 +230,14 @@ def register_flux_element(self, qubit, intermediate_frequency=0): def register_element(self, qubit, pulse, time_of_flight=0, smearing=0): if pulse.type is PulseType.DRIVE: # register drive element - if_frequency = pulse.frequency - math.floor( - qubit.drive.local_oscillator.frequency - ) + if_frequency = pulse.frequency - math.floor(qubit.drive.lo_frequency) self.register_drive_element(qubit, if_frequency) # register flux element (if available) if qubit.flux: self.register_flux_element(qubit) elif pulse.type is PulseType.READOUT: # register readout element (if it does not already exist) - if_frequency = pulse.frequency - math.floor( - qubit.readout.local_oscillator.frequency - ) + if_frequency = pulse.frequency - math.floor(qubit.readout.lo_frequency) self.register_readout_element(qubit, if_frequency, time_of_flight, smearing) # register flux element (if available) if qubit.flux: diff --git a/src/qibolab/instruments/qm/devices.py b/src/qibolab/instruments/qm/devices.py new file mode 100644 index 000000000..d374c0190 --- /dev/null +++ b/src/qibolab/instruments/qm/devices.py @@ -0,0 +1,45 @@ +from dataclasses import dataclass +from typing import Optional + +from .ports import OctaveInput, OctaveOutput, OPXInput, OPXOutput, Ports + + +@dataclass +class Octave: + name: str + port: int + connectivity: str + + outputs: Optional[Ports[int, OctaveOutput]] = None + inputs: Optional[Ports[int, OctaveInput]] = None + + def __post_init__(self): + self.outputs = Ports(OctaveOutput, self.name) + self.inputs = Ports(OctaveInput, self.name) + + def ports(self, name, input=False): + if input: + return self.inputs[name] + else: + return self.outputs[name] + + +@dataclass +class OPXplus: + number: int + outputs: Optional[Ports[int, OPXOutput]] = None + inputs: Optional[Ports[int, OPXInput]] = None + + @property + def name(self): + return f"con{self.number}" + + def __post_init__(self): + self.outputs = Ports(OPXOutput, self.name) + self.inputs = Ports(OPXInput, self.name) + + def ports(self, name, input=False): + if input: + return self.inputs[name] + else: + return self.outputs[name] diff --git a/src/qibolab/instruments/qm/driver.py b/src/qibolab/instruments/qm/driver.py index 5156a4f0c..75f9a692c 100644 --- a/src/qibolab/instruments/qm/driver.py +++ b/src/qibolab/instruments/qm/driver.py @@ -1,20 +1,46 @@ from dataclasses import dataclass, field -from typing import ClassVar, Dict, Optional +from typing import Dict, Optional from qm import generate_qua_script, qua +from qm.octave import QmOctaveConfig from qm.qua import declare, for_ from qm.QuantumMachinesManager import QuantumMachinesManager from qibolab import AveragingMode from qibolab.instruments.abstract import Controller -from .config import IQPortId, QMConfig, QMPort +from .config import QMConfig +from .ports import OPXIQ, Octave, OPXInput, OPXOutput, OPXplus from .sequence import Sequence from .sweepers import sweep +IQPortId = Tuple[Tuple[str, int], Tuple[str, int]] + +OCTAVE_ADDRESS = 11000 +"""Must be 11xxx, where xxx are the last three digits of the Octave IP +address.""" + + +def declare_octaves(octaves, host): + """Initiate octave_config class, set the calibration file and add octaves + info. + + :param octaves: objects that holds the information about octave's + name, the controller that is connected to this octave, octave's + ip and octave's port. + """ + # TODO: Fix docstring + config = None + if len(octaves) > 0: + config = QmOctaveConfig() + # config.set_calibration_db(os.getcwd()) + for octave in octaves: + config.add_device_info(octave.name, host, OCTAVE_ADDRESS + octave.port) + return config + @dataclass -class QMOPX(Controller): +class QMController(Controller): """Instrument object for controlling Quantum Machines (QM) OPX controllers. Playing pulses on QM controllers requires a ``config`` dictionary and a program @@ -30,23 +56,16 @@ class QMOPX(Controller): address (str): IP address and port for connecting to the OPX instruments. """ - PortType: ClassVar = QMPort - name: str address: str - manager: Optional[QuantumMachinesManager] = None - """Manager object used for controlling the QM OPXs.""" - config: QMConfig = field(default_factory=QMConfig) - """Configuration dictionary required for pulse execution on the OPXs.""" - is_connected: bool = False - """Boolean that shows whether we are connected to the QM manager.""" + opxs: Dict[int, OPXplus] = field(default_factory=dict) + octaves: Dict[int, Octave] = field(default_factory=dict) + time_of_flight: int = 0 """Time of flight used for hardware signal integration.""" smearing: int = 0 """Smearing used for hardware signal integration.""" - _ports: Dict[IQPortId, QMPort] = field(default_factory=dict) - """Dictionary holding the ports of controllers that are connected.""" script_file_name: Optional[str] = "qua_script.txt" """Name of the file that the QUA program will dumped in that after every execution. @@ -54,13 +73,35 @@ class QMOPX(Controller): If ``None`` the program will not be dumped. """ + output_ports: Dict[IQPortId, OPXIQ] = field(default_factory=dict) + input_ports: Dict[IQPortId, OPXIQ] = field(default_factory=dict) + """Dictionary holding the ports of IQ pairs. + + Not needed when using only Octaves. + """ + + manager: Optional[QuantumMachinesManager] = None + """Manager object used for controlling the QM OPXs.""" + config: QMConfig = field(default_factory=QMConfig) + """Configuration dictionary required for pulse execution on the OPXs.""" + is_connected: bool = False + """Boolean that shows whether we are connected to the QM manager.""" + def __post_init__(self): super().__init__(self.name, self.address) + def ports(self, name, input=False): + _ports = self.input_ports if input else self.output_ports + if name not in self._ports: + port_cls = OPXInput if input else OPXOutput + _ports[name] = OPXIQ(port_cls(*name[0]), port_cls(*name[1])) + return _ports[name] + def connect(self): """Connect to the QM manager.""" host, port = self.address.split(":") - self.manager = QuantumMachinesManager(host, int(port)) + octave = declare_octaves(self.octaves, host) + self.manager = QuantumMachinesManager(host=host, port=int(port), octave=octave) def setup(self): """Deprecated method.""" diff --git a/src/qibolab/instruments/qm/ports.py b/src/qibolab/instruments/qm/ports.py new file mode 100644 index 000000000..5ca14d807 --- /dev/null +++ b/src/qibolab/instruments/qm/ports.py @@ -0,0 +1,84 @@ +from dataclasses import asdict, dataclass +from typing import ClassVar, Dict, Optional, Union + + +@dataclass +class QMPort(Port): + device: str + number: int + + _replace: ClassVar[dict] = {} + + @property + def pair(self): + return (self.device, self.number) + + @property + def serial(self): + data = asdict(self) + del data["device"] + del data["number"] + for old, new in self._replace.items(): + data[new] = data.pop(old) + return {self.number: data} + + +@dataclass +class OPXOutput(QMPort): + offset: float = 0.0 + filter: Optional[Dict[str, float]] = None + + +@dataclass +class OPXInput(QMPort): + _replace: ClassVar[dict] = {"gain": "gain_db"} + + offset: float = 0.0 + gain: int = 0 + + +@dataclass +class OPXIQ(Port): + i: Union[OPXOutput, OPXInput] + q: Union[OPXOutput, OPXInput] + + +@dataclass +class OctaveOutput(QMPort): + _replace: ClassVar[dict] = { + "lo_frequency": "LO_frequency", + "lo_source": "LO_source", + } + + lo_frequency: float = 0.0 + gain: float = 0 + """Can be in the range [-20 : 0.5 : 20]dB.""" + lo_source: str = "internal" + """Can be external or internal.""" + output_mode: str = "always_on" + """Can be: "always_on" / "always_off"/ "triggered" / "triggered_reversed".""" + + +@dataclass +class OctaveInput(QMPort): + _replace: ClassVar[dict] = { + "lo_frequency": "LO_frequency", + "lo_source": "LO_source", + } + + lo_frequency: float = 0.0 + lo_source: str = "internal" + IF_mode_I: str = "direct" + IF_mode_Q: str = "direct" + + +class Ports(dict): + def __init__(self, constructor, device): + self.constructor = constructor + self.device = device + super().__init__() + + def __getitem__(self, number): + if number not in self: + self[number] = self.constructor(self.device, number) + return super().__getitem__(number) From cbda43d52afdb3d3174bc5a3d44de0e0888813f5 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Sun, 31 Dec 2023 00:19:41 +0400 Subject: [PATCH 02/46] fix: add connectivity to octave section in config --- src/qibolab/instruments/qm/config.py | 61 +++++------------- src/qibolab/instruments/qm/driver.py | 86 +++++++++++++++++++++++++- src/qibolab/instruments/qm/ports.py | 6 ++ src/qibolab/instruments/qm/sequence.py | 57 ----------------- 4 files changed, 104 insertions(+), 106 deletions(-) diff --git a/src/qibolab/instruments/qm/config.py b/src/qibolab/instruments/qm/config.py index 8dece48a8..803f28d44 100644 --- a/src/qibolab/instruments/qm/config.py +++ b/src/qibolab/instruments/qm/config.py @@ -6,7 +6,7 @@ from qibolab.pulses import PulseType, Rectangular -from .ports import OPXIQ, OctaveOutput, OPXOutput +from .ports import OPXOutput SAMPLING_RATE = 1 """Sampling rate of Quantum Machines OPX in GSps.""" @@ -18,6 +18,7 @@ class QMConfig: version: int = 1 controllers: dict = field(default_factory=dict) + octaves: dict = field(default_factory=dict) elements: dict = field(default_factory=dict) pulses: dict = field(default_factory=dict) waveforms: dict = field(default_factory=dict) @@ -27,48 +28,25 @@ class QMConfig: integration_weights: dict = field(default_factory=dict) mixers: dict = field(default_factory=dict) - def register_opxplus_port(self, port): - if port.device not in self.controllers: - self.controllers[port.device] = {} - - key = "analog_outputs" if isinstance(port, OPXOutput) else "analog_inputs" - data = self.controllers[port.device] - if key in data: - data[key].update(port.serial) - else: - data[key] = port.serial - - def register_octave_port(self, port): - if port.device not in self.controllers: - self.controllers[port.device] = {} - - key = "RF_outputs" if isinstance(port, OctaveOutput) else "RF_inputs" - data = self.controllers[port.device] - if key in data: - data[key].update(port.serial) - else: - data[key] = port.serial - # TODO: Missing ``connectivity`` key here which declares which OPX+ - # is connected to this Octave - # Probably need to move this to ``Octave`` to make it work - def register_port(self, port): - """Register controllers in the ``config``. + """Register controllers and octaves sections in the ``config``. Args: ports (QMPort): Port we are registering. Contains information about the controller and port number and - some parameters (offset, gain, filter, etc.). + some parameters, such as offset, gain, filter, etc.). """ - # TODO: Update docstring - if isinstance(port, OPXIQ): - self.register_output_port(port.i) - self.register_output_port(port.q) + data = ( + self.controllers + if isinstance(port, (OPXInput, OPXOutput)) + else self.octaves + ) + if port.device not in self.controllers: + data[port.device] = {} + if port.key in data: + data[port.device][port.key].update(port.serial) else: - if isinstance(port, (OPXInput, OPXOutput)): - self.register_opxplus_port(port) - else: - self.register_octave_port(port) + data[port.device][port.key] = port.serial @staticmethod def iq_imbalance(g, phi): @@ -101,9 +79,6 @@ def register_drive_element(self, qubit, intermediate_frequency=0): LO connected to the same channel. """ if f"drive{qubit.name}" not in self.elements: - # register drive controllers - self.register_port(qubit.drive.port) - # register element if isinstance(qubit.drive.port, OPXOutput): lo_frequency = math.floor(qubit.drive.lo_frequency) self.elements[f"drive{qubit.name}"] = { @@ -153,11 +128,6 @@ def register_readout_element( LO connected to the same channel. """ if f"readout{qubit.name}" not in self.elements: - # register readout controllers - self.register_port(qubit.readout.port) - # register feedback controllers - self.register_port(qubit.feedback.port) - # register element if isinstance(qubit.readout.port, OPXOutput): lo_frequency = math.floor(qubit.readout.lo_frequency) self.elements[f"readout{qubit.name}"] = { @@ -212,9 +182,6 @@ def register_flux_element(self, qubit, intermediate_frequency=0): LO connected to the same channel. """ if f"flux{qubit.name}" not in self.elements: - # register controller - self.register_port(qubit.flux.port) - # register element self.elements[f"flux{qubit.name}"] = { "singleInput": { "port": qubit.flux.port.pair, diff --git a/src/qibolab/instruments/qm/driver.py b/src/qibolab/instruments/qm/driver.py index 75f9a692c..baf752aec 100644 --- a/src/qibolab/instruments/qm/driver.py +++ b/src/qibolab/instruments/qm/driver.py @@ -8,10 +8,19 @@ from qibolab import AveragingMode from qibolab.instruments.abstract import Controller +from qibolab.pulses import PulseType from .config import QMConfig -from .ports import OPXIQ, Octave, OPXInput, OPXOutput, OPXplus -from .sequence import Sequence +from .ports import ( + OPXIQ, + Octave, + OctaveInput, + OctaveOutput, + OPXInput, + OPXOutput, + OPXplus, +) +from .sequence import BakedPulse, QMPulse, Sequence from .sweepers import sweep IQPortId = Tuple[Tuple[str, int], Tuple[str, int]] @@ -39,6 +48,23 @@ def declare_octaves(octaves, host): return config +def find_duration_sweeper_pulses(sweepers): + """Find all pulses that require baking because we are sweeping their + duration.""" + duration_sweep_pulses = set() + for sweeper in sweepers: + try: + step = sweeper.values[1] - sweeper.values[0] + except IndexError: + step = sweeper.values[0] + + if sweeper.parameter is Parameter.duration and step % 4 != 0: + for pulse in sweeper.pulses: + duration_sweep_pulses.add(pulse.serial) + + return duration_sweep_pulses + + @dataclass class QMController(Controller): """Instrument object for controlling Quantum Machines (QM) OPX controllers. @@ -157,6 +183,62 @@ def fetch_results(result, ro_pulses): ) return results + def register_port(self, port): + if isinstance(port, OPXIQ): + self.register_port(port.i) + self.register_port(port.q) + else: + self.config.register_port(port) + if isinstance(port, (OctaveInput, OctaveOutput)): + self.config.octaves[port.device]["connectivity"] = self.octaves[ + port.device + ].connectivity + + def create_sequence(self, qubits, sequence, sweepers): + """Translates a :class:`qibolab.pulses.PulseSequence` to a + :class:`qibolab.instruments.qm.sequence.Sequence`. + + Args: + qubits (list): List of :class:`qibolab.platforms.abstract.Qubit` objects + passed from the platform. + sequence (:class:`qibolab.pulses.PulseSequence`). Pulse sequence to translate. + sweepers (list): List of sweeper objects so that pulses that require baking are identified. + Returns: + (:class:`qibolab.instruments.qm.sequence.Sequence`) containing the pulses from given pulse sequence. + """ + # Current driver cannot play overlapping pulses on drive and flux channels + # If we want to play overlapping pulses we need to define different elements on the same ports + # like we do for readout multiplex + + duration_sweep_pulses = find_duration_sweeper_pulses(sweepers) + + qmsequence = Sequence() + sort_key = lambda pulse: (pulse.start, pulse.duration) + for pulse in sorted(sequence.pulses, key=sort_key): + qubit = qubits[pulse.qubit] + + self.register_port(getattr(qubit, pulse.type.name.lower()).port) + if pulse.type is PulseType.READOUT: + self.register_port(qubit.feedback.port) + + self.config.register_element( + qubit, pulse, self.time_of_flight, self.smearing + ) + if ( + pulse.duration % 4 != 0 + or pulse.duration < 16 + or pulse.serial in duration_sweep_pulses + ): + qmpulse = BakedPulse(pulse) + qmpulse.bake(self.config, durations=[pulse.duration]) + else: + qmpulse = QMPulse(pulse) + self.config.register_pulse(qubit, pulse) + qmsequence.add(qmpulse) + + qmsequence.shift() + return qmsequence + def play(self, qubits, couplers, sequence, options): return self.sweep(qubits, couplers, sequence, options) diff --git a/src/qibolab/instruments/qm/ports.py b/src/qibolab/instruments/qm/ports.py index 5ca14d807..1596897e4 100644 --- a/src/qibolab/instruments/qm/ports.py +++ b/src/qibolab/instruments/qm/ports.py @@ -7,6 +7,7 @@ class QMPort(Port): device: str number: int + key: ClassVar[Optional[str]] = None _replace: ClassVar[dict] = {} @property @@ -25,12 +26,15 @@ def serial(self): @dataclass class OPXOutput(QMPort): + key: ClassVar[str] = "analog_outputs" + offset: float = 0.0 filter: Optional[Dict[str, float]] = None @dataclass class OPXInput(QMPort): + key: ClassVar[str] = "analog_inputs" _replace: ClassVar[dict] = {"gain": "gain_db"} offset: float = 0.0 @@ -45,6 +49,7 @@ class OPXIQ(Port): @dataclass class OctaveOutput(QMPort): + key: ClassVar[str] = "RF_outputs" _replace: ClassVar[dict] = { "lo_frequency": "LO_frequency", "lo_source": "LO_source", @@ -61,6 +66,7 @@ class OctaveOutput(QMPort): @dataclass class OctaveInput(QMPort): + key: ClassVar[str] = "RF_inputs" _replace: ClassVar[dict] = { "lo_frequency": "LO_frequency", "lo_source": "LO_source", diff --git a/src/qibolab/instruments/qm/sequence.py b/src/qibolab/instruments/qm/sequence.py index 37042cf26..1457a1871 100644 --- a/src/qibolab/instruments/qm/sequence.py +++ b/src/qibolab/instruments/qm/sequence.py @@ -18,7 +18,6 @@ ShotsAcquisition, ) from qibolab.pulses import Pulse, PulseType -from qibolab.sweeper import Parameter from .config import SAMPLING_RATE, QMConfig @@ -207,23 +206,6 @@ def play(self): segment.run(amp_array=self.amplitude_array) -def find_duration_sweeper_pulses(sweepers): - """Find all pulses that require baking because we are sweeping their - duration.""" - duration_sweep_pulses = set() - for sweeper in sweepers: - try: - step = sweeper.values[1] - sweeper.values[0] - except IndexError: - step = sweeper.values[0] - - if sweeper.parameter is Parameter.duration and step % 4 != 0: - for pulse in sweeper.pulses: - duration_sweep_pulses.add(pulse.serial) - - return duration_sweep_pulses - - @dataclass class Sequence: """Pulse sequence containing QM specific pulses (``qmpulse``). @@ -249,45 +231,6 @@ class Sequence: """Map to find all pulses that finish at a given time (useful for ``_find_previous``).""" - @classmethod - def create(cls, qubits, sequence, sweepers, config, time_of_flight, smearing): - """Translates a :class:`qibolab.pulses.PulseSequence` to a - :class:`qibolab.instruments.qm.sequence.Sequence`. - - Args: - qubits (list): List of :class:`qibolab.platforms.abstract.Qubit` objects - passed from the platform. - sequence (:class:`qibolab.pulses.PulseSequence`). Pulse sequence to translate. - Returns: - (:class:`qibolab.instruments.qm.Sequence`) containing the pulses from given pulse sequence. - """ - # Current driver cannot play overlapping pulses on drive and flux channels - # If we want to play overlapping pulses we need to define different elements on the same ports - # like we do for readout multiplex - duration_sweep_pulses = find_duration_sweeper_pulses(sweepers) - qmsequence = cls() - for pulse in sorted( - sequence.pulses, key=lambda pulse: (pulse.start, pulse.duration) - ): - config.register_element( - qubits[pulse.qubit], pulse, time_of_flight, smearing - ) - if ( - pulse.duration % 4 != 0 - or pulse.duration < 16 - or pulse.serial in duration_sweep_pulses - ): - qmpulse = BakedPulse(pulse) - qmpulse.bake(config, durations=[pulse.duration]) - else: - qmpulse = QMPulse(pulse) - config.register_pulse(qubits[pulse.qubit], pulse) - qmsequence.add(qmpulse) - - qmsequence.shift() - - return qmsequence - def _find_previous(self, pulse): for finish in reversed(sorted(self.pulse_finish.keys())): if finish <= pulse.start: From fbd07dff61681216d699e02b3db1d22b825c5ea1 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Sun, 31 Dec 2023 01:46:06 +0400 Subject: [PATCH 03/46] fix: working without octaves --- src/qibolab/instruments/port.py | 8 ------- src/qibolab/instruments/qm/config.py | 19 ++++++++------- src/qibolab/instruments/qm/driver.py | 27 ++++++++++----------- src/qibolab/instruments/qm/ports.py | 8 +++---- src/qibolab/instruments/qm/simulator.py | 4 ++-- tests/dummy_qrc/qm.py | 31 ++++++++++++++----------- 6 files changed, 45 insertions(+), 52 deletions(-) diff --git a/src/qibolab/instruments/port.py b/src/qibolab/instruments/port.py index 51aecff6a..d4759fa66 100644 --- a/src/qibolab/instruments/port.py +++ b/src/qibolab/instruments/port.py @@ -33,11 +33,3 @@ class Port: power_range: int """Similar to attenuation (negative) and gain (positive) for (Zurich instruments).""" - filters: dict - """Filters to be applied to the channel to reduce the distortions when - sending flux pulses. - - Useful for two-qubit gates. Quantum Machines ( - :class: `qibolab.instruments.qm.QMOPX`) associate filters to - channels but this may not be the case in other instruments. - """ diff --git a/src/qibolab/instruments/qm/config.py b/src/qibolab/instruments/qm/config.py index 803f28d44..d0eeb4abc 100644 --- a/src/qibolab/instruments/qm/config.py +++ b/src/qibolab/instruments/qm/config.py @@ -6,7 +6,7 @@ from qibolab.pulses import PulseType, Rectangular -from .ports import OPXOutput +from .ports import OPXIQ, OPXInput, OPXOutput SAMPLING_RATE = 1 """Sampling rate of Quantum Machines OPX in GSps.""" @@ -36,17 +36,18 @@ def register_port(self, port): Contains information about the controller and port number and some parameters, such as offset, gain, filter, etc.). """ - data = ( + controllers = ( self.controllers if isinstance(port, (OPXInput, OPXOutput)) else self.octaves ) - if port.device not in self.controllers: - data[port.device] = {} - if port.key in data: - data[port.device][port.key].update(port.serial) + if port.device not in controllers: + controllers[port.device] = {} + device = controllers[port.device] + if port.key in device: + device[port.key].update(port.serial) else: - data[port.device][port.key] = port.serial + device[port.key] = port.serial @staticmethod def iq_imbalance(g, phi): @@ -79,7 +80,7 @@ def register_drive_element(self, qubit, intermediate_frequency=0): LO connected to the same channel. """ if f"drive{qubit.name}" not in self.elements: - if isinstance(qubit.drive.port, OPXOutput): + if isinstance(qubit.drive.port, OPXIQ): lo_frequency = math.floor(qubit.drive.lo_frequency) self.elements[f"drive{qubit.name}"] = { "mixInputs": { @@ -128,7 +129,7 @@ def register_readout_element( LO connected to the same channel. """ if f"readout{qubit.name}" not in self.elements: - if isinstance(qubit.readout.port, OPXOutput): + if isinstance(qubit.readout.port, OPXIQ): lo_frequency = math.floor(qubit.readout.lo_frequency) self.elements[f"readout{qubit.name}"] = { "mixInputs": { diff --git a/src/qibolab/instruments/qm/driver.py b/src/qibolab/instruments/qm/driver.py index baf752aec..9be39438f 100644 --- a/src/qibolab/instruments/qm/driver.py +++ b/src/qibolab/instruments/qm/driver.py @@ -1,5 +1,5 @@ from dataclasses import dataclass, field -from typing import Dict, Optional +from typing import Dict, Optional, Tuple from qm import generate_qua_script, qua from qm.octave import QmOctaveConfig @@ -9,17 +9,11 @@ from qibolab import AveragingMode from qibolab.instruments.abstract import Controller from qibolab.pulses import PulseType +from qibolab.sweeper import Parameter from .config import QMConfig -from .ports import ( - OPXIQ, - Octave, - OctaveInput, - OctaveOutput, - OPXInput, - OPXOutput, - OPXplus, -) +from .devices import Octave, OPXplus +from .ports import OPXIQ, OctaveInput, OctaveOutput, OPXInput, OPXOutput from .sequence import BakedPulse, QMPulse, Sequence from .sweepers import sweep @@ -92,7 +86,7 @@ class QMController(Controller): """Time of flight used for hardware signal integration.""" smearing: int = 0 """Smearing used for hardware signal integration.""" - script_file_name: Optional[str] = "qua_script.txt" + script_file_name: Optional[str] = "qua_script.py" """Name of the file that the QUA program will dumped in that after every execution. @@ -117,8 +111,12 @@ def __post_init__(self): super().__init__(self.name, self.address) def ports(self, name, input=False): + if len(name) != 2: + raise ValueError( + "QMController provides only IQ ports. Please access individual ports from the specific device." + ) _ports = self.input_ports if input else self.output_ports - if name not in self._ports: + if name not in _ports: port_cls = OPXInput if input else OPXOutput _ports[name] = OPXIQ(port_cls(*name[0]), port_cls(*name[1])) return _ports[name] @@ -254,11 +252,10 @@ def sweep(self, qubits, couplers, sequence, options, *sweepers): # always at sweetspot even when they are not used for qubit in qubits.values(): if qubit.flux: + self.register_port(qubit.flux.port) self.config.register_flux_element(qubit) - qmsequence = Sequence.create( - qubits, sequence, sweepers, self.config, self.time_of_flight, self.smearing - ) + qmsequence = self.create_sequence(qubits, sequence, sweepers) # play pulses using QUA with qua.program() as experiment: n = declare(int) diff --git a/src/qibolab/instruments/qm/ports.py b/src/qibolab/instruments/qm/ports.py index 1596897e4..6f9234737 100644 --- a/src/qibolab/instruments/qm/ports.py +++ b/src/qibolab/instruments/qm/ports.py @@ -1,9 +1,9 @@ -from dataclasses import asdict, dataclass +from dataclasses import asdict, dataclass, field from typing import ClassVar, Dict, Optional, Union @dataclass -class QMPort(Port): +class QMPort: device: str number: int @@ -29,7 +29,7 @@ class OPXOutput(QMPort): key: ClassVar[str] = "analog_outputs" offset: float = 0.0 - filter: Optional[Dict[str, float]] = None + filter: Dict[str, float] = field(default_factory=dict) @dataclass @@ -42,7 +42,7 @@ class OPXInput(QMPort): @dataclass -class OPXIQ(Port): +class OPXIQ: i: Union[OPXOutput, OPXInput] q: Union[OPXOutput, OPXInput] diff --git a/src/qibolab/instruments/qm/simulator.py b/src/qibolab/instruments/qm/simulator.py index 9bbe56a66..7e61aa34b 100644 --- a/src/qibolab/instruments/qm/simulator.py +++ b/src/qibolab/instruments/qm/simulator.py @@ -4,11 +4,11 @@ from qm.QuantumMachinesManager import QuantumMachinesManager from qualang_tools.simulator_tools import create_simulator_controller_connections -from .driver import QMOPX +from .driver import QMController @dataclass -class QMSim(QMOPX): +class QMSim(QMController): """Instrument for using the Quantum Machines (QM) OPX simulator. Args: diff --git a/tests/dummy_qrc/qm.py b/tests/dummy_qrc/qm.py index 69cc239a3..2f3692b69 100644 --- a/tests/dummy_qrc/qm.py +++ b/tests/dummy_qrc/qm.py @@ -2,7 +2,7 @@ from qibolab.channels import Channel, ChannelMap from qibolab.instruments.dummy import DummyLocalOscillator as LocalOscillator -from qibolab.instruments.qm import QMSim +from qibolab.instruments.qm import OPXplus, QMController from qibolab.platform import Platform from qibolab.serialize import ( load_instrument_settings, @@ -22,32 +22,35 @@ def create(runcard_path=RUNCARD): Used in ``test_instruments_qm.py`` and ``test_instruments_qmsim.py`` """ - controller = QMSim( - "qmopx", "0.0.0.0:0", simulation_duration=1000, cloud=False, time_of_flight=280 - ) + opxs = [OPXplus(i) for i in range(1, 4)] + controller = QMController("qm", "192.168.0.101:80", opxs=opxs, time_of_flight=280) # Create channel objects and map controllers to channels channels = ChannelMap() # readout - channels |= Channel("L3-25_a", port=controller[(("con1", 10), ("con1", 9))]) - channels |= Channel("L3-25_b", port=controller[(("con2", 10), ("con2", 9))]) + channels |= Channel("L3-25_a", port=controller.ports((("con1", 10), ("con1", 9)))) + channels |= Channel("L3-25_b", port=controller.ports((("con2", 10), ("con2", 9)))) # feedback - channels |= Channel("L2-5_a", port=controller[(("con1", 2), ("con1", 1))]) - channels |= Channel("L2-5_b", port=controller[(("con2", 2), ("con2", 1))]) + channels |= Channel( + "L2-5_a", port=controller.ports((("con1", 2), ("con1", 1)), input=True) + ) + channels |= Channel( + "L2-5_b", port=controller.ports((("con2", 2), ("con2", 1)), input=True) + ) # drive channels |= ( - Channel(f"L3-1{i}", port=controller[(("con1", 2 * i), ("con1", 2 * i - 1))]) + Channel( + f"L3-1{i}", port=controller.ports((("con1", 2 * i), ("con1", 2 * i - 1))) + ) for i in range(1, 5) ) - channels |= Channel("L3-15", port=controller[(("con3", 2), ("con3", 1))]) + channels |= Channel("L3-15", port=controller.ports((("con3", 2), ("con3", 1)))) # flux - channels |= ( - Channel(f"L4-{i}", port=controller[(("con2", i),)]) for i in range(1, 6) - ) + channels |= (Channel(f"L4-{i}", port=opxs[1].ports(i)) for i in range(1, 6)) # TWPA channels |= "L4-26" - # Instantiate local oscillators (HARDCODED) + # Instantiate local oscillators local_oscillators = [ LocalOscillator("lo_readout_a", "192.168.0.39"), LocalOscillator("lo_readout_b", "192.168.0.31"), From bf075a6cd380ba579f0510111abf0476bcd71678 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Sun, 31 Dec 2023 02:17:34 +0400 Subject: [PATCH 04/46] fix: bump qm-qua version --- poetry.lock | 244 +++++++++++++++++++++++++++++++++++++++---------- pyproject.toml | 6 +- 2 files changed, 197 insertions(+), 53 deletions(-) diff --git a/poetry.lock b/poetry.lock index 57fa19194..78f022ed0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -48,6 +48,28 @@ files = [ {file = "antlr4_python3_runtime-4.11.1-py3-none-any.whl", hash = "sha256:ff1954eda1ca9072c02bf500387d0c86cb549bef4dbb3b64f39468b547ec5f6b"}, ] +[[package]] +name = "anyio" +version = "4.2.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"}, + {file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] + [[package]] name = "astroid" version = "3.0.2" @@ -184,6 +206,24 @@ python-dateutil = ">=2.8,<3.0" [package.extras] compiler = ["black (>=19.3b0)", "isort (>=5.10.1,<6.0.0)", "jinja2 (>=3.0.3)"] +[[package]] +name = "betterproto" +version = "2.0.0b6" +description = "A better Protobuf / gRPC generator & library" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "betterproto-2.0.0b6-py3-none-any.whl", hash = "sha256:a0839ec165d110a69d0d116f4d0e2bec8d186af4db826257931f0831dab73fcf"}, + {file = "betterproto-2.0.0b6.tar.gz", hash = "sha256:720ae92697000f6fcf049c69267d957f0871654c8b0d7458906607685daee784"}, +] + +[package.dependencies] +grpclib = ">=0.4.1,<0.5.0" +python-dateutil = ">=2.8,<3.0" + +[package.extras] +compiler = ["black (>=19.3b0)", "isort (>=5.11.5,<6.0.0)", "jinja2 (>=3.0.3)"] + [[package]] name = "bleach" version = "6.1.0" @@ -1296,6 +1336,17 @@ multidict = "*" [package.extras] protobuf = ["protobuf (>=3.20.0)"] +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + [[package]] name = "h2" version = "4.1.0" @@ -1377,6 +1428,51 @@ files = [ {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, ] +[[package]] +name = "httpcore" +version = "0.16.3" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.7" +files = [ + {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, + {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, +] + +[package.dependencies] +anyio = ">=3.0,<5.0" +certifi = "*" +h11 = ">=0.13,<0.15" +sniffio = "==1.*" + +[package.extras] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + +[[package]] +name = "httpx" +version = "0.23.3" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.7" +files = [ + {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, + {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, +] + +[package.dependencies] +certifi = "*" +h2 = {version = ">=3,<5", optional = true, markers = "extra == \"http2\""} +httpcore = ">=0.15.0,<0.17.0" +rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<13)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + [[package]] name = "hyperframe" version = "6.0.1" @@ -2062,22 +2158,19 @@ files = [ [[package]] name = "marshmallow" -version = "3.20.1" +version = "3.0.0" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." optional = false -python-versions = ">=3.8" +python-versions = ">=3.5" files = [ - {file = "marshmallow-3.20.1-py3-none-any.whl", hash = "sha256:684939db93e80ad3561392f47be0230743131560a41c5110684c16e21ade0a5c"}, - {file = "marshmallow-3.20.1.tar.gz", hash = "sha256:5d2371bbe42000f2b3fb5eaa065224df7d8f8597bc19a1bbfa5bfe7fba8da889"}, + {file = "marshmallow-3.0.0-py2.py3-none-any.whl", hash = "sha256:e5e9fd0c2e919b4ece915eb30808206349a49a45df72e99ed20e27a9053d574b"}, + {file = "marshmallow-3.0.0.tar.gz", hash = "sha256:fa2d8a4b61d09b0e161a14acc5ad8ab7aaaf1477f3dd52819ddd6c6c8275733a"}, ] -[package.dependencies] -packaging = ">=17.0" - [package.extras] -dev = ["flake8 (==6.0.0)", "flake8-bugbear (==23.7.10)", "mypy (==1.4.1)", "pre-commit (>=2.4,<4.0)", "pytest", "pytz", "simplejson", "tox"] -docs = ["alabaster (==0.7.13)", "autodocsumm (==0.2.11)", "sphinx (==7.0.1)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"] -lint = ["flake8 (==6.0.0)", "flake8-bugbear (==23.7.10)", "mypy (==1.4.1)", "pre-commit (>=2.4,<4.0)"] +dev = ["flake8 (==3.7.8)", "flake8-bugbear (==19.8.0)", "pre-commit (>=1.17,<2.0)", "pytest", "pytz", "simplejson", "tox"] +docs = ["alabaster (==0.7.12)", "sphinx (==2.1.2)", "sphinx-issues (==1.2.0)", "sphinx-version-warning (==1.1.2)"] +lint = ["flake8 (==3.7.8)", "flake8-bugbear (==19.8.0)", "pre-commit (>=1.17,<2.0)"] tests = ["pytest", "pytz", "simplejson"] [[package]] @@ -2877,20 +2970,6 @@ docs = ["sphinx (>=1.7.1)"] redis = ["redis"] tests = ["pytest (>=5.4.1)", "pytest-cov (>=2.8.1)", "pytest-mypy (>=0.8.0)", "pytest-timeout (>=2.1.0)", "redis", "sphinx (>=6.0.0)", "types-redis"] -[[package]] -name = "pretty-errors" -version = "1.2.25" -description = "Prettifies Python exception output to make it legible." -optional = false -python-versions = "*" -files = [ - {file = "pretty_errors-1.2.25-py3-none-any.whl", hash = "sha256:8ce68ccd99e0f2a099265c8c1f1c23b7c60a15d69bb08816cb336e237d5dc983"}, - {file = "pretty_errors-1.2.25.tar.gz", hash = "sha256:a16ba5c752c87c263bf92f8b4b58624e3b1e29271a9391f564f12b86e93c6755"}, -] - -[package.dependencies] -colorama = "*" - [[package]] name = "prompt-toolkit" version = "3.0.43" @@ -2936,6 +3015,26 @@ files = [ {file = "protobuf-3.20.3.tar.gz", hash = "sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2"}, ] +[[package]] +name = "protobuf" +version = "4.25.1" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "protobuf-4.25.1-cp310-abi3-win32.whl", hash = "sha256:193f50a6ab78a970c9b4f148e7c750cfde64f59815e86f686c22e26b4fe01ce7"}, + {file = "protobuf-4.25.1-cp310-abi3-win_amd64.whl", hash = "sha256:3497c1af9f2526962f09329fd61a36566305e6c72da2590ae0d7d1322818843b"}, + {file = "protobuf-4.25.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:0bf384e75b92c42830c0a679b0cd4d6e2b36ae0cf3dbb1e1dfdda48a244f4bcd"}, + {file = "protobuf-4.25.1-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:0f881b589ff449bf0b931a711926e9ddaad3b35089cc039ce1af50b21a4ae8cb"}, + {file = "protobuf-4.25.1-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:ca37bf6a6d0046272c152eea90d2e4ef34593aaa32e8873fc14c16440f22d4b7"}, + {file = "protobuf-4.25.1-cp38-cp38-win32.whl", hash = "sha256:abc0525ae2689a8000837729eef7883b9391cd6aa7950249dcf5a4ede230d5dd"}, + {file = "protobuf-4.25.1-cp38-cp38-win_amd64.whl", hash = "sha256:1484f9e692091450e7edf418c939e15bfc8fc68856e36ce399aed6889dae8bb0"}, + {file = "protobuf-4.25.1-cp39-cp39-win32.whl", hash = "sha256:8bdbeaddaac52d15c6dce38c71b03038ef7772b977847eb6d374fc86636fa510"}, + {file = "protobuf-4.25.1-cp39-cp39-win_amd64.whl", hash = "sha256:becc576b7e6b553d22cbdf418686ee4daa443d7217999125c045ad56322dda10"}, + {file = "protobuf-4.25.1-py3-none-any.whl", hash = "sha256:a19731d5e83ae4737bb2a089605e636077ac001d18781b3cf489b9546c7c80d6"}, + {file = "protobuf-4.25.1.tar.gz", hash = "sha256:57d65074b4f5baa4ab5da1605c02be90ac20c8b40fb137d6a8df9f416b0d0ce2"}, +] + [[package]] name = "psutil" version = "5.9.6" @@ -3982,62 +4081,79 @@ tqdm = "*" [[package]] name = "qm-octave" -version = "1.1.0" +version = "2.0.1" description = "SDK to control an Octave with QUA" optional = false -python-versions = ">=3.7,<4.0" +python-versions = ">=3.7,<3.12" files = [ - {file = "qm_octave-1.1.0-py3-none-any.whl", hash = "sha256:ff4c631bfdaf19c389ced7d84550e2f7d3d131b268bb67132211295e3cc6b468"}, - {file = "qm_octave-1.1.0.tar.gz", hash = "sha256:374626b144f1597af9c1a70d93001b44cc421b80c4303aea95e36d031f90b601"}, + {file = "qm_octave-2.0.1-py3-none-any.whl", hash = "sha256:ef2607101b978beb306f30bfc56656f087fe7764133ba22e3ce5739a409c4edb"}, + {file = "qm_octave-2.0.1.tar.gz", hash = "sha256:f20844d98e493a253bedb72f86fcb665bc7461a46d502de3cc7c91f3d54416d6"}, ] [package.dependencies] -betterproto = "2.0.0b5" -grpcio = ">=1.39.0,<2.0.0" +betterproto = [ + {version = "2.0.0b5", markers = "python_version >= \"3.7\" and python_version < \"3.11\""}, + {version = "2.0.0b6", markers = "python_version >= \"3.11\" and python_version < \"4.0\""}, +] +grpcio = ">=1.59.2,<2.0.0" grpclib = {version = ">=0.4.3rc3,<0.5.0", markers = "python_version >= \"3.10\" and python_version < \"4.0\""} -nest-asyncio = ">=1.5.4,<2.0.0" -numpy = ">=1.21.0" -protobuf = ">=3.17.3,<4.0.0" +protobuf = [ + {version = ">=3.17.3,<4.0.0", markers = "python_version >= \"3.7\" and python_version < \"3.11\""}, + {version = ">=4.24,<5.0", markers = "python_version >= \"3.11\""}, +] [[package]] name = "qm-qua" -version = "1.1.1" +version = "1.1.6" description = "QUA language SDK to control a Quantum Computer" optional = false -python-versions = ">=3.7,<4.0" +python-versions = ">=3.7,<3.12" files = [ - {file = "qm_qua-1.1.1-py3-none-any.whl", hash = "sha256:c6e9c68690b40b3b97034dc1d4c39c23b9f9cb0ab1f34d18813a80490361730d"}, - {file = "qm_qua-1.1.1.tar.gz", hash = "sha256:05a13841462f522afa61b6f63ff88d845a4734b81c1eb6b1c4083ed0e371ef95"}, + {file = "qm_qua-1.1.6-py3-none-any.whl", hash = "sha256:69f8159805889fe9389b1acb4e94afacc37d68e1455017ec0e58a4fcc238bd8c"}, + {file = "qm_qua-1.1.6.tar.gz", hash = "sha256:9e09240bf1d9623c0f5b15fb2bc955f7387da15280d0b02c266e42a40818a2b1"}, ] [package.dependencies] -betterproto = "2.0.0b5" +betterproto = [ + {version = "2.0.0b5", markers = "python_version >= \"3.7\" and python_version < \"3.11\""}, + {version = "2.0.0b6", markers = "python_version == \"3.11\""}, +] datadog-api-client = ">=2.6.0,<3.0.0" dependency_injector = ">=4.41.0,<5.0.0" deprecation = ">=2.1.0,<3.0.0" -grpcio = ">=1.39.0,<2.0.0" -grpclib = {version = ">=0.4.3rc3,<0.5.0", markers = "python_version >= \"3.10\" and python_version < \"4.0\""} -marshmallow = ">=3.0.0,<4.0.0" +grpcio = [ + {version = ">=1.39.0,<2.0.0", markers = "python_version >= \"3.7\" and python_version < \"3.11\""}, + {version = ">=1.57,<2.0", markers = "python_version >= \"3.11\""}, +] +grpclib = {version = ">=0.4.5,<0.5.0", markers = "python_version >= \"3.10\""} +httpx = {version = ">=0.23.3,<0.24.0", extras = ["http2"]} +marshmallow = "3" marshmallow-polyfield = ">=5.7,<6.0" -numpy = ">=1.17.0,<2.0.0" +numpy = [ + {version = ">=1.17.0,<2.0.0", markers = "python_version >= \"3.7\" and python_version < \"3.11\""}, + {version = ">=1.24,<2.0", markers = "python_version >= \"3.11\""}, +] plotly = ">=5.13.0,<6.0.0" -pretty_errors = ">=1.2.25,<2.0.0" -protobuf = ">=3.17.3,<4.0.0" -qm-octave = ">=1.1.0,<2.0.0" +protobuf = [ + {version = ">=3.17.3,<4.0.0", markers = "python_version >= \"3.7\" and python_version < \"3.11\""}, + {version = ">=4.24,<5.0", markers = "python_version >= \"3.11\""}, +] +qm-octave = ">=2.0.1,<2.1.0" tinydb = ">=4.6.1,<5.0.0" +typing-extensions = ">=4.5,<5.0" [package.extras] simulation = ["certifi"] [[package]] name = "qualang-tools" -version = "0.14.0" +version = "0.15.2" description = "The qualang_tools package includes various tools related to QUA programs in Python" optional = false python-versions = ">=3.7.1,<4.0" files = [ - {file = "qualang_tools-0.14.0-py3-none-any.whl", hash = "sha256:c28dafdd3ab8dc0c75655f86872ff5e66e25d4d0d22e765297661cd6f0db952b"}, - {file = "qualang_tools-0.14.0.tar.gz", hash = "sha256:4bc45bc65d86bb933dce34a84b9d4fb50876b2dd015400e48297a53c55af3aec"}, + {file = "qualang_tools-0.15.2-py3-none-any.whl", hash = "sha256:37a21a889eef947c144b08bb1c8b6e90142c7ca71fa0c9e3d4eb0591db48f71c"}, + {file = "qualang_tools-0.15.2.tar.gz", hash = "sha256:3db332071073e4fe503c178ae0f83477176d72ba6636f3f6df1383d694f69284"}, ] [package.dependencies] @@ -4051,7 +4167,7 @@ dash-table = ">=5.0.0,<6.0.0" docutils = ">=0.14.0" matplotlib = ">=3.4.2,<4.0.0" numpy = ">=1.17.0,<2.0.0" -pandas = ">=1.2.4,<2.0.0" +pandas = ">=1.2.4" qm-qua = ">=1.1.0" scikit-learn = ">=1.0.2,<2.0.0" scipy = ">=1.7.1,<2.0.0" @@ -4126,6 +4242,23 @@ files = [ [package.dependencies] six = ">=1.7.0" +[[package]] +name = "rfc3986" +version = "1.5.0" +description = "Validating URI References per RFC 3986" +optional = false +python-versions = "*" +files = [ + {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, + {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, +] + +[package.dependencies] +idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} + +[package.extras] +idna2008 = ["idna"] + [[package]] name = "rich" version = "13.7.0" @@ -4473,6 +4606,17 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, +] + [[package]] name = "snowballstemmer" version = "2.2.0" @@ -5349,4 +5493,4 @@ zh = ["laboneq"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.12" -content-hash = "2dab07219f7c824c3f992fa25e9282b09b9ea3f22e0c2733ce33603f33b3533f" +content-hash = "952e2f183cb97eb7b50db8874833ce9f0e9926ec22a43c8ba061a3282b0483f7" diff --git a/pyproject.toml b/pyproject.toml index 97743b07a..3cf9faca8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,8 +31,8 @@ qblox-instruments = { version = "0.11.0", optional = true} qcodes = { version = "^0.37.0", optional = true } qcodes_contrib_drivers = { version ="0.18.0", optional = true } pyvisa-py = { version = "0.5.3", optional = true } -qm-qua = { version = "==1.1.1", optional = true } -qualang-tools = { version = "==0.14.0", optional = true} +qm-qua = { version = "^1.1.6", optional = true } +qualang-tools = { version = "^0.15.0", optional = true} setuptools = { version = ">67.0.0", optional = true } laboneq = { version = "==2.21.0", optional = true } qibosoq = { version = ">=0.0.4,<0.2", optional = true } @@ -54,7 +54,7 @@ qblox-instruments = "0.11.0" qcodes = "^0.37.0" qcodes_contrib_drivers = "0.18.0" qibosoq = ">=0.0.4,<0.2" -qualang-tools = "==0.14.0" +qualang-tools = "^0.15.0" laboneq = "==2.21.0" [tool.poetry.group.tests] From 5278b0f9be14995b9b041a07a41a95ee7f17d66a Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Mon, 1 Jan 2024 18:53:23 +0400 Subject: [PATCH 05/46] feat: instrument settings in runcard for QM --- doc/source/main-documentation/qibolab.rst | 2 +- src/qibolab/instruments/qm/__init__.py | 2 +- src/qibolab/instruments/qm/config.py | 4 +- .../qm/{driver.py => controller.py} | 4 + src/qibolab/instruments/qm/devices.py | 74 ++++++++++------- src/qibolab/instruments/qm/ports.py | 81 +++++++++---------- src/qibolab/instruments/qm/simulator.py | 2 +- src/qibolab/serialize.py | 17 ++-- tests/dummy_qrc/qm.py | 3 +- tests/test_instruments_qm.py | 2 +- 10 files changed, 109 insertions(+), 82 deletions(-) rename src/qibolab/instruments/qm/{driver.py => controller.py} (97%) diff --git a/doc/source/main-documentation/qibolab.rst b/doc/source/main-documentation/qibolab.rst index 0011fc589..5764871ef 100644 --- a/doc/source/main-documentation/qibolab.rst +++ b/doc/source/main-documentation/qibolab.rst @@ -706,7 +706,7 @@ A list of all the supported instruments follows: Controllers (subclasses of :class:`qibolab.instruments.abstract.Controller`): - Dummy Instrument: :class:`qibolab.instruments.dummy.DummyInstrument` - Zurich Instruments: :class:`qibolab.instruments.zhinst.Zurich` - - Quantum Machines: :class:`qibolab.instruments.qm.driver.QMOPX` + - Quantum Machines: :class:`qibolab.instruments.qm.controller.QMController` - Qblox: :class:`qibolab.instruments.qblox.cluster.Cluster` - Xilinx RFSoCs: :class:`qibolab.instruments.rfsoc.driver.RFSoC` diff --git a/src/qibolab/instruments/qm/__init__.py b/src/qibolab/instruments/qm/__init__.py index 8389e395d..041a7b916 100644 --- a/src/qibolab/instruments/qm/__init__.py +++ b/src/qibolab/instruments/qm/__init__.py @@ -1,3 +1,3 @@ +from .controller import QMController from .devices import Octave, OPXplus -from .driver import QMController from .simulator import QMSim diff --git a/src/qibolab/instruments/qm/config.py b/src/qibolab/instruments/qm/config.py index d0eeb4abc..14263a2dc 100644 --- a/src/qibolab/instruments/qm/config.py +++ b/src/qibolab/instruments/qm/config.py @@ -45,9 +45,9 @@ def register_port(self, port): controllers[port.device] = {} device = controllers[port.device] if port.key in device: - device[port.key].update(port.serial) + device[port.key].update(port.config) else: - device[port.key] = port.serial + device[port.key] = port.config @staticmethod def iq_imbalance(g, phi): diff --git a/src/qibolab/instruments/qm/driver.py b/src/qibolab/instruments/qm/controller.py similarity index 97% rename from src/qibolab/instruments/qm/driver.py rename to src/qibolab/instruments/qm/controller.py index 9be39438f..adc090648 100644 --- a/src/qibolab/instruments/qm/driver.py +++ b/src/qibolab/instruments/qm/controller.py @@ -109,6 +109,10 @@ class QMController(Controller): def __post_init__(self): super().__init__(self.name, self.address) + if isinstance(self.opxs, list): + self.opxs = {instr.name: instr for instr in self.opxs} + if isinstance(self.octaves, list): + self.octaves = {instr.name: instr for instr in self.octaves} def ports(self, name, input=False): if len(name) != 2: diff --git a/src/qibolab/instruments/qm/devices.py b/src/qibolab/instruments/qm/devices.py index d374c0190..e399541da 100644 --- a/src/qibolab/instruments/qm/devices.py +++ b/src/qibolab/instruments/qm/devices.py @@ -1,45 +1,65 @@ from dataclasses import dataclass from typing import Optional -from .ports import OctaveInput, OctaveOutput, OPXInput, OPXOutput, Ports +from .ports import OctaveInput, OctaveOutput, OPXInput, OPXOutput, QMPort + + +class Ports(dict): + def __init__(self, constructor, device): + self.constructor = constructor + self.device = device + super().__init__() + + def __getitem__(self, number): + if number not in self: + self[number] = self.constructor(self.device, number) + return super().__getitem__(number) @dataclass -class Octave: - name: str - port: int - connectivity: str +class QMDevice: + output_type = QMPort + input_type = QMPort - outputs: Optional[Ports[int, OctaveOutput]] = None - inputs: Optional[Ports[int, OctaveInput]] = None + name: str + outputs: Optional[Ports[int, QMPort]] = None + inputs: Optional[Ports[int, QMPort]] = None def __post_init__(self): - self.outputs = Ports(OctaveOutput, self.name) - self.inputs = Ports(OctaveInput, self.name) + self.outputs = Ports(self.output_type, self.name) + self.inputs = Ports(self.input_type, self.name) - def ports(self, name, input=False): + def ports(self, number, input=False): if input: - return self.inputs[name] + return self.inputs[number] else: - return self.outputs[name] + return self.outputs[number] + + def setup(self, **kwargs): + for number, settings in kwargs.items(): + if settings.pop("input", False): + self.inputs[number].setup(**settings) + else: + self.outputs[number].setup(**settings) + + def dump(self): + data = {port.name: port.settings for port in self.outputs} + data.update( + {port.name: port.settings | {"input": True} for port in self.inputs} + ) + return data @dataclass -class OPXplus: - number: int - outputs: Optional[Ports[int, OPXOutput]] = None - inputs: Optional[Ports[int, OPXInput]] = None +class OPXplus(QMDevice): + output_type = OPXOutput + input_type = OPXInput - @property - def name(self): - return f"con{self.number}" - def __post_init__(self): - self.outputs = Ports(OPXOutput, self.name) - self.inputs = Ports(OPXInput, self.name) +@dataclass +class Octave(QMDevice): + output_type = OctaveOutput + input_type = OctaveInput - def ports(self, name, input=False): - if input: - return self.inputs[name] - else: - return self.outputs[name] + port: int = 0 + connectivity: Optional[str] = None diff --git a/src/qibolab/instruments/qm/ports.py b/src/qibolab/instruments/qm/ports.py index 6f9234737..ad85e6292 100644 --- a/src/qibolab/instruments/qm/ports.py +++ b/src/qibolab/instruments/qm/ports.py @@ -1,4 +1,4 @@ -from dataclasses import asdict, dataclass, field +from dataclasses import dataclass, field, fields from typing import ClassVar, Dict, Optional, Union @@ -8,19 +8,31 @@ class QMPort: number: int key: ClassVar[Optional[str]] = None - _replace: ClassVar[dict] = {} @property def pair(self): return (self.device, self.number) + def setup(self, **kwargs): + for name, value in kwargs.items(): + if not hasattr(self, name): + raise KeyError(f"Unknown port setting {name}.") + setattr(self, name, value) + + @property + def settings(self): + return { + fld.name: fld.value + for fld in fields(self) + if fld.metadata.get("settings", False) + } + @property - def serial(self): - data = asdict(self) - del data["device"] - del data["number"] - for old, new in self._replace.items(): - data[new] = data.pop(old) + def config(self): + data = {} + for fld in fields(self): + if "config" in fld.metadata: + data[fld.metadata["config"]] = fld.value return {self.number: data} @@ -28,17 +40,18 @@ def serial(self): class OPXOutput(QMPort): key: ClassVar[str] = "analog_outputs" - offset: float = 0.0 - filter: Dict[str, float] = field(default_factory=dict) + offset: float = field(default=0.0, metadata={"config": "offset", "settings": True}) + filter: Dict[str, float] = field( + default_factory=dict, metadata={"config": "filter", "settings": True} + ) @dataclass class OPXInput(QMPort): key: ClassVar[str] = "analog_inputs" - _replace: ClassVar[dict] = {"gain": "gain_db"} - offset: float = 0.0 - gain: int = 0 + offset: float = field(default=0.0, metadata={"config": "offset", "settings": True}) + gain: int = field(default=0, metadata={"config": "gain_db", "settings": True}) @dataclass @@ -50,41 +63,25 @@ class OPXIQ: @dataclass class OctaveOutput(QMPort): key: ClassVar[str] = "RF_outputs" - _replace: ClassVar[dict] = { - "lo_frequency": "LO_frequency", - "lo_source": "LO_source", - } - lo_frequency: float = 0.0 - gain: float = 0 + lo_frequency: float = field( + default=0.0, metadata={"config": "LO_frequency", "settings": True} + ) + gain: int = field(default=0, metadata={"config": "gain", "settings": True}) """Can be in the range [-20 : 0.5 : 20]dB.""" - lo_source: str = "internal" + lo_source: str = field(default="internal", metadata={"config": "LO_source"}) """Can be external or internal.""" - output_mode: str = "always_on" + output_mode: str = field(default="always_on", metadata={"config": "output_mode"}) """Can be: "always_on" / "always_off"/ "triggered" / "triggered_reversed".""" @dataclass class OctaveInput(QMPort): key: ClassVar[str] = "RF_inputs" - _replace: ClassVar[dict] = { - "lo_frequency": "LO_frequency", - "lo_source": "LO_source", - } - - lo_frequency: float = 0.0 - lo_source: str = "internal" - IF_mode_I: str = "direct" - IF_mode_Q: str = "direct" - - -class Ports(dict): - def __init__(self, constructor, device): - self.constructor = constructor - self.device = device - super().__init__() - - def __getitem__(self, number): - if number not in self: - self[number] = self.constructor(self.device, number) - return super().__getitem__(number) + + lo_frequency: float = field( + default=0.0, metadata={"config": "LO_frequency", "settings": True} + ) + lo_source: str = field(default="internal", metadata={"config": "LO_source"}) + IF_mode_I: str = field(default="direct", metadata={"config": "IF_mode_I"}) + IF_mode_Q: str = field(default="direct", metadata={"config": "IF_mode_Q"}) diff --git a/src/qibolab/instruments/qm/simulator.py b/src/qibolab/instruments/qm/simulator.py index 7e61aa34b..91a7a5f6a 100644 --- a/src/qibolab/instruments/qm/simulator.py +++ b/src/qibolab/instruments/qm/simulator.py @@ -4,7 +4,7 @@ from qm.QuantumMachinesManager import QuantumMachinesManager from qualang_tools.simulator_tools import create_simulator_controller_connections -from .driver import QMController +from .controller import QMController @dataclass diff --git a/src/qibolab/serialize.py b/src/qibolab/serialize.py index 5cb6476b3..0da32a471 100644 --- a/src/qibolab/serialize.py +++ b/src/qibolab/serialize.py @@ -154,12 +154,17 @@ def dump_instruments(instruments: InstrumentMap) -> dict: # Qblox modules settings are dictionaries and not dataclasses data = {} for name, instrument in instruments.items(): - settings = instrument.settings - if settings is not None: - if isinstance(settings, dict): - data[name] = settings - else: - data[name] = settings.dump() + try: + # TODO: Migrate all instruments to this approach + # (I think it is also useful for qblox) + data[name] = instrument.dump() + except AttributeError: + settings = instrument.settings + if settings is not None: + if isinstance(settings, dict): + data[name] = settings + else: + data[name] = settings.dump() return data diff --git a/tests/dummy_qrc/qm.py b/tests/dummy_qrc/qm.py index 2f3692b69..8219397c4 100644 --- a/tests/dummy_qrc/qm.py +++ b/tests/dummy_qrc/qm.py @@ -22,7 +22,7 @@ def create(runcard_path=RUNCARD): Used in ``test_instruments_qm.py`` and ``test_instruments_qmsim.py`` """ - opxs = [OPXplus(i) for i in range(1, 4)] + opxs = [OPXplus(f"con{i}") for i in range(1, 4)] controller = QMController("qm", "192.168.0.101:80", opxs=opxs, time_of_flight=280) # Create channel objects and map controllers to channels @@ -99,6 +99,7 @@ def create(runcard_path=RUNCARD): qubits[q].flux.max_bias = 0.2 instruments = {controller.name: controller} + instruments.update(controller.opxs) instruments.update({lo.name: lo for lo in local_oscillators}) settings = load_settings(runcard) instruments = load_instrument_settings(runcard, instruments) diff --git a/tests/test_instruments_qm.py b/tests/test_instruments_qm.py index 5c369ca43..3c540a25d 100644 --- a/tests/test_instruments_qm.py +++ b/tests/test_instruments_qm.py @@ -5,8 +5,8 @@ from qm import qua from qibolab import AcquisitionType, ExecutionParameters, create_platform -from qibolab.instruments.qm import QMOPX, QMPort from qibolab.instruments.qm.acquisition import Acquisition +from qibolab.instruments.qm.ports import QMPort from qibolab.instruments.qm.sequence import BakedPulse, QMPulse, Sequence from qibolab.pulses import FluxPulse, Pulse, PulseSequence, ReadoutPulse, Rectangular from qibolab.sweeper import Parameter, Sweeper From 30ce83ffc14fde9e755e0a36a6f5736b5c77d9e7 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Mon, 1 Jan 2024 19:19:43 +0400 Subject: [PATCH 06/46] fix: fix qm devices serialization --- src/qibolab/instruments/qm/devices.py | 11 +++++++---- src/qibolab/instruments/qm/ports.py | 8 ++++---- src/qibolab/serialize.py | 15 ++++++++++----- tests/dummy_qrc/qm.yml | 12 ++++++++++++ 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/qibolab/instruments/qm/devices.py b/src/qibolab/instruments/qm/devices.py index e399541da..2a3acc68f 100644 --- a/src/qibolab/instruments/qm/devices.py +++ b/src/qibolab/instruments/qm/devices.py @@ -35,17 +35,20 @@ def ports(self, number, input=False): else: return self.outputs[number] - def setup(self, **kwargs): - for number, settings in kwargs.items(): + def setup(self, port_settings): + for number, settings in port_settings.items(): if settings.pop("input", False): self.inputs[number].setup(**settings) else: self.outputs[number].setup(**settings) def dump(self): - data = {port.name: port.settings for port in self.outputs} + data = {port.number: port.settings for port in self.outputs.values()} data.update( - {port.name: port.settings | {"input": True} for port in self.inputs} + { + port.number: port.settings | {"input": True} + for port in self.inputs.values() + } ) return data diff --git a/src/qibolab/instruments/qm/ports.py b/src/qibolab/instruments/qm/ports.py index ad85e6292..316ebb076 100644 --- a/src/qibolab/instruments/qm/ports.py +++ b/src/qibolab/instruments/qm/ports.py @@ -22,7 +22,7 @@ def setup(self, **kwargs): @property def settings(self): return { - fld.name: fld.value + fld.name: getattr(self, fld.name) for fld in fields(self) if fld.metadata.get("settings", False) } @@ -32,7 +32,7 @@ def config(self): data = {} for fld in fields(self): if "config" in fld.metadata: - data[fld.metadata["config"]] = fld.value + data[fld.metadata["config"]] = getattr(self, fld.name) return {self.number: data} @@ -40,7 +40,7 @@ def config(self): class OPXOutput(QMPort): key: ClassVar[str] = "analog_outputs" - offset: float = field(default=0.0, metadata={"config": "offset", "settings": True}) + offset: float = field(default=0.0, metadata={"config": "offset"}) filter: Dict[str, float] = field( default_factory=dict, metadata={"config": "filter", "settings": True} ) @@ -50,7 +50,7 @@ class OPXOutput(QMPort): class OPXInput(QMPort): key: ClassVar[str] = "analog_inputs" - offset: float = field(default=0.0, metadata={"config": "offset", "settings": True}) + offset: float = field(default=0.0, metadata={"config": "offset"}) gain: int = field(default=0, metadata={"config": "gain_db", "settings": True}) diff --git a/src/qibolab/serialize.py b/src/qibolab/serialize.py index 0da32a471..31b28dd1f 100644 --- a/src/qibolab/serialize.py +++ b/src/qibolab/serialize.py @@ -103,7 +103,10 @@ def load_instrument_settings( ) -> InstrumentMap: """Setup instruments according to the settings given in the runcard.""" for name, settings in runcard.get("instruments", {}).items(): - instruments[name].setup(**settings) + try: + instruments[name].setup(**settings) + except TypeError: + instruments[name].setup(settings) return instruments @@ -155,16 +158,18 @@ def dump_instruments(instruments: InstrumentMap) -> dict: data = {} for name, instrument in instruments.items(): try: - # TODO: Migrate all instruments to this approach - # (I think it is also useful for qblox) - data[name] = instrument.dump() - except AttributeError: settings = instrument.settings if settings is not None: if isinstance(settings, dict): data[name] = settings else: data[name] = settings.dump() + except AttributeError: + # TODO: Migrate all instruments to this approach + # (I think it is also useful for qblox) + settings = instrument.dump() + if len(settings) > 0: + data[name] = settings return data diff --git a/tests/dummy_qrc/qm.yml b/tests/dummy_qrc/qm.yml index 812855afe..d675131d9 100644 --- a/tests/dummy_qrc/qm.yml +++ b/tests/dummy_qrc/qm.yml @@ -9,6 +9,18 @@ settings: topology: [[0, 2], [1, 2], [2, 3], [2, 4]] instruments: + con2: + 1: + filter: {} + 2: + filter: {} + 3: + filter: {} + 4: + filter: {} + 5: + filter: {} + lo_readout_a: frequency: 7_300_000_000 power: 18 From 898232a2c2cec9e7a638613487aa0876460835ec Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Mon, 1 Jan 2024 19:28:28 +0400 Subject: [PATCH 07/46] fix: inherit QM devices from Instrument --- src/qibolab/instruments/qm/devices.py | 37 ++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/qibolab/instruments/qm/devices.py b/src/qibolab/instruments/qm/devices.py index 2a3acc68f..ad5f25a9e 100644 --- a/src/qibolab/instruments/qm/devices.py +++ b/src/qibolab/instruments/qm/devices.py @@ -1,6 +1,8 @@ from dataclasses import dataclass from typing import Optional +from qibolab.instruments.abstract import Instrument + from .ports import OctaveInput, OctaveOutput, OPXInput, OPXOutput, QMPort @@ -17,7 +19,7 @@ def __getitem__(self, number): @dataclass -class QMDevice: +class QMDevice(Instrument): output_type = QMPort input_type = QMPort @@ -35,12 +37,33 @@ def ports(self, number, input=False): else: return self.outputs[number] - def setup(self, port_settings): - for number, settings in port_settings.items(): - if settings.pop("input", False): - self.inputs[number].setup(**settings) - else: - self.outputs[number].setup(**settings) + def connect(self): + """Only applicable for + :class:`qibolab.instruments.qm.controller.QMController`, not individual + devices.""" + + def start(self): + """Only applicable for + :class:`qibolab.instruments.qm.controller.QMController`, not individual + devices.""" + + def setup(self, port_settings=None): + if port_settings is not None: + for number, settings in port_settings.items(): + if settings.pop("input", False): + self.inputs[number].setup(**settings) + else: + self.outputs[number].setup(**settings) + + def stop(self): + """Only applicable for + :class:`qibolab.instruments.qm.controller.QMController`, not individual + devices.""" + + def disconnect(self): + """Only applicable for + :class:`qibolab.instruments.qm.controller.QMController`, not individual + devices.""" def dump(self): data = {port.number: port.settings for port in self.outputs.values()} From fbc1f7cca0bb4e4d54e2d97956f9ee344983f134 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 3 Jan 2024 17:59:29 +0400 Subject: [PATCH 08/46] fix: fix execution when using Octaves --- src/qibolab/instruments/qm/config.py | 28 ++++++++++++--------- src/qibolab/instruments/qm/controller.py | 26 ++++++++------------ src/qibolab/instruments/qm/devices.py | 31 +++++++++++++----------- src/qibolab/instruments/qm/ports.py | 4 +++ src/qibolab/instruments/qm/sweepers.py | 4 +-- 5 files changed, 49 insertions(+), 44 deletions(-) diff --git a/src/qibolab/instruments/qm/config.py b/src/qibolab/instruments/qm/config.py index 14263a2dc..8d03378a5 100644 --- a/src/qibolab/instruments/qm/config.py +++ b/src/qibolab/instruments/qm/config.py @@ -6,7 +6,7 @@ from qibolab.pulses import PulseType, Rectangular -from .ports import OPXIQ, OPXInput, OPXOutput +from .ports import OPXIQ, OctaveInput, OctaveOutput SAMPLING_RATE = 1 """Sampling rate of Quantum Machines OPX in GSps.""" @@ -36,18 +36,22 @@ def register_port(self, port): Contains information about the controller and port number and some parameters, such as offset, gain, filter, etc.). """ - controllers = ( - self.controllers - if isinstance(port, (OPXInput, OPXOutput)) - else self.octaves - ) - if port.device not in controllers: - controllers[port.device] = {} - device = controllers[port.device] - if port.key in device: - device[port.key].update(port.config) + if isinstance(port, OPXIQ): + self.register_port(port.i) + self.register_port(port.q) else: - device[port.key] = port.config + is_octave = isinstance(port, (OctaveOutput, OctaveInput)) + controllers = self.octaves if is_octave else self.controllers + if port.device not in controllers: + controllers[port.device] = {} + device = controllers[port.device] + if port.key in device: + device[port.key].update(port.config) + else: + device[port.key] = port.config + if is_octave: + device["connectivity"] = port.opx_port.i.device + self.register_port(port.opx_port) @staticmethod def iq_imbalance(g, phi): diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index adc090648..0828d29bf 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -13,7 +13,7 @@ from .config import QMConfig from .devices import Octave, OPXplus -from .ports import OPXIQ, OctaveInput, OctaveOutput, OPXInput, OPXOutput +from .ports import OPXIQ, OPXInput, OPXOutput from .sequence import BakedPulse, QMPulse, Sequence from .sweepers import sweep @@ -37,7 +37,7 @@ def declare_octaves(octaves, host): if len(octaves) > 0: config = QmOctaveConfig() # config.set_calibration_db(os.getcwd()) - for octave in octaves: + for octave in octaves.values(): config.add_device_info(octave.name, host, OCTAVE_ADDRESS + octave.port) return config @@ -185,17 +185,6 @@ def fetch_results(result, ro_pulses): ) return results - def register_port(self, port): - if isinstance(port, OPXIQ): - self.register_port(port.i) - self.register_port(port.q) - else: - self.config.register_port(port) - if isinstance(port, (OctaveInput, OctaveOutput)): - self.config.octaves[port.device]["connectivity"] = self.octaves[ - port.device - ].connectivity - def create_sequence(self, qubits, sequence, sweepers): """Translates a :class:`qibolab.pulses.PulseSequence` to a :class:`qibolab.instruments.qm.sequence.Sequence`. @@ -219,9 +208,9 @@ def create_sequence(self, qubits, sequence, sweepers): for pulse in sorted(sequence.pulses, key=sort_key): qubit = qubits[pulse.qubit] - self.register_port(getattr(qubit, pulse.type.name.lower()).port) + self.config.register_port(getattr(qubit, pulse.type.name.lower()).port) if pulse.type is PulseType.READOUT: - self.register_port(qubit.feedback.port) + self.config.register_port(qubit.feedback.port) self.config.register_element( qubit, pulse, self.time_of_flight, self.smearing @@ -256,7 +245,7 @@ def sweep(self, qubits, couplers, sequence, options, *sweepers): # always at sweetspot even when they are not used for qubit in qubits.values(): if qubit.flux: - self.register_port(qubit.flux.port) + self.config.register_port(qubit.flux.port) self.config.register_flux_element(qubit) qmsequence = self.create_sequence(qubits, sequence, sweepers) @@ -281,6 +270,11 @@ def sweep(self, qubits, couplers, sequence, options, *sweepers): for qmpulse in qmsequence.ro_pulses: qmpulse.acquisition.download(*buffer_dims) + import json + + with open("qm_config.json", "w") as file: + file.write(json.dumps(self.config.__dict__, indent=4)) + if self.script_file_name is not None: with open(self.script_file_name, "w") as file: file.write(generate_qua_script(experiment, self.config.__dict__)) diff --git a/src/qibolab/instruments/qm/devices.py b/src/qibolab/instruments/qm/devices.py index ad5f25a9e..d55f5808e 100644 --- a/src/qibolab/instruments/qm/devices.py +++ b/src/qibolab/instruments/qm/devices.py @@ -3,7 +3,7 @@ from qibolab.instruments.abstract import Instrument -from .ports import OctaveInput, OctaveOutput, OPXInput, OPXOutput, QMPort +from .ports import OPXIQ, OctaveInput, OctaveOutput, OPXInput, OPXOutput, QMPort class Ports(dict): @@ -20,17 +20,13 @@ def __getitem__(self, number): @dataclass class QMDevice(Instrument): - output_type = QMPort - input_type = QMPort - name: str + port: Optional[int] = None + connectivity: Optional["QMDevice"] = None + outputs: Optional[Ports[int, QMPort]] = None inputs: Optional[Ports[int, QMPort]] = None - def __post_init__(self): - self.outputs = Ports(self.output_type, self.name) - self.inputs = Ports(self.input_type, self.name) - def ports(self, number, input=False): if input: return self.inputs[number] @@ -78,14 +74,21 @@ def dump(self): @dataclass class OPXplus(QMDevice): - output_type = OPXOutput - input_type = OPXInput + def __post_init__(self): + self.outputs = Ports(OPXOutput, self.name) + self.inputs = Ports(OPXInput, self.name) @dataclass class Octave(QMDevice): - output_type = OctaveOutput - input_type = OctaveInput + def __post_init__(self): + self.outputs = Ports(OctaveOutput, self.name) + self.inputs = Ports(OctaveInput, self.name) - port: int = 0 - connectivity: Optional[str] = None + def ports(self, number, input=False): + port = super().ports(number, input) + if port.opx_port is None: + iport = self.connectivity.ports(2 * number - 1, input) + qport = self.connectivity.ports(2 * number, input) + port.opx_port = OPXIQ(iport, qport) + return port diff --git a/src/qibolab/instruments/qm/ports.py b/src/qibolab/instruments/qm/ports.py index 316ebb076..d23daf893 100644 --- a/src/qibolab/instruments/qm/ports.py +++ b/src/qibolab/instruments/qm/ports.py @@ -74,6 +74,8 @@ class OctaveOutput(QMPort): output_mode: str = field(default="always_on", metadata={"config": "output_mode"}) """Can be: "always_on" / "always_off"/ "triggered" / "triggered_reversed".""" + opx_port: Optional[OPXOutput] = None + @dataclass class OctaveInput(QMPort): @@ -85,3 +87,5 @@ class OctaveInput(QMPort): lo_source: str = field(default="internal", metadata={"config": "LO_source"}) IF_mode_I: str = field(default="direct", metadata={"config": "IF_mode_I"}) IF_mode_Q: str = field(default="direct", metadata={"config": "IF_mode_Q"}) + + opx_port: Optional[OPXOutput] = None diff --git a/src/qibolab/instruments/qm/sweepers.py b/src/qibolab/instruments/qm/sweepers.py index 44ed5eef7..e23b79ebe 100644 --- a/src/qibolab/instruments/qm/sweepers.py +++ b/src/qibolab/instruments/qm/sweepers.py @@ -73,9 +73,9 @@ def _sweep_frequency(sweepers, qubits, qmsequence, relaxation_time): for pulse in sweeper.pulses: qubit = qubits[pulse.qubit] if pulse.type is PulseType.DRIVE: - lo_frequency = math.floor(qubit.drive.local_oscillator.frequency) + lo_frequency = math.floor(qubit.drive.lo_frequency) elif pulse.type is PulseType.READOUT: - lo_frequency = math.floor(qubit.readout.local_oscillator.frequency) + lo_frequency = math.floor(qubit.readout.lo_frequency) else: raise_error( NotImplementedError, From 53799da0f259053b645dae570c7949432b3f8c5c Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 3 Jan 2024 18:18:55 +0400 Subject: [PATCH 09/46] fix: fix duplicate port names for input/output in runcard --- src/qibolab/instruments/qm/devices.py | 41 ++-- src/qibolab/instruments/qm/ports.py | 20 +- tests/conftest.py | 2 +- tests/dummy_qrc/qm.yml | 10 +- tests/dummy_qrc/qm_octave.py | 94 ++++++++ tests/dummy_qrc/qm_octave.yml | 301 ++++++++++++++++++++++++++ 6 files changed, 440 insertions(+), 28 deletions(-) create mode 100644 tests/dummy_qrc/qm_octave.py create mode 100644 tests/dummy_qrc/qm_octave.yml diff --git a/src/qibolab/instruments/qm/devices.py b/src/qibolab/instruments/qm/devices.py index d55f5808e..0a606c798 100644 --- a/src/qibolab/instruments/qm/devices.py +++ b/src/qibolab/instruments/qm/devices.py @@ -3,7 +3,15 @@ from qibolab.instruments.abstract import Instrument -from .ports import OPXIQ, OctaveInput, OctaveOutput, OPXInput, OPXOutput, QMPort +from .ports import ( + OPXIQ, + OctaveInput, + OctaveOutput, + OPXInput, + OPXOutput, + QMInput, + QMOutput, +) class Ports(dict): @@ -24,8 +32,8 @@ class QMDevice(Instrument): port: Optional[int] = None connectivity: Optional["QMDevice"] = None - outputs: Optional[Ports[int, QMPort]] = None - inputs: Optional[Ports[int, QMPort]] = None + outputs: Optional[Ports[int, QMOutput]] = None + inputs: Optional[Ports[int, QMInput]] = None def ports(self, number, input=False): if input: @@ -43,13 +51,17 @@ def start(self): :class:`qibolab.instruments.qm.controller.QMController`, not individual devices.""" - def setup(self, port_settings=None): - if port_settings is not None: - for number, settings in port_settings.items(): - if settings.pop("input", False): - self.inputs[number].setup(**settings) - else: - self.outputs[number].setup(**settings) + def setup(self, **kwargs): + for name, settings in kwargs.items(): + number = int(name[1:]) + if name[0] == "o": + self.outputs[number].setup(**settings) + elif name[0] == "i": + self.inputs[number].setup(**settings) + else: + raise ValueError( + f"Invalid port name {name} in instrument settings for {self.name}." + ) def stop(self): """Only applicable for @@ -62,14 +74,7 @@ def disconnect(self): devices.""" def dump(self): - data = {port.number: port.settings for port in self.outputs.values()} - data.update( - { - port.number: port.settings | {"input": True} - for port in self.inputs.values() - } - ) - return data + return {port.name: port.settings for port in self.outputs.values()} @dataclass diff --git a/src/qibolab/instruments/qm/ports.py b/src/qibolab/instruments/qm/ports.py index d23daf893..70518bd75 100644 --- a/src/qibolab/instruments/qm/ports.py +++ b/src/qibolab/instruments/qm/ports.py @@ -36,8 +36,20 @@ def config(self): return {self.number: data} +class QMOutput(QMPort): + @property + def name(self): + return f"o{self.number}" + + +class QMInput(QMPort): + @property + def name(self): + return f"i{self.number}" + + @dataclass -class OPXOutput(QMPort): +class OPXOutput(QMOutput): key: ClassVar[str] = "analog_outputs" offset: float = field(default=0.0, metadata={"config": "offset"}) @@ -47,7 +59,7 @@ class OPXOutput(QMPort): @dataclass -class OPXInput(QMPort): +class OPXInput(QMInput): key: ClassVar[str] = "analog_inputs" offset: float = field(default=0.0, metadata={"config": "offset"}) @@ -61,7 +73,7 @@ class OPXIQ: @dataclass -class OctaveOutput(QMPort): +class OctaveOutput(QMOutput): key: ClassVar[str] = "RF_outputs" lo_frequency: float = field( @@ -78,7 +90,7 @@ class OctaveOutput(QMPort): @dataclass -class OctaveInput(QMPort): +class OctaveInput(QMInput): key: ClassVar[str] = "RF_inputs" lo_frequency: float = field( diff --git a/tests/conftest.py b/tests/conftest.py index 9ca69576f..d18efa987 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ from qibolab import PLATFORMS, create_platform ORIGINAL_PLATFORMS = os.environ.get(PLATFORMS, "") -DUMMY_PLATFORM_NAMES = ["qm", "qblox", "rfsoc", "zurich"] +DUMMY_PLATFORM_NAMES = ["qm", "qm_octave", "qblox", "rfsoc", "zurich"] def pytest_addoption(parser): diff --git a/tests/dummy_qrc/qm.yml b/tests/dummy_qrc/qm.yml index d675131d9..9e42d58b6 100644 --- a/tests/dummy_qrc/qm.yml +++ b/tests/dummy_qrc/qm.yml @@ -10,15 +10,15 @@ topology: [[0, 2], [1, 2], [2, 3], [2, 4]] instruments: con2: - 1: + o1: filter: {} - 2: + o2: filter: {} - 3: + o3: filter: {} - 4: + o4: filter: {} - 5: + o5: filter: {} lo_readout_a: diff --git a/tests/dummy_qrc/qm_octave.py b/tests/dummy_qrc/qm_octave.py new file mode 100644 index 000000000..024682a5f --- /dev/null +++ b/tests/dummy_qrc/qm_octave.py @@ -0,0 +1,94 @@ +import pathlib + +from qibolab.channels import Channel, ChannelMap +from qibolab.instruments.dummy import DummyLocalOscillator as LocalOscillator +from qibolab.instruments.qm import Octave, OPXplus, QMController +from qibolab.platform import Platform +from qibolab.serialize import ( + load_instrument_settings, + load_qubits, + load_runcard, + load_settings, +) + +RUNCARD = pathlib.Path(__file__).parent / "qm_octave.yml" + + +def create(runcard_path=RUNCARD): + """Dummy platform using Quantum Machines (QM) OPXs and Rohde Schwarz local + oscillators. + + Based on QuantWare 5-qubit device. + + Used in ``test_instruments_qm.py`` and ``test_instruments_qmsim.py`` + """ + opxs = [OPXplus(f"con{i}") for i in range(1, 4)] + octave1 = Octave("octave1", port=100, connectivity=opxs[0]) + octave2 = Octave("octave2", port=101, connectivity=opxs[1]) + octave3 = Octave("octave3", port=102, connectivity=opxs[2]) + controller = QMController( + "qm", + "192.168.0.101:80", + opxs=opxs, + octaves=[octave1, octave2, octave3], + time_of_flight=280, + ) + + # Create channel objects and map controllers to channels + channels = ChannelMap() + # readout + channels |= Channel("L3-25_a", port=octave1.ports(5)) + channels |= Channel("L3-25_b", port=octave2.ports(5)) + # feedback + channels |= Channel("L2-5_a", port=octave1.ports(1, input=True)) + channels |= Channel("L2-5_b", port=octave2.ports(1, input=True)) + # drive + channels |= (Channel(f"L3-1{i}", port=octave1.ports(i)) for i in range(1, 5)) + channels |= Channel("L3-15", port=octave3.ports(1)) + # flux + channels |= (Channel(f"L4-{i}", port=opxs[1].ports(i)) for i in range(1, 6)) + # TWPA + channels |= "L4-26" + + # Instantiate local oscillators + twpa = LocalOscillator("twpa_a", "192.168.0.35") + # Map LOs to channels + channels["L4-26"].local_oscillator = twpa + + # create qubit objects + runcard = load_runcard(runcard_path) + qubits, couplers, pairs = load_qubits(runcard) + + # assign channels to qubits + for q in [0, 1]: + qubits[q].readout = channels["L3-25_a"] + qubits[q].feedback = channels["L2-5_a"] + for q in [2, 3, 4]: + qubits[q].readout = channels["L3-25_b"] + qubits[q].feedback = channels["L2-5_b"] + + qubits[0].drive = channels["L3-15"] + qubits[0].flux = channels["L4-5"] + for q in range(1, 5): + qubits[q].drive = channels[f"L3-{10 + q}"] + qubits[q].flux = channels[f"L4-{q}"] + + # set filter for flux channel + qubits[2].flux.filters = { + "feedforward": [1.0684635881381783, -1.0163217174522334], + "feedback": [0.947858129314055], + } + + # set maximum allowed bias values to protect amplifier + # relevant only for qubits where an amplifier is used + for q in range(5): + qubits[q].flux.max_bias = 0.2 + + instruments = {controller.name: controller, twpa.name: twpa} + instruments.update(controller.opxs) + instruments.update(controller.octaves) + settings = load_settings(runcard) + instruments = load_instrument_settings(runcard, instruments) + return Platform( + "qm_octave", qubits, pairs, instruments, settings, resonator_type="2D" + ) diff --git a/tests/dummy_qrc/qm_octave.yml b/tests/dummy_qrc/qm_octave.yml new file mode 100644 index 000000000..f11440dc2 --- /dev/null +++ b/tests/dummy_qrc/qm_octave.yml @@ -0,0 +1,301 @@ +nqubits: 5 + +qubits: [0, 1, 2, 3, 4] + +settings: + nshots: 1024 + relaxation_time: 50_000 + +topology: [[0, 2], [1, 2], [2, 3], [2, 4]] + +instruments: + con2: + o1: + filter: {} + o2: + filter: {} + o3: + filter: {} + o4: + filter: {} + o5: + filter: {} + octave1: + o1: + lo_frequency: 4_700_000_000 + o2: + lo_frequency: 5_600_000_000 + o3: + lo_frequency: 6_500_000_000 + o4: + lo_frequency: 6_500_000_000 + o5: + lo_frequency: 7_300_000_000 + i1: + lo_frequency: 7_300_000_000 + octave2: + o5: + lo_frequency: 7_900_000_000 + i1: + lo_frequency: 7_900_000_000 + octave3: + o1: + lo_frequency: 4_700_000_000 + twpa_a: + frequency: 6_511_000_000 + power: 4.5 + + +native_gates: + single_qubit: + 0: # qubit number + RX: + duration: 40 + amplitude: 0.005 + frequency: 4_700_000_000 + shape: Gaussian(5) + type: qd # qubit drive + relative_start: 0 + phase: 0 + RX12: + duration: 40 + amplitude: 0.005 + frequency: 4_700_000_000 + shape: Gaussian(5) + type: qd # qubit drive + relative_start: 0 + phase: 0 + MZ: + duration: 1000 + amplitude: 0.0025 + frequency: 7_226_500_000 + shape: Rectangular() + type: ro # readout + relative_start: 0 + phase: 0 + 1: # qubit number + RX: + duration: 40 + amplitude: 0.0484 + frequency: 4_855_663_000 + #frequency: 4_718_515_000 # 02 transition (more likely) + shape: Drag(5, -0.02) + type: qd # qubit drive + relative_start: 0 + phase: 0 + RX12: + duration: 40 + amplitude: 0.0484 + frequency: 4_855_663_000 + #frequency: 4_718_515_000 # 02 transition (more likely) + shape: Drag(5, -0.02) + type: qd # qubit drive + relative_start: 0 + phase: 0 + MZ: + duration: 620 + amplitude: 0.003575 + frequency: 7_453_265_000 + shape: Rectangular() + type: ro # readout + relative_start: 0 + phase: 0 + 2: # qubit number + RX: + duration: 40 + amplitude: 0.05682 + frequency: 5_800_563_000 + #frequency: 5_661_400_000 # 02 transition + shape: Drag(5, -0.04) #Gaussian(5) + type: qd # qubit drive + relative_start: 0 + phase: 0 + RX12: + duration: 40 + amplitude: 0.05682 + frequency: 5_800_563_000 + #frequency: 5_661_400_000 # 02 transition + shape: Drag(5, -0.04) #Gaussian(5) + type: qd # qubit drive + relative_start: 0 + phase: 0 + MZ: + duration: 960 + amplitude: 0.00325 + frequency: 7_655_107_000 + shape: Rectangular() + type: ro # readout + relative_start: 0 + phase: 0 + 3: # qubit number + RX: + duration: 40 + amplitude: 0.138 + frequency: 6_760_922_000 + #frequency: 6_628_822_000 # 02 transition + shape: Gaussian(5) + type: qd # qubit drive + relative_start: 0 + phase: 0 + RX12: + duration: 40 + amplitude: 0.138 + frequency: 6_760_922_000 + #frequency: 6_628_822_000 # 02 transition + shape: Gaussian(5) + type: qd # qubit drive + relative_start: 0 + phase: 0 + MZ: + duration: 960 + amplitude: 0.004225 + frequency: 7_802_191_000 + shape: Rectangular() + type: ro # readout + relative_start: 0 + phase: 0 + 4: # qubit number + RX: + duration: 40 + amplitude: 0.0617 + frequency: 6_585_053_000 + shape: Drag(5, 0.0) #Gaussian(5) + type: qd # qubit drive + relative_start: 0 + phase: 0 + RX12: + duration: 40 + amplitude: 0.0617 + frequency: 6_585_053_000 + shape: Drag(5, 0.0) #Gaussian(5) + type: qd # qubit drive + relative_start: 0 + phase: 0 + MZ: + duration: 640 + amplitude: 0.0039 + frequency: 8_057_668_000 + shape: Rectangular() + type: ro # readout + relative_start: 0 + phase: 0 + + two_qubit: + 1-2: + CZ: + - duration: 30 + amplitude: 0.055 + shape: Rectangular() + qubit: 2 + relative_start: 0 + type: qf + - type: virtual_z + phase: -1.5707963267948966 + qubit: 1 + - type: virtual_z + phase: -1.5707963267948966 + qubit: 2 + 2-3: + CZ: + - duration: 32 + amplitude: -0.0513 + shape: Rectangular() + qubit: 3 + relative_start: 0 + type: qf + - type: virtual_z + phase: -1.5707963267948966 + qubit: 2 + - type: virtual_z + phase: -1.5707963267948966 + qubit: 3 + + +characterization: + single_qubit: + 0: + readout_frequency: 0.0 + drive_frequency: 0.0 + T1: 0.0 + T2: 0.0 + sweetspot: 0.0 + # parameters for single shot classification + threshold: 0.0 + iq_angle: 0.00 + # required for mixers (not sure if it should be here) + mixer_drive_g: 0.0 + mixer_drive_phi: 0.0 + mixer_readout_g: 0.0 + mixer_readout_phi: 0.0 + 1: + readout_frequency: 7_453_265_000 + drive_frequency: 4_855_663_000 + T1: 0.0 + T2: 0.0 + sweetspot: -0.047 + # parameters for single shot classification + threshold: 0.00028502261712637096 + iq_angle: 1.283105298787488 + # Software classifier + # classifier: + # type: "scikit" + # model: "qw5q_gold/RBF_SVM_1.pkl" + # required for mixers (not sure if it should be here) + mixer_drive_g: 0.0 + mixer_drive_phi: 0.0 + mixer_readout_g: 0.0 + mixer_readout_phi: 0.0 + 2: + readout_frequency: 7_655_107_000 + drive_frequency: 5_799_876_000 + T1: 0.0 + T2: 0.0 + sweetspot: -0.045 + # parameters for single shot classification + threshold: 0.0002694329123116206 + iq_angle: 4.912447775569025 + # Software classifier + # classifier: + # type: "scikit" + # model: "qw5q_gold/RBF_SVM_2.pkl" + # required for mixers (not sure if it should be here) + mixer_drive_g: 0.0 + mixer_drive_phi: 0.0 + mixer_readout_g: 0.0 + mixer_readout_phi: 0.0 + 3: + readout_frequency: 7_802_391_000 + drive_frequency: 6_760_700_000 + T1: 0.0 + T2: 0.0 + sweetspot: 0.034 + # parameters for single shot classification + threshold: 0.0003363427381347193 + iq_angle: 1.6124890998581591 + # Software classifier + # classifier: + # type: "scikit" + # model: "qw5q_gold/RBF_SVM_3.pkl" + # required for mixers (not sure if it should be here) + mixer_drive_g: 0.0 + mixer_drive_phi: 0.0 + mixer_readout_g: 0.0 + mixer_readout_phi: 0.0 + 4: + readout_frequency: 8_057_668_000 + drive_frequency: 6_585_053_000 + T1: 0.0 + T2: 0.0 + sweetspot: -0.057 + # parameters for single shot classification + threshold: 0.00013079660165463033 + iq_angle: 5.6303684840135 + # Software classifier + # classifier: + # type: "scikit" + # model: "qw5q_gold/RBF_SVM_4.pkl" + # required for mixers (not sure if it should be here) + mixer_drive_g: 0.0 + mixer_drive_phi: 0.0 + mixer_readout_g: 0.0 + mixer_readout_phi: 0.0 From 2c2f679dcecd946aecd4839e62a21ed1dfa72318 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Thu, 4 Jan 2024 15:42:49 +0400 Subject: [PATCH 10/46] fix: port serialization --- src/qibolab/instruments/qm/devices.py | 4 +++- src/qibolab/instruments/qm/ports.py | 7 +++++++ tests/dummy_qrc/qm.yml | 12 ------------ tests/dummy_qrc/qm_octave.yml | 26 ++++++++++++++++---------- 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/qibolab/instruments/qm/devices.py b/src/qibolab/instruments/qm/devices.py index 0a606c798..0f8d48ab0 100644 --- a/src/qibolab/instruments/qm/devices.py +++ b/src/qibolab/instruments/qm/devices.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from itertools import chain from typing import Optional from qibolab.instruments.abstract import Instrument @@ -74,7 +75,8 @@ def disconnect(self): devices.""" def dump(self): - return {port.name: port.settings for port in self.outputs.values()} + ports = chain(self.outputs.values(), self.inputs.values()) + return {port.name: port.settings for port in ports if len(port.settings) > 0} @dataclass diff --git a/src/qibolab/instruments/qm/ports.py b/src/qibolab/instruments/qm/ports.py index 70518bd75..ff2386f39 100644 --- a/src/qibolab/instruments/qm/ports.py +++ b/src/qibolab/instruments/qm/ports.py @@ -57,6 +57,13 @@ class OPXOutput(QMOutput): default_factory=dict, metadata={"config": "filter", "settings": True} ) + @property + def settings(self): + data = super().settings + if len(self.filter) == 0: + del data["filter"] + return data + @dataclass class OPXInput(QMInput): diff --git a/tests/dummy_qrc/qm.yml b/tests/dummy_qrc/qm.yml index 9e42d58b6..812855afe 100644 --- a/tests/dummy_qrc/qm.yml +++ b/tests/dummy_qrc/qm.yml @@ -9,18 +9,6 @@ settings: topology: [[0, 2], [1, 2], [2, 3], [2, 4]] instruments: - con2: - o1: - filter: {} - o2: - filter: {} - o3: - filter: {} - o4: - filter: {} - o5: - filter: {} - lo_readout_a: frequency: 7_300_000_000 power: 18 diff --git a/tests/dummy_qrc/qm_octave.yml b/tests/dummy_qrc/qm_octave.yml index f11440dc2..760f6a8b7 100644 --- a/tests/dummy_qrc/qm_octave.yml +++ b/tests/dummy_qrc/qm_octave.yml @@ -9,38 +9,44 @@ settings: topology: [[0, 2], [1, 2], [2, 3], [2, 4]] instruments: + con1: + i1: + gain: 0 + i2: + gain: 0 con2: - o1: - filter: {} - o2: - filter: {} - o3: - filter: {} - o4: - filter: {} - o5: - filter: {} + i1: + gain: 0 + i2: + gain: 0 octave1: o1: lo_frequency: 4_700_000_000 + gain: 0 o2: lo_frequency: 5_600_000_000 + gain: 0 o3: lo_frequency: 6_500_000_000 + gain: 0 o4: lo_frequency: 6_500_000_000 + gain: 0 o5: lo_frequency: 7_300_000_000 + gain: 0 i1: lo_frequency: 7_300_000_000 octave2: o5: lo_frequency: 7_900_000_000 + gain: 0 i1: lo_frequency: 7_900_000_000 octave3: o1: lo_frequency: 4_700_000_000 + gain: 0 twpa_a: frequency: 6_511_000_000 power: 4.5 From 553870b82a3325673639c3cf9c9c91f2c228b46a Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Thu, 4 Jan 2024 16:02:18 +0400 Subject: [PATCH 11/46] fix: port access from QMController --- src/qibolab/instruments/qm/controller.py | 24 ++++++++++-------------- tests/dummy_qrc/qm.py | 11 ----------- tests/dummy_qrc/qm.yml | 14 ++++++++++++++ tests/dummy_qrc/qm_octave.py | 14 +------------- 4 files changed, 25 insertions(+), 38 deletions(-) diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index 0828d29bf..a49e3ac50 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -13,7 +13,7 @@ from .config import QMConfig from .devices import Octave, OPXplus -from .ports import OPXIQ, OPXInput, OPXOutput +from .ports import OPXIQ from .sequence import BakedPulse, QMPulse, Sequence from .sweepers import sweep @@ -115,15 +115,16 @@ def __post_init__(self): self.octaves = {instr.name: instr for instr in self.octaves} def ports(self, name, input=False): - if len(name) != 2: - raise ValueError( - "QMController provides only IQ ports. Please access individual ports from the specific device." + if len(name) == 1: + con, port = name[0] + return self.opxs[con].ports(port, input) + elif len(name) == 2: + (con1, port1), (con2, port2) = name + return OPXIQ( + self.opxs[con1].ports(port1, input), self.opxs[con2].ports(port2, input) ) - _ports = self.input_ports if input else self.output_ports - if name not in _ports: - port_cls = OPXInput if input else OPXOutput - _ports[name] = OPXIQ(port_cls(*name[0]), port_cls(*name[1])) - return _ports[name] + else: + raise ValueError(f"Invalid port {name} for Quantum Machines controller.") def connect(self): """Connect to the QM manager.""" @@ -270,11 +271,6 @@ def sweep(self, qubits, couplers, sequence, options, *sweepers): for qmpulse in qmsequence.ro_pulses: qmpulse.acquisition.download(*buffer_dims) - import json - - with open("qm_config.json", "w") as file: - file.write(json.dumps(self.config.__dict__, indent=4)) - if self.script_file_name is not None: with open(self.script_file_name, "w") as file: file.write(generate_qua_script(experiment, self.config.__dict__)) diff --git a/tests/dummy_qrc/qm.py b/tests/dummy_qrc/qm.py index 8219397c4..6c351c431 100644 --- a/tests/dummy_qrc/qm.py +++ b/tests/dummy_qrc/qm.py @@ -87,17 +87,6 @@ def create(runcard_path=RUNCARD): qubits[q].drive = channels[f"L3-{10 + q}"] qubits[q].flux = channels[f"L4-{q}"] - # set filter for flux channel - qubits[2].flux.filters = { - "feedforward": [1.0684635881381783, -1.0163217174522334], - "feedback": [0.947858129314055], - } - - # set maximum allowed bias values to protect amplifier - # relevant only for qubits where an amplifier is used - for q in range(5): - qubits[q].flux.max_bias = 0.2 - instruments = {controller.name: controller} instruments.update(controller.opxs) instruments.update({lo.name: lo for lo in local_oscillators}) diff --git a/tests/dummy_qrc/qm.yml b/tests/dummy_qrc/qm.yml index 812855afe..1f1f30ced 100644 --- a/tests/dummy_qrc/qm.yml +++ b/tests/dummy_qrc/qm.yml @@ -9,6 +9,20 @@ settings: topology: [[0, 2], [1, 2], [2, 3], [2, 4]] instruments: + con1: + i1: + gain: 0 + i2: + gain: 0 + con2: + o2: + filter: + feedforward: [1.0684635881381783, -1.0163217174522334] + feedback: [0.947858129314055] + i1: + gain: 0 + i2: + gain: 0 lo_readout_a: frequency: 7_300_000_000 power: 18 diff --git a/tests/dummy_qrc/qm_octave.py b/tests/dummy_qrc/qm_octave.py index 024682a5f..de5a9ebe6 100644 --- a/tests/dummy_qrc/qm_octave.py +++ b/tests/dummy_qrc/qm_octave.py @@ -15,8 +15,7 @@ def create(runcard_path=RUNCARD): - """Dummy platform using Quantum Machines (QM) OPXs and Rohde Schwarz local - oscillators. + """Dummy platform using Quantum Machines (QM) OPXs and Octaves. Based on QuantWare 5-qubit device. @@ -73,17 +72,6 @@ def create(runcard_path=RUNCARD): qubits[q].drive = channels[f"L3-{10 + q}"] qubits[q].flux = channels[f"L4-{q}"] - # set filter for flux channel - qubits[2].flux.filters = { - "feedforward": [1.0684635881381783, -1.0163217174522334], - "feedback": [0.947858129314055], - } - - # set maximum allowed bias values to protect amplifier - # relevant only for qubits where an amplifier is used - for q in range(5): - qubits[q].flux.max_bias = 0.2 - instruments = {controller.name: controller, twpa.name: twpa} instruments.update(controller.opxs) instruments.update(controller.octaves) From e6e282581ac95633052e233a0dc1eeb125a44d22 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Thu, 4 Jan 2024 16:31:41 +0400 Subject: [PATCH 12/46] fix: tests --- tests/test_instruments_qm.py | 135 ++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 67 deletions(-) diff --git a/tests/test_instruments_qm.py b/tests/test_instruments_qm.py index 3c540a25d..fa72d64b1 100644 --- a/tests/test_instruments_qm.py +++ b/tests/test_instruments_qm.py @@ -5,8 +5,8 @@ from qm import qua from qibolab import AcquisitionType, ExecutionParameters, create_platform +from qibolab.instruments.qm import OPXplus, QMController from qibolab.instruments.qm.acquisition import Acquisition -from qibolab.instruments.qm.ports import QMPort from qibolab.instruments.qm.sequence import BakedPulse, QMPulse, Sequence from qibolab.pulses import FluxPulse, Pulse, PulseSequence, ReadoutPulse, Rectangular from qibolab.sweeper import Parameter, Sweeper @@ -142,44 +142,42 @@ def test_qmpulse_previous_and_next_flux(): # TODO: Test connect/disconnect -def test_qmopx_setup(dummy_qrc): +def test_qm_setup(dummy_qrc): platform = create_platform("qm") platform.setup() - opx = platform.instruments["qmopx"] - assert opx.time_of_flight == 280 + controller = platform.instruments["qm"] + assert controller.time_of_flight == 280 # TODO: Test start/stop -def test_qmopx_register_analog_output_controllers(): +@pytest.fixture +def qmcontroller(): name = "test" address = "0.0.0.0:0" - opx = QMOPX(name, address) - port = QMPort((("con1", 1), ("con1", 2))) - opx.config.register_analog_output_controllers(port) - controllers = opx.config.controllers - assert controllers == { - "con1": {"analog_outputs": {1: {"offset": 0.0}, 2: {"offset": 0.0}}} - } + return QMController(name, address, opxs=[OPXplus("con1")]) - opx = QMOPX(name, address) - port = QMPort((("con1", 1), ("con1", 2))) - port.offset = 0.005 - opx.config.register_analog_output_controllers(port) - controllers = opx.config.controllers + +@pytest.mark.parametrize("offset", [0.0, 0.005]) +def test_qm_register_port(qmcontroller, offset): + port = qmcontroller.ports((("con1", 1),)) + port.offset = offset + qmcontroller.config.register_port(port) + controllers = qmcontroller.config.controllers assert controllers == { - "con1": {"analog_outputs": {1: {"offset": 0.005}, 2: {"offset": 0.005}}} + "con1": {"analog_outputs": {1: {"offset": offset, "filter": {}}}} } - opx = QMOPX(name, address) - port = QMPort((("con2", 2),)) + +def test_qm_register_port_filter(qmcontroller): + port = qmcontroller.ports((("con1", 2),)) port.offset = 0.005 - port.filters = {"feedforward": [1, -1], "feedback": [0.95]} - opx.config.register_analog_output_controllers(port) - controllers = opx.config.controllers + port.filter = {"feedforward": [1, -1], "feedback": [0.95]} + qmcontroller.config.register_port(port) + controllers = qmcontroller.config.controllers assert controllers == { - "con2": { + "con1": { "analog_outputs": { 2: { "filter": {"feedback": [0.95], "feedforward": [1, -1]}, @@ -190,13 +188,13 @@ def test_qmopx_register_analog_output_controllers(): } -def test_qmopx_register_drive_element(dummy_qrc): +def test_qm_register_drive_element(dummy_qrc): platform = create_platform("qm") - opx = platform.instruments["qmopx"] - opx.config.register_drive_element( + controller = platform.instruments["qm"] + controller.config.register_drive_element( platform.qubits[0], intermediate_frequency=int(1e6) ) - assert "drive0" in opx.config.elements + assert "drive0" in controller.config.elements target_element = { "mixInputs": { "I": ("con3", 2), @@ -207,7 +205,7 @@ def test_qmopx_register_drive_element(dummy_qrc): "intermediate_frequency": 1000000, "operations": {}, } - assert opx.config.elements["drive0"] == target_element + assert controller.config.elements["drive0"] == target_element target_mixer = [ { "intermediate_frequency": 1000000, @@ -215,16 +213,16 @@ def test_qmopx_register_drive_element(dummy_qrc): "correction": [1.0, 0.0, 0.0, 1.0], } ] - assert opx.config.mixers["mixer_drive0"] == target_mixer + assert controller.config.mixers["mixer_drive0"] == target_mixer -def test_qmopx_register_readout_element(dummy_qrc): +def test_qm_register_readout_element(dummy_qrc): platform = create_platform("qm") - opx = platform.instruments["qmopx"] - opx.config.register_readout_element( - platform.qubits[2], int(1e6), opx.time_of_flight, opx.smearing + controller = platform.instruments["qm"] + controller.config.register_readout_element( + platform.qubits[2], int(1e6), controller.time_of_flight, controller.smearing ) - assert "readout2" in opx.config.elements + assert "readout2" in controller.config.elements target_element = { "mixInputs": { "I": ("con2", 10), @@ -241,7 +239,7 @@ def test_qmopx_register_readout_element(dummy_qrc): "time_of_flight": 280, "smearing": 0, } - assert opx.config.elements["readout2"] == target_element + assert controller.config.elements["readout2"] == target_element target_mixer = [ { "intermediate_frequency": 1000000, @@ -249,13 +247,13 @@ def test_qmopx_register_readout_element(dummy_qrc): "correction": [1.0, 0.0, 0.0, 1.0], } ] - assert opx.config.mixers["mixer_readout2"] == target_mixer + assert controller.config.mixers["mixer_readout2"] == target_mixer @pytest.mark.parametrize("pulse_type,qubit", [("drive", 2), ("readout", 1)]) -def test_qmopx_register_pulse(dummy_qrc, pulse_type, qubit): +def test_qm_register_pulse(dummy_qrc, pulse_type, qubit): platform = create_platform("qm") - opx = platform.instruments["qmopx"] + controller = platform.instruments["qm"] if pulse_type == "drive": pulse = platform.create_RX_pulse(qubit, start=0) target_pulse = { @@ -281,23 +279,23 @@ def test_qmopx_register_pulse(dummy_qrc, pulse_type, qubit): }, } - opx.config.register_element( - platform.qubits[qubit], pulse, opx.time_of_flight, opx.smearing + controller.config.register_element( + platform.qubits[qubit], pulse, controller.time_of_flight, controller.smearing ) - opx.config.register_pulse(platform.qubits[qubit], pulse) - assert opx.config.pulses[pulse.serial] == target_pulse - assert target_pulse["waveforms"]["I"] in opx.config.waveforms - assert target_pulse["waveforms"]["Q"] in opx.config.waveforms + controller.config.register_pulse(platform.qubits[qubit], pulse) + assert controller.config.pulses[pulse.serial] == target_pulse + assert target_pulse["waveforms"]["I"] in controller.config.waveforms + assert target_pulse["waveforms"]["Q"] in controller.config.waveforms assert ( - opx.config.elements[f"{pulse_type}{qubit}"]["operations"][pulse.serial] + controller.config.elements[f"{pulse_type}{qubit}"]["operations"][pulse.serial] == pulse.serial ) -def test_qmopx_register_flux_pulse(dummy_qrc): +def test_qm_register_flux_pulse(dummy_qrc): qubit = 2 platform = create_platform("qm") - opx = platform.instruments["qmopx"] + controller = platform.instruments["qm"] pulse = FluxPulse( 0, 30, 0.005, Rectangular(), platform.qubits[qubit].flux.name, qubit ) @@ -306,26 +304,27 @@ def test_qmopx_register_flux_pulse(dummy_qrc): "length": pulse.duration, "waveforms": {"single": "constant_wf0.005"}, } - opx.config.register_element(platform.qubits[qubit], pulse) - opx.config.register_pulse(platform.qubits[qubit], pulse) - assert opx.config.pulses[pulse.serial] == target_pulse - assert target_pulse["waveforms"]["single"] in opx.config.waveforms + controller.config.register_element(platform.qubits[qubit], pulse) + controller.config.register_pulse(platform.qubits[qubit], pulse) + assert controller.config.pulses[pulse.serial] == target_pulse + assert target_pulse["waveforms"]["single"] in controller.config.waveforms assert ( - opx.config.elements[f"flux{qubit}"]["operations"][pulse.serial] == pulse.serial + controller.config.elements[f"flux{qubit}"]["operations"][pulse.serial] + == pulse.serial ) @pytest.mark.parametrize("duration", [0, 30]) -def test_qmopx_register_baked_pulse(dummy_qrc, duration): +def test_qm_register_baked_pulse(dummy_qrc, duration): platform = create_platform("qm") qubit = platform.qubits[3] - opx = platform.instruments["qmopx"] - opx.config.register_flux_element(qubit) + controller = platform.instruments["qm"] + controller.config.register_flux_element(qubit) pulse = FluxPulse( 3, duration, 0.05, Rectangular(), qubit.flux.name, qubit=qubit.name ) qmpulse = BakedPulse(pulse) - config = opx.config + config = controller.config qmpulse.bake(config, [pulse.duration]) assert config.elements["flux3"]["operations"] == { @@ -355,13 +354,13 @@ def test_qmopx_register_baked_pulse(dummy_qrc, duration): } -@patch("qibolab.instruments.qm.simulator.QMSim.execute_program") -def test_qmopx_qubit_spectroscopy(mocker): +@patch("qibolab.instruments.qm.QMController.execute_program") +def test_qm_qubit_spectroscopy(mocker): platform = create_platform("qm") platform.setup() - opx = platform.instruments["qmopx"] + controller = platform.instruments["qm"] # disable program dump otherwise it will fail if we don't connect - opx.script_file_name = None + controller.script_file_name = None sequence = PulseSequence() qd_pulses = {} ro_pulses = {} @@ -375,16 +374,16 @@ def test_qmopx_qubit_spectroscopy(mocker): sequence.add(qd_pulses[qubit]) sequence.add(ro_pulses[qubit]) options = ExecutionParameters(nshots=1024, relaxation_time=100000) - result = opx.play(platform.qubits, platform.couplers, sequence, options) + result = controller.play(platform.qubits, platform.couplers, sequence, options) -@patch("qibolab.instruments.qm.simulator.QMSim.execute_program") -def test_qmopx_duration_sweeper(mocker): +@patch("qibolab.instruments.qm.QMController.execute_program") +def test_qm_duration_sweeper(mocker): platform = create_platform("qm") platform.setup() - opx = platform.instruments["qmopx"] + controller = platform.instruments["qm"] # disable program dump otherwise it will fail if we don't connect - opx.script_file_name = None + controller.script_file_name = None qubit = 1 sequence = PulseSequence() qd_pulse = platform.create_RX_pulse(qubit, start=0) @@ -392,4 +391,6 @@ def test_qmopx_duration_sweeper(mocker): sequence.add(platform.create_MZ_pulse(qubit, start=qd_pulse.finish)) sweeper = Sweeper(Parameter.duration, np.arange(2, 12, 2), pulses=[qd_pulse]) options = ExecutionParameters(nshots=1024, relaxation_time=100000) - result = opx.sweep(platform.qubits, platform.couplers, sequence, options, sweeper) + result = controller.sweep( + platform.qubits, platform.couplers, sequence, options, sweeper + ) From b994d10bb9659b776f0d9e8982443020e9f8b310 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Thu, 4 Jan 2024 17:00:15 +0400 Subject: [PATCH 13/46] docs: docstrings for QM devices --- src/qibolab/instruments/qm/controller.py | 79 ++++++++++++++++-------- src/qibolab/instruments/qm/devices.py | 17 +++++ 2 files changed, 69 insertions(+), 27 deletions(-) diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index c36481255..98c95ed7d 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -1,5 +1,5 @@ from dataclasses import dataclass, field -from typing import Dict, Optional, Tuple +from typing import Dict, Optional from qm import generate_qua_script, qua from qm.octave import QmOctaveConfig @@ -17,22 +17,19 @@ from .sequence import BakedPulse, QMPulse, Sequence from .sweepers import sweep -IQPortId = Tuple[Tuple[str, int], Tuple[str, int]] - OCTAVE_ADDRESS = 11000 -"""Must be 11xxx, where xxx are the last three digits of the Octave IP -address.""" +"""Offset to be added to Octave addresses, because they must be 11xxx, where +xxx are the last three digits of the Octave IP address.""" def declare_octaves(octaves, host): - """Initiate octave_config class, set the calibration file and add octaves - info. + """Initiate Octave configuration and add octaves info. - :param octaves: objects that holds the information about octave's - name, the controller that is connected to this octave, octave's - ip and octave's port. + Args: + octaves (dict): Dictionary containing :class:`qibolab.instruments.qm.devices.Octave` objects + for each Octave device in the experiment configuration. + host (str): IP of the Quantum Machines controller. """ - # TODO: Fix docstring config = None if len(octaves) > 0: config = QmOctaveConfig() @@ -44,7 +41,11 @@ def declare_octaves(octaves, host): def find_duration_sweeper_pulses(sweepers): """Find all pulses that require baking because we are sweeping their - duration.""" + duration. + + Args: + sweepers (list): List of :class:`qibolab.sweeper.Sweeper` objects. + """ duration_sweep_pulses = set() for sweeper in sweepers: try: @@ -61,26 +62,35 @@ def find_duration_sweeper_pulses(sweepers): @dataclass class QMController(Controller): - """Instrument object for controlling Quantum Machines (QM) OPX controllers. + """:class:`qibolab.instruments.abstract.Controller` object for controlling + a Quantum Machines cluster. - Playing pulses on QM controllers requires a ``config`` dictionary and a program - written in QUA language. The ``config`` file is generated in parts in the following places - in the ``register_*`` methods. The controllers, elements and pulses are all - registered after a pulse sequence is given, so that the config contains only - elements related to the participating qubits. - The QUA program for executing an arbitrary qibolab ``PulseSequence`` is written in - ``play`` and ``play_pulses`` and executed in ``execute_program``. + A cluster consists of multiple :class:`qibolab.instruments.qm.devices.QMDevice` devices. - Args: - name (str): Name of the instrument instance. - address (str): IP address and port for connecting to the OPX instruments. + Playing pulses on QM controllers requires a ``config`` dictionary and a program + written in QUA language. + The ``config`` file is generated in parts in :class:`qibolab.instruments.qm.config.QMConfig`. + Controllers, elements and pulses are all registered after a pulse sequence is given, so that + the config contains only elements related to the participating qubits. + The QUA program for executing an arbitrary :class:`qibolab.pulses.PulseSequence` is written in + :meth:`qibolab.instruments.qm.controller.QMController.play` and executed in + :meth:`qibolab.instruments.qm.controller.QMController.execute_program`. """ name: str + """Name of the instrument instance.""" address: str + """IP address and port for connecting to the OPX instruments. + + Has the form XXX.XXX.XXX.XXX:XXX. + """ opxs: Dict[int, OPXplus] = field(default_factory=dict) + """Dictionary containing the + :class:`qibolab.instruments.qm.devices.OPXplus` instruments being used.""" octaves: Dict[int, Octave] = field(default_factory=dict) + """Dictionary containing the :class:`qibolab.instruments.qm.devices.Octave` + instruments being used.""" time_of_flight: int = 0 """Time of flight used for hardware signal integration.""" @@ -94,7 +104,7 @@ class QMController(Controller): """ manager: Optional[QuantumMachinesManager] = None - """Manager object used for controlling the QM OPXs.""" + """Manager object used for controlling the Quantum Machines cluster.""" config: QMConfig = field(default_factory=QMConfig) """Configuration dictionary required for pulse execution on the OPXs.""" is_connected: bool = False @@ -102,12 +112,26 @@ class QMController(Controller): def __post_init__(self): super().__init__(self.name, self.address) - if isinstance(self.opxs, list): + # convert lists to dicts + if not isinstance(self.opxs, dict): self.opxs = {instr.name: instr for instr in self.opxs} - if isinstance(self.octaves, list): + if not isinstance(self.octaves, dict): self.octaves = {instr.name: instr for instr in self.octaves} def ports(self, name, input=False): + """Provides instrument ports to the user. + + Note that individual ports can also be accessed from the corresponding devices + using :meth:`qibolab.instruments.qm.devices.QMDevice.ports`. + + Args: + name (tuple): Contains the numbers of controller and port to be obtained. + For example ``((conX, Y),)`` returns port-Y of OPX+ controller X. + ``((conX, Y), (conX, Z))`` returns port-Y and Z of OPX+ controller X + as an :class:`qibolab.instruments.qm.ports.OPXIQ` port pair. + input (bool): ``True`` for obtaining an input port, otherwise an + output port is returned. Default is ``False``. + """ if len(name) == 1: con, port = name[0] return self.opxs[con].ports(port, input) @@ -121,10 +145,11 @@ def ports(self, name, input=False): @property def sampling_rate(self): + """Sampling rate of Quantum Machines instruments.""" return SAMPLING_RATE def connect(self): - """Connect to the QM manager.""" + """Connect to the Quantum Machines manager.""" host, port = self.address.split(":") octave = declare_octaves(self.octaves, host) self.manager = QuantumMachinesManager(host=host, port=int(port), octave=octave) diff --git a/src/qibolab/instruments/qm/devices.py b/src/qibolab/instruments/qm/devices.py index 0f8d48ab0..b7d5cebcd 100644 --- a/src/qibolab/instruments/qm/devices.py +++ b/src/qibolab/instruments/qm/devices.py @@ -29,14 +29,31 @@ def __getitem__(self, number): @dataclass class QMDevice(Instrument): + """Abstract class for an individual Quantum Machines devices.""" + name: str + """Name of the device.""" port: Optional[int] = None + """Network port of the device in the cluster configuration (relevant for + Octaves).""" connectivity: Optional["QMDevice"] = None + """OPXplus that acts as the waveform generator for the Octave.""" outputs: Optional[Ports[int, QMOutput]] = None + """Dictionary containing the instrument's output ports.""" inputs: Optional[Ports[int, QMInput]] = None + """Dictionary containing the instrument's input ports.""" def ports(self, number, input=False): + """Provides instrument's ports to the user. + + Args: + number (int): Port number. + Can be 1 to 10 for :class:`qibolab.instruments.qm.devices.OPXplus` + and 1 to 5 for :class:`qibolab.instruments.qm.devices.Octave`. + input (bool): ``True`` for obtaining an input port, otherwise an + output port is returned. Default is ``False``. + """ if input: return self.inputs[number] else: From cfe01447a4a8adb9868d0d4e635e34cc53413fa4 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Thu, 4 Jan 2024 17:28:46 +0400 Subject: [PATCH 14/46] docs: docstring for QM ports --- src/qibolab/instruments/qm/devices.py | 23 ++++++++++ src/qibolab/instruments/qm/ports.py | 63 +++++++++++++++++++++++++-- 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/src/qibolab/instruments/qm/devices.py b/src/qibolab/instruments/qm/devices.py index b7d5cebcd..7c1f2b5ea 100644 --- a/src/qibolab/instruments/qm/devices.py +++ b/src/qibolab/instruments/qm/devices.py @@ -16,6 +16,18 @@ class Ports(dict): + """Dictionary mapping port numbers to + :class:`qibolab.instruments.qm.ports.QMPort` objects. + + Automatically instantiates ports that have not yet been created. + Used by :class:`qibolab.instruments.qm.devices.QMDevice` + + Args: + constructor (type): Type of :class:`qibolab.instruments.qm.ports.QMPort` to be used for + initializing new ports. + device (str): Name of device holding these ports. + """ + def __init__(self, constructor, device): self.constructor = constructor self.device = device @@ -92,12 +104,16 @@ def disconnect(self): devices.""" def dump(self): + """Serializes device settings to a dictionary for dumping to the + runcard YAML.""" ports = chain(self.outputs.values(), self.inputs.values()) return {port.name: port.settings for port in ports if len(port.settings) > 0} @dataclass class OPXplus(QMDevice): + """Device handling OPX+ controllers.""" + def __post_init__(self): self.outputs = Ports(OPXOutput, self.name) self.inputs = Ports(OPXInput, self.name) @@ -105,11 +121,18 @@ def __post_init__(self): @dataclass class Octave(QMDevice): + """Device handling Octaves.""" + def __post_init__(self): self.outputs = Ports(OctaveOutput, self.name) self.inputs = Ports(OctaveInput, self.name) def ports(self, number, input=False): + """Provides Octave ports. + + Extension of the abstract :meth:`qibolab.instruments.qm.devices.QMDevice.ports` + because Octave ports are used for mixing two existing (I, Q) OPX+ ports. + """ port = super().ports(number, input) if port.opx_port is None: iport = self.connectivity.ports(2 * number - 1, input) diff --git a/src/qibolab/instruments/qm/ports.py b/src/qibolab/instruments/qm/ports.py index ff2386f39..028cdbe9e 100644 --- a/src/qibolab/instruments/qm/ports.py +++ b/src/qibolab/instruments/qm/ports.py @@ -4,16 +4,29 @@ @dataclass class QMPort: + """Abstract representation of Quantum Machine instrument ports. + + Contains the ports settings for each device. + """ + device: str + """Name of the device holding this port.""" number: int + """Number of this port in the device.""" key: ClassVar[Optional[str]] = None + """Key corresponding to this port type in the Quantum Machines config. + + Used in :meth:`qibolab.instruments.qm.config.QMConfig.register_port`. + """ @property def pair(self): + """Representation of the port in the Quantum Machines config.""" return (self.device, self.number) def setup(self, **kwargs): + """Updates port settings.""" for name, value in kwargs.items(): if not hasattr(self, name): raise KeyError(f"Unknown port setting {name}.") @@ -21,6 +34,11 @@ def setup(self, **kwargs): @property def settings(self): + """Serialization of the port settings to dump in the runcard YAML. + + Only fields that provide the ``metadata['settings']`` flag are dumped + in the serialization. + """ return { fld.name: getattr(self, fld.name) for fld in fields(self) @@ -29,6 +47,11 @@ def settings(self): @property def config(self): + """Port settings in the format of the Quantum Machines config. + + Field ``metadata['config']`` are used to translate qibolab port setting names + to the corresponding Quantum Machine config properties. + """ data = {} for fld in fields(self): if "config" in fld.metadata: @@ -37,14 +60,22 @@ def config(self): class QMOutput(QMPort): + """Abstract Quantum Machines output port.""" + @property def name(self): + """Name of the port when dumping instrument settings on the runcard + YAML.""" return f"o{self.number}" class QMInput(QMPort): + """Abstract Quantum Machines input port.""" + @property def name(self): + """Name of the port when dumping instrument settings on the runcard + YAML.""" return f"i{self.number}" @@ -53,12 +84,18 @@ class OPXOutput(QMOutput): key: ClassVar[str] = "analog_outputs" offset: float = field(default=0.0, metadata={"config": "offset"}) + """Constant voltage to be applied on the output.""" filter: Dict[str, float] = field( default_factory=dict, metadata={"config": "filter", "settings": True} ) + """FIR and IIR filters to be applied to correct signal distortions.""" @property def settings(self): + """OPX+ output settings to be dumped to the runcard YAML. + + Filter is removed if empty to simplify the runcard. + """ data = super().settings if len(self.filter) == 0: del data["filter"] @@ -70,13 +107,19 @@ class OPXInput(QMInput): key: ClassVar[str] = "analog_inputs" offset: float = field(default=0.0, metadata={"config": "offset"}) + """Constant voltage to be applied on the output.""" gain: int = field(default=0, metadata={"config": "gain_db", "settings": True}) + """Gain applied to amplify the input.""" @dataclass class OPXIQ: + """Pair of I-Q ports.""" + i: Union[OPXOutput, OPXInput] + """Port implementing the I-component of the signal.""" q: Union[OPXOutput, OPXInput] + """Port implementing the Q-component of the signal.""" @dataclass @@ -86,14 +129,22 @@ class OctaveOutput(QMOutput): lo_frequency: float = field( default=0.0, metadata={"config": "LO_frequency", "settings": True} ) + """Local oscillator frequency.""" gain: int = field(default=0, metadata={"config": "gain", "settings": True}) - """Can be in the range [-20 : 0.5 : 20]dB.""" + """Local oscillator gain. + + Can be in the range [-20 : 0.5 : 20] dB. + """ lo_source: str = field(default="internal", metadata={"config": "LO_source"}) - """Can be external or internal.""" + """Local oscillator clock source. + + Can be external or internal. + """ output_mode: str = field(default="always_on", metadata={"config": "output_mode"}) """Can be: "always_on" / "always_off"/ "triggered" / "triggered_reversed".""" opx_port: Optional[OPXOutput] = None + """OPX+ port that is connected to the Octave port.""" @dataclass @@ -103,8 +154,14 @@ class OctaveInput(QMInput): lo_frequency: float = field( default=0.0, metadata={"config": "LO_frequency", "settings": True} ) + """Local oscillator frequency.""" lo_source: str = field(default="internal", metadata={"config": "LO_source"}) + """Local oscillator clock source. + + Can be external or internal. + """ IF_mode_I: str = field(default="direct", metadata={"config": "IF_mode_I"}) IF_mode_Q: str = field(default="direct", metadata={"config": "IF_mode_Q"}) - opx_port: Optional[OPXOutput] = None + opx_port: Optional[OPXIQ] = None + """OPX+ port that is connected to the Octave port.""" From df310a9da55b108de4d0972a3ffa377c9a79e163 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Thu, 4 Jan 2024 17:54:18 +0400 Subject: [PATCH 15/46] refactor: simplify devices --- src/qibolab/instruments/qm/devices.py | 32 +++++++++++++-------------- src/qibolab/serialize.py | 5 +---- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/qibolab/instruments/qm/devices.py b/src/qibolab/instruments/qm/devices.py index 7c1f2b5ea..e93523dda 100644 --- a/src/qibolab/instruments/qm/devices.py +++ b/src/qibolab/instruments/qm/devices.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from itertools import chain from typing import Optional @@ -15,22 +15,22 @@ ) +@dataclass class Ports(dict): """Dictionary mapping port numbers to :class:`qibolab.instruments.qm.ports.QMPort` objects. Automatically instantiates ports that have not yet been created. Used by :class:`qibolab.instruments.qm.devices.QMDevice` - - Args: - constructor (type): Type of :class:`qibolab.instruments.qm.ports.QMPort` to be used for - initializing new ports. - device (str): Name of device holding these ports. """ - def __init__(self, constructor, device): - self.constructor = constructor - self.device = device + constructor: type + """Type of :class:`qibolab.instruments.qm.ports.QMPort` to be used for + initializing new ports.""" + device: str + """Name of device holding these ports.""" + + def __post_init__(self): super().__init__() def __getitem__(self, number): @@ -45,15 +45,10 @@ class QMDevice(Instrument): name: str """Name of the device.""" - port: Optional[int] = None - """Network port of the device in the cluster configuration (relevant for - Octaves).""" - connectivity: Optional["QMDevice"] = None - """OPXplus that acts as the waveform generator for the Octave.""" - outputs: Optional[Ports[int, QMOutput]] = None + outputs: Ports[int, QMOutput] = field(init=False) """Dictionary containing the instrument's output ports.""" - inputs: Optional[Ports[int, QMInput]] = None + inputs: Ports[int, QMInput] = field(init=False) """Dictionary containing the instrument's input ports.""" def ports(self, number, input=False): @@ -123,6 +118,11 @@ def __post_init__(self): class Octave(QMDevice): """Device handling Octaves.""" + port: Optional[int] = None + """Network port of the Octave in the cluster configuration.""" + connectivity: Optional["QMDevice"] = None + """OPXplus that acts as the waveform generator for the Octave.""" + def __post_init__(self): self.outputs = Ports(OctaveOutput, self.name) self.inputs = Ports(OctaveInput, self.name) diff --git a/src/qibolab/serialize.py b/src/qibolab/serialize.py index 31b28dd1f..0895dc0cd 100644 --- a/src/qibolab/serialize.py +++ b/src/qibolab/serialize.py @@ -103,10 +103,7 @@ def load_instrument_settings( ) -> InstrumentMap: """Setup instruments according to the settings given in the runcard.""" for name, settings in runcard.get("instruments", {}).items(): - try: - instruments[name].setup(**settings) - except TypeError: - instruments[name].setup(settings) + instruments[name].setup(**settings) return instruments From 05ea6ea2e2560d0a90ad9a11b63ea4796292f7b3 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Thu, 4 Jan 2024 18:16:12 +0400 Subject: [PATCH 16/46] test: test using qm_octave platform --- src/qibolab/instruments/qm/devices.py | 5 +- tests/test_instruments_qm.py | 163 +++++++++++++++----------- 2 files changed, 98 insertions(+), 70 deletions(-) diff --git a/src/qibolab/instruments/qm/devices.py b/src/qibolab/instruments/qm/devices.py index e93523dda..e7cb3c048 100644 --- a/src/qibolab/instruments/qm/devices.py +++ b/src/qibolab/instruments/qm/devices.py @@ -1,6 +1,5 @@ from dataclasses import dataclass, field from itertools import chain -from typing import Optional from qibolab.instruments.abstract import Instrument @@ -118,9 +117,9 @@ def __post_init__(self): class Octave(QMDevice): """Device handling Octaves.""" - port: Optional[int] = None + port: int """Network port of the Octave in the cluster configuration.""" - connectivity: Optional["QMDevice"] = None + connectivity: OPXplus """OPXplus that acts as the waveform generator for the Octave.""" def __post_init__(self): diff --git a/tests/test_instruments_qm.py b/tests/test_instruments_qm.py index fa72d64b1..810280c42 100644 --- a/tests/test_instruments_qm.py +++ b/tests/test_instruments_qm.py @@ -11,6 +11,8 @@ from qibolab.pulses import FluxPulse, Pulse, PulseSequence, ReadoutPulse, Rectangular from qibolab.sweeper import Parameter, Sweeper +from .conftest import set_platform_profile + def test_qmpulse(): pulse = Pulse(0, 40, 0.05, int(3e9), 0.0, Rectangular(), "ch0", qubit=0) @@ -139,19 +141,6 @@ def test_qmpulse_previous_and_next_flux(): assert drive22.next_ == {measure2} -# TODO: Test connect/disconnect - - -def test_qm_setup(dummy_qrc): - platform = create_platform("qm") - platform.setup() - controller = platform.instruments["qm"] - assert controller.time_of_flight == 280 - - -# TODO: Test start/stop - - @pytest.fixture def qmcontroller(): name = "test" @@ -188,71 +177,111 @@ def test_qm_register_port_filter(qmcontroller): } -def test_qm_register_drive_element(dummy_qrc): - platform = create_platform("qm") +@pytest.fixture(params=["qm", "qm_octave"]) +def qmplatform(request): + set_platform_profile() + return create_platform(request.param) + + +# TODO: Test connect/disconnect + + +def test_qm_setup(qmplatform): + platform = qmplatform + platform.setup() + controller = platform.instruments["qm"] + assert controller.time_of_flight == 280 + + +# TODO: Test start/stop + + +def test_qm_register_drive_element(qmplatform): + platform = qmplatform controller = platform.instruments["qm"] controller.config.register_drive_element( platform.qubits[0], intermediate_frequency=int(1e6) ) assert "drive0" in controller.config.elements - target_element = { - "mixInputs": { - "I": ("con3", 2), - "Q": ("con3", 1), - "lo_frequency": 4700000000, - "mixer": "mixer_drive0", - }, - "intermediate_frequency": 1000000, - "operations": {}, - } - assert controller.config.elements["drive0"] == target_element - target_mixer = [ - { + if platform.name == "qm": + target_element = { + "mixInputs": { + "I": ("con3", 2), + "Q": ("con3", 1), + "lo_frequency": 4700000000, + "mixer": "mixer_drive0", + }, "intermediate_frequency": 1000000, - "lo_frequency": 4700000000, - "correction": [1.0, 0.0, 0.0, 1.0], + "operations": {}, } - ] - assert controller.config.mixers["mixer_drive0"] == target_mixer + assert controller.config.elements["drive0"] == target_element + target_mixer = [ + { + "intermediate_frequency": 1000000, + "lo_frequency": 4700000000, + "correction": [1.0, 0.0, 0.0, 1.0], + } + ] + assert controller.config.mixers["mixer_drive0"] == target_mixer + else: + target_element = { + "RF_inputs": {"port": ("octave3", 1)}, + "intermediate_frequency": 1000000, + "operations": {}, + } + assert controller.config.elements["drive0"] == target_element + assert "mixer_drive0" not in controller.config.mixers -def test_qm_register_readout_element(dummy_qrc): - platform = create_platform("qm") +def test_qm_register_readout_element(qmplatform): + platform = qmplatform controller = platform.instruments["qm"] controller.config.register_readout_element( platform.qubits[2], int(1e6), controller.time_of_flight, controller.smearing ) assert "readout2" in controller.config.elements - target_element = { - "mixInputs": { - "I": ("con2", 10), - "Q": ("con2", 9), - "lo_frequency": 7900000000, - "mixer": "mixer_readout2", - }, - "intermediate_frequency": 1000000, - "operations": {}, - "outputs": { - "out1": ("con2", 2), - "out2": ("con2", 1), - }, - "time_of_flight": 280, - "smearing": 0, - } - assert controller.config.elements["readout2"] == target_element - target_mixer = [ - { + if platform.name == "qm": + target_element = { + "mixInputs": { + "I": ("con2", 10), + "Q": ("con2", 9), + "lo_frequency": 7900000000, + "mixer": "mixer_readout2", + }, + "intermediate_frequency": 1000000, + "operations": {}, + "outputs": { + "out1": ("con2", 2), + "out2": ("con2", 1), + }, + "time_of_flight": 280, + "smearing": 0, + } + assert controller.config.elements["readout2"] == target_element + target_mixer = [ + { + "intermediate_frequency": 1000000, + "lo_frequency": 7900000000, + "correction": [1.0, 0.0, 0.0, 1.0], + } + ] + assert controller.config.mixers["mixer_readout2"] == target_mixer + else: + target_element = { + "RF_inputs": {"port": ("octave2", 5)}, + "RF_outputs": {"port": ("octave2", 1)}, "intermediate_frequency": 1000000, - "lo_frequency": 7900000000, - "correction": [1.0, 0.0, 0.0, 1.0], + "operations": {}, + "time_of_flight": 280, + "smearing": 0, } - ] - assert controller.config.mixers["mixer_readout2"] == target_mixer + assert controller.config.elements["readout2"] == target_element + assert "mixer_readout2" not in controller.config.mixers @pytest.mark.parametrize("pulse_type,qubit", [("drive", 2), ("readout", 1)]) -def test_qm_register_pulse(dummy_qrc, pulse_type, qubit): - platform = create_platform("qm") +def test_qm_register_pulse(qmplatform, pulse_type, qubit): + platform = qmplatform controller = platform.instruments["qm"] if pulse_type == "drive": pulse = platform.create_RX_pulse(qubit, start=0) @@ -292,9 +321,9 @@ def test_qm_register_pulse(dummy_qrc, pulse_type, qubit): ) -def test_qm_register_flux_pulse(dummy_qrc): +def test_qm_register_flux_pulse(qmplatform): qubit = 2 - platform = create_platform("qm") + platform = qmplatform controller = platform.instruments["qm"] pulse = FluxPulse( 0, 30, 0.005, Rectangular(), platform.qubits[qubit].flux.name, qubit @@ -315,8 +344,8 @@ def test_qm_register_flux_pulse(dummy_qrc): @pytest.mark.parametrize("duration", [0, 30]) -def test_qm_register_baked_pulse(dummy_qrc, duration): - platform = create_platform("qm") +def test_qm_register_baked_pulse(qmplatform, duration): + platform = qmplatform qubit = platform.qubits[3] controller = platform.instruments["qm"] controller.config.register_flux_element(qubit) @@ -355,8 +384,8 @@ def test_qm_register_baked_pulse(dummy_qrc, duration): @patch("qibolab.instruments.qm.QMController.execute_program") -def test_qm_qubit_spectroscopy(mocker): - platform = create_platform("qm") +def test_qm_qubit_spectroscopy(qmplatform, mocker): + platform = qmplatform platform.setup() controller = platform.instruments["qm"] # disable program dump otherwise it will fail if we don't connect @@ -378,8 +407,8 @@ def test_qm_qubit_spectroscopy(mocker): @patch("qibolab.instruments.qm.QMController.execute_program") -def test_qm_duration_sweeper(mocker): - platform = create_platform("qm") +def test_qm_duration_sweeper(qmplatform, mocker): + platform = qmplatform platform.setup() controller = platform.instruments["qm"] # disable program dump otherwise it will fail if we don't connect From b39a5a2a1d373ed0148d1e3e9ced28d12a659220 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Thu, 4 Jan 2024 20:12:26 +0400 Subject: [PATCH 17/46] test: fix mocker order --- tests/test_instruments_qm.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/test_instruments_qm.py b/tests/test_instruments_qm.py index 810280c42..48d3532f5 100644 --- a/tests/test_instruments_qm.py +++ b/tests/test_instruments_qm.py @@ -384,7 +384,7 @@ def test_qm_register_baked_pulse(qmplatform, duration): @patch("qibolab.instruments.qm.QMController.execute_program") -def test_qm_qubit_spectroscopy(qmplatform, mocker): +def test_qm_qubit_spectroscopy(mocker, qmplatform): platform = qmplatform platform.setup() controller = platform.instruments["qm"] @@ -407,7 +407,7 @@ def test_qm_qubit_spectroscopy(qmplatform, mocker): @patch("qibolab.instruments.qm.QMController.execute_program") -def test_qm_duration_sweeper(qmplatform, mocker): +def test_qm_duration_sweeper(mocker, qmplatform): platform = qmplatform platform.setup() controller = platform.instruments["qm"] @@ -420,6 +420,13 @@ def test_qm_duration_sweeper(qmplatform, mocker): sequence.add(platform.create_MZ_pulse(qubit, start=qd_pulse.finish)) sweeper = Sweeper(Parameter.duration, np.arange(2, 12, 2), pulses=[qd_pulse]) options = ExecutionParameters(nshots=1024, relaxation_time=100000) - result = controller.sweep( - platform.qubits, platform.couplers, sequence, options, sweeper - ) + if platform.name == "qm": + result = controller.sweep( + platform.qubits, platform.couplers, sequence, options, sweeper + ) + else: + with pytest.raises(ValueError): + # TODO: Figure what is wrong with baking and Octaves + result = controller.sweep( + platform.qubits, platform.couplers, sequence, options, sweeper + ) From 235e22fe0bfa65e52b449672b53fbb4baa831eb9 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Sun, 7 Jan 2024 00:26:19 +0400 Subject: [PATCH 18/46] feat: Add method to calibrate mixers when using Octaves --- src/qibolab/instruments/qm/config.py | 10 +++++++ src/qibolab/instruments/qm/controller.py | 35 ++++++++++++++++++++++-- src/qibolab/qubits.py | 20 ++++++++++++++ tests/dummy_qrc/qm_octave.py | 1 + 4 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/qibolab/instruments/qm/config.py b/src/qibolab/instruments/qm/config.py index 8d03378a5..016ce6205 100644 --- a/src/qibolab/instruments/qm/config.py +++ b/src/qibolab/instruments/qm/config.py @@ -11,6 +11,13 @@ SAMPLING_RATE = 1 """Sampling rate of Quantum Machines OPX in GSps.""" +DEFAULT_INPUTS = {1: {}, 2: {}} +"""Default controller config section. + +Inputs are always registered to avoid issues with automatic mixer +calibration when using Octaves. +""" + @dataclass class QMConfig: @@ -44,6 +51,9 @@ def register_port(self, port): controllers = self.octaves if is_octave else self.controllers if port.device not in controllers: controllers[port.device] = {} + if not is_octave: + controllers[port.device]["analog_inputs"] = DEFAULT_INPUTS + device = controllers[port.device] if port.key in device: device[port.key].update(port.config) diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index 98c95ed7d..0cf42af25 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -22,18 +22,20 @@ xxx are the last three digits of the Octave IP address.""" -def declare_octaves(octaves, host): +def declare_octaves(octaves, host, calibration_path=None): """Initiate Octave configuration and add octaves info. Args: octaves (dict): Dictionary containing :class:`qibolab.instruments.qm.devices.Octave` objects for each Octave device in the experiment configuration. host (str): IP of the Quantum Machines controller. + calibration_path (str): Path to the JSON file with the mixer calibration. """ config = None if len(octaves) > 0: config = QmOctaveConfig() - # config.set_calibration_db(os.getcwd()) + if calibration_path is not None: + config.set_calibration_db(calibration_path) for octave in octaves.values(): config.add_device_info(octave.name, host, OCTAVE_ADDRESS + octave.port) return config @@ -96,6 +98,8 @@ class QMController(Controller): """Time of flight used for hardware signal integration.""" smearing: int = 0 """Smearing used for hardware signal integration.""" + calibration_path: Optional[str] = None + """Path to the JSON file that contains the mixer calibration.""" script_file_name: Optional[str] = "qua_script.py" """Name of the file that the QUA program will dumped in that after every execution. @@ -151,7 +155,7 @@ def sampling_rate(self): def connect(self): """Connect to the Quantum Machines manager.""" host, port = self.address.split(":") - octave = declare_octaves(self.octaves, host) + octave = declare_octaves(self.octaves, host, self.calibration_path) self.manager = QuantumMachinesManager(host=host, port=int(port), octave=octave) def setup(self): @@ -176,6 +180,31 @@ def disconnect(self): self.manager.close() self.is_connected = False + def calibrate_mixers(self, qubits): + if isinstance(qubits, dict): + qubits = list(qubits.values()) + + config = QMConfig() + for qubit in qubits: + if qubit.readout is not None: + config.register_port(qubit.readout.port) + config.register_readout_element( + qubit, qubit.mz_frequencies[1], self.time_of_flight, self.smearing + ) + if qubit.drive is not None: + config.register_port(qubit.drive.port) + config.register_drive_element(qubit, qubit.rx_frequencies[1]) + + machine = self.manager.open_qm(config.__dict__) + for qubit in qubits: + print(f"Calibrating mixers for qubit {qubit.name}") + if qubit.readout is not None: + _lo, _if = qubit.mz_frequencies + machine.calibrate_element(f"readout{qubit.name}", {_lo: (_if,)}) + if qubit.drive is not None: + _lo, _if = qubit.rx_frequencies + machine.calibrate_element(f"drive{qubit.name}", {_lo: (_if,)}) + def execute_program(self, program): """Executes an arbitrary program written in QUA language. diff --git a/src/qibolab/qubits.py b/src/qibolab/qubits.py index 02b9fdb6e..536c9f697 100644 --- a/src/qibolab/qubits.py +++ b/src/qibolab/qubits.py @@ -110,6 +110,26 @@ def characterization(self): if fld.name not in EXCLUDED_FIELDS } + @property + def mz_frequencies(self): + """Get local oscillator and intermediate frequency used for readout. + + Assumes RF = LO + IF. + """ + _lo = self.readout.lo_frequency + _if = self.native_gates.MZ.frequency - _lo + return _lo, _if + + @property + def rx_frequencies(self): + """Get local oscillator and intermediate frequency used for drive. + + Assumes RF = LO + IF. + """ + _lo = self.drive.lo_frequency + _if = self.native_gates.RX.frequency - _lo + return _lo, _if + QubitPairId = Tuple[QubitId, QubitId] """Type for holding ``QubitPair``s in the ``platform.pairs`` dictionary.""" diff --git a/tests/dummy_qrc/qm_octave.py b/tests/dummy_qrc/qm_octave.py index de5a9ebe6..a8c372555 100644 --- a/tests/dummy_qrc/qm_octave.py +++ b/tests/dummy_qrc/qm_octave.py @@ -31,6 +31,7 @@ def create(runcard_path=RUNCARD): opxs=opxs, octaves=[octave1, octave2, octave3], time_of_flight=280, + calibration_path=str(runcard_path.parent), ) # Create channel objects and map controllers to channels From c5a1e2719e87c41d3310ba261d0d7cd343dd3542 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Sun, 7 Jan 2024 00:37:55 +0400 Subject: [PATCH 19/46] test: Test newly introduced methods --- src/qibolab/instruments/qm/controller.py | 43 ++++++++++++++++++------ tests/test_instruments_qm.py | 15 +++++++-- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index 0cf42af25..d81917cda 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -62,6 +62,31 @@ def find_duration_sweeper_pulses(sweepers): return duration_sweep_pulses +def controllers_config(qubits, time_of_flight, smearing=0): + """Create a Quantum Machines configuration without pulses. + + This contains the readout and drive elements and controllers and + is used by :meth:`qibolab.instruments.qm.controller.QMController.calibrate_mixers`. + + Args: + qubits (list): List of :class:`qibolab.qubits.Qubit` objects to be + included in the config. + time_of_flight (int): Time of flight used on readout elements. + smearing (int): Smearing used on readout elements. + """ + config = QMConfig() + for qubit in qubits: + if qubit.readout is not None: + config.register_port(qubit.readout.port) + config.register_readout_element( + qubit, qubit.mz_frequencies[1], time_of_flight, smearing + ) + if qubit.drive is not None: + config.register_port(qubit.drive.port) + config.register_drive_element(qubit, qubit.rx_frequencies[1]) + return config + + @dataclass class QMController(Controller): """:class:`qibolab.instruments.abstract.Controller` object for controlling @@ -181,20 +206,16 @@ def disconnect(self): self.is_connected = False def calibrate_mixers(self, qubits): + """Calibrate Octave mixers for readout and drive lines of given qubits. + + Args: + qubits (list): List of :class:`qibolab.qubits.Qubit` objects for + which mixers will be calibrated. + """ if isinstance(qubits, dict): qubits = list(qubits.values()) - config = QMConfig() - for qubit in qubits: - if qubit.readout is not None: - config.register_port(qubit.readout.port) - config.register_readout_element( - qubit, qubit.mz_frequencies[1], self.time_of_flight, self.smearing - ) - if qubit.drive is not None: - config.register_port(qubit.drive.port) - config.register_drive_element(qubit, qubit.rx_frequencies[1]) - + config = controllers_config(qubits, self.time_of_flight, self.smearing) machine = self.manager.open_qm(config.__dict__) for qubit in qubits: print(f"Calibrating mixers for qubit {qubit.name}") diff --git a/tests/test_instruments_qm.py b/tests/test_instruments_qm.py index 48d3532f5..f433cc437 100644 --- a/tests/test_instruments_qm.py +++ b/tests/test_instruments_qm.py @@ -7,6 +7,7 @@ from qibolab import AcquisitionType, ExecutionParameters, create_platform from qibolab.instruments.qm import OPXplus, QMController from qibolab.instruments.qm.acquisition import Acquisition +from qibolab.instruments.qm.controller import controllers_config from qibolab.instruments.qm.sequence import BakedPulse, QMPulse, Sequence from qibolab.pulses import FluxPulse, Pulse, PulseSequence, ReadoutPulse, Rectangular from qibolab.sweeper import Parameter, Sweeper @@ -155,7 +156,10 @@ def test_qm_register_port(qmcontroller, offset): qmcontroller.config.register_port(port) controllers = qmcontroller.config.controllers assert controllers == { - "con1": {"analog_outputs": {1: {"offset": offset, "filter": {}}}} + "con1": { + "analog_inputs": {1: {}, 2: {}}, + "analog_outputs": {1: {"offset": offset, "filter": {}}}, + } } @@ -167,12 +171,13 @@ def test_qm_register_port_filter(qmcontroller): controllers = qmcontroller.config.controllers assert controllers == { "con1": { + "analog_inputs": {1: {}, 2: {}}, "analog_outputs": { 2: { "filter": {"feedback": [0.95], "feedforward": [1, -1]}, "offset": 0.005, } - } + }, } } @@ -183,6 +188,12 @@ def qmplatform(request): return create_platform(request.param) +def test_controllers_config(qmplatform): + config = controllers_config(list(qmplatform.qubits.values()), time_of_flight=30) + assert len(config.controllers) == 3 + assert len(config.elements) == 10 + + # TODO: Test connect/disconnect From 96656f50ba055d6077e6ec746105bfdda5d53564 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Mon, 8 Jan 2024 16:48:05 +0400 Subject: [PATCH 20/46] feat: Support sequence unrolling for QM --- src/qibolab/instruments/qm/acquisition.py | 74 +++++++++++++++-------- src/qibolab/instruments/qm/config.py | 23 +++---- src/qibolab/instruments/qm/controller.py | 27 ++++++--- src/qibolab/instruments/qm/sequence.py | 42 +++++++++---- 4 files changed, 111 insertions(+), 55 deletions(-) diff --git a/src/qibolab/instruments/qm/acquisition.py b/src/qibolab/instruments/qm/acquisition.py index 44f1cb61e..80249522b 100644 --- a/src/qibolab/instruments/qm/acquisition.py +++ b/src/qibolab/instruments/qm/acquisition.py @@ -27,9 +27,11 @@ class Acquisition(ABC): variables. """ - serial: str - """Serial of the readout pulse that generates this acquisition.""" + name: str + """Name of the acquisition used as identifier to download results from the + instruments.""" average: bool + npulses: int @abstractmethod def assign_element(self, element): @@ -91,19 +93,18 @@ def download(self, *dimensions): if self.average: i_stream = i_stream.average() q_stream = q_stream.average() - i_stream.save(f"{self.serial}_I") - q_stream.save(f"{self.serial}_Q") + i_stream.save(f"{self.name}_I") + q_stream.save(f"{self.name}_Q") def fetch(self, handles): - ires = handles.get(f"{self.serial}_I").fetch_all() - qres = handles.get(f"{self.serial}_Q").fetch_all() + ires = handles.get(f"{self.name}_I").fetch_all() + qres = handles.get(f"{self.name}_Q").fetch_all() # convert raw ADC signal to volts u = unit() - ires = u.raw2volts(ires) - qres = u.raw2volts(qres) + signal = u.raw2volts(ires) + 1j * u.raw2volts(qres) if self.average: - return AveragedRawWaveformResults(ires + 1j * qres) - return RawWaveformResults(ires + 1j * qres) + return [AveragedRawWaveformResults(signal)] + return [RawWaveformResults(signal)] @dataclass @@ -136,22 +137,35 @@ def save(self): def download(self, *dimensions): Istream = self.I_stream Qstream = self.Q_stream + if self.npulses > 1: + Istream = Istream.buffer(self.npulses) + Qstream = Qstream.buffer(self.npulses) for dim in dimensions: Istream = Istream.buffer(dim) Qstream = Qstream.buffer(dim) if self.average: Istream = Istream.average() Qstream = Qstream.average() - Istream.save(f"{self.serial}_I") - Qstream.save(f"{self.serial}_Q") + Istream.save(f"{self.name}_I") + Qstream.save(f"{self.name}_Q") def fetch(self, handles): - ires = handles.get(f"{self.serial}_I").fetch_all() - qres = handles.get(f"{self.serial}_Q").fetch_all() - if self.average: - # TODO: calculate std - return AveragedIntegratedResults(ires + 1j * qres) - return IntegratedResults(ires + 1j * qres) + ires = handles.get(f"{self.name}_I").fetch_all() + qres = handles.get(f"{self.name}_Q").fetch_all() + signal = ires + 1j * qres + if self.npulses > 1: + if self.average: + # TODO: calculate std + return [ + AveragedIntegratedResults(signal[..., i]) + for i in range(self.npulses) + ] + return [IntegratedResults(signal[..., i]) for i in range(self.npulses)] + else: + if self.average: + # TODO: calculate std + return [AveragedIntegratedResults(signal)] + return [IntegratedResults(signal)] @dataclass @@ -199,15 +213,27 @@ def save(self): def download(self, *dimensions): shots = self.shots + if self.npulses > 1: + shots = shots.buffer(self.npulses) for dim in dimensions: shots = shots.buffer(dim) if self.average: shots = shots.average() - shots.save(f"{self.serial}_shots") + shots.save(f"{self.name}_shots") def fetch(self, handles): - shots = handles.get(f"{self.serial}_shots").fetch_all() - if self.average: - # TODO: calculate std - return AveragedSampleResults(shots) - return SampleResults(shots.astype(int)) + shots = handles.get(f"{self.name}_shots").fetch_all() + if len(self.npulses) > 1: + if self.average: + # TODO: calculate std + return [ + AveragedSampleResults(shots[..., i]) for i in range(self.npulses) + ] + return [ + SampleResults(shots[..., i].astype(int)) for i in range(self.npulses) + ] + else: + if self.average: + # TODO: calculate std + return [AveragedSampleResults(shots)] + return [SampleResults(shots.astype(int))] diff --git a/src/qibolab/instruments/qm/config.py b/src/qibolab/instruments/qm/config.py index 016ce6205..5e58d64ef 100644 --- a/src/qibolab/instruments/qm/config.py +++ b/src/qibolab/instruments/qm/config.py @@ -228,7 +228,7 @@ def register_element(self, qubit, pulse, time_of_flight=0, smearing=0): # register flux element self.register_flux_element(qubit, pulse.frequency) - def register_pulse(self, qubit, pulse): + def register_pulse(self, qubit, qmpulse): """Registers pulse, waveforms and integration weights in QM config. Args: @@ -241,23 +241,24 @@ def register_pulse(self, qubit, pulse): instantiation of the Qubit objects. They are named as "drive0", "drive1", "flux0", "readout0", ... """ - if pulse.serial not in self.pulses: + pulse = qmpulse.pulse + if qmpulse.operation not in self.pulses: if pulse.type is PulseType.DRIVE: serial_i = self.register_waveform(pulse, "i") serial_q = self.register_waveform(pulse, "q") - self.pulses[pulse.serial] = { + self.pulses[qmpulse.operation] = { "operation": "control", "length": pulse.duration, "waveforms": {"I": serial_i, "Q": serial_q}, } # register drive pulse in elements self.elements[f"drive{qubit.name}"]["operations"][ - pulse.serial - ] = pulse.serial + qmpulse.operation + ] = qmpulse.operation elif pulse.type is PulseType.FLUX: serial = self.register_waveform(pulse) - self.pulses[pulse.serial] = { + self.pulses[qmpulse.operation] = { "operation": "control", "length": pulse.duration, "waveforms": { @@ -266,14 +267,14 @@ def register_pulse(self, qubit, pulse): } # register flux pulse in elements self.elements[f"flux{qubit.name}"]["operations"][ - pulse.serial - ] = pulse.serial + qmpulse.operation + ] = qmpulse.operation elif pulse.type is PulseType.READOUT: serial_i = self.register_waveform(pulse, "i") serial_q = self.register_waveform(pulse, "q") self.register_integration_weights(qubit, pulse.duration) - self.pulses[pulse.serial] = { + self.pulses[qmpulse.operation] = { "operation": "measurement", "length": pulse.duration, "waveforms": { @@ -289,8 +290,8 @@ def register_pulse(self, qubit, pulse): } # register readout pulse in elements self.elements[f"readout{qubit.name}"]["operations"][ - pulse.serial - ] = pulse.serial + qmpulse.operation + ] = qmpulse.operation else: raise_error(TypeError, f"Unknown pulse type {pulse.type.name}.") diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index d81917cda..8ca52e251 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -252,10 +252,9 @@ def fetch_results(result, ro_pulses): handles.wait_for_all_values() results = {} for qmpulse in ro_pulses: - pulse = qmpulse.pulse - results[pulse.qubit] = results[pulse.serial] = qmpulse.acquisition.fetch( - handles - ) + data = qmpulse.acquisition.fetch(handles) + for pulse, result in zip(qmpulse.pulses, data): + results[pulse.qubit] = results[pulse.serial] = result return results def create_sequence(self, qubits, sequence, sweepers): @@ -276,6 +275,17 @@ def create_sequence(self, qubits, sequence, sweepers): duration_sweep_pulses = find_duration_sweeper_pulses(sweepers) + qmpulses = {} + + def create_qmpulse(pulse, qmpulse_cls): + qmpulse = qmpulse_cls(pulse) + key = (qmpulse.operation, pulse.qubit) + if key not in qmpulses: + qmpulses[key] = qmpulse + else: + qmpulses[key].pulses.append(pulse) + return qmpulses[key] + qmsequence = Sequence() sort_key = lambda pulse: (pulse.start, pulse.duration) for pulse in sorted(sequence.pulses, key=sort_key): @@ -293,11 +303,11 @@ def create_sequence(self, qubits, sequence, sweepers): or pulse.duration < 16 or pulse.serial in duration_sweep_pulses ): - qmpulse = BakedPulse(pulse) + qmpulse = create_qmpulse(pulse, BakedPulse) qmpulse.bake(self.config, durations=[pulse.duration]) else: - qmpulse = QMPulse(pulse) - self.config.register_pulse(qubit, pulse) + qmpulse = create_qmpulse(pulse, QMPulse) + self.config.register_pulse(qubit, qmpulse) qmsequence.add(qmpulse) qmsequence.shift() @@ -349,3 +359,6 @@ def sweep(self, qubits, couplers, sequence, options, *sweepers): result = self.execute_program(experiment) return self.fetch_results(result, qmsequence.ro_pulses) + + def split_batches(self, sequences): + return [sequences] diff --git a/src/qibolab/instruments/qm/sequence.py b/src/qibolab/instruments/qm/sequence.py index 1457a1871..4522455af 100644 --- a/src/qibolab/instruments/qm/sequence.py +++ b/src/qibolab/instruments/qm/sequence.py @@ -35,7 +35,7 @@ class QMPulse: as defined in the QM config. """ - pulse: Pulse + pulses: List[Pulse] """:class:`qibolab.pulses.Pulse` implemting the current pulse.""" element: Optional[str] = None """Element that the pulse will be played on, as defined in the QM @@ -73,11 +73,22 @@ class QMPulse: elements_to_align: Set[str] = field(default_factory=set) def __post_init__(self): - self.element: str = f"{self.pulse.type.name.lower()}{self.pulse.qubit}" - self.operation: str = self.pulse.serial + if isinstance(self.pulses, Pulse): + self.pulses = [self.pulses] + + pulse_type = self.pulse.type.name.lower() + amplitude = format(self.pulse.amplitude, ".6f").rstrip("0").rstrip(".") + self.element: str = f"{pulse_type}{self.pulse.qubit}" + self.operation: str = ( + f"{pulse_type}({self.pulse.duration}, {amplitude}, {self.pulse.shape})" + ) self.relative_phase: float = self.pulse.relative_phase / (2 * np.pi) self.elements_to_align.add(self.element) + @property + def pulse(self): + return self.pulses[0] + def __hash__(self): return hash(self.pulse) @@ -115,10 +126,15 @@ def play(self): def declare_output(self, options, threshold=None, angle=None): average = options.averaging_mode is AveragingMode.CYCLIC acquisition_type = options.acquisition_type + acquisition_name = f"{self.operation}_{self.pulse.qubit}" if acquisition_type is AcquisitionType.RAW: - self.acquisition = RawAcquisition(self.pulse.serial, average) + self.acquisition = RawAcquisition( + acquisition_name, average, len(self.pulses) + ) elif acquisition_type is AcquisitionType.INTEGRATION: - self.acquisition = IntegratedAcquisition(self.pulse.serial, average) + self.acquisition = IntegratedAcquisition( + acquisition_name, average, len(self.pulses) + ) elif acquisition_type is AcquisitionType.DISCRIMINATION: if threshold is None or angle is None: raise_error( @@ -127,7 +143,7 @@ def declare_output(self, options, threshold=None, angle=None): "if threshold and angle are not given.", ) self.acquisition = ShotsAcquisition( - self.pulse.serial, average, threshold, angle + acquisition_name, average, len(self.pulses), threshold, angle ) else: raise_error(ValueError, f"Invalid acquisition type {acquisition_type}.") @@ -185,8 +201,8 @@ def bake(self, config: QMConfig, durations: DurationsType): self.calculate_waveform(waveform_i, t), self.calculate_waveform(waveform_q, t), ] - segment.add_op(self.pulse.serial, self.element, waveform) - segment.play(self.pulse.serial, self.element) + segment.add_op(self.operation, self.element, waveform) + segment.play(self.operation, self.element) self.segments.append(segment) @property @@ -248,7 +264,9 @@ def add(self, qmpulse: QMPulse): pulse = qmpulse.pulse self.pulse_to_qmpulse[pulse.serial] = qmpulse if pulse.type is PulseType.READOUT: - self.ro_pulses.append(qmpulse) + if len(qmpulse.pulses) == 1: + # if ``qmpulse`` contains more than one pulses it is already added in ``ro_pulses`` + self.ro_pulses.append(qmpulse) previous = self._find_previous(pulse) if previous is not None: @@ -287,7 +305,9 @@ def play(self, relaxation_time=0): if qmpulse.wait_cycles is not None: qua.wait(qmpulse.wait_cycles, qmpulse.element) if pulse.type is PulseType.READOUT: + # Save data to the stream processing qmpulse.acquisition.measure(qmpulse.operation, qmpulse.element) + qmpulse.acquisition.save() else: if ( not isinstance(qmpulse.relative_phase, float) @@ -304,7 +324,3 @@ def play(self, relaxation_time=0): if relaxation_time > 0: qua.wait(relaxation_time // 4) - - # Save data to the stream processing - for qmpulse in self.ro_pulses: - qmpulse.acquisition.save() From 4c169d88ad17a7c8f2168c921fd42177b77d0704 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 9 Jan 2024 15:41:13 +0400 Subject: [PATCH 21/46] fix: Recover old QMPulse to fix pulse scheduling --- src/qibolab/instruments/qm/acquisition.py | 64 ++++++++++++++++++++--- src/qibolab/instruments/qm/controller.py | 34 ++++++------ src/qibolab/instruments/qm/sequence.py | 52 ++---------------- src/qibolab/instruments/qm/simulator.py | 2 +- src/qibolab/platform.py | 5 ++ 5 files changed, 84 insertions(+), 73 deletions(-) diff --git a/src/qibolab/instruments/qm/acquisition.py b/src/qibolab/instruments/qm/acquisition.py index 80249522b..b60fd6efc 100644 --- a/src/qibolab/instruments/qm/acquisition.py +++ b/src/qibolab/instruments/qm/acquisition.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod from dataclasses import dataclass, field +from typing import List, Optional import numpy as np from qm import qua @@ -8,6 +9,8 @@ from qualang_tools.addons.variables import assign_variables_to_element from qualang_tools.units import unit +from qibolab.execution_parameters import AcquisitionType, AveragingMode +from qibolab.qubits import QubitId from qibolab.result import ( AveragedIntegratedResults, AveragedRawWaveformResults, @@ -30,8 +33,18 @@ class Acquisition(ABC): name: str """Name of the acquisition used as identifier to download results from the instruments.""" + qubit: QubitId average: bool - npulses: int + threshold: Optional[float] = None + """Threshold to be used for classification of single shots.""" + angle: Optional[float] = None + """Angle in the IQ plane to be used for classification of single shots.""" + + keys: List[str] = field(default_factory=list) + + @property + def npulses(self): + return len(self.keys) @abstractmethod def assign_element(self, element): @@ -175,11 +188,6 @@ class ShotsAcquisition(Acquisition): Threshold and angle must be given in order to classify shots. """ - threshold: float - """Threshold to be used for classification of single shots.""" - angle: float - """Angle in the IQ plane to be used for classification of single shots.""" - I: _Variable = field(default_factory=lambda: declare(fixed)) Q: _Variable = field(default_factory=lambda: declare(fixed)) """Variables to save the (I, Q) values acquired from a single shot.""" @@ -189,6 +197,12 @@ class ShotsAcquisition(Acquisition): """Stream to collect multiple shots.""" def __post_init__(self): + if threshold is None or angle is None: + raise_error( + ValueError, + "Cannot use ``AcquisitionType.DISCRIMINATION`` " + "if threshold and angle are not given.", + ) self.cos = np.cos(self.angle) self.sin = np.sin(self.angle) @@ -237,3 +251,41 @@ def fetch(self, handles): # TODO: calculate std return [AveragedSampleResults(shots)] return [SampleResults(shots.astype(int))] + + +ACQUISITION_TYPES = { + AcquisitionType.RAW: RawAcquisition, + AcquisitionType.INTEGRATION: IntegratedAcquisition, + AcquisitionType.DISCRIMINATION: ShotsAcquisition, +} + + +def declare_acquisitions(ro_pulses, qubits, options): + """Declares variables for saving acquisition in the QUA program. + + Args: + ro_pulses (list): List of readout pulses in the sequence. + qubits (dict): Dictionary containing all the :class:`qibolab.qubits.Qubit` + objects of the platform. + options (:class:`qibolab.execution_parameters.ExecutionParameters`): Execution + options containing acquisition type and averaging mode. + + Returns: + Dictionary containing the different :class:`qibolab.instruments.qm.acquisition.Acquisition` objects. + """ + acquisitions = {} + for qmpulse in ro_pulses: + qubit = qmpulse.pulse.qubit + name = f"{qmpulse.operation}_{qubit}" + if name not in acquisitions: + threshold = qubits[qubit].threshold + iq_angle = qubits[qubit].iq_angle + average = options.averaging_mode is AveragingMode.CYCLIC + acquisition_cls = ACQUISITION_TYPES[options.acquisition_type] + acquisition = acquisition_cls(name, qubit, options, threshold, iq_angle) + acquisition.assign_element(qmpulse.element) + acquisitions[name] = acquisition + + acquisitions[name].keys.append(qmpulse.pulse.serial) + qmpulse.acquisition = acquisitions[name] + return acquisitions diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index 8ca52e251..466854bc4 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -11,6 +11,7 @@ from qibolab.pulses import PulseType from qibolab.sweeper import Parameter +from .acquisition import declare_acquisitions from .config import SAMPLING_RATE, QMConfig from .devices import Octave, OPXplus from .ports import OPXIQ @@ -239,7 +240,7 @@ def execute_program(self, program): return machine.execute(program) @staticmethod - def fetch_results(result, ro_pulses): + def fetch_results(result, acquisitions): """Fetches results from an executed experiment. Defined as ``@staticmethod`` because it is overwritten @@ -251,10 +252,10 @@ def fetch_results(result, ro_pulses): handles = result.result_handles handles.wait_for_all_values() results = {} - for qmpulse in ro_pulses: - data = qmpulse.acquisition.fetch(handles) - for pulse, result in zip(qmpulse.pulses, data): - results[pulse.qubit] = results[pulse.serial] = result + for acquisition in acquisitions.values(): + data = acquisition.fetch(handles) + for serial, result in zip(acquisition.keys, data): + results[acquisition.qubit] = results[serial] = result return results def create_sequence(self, qubits, sequence, sweepers): @@ -287,6 +288,7 @@ def create_qmpulse(pulse, qmpulse_cls): return qmpulses[key] qmsequence = Sequence() + ro_pulses = [] sort_key = lambda pulse: (pulse.start, pulse.duration) for pulse in sorted(sequence.pulses, key=sort_key): qubit = qubits[pulse.qubit] @@ -303,15 +305,17 @@ def create_qmpulse(pulse, qmpulse_cls): or pulse.duration < 16 or pulse.serial in duration_sweep_pulses ): - qmpulse = create_qmpulse(pulse, BakedPulse) + qmpulse = BakedPulse(pulse) qmpulse.bake(self.config, durations=[pulse.duration]) else: - qmpulse = create_qmpulse(pulse, QMPulse) + qmpulse = QMPulse(pulse) + if pulse.type is PulseType.READOUT: + ro_pulses.append(qmpulse) self.config.register_pulse(qubit, qmpulse) qmsequence.add(qmpulse) qmsequence.shift() - return qmsequence + return qmsequence, ro_pulses def play(self, qubits, couplers, sequence, options): return self.sweep(qubits, couplers, sequence, options) @@ -331,15 +335,11 @@ def sweep(self, qubits, couplers, sequence, options, *sweepers): self.config.register_port(qubit.flux.port) self.config.register_flux_element(qubit) - qmsequence = self.create_sequence(qubits, sequence, sweepers) + qmsequence, ro_pulses = self.create_sequence(qubits, sequence, sweepers) # play pulses using QUA with qua.program() as experiment: n = declare(int) - for qmpulse in qmsequence.ro_pulses: - threshold = qubits[qmpulse.pulse.qubit].threshold - iq_angle = qubits[qmpulse.pulse.qubit].iq_angle - qmpulse.declare_output(options, threshold, iq_angle) - + acquisitions = declare_acquisitions(ro_pulses, qubits, options) with for_(n, 0, n < options.nshots, n + 1): sweep( list(sweepers), @@ -350,15 +350,15 @@ def sweep(self, qubits, couplers, sequence, options, *sweepers): ) with qua.stream_processing(): - for qmpulse in qmsequence.ro_pulses: - qmpulse.acquisition.download(*buffer_dims) + for acquisition in acquisitions.values(): + acquisition.download(*buffer_dims) if self.script_file_name is not None: with open(self.script_file_name, "w") as file: file.write(generate_qua_script(experiment, self.config.__dict__)) result = self.execute_program(experiment) - return self.fetch_results(result, qmsequence.ro_pulses) + return self.fetch_results(result, acquisitions) def split_batches(self, sequences): return [sequences] diff --git a/src/qibolab/instruments/qm/sequence.py b/src/qibolab/instruments/qm/sequence.py index 4522455af..d7127f788 100644 --- a/src/qibolab/instruments/qm/sequence.py +++ b/src/qibolab/instruments/qm/sequence.py @@ -4,19 +4,12 @@ import numpy as np from numpy import typing as npt -from qibo.config import raise_error from qm import qua from qm.qua._dsl import _Variable # for type declaration only from qualang_tools.bakery import baking from qualang_tools.bakery.bakery import Baking -from qibolab import AcquisitionType, AveragingMode -from qibolab.instruments.qm.acquisition import ( - Acquisition, - IntegratedAcquisition, - RawAcquisition, - ShotsAcquisition, -) +from qibolab.instruments.qm.acquisition import Acquisition from qibolab.pulses import Pulse, PulseType from .config import SAMPLING_RATE, QMConfig @@ -35,8 +28,8 @@ class QMPulse: as defined in the QM config. """ - pulses: List[Pulse] - """:class:`qibolab.pulses.Pulse` implemting the current pulse.""" + pulse: Pulse + """:class:`qibolab.pulses.Pulse` corresponding to the ``QMPulse``.""" element: Optional[str] = None """Element that the pulse will be played on, as defined in the QM config.""" @@ -73,9 +66,6 @@ class QMPulse: elements_to_align: Set[str] = field(default_factory=set) def __post_init__(self): - if isinstance(self.pulses, Pulse): - self.pulses = [self.pulses] - pulse_type = self.pulse.type.name.lower() amplitude = format(self.pulse.amplitude, ".6f").rstrip("0").rstrip(".") self.element: str = f"{pulse_type}{self.pulse.qubit}" @@ -85,10 +75,6 @@ def __post_init__(self): self.relative_phase: float = self.pulse.relative_phase / (2 * np.pi) self.elements_to_align.add(self.element) - @property - def pulse(self): - return self.pulses[0] - def __hash__(self): return hash(self.pulse) @@ -123,32 +109,6 @@ def play(self): """ qua.play(self.operation, self.element, duration=self.swept_duration) - def declare_output(self, options, threshold=None, angle=None): - average = options.averaging_mode is AveragingMode.CYCLIC - acquisition_type = options.acquisition_type - acquisition_name = f"{self.operation}_{self.pulse.qubit}" - if acquisition_type is AcquisitionType.RAW: - self.acquisition = RawAcquisition( - acquisition_name, average, len(self.pulses) - ) - elif acquisition_type is AcquisitionType.INTEGRATION: - self.acquisition = IntegratedAcquisition( - acquisition_name, average, len(self.pulses) - ) - elif acquisition_type is AcquisitionType.DISCRIMINATION: - if threshold is None or angle is None: - raise_error( - ValueError, - "Cannot use ``AcquisitionType.DISCRIMINATION`` " - "if threshold and angle are not given.", - ) - self.acquisition = ShotsAcquisition( - acquisition_name, average, len(self.pulses), threshold, angle - ) - else: - raise_error(ValueError, f"Invalid acquisition type {acquisition_type}.") - self.acquisition.assign_element(self.element) - @dataclass class BakedPulse(QMPulse): @@ -234,8 +194,6 @@ class Sequence: qmpulses: List[QMPulse] = field(default_factory=list) """List of :class:`qibolab.instruments.qm.QMPulse` objects corresponding to the original pulses.""" - ro_pulses: List[QMPulse] = field(default_factory=list) - """List of readout pulses used for registering outputs.""" pulse_to_qmpulse: Dict[Pulse, QMPulse] = field(default_factory=dict) """Map from qibolab pulses to QMPulses (useful when sweeping).""" clock: Dict[str, int] = field(default_factory=lambda: collections.defaultdict(int)) @@ -263,10 +221,6 @@ def _find_previous(self, pulse): def add(self, qmpulse: QMPulse): pulse = qmpulse.pulse self.pulse_to_qmpulse[pulse.serial] = qmpulse - if pulse.type is PulseType.READOUT: - if len(qmpulse.pulses) == 1: - # if ``qmpulse`` contains more than one pulses it is already added in ``ro_pulses`` - self.ro_pulses.append(qmpulse) previous = self._find_previous(pulse) if previous is not None: diff --git a/src/qibolab/instruments/qm/simulator.py b/src/qibolab/instruments/qm/simulator.py index 91a7a5f6a..2542aaa72 100644 --- a/src/qibolab/instruments/qm/simulator.py +++ b/src/qibolab/instruments/qm/simulator.py @@ -42,7 +42,7 @@ def connect(self): self.manager = QuantumMachinesManager(host, int(port)) @staticmethod - def fetch_results(result, ro_pulses): + def fetch_results(result, acquisitions): return result def execute_program(self, program): diff --git a/src/qibolab/platform.py b/src/qibolab/platform.py index 60da8c097..0848792b1 100644 --- a/src/qibolab/platform.py +++ b/src/qibolab/platform.py @@ -283,6 +283,11 @@ def execute_pulse_sequences( results = defaultdict(list) for batch in self._controller.split_batches(sequences): sequence, readouts = unroll_sequences(batch, options.relaxation_time) + + print() + print(sequence) + print() + result = self._execute(sequence, options, **kwargs) for serial, new_serials in readouts.items(): results[serial].extend(result[ser] for ser in new_serials) From ecd161157c35afc75e3928f6c49288bb88d40cc0 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 9 Jan 2024 17:28:56 +0400 Subject: [PATCH 22/46] refactor: Remove QMSim class --- src/qibolab/instruments/qm/__init__.py | 1 - src/qibolab/instruments/qm/acquisition.py | 30 ++++++-- src/qibolab/instruments/qm/controller.py | 86 +++++++++++++---------- src/qibolab/instruments/qm/simulator.py | 55 --------------- src/qibolab/platform.py | 12 ---- 5 files changed, 76 insertions(+), 108 deletions(-) delete mode 100644 src/qibolab/instruments/qm/simulator.py diff --git a/src/qibolab/instruments/qm/__init__.py b/src/qibolab/instruments/qm/__init__.py index 041a7b916..e053aa970 100644 --- a/src/qibolab/instruments/qm/__init__.py +++ b/src/qibolab/instruments/qm/__init__.py @@ -1,3 +1,2 @@ from .controller import QMController from .devices import Octave, OPXplus -from .simulator import QMSim diff --git a/src/qibolab/instruments/qm/acquisition.py b/src/qibolab/instruments/qm/acquisition.py index b60fd6efc..c9efd1c08 100644 --- a/src/qibolab/instruments/qm/acquisition.py +++ b/src/qibolab/instruments/qm/acquisition.py @@ -197,11 +197,10 @@ class ShotsAcquisition(Acquisition): """Stream to collect multiple shots.""" def __post_init__(self): - if threshold is None or angle is None: - raise_error( - ValueError, + if self.threshold is None or self.angle is None: + raise ValueError( "Cannot use ``AcquisitionType.DISCRIMINATION`` " - "if threshold and angle are not given.", + "if threshold and angle are not given." ) self.cos = np.cos(self.angle) self.sin = np.sin(self.angle) @@ -289,3 +288,26 @@ def declare_acquisitions(ro_pulses, qubits, options): acquisitions[name].keys.append(qmpulse.pulse.serial) qmpulse.acquisition = acquisitions[name] return acquisitions + + +def fetch_results(result, acquisitions): + """Fetches results from an executed experiment. + + Args: + result: Result of the executed experiment. + acquisition (dict): Dictionary containing :class:`qibolab.instruments.qm.acquisition.Acquisition` objects. + + Returns: + Dictionary with the results in the format required by the platform. + """ + # TODO: Update result asynchronously instead of waiting + # for all values, in order to allow live plotting + # using ``handles.is_processing()`` + handles = result.result_handles + handles.wait_for_all_values() + results = {} + for acquisition in acquisitions.values(): + data = acquisition.fetch(handles) + for serial, result in zip(acquisition.keys, data): + results[acquisition.qubit] = results[serial] = result + return results diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index 466854bc4..61ddcbb20 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -1,17 +1,18 @@ from dataclasses import dataclass, field from typing import Dict, Optional -from qm import generate_qua_script, qua +from qm import QuantumMachinesManager, SimulationConfig, generate_qua_script, qua from qm.octave import QmOctaveConfig from qm.qua import declare, for_ -from qm.QuantumMachinesManager import QuantumMachinesManager +from qm.simulate.credentials import create_credentials +from qualang_tools.simulator_tools import create_simulator_controller_connections from qibolab import AveragingMode from qibolab.instruments.abstract import Controller from qibolab.pulses import PulseType from qibolab.sweeper import Parameter -from .acquisition import declare_acquisitions +from .acquisition import declare_acquisitions, fetch_results from .config import SAMPLING_RATE, QMConfig from .devices import Octave, OPXplus from .ports import OPXIQ @@ -140,6 +141,22 @@ class QMController(Controller): is_connected: bool = False """Boolean that shows whether we are connected to the QM manager.""" + simulation_duration: Optional[int] = None + """Duration for the simulation in ns. + + If given the simulator will be used instead of actual hardware + execution. + """ + cloud: bool = False + """If ``True`` the QM cloud simulator is used which does not require access + to physical instruments. + + This assumes that a proper cloud address has been given. + If ``False`` and ``simulation_duration`` was given, then the built-in simulator + of the instruments is used. This requires connection to instruments. + Default is ``False``. + """ + def __post_init__(self): super().__init__(self.name, self.address) # convert lists to dicts @@ -148,6 +165,10 @@ def __post_init__(self): if not isinstance(self.octaves, dict): self.octaves = {instr.name: instr for instr in self.octaves} + if self.simulation_duration is not None: + # convert simulation duration from ns to clock cycles + self.simulation_duration //= 4 + def ports(self, name, input=False): """Provides instrument ports to the user. @@ -182,7 +203,12 @@ def connect(self): """Connect to the Quantum Machines manager.""" host, port = self.address.split(":") octave = declare_octaves(self.octaves, host, self.calibration_path) - self.manager = QuantumMachinesManager(host=host, port=int(port), octave=octave) + credentials = None + if self.cloud: + credentials = create_credentials() + self.manager = QuantumMachinesManager( + host=host, port=int(port), octave=octave, credentials=credentials + ) def setup(self): """Deprecated method.""" @@ -232,31 +258,23 @@ def execute_program(self, program): Args: program: QUA program. - - Returns: - TODO """ machine = self.manager.open_qm(self.config.__dict__) return machine.execute(program) - @staticmethod - def fetch_results(result, acquisitions): - """Fetches results from an executed experiment. + def simulate_program(self, program): + """Simulates an arbitrary program written in QUA language. - Defined as ``@staticmethod`` because it is overwritten - in :class:`qibolab.instruments.qm.simulator.QMSim`. + Args: + program: QUA program. """ - # TODO: Update result asynchronously instead of waiting - # for all values, in order to allow live plotting - # using ``handles.is_processing()`` - handles = result.result_handles - handles.wait_for_all_values() - results = {} - for acquisition in acquisitions.values(): - data = acquisition.fetch(handles) - for serial, result in zip(acquisition.keys, data): - results[acquisition.qubit] = results[serial] = result - return results + ncontrollers = len(self.config.controllers) + controller_connections = create_simulator_controller_connections(ncontrollers) + simulation_config = SimulationConfig( + duration=self.simulation_duration, + controller_connections=controller_connections, + ) + return self.manager.simulate(self.config.__dict__, program, simulation_config) def create_sequence(self, qubits, sequence, sweepers): """Translates a :class:`qibolab.pulses.PulseSequence` to a @@ -276,17 +294,6 @@ def create_sequence(self, qubits, sequence, sweepers): duration_sweep_pulses = find_duration_sweeper_pulses(sweepers) - qmpulses = {} - - def create_qmpulse(pulse, qmpulse_cls): - qmpulse = qmpulse_cls(pulse) - key = (qmpulse.operation, pulse.qubit) - if key not in qmpulses: - qmpulses[key] = qmpulse - else: - qmpulses[key].pulses.append(pulse) - return qmpulses[key] - qmsequence = Sequence() ro_pulses = [] sort_key = lambda pulse: (pulse.start, pulse.duration) @@ -357,8 +364,15 @@ def sweep(self, qubits, couplers, sequence, options, *sweepers): with open(self.script_file_name, "w") as file: file.write(generate_qua_script(experiment, self.config.__dict__)) - result = self.execute_program(experiment) - return self.fetch_results(result, acquisitions) + if self.simulation_duration is not None: + result = self.simulate_program(experiment) + results = {} + for pulse in ro_pulses: + results[pulse.qubit] = results[pulse.serial] = result + return results + else: + result = self.execute_program(experiment) + return fetch_results(result, acquisitions) def split_batches(self, sequences): return [sequences] diff --git a/src/qibolab/instruments/qm/simulator.py b/src/qibolab/instruments/qm/simulator.py deleted file mode 100644 index 2542aaa72..000000000 --- a/src/qibolab/instruments/qm/simulator.py +++ /dev/null @@ -1,55 +0,0 @@ -from dataclasses import dataclass - -from qm import SimulationConfig -from qm.QuantumMachinesManager import QuantumMachinesManager -from qualang_tools.simulator_tools import create_simulator_controller_connections - -from .controller import QMController - - -@dataclass -class QMSim(QMController): - """Instrument for using the Quantum Machines (QM) OPX simulator. - - Args: - address (str): Address and port of the simulator. - simulation_duration (int): Duration for the simulation in ns. - cloud (bool): If ``True`` the QM cloud simulator is used which does not - require access to physical instruments. This assumes that a proper - cloud address has been given. - If ``False`` the simulator built-in the instruments is used. - This requires connection to instruments. - Default is ``False``. - """ - - simulation_duration: int = 1000 - cloud: bool = False - - def __post_init__(self): - """Convert simulation duration from ns to clock cycles.""" - self.simulation_duration //= 4 - self.settings = None - - def connect(self): - host, port = self.address.split(":") - if self.cloud: - from qm.simulate.credentials import create_credentials - - self.manager = QuantumMachinesManager( - host, int(port), credentials=create_credentials() - ) - else: - self.manager = QuantumMachinesManager(host, int(port)) - - @staticmethod - def fetch_results(result, acquisitions): - return result - - def execute_program(self, program): - ncontrollers = len(self.config.controllers) - controller_connections = create_simulator_controller_connections(ncontrollers) - simulation_config = SimulationConfig( - duration=self.simulation_duration, - controller_connections=controller_connections, - ) - return self.manager.simulate(self.config.__dict__, program, simulation_config) diff --git a/src/qibolab/platform.py b/src/qibolab/platform.py index 0848792b1..68d29e295 100644 --- a/src/qibolab/platform.py +++ b/src/qibolab/platform.py @@ -210,9 +210,6 @@ def _execute(self, sequence, options, **kwargs): ) if isinstance(new_result, dict): result.update(new_result) - elif new_result is not None: - # currently the result of QMSim is not a dict - result = new_result return result @@ -283,11 +280,6 @@ def execute_pulse_sequences( results = defaultdict(list) for batch in self._controller.split_batches(sequences): sequence, readouts = unroll_sequences(batch, options.relaxation_time) - - print() - print(sequence) - print() - result = self._execute(sequence, options, **kwargs) for serial, new_serials in readouts.items(): results[serial].extend(result[ser] for ser in new_serials) @@ -348,10 +340,6 @@ def sweep( ) if isinstance(new_result, dict): result.update(new_result) - elif new_result is not None: - # currently the result of QMSim is not a dict - result = new_result - return result def __call__(self, sequence, options): From b4841f75ef3f937c4ee8abc4f103fe5fdb9d3e09 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 9 Jan 2024 17:38:52 +0400 Subject: [PATCH 23/46] fix: Fix simulator results --- src/qibolab/instruments/qm/controller.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index 61ddcbb20..9ec2290ea 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -367,7 +367,8 @@ def sweep(self, qubits, couplers, sequence, options, *sweepers): if self.simulation_duration is not None: result = self.simulate_program(experiment) results = {} - for pulse in ro_pulses: + for qmpulse in ro_pulses: + pulse = qmpulse.pulse results[pulse.qubit] = results[pulse.serial] = result return results else: From b935dfbc0541e2befae672ee45f7f8cab0c252fe Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 9 Jan 2024 18:05:27 +0400 Subject: [PATCH 24/46] test: Update tests --- src/qibolab/instruments/qm/acquisition.py | 2 +- tests/test_instruments_qm.py | 31 ++++++++++------------- tests/test_instruments_qmsim.py | 8 +++--- 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/src/qibolab/instruments/qm/acquisition.py b/src/qibolab/instruments/qm/acquisition.py index c9efd1c08..38b6c46f5 100644 --- a/src/qibolab/instruments/qm/acquisition.py +++ b/src/qibolab/instruments/qm/acquisition.py @@ -236,7 +236,7 @@ def download(self, *dimensions): def fetch(self, handles): shots = handles.get(f"{self.name}_shots").fetch_all() - if len(self.npulses) > 1: + if self.npulses > 1: if self.average: # TODO: calculate std return [ diff --git a/tests/test_instruments_qm.py b/tests/test_instruments_qm.py index f433cc437..9d6055fd8 100644 --- a/tests/test_instruments_qm.py +++ b/tests/test_instruments_qm.py @@ -6,10 +6,11 @@ from qibolab import AcquisitionType, ExecutionParameters, create_platform from qibolab.instruments.qm import OPXplus, QMController -from qibolab.instruments.qm.acquisition import Acquisition +from qibolab.instruments.qm.acquisition import Acquisition, declare_acquisitions from qibolab.instruments.qm.controller import controllers_config from qibolab.instruments.qm.sequence import BakedPulse, QMPulse, Sequence from qibolab.pulses import FluxPulse, Pulse, PulseSequence, ReadoutPulse, Rectangular +from qibolab.qubits import Qubit from qibolab.sweeper import Parameter, Sweeper from .conftest import set_platform_profile @@ -18,7 +19,7 @@ def test_qmpulse(): pulse = Pulse(0, 40, 0.05, int(3e9), 0.0, Rectangular(), "ch0", qubit=0) qmpulse = QMPulse(pulse) - assert qmpulse.operation == pulse.serial + assert qmpulse.operation == "drive(40, 0.05, Rectangular())" assert qmpulse.relative_phase == 0 @@ -27,13 +28,14 @@ def test_qmpulse_declare_output(acquisition_type): options = ExecutionParameters(acquisition_type=acquisition_type) pulse = Pulse(0, 40, 0.05, int(3e9), 0.0, Rectangular(), "ch0", qubit=0) qmpulse = QMPulse(pulse) + qubits = {0: Qubit(0, threshold=0.1, iq_angle=0.2)} if acquisition_type is AcquisitionType.SPECTROSCOPY: - with pytest.raises(ValueError): + with pytest.raises(KeyError): with qua.program() as _: - qmpulse.declare_output(options, 0.1, 0.2) + declare_acquisitions([qmpulse], qubits, options) else: with qua.program() as _: - qmpulse.declare_output(options, 0.1, 0.2) + declare_acquisitions([qmpulse], qubits, options) acquisition = qmpulse.acquisition assert isinstance(acquisition, Acquisition) if acquisition_type is AcquisitionType.DISCRIMINATION: @@ -61,7 +63,6 @@ def test_qmsequence(): qmsequence.add(QMPulse(ro_pulse)) assert len(qmsequence.pulse_to_qmpulse) == 2 assert len(qmsequence.qmpulses) == 2 - assert len(qmsequence.ro_pulses) == 1 def test_qmpulse_previous_and_next(): @@ -322,14 +323,11 @@ def test_qm_register_pulse(qmplatform, pulse_type, qubit): controller.config.register_element( platform.qubits[qubit], pulse, controller.time_of_flight, controller.smearing ) - controller.config.register_pulse(platform.qubits[qubit], pulse) - assert controller.config.pulses[pulse.serial] == target_pulse + qmpulse = QMPulse(pulse) + controller.config.register_pulse(platform.qubits[qubit], qmpulse) + assert controller.config.pulses[qmpulse.operation] == target_pulse assert target_pulse["waveforms"]["I"] in controller.config.waveforms assert target_pulse["waveforms"]["Q"] in controller.config.waveforms - assert ( - controller.config.elements[f"{pulse_type}{qubit}"]["operations"][pulse.serial] - == pulse.serial - ) def test_qm_register_flux_pulse(qmplatform): @@ -344,14 +342,11 @@ def test_qm_register_flux_pulse(qmplatform): "length": pulse.duration, "waveforms": {"single": "constant_wf0.005"}, } + qmpulse = QMPulse(pulse) controller.config.register_element(platform.qubits[qubit], pulse) - controller.config.register_pulse(platform.qubits[qubit], pulse) - assert controller.config.pulses[pulse.serial] == target_pulse + controller.config.register_pulse(platform.qubits[qubit], qmpulse) + assert controller.config.pulses[qmpulse.operation] == target_pulse assert target_pulse["waveforms"]["single"] in controller.config.waveforms - assert ( - controller.config.elements[f"flux{qubit}"]["operations"][pulse.serial] - == pulse.serial - ) @pytest.mark.parametrize("duration", [0, 30]) diff --git a/tests/test_instruments_qmsim.py b/tests/test_instruments_qmsim.py index 27940ba23..74ab6ef25 100644 --- a/tests/test_instruments_qmsim.py +++ b/tests/test_instruments_qmsim.py @@ -22,7 +22,6 @@ from qibolab import AcquisitionType, AveragingMode, ExecutionParameters, create_platform from qibolab.backends import QibolabBackend -from qibolab.instruments.qm import QMSim from qibolab.pulses import SNZ, FluxPulse, PulseSequence, Rectangular from qibolab.sweeper import Parameter, Sweeper @@ -43,10 +42,11 @@ def simulator(request): pytest.skip("Skipping QM simulator tests because address was not provided.") platform = create_platform("qm") - duration = request.config.getoption("--simulation-duration") - controller = QMSim("qmopx", address, simulation_duration=duration, cloud=True) + controller = platform.instruments["qm"] + controller.simulation_duration = request.config.getoption("--simulation-duration") controller.time_of_flight = 280 - platform.instruments["qmopx"] = controller + # controller.cloud = True + platform.connect() platform.setup() yield platform From 7d40211a9e0d1287d7444967741b547650ed3ff5 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 10 Jan 2024 17:00:40 +0400 Subject: [PATCH 25/46] Fix bug with singleshot discrimination --- src/qibolab/instruments/qm/acquisition.py | 2 +- tests/test_result_shapes.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/qibolab/instruments/qm/acquisition.py b/src/qibolab/instruments/qm/acquisition.py index 38b6c46f5..ce9a6069d 100644 --- a/src/qibolab/instruments/qm/acquisition.py +++ b/src/qibolab/instruments/qm/acquisition.py @@ -281,7 +281,7 @@ def declare_acquisitions(ro_pulses, qubits, options): iq_angle = qubits[qubit].iq_angle average = options.averaging_mode is AveragingMode.CYCLIC acquisition_cls = ACQUISITION_TYPES[options.acquisition_type] - acquisition = acquisition_cls(name, qubit, options, threshold, iq_angle) + acquisition = acquisition_cls(name, qubit, average, threshold, iq_angle) acquisition.assign_element(qmpulse.element) acquisitions[name] = acquisition diff --git a/tests/test_result_shapes.py b/tests/test_result_shapes.py index 6498936bb..9bb149d3e 100644 --- a/tests/test_result_shapes.py +++ b/tests/test_result_shapes.py @@ -29,8 +29,8 @@ def execute(platform, acquisition_type, averaging_mode, sweep=False): nshots=NSHOTS, acquisition_type=acquisition_type, averaging_mode=averaging_mode ) if sweep: - amp_values = np.linspace(0.01, 0.05, NSWEEP1) - freq_values = np.linspace(-2e6, 2e6, NSWEEP2) + amp_values = np.arange(0.01, 0.06, 0.01) + freq_values = np.arange(-4e6, 4e6, 1e6) sweeper1 = Sweeper(Parameter.bias, amp_values, qubits=[platform.qubits[qubit]]) # sweeper1 = Sweeper(Parameter.amplitude, amp_values, pulses=[qd_pulse]) sweeper2 = Sweeper(Parameter.frequency, freq_values, pulses=[ro_pulse]) From 0333f4833aff893f155cb0c4fb4567cc9e5ef617 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Fri, 12 Jan 2024 12:31:09 +0400 Subject: [PATCH 26/46] refactor: Simplify Ports dict --- src/qibolab/instruments/qm/devices.py | 35 +++++++++++---------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/src/qibolab/instruments/qm/devices.py b/src/qibolab/instruments/qm/devices.py index e7cb3c048..b7440d98b 100644 --- a/src/qibolab/instruments/qm/devices.py +++ b/src/qibolab/instruments/qm/devices.py @@ -1,5 +1,7 @@ +from collections import defaultdict from dataclasses import dataclass, field from itertools import chain +from typing import Dict from qibolab.instruments.abstract import Instrument @@ -14,28 +16,19 @@ ) -@dataclass -class Ports(dict): +class PortsDefaultdict(defaultdict): """Dictionary mapping port numbers to :class:`qibolab.instruments.qm.ports.QMPort` objects. Automatically instantiates ports that have not yet been created. Used by :class:`qibolab.instruments.qm.devices.QMDevice` - """ - - constructor: type - """Type of :class:`qibolab.instruments.qm.ports.QMPort` to be used for - initializing new ports.""" - device: str - """Name of device holding these ports.""" - def __post_init__(self): - super().__init__() + https://stackoverflow.com/questions/2912231/is-there-a-clever-way-to-pass-the-key-to-defaultdicts-default-factory + """ - def __getitem__(self, number): - if number not in self: - self[number] = self.constructor(self.device, number) - return super().__getitem__(number) + def __missing__(self, key): + ret = self[key] = self.default_factory(key) # pylint: disable=E1102 + return ret @dataclass @@ -45,9 +38,9 @@ class QMDevice(Instrument): name: str """Name of the device.""" - outputs: Ports[int, QMOutput] = field(init=False) + outputs: Dict[int, QMOutput] = field(init=False) """Dictionary containing the instrument's output ports.""" - inputs: Ports[int, QMInput] = field(init=False) + inputs: Dict[int, QMInput] = field(init=False) """Dictionary containing the instrument's input ports.""" def ports(self, number, input=False): @@ -109,8 +102,8 @@ class OPXplus(QMDevice): """Device handling OPX+ controllers.""" def __post_init__(self): - self.outputs = Ports(OPXOutput, self.name) - self.inputs = Ports(OPXInput, self.name) + self.outputs = PortsDefaultdict(lambda n: OPXOutput(self.name, n)) + self.inputs = PortsDefaultdict(lambda n: OPXInput(self.name, n)) @dataclass @@ -123,8 +116,8 @@ class Octave(QMDevice): """OPXplus that acts as the waveform generator for the Octave.""" def __post_init__(self): - self.outputs = Ports(OctaveOutput, self.name) - self.inputs = Ports(OctaveInput, self.name) + self.outputs = PortsDefaultdict(lambda n: OctaveOutput(self.name, n)) + self.inputs = PortsDefaultdict(lambda n: OctaveInput(self.name, n)) def ports(self, number, input=False): """Provides Octave ports. From 932725c2572c55dcd25f2168fd3582a10f04cbf4 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 23 Jan 2024 19:11:44 +0400 Subject: [PATCH 27/46] Review comments for controller and devices --- src/qibolab/instruments/qm/controller.py | 49 +++++++++++------------- src/qibolab/instruments/qm/devices.py | 16 +------- 2 files changed, 24 insertions(+), 41 deletions(-) diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index 58ef530f2..25724a82b 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -17,7 +17,7 @@ from .sequence import BakedPulse, QMPulse, Sequence from .sweepers import sweep -OCTAVE_ADDRESS = 11000 +OCTAVE_ADDRESS_OFFSET = 11000 """Offset to be added to Octave addresses, because they must be 11xxx, where xxx are the last three digits of the Octave IP address.""" @@ -31,35 +31,32 @@ def declare_octaves(octaves, host, calibration_path=None): host (str): IP of the Quantum Machines controller. calibration_path (str): Path to the JSON file with the mixer calibration. """ - config = None - if len(octaves) > 0: - config = QmOctaveConfig() - if calibration_path is not None: - config.set_calibration_db(calibration_path) - for octave in octaves.values(): - config.add_device_info(octave.name, host, OCTAVE_ADDRESS + octave.port) + if len(octaves) == 0: + return None + + config = QmOctaveConfig() + if calibration_path is not None: + config.set_calibration_db(calibration_path) + for octave in octaves.values(): + config.add_device_info(octave.name, host, OCTAVE_ADDRESS_OFFSET + octave.port) return config -def find_duration_sweeper_pulses(sweepers): - """Find all pulses that require baking because we are sweeping their - duration. +def find_baking_pulses(sweepers): + """Find pulses that require baking because we are sweeping their duration. Args: sweepers (list): List of :class:`qibolab.sweeper.Sweeper` objects. """ - duration_sweep_pulses = set() + to_bake = set() for sweeper in sweepers: - try: - step = sweeper.values[1] - sweeper.values[0] - except IndexError: - step = sweeper.values[0] - + values = sweeper.values + step = values[1] - values[0] if len(values) > 0 else values[0] if sweeper.parameter is Parameter.duration and step % 4 != 0: for pulse in sweeper.pulses: - duration_sweep_pulses.add(pulse.serial) + to_bake.add(pulse.serial) - return duration_sweep_pulses + return to_bake def controllers_config(qubits, time_of_flight, smearing=0): @@ -233,11 +230,8 @@ def fetch_results(result, ro_pulses): Defined as ``@staticmethod`` because it is overwritten in :class:`qibolab.instruments.qm.simulator.QMSim`. """ - # TODO: Update result asynchronously instead of waiting - # for all values, in order to allow live plotting - # using ``handles.is_processing()`` handles = result.result_handles - handles.wait_for_all_values() + handles.wait_for_all_values() # for async replace with ``handles.is_processing()`` results = {} for qmpulse in ro_pulses: pulse = qmpulse.pulse @@ -262,11 +256,12 @@ def create_sequence(self, qubits, sequence, sweepers): # If we want to play overlapping pulses we need to define different elements on the same ports # like we do for readout multiplex - duration_sweep_pulses = find_duration_sweeper_pulses(sweepers) + pulses_to_bake = find_baking_pulses(sweepers) qmsequence = Sequence() - sort_key = lambda pulse: (pulse.start, pulse.duration) - for pulse in sorted(sequence.pulses, key=sort_key): + for pulse in sorted( + sequence.pulses, key=lambda pulse: (pulse.start, pulse.duration) + ): qubit = qubits[pulse.qubit] self.config.register_port(getattr(qubit, pulse.type.name.lower()).port) @@ -279,7 +274,7 @@ def create_sequence(self, qubits, sequence, sweepers): if ( pulse.duration % 4 != 0 or pulse.duration < 16 - or pulse.serial in duration_sweep_pulses + or pulse.serial in pulses_to_bake ): qmpulse = BakedPulse(pulse) qmpulse.bake(self.config, durations=[pulse.duration]) diff --git a/src/qibolab/instruments/qm/devices.py b/src/qibolab/instruments/qm/devices.py index b7440d98b..2aa7a79d4 100644 --- a/src/qibolab/instruments/qm/devices.py +++ b/src/qibolab/instruments/qm/devices.py @@ -53,21 +53,14 @@ def ports(self, number, input=False): input (bool): ``True`` for obtaining an input port, otherwise an output port is returned. Default is ``False``. """ - if input: - return self.inputs[number] - else: - return self.outputs[number] + ports_ = self.inputs if input else self.outputs + return ports_[number] def connect(self): """Only applicable for :class:`qibolab.instruments.qm.controller.QMController`, not individual devices.""" - def start(self): - """Only applicable for - :class:`qibolab.instruments.qm.controller.QMController`, not individual - devices.""" - def setup(self, **kwargs): for name, settings in kwargs.items(): number = int(name[1:]) @@ -80,11 +73,6 @@ def setup(self, **kwargs): f"Invalid port name {name} in instrument settings for {self.name}." ) - def stop(self): - """Only applicable for - :class:`qibolab.instruments.qm.controller.QMController`, not individual - devices.""" - def disconnect(self): """Only applicable for :class:`qibolab.instruments.qm.controller.QMController`, not individual From f8b3c163c4b18743b6f85f827a9d3f465199bc0d Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 23 Jan 2024 19:24:33 +0400 Subject: [PATCH 28/46] Review comment about qubit.frequencies --- src/qibolab/instruments/qm/controller.py | 8 ++++---- src/qibolab/qubits.py | 26 ++++++++++-------------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index 25724a82b..7b235cc9a 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -76,11 +76,11 @@ def controllers_config(qubits, time_of_flight, smearing=0): if qubit.readout is not None: config.register_port(qubit.readout.port) config.register_readout_element( - qubit, qubit.mz_frequencies[1], time_of_flight, smearing + qubit, qubit.mixer_frequencies["MZ"][1], time_of_flight, smearing ) if qubit.drive is not None: config.register_port(qubit.drive.port) - config.register_drive_element(qubit, qubit.rx_frequencies[1]) + config.register_drive_element(qubit, qubit.mixer_frequencies["RX"][1]) return config @@ -205,10 +205,10 @@ def calibrate_mixers(self, qubits): for qubit in qubits: print(f"Calibrating mixers for qubit {qubit.name}") if qubit.readout is not None: - _lo, _if = qubit.mz_frequencies + _lo, _if = qubit.mixer_frequencies["MZ"] machine.calibrate_element(f"readout{qubit.name}", {_lo: (_if,)}) if qubit.drive is not None: - _lo, _if = qubit.rx_frequencies + _lo, _if = qubit.mixer_frequencies["RX"] machine.calibrate_element(f"drive{qubit.name}", {_lo: (_if,)}) def execute_program(self, program): diff --git a/src/qibolab/qubits.py b/src/qibolab/qubits.py index c9a807cc2..c79ed331e 100644 --- a/src/qibolab/qubits.py +++ b/src/qibolab/qubits.py @@ -121,24 +121,20 @@ def characterization(self): } @property - def mz_frequencies(self): - """Get local oscillator and intermediate frequency used for readout. + def mixer_frequencies(self): + """Get local oscillator and intermediate frequencies of native gates. Assumes RF = LO + IF. """ - _lo = self.readout.lo_frequency - _if = self.native_gates.MZ.frequency - _lo - return _lo, _if - - @property - def rx_frequencies(self): - """Get local oscillator and intermediate frequency used for drive. - - Assumes RF = LO + IF. - """ - _lo = self.drive.lo_frequency - _if = self.native_gates.RX.frequency - _lo - return _lo, _if + freqs = {} + for gate in fields(self.native_gates): + native = getattr(self.native_gates, gate.name) + if native is not None: + channel_type = native.pulse_type.name.lower() + _lo = getattr(self, channel_type).lo_frequency + _if = native.frequency - _lo + freqs[gate.name] = _lo, _if + return freqs QubitPairId = Tuple[QubitId, QubitId] From 510e3ca3a17c72a806d0368c9fe79d22f3f0beee Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 23 Jan 2024 19:30:36 +0400 Subject: [PATCH 29/46] Replace port input with output --- src/qibolab/instruments/qm/controller.py | 11 ++++++----- src/qibolab/instruments/qm/devices.py | 16 ++++++++-------- tests/dummy_qrc/qm.py | 4 ++-- tests/dummy_qrc/qm_octave.py | 4 ++-- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index 7b235cc9a..35fb90b56 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -144,7 +144,7 @@ def __post_init__(self): if not isinstance(self.octaves, dict): self.octaves = {instr.name: instr for instr in self.octaves} - def ports(self, name, input=False): + def ports(self, name, output=True): """Provides instrument ports to the user. Note that individual ports can also be accessed from the corresponding devices @@ -155,16 +155,17 @@ def ports(self, name, input=False): For example ``((conX, Y),)`` returns port-Y of OPX+ controller X. ``((conX, Y), (conX, Z))`` returns port-Y and Z of OPX+ controller X as an :class:`qibolab.instruments.qm.ports.OPXIQ` port pair. - input (bool): ``True`` for obtaining an input port, otherwise an - output port is returned. Default is ``False``. + output (bool): ``True`` for obtaining an output port, otherwise an + input port is returned. Default is ``True``. """ if len(name) == 1: con, port = name[0] - return self.opxs[con].ports(port, input) + return self.opxs[con].ports(port, output) elif len(name) == 2: (con1, port1), (con2, port2) = name return OPXIQ( - self.opxs[con1].ports(port1, input), self.opxs[con2].ports(port2, input) + self.opxs[con1].ports(port1, output), + self.opxs[con2].ports(port2, output), ) else: raise ValueError(f"Invalid port {name} for Quantum Machines controller.") diff --git a/src/qibolab/instruments/qm/devices.py b/src/qibolab/instruments/qm/devices.py index 2aa7a79d4..6ec0674d8 100644 --- a/src/qibolab/instruments/qm/devices.py +++ b/src/qibolab/instruments/qm/devices.py @@ -43,17 +43,17 @@ class QMDevice(Instrument): inputs: Dict[int, QMInput] = field(init=False) """Dictionary containing the instrument's input ports.""" - def ports(self, number, input=False): + def ports(self, number, output=True): """Provides instrument's ports to the user. Args: number (int): Port number. Can be 1 to 10 for :class:`qibolab.instruments.qm.devices.OPXplus` and 1 to 5 for :class:`qibolab.instruments.qm.devices.Octave`. - input (bool): ``True`` for obtaining an input port, otherwise an - output port is returned. Default is ``False``. + output (bool): ``True`` for obtaining an output port, otherwise an + input port is returned. Default is ``True``. """ - ports_ = self.inputs if input else self.outputs + ports_ = self.outputs if output else self.inputs return ports_[number] def connect(self): @@ -107,15 +107,15 @@ def __post_init__(self): self.outputs = PortsDefaultdict(lambda n: OctaveOutput(self.name, n)) self.inputs = PortsDefaultdict(lambda n: OctaveInput(self.name, n)) - def ports(self, number, input=False): + def ports(self, number, output=True): """Provides Octave ports. Extension of the abstract :meth:`qibolab.instruments.qm.devices.QMDevice.ports` because Octave ports are used for mixing two existing (I, Q) OPX+ ports. """ - port = super().ports(number, input) + port = super().ports(number, output) if port.opx_port is None: - iport = self.connectivity.ports(2 * number - 1, input) - qport = self.connectivity.ports(2 * number, input) + iport = self.connectivity.ports(2 * number - 1, output) + qport = self.connectivity.ports(2 * number, output) port.opx_port = OPXIQ(iport, qport) return port diff --git a/tests/dummy_qrc/qm.py b/tests/dummy_qrc/qm.py index 6c351c431..95d4f4b95 100644 --- a/tests/dummy_qrc/qm.py +++ b/tests/dummy_qrc/qm.py @@ -32,10 +32,10 @@ def create(runcard_path=RUNCARD): channels |= Channel("L3-25_b", port=controller.ports((("con2", 10), ("con2", 9)))) # feedback channels |= Channel( - "L2-5_a", port=controller.ports((("con1", 2), ("con1", 1)), input=True) + "L2-5_a", port=controller.ports((("con1", 2), ("con1", 1)), output=False) ) channels |= Channel( - "L2-5_b", port=controller.ports((("con2", 2), ("con2", 1)), input=True) + "L2-5_b", port=controller.ports((("con2", 2), ("con2", 1)), output=False) ) # drive channels |= ( diff --git a/tests/dummy_qrc/qm_octave.py b/tests/dummy_qrc/qm_octave.py index a8c372555..37916fb07 100644 --- a/tests/dummy_qrc/qm_octave.py +++ b/tests/dummy_qrc/qm_octave.py @@ -40,8 +40,8 @@ def create(runcard_path=RUNCARD): channels |= Channel("L3-25_a", port=octave1.ports(5)) channels |= Channel("L3-25_b", port=octave2.ports(5)) # feedback - channels |= Channel("L2-5_a", port=octave1.ports(1, input=True)) - channels |= Channel("L2-5_b", port=octave2.ports(1, input=True)) + channels |= Channel("L2-5_a", port=octave1.ports(1, output=False)) + channels |= Channel("L2-5_b", port=octave2.ports(1, output=False)) # drive channels |= (Channel(f"L3-1{i}", port=octave1.ports(i)) for i in range(1, 5)) channels |= Channel("L3-15", port=octave3.ports(1)) From f3aac36ab114c63f5ff58e6c36b309a95e67b9d7 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Mon, 5 Feb 2024 17:58:02 +0400 Subject: [PATCH 30/46] refactor: Move threshold and angle to ShotsAcquisition --- src/qibolab/instruments/qm/acquisition.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/qibolab/instruments/qm/acquisition.py b/src/qibolab/instruments/qm/acquisition.py index db5abc09d..3b112689b 100644 --- a/src/qibolab/instruments/qm/acquisition.py +++ b/src/qibolab/instruments/qm/acquisition.py @@ -35,10 +35,6 @@ class Acquisition(ABC): instruments.""" qubit: QubitId average: bool - threshold: Optional[float] = None - """Threshold to be used for classification of single shots.""" - angle: Optional[float] = None - """Angle in the IQ plane to be used for classification of single shots.""" keys: List[str] = field(default_factory=list) @@ -188,6 +184,11 @@ class ShotsAcquisition(Acquisition): Threshold and angle must be given in order to classify shots. """ + threshold: Optional[float] = None + """Threshold to be used for classification of single shots.""" + angle: Optional[float] = None + """Angle in the IQ plane to be used for classification of single shots.""" + I: _Variable = field(default_factory=lambda: declare(fixed)) Q: _Variable = field(default_factory=lambda: declare(fixed)) """Variables to save the (I, Q) values acquired from a single shot.""" @@ -277,11 +278,17 @@ def declare_acquisitions(ro_pulses, qubits, options): qubit = qmpulse.pulse.qubit name = f"{qmpulse.operation}_{qubit}" if name not in acquisitions: - threshold = qubits[qubit].threshold - iq_angle = qubits[qubit].iq_angle average = options.averaging_mode is AveragingMode.CYCLIC acquisition_cls = ACQUISITION_TYPES[options.acquisition_type] - acquisition = acquisition_cls(name, qubit, average, threshold, iq_angle) + if options.acquisition_type is AcquisitionType.DISCRIMINATION: + threshold = qubits[qubit].threshold + iq_angle = qubits[qubit].iq_angle + acquisition = acquisition_cls( + name, qubit, average, threshold=threshold, angle=iq_angle + ) + else: + acquisition = acquisition_cls(name, qubit, average) + acquisition.assign_element(qmpulse.element) acquisitions[name] = acquisition From 555b12fa4c4dde3723b799211dc04de9fea94c4c Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Mon, 5 Feb 2024 17:59:02 +0400 Subject: [PATCH 31/46] refactor: Drop error --- src/qibolab/instruments/qm/acquisition.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/qibolab/instruments/qm/acquisition.py b/src/qibolab/instruments/qm/acquisition.py index 3b112689b..648237656 100644 --- a/src/qibolab/instruments/qm/acquisition.py +++ b/src/qibolab/instruments/qm/acquisition.py @@ -198,11 +198,6 @@ class ShotsAcquisition(Acquisition): """Stream to collect multiple shots.""" def __post_init__(self): - if self.threshold is None or self.angle is None: - raise ValueError( - "Cannot use ``AcquisitionType.DISCRIMINATION`` " - "if threshold and angle are not given." - ) self.cos = np.cos(self.angle) self.sin = np.sin(self.angle) From 50af0a8f202c1e8666b806560989e33ffc5b24d6 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Mon, 5 Feb 2024 18:03:31 +0400 Subject: [PATCH 32/46] refactor: Merge acquisition measure and save --- src/qibolab/instruments/qm/acquisition.py | 11 ----------- src/qibolab/instruments/qm/sequence.py | 2 -- 2 files changed, 13 deletions(-) diff --git a/src/qibolab/instruments/qm/acquisition.py b/src/qibolab/instruments/qm/acquisition.py index 648237656..0a4fce600 100644 --- a/src/qibolab/instruments/qm/acquisition.py +++ b/src/qibolab/instruments/qm/acquisition.py @@ -61,10 +61,6 @@ def measure(self, operation, element): element (str): Element (from ``config``) that the pulse will be applied on. """ - @abstractmethod - def save(self): - """Save acquired results from variables to streams.""" - @abstractmethod def download(self, *dimensions): """Save streams to prepare for fetching from host device. @@ -93,9 +89,6 @@ def assign_element(self, element): def measure(self, operation, element): qua.measure(operation, element, self.adc_stream) - def save(self): - pass - def download(self, *dimensions): i_stream = self.adc_stream.input1() q_stream = self.adc_stream.input2() @@ -138,8 +131,6 @@ def measure(self, operation, element): qua.dual_demod.full("cos", "out1", "sin", "out2", self.I), qua.dual_demod.full("minus_sin", "out1", "cos", "out2", self.Q), ) - - def save(self): qua.save(self.I, self.I_stream) qua.save(self.Q, self.Q_stream) @@ -216,8 +207,6 @@ def measure(self, operation, element): self.shot, qua.Cast.to_int(self.I * self.cos - self.Q * self.sin > self.threshold), ) - - def save(self): qua.save(self.shot, self.shots) def download(self, *dimensions): diff --git a/src/qibolab/instruments/qm/sequence.py b/src/qibolab/instruments/qm/sequence.py index d7127f788..21b2ded97 100644 --- a/src/qibolab/instruments/qm/sequence.py +++ b/src/qibolab/instruments/qm/sequence.py @@ -259,9 +259,7 @@ def play(self, relaxation_time=0): if qmpulse.wait_cycles is not None: qua.wait(qmpulse.wait_cycles, qmpulse.element) if pulse.type is PulseType.READOUT: - # Save data to the stream processing qmpulse.acquisition.measure(qmpulse.operation, qmpulse.element) - qmpulse.acquisition.save() else: if ( not isinstance(qmpulse.relative_phase, float) From 77d2d7111ddc40a09ee31adb501bc84dc5319054 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Mon, 5 Feb 2024 18:11:02 +0400 Subject: [PATCH 33/46] test: remove calibration path from testing platform --- tests/dummy_qrc/qm_octave/platform.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/dummy_qrc/qm_octave/platform.py b/tests/dummy_qrc/qm_octave/platform.py index 19a04a019..3d3d307bf 100644 --- a/tests/dummy_qrc/qm_octave/platform.py +++ b/tests/dummy_qrc/qm_octave/platform.py @@ -31,7 +31,6 @@ def create(runcard_path=RUNCARD): opxs=opxs, octaves=[octave1, octave2, octave3], time_of_flight=280, - calibration_path=str(runcard_path.parent), ) # Create channel objects and map controllers to channels From 7711fe991c9ff58128bdb042966b3e3655df305b Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Thu, 15 Feb 2024 13:39:36 +0400 Subject: [PATCH 34/46] fix: Mixers update when multiple pulses are used --- src/qibolab/instruments/qm/config.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/qibolab/instruments/qm/config.py b/src/qibolab/instruments/qm/config.py index 016ce6205..5021a01e2 100644 --- a/src/qibolab/instruments/qm/config.py +++ b/src/qibolab/instruments/qm/config.py @@ -127,9 +127,10 @@ def register_drive_element(self, qubit, intermediate_frequency=0): self.elements[f"drive{qubit.name}"][ "intermediate_frequency" ] = intermediate_frequency - self.mixers[f"mixer_drive{qubit.name}"][0][ - "intermediate_frequency" - ] = intermediate_frequency + if isinstance(qubit.drive.port, OPXIQ): + self.mixers[f"mixer_drive{qubit.name}"][0][ + "intermediate_frequency" + ] = intermediate_frequency def register_readout_element( self, qubit, intermediate_frequency=0, time_of_flight=0, smearing=0 @@ -183,9 +184,10 @@ def register_readout_element( self.elements[f"readout{qubit.name}"][ "intermediate_frequency" ] = intermediate_frequency - self.mixers[f"mixer_readout{qubit.name}"][0][ - "intermediate_frequency" - ] = intermediate_frequency + if isinstance(qubit.readout.port, OPXIQ): + self.mixers[f"mixer_readout{qubit.name}"][0][ + "intermediate_frequency" + ] = intermediate_frequency def register_flux_element(self, qubit, intermediate_frequency=0): """Register qubit flux elements and controllers in the QM config. From 7af1dee3a53d54cc32f00612e2631d9e51ac277c Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Thu, 15 Feb 2024 18:22:15 +0400 Subject: [PATCH 35/46] Set batch size for unrolling --- src/qibolab/instruments/qm/controller.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index 13ee9dde6..39d807ba3 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -9,6 +9,7 @@ from qibolab import AveragingMode from qibolab.instruments.abstract import Controller +from qibolab.instruments.unrolling import batch_max_sequences from qibolab.pulses import PulseType from qibolab.sweeper import Parameter @@ -22,6 +23,7 @@ OCTAVE_ADDRESS_OFFSET = 11000 """Offset to be added to Octave addresses, because they must be 11xxx, where xxx are the last three digits of the Octave IP address.""" +MAX_BATCH_SIZE = 30 def declare_octaves(octaves, host, calibration_path=None): @@ -363,4 +365,4 @@ def sweep(self, qubits, couplers, sequence, options, *sweepers): return fetch_results(result, acquisitions) def split_batches(self, sequences): - return [sequences] + return batch_max_sequences(sequences, MAX_BATCH_SIZE) From c1ccab7d99cbea9c762d1a0166cd51bf313de88c Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Fri, 16 Feb 2024 11:43:11 +0400 Subject: [PATCH 36/46] fix: Remove default debug script file name --- src/qibolab/instruments/qm/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index 35fb90b56..2267a81ba 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -122,7 +122,7 @@ class QMController(Controller): """Smearing used for hardware signal integration.""" calibration_path: Optional[str] = None """Path to the JSON file that contains the mixer calibration.""" - script_file_name: Optional[str] = "qua_script.py" + script_file_name: Optional[str] = None """Name of the file that the QUA program will dumped in that after every execution. From 1affed079857eb1c6c3f28f8b32d5655baee3afa Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Fri, 16 Feb 2024 12:20:47 +0400 Subject: [PATCH 37/46] feat: Integration weights from qubit.kernel --- src/qibolab/instruments/qm/config.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/qibolab/instruments/qm/config.py b/src/qibolab/instruments/qm/config.py index 5021a01e2..4dab43379 100644 --- a/src/qibolab/instruments/qm/config.py +++ b/src/qibolab/instruments/qm/config.py @@ -340,19 +340,27 @@ def register_integration_weights(self, qubit, readout_len): readout_len (int): Duration of the readout pulse in ns. """ angle = 0 + cos, sin = np.cos(angle), np.sin(angle) + if qubit.kernel is None: + convert = lambda x: [(x, readout_len)] + else: + cos = qubit.kernel * cos + sin = qubit.kernel * sin + convert = lambda x: x + self.integration_weights.update( { f"cosine_weights{qubit.name}": { - "cosine": [(np.cos(angle), readout_len)], - "sine": [(-np.sin(angle), readout_len)], + "cosine": convert(cos), + "sine": convert(-sin), }, f"sine_weights{qubit.name}": { - "cosine": [(np.sin(angle), readout_len)], - "sine": [(np.cos(angle), readout_len)], + "cosine": convert(sin), + "sine": convert(cos), }, f"minus_sine_weights{qubit.name}": { - "cosine": [(-np.sin(angle), readout_len)], - "sine": [(-np.cos(angle), readout_len)], + "cosine": convert(-sin), + "sine": convert(-cos), }, } ) From 4509421c331516825f822e6d049b8e89aebe4e9b Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Mon, 19 Feb 2024 17:11:43 +0400 Subject: [PATCH 38/46] fix: Zero q-component for rectangular readout pulse --- src/qibolab/instruments/qm/config.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/qibolab/instruments/qm/config.py b/src/qibolab/instruments/qm/config.py index 4dab43379..f93bb9579 100644 --- a/src/qibolab/instruments/qm/config.py +++ b/src/qibolab/instruments/qm/config.py @@ -312,12 +312,12 @@ def register_waveform(self, pulse, mode="i"): serial (str): String with a serialization of the waveform. Used as key to identify the waveform in the config. """ - # Maybe need to force zero q waveforms - # if pulse.type.name == "READOUT" and mode == "q": - # serial = "zero_wf" - # if serial not in self.waveforms: - # self.waveforms[serial] = {"type": "constant", "sample": 0.0} - if isinstance(pulse.shape, Rectangular): + if pulse.type is PulseType.READOUT and mode == "q": + # Force zero q waveforms for readout + serial = "zero_wf" + if serial not in self.waveforms: + self.waveforms[serial] = {"type": "constant", "sample": 0.0} + elif isinstance(pulse.shape, Rectangular): serial = f"constant_wf{pulse.amplitude}" if serial not in self.waveforms: self.waveforms[serial] = {"type": "constant", "sample": pulse.amplitude} From ac31d56ec09c1ca711191ffb9bc1a685acb27a79 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Mon, 19 Feb 2024 17:25:30 +0400 Subject: [PATCH 39/46] fix: tests --- tests/test_instruments_qm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_instruments_qm.py b/tests/test_instruments_qm.py index 32e756159..f7970ccae 100644 --- a/tests/test_instruments_qm.py +++ b/tests/test_instruments_qm.py @@ -307,7 +307,7 @@ def test_qm_register_pulse(qmplatform, pulse_type, qubit): target_pulse = { "operation": "measurement", "length": pulse.duration, - "waveforms": {"I": "constant_wf0.003575", "Q": "constant_wf0.003575"}, + "waveforms": {"I": "constant_wf0.003575", "Q": "zero_wf"}, "digital_marker": "ON", "integration_weights": { "cos": "cosine_weights1", From eb853ecdfbb9b355af6a5c7f58714a155241306c Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Mon, 19 Feb 2024 17:33:42 +0400 Subject: [PATCH 40/46] chore: naming conventions --- src/qibolab/instruments/qm/acquisition.py | 66 +++++++++++------------ tests/test_instruments_qm.py | 4 +- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/qibolab/instruments/qm/acquisition.py b/src/qibolab/instruments/qm/acquisition.py index 0a4fce600..e9be966b0 100644 --- a/src/qibolab/instruments/qm/acquisition.py +++ b/src/qibolab/instruments/qm/acquisition.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod from dataclasses import dataclass, field -from typing import List, Optional +from typing import Optional import numpy as np from qm import qua @@ -36,7 +36,7 @@ class Acquisition(ABC): qubit: QubitId average: bool - keys: List[str] = field(default_factory=list) + keys: list[str] = field(default_factory=list) @property def npulses(self): @@ -90,13 +90,13 @@ def measure(self, operation, element): qua.measure(operation, element, self.adc_stream) def download(self, *dimensions): - i_stream = self.adc_stream.input1() - q_stream = self.adc_stream.input2() + istream = self.adc_stream.input1() + qstream = self.adc_stream.input2() if self.average: - i_stream = i_stream.average() - q_stream = q_stream.average() - i_stream.save(f"{self.name}_I") - q_stream.save(f"{self.name}_Q") + istream = istream.average() + qstream = qstream.average() + istream.save(f"{self.name}_I") + qstream.save(f"{self.name}_Q") def fetch(self, handles): ires = handles.get(f"{self.name}_I").fetch_all() @@ -113,41 +113,41 @@ def fetch(self, handles): class IntegratedAcquisition(Acquisition): """QUA variables used for integrated acquisition.""" - I: _Variable = field(default_factory=lambda: declare(fixed)) - Q: _Variable = field(default_factory=lambda: declare(fixed)) + i: _Variable = field(default_factory=lambda: declare(fixed)) + q: _Variable = field(default_factory=lambda: declare(fixed)) """Variables to save the (I, Q) values acquired from a single shot.""" - I_stream: _ResultSource = field(default_factory=lambda: declare_stream()) - Q_stream: _ResultSource = field(default_factory=lambda: declare_stream()) + istream: _ResultSource = field(default_factory=lambda: declare_stream()) + qstream: _ResultSource = field(default_factory=lambda: declare_stream()) """Streams to collect the results of all shots.""" def assign_element(self, element): - assign_variables_to_element(element, self.I, self.Q) + assign_variables_to_element(element, self.i, self.q) def measure(self, operation, element): qua.measure( operation, element, None, - qua.dual_demod.full("cos", "out1", "sin", "out2", self.I), - qua.dual_demod.full("minus_sin", "out1", "cos", "out2", self.Q), + qua.dual_demod.full("cos", "out1", "sin", "out2", self.i), + qua.dual_demod.full("minus_sin", "out1", "cos", "out2", self.q), ) - qua.save(self.I, self.I_stream) - qua.save(self.Q, self.Q_stream) + qua.save(self.I, self.istream) + qua.save(self.Q, self.qstream) def download(self, *dimensions): - Istream = self.I_stream - Qstream = self.Q_stream + istream = self.istream + qstream = self.qstream if self.npulses > 1: - Istream = Istream.buffer(self.npulses) - Qstream = Qstream.buffer(self.npulses) + istream = istream.buffer(self.npulses) + qstream = qstream.buffer(self.npulses) for dim in dimensions: - Istream = Istream.buffer(dim) - Qstream = Qstream.buffer(dim) + istream = istream.buffer(dim) + qstream = qstream.buffer(dim) if self.average: - Istream = Istream.average() - Qstream = Qstream.average() - Istream.save(f"{self.name}_I") - Qstream.save(f"{self.name}_Q") + istream = istream.average() + qstream = qstream.average() + istream.save(f"{self.name}_I") + qstream.save(f"{self.name}_Q") def fetch(self, handles): ires = handles.get(f"{self.name}_I").fetch_all() @@ -180,8 +180,8 @@ class ShotsAcquisition(Acquisition): angle: Optional[float] = None """Angle in the IQ plane to be used for classification of single shots.""" - I: _Variable = field(default_factory=lambda: declare(fixed)) - Q: _Variable = field(default_factory=lambda: declare(fixed)) + i: _Variable = field(default_factory=lambda: declare(fixed)) + q: _Variable = field(default_factory=lambda: declare(fixed)) """Variables to save the (I, Q) values acquired from a single shot.""" shot: _Variable = field(default_factory=lambda: declare(int)) """Variable for calculating an individual shots.""" @@ -193,19 +193,19 @@ def __post_init__(self): self.sin = np.sin(self.angle) def assign_element(self, element): - assign_variables_to_element(element, self.I, self.Q, self.shot) + assign_variables_to_element(element, self.i, self.q, self.shot) def measure(self, operation, element): qua.measure( operation, element, None, - qua.dual_demod.full("cos", "out1", "sin", "out2", self.I), - qua.dual_demod.full("minus_sin", "out1", "cos", "out2", self.Q), + qua.dual_demod.full("cos", "out1", "sin", "out2", self.i), + qua.dual_demod.full("minus_sin", "out1", "cos", "out2", self.q), ) qua.assign( self.shot, - qua.Cast.to_int(self.I * self.cos - self.Q * self.sin > self.threshold), + qua.Cast.to_int(self.i * self.cos - self.q * self.sin > self.threshold), ) qua.save(self.shot, self.shots) diff --git a/tests/test_instruments_qm.py b/tests/test_instruments_qm.py index f7970ccae..ce7d1fbf6 100644 --- a/tests/test_instruments_qm.py +++ b/tests/test_instruments_qm.py @@ -47,8 +47,8 @@ def test_qmpulse_declare_output(acquisition_type): elif acquisition_type is AcquisitionType.INTEGRATION: assert isinstance(acquisition.I, qua._dsl._Variable) assert isinstance(acquisition.Q, qua._dsl._Variable) - assert isinstance(acquisition.I_stream, qua._dsl._ResultSource) - assert isinstance(acquisition.Q_stream, qua._dsl._ResultSource) + assert isinstance(acquisition.istream, qua._dsl._ResultSource) + assert isinstance(acquisition.qstream, qua._dsl._ResultSource) elif acquisition_type is AcquisitionType.RAW: assert isinstance(acquisition.adc_stream, qua._dsl._ResultSource) From d5b8b45738b829e76f26d798d91dbf75a51bc6d5 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Mon, 19 Feb 2024 18:09:19 +0400 Subject: [PATCH 41/46] refactor: simplify fetching --- src/qibolab/instruments/qm/acquisition.py | 35 ++++++----------------- src/qibolab/result.py | 2 +- 2 files changed, 9 insertions(+), 28 deletions(-) diff --git a/src/qibolab/instruments/qm/acquisition.py b/src/qibolab/instruments/qm/acquisition.py index e9be966b0..c79e5a31c 100644 --- a/src/qibolab/instruments/qm/acquisition.py +++ b/src/qibolab/instruments/qm/acquisition.py @@ -131,8 +131,8 @@ def measure(self, operation, element): qua.dual_demod.full("cos", "out1", "sin", "out2", self.i), qua.dual_demod.full("minus_sin", "out1", "cos", "out2", self.q), ) - qua.save(self.I, self.istream) - qua.save(self.Q, self.qstream) + qua.save(self.i, self.istream) + qua.save(self.q, self.qstream) def download(self, *dimensions): istream = self.istream @@ -153,19 +153,10 @@ def fetch(self, handles): ires = handles.get(f"{self.name}_I").fetch_all() qres = handles.get(f"{self.name}_Q").fetch_all() signal = ires + 1j * qres + res_cls = AveragedIntegratedResults if self.average else IntegratedResults if self.npulses > 1: - if self.average: - # TODO: calculate std - return [ - AveragedIntegratedResults(signal[..., i]) - for i in range(self.npulses) - ] - return [IntegratedResults(signal[..., i]) for i in range(self.npulses)] - else: - if self.average: - # TODO: calculate std - return [AveragedIntegratedResults(signal)] - return [IntegratedResults(signal)] + return [res_cls(signal[..., i]) for i in range(self.npulses)] + return [res_cls(signal)] @dataclass @@ -221,20 +212,10 @@ def download(self, *dimensions): def fetch(self, handles): shots = handles.get(f"{self.name}_shots").fetch_all() + res_cls = AveragedSampleResults if self.average else SampleResults if self.npulses > 1: - if self.average: - # TODO: calculate std - return [ - AveragedSampleResults(shots[..., i]) for i in range(self.npulses) - ] - return [ - SampleResults(shots[..., i].astype(int)) for i in range(self.npulses) - ] - else: - if self.average: - # TODO: calculate std - return [AveragedSampleResults(shots)] - return [SampleResults(shots.astype(int))] + return [res_cls(shots[..., i]) for i in range(self.npulses)] + return [res_cls(shots.astype(int))] ACQUISITION_TYPES = { diff --git a/src/qibolab/result.py b/src/qibolab/result.py index 1ee07539a..a72757898 100644 --- a/src/qibolab/result.py +++ b/src/qibolab/result.py @@ -108,7 +108,7 @@ class SampleResults: """ def __init__(self, data: np.ndarray): - self.samples: npt.NDArray[np.uint32] = data + self.samples: npt.NDArray[np.uint32] = np.array(data).astype(int) def __add__(self, data): return self.__class__(np.append(self.samples, data.samples)) From 0aaf828df50f48fa390d051bad0616b7c0f4d2bd Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Mon, 19 Feb 2024 18:15:12 +0400 Subject: [PATCH 42/46] refactor: simplify declare_acquisition --- src/qibolab/instruments/qm/acquisition.py | 14 ++++++-------- tests/test_instruments_qm.py | 4 ++-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/qibolab/instruments/qm/acquisition.py b/src/qibolab/instruments/qm/acquisition.py index c79e5a31c..896832adf 100644 --- a/src/qibolab/instruments/qm/acquisition.py +++ b/src/qibolab/instruments/qm/acquisition.py @@ -244,15 +244,13 @@ def declare_acquisitions(ro_pulses, qubits, options): name = f"{qmpulse.operation}_{qubit}" if name not in acquisitions: average = options.averaging_mode is AveragingMode.CYCLIC - acquisition_cls = ACQUISITION_TYPES[options.acquisition_type] + kwargs = {} if options.acquisition_type is AcquisitionType.DISCRIMINATION: - threshold = qubits[qubit].threshold - iq_angle = qubits[qubit].iq_angle - acquisition = acquisition_cls( - name, qubit, average, threshold=threshold, angle=iq_angle - ) - else: - acquisition = acquisition_cls(name, qubit, average) + kwargs["threshold"] = qubits[qubit].threshold + kwargs["angle"] = qubits[qubit].iq_angle + acquisition = ACQUISITION_TYPES[options.acquisition_type]( + name, qubit, average, **kwargs + ) acquisition.assign_element(qmpulse.element) acquisitions[name] = acquisition diff --git a/tests/test_instruments_qm.py b/tests/test_instruments_qm.py index ce7d1fbf6..b013801de 100644 --- a/tests/test_instruments_qm.py +++ b/tests/test_instruments_qm.py @@ -45,8 +45,8 @@ def test_qmpulse_declare_output(acquisition_type): assert isinstance(acquisition.shot, qua._dsl._Variable) assert isinstance(acquisition.shots, qua._dsl._ResultSource) elif acquisition_type is AcquisitionType.INTEGRATION: - assert isinstance(acquisition.I, qua._dsl._Variable) - assert isinstance(acquisition.Q, qua._dsl._Variable) + assert isinstance(acquisition.i, qua._dsl._Variable) + assert isinstance(acquisition.q, qua._dsl._Variable) assert isinstance(acquisition.istream, qua._dsl._ResultSource) assert isinstance(acquisition.qstream, qua._dsl._ResultSource) elif acquisition_type is AcquisitionType.RAW: From 27893f87d54baf03be93f1d54172609888e877ac Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Mon, 19 Feb 2024 23:57:28 +0400 Subject: [PATCH 43/46] refactor: Lift result object creation to base Acquisition --- src/qibolab/instruments/qm/acquisition.py | 37 +++++++++++++++-------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/qibolab/instruments/qm/acquisition.py b/src/qibolab/instruments/qm/acquisition.py index 896832adf..1b5470966 100644 --- a/src/qibolab/instruments/qm/acquisition.py +++ b/src/qibolab/instruments/qm/acquisition.py @@ -38,6 +38,12 @@ class Acquisition(ABC): keys: list[str] = field(default_factory=list) + RESULT_CLS = IntegratedResults + """Result object type that corresponds to this acquisition type.""" + AVERAGED_RESULT_CLS = AveragedIntegratedResults + """Averaged result object type that corresponds to this acquisition + type.""" + @property def npulses(self): return len(self.keys) @@ -73,6 +79,13 @@ def download(self, *dimensions): def fetch(self): """Fetch downloaded streams to host device.""" + def result(self, data): + """Creates Qibolab result object that is returned to the platform.""" + res_cls = self.AVERAGED_RESULT_CLS if self.average else self.RESULT_CLS + if self.npulses > 1: + return [res_cls(data[..., i]) for i in range(self.npulses)] + return [res_cls(data)] + @dataclass class RawAcquisition(Acquisition): @@ -83,6 +96,9 @@ class RawAcquisition(Acquisition): ) """Stream to collect raw ADC data.""" + _result_cls = RawWaveformResults + _averaged_result_cls = AveragedRawWaveformResults + def assign_element(self, element): pass @@ -104,9 +120,7 @@ def fetch(self, handles): # convert raw ADC signal to volts u = unit() signal = u.raw2volts(ires) + 1j * u.raw2volts(qres) - if self.average: - return [AveragedRawWaveformResults(signal)] - return [RawWaveformResults(signal)] + return self.result(signal) @dataclass @@ -120,6 +134,9 @@ class IntegratedAcquisition(Acquisition): qstream: _ResultSource = field(default_factory=lambda: declare_stream()) """Streams to collect the results of all shots.""" + _result_cls = IntegratedResults + _averaged_result_cls = AveragedIntegratedResults + def assign_element(self, element): assign_variables_to_element(element, self.i, self.q) @@ -152,11 +169,7 @@ def download(self, *dimensions): def fetch(self, handles): ires = handles.get(f"{self.name}_I").fetch_all() qres = handles.get(f"{self.name}_Q").fetch_all() - signal = ires + 1j * qres - res_cls = AveragedIntegratedResults if self.average else IntegratedResults - if self.npulses > 1: - return [res_cls(signal[..., i]) for i in range(self.npulses)] - return [res_cls(signal)] + return self.result(ires + 1j * qres) @dataclass @@ -179,6 +192,9 @@ class ShotsAcquisition(Acquisition): shots: _ResultSource = field(default_factory=lambda: declare_stream()) """Stream to collect multiple shots.""" + _result_cls = SampleResults + _averaged_result_cls = AveragedSampleResults + def __post_init__(self): self.cos = np.cos(self.angle) self.sin = np.sin(self.angle) @@ -212,10 +228,7 @@ def download(self, *dimensions): def fetch(self, handles): shots = handles.get(f"{self.name}_shots").fetch_all() - res_cls = AveragedSampleResults if self.average else SampleResults - if self.npulses > 1: - return [res_cls(shots[..., i]) for i in range(self.npulses)] - return [res_cls(shots.astype(int))] + return self.result(shots) ACQUISITION_TYPES = { From 2d0420e9b5f2e9fc734f77d69d77d46c5832faeb Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 20 Feb 2024 00:01:39 +0400 Subject: [PATCH 44/46] fix: names of result_cls --- src/qibolab/instruments/qm/acquisition.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/qibolab/instruments/qm/acquisition.py b/src/qibolab/instruments/qm/acquisition.py index 1b5470966..4b7c31794 100644 --- a/src/qibolab/instruments/qm/acquisition.py +++ b/src/qibolab/instruments/qm/acquisition.py @@ -96,8 +96,8 @@ class RawAcquisition(Acquisition): ) """Stream to collect raw ADC data.""" - _result_cls = RawWaveformResults - _averaged_result_cls = AveragedRawWaveformResults + RESULT_CLS = RawWaveformResults + AVERAGED_RESULT_CLS = AveragedRawWaveformResults def assign_element(self, element): pass @@ -134,8 +134,8 @@ class IntegratedAcquisition(Acquisition): qstream: _ResultSource = field(default_factory=lambda: declare_stream()) """Streams to collect the results of all shots.""" - _result_cls = IntegratedResults - _averaged_result_cls = AveragedIntegratedResults + RESULT_CLS = IntegratedResults + AVERAGED_RESULT_CLS = AveragedIntegratedResults def assign_element(self, element): assign_variables_to_element(element, self.i, self.q) @@ -192,8 +192,8 @@ class ShotsAcquisition(Acquisition): shots: _ResultSource = field(default_factory=lambda: declare_stream()) """Stream to collect multiple shots.""" - _result_cls = SampleResults - _averaged_result_cls = AveragedSampleResults + RESULT_CLS = SampleResults + AVERAGED_RESULT_CLS = AveragedSampleResults def __post_init__(self): self.cos = np.cos(self.angle) From 5f77a7101d9b0e08d9f4bd2d9fa8756dfd68c72b Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 20 Feb 2024 00:05:01 +0400 Subject: [PATCH 45/46] fix: cast samples to uint32 Co-authored-by: Alessandro Candido --- src/qibolab/result.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibolab/result.py b/src/qibolab/result.py index a72757898..8124bc9d4 100644 --- a/src/qibolab/result.py +++ b/src/qibolab/result.py @@ -108,7 +108,7 @@ class SampleResults: """ def __init__(self, data: np.ndarray): - self.samples: npt.NDArray[np.uint32] = np.array(data).astype(int) + self.samples: npt.NDArray[np.uint32] = np.array(data).astype(np.uint32) def __add__(self, data): return self.__class__(np.append(self.samples, data.samples)) From 12841b3fe2511e5f01c6564a7a89a0240d43e794 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 20 Feb 2024 14:18:39 +0400 Subject: [PATCH 46/46] Convert acquisitions to list --- src/qibolab/instruments/qm/acquisition.py | 8 ++++---- src/qibolab/instruments/qm/controller.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/qibolab/instruments/qm/acquisition.py b/src/qibolab/instruments/qm/acquisition.py index 4b7c31794..f3ceafbb2 100644 --- a/src/qibolab/instruments/qm/acquisition.py +++ b/src/qibolab/instruments/qm/acquisition.py @@ -249,7 +249,7 @@ def declare_acquisitions(ro_pulses, qubits, options): options containing acquisition type and averaging mode. Returns: - Dictionary containing the different :class:`qibolab.instruments.qm.acquisition.Acquisition` objects. + List of all :class:`qibolab.instruments.qm.acquisition.Acquisition` objects. """ acquisitions = {} for qmpulse in ro_pulses: @@ -261,16 +261,16 @@ def declare_acquisitions(ro_pulses, qubits, options): if options.acquisition_type is AcquisitionType.DISCRIMINATION: kwargs["threshold"] = qubits[qubit].threshold kwargs["angle"] = qubits[qubit].iq_angle + acquisition = ACQUISITION_TYPES[options.acquisition_type]( name, qubit, average, **kwargs ) - acquisition.assign_element(qmpulse.element) acquisitions[name] = acquisition acquisitions[name].keys.append(qmpulse.pulse.serial) qmpulse.acquisition = acquisitions[name] - return acquisitions + return list(acquisitions.values()) def fetch_results(result, acquisitions): @@ -286,7 +286,7 @@ def fetch_results(result, acquisitions): handles = result.result_handles handles.wait_for_all_values() # for async replace with ``handles.is_processing()`` results = {} - for acquisition in acquisitions.values(): + for acquisition in acquisitions: data = acquisition.fetch(handles) for serial, result in zip(acquisition.keys, data): results[acquisition.qubit] = results[serial] = result diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index 114e71367..aea41b4cb 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -346,7 +346,7 @@ def sweep(self, qubits, couplers, sequence, options, *sweepers): ) with qua.stream_processing(): - for acquisition in acquisitions.values(): + for acquisition in acquisitions: acquisition.download(*buffer_dims) if self.script_file_name is not None: