diff --git a/.github/workflows/capi.yml b/.github/workflows/capi.yml index af0d6d402..cae687e22 100644 --- a/.github/workflows/capi.yml +++ b/.github/workflows/capi.yml @@ -13,11 +13,11 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.11" - name: Prepare virtual environment run: | python -m venv env diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 854ec7f7c..fabee8daf 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -11,7 +11,7 @@ jobs: build: strategy: matrix: - os: [ubuntu-latest, macos-13, windows-latest] + os: [ubuntu-latest, macos-latest, windows-latest] python-version: [3.9, "3.10", "3.11"] uses: qiboteam/workflows/.github/workflows/deploy-pip-poetry.yml@v1 with: diff --git a/.github/workflows/rules.yml b/.github/workflows/rules.yml index 462f54419..4e6b11f35 100644 --- a/.github/workflows/rules.yml +++ b/.github/workflows/rules.yml @@ -11,7 +11,7 @@ jobs: build: strategy: matrix: - os: [ubuntu-latest, macos-13, windows-latest] + os: [ubuntu-latest, macos-latest, windows-latest] python-version: [3.9, "3.10", "3.11"] uses: qiboteam/workflows/.github/workflows/rules-poetry.yml@v1 with: diff --git a/.github/workflows/rustapi.yml b/.github/workflows/rustapi.yml index ed69c5a83..3b3ac52ac 100644 --- a/.github/workflows/rustapi.yml +++ b/.github/workflows/rustapi.yml @@ -2,7 +2,6 @@ name: Rust API on: - push: workflow_dispatch: jobs: @@ -13,18 +12,15 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.11" - name: Prepare virtual environment run: | pip install . - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable + - uses: dtolnay/rust-toolchain@stable - name: Check and lint working-directory: crate run: | diff --git a/doc/source/getting-started/experiment.rst b/doc/source/getting-started/experiment.rst index 4991c49f4..cd4c4d244 100644 --- a/doc/source/getting-started/experiment.rst +++ b/doc/source/getting-started/experiment.rst @@ -21,7 +21,7 @@ file with additional calibration parameters. More information about defining platforms is provided in :doc:`../tutorials/lab` and several examples can be found at `TII dedicated repository `_. For a first experiment, let's define a single qubit platform at the path previously specified. -For simplicity, the qubit will be controlled by a RFSoC-based system, althought minimal changes are needed to use other devices. +In this example, the qubit is controlled by a Zurich Instruments' SHFQC instrument, although minimal changes are needed to use other devices. .. testcode:: python @@ -29,41 +29,79 @@ For simplicity, the qubit will be controlled by a RFSoC-based system, althought import pathlib - from qibolab.channels import Channel, ChannelMap - from qibolab.instruments.rfsoc import RFSoC - from qibolab.instruments.rohde_schwarz import SGS100A as LocalOscillator + from laboneq.simple import DeviceSetup, SHFQC + from qibolab.components import ( + AcquireChannel, + IqChannel, + IqConfig, + AcquisitionConfig, + OscillatorConfig, + ) + from qibolab.instruments.zhinst import ZiChannel, Zurich from qibolab.platform import Platform - from qibolab.serialize import load_qubits, load_runcard, load_settings + from qibolab.serialize import ( + load_instrument_settings, + load_qubits, + load_runcard, + load_settings, + ) NAME = "my_platform" # name of the platform - ADDRESS = "192.168.0.1" # ip address of the controller - PORT = 6000 # port of the controller + ADDRESS = "localhost" # ip address of the ZI data server + PORT = 8004 # port of the ZI data server # folder containing runcard with calibration parameters FOLDER = pathlib.Path.cwd() def create(): - # Instantiate controller instruments - controller = RFSoC(NAME, ADDRESS, PORT) + # Define available instruments + device_setup = DeviceSetup() + device_setup.add_dataserver(host=ADDRESS, port=PORT) + device_setup.add_instruments(SHFQC("device_shfqc", address="DEV12146")) - # Create channel objects and port assignment - channels = ChannelMap() - channels |= Channel("readout", port=controller[1]) - channels |= Channel("feedback", port=controller[0]) - channels |= Channel("drive", port=controller[0]) - - # create qubit objects + # Load and parse the runcard (i.e. parameters.json) runcard = load_runcard(FOLDER) - qubits, pairs = load_qubits(runcard) - # assign channels to qubits - qubits[0].readout = channels["L3-22_ro"] - qubits[0].feedback = channels["L1-2-RO"] - qubits[0].drive = channels["L3-22_qd"] + qubits, _, pairs = load_qubits(runcard) + qubit = qubits[0] - instruments = {controller.name: controller} + # define component names, and load their configurations + drive, probe, acquire = "q0/drive", "q0/probe", "q0/acquire" + drive_lo, readout_lo = "q0/drive/lo", "q0/readout/lo" + + # assign channels to qubits + qubit.drive = IqChannel(name=drive, lo=drive_lo, mixer=None) + qubit.probe = IqChannel(name=probe, lo=readout_lo, mixer=None, acquisition=acquire) + qubit.acquisition = AcquireChannel(name=acquire, probe=probe, twpa_pump=None) + + configs = {} + component_params = runcard["components"] + configs[drive] = IqConfig(**component_params[drive]) + configs[probe] = IqConfig(**component_params[probe]) + configs[acquire] = AcquisitionConfig(**component_params[acquire]) + configs[drive_lo] = OscillatorConfig(**component_params[drive_lo]) + configs[readout_lo] = OscillatorConfig(**component_params[readout_lo]) + + zi_channels = [ + ZiChannel(qubit.drive, device="device_shfqc", path="SGCHANNELS/0/OUTPUT"), + ZiChannel(qubit.probe, device="device_shfqc", path="QACHANNELS/0/OUTPUT"), + ZiChannel(qubit.acquisition, device="device_shfqc", path="QACHANNELS/0/INPUT"), + ] + + controller = Zurich(NAME, device_setup=device_setup, channels=zi_channels) + + instruments = load_instrument_settings(runcard, {controller.name: controller}) settings = load_settings(runcard) - return Platform(NAME, qubits, pairs, instruments, settings, resonator_type="3D") + return Platform( + NAME, + qubits, + pairs, + configs, + instruments, + settings, + resonator_type="3D", + ) + .. note:: @@ -93,22 +131,51 @@ And the we can define the runcard ``my_platform/parameters.json``: "relaxation_time": 70000, "sampling_rate": 9830400000 }, + "components": { + "qubit_0/drive": { + "frequency": 4833726197, + "power_range": 5 + }, + "qubit_0/drive/lo": { + "frequency": 5200000000, + "power": null + }, + "qubit_0/probe": { + "frequency": 7320000000, + "power_range": 1 + }, + "qubit_0/readout/lo": { + "frequency": 7300000000, + "power": null + }, + "qubit_0/acquire": { + "delay": 0, + "smearing": 0, + "power_range": 10 + } + } "native_gates": { "single_qubit": { "0": { "RX": { - "duration": 40, - "amplitude": 0.5, - "frequency": 5500000000, - "shape": "Gaussian(3)", - "type": "qd", + "qubit_0/drive": [ + { + "duration": 40, + "amplitude": 0.5, + "envelope": { "kind": "gaussian", "rel_sigma": 3.0 }, + "type": "qd" + } + ] }, "MZ": { - "duration": 2000, - "amplitude": 0.02, - "frequency": 7370000000, - "shape": "Rectangular()", - "type": "ro", + "qubit_0/probe": [ + { + "duration": 2000, + "amplitude": 0.02, + "envelope": { "kind": "rectangular" }, + "type": "ro" + } + ] } } }, @@ -117,8 +184,6 @@ And the we can define the runcard ``my_platform/parameters.json``: "characterization": { "single_qubit": { "0": { - "readout_frequency": 7370000000, - "drive_frequency": 5500000000, "anharmonicity": 0, "Ec": 0, "Ej": 0, @@ -187,16 +252,15 @@ We leave to the dedicated tutorial a full explanation of the experiment, but her # load the platform from ``dummy.py`` and ``dummy.json`` platform = create_platform("dummy") + qubit = platform.qubits[0] # define the pulse sequence - sequence = PulseSequence() - ro_pulse = platform.create_MZ_pulse(qubit=0) - sequence.append(ro_pulse) + sequence = qubit.native_gates.MZ.create_sequence() # define a sweeper for a frequency scan sweeper = Sweeper( parameter=Parameter.frequency, values=np.arange(-2e8, +2e8, 1e6), - pulses=[ro_pulse], + channels=[qubit.probe.name], type=SweeperType.OFFSET, ) @@ -209,10 +273,11 @@ We leave to the dedicated tutorial a full explanation of the experiment, but her ) results = platform.execute([sequence], options, [[sweeper]]) + probe_pulse = next(iter(sequence.probe_pulses)) # plot the results - amplitudes = results[ro_pulse.id][0].magnitude - frequencies = np.arange(-2e8, +2e8, 1e6) + ro_pulse.frequency + amplitudes = results[probe_pulse.id][0].magnitude + frequencies = np.arange(-2e8, +2e8, 1e6) + platform.config(qubit.probe.name).frequency plt.title("Resonator Spectroscopy") plt.xlabel("Frequencies [Hz]") diff --git a/doc/source/main-documentation/qibolab.rst b/doc/source/main-documentation/qibolab.rst index 4794c7e59..48df33165 100644 --- a/doc/source/main-documentation/qibolab.rst +++ b/doc/source/main-documentation/qibolab.rst @@ -16,7 +16,6 @@ In the platform, the main methods can be divided in different sections: - functions save and change qubit parameters (``dump``, ``update``) - functions to coordinate the instruments (``connect``, ``setup``, ``disconnect``) - a unique interface to execute experiments (``execute``) -- functions to initialize gates (``create_RX90_pulse``, ``create_RX_pulse``, ``create_CZ_pulse``, ``create_MZ_pulse``, ``create_qubit_drive_pulse``, ``create_qubit_readout_pulse``, ``create_RX90_drag_pulse``, ``create_RX_drag_pulse``) - setters and getters of channel/qubit parameters (local oscillator parameters, attenuations, gain and biases) The idea of the ``Platform`` is to serve as the only object exposed to the user, so that we can deploy experiments, without any need of going into the low-level instrument-specific code. @@ -35,39 +34,44 @@ Now we connect to the instruments (note that we, the user, do not need to know w platform.connect() -We can easily print some of the parameters of the channels (similarly we can set those, if needed): +We can easily access the names of channels and other components, and based on the name retrieve the corresponding configuration. As an example let's print some things: .. note:: - If the get_method does not apply to the platform (for example there is no local oscillator, to TWPA or no flux tunability...) a ``NotImplementedError`` will be raised. + If requested component does not exist in a particular platform, its name will be `None`, so watch out for such names, and make sure what you need exists before requesting its configuration. .. testcode:: python - print(f"Drive LO frequency: {platform.qubits[0].drive.lo_frequency}") - print(f"Readout LO frequency: {platform.qubits[0].readout.lo_frequency}") - print(f"TWPA LO frequency: {platform.qubits[0].twpa.lo_frequency}") - print(f"Qubit bias: {platform.qubits[0].flux.offset}") - print(f"Qubit attenuation: {platform.qubits[0].readout.attenuation}") + drive_channel = platform.qubits[0].drive + print(f"Drive channel name: {drive_channel.name}") + print(f"Drive frequency: {platform.config(drive_channel.name).frequency}") + + drive_lo = drive_channel.lo + if drive_lo is None: + print(f"Drive channel {drive_channel.name} does not use an LO.") + else: + print(f"Name of LO for channel {drive_channel.name} is {drive_lo}") + print(f"LO frequency: {platform.config(drive_lo).frequency}") .. testoutput:: python :hide: - Drive LO frequency: 0 - Readout LO frequency: 0 - TWPA LO frequency: 1000000000.0 - Qubit bias: 0.0 - Qubit attenuation: 0 + Drive channel name: qubit_0/drive + Drive frequency: 4000000000 + Drive channel qubit_0/drive does not use an LO. Now we can create a simple sequence (again, without explicitly giving any qubit specific parameter, as these are loaded automatically from the platform, as defined in the runcard): .. testcode:: python from qibolab.pulses import PulseSequence, Delay + import numpy as np ps = PulseSequence() - ps.append(platform.create_RX_pulse(qubit=0)) - ps.append(platform.create_RX_pulse(qubit=0)) - ps.append(Delay(duration=200, channel=platform.qubits[0].readout.name)) - ps.append(platform.create_MZ_pulse(qubit=0)) + qubit = platform.qubits[0] + ps.extend(qubit.native_gates.RX.create_sequence()) + ps.extend(qubit.native_gates.RX.create_sequence(phi=np.pi / 2)) + ps[qubit.probe.name].append(Delay(duration=200)) + ps.extend(qubit.native_gates.MZ.create_sequence()) Now we can execute the sequence on hardware: @@ -163,9 +167,8 @@ It encapsulates three fundamental elements crucial to qubit control and operatio Channels play a pivotal role in connecting the quantum system to the control infrastructure. They are optional and encompass distinct types, each serving a specific purpose: -- readout (from controller device to the qubits) -- feedback (from qubits to controller) -- twpa (pump to the TWPA) +- measure (from controller device to the qubits) +- acquisition (from qubits to controller) - drive - flux @@ -197,9 +200,6 @@ and usually extracted from the runcard during platform initialization. Channels -------- -In Qibolab, channels serve as abstractions for physical wires within a laboratory setup. -Each :class:`qibolab.channels.Channel` object corresponds to a specific type of connection, simplifying the process of controlling quantum pulses across the experimental setup. - Various types of channels are typically present in a quantum laboratory setup, including: - the drive line @@ -219,55 +219,6 @@ Although logically distinct from the qubit, the LO's frequency must align with t Qibolab accommodates this by enabling the assignment of a :class:`qibolab.instruments.oscillator.LocalOscillator` object to the relevant channel. The controller's driver ensures the correct pulse frequency is set based on the LO's configuration. -Let's explore an example using an RFSoC controller. -Note that while channels are defined in a device-independent manner, the port parameter varies based on the specific instrument. - -.. testcode:: python - - from qibolab.channels import Channel, ChannelMap - from qibolab.instruments.rfsoc import RFSoC - - controller = RFSoC(name="dummy", address="192.168.0.10", port="6000") - channel1 = Channel("my_channel_name_1", port=controller.ports(1)) - channel2 = Channel("my_channel_name_2", port=controller.ports(2)) - channel3 = Channel("my_channel_name_3", port=controller.ports(3)) - -Channels are then organized in :class:`qibolab.channels.ChannelMap` to be passed as a single argument to the platform. -Following the tutorial in :doc:`/tutorials/lab`, we can continue the initialization: - -.. testcode:: python - - from pathlib import Path - from qibolab.serialize import load_qubits, load_runcard - - path = Path.cwd().parent / "src" / "qibolab" / "dummy" - - ch_map = ChannelMap() - ch_map |= channel1 - ch_map |= channel2 - ch_map |= channel3 - - runcard = load_runcard(path) - qubits, couplers, pairs = load_qubits(runcard) - - qubits[0].drive = channel1 - qubits[0].readout = channel2 - qubits[0].feedback = channel3 - -Where, in the last lines, we assign the channels to the qubits. - -To assign local oscillators, the procedure is simple: - -.. testcode:: python - - from qibolab.instruments.erasynth import ERA as LocalOscillator - - LO_ADDRESS = "192.168.0.10" - local_oscillator = LocalOscillator("NameLO", LO_ADDRESS) - local_oscillator.frequency = 6e9 # Hz - local_oscillator.power = 5 # dB - channel2.local_oscillator = local_oscillator - .. _main_doc_pulses: Pulses @@ -336,54 +287,39 @@ To organize pulses into sequences, Qibolab provides the :class:`qibolab.pulses.P pulse1 = Pulse( duration=40, # timing, in all qibolab, is expressed in ns amplitude=0.5, # this amplitude is relative to the range of the instrument - frequency=1e8, # frequency are in Hz relative_phase=0, # phases are in radians envelope=Rectangular(), - channel="channel", - qubit=0, ) pulse2 = Pulse( duration=40, # timing, in all qibolab, is expressed in ns amplitude=0.5, # this amplitude is relative to the range of the instrument - frequency=1e8, # frequency are in Hz relative_phase=0, # phases are in radians envelope=Rectangular(), - channel="channel", - qubit=0, ) pulse3 = Pulse( duration=40, # timing, in all qibolab, is expressed in ns amplitude=0.5, # this amplitude is relative to the range of the instrument - frequency=1e8, # frequency are in Hz relative_phase=0, # phases are in radians envelope=Rectangular(), - channel="channel", - qubit=0, ) pulse4 = Pulse( duration=40, # timing, in all qibolab, is expressed in ns amplitude=0.5, # this amplitude is relative to the range of the instrument - frequency=1e8, # frequency are in Hz relative_phase=0, # phases are in radians envelope=Rectangular(), - channel="channel", - qubit=0, ) - sequence.append(pulse1) - sequence.append(pulse2) - sequence.append(pulse3) - sequence.append(pulse4) + sequence["channel"].append(pulse1) + sequence["channel"].append(pulse2) + sequence["channel"].append(pulse3) + sequence["channel"].append(pulse4) print(f"Total duration: {sequence.duration}") - sequence_ch1 = sequence.get_channel_pulses("channel1") # Selecting pulses on channel 1 - print(f"We have {len(sequence_ch1)} pulses on channel 1.") .. testoutput:: python :hide: Total duration: 160.0 - We have 0 pulses on channel 1. When conducting experiments on quantum hardware, pulse sequences are vital. Assuming you have already initialized a platform, executing an experiment is as simple as: @@ -402,18 +338,16 @@ Typical experiments may include both pre-defined pulses and new ones: from qibolab.pulses import Rectangular sequence = PulseSequence() - sequence.append(platform.create_RX_pulse(0)) - sequence.append( + sequence.extend(platform.qubits[0].native_gates.RX.create_sequence()) + sequence["some_channel"].append( Pulse( duration=10, amplitude=0.5, - frequency=2500000000, relative_phase=0, envelope=Rectangular(), - channel="0", ) ) - sequence.append(platform.create_MZ_pulse(0)) + sequence.extend(platform.qubits[0].native_gates.MZ.create_sequence()) results = platform.execute([sequence], options=options) @@ -444,7 +378,6 @@ To address the inefficiency, Qibolab introduces the concept of Sweeper objects. Sweeper objects in Qibolab are characterized by a :class:`qibolab.sweeper.Parameter`. This parameter, crucial to the sweeping process, can be one of several types: -- Frequency - Amplitude - Duration - Relative_phase @@ -452,17 +385,18 @@ Sweeper objects in Qibolab are characterized by a :class:`qibolab.sweeper.Parame -- +- Frequency - Attenuation - Gain - Bias -The first group includes parameters of the pulses, while the second group include parameters of a different type that, in qibolab, are linked to a qubit object. +The first group includes parameters of the pulses, while the second group includes parameters of channels. -To designate the qubit or pulse to which a sweeper is applied, you can utilize the ``pulses`` or ``qubits`` parameter within the Sweeper object. +To designate the pulse(s) or channel(s) to which a sweeper is applied, you can utilize the ``pulses`` or ``channels`` parameter within the Sweeper object. .. note:: - It is possible to simultaneously execute the same sweeper on different pulses or qubits. The ``pulses`` or ``qubits`` attribute is designed as a list, allowing for this flexibility. + It is possible to simultaneously execute the same sweeper on different pulses or channels. The ``pulses`` or ``channels`` attribute is designed as a list, allowing for this flexibility. To effectively specify the sweeping behavior, Qibolab provides the ``values`` attribute along with the ``type`` attribute. @@ -485,14 +419,20 @@ A tipical resonator spectroscopy experiment could be defined with: from qibolab.sweeper import Parameter, Sweeper, SweeperType sequence = PulseSequence() - sequence.append(platform.create_MZ_pulse(0)) # readout pulse for qubit 0 at 4 GHz - sequence.append(platform.create_MZ_pulse(1)) # readout pulse for qubit 1 at 5 GHz - sequence.append(platform.create_MZ_pulse(2)) # readout pulse for qubit 2 at 6 GHz + sequence.extend( + platform.qubits[0].native_gates.MZ.create_sequence() + ) # readout pulse for qubit 0 at 4 GHz + sequence.extend( + platform.qubits[1].native_gates.MZ.create_sequence() + ) # readout pulse for qubit 1 at 5 GHz + sequence.extend( + platform.qubits[2].native_gates.MZ.create_sequence() + ) # readout pulse for qubit 2 at 6 GHz sweeper = Sweeper( parameter=Parameter.frequency, values=np.arange(-200_000, +200_000, 1), # define an interval of swept values - pulses=[sequence[0], sequence[1], sequence[2]], + channels=[qubit.probe.name for qubit in platform.qubits.values()], type=SweeperType.OFFSET, ) @@ -522,24 +462,22 @@ For example: from qibolab.pulses import PulseSequence, Delay + qubit = platform.qubits[0] sequence = PulseSequence() - - sequence.append(platform.create_RX_pulse(0)) - sequence.append( - Delay(duration=sequence.duration, channel=platform.qubits[0].readout.name) - ) - sequence.append(platform.create_MZ_pulse(0)) + sequence.extend(qubit.native_gates.RX.create_sequence()) + sequence[qubit.probe.name].append(Delay(duration=sequence.duration)) + sequence.extend(qubit.native_gates.MZ.create_sequence()) sweeper_freq = Sweeper( parameter=Parameter.frequency, values=np.arange(-100_000, +100_000, 10_000), - pulses=[sequence[0]], + channels=[qubit.drive.name], type=SweeperType.OFFSET, ) sweeper_amp = Sweeper( parameter=Parameter.amplitude, values=np.arange(0, 1.5, 0.1), - pulses=[sequence[0]], + pulses=[sequence[qubit.drive.name][0]], type=SweeperType.FACTOR, ) @@ -620,12 +558,12 @@ Let's now delve into a typical use case for result objects within the qibolab fr .. testcode:: python - drive_pulse_1 = platform.create_RX_pulse(0) - measurement_pulse = platform.create_MZ_pulse(0) + qubit = platform.qubits[0] sequence = PulseSequence() - sequence.append(drive_pulse_1) - sequence.append(measurement_pulse) + sequence.extend(qubit.native_gates.RX.create_sequence()) + sequence[qubit.probe.name].append(Delay(duration=sequence.duration)) + sequence.extend(qubit.native_gates.MZ.create_sequence()) options = ExecutionParameters( nshots=1000, @@ -651,13 +589,13 @@ The shape of the values of an integreted acquisition with 2 sweepers will be: sweeper1 = Sweeper( parameter=Parameter.frequency, values=np.arange(-100_000, +100_000, 1), # define an interval of swept values - pulses=[sequence[0]], + channels=[qubit.drive.name], type=SweeperType.OFFSET, ) sweeper2 = Sweeper( parameter=Parameter.frequency, values=np.arange(-200_000, +200_000, 1), # define an interval of swept values - pulses=[sequence[0]], + channels=[qubit.probe.name], type=SweeperType.OFFSET, ) shape = (options.nshots, len(sweeper1.values), len(sweeper2.values)) diff --git a/doc/source/tutorials/calibration.rst b/doc/source/tutorials/calibration.rst index 375a52141..6d699ed78 100644 --- a/doc/source/tutorials/calibration.rst +++ b/doc/source/tutorials/calibration.rst @@ -41,16 +41,14 @@ around the pre-defined frequency. # allocate platform platform = create_platform("dummy") - # create pulse sequence and add pulse - sequence = PulseSequence() - readout_pulse = platform.create_MZ_pulse(qubit=0) - sequence.append(readout_pulse) + qubit = platform.qubits[0] + sequence = qubit.native_gates.MZ.create_sequence() # allocate frequency sweeper sweeper = Sweeper( parameter=Parameter.frequency, values=np.arange(-2e8, +2e8, 1e6), - pulses=[readout_pulse], + channels=[qubit.probe.name], type=SweeperType.OFFSET, ) @@ -73,8 +71,9 @@ In few seconds, the experiment will be finished and we can proceed to plot it. import matplotlib.pyplot as plt - amplitudes = results[readout_pulse.id][0].magnitude - frequencies = np.arange(-2e8, +2e8, 1e6) + readout_pulse.frequency + probe_pulse = next(iter(sequence.probe_pulses)) + amplitudes = results[probe_pulse.id][0].magnitude + frequencies = np.arange(-2e8, +2e8, 1e6) + platform.config(qubit.probe.name).frequency plt.title("Resonator Spectroscopy") plt.xlabel("Frequencies [Hz]") @@ -110,7 +109,7 @@ complex pulse sequence. Therefore with start with that: import numpy as np import matplotlib.pyplot as plt from qibolab import create_platform - from qibolab.pulses import PulseSequence, Delay + from qibolab.pulses import Pulse, PulseSequence, Delay, Gaussian from qibolab.sweeper import Sweeper, SweeperType, Parameter from qibolab.execution_parameters import ( ExecutionParameters, @@ -122,20 +121,21 @@ complex pulse sequence. Therefore with start with that: # allocate platform platform = create_platform("dummy") + qubit = platform.qubits[0] + # create pulse sequence and add pulses sequence = PulseSequence() - drive_pulse = platform.create_RX_pulse(qubit=0) - drive_pulse = replace(drive_pulse, duration=2000, amplitude=0.01) - readout_pulse = platform.create_MZ_pulse(qubit=0) - sequence.append(drive_pulse) - sequence.append(Delay(duration=drive_pulse.duration, channel=readout_pulse.channel)) - sequence.append(readout_pulse) + sequence[qubit.drive.name].append( + Pulse(duration=2000, amplitude=0.01, envelope=Gaussian(rel_sigma=5)) + ) + sequence[qubit.probe.name].append(Delay(duration=sequence.duration)) + sequence.extend(qubit.native_gates.MZ.create_sequence()) # allocate frequency sweeper sweeper = Sweeper( parameter=Parameter.frequency, values=np.arange(-2e8, +2e8, 1e6), - pulses=[drive_pulse], + channels=[qubit.drive.name], type=SweeperType.OFFSET, ) @@ -155,8 +155,9 @@ We can now proceed to launch on hardware: results = platform.execute([sequence], options, [[sweeper]]) - amplitudes = results[readout_pulse.id][0].magnitude - frequencies = np.arange(-2e8, +2e8, 1e6) + drive_pulse.frequency + probe_pulse = next(iter(sequence.probe_pulses)) + amplitudes = results[probe_pulse.id][0].magnitude + frequencies = np.arange(-2e8, +2e8, 1e6) + platform.config(qubit.drive.name).frequency plt.title("Resonator Spectroscopy") plt.xlabel("Frequencies [Hz]") @@ -217,20 +218,16 @@ and its impact on qubit states in the IQ plane. # allocate platform platform = create_platform("dummy") + qubit = platform.qubits[0] + # create pulse sequence 1 and add pulses one_sequence = PulseSequence() - drive_pulse = platform.create_RX_pulse(qubit=0) - readout_pulse1 = platform.create_MZ_pulse(qubit=0) - one_sequence.append(drive_pulse) - one_sequence.append( - Delay(duration=drive_pulse.duration, channel=readout_pulse1.channel) - ) - one_sequence.append(readout_pulse1) + one_sequence.extend(qubit.native_gates.RX.create_sequence()) + one_sequence[qubit.probe.name].append(Delay(duration=one_sequence.duration)) + one_sequence.extend(qubit.native_gates.MZ.create_sequence()) # create pulse sequence 2 and add pulses - zero_sequence = PulseSequence() - readout_pulse2 = platform.create_MZ_pulse(qubit=0) - zero_sequence.append(readout_pulse2) + zero_sequence = qubit.native_gates.MZ.create_sequence() options = ExecutionParameters( nshots=1000, @@ -242,17 +239,20 @@ and its impact on qubit states in the IQ plane. results_one = platform.execute([one_sequence], options) results_zero = platform.execute([zero_sequence], options) + probe_pulse1 = next(iter(one_sequence.probe_pulses)) + probe_pulse2 = next(iter(zero_sequence.probe_pulses)) + plt.title("Single shot classification") plt.xlabel("I [a.u.]") plt.ylabel("Q [a.u.]") plt.scatter( - results_one[readout_pulse1.id][0].voltage_i, - results_one[readout_pulse1.id][0].voltage_q, + results_one[probe_pulse1.id][0].voltage_i, + results_one[probe_pulse1.id][0].voltage_q, label="One state", ) plt.scatter( - results_zero[readout_pulse2.id][0].voltage_i, - results_zero[readout_pulse2.id][0].voltage_q, + results_zero[probe_pulse2.id][0].voltage_i, + results_zero[probe_pulse2.id][0].voltage_q, label="Zero state", ) plt.show() diff --git a/doc/source/tutorials/compiler.rst b/doc/source/tutorials/compiler.rst index 1c7cdb134..dd335fe4c 100644 --- a/doc/source/tutorials/compiler.rst +++ b/doc/source/tutorials/compiler.rst @@ -82,7 +82,7 @@ The following example shows how to modify the compiler in order to execute a cir # define a compiler rule that translates X to the pi-pulse def x_rule(gate, qubit): """X gate applied with a single pi-pulse.""" - return PulseSequence([qubit.native_gates.RX]) + return qubit.native_gates.RX.create_sequence() # the empty dictionary is needed because the X gate does not require any virtual Z-phases diff --git a/doc/source/tutorials/instrument.rst b/doc/source/tutorials/instrument.rst index db30ce641..a0bc645ff 100644 --- a/doc/source/tutorials/instrument.rst +++ b/doc/source/tutorials/instrument.rst @@ -178,30 +178,3 @@ Let's see a minimal example: ) -> dict[str, Union[IntegratedResults, SampleResults]]: """This method is used for sequence unrolling sweeps. Here not implemented.""" raise NotImplementedError - -As we saw in :doc:`lab`, to instantiate a platform at some point you have to -write something like this: - -.. testcode:: python - - from qibolab.channels import Channel, ChannelMap - from qibolab.instruments.dummy import DummyInstrument - - instrument = DummyInstrument("my_instrument", "0.0.0.0:0") - channels = ChannelMap() - channels |= Channel("ch1out", port=instrument.ports("o1")) - - -The interesting part of this section is the ``port`` parameter that works as an -attribute of the controller. A :class:`qibolab.instruments.port.Port` object -describes the physical connections that a device may have. A Controller has, by -default, ports characterized just by ``port_name`` (see also -:class:`qibolab.instruments.abstract.Controller`), but different devices may -need to add attributes and methods to the ports. This can be done by defining in -the new controller a new port type. See, for example, the already implemented -ports: - -* :class:`qibolab.instruments.rfsoc.driver.RFSoCPort` -* :class:`qibolab.instruments.qm.ports.QMPort` -* :class:`qibolab.instruments.zhinst.ZhPort` -* :class:`qibolab.instruments.qblox.port` diff --git a/doc/source/tutorials/lab.rst b/doc/source/tutorials/lab.rst index 7ca2ca210..6bfd1dd23 100644 --- a/doc/source/tutorials/lab.rst +++ b/doc/source/tutorials/lab.rst @@ -23,10 +23,10 @@ using different Qibolab primitives. .. testcode:: python from qibolab import Platform + from qibolab.components import IqChannel, AcquireChannel, IqConfig from qibolab.qubits import Qubit from qibolab.pulses import Gaussian, Pulse, PulseType, Rectangular - from qibolab.channels import ChannelMap, Channel - from qibolab.native import SingleQubitNatives + from qibolab.native import RxyFactory, FixedSequenceFactory, SingleQubitNatives from qibolab.instruments.dummy import DummyInstrument @@ -34,39 +34,46 @@ using different Qibolab primitives. # Create a controller instrument instrument = DummyInstrument("my_instrument", "0.0.0.0:0") - # Create channel objects and assign to them the controller ports - channels = ChannelMap() - channels |= Channel("ch1out", port=instrument["o1"]) - channels |= Channel("ch2", port=instrument["o2"]) - channels |= Channel("ch1in", port=instrument["i1"]) - # create the qubit object qubit = Qubit(0) - # assign native gates to the qubit - qubit.native_gates = SingleQubitNatives( - RX=Pulse( + # assign channels to the qubit + qubit.probe = IqChannel(name="probe", mixer=None, lo=None, acquisition="acquire") + qubit.acquire = AcquireChannel(name="acquire", twpa_pump=None, probe="probe") + qubit.drive = Iqchannel(name="drive", mixer=None, lo=None) + + # define configuration for channels + configs = {} + configs[qubit.drive.name] = IqConfig(frequency=3e9) + configs[qubit.probe.name] = IqConfig(frequency=7e9) + + # create sequence that drives qubit from state 0 to 1 + drive_seq = PulseSequence() + drive_seq[qubit.drive.name].append( + Pulse( duration=40, amplitude=0.05, envelope=Gaussian(rel_sigma=0.2), type=PulseType.DRIVE, - qubit=qubit.name, - frequency=4.5e9, - ), - MZ=Pulse( + ) + ) + + # create sequence that can be used for measuring the qubit + probe_seq = PulseSequence() + probe_seq[qubit.probe.name].append( + Pulse( duration=1000, amplitude=0.005, envelope=Rectangular(), type=PulseType.READOUT, - qubit=qubit.name, - frequency=7e9, - ), + ) ) - # assign channels to the qubit - qubit.readout = channels["ch1out"] - qubit.feedback = channels["ch1in"] - qubit.drive = channels["ch2"] + # assign native gates to the qubit + qubit.native_gates = SingleQubitNatives( + RX=RxyFactory(drive_seq), + MZ=FixedSequenceFactory(probe_seq), + ) # create dictionaries of the different objects qubits = {qubit.name: qubit} @@ -74,7 +81,9 @@ using different Qibolab primitives. instruments = {instrument.name: instrument} # allocate and return Platform object - return Platform("my_platform", qubits, pairs, instruments, resonator_type="3D") + return Platform( + "my_platform", qubits, pairs, configs, instruments, resonator_type="3D" + ) This code creates a platform with a single qubit that is controlled by the @@ -96,9 +105,12 @@ hold the parameters of the two-qubit gates. .. testcode:: python + from qibolab.components import IqChannel, AcquireChannel, DcChannel, IqConfig from qibolab.qubits import Qubit, QubitPair from qibolab.pulses import Gaussian, PulseType, Pulse, PulseSequence, Rectangular from qibolab.native import ( + RxyFactory, + FixedSequenceFactory, SingleQubitNatives, TwoQubitNatives, ) @@ -107,58 +119,93 @@ hold the parameters of the two-qubit gates. qubit0 = Qubit(0) qubit1 = Qubit(1) + # assign channels to the qubits + qubit0.probe = IqChannel(name="probe_0", mixer=None, lo=None, acquisition="acquire_0") + qubit0.acquire = AcquireChannel(name="acquire_0", twpa_pump=None, probe="probe_0") + qubit0.drive = IqChannel(name="drive_0", mixer=None, lo=None) + qubit0.flux = DcChannel(name="flux_0") + qubit1.probe = IqChannel(name="probe_1", mixer=None, lo=None, acquisition="acquire_1") + qubit1.acquire = AcquireChannel(name="acquire_1", twpa_pump=None, probe="probe_1") + qubit1.drive = IqChannel(name="drive_1", mixer=None, lo=None) + # assign single-qubit native gates to each qubit qubit0.native_gates = SingleQubitNatives( - RX=Pulse( - duration=40, - amplitude=0.05, - envelope=Gaussian(rel_sigma=0.2), - type=PulseType.DRIVE, - qubit=qubit0.name, - frequency=4.7e9, + RX=RxyFactory( + PulseSequence( + { + qubit0.drive.name: [ + Pulse( + duration=40, + amplitude=0.05, + envelope=Gaussian(rel_sigma=0.2), + type=PulseType.DRIVE, + ) + ] + } + ) ), - MZ=Pulse( - duration=1000, - amplitude=0.005, - envelope=Rectangular(), - type=PulseType.READOUT, - qubit=qubit0.name, - frequency=7e9, + MZ=FixedSequenceFactory( + PulseSequence( + { + qubit0.probe.name: [ + Pulse( + duration=1000, + amplitude=0.005, + envelope=Rectangular(), + type=PulseType.READOUT, + ) + ] + } + ) ), ) qubit1.native_gates = SingleQubitNatives( - RX=Pulse( - duration=40, - amplitude=0.05, - envelope=Gaussian(rel_sigma=0.2), - type=PulseType.DRIVE, - qubit=qubit1.name, - frequency=5.1e9, + RX=RxyFactory( + PulseSequence( + { + qubit1.drive.name: [ + Pulse( + duration=40, + amplitude=0.05, + envelope=Gaussian(rel_sigma=0.2), + type=PulseType.DRIVE, + ) + ] + } + ) ), - MZ=Pulse( - duration=1000, - amplitude=0.005, - envelope=Rectangular(), - type=PulseType.READOUT, - qubit=qubit1.name, - frequency=7.5e9, + MZ=FixedSequenceFactory( + PulseSequence( + { + qubit1.probe.name: [ + Pulse( + duration=1000, + amplitude=0.005, + envelope=Rectangular(), + type=PulseType.READOUT, + ) + ] + } + ) ), ) # define the pair of qubits pair = QubitPair(qubit0, qubit1) pair.native_gates = TwoQubitNatives( - CZ=PulseSequence( - [ - Pulse( - duration=30, - amplitude=0.005, - envelope=Rectangular(), - type=PulseType.FLUX, - qubit=qubit1.name, - frequency=1e9, - ) - ], + CZ=FixedSequenceFactory( + PulseSequence( + { + qubit0.flux.name: [ + Pulse( + duration=30, + amplitude=0.005, + envelope=Rectangular(), + type=PulseType.FLUX, + ) + ], + } + ) ) ) @@ -171,10 +218,12 @@ coupler but qibolab will take them into account when calling :class:`qibolab.nat .. testcode:: python + from qibolab.components import DcChannel from qibolab.couplers import Coupler from qibolab.qubits import Qubit, QubitPair from qibolab.pulses import PulseType, Pulse, PulseSequence from qibolab.native import ( + FixedSequenceFactory, SingleQubitNatives, TwoQubitNatives, ) @@ -184,23 +233,30 @@ coupler but qibolab will take them into account when calling :class:`qibolab.nat qubit1 = Qubit(1) coupler_01 = Coupler(0) + # assign channel(s) to the coupler + coupler_01.flux = DcChannel(name="flux_coupler_01") + # assign single-qubit native gates to each qubit # Look above example # define the pair of qubits pair = QubitPair(qubit0, qubit1, coupler_01) pair.native_gates = TwoQubitNatives( - CZ=PulseSequence( - [ - Pulse( - duration=30, - amplitude=0.005, - frequency=1e9, - envelope=Rectangular(), - type=PulseType.FLUX, - qubit=qubit1.name, - ) - ], + CZ=FixedSequenceFactory( + PulseSequence( + { + coupler_01.flux.name: [ + Pulse( + duration=30, + amplitude=0.005, + frequency=1e9, + envelope=Rectangular(), + type=PulseType.FLUX, + qubit=qubit1.name, + ) + ] + }, + ) ) ) @@ -264,47 +320,84 @@ a two-qubit system: 1 ] ], + "components": { + "drive_0": { + "frequency": 4855663000 + }, + "drive_1": { + "frequency": 5800563000 + }, + "flux_0": { + "bias": 0.0 + }, + "probe_0": { + "frequency": 7453265000 + }, + "probe_1": { + "frequency": 7655107000 + }, + "acquire_0": { + "delay": 0, + "smearing": 0 + }, + "acquire_1": { + "delay": 0, + "smearing": 0 + } + } "native_gates": { "single_qubit": { "0": { "RX": { - "duration": 40, - "amplitude": 0.0484, - "frequency": 4855663000, - "envelope": { - "kind": "drag", - "rel_sigma": 0.2, - "beta": -0.02, - }, - "type": "qd", - }, + "drive_0": [ + { + "duration": 40, + "amplitude": 0.0484, + "envelope": { + "kind": "drag", + "rel_sigma": 0.2, + "beta": -0.02, + }, + "type": "qd", + } + ] + }, "MZ": { - "duration": 620, - "amplitude": 0.003575, - "frequency": 7453265000, - "envelope": {"kind": "rectangular"}, - "type": "ro", - } + "probe_0": [ + { + "duration": 620, + "amplitude": 0.003575, + "envelope": {"kind": "rectangular"}, + "type": "ro", + } + ] + } }, "1": { "RX": { - "duration": 40, - "amplitude": 0.05682, - "frequency": 5800563000, - "envelope": { - "kind": "drag", - "rel_sigma": 0.2, - "beta": -0.04, - }, - "type": "qd", - }, + "drive_1" : [ + { + "duration": 40, + "amplitude": 0.05682, + "envelope": { + "kind": "drag", + "rel_sigma": 0.2, + "beta": -0.04, + }, + "type": "qd", + } + ] + }, "MZ": { - "duration": 960, - "amplitude": 0.00325, - "frequency": 7655107000, - "envelope": {"kind": "rectangular"}, - "type": "ro", - } + "probe_1": [ + { + "duration": 960, + "amplitude": 0.00325, + "envelope": {"kind": "rectangular"}, + "type": "ro", + } + ] + } } }, "two_qubit": { @@ -334,20 +427,14 @@ a two-qubit system: "characterization": { "single_qubit": { "0": { - "readout_frequency": 7453265000, - "drive_frequency": 4855663000, "T1": 0.0, "T2": 0.0, - "sweetspot": -0.047, "threshold": 0.00028502261712637096, "iq_angle": 1.283105298787488 }, "1": { - "readout_frequency": 7655107000, - "drive_frequency": 5800563000, "T1": 0.0, "T2": 0.0, - "sweetspot": -0.045, "threshold": 0.0002694329123116206, "iq_angle": 4.912447775569025 } @@ -374,44 +461,52 @@ we need the following changes to the previous runcard: 1 ] }, + "components": { + "flux_coupler_01": { + "bias": 0.12 + } + } "native_gates": { "two_qubit": { "0-1": { + "CZZ": { + "flux_coupler_01": [ + { + "type": "cf", + "duration": 40, + "amplitude": 0.1, + "envelope": {"kind": "rectangular"}, + "coupler": 0, + } + ] + "flux_0": [ + { + "duration": 30, + "amplitude": 0.6025, + "envelope": {"kind": "rectangular"}, + "type": "qf" + } + ], + "drive_0": [ + { + "type": "virtual_z", + "phase": -1, + "qubit": 0 + } + ], + "drive_1": [ + { + "type": "virtual_z", + "phase": -3, + "qubit": 1 + } + ] + } "CZ": [ - { - "duration": 30, - "amplitude": 0.6025, - "envelope": {"kind": "rectangular"}, - "qubit": 1, - "type": "qf" - }, - { - "type": "virtual_z", - "phase": -1, - "qubit": 0 - }, - { - "type": "virtual_z", - "phase": -3, - "qubit": 1 - }, - { - "type": "cf", - "duration": 40, - "amplitude": 0.1, - "envelope": {"kind": "rectangular"}, - "coupler": 0, - } + ] } } - }, - "characterization": { - "coupler": { - "0": { - "sweetspot": 0.0 - } - } } } @@ -439,8 +534,19 @@ the above runcard: from pathlib import Path from qibolab import Platform - from qibolab.channels import ChannelMap, Channel - from qibolab.serialize import load_runcard, load_qubits, load_settings + from qibolab.components import ( + AcquireChannel, + DcChannel, + IqChannel, + AcquisitionConfig, + DcConfig, + IqConfig, + ) + from qibolab.serialize import ( + load_runcard, + load_qubits, + load_settings, + ) from qibolab.instruments.dummy import DummyInstrument FOLDER = Path.cwd() @@ -451,32 +557,45 @@ the above runcard: # Create a controller instrument instrument = DummyInstrument("my_instrument", "0.0.0.0:0") - # Create channel objects and assign to them the controller ports - channels = ChannelMap() - channels |= Channel("ch1out", port=instrument["o1"]) - channels |= Channel("ch1in", port=instrument["i1"]) - channels |= Channel("ch2", port=instrument["o2"]) - channels |= Channel("ch3", port=instrument["o3"]) - channels |= Channel("chf1", port=instrument["o4"]) - channels |= Channel("chf2", port=instrument["o5"]) - # create ``Qubit`` and ``QubitPair`` objects by loading the runcard runcard = load_runcard(folder) - qubits, couplers, pairs = load_qubits(runcard) + qubits, _, pairs = load_qubits(runcard) - # assign channels to the qubit + # define channels and load component configs + configs = {} + component_params = runcard["components"] for q in range(2): - qubits[q].readout = channels["ch1out"] - qubits[q].feedback = channels["ch1in"] - qubits[q].drive = channels[f"ch{q + 2}"] - qubits[q].flux = channels[f"chf{q + 1}"] + drive_name = f"qubit_{q}/drive" + configs[drive_name] = IqConfig(**component_params[drive_name]) + qubits[q].drive = IqChannel(drive_name, mixer=None, lo=None) + + flux_name = f"qubit_{q}/flux" + configs[flux_name] = DcConfig(**component_params[flux_name]) + qubits[q].flux = DcChannel(flux_name) + + probe_name, acquire_name = f"qubit_{q}/probe", f"qubit_{q}/acquire" + configs[probe_name] = IqConfig(**component_params[probe_name]) + qubits[q].probe = IqChannel( + probe_name, mixer=None, lo=None, acquistion=acquire_name + ) + + configs[acquire_name] = AcquisitionConfig(**component_params[acquire_name]) + quibts[q].acquisition = AcquireChannel( + acquire_name, twpa_pump=None, probe=probe_name + ) # create dictionary of instruments instruments = {instrument.name: instrument} # load ``settings`` from the runcard settings = load_settings(runcard) return Platform( - "my_platform", qubits, pairs, instruments, settings, resonator_type="2D" + "my_platform", + qubits, + pairs, + configs, + instruments, + settings, + resonator_type="2D", ) With the following additions for coupler architectures: @@ -490,29 +609,36 @@ With the following additions for coupler architectures: # Create a controller instrument instrument = DummyInstrument("my_instrument", "0.0.0.0:0") - # Create channel objects and assign to them the controller ports - channels = ChannelMap() - channels |= Channel("ch1out", port=instrument["o1"]) - channels |= Channel("ch1in", port=instrument["i1"]) - channels |= Channel("ch2", port=instrument["o2"]) - channels |= Channel("ch3", port=instrument["o3"]) - channels |= Channel("chf1", port=instrument["o4"]) - channels |= Channel("chf2", port=instrument["o5"]) - channels |= Channel("chfc0", port=instrument["o6"]) - # create ``Qubit`` and ``QubitPair`` objects by loading the runcard - runcard = load_runcard(FOLDER) + runcard = load_runcard(folder) qubits, couplers, pairs = load_qubits(runcard) - # assign channels to the qubit + # define channels and load component configs + configs = {} + component_params = runcard["components"] for q in range(2): - qubits[q].readout = channels["ch1out"] - qubits[q].feedback = channels["ch1in"] - qubits[q].drive = channels[f"ch{q + 2}"] - qubits[q].flux = channels[f"chf{q + 1}"] + drive_name = f"qubit_{q}/drive" + configs[drive_name] = IqConfig(**component_params[drive_name]) + qubits[q].drive = IqChannel(drive_name, mixer=None, lo=None) + + flux_name = f"qubit_{q}/flux" + configs[flux_name] = DcConfig(**component_params[flux_name]) + qubits[q].flux = DcChannel(flux_name) + + probe_name, acquire_name = f"qubit_{q}/probe", f"qubit_{q}/acquire" + configs[probe_name] = IqConfig(**component_params[probe_name]) + qubits[q].probe = IqChannel( + probe_name, mixer=None, lo=None, acquistion=acquire_name + ) - # assign channels to the coupler - couplers[0].flux = channels["chfc0"] + configs[acquire_name] = AcquisitionConfig(**component_params[acquire_name]) + quibts[q].acquisition = AcquireChannel( + acquire_name, twpa_pump=None, probe=probe_name + ) + + coupler_flux_name = "coupler_0/flux" + configs[coupler_flux_name] = DcConfig(**component_params[coupler_flux_name]) + couplers[0].flux = DcChannel(coupler_flux_name) # create dictionary of instruments instruments = {instrument.name: instrument} @@ -522,6 +648,7 @@ With the following additions for coupler architectures: "my_platform", qubits, pairs, + configs, instruments, settings, resonator_type="2D", @@ -568,89 +695,20 @@ The runcard can contain an ``instruments`` section that provides these parameter } }, "native_gates": { - "single_qubit": { - "0": { - "RX": { - "duration": 40, - "amplitude": 0.0484, - "frequency": 4855663000, - "envelope": { - "kind": "drag", - "rel_sigma": 0.2, - "beta": -0.02, - }, - "type": "qd", - }, - "MZ": { - "duration": 620, - "amplitude": 0.003575, - "frequency": 7453265000, - "envelope": {"kind": "rectangular"}, - "type": "ro", - } - }, - "1": { - "RX": { - "duration": 40, - "amplitude": 0.05682, - "frequency": 5800563000, - "envelope": { - "kind": "drag", - "rel_sigma": 0.2, - "beta": -0.04, - }, - "type": "qd", - }, - "MZ": { - "duration": 960, - "amplitude": 0.00325, - "frequency": 7655107000, - "envelope": {"kind": "rectangular"}, - "type": "ro", - } - } - }, - "two_qubit": { - "0-1": { - "CZ": [ - { - "duration": 30, - "amplitude": 0.055, - "envelope": {"kind": "rectangular"}, - "qubit": 1, - "type": "qf" - }, - { - "type": "virtual_z", - "phase": -1.5707963267948966, - "qubit": 0 - }, - { - "type": "virtual_z", - "phase": -1.5707963267948966, - "qubit": 1 - } - ] - } - } + "single_qubit": {}, + "two_qubit": {} }, "characterization": { "single_qubit": { "0": { - "readout_frequency": 7453265000, - "drive_frequency": 4855663000, "T1": 0.0, "T2": 0.0, - "sweetspot": -0.047, "threshold": 0.00028502261712637096, "iq_angle": 1.283105298787488 }, "1": { - "readout_frequency": 7655107000, - "drive_frequency": 5800563000, "T1": 0.0, "T2": 0.0, - "sweetspot": -0.045, "threshold": 0.0002694329123116206, "iq_angle": 4.912447775569025 } @@ -669,47 +727,68 @@ in this case ``"twpa_pump"``. from pathlib import Path from qibolab import Platform - from qibolab.channels import ChannelMap, Channel + from qibolab.components import ( + AcquireChannel, + DcChannel, + IqChannel, + AcquisitionConfig, + DcConfig, + IqConfig, + ) from qibolab.serialize import ( load_runcard, load_qubits, load_settings, - load_instrument_settings, ) from qibolab.instruments.dummy import DummyInstrument - from qibolab.instruments.oscillator import LocalOscillator FOLDER = Path.cwd() + # assumes runcard is storred in the same folder as platform.py def create(): # Create a controller instrument instrument = DummyInstrument("my_instrument", "0.0.0.0:0") - twpa = LocalOscillator("twpa_pump", "0.0.0.1") - - # Create channel objects and assign to them the controller ports - channels = ChannelMap() - channels |= Channel("ch1out", port=instrument["o1"]) - channels |= Channel("ch2", port=instrument["o2"]) - channels |= Channel("ch3", port=instrument["o3"]) - channels |= Channel("ch1in", port=instrument["i1"]) # create ``Qubit`` and ``QubitPair`` objects by loading the runcard - runcard = load_runcard(FOLDER) - qubits, pairs = load_qubits(runcard) + runcard = load_runcard(folder) + qubits, _, pairs = load_qubits(runcard) - # assign channels to the qubit + # define channels and load component configs + configs = {} + component_params = runcard["components"] for q in range(2): - qubits[q].readout = channels["ch1out"] - qubits[q].feedback = channels["ch1in"] - qubits[q].drive = channels[f"ch{q + 2}"] + drive_name = f"qubit_{q}/drive" + configs[drive_name] = IqConfig(**component_params[drive_name]) + qubits[q].drive = IqChannel(drive_name, mixer=None, lo=None) + + flux_name = f"qubit_{q}/flux" + configs[flux_name] = DcConfig(**component_params[flux_name]) + qubits[q].flux = DcChannel(flux_name) + + probe_name, acquire_name = f"qubit_{q}/probe", f"qubit_{q}/acquire" + configs[probe_name] = IqConfig(**component_params[probe_name]) + qubits[q].probe = IqChannel( + probe_name, mixer=None, lo=None, acquistion=acquire_name + ) + + configs[acquire_name] = AcquisitionConfig(**component_params[acquire_name]) + quibts[q].acquisition = AcquireChannel( + acquire_name, twpa_pump=None, probe=probe_name + ) # create dictionary of instruments - instruments = {instrument.name: instrument, twpa.name: twpa} + instruments = {instrument.name: instrument} # load instrument settings from the runcard instruments = load_instrument_settings(runcard, instruments) # load ``settings`` from the runcard settings = load_settings(runcard) return Platform( - "my_platform", qubits, pairs, instruments, settings, resonator_type="2D" + "my_platform", + qubits, + pairs, + configs, + instruments, + settings, + resonator_type="2D", ) diff --git a/doc/source/tutorials/pulses.rst b/doc/source/tutorials/pulses.rst index 0b336addf..2f039d7d3 100644 --- a/doc/source/tutorials/pulses.rst +++ b/doc/source/tutorials/pulses.rst @@ -14,29 +14,23 @@ pulses (:class:`qibolab.pulses.Pulse`) through the sequence = PulseSequence() # Add some pulses to the pulse sequence - sequence.append( + sequence["channel_0"].append( Pulse( - frequency=200000000, amplitude=0.3, duration=60, relative_phase=0, envelope=Gaussian(rel_sigma=0.2), - qubit=0, type=PulseType.DRIVE, - channel="0", ) ) - sequence.append(Delay(duration=100, channel="1")) - sequence.append( + sequence["channel_1"].append(Delay(duration=100, channel="1")) + sequence["channel_1"].append( Pulse( - frequency=20000000.0, amplitude=0.5, duration=3000, relative_phase=0, envelope=Rectangular(), - qubit=0, type=PulseType.READOUT, - channel="1", ) ) diff --git a/src/qibolab/backends.py b/src/qibolab/backends.py index e3016e7b5..c26928b87 100644 --- a/src/qibolab/backends.py +++ b/src/qibolab/backends.py @@ -69,7 +69,7 @@ def assign_measurements(self, measurement_map, readout): containing the readout measurement shots. This is created in ``execute_circuit``. """ for gate, sequence in measurement_map.items(): - _samples = (readout[pulse.id].samples for pulse in sequence) + _samples = (readout[pulse.id].samples for pulse in sequence.probe_pulses) samples = list(filter(lambda x: x is not None, _samples)) gate.result.backend = self gate.result.register_samples(np.array(samples).T) @@ -159,7 +159,10 @@ def execute_circuits(self, circuits, initial_states=None, nshots=1000): MeasurementOutcomes(circuit.measurements, self, nshots=nshots) ) for gate, sequence in measurement_map.items(): - samples = [readout[pulse.id].popleft().samples for pulse in sequence] + samples = [ + readout[pulse.id].popleft().samples + for pulse in sequence.probe_pulses + ] gate.result.backend = self gate.result.register_samples(np.array(samples).T) return results diff --git a/src/qibolab/channels.py b/src/qibolab/channels.py deleted file mode 100644 index b4a916478..000000000 --- a/src/qibolab/channels.py +++ /dev/null @@ -1,168 +0,0 @@ -from dataclasses import dataclass, field -from typing import Dict, Optional - -from qibo.config import raise_error - -from qibolab.instruments.oscillator import LocalOscillator -from qibolab.instruments.port import Port - - -def check_max_offset(offset, max_offset): - """Checks if a given offset value exceeds the maximum supported offset. - - This is to avoid sending high currents that could damage lab - equipment such as amplifiers. - """ - if max_offset is not None and abs(offset) > max_offset: - raise_error( - ValueError, f"{offset} exceeds the maximum allowed offset {max_offset}." - ) - - -@dataclass -class Channel: - """Representation of physical wire connection (channel).""" - - name: str - """Name of the channel from the lab schematics.""" - port: Optional[Port] = None - """Instrument port that is connected to this channel.""" - local_oscillator: Optional[LocalOscillator] = None - """Instrument object controlling the local oscillator connected to this - channel. - - Not applicable for setups that do not use external local oscillators - because the controller can send sufficiently high frequencies or - contains internal local oscillators. - """ - max_offset: Optional[float] = None - """Maximum DC voltage that we can safely send through this channel. - - Sending high voltages for prolonged times may damage amplifiers or - other lab equipment. If the user attempts to send a higher value an - error will be raised to prevent execution in real instruments. - """ - - @property - def offset(self): - """DC offset that is applied to this port.""" - return self.port.offset - - @offset.setter - def offset(self, value): - check_max_offset(value, self.max_offset) - self.port.offset = value - - @property - def lo_frequency(self): - if self.local_oscillator is not None: - return self.local_oscillator.frequency - return self.port.lo_frequency - - @lo_frequency.setter - def lo_frequency(self, value): - if self.local_oscillator is not None: - self.local_oscillator.frequency = value - else: - self.port.lo_frequency = value - - @property - def lo_power(self): - if self.local_oscillator is not None: - return self.local_oscillator.power - return self.port.lo_power - - @lo_power.setter - def lo_power(self, value): - if self.local_oscillator is not None: - self.local_oscillator.power = value - else: - self.port.lo_power = value - - # TODO: gain, attenuation and power range can be unified to a single property - @property - def gain(self): - return self.port.gain - - @gain.setter - def gain(self, value): - self.port.gain = value - - @property - def attenuation(self): - return self.port.attenuation - - @attenuation.setter - def attenuation(self, value): - self.port.attenuation = value - - @property - def power_range(self): - return self.port.power_range - - @power_range.setter - def power_range(self, value): - self.port.power_range = value - - @property - def filter(self): - return self.port.filter - - @filter.setter - def filter(self, value): - self.port.filter = value - - -@dataclass -class ChannelMap: - """Collection of :class:`qibolab.designs.channel.Channel` objects - identified by name. - - Essentially, it allows creating a mapping of names to channels just - specifying the names. - """ - - _channels: Dict[str, Channel] = field(default_factory=dict) - - def add(self, *items): - """Add multiple items to the channel map. - - If :class: `qibolab.channels.Channel` objects are given they are dded to the channel map. - If a different type is given, a :class: `qibolab.channels.Channel` with the corresponding name - is created and added to the channel map. - """ - for item in items: - if isinstance(item, Channel): - self[item.name] = item - else: - self[item] = Channel(item) - return self - - def __getitem__(self, name): - return self._channels[name] - - def __setitem__(self, name, channel): - if not isinstance(channel, Channel): - raise_error( - TypeError, f"Cannot add channel of type {type(channel)} to ChannelMap." - ) - self._channels[name] = channel - - def __contains__(self, name): - return name in self._channels - - def __or__(self, channel_map): - channels = self._channels.copy() - channels.update(channel_map._channels) - return self.__class__(channels) - - def __ior__(self, items): - if not isinstance(items, type(self)): - try: - if isinstance(items, str): - raise TypeError - items = type(self)().add(*items) - except TypeError: - items = type(self)().add(items) - self._channels.update(items._channels) - return self diff --git a/src/qibolab/compilers/compiler.py b/src/qibolab/compilers/compiler.py index 52d220f3c..b6342c905 100644 --- a/src/qibolab/compilers/compiler.py +++ b/src/qibolab/compilers/compiler.py @@ -121,6 +121,7 @@ def get_sequence(self, gate, platform): raise NotImplementedError(f"{type(gate)} is not a native gate.") return gate_sequence + # FIXME: pulse.qubit and pulse.channel do not exist anymore def compile(self, circuit, platform): """Transforms a circuit to pulse sequence. @@ -134,6 +135,12 @@ def compile(self, circuit, platform): sequence (qibolab.pulses.PulseSequence): Pulse sequence that implements the circuit. measurement_map (dict): Map from each measurement gate to the sequence of readout pulse implementing it. """ + ch_to_qb = { + ch.name: qubit_id + for qubit_id, qubit in platform.qubits.items() + for ch in qubit.channels + } + sequence = PulseSequence() # FIXME: This will not work with qubits that have string names # TODO: Implement a mapping between circuit qubit ids and platform ``Qubit``s @@ -149,17 +156,18 @@ def compile(self, circuit, platform): qubit_clock[qubit] += gate.delay continue + delay_sequence = PulseSequence() gate_sequence = self.get_sequence(gate, platform) - for pulse in gate_sequence: - if qubit_clock[pulse.qubit] > channel_clock[pulse.qubit]: - delay = qubit_clock[pulse.qubit] - channel_clock[pulse.channel] - sequence.append(Delay(duration=delay, channel=pulse.channel)) - channel_clock[pulse.channel] += delay - - sequence.append(pulse) - # update clocks - qubit_clock[pulse.qubit] += pulse.duration - channel_clock[pulse.channel] += pulse.duration + for ch in gate_sequence.keys(): + qubit = ch_to_qb[ch] + if (delay := qubit_clock[qubit] - channel_clock[ch]) > 0: + delay_sequence[ch].append(Delay(duration=delay)) + channel_clock[ch] += delay + channel_duration = gate_sequence.channel_duration(ch) + qubit_clock[qubit] += channel_duration + channel_clock[ch] += channel_duration + sequence.extend(delay_sequence) + sequence.extend(gate_sequence) # register readout sequences to ``measurement_map`` so that we can # properly map acquisition results to measurement gates diff --git a/src/qibolab/compilers/default.py b/src/qibolab/compilers/default.py index 70db42f52..bf25bed70 100644 --- a/src/qibolab/compilers/default.py +++ b/src/qibolab/compilers/default.py @@ -5,8 +5,9 @@ import math +import numpy as np + from qibolab.pulses import PulseSequence, VirtualZ -from qibolab.serialize_ import replace def identity_rule(gate, qubit): @@ -16,36 +17,32 @@ def identity_rule(gate, qubit): def z_rule(gate, qubit): """Z gate applied virtually.""" - return PulseSequence( - [VirtualZ(phase=math.pi, channel=qubit.drive.name, qubit=qubit.name)] - ) + seq = PulseSequence() + seq[qubit.drive.name].append(VirtualZ(phase=math.pi)) + return seq def rz_rule(gate, qubit): """RZ gate applied virtually.""" - return PulseSequence( - [VirtualZ(phase=gate.parameters[0], channel=qubit.drive.name, qubit=qubit.name)] - ) + seq = PulseSequence() + seq[qubit.drive.name].append(VirtualZ(phase=gate.parameters[0])) + return seq def gpi2_rule(gate, qubit): """Rule for GPI2.""" - theta = gate.parameters[0] - pulse = replace(qubit.native_gates.RX90, relative_phase=theta) - sequence = PulseSequence([pulse]) - return sequence + return qubit.native_gates.RX.create_sequence( + theta=np.pi / 2, phi=gate.parameters[0] + ) def gpi_rule(gate, qubit): """Rule for GPI.""" - theta = gate.parameters[0] # the following definition has a global phase difference compare to # to the matrix representation. See # https://github.com/qiboteam/qibolab/pull/804#pullrequestreview-1890205509 # for more detail. - pulse = replace(qubit.native_gates.RX, relative_phase=theta) - sequence = PulseSequence([pulse]) - return sequence + return qubit.native_gates.RX.create_sequence(theta=np.pi, phi=gate.parameters[0]) def cz_rule(gate, pair): @@ -54,14 +51,17 @@ def cz_rule(gate, pair): Applying the CZ gate may involve sending pulses on qubits that the gate is not directly acting on. """ - return pair.native_gates.CZ + return pair.native_gates.CZ.create_sequence() def cnot_rule(gate, pair): """CNOT applied as defined in the platform runcard.""" - return pair.native_gates.CNOT + return pair.native_gates.CNOT.create_sequence() def measurement_rule(gate, qubits): """Measurement gate applied using the platform readout pulse.""" - return PulseSequence([qubit.native_gates.MZ for qubit in qubits]) + seq = PulseSequence() + for qubit in qubits: + seq.extend(qubit.native_gates.MZ.create_sequence()) + return seq diff --git a/src/qibolab/components/__init__.py b/src/qibolab/components/__init__.py new file mode 100644 index 000000000..ee977ce99 --- /dev/null +++ b/src/qibolab/components/__init__.py @@ -0,0 +1,19 @@ +"""Component (a.k.a. logical component) is a concept that is part of the +qibolab interface exposed to its users. + +Interacting with, and controlling a quantum computer means orchestrating +its control electronics/equipment. For a qibolab user quantum computer +is just a collection of control instruments. A component represents a +piece of equipment, functionality, or configuration that can be +individually addressed, configured, and referred to in any relevant +context. It can represent a single device, part of a bigger device, or a +collection of multiple devices. Qibolab defines a handful of specific +categories of components, and each platform definition shall have only +such components, independent of what specific electronics is used, how +it is used, etc. One basic requirement for all components is that they +have unique names, and in any relevant context can be referred to by +their name. +""" + +from .channels import * +from .configs import * diff --git a/src/qibolab/components/channels.py b/src/qibolab/components/channels.py new file mode 100644 index 000000000..548efa7b5 --- /dev/null +++ b/src/qibolab/components/channels.py @@ -0,0 +1,77 @@ +"""Channels are a specific type of component, that are responsible for +generating signals. A channel has a name, and it can refer to the names of +other components as needed. The default configuration of components should be +stored elsewhere (in Platform). By dissecting configuration in smaller pieces +and storing them externally (as opposed to storing the channel configuration +inside the channel itself) has multiple benefits, that. + +all revolve around the fact that channels may have shared components, e.g. + - Some instruments use one LO for more than one channel, + - For some use cases (like qutrit experiments, or CNOT gates), we need to define multiple channels that point to the same physical wire. +If channels contain their configuration, it becomes cumbersome to make sure that user can easily see that some channels have shared +components, and changing configuration for one may affect the other as well. By storing component configurations externally we +make sure that there is only one copy of configuration for a component, plus users can clearly see when two different channels +share a component, because channels will refer to the same name for the component under discussion. +""" + +from dataclasses import dataclass +from typing import Optional + +__all__ = [ + "Channel", + "DcChannel", + "IqChannel", + "AcquireChannel", +] + + +@dataclass(frozen=True) +class Channel: + + name: str + """Name of the channel.""" + + +@dataclass(frozen=True) +class DcChannel(Channel): + """Channel that can be used to send DC pulses.""" + + +@dataclass(frozen=True) +class IqChannel(Channel): + """Channel that can be used to send IQ pulses.""" + + mixer: Optional[str] + """Name of the IQ mixer component corresponding to this channel. + + None, if the channel does not have a mixer, or it does not need + configuration. + """ + lo: Optional[str] + """Name of the local oscillator component corresponding to this channel. + + None, if the channel does not have an LO, or it is not configurable. + """ + acquisition: Optional[str] = None + """In case self is a readout channel this shall contain the name of the + corresponding acquire channel. + + FIXME: This is temporary solution to be able to generate acquisition commands on correct channel in drivers, + until we make acquire channels completely independent, and users start putting explicit acquisition commands in pulse sequence. + """ + + +@dataclass(frozen=True) +class AcquireChannel(Channel): + + twpa_pump: Optional[str] + """Name of the TWPA pump component. + + None, if there is no TWPA, or it is not configurable. + """ + probe: Optional[str] = None + """Name of the corresponding measure/probe channel. + + FIXME: This is temporary solution to be able to relate acquisition channel to corresponding probe channel wherever needed in drivers, + until we make acquire channels completely independent, and users start putting explicit acquisition commands in pulse sequence. + """ diff --git a/src/qibolab/components/configs.py b/src/qibolab/components/configs.py new file mode 100644 index 000000000..06dc7ded1 --- /dev/null +++ b/src/qibolab/components/configs.py @@ -0,0 +1,82 @@ +"""Configuration for various components. + +These represent the minimal needed configuration that needs to be +exposed to users. Specific definitions of components can expose more, +and that can be used in any troubleshooting or debugging purposes by +users, but in general any user tool should try to depend only on the +configuration defined by these classes. +""" + +from dataclasses import dataclass +from typing import Union + +__all__ = [ + "DcConfig", + "IqConfig", + "AcquisitionConfig", + "IqMixerConfig", + "OscillatorConfig", + "Config", +] + + +@dataclass(frozen=True) +class DcConfig: + """Configuration for a channel that can be used to send DC pulses (i.e. + just envelopes without modulation).""" + + offset: float + """DC offset/bias of the channel.""" + + +@dataclass(frozen=True) +class OscillatorConfig: + """Configuration for an oscillator.""" + + frequency: float + power: float + + +@dataclass(frozen=True) +class IqMixerConfig: + """Configuration for IQ mixer. + + Mixers usually have various imperfections, and one needs to + compensate for them. This class holds the compensation + configuration. + """ + + offset_i: float = 0.0 + """DC offset for the I component.""" + offset_q: float = 0.0 + """DC offset for the Q component.""" + scale_q: float = 1.0 + """The relative amplitude scale/factor of the q channel, to account for I-Q + amplitude imbalance.""" + phase_q: float = 0.0 + """The phase offset of the q channel, to account for I-Q phase + imbalance.""" + + +@dataclass(frozen=True) +class IqConfig: + """Configuration for an IQ channel.""" + + frequency: float + """The carrier frequency of the channel.""" + + +@dataclass(frozen=True) +class AcquisitionConfig: + """Configuration for acquisition channel. + + Currently, in qibolab, acquisition channels are FIXME: + """ + + delay: float + """Delay between readout pulse start and acquisition start.""" + smearing: float + """FIXME:""" + + +Config = Union[DcConfig, IqMixerConfig, OscillatorConfig, IqConfig, AcquisitionConfig] diff --git a/src/qibolab/couplers.py b/src/qibolab/couplers.py index cd384ecf4..e499f987c 100644 --- a/src/qibolab/couplers.py +++ b/src/qibolab/couplers.py @@ -1,7 +1,7 @@ from dataclasses import dataclass, field from typing import Dict, Optional, Union -from qibolab.channels import Channel +from qibolab.components import DcChannel from qibolab.native import SingleQubitNatives QubitId = Union[str, int] @@ -25,7 +25,7 @@ class Coupler: native_gates: SingleQubitNatives = field(default_factory=SingleQubitNatives) "For now this only contains the calibrated pulse to activate the coupler." - _flux: Optional[Channel] = None + flux: Optional[DcChannel] = None "flux (:class:`qibolab.platforms.utils.Channel`): Channel used to send flux pulses to the qubit." # TODO: With topology or conectivity @@ -33,20 +33,6 @@ class Coupler: qubits: Dict = field(default_factory=dict) "Qubits the coupler acts on" - def __post_init__(self): - if self.flux is not None and self.sweetspot != 0: - self.flux.offset = self.sweetspot - - @property - def flux(self): - return self._flux - - @flux.setter - def flux(self, channel): - if self.sweetspot != 0: - channel.offset = self.sweetspot - self._flux = channel - @property def channels(self): if self.flux is not None: diff --git a/src/qibolab/dummy/parameters.json b/src/qibolab/dummy/parameters.json index 3a5f5846e..42da6014b 100644 --- a/src/qibolab/dummy/parameters.json +++ b/src/qibolab/dummy/parameters.json @@ -25,514 +25,609 @@ "frequency": 1000000000.0 } }, + "components": { + "qubit_0/drive" : { + "frequency": 4000000000 + }, + "qubit_1/drive" : { + "frequency": 4200000000 + }, + "qubit_2/drive" : { + "frequency": 4500000000 + }, + "qubit_3/drive" : { + "frequency": 4150000000 + }, + "qubit_4/drive" : { + "frequency": 4155663000 + }, + "qubit_0/drive12" : { + "frequency": 4700000000 + }, + "qubit_1/drive12" : { + "frequency": 4855663000 + }, + "qubit_2/drive12" : { + "frequency": 2700000000 + }, + "qubit_3/drive12" : { + "frequency": 5855663000 + }, + "qubit_4/drive12" : { + "frequency": 5855663000 + }, + "qubit_0/flux" : { + "offset": -0.1 + }, + "qubit_1/flux" : { + "offset": 0.0 + }, + "qubit_2/flux" : { + "offset": 0.1 + }, + "qubit_3/flux" : { + "offset": 0.2 + }, + "qubit_4/flux" : { + "offset": 0.15 + }, + "qubit_0/probe" : { + "frequency": 5200000000 + }, + "qubit_1/probe" : { + "frequency": 4900000000 + }, + "qubit_2/probe" : { + "frequency": 6100000000 + }, + "qubit_3/probe" : { + "frequency": 5800000000 + }, + "qubit_4/probe" : { + "frequency": 5500000000 + }, + "qubit_0/acquire": { + "delay": 0, + "smearing": 0 + }, + "qubit_1/acquire": { + "delay": 0, + "smearing": 0 + }, + "qubit_2/acquire": { + "delay": 0, + "smearing": 0 + }, + "qubit_3/acquire": { + "delay": 0, + "smearing": 0 + }, + "qubit_4/acquire": { + "delay": 0, + "smearing": 0 + }, + "coupler_0/flux" : { + "offset": 0.0 + }, + "coupler_1/flux" : { + "offset": 0.0 + }, + "coupler_3/flux" : { + "offset": 0.0 + }, + "coupler_4/flux" : { + "offset": 0.0 + }, + "twpa_pump": { + "power": 10, + "frequency": 1000000000.0 + } + }, "native_gates": { "single_qubit": { "0": { "RX": { - "duration": 40, - "amplitude": 0.1, - "envelope": { "kind": "gaussian", "rel_sigma": 5 }, - "frequency": 4000000000.0, - "type": "qd" + "qubit_0/drive": [ + { + "duration": 40, + "amplitude": 0.1, + "envelope": { "kind": "gaussian", "rel_sigma": 5 }, + "type": "qd" + } + ] }, "RX12": { - "duration": 40, - "amplitude": 0.005, - "envelope": { "kind": "gaussian", "rel_sigma": 5 }, - "frequency": 4700000000, - "type": "qd" + "qubit_0/drive12": [ + { + "duration": 40, + "amplitude": 0.005, + "envelope": { "kind": "gaussian", "rel_sigma": 5 }, + "type": "qd" + } + ] }, "MZ": { - "duration": 2000, - "amplitude": 0.1, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5, - "width": 0.75 - }, - "frequency": 5200000000.0, - "type": "ro" + "qubit_0/probe": [ + { + "duration": 2000, + "amplitude": 0.1, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "ro" + } + ] } }, "1": { "RX": { - "duration": 40, - "amplitude": 0.3, - "envelope": { "kind": "drag", "rel_sigma": 5, "beta": 0.02 }, - "frequency": 4200000000.0, - "type": "qd" + "qubit_1/drive": [ + { + "duration": 40, + "amplitude": 0.3, + "envelope": { "kind": "drag", "rel_sigma": 5, "beta": 0.02 }, + "type": "qd" + } + ] }, "RX12": { - "duration": 40, - "amplitude": 0.0484, - "envelope": { "kind": "drag", "rel_sigma": 5, "beta": 0.02 }, - "frequency": 4855663000, - "type": "qd" - }, + "qubit_1/drive12": [ + { + "duration": 40, + "amplitude": 0.0484, + "envelope": { "kind": "drag", "rel_sigma": 5, "beta": 0.02 }, + "type": "qd" + } + ] + } , "MZ": { - "duration": 2000, - "amplitude": 0.1, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5, - "width": 0.75 - }, - "frequency": 4900000000.0, - "type": "ro" + "qubit_1/probe": [ + { + "duration": 2000, + "amplitude": 0.1, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "ro" + } + ] } }, "2": { "RX": { - "duration": 40, - "amplitude": 0.3, - "envelope": { "kind": "drag", "rel_sigma": 5, "beta": 0.02 }, - "frequency": 4500000000.0, - "type": "qd" + "qubit_2/drive": [ + { + "duration": 40, + "amplitude": 0.3, + "envelope": { "kind": "drag", "rel_sigma": 5, "beta": 0.02 }, + "type": "qd" + } + ] }, "RX12": { - "duration": 40, - "amplitude": 0.005, - "envelope": { "kind": "gaussian", "rel_sigma": 5 }, - "frequency": 2700000000, - "type": "qd" + "qubit_2/drive12": [ + { + "duration": 40, + "amplitude": 0.005, + "envelope": { "kind": "gaussian", "rel_sigma": 5 }, + "type": "qd" + } + ] }, "MZ": { - "duration": 2000, - "amplitude": 0.1, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5, - "width": 0.75 - }, - "frequency": 6100000000.0, - "type": "ro" + "qubit_2/probe": [ + { + "duration": 2000, + "amplitude": 0.1, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "ro" + } + ] } }, "3": { "RX": { - "duration": 40, - "amplitude": 0.3, - "envelope": { "kind": "drag", "rel_sigma": 5, "beta": 0.02 }, - "frequency": 4150000000.0, - "type": "qd" + "qubit_3/drive": [ + { + "duration": 40, + "amplitude": 0.3, + "envelope": { "kind": "drag", "rel_sigma": 5, "beta": 0.02 }, + "type": "qd" + } + ] }, "RX12": { - "duration": 40, - "amplitude": 0.0484, - "envelope": { "kind": "drag", "rel_sigma": 5, "beta": 0.02 }, - "frequency": 5855663000, - "type": "qd" + "qubit_3/drive12": [ + { + "duration": 40, + "amplitude": 0.0484, + "envelope": { "kind": "drag", "rel_sigma": 5, "beta": 0.02 }, + "type": "qd" + } + ] }, "MZ": { - "duration": 2000, - "amplitude": 0.1, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5, - "width": 0.75 - }, - "frequency": 5800000000.0, - "type": "ro" + "qubit_3/probe": [ + { + "duration": 2000, + "amplitude": 0.1, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "ro" + } + ] } }, "4": { "RX": { - "duration": 40, - "amplitude": 0.3, - "envelope": { "kind": "drag", "rel_sigma": 5, "beta": 0.02 }, - "frequency": 4155663000, - "type": "qd" + "qubit_4/drive": [ + { + "duration": 40, + "amplitude": 0.3, + "envelope": { "kind": "drag", "rel_sigma": 5, "beta": 0.02 }, + "type": "qd" + } + ] }, "RX12": { - "duration": 40, - "amplitude": 0.0484, - "envelope": { "kind": "drag", "rel_sigma": 5, "beta": 0.02 }, - "frequency": 5855663000, - "type": "qd" + "qubit_4/drive12": [ + { + "duration": 40, + "amplitude": 0.0484, + "envelope": { "kind": "drag", "rel_sigma": 5, "beta": 0.02 }, + "type": "qd" + } + ] }, "MZ": { - "duration": 2000, - "amplitude": 0.1, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5, - "width": 0.75 - }, - "frequency": 5500000000.0, - "type": "ro" - } - } - }, - "coupler": { - "0": { - "CP": { - "duration": 30, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5, - "width": 0.75 - }, - "frequency": 0, - "type": "cf" - } - }, - "1": { - "CP": { - "duration": 30, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5, - "width": 0.75 - }, - "frequency": 0, - "type": "cf" - } - }, - "3": { - "CP": { - "duration": 30, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5, - "width": 0.75 - }, - "frequency": 0, - "type": "cf" - } - }, - "4": { - "CP": { - "duration": 30, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5, - "width": 0.75 - }, - "frequency": 0, - "type": "cf" + "qubit_4/probe": [ + { + "duration": 2000, + "amplitude": 0.1, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "ro" + } + ] } } }, "two_qubit": { "0-2": { - "CZ": [ - { - "duration": 30, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5, - "width": 0.75 - }, - "qubit": 2, - "frequency": 0, - "type": "qf" - }, - { - "type": "vz", - "phase": 0.0, - "qubit": 0 - }, - { - "type": "vz", - "phase": 0.0, - "qubit": 2 - }, - { - "duration": 30, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5, - "width": 0.75 - }, - "coupler": 0, - "frequency": 0, - "type": "cf" - } - ], - "iSWAP": [ - { - "duration": 30, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5, - "width": 0.75 - }, - "qubit": 2, - "frequency": 0, - "type": "qf" - }, - { - "type": "vz", - "phase": 0.0, - "qubit": 1 - }, - { - "type": "vz", - "phase": 0.0, - "qubit": 2 - }, - { - "duration": 30, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5, - "width": 0.75 - }, - "coupler": 0, - "frequency": 0, - "type": "cf" - } - ] + "CZ": { + "qubit_2/flux": [ + { + "duration": 30, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "qf" + } + ], + "qubit_0/drive": [ + { + "type": "vz", + "phase": 0.0 + } + ], + "qubit_2/drive": [ + { + "type": "vz", + "phase": 0.0 + } + ], + "coupler_0/flux": [ + { + "duration": 30, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "cf" + } + ] + }, + "iSWAP": { + "qubit_2/flux": [ + { + "duration": 30, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "qf" + } + ], + "qubit_0/drive": [ + { + "type": "vz", + "phase": 0.0 + } + ], + "qubit_2/drive": [ + { + "type": "vz", + "phase": 0.0 + } + ], + "coupler_0/flux": [ + { + "duration": 30, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "cf" + } + ] + } }, "1-2": { - "CZ": [ - { - "duration": 30, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5, - "width": 0.75 - }, - "qubit": 2, - "frequency": 0, - "type": "qf" - }, - { - "type": "vz", - "phase": 0.0, - "qubit": 1 - }, - { - "type": "vz", - "phase": 0.0, - "qubit": 2 - }, - { - "duration": 30, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5, - "width": 0.75 - }, - "coupler": 1, - "frequency": 0, - "type": "cf" - } - ], - "iSWAP": [ - { - "duration": 30, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5, - "width": 0.75 - }, - "qubit": 2, - "frequency": 0, - "type": "qf" - }, - { - "type": "vz", - "phase": 0.0, - "qubit": 1 - }, - { - "type": "vz", - "phase": 0.0, - "qubit": 2 - }, - { - "duration": 30, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5, - "width": 0.75 - }, - "coupler": 1, - "frequency": 0, - "type": "cf" - } - ] + "CZ": { + "qubit_2/flux": [ + { + "duration": 30, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "qf" + } + ], + "qubit_1/drive": [ + { + "type": "vz", + "phase": 0.0 + } + ], + "qubit_2/drive": [ + { + "type": "vz", + "phase": 0.0 + } + ], + "coupler_1/flux": [ + { + "duration": 30, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "cf" + } + ] + }, + "iSWAP": { + "qubit_2/flux": [ + { + "duration": 30, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "qf" + } + ], + "qubit_1/drive": [ + { + "type": "vz", + "phase": 0.0 + } + ], + "qubit_2/drive": [ + { + "type": "vz", + "phase": 0.0 + } + ], + "coupler_1/flux": [ + { + "duration": 30, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "cf" + } + ] + } }, "2-3": { - "CZ": [ - { - "duration": 30, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5, - "width": 0.75 - }, - "qubit": 2, - "frequency": 0, - "type": "qf" - }, - { - "type": "vz", - "phase": 0.0, - "qubit": 3 - }, - { - "type": "vz", - "phase": 0.0, - "qubit": 2 - }, - { - "duration": 30, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5, - "width": 0.75 - }, - "coupler": 3, - "frequency": 0, - "type": "cf" - } - ], - "iSWAP": [ - { - "duration": 30, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5, - "width": 0.75 - }, - "qubit": 2, - "frequency": 0, - "type": "qf" - }, - { - "type": "vz", - "phase": 0.0, - "qubit": 1 - }, - { - "type": "vz", - "phase": 0.0, - "qubit": 2 - }, - { - "duration": 30, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5, - "width": 0.75 - }, - "coupler": 3, - "frequency": 0, - "type": "cf" - } - ], - "CNOT": [ - { - "duration": 40, - "amplitude": 0.3, - "envelope": { "kind": "drag", "rel_sigma": 5, "beta": 0.02 }, - "frequency": 4150000000.0, - "type": "qd", - "qubit": 2 - }, - { - "type": "vz", - "phase": 0.0, - "qubit": 1 - }, - { - "type": "vz", - "phase": 0.0, - "qubit": 2 - } - ] + "CZ": { + "qubit_2/flux": [ + { + "duration": 30, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "qf" + } + ], + "qubit_2/drive": [ + { + "type": "vz", + "phase": 0.0 + } + ], + "qubit_3/drive": [ + { + "type": "vz", + "phase": 0.0 + } + ], + "coupler_3/flux": [ + { + "duration": 30, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "cf" + } + ] + }, + "iSWAP": { + "qubit_2/flux": [ + { + "duration": 30, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "qf" + } + ], + "qubit_2/drive": [ + { + "type": "vz", + "phase": 0.0 + } + ], + "qubit_3/drive": [ + { + "type": "vz", + "phase": 0.0 + } + ], + "coupler_3/flux": [ + { + "duration": 30, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "cf" + } + ] + }, + "CNOT": { + "qubit_2/drive": [ + { + "duration": 40, + "amplitude": 0.3, + "envelope": { "kind": "drag", "rel_sigma": 5, "beta": 0.02 }, + "type": "qd" + } + ] + } }, "2-4": { - "CZ": [ - { - "duration": 30, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5, - "width": 0.75 - }, - "qubit": 2, - "frequency": 0, - "type": "qf" - }, - { - "type": "vz", - "phase": 0.0, - "qubit": 4 - }, - { - "type": "vz", - "phase": 0.0, - "qubit": 2 - }, - { - "duration": 30, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5, - "width": 0.75 - }, - "coupler": 4, - "frequency": 0, - "type": "cf" - } - ], - "iSWAP": [ - { - "duration": 30, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5, - "width": 0.75 - }, - "qubit": 2, - "frequency": 0, - "type": "qf" - }, - { + "CZ": { + "qubit_2/flux": [ + { + "duration": 30, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "qf" + } + ], + "qubit_2/drive": [ + { + "type": "vz", + "phase": 0.0 + } + ], + "qubit_4/drive": [ + { "type": "vz", - "phase": 0.0, - "qubit": 1 - }, - { + "phase": 0.0 + } + ], + "coupler_4/flux": [ + { + "duration": 30, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "cf" + } + ] + }, + "iSWAP": { + "qubit_2/flux": [ + { + "duration": 30, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "qf" + } + ], + "qubit_2/drive": [ + { + "type": "vz", + "phase": 0.0 + } + ], + "qubit_4/drive": [ + { "type": "vz", - "phase": 0.0, - "qubit": 2 - }, - { - "duration": 30, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5, - "width": 0.75 - }, - "coupler": 4, - "frequency": 0, - "type": "cf" - } - ] + "phase": 0.0 + } + ], + "coupler_4/flux": [ + { + "duration": 30, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "cf" + } + ] + } } } }, @@ -540,10 +635,7 @@ "single_qubit": { "0": { "bare_resonator_frequency": 0, - "readout_frequency": 5200000000.0, - "drive_frequency": 4000000000.0, "anharmonicity": 0, - "sweetspot": 0.0, "asymmetry": 0.0, "crosstalk_matrix": { "0": 1 @@ -551,8 +643,7 @@ "Ec": 0.0, "Ej": 0.0, "g": 0.0, - "assignment_fidelity": [0.5, 0.1], - "gate_fidelity": [0.5, 0.1], + "assignment_fidelity": 0.0, "peak_voltage": 0, "pi_pulse_amplitude": 0, "T1": 0.0, @@ -563,18 +654,11 @@ "mean_gnd_states": [0, 1], "mean_exc_states": [1, 0], "threshold": 0.0, - "iq_angle": 0.0, - "mixer_drive_g": 0.0, - "mixer_drive_phi": 0.0, - "mixer_readout_g": 0.0, - "mixer_readout_phi": 0.0 + "iq_angle": 0.0 }, "1": { "bare_resonator_frequency": 0, - "readout_frequency": 4900000000.0, - "drive_frequency": 4200000000.0, "anharmonicity": 0, - "sweetspot": 0.0, "asymmetry": 0.0, "crosstalk_matrix": { "1": 1 @@ -582,8 +666,7 @@ "Ec": 0.0, "Ej": 0.0, "g": 0.0, - "assignment_fidelity": [0.5, 0.1], - "gate_fidelity": [0.5, 0.1], + "assignment_fidelity": 0.0, "peak_voltage": 0, "pi_pulse_amplitude": 0, "T1": 0.0, @@ -594,18 +677,11 @@ "mean_gnd_states": [0.25, 0], "mean_exc_states": [0, 0.25], "threshold": 0.0, - "iq_angle": 0.0, - "mixer_drive_g": 0.0, - "mixer_drive_phi": 0.0, - "mixer_readout_g": 0.0, - "mixer_readout_phi": 0.0 + "iq_angle": 0.0 }, "2": { "bare_resonator_frequency": 0, - "readout_frequency": 6100000000.0, - "drive_frequency": 4500000000.0, "anharmonicity": 0, - "sweetspot": 0.0, "asymmetry": 0.0, "crosstalk_matrix": { "2": 1 @@ -613,8 +689,7 @@ "Ec": 0.0, "Ej": 0.0, "g": 0.0, - "assignment_fidelity": [0.5, 0.1], - "gate_fidelity": [0.5, 0.1], + "assignment_fidelity": 0.0, "peak_voltage": 0, "pi_pulse_amplitude": 0, "T1": 0.0, @@ -625,18 +700,11 @@ "mean_gnd_states": [0.5, 0], "mean_exc_states": [0, 0.5], "threshold": 0.0, - "iq_angle": 0.0, - "mixer_drive_g": 0.0, - "mixer_drive_phi": 0.0, - "mixer_readout_g": 0.0, - "mixer_readout_phi": 0.0 + "iq_angle": 0.0 }, "3": { "bare_resonator_frequency": 0, - "readout_frequency": 5800000000.0, - "drive_frequency": 4150000000.0, "anharmonicity": 0, - "sweetspot": 0.0, "asymmetry": 0.0, "crosstalk_matrix": { "3": 1 @@ -644,8 +712,7 @@ "Ec": 0.0, "Ej": 0.0, "g": 0.0, - "assignment_fidelity": [0.5, 0.1], - "gate_fidelity": [0.5, 0.1], + "assignment_fidelity": 0.0, "peak_voltage": 0, "pi_pulse_amplitude": 0, "T1": 0.0, @@ -656,18 +723,11 @@ "mean_gnd_states": [0.75, 0], "mean_exc_states": [0, 0.75], "threshold": 0.0, - "iq_angle": 0.0, - "mixer_drive_g": 0.0, - "mixer_drive_phi": 0.0, - "mixer_readout_g": 0.0, - "mixer_readout_phi": 0.0 + "iq_angle": 0.0 }, "4": { "bare_resonator_frequency": 0, - "readout_frequency": 5500000000.0, - "drive_frequency": 4100000000.0, "anharmonicity": 0, - "sweetspot": 0.0, "asymmetry": 0.0, "crosstalk_matrix": { "4": 1 @@ -675,8 +735,7 @@ "Ec": 0.0, "Ej": 0.0, "g": 0.0, - "assignment_fidelity": [0.5, 0.1], - "gate_fidelity": [0.5, 0.1], + "assignment_fidelity": 0.0, "peak_voltage": 0, "pi_pulse_amplitude": 0, "T1": 0.0, @@ -687,29 +746,7 @@ "mean_gnd_states": [1, 0], "mean_exc_states": [0, 1], "threshold": 0.0, - "iq_angle": 0.0, - "mixer_drive_g": 0.0, - "mixer_drive_phi": 0.0, - "mixer_readout_g": 0.0, - "mixer_readout_phi": 0.0 - } - }, - "two_qubit": { - "0-2": { - "gate_fidelity": [0.5, 0.1], - "cz_fidelity": [0.5, 0.1] - }, - "1-2": { - "gate_fidelity": [0.5, 0.1], - "cz_fidelity": [0.5, 0.1] - }, - "2-3": { - "gate_fidelity": [0.5, 0.1], - "cz_fidelity": [0.5, 0.1] - }, - "2-4": { - "gate_fidelity": [0.5, 0.1], - "cz_fidelity": [0.5, 0.1] + "iq_angle": 0.0 } }, "coupler": { diff --git a/src/qibolab/dummy/platform.py b/src/qibolab/dummy/platform.py index 614f8f6dc..0a978e2c7 100644 --- a/src/qibolab/dummy/platform.py +++ b/src/qibolab/dummy/platform.py @@ -1,11 +1,23 @@ -import itertools import pathlib -from qibolab.channels import Channel, ChannelMap +from qibolab.components import ( + AcquireChannel, + AcquisitionConfig, + DcChannel, + DcConfig, + IqChannel, + IqConfig, + OscillatorConfig, +) from qibolab.instruments.dummy import DummyInstrument, DummyLocalOscillator from qibolab.kernels import Kernels from qibolab.platform import Platform -from qibolab.serialize import load_qubits, load_runcard, load_settings +from qibolab.serialize import ( + load_instrument_settings, + load_qubits, + load_runcard, + load_settings, +) FOLDER = pathlib.Path(__file__).parent @@ -15,12 +27,13 @@ def remove_couplers(runcard): couplers.""" runcard["topology"] = list(runcard["topology"].values()) del runcard["couplers"] - del runcard["native_gates"]["coupler"] del runcard["characterization"]["coupler"] two_qubit = runcard["native_gates"]["two_qubit"] for i, gates in two_qubit.items(): for j, gate in gates.items(): - two_qubit[i][j] = [pulse for pulse in gate if "coupler" not in pulse] + two_qubit[i][j] = { + ch: pulses for ch, pulses in gate.items() if "coupler" not in ch + } return runcard @@ -30,13 +43,10 @@ def create_dummy(with_couplers: bool = True): Args: with_couplers (bool): Selects whether the dummy platform will have coupler qubits. """ - # Create dummy controller - instrument = DummyInstrument("dummy", 0) + instrument = DummyInstrument("dummy", "0.0.0.0") - # Create local oscillator - twpa_pump = DummyLocalOscillator(name="twpa_pump", address=0) - twpa_pump.frequency = 1e9 - twpa_pump.power = 10 + twpa_pump_name = "twpa_pump" + twpa_pump = DummyLocalOscillator(twpa_pump_name, "0.0.0.0") runcard = load_runcard(FOLDER) kernels = Kernels.load(FOLDER) @@ -44,47 +54,52 @@ def create_dummy(with_couplers: bool = True): if not with_couplers: runcard = remove_couplers(runcard) - # Create channel objects - nqubits = runcard["nqubits"] - channels = ChannelMap() - channels |= Channel("readout", port=instrument.ports("readout")) - channels |= ( - Channel(f"drive-{i}", port=instrument.ports(f"drive-{i}")) - for i in range(nqubits) - ) - channels |= ( - Channel(f"flux-{i}", port=instrument.ports(f"flux-{i}")) for i in range(nqubits) - ) - channels |= Channel("twpa", port=None) - if with_couplers: - channels |= ( - Channel(f"flux_coupler-{c}", port=instrument.ports(f"flux_coupler-{c}")) - for c in itertools.chain(range(0, 2), range(3, 5)) - ) - channels["readout"].attenuation = 0 - channels["twpa"].local_oscillator = twpa_pump - qubits, couplers, pairs = load_qubits(runcard, kernels) settings = load_settings(runcard) - # map channels to qubits + configs = {} + component_params = runcard["components"] + configs[twpa_pump_name] = OscillatorConfig(**component_params[twpa_pump_name]) for q, qubit in qubits.items(): - qubit.readout = channels["readout"] - qubit.drive = channels[f"drive-{q}"] - qubit.flux = channels[f"flux-{q}"] - qubit.twpa = channels["twpa"] + acquisition_name = f"qubit_{q}/acquire" + probe_name = f"qubit_{q}/probe" + qubit.probe = IqChannel( + probe_name, mixer=None, lo=None, acquisition=acquisition_name + ) + qubit.acquisition = AcquireChannel( + acquisition_name, twpa_pump=twpa_pump_name, probe=probe_name + ) + configs[probe_name] = IqConfig(**component_params[probe_name]) + configs[acquisition_name] = AcquisitionConfig( + **component_params[acquisition_name] + ) + + drive_name = f"qubit_{q}/drive" + qubit.drive = IqChannel(drive_name, mixer=None, lo=None, acquisition=None) + configs[drive_name] = IqConfig(**component_params[drive_name]) + + drive_12_name = f"qubit_{q}/drive12" + qubit.drive12 = IqChannel(drive_12_name, mixer=None, lo=None, acquisition=None) + configs[drive_12_name] = IqConfig(**component_params[drive_12_name]) + + flux_name = f"qubit_{q}/flux" + qubit.flux = DcChannel(flux_name) + configs[flux_name] = DcConfig(**component_params[flux_name]) if with_couplers: - # map channels to couplers for c, coupler in couplers.items(): - coupler.flux = channels[f"flux_coupler-{c}"] + flux_name = f"coupler_{c}/flux" + coupler.flux = DcChannel(flux_name) + configs[flux_name] = DcConfig(**component_params[flux_name]) instruments = {instrument.name: instrument, twpa_pump.name: twpa_pump} + instruments = load_instrument_settings(runcard, instruments) name = "dummy_couplers" if with_couplers else "dummy" return Platform( name, qubits, pairs, + configs, instruments, settings, resonator_type="2D", diff --git a/src/qibolab/execution_parameters.py b/src/qibolab/execution_parameters.py index 13a6ff0f2..7ac569e08 100644 --- a/src/qibolab/execution_parameters.py +++ b/src/qibolab/execution_parameters.py @@ -1,5 +1,5 @@ from enum import Enum, auto -from typing import Optional +from typing import Any, Optional from qibolab.result import ( AveragedIntegratedResults, @@ -51,6 +51,14 @@ class AveragingMode(Enum): } +ConfigUpdate = dict[str, dict[str, Any]] +"""Update for component configs. + +Maps component name to corresponding update, which in turn is a map from +config property name that needs an update to its new value. +""" + + class ExecutionParameters(Model): """Data structure to deal with execution parameters.""" @@ -71,6 +79,13 @@ class ExecutionParameters(Model): """Data acquisition type.""" averaging_mode: AveragingMode = AveragingMode.SINGLESHOT """Data averaging mode.""" + updates: list[ConfigUpdate] = [] + """List of updates for component configs. + + Later entries in the list take precedence over earlier ones (if they + happen to update the same thing). These updates will be applied on + top of platform defaults. + """ @property def results_type(self): diff --git a/src/qibolab/instruments/abstract.py b/src/qibolab/instruments/abstract.py index c42bbdd49..209db0769 100644 --- a/src/qibolab/instruments/abstract.py +++ b/src/qibolab/instruments/abstract.py @@ -4,8 +4,6 @@ from qibolab.unrolling import Bounds -from .port import Port - InstrumentId = str @@ -55,12 +53,8 @@ def setup(self, *args, **kwargs): class Controller(Instrument): """Instrument that can play pulses (using waveform generator).""" - PortType = Port - """Class used by the instrument to instantiate ports.""" - def __init__(self, name, address): super().__init__(name, address) - self._ports = {} self.bounds: Bounds = Bounds(0, 0, 0) """Estimated limitations of the device memory.""" @@ -78,21 +72,6 @@ def sampling_rate(self): """Sampling rate of control electronics in giga samples per second (GSps).""" - def ports(self, port_name, *args, **kwargs): - """Get ports associated to this controller. - - Args: - port_name: Identifier for the port. The type of the identifier - depends on the specialized port defined for each instrument. - - Returns: - :class:`qibolab.instruments.port.Port` object providing the interface - for setting instrument parameters. - """ - if port_name not in self._ports: - self._ports[port_name] = self.PortType(port_name) - return self._ports[port_name] - @abstractmethod def play(self, *args, **kwargs): """Play a pulse sequence and retrieve feedback. diff --git a/src/qibolab/instruments/dummy.py b/src/qibolab/instruments/dummy.py index b9645ac21..d23b2a195 100644 --- a/src/qibolab/instruments/dummy.py +++ b/src/qibolab/instruments/dummy.py @@ -1,39 +1,18 @@ -from dataclasses import dataclass -from typing import Dict, Optional - import numpy as np from qibo.config import log -from qibolab.couplers import Coupler -from qibolab.execution_parameters import ( - AcquisitionType, - AveragingMode, - ExecutionParameters, -) +from qibolab import AcquisitionType, AveragingMode, ExecutionParameters from qibolab.pulses import PulseSequence -from qibolab.qubits import Qubit, QubitId from qibolab.sweeper import ParallelSweepers from qibolab.unrolling import Bounds +from ..components import Config from .abstract import Controller from .oscillator import LocalOscillator -from .port import Port SAMPLING_RATE = 1 -@dataclass -class DummyPort(Port): - name: str - offset: float = 0.0 - lo_frequency: int = 0 - lo_power: int = 0 - gain: int = 0 - attenuation: int = 0 - power_range: int = 0 - filters: Optional[dict] = None - - class DummyDevice: """Dummy device that does nothing but follows the QCoDeS interface. @@ -82,8 +61,6 @@ class DummyInstrument(Controller): BOUNDS = Bounds(1, 1, 1) - PortType = DummyPort - @property def sampling_rate(self): return SAMPLING_RATE @@ -116,14 +93,12 @@ def get_values(self, options, ro_pulse, shape): def play( self, - qubits: Dict[QubitId, Qubit], - couplers: Dict[QubitId, Coupler], - sequence: PulseSequence, + configs: dict[str, Config], + sequences: list[PulseSequence], options: ExecutionParameters, + integration_setup: dict[str, tuple[np.ndarray, float]], sweepers: list[ParallelSweepers], ): - results = {} - if options.averaging_mode is not AveragingMode.CYCLIC: shape = (options.nshots,) + tuple( min(len(sweep.values) for sweep in parsweeps) for parsweeps in sweepers @@ -133,10 +108,10 @@ def play( min(len(sweep.values) for sweep in parsweeps) for parsweeps in sweepers ) - for ro_pulse in sequence.ro_pulses: - values = self.get_values(options, ro_pulse, shape) - results[ro_pulse.qubit] = results[ro_pulse.id] = options.results_type( - values - ) + results = {} + for seq in sequences: + for ro_pulse in seq.probe_pulses: + values = self.get_values(options, ro_pulse, shape) + results[ro_pulse.id] = options.results_type(values) return results diff --git a/src/qibolab/instruments/emulator/pulse_simulator.py b/src/qibolab/instruments/emulator/pulse_simulator.py index 7d657a8b6..b13eafa56 100644 --- a/src/qibolab/instruments/emulator/pulse_simulator.py +++ b/src/qibolab/instruments/emulator/pulse_simulator.py @@ -154,7 +154,7 @@ def play( Qibolab results object, as well as simulation-related time and states data. """ nshots = execution_parameters.nshots - ro_pulse_list = sequence.ro_pulses + ro_pulse_list = sequence.probe_pulses times_dict, output_states, ro_reduced_dm, rdm_qubit_list = ( self.run_pulse_simulation(sequence, self.instant_measurement) ) @@ -249,7 +249,7 @@ def sweep( # reshape and reformat samples to results format results = get_results_from_samples( - sequence.ro_pulses, sweep_samples, execution_parameters, sweeper_shape + sequence.probe_pulses, sweep_samples, execution_parameters, sweeper_shape ) # Reset pulse values back to original values (following icarusqfpga) @@ -358,7 +358,7 @@ def _sweep_play( dict: A tuple with dictionary containing simulation-related time data, a list of states at each time step in the simulation, and a dictionary mapping the qubit indices to list of sampled values. """ nshots = execution_parameters.nshots - ro_pulse_list = sequence.ro_pulses + ro_pulse_list = sequence.probe_pulses # run pulse simulation times_dict, state_history, ro_reduced_dm, rdm_qubit_list = ( diff --git a/src/qibolab/instruments/icarusqfpga.py b/src/qibolab/instruments/icarusqfpga.py index 2dbc92a46..4c412e9dd 100644 --- a/src/qibolab/instruments/icarusqfpga.py +++ b/src/qibolab/instruments/icarusqfpga.py @@ -13,7 +13,6 @@ ExecutionParameters, ) from qibolab.instruments.abstract import Controller -from qibolab.instruments.port import Port from qibolab.pulses import Pulse, PulseSequence, PulseType from qibolab.qubits import Qubit, QubitId from qibolab.result import IntegratedResults, SampleResults @@ -25,7 +24,7 @@ @dataclass -class RFSOCPort(Port): +class RFSOCPort: name: str dac: int = None adc: int = None @@ -90,6 +89,8 @@ def play( # We iterate over the seuence of pulses and generate the waveforms for each type of pulses for pulse in sequence.pulses: + # pylint: disable=no-member + # FIXME: ignore complaint about non-existent ports and _ports properties, until we upgrade this driver to qibolab 0.2 if pulse.channel not in self._ports: continue diff --git a/src/qibolab/instruments/port.py b/src/qibolab/instruments/port.py deleted file mode 100644 index d4759fa66..000000000 --- a/src/qibolab/instruments/port.py +++ /dev/null @@ -1,35 +0,0 @@ -class Port: - """Abstract interface for instrument parameters. - - These parameters are exposed to the user through :class:`qibolab.channels.Channel`. - - Drivers should subclass this interface and implement the getters - and setters for all the parameters that are available for the - corresponding instruments. - - Each port is identified by the ``name`` attribute. - Note that the type of the identifier can be different of each port implementation. - """ - - name: str - """Name of the port that acts as its identifier.""" - offset: float - """DC offset that is applied to this port.""" - lo_frequency: float - """Local oscillator frequency for the given port. - - Relevant only for controllers with internal local oscillators. - """ - lo_power: float - """Local oscillator power for the given port. - - Relevant only for controllers with internal local oscillators. - """ - # TODO: Maybe gain, attenuation and power range can be unified to a single attribute - gain: float - """Gain that is applied to this port.""" - attenuation: float - """Attenuation that is applied to this port.""" - power_range: int - """Similar to attenuation (negative) and gain (positive) for (Zurich - instruments).""" diff --git a/src/qibolab/instruments/qblox/cluster_qrm_rf.py b/src/qibolab/instruments/qblox/cluster_qrm_rf.py index 9d582e859..92aacaf98 100644 --- a/src/qibolab/instruments/qblox/cluster_qrm_rf.py +++ b/src/qibolab/instruments/qblox/cluster_qrm_rf.py @@ -615,7 +615,9 @@ def process_pulse_sequence( # Acquisitions pulse = None - for acquisition_index, pulse in enumerate(sequencer.pulses.ro_pulses): + for acquisition_index, pulse in enumerate( + sequencer.pulses.probe_pulses + ): sequencer.acquisitions[pulse.id] = { "num_bins": num_bins, "index": acquisition_index, @@ -759,14 +761,14 @@ def process_pulse_sequence( # Prepare acquire instruction: acquire acquisition_index, bin_index, delay_next_instruction if active_reset: pulses_block.append( - f"acquire {pulses.ro_pulses.index(pulses[n])},{bin_n},4" + f"acquire {pulses.probe_pulses.index(pulses[n])},{bin_n},4" ) pulses_block.append( f"latch_rst {delay_after_acquire + 300 - 4}" ) else: pulses_block.append( - f"acquire {pulses.ro_pulses.index(pulses[n])},{bin_n},{delay_after_acquire}" + f"acquire {pulses.probe_pulses.index(pulses[n])},{bin_n},{delay_after_acquire}" ) else: @@ -990,8 +992,8 @@ def acquire(self): "scope_acquisition" ] if not hardware_demod_enabled: # Software Demodulation - if len(sequencer.pulses.ro_pulses) == 1: - pulse = sequencer.pulses.ro_pulses[0] + if len(sequencer.pulses.probe_pulses) == 1: + pulse = sequencer.pulses.probe_pulses[0] frequency = self.get_if(pulse) acquisitions[pulse.qubit] = acquisitions[pulse.id] = ( AveragedAcquisition(scope, duration, frequency) @@ -1003,7 +1005,7 @@ def acquire(self): ) else: # Hardware Demodulation results = self.device.get_acquisitions(sequencer.number) - for pulse in sequencer.pulses.ro_pulses: + for pulse in sequencer.pulses.probe_pulses: bins = results[pulse.id]["acquisition"]["bins"] acquisitions[pulse.qubit] = acquisitions[pulse.id] = ( DemodulatedAcquisition(scope, bins, duration) diff --git a/src/qibolab/instruments/qblox/controller.py b/src/qibolab/instruments/qblox/controller.py index 099d8c9da..fb9c691e4 100644 --- a/src/qibolab/instruments/qblox/controller.py +++ b/src/qibolab/instruments/qblox/controller.py @@ -196,14 +196,17 @@ def _execute_pulse_sequence( # retrieve the results acquisition_results = {} for name, module in self.modules.items(): - if isinstance(module, QrmRf) and not module_pulses[name].ro_pulses.is_empty: + if ( + isinstance(module, QrmRf) + and not module_pulses[name].probe_pulses.is_empty + ): results = module.acquire() for key, value in results.items(): acquisition_results[key] = value # TODO: move to QRM_RF.acquire() shape = tuple(len(sweeper.values) for sweeper in reversed(sweepers)) shots_shape = (nshots,) + shape - for ro_pulse in sequence.ro_pulses: + for ro_pulse in sequence.probe_pulses: if options.acquisition_type is AcquisitionType.DISCRIMINATION: _res = acquisition_results[ro_pulse.id].classified _res = np.reshape(_res, shots_shape) @@ -284,7 +287,7 @@ def sweep( sweepers_copy.reverse() # create a map between the pulse id, which never changes, and the original serial - for pulse in sequence_copy.ro_pulses: + for pulse in sequence_copy.probe_pulses: map_id_serial[pulse.id] = pulse.id id_results[pulse.id] = None id_results[pulse.qubit] = None @@ -300,7 +303,7 @@ def sweep( # return the results using the original serials serial_results = {} - for pulse in sequence_copy.ro_pulses: + for pulse in sequence_copy.probe_pulses: serial_results[map_id_serial[pulse.id]] = id_results[pulse.id] serial_results[pulse.qubit] = id_results[pulse.id] return serial_results @@ -395,7 +398,7 @@ def _sweep_recursion( result = self._execute_pulse_sequence( qubits=qubits, sequence=sequence, options=options ) - for pulse in sequence.ro_pulses: + for pulse in sequence.probe_pulses: if results[pulse.id]: results[pulse.id] += result[pulse.id] else: @@ -532,7 +535,7 @@ def _combine_result_chunks(chunks): @staticmethod def _add_to_results(sequence, results, results_to_add): - for pulse in sequence.ro_pulses: + for pulse in sequence.probe_pulses: if results[pulse.id]: results[pulse.id] += results_to_add[pulse.id] else: diff --git a/src/qibolab/instruments/qblox/port.py b/src/qibolab/instruments/qblox/port.py index c83cf391e..9eb19f957 100644 --- a/src/qibolab/instruments/qblox/port.py +++ b/src/qibolab/instruments/qblox/port.py @@ -3,8 +3,6 @@ import numpy as np from qibo.config import log, raise_error -from qibolab.instruments.port import Port - FREQUENCY_LIMIT = 500e6 MAX_OFFSET = 2.5 MIN_PULSE_DURATION = 4 @@ -29,7 +27,7 @@ class QbloxInputPort_Settings: hardware_demod_en: bool = True -class QbloxOutputPort(Port): +class QbloxOutputPort: """qibolab.instruments.port.Port interface implementation for Qblox instruments.""" diff --git a/src/qibolab/instruments/qm/sweepers.py b/src/qibolab/instruments/qm/sweepers.py index e745d6bde..8eb5ea196 100644 --- a/src/qibolab/instruments/qm/sweepers.py +++ b/src/qibolab/instruments/qm/sweepers.py @@ -6,7 +6,6 @@ from qm.qua import declare, fixed, for_ from qualang_tools.loops import from_array -from qibolab.channels import check_max_offset from qibolab.instruments.qm.sequence import BakedPulse from qibolab.pulses import PulseType from qibolab.sweeper import Parameter @@ -27,6 +26,18 @@ def maximum_sweep_value(values, value0): return max(abs(min(values) + value0), abs(max(values) + value0)) +def check_max_offset(offset, max_offset): + """Checks if a given offset value exceeds the maximum supported offset. + + This is to avoid sending high currents that could damage lab + equipment such as amplifiers. + """ + if max_offset is not None and abs(offset) > max_offset: + raise_error( + ValueError, f"{offset} exceeds the maximum allowed offset {max_offset}." + ) + + def _update_baked_pulses(sweeper, qmsequence, config): """Updates baked pulse if duration sweeper is used.""" qmpulse = qmsequence.pulse_to_qmpulse[sweeper.pulses[0].id] diff --git a/src/qibolab/instruments/rfsoc/driver.py b/src/qibolab/instruments/rfsoc/driver.py index 3cad39f7c..b9cdd4905 100644 --- a/src/qibolab/instruments/rfsoc/driver.py +++ b/src/qibolab/instruments/rfsoc/driver.py @@ -13,7 +13,6 @@ from qibolab import AcquisitionType, AveragingMode, ExecutionParameters from qibolab.couplers import Coupler from qibolab.instruments.abstract import Controller -from qibolab.instruments.port import Port from qibolab.pulses import PulseSequence, PulseType from qibolab.qubits import Qubit from qibolab.result import AveragedSampleResults, IntegratedResults, SampleResults @@ -26,7 +25,7 @@ @dataclass -class RFSoCPort(Port): +class RFSoCPort: """Port object of the RFSoC.""" name: int @@ -109,7 +108,7 @@ def validate_input_command( raise NotImplementedError( "Raw data acquisition is not compatible with sweepers" ) - if len(sequence.ro_pulses) != 1: + if len(sequence.probe_pulses) != 1: raise NotImplementedError( "Raw data acquisition is compatible only with a single readout" ) @@ -260,10 +259,10 @@ def play( toti, totq = self._execute_pulse_sequence(sequence, qubits, opcode) results = {} - probed_qubits = np.unique([p.qubit for p in sequence.ro_pulses]) + probed_qubits = np.unique([p.qubit for p in sequence.probe_pulses]) for j, qubit in enumerate(probed_qubits): - for i, ro_pulse in enumerate(sequence.ro_pulses.get_qubit_pulses(qubit)): + for i, ro_pulse in enumerate(sequence.probe_pulses.get_qubit_pulses(qubit)): i_pulse = np.array(toti[j][i]) q_pulse = np.array(totq[j][i]) @@ -329,7 +328,7 @@ def play_sequence_in_sweep_recursion( """ res = self.play(qubits, couplers, sequence, execution_parameters) newres = {} - serials = [pulse.id for pulse in or_sequence.ro_pulses] + serials = [pulse.id for pulse in or_sequence.probe_pulses] for idx, key in enumerate(res): if idx % 2 == 1: newres[serials[idx // 2]] = res[key] @@ -597,7 +596,7 @@ def sweep( qubits, couplers, sweepsequence, - sequence.ro_pulses, + sequence.probe_pulses, *rfsoc_sweepers, execution_parameters=execution_parameters, ) diff --git a/src/qibolab/instruments/zhinst/__init__.py b/src/qibolab/instruments/zhinst/__init__.py index 6eff487e6..e5a00e1b8 100644 --- a/src/qibolab/instruments/zhinst/__init__.py +++ b/src/qibolab/instruments/zhinst/__init__.py @@ -1,4 +1,3 @@ +from .components import * from .executor import Zurich -from .pulse import ZhPulse from .sweep import ProcessedSweeps, classify_sweepers -from .util import acquire_channel_name, measure_channel_name diff --git a/src/qibolab/instruments/zhinst/components/__init__.py b/src/qibolab/instruments/zhinst/components/__init__.py new file mode 100644 index 000000000..f1fd439eb --- /dev/null +++ b/src/qibolab/instruments/zhinst/components/__init__.py @@ -0,0 +1,2 @@ +from .channel import * +from .configs import * diff --git a/src/qibolab/instruments/zhinst/components/channel.py b/src/qibolab/instruments/zhinst/components/channel.py new file mode 100644 index 000000000..a7fbf7cad --- /dev/null +++ b/src/qibolab/instruments/zhinst/components/channel.py @@ -0,0 +1,19 @@ +from dataclasses import dataclass + +from qibolab.components import Channel + +__all__ = [ + "ZiChannel", +] + + +@dataclass(frozen=True) +class ZiChannel: + """Channel for Zurich Instruments (ZI) devices.""" + + logical_channel: Channel + """Corresponding logical channel.""" + device: str + """Name of the device.""" + path: str + """Path of the device node.""" diff --git a/src/qibolab/instruments/zhinst/components/configs.py b/src/qibolab/instruments/zhinst/components/configs.py new file mode 100644 index 000000000..6211fa69b --- /dev/null +++ b/src/qibolab/instruments/zhinst/components/configs.py @@ -0,0 +1,42 @@ +from dataclasses import dataclass + +from qibolab.components import AcquisitionConfig, DcConfig, IqConfig + +__all__ = [ + "ZiDcConfig", + "ZiIqConfig", + "ZiAcquisitionConfig", +] + + +@dataclass(frozen=True) +class ZiDcConfig(DcConfig): + """DC channel config using ZI HDAWG.""" + + power_range: float + """Power range in volts. + + Possible values are [0.2 0.4 0.6 0.8 1. 2. 3. 4. 5.]. + """ + + +@dataclass(frozen=True) +class ZiIqConfig(IqConfig): + """IQ channel config for ZI SHF* line instrument.""" + + power_range: float + """Power range in dBm. + + Possible values are [-30. -25. -20. -15. -10. -5. 0. 5. 10.]. + """ + + +@dataclass(frozen=True) +class ZiAcquisitionConfig(AcquisitionConfig): + """Acquisition config for ZI SHF* line instrument.""" + + power_range: float + """Power range in dBm. + + Possible values are [-30. -25. -20. -15. -10. -5. 0. 5. 10.]. + """ diff --git a/src/qibolab/instruments/zhinst/constants.py b/src/qibolab/instruments/zhinst/constants.py new file mode 100644 index 000000000..a9c1a449e --- /dev/null +++ b/src/qibolab/instruments/zhinst/constants.py @@ -0,0 +1,4 @@ +"""Shared constants.""" + +SAMPLING_RATE = 2 +NANO_TO_SECONDS = 1e-9 diff --git a/src/qibolab/instruments/zhinst/executor.py b/src/qibolab/instruments/zhinst/executor.py index ad7693124..33cf8c858 100644 --- a/src/qibolab/instruments/zhinst/executor.py +++ b/src/qibolab/instruments/zhinst/executor.py @@ -1,31 +1,24 @@ """Executing pulse sequences on a Zurich Instruments devices.""" import re -from collections import defaultdict -from dataclasses import dataclass, replace -from typing import Any, Optional +from itertools import chain +from typing import Any, Optional, Union -import laboneq.simple as lo +import laboneq.simple as laboneq import numpy as np -from qibo.config import log +from laboneq.dsl.device import create_connection from qibolab import AcquisitionType, AveragingMode, ExecutionParameters -from qibolab.couplers import Coupler from qibolab.instruments.abstract import Controller -from qibolab.instruments.port import Port -from qibolab.pulses import PulseSequence, PulseType -from qibolab.qubits import Qubit +from qibolab.pulses import Delay, Pulse, PulseSequence from qibolab.sweeper import Parameter, Sweeper from qibolab.unrolling import Bounds -from .pulse import ZhPulse +from ...components import AcquireChannel, Config, DcChannel, IqChannel +from .components import ZiChannel +from .constants import NANO_TO_SECONDS, SAMPLING_RATE +from .pulse import select_pulse from .sweep import ProcessedSweeps, classify_sweepers -from .util import ( - NANO_TO_SECONDS, - SAMPLING_RATE, - acquire_channel_name, - measure_channel_name, -) COMPILER_SETTINGS = { "SHFSG_MIN_PLAYWAVE_HINT": 32, @@ -35,64 +28,51 @@ } """Translating to Zurich ExecutionParameters.""" ACQUISITION_TYPE = { - AcquisitionType.INTEGRATION: lo.AcquisitionType.INTEGRATION, - AcquisitionType.RAW: lo.AcquisitionType.RAW, - AcquisitionType.DISCRIMINATION: lo.AcquisitionType.DISCRIMINATION, + AcquisitionType.INTEGRATION: laboneq.AcquisitionType.INTEGRATION, + AcquisitionType.RAW: laboneq.AcquisitionType.RAW, + AcquisitionType.DISCRIMINATION: laboneq.AcquisitionType.DISCRIMINATION, } AVERAGING_MODE = { - AveragingMode.CYCLIC: lo.AveragingMode.CYCLIC, - AveragingMode.SINGLESHOT: lo.AveragingMode.SINGLE_SHOT, + AveragingMode.CYCLIC: laboneq.AveragingMode.CYCLIC, + AveragingMode.SINGLESHOT: laboneq.AveragingMode.SINGLE_SHOT, } -@dataclass -class ZhPort(Port): - name: tuple[str, str] - offset: float = 0.0 - power_range: int = 0 - - -@dataclass -class SubSequence: - """A subsequence is a slice (in time) of a sequence that contains at most - one measurement per qubit. - - When the driver is asked to execute a sequence, it will first split - it into sub-sequences. This is needed so that we can create a - separate laboneq section for each measurement (multiple measurements - per section are not allowed). When splitting a sequence, it is - assumed that 1. a measurement operation can be parallel (in time) to - another measurement operation (i.e. measuring multiple qubits - simultaneously), but other channels (e.g. drive) do not contain any - pulses parallel to measurements, 2. ith measurement on some channel - is in the same subsequence as the ith measurement (if any) on - another measurement channel, 3. all measurements in one subsequence - happen at the same time. - """ - - measurements: list[tuple[str, ZhPulse]] - control_sequence: dict[str, list[ZhPulse]] +def _acquisition_handle(seq_idx: int, pulse_idx: int, acquisition_name: str) -> str: + return f"sequence_{seq_idx}_{acquisition_name}_{pulse_idx}" class Zurich(Controller): """Driver for a collection of ZI instruments that are automatically synchronized via ZSync protocol.""" - PortType = ZhPort - - def __init__(self, name, device_setup, time_of_flight=0.0, smearing=0.0): + def __init__( + self, + name, + device_setup, + channels: list[ZiChannel], + time_of_flight=0.0, + smearing=0.0, + ): super().__init__(name, None) self.signal_map = {} "Signals to lines mapping" - self.calibration = lo.Calibration() + self.calibration = laboneq.Calibration() "Zurich calibration object)" + for ch in channels: + device_setup.add_connections( + ch.device, + create_connection(to_signal=ch.logical_channel.name, ports=ch.path), + ) self.device_setup = device_setup self.session = None "Zurich device parameters for connection" + self.channels = {ch.logical_channel.name: ch for ch in channels} + self.time_of_flight = time_of_flight self.smearing = smearing "Parameters read from the runcard not part of ExecutionParameters" @@ -110,12 +90,8 @@ def __init__(self, name, device_setup, time_of_flight=0.0, smearing=0.0): self.acquisition_type = None "To store if the AcquisitionType.SPECTROSCOPY needs to be enabled by parsing the sequence" - self.sequence = defaultdict(list) - "Zurich pulse sequence" - self.sub_sequences: list[SubSequence] = [] - "Sub sequences between each measurement" - self.unsplit_channels: set[str] = set() - "Names of channels that were not split into sub-sequences" + self.sequences: list[PulseSequence] = [] + "Pulse sequences" self.processed_sweeps: Optional[ProcessedSweeps] = None self.nt_sweeps: list[Sweeper] = [] @@ -125,11 +101,19 @@ def __init__(self, name, device_setup, time_of_flight=0.0, smearing=0.0): def sampling_rate(self): return SAMPLING_RATE + def _probe_channels(self) -> set[str]: + return { + ch.logical_channel.name + for ch in self.channels.values() + if isinstance(ch.logical_channel, IqChannel) + and ch.logical_channel.acquisition is not None + } + def connect(self): if self.is_connected is False: # To fully remove logging #configure_logging=False # I strongly advise to set it to 20 to have time estimates of the experiment duration! - self.session = lo.Session(self.device_setup, log_level=20) + self.session = laboneq.Session(self.device_setup, log_level=20) _ = self.session.connect() self.is_connected = True @@ -138,152 +122,84 @@ def disconnect(self): _ = self.session.disconnect() self.is_connected = False - def calibration_step(self, qubits, couplers, options): + def calibration_step(self, configs: dict[str, Config], options): """Zurich general pre experiment calibration definitions. Change to get frequencies from sequence """ - for coupler in couplers.values(): - self.register_couplerflux_line(coupler) - - for qubit in qubits.values(): - if qubit.flux is not None: - self.register_flux_line(qubit) - if len(self.sequence[qubit.drive.name]) != 0: - self.register_drive_line( - qubit=qubit, - intermediate_frequency=qubit.drive_frequency - - qubit.drive.local_oscillator.frequency, - ) - if len(self.sequence[measure_channel_name(qubit)]) != 0: - self.register_readout_line( - qubit=qubit, - intermediate_frequency=qubit.readout_frequency - - qubit.readout.local_oscillator.frequency, - options=options, - ) + for ch in self.channels.values(): + if isinstance(ch.logical_channel, DcChannel): + self.configure_dc_line(ch.logical_channel, configs) + if isinstance(ch.logical_channel, IqChannel): + self.configure_iq_line(ch.logical_channel, configs) + if isinstance(ch.logical_channel, AcquireChannel): + self.configure_acquire_line(ch.logical_channel, configs) self.device_setup.set_calibration(self.calibration) - def register_readout_line(self, qubit, intermediate_frequency, options): - """Registers qubit measure and acquire lines to calibration and signal - map. - - Note - ---- - To allow debugging with and oscilloscope, just set the following:: - - self.calibration[f"/logical_signal_groups/q{q}/measure_line"] = lo.SignalCalibration( - ..., - local_oscillator=lo.Oscillator( - ... - frequency=0.0, - ), - ..., - port_mode=lo.PortMode.LF, - ..., - ) - """ + def configure_dc_line(self, channel: DcChannel, configs: dict[str, Config]): + signal = self.device_setup.logical_signal_by_uid(channel.name) + self.signal_map[channel.name] = signal + self.calibration[signal.name] = laboneq.SignalCalibration( + range=configs[channel.name].power_range, + port_delay=None, + delay_signal=0, + voltage_offset=configs[channel.name].offset, + ) - q = qubit.name # pylint: disable=C0103 - self.signal_map[measure_channel_name(qubit)] = ( - self.device_setup.logical_signal_groups[f"q{q}"].logical_signals[ - "measure_line" - ] + def configure_iq_line(self, channel: IqChannel, configs: dict[str, Config]): + intermediate_frequency = ( + configs[channel.name].frequency - configs[channel.lo].frequency ) - self.calibration[f"/logical_signal_groups/q{q}/measure_line"] = ( - lo.SignalCalibration( - oscillator=lo.Oscillator( - frequency=intermediate_frequency, - modulation_type=lo.ModulationType.SOFTWARE, - ), - local_oscillator=lo.Oscillator( - frequency=int(qubit.readout.local_oscillator.frequency), + signal = self.device_setup.logical_signal_by_uid(channel.name) + self.signal_map[channel.name] = signal + self.calibration[signal.path] = laboneq.SignalCalibration( + oscillator=laboneq.Oscillator( + frequency=intermediate_frequency, + modulation_type=( + laboneq.ModulationType.HARDWARE + if channel.acquisition is None + else laboneq.ModulationType.SOFTWARE ), - range=qubit.readout.power_range, - port_delay=None, - delay_signal=0, - ) + ), + local_oscillator=laboneq.Oscillator( + frequency=int(configs[channel.lo].frequency), + ), + range=configs[channel.name].power_range, + port_delay=None, + delay_signal=0, ) - self.signal_map[acquire_channel_name(qubit)] = ( - self.device_setup.logical_signal_groups[f"q{q}"].logical_signals[ - "acquire_line" - ] + def configure_acquire_line( + self, channel: AcquireChannel, configs: dict[str, Config] + ): + intermediate_frequency = ( + configs[channel.probe].frequency + - configs[self.channels[channel.probe].logical_channel.lo].frequency ) + acquire_signal = self.device_setup.logical_signal_by_uid(channel.name) + self.signal_map[channel.name] = acquire_signal - oscillator = lo.Oscillator( + oscillator = laboneq.Oscillator( frequency=intermediate_frequency, - modulation_type=lo.ModulationType.SOFTWARE, + modulation_type=laboneq.ModulationType.SOFTWARE, ) threshold = None - if options.acquisition_type == AcquisitionType.DISCRIMINATION: - if qubit.kernel is not None: - # Kernels don't work with the software modulation on the acquire signal - oscillator = None - else: - # To keep compatibility with angle and threshold discrimination (Remove when possible) - threshold = qubit.threshold - - self.calibration[f"/logical_signal_groups/q{q}/acquire_line"] = ( - lo.SignalCalibration( - oscillator=oscillator, - range=qubit.feedback.power_range, - port_delay=self.time_of_flight * NANO_TO_SECONDS, - threshold=threshold, - ) - ) - - def register_drive_line(self, qubit, intermediate_frequency): - """Registers qubit drive line to calibration and signal map.""" - q = qubit.name # pylint: disable=C0103 - self.signal_map[qubit.drive.name] = self.device_setup.logical_signal_groups[ - f"q{q}" - ].logical_signals["drive_line"] - self.calibration[f"/logical_signal_groups/q{q}/drive_line"] = ( - lo.SignalCalibration( - oscillator=lo.Oscillator( - frequency=intermediate_frequency, - modulation_type=lo.ModulationType.HARDWARE, - ), - local_oscillator=lo.Oscillator( - frequency=int(qubit.drive.local_oscillator.frequency), - ), - range=qubit.drive.power_range, - port_delay=None, - delay_signal=0, - ) - ) - - def register_flux_line(self, qubit): - """Registers qubit flux line to calibration and signal map.""" - q = qubit.name # pylint: disable=C0103 - self.signal_map[qubit.flux.name] = self.device_setup.logical_signal_groups[ - f"q{q}" - ].logical_signals["flux_line"] - self.calibration[f"/logical_signal_groups/q{q}/flux_line"] = ( - lo.SignalCalibration( - range=qubit.flux.power_range, - port_delay=None, - delay_signal=0, - voltage_offset=qubit.flux.offset, - ) - ) - - def register_couplerflux_line(self, coupler): - """Registers qubit flux line to calibration and signal map.""" - c = coupler.name # pylint: disable=C0103 - self.signal_map[coupler.flux.name] = self.device_setup.logical_signal_groups[ - f"qc{c}" - ].logical_signals["flux_line"] - self.calibration[f"/logical_signal_groups/qc{c}/flux_line"] = ( - lo.SignalCalibration( - range=coupler.flux.power_range, - port_delay=None, - delay_signal=0, - voltage_offset=coupler.flux.offset, - ) + # FIXME: + # if options.acquisition_type == AcquisitionType.DISCRIMINATION: + # if qubit.kernel is not None: + # # Kernels don't work with the software modulation on the acquire signal + # oscillator = None + # else: + # # To keep compatibility with angle and threshold discrimination (Remove when possible) + # threshold = qubit.threshold + + self.calibration[acquire_signal.path] = laboneq.SignalCalibration( + oscillator=oscillator, + range=configs[channel.name].power_range, + port_delay=self.time_of_flight * NANO_TO_SECONDS, + threshold=threshold, ) def run_exp(self): @@ -299,77 +215,11 @@ def run_exp(self): ) self.results = self.session.run(compiled_experiment) - @staticmethod - def frequency_from_pulses(qubits, sequence): - """Gets the frequencies from the pulses to the qubits.""" - for pulse in sequence: - qubit = qubits[pulse.qubit] - if pulse.type is PulseType.READOUT: - qubit.readout_frequency = pulse.frequency - if pulse.type is PulseType.DRIVE: - qubit.drive_frequency = pulse.frequency - - def create_sub_sequences( - self, qubits: list[Qubit] - ) -> tuple[list[SubSequence], set[str]]: - """Create subsequences based on locations of measurements. - - Returns list of subsequences and a set of channel names that - were not split - """ - measure_channels = {measure_channel_name(qb) for qb in qubits} - other_channels = set(self.sequence.keys()) - measure_channels - - measurement_groups = defaultdict(list) - for ch in measure_channels: - for i, pulse in enumerate(self.sequence[ch]): - measurement_groups[i].append((ch, pulse)) - - measurement_start_end = {} - for i, group in measurement_groups.items(): - starts = np.array([meas.pulse.start for _, meas in group]) - ends = np.array([meas.pulse.finish for _, meas in group]) - measurement_start_end[i] = ( - max(starts), - max(ends), - ) # max is intended for float arithmetic errors only - - # FIXME: this is a hotfix specifically made for any flux experiments in flux pulse mode, where the flux - # pulses extend through the entire duration of the experiment. This should be removed once the sub-sequence - # splitting logic is removed from the driver. - channels_overlapping_measurement = set() - if len(measurement_groups) == 1: - for ch in other_channels: - for pulse in self.sequence[ch]: - if not pulse.pulse.type in (PulseType.FLUX, PulseType.COUPLERFLUX): - break - start, end = measurement_start_end[0] - if pulse.pulse.start < end and pulse.pulse.finish > start: - channels_overlapping_measurement.add(ch) - break - - # split non-measurement channels according to the locations of the measurements - sub_sequences = defaultdict(lambda: defaultdict(list)) - for ch in other_channels - channels_overlapping_measurement: - measurement_index = 0 - for pulse in self.sequence[ch]: - start, _ = measurement_start_end[measurement_index] - if pulse.pulse.finish > start: - measurement_index += 1 - sub_sequences[measurement_index][ch].append(pulse) - if len(sub_sequences) > len(measurement_groups): - log.warning("There are control pulses after the last measurement start.") - - return [ - SubSequence(measurement_groups[i], sub_sequences[i]) - for i in range(len(measurement_groups)) - ], channels_overlapping_measurement - def experiment_flow( self, - qubits: dict[str, Qubit], - couplers: dict[str, Coupler], - sequence: PulseSequence, + configs: dict[str, Config], + sequences: list[PulseSequence], + integration_setup, options: ExecutionParameters, ): """Create the experiment object for the devices, following the steps @@ -378,77 +228,41 @@ def experiment_flow( Translation, Calibration, Experiment Definition. Args: - qubits (dict[str, Qubit]): qubits for the platform. - couplers (dict[str, Coupler]): couplers for the platform. - sequence (PulseSequence): sequence of pulses to be played in the experiment. + sequences: list of sequences to be played in the experiment. + options: execution options/parameters """ - self.sequence = self.sequence_zh(sequence, qubits) - self.sub_sequences, self.unsplit_channels = self.create_sub_sequences( - list(qubits.values()) - ) - self.calibration_step(qubits, couplers, options) - self.create_exp(qubits, options) - - # pylint: disable=W0221 - def play(self, qubits, couplers, sequence, options): - """Play pulse sequence.""" - return self.sweep(qubits, couplers, sequence, options) - - def sequence_zh( - self, sequence: PulseSequence, qubits: dict[str, Qubit] - ) -> dict[str, list[ZhPulse]]: - """Convert Qibo sequence to a sequence where all pulses are replaced - with ZhPulse instances. - - The resulting object is a dictionary mapping from channel name - to corresponding sequence of ZhPulse instances - """ - # Define and assign the sequence - zhsequence = defaultdict(list) - - # Fill the sequences with pulses according to their lines in temporal order - for pulse in sequence: - if pulse.type == PulseType.READOUT: - ch = measure_channel_name(qubits[pulse.qubit]) - else: - ch = pulse.channel - zhsequence[ch].append(ZhPulse(pulse)) - - if self.processed_sweeps: - for ch, zhpulses in zhsequence.items(): - for zhpulse in zhpulses: - for param, sweep in self.processed_sweeps.sweeps_for_pulse( - zhpulse.pulse - ): - zhpulse.add_sweeper(param, sweep) - - return zhsequence + self.sequences = sequences + self.calibration_step(configs, options) + self.create_exp(integration_setup, options) - def create_exp(self, qubits, options): + def create_exp(self, integration_setup, options): """Zurich experiment initialization using their Experiment class.""" if self.acquisition_type: acquisition_type = self.acquisition_type else: acquisition_type = ACQUISITION_TYPE[options.acquisition_type] averaging_mode = AVERAGING_MODE[options.averaging_mode] - exp_options = replace( - options, acquisition_type=acquisition_type, averaging_mode=averaging_mode + exp_options = options.copy( + update={ + "acquisition_type": acquisition_type, + "averaging_mode": averaging_mode, + } ) - signals = [lo.ExperimentSignal(name) for name in self.signal_map.keys()] - exp = lo.Experiment( + signals = [laboneq.ExperimentSignal(name) for name in self.signal_map.keys()] + exp = laboneq.Experiment( uid="Sequence", signals=signals, ) contexts = self._contexts(exp, exp_options) - self._populate_exp(qubits, exp, exp_options, contexts) + self._populate_exp(exp, integration_setup, exp_options, contexts) self.set_calibration_for_rt_sweep(exp) exp.set_signal_map(self.signal_map) self.experiment = exp def _contexts( - self, exp: lo.Experiment, exp_options: ExecutionParameters + self, exp: laboneq.Experiment, exp_options: ExecutionParameters ) -> list[tuple[Optional[Sweeper], Any]]: """To construct a laboneq experiment, we need to first define a certain sequence of nested contexts. @@ -490,43 +304,44 @@ def _contexts( def _populate_exp( self, - qubits: dict[str, Qubit], - exp: lo.Experiment, + exp: laboneq.Experiment, + integration_setup, exp_options: ExecutionParameters, contexts, ): """Recursively activate the nested contexts, then define the main experiment body inside the innermost context.""" if len(contexts) == 0: - self.select_exp(exp, qubits, exp_options) + self.select_exp(exp, integration_setup, exp_options) return sweeper, ctx = contexts[0] with ctx: if sweeper in self.nt_sweeps: self.set_instrument_nodes_for_nt_sweep(exp, sweeper) - self._populate_exp(qubits, exp, exp_options, contexts[1:]) + self._populate_exp(exp, integration_setup, exp_options, contexts[1:]) - def set_calibration_for_rt_sweep(self, exp: lo.Experiment) -> None: + def set_calibration_for_rt_sweep(self, exp: laboneq.Experiment) -> None: """Set laboneq calibration of parameters that are to be swept in real- time.""" if self.processed_sweeps: - calib = lo.Calibration() + calib = laboneq.Calibration() for ch in ( - set(self.sequence.keys()) | self.processed_sweeps.channels_with_sweeps() + set(chain(*(seq.keys() for seq in self.sequences))) + | self.processed_sweeps.channels_with_sweeps() ): for param, sweep_param in self.processed_sweeps.sweeps_for_channel(ch): if param is Parameter.frequency: - calib[ch] = lo.SignalCalibration( - oscillator=lo.Oscillator( + calib[ch] = laboneq.SignalCalibration( + oscillator=laboneq.Oscillator( frequency=sweep_param, - modulation_type=lo.ModulationType.HARDWARE, + modulation_type=laboneq.ModulationType.HARDWARE, ) ) exp.set_calibration(calib) def set_instrument_nodes_for_nt_sweep( - self, exp: lo.Experiment, sweeper: Sweeper + self, exp: laboneq.Experiment, sweeper: Sweeper ) -> None: """In some cases there is no straightforward way to sweep a parameter. @@ -559,177 +374,176 @@ def get_channel_node_path(self, channel_name: str) -> str: f"Could not find instrument node corresponding to channel {channel_name}" ) - def select_exp(self, exp, qubits, exp_options): + def select_exp(self, exp, integration_setup, exp_options): """Build Zurich Experiment selecting the relevant sections.""" - # channels that were not split are just applied in parallel to the rest of the experiment - with exp.section(uid="unsplit_channels"): - for ch in self.unsplit_channels: - for pulse in self.sequence[ch]: - exp.delay(signal=ch, time=pulse.pulse.start) - self.play_sweep(exp, ch, pulse) - - weights = {} + probe_channels = self._probe_channels() + kernels = {} previous_section = None - for i, seq in enumerate(self.sub_sequences): - section_uid = f"control_{i}" + for i, seq in enumerate(self.sequences): + other_channels = set(seq.keys()) - probe_channels + section_uid = f"sequence_{i}" with exp.section(uid=section_uid, play_after=previous_section): - for ch, pulses in seq.control_sequence.items(): - time = 0 - for pulse in pulses: - if pulse.delay_sweeper: - exp.delay(signal=ch, time=pulse.delay_sweeper) - exp.delay( - signal=ch, - time=round(pulse.pulse.start * NANO_TO_SECONDS, 9) - time, - ) - time = round(pulse.pulse.duration * NANO_TO_SECONDS, 9) + round( - pulse.pulse.start * NANO_TO_SECONDS, 9 - ) - if pulse.zhsweepers: - self.play_sweep(exp, ch, pulse) - else: - exp.play( - signal=ch, - pulse=pulse.zhpulse, - phase=pulse.pulse.relative_phase, + with exp.section(uid=f"sequence_{i}_control"): + for ch in other_channels: + for pulse in seq[ch]: + self.play_pulse( + exp, + ch, + pulse, + self.processed_sweeps.sweeps_for_pulse(pulse), + ) + for ch in set(seq.keys()) - other_channels: + for j, pulse in enumerate(seq[ch]): + with exp.section(uid=f"sequence_{i}_probe_{j}"): + acquisition_name = self.channels[ + ch + ].logical_channel.acquisition + + exp.delay( + signal=acquisition_name, + time=self.smearing * NANO_TO_SECONDS, ) - previous_section = section_uid - - if any(m.delay_sweeper is not None for _, m in seq.measurements): - section_uid = f"measurement_delay_{i}" - with exp.section(uid=section_uid, play_after=previous_section): - for ch, m in seq.measurements: - if m.delay_sweeper: - exp.delay(signal=ch, time=m.delay_sweeper) - previous_section = section_uid - - section_uid = f"measure_{i}" - with exp.section(uid=section_uid, play_after=previous_section): - for ch, pulse in seq.measurements: - qubit = qubits[pulse.pulse.qubit] - q = qubit.name - exp.delay( - signal=acquire_channel_name(qubit), - time=self.smearing * NANO_TO_SECONDS, - ) + self.play_pulse( + exp, + ch, + pulse, + self.processed_sweeps.sweeps_for_pulse(pulse), + ) + exp.delay( + signal=acquisition_name, + time=self.time_of_flight * NANO_TO_SECONDS, + ) # FIXME + + kernel = kernels.setdefault( + acquisition_name, + self._construct_kernel( + acquisition_name, + integration_setup, + pulse.duration, + exp_options, + ), + ) + exp.acquire( + signal=acquisition_name, + handle=_acquisition_handle(i, j, acquisition_name), + kernel=kernel, + ) + if j == len(seq[ch]) - 1: + exp.delay( + signal=acquisition_name, + time=exp_options.relaxation_time * NANO_TO_SECONDS, + ) - if ( - qubit.kernel is not None - and exp_options.acquisition_type - == lo.AcquisitionType.DISCRIMINATION - ): - weight = lo.pulse_library.sampled_pulse_complex( - samples=qubit.kernel * np.exp(1j * qubit.iq_angle), - ) + previous_section = section_uid - else: - if i == 0: - if ( - exp_options.acquisition_type - == lo.AcquisitionType.DISCRIMINATION - ): - weight = lo.pulse_library.sampled_pulse_complex( - samples=np.ones( - [ - int( - pulse.pulse.duration * 2 - - 3 * self.smearing * NANO_TO_SECONDS - ) - ] - ) - * np.exp(1j * qubit.iq_angle), - ) - weights[q] = weight - else: - weight = lo.pulse_library.const( - length=round( - pulse.pulse.duration * NANO_TO_SECONDS, 9 - ) - - 1.5 * self.smearing * NANO_TO_SECONDS, - amplitude=1, - ) + def _construct_kernel( + self, + acquisition_name: str, + integration_setup: dict[str, tuple[np.ndarray, float]], + duration: float, + exp_options, + ): + kernel, iq_angle = integration_setup[acquisition_name] + if ( + kernel is not None + and exp_options.acquisition_type == laboneq.AcquisitionType.DISCRIMINATION + ): + return laboneq.pulse_library.sampled_pulse_complex( + samples=kernel * np.exp(1j * iq_angle), + ) - weights[q] = weight - elif i != 0: - weight = weights[q] - - measure_pulse_parameters = {"phase": 0} - - if i == len(self.sequence[measure_channel_name(qubit)]) - 1: - reset_delay = exp_options.relaxation_time * NANO_TO_SECONDS - else: - reset_delay = 0 - - exp.measure( - acquire_signal=acquire_channel_name(qubit), - handle=f"sequence{q}_{i}", - integration_kernel=weight, - integration_kernel_parameters=None, - integration_length=None, - measure_signal=measure_channel_name(qubit), - measure_pulse=pulse.zhpulse, - measure_pulse_length=round( - pulse.pulse.duration * NANO_TO_SECONDS, 9 - ), - measure_pulse_parameters=measure_pulse_parameters, - measure_pulse_amplitude=None, - acquire_delay=self.time_of_flight * NANO_TO_SECONDS, - reset_delay=reset_delay, + else: + if exp_options.acquisition_type == laboneq.AcquisitionType.DISCRIMINATION: + return laboneq.pulse_library.sampled_pulse_complex( + samples=np.ones( + [int(duration * 2 - 3 * self.smearing * NANO_TO_SECONDS)] ) - previous_section = section_uid + * np.exp(1j * iq_angle), + ) + else: + return laboneq.pulse_library.const( + length=round(duration * NANO_TO_SECONDS, 9) + - 1.5 * self.smearing * NANO_TO_SECONDS, + amplitude=1, + ) @staticmethod - def play_sweep(exp, channel_name, pulse): - """Play Zurich pulse when a single sweeper is involved.""" - play_parameters = {} - for p, zhs in pulse.zhsweepers: - if p is Parameter.amplitude: - max_value = max(np.abs(zhs.values)) - pulse.zhpulse.amplitude *= max_value - zhs.values /= max_value - play_parameters["amplitude"] = zhs - if p is Parameter.duration: - play_parameters["length"] = zhs - if p is Parameter.relative_phase: - play_parameters["phase"] = zhs - if "phase" not in play_parameters: - play_parameters["phase"] = pulse.pulse.relative_phase - - exp.play(signal=channel_name, pulse=pulse.zhpulse, **play_parameters) - - def sweep(self, qubits, couplers, sequence: PulseSequence, options, *sweepers): + def play_pulse( + exp, + channel: str, + pulse: Union[Pulse, Delay], + sweeps: list[tuple[Parameter, laboneq.SweepParameter]], + ): + """Play a pulse or delay by taking care of setting parameters to any + associated sweeps.""" + if isinstance(pulse, Delay): + duration = pulse.duration + for p, zhs in sweeps: + if p is Parameter.duration: + duration = zhs + else: + raise ValueError(f"Cannot sweep parameter {p} of a delay.") + exp.delay(signal=channel, time=duration) + elif isinstance(pulse, Pulse): + zhpulse = select_pulse(pulse) + play_parameters = {} + for p, zhs in sweeps: + if p is Parameter.amplitude: + max_value = max(np.abs(zhs.values)) + zhpulse.amplitude *= max_value + zhs.values /= max_value + play_parameters["amplitude"] = zhs + if p is Parameter.duration: + play_parameters["length"] = zhs + if p is Parameter.relative_phase: + play_parameters["phase"] = zhs + if "phase" not in play_parameters: + play_parameters["phase"] = pulse.relative_phase + exp.play(signal=channel, pulse=zhpulse, **play_parameters) + else: + raise ValueError(f"Cannot play pulse: {pulse}") + + def play( + self, + configs: dict[str, Config], + sequences: list[PulseSequence], + options, + integration_setup: dict[str, tuple[np.ndarray, float]], + *sweepers, + ): """Play pulse and sweepers sequence.""" self.signal_map = {} - self.frequency_from_pulses(qubits, sequence) - self.processed_sweeps = ProcessedSweeps(sweepers, qubits) + self.processed_sweeps = ProcessedSweeps(sweepers, self.channels, configs) self.nt_sweeps, self.rt_sweeps = classify_sweepers(sweepers) self.acquisition_type = None + probe_channels = self._probe_channels() for sweeper in sweepers: - if sweeper.parameter in {Parameter.frequency, Parameter.amplitude}: - for pulse in sweeper.pulses: - if pulse.type is PulseType.READOUT: - self.acquisition_type = lo.AcquisitionType.SPECTROSCOPY + if sweeper.parameter in {Parameter.frequency}: + for ch in sweeper.channels: + if ch in probe_channels: + self.acquisition_type = laboneq.AcquisitionType.SPECTROSCOPY - self.experiment_flow(qubits, couplers, sequence, options) + self.experiment_flow(configs, sequences, integration_setup, options) self.run_exp() # Get the results back results = {} - for qubit in qubits.values(): - q = qubit.name # pylint: disable=C0103 - for i, ropulse in enumerate(self.sequence[measure_channel_name(qubit)]): - data = self.results.get_data(f"sequence{q}_{i}") - - if options.acquisition_type is AcquisitionType.DISCRIMINATION: - data = ( - np.ones(data.shape) - data.real - ) # Probability inversion patch - - id_ = ropulse.pulse.id - qubit = ropulse.pulse.qubit - results[id_] = results[qubit] = options.results_type(data) + for ch in probe_channels: + acquisition_name = self.channels[ch].logical_channel.acquisition + for i, seq in enumerate(self.sequences): + for j, ropulse in enumerate(seq[ch]): + handle = _acquisition_handle(i, j, acquisition_name) + data = self.results.get_data(handle) + + if options.acquisition_type is AcquisitionType.DISCRIMINATION: + data = ( + np.ones(data.shape) - data.real + ) # Probability inversion patch + + id_ = ropulse.id + results[id_] = options.results_type(data) return results diff --git a/src/qibolab/instruments/zhinst/pulse.py b/src/qibolab/instruments/zhinst/pulse.py index b9e068acf..cdfa2608e 100644 --- a/src/qibolab/instruments/zhinst/pulse.py +++ b/src/qibolab/instruments/zhinst/pulse.py @@ -1,8 +1,6 @@ """Wrapper for qibolab and laboneq pulses and sweeps.""" -from typing import Optional - -import laboneq.simple as lo +import laboneq.simple as laboneq import numpy as np from laboneq.dsl.experiment.pulse_library import ( sampled_pulse_complex, @@ -10,23 +8,22 @@ ) from qibolab.pulses import Drag, Gaussian, GaussianSquare, Pulse, PulseType, Rectangular -from qibolab.sweeper import Parameter -from .util import NANO_TO_SECONDS, SAMPLING_RATE +from .constants import NANO_TO_SECONDS, SAMPLING_RATE def select_pulse(pulse: Pulse): """Return laboneq pulse object corresponding to the given qibolab pulse.""" if isinstance(pulse.envelope, Rectangular): can_compress = pulse.type is not PulseType.READOUT - return lo.pulse_library.const( + return laboneq.pulse_library.const( length=round(pulse.duration * NANO_TO_SECONDS, 9), amplitude=pulse.amplitude, can_compress=can_compress, ) if isinstance(pulse.envelope, Gaussian): sigma = pulse.envelope.rel_sigma - return lo.pulse_library.gaussian( + return laboneq.pulse_library.gaussian( length=round(pulse.duration * NANO_TO_SECONDS, 9), amplitude=pulse.amplitude, sigma=2 / sigma, @@ -37,7 +34,7 @@ def select_pulse(pulse: Pulse): sigma = pulse.envelope.rel_sigma width = pulse.envelope.width can_compress = pulse.type is not PulseType.READOUT - return lo.pulse_library.gaussian_square( + return laboneq.pulse_library.gaussian_square( length=round(pulse.duration * NANO_TO_SECONDS, 9), width=round(pulse.duration * NANO_TO_SECONDS, 9) * width, amplitude=pulse.amplitude, @@ -49,7 +46,7 @@ def select_pulse(pulse: Pulse): if isinstance(pulse.envelope, Drag): sigma = pulse.envelope.rel_sigma beta = pulse.envelope.beta - return lo.pulse_library.drag( + return laboneq.pulse_library.drag( length=round(pulse.duration * NANO_TO_SECONDS, 9), amplitude=pulse.amplitude, sigma=2 / sigma, @@ -67,40 +64,3 @@ def select_pulse(pulse: Pulse): samples=pulse.i(SAMPLING_RATE) + (1j * pulse.q(SAMPLING_RATE)), can_compress=True, ) - - -class ZhPulse: - """Wrapper data type that holds a qibolab pulse, the corresponding laboneq - pulse object, and any sweeps associated with this pulse.""" - - def __init__(self, pulse): - self.pulse: Pulse = pulse - """Qibolab pulse.""" - self.zhpulse = select_pulse(pulse) - """Laboneq pulse.""" - self.zhsweepers: list[tuple[Parameter, lo.SweepParameter]] = [] - """Parameters to be swept, along with their laboneq sweep parameter - definitions.""" - self.delay_sweeper: Optional[lo.SweepParameter] = None - """Laboneq sweep parameter if the delay of the pulse should be - swept.""" - - # pylint: disable=R0903,E1101 - def add_sweeper(self, param: Parameter, sweeper: lo.SweepParameter): - """Add sweeper to list of sweepers associated with this pulse.""" - if param in { - Parameter.amplitude, - Parameter.frequency, - Parameter.duration, - Parameter.relative_phase, - }: - self.zhsweepers.append((param, sweeper)) - elif param is Parameter.start: - # TODO: Change this case to ``Delay.duration`` - if self.delay_sweeper: - raise ValueError( - "Cannot have multiple delay sweepers for a single pulse" - ) - self.delay_sweeper = sweeper - else: - raise ValueError(f"Sweeping {param} is not supported") diff --git a/src/qibolab/instruments/zhinst/sweep.py b/src/qibolab/instruments/zhinst/sweep.py index 4b918aa1e..24401b0e2 100644 --- a/src/qibolab/instruments/zhinst/sweep.py +++ b/src/qibolab/instruments/zhinst/sweep.py @@ -3,14 +3,14 @@ from collections.abc import Iterable from copy import copy -import laboneq.simple as lo -import numpy as np +import laboneq.simple as laboneq +from qibolab.components import Config from qibolab.pulses import Pulse, PulseType -from qibolab.qubits import Qubit from qibolab.sweeper import Parameter, Sweeper -from .util import NANO_TO_SECONDS, measure_channel_name +from . import ZiChannel +from .constants import NANO_TO_SECONDS def classify_sweepers( @@ -56,84 +56,45 @@ class ProcessedSweeps: in the sweep loop definition """ - def __init__(self, sweepers: Iterable[Sweeper], qubits: dict[str, Qubit]): + def __init__( + self, + sweepers: Iterable[Sweeper], + channels: dict[str, ZiChannel], + configs: dict[str, Config], + ): pulse_sweeps = [] channel_sweeps = [] parallel_sweeps = [] for sweeper in sweepers: for pulse in sweeper.pulses or []: if sweeper.parameter is Parameter.duration: - sweep_param = lo.SweepParameter( + sweep_param = laboneq.SweepParameter( values=sweeper.values * NANO_TO_SECONDS ) - pulse_sweeps.append((pulse, sweeper.parameter, sweep_param)) - elif sweeper.parameter is Parameter.frequency: - ptype, qubit = pulse.type, qubits[pulse.qubit] - if ptype is PulseType.READOUT: - ch = measure_channel_name(qubit) - intermediate_frequency = ( - qubit.readout_frequency - - qubit.readout.local_oscillator.frequency - ) - elif ptype is PulseType.DRIVE: - ch = qubit.drive.name - intermediate_frequency = ( - qubit.drive_frequency - - qubit.drive.local_oscillator.frequency - ) - else: - raise ValueError( - f"Cannot sweep frequency of pulse of type {ptype}, because it does not have associated frequency" - ) - sweep_param = lo.SweepParameter( - values=sweeper.values + intermediate_frequency - ) - channel_sweeps.append((ch, sweeper.parameter, sweep_param)) - elif ( - pulse.type is PulseType.READOUT - and sweeper.parameter is Parameter.amplitude - ): - max_value = max(np.abs(sweeper.values)) - sweep_param = lo.SweepParameter(values=sweeper.values / max_value) - # FIXME: this implicitly relies on the fact that pulse is the same python object as appears in the - # sequence that is being executed, hence the mutation is propagated. This is bad programming and - # should be fixed once things become simpler - pulse.amplitude *= max_value - - channel_sweeps.append( - ( - measure_channel_name(qubits[pulse.qubit]), - sweeper.parameter, - sweep_param, - ) - ) else: - sweep_param = lo.SweepParameter(values=copy(sweeper.values)) - pulse_sweeps.append((pulse, sweeper.parameter, sweep_param)) + sweep_param = laboneq.SweepParameter(values=copy(sweeper.values)) + pulse_sweeps.append((pulse, sweeper.parameter, sweep_param)) parallel_sweeps.append((sweeper, sweep_param)) - for qubit in sweeper.qubits or []: - if sweeper.parameter is not Parameter.bias: - raise ValueError( - f"Sweeping {sweeper.parameter.name} for {qubit} is not supported" + for ch in sweeper.channels or []: + logical_channel = channels[ch].logical_channel + if sweeper.parameter is Parameter.bias: + sweep_param = laboneq.SweepParameter( + values=sweeper.values + configs[logical_channel.name].offset ) - sweep_param = lo.SweepParameter( - values=sweeper.values + qubit.flux.offset - ) - channel_sweeps.append((qubit.flux.name, sweeper.parameter, sweep_param)) - parallel_sweeps.append((sweeper, sweep_param)) - - for coupler in sweeper.couplers or []: - if sweeper.parameter is not Parameter.bias: + elif sweeper.parameter is Parameter.frequency: + intermediate_frequency = ( + configs[logical_channel.name].frequency + - configs[logical_channel.lo].frequency + ) + sweep_param = laboneq.SweepParameter( + values=sweeper.values + intermediate_frequency + ) + else: raise ValueError( - f"Sweeping {sweeper.parameter.name} for {coupler} is not supported" + f"Sweeping {sweeper.parameter.name} for {ch} is not supported" ) - sweep_param = lo.SweepParameter( - values=sweeper.values + coupler.flux.offset - ) - channel_sweeps.append( - (coupler.flux.name, sweeper.parameter, sweep_param) - ) + channel_sweeps.append((ch, sweeper.parameter, sweep_param)) parallel_sweeps.append((sweeper, sweep_param)) self._pulse_sweeps = pulse_sweeps @@ -142,18 +103,20 @@ def __init__(self, sweepers: Iterable[Sweeper], qubits: dict[str, Qubit]): def sweeps_for_pulse( self, pulse: Pulse - ) -> list[tuple[Parameter, lo.SweepParameter]]: + ) -> list[tuple[Parameter, laboneq.SweepParameter]]: return [item[1:] for item in self._pulse_sweeps if item[0] == pulse] - def sweeps_for_channel(self, ch: str) -> list[tuple[Parameter, lo.SweepParameter]]: + def sweeps_for_channel( + self, ch: str + ) -> list[tuple[Parameter, laboneq.SweepParameter]]: return [item[1:] for item in self._channel_sweeps if item[0] == ch] - def sweeps_for_sweeper(self, sweeper: Sweeper) -> list[lo.SweepParameter]: + def sweeps_for_sweeper(self, sweeper: Sweeper) -> list[laboneq.SweepParameter]: return [item[1] for item in self._parallel_sweeps if item[0] == sweeper] def channel_sweeps_for_sweeper( self, sweeper: Sweeper - ) -> list[tuple[str, Parameter, lo.SweepParameter]]: + ) -> list[tuple[str, Parameter, laboneq.SweepParameter]]: return [ item for item in self._channel_sweeps diff --git a/src/qibolab/instruments/zhinst/util.py b/src/qibolab/instruments/zhinst/util.py deleted file mode 100644 index 8ca884306..000000000 --- a/src/qibolab/instruments/zhinst/util.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Utility methods.""" - -from qibolab.qubits import Qubit - -SAMPLING_RATE = 2 -NANO_TO_SECONDS = 1e-9 - - -def measure_channel_name(qubit: Qubit) -> str: - """Construct and return a name for qubit's measure channel. - - FIXME: We cannot use channel name directly, because currently channels are named after wires, and due to multiplexed readout - multiple qubits have the same channel name for their readout. Should be fixed once channels are refactored. - """ - return f"{qubit.readout.name}_{qubit.name}" - - -def acquire_channel_name(qubit: Qubit) -> str: - """Construct and return a name for qubit's acquire channel. - - FIXME: We cannot use acquire channel name, because qibolab does not have a concept of acquire channel. This function shall be removed - once all channel refactoring is done. - """ - return f"acquire{qubit.name}" diff --git a/src/qibolab/native.py b/src/qibolab/native.py index a3454eccb..681520031 100644 --- a/src/qibolab/native.py +++ b/src/qibolab/native.py @@ -1,28 +1,93 @@ from dataclasses import dataclass, field, fields from typing import Optional -from .pulses import Pulse, PulseSequence +import numpy as np + +from .pulses import Drag, Gaussian, PulseSequence from .serialize_ import replace +def _normalize_angles(theta, phi): + """Normalize theta to (-pi, pi], and phi to [0, 2*pi).""" + theta = theta % (2 * np.pi) + theta = theta - 2 * np.pi * (theta > np.pi) + phi = phi % (2 * np.pi) + return theta, phi + + +class RxyFactory: + """Factory for pulse sequences that generate single-qubit rotations around + an axis in xy plane. + + It is assumed that the underlying sequence contains only a single pulse. + It is assumed that the base sequence corresponds to a calibrated pi rotation around X axis. + Other rotation angles are achieved by scaling the amplitude, assuming a linear transfer function. + + Args: + sequence: The base sequence for the factory. + """ + + def __init__(self, sequence: PulseSequence): + if len(sequence) != 1: + raise ValueError( + f"Incompatible number of channels: {len(sequence)}. " + f"{self.__class__} expects a sequence on exactly one channel." + ) + + pulses = next(iter(sequence.values())) + if len(pulses) != 1: + raise ValueError( + f"Incompatible number of pulses: {len(pulses)}. " + f"{self.__class__} expects a sequence with exactly one pulse." + ) + + pulse = pulses[0] + expected_envelopes = (Gaussian, Drag) + if not isinstance(pulse.envelope, expected_envelopes): + raise ValueError( + f"Incompatible pulse envelope: {pulse.envelope.__class__}. " + f"{self.__class__} expects {expected_envelopes} envelope." + ) + + self._seq = sequence + + def create_sequence(self, theta: float = np.pi, phi: float = 0.0) -> PulseSequence: + """Create a sequence for single-qubit rotation. + + Args: + theta: the angle of rotation. + phi: the angle that rotation axis forms with x axis. + """ + theta, phi = _normalize_angles(theta, phi) + seq = self._seq.copy() + channel = next(iter(seq.keys())) + pulse = seq[channel][0] + new_amplitude = pulse.amplitude * theta / np.pi + seq[channel][0] = replace(pulse, amplitude=new_amplitude, relative_phase=phi) + return seq + + +class FixedSequenceFactory: + """Simple factory for a fixed arbitrary sequence.""" + + def __init__(self, sequence: PulseSequence): + self._seq = sequence + + def create_sequence(self) -> PulseSequence: + return self._seq.copy() + + @dataclass class SingleQubitNatives: """Container with the native single-qubit gates acting on a specific qubit.""" - RX: Optional[Pulse] = None + RX: Optional[RxyFactory] = None """Pulse to drive the qubit from state 0 to state 1.""" - RX12: Optional[Pulse] = None + RX12: Optional[FixedSequenceFactory] = None """Pulse to drive to qubit from state 1 to state 2.""" - MZ: Optional[Pulse] = None + MZ: Optional[FixedSequenceFactory] = None """Measurement pulse.""" - CP: Optional[Pulse] = None - """Pulse to activate a coupler.""" - - @property - def RX90(self) -> Pulse: - """RX90 native pulse is inferred from RX by halving its amplitude.""" - return replace(self.RX, amplitude=self.RX.amplitude / 2.0) @dataclass @@ -30,14 +95,14 @@ class TwoQubitNatives: """Container with the native two-qubit gates acting on a specific pair of qubits.""" - CZ: PulseSequence = field( - default_factory=lambda: PulseSequence(), metadata={"symmetric": True} + CZ: Optional[FixedSequenceFactory] = field( + default=None, metadata={"symmetric": True} ) - CNOT: PulseSequence = field( - default_factory=lambda: PulseSequence(), metadata={"symmetric": False} + CNOT: Optional[FixedSequenceFactory] = field( + default=None, metadata={"symmetric": False} ) - iSWAP: PulseSequence = field( - default_factory=lambda: PulseSequence(), metadata={"symmetric": True} + iSWAP: Optional[FixedSequenceFactory] = field( + default=None, metadata={"symmetric": True} ) @property @@ -45,6 +110,6 @@ def symmetric(self): """Check if the defined two-qubit gates are symmetric between target and control qubits.""" return all( - fld.metadata["symmetric"] or len(getattr(self, fld.name)) == 0 + fld.metadata["symmetric"] or getattr(self, fld.name) is None for fld in fields(self) ) diff --git a/src/qibolab/platform/platform.py b/src/qibolab/platform/platform.py index 3fbdfe147..edda234cc 100644 --- a/src/qibolab/platform/platform.py +++ b/src/qibolab/platform/platform.py @@ -1,17 +1,20 @@ """A platform for executing quantum algorithms.""" +import dataclasses from collections import defaultdict -from dataclasses import dataclass, field, fields +from dataclasses import asdict, dataclass, field from math import prod from typing import Any, Dict, List, Optional, Tuple, TypeVar import networkx as nx +import numpy as np from qibo.config import log, raise_error +from qibolab.components import Config from qibolab.couplers import Coupler -from qibolab.execution_parameters import ExecutionParameters +from qibolab.execution_parameters import ConfigUpdate, ExecutionParameters from qibolab.instruments.abstract import Controller, Instrument, InstrumentId -from qibolab.pulses import Delay, Drag, PulseSequence, PulseType +from qibolab.pulses import Delay, PulseSequence from qibolab.qubits import Qubit, QubitId, QubitPair, QubitPairId from qibolab.serialize_ import replace from qibolab.sweeper import ParallelSweepers @@ -53,23 +56,37 @@ def unroll_sequences( """ total_sequence = PulseSequence() readout_map = defaultdict(list) - channels = {pulse.channel for sequence in sequences for pulse in sequence} for sequence in sequences: total_sequence.extend(sequence) # TODO: Fix unrolling results - for pulse in sequence: - if pulse.type is PulseType.READOUT: - readout_map[pulse.id].append(pulse.id) + for pulse in sequence.probe_pulses: + readout_map[pulse.id].append(pulse.id) length = sequence.duration + relaxation_time - pulses_per_channel = sequence.pulses_per_channel - for channel in channels: - delay = length - pulses_per_channel[channel].duration - total_sequence.append(Delay(duration=delay, channel=channel)) + for channel in sequence.keys(): + delay = length - sequence.channel_duration(channel) + total_sequence[channel].append(Delay(duration=delay)) return total_sequence, readout_map +def update_configs(configs: dict[str, Config], updates: list[ConfigUpdate]): + """Apply updates to configs in place. + + Args: + configs: configs to update. Maps component name to respective config. + updates: list of config updates. Later entries in the list take precedence over earlier entries + (if they happen to update the same thing). + """ + for update in updates: + for name, changes in update.items(): + if name not in configs: + raise ValueError( + f"Cannot update configuration for unknown component {name}" + ) + configs[name] = dataclasses.replace(configs[name], **changes) + + def estimate_duration( sequences: list[PulseSequence], options: ExecutionParameters, @@ -120,6 +137,8 @@ class Platform: pairs: QubitPairMap """Dictionary mapping tuples of qubit names to :class:`qibolab.qubits.QubitPair` objects.""" + configs: dict[str, Config] + """Maps name of component to its default config.""" instruments: InstrumentMap """Dictionary mapping instrument names to :class:`qibolab.instruments.abstract.Instrument` objects.""" @@ -151,53 +170,6 @@ def __post_init__(self): self.topology.add_edges_from( [(pair.qubit1.name, pair.qubit2.name) for pair in self.pairs.values()] ) - self._set_channels_to_single_qubit_gates() - self._set_channels_to_two_qubit_gates() - - def _set_channels_to_single_qubit_gates(self): - """Set channels to pulses that implement single-qubit gates. - - This function should be removed when the duplication caused by - (``pulse.qubit``, ``pulse.type``) -> ``pulse.channel`` - is resolved. For now it just makes sure that the channels of - native pulses are consistent in order to test the rest of the code. - """ - for qubit in self.qubits.values(): - gates = qubit.native_gates - for fld in fields(gates): - pulse = getattr(gates, fld.name) - if pulse is not None: - channel = getattr(qubit, pulse.type.name.lower()).name - setattr(gates, fld.name, replace(pulse, channel=channel)) - for coupler in self.couplers.values(): - if gates.CP is not None: - gates.CP = replace(gates.CP, channel=coupler.flux.name) - - def _set_channels_to_two_qubit_gates(self): - """Set channels to pulses that implement single-qubit gates. - - This function should be removed when the duplication caused by - (``pulse.qubit``, ``pulse.type``) -> ``pulse.channel`` - is resolved. For now it just makes sure that the channels of - native pulses are consistent in order to test the rest of the code. - """ - for pair in self.pairs.values(): - gates = pair.native_gates - for fld in fields(gates): - sequence = getattr(gates, fld.name) - if len(sequence) > 0: - new_sequence = PulseSequence() - for pulse in sequence: - if pulse.type is PulseType.VIRTUALZ: - channel = self.qubits[pulse.qubit].drive.name - elif pulse.type is PulseType.COUPLERFLUX: - channel = self.couplers[pulse.qubit].flux.name - else: - channel = getattr( - self.qubits[pulse.qubit], pulse.type.name.lower() - ).name - new_sequence.append(replace(pulse, channel=channel)) - setattr(gates, fld.name, new_sequence) def __str__(self): return self.name @@ -220,6 +192,15 @@ def sampling_rate(self): if isinstance(instrument, Controller): return instrument.sampling_rate + @property + def components(self) -> set[str]: + """Names of all components available in the platform.""" + return set(self.configs.keys()) + + def config(self, name: str) -> Config: + """Returns configuration of given component.""" + return self.configs[name] + def connect(self): """Connect to all instruments.""" if not self.is_connected: @@ -258,14 +239,14 @@ def _controller(self): assert len(controllers) == 1 return controllers[0] - def _execute(self, sequence, options, sweepers): - """Executes sequence on the controllers.""" + def _execute(self, sequences, options, integration_setup, sweepers): + """Execute sequences on the controllers.""" result = {} for instrument in self.instruments.values(): if isinstance(instrument, Controller): new_result = instrument.play( - self.qubits, self.couplers, sequence, options, sweepers + options.updates, sequences, options, integration_setup, sweepers ) if isinstance(new_result, dict): result.update(new_result) @@ -278,7 +259,7 @@ def execute( options: ExecutionParameters, sweepers: Optional[list[ParallelSweepers]] = None, ) -> dict[Any, list]: - """Execute a pulse sequences. + """Execute pulse sequences. If any sweeper is passed, the execution is performed for the different values of sweeped parameters. @@ -296,12 +277,11 @@ def execute( platform = create_dummy() - sequence = PulseSequence() + qubit = platform.qubits[0] + sequence = qubit.native_gates.MZ.create_sequence() parameter = Parameter.frequency - pulse = platform.create_qubit_readout_pulse(qubit=0) - sequence.append(pulse) parameter_range = np.random.randint(10, size=10) - sweeper = [Sweeper(parameter, parameter_range, [pulse])] + sweeper = [Sweeper(parameter, parameter_range, channels=[qubit.probe.name])] platform.execute([sequence], ExecutionParameters(), [sweeper]) """ if sweepers is None: @@ -312,22 +292,28 @@ def execute( time = estimate_duration(sequences, options, sweepers) log.info(f"Minimal execution time: {time}") - # find readout pulses - ro_pulses = { - pulse.id: pulse.qubit - for sequence in sequences - for pulse in sequence.ro_pulses - } + configs = self.configs.copy() + update_configs(configs, options.updates) + + # for components that represent aux external instruments (e.g. lo) to the main control instrument + # set the config directly + for name, cfg in configs.items(): + if name in self.instruments: + self.instruments[name].setup(**asdict(cfg)) + + # maps acquisition channel name to corresponding kernel and iq_angle + # FIXME: this is temporary solution to deliver the information to drivers + # until we make acquisition channels first class citizens in the sequences + # so that each acquisition command carries the info with it. + integration_setup: dict[str, tuple[np.ndarray, float]] = {} + for qubit in self.qubits.values(): + integration_setup[qubit.acquisition.name] = (qubit.kernel, qubit.iq_angle) results = defaultdict(list) for b in batch(sequences, self._controller.bounds): - sequence, readouts = unroll_sequences(b, options.relaxation_time) - result = self._execute(sequence, options, sweepers) - for serial, new_serials in readouts.items(): - results[serial].extend(result[ser] for ser in new_serials) - - for serial, qubit in ro_pulses.items(): - results[qubit] = results[serial] + result = self._execute(b, options, integration_setup, sweepers) + for serial, data in result.items(): + results[serial].append(data) return results @@ -354,104 +340,3 @@ def get_coupler(self, coupler): return self.couplers[coupler] except KeyError: return list(self.couplers.values())[coupler] - - def create_RX90_pulse(self, qubit, relative_phase=0): - qubit = self.get_qubit(qubit) - return replace( - qubit.native_gates.RX90, - relative_phase=relative_phase, - channel=qubit.drive.name, - ) - - def create_RX_pulse(self, qubit, relative_phase=0): - qubit = self.get_qubit(qubit) - return replace( - qubit.native_gates.RX, - relative_phase=relative_phase, - channel=qubit.drive.name, - ) - - def create_RX12_pulse(self, qubit, relative_phase=0): - qubit = self.get_qubit(qubit) - return replace( - qubit.native_gates.RX12, - relative_phase=relative_phase, - channel=qubit.drive.name, - ) - - def create_CZ_pulse_sequence(self, qubits): - pair = tuple(self.get_qubit(q).name for q in qubits) - if pair not in self.pairs or len(self.pairs[pair].native_gates.CZ) == 0: - raise_error( - ValueError, - f"Calibration for CZ gate between qubits {qubits[0]} and {qubits[1]} not found.", - ) - return self.pairs[pair].native_gates.CZ - - def create_iSWAP_pulse_sequence(self, qubits): - pair = tuple(self.get_qubit(q).name for q in qubits) - if pair not in self.pairs or len(self.pairs[pair].native_gates.iSWAP) == 0: - raise_error( - ValueError, - f"Calibration for iSWAP gate between qubits {qubits[0]} and {qubits[1]} not found.", - ) - return self.pairs[pair].native_gates.iSWAP - - def create_CNOT_pulse_sequence(self, qubits): - pair = tuple(self.get_qubit(q).name for q in qubits) - if pair not in self.pairs or len(self.pairs[pair].native_gates.CNOT) == 0: - raise_error( - ValueError, - f"Calibration for CNOT gate between qubits {qubits[0]} and {qubits[1]} not found.", - ) - return self.pairs[pair].native_gates.CNOT - - def create_MZ_pulse(self, qubit): - qubit = self.get_qubit(qubit) - return replace(qubit.native_gates.MZ, channel=qubit.readout.name) - - def create_qubit_drive_pulse(self, qubit, duration, relative_phase=0): - qubit = self.get_qubit(qubit) - return replace( - qubit.native_gates.RX, - duration=duration, - relative_phase=relative_phase, - channel=qubit.drive.name, - ) - - def create_qubit_readout_pulse(self, qubit): - return self.create_MZ_pulse(qubit) - - def create_coupler_pulse(self, coupler, duration=None, amplitude=None): - coupler = self.get_coupler(coupler) - pulse = coupler.native_gates.CP - if duration is not None: - pulse = replace(pulse, duration=duration) - if amplitude is not None: - pulse = replace(pulse, amplitude=amplitude) - return replace(pulse, channel=coupler.flux.name) - - # TODO Remove RX90_drag_pulse and RX_drag_pulse, replace them with create_qubit_drive_pulse - # TODO Add RY90 and RY pulses - - def create_RX90_drag_pulse(self, qubit, beta, relative_phase=0): - """Create native RX90 pulse with Drag shape.""" - qubit = self.get_qubit(qubit) - pulse = qubit.native_gates.RX90 - return replace( - pulse, - relative_phase=relative_phase, - envelope=Drag(rel_sigma=pulse.envelope.rel_sigma, beta=beta), - channel=qubit.drive.name, - ) - - def create_RX_drag_pulse(self, qubit, beta, relative_phase=0): - """Create native RX pulse with Drag shape.""" - qubit = self.get_qubit(qubit) - pulse = qubit.native_gates.RX - return replace( - pulse, - relative_phase=relative_phase, - envelope=Drag(rel_sigma=pulse.envelope.rel_sigma, beta=beta), - channel=qubit.drive.name, - ) diff --git a/src/qibolab/pulses/plot.py b/src/qibolab/pulses/plot.py index 87319febf..16e7189c8 100644 --- a/src/qibolab/pulses/plot.py +++ b/src/qibolab/pulses/plot.py @@ -1,6 +1,7 @@ """Plotting tools for pulses and related entities.""" from collections import defaultdict +from typing import Optional import matplotlib.pyplot as plt import numpy as np @@ -36,10 +37,11 @@ def waveform(wf: Waveform, filename=None): plt.close() -def pulse(pulse_: Pulse, filename=None): +def pulse(pulse_: Pulse, freq: Optional[float] = None, filename: Optional[str] = None): """Plot the pulse envelope and modulated waveforms. Args: + freq: Carrier frequency used to plot modulated waveform. None if modulated plot is not needed. filename (str): a file path. If provided the plot is save to a file. """ import matplotlib.pyplot as plt @@ -69,9 +71,16 @@ def pulse(pulse_: Pulse, filename=None): ) envelope = pulse_.envelopes(SAMPLING_RATE) - modulated = modulate(np.array(envelope), pulse_.frequency, rate=SAMPLING_RATE) - ax1.plot(time, modulated[0], label="modulated i", c="C0") - ax1.plot(time, modulated[1], label="modulated q", c="C1") + modulated = ( + modulate(np.array(envelope), freq, rate=SAMPLING_RATE) + if freq is not None + else None + ) + + if modulated is not None: + ax1.plot(time, modulated[0], label="modulated i", c="C0") + ax1.plot(time, modulated[1], label="modulated q", c="C1") + ax1.plot(time, -waveform_i, c="silver", linestyle="dashed") ax1.set_xlabel("Time [ns]") ax1.set_ylabel("Amplitude") @@ -83,24 +92,25 @@ def pulse(pulse_: Pulse, filename=None): ax1.legend() ax2 = plt.subplot(gs[1]) - ax2.plot(modulated[0], modulated[1], label="modulated", c="C3") ax2.plot(waveform_i, waveform_q, label="envelope", c="C2") - ax2.plot( - modulated[0][0], - modulated[1][0], - marker="o", - markersize=5, - label="start", - c="lightcoral", - ) - ax2.plot( - modulated[0][-1], - modulated[1][-1], - marker="o", - markersize=5, - label="finish", - c="darkred", - ) + if modulated is not None: + ax2.plot(modulated[0], modulated[1], label="modulated", c="C3") + ax2.plot( + modulated[0][0], + modulated[1][0], + marker="o", + markersize=5, + label="start", + c="lightcoral", + ) + ax2.plot( + modulated[0][-1], + modulated[1][-1], + marker="o", + markersize=5, + label="finish", + c="darkred", + ) ax2.plot( np.cos(time * 2 * np.pi / pulse_.duration), @@ -120,64 +130,66 @@ def pulse(pulse_: Pulse, filename=None): plt.close() -def sequence(ps: PulseSequence, filename=None): +def sequence(ps: PulseSequence, freq: dict[str, float], filename=None): """Plot the sequence of pulses. Args: + freq: frequency per channel, used to plot modulated waveforms of corresponding pulses. If a channel is missing from this dict, + only /un-modulated/ waveforms are plotted for that channel. filename (str): a file path. If provided the plot is save to a file. """ if len(ps) > 0: import matplotlib.pyplot as plt from matplotlib import gridspec - _ = plt.figure(figsize=(14, 2 * len(ps)), dpi=200) - gs = gridspec.GridSpec(ncols=1, nrows=len(ps)) + num_pulses = sum(len(pulses) for pulses in ps.values()) + _ = plt.figure(figsize=(14, 2 * num_pulses), dpi=200) + gs = gridspec.GridSpec(ncols=1, nrows=num_pulses) vertical_lines = [] starts = defaultdict(int) - for pulse in ps: - if not isinstance(pulse, Delay): - vertical_lines.append(starts[pulse.channel]) - vertical_lines.append(starts[pulse.channel] + pulse.duration) - starts[pulse.channel] += pulse.duration + for ch, pulses in ps.items(): + for pulse in pulses: + if not isinstance(pulse, Delay): + vertical_lines.append(starts[ch]) + vertical_lines.append(starts[ch] + pulse.duration) + starts[ch] += pulse.duration n = -1 - for qubit in ps.qubits: - qubit_pulses = ps.get_qubit_pulses(qubit) - for channel in qubit_pulses.channels: - n += 1 - channel_pulses = qubit_pulses.get_channel_pulses(channel) - ax = plt.subplot(gs[n]) - ax.axis([0, ps.duration, -1, 1]) - start = 0 - for pulse in channel_pulses: - if isinstance(pulse, Delay): - start += pulse.duration - continue - - envelope = pulse.envelopes(SAMPLING_RATE) - num_samples = envelope[0].size - time = start + np.arange(num_samples) / SAMPLING_RATE + for ch, pulses in ps.items(): + n += 1 + ax = plt.subplot(gs[n]) + ax.axis([0, ps.duration, -1, 1]) + start = 0 + for pulse in pulses: + if isinstance(pulse, Delay): + start += pulse.duration + continue + + envelope = pulse.envelopes(SAMPLING_RATE) + num_samples = envelope[0].size + time = start + np.arange(num_samples) / SAMPLING_RATE + if ch in freq: modulated = modulate( - np.array(envelope), pulse.frequency, rate=SAMPLING_RATE + np.array(envelope), freq[ch], rate=SAMPLING_RATE ) ax.plot(time, modulated[1], c="lightgrey") ax.plot(time, modulated[0], c=f"C{str(n)}") - ax.plot(time, pulse.i(SAMPLING_RATE), c=f"C{str(n)}") - ax.plot(time, -pulse.i(SAMPLING_RATE), c=f"C{str(n)}") - # TODO: if they overlap use different shades - ax.axhline(0, c="dimgrey") - ax.set_ylabel(f"qubit {qubit} \n channel {channel}") - for vl in vertical_lines: - ax.axvline(vl, c="slategrey", linestyle="--") - ax.axis((0, ps.duration, -1, 1)) - ax.grid( - visible=True, - which="both", - axis="both", - color="#CCCCCC", - linestyle="-", - ) - start += pulse.duration + ax.plot(time, pulse.i(SAMPLING_RATE), c=f"C{str(n)}") + ax.plot(time, -pulse.i(SAMPLING_RATE), c=f"C{str(n)}") + # TODO: if they overlap use different shades + ax.axhline(0, c="dimgrey") + ax.set_ylabel(f"channel {ch}") + for vl in vertical_lines: + ax.axvline(vl, c="slategrey", linestyle="--") + ax.axis((0, ps.duration, -1, 1)) + ax.grid( + visible=True, + which="both", + axis="both", + color="#CCCCCC", + linestyle="-", + ) + start += pulse.duration if filename: plt.savefig(filename) diff --git a/src/qibolab/pulses/pulse.py b/src/qibolab/pulses/pulse.py index 3c2c59b55..c2b8259e9 100644 --- a/src/qibolab/pulses/pulse.py +++ b/src/qibolab/pulses/pulse.py @@ -2,7 +2,7 @@ from dataclasses import fields from enum import Enum -from typing import Optional, Union +from typing import Union import numpy as np @@ -38,11 +38,6 @@ class Pulse(Model): Pulse amplitudes are normalised between -1 and 1. """ - frequency: float - """Pulse Intermediate Frequency in Hz. - - The value has to be in the range [10e6 to 300e6]. - """ envelope: Envelope """The pulse envelope shape. @@ -51,18 +46,8 @@ class Pulse(Model): """ relative_phase: float = 0.0 """Relative phase of the pulse, in radians.""" - channel: Optional[str] = None - """Channel on which the pulse should be played. - - When a sequence of pulses is sent to the platform for execution, - each pulse is sent to the instrument responsible for playing pulses - the pulse channel. The connection of instruments with channels is - defined in the platform runcard. - """ type: PulseType = PulseType.DRIVE """Pulse type, as an element of PulseType enumeration.""" - qubit: int = 0 - """Qubit or coupler addressed by the pulse.""" @classmethod def flux(cls, **kwargs): @@ -71,7 +56,6 @@ def flux(cls, **kwargs): It provides a simplified syntax for the :cls:`Pulse` constructor, by applying suitable defaults. """ - kwargs["frequency"] = 0 kwargs["relative_phase"] = 0 if "type" not in kwargs: kwargs["type"] = PulseType.FLUX @@ -125,8 +109,6 @@ class Delay(Model): duration: int """Delay duration in ns.""" - channel: str - """Channel on which the delay should be implemented.""" type: PulseType = PulseType.DELAY """Type fixed to ``DELAY`` to comply with ``Pulse`` interface.""" @@ -136,10 +118,6 @@ class VirtualZ(Model): phase: float """Phase that implements the rotation.""" - channel: Optional[str] = None - """Channel on which the virtual phase should be added.""" - qubit: int = 0 - """Qubit on the drive of which the virtual phase should be added.""" type: PulseType = PulseType.VIRTUALZ @property diff --git a/src/qibolab/pulses/sequence.py b/src/qibolab/pulses/sequence.py index b48cb62cd..beb6b03c6 100644 --- a/src/qibolab/pulses/sequence.py +++ b/src/qibolab/pulses/sequence.py @@ -1,159 +1,54 @@ """PulseSequence class.""" from collections import defaultdict +from typing import Optional -from .pulse import PulseType +from .pulse import Delay, PulseLike, PulseType -class PulseSequence(list): - """A collection of scheduled pulses. +class PulseSequence(defaultdict[str, list[PulseLike]]): + """Synchronized sequence of control instructions across multiple channels. - A quantum circuit can be translated into a set of scheduled pulses - that implement the circuit gates. This class contains many - supporting fuctions to facilitate the creation and manipulation of - these collections of pulses. None of the methods of PulseSequence - modify any of the properties of its pulses. + The keys are names of channels, and the values are lists of pulses + and delays """ - def __add__(self, other): - """Return self+value.""" - return type(self)(super().__add__(other)) - - def __mul__(self, other): - """Return self*value.""" - return type(self)(super().__mul__(other)) - - def __repr__(self): - """Return repr(self).""" - return f"{type(self).__name__}({super().__repr__()})" - - def copy(self): - """Return a shallow copy of the sequence.""" - return type(self)(super().copy()) + def __init__(self, seq_dict: Optional[dict[str, list[PulseLike]]] = None): + initial_content = seq_dict if seq_dict is not None else {} + super().__init__(list, **initial_content) @property - def ro_pulses(self): - """A new sequence containing only its readout pulses.""" - new_pc = PulseSequence() - for pulse in self: - if pulse.type == PulseType.READOUT: - new_pc.append(pulse) - return new_pc - - @property - def qd_pulses(self): - """A new sequence containing only its qubit drive pulses.""" - new_pc = PulseSequence() - for pulse in self: - if pulse.type == PulseType.DRIVE: - new_pc.append(pulse) - return new_pc - - @property - def qf_pulses(self): - """A new sequence containing only its qubit flux pulses.""" - new_pc = PulseSequence() - for pulse in self: - if pulse.type == PulseType.FLUX: - new_pc.append(pulse) - return new_pc - - @property - def cf_pulses(self): - """A new sequence containing only its coupler flux pulses.""" - new_pc = PulseSequence() - for pulse in self: - if pulse.type is PulseType.COUPLERFLUX: - new_pc.append(pulse) - return new_pc - - def get_channel_pulses(self, *channels): - """Return a new sequence containing the pulses on some channels.""" - new_pc = PulseSequence() - for pulse in self: - if pulse.channel in channels: - new_pc.append(pulse) - return new_pc - - def get_qubit_pulses(self, *qubits): - """Return a new sequence containing the pulses on some qubits.""" - new_pc = PulseSequence() - for pulse in self: - if pulse.type is not PulseType.COUPLERFLUX: - if pulse.qubit in qubits: - new_pc.append(pulse) - return new_pc - - def coupler_pulses(self, *couplers): - """Return a new sequence containing the pulses on some couplers.""" - new_pc = PulseSequence() - for pulse in self: - if pulse.type is not PulseType.COUPLERFLUX: - if pulse.qubit in couplers: - new_pc.append(pulse) - return new_pc - - @property - def pulses_per_channel(self): - """Return a dictionary with the sequence per channel.""" - sequences = defaultdict(type(self)) - for pulse in self: - sequences[pulse.channel].append(pulse) - return sequences + def probe_pulses(self): + """Return list of the readout pulses in this sequence.""" + pulses = [] + for seq in self.values(): + for pulse in seq: + if pulse.type == PulseType.READOUT: + pulses.append(pulse) + return pulses @property def duration(self) -> int: - """The time when the last pulse of the sequence finishes.""" - channel_pulses = self.pulses_per_channel - if len(channel_pulses) == 1: - pulses = next(iter(channel_pulses.values())) - return sum(pulse.duration for pulse in pulses) - return max( - (sequence.duration for sequence in channel_pulses.values()), default=0 - ) - - @property - def channels(self) -> list: - """List containing the channels used by the pulses in the sequence.""" - channels = [] - for pulse in self: - if pulse.channel not in channels: - channels.append(pulse.channel) - - return channels - - @property - def qubits(self) -> list: - """The qubits associated with the pulses in the sequence.""" - qubits = [] - for pulse in self: - if not pulse.qubit in qubits: - qubits.append(pulse.qubit) - qubits.sort() - return qubits - - def separate_overlapping_pulses(self): # -> dict((int,int): PulseSequence): - """Separate a sequence of overlapping pulses into a list of non- - overlapping sequences.""" - # This routine separates the pulses of a sequence into non-overlapping sets - # but it does not check if the frequencies of the pulses within a set have the same frequency - - separated_pulses = [] - for new_pulse in self: - stored = False - for ps in separated_pulses: - overlaps = False - for existing_pulse in ps: - if ( - new_pulse.start < existing_pulse.finish - and new_pulse.finish > existing_pulse.start - ): - overlaps = True - break - if not overlaps: - ps.append(new_pulse) - stored = True - break - if not stored: - separated_pulses.append(PulseSequence([new_pulse])) - return separated_pulses + """Duration of the entire sequence.""" + return max((self.channel_duration(ch) for ch in self), default=0) + + def channel_duration(self, channel: str) -> float: + """Duration of the given channel.""" + return sum(item.duration for item in self[channel]) + + def extend(self, other: "PulseSequence") -> None: + """Appends other in-place such that the result is self + necessary + delays to synchronize channels + other.""" + tol = 1e-12 + durations = {ch: self.channel_duration(ch) for ch in other} + max_duration = max(durations.values(), default=0.0) + for ch, duration in durations.items(): + if (delay := round(max_duration - duration, int(1 / tol))) > 0: + self[ch].append(Delay(duration=delay)) + self[ch].extend(other[ch]) + + def copy(self) -> "PulseSequence": + """Return shallow copy of self.""" + ps = PulseSequence() + ps.extend(self) + return ps diff --git a/src/qibolab/qubits.py b/src/qibolab/qubits.py index 547b1f2b1..885845053 100644 --- a/src/qibolab/qubits.py +++ b/src/qibolab/qubits.py @@ -3,14 +3,14 @@ import numpy as np -from qibolab.channels import Channel +from qibolab.components import AcquireChannel, DcChannel, IqChannel from qibolab.couplers import Coupler from qibolab.native import SingleQubitNatives, TwoQubitNatives QubitId = Union[str, int] """Type for qubit names.""" -CHANNEL_NAMES = ("readout", "feedback", "drive", "flux", "twpa") +CHANNEL_NAMES = ("probe", "acquisition", "drive", "drive12", "drive_cross", "flux") """Names of channels that belong to a qubit. Not all channels are required to operate a qubit. @@ -19,7 +19,6 @@ "name", "native_gates", "kernel", - "_flux", "qubit1", "qubit2", "coupler", @@ -39,8 +38,6 @@ class Qubit: name (int, str): Qubit number or name. readout (:class:`qibolab.platforms.utils.Channel`): Channel used to readout pulses to the qubit. - feedback (:class:`qibolab.platforms.utils.Channel`): Channel used to - get readout feedback from the qubit. drive (:class:`qibolab.platforms.utils.Channel`): Channel used to send drive pulses to the qubit. flux (:class:`qibolab.platforms.utils.Channel`): Channel used to @@ -51,11 +48,7 @@ class Qubit: name: QubitId bare_resonator_frequency: int = 0 - readout_frequency: int = 0 - """Readout dressed frequency.""" - drive_frequency: int = 0 anharmonicity: int = 0 - sweetspot: float = 0.0 asymmetry: float = 0.0 crosstalk_matrix: dict[QubitId, float] = field(default_factory=dict) """Crosstalk matrix for voltages.""" @@ -89,33 +82,15 @@ class Qubit: threshold: Optional[float] = None iq_angle: float = 0.0 kernel: Optional[np.ndarray] = field(default=None, repr=False) - # required for mixers (not sure if it should be here) - mixer_drive_g: float = 0.0 - mixer_drive_phi: float = 0.0 - mixer_readout_g: float = 0.0 - mixer_readout_phi: float = 0.0 - - readout: Optional[Channel] = None - feedback: Optional[Channel] = None - twpa: Optional[Channel] = None - drive: Optional[Channel] = None - _flux: Optional[Channel] = None - native_gates: SingleQubitNatives = field(default_factory=SingleQubitNatives) - - def __post_init__(self): - if self.flux is not None and self.sweetspot != 0: - self.flux.offset = self.sweetspot + probe: Optional[IqChannel] = None + acquisition: Optional[AcquireChannel] = None + drive: Optional[IqChannel] = None + drive12: Optional[IqChannel] = None + drive_cross: Optional[dict[QubitId, IqChannel]] = None + flux: Optional[DcChannel] = None - @property - def flux(self): - return self._flux - - @flux.setter - def flux(self, channel): - if self.sweetspot != 0: - channel.offset = self.sweetspot - self._flux = channel + native_gates: SingleQubitNatives = field(default_factory=SingleQubitNatives) @property def channels(self): diff --git a/src/qibolab/serialize.py b/src/qibolab/serialize.py index 751e28749..0c10f7135 100644 --- a/src/qibolab/serialize.py +++ b/src/qibolab/serialize.py @@ -8,13 +8,19 @@ import json from dataclasses import asdict, fields from pathlib import Path -from typing import Tuple +from typing import Optional, Tuple, Union from pydantic import ConfigDict, TypeAdapter from qibolab.couplers import Coupler +from qibolab.execution_parameters import ConfigUpdate from qibolab.kernels import Kernels -from qibolab.native import SingleQubitNatives, TwoQubitNatives +from qibolab.native import ( + FixedSequenceFactory, + RxyFactory, + SingleQubitNatives, + TwoQubitNatives, +) from qibolab.platform.platform import ( CouplerMap, InstrumentMap, @@ -22,8 +28,9 @@ QubitMap, QubitPairMap, Settings, + update_configs, ) -from qibolab.pulses import Pulse, PulseSequence, PulseType +from qibolab.pulses import Pulse, PulseSequence from qibolab.pulses.pulse import PulseLike from qibolab.qubits import Qubit, QubitId, QubitPair @@ -111,45 +118,43 @@ def load_qubits( """ -def _load_pulse(pulse_kwargs: dict, qubit: Qubit): - coupler = "coupler" in pulse_kwargs - pulse_kwargs["qubit"] = pulse_kwargs.pop( - "coupler" if coupler else "qubit", qubit.name - ) - +def _load_pulse(pulse_kwargs: dict): return _PulseLike.validate_python(pulse_kwargs) -def _load_single_qubit_natives(qubit, gates) -> SingleQubitNatives: - """Parse native gates of the qubit from the runcard. +def _load_sequence(raw_sequence): + seq = PulseSequence() + for ch, pulses in raw_sequence.items(): + seq[ch] = [_load_pulse(raw_pulse) for raw_pulse in pulses] + return seq + + +def _load_single_qubit_natives(gates) -> SingleQubitNatives: + """Parse native gates from the runcard. Args: - qubit (:class:`qibolab.qubits.Qubit`): Qubit object that the - native gates are acting on. gates (dict): Dictionary with native gate pulse parameters as loaded from the runcard. """ return SingleQubitNatives( - **{name: _load_pulse(kwargs, qubit) for name, kwargs in gates.items()} + **{ + gate_name: ( + RxyFactory(_load_sequence(raw_sequence)) + if gate_name == "RX" + else FixedSequenceFactory(_load_sequence(raw_sequence)) + ) + for gate_name, raw_sequence in gates.items() + } ) -def _load_two_qubit_natives(qubits, couplers, gates) -> TwoQubitNatives: - sequences = {} - for name, seq_kwargs in gates.items(): - if isinstance(seq_kwargs, dict): - seq_kwargs = [seq_kwargs] - - sequence = PulseSequence() - for kwargs in seq_kwargs: - if "coupler" in kwargs: - qubit = couplers[kwargs["coupler"]] - else: - qubit = qubits[kwargs["qubit"]] - sequence.append(_load_pulse(kwargs, qubit)) - sequences[name] = sequence - - return TwoQubitNatives(**sequences) +def _load_two_qubit_natives(gates) -> TwoQubitNatives: + return TwoQubitNatives( + **{ + gate_name: FixedSequenceFactory(_load_sequence(raw_sequence)) + for gate_name, raw_sequence in gates.items() + } + ) def register_gates( @@ -166,16 +171,16 @@ def register_gates( native_gates = runcard.get("native_gates", {}) for q, gates in native_gates.get("single_qubit", {}).items(): qubit = qubits[load_qubit_name(q)] - qubit.native_gates = _load_single_qubit_natives(qubit, gates) + qubit.native_gates = _load_single_qubit_natives(gates) for c, gates in native_gates.get("coupler", {}).items(): coupler = couplers[load_qubit_name(c)] - coupler.native_gates = _load_single_qubit_natives(coupler, gates) + coupler.native_gates = _load_single_qubit_natives(gates) # register two-qubit native gates to ``QubitPair`` objects for pair, gatedict in native_gates.get("two_qubit", {}).items(): q0, q1 = tuple(int(q) if q.isdigit() else q for q in pair.split("-")) - native_gates = _load_two_qubit_natives(qubits, couplers, gatedict) + native_gates = _load_two_qubit_natives(gatedict) coupler = pairs[(q0, q1)].coupler pairs[(q0, q1)] = QubitPair( qubits[q0], qubits[q1], coupler=coupler, native_gates=native_gates @@ -212,27 +217,16 @@ def _dump_pulse(pulse: Pulse): return data -def _dump_single_qubit_natives(natives: SingleQubitNatives): - data = {} - for fld in fields(natives): - pulse = getattr(natives, fld.name) - if pulse is not None: - data[fld.name] = _dump_pulse(pulse) - del data[fld.name]["qubit"] - return data +def _dump_sequence(sequence: PulseSequence): + return {ch: [_dump_pulse(p) for p in pulses] for ch, pulses in sequence.items()} -def _dump_two_qubit_natives(natives: TwoQubitNatives): +def _dump_natives(natives: Union[SingleQubitNatives, TwoQubitNatives]): data = {} for fld in fields(natives): - sequence = getattr(natives, fld.name) - if len(sequence) > 0: - data[fld.name] = [] - for pulse in sequence: - pulse_serial = _dump_pulse(pulse) - if pulse.type == PulseType.COUPLERFLUX: - pulse_serial["coupler"] = pulse_serial.pop("qubit") - data[fld.name].append(pulse_serial) + factory = getattr(natives, fld.name) + if factory is not None: + data[fld.name] = _dump_sequence(factory._seq) return data @@ -244,21 +238,15 @@ def dump_native_gates( # single-qubit native gates native_gates = { "single_qubit": { - dump_qubit_name(q): _dump_single_qubit_natives(qubit.native_gates) + dump_qubit_name(q): _dump_natives(qubit.native_gates) for q, qubit in qubits.items() } } - if couplers: - native_gates["coupler"] = { - dump_qubit_name(c): _dump_single_qubit_natives(coupler.native_gates) - for c, coupler in couplers.items() - } - # two-qubit native gates native_gates["two_qubit"] = {} for pair in pairs.values(): - natives = _dump_two_qubit_natives(pair.native_gates) + natives = _dump_natives(pair.native_gates) if len(natives) > 0: pair_name = f"{pair.qubit1.name}-{pair.qubit2.name}" native_gates["two_qubit"][pair_name] = natives @@ -316,7 +304,14 @@ def dump_instruments(instruments: InstrumentMap) -> dict: return data -def dump_runcard(platform: Platform, path: Path): +def dump_component_configs(component_configs) -> dict: + """Dump channel configs.""" + return {name: asdict(cfg) for name, cfg in component_configs.items()} + + +def dump_runcard( + platform: Platform, path: Path, updates: Optional[list[ConfigUpdate]] = None +): """Serializes the platform and saves it as a json runcard file. The file saved follows the format explained in :ref:`Using runcards `. @@ -324,14 +319,20 @@ def dump_runcard(platform: Platform, path: Path): Args: platform (qibolab.platform.Platform): The platform to be serialized. path (pathlib.Path): Path that the json file will be saved. + updates: List if updates for platform configs. + Later entries in the list take precedence over earlier ones (if they happen to update the same thing). """ + configs = platform.configs.copy() + update_configs(configs, updates or []) + settings = { "nqubits": platform.nqubits, "settings": asdict(platform.settings), "qubits": list(platform.qubits), "topology": [list(pair) for pair in platform.ordered_pairs], "instruments": dump_instruments(platform.instruments), + "components": dump_component_configs(configs), } if platform.couplers: @@ -371,13 +372,17 @@ def dump_kernels(platform: Platform, path: Path): kernels.dump(path) -def dump_platform(platform: Platform, path: Path): +def dump_platform( + platform: Platform, path: Path, updates: Optional[list[ConfigUpdate]] = None +): """Platform serialization as runcard (json) and kernels (npz). Args: platform (qibolab.platform.Platform): The platform to be serialized. path (pathlib.Path): Path where json and npz will be dumped. + updates: List if updates for platform configs. + Later entries in the list take precedence over earlier ones (if they happen to update the same thing). """ dump_kernels(platform=platform, path=path) - dump_runcard(platform=platform, path=path) + dump_runcard(platform=platform, path=path, updates=updates) diff --git a/src/qibolab/sweeper.py b/src/qibolab/sweeper.py index 4a49bc63f..91107aef6 100644 --- a/src/qibolab/sweeper.py +++ b/src/qibolab/sweeper.py @@ -38,7 +38,12 @@ class SweeperType(Enum): OFFSET = operator.add -QubitParameter = {Parameter.bias, Parameter.attenuation, Parameter.gain} +ChannelParameter = { + Parameter.frequency, + Parameter.bias, + Parameter.attenuation, + Parameter.gain, +} @dataclass @@ -60,21 +65,20 @@ class Sweeper: platform = create_dummy() - sequence = PulseSequence() + qubit = platform.qubits[0] + sequence = qubit.native_gates.MZ.create_sequence() parameter = Parameter.frequency - pulse = platform.create_qubit_readout_pulse(qubit=0) - sequence.append(pulse) parameter_range = np.random.randint(10, size=10) - sweeper = Sweeper(parameter, parameter_range, [pulse]) + sweeper = Sweeper(parameter, parameter_range, channels=[qubit.probe.name]) platform.execute([sequence], ExecutionParameters(), [[sweeper]]) Args: - parameter (`qibolab.sweeper.Parameter`): parameter to be swept, possible choices are frequency, attenuation, amplitude, current and gain. - _values (np.ndarray): sweep range. If the parameter of the sweep is a pulse parameter, if the sweeper type is not ABSOLUTE, the base value + parameter: parameter to be swept, possible choices are frequency, attenuation, amplitude, current and gain. + values: sweep range. If the parameter of the sweep is a pulse parameter, if the sweeper type is not ABSOLUTE, the base value will be taken from the runcard pulse parameters. If the sweep parameter is Bias, the base value will be the sweetspot of the qubits. - pulses (list) : list of `qibolab.pulses.Pulse` to be swept (optional). - qubits (list): list of `qibolab.platforms.abstract.Qubit` to be swept (optional). - type (SweeperType): can be ABSOLUTE (the sweeper range is swept directly), + pulses : list of `qibolab.pulses.Pulse` to be swept. + channels: list of channel names for which the parameter should be swept. + type: can be ABSOLUTE (the sweeper range is swept directly), FACTOR (sweeper values are multiplied by base value), OFFSET (sweeper values are added to base value) """ @@ -82,30 +86,25 @@ class Sweeper: parameter: Parameter values: npt.NDArray pulses: Optional[list] = None - qubits: Optional[list] = None - couplers: Optional[list] = None + channels: Optional[list] = None type: Optional[SweeperType] = SweeperType.ABSOLUTE def __post_init__(self): - if ( - self.pulses is not None - and self.qubits is not None - and self.couplers is not None - ): - raise ValueError("Cannot use a sweeper on both pulses and qubits.") - if self.pulses is not None and self.parameter in QubitParameter: + if self.pulses is not None and self.channels is not None: raise ValueError( - f"Cannot sweep {self.parameter} without specifying qubits or couplers." + "Cannot create a sweeper by using both pulses and channels." ) - if self.parameter not in QubitParameter and ( - self.qubits is not None or self.couplers is not None - ): + if self.pulses is not None and self.parameter in ChannelParameter: raise ValueError( - f"Cannot sweep {self.parameter} without specifying pulses." + f"Cannot create a sweeper for {self.parameter} without specifying channels." ) - if self.pulses is None and self.qubits is None and self.couplers is None: + if self.parameter not in ChannelParameter and (self.channels is not None): raise ValueError( - "Cannot use a sweeper without specifying pulses, qubits or couplers." + f"Cannot create a sweeper for {self.parameter} without specifying pulses." + ) + if self.pulses is None and self.channels is None: + raise ValueError( + "Cannot create a sweeper without specifying pulses or channels." ) def get_values(self, base_value): diff --git a/src/qibolab/unrolling.py b/src/qibolab/unrolling.py index 273b7043f..bccf99fa6 100644 --- a/src/qibolab/unrolling.py +++ b/src/qibolab/unrolling.py @@ -5,6 +5,7 @@ from dataclasses import asdict, dataclass, field, fields from functools import total_ordering +from itertools import chain from .pulses import Pulse, PulseSequence from .pulses.envelope import Rectangular @@ -21,13 +22,13 @@ def _waveform(sequence: PulseSequence): if isinstance(pulse, Pulse) else 1 ) - for pulse in sequence + for pulse in chain(*sequence.values()) ) def _readout(sequence: PulseSequence): # TODO: Do we count 1 readout per pulse or 1 readout per multiplexed readout ? - return len(sequence.ro_pulses) + return len(sequence.probe_pulses) def _instructions(sequence: PulseSequence): diff --git a/tests/conftest.py b/tests/conftest.py index 39be15868..29419dc58 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,12 +7,12 @@ from qibolab.platform.load import PLATFORMS ORIGINAL_PLATFORMS = os.environ.get(PLATFORMS, "") -TESTING_PLATFORM_NAMES = [ +TESTING_PLATFORM_NAMES = [ # FIXME: uncomment platforms as they get upgraded to 0.2 "dummy_couplers", - "qm", - "qm_octave", - "qblox", - "rfsoc", + # "qm", + # "qm_octave", + # "qblox", + # "rfsoc", "zurich", ] """Platforms used for testing without access to real instruments.""" diff --git a/tests/dummy_qrc/qblox/platform.py b/tests/dummy_qrc/qblox/platform.py index a60a600d8..10aa6c036 100644 --- a/tests/dummy_qrc/qblox/platform.py +++ b/tests/dummy_qrc/qblox/platform.py @@ -1,6 +1,6 @@ import pathlib -from qibolab.channels import Channel, ChannelMap +from qibolab.channel import Channel, ChannelMap from qibolab.instruments.qblox.cluster_qcm_bb import QcmBb from qibolab.instruments.qblox.cluster_qcm_rf import QcmRf from qibolab.instruments.qblox.cluster_qrm_rf import QrmRf diff --git a/tests/dummy_qrc/qm/platform.py b/tests/dummy_qrc/qm/platform.py index 40e2db638..d0a431143 100644 --- a/tests/dummy_qrc/qm/platform.py +++ b/tests/dummy_qrc/qm/platform.py @@ -1,6 +1,6 @@ import pathlib -from qibolab.channels import Channel, ChannelMap +from qibolab.channel import Channel, ChannelMap from qibolab.instruments.dummy import DummyLocalOscillator as LocalOscillator from qibolab.instruments.qm import OPXplus, QMController from qibolab.platform import Platform diff --git a/tests/dummy_qrc/qm_octave/platform.py b/tests/dummy_qrc/qm_octave/platform.py index 3d3d307bf..ac11a5fe8 100644 --- a/tests/dummy_qrc/qm_octave/platform.py +++ b/tests/dummy_qrc/qm_octave/platform.py @@ -1,6 +1,6 @@ import pathlib -from qibolab.channels import Channel, ChannelMap +from qibolab.channel import Channel, ChannelMap from qibolab.instruments.dummy import DummyLocalOscillator as LocalOscillator from qibolab.instruments.qm import Octave, OPXplus, QMController from qibolab.platform import Platform diff --git a/tests/dummy_qrc/rfsoc/platform.py b/tests/dummy_qrc/rfsoc/platform.py index 48082cda0..ef6f5da36 100644 --- a/tests/dummy_qrc/rfsoc/platform.py +++ b/tests/dummy_qrc/rfsoc/platform.py @@ -1,6 +1,6 @@ import pathlib -from qibolab.channels import Channel, ChannelMap +from qibolab.channel import Channel, ChannelMap from qibolab.instruments.erasynth import ERA from qibolab.instruments.rfsoc import RFSoC from qibolab.instruments.rohde_schwarz import SGS100A diff --git a/tests/dummy_qrc/zurich/parameters.json b/tests/dummy_qrc/zurich/parameters.json index 0ff76701e..7b1cb9e9c 100644 --- a/tests/dummy_qrc/zurich/parameters.json +++ b/tests/dummy_qrc/zurich/parameters.json @@ -19,258 +19,519 @@ "readout": 250, "waveforms": 40000 } + } + }, + "components": { + "qubit_0/drive" : { + "frequency": 4000000000, + "power_range": 5 + }, + "qubit_1/drive" : { + "frequency": 4200000000, + "power_range": 0 + }, + "qubit_2/drive" : { + "frequency": 4500000000, + "power_range": -5 + }, + "qubit_3/drive" : { + "frequency": 4150000000, + "power_range": -10 + }, + "qubit_4/drive" : { + "frequency": 4155663000, + "power_range": 5 + }, + "qubit_0/flux" : { + "offset": -0.1, + "power_range": 0.2 + }, + "qubit_1/flux" : { + "offset": 0.0, + "power_range": 0.6 + }, + "qubit_2/flux" : { + "offset": 0.1, + "power_range": 0.4 + }, + "qubit_3/flux" : { + "offset": 0.2, + "power_range": 1 + }, + "qubit_4/flux" : { + "offset": 0.15, + "power_range": 5 + }, + "qubit_0/probe" : { + "frequency": 5200000000, + "power_range": -10 + }, + "qubit_1/probe" : { + "frequency": 4900000000, + "power_range": -10 + }, + "qubit_2/probe" : { + "frequency": 6100000000, + "power_range": -10 + }, + "qubit_3/probe" : { + "frequency": 5800000000, + "power_range": -10 + }, + "qubit_4/probe" : { + "frequency": 5500000000, + "power_range": -10 + }, + "qubit_0/acquire": { + "delay": 0, + "smearing": 0, + "power_range": 10 + }, + "qubit_1/acquire": { + "delay": 0, + "smearing": 0, + "power_range": 10 + }, + "qubit_2/acquire": { + "delay": 0, + "smearing": 0, + "power_range": 10 }, - "lo_readout": { - "frequency": 5500000000 + "qubit_3/acquire": { + "delay": 0, + "smearing": 0, + "power_range": 10 }, - "lo_drive_0": { - "frequency": 4200000000 + "qubit_4/acquire": { + "delay": 0, + "smearing": 0, + "power_range": 10 }, - "lo_drive_1": { - "frequency": 4600000000 + "coupler_0/flux" : { + "offset": 0.0, + "power_range": 3 }, - "lo_drive_2": { - "frequency": 4800000000 + "coupler_1/flux" : { + "offset": 0.0, + "power_range": 1 + }, + "coupler_3/flux" : { + "offset": 0.0, + "power_range": 0.4 + }, + "coupler_4/flux" : { + "offset": 0.0, + "power_range": 0.4 + }, + "readout/lo": { + "power": 10, + "frequency": 6000000000.0 + }, + "qubit_0_1/drive/lo": { + "power": 10, + "frequency": 3000000000.0 + }, + "qubit_2_3/drive/lo": { + "power": 10, + "frequency": 3500000000.0 + }, + "qubit_4/drive/lo": { + "power": 10, + "frequency": 4000000000.0 } }, "native_gates": { "single_qubit": { - "0": { - "RX": { - "duration": 40, - "amplitude": 0.625, - "frequency": 4095830788, - "envelope": { "kind": "drag", "rel_sigma": 0.2, "beta": 0.04 }, - "type": "qd" - }, - "RX12": { - "duration": 40, - "amplitude": 0.625, - "frequency": 4095830788, - "envelope": { "kind": "drag", "rel_sigma": 0.2, "beta": 0.04 }, - "type": "qd" - }, - "MZ": { - "duration": 2000, - "amplitude": 0.5, - "frequency": 5229200000, - "envelope": { "kind": "rectangular" }, - "type": "ro" - } - }, - "1": { - "RX": { - "duration": 90, - "amplitude": 0.2, - "frequency": 4170000000, - "envelope": { "kind": "gaussian", "rel_sigma": 0.2 }, - "type": "qd" - }, - "RX12": { - "duration": 90, - "amplitude": 0.2, - "frequency": 4170000000, - "envelope": { "kind": "gaussian", "rel_sigma": 0.2 }, - "type": "qd" - }, - "MZ": { - "duration": 1000, - "amplitude": 0.1, - "frequency": 4931000000, - "envelope": { "kind": "rectangular" }, - "type": "ro" - } - }, - "2": { - "RX": { - "duration": 40, - "amplitude": 0.59, - "frequency": 4300587281, - "envelope": { "kind": "gaussian", "rel_sigma": 0.2 }, - "type": "qd" - }, - "RX12": { - "duration": 40, - "amplitude": 0.59, - "frequency": 4300587281, - "envelope": { "kind": "gaussian", "rel_sigma": 0.2 }, - "type": "qd" - }, - "MZ": { - "duration": 2000, - "amplitude": 0.54, - "frequency": 6109000000.0, - "envelope": { "kind": "rectangular" }, - "type": "ro" - } - }, - "3": { - "RX": { - "duration": 90, - "amplitude": 0.75, - "frequency": 4100000000, - "envelope": { "kind": "gaussian", "rel_sigma": 0.2 }, - "type": "qd" - }, - "RX12": { - "duration": 90, - "amplitude": 0.75, - "frequency": 4100000000, - "envelope": { "kind": "gaussian", "rel_sigma": 0.2 }, - "type": "qd" - }, - "MZ": { - "duration": 2000, - "amplitude": 0.01, - "frequency": 5783000000, - "envelope": { "kind": "rectangular" }, - "type": "ro" - } - }, - "4": { - "RX": { - "duration": 53, - "amplitude": 1, - "frequency": 4196800000, - "envelope": { "kind": "gaussian", "rel_sigma": 0.2 }, - "type": "qd" - }, - "RX12": { - "duration": 53, - "amplitude": 1, - "frequency": 4196800000, - "envelope": { "kind": "gaussian", "rel_sigma": 0.2 }, - "type": "qd" - }, - "MZ": { - "duration": 1000, - "amplitude": 0.5, - "frequency": 5515000000, - "envelope": { "kind": "rectangular" }, - "type": "ro" - } - } + "0": { + "RX": { + "qubit_0/drive": [ + { + "duration": 40, + "amplitude": 0.5, + "envelope": { "kind": "gaussian", "rel_sigma": 2.0 }, + "type": "qd" + } + ] + }, + "MZ": { + "qubit_0/probe": [ + { + "duration": 2000, + "amplitude": 0.1, + "envelope": { "kind": "rectangular" }, + "type": "ro" + } + ] + } + }, + "1": { + "RX": { + "qubit_1/drive": [ + { + "duration": 40, + "amplitude": 0.5, + "envelope": { "kind": "gaussian", "rel_sigma": 2.0 }, + "type": "qd" + } + ] + }, + "MZ": { + "qubit_1/probe": [ + { + "duration": 2000, + "amplitude": 0.2, + "envelope": { "kind": "rectangular" }, + "type": "ro" + } + ] + } + }, + "2": { + "RX": { + "qubit_2/drive": [ + { + "duration": 40, + "amplitude": 0.54, + "envelope": { "kind": "gaussian", "rel_sigma": 2.0 }, + "type": "qd" + } + ] + }, + "MZ": { + "qubit_2/probe": [ + { + "duration": 2000, + "amplitude": 0.02, + "envelope": { "kind": "rectangular" }, + "type": "ro" + } + ] + } + }, + "3": { + "RX": { + "qubit_3/drive": [ + { + "duration": 40, + "amplitude": 0.454, + "envelope": { "kind": "gaussian", "rel_sigma": 2.0 }, + "type": "qd" + } + ] + }, + "MZ": { + "qubit_3/probe": [ + { + "duration": 2000, + "amplitude": 0.25, + "envelope": { "kind": "rectangular" }, + "type": "ro" + } + ] + } + }, + "4": { + "RX": { + "qubit_4/drive": [ + { + "duration": 40, + "amplitude": 0.6, + "envelope": { "kind": "gaussian", "rel_sigma": 2.0 }, + "type": "qd" + } + ] + }, + "MZ": { + "qubit_4/probe": [ + { + "duration": 2000, + "amplitude": 0.31, + "envelope": { "kind": "rectangular" }, + "type": "ro" + } + ] + } + } }, - "coupler": { - "0": { - "CP": { - "type": "cf", - "duration": 1000, - "amplitude": 0.5, - "frequency": 0, - "envelope": { "kind": "rectangular" } + "two_qubit": { + "0-2": { + "CZ": { + "qubit_2/flux": [ + { + "duration": 80, + "amplitude": 0.057, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "qf" + } + ], + "qubit_0/drive": [ + { + "type": "vz", + "phase": 0.0 + } + ], + "qubit_2/drive": [ + { + "type": "vz", + "phase": 0.0 + } + ], + "coupler_0/flux": [ + { + "duration": 30, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "cf" + } + ] } }, - "1": { - "CP": { - "type": "cf", - "duration": 1000, - "amplitude": 0.5, - "frequency": 0, - "envelope": { "kind": "rectangular" } + "1-2": { + "CZ": { + "qubit_2/flux": [ + { + "duration": 30, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "qf" + } + ], + "qubit_1/drive": [ + { + "type": "vz", + "phase": 0.0 + } + ], + "qubit_2/drive": [ + { + "type": "vz", + "phase": 0.0 + } + ], + "coupler_1/flux": [ + { + "duration": 30, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "cf" + } + ] } }, - "3": { - "CP": { - "type": "cf", - "duration": 1000, - "amplitude": 0.5, - "frequency": 0, - "envelope": { "kind": "rectangular" } + "2-3": { + "CZ": { + "qubit_2/flux": [ + { + "duration": 30, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "qf" + } + ], + "qubit_2/drive": [ + { + "type": "vz", + "phase": 0.0 + } + ], + "qubit_3/drive": [ + { + "type": "vz", + "phase": 0.0 + } + ], + "coupler_3/flux": [ + { + "duration": 30, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "cf" + } + ] } }, - "4": { - "CP": { - "type": "cf", - "duration": 1000, - "amplitude": 0.5, - "frequency": 0, - "envelope": { "kind": "rectangular" } - } - } - }, - "two_qubit": { - "1-2": { - "CZ": [ - { - "duration": 32, - "amplitude": -0.6025, - "envelope": { - "kind": "exponential", - "tau": 12, - "upsilon": 5000, - "g": 0.1 - }, - "qubit": 3, - "frequency": 0, - "type": "qf" - }, - { - "type": "vz", - "phase": -3.63, - "qubit": 1 - }, - { + "2-4": { + "CZ": { + "qubit_2/flux": [ + { + "duration": 30, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "qf" + } + ], + "qubit_2/drive": [ + { + "type": "vz", + "phase": 0.0 + } + ], + "qubit_4/drive": [ + { "type": "vz", - "phase": -0.041, - "qubit": 2 - } - ] + "phase": 0.0 + } + ], + "coupler_4/flux": [ + { + "duration": 30, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5, + "width": 0.75 + }, + "type": "cf" + } + ] + } } } }, - "characterization": { + "characterization": { "single_qubit": { "0": { - "readout_frequency": 5229200000, - "drive_frequency": 4095830788, + "bare_resonator_frequency": 0, + "anharmonicity": 0, + "asymmetry": 0.0, + "crosstalk_matrix": { + "0": 1 + }, + "Ec": 0.0, + "Ej": 0.0, + "g": 0.0, + "assignment_fidelity": 0.0, + "peak_voltage": 0, + "pi_pulse_amplitude": 0, "T1": 0.0, "T2": 0.0, - "sweetspot": 0.05, - "mean_gnd_states": [1.542, 0.1813], - "mean_exc_states": [2.4499, -0.5629], - "threshold": 0.8836, - "iq_angle": -1.551 + "T2_spin_echo": 0, + "state0_voltage": 0, + "state1_voltage": 0, + "mean_gnd_states": [0, 1], + "mean_exc_states": [1, 0], + "threshold": 0.0, + "iq_angle": 0.0 }, "1": { - "readout_frequency": 4931000000, - "drive_frequency": 4170000000, + "bare_resonator_frequency": 0, + "anharmonicity": 0, + "asymmetry": 0.0, + "crosstalk_matrix": { + "1": 1 + }, + "Ec": 0.0, + "Ej": 0.0, + "g": 0.0, + "assignment_fidelity": 0.0, + "peak_voltage": 0, + "pi_pulse_amplitude": 0, "T1": 0.0, "T2": 0.0, - "sweetspot": 0.0, - "mean_gnd_states": [0, 0], - "mean_exc_states": [0, 0] + "T2_spin_echo": 0, + "state0_voltage": 0, + "state1_voltage": 0, + "mean_gnd_states": [0.25, 0], + "mean_exc_states": [0, 0.25], + "threshold": 0.0, + "iq_angle": 0.0 }, "2": { - "readout_frequency": 6109000000.0, - "drive_frequency": 4300587281, + "bare_resonator_frequency": 0, + "anharmonicity": 0, + "asymmetry": 0.0, + "crosstalk_matrix": { + "2": 1 + }, + "Ec": 0.0, + "Ej": 0.0, + "g": 0.0, + "assignment_fidelity": 0.0, + "peak_voltage": 0, + "pi_pulse_amplitude": 0, "T1": 0.0, "T2": 0.0, - "sweetspot": 0.0, - "mean_gnd_states": [-1.8243, 1.5926], - "mean_exc_states": [-0.8083, 2.3929], - "threshold": -0.0593, - "iq_angle": -0.667 + "T2_spin_echo": 0, + "state0_voltage": 0, + "state1_voltage": 0, + "mean_gnd_states": [0.5, 0], + "mean_exc_states": [0, 0.5], + "threshold": 0.0, + "iq_angle": 0.0 }, "3": { - "readout_frequency": 5783000000, - "drive_frequency": 4100000000, + "bare_resonator_frequency": 0, + "anharmonicity": 0, + "asymmetry": 0.0, + "crosstalk_matrix": { + "3": 1 + }, + "Ec": 0.0, + "Ej": 0.0, + "g": 0.0, + "assignment_fidelity": 0.0, + "peak_voltage": 0, + "pi_pulse_amplitude": 0, "T1": 0.0, "T2": 0.0, - "sweetspot": 0.0, - "mean_gnd_states": [0, 0], - "mean_exc_states": [0, 0] + "T2_spin_echo": 0, + "state0_voltage": 0, + "state1_voltage": 0, + "mean_gnd_states": [0.75, 0], + "mean_exc_states": [0, 0.75], + "threshold": 0.0, + "iq_angle": 0.0 }, "4": { - "readout_frequency": 5515000000, - "drive_frequency": 4196800000, + "bare_resonator_frequency": 0, + "anharmonicity": 0, + "asymmetry": 0.0, + "crosstalk_matrix": { + "4": 1 + }, + "Ec": 0.0, + "Ej": 0.0, + "g": 0.0, + "assignment_fidelity": 0.0, + "peak_voltage": 0, + "pi_pulse_amplitude": 0, "T1": 0.0, "T2": 0.0, - "sweetspot": 0.0, - "mean_gnd_states": [0, 0], - "mean_exc_states": [0, 0], - "threshold": 0.233806, - "iq_angle": 0.481 + "T2_spin_echo": 0, + "state0_voltage": 0, + "state1_voltage": 0, + "mean_gnd_states": [1, 0], + "mean_exc_states": [0, 1], + "threshold": 0.0, + "iq_angle": 0.0 } }, "coupler": { diff --git a/tests/dummy_qrc/zurich/platform.py b/tests/dummy_qrc/zurich/platform.py index b9254813e..8c79638c9 100644 --- a/tests/dummy_qrc/zurich/platform.py +++ b/tests/dummy_qrc/zurich/platform.py @@ -1,4 +1,3 @@ -import itertools import pathlib from laboneq.dsl.device import create_connection @@ -6,9 +5,14 @@ from laboneq.simple import DeviceSetup from qibolab import Platform -from qibolab.channels import Channel, ChannelMap -from qibolab.instruments.dummy import DummyLocalOscillator as LocalOscillator -from qibolab.instruments.zhinst import Zurich +from qibolab.components import AcquireChannel, DcChannel, IqChannel, OscillatorConfig +from qibolab.instruments.zhinst import ( + ZiAcquisitionConfig, + ZiChannel, + ZiDcConfig, + ZiIqConfig, + Zurich, +) from qibolab.kernels import Kernels from qibolab.serialize import ( load_instrument_settings, @@ -18,61 +22,22 @@ ) FOLDER = pathlib.Path(__file__).parent -N_QUBITS = 5 +QUBITS = [0, 1, 2, 3, 4] +COUPLERS = [0, 1, 3, 4] def create(): - """IQM 5q-chip controlled Zurich Instruments (Zh) SHFQC, HDAWGs and - PQSC.""" + """A platform representing a chip with star topology, featuring 5 qubits + and 4 couplers, controlled by Zurich Instruments SHFQC, HDAWGs and PQSC.""" - device_setup = DeviceSetup("EL_ZURO") - # Dataserver + device_setup = DeviceSetup("device setup") device_setup.add_dataserver(host="localhost", port=8004) - # Instruments device_setup.add_instruments( HDAWG("device_hdawg", address="DEV8660"), HDAWG("device_hdawg2", address="DEV8673"), PQSC("device_pqsc", address="DEV10055", reference_clock_source="internal"), SHFQC("device_shfqc", address="DEV12146"), ) - device_setup.add_connections( - "device_shfqc", - *[ - create_connection( - to_signal=f"q{i}/drive_line", ports=[f"SGCHANNELS/{i}/OUTPUT"] - ) - for i in range(N_QUBITS) - ], - *[ - create_connection( - to_signal=f"q{i}/measure_line", ports=["QACHANNELS/0/OUTPUT"] - ) - for i in range(N_QUBITS) - ], - *[ - create_connection( - to_signal=f"q{i}/acquire_line", ports=["QACHANNELS/0/INPUT"] - ) - for i in range(N_QUBITS) - ], - ) - device_setup.add_connections( - "device_hdawg", - *[ - create_connection(to_signal=f"q{i}/flux_line", ports=f"SIGOUTS/{i}") - for i in range(N_QUBITS) - ], - *[ - create_connection(to_signal=f"qc{c}/flux_line", ports=f"SIGOUTS/{i}") - for c, i in zip(itertools.chain(range(0, 2), range(3, 4)), range(5, 8)) - ], - ) - - device_setup.add_connections( - "device_hdawg2", - create_connection(to_signal="qc4/flux_line", ports=["SIGOUTS/0"]), - ) - device_setup.add_connections( "device_pqsc", create_connection(to_instrument="device_hdawg2", ports="ZSYNCS/1"), @@ -80,121 +45,98 @@ def create(): create_connection(to_instrument="device_shfqc", ports="ZSYNCS/2"), ) - controller = Zurich( - "EL_ZURO", - device_setup=device_setup, - time_of_flight=75, - smearing=50, - ) + runcard = load_runcard(FOLDER) + kernels = Kernels.load(FOLDER) + qubits, couplers, pairs = load_qubits(runcard, kernels) + settings = load_settings(runcard) - # Create channel objects and map controllers - channels = ChannelMap() - # feedback - channels |= Channel( - "L2-7", port=controller.ports(("device_shfqc", "[QACHANNELS/0/INPUT]")) - ) - # readout - channels |= Channel( - "L3-31", port=controller.ports(("device_shfqc", "[QACHANNELS/0/OUTPUT]")) - ) - # drive - channels |= ( - Channel( - f"L4-{i}", - port=controller.ports(("device_shfqc", f"SGCHANNELS/{i-5}/OUTPUT")), + configs = {} + component_params = runcard["components"] + readout_lo = "readout/lo" + drive_los = { + 0: "qubit_0_1/drive/lo", + 1: "qubit_0_1/drive/lo", + 2: "qubit_2_3/drive/lo", + 3: "qubit_2_3/drive/lo", + 4: "qubit_4/drive/lo", + } + configs[readout_lo] = OscillatorConfig(**component_params[readout_lo]) + zi_channels = [] + for q in QUBITS: + probe_name = f"qubit_{q}/probe" + acquisition_name = f"qubit_{q}/acquire" + configs[probe_name] = ZiIqConfig(**component_params[probe_name]) + qubits[q].probe = IqChannel( + name=probe_name, lo=readout_lo, mixer=None, acquisition=acquisition_name + ) + zi_channels.append( + ZiChannel( + qubits[q].probe, device="device_shfqc", path="QACHANNELS/0/OUTPUT" + ) ) - for i in range(15, 20) - ) - # flux qubits (CAREFUL WITH THIS !!!) - channels |= ( - Channel(f"L4-{i}", port=controller.ports(("device_hdawg", f"SIGOUTS/{i-6}"))) - for i in range(6, 11) - ) - # flux couplers - channels |= ( - Channel(f"L4-{i}", port=controller.ports(("device_hdawg", f"SIGOUTS/{i-11+5}"))) - for i in range(11, 14) - ) - channels |= Channel("L4-14", port=controller.ports(("device_hdawg2", "SIGOUTS/0"))) - # TWPA pump(EraSynth) - channels |= Channel("L3-32") - - # SHFQC - # Sets the maximal Range of the Signal Output power. - # The instrument selects the closest available Range [-50. -30. -25. -20. -15. -10. -5. 0. 5. 10.] - # with a resolution of 5 dBm. - - # readout "gain": Set to max power range (10 Dbm) if no distorsion - channels["L3-31"].power_range = -15 # -15 - # feedback "gain": play with the power range to calibrate the best RO - channels["L2-7"].power_range = 10 - - # drive - # The instrument selects the closest available Range [-30. -25. -20. -15. -10. -5. 0. 5. 10.] - channels["L4-15"].power_range = -10 # q0 - channels["L4-16"].power_range = -5 # q1 - channels["L4-17"].power_range = -10 # q2 - channels["L4-18"].power_range = -5 # q3 - channels["L4-19"].power_range = -10 # q4 - - # HDAWGS - # Sets the output voltage range. - # The instrument selects the next higher available Range with a resolution of 0.4 Volts. - - # flux - for i in range(6, 11): - channels[f"L4-{i}"].power_range = 0.8 - # flux couplers - for i in range(11, 15): - channels[f"L4-{i}"].power_range = 0.8 - # Instantiate local oscillators - local_oscillators = [ - LocalOscillator(f"lo_{kind}", None) - for kind in ["readout"] + [f"drive_{n}" for n in range(3)] - ] + configs[acquisition_name] = ZiAcquisitionConfig( + **component_params[acquisition_name] + ) + qubits[q].acquisition = AcquireChannel( + name=acquisition_name, + twpa_pump=None, + probe=probe_name, + ) + zi_channels.append( + ZiChannel( + qubits[q].acquisition, device="device_shfqc", path="QACHANNELS/0/INPUT" + ) + ) - # Map LOs to channels - ch_to_lo = { - "L3-31": 0, - "L4-15": 1, - "L4-16": 1, - "L4-17": 2, - "L4-18": 2, - "L4-19": 3, - } - for ch, lo in ch_to_lo.items(): - channels[ch].local_oscillator = local_oscillators[lo] + drive_name = f"qubit_{q}/drive" + configs[drive_los[q]] = OscillatorConfig(**component_params[drive_los[q]]) + configs[drive_name] = ZiIqConfig(**component_params[drive_name]) + qubits[q].drive = IqChannel( + name=drive_name, + mixer=None, + lo=drive_los[q], + ) + zi_channels.append( + ZiChannel( + qubits[q].drive, device="device_shfqc", path=f"SGCHANNELS/{q}/OUTPUT" + ) + ) - # create qubit objects - runcard = load_runcard(FOLDER) - kernels = Kernels.load(FOLDER) - qubits, couplers, pairs = load_qubits(runcard, kernels) - settings = load_settings(runcard) + flux_name = f"qubit_{q}/flux" + configs[flux_name] = ZiDcConfig(**component_params[flux_name]) + qubits[q].flux = DcChannel( + name=flux_name, + ) + zi_channels.append( + ZiChannel(qubits[q].flux, device="device_hdawg", path=f"SIGOUTS/{q}") + ) - # assign channels to qubits and sweetspots(operating points) - for q in range(0, 5): - qubits[q].readout = channels["L3-31"] - qubits[q].feedback = channels["L2-7"] + for i, c in enumerate(COUPLERS): + flux_name = f"coupler_{c}/flux" + configs[flux_name] = ZiDcConfig(**component_params[flux_name]) + couplers[c].flux = DcChannel(name=flux_name) + zi_channels.append( + ZiChannel(couplers[c].flux, device="device_hdawg2", path=f"SIGOUTS/{i}") + ) - for q in range(0, 5): - qubits[q].drive = channels[f"L4-{15 + q}"] - qubits[q].flux = channels[f"L4-{6 + q}"] - qubits[q].twpa = channels["L3-32"] - channels[f"L4-{6 + q}"].qubit = qubits[q] + controller = Zurich( + "EL_ZURO", + device_setup=device_setup, + channels=zi_channels, + time_of_flight=75, + smearing=50, + ) - # assign channels to couplers and sweetspots(operating points) - for c, coupler in enumerate(couplers.values()): - coupler.flux = channels[f"L4-{11 + c}"] instruments = {controller.name: controller} - instruments.update({lo.name: lo for lo in local_oscillators}) instruments = load_instrument_settings(runcard, instruments) return Platform( str(FOLDER), qubits, pairs, + configs, instruments, settings, - resonator_type="2D", + resonator_type="3D", couplers=couplers, ) diff --git a/tests/emulators/default_q0/platform.py b/tests/emulators/default_q0/platform.py index 136350a80..b1d849841 100644 --- a/tests/emulators/default_q0/platform.py +++ b/tests/emulators/default_q0/platform.py @@ -1,6 +1,6 @@ import pathlib -from qibolab.channels import ChannelMap +from qibolab.components import AcquireChannel, IqChannel from qibolab.instruments.emulator.pulse_simulator import PulseSimulator from qibolab.platform import Platform from qibolab.serialize import ( @@ -29,23 +29,20 @@ def create(): qubits, couplers, pairs = load_qubits(runcard) settings = load_settings(runcard) - # Create channel object - channels = ChannelMap() - channels |= (f"readout-{q}" for q in qubits) - channels |= (f"drive-{q}" for q in qubits) - - # map channels to qubits + # define channels for qubits for q, qubit in qubits.items(): - qubit.readout = channels[f"readout-{q}"] - qubit.drive = channels[f"drive-{q}"] - - channels[f"drive-{q}"].qubit = qubit - qubit.sweetspot = 0 # not used + qubit.probe = IqChannel( + "probe-{q}", mixer=None, lo=None, acquisition="acquire-{q}" + ) + qubit.acquisition = AcquireChannel( + "acquire-{q}", mixer=None, lo=None, twpa_pump=None, probe="probe-{q}" + ) return Platform( device_name, qubits, pairs, + {}, instruments, settings, resonator_type="2D", diff --git a/tests/pulses/test_plot.py b/tests/pulses/test_plot.py index c895404fd..156df65b4 100644 --- a/tests/pulses/test_plot.py +++ b/tests/pulses/test_plot.py @@ -30,7 +30,6 @@ def test_plot_functions(): envelope=Rectangular(), relative_phase=0, type=PulseType.FLUX, - qubit=0, ) p1 = Pulse( duration=40, @@ -39,7 +38,6 @@ def test_plot_functions(): envelope=Gaussian(rel_sigma=0.2), relative_phase=0, type=PulseType.DRIVE, - qubit=2, ) p2 = Pulse( duration=40, @@ -48,18 +46,13 @@ def test_plot_functions(): envelope=Drag(rel_sigma=0.2, beta=2), relative_phase=0, type=PulseType.DRIVE, - qubit=200, ) p3 = Pulse.flux( duration=40, amplitude=0.9, envelope=Iir(a=np.array([-0.5, 2]), b=np.array([1]), target=Rectangular()), - channel="0", - qubit=200, - ) - p4 = Pulse.flux( - duration=40, amplitude=0.9, envelope=Snz(t_idling=10), channel="0", qubit=200 ) + p4 = Pulse.flux(duration=40, amplitude=0.9, envelope=Snz(t_idling=10)) p5 = Pulse( duration=40, amplitude=0.9, @@ -75,22 +68,34 @@ def test_plot_functions(): envelope=GaussianSquare(rel_sigma=0.2, width=0.9), relative_phase=0, type=PulseType.DRIVE, - qubit=2, ) - ps = PulseSequence([p0, p1, p2, p3, p4, p5, p6]) + ps = PulseSequence() + ps.update( + { + "q0/flux": [p0], + "q2/drive": [p1, p6], + "q200/drive": [p2], + "q200/flux": [p3, p4], + "q0/drive": [p5], + } + ) envelope = p0.envelopes(SAMPLING_RATE) wf = modulate(np.array(envelope), 0.0, rate=SAMPLING_RATE) plot_file = HERE / "test_plot.png" - plot.waveform(wf, plot_file) + plot.waveform(wf, filename=plot_file) + assert os.path.exists(plot_file) + os.remove(plot_file) + + plot.pulse(p0, filename=plot_file) assert os.path.exists(plot_file) os.remove(plot_file) - plot.pulse(p0, plot_file) + plot.pulse(p0, freq=2e9, filename=plot_file) assert os.path.exists(plot_file) os.remove(plot_file) - plot.sequence(ps, plot_file) + plot.sequence(ps, {"q200/drive": 3e9}, filename=plot_file) assert os.path.exists(plot_file) os.remove(plot_file) diff --git a/tests/pulses/test_pulse.py b/tests/pulses/test_pulse.py index 29650dc8e..1cb65ed9b 100644 --- a/tests/pulses/test_pulse.py +++ b/tests/pulses/test_pulse.py @@ -23,84 +23,35 @@ def test_init(): p0 = Pulse( duration=50, amplitude=0.9, - frequency=20_000_000, relative_phase=0.0, envelope=Rectangular(), - channel="0", type=PulseType.READOUT, - qubit=0, ) assert p0.relative_phase == 0.0 p1 = Pulse( duration=50, amplitude=0.9, - frequency=20_000_000, relative_phase=0.0, envelope=Rectangular(), - channel="0", type=PulseType.READOUT, - qubit=0, ) assert p1.type is PulseType.READOUT - # initialisation with non int (float) frequency - p2 = Pulse( - duration=50, - amplitude=0.9, - frequency=int(20e6), - relative_phase=0, - envelope=Rectangular(), - channel="0", - type=PulseType.READOUT, - qubit=0, - ) - assert isinstance(p2.frequency, float) and p2.frequency == 20_000_000 - # initialisation with non float (int) relative_phase - p3 = Pulse( + p2 = Pulse( duration=50, amplitude=0.9, - frequency=20_000_000, relative_phase=1.0, envelope=Rectangular(), - channel="0", - type=PulseType.READOUT, - qubit=0, - ) - assert isinstance(p3.relative_phase, float) and p3.relative_phase == 1.0 - - # initialisation with str shape - p4 = Pulse( - duration=50, - amplitude=0.9, - frequency=20_000_000, - relative_phase=0, - envelope=Rectangular(), - channel="0", - type=PulseType.READOUT, - qubit=0, - ) - assert isinstance(p4.envelope, Rectangular) - - # initialisation with str channel and str qubit - p5 = Pulse( - duration=50, - amplitude=0.9, - frequency=20_000_000, - relative_phase=0, - envelope=Rectangular(), - channel="channel0", type=PulseType.READOUT, - qubit=0, ) - assert p5.qubit == 0 + assert isinstance(p2.relative_phase, float) and p2.relative_phase == 1.0 - # initialisation with different frequencies, shapes and types + # initialisation with different shapes and types p6 = Pulse( duration=40, amplitude=0.9, - frequency=-50e6, envelope=Rectangular(), relative_phase=0, type=PulseType.READOUT, @@ -108,29 +59,23 @@ def test_init(): p7 = Pulse( duration=40, amplitude=0.9, - frequency=0, envelope=Rectangular(), relative_phase=0, type=PulseType.FLUX, - qubit=0, ) p8 = Pulse( duration=40, amplitude=0.9, - frequency=50e6, envelope=Gaussian(rel_sigma=0.2), relative_phase=0, type=PulseType.DRIVE, - qubit=2, ) p9 = Pulse( duration=40, amplitude=0.9, - frequency=50e6, envelope=Drag(rel_sigma=0.2, beta=2), relative_phase=0, type=PulseType.DRIVE, - qubit=200, ) p10 = Pulse.flux( duration=40, @@ -138,20 +83,15 @@ def test_init(): envelope=Iir( a=np.array([-1, 1]), b=np.array([-0.1, 0.1001]), target=Rectangular() ), - channel="0", - qubit=200, ) p11 = Pulse.flux( duration=40, amplitude=0.9, envelope=Snz(t_idling=10, b_amplitude=0.5), - channel="0", - qubit=200, ) p13 = Pulse( duration=40, amplitude=0.9, - frequency=400e6, envelope=ECap(alpha=2), relative_phase=0, type=PulseType.DRIVE, @@ -159,79 +99,36 @@ def test_init(): p14 = Pulse( duration=40, amplitude=0.9, - frequency=50e6, envelope=GaussianSquare(rel_sigma=0.2, width=0.9), relative_phase=0, type=PulseType.READOUT, - qubit=2, ) # initialisation with float duration p12 = Pulse( duration=34.33, amplitude=0.9, - frequency=20_000_000, relative_phase=1, envelope=Rectangular(), - channel="0", type=PulseType.READOUT, - qubit=0, ) assert isinstance(p12.duration, float) assert p12.duration == 34.33 def test_attributes(): - channel = "0" - qubit = 0 - - p10 = Pulse( + p = Pulse( duration=50, amplitude=0.9, - frequency=20_000_000, relative_phase=0.0, envelope=Rectangular(), - channel=channel, - qubit=qubit, ) - assert isinstance(p10.duration, float) and p10.duration == 50 - assert isinstance(p10.amplitude, float) and p10.amplitude == 0.9 - assert isinstance(p10.frequency, float) and p10.frequency == 20_000_000 - assert isinstance(p10.envelope, BaseEnvelope) - assert isinstance(p10.channel, type(channel)) and p10.channel == channel - assert isinstance(p10.qubit, type(qubit)) and p10.qubit == qubit - - -def test_aliases(): - rop = Pulse( - duration=50, - amplitude=0.9, - frequency=20_000_000, - relative_phase=0.0, - envelope=Rectangular(), - type=PulseType.READOUT, - channel="0", - qubit=0, - ) - assert rop.qubit == 0 - - dp = Pulse( - duration=2000, - amplitude=0.9, - frequency=200_000_000, - relative_phase=0.0, - envelope=Gaussian(rel_sigma=5), - channel="0", - qubit=0, - ) - assert dp.amplitude == 0.9 - assert isinstance(dp.envelope, Gaussian) - - fp = Pulse.flux( - duration=300, amplitude=0.9, envelope=Rectangular(), channel="0", qubit=0 - ) - assert fp.channel == "0" + assert isinstance(p.duration, float) and p.duration == 50 + assert isinstance(p.amplitude, float) and p.amplitude == 0.9 + assert isinstance(p.relative_phase, float) and p.relative_phase == 0.0 + assert isinstance(p.envelope, BaseEnvelope) + assert isinstance(p.type, PulseType) def test_pulse(): @@ -239,12 +136,10 @@ def test_pulse(): rel_sigma = 5 beta = 2 pulse = Pulse( - frequency=200_000_000, amplitude=1, duration=duration, relative_phase=0, envelope=Drag(rel_sigma=rel_sigma, beta=beta), - channel="1", ) assert pulse.duration == duration @@ -253,12 +148,10 @@ def test_pulse(): def test_readout_pulse(): duration = 2000 pulse = Pulse( - frequency=200_000_000, amplitude=1, duration=duration, relative_phase=0, envelope=Rectangular(), - channel="11", type=PulseType.READOUT, ) @@ -272,14 +165,11 @@ def test_envelope_waveform_i_q(): pulse = Pulse( duration=1000, amplitude=1, - frequency=10e6, relative_phase=0, envelope=Rectangular(), - channel="1", ) custom_shape_pulse = custom_shape_pulse.model_copy(update={"i_": pulse.i(1)}) - pulse = pulse.model_copy(update={"duration": 2000}) with pytest.raises(ValueError): custom_shape_pulse.i(samples=10) with pytest.raises(ValueError): diff --git a/tests/pulses/test_sequence.py b/tests/pulses/test_sequence.py index c44eacbe8..2963c8021 100644 --- a/tests/pulses/test_sequence.py +++ b/tests/pulses/test_sequence.py @@ -1,3 +1,5 @@ +from collections import defaultdict + from qibolab.pulses import ( Delay, Drag, @@ -9,335 +11,205 @@ ) -def test_add_readout(): +def test_init(): + sequence = PulseSequence() + assert isinstance(sequence, defaultdict) + assert len(sequence) == 0 + + +def test_init_with_dict(): + seq_dict = { + "some channel": [ + Pulse(duration=20, amplitude=0.1, envelope=Gaussian(rel_sigma=3)), + Pulse(duration=30, amplitude=0.5, envelope=Gaussian(rel_sigma=3)), + ], + "other channel": [ + Pulse(duration=40, amplitude=0.2, envelope=Gaussian(rel_sigma=3)) + ], + "chanel #5": [ + Pulse(duration=45, amplitude=1.0, envelope=Gaussian(rel_sigma=3)), + Pulse(duration=50, amplitude=0.7, envelope=Gaussian(rel_sigma=3)), + Pulse(duration=60, amplitude=-0.65, envelope=Gaussian(rel_sigma=3)), + ], + } + seq = PulseSequence(seq_dict) + + assert len(seq) == 3 + assert set(seq.keys()) == set(seq_dict.keys()) + assert len(seq["some channel"]) == 2 + assert len(seq["other channel"]) == 1 + assert len(seq["chanel #5"]) == 3 + + +def test_default_factory(): + sequence = PulseSequence() + some = sequence["some channel"] + assert isinstance(some, list) + assert len(some) == 0 + + +def test_ro_pulses(): + Pulse( + amplitude=0.3, + duration=60, + relative_phase=0, + envelope=Gaussian(rel_sigma=0.2), + ) sequence = PulseSequence() - sequence.append( + sequence["ch1"].append( Pulse( - frequency=200_000_000, amplitude=0.3, duration=60, relative_phase=0, envelope=Gaussian(rel_sigma=0.2), - channel="1", ) ) - sequence.append(Delay(duration=4, channel="1")) - sequence.append( + sequence["ch2"].append(Delay(duration=4)) + sequence["ch2"].append( Pulse( - frequency=200_000_000, amplitude=0.3, duration=60, relative_phase=0, envelope=Drag(rel_sigma=0.2, beta=2), - channel="1", type=PulseType.FLUX, ) ) - sequence.append(Delay(duration=4, channel="1")) - sequence.append( - Pulse( - frequency=20_000_000, - amplitude=0.9, - duration=2000, - relative_phase=0, - envelope=Rectangular(), - channel="11", - type=PulseType.READOUT, - ) - ) - assert len(sequence) == 5 - assert len(sequence.ro_pulses) == 1 - assert len(sequence.qd_pulses) == 1 - assert len(sequence.qf_pulses) == 1 - - -def test_get_qubit_pulses(): - p1 = Pulse( - duration=400, - amplitude=0.9, - frequency=20e6, - envelope=Gaussian(rel_sigma=0.2), - relative_phase=10, - qubit=0, - ) - p2 = Pulse( - duration=400, - amplitude=0.9, - frequency=20e6, - envelope=Rectangular(), - channel="30", - qubit=0, - type=PulseType.READOUT, - ) - p3 = Pulse( - duration=400, - amplitude=0.9, - frequency=20e6, - envelope=Drag(rel_sigma=0.2, beta=50), - relative_phase=20, - qubit=1, - ) - p4 = Pulse( - duration=400, + sequence["ch3"].append(Delay(duration=4)) + ro_pulse = Pulse( amplitude=0.9, - frequency=20e6, - envelope=Drag(rel_sigma=0.2, beta=50), - relative_phase=30, - qubit=1, - ) - p5 = Pulse( - duration=400, - amplitude=0.9, - frequency=20e6, + duration=2000, + relative_phase=0, envelope=Rectangular(), - channel="30", - qubit=1, type=PulseType.READOUT, ) - p6 = Pulse.flux( - duration=400, amplitude=0.9, envelope=Rectangular(), channel="40", qubit=1 - ) - p7 = Pulse.flux( - duration=400, amplitude=0.9, envelope=Rectangular(), channel="40", qubit=2 - ) - - ps = PulseSequence([p1, p2, p3, p4, p5, p6, p7]) - assert ps.qubits == [0, 1, 2] - assert len(ps.get_qubit_pulses(0)) == 2 - assert len(ps.get_qubit_pulses(1)) == 4 - assert len(ps.get_qubit_pulses(2)) == 1 - assert len(ps.get_qubit_pulses(0, 1)) == 6 + sequence["ch3"].append(ro_pulse) + assert set(sequence.keys()) == {"ch1", "ch2", "ch3"} + assert sum(len(pulses) for pulses in sequence.values()) == 5 + assert len(sequence.probe_pulses) == 1 + assert sequence.probe_pulses[0] == ro_pulse -def test_get_channel_pulses(): - p1 = Pulse( - duration=400, - frequency=0.9, - amplitude=20e6, - envelope=Gaussian(rel_sigma=0.2), - channel="10", - ) - p2 = Pulse( - duration=400, - frequency=0.9, - amplitude=20e6, - envelope=Rectangular(), - channel="30", - type=PulseType.READOUT, - ) - p3 = Pulse( - duration=400, - frequency=0.9, - amplitude=20e6, - envelope=Drag(rel_sigma=0.2, beta=5), - channel="20", - ) - p4 = Pulse( - duration=400, - frequency=0.9, - amplitude=20e6, - envelope=Drag(rel_sigma=0.2, beta=5), - channel="30", - ) - p5 = Pulse( - duration=400, - frequency=0.9, - amplitude=20e6, - envelope=Rectangular(), - channel="20", - type=PulseType.READOUT, +def test_durations(): + sequence = PulseSequence() + sequence["ch1"].append(Delay(duration=20)) + sequence["ch1"].append( + Pulse( + duration=1000, + amplitude=0.9, + envelope=Rectangular(), + type=PulseType.READOUT, + ) ) - p6 = Pulse( - duration=400, - frequency=0.9, - amplitude=20e6, - envelope=Gaussian(rel_sigma=0.2), - channel="30", + sequence["ch2"].append( + Pulse( + duration=40, + amplitude=0.9, + envelope=Drag(rel_sigma=0.2, beta=1), + type=PulseType.DRIVE, + ) ) + assert sequence.channel_duration("ch1") == 20 + 1000 + assert sequence.channel_duration("ch2") == 40 + assert sequence.duration == 20 + 1000 - ps = PulseSequence([p1, p2, p3, p4, p5, p6]) - assert sorted(ps.channels) == ["10", "20", "30"] - assert len(ps.get_channel_pulses("10")) == 1 - assert len(ps.get_channel_pulses("20")) == 2 - assert len(ps.get_channel_pulses("30")) == 3 - assert len(ps.get_channel_pulses("20", "30")) == 5 + sequence["ch2"].append( + Pulse( + duration=1200, + amplitude=0.9, + envelope=Rectangular(), + type=PulseType.READOUT, + ) + ) + assert sequence.duration == 40 + 1200 -def test_sequence_duration(): - p0 = Delay(duration=20, channel="1") - p1 = Pulse( - duration=40, - amplitude=0.9, - frequency=200e6, - envelope=Drag(rel_sigma=0.2, beta=1), - channel="1", - type=PulseType.DRIVE, +def test_extend(): + sequence1 = PulseSequence() + sequence1["ch1"].append( + Pulse( + duration=40, + amplitude=0.9, + envelope=Drag(rel_sigma=0.2, beta=1), + ) ) - p2 = Pulse( - duration=1000, - amplitude=0.9, - frequency=20e6, - envelope=Rectangular(), - channel="1", - type=PulseType.READOUT, + + sequence2 = PulseSequence() + sequence2["ch2"].append( + Pulse( + duration=60, + amplitude=0.9, + envelope=Drag(rel_sigma=0.2, beta=1), + ) ) - ps = PulseSequence([p0, p1]) + [p2] - assert ps.duration == 20 + 40 + 1000 - ps[-1] = p2.model_copy(update={"channel": "2"}) - assert ps.duration == 1000 + sequence1.extend(sequence2) + assert set(sequence1.keys()) == {"ch1", "ch2"} + assert len(sequence1["ch1"]) == 1 + assert len(sequence1["ch2"]) == 1 + assert sequence1.duration == 60 -def test_init(): - p1 = Pulse( - duration=40, - amplitude=0.9, - frequency=100e6, - envelope=Drag(rel_sigma=0.2, beta=1), - channel="3", - type=PulseType.DRIVE, - ) - p2 = Pulse( - duration=40, - amplitude=0.9, - frequency=100e6, - envelope=Drag(rel_sigma=0.2, beta=1), - channel="2", - type=PulseType.DRIVE, + sequence3 = PulseSequence() + sequence3["ch2"].append( + Pulse( + duration=80, + amplitude=0.9, + envelope=Drag(rel_sigma=0.2, beta=1), + ) ) - p3 = Pulse( - duration=40, - amplitude=0.9, - frequency=100e6, - envelope=Drag(rel_sigma=0.2, beta=1), - channel="1", - type=PulseType.DRIVE, + sequence3["ch3"].append( + Pulse( + duration=100, + amplitude=0.9, + envelope=Drag(rel_sigma=0.2, beta=1), + ) ) - ps = PulseSequence() - assert type(ps) == PulseSequence + sequence1.extend(sequence3) + assert set(sequence1.keys()) == {"ch1", "ch2", "ch3"} + assert len(sequence1["ch1"]) == 1 + assert len(sequence1["ch2"]) == 2 + assert len(sequence1["ch3"]) == 2 + assert isinstance(sequence1["ch3"][0], Delay) + assert sequence1.duration == 60 + 100 + assert sequence1.channel_duration("ch1") == 40 + assert sequence1.channel_duration("ch2") == 60 + 80 + assert sequence1.channel_duration("ch3") == 60 + 100 - ps = PulseSequence([p1, p2, p3]) - assert len(ps) == 3 - assert ps[0] == p1 - assert ps[1] == p2 - assert ps[2] == p3 - other_ps = PulseSequence([p1, p2, p3]) - assert len(other_ps) == 3 - assert other_ps[0] == p1 - assert other_ps[1] == p2 - assert other_ps[2] == p3 - - plist = [p1, p2, p3] - n = 0 - for pulse in ps: - assert plist[n] == pulse - n += 1 - - -def test_operators(): - ps = PulseSequence() - ps += [ +def test_copy(): + sequence = PulseSequence() + sequence["ch1"].append( Pulse( - duration=200, + duration=40, amplitude=0.9, - frequency=20e6, - envelope=Rectangular(), - channel="3", - type=PulseType.DRIVE, + envelope=Drag(rel_sigma=0.2, beta=1), ) - ] - ps = ps + [ + ) + + sequence["ch2"].append( Pulse( - duration=200, + duration=60, amplitude=0.9, - frequency=20e6, - envelope=Rectangular(), - channel="2", - type=PulseType.DRIVE, + envelope=Drag(rel_sigma=0.2, beta=1), ) - ] - ps = [ + ) + sequence["ch2"].append( Pulse( - duration=200, + duration=80, amplitude=0.9, - frequency=20e6, - envelope=Rectangular(), - channel="3", - type=PulseType.DRIVE, + envelope=Drag(rel_sigma=0.2, beta=1), ) - ] + ps - - p4 = Pulse( - duration=40, - amplitude=0.9, - frequency=50e6, - envelope=Gaussian(rel_sigma=0.2), - channel="3", - type=PulseType.DRIVE, ) - p5 = Pulse( - duration=40, - amplitude=0.9, - frequency=50e6, - envelope=Gaussian(rel_sigma=0.2), - channel="2", - type=PulseType.DRIVE, - ) - p6 = Pulse( - duration=40, - amplitude=0.9, - frequency=50e6, - envelope=Gaussian(rel_sigma=0.2), - channel="1", - type=PulseType.DRIVE, - ) - - another_ps = PulseSequence() - another_ps.append(p4) - another_ps.extend([p5, p6]) - - assert another_ps[0] == p4 - assert another_ps[1] == p5 - assert another_ps[2] == p6 - - ps += another_ps - assert len(ps) == 6 - assert p5 in ps - - # ps.plot() - - p7 = Pulse( - duration=40, - amplitude=0.9, - frequency=100e6, - envelope=Drag(rel_sigma=0.2, beta=1), - channel="1", - type=PulseType.DRIVE, - ) - yet_another_ps = PulseSequence([p7]) - assert len(yet_another_ps) == 1 - yet_another_ps *= 3 - assert len(yet_another_ps) == 3 - yet_another_ps *= 3 - assert len(yet_another_ps) == 9 + sequence_copy = sequence.copy() + assert isinstance(sequence_copy, PulseSequence) + assert sequence_copy == sequence - p8 = Pulse( - duration=40, - amplitude=0.9, - frequency=100e6, - envelope=Drag(rel_sigma=0.2, beta=1), - channel="1", - type=PulseType.DRIVE, - ) - p9 = Pulse( - duration=40, - amplitude=0.9, - frequency=100e6, - envelope=Drag(rel_sigma=0.2, beta=1), - channel="2", - type=PulseType.DRIVE, + sequence_copy["ch3"].append( + Pulse( + duration=100, + amplitude=0.9, + envelope=Drag(rel_sigma=0.2, beta=1), + ) ) - and_yet_another_ps = 2 * PulseSequence([p9]) + [p8] * 3 - assert len(and_yet_another_ps) == 5 + assert "ch3" not in sequence diff --git a/tests/test_backends.py b/tests/test_backends.py index f432b6c32..c7d7b90b3 100644 --- a/tests/test_backends.py +++ b/tests/test_backends.py @@ -195,11 +195,10 @@ def test_superposition_for_all_qubits(connected_backend): # TODO: test_circuit_result_representation -def test_metabackend_load(dummy_qrc): - for platform in Path("tests/dummy_qrc/").iterdir(): - backend = MetaBackend.load(platform.name) - assert isinstance(backend, QibolabBackend) - assert Path(backend.platform.name).name == platform.name +def test_metabackend_load(platform): + backend = MetaBackend.load(platform.name) + assert isinstance(backend, QibolabBackend) + assert backend.platform.name == platform.name def test_metabackend_list_available(tmpdir): diff --git a/tests/test_channels.py b/tests/test_channels.py deleted file mode 100644 index ea16b1437..000000000 --- a/tests/test_channels.py +++ /dev/null @@ -1,90 +0,0 @@ -import pytest - -from qibolab.channels import Channel, ChannelMap -from qibolab.instruments.dummy import DummyPort - - -def test_channel_init(): - channel = Channel("L1-test") - assert channel.name == "L1-test" - - -def test_channel_errors(): - channel = Channel("L1-test", port=DummyPort("test")) - channel.offset = 0.1 - channel.filter = {} - # attempt to set bias higher than the allowed value - channel.max_offset = 0.2 - with pytest.raises(ValueError): - channel.offset = 0.3 - - -def test_channel_map_add(): - channels = ChannelMap().add("a", "b") - assert "a" in channels - assert "b" in channels - assert isinstance(channels["a"], Channel) - assert isinstance(channels["b"], Channel) - assert channels["a"].name == "a" - assert channels["b"].name == "b" - - -def test_channel_map_setitem(): - channels = ChannelMap() - with pytest.raises(TypeError): - channels["c"] = "test" - channels["c"] = Channel("c") - assert isinstance(channels["c"], Channel) - - -def test_channel_map_union(): - channels1 = ChannelMap().add("a", "b") - channels2 = ChannelMap().add("c", "d") - channels = channels1 | channels2 - for name in ["a", "b", "c", "d"]: - assert name in channels - assert isinstance(channels[name], Channel) - assert channels[name].name == name - assert "a" not in channels2 - assert "b" not in channels2 - assert "c" not in channels1 - assert "d" not in channels1 - - -def test_channel_map_union_update(): - channels = ChannelMap().add("a", "b") - channels |= ChannelMap().add("c", "d") - for name in ["a", "b", "c", "d"]: - assert name in channels - assert isinstance(channels[name], Channel) - assert channels[name].name == name - - -@pytest.fixture -def first_qubit(platform): - return next(iter(platform.qubits.values())) - - -def test_platform_lo_drive_frequency(first_qubit): - first_qubit.drive.lo_frequency = 5.5e9 - assert first_qubit.drive.lo_frequency == 5.5e9 - - -def test_platform_lo_readout_frequency(first_qubit): - first_qubit.readout.lo_frequency = 7e9 - assert first_qubit.readout.lo_frequency == 7e9 - - -def test_platform_attenuation(first_qubit): - first_qubit.drive.attenuation = 10 - assert first_qubit.drive.attenuation == 10 - - -def test_platform_gain(first_qubit): - first_qubit.readout.gain = 0 - assert first_qubit.readout.gain == 0 - - -def test_platform_bias(first_qubit): - first_qubit.flux.offset = 0.05 - assert first_qubit.flux.offset == 0.05 diff --git a/tests/test_compilers_default.py b/tests/test_compilers_default.py index 3af68316a..c1b726212 100644 --- a/tests/test_compilers_default.py +++ b/tests/test_compilers_default.py @@ -37,20 +37,20 @@ def compile_circuit(circuit, platform): @pytest.mark.parametrize( - "gateargs,sequence_len", + "gateargs", [ - ((gates.I,), 1), - ((gates.Z,), 2), - ((gates.GPI, np.pi / 8), 3), - ((gates.GPI2, -np.pi / 8), 3), - ((gates.RZ, np.pi / 4), 2), + (gates.I,), + (gates.Z,), + (gates.GPI, np.pi / 8), + (gates.GPI2, -np.pi / 8), + (gates.RZ, np.pi / 4), ], ) -def test_compile(platform, gateargs, sequence_len): +def test_compile(platform, gateargs): nqubits = platform.nqubits circuit = generate_circuit_with_gate(nqubits, *gateargs) sequence = compile_circuit(circuit, platform) - assert len(sequence) == nqubits * sequence_len + assert len(sequence) == nqubits * int(gateargs[0] != gates.I) + nqubits def test_compile_two_gates(platform): @@ -61,9 +61,10 @@ def test_compile_two_gates(platform): sequence = compile_circuit(circuit, platform) - assert len(sequence) == 5 - assert len(sequence.qd_pulses) == 2 - assert len(sequence.ro_pulses) == 1 + qubit = platform.qubits[0] + assert len(sequence) == 2 + assert len(sequence[qubit.drive.name]) == 2 + assert len(sequence[qubit.probe.name]) == 2 # includes delay def test_measurement(platform): @@ -74,9 +75,7 @@ def test_measurement(platform): sequence = compile_circuit(circuit, platform) assert len(sequence) == 1 * nqubits - assert len(sequence.qd_pulses) == 0 * nqubits - assert len(sequence.qf_pulses) == 0 * nqubits - assert len(sequence.ro_pulses) == 1 * nqubits + assert len(sequence.probe_pulses) == 1 * nqubits def test_rz_to_sequence(platform): @@ -84,7 +83,8 @@ def test_rz_to_sequence(platform): circuit.add(gates.RZ(0, theta=0.2)) circuit.add(gates.Z(0)) sequence = compile_circuit(circuit, platform) - assert len(sequence) == 2 + assert len(sequence) == 1 + assert len(next(iter(sequence.values()))) == 2 def test_gpi_to_sequence(platform): @@ -92,12 +92,10 @@ def test_gpi_to_sequence(platform): circuit.add(gates.GPI(0, phi=0.2)) sequence = compile_circuit(circuit, platform) assert len(sequence) == 1 - assert len(sequence.qd_pulses) == 1 - rx_pulse = platform.create_RX_pulse(0, relative_phase=0.2) - s = PulseSequence([rx_pulse]) + rx_seq = platform.qubits[0].native_gates.RX.create_sequence(phi=0.2) - np.testing.assert_allclose(sequence.duration, rx_pulse.duration) + np.testing.assert_allclose(sequence.duration, rx_seq.duration) def test_gpi2_to_sequence(platform): @@ -105,13 +103,13 @@ def test_gpi2_to_sequence(platform): circuit.add(gates.GPI2(0, phi=0.2)) sequence = compile_circuit(circuit, platform) assert len(sequence) == 1 - assert len(sequence.qd_pulses) == 1 - rx90_pulse = platform.create_RX90_pulse(0, relative_phase=0.2) - s = PulseSequence([rx90_pulse]) + rx90_seq = platform.qubits[0].native_gates.RX.create_sequence( + theta=np.pi / 2, phi=0.2 + ) - np.testing.assert_allclose(sequence.duration, rx90_pulse.duration) - assert sequence == s + np.testing.assert_allclose(sequence.duration, rx90_seq.duration) + assert sequence == rx90_seq def test_cz_to_sequence(): @@ -120,7 +118,7 @@ def test_cz_to_sequence(): circuit.add(gates.CZ(1, 2)) sequence = compile_circuit(circuit, platform) - test_sequence = platform.create_CZ_pulse_sequence((2, 1)) + test_sequence = platform.pairs[(2, 1)].native_gates.CZ.create_sequence() assert sequence[0] == test_sequence[0] @@ -130,9 +128,8 @@ def test_cnot_to_sequence(): circuit.add(gates.CNOT(2, 3)) sequence = compile_circuit(circuit, platform) - test_sequence = platform.create_CNOT_pulse_sequence((2, 3)) - assert len(sequence) == len(test_sequence) + 1 - assert sequence[0] == test_sequence[0] + test_sequence = platform.pairs[(2, 3)].native_gates.CNOT.create_sequence() + assert sequence == test_sequence def test_add_measurement_to_sequence(platform): @@ -142,23 +139,18 @@ def test_add_measurement_to_sequence(platform): circuit.add(gates.M(0)) sequence = compile_circuit(circuit, platform) - assert len(sequence) == 5 - assert len(sequence.qd_pulses) == 2 - assert len(sequence.ro_pulses) == 1 - - rx90_pulse1 = platform.create_RX90_pulse(0, relative_phase=0.3) - rx90_pulse2 = platform.create_RX90_pulse(0, relative_phase=0.4 - np.pi) - mz_pulse = platform.create_MZ_pulse(0) - delay = 2 * rx90_pulse1.duration - s = PulseSequence( - [ - rx90_pulse1, - rx90_pulse2, - Delay(duration=delay, channel=mz_pulse.channel), - mz_pulse, - ] - ) - # assert sequence == s + qubit = platform.qubits[0] + assert len(sequence) == 2 + assert len(sequence[qubit.drive.name]) == 2 + assert len(sequence[qubit.probe.name]) == 2 # include delay + + s = PulseSequence() + s.extend(qubit.native_gates.RX.create_sequence(theta=np.pi / 2, phi=0.1)) + s.extend(qubit.native_gates.RX.create_sequence(theta=np.pi / 2, phi=0.2)) + s[qubit.probe.name].append(Delay(duration=s.duration)) + s.extend(qubit.native_gates.MZ.create_sequence()) + + assert sequence == s @pytest.mark.parametrize("delay", [0, 100]) @@ -168,10 +160,9 @@ def test_align_delay_measurement(platform, delay): circuit.add(gates.M(0)) sequence = compile_circuit(circuit, platform) - mz_pulse = platform.create_MZ_pulse(0) target_sequence = PulseSequence() if delay > 0: - target_sequence.append(Delay(duration=delay, channel=mz_pulse.channel)) - target_sequence.append(mz_pulse) + target_sequence[platform.qubits[0].probe.name].append(Delay(duration=delay)) + target_sequence.extend(platform.qubits[0].native_gates.MZ.create_sequence()) assert sequence == target_sequence - assert len(sequence.ro_pulses) == 1 + assert len(sequence.probe_pulses) == 1 diff --git a/tests/test_dummy.py b/tests/test_dummy.py index de37612a3..f55dcda39 100644 --- a/tests/test_dummy.py +++ b/tests/test_dummy.py @@ -2,10 +2,15 @@ import pytest from qibolab import AcquisitionType, AveragingMode, ExecutionParameters, create_platform -from qibolab.pulses import Delay, GaussianSquare, Pulse, PulseSequence, PulseType -from qibolab.qubits import QubitPair -from qibolab.serialize_ import replace -from qibolab.sweeper import Parameter, QubitParameter, Sweeper +from qibolab.pulses import ( + Delay, + Gaussian, + GaussianSquare, + Pulse, + PulseSequence, + PulseType, +) +from qibolab.sweeper import ChannelParameter, Parameter, Sweeper SWEPT_POINTS = 5 PLATFORM_NAMES = ["dummy", "dummy_couplers"] @@ -25,57 +30,60 @@ def test_dummy_initialization(name): def test_dummy_execute_pulse_sequence(name, acquisition): nshots = 100 platform = create_platform(name) - ro_pulse = platform.create_MZ_pulse(0) + probe_seq = platform.qubits[0].native_gates.MZ.create_sequence() + probe_pulse = next(iter(probe_seq.values()))[0] sequence = PulseSequence() - sequence.append(platform.create_MZ_pulse(0)) - sequence.append(platform.create_RX12_pulse(0)) + sequence.extend(probe_seq) + sequence.extend(platform.qubits[0].native_gates.RX12.create_sequence()) options = ExecutionParameters(nshots=100, acquisition_type=acquisition) result = platform.execute([sequence], options) if acquisition is AcquisitionType.INTEGRATION: - assert result[0][0].magnitude.shape == (nshots,) + assert result[probe_pulse.id][0].magnitude.shape == (nshots,) elif acquisition is AcquisitionType.RAW: - assert result[0][0].magnitude.shape == (nshots * ro_pulse.duration,) + assert result[probe_pulse.id][0].magnitude.shape == ( + nshots * probe_seq.duration, + ) def test_dummy_execute_coupler_pulse(): platform = create_platform("dummy_couplers") sequence = PulseSequence() - pulse = platform.create_coupler_pulse(coupler=0) - sequence.append(pulse) + channel = platform.get_coupler(0).flux + pulse = Pulse( + duration=30, + amplitude=0.05, + envelope=GaussianSquare(rel_sigma=5, width=0.75), + type=PulseType.COUPLERFLUX, + ) + sequence[channel.name].append(pulse) options = ExecutionParameters(nshots=None) - result = platform.execute([sequence], options) + _ = platform.execute([sequence], options) def test_dummy_execute_pulse_sequence_couplers(): platform = create_platform("dummy_couplers") - qubit_ordered_pair = QubitPair( - platform.qubits[1], platform.qubits[2], coupler=platform.couplers[1] - ) sequence = PulseSequence() - cz = platform.create_CZ_pulse_sequence( - qubits=(qubit_ordered_pair.qubit1.name, qubit_ordered_pair.qubit2.name), - ) - sequence.extend(cz.get_qubit_pulses(qubit_ordered_pair.qubit1.name)) - sequence.extend(cz.get_qubit_pulses(qubit_ordered_pair.qubit2.name)) - sequence.extend(cz.coupler_pulses(qubit_ordered_pair.coupler.name)) - sequence.append(Delay(duration=40, channel=platform.qubits[0].readout.name)) - sequence.append(Delay(duration=40, channel=platform.qubits[2].readout.name)) - sequence.append(platform.create_MZ_pulse(0)) - sequence.append(platform.create_MZ_pulse(2)) + cz = platform.pairs[(1, 2)].native_gates.CZ.create_sequence() + + sequence.extend(cz) + sequence[platform.qubits[0].probe.name].append(Delay(duration=40)) + sequence[platform.qubits[2].probe.name].append(Delay(duration=40)) + sequence.extend(platform.qubits[0].native_gates.MZ.create_sequence()) + sequence.extend(platform.qubits[2].native_gates.MZ.create_sequence()) options = ExecutionParameters(nshots=None) - result = platform.execute([sequence], options) + _ = platform.execute([sequence], options) @pytest.mark.parametrize("name", PLATFORM_NAMES) def test_dummy_execute_pulse_sequence_fast_reset(name): platform = create_platform(name) sequence = PulseSequence() - sequence.append(platform.create_MZ_pulse(0)) + sequence.extend(platform.qubits[0].native_gates.MZ.create_sequence()) options = ExecutionParameters(nshots=None, fast_reset=True) - result = platform.execute([sequence], options) + _ = platform.execute([sequence], options) @pytest.mark.parametrize("name", PLATFORM_NAMES) @@ -90,12 +98,12 @@ def test_dummy_execute_pulse_sequence_unrolling(name, acquisition, batch_size): platform.instruments["dummy"].UNROLLING_BATCH_SIZE = batch_size sequences = [] sequence = PulseSequence() - sequence.append(platform.create_MZ_pulse(0)) + sequence.extend(platform.qubits[0].native_gates.MZ.create_sequence()) for _ in range(nsequences): sequences.append(sequence) options = ExecutionParameters(nshots=nshots, acquisition_type=acquisition) result = platform.execute(sequences, options) - assert len(result[0]) == nsequences + assert len(next(iter(result.values()))) == nsequences for r in result[0]: if acquisition is AcquisitionType.INTEGRATION: assert r.magnitude.shape == (nshots,) @@ -107,19 +115,24 @@ def test_dummy_execute_pulse_sequence_unrolling(name, acquisition, batch_size): def test_dummy_single_sweep_raw(name): platform = create_platform(name) sequence = PulseSequence() - pulse = platform.create_MZ_pulse(qubit=0) + probe_seq = platform.qubits[0].native_gates.MZ.create_sequence() + pulse = next(iter(probe_seq.values()))[0] parameter_range = np.random.randint(SWEPT_POINTS, size=SWEPT_POINTS) - sequence.append(pulse) - sweeper = Sweeper(Parameter.frequency, parameter_range, pulses=[pulse]) + sequence.extend(probe_seq) + sweeper = Sweeper( + Parameter.frequency, + parameter_range, + channels=[platform.get_qubit(0).probe.name], + ) options = ExecutionParameters( nshots=10, averaging_mode=AveragingMode.CYCLIC, acquisition_type=AcquisitionType.RAW, ) results = platform.execute([sequence], options, [[sweeper]]) - assert pulse.id and pulse.qubit in results - shape = results[pulse.qubit][0].magnitude.shape + assert pulse.id in results + shape = results[pulse.id][0].magnitude.shape assert shape == (pulse.duration * SWEPT_POINTS,) @@ -137,22 +150,24 @@ def test_dummy_single_sweep_coupler( ): platform = create_platform("dummy_couplers") sequence = PulseSequence() - ro_pulse = platform.create_MZ_pulse(qubit=0) + probe_seq = platform.qubits[0].native_gates.MZ.create_sequence() + probe_pulse = next(iter(probe_seq.values()))[0] coupler_pulse = Pulse.flux( duration=40, amplitude=0.5, envelope=GaussianSquare(rel_sigma=0.2, width=0.75), - channel="flux_coupler-0", - qubit=0, + type=PulseType.COUPLERFLUX, ) - coupler_pulse = replace(coupler_pulse, type=PulseType.COUPLERFLUX) + sequence.extend(probe_seq) + sequence[platform.get_coupler(0).flux.name].append(coupler_pulse) if parameter is Parameter.amplitude: parameter_range = np.random.rand(SWEPT_POINTS) else: parameter_range = np.random.randint(SWEPT_POINTS, size=SWEPT_POINTS) - sequence.append(ro_pulse) - if parameter in QubitParameter: - sweeper = Sweeper(parameter, parameter_range, couplers=[platform.couplers[0]]) + if parameter in ChannelParameter: + sweeper = Sweeper( + parameter, parameter_range, channels=[platform.couplers[0].flux.name] + ) else: sweeper = Sweeper(parameter, parameter_range, pulses=[coupler_pulse]) options = ExecutionParameters( @@ -164,18 +179,18 @@ def test_dummy_single_sweep_coupler( average = not options.averaging_mode is AveragingMode.SINGLESHOT results = platform.execute([sequence], options, [[sweeper]]) - assert ro_pulse.id and ro_pulse.qubit in results + assert probe_pulse.id in results if average: results_shape = ( - results[ro_pulse.qubit][0].magnitude.shape + results[probe_pulse.id][0].magnitude.shape if acquisition is AcquisitionType.INTEGRATION - else results[ro_pulse.qubit][0].statistical_frequency.shape + else results[probe_pulse.id][0].statistical_frequency.shape ) else: results_shape = ( - results[ro_pulse.qubit][0].magnitude.shape + results[probe_pulse.id][0].magnitude.shape if acquisition is AcquisitionType.INTEGRATION - else results[ro_pulse.qubit][0].samples.shape + else results[probe_pulse.id][0].samples.shape ) assert results_shape == (SWEPT_POINTS,) if average else (nshots, SWEPT_POINTS) @@ -191,14 +206,20 @@ def test_dummy_single_sweep_coupler( def test_dummy_single_sweep(name, fast_reset, parameter, average, acquisition, nshots): platform = create_platform(name) sequence = PulseSequence() - pulse = platform.create_MZ_pulse(qubit=0) + probe_seq = platform.qubits[0].native_gates.MZ.create_sequence() + pulse = next(iter(probe_seq.values()))[0] if parameter is Parameter.amplitude: parameter_range = np.random.rand(SWEPT_POINTS) else: parameter_range = np.random.randint(SWEPT_POINTS, size=SWEPT_POINTS) - sequence.append(pulse) - if parameter in QubitParameter: - sweeper = Sweeper(parameter, parameter_range, qubits=[platform.qubits[0]]) + sequence.extend(probe_seq) + if parameter in ChannelParameter: + channel = ( + platform.qubits[0].drive.name + if parameter is Parameter.frequency + else platform.qubits[0].flux.name + ) + sweeper = Sweeper(parameter, parameter_range, channels=[channel]) else: sweeper = Sweeper(parameter, parameter_range, pulses=[pulse]) options = ExecutionParameters( @@ -210,18 +231,18 @@ def test_dummy_single_sweep(name, fast_reset, parameter, average, acquisition, n average = not options.averaging_mode is AveragingMode.SINGLESHOT results = platform.execute([sequence], options, [[sweeper]]) - assert pulse.id and pulse.qubit in results + assert pulse.id in results if average: results_shape = ( - results[pulse.qubit][0].magnitude.shape + results[pulse.id][0].magnitude.shape if acquisition is AcquisitionType.INTEGRATION - else results[pulse.qubit][0].statistical_frequency.shape + else results[pulse.id][0].statistical_frequency.shape ) else: results_shape = ( - results[pulse.qubit][0].magnitude.shape + results[pulse.id][0].magnitude.shape if acquisition is AcquisitionType.INTEGRATION - else results[pulse.qubit][0].samples.shape + else results[pulse.id][0].samples.shape ) assert results_shape == (SWEPT_POINTS,) if average else (nshots, SWEPT_POINTS) @@ -237,13 +258,14 @@ def test_dummy_single_sweep(name, fast_reset, parameter, average, acquisition, n def test_dummy_double_sweep(name, parameter1, parameter2, average, acquisition, nshots): platform = create_platform(name) sequence = PulseSequence() - pulse = platform.create_qubit_drive_pulse(qubit=0, duration=1000) - ro_pulse = platform.create_MZ_pulse(qubit=0) - sequence.append(pulse) - sequence.append( - Delay(duration=pulse.duration, channel=platform.qubits[0].readout.name) + pulse = Pulse( + duration=40, amplitude=0.1, envelope=Gaussian(rel_sigma=5), type=PulseType.DRIVE ) - sequence.append(ro_pulse) + probe_seq = platform.qubits[0].native_gates.MZ.create_sequence() + probe_pulse = next(iter(probe_seq.values()))[0] + sequence[platform.get_qubit(0).drive.name].append(pulse) + sequence[platform.qubits[0].probe.name].append(Delay(duration=pulse.duration)) + sequence.extend(probe_seq) parameter_range_1 = ( np.random.rand(SWEPT_POINTS) if parameter1 is Parameter.amplitude @@ -255,12 +277,19 @@ def test_dummy_double_sweep(name, parameter1, parameter2, average, acquisition, else np.random.randint(SWEPT_POINTS, size=SWEPT_POINTS) ) - if parameter1 in QubitParameter: - sweeper1 = Sweeper(parameter1, parameter_range_1, qubits=[platform.qubits[0]]) + if parameter1 in ChannelParameter: + channel = ( + platform.qubits[0].probe.name + if parameter1 is Parameter.frequency + else platform.qubits[0].flux.name + ) + sweeper1 = Sweeper(parameter1, parameter_range_1, channels=[channel]) else: - sweeper1 = Sweeper(parameter1, parameter_range_1, pulses=[ro_pulse]) - if parameter2 in QubitParameter: - sweeper2 = Sweeper(parameter2, parameter_range_2, qubits=[platform.qubits[0]]) + sweeper1 = Sweeper(parameter1, parameter_range_1, pulses=[probe_pulse]) + if parameter2 in ChannelParameter: + sweeper2 = Sweeper( + parameter2, parameter_range_2, channels=[platform.qubits[0].flux.name] + ) else: sweeper2 = Sweeper(parameter2, parameter_range_2, pulses=[pulse]) @@ -272,19 +301,19 @@ def test_dummy_double_sweep(name, parameter1, parameter2, average, acquisition, average = not options.averaging_mode is AveragingMode.SINGLESHOT results = platform.execute([sequence], options, [[sweeper1], [sweeper2]]) - assert ro_pulse.id and ro_pulse.qubit in results + assert probe_pulse.id in results if average: results_shape = ( - results[pulse.qubit][0].magnitude.shape + results[probe_pulse.id][0].magnitude.shape if acquisition is AcquisitionType.INTEGRATION - else results[pulse.qubit][0].statistical_frequency.shape + else results[probe_pulse.id][0].statistical_frequency.shape ) else: results_shape = ( - results[pulse.qubit][0].magnitude.shape + results[probe_pulse.id][0].magnitude.shape if acquisition is AcquisitionType.INTEGRATION - else results[pulse.qubit][0].samples.shape + else results[probe_pulse.id][0].samples.shape ) assert ( @@ -304,27 +333,28 @@ def test_dummy_double_sweep(name, parameter1, parameter2, average, acquisition, def test_dummy_single_sweep_multiplex(name, parameter, average, acquisition, nshots): platform = create_platform(name) sequence = PulseSequence() - ro_pulses = {} + probe_pulses = {} for qubit in platform.qubits: - ro_pulses[qubit] = platform.create_qubit_readout_pulse(qubit=qubit) - sequence.append(ro_pulses[qubit]) + probe_seq = platform.qubits[qubit].native_gates.MZ.create_sequence() + probe_pulses[qubit] = next(iter(probe_seq.values()))[0] + sequence.extend(probe_seq) parameter_range = ( np.random.rand(SWEPT_POINTS) if parameter is Parameter.amplitude else np.random.randint(SWEPT_POINTS, size=SWEPT_POINTS) ) - if parameter in QubitParameter: + if parameter in ChannelParameter: sweeper1 = Sweeper( parameter, parameter_range, - qubits=[platform.qubits[qubit] for qubit in platform.qubits], + channels=[qubit.probe.name for qubit in platform.qubits.values()], ) else: sweeper1 = Sweeper( parameter, parameter_range, - pulses=[ro_pulses[qubit] for qubit in platform.qubits], + pulses=[probe_pulses[qubit] for qubit in platform.qubits], ) options = ExecutionParameters( @@ -335,19 +365,19 @@ def test_dummy_single_sweep_multiplex(name, parameter, average, acquisition, nsh average = not options.averaging_mode is AveragingMode.SINGLESHOT results = platform.execute([sequence], options, [[sweeper1]]) - for ro_pulse in ro_pulses.values(): - assert ro_pulse.id and ro_pulse.qubit in results + for pulse in probe_pulses.values(): + assert pulse.id in results if average: results_shape = ( - results[ro_pulse.qubit][0].magnitude.shape + results[pulse.id][0].magnitude.shape if acquisition is AcquisitionType.INTEGRATION - else results[ro_pulse.qubit][0].statistical_frequency.shape + else results[pulse.id][0].statistical_frequency.shape ) else: results_shape = ( - results[ro_pulse.qubit][0].magnitude.shape + results[pulse.id][0].magnitude.shape if acquisition is AcquisitionType.INTEGRATION - else results[ro_pulse.qubit][0].samples.shape + else results[pulse.id][0].samples.shape ) assert results_shape == (SWEPT_POINTS,) if average else (nshots, SWEPT_POINTS) diff --git a/tests/test_emulator.py b/tests/test_emulator.py index 78612bfba..9c32d40f0 100644 --- a/tests/test_emulator.py +++ b/tests/test_emulator.py @@ -19,7 +19,7 @@ from qibolab.instruments.emulator.pulse_simulator import AVAILABLE_SWEEP_PARAMETERS from qibolab.platform.load import PLATFORMS from qibolab.pulses import PulseSequence -from qibolab.sweeper import Parameter, QubitParameter, Sweeper +from qibolab.sweeper import ChannelParameter, Parameter, Sweeper os.environ[PLATFORMS] = str(pathlib.Path(__file__).parent / "emulators/") @@ -95,7 +95,7 @@ def test_emulator_single_sweep( else: parameter_range = np.random.randint(1, 4, size=SWEPT_POINTS) sequence.add(pulse) - if parameter in QubitParameter: + if parameter in ChannelParameter: sweeper = Sweeper(parameter, parameter_range, qubits=[platform.qubits[0]]) else: sweeper = Sweeper(parameter, parameter_range, pulses=[pulse]) @@ -149,11 +149,11 @@ def test_emulator_double_sweep_false_history( if parameter2 is Parameter.amplitude else np.random.randint(1, 4, size=SWEPT_POINTS) ) - if parameter1 in QubitParameter: + if parameter1 in ChannelParameter: sweeper1 = Sweeper(parameter1, parameter_range_1, qubits=[platform.qubits[0]]) else: sweeper1 = Sweeper(parameter1, parameter_range_1, pulses=[ro_pulse]) - if parameter2 in QubitParameter: + if parameter2 in ChannelParameter: sweeper2 = Sweeper(parameter2, parameter_range_2, qubits=[platform.qubits[0]]) else: sweeper2 = Sweeper(parameter2, parameter_range_2, pulses=[pulse]) @@ -205,7 +205,7 @@ def test_emulator_single_sweep_multiplex( else np.random.randint(1, 4, size=SWEPT_POINTS) ) - if parameter in QubitParameter: + if parameter in ChannelParameter: sweeper1 = Sweeper( parameter, parameter_range, diff --git a/tests/test_instruments_rfsoc.py b/tests/test_instruments_rfsoc.py index 2399ba489..547078f47 100644 --- a/tests/test_instruments_rfsoc.py +++ b/tests/test_instruments_rfsoc.py @@ -688,7 +688,7 @@ def test_convert_av_sweep_results(dummy_qrc): averaging_mode=AveragingMode.CYCLIC, ) out_dict = instrument.convert_sweep_results( - sequence.ro_pulses, platform.qubits, avgi, avgq, execution_parameters + sequence.probe_pulses, platform.qubits, avgi, avgq, execution_parameters ) targ_dict = { serial1: AveragedIntegratedResults( @@ -741,7 +741,7 @@ def test_convert_nav_sweep_results(dummy_qrc): averaging_mode=AveragingMode.CYCLIC, ) out_dict = instrument.convert_sweep_results( - sequence.ro_pulses, platform.qubits, avgi, avgq, execution_parameters + sequence.probe_pulses, platform.qubits, avgi, avgq, execution_parameters ) targ_dict = { serial1: AveragedIntegratedResults( diff --git a/tests/test_instruments_zhinst.py b/tests/test_instruments_zhinst.py index b41be56e6..ea431ec9c 100644 --- a/tests/test_instruments_zhinst.py +++ b/tests/test_instruments_zhinst.py @@ -1,21 +1,13 @@ -import math -from collections import defaultdict - import laboneq.dsl.experiment.pulse as laboneq_pulse import laboneq.simple as lo import numpy as np import pytest from qibolab import AcquisitionType, AveragingMode, ExecutionParameters, create_platform -from qibolab.instruments.zhinst import ( - ProcessedSweeps, - ZhPulse, - Zurich, - acquire_channel_name, - classify_sweepers, - measure_channel_name, -) +from qibolab.instruments.zhinst import ProcessedSweeps, Zurich, classify_sweepers +from qibolab.instruments.zhinst.pulse import select_pulse from qibolab.pulses import ( + Delay, Drag, Gaussian, Iir, @@ -92,9 +84,9 @@ ), ], ) -def test_zhpulse_pulse_conversion(pulse): +def test_pulse_conversion(pulse): shape = pulse.shape - zhpulse = ZhPulse(pulse).zhpulse + zhpulse = select_pulse(pulse) assert isinstance(zhpulse, laboneq_pulse.Pulse) if isinstance(shape, (Snz, Iir)): assert len(zhpulse.samples) == 80 @@ -102,68 +94,29 @@ def test_zhpulse_pulse_conversion(pulse): assert zhpulse.length == 40e-9 -def test_zhpulse_add_sweeper(): - pulse = Pulse(0, 40, 0.05, int(3e9), 0.0, Gaussian(5), "ch", qubit=0) - zhpulse = ZhPulse(pulse) - assert zhpulse.zhsweepers == [] - assert zhpulse.delay_sweeper is None - - zhpulse.add_sweeper( - Parameter.duration, lo.SweepParameter(values=np.array([1, 2, 3])) - ) - assert len(zhpulse.zhsweepers) == 1 - assert zhpulse.delay_sweeper is None - - zhpulse.add_sweeper( - Parameter.start, lo.SweepParameter(values=np.array([4, 5, 6, 7])) - ) - assert len(zhpulse.zhsweepers) == 1 - assert zhpulse.delay_sweeper is not None - - zhpulse.add_sweeper( - Parameter.amplitude, lo.SweepParameter(values=np.array([3, 2, 1, 0])) - ) - assert len(zhpulse.zhsweepers) == 2 - assert zhpulse.delay_sweeper is not None - - -def test_measure_channel_name(dummy_qrc): - platform = create_platform("zurich") - qubits = platform.qubits.values() - meas_ch_names = {measure_channel_name(q) for q in qubits} - assert len(qubits) > 0 - assert len(meas_ch_names) == len(qubits) - - -def test_acquire_channel_name(dummy_qrc): - platform = create_platform("zurich") - qubits = platform.qubits.values() - acq_ch_names = {acquire_channel_name(q) for q in qubits} - assert len(qubits) > 0 - assert len(acq_ch_names) == len(qubits) - - def test_classify_sweepers(dummy_qrc): platform = create_platform("zurich") - qubit_id, qubit = 0, platform.qubits[0] - pulse_1 = Pulse(0, 40, 0.05, int(3e9), 0.0, Gaussian(5), "ch0", qubit=qubit_id) + qubit = platform.qubits[0] + pulse_1 = Pulse( + duration=40, + amplitude=0.05, + envelope=Gaussian(rel_sigma=5), + type=PulseType.DRIVE, + ) pulse_2 = Pulse( - 0, - 40, - 0.05, - int(3e9), - 0.0, - Rectangular(), - "ch7", - PulseType.READOUT, - qubit=qubit_id, + duration=40, + amplitude=0.05, + envelope=Rectangular(), + type=PulseType.READOUT, ) amplitude_sweeper = Sweeper(Parameter.amplitude, np.array([1, 2, 3]), [pulse_1]) readout_amplitude_sweeper = Sweeper( Parameter.amplitude, np.array([1, 2, 3, 4, 5]), [pulse_2] ) freq_sweeper = Sweeper(Parameter.frequency, np.array([4, 5, 6, 7]), [pulse_1]) - bias_sweeper = Sweeper(Parameter.bias, np.array([3, 2, 1]), qubits=[qubit]) + bias_sweeper = Sweeper( + Parameter.bias, np.array([3, 2, 1]), channels=[qubit.flux.name] + ) nt_sweeps, rt_sweeps = classify_sweepers( [amplitude_sweeper, readout_amplitude_sweeper, bias_sweeper, freq_sweeper] ) @@ -176,20 +129,27 @@ def test_classify_sweepers(dummy_qrc): def test_processed_sweeps_pulse_properties(dummy_qrc): platform = create_platform("zurich") - qubit_id_1, qubit_1 = 0, platform.qubits[0] - qubit_id_2, qubit_2 = 3, platform.qubits[3] + zi_instrument = platform.instruments["EL_ZURO"] pulse_1 = Pulse( - 0, 40, 0.05, int(3e9), 0.0, Gaussian(5), qubit_1.drive.name, qubit=qubit_id_1 + duration=40, + amplitude=0.05, + envelope=Gaussian(rel_sigma=5), + type=PulseType.DRIVE, ) pulse_2 = Pulse( - 0, 40, 0.05, int(3e9), 0.0, Gaussian(5), qubit_2.drive.name, qubit=qubit_id_2 + duration=40, + amplitude=0.05, + envelope=Gaussian(rel_sigma=5), + type=PulseType.DRIVE, ) sweeper_amplitude = Sweeper( Parameter.amplitude, np.array([1, 2, 3]), [pulse_1, pulse_2] ) sweeper_duration = Sweeper(Parameter.duration, np.array([1, 2, 3, 4]), [pulse_2]) processed_sweeps = ProcessedSweeps( - [sweeper_duration, sweeper_amplitude], qubits=platform.qubits + [sweeper_duration, sweeper_amplitude], + zi_instrument.channels.values(), + platform.configs, ) assert len(processed_sweeps.sweeps_for_pulse(pulse_1)) == 1 @@ -221,248 +181,108 @@ def test_processed_sweeps_pulse_properties(dummy_qrc): assert processed_sweeps.channels_with_sweeps() == set() -def test_processed_sweeps_frequency(dummy_qrc): - platform = create_platform("zurich") - qubit_id, qubit = 1, platform.qubits[1] - pulse = Pulse( - 0, 40, 0.05, int(3e9), 0.0, Gaussian(5), qubit.drive.name, qubit=qubit_id - ) - freq_sweeper = Sweeper(Parameter.frequency, np.array([1, 2, 3]), [pulse]) - processed_sweeps = ProcessedSweeps([freq_sweeper], platform.qubits) - - # Frequency sweepers should result into channel property sweeps - assert len(processed_sweeps.sweeps_for_pulse(pulse)) == 0 - assert processed_sweeps.channels_with_sweeps() == {qubit.drive.name} - assert len(processed_sweeps.sweeps_for_channel(qubit.drive.name)) == 1 - - with pytest.raises(ValueError): - flux_pulse = Pulse( - 0, - 40, - 0.05, - int(3e9), - 0.0, - Gaussian(5), - qubit.flux.name, - PulseType.FLUX, - qubit=qubit_id, - ) - freq_sweeper = Sweeper( - Parameter.frequency, np.array([1, 3, 5, 7]), [flux_pulse] - ) - ProcessedSweeps([freq_sweeper], platform.qubits) - - -def test_processed_sweeps_readout_amplitude(dummy_qrc): - platform = create_platform("zurich") - qubit_id, qubit = 0, platform.qubits[0] - readout_ch = measure_channel_name(qubit) - pulse_readout = Pulse( - 0, - 40, - 0.05, - int(3e9), - 0.0, - Rectangular(), - readout_ch, - PulseType.READOUT, - qubit_id, - ) - readout_amplitude_sweeper = Sweeper( - Parameter.amplitude, np.array([1, 2, 3, 4]), [pulse_readout] - ) - processed_sweeps = ProcessedSweeps( - [readout_amplitude_sweeper], qubits=platform.qubits - ) - - # Readout amplitude should result into channel property (gain) sweep - assert len(processed_sweeps.sweeps_for_pulse(pulse_readout)) == 0 - assert processed_sweeps.channels_with_sweeps() == { - readout_ch, - } - assert len(processed_sweeps.sweeps_for_channel(readout_ch)) == 1 +# def test_processed_sweeps_readout_amplitude(dummy_qrc): +# platform = create_platform("zurich") +# qubit_id, qubit = 0, platform.qubits[0] +# readout_ch = measure_channel_name(qubit) +# pulse_readout = Pulse( +# 0, +# 40, +# 0.05, +# int(3e9), +# 0.0, +# Rectangular(), +# readout_ch, +# PulseType.READOUT, +# qubit_id, +# ) +# readout_amplitude_sweeper = Sweeper( +# Parameter.amplitude, np.array([1, 2, 3, 4]), [pulse_readout] +# ) +# processed_sweeps = ProcessedSweeps( +# [readout_amplitude_sweeper], qubits=platform.qubits +# ) +# +# # Readout amplitude should result into channel property (gain) sweep +# assert len(processed_sweeps.sweeps_for_pulse(pulse_readout)) == 0 +# assert processed_sweeps.channels_with_sweeps() == { +# readout_ch, +# } +# assert len(processed_sweeps.sweeps_for_channel(readout_ch)) == 1 def test_zhinst_setup(dummy_qrc): platform = create_platform("zurich") - IQM5q = platform.instruments["EL_ZURO"] - assert IQM5q.time_of_flight == 75 - - -def test_zhsequence(dummy_qrc): - IQM5q = create_platform("zurich") - controller = IQM5q.instruments["EL_ZURO"] - - drive_channel, readout_channel = ( - IQM5q.qubits[0].drive.name, - measure_channel_name(IQM5q.qubits[0]), - ) - qd_pulse = Pulse(0, 40, 0.05, int(3e9), 0.0, Rectangular(), drive_channel, qubit=0) - ro_pulse = Pulse( - 0, - 40, - 0.05, - int(3e9), - 0.0, - Rectangular(), - readout_channel, - PulseType.READOUT, - qubit=0, - ) - sequence = PulseSequence() - sequence.append(qd_pulse) - sequence.append(qd_pulse) - sequence.append(ro_pulse) - - zhsequence = controller.sequence_zh(sequence, IQM5q.qubits) - - assert len(zhsequence) == 2 - assert len(zhsequence[drive_channel]) == 2 - assert len(zhsequence[readout_channel]) == 1 + zi_instrument = platform.instruments["EL_ZURO"] + assert zi_instrument.time_of_flight == 75 - with pytest.raises(AttributeError): - controller.sequence_zh("sequence", IQM5q.qubits) - -def test_zhsequence_couplers(dummy_qrc): - IQM5q = create_platform("zurich") - controller = IQM5q.instruments["EL_ZURO"] - - drive_channel, readout_channel = ( - IQM5q.qubits[0].drive.name, - measure_channel_name(IQM5q.qubits[0]), - ) - couplerflux_channel = IQM5q.couplers[0].flux.name - qd_pulse = Pulse(0, 40, 0.05, int(3e9), 0.0, Rectangular(), drive_channel, qubit=0) - ro_pulse = Pulse( - 0, - 40, - 0.05, - int(3e9), - 0.0, - Rectangular(), - readout_channel, - PulseType.READOUT, - qubit=0, - ) - qc_pulse = Pulse.flux( - 0, 40, 0.05, Rectangular(), channel=couplerflux_channel, qubit=3 - ) - qc_pulse.type = PulseType.COUPLERFLUX - sequence = PulseSequence() - sequence.append(qd_pulse) - sequence.append(ro_pulse) - sequence.append(qc_pulse) - - zhsequence = controller.sequence_zh(sequence, IQM5q.qubits) - - assert len(zhsequence) == 3 - assert len(zhsequence[couplerflux_channel]) == 1 - - -def test_zhsequence_multiple_ro(dummy_qrc): - platform = create_platform("zurich") - readout_channel = measure_channel_name(platform.qubits[0]) - sequence = PulseSequence() - qd_pulse = Pulse(0, 40, 0.05, int(3e9), 0.0, Rectangular(), "ch0", qubit=0) - sequence.append(qd_pulse) - ro_pulse = Pulse( - 0, - 40, - 0.05, - int(3e9), - 0.0, - Rectangular(), - readout_channel, - PulseType.READOUT, - qubit=0, - ) - sequence.append(ro_pulse) - ro_pulse = Pulse( - 0, - 5000, - 0.05, - int(3e9), - 0.0, - Rectangular(), - readout_channel, - PulseType.READOUT, - qubit=0, - ) - sequence.append(ro_pulse) +def test_zhinst_configure_acquire_line(dummy_qrc): platform = create_platform("zurich") + zi_instrument = platform.instruments["EL_ZURO"] + qubit = platform.qubits[0] - controller = platform.instruments["EL_ZURO"] - zhsequence = controller.sequence_zh(sequence, platform.qubits) + zi_instrument.configure_acquire_line(qubit.acquisition.name, platform.configs) - assert len(zhsequence) == 2 - assert len(zhsequence[readout_channel]) == 2 + assert qubit.acquisition.name in zi_instrument.signal_map + assert ( + "/logical_signal_groups/q0/acquire_line" + in zi_instrument.calibration.calibration_items + ) -def test_zhinst_register_readout_line(dummy_qrc): +def test_zhinst_configure_iq_line(dummy_qrc): platform = create_platform("zurich") - IQM5q = platform.instruments["EL_ZURO"] + zi_instrument = platform.instruments["EL_ZURO"] qubit = platform.qubits[0] + zi_instrument.configure_iq_line(qubit.drive.name, platform.configs) + zi_instrument.configure_iq_line(qubit.probe.name, platform.configs) - options = ExecutionParameters( - relaxation_time=300e-6, - acquisition_type=AcquisitionType.INTEGRATION, - averaging_mode=AveragingMode.CYCLIC, + assert qubit.drive.name in zi_instrument.signal_map + assert ( + "/logical_signal_groups/q0/drive_line" + in zi_instrument.calibration.calibration_items ) - IQM5q.register_readout_line(qubit, intermediate_frequency=int(1e6), options=options) - - assert measure_channel_name(qubit) in IQM5q.signal_map - assert acquire_channel_name(qubit) in IQM5q.signal_map + assert qubit.probe.name in zi_instrument.signal_map assert ( - "/logical_signal_groups/q0/measure_line" in IQM5q.calibration.calibration_items + "/logical_signal_groups/q0/measure_line" + in zi_instrument.calibration.calibration_items ) -def test_zhinst_register_drive_line(dummy_qrc): - platform = create_platform("zurich") - IQM5q = platform.instruments["EL_ZURO"] - qubit = platform.qubits[0] - IQM5q.register_drive_line(qubit, intermediate_frequency=int(1e6)) - - assert qubit.drive.name in IQM5q.signal_map - assert "/logical_signal_groups/q0/drive_line" in IQM5q.calibration.calibration_items - - -def test_zhinst_register_flux_line(dummy_qrc): +def test_zhinst_configure_dc_line(dummy_qrc): platform = create_platform("zurich") - IQM5q = platform.instruments["EL_ZURO"] + zi_instrument = platform.instruments["EL_ZURO"] qubit = platform.qubits[0] - IQM5q.register_flux_line(qubit) + zi_instrument.configure_dc_line(qubit.flux.name, platform.configs) - assert qubit.flux.name in IQM5q.signal_map - assert "/logical_signal_groups/q0/flux_line" in IQM5q.calibration.calibration_items + assert qubit.flux.name in zi_instrument.signal_map + assert ( + "/logical_signal_groups/q0/flux_line" + in zi_instrument.calibration.calibration_items + ) def test_experiment_flow(dummy_qrc): platform = create_platform("zurich") - IQM5q = platform.instruments["EL_ZURO"] + zi_instrument = platform.instruments["EL_ZURO"] sequence = PulseSequence() qubits = {0: platform.qubits[0], 2: platform.qubits[2]} platform.qubits = qubits couplers = {} - ro_pulses = {} - qf_pulses = {} for qubit in qubits.values(): - q = qubit.name - qf_pulses[q] = Pulse.flux( - start=0, - duration=500, - amplitude=1, - shape=Rectangular(), - channel=platform.qubits[q].flux.name, - qubit=q, + sequence[qubit.flux.name].append( + Pulse.flux( + duration=500, + amplitude=1, + envelope=Rectangular(), + ) ) - sequence.append(qf_pulses[q]) - ro_pulses[q] = platform.create_qubit_readout_pulse(q, start=qf_pulses[q].finish) - sequence.append(ro_pulses[q]) + sequence[qubit.probe.name].append(Delay(duration=sequence.duration)) + sequence.extend(qubit.native_gates.MZ.create_sequence()) options = ExecutionParameters( relaxation_time=300e-6, @@ -470,16 +290,16 @@ def test_experiment_flow(dummy_qrc): averaging_mode=AveragingMode.CYCLIC, ) - IQM5q.experiment_flow(qubits, couplers, sequence, options) + zi_instrument.experiment_flow(qubits, couplers, sequence, options) - assert qubits[0].flux.name in IQM5q.experiment.signals - assert measure_channel_name(qubits[0]) in IQM5q.experiment.signals - assert acquire_channel_name(qubits[0]) in IQM5q.experiment.signals + assert qubits[0].flux.name in zi_instrument.experiment.signals + assert qubits[0].probe.name in zi_instrument.experiment.signals + assert qubits[0].acquisition.name in zi_instrument.experiment.signals def test_experiment_flow_coupler(dummy_qrc): platform = create_platform("zurich") - IQM5q = platform.instruments["EL_ZURO"] + zi_instrument = platform.instruments["EL_ZURO"] sequence = PulseSequence() qubits = {0: platform.qubits[0], 2: platform.qubits[2]} @@ -487,35 +307,26 @@ def test_experiment_flow_coupler(dummy_qrc): couplers = {0: platform.couplers[0]} platform.couplers = couplers - ro_pulses = {} - qf_pulses = {} for qubit in qubits.values(): - q = qubit.name - qf_pulses[q] = Pulse.flux( - start=0, - duration=500, - amplitude=1, - shape=Rectangular(), - channel=platform.qubits[q].flux.name, - qubit=q, + sequence[qubit.flux.name].append( + Pulse.flux( + duration=500, + amplitude=1, + envelope=Rectangular(), + ) ) - sequence.append(qf_pulses[q]) - ro_pulses[q] = platform.create_qubit_readout_pulse(q, start=qf_pulses[q].finish) - sequence.append(ro_pulses[q]) + sequence[qubit.probe.name].append(Delay(duration=sequence.duration)) + sequence.extend(qubit.native_gates.MZ.create_sequence()) - cf_pulses = {} for coupler in couplers.values(): - c = coupler.name - cf_pulses[c] = Pulse.flux( - start=0, - duration=500, - amplitude=1, - shape=Rectangular(), - channel=platform.couplers[c].flux.name, - qubit=c, + sequence[coupler.flux.name].append( + Pulse( + duration=500, + amplitude=1, + envelope=Rectangular(), + type=PulseType.COUPLERFLUX, + ) ) - cf_pulses[c].type = PulseType.COUPLERFLUX - sequence.append(cf_pulses[c]) options = ExecutionParameters( relaxation_time=300e-6, @@ -523,38 +334,33 @@ def test_experiment_flow_coupler(dummy_qrc): averaging_mode=AveragingMode.CYCLIC, ) - IQM5q.experiment_flow(qubits, couplers, sequence, options) + zi_instrument.experiment_flow(qubits, couplers, sequence, options) - assert qubits[0].flux.name in IQM5q.experiment.signals - assert measure_channel_name(qubits[0]) in IQM5q.experiment.signals - assert acquire_channel_name(qubits[0]) in IQM5q.experiment.signals + assert qubits[0].flux.name in zi_instrument.experiment.signals + assert qubits[0].probe.name in zi_instrument.experiment.signals + assert qubits[0].acquisition.name in zi_instrument.experiment.signals def test_sweep_and_play_sim(dummy_qrc): """Test end-to-end experiment run using ZI emulated connection.""" platform = create_platform("zurich") - IQM5q = platform.instruments["EL_ZURO"] + zi_instrument = platform.instruments["EL_ZURO"] sequence = PulseSequence() qubits = {0: platform.qubits[0], 2: platform.qubits[2]} platform.qubits = qubits couplers = {} - ro_pulses = {} - qf_pulses = {} for qubit in qubits.values(): - q = qubit.name - qf_pulses[q] = Pulse.flux( - start=0, - duration=500, - amplitude=1, - shape=Rectangular(), - channel=platform.qubits[q].flux.name, - qubit=q, + sequence[qubit.flux.name].append( + Pulse.flux( + duration=500, + amplitude=1, + envelope=Rectangular(), + ) ) - sequence.append(qf_pulses[q]) - ro_pulses[q] = platform.create_qubit_readout_pulse(q, start=qf_pulses[q].finish) - sequence.append(ro_pulses[q]) + sequence[qubit.probe.name].append(Delay(duration=sequence.duration)) + sequence.extend(qubit.native_gates.MZ.create_sequence()) options = ExecutionParameters( relaxation_time=300e-6, @@ -564,21 +370,29 @@ def test_sweep_and_play_sim(dummy_qrc): ) # check play - IQM5q.session = lo.Session(IQM5q.device_setup) - IQM5q.session.connect(do_emulation=True) - res = IQM5q.play(qubits, couplers, sequence, options) + zi_instrument.session = lo.Session(zi_instrument.device_setup) + zi_instrument.session.connect(do_emulation=True) + res = zi_instrument.play(platform.configs, [sequence], options, {}) assert res is not None assert all(qubit in res for qubit in qubits) # check sweep with empty list of sweeps - res = IQM5q.sweep(qubits, couplers, sequence, options) + res = zi_instrument.sweep(platform.configs, [sequence], options, {}) assert res is not None assert all(qubit in res for qubit in qubits) # check sweep with sweeps - sweep_1 = Sweeper(Parameter.start, np.array([1, 2, 3, 4]), list(qf_pulses.values())) - sweep_2 = Sweeper(Parameter.bias, np.array([1, 2, 3]), qubits=[qubits[0]]) - res = IQM5q.sweep(qubits, couplers, sequence, options, sweep_1, sweep_2) + sweep_1 = Sweeper( + Parameter.amplitude, + np.array([1, 2, 3, 4]), + pulses=[sequence[qubit.flux.name][0] for qubit in qubits.values()], + ) + sweep_2 = Sweeper( + Parameter.bias, np.array([1, 2, 3]), channels=[qubits[0].flux.name] + ) + res = zi_instrument.sweep( + platform.configs, [sequence], options, {}, sweep_1, sweep_2 + ) assert res is not None assert all(qubit in res for qubit in qubits) @@ -586,22 +400,16 @@ def test_sweep_and_play_sim(dummy_qrc): @pytest.mark.parametrize("parameter1", [Parameter.duration]) def test_experiment_sweep_single(dummy_qrc, parameter1): platform = create_platform("zurich") - IQM5q = platform.instruments["EL_ZURO"] + zi_instrument = platform.instruments["EL_ZURO"] - qubits = {0: platform.qubits[0]} + qubit_id, qubit = 0, platform.qubits[0] couplers = {} swept_points = 5 sequence = PulseSequence() - ro_pulses = {} - qd_pulses = {} - for qubit in qubits: - qd_pulses[qubit] = platform.create_RX_pulse(qubit, start=0) - sequence.append(qd_pulses[qubit]) - ro_pulses[qubit] = platform.create_qubit_readout_pulse( - qubit, start=qd_pulses[qubit].finish - ) - sequence.append(ro_pulses[qubit]) + sequence.extend(qubit.native_gates.RX.create_sequence(theta=np.pi, phi=0.0)) + sequence[qubit.probe.name].append(Delay(duration=sequence.duration)) + sequence.extend(qubit.native_gates.MZ.create_sequence()) parameter_range_1 = ( np.random.rand(swept_points) @@ -610,7 +418,9 @@ def test_experiment_sweep_single(dummy_qrc, parameter1): ) sweepers = [] - sweepers.append(Sweeper(parameter1, parameter_range_1, pulses=[qd_pulses[qubit]])) + sweepers.append( + Sweeper(parameter1, parameter_range_1, pulses=[sequence[qubit.drive.name][0]]) + ) options = ExecutionParameters( relaxation_time=300e-6, @@ -618,46 +428,37 @@ def test_experiment_sweep_single(dummy_qrc, parameter1): averaging_mode=AveragingMode.CYCLIC, ) - IQM5q.experiment_flow(qubits, couplers, sequence, options) + zi_instrument.experiment_flow({qubit_id: qubit}, couplers, sequence, options) - assert qubits[0].drive.name in IQM5q.experiment.signals - assert measure_channel_name(qubits[0]) in IQM5q.experiment.signals - assert acquire_channel_name(qubits[0]) in IQM5q.experiment.signals + assert qubit.drive.name in zi_instrument.experiment.signals + assert qubit.probe.name in zi_instrument.experiment.signals + assert qubit.acquisition.name in zi_instrument.experiment.signals @pytest.mark.parametrize("parameter1", [Parameter.duration]) def test_experiment_sweep_single_coupler(dummy_qrc, parameter1): platform = create_platform("zurich") - IQM5q = platform.instruments["EL_ZURO"] + zi_instrument = platform.instruments["EL_ZURO"] qubits = {0: platform.qubits[0], 2: platform.qubits[2]} couplers = {0: platform.couplers[0]} swept_points = 5 sequence = PulseSequence() - ro_pulses = {} - qd_pulses = {} - for qubit in qubits: - qd_pulses[qubit] = platform.create_RX_pulse(qubit, start=0) - sequence.append(qd_pulses[qubit]) - ro_pulses[qubit] = platform.create_qubit_readout_pulse( - qubit, start=qd_pulses[qubit].finish - ) - sequence.append(ro_pulses[qubit]) + for qubit in qubits.values(): + sequence.extend(qubit.native_gates.RX.create_sequence(theta=np.pi, phi=0.0)) + sequence[qubit.probe.name].append(Delay(duration=sequence.duration)) + sequence.extend(qubit.native_gates.MZ.create_sequence()) - cf_pulses = {} for coupler in couplers.values(): - c = coupler.name - cf_pulses[c] = Pulse.flux( - start=0, - duration=500, - amplitude=1, - shape=Rectangular(), - channel=platform.couplers[c].flux.name, - qubit=c, + sequence[coupler.flux.name].append( + Pulse( + duration=500, + amplitude=1, + envelope=Rectangular(), + type=PulseType.COUPLERFLUX, + ) ) - cf_pulses[c].type = PulseType.COUPLERFLUX - sequence.append(cf_pulses[c]) parameter_range_1 = ( np.random.rand(swept_points) @@ -666,7 +467,13 @@ def test_experiment_sweep_single_coupler(dummy_qrc, parameter1): ) sweepers = [] - sweepers.append(Sweeper(parameter1, parameter_range_1, pulses=[cf_pulses[c]])) + sweepers.append( + Sweeper( + parameter1, + parameter_range_1, + pulses=[sequence[coupler.flux.name][0] for coupler in couplers.values()], + ) + ) options = ExecutionParameters( relaxation_time=300e-6, @@ -674,12 +481,12 @@ def test_experiment_sweep_single_coupler(dummy_qrc, parameter1): averaging_mode=AveragingMode.CYCLIC, ) - IQM5q.experiment_flow(qubits, couplers, sequence, options) + zi_instrument.experiment_flow(qubits, couplers, sequence, options) - assert couplers[0].flux.name in IQM5q.experiment.signals - assert qubits[0].drive.name in IQM5q.experiment.signals - assert measure_channel_name(qubits[0]) in IQM5q.experiment.signals - assert acquire_channel_name(qubits[0]) in IQM5q.experiment.signals + assert couplers[0].flux.name in zi_instrument.experiment.signals + assert qubits[0].drive.name in zi_instrument.experiment.signals + assert qubits[0].probe.name in zi_instrument.experiment.signals + assert qubits[0].acquisition.name in zi_instrument.experiment.signals SweeperParameter = { @@ -694,22 +501,17 @@ def test_experiment_sweep_single_coupler(dummy_qrc, parameter1): @pytest.mark.parametrize("parameter2", Parameter) def test_experiment_sweep_2d_general(dummy_qrc, parameter1, parameter2): platform = create_platform("zurich") - IQM5q = platform.instruments["EL_ZURO"] + zi_instrument = platform.instruments["EL_ZURO"] qubits = {0: platform.qubits[0]} couplers = {} swept_points = 5 sequence = PulseSequence() - ro_pulses = {} - qd_pulses = {} - for qubit in qubits: - qd_pulses[qubit] = platform.create_RX_pulse(qubit, start=0) - sequence.append(qd_pulses[qubit]) - ro_pulses[qubit] = platform.create_qubit_readout_pulse( - qubit, start=qd_pulses[qubit].finish - ) - sequence.append(ro_pulses[qubit]) + for qubit in qubits.values(): + sequence.extend(qubit.native_gates.RX.create_sequence(theta=np.pi, phi=0.0)) + sequence[qubit.probe.name].append(Delay(duration=sequence.duration)) + sequence.extend(qubit.native_gates.MZ.create_sequence()) parameter_range_1 = ( np.random.rand(swept_points) @@ -727,13 +529,23 @@ def test_experiment_sweep_2d_general(dummy_qrc, parameter1, parameter2): if parameter1 in SweeperParameter: if parameter1 is not Parameter.start: sweepers.append( - Sweeper(parameter1, parameter_range_1, pulses=[ro_pulses[qubit]]) + Sweeper( + parameter1, + parameter_range_1, + pulses=[sequence[qubit.probe.name][0] for qubit in qubits.values()], + ) ) if parameter2 in SweeperParameter: if parameter2 is Parameter.amplitude: if parameter1 is not Parameter.amplitude: sweepers.append( - Sweeper(parameter2, parameter_range_2, pulses=[qd_pulses[qubit]]) + Sweeper( + parameter2, + parameter_range_2, + pulses=[ + sequence[qubit.drive.name][0] for qubit in qubits.values() + ], + ) ) options = ExecutionParameters( @@ -742,31 +554,26 @@ def test_experiment_sweep_2d_general(dummy_qrc, parameter1, parameter2): averaging_mode=AveragingMode.CYCLIC, ) - IQM5q.experiment_flow(qubits, couplers, sequence, options) + zi_instrument.experiment_flow(qubits, couplers, sequence, options) - assert qubits[0].drive.name in IQM5q.experiment.signals - assert measure_channel_name(qubits[0]) in IQM5q.experiment.signals - assert acquire_channel_name(qubits[0]) in IQM5q.experiment.signals + assert qubits[0].drive.name in zi_instrument.experiment.signals + assert qubits[0].probe.name in zi_instrument.experiment.signals + assert qubits[0].acquisition.name in zi_instrument.experiment.signals def test_experiment_sweep_2d_specific(dummy_qrc): platform = create_platform("zurich") - IQM5q = platform.instruments["EL_ZURO"] + zi_instrument = platform.instruments["EL_ZURO"] qubits = {0: platform.qubits[0]} couplers = {} swept_points = 5 sequence = PulseSequence() - ro_pulses = {} - qd_pulses = {} - for qubit in qubits: - qd_pulses[qubit] = platform.create_RX_pulse(qubit, start=0) - sequence.append(qd_pulses[qubit]) - ro_pulses[qubit] = platform.create_qubit_readout_pulse( - qubit, start=qd_pulses[qubit].finish - ) - sequence.append(ro_pulses[qubit]) + for qubit in qubits.values(): + sequence.extend(qubit.native_gates.RX.create_sequence(theta=np.pi, phi=0.0)) + sequence[qubit.probe.name].append(Delay(duration=sequence.duration)) + sequence.extend(qubit.native_gates.MZ.create_sequence()) parameter1 = Parameter.relative_phase parameter2 = Parameter.frequency @@ -784,8 +591,9 @@ def test_experiment_sweep_2d_specific(dummy_qrc): ) sweepers = [] - sweepers.append(Sweeper(parameter1, parameter_range_1, pulses=[qd_pulses[qubit]])) - sweepers.append(Sweeper(parameter2, parameter_range_2, pulses=[qd_pulses[qubit]])) + qd_pulses = [sequence[qubit.drive.name][0] for qubit in qubits.values()] + sweepers.append(Sweeper(parameter1, parameter_range_1, pulses=qd_pulses)) + sweepers.append(Sweeper(parameter2, parameter_range_2, pulses=qd_pulses)) options = ExecutionParameters( relaxation_time=300e-6, @@ -793,11 +601,11 @@ def test_experiment_sweep_2d_specific(dummy_qrc): averaging_mode=AveragingMode.CYCLIC, ) - IQM5q.experiment_flow(qubits, couplers, sequence, options) + zi_instrument.experiment_flow(qubits, couplers, sequence, options) - assert qubits[0].drive.name in IQM5q.experiment.signals - assert measure_channel_name(qubits[0]) in IQM5q.experiment.signals - assert acquire_channel_name(qubits[0]) in IQM5q.experiment.signals + assert qubits[0].drive.name in zi_instrument.experiment.signals + assert qubits[0].probe.name in zi_instrument.experiment.signals + assert qubits[0].acquisition.name in zi_instrument.experiment.signals @pytest.mark.parametrize( @@ -805,7 +613,7 @@ def test_experiment_sweep_2d_specific(dummy_qrc): ) def test_experiment_sweep_punchouts(dummy_qrc, parameter): platform = create_platform("zurich") - IQM5q = platform.instruments["EL_ZURO"] + zi_instrument = platform.instruments["EL_ZURO"] qubits = {0: platform.qubits[0]} couplers = {} @@ -822,10 +630,8 @@ def test_experiment_sweep_punchouts(dummy_qrc, parameter): swept_points = 5 sequence = PulseSequence() - ro_pulses = {} - for qubit in qubits: - ro_pulses[qubit] = platform.create_qubit_readout_pulse(qubit, start=0) - sequence.append(ro_pulses[qubit]) + for qubit in qubits.values(): + sequence.extend(qubit.native_gates.MZ.create_sequence()) parameter_range_1 = ( np.random.rand(swept_points) @@ -840,13 +646,18 @@ def test_experiment_sweep_punchouts(dummy_qrc, parameter): ) sweepers = [] + ro_pulses = [sequence[qubit.probe.name][0] for qubit in qubits.values()] if parameter1 is Parameter.bias: - sweepers.append(Sweeper(parameter1, parameter_range_1, qubits=[qubits[qubit]])) - else: sweepers.append( - Sweeper(parameter1, parameter_range_1, pulses=[ro_pulses[qubit]]) + Sweeper( + parameter1, + parameter_range_1, + channels=[qubit.probe.name for qubit in qubits.values()], + ) ) - sweepers.append(Sweeper(parameter2, parameter_range_2, pulses=[ro_pulses[qubit]])) + else: + sweepers.append(Sweeper(parameter1, parameter_range_1, pulses=ro_pulses)) + sweepers.append(Sweeper(parameter2, parameter_range_2, pulses=ro_pulses)) options = ExecutionParameters( relaxation_time=300e-6, @@ -854,10 +665,10 @@ def test_experiment_sweep_punchouts(dummy_qrc, parameter): averaging_mode=AveragingMode.CYCLIC, ) - IQM5q.experiment_flow(qubits, couplers, sequence, options) + zi_instrument.experiment_flow(qubits, couplers, sequence, options) - assert measure_channel_name(qubits[0]) in IQM5q.experiment.signals - assert acquire_channel_name(qubits[0]) in IQM5q.experiment.signals + assert qubits[0].probe.name in zi_instrument.experiment.signals + assert qubits[0].acquisition.name in zi_instrument.experiment.signals def test_batching(dummy_qrc): @@ -865,11 +676,17 @@ def test_batching(dummy_qrc): instrument = platform.instruments["EL_ZURO"] sequence = PulseSequence() - sequence.append(platform.create_RX_pulse(0, start=0)) - sequence.append(platform.create_RX_pulse(1, start=0)) - measurement_start = sequence.finish - sequence.append(platform.create_MZ_pulse(0, start=measurement_start)) - sequence.append(platform.create_MZ_pulse(1, start=measurement_start)) + sequence.extend( + platform.qubits[0].native_gates.RX.create_sequence(theta=np.pi, phi=0.0) + ) + sequence.extend( + platform.qubits[1].native_gates.RX.create_sequence(theta=np.pi, phi=0.0) + ) + measurement_start = sequence.duration + sequence[platform.qubits[0].probe.name].append(Delay(duration=measurement_start)) + sequence[platform.qubits[1].probe.name].append(Delay(duration=measurement_start)) + sequence.extend(platform.qubits[0].native_gates.MZ.create_sequence()) + sequence.extend(platform.qubits[1].native_gates.MZ.create_sequence()) batches = list(batch(600 * [sequence], instrument.bounds)) # These sequences get limited by the number of measuraments (600/250/2) @@ -898,23 +715,19 @@ def test_experiment_execute_qpu(connected_platform, instrument): qubits = {0: platform.qubits[0], "c0": platform.qubits["c0"]} platform.qubits = qubits - ro_pulses = {} - qf_pulses = {} for qubit in qubits.values(): - q = qubit.name - qf_pulses[q] = Pulse.flux( - start=0, - duration=500, - amplitude=1, - shape=Rectangular(), - channel=platform.qubits[q].flux.name, - qubit=q, + sequence[qubit.flux.name].append( + Pulse.flux( + duration=500, + amplitude=1, + envelope=Rectangular(), + ) ) - sequence.append(qf_pulses[q]) if qubit.flux_coupler: continue - ro_pulses[q] = platform.create_qubit_readout_pulse(q, start=qf_pulses[q].finish) - sequence.append(ro_pulses[q]) + + sequence[qubit.probe.name].append(Delay(duration=sequence.duration)) + sequence.extend(qubit.native_gates.MZ.create_sequence()) options = ExecutionParameters( relaxation_time=300e-6, @@ -923,8 +736,7 @@ def test_experiment_execute_qpu(connected_platform, instrument): ) results = platform.execute([sequence], options) - - assert len(results[ro_pulses[q][0].id]) > 0 + assert all(len(results[sequence.probe_pulses[q].id]) > 1 for q in qubits) @pytest.mark.qpu @@ -934,15 +746,10 @@ def test_experiment_sweep_2d_specific_qpu(connected_platform, instrument): swept_points = 5 sequence = PulseSequence() - ro_pulses = {} - qd_pulses = {} - for qubit in qubits: - qd_pulses[qubit] = platform.create_RX_pulse(qubit, start=0) - sequence.append(qd_pulses[qubit]) - ro_pulses[qubit] = platform.create_qubit_readout_pulse( - qubit, start=qd_pulses[qubit].finish - ) - sequence.append(ro_pulses[qubit]) + for qubit in qubits.values(): + sequence.extend(qubit.native_gates.RX.create_sequence(theta=np.pi, phi=0.0)) + sequence[qubit.probe.name].append(Delay(duration=sequence.duration)) + sequence.extend(qubit.native_gates.MZ.create_sequence()) parameter1 = Parameter.relative_phase parameter2 = Parameter.frequency @@ -960,8 +767,9 @@ def test_experiment_sweep_2d_specific_qpu(connected_platform, instrument): ) sweepers = [] - sweepers.append(Sweeper(parameter1, parameter_range_1, pulses=[qd_pulses[qubit]])) - sweepers.append(Sweeper(parameter2, parameter_range_2, pulses=[qd_pulses[qubit]])) + qd_pulses = [sequence[qubit.drive.name][0] for qubit in qubits.values()] + sweepers.append(Sweeper(parameter1, parameter_range_1, pulses=qd_pulses)) + sweepers.append(Sweeper(parameter2, parameter_range_2, pulses=qd_pulses)) options = ExecutionParameters( relaxation_time=300e-6, @@ -976,69 +784,4 @@ def test_experiment_sweep_2d_specific_qpu(connected_platform, instrument): sweepers[1], ) - assert len(results[ro_pulses[qubit].id]) > 0 - - -def get_previous_subsequence_finish(instrument, name): - """Look recursively for sub_section finish times.""" - section = next( - iter(ch for ch in instrument.experiment.sections[0].children if ch.uid == name) - ) - finish = defaultdict(int) - for pulse in section.children: - try: - finish[pulse.signal] += pulse.time - except AttributeError: - # not a laboneq Delay class object, skipping - pass - try: - finish[pulse.signal] += pulse.pulse.length - except AttributeError: - # not a laboneq PlayPulse class object, skipping - pass - return max(finish.values()) - - -def test_experiment_measurement_sequence(dummy_qrc): - platform = create_platform("zurich") - IQM5q = platform.instruments["EL_ZURO"] - - sequence = PulseSequence() - qubits = {0: platform.qubits[0]} - platform.qubits = qubits - couplers = {} - - readout_pulse_start = 40 - - for qubit in qubits: - qubit_drive_pulse_1 = platform.create_qubit_drive_pulse( - qubit, start=0, duration=40 - ) - ro_pulse = platform.create_qubit_readout_pulse(qubit, start=readout_pulse_start) - qubit_drive_pulse_2 = platform.create_qubit_drive_pulse( - qubit, start=readout_pulse_start + 50, duration=40 - ) - sequence.append(qubit_drive_pulse_1) - sequence.append(ro_pulse) - sequence.append(qubit_drive_pulse_2) - - options = ExecutionParameters( - relaxation_time=4, - acquisition_type=AcquisitionType.INTEGRATION, - averaging_mode=AveragingMode.CYCLIC, - ) - - IQM5q.experiment_flow(qubits, couplers, sequence, options) - measure_start = 0 - for section in IQM5q.experiment.sections[0].children: - if section.uid == "measure_0": - measure_start += get_previous_subsequence_finish(IQM5q, section.play_after) - for pulse in section.children: - try: - if pulse.signal == measure_channel_name(qubits[0]): - measure_start += pulse.time - except AttributeError: - # not a laboneq delay class object, skipping - pass - - assert math.isclose(measure_start * 1e9, readout_pulse_start, rel_tol=1e-4) + assert all(len(results[sequence.probe_pulses[q].id]) for q in qubits) > 0 diff --git a/tests/test_native.py b/tests/test_native.py new file mode 100644 index 000000000..c275773df --- /dev/null +++ b/tests/test_native.py @@ -0,0 +1,213 @@ +import contextlib + +import numpy as np +import pytest + +from qibolab.native import FixedSequenceFactory, RxyFactory, TwoQubitNatives +from qibolab.pulses import ( + Drag, + Exponential, + Gaussian, + GaussianSquare, + Pulse, + PulseSequence, + Rectangular, +) + + +def test_fixed_sequence_factory(): + seq = PulseSequence() + seq["channel_1"].append( + Pulse( + duration=40, + amplitude=0.3, + envelope=Gaussian(rel_sigma=3.0), + ) + ) + seq["channel_17"].append( + Pulse( + duration=125, + amplitude=1.0, + envelope=Rectangular(), + ) + ) + factory = FixedSequenceFactory(seq) + + fseq1 = factory.create_sequence() + fseq2 = factory.create_sequence() + assert fseq1 == seq + assert fseq2 == seq + + fseq1["new channel"].append( + Pulse( + duration=30, + amplitude=0.04, + envelope=Drag(rel_sigma=4.0, beta=0.02), + ) + ) + assert "new channel" not in seq + assert "new channel" not in fseq2 + + +@pytest.mark.parametrize( + "args,amplitude,phase", + [ + ({}, 1.0, 0.0), + ({"theta": np.pi / 2}, 0.5, 0.0), + ({"phi": np.pi / 4}, 1.0, np.pi / 4), + ({"theta": np.pi / 4, "phi": np.pi / 3}, 1.0 / 4, np.pi / 3), + ({"theta": 3 * np.pi / 2}, -0.5, 0.0), + ({"theta": 7 * np.pi}, 1.0, 0.0), + ({"theta": 7.5 * np.pi}, -0.5, 0.0), + ({"phi": 7.5 * np.pi}, 1.0, 1.5 * np.pi), + ], +) +def test_rxy_rotation_factory(args, amplitude, phase): + seq = PulseSequence( + { + "channel_1": [ + Pulse( + duration=40, + amplitude=1.0, + envelope=Gaussian(rel_sigma=3.0), + ) + ] + } + ) + factory = RxyFactory(seq) + + fseq1 = factory.create_sequence(**args) + fseq2 = factory.create_sequence(**args) + assert fseq1 == fseq2 + fseq2["new channel"].append( + Pulse( + duration=56, + amplitude=0.43, + envelope=Rectangular(), + ) + ) + assert "new channel" not in fseq1 + + pulse = fseq1["channel_1"][0] + assert pulse.amplitude == pytest.approx(amplitude) + assert pulse.relative_phase == pytest.approx(phase) + + +def test_rxy_factory_multiple_channels(): + seq = PulseSequence( + { + "channel_1": [ + Pulse( + duration=40, + amplitude=0.7, + envelope=Gaussian(rel_sigma=5.0), + ) + ], + "channel_2": [ + Pulse( + duration=30, + amplitude=1.0, + envelope=Gaussian(rel_sigma=3.0), + ) + ], + } + ) + + with pytest.raises(ValueError, match="Incompatible number of channels"): + _ = RxyFactory(seq) + + +def test_rxy_factory_multiple_pulses(): + seq = PulseSequence( + { + "channel_1": [ + Pulse( + duration=40, + amplitude=0.08, + envelope=Gaussian(rel_sigma=4.0), + ), + Pulse( + duration=80, + amplitude=0.76, + envelope=Gaussian(rel_sigma=4.0), + ), + ] + } + ) + + with pytest.raises(ValueError, match="Incompatible number of pulses"): + _ = RxyFactory(seq) + + +@pytest.mark.parametrize( + "envelope", + [ + Gaussian(rel_sigma=3.0), + GaussianSquare(rel_sigma=3.0, width=80), + Drag(rel_sigma=5.0, beta=-0.037), + Rectangular(), + Exponential(tau=0.7, upsilon=0.8), + ], +) +def test_rxy_rotation_factory_envelopes(envelope): + seq = PulseSequence( + { + "channel_1": [ + Pulse( + duration=100, + amplitude=1.0, + envelope=envelope, + ) + ] + } + ) + + if isinstance(envelope, (Gaussian, Drag)): + context = contextlib.nullcontext() + else: + context = pytest.raises(ValueError, match="Incompatible pulse envelope") + + with context: + _ = RxyFactory(seq) + + +def test_two_qubit_natives_symmetric(): + natives = TwoQubitNatives( + CZ=FixedSequenceFactory(PulseSequence()), + CNOT=FixedSequenceFactory(PulseSequence()), + iSWAP=FixedSequenceFactory(PulseSequence()), + ) + assert natives.symmetric is False + + natives = TwoQubitNatives( + CZ=FixedSequenceFactory(PulseSequence()), + iSWAP=FixedSequenceFactory(PulseSequence()), + ) + assert natives.symmetric is True + + natives = TwoQubitNatives( + CZ=FixedSequenceFactory(PulseSequence()), + ) + assert natives.symmetric is True + + natives = TwoQubitNatives( + iSWAP=FixedSequenceFactory(PulseSequence()), + ) + assert natives.symmetric is True + + natives = TwoQubitNatives( + CNOT=FixedSequenceFactory(PulseSequence()), + ) + assert natives.symmetric is False + + natives = TwoQubitNatives( + CZ=FixedSequenceFactory(PulseSequence()), + CNOT=FixedSequenceFactory(PulseSequence()), + ) + assert natives.symmetric is False + + natives = TwoQubitNatives( + CNOT=FixedSequenceFactory(PulseSequence()), + iSWAP=FixedSequenceFactory(PulseSequence()), + ) + assert natives.symmetric is False diff --git a/tests/test_platform.py b/tests/test_platform.py index 9ae82b0ff..2b12df79e 100644 --- a/tests/test_platform.py +++ b/tests/test_platform.py @@ -14,15 +14,16 @@ from qibolab import create_platform from qibolab.backends import QibolabBackend +from qibolab.components import IqConfig, OscillatorConfig from qibolab.dummy import create_dummy from qibolab.dummy.platform import FOLDER from qibolab.execution_parameters import ExecutionParameters from qibolab.instruments.qblox.controller import QbloxController -from qibolab.instruments.rfsoc.driver import RFSoC from qibolab.kernels import Kernels from qibolab.platform import Platform, unroll_sequences from qibolab.platform.load import PLATFORMS -from qibolab.pulses import Delay, Drag, PulseSequence, Rectangular +from qibolab.platform.platform import update_configs +from qibolab.pulses import Delay, Gaussian, Pulse, PulseSequence, PulseType, Rectangular from qibolab.serialize import ( PLATFORM, dump_kernels, @@ -38,19 +39,15 @@ def test_unroll_sequences(platform): - qubit = next(iter(platform.qubits)) + qubit = next(iter(platform.qubits.values())) sequence = PulseSequence() - qd_pulse = platform.create_RX_pulse(qubit) - ro_pulse = platform.create_MZ_pulse(qubit) - sequence.append(qd_pulse) - sequence.append( - Delay(duration=qd_pulse.duration, channel=platform.qubits[qubit].readout.name) - ) - sequence.append(ro_pulse) + sequence.extend(qubit.native_gates.RX.create_sequence()) + sequence[qubit.probe.name].append(Delay(duration=sequence.duration)) + sequence.extend(qubit.native_gates.MZ.create_sequence()) total_sequence, readouts = unroll_sequences(10 * [sequence], relaxation_time=10000) - assert len(total_sequence.ro_pulses) == 10 + assert len(total_sequence.probe_pulses) == 10 assert len(readouts) == 1 - assert len(readouts[ro_pulse.id]) == 10 + assert all(len(readouts[pulse.id]) == 10 for pulse in sequence.probe_pulses) def test_create_platform(platform): @@ -81,7 +78,7 @@ def test_create_platform_multipath(tmp_path: Path): from qibolab.platform import Platform def create(): - return Platform("{p.parent.name}-{p.name}", {{}}, {{}}, {{}}) + return Platform("{p.parent.name}-{p.name}", {{}}, {{}}, {{}}, {{}}) """ ) ) @@ -102,6 +99,38 @@ def test_platform_sampling_rate(platform): assert platform.sampling_rate >= 1 +def test_update_configs(platform): + drive_name = "q0/drive" + pump_name = "twpa_pump" + configs = { + drive_name: IqConfig(4.1e9), + pump_name: OscillatorConfig(3e9, -5), + } + + updated = update_configs(configs, [{drive_name: {"frequency": 4.2e9}}]) + assert updated is None + assert configs[drive_name].frequency == 4.2e9 + + update_configs( + configs, [{drive_name: {"frequency": 4.3e9}, pump_name: {"power": -10}}] + ) + assert configs[drive_name].frequency == 4.3e9 + assert configs[pump_name].frequency == 3e9 + assert configs[pump_name].power == -10 + + update_configs( + configs, + [{drive_name: {"frequency": 4.4e9}}, {drive_name: {"frequency": 4.5e9}}], + ) + assert configs[drive_name].frequency == 4.5e9 + + with pytest.raises(ValueError, match="unknown component"): + update_configs(configs, [{"non existent": {"property": 1.0}}]) + + with pytest.raises(TypeError, match="prprty"): + update_configs(configs, [{pump_name: {"prprty": 0.7}}]) + + def test_dump_runcard(platform, tmp_path): dump_runcard(platform, tmp_path) final_runcard = load_runcard(tmp_path) @@ -110,6 +139,7 @@ def test_dump_runcard(platform, tmp_path): else: target_path = pathlib.Path(__file__).parent / "dummy_qrc" / f"{platform.name}" target_runcard = load_runcard(target_path) + # for the characterization section the dumped runcard may contain # some default ``Qubit`` parameters target_char = target_runcard.pop("characterization")["single_qubit"] @@ -125,6 +155,20 @@ def test_dump_runcard(platform, tmp_path): assert final_instruments == target_instruments +def test_dump_runcard_with_updates(platform, tmp_path): + qubit = next(iter(platform.qubits.values())) + frequency = platform.config(qubit.drive.name).frequency + 1.5e9 + smearing = platform.config(qubit.acquisition.name).smearing + 10 + update = { + qubit.drive.name: {"frequency": frequency}, + qubit.acquisition.name: {"smearing": smearing}, + } + dump_runcard(platform, tmp_path, [update]) + final_runcard = load_runcard(tmp_path) + assert final_runcard["components"][qubit.drive.name]["frequency"] == frequency + assert final_runcard["components"][qubit.acquisition.name]["smearing"] == smearing + + @pytest.mark.parametrize("has_kernels", [False, True]) def test_kernels(tmp_path, has_kernels): """Test dumping and loading of `Kernels`.""" @@ -176,17 +220,30 @@ def test_platform_execute_empty(qpu_platform): # an empty pulse sequence platform = qpu_platform sequence = PulseSequence() - platform.execute_pulse_sequence(sequence, ExecutionParameters(nshots=nshots)) + result = platform.execute_pulse_sequence( + sequence, ExecutionParameters(nshots=nshots) + ) + assert result is not None @pytest.mark.qpu def test_platform_execute_one_drive_pulse(qpu_platform): # One drive pulse platform = qpu_platform - qubit = next(iter(platform.qubits)) + qubit = next(iter(platform.qubits.values())) sequence = PulseSequence() - sequence.append(platform.create_qubit_drive_pulse(qubit, duration=200)) - platform.execute_pulse_sequence(sequence, ExecutionParameters(nshots=nshots)) + sequence[qubit.drive.name].append( + Pulse( + duration=200, + amplitude=0.07, + envelope=Gaussian(5), + type=PulseType.DRIVE, + ) + ) + result = platform.execute_pulse_sequence( + sequence, ExecutionParameters(nshots=nshots) + ) + assert result is not None @pytest.mark.qpu @@ -195,42 +252,56 @@ def test_platform_execute_one_coupler_pulse(qpu_platform): platform = qpu_platform if len(platform.couplers) == 0: pytest.skip("The platform does not have couplers") - coupler = next(iter(platform.couplers)) + coupler = next(iter(platform.couplers.values())) sequence = PulseSequence() - sequence.append(platform.create_coupler_pulse(coupler, duration=200, amplitude=1)) - platform.execute_pulse_sequence(sequence, ExecutionParameters(nshots=nshots)) - assert len(sequence.cf_pulses) > 0 + sequence[coupler.flux.name].append( + Pulse( + duration=200, + amplitude=0.31, + envelope=Rectangular(), + type=PulseType.COUPLERFLUX, + ) + ) + result = platform.execute_pulse_sequence( + sequence, ExecutionParameters(nshots=nshots) + ) + assert result is not None @pytest.mark.qpu def test_platform_execute_one_flux_pulse(qpu_platform): # One flux pulse platform = qpu_platform - qubit = next(iter(platform.qubits)) + qubit = next(iter(platform.qubits.values())) sequence = PulseSequence() - sequence.add(platform.create_qubit_flux_pulse(qubit, duration=200, amplitude=1)) - platform.execute_pulse_sequence(sequence, ExecutionParameters(nshots=nshots)) - assert len(sequence.qf_pulses) == 1 - assert len(sequence) == 1 + sequence[qubit.flux.name].append( + Pulse( + duration=200, + amplitude=0.28, + envelope=Rectangular(), + type=PulseType.FLUX, + ) + ) + result = platform.execute_pulse_sequence( + sequence, ExecutionParameters(nshots=nshots) + ) + assert result is not None @pytest.mark.qpu def test_platform_execute_one_long_drive_pulse(qpu_platform): # Long duration platform = qpu_platform - qubit = next(iter(platform.qubits)) - pulse = platform.create_qubit_drive_pulse(qubit, duration=8192 + 200) + qubit = next(iter(platform.qubits.values())) + pulse = Pulse( + duration=8192 + 200, amplitude=0.12, envelope=Gaussian(5), type=PulseType.DRIVE + ) sequence = PulseSequence() - sequence.append(pulse) + sequence[qubit.drive.name].append(pulse) options = ExecutionParameters(nshots=nshots) if find_instrument(platform, QbloxController) is not None: with pytest.raises(NotImplementedError): platform.execute_pulse_sequence(sequence, options) - elif find_instrument(platform, RFSoC) is not None and not isinstance( - pulse.shape, Rectangular - ): - with pytest.raises(RuntimeError): - platform.execute_pulse_sequence(sequence, options) else: platform.execute_pulse_sequence(sequence, options) @@ -239,19 +310,19 @@ def test_platform_execute_one_long_drive_pulse(qpu_platform): def test_platform_execute_one_extralong_drive_pulse(qpu_platform): # Extra Long duration platform = qpu_platform - qubit = next(iter(platform.qubits)) - pulse = platform.create_qubit_drive_pulse(qubit, duration=2 * 8192 + 200) + qubit = next(iter(platform.qubits.values())) + pulse = Pulse( + duration=2 * 8192 + 200, + amplitude=0.12, + envelope=Gaussian(5), + type=PulseType.DRIVE, + ) sequence = PulseSequence() - sequence.append(pulse) + sequence[qubit.drive.name].append(pulse) options = ExecutionParameters(nshots=nshots) if find_instrument(platform, QbloxController) is not None: with pytest.raises(NotImplementedError): platform.execute_pulse_sequence(sequence, options) - elif find_instrument(platform, RFSoC) is not None and not isinstance( - pulse.shape, Rectangular - ): - with pytest.raises(RuntimeError): - platform.execute_pulse_sequence(sequence, options) else: platform.execute_pulse_sequence(sequence, options) @@ -260,11 +331,11 @@ def test_platform_execute_one_extralong_drive_pulse(qpu_platform): def test_platform_execute_one_drive_one_readout(qpu_platform): """One drive pulse and one readout pulse.""" platform = qpu_platform - qubit = next(iter(platform.qubits)) + qubit_id, qubit = next(iter(platform.qubits.items())) sequence = PulseSequence() - sequence.append(platform.create_qubit_drive_pulse(qubit, duration=200)) - sequence.append(Delay(200, platform.qubits[qubit].readout.name)) - sequence.append(platform.create_qubit_readout_pulse(qubit)) + sequence.extend(platform.create_RX_pulse(qubit_id)) + sequence[qubit.probe.name].append(Delay(duration=200)) + sequence.extend(platform.create_MZ_pulse(qubit_id)) platform.execute_pulse_sequence(sequence, ExecutionParameters(nshots=nshots)) @@ -272,15 +343,15 @@ def test_platform_execute_one_drive_one_readout(qpu_platform): def test_platform_execute_multiple_drive_pulses_one_readout(qpu_platform): """Multiple qubit drive pulses and one readout pulse.""" platform = qpu_platform - qubit = next(iter(platform.qubits)) + qubit_id, qubit = next(iter(platform.qubits.items())) sequence = PulseSequence() - sequence.append(platform.create_qubit_drive_pulse(qubit, duration=200)) - sequence.append(Delay(4, platform.qubits[qubit].drive.name)) - sequence.append(platform.create_qubit_drive_pulse(qubit, duration=200)) - sequence.append(Delay(4, platform.qubits[qubit].drive.name)) - sequence.append(platform.create_qubit_drive_pulse(qubit, duration=400)) - sequence.append(Delay(808, platform.qubits[qubit].readout.name)) - sequence.append(platform.create_qubit_readout_pulse(qubit)) + sequence.extend(platform.create_RX_pulse(qubit_id)) + sequence[qubit.drive.name].append(Delay(duration=4)) + sequence.extend(platform.create_RX_pulse(qubit_id)) + sequence[qubit.drive.name].append(Delay(duration=4)) + sequence.extend(platform.create_RX_pulse(qubit_id)) + sequence[qubit.probe.name].append(Delay(duration=808)) + sequence.extend(platform.create_MZ_pulse(qubit_id)) platform.execute_pulse_sequence(sequence, ExecutionParameters(nshots=nshots)) @@ -291,13 +362,13 @@ def test_platform_execute_multiple_drive_pulses_one_readout_no_spacing( """Multiple qubit drive pulses and one readout pulse with no spacing between them.""" platform = qpu_platform - qubit = next(iter(platform.qubits)) + qubit_id, qubit = next(iter(platform.qubits.items())) sequence = PulseSequence() - sequence.append(platform.create_qubit_drive_pulse(qubit, duration=200)) - sequence.append(platform.create_qubit_drive_pulse(qubit, duration=200)) - sequence.append(platform.create_qubit_drive_pulse(qubit, duration=400)) - sequence.append(Delay(800, platform.qubits[qubit].readout.name)) - sequence.append(platform.create_qubit_readout_pulse(qubit)) + sequence.extend(platform.create_RX_pulse(qubit_id)) + sequence.extend(platform.create_RX_pulse(qubit_id)) + sequence.extend(platform.create_RX_pulse(qubit_id)) + sequence[qubit.probe.name].append(Delay(duration=800)) + sequence.extend(platform.create_MZ_pulse(qubit_id)) platform.execute_pulse_sequence(sequence, ExecutionParameters(nshots=nshots)) @@ -306,15 +377,16 @@ def test_platform_execute_multiple_overlaping_drive_pulses_one_readout( qpu_platform, ): """Multiple overlapping qubit drive pulses and one readout pulse.""" - # TODO: This requires defining different logical channels on the same qubit platform = qpu_platform - qubit = next(iter(platform.qubits)) + qubit_id, qubit = next(iter(platform.qubits.items())) sequence = PulseSequence() - sequence.append(platform.create_qubit_drive_pulse(qubit, duration=200)) - sequence.append(platform.create_qubit_drive_pulse(qubit, duration=200)) - sequence.append(platform.create_qubit_drive_pulse(qubit, duration=400)) - sequence.append(Delay(800, platform.qubits[qubit].readout.name)) - sequence.append(platform.create_qubit_readout_pulse(qubit)) + pulse = Pulse( + duration=200, amplitude=0.08, envelope=Gaussian(7), type=PulseType.DRIVE + ) + sequence[qubit.drive.name].append(pulse) + sequence[qubit.drive12.name].append(pulse.copy()) + sequence[qubit.probe.name].append(Delay(duration=800)) + sequence.extend(platform.create_MZ_pulse(qubit_id)) platform.execute_pulse_sequence(sequence, ExecutionParameters(nshots=nshots)) @@ -322,21 +394,19 @@ def test_platform_execute_multiple_overlaping_drive_pulses_one_readout( def test_platform_execute_multiple_readout_pulses(qpu_platform): """Multiple readout pulses.""" platform = qpu_platform - qubit = next(iter(platform.qubits)) + qubit_id, qubit = next(iter(platform.qubits.items())) sequence = PulseSequence() - qd_pulse1 = platform.create_qubit_drive_pulse(qubit, duration=200) - ro_pulse1 = platform.create_qubit_readout_pulse(qubit) - qd_pulse2 = platform.create_qubit_drive_pulse(qubit, duration=400) - ro_pulse2 = platform.create_qubit_readout_pulse(qubit) - sequence.append(qd_pulse1) - sequence.append(Delay(200, platform.qubits[qubit].readout.name)) - sequence.append(ro_pulse1) - sequence.append(Delay(200 + ro_pulse1.duration, platform.qubits[qubit].drive.name)) - sequence.append(qd_pulse2) - sequence.append( - Delay(200 + ro_pulse1.duration + 400, platform.qubits[qubit].readout.name) - ) - sequence.append(ro_pulse2) + qd_seq1 = platform.create_RX_pulse(qubit_id) + ro_seq1 = platform.create_MZ_pulse(qubit_id) + qd_seq2 = platform.create_RX_pulse(qubit_id) + ro_seq2 = platform.create_MZ_pulse(qubit_id) + sequence.extend(qd_seq1) + sequence[qubit.probe.name].append(Delay(duration=qd_seq1.duration)) + sequence.extend(ro_seq1) + sequence[qubit.drive.name].append(Delay(duration=ro_seq1.duration)) + sequence.extend(qd_seq2) + sequence[qubit.probe.name].append(Delay(duration=qd_seq2.duration)) + sequence.extend(ro_seq2) platform.execute_pulse_sequence(sequence, ExecutionParameters(nshots=nshots)) @@ -347,21 +417,19 @@ def test_platform_execute_multiple_readout_pulses(qpu_platform): ) def test_excited_state_probabilities_pulses(qpu_platform): platform = qpu_platform - qubits = [q for q, qb in platform.qubits.items() if qb.drive is not None] backend = QibolabBackend(platform) sequence = PulseSequence() - for qubit in qubits: - qd_pulse = platform.create_RX_pulse(qubit) - ro_pulse = platform.create_MZ_pulse(qubit) - sequence.append(qd_pulse) - sequence.append(Delay(qd_pulse.duration, platform.qubits[qubit].readout.name)) - sequence.append(ro_pulse) + for qubit_id, qubit in platform.qubits.items(): + sequence.extend(platform.create_RX_pulse(qubit_id)) + sequence[qubit.probe.name].append(Delay(duration=sequence.duration)) + sequence.extend(platform.create_MZ_pulse(qubit_id)) result = platform.execute_pulse_sequence(sequence, ExecutionParameters(nshots=5000)) - nqubits = len(qubits) + nqubits = len(platform.qubits) cr = CircuitResult(backend, Circuit(nqubits), result, nshots=5000) probs = [ - backend.circuit_result_probabilities(cr, qubits=[qubit]) for qubit in qubits + backend.circuit_result_probabilities(cr, qubits=[qubit]) + for qubit in platform.qubits ] warnings.warn(f"Excited state probabilities: {probs}") target_probs = np.zeros((nqubits, 2)) @@ -377,45 +445,26 @@ def test_excited_state_probabilities_pulses(qpu_platform): ) def test_ground_state_probabilities_pulses(qpu_platform, start_zero): platform = qpu_platform - qubits = [q for q, qb in platform.qubits.items() if qb.drive is not None] backend = QibolabBackend(platform) sequence = PulseSequence() - for qubit in qubits: + for qubit_id, qubit in platform.qubits.items(): if not start_zero: - qd_pulse = platform.create_RX_pulse(qubit) - sequence.append( + sequence[qubit.probe.name].append( Delay( - duration=qd_pulse.duration, + duration=platform.create_RX_pulse(qubit_id).duration, channel=platform.qubits[qubit].readout.name, ) ) - ro_pulse = platform.create_MZ_pulse(qubit) - sequence.append(ro_pulse) + sequence.extend(platform.create_MZ_pulse(qubit_id)) result = platform.execute_pulse_sequence(sequence, ExecutionParameters(nshots=5000)) - nqubits = len(qubits) + nqubits = len(platform.qubits) cr = CircuitResult(backend, Circuit(nqubits), result, nshots=5000) probs = [ - backend.circuit_result_probabilities(cr, qubits=[qubit]) for qubit in qubits + backend.circuit_result_probabilities(cr, qubits=[qubit]) + for qubit in platform.qubits ] warnings.warn(f"Ground state probabilities: {probs}") target_probs = np.zeros((nqubits, 2)) target_probs[:, 0] = 1 np.testing.assert_allclose(probs, target_probs, atol=0.05) - - -def test_create_RX_drag_pulses(): - platform = create_dummy() - qubits = [q for q, qb in platform.qubits.items() if qb.drive is not None] - beta = 0.1234 - for qubit in qubits: - drag_pi = platform.create_RX_drag_pulse(qubit, beta=beta) - assert drag_pi.envelope == Drag(rel_sigma=drag_pi.envelope.rel_sigma, beta=beta) - drag_pi_half = platform.create_RX90_drag_pulse(qubit, beta=beta) - assert drag_pi_half.envelope == Drag( - rel_sigma=drag_pi_half.envelope.rel_sigma, beta=beta - ) - np.testing.assert_almost_equal(drag_pi.amplitude, 2 * drag_pi_half.amplitude) - - drag_pi.envelopes(sampling_rate=1) - drag_pi_half.envelopes(sampling_rate=1) diff --git a/tests/test_port.py b/tests/test_port.py new file mode 100644 index 000000000..7341ec799 --- /dev/null +++ b/tests/test_port.py @@ -0,0 +1,32 @@ +from unittest.mock import Mock + +import pytest + +from qibolab.instruments.qblox.port import QbloxOutputPort + + +def test_set_attenuation(caplog): + module = Mock() + # module.device = None + port = QbloxOutputPort(module, 17) + + port.attenuation = 40 + assert port.attenuation == 40 + + port.attenuation = 40.1 + assert port.attenuation == 40 + + port.attenuation = 65 + assert port.attenuation == 60 + if caplog.messages: + assert "attenuation needs to be between 0 and 60 dB" in caplog.messages[0] + caplog.clear() + + port.attenuation = -10 + assert port.attenuation == 0 + if caplog.messages: + assert "attenuation needs to be between 0 and 60 dB" in caplog.messages[0] + caplog.clear() + + with pytest.raises(ValueError, match="Invalid"): + port.attenuation = "something" diff --git a/tests/test_result_shapes.py b/tests/test_result_shapes.py index 3931d0373..1b6e14b13 100644 --- a/tests/test_result_shapes.py +++ b/tests/test_result_shapes.py @@ -18,13 +18,14 @@ def execute(platform: Platform, acquisition_type, averaging_mode, sweep=False): - qubit = next(iter(platform.qubits)) + qubit = next(iter(platform.qubits.values())) - qd_pulse = platform.create_RX_pulse(qubit, start=0) - ro_pulse = platform.create_MZ_pulse(qubit, start=qd_pulse.finish) + qd_seq = qubit.native_gates.RX.create_sequence() + probe_seq = qubit.native_gates.MZ.create_sequence() + probe_pulse = next(iter(probe_seq.values()))[0] sequence = PulseSequence() - sequence.append(qd_pulse) - sequence.append(ro_pulse) + sequence.extend(qd_seq) + sequence.extend(probe_seq) options = ExecutionParameters( nshots=NSHOTS, acquisition_type=acquisition_type, averaging_mode=averaging_mode @@ -32,13 +33,12 @@ def execute(platform: Platform, acquisition_type, averaging_mode, sweep=False): if sweep: 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]) + sweeper1 = Sweeper(Parameter.bias, amp_values, channels=[qubit.flux.name]) + sweeper2 = Sweeper(Parameter.amplitude, freq_values, pulses=[probe_pulse]) results = platform.execute([sequence], options, [[sweeper1], [sweeper2]]) else: results = platform.execute([sequence], options) - return results[qubit][0] + return results[probe_pulse.id][0] @pytest.mark.qpu diff --git a/tests/test_sweeper.py b/tests/test_sweeper.py index b4b660f99..848f11411 100644 --- a/tests/test_sweeper.py +++ b/tests/test_sweeper.py @@ -2,8 +2,7 @@ import pytest from qibolab.pulses import Pulse, Rectangular -from qibolab.qubits import Qubit -from qibolab.sweeper import Parameter, QubitParameter, Sweeper +from qibolab.sweeper import ChannelParameter, Parameter, Sweeper @pytest.mark.parametrize("parameter", Parameter) @@ -11,45 +10,53 @@ def test_sweeper_pulses(parameter): pulse = Pulse( duration=40, amplitude=0.1, - frequency=1e9, envelope=Rectangular(), - channel="channel", ) if parameter is Parameter.amplitude: parameter_range = np.random.rand(10) else: parameter_range = np.random.randint(10, size=10) - if parameter in QubitParameter: - with pytest.raises(ValueError): - sweeper = Sweeper(parameter, parameter_range, [pulse]) + if parameter in ChannelParameter: + with pytest.raises( + ValueError, match="Cannot create a sweeper .* without specifying channels" + ): + _ = Sweeper(parameter, parameter_range, pulses=[pulse]) else: - sweeper = Sweeper(parameter, parameter_range, [pulse]) + sweeper = Sweeper(parameter, parameter_range, pulses=[pulse]) assert sweeper.parameter is parameter @pytest.mark.parametrize("parameter", Parameter) -def test_sweeper_qubits(parameter): - qubit = Qubit(0) +def test_sweeper_channels(parameter): parameter_range = np.random.randint(10, size=10) - if parameter in QubitParameter: - sweeper = Sweeper(parameter, parameter_range, qubits=[qubit]) + if parameter in ChannelParameter: + sweeper = Sweeper(parameter, parameter_range, channels=["some channel"]) assert sweeper.parameter is parameter else: - with pytest.raises(ValueError): - sweeper = Sweeper(parameter, parameter_range, qubits=[qubit]) + with pytest.raises( + ValueError, match="Cannot create a sweeper .* without specifying pulses" + ): + _ = Sweeper(parameter, parameter_range, channels=["canal"]) def test_sweeper_errors(): pulse = Pulse( duration=40, amplitude=0.1, - frequency=1e9, envelope=Rectangular(), - channel="channel", ) - qubit = Qubit(0) parameter_range = np.random.randint(10, size=10) - with pytest.raises(ValueError): + with pytest.raises( + ValueError, + match="Cannot create a sweeper without specifying pulses or channels", + ): Sweeper(Parameter.frequency, parameter_range) - with pytest.raises(ValueError): - Sweeper(Parameter.frequency, parameter_range, [pulse], [qubit]) + with pytest.raises( + ValueError, match="Cannot create a sweeper by using both pulses and channels" + ): + Sweeper( + Parameter.frequency, + parameter_range, + pulses=[pulse], + channels=["some channel"], + ) diff --git a/tests/test_unrolling.py b/tests/test_unrolling.py index 65411acf0..0624db1e6 100644 --- a/tests/test_unrolling.py +++ b/tests/test_unrolling.py @@ -7,57 +7,57 @@ def test_bounds_update(): - p1 = Pulse( - duration=40, - amplitude=0.9, - frequency=int(100e6), - envelope=Drag(rel_sigma=0.2, beta=1), - channel="3", - type=PulseType.DRIVE, + ps = PulseSequence() + ps["ch3"].append( + Pulse( + duration=40, + amplitude=0.9, + envelope=Drag(rel_sigma=0.2, beta=1), + type=PulseType.DRIVE, + ) ) - p2 = Pulse( - duration=40, - amplitude=0.9, - frequency=int(100e6), - envelope=Drag(rel_sigma=0.2, beta=1), - channel="2", - type=PulseType.DRIVE, + ps["ch2"].append( + Pulse( + duration=40, + amplitude=0.9, + envelope=Drag(rel_sigma=0.2, beta=1), + type=PulseType.DRIVE, + ) ) - p3 = Pulse( - duration=40, - amplitude=0.9, - frequency=int(100e6), - envelope=Drag(rel_sigma=0.2, beta=1), - channel="1", - type=PulseType.DRIVE, + ps["ch1"].append( + Pulse( + duration=40, + amplitude=0.9, + envelope=Drag(rel_sigma=0.2, beta=1), + type=PulseType.DRIVE, + ) ) - p4 = Pulse( - duration=1000, - amplitude=0.9, - frequency=int(20e6), - envelope=Rectangular(), - channel="3", - type=PulseType.READOUT, + ps["ch3"].append( + Pulse( + duration=1000, + amplitude=0.9, + envelope=Rectangular(), + type=PulseType.READOUT, + ) ) - p5 = Pulse( - duration=1000, - amplitude=0.9, - frequency=int(20e6), - envelope=Rectangular(), - channel="2", - type=PulseType.READOUT, + ps["ch2"].append( + Pulse( + duration=1000, + amplitude=0.9, + envelope=Rectangular(), + type=PulseType.READOUT, + ) ) - p6 = Pulse( - duration=1000, - amplitude=0.9, - frequency=int(20e6), - envelope=Rectangular(), - channel="1", - type=PulseType.READOUT, + ps["ch1"].append( + Pulse( + duration=1000, + amplitude=0.9, + envelope=Rectangular(), + type=PulseType.READOUT, + ) ) - ps = PulseSequence([p1, p2, p3, p4, p5, p6]) bounds = Bounds.update(ps) assert bounds.waveforms >= 40 @@ -93,58 +93,57 @@ def test_bounds_comparison(): ], ) def test_batch(bounds): - p1 = Pulse( - duration=40, - amplitude=0.9, - frequency=int(100e6), - envelope=Drag(rel_sigma=0.2, beta=1), - channel="3", - type=PulseType.DRIVE, + ps = PulseSequence() + ps["ch3"].append( + Pulse( + duration=40, + amplitude=0.9, + envelope=Drag(rel_sigma=0.2, beta=1), + type=PulseType.DRIVE, + ) ) - p2 = Pulse( - duration=40, - amplitude=0.9, - frequency=int(100e6), - envelope=Drag(rel_sigma=0.2, beta=1), - channel="2", - type=PulseType.DRIVE, + ps["ch2"].append( + Pulse( + duration=40, + amplitude=0.9, + envelope=Drag(rel_sigma=0.2, beta=1), + type=PulseType.DRIVE, + ) ) - p3 = Pulse( - duration=40, - amplitude=0.9, - frequency=int(100e6), - envelope=Drag(rel_sigma=0.2, beta=1), - channel="1", - type=PulseType.DRIVE, + ps["ch1"].append( + Pulse( + duration=40, + amplitude=0.9, + envelope=Drag(rel_sigma=0.2, beta=1), + type=PulseType.DRIVE, + ) ) - p4 = Pulse( - duration=1000, - amplitude=0.9, - frequency=int(20e6), - envelope=Rectangular(), - channel="3", - type=PulseType.READOUT, + ps["ch3"].append( + Pulse( + duration=1000, + amplitude=0.9, + envelope=Rectangular(), + type=PulseType.READOUT, + ) ) - p5 = Pulse( - duration=1000, - amplitude=0.9, - frequency=int(20e6), - envelope=Rectangular(), - channel="2", - type=PulseType.READOUT, + ps["ch2"].append( + Pulse( + duration=1000, + amplitude=0.9, + envelope=Rectangular(), + type=PulseType.READOUT, + ) ) - p6 = Pulse( - duration=1000, - amplitude=0.9, - frequency=int(20e6), - envelope=Rectangular(), - channel="1", - type=PulseType.READOUT, + ps["ch1"].append( + Pulse( + duration=1000, + amplitude=0.9, + envelope=Rectangular(), + type=PulseType.READOUT, + ) ) - ps = PulseSequence([p1, p2, p3, p4, p5, p6]) - sequences = 10 * [ps] batches = list(batch(sequences, bounds))