diff --git a/doc/source/conf.py b/doc/source/conf.py index a874daf96a..45478d986b 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -18,6 +18,11 @@ import qibolab +# TODO: the following is a workaround for Sphinx doctest, cf. +# - https://github.com/qiboteam/qibolab/commit/e04a6ab +# - https://github.com/pydantic/pydantic/discussions/7763 +import qibolab.instruments.zhinst + # -- Project information ----------------------------------------------------- project = "qibolab" diff --git a/doc/source/getting-started/experiment.rst b/doc/source/getting-started/experiment.rst index 3908d23f3d..40f6d2a392 100644 --- a/doc/source/getting-started/experiment.rst +++ b/doc/source/getting-started/experiment.rst @@ -37,14 +37,10 @@ In this example, the qubit is controlled by a Zurich Instruments' SHFQC instrume AcquisitionConfig, OscillatorConfig, ) + from qibolab.instruments.zhinst import ZiChannel, Zurich + from qibolab.parameters import Parameters from qibolab.platform import Platform - from qibolab.serialize import ( - load_instrument_settings, - load_qubits, - load_runcard, - load_settings, - ) NAME = "my_platform" # name of the platform ADDRESS = "localhost" # ip address of the ZI data server @@ -61,8 +57,9 @@ In this example, the qubit is controlled by a Zurich Instruments' SHFQC instrume device_setup.add_instruments(SHFQC("device_shfqc", address="DEV12146")) # Load and parse the runcard (i.e. parameters.json) - runcard = load_runcard(FOLDER) - qubits, _, pairs = load_qubits(runcard) + runcard = Parameters.load(FOLDER) + qubits = runcard.native_gates.single_qubit + pairs = runcard.native_gates.pairs qubit = qubits[0] # define component names, and load their configurations @@ -74,14 +71,6 @@ In this example, the qubit is controlled by a Zurich Instruments' SHFQC instrume 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"), @@ -90,15 +79,10 @@ In this example, the qubit is controlled by a Zurich Instruments' SHFQC instrume 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, - configs, - instruments, - settings, + name=NAME, + runcard=runcard, + instruments={controller.name: controller}, resonator_type="3D", ) @@ -232,8 +216,9 @@ We leave to the dedicated tutorial a full explanation of the experiment, but her platform = create_platform("dummy") qubit = platform.qubits[0] + natives = platform.natives.single_qubit[0] # define the pulse sequence - sequence = qubit.native_gates.MZ.create_sequence() + sequence = natives.MZ.create_sequence() # define a sweeper for a frequency scan sweeper = Sweeper( diff --git a/doc/source/main-documentation/qibolab.rst b/doc/source/main-documentation/qibolab.rst index 01921032c8..ec4b3758b7 100644 --- a/doc/source/main-documentation/qibolab.rst +++ b/doc/source/main-documentation/qibolab.rst @@ -56,7 +56,7 @@ We can easily access the names of channels and other components, and based on th :hide: Drive channel name: qubit_0/drive - Drive frequency: 4000000000 + Drive frequency: 4000000000.0 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): @@ -68,10 +68,11 @@ Now we can create a simple sequence (again, without explicitly giving any qubit ps = PulseSequence() qubit = platform.qubits[0] - ps.concatenate(qubit.native_gates.RX.create_sequence()) - ps.concatenate(qubit.native_gates.RX.create_sequence(phi=np.pi / 2)) + natives = platform.natives.single_qubit[0] + ps.concatenate(natives.RX.create_sequence()) + ps.concatenate(natives.RX.create_sequence(phi=np.pi / 2)) ps.append((qubit.probe.name, Delay(duration=200))) - ps.concatenate(qubit.native_gates.MZ.create_sequence()) + ps.concatenate(natives.MZ.create_sequence()) Now we can execute the sequence on hardware: @@ -251,11 +252,8 @@ To illustrate, here are some examples of single pulses using the Qibolab API: pulse = Pulse( duration=40, # Pulse duration in ns amplitude=0.5, # Amplitude relative to instrument range - frequency=1e8, # Frequency in Hz relative_phase=0, # Phase in radians envelope=Rectangular(), - channel="channel", - qubit=0, ) In this way, we defined a rectangular drive pulse using the generic Pulse object. @@ -268,11 +266,8 @@ Alternatively, you can achieve the same result using the dedicated :class:`qibol pulse = 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, ) Both the Pulses objects and the PulseShape object have useful plot functions and several different various helper methods. @@ -341,15 +336,16 @@ Typical experiments may include both pre-defined pulses and new ones: from qibolab.pulses import Rectangular + natives = platform.natives.single_qubit[0] sequence = PulseSequence() - sequence.concatenate(platform.qubits[0].native_gates.RX.create_sequence()) + sequence.concatenate(natives.RX.create_sequence()) sequence.append( ( "some_channel", Pulse(duration=10, amplitude=0.5, relative_phase=0, envelope=Rectangular()), ) ) - sequence.concatenate(platform.qubits[0].native_gates.MZ.create_sequence()) + sequence.concatenate(natives.MZ.create_sequence()) results = platform.execute([sequence], options=options) @@ -420,15 +416,17 @@ A tipical resonator spectroscopy experiment could be defined with: from qibolab.sweeper import Parameter, Sweeper, SweeperType + natives = platform.natives.single_qubit + sequence = PulseSequence() sequence.concatenate( - platform.qubits[0].native_gates.MZ.create_sequence() + natives[0].MZ.create_sequence() ) # readout pulse for qubit 0 at 4 GHz sequence.concatenate( - platform.qubits[1].native_gates.MZ.create_sequence() + natives[1].MZ.create_sequence() ) # readout pulse for qubit 1 at 5 GHz sequence.concatenate( - platform.qubits[2].native_gates.MZ.create_sequence() + natives[2].MZ.create_sequence() ) # readout pulse for qubit 2 at 6 GHz sweeper = Sweeper( @@ -465,10 +463,11 @@ For example: from qibolab.pulses import PulseSequence, Delay qubit = platform.qubits[0] + natives = platform.natives.single_qubit[0] sequence = PulseSequence() - sequence.concatenate(qubit.native_gates.RX.create_sequence()) + sequence.concatenate(natives.RX.create_sequence()) sequence.append((qubit.probe.name, Delay(duration=sequence.duration))) - sequence.concatenate(qubit.native_gates.MZ.create_sequence()) + sequence.concatenate(natives.MZ.create_sequence()) sweeper_freq = Sweeper( parameter=Parameter.frequency, @@ -561,11 +560,12 @@ Let's now delve into a typical use case for result objects within the qibolab fr .. testcode:: python qubit = platform.qubits[0] + natives = platform.natives.single_qubit[0] sequence = PulseSequence() - sequence.concatenate(qubit.native_gates.RX.create_sequence()) + sequence.concatenate(natives.RX.create_sequence()) sequence.append((qubit.probe.name, Delay(duration=sequence.duration))) - sequence.concatenate(qubit.native_gates.MZ.create_sequence()) + sequence.concatenate(natives.MZ.create_sequence()) options = ExecutionParameters( nshots=1000, diff --git a/doc/source/tutorials/calibration.rst b/doc/source/tutorials/calibration.rst index 001284b626..71fd99c6fe 100644 --- a/doc/source/tutorials/calibration.rst +++ b/doc/source/tutorials/calibration.rst @@ -43,7 +43,8 @@ around the pre-defined frequency. platform = create_platform("dummy") qubit = platform.qubits[0] - sequence = qubit.native_gates.MZ.create_sequence() + natives = platform.natives.single_qubit[0] + sequence = natives.MZ.create_sequence() # allocate frequency sweeper sweeper = Sweeper( @@ -118,12 +119,13 @@ complex pulse sequence. Therefore with start with that: AveragingMode, AcquisitionType, ) - from qibolab.serialize_ import replace + from qibolab.serialize import replace # allocate platform platform = create_platform("dummy") qubit = platform.qubits[0] + natives = platform.natives.single_qubit[0] # create pulse sequence and add pulses sequence = PulseSequence( @@ -135,7 +137,7 @@ complex pulse sequence. Therefore with start with that: (qubit.probe.name, Delay(duration=sequence.duration)), ] ) - sequence.concatenate(qubit.native_gates.MZ.create_sequence()) + sequence.concatenate(natives.MZ.create_sequence()) # allocate frequency sweeper sweeper = Sweeper( @@ -226,15 +228,16 @@ and its impact on qubit states in the IQ plane. platform = create_platform("dummy") qubit = platform.qubits[0] + natives = platform.natives.single_qubit[0] # create pulse sequence 1 and add pulses one_sequence = PulseSequence() - one_sequence.concatenate(qubit.native_gates.RX.create_sequence()) + one_sequence.concatenate(natives.RX.create_sequence()) one_sequence.append((qubit.probe.name, Delay(duration=one_sequence.duration))) - one_sequence.concatenate(qubit.native_gates.MZ.create_sequence()) + one_sequence.concatenate(natives.MZ.create_sequence()) # create pulse sequence 2 and add pulses - zero_sequence = qubit.native_gates.MZ.create_sequence() + zero_sequence = natives.MZ.create_sequence() options = ExecutionParameters( nshots=1000, diff --git a/doc/source/tutorials/compiler.rst b/doc/source/tutorials/compiler.rst index dd335fe4ce..8283c2eb4f 100644 --- a/doc/source/tutorials/compiler.rst +++ b/doc/source/tutorials/compiler.rst @@ -82,14 +82,14 @@ 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 qubit.native_gates.RX.create_sequence() + return qubit.RX.create_sequence() # the empty dictionary is needed because the X gate does not require any virtual Z-phases backend = QibolabBackend(platform="dummy") # register the new X rule in the compiler - backend.compiler[gates.X] = x_rule + backend.compiler.rules[gates.X] = x_rule # execute the circuit result = backend.execute_circuit(circuit, nshots=1000) diff --git a/doc/source/tutorials/lab.rst b/doc/source/tutorials/lab.rst index 764a3775cb..781e688d86 100644 --- a/doc/source/tutorials/lab.rst +++ b/doc/source/tutorials/lab.rst @@ -26,6 +26,7 @@ using different Qibolab primitives. from qibolab.qubits import Qubit from qibolab.pulses import Gaussian, Pulse, Rectangular from qibolab.native import RxyFactory, FixedSequenceFactory, SingleQubitNatives + from qibolab.parameters import NativeGates, Parameters from qibolab.instruments.dummy import DummyInstrument @@ -34,11 +35,11 @@ using different Qibolab primitives. instrument = DummyInstrument("my_instrument", "0.0.0.0:0") # create the qubit object - qubit = Qubit(0) + qubit = Qubit(name=0) # 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.acquisition = AcquireChannel(name="acquire", twpa_pump=None, probe="probe") qubit.drive = Iqchannel(name="drive", mixer=None, lo=None) # define configuration for channels @@ -67,20 +68,22 @@ using different Qibolab primitives. ) # assign native gates to the qubit - qubit.native_gates = SingleQubitNatives( + native_gates = SingleQubitNatives( RX=RxyFactory(drive_seq), MZ=FixedSequenceFactory(probe_seq), ) + # create a parameters instance + parameters = Parameters( + native_gates=NativeGates(single_qubit=native_gates), configs=configs + ) + # create dictionaries of the different objects qubits = {qubit.name: qubit} - pairs = {} # empty as for single qubit we have no qubit pairs instruments = {instrument.name: instrument} # allocate and return Platform object - return Platform( - "my_platform", qubits, pairs, configs, instruments, resonator_type="3D" - ) + return Platform("my_platform", parameters, instruments, qubits) This code creates a platform with a single qubit that is controlled by the @@ -95,14 +98,14 @@ control instrument and we assigned two native gates to the qubit. These can be passed when defining the :class:`qibolab.qubits.Qubit` objects. When the QPU contains more than one qubit, some of the qubits are connected so -that two-qubit gates can be applied. For such connected pairs of qubits one -needs to additionally define :class:`qibolab.qubits.QubitPair` objects, which -hold the parameters of the two-qubit gates. +that two-qubit gates can be applied. These are called in a single dictionary, within +the native gates, but separately from the single-qubit ones. .. testcode:: python from qibolab.components import IqChannel, AcquireChannel, DcChannel, IqConfig - from qibolab.qubits import Qubit, QubitPair + from qibolab.qubits import Qubit + from qibolab.parameters import Parameters, TwoQubitContainer from qibolab.pulses import Gaussian, Pulse, PulseSequence, Rectangular from qibolab.native import ( RxyFactory, @@ -112,20 +115,21 @@ hold the parameters of the two-qubit gates. ) # create the qubit objects - qubit0 = Qubit(0) - qubit1 = Qubit(1) + qubit0 = Qubit(name=0) + qubit1 = Qubit(name=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.acquisition = 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.acquisition = 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( + single_qubit = {} + single_qubit[qubit0.name] = SingleQubitNatives( RX=RxyFactory( PulseSequence( [ @@ -151,7 +155,7 @@ hold the parameters of the two-qubit gates. ) ), ) - qubit1.native_gates = SingleQubitNatives( + single_qubit[qubit1.name] = SingleQubitNatives( RX=RxyFactory( PulseSequence( [ @@ -177,30 +181,32 @@ hold the parameters of the two-qubit gates. ) # define the pair of qubits - pair = QubitPair(qubit0.name, qubit1.name) - pair.native_gates = TwoQubitNatives( - CZ=FixedSequenceFactory( - PulseSequence( - [ - ( - qubit0.flux.name, - Pulse(duration=30, amplitude=0.005, envelope=Rectangular()), - ), - ] + two_qubit = TwoQubitContainer( + { + f"{qubit0.name}-{qubit1.name}": TwoQubitNatives( + CZ=FixedSequenceFactory( + PulseSequence( + [ + ( + qubit0.flux.name, + Pulse(duration=30, amplitude=0.005, envelope=Rectangular()), + ), + ] + ) + ) ) - ) + } ) Some architectures may also have coupler qubits that mediate the interactions. -Then we add them to their corresponding :class:`qibolab.qubits.QubitPair` objects according -to the chip topology. We neglected characterization parameters associated to the -coupler but qibolab will take them into account when calling :class:`qibolab.native.TwoQubitNatives`. +We neglected characterization parameters associated to the coupler but qibolab +will take them into account when calling :class:`qibolab.native.TwoQubitNatives`. .. testcode:: python from qibolab.components import DcChannel - from qibolab.qubits import Qubit, QubitPair + from qibolab.qubits import Qubit from qibolab.pulses import Pulse, PulseSequence from qibolab.native import ( FixedSequenceFactory, @@ -209,9 +215,9 @@ coupler but qibolab will take them into account when calling :class:`qibolab.nat ) # create the qubit and coupler objects - qubit0 = Qubit(0) - qubit1 = Qubit(1) - coupler_01 = Qubit(100) + qubit0 = Qubit(name=0) + qubit1 = Qubit(name=1) + coupler_01 = Qubit(name=100) # assign channel(s) to the coupler coupler_01.flux = DcChannel(name="flux_coupler_01") @@ -220,28 +226,25 @@ coupler but qibolab will take them into account when calling :class:`qibolab.nat # Look above example # define the pair of qubits - pair = QubitPair(qubit0.name, qubit1.name) - pair.native_gates = TwoQubitNatives( - CZ=FixedSequenceFactory( - PulseSequence( - [ - ( - coupler_01.flux.name, - Pulse( - duration=30, - amplitude=0.005, - frequency=1e9, - envelope=Rectangular(), - qubit=qubit1.name, - ), + two_qubit = TwoQubitContainer( + { + f"{qubit0.name}-{qubit1.name}": TwoQubitNatives( + CZ=FixedSequenceFactory( + PulseSequence( + [ + ( + coupler_01.flux.name, + Pulse(duration=30, amplitude=0.005, envelope=Rectangular()), + ) + ], ) - ], - ) - ) + ) + ), + } ) -The platform automatically creates the connectivity graph of the given chip -using the dictionary of :class:`qibolab.qubits.QubitPair` objects. +The platform automatically creates the connectivity graph of the given chip, +using the keys of :class:`qibolab.parameters.TwoQubitContainer` map. Registering platforms ^^^^^^^^^^^^^^^^^^^^^ @@ -277,21 +280,18 @@ since ``create()`` is part of a Python module, is is possible to load parameters from an external file or database. Qibolab provides some utility functions, accessible through -:py:mod:`qibolab.serialize`, for loading calibration parameters stored in a JSON +:py:mod:`qibolab.parameters`, for loading calibration parameters stored in a JSON file with a specific format. We call such file a runcard. Here is a runcard for a two-qubit system: .. code-block:: json { - "nqubits": 2, - "qubits": [0, 1], "settings": { "nshots": 1024, "sampling_rate": 1000000000, "relaxation_time": 50000 }, - "topology": [[0, 1]], "components": { "drive_0": { "frequency": 4855663000 @@ -476,7 +476,7 @@ however the pulses under ``native_gates`` should comply with the Providing the above runcard is not sufficient to instantiate a :class:`qibolab.platform.Platform`. This should still be done using a ``create()`` method, however this is significantly simplified by -``qibolab.serialize``. The ``create()`` method should be put in a +``qibolab.parameters``. The ``create()`` method should be put in a file named ``platform.py`` inside the ``my_platform`` directory. Here is the ``create()`` method that loads the parameters of the above runcard: @@ -495,11 +495,6 @@ the above runcard: DcConfig, IqConfig, ) - from qibolab.serialize import ( - load_runcard, - load_qubits, - load_settings, - ) from qibolab.instruments.dummy import DummyInstrument FOLDER = Path.cwd() @@ -507,49 +502,25 @@ the above runcard: def create(): - # Create a controller instrument + # create a controller instrument instrument = DummyInstrument("my_instrument", "0.0.0.0:0") - # create ``Qubit`` and ``QubitPair`` objects by loading the runcard - runcard = load_runcard(folder) - qubits, _, pairs = load_qubits(runcard) - # define channels and load component configs - configs = {} - component_params = runcard["components"] + qubits = {} for q in range(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 + qubits[q] = Qubit( + name=q, + drive=IqChannel(f"qubit_{q}/drive", mixer=None, lo=None), + flux=DcChannel(f"qubit_{q}/flux"), + probe=IqChannel(probe_name, mixer=None, lo=None, acquistion=acquire_name), + 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, - configs, - instruments, - settings, - resonator_type="2D", - ) + return Platform.load(FOLDER, instruments, qubits) With the following additions for coupler architectures: @@ -557,56 +528,30 @@ With the following additions for coupler architectures: # my_platform / platform.py + FOLDER = Path.cwd() + def create(): # Create a controller instrument instrument = DummyInstrument("my_instrument", "0.0.0.0:0") - # create ``Qubit`` and ``QubitPair`` objects by loading the runcard - runcard = load_runcard(folder) - qubits, couplers, pairs = load_qubits(runcard) - + qubits = {} # define channels and load component configs - configs = {} - component_params = runcard["components"] for q in range(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 + qubits[q] = Qubit( + name=q, + drive=IqChannel(f"qubit_{q}/drive", mixer=None, lo=None), + flux=DcChannel(f"qubit_{q}/flux"), + probe=IqChannel(probe_name, mixer=None, lo=None, acquistion=acquire_name), + acquisition=AcquireChannel(acquire_name, twpa_pump=None, probe=probe_name), ) - 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) + couplers = {0: Qubit(name=0, flux=DcChannel("coupler_0/flux"))} # create dictionary of instruments instruments = {instrument.name: instrument} - # load ``settings`` from the runcard - settings = load_settings(runcard) - return Platform( - "my_platform", - qubits, - pairs, - configs, - instruments, - settings, - resonator_type="2D", - couplers=couplers, - ) + return Platform.load(FOLDER, instruments, qubits, couplers=couplers) Note that this assumes that the runcard is saved as ``/parameters.json`` where ```` is the directory containing ``platform.py``. @@ -625,13 +570,12 @@ The runcard can contain an ``instruments`` section that provides these parameter .. code-block:: json { - "nqubits": 2, "settings": { "nshots": 1024, "sampling_rate": 1000000000, "relaxation_time": 50000 }, - "instruments": { + "configs": { "twpa_pump": { "frequency": 4600000000, "power": 5 @@ -644,7 +588,7 @@ The runcard can contain an ``instruments`` section that provides these parameter } -These settings are loaded when creating the platform using :meth:`qibolab.serialize.load_instrument_settings`. +These settings are loaded when creating the platform using :meth:`qibolab.parameters.load_instrument_settings`. Note that the key used in the runcard should be the same with the name used when instantiating the instrument, in this case ``"twpa_pump"``. @@ -662,11 +606,7 @@ in this case ``"twpa_pump"``. DcConfig, IqConfig, ) - from qibolab.serialize import ( - load_runcard, - load_qubits, - load_settings, - ) + from qibolab.parameters import Parameters from qibolab.instruments.dummy import DummyInstrument FOLDER = Path.cwd() @@ -677,45 +617,18 @@ in this case ``"twpa_pump"``. # Create a controller instrument instrument = DummyInstrument("my_instrument", "0.0.0.0:0") - # create ``Qubit`` and ``QubitPair`` objects by loading the runcard - runcard = load_runcard(folder) - qubits, _, pairs = load_qubits(runcard) - # define channels and load component configs - configs = {} - component_params = runcard["components"] + qubits = {} for q in range(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 + qubits[q] = Qubit( + name=q, + drive=IqChannel(f"qubit_{q}/drive", mixer=None, lo=None), + flux=DcChannel(f"qubit_{q}/flux"), + probe=IqChannel(probe_name, mixer=None, lo=None, acquistion=acquire_name), + acquisition=AcquireChannel(acquire_name, twpa_pump=None, probe=probe_name), ) # create dictionary of instruments 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, - configs, - instruments, - settings, - resonator_type="2D", - ) + return Platform.load(FOLDER, instruments, qubits) diff --git a/doc/source/tutorials/pulses.rst b/doc/source/tutorials/pulses.rst index 303bd47046..a5945d1190 100644 --- a/doc/source/tutorials/pulses.rst +++ b/doc/source/tutorials/pulses.rst @@ -22,7 +22,7 @@ pulses (:class:`qibolab.pulses.Pulse`) through the envelope=Gaussian(rel_sigma=0.2), ), ), - ("channel_1", Delay(duration=100, channel="1")), + ("channel_1", Delay(duration=100)), ( "channel_1", Pulse( diff --git a/poetry.lock b/poetry.lock index 31ed3766a9..df6c1e5e05 100644 --- a/poetry.lock +++ b/poetry.lock @@ -5894,9 +5894,10 @@ los = ["pyvisa-py", "qcodes", "qcodes_contrib_drivers"] qblox = ["pyvisa-py", "qblox-instruments", "qcodes", "qcodes_contrib_drivers"] qm = ["qm-qua", "qualang-tools"] rfsoc = ["qibosoq"] +twpa = ["pyvisa-py", "qcodes", "qcodes_contrib_drivers"] zh = ["laboneq"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.12" -content-hash = "30a7e1b0b8caa372b4076bce71583a537c7ad92555c5249a883c96b799f9cc5f" +content-hash = "a5bf84cc1a4fa49d87dd8a53e5bc48949e4f1bef7c09fd73aca1f26aed3906cd" diff --git a/pyproject.toml b/pyproject.toml index c351e8bcfb..f5785ef5af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,6 +87,7 @@ qm = ["qm-qua", "qualang-tools"] zh = ["laboneq"] rfsoc = ["qibosoq"] los = ["qcodes", "qcodes_contrib_drivers", "pyvisa-py"] +twpa = ["qcodes", "qcodes_contrib_drivers", "pyvisa-py"] emulator = ["qutip"] @@ -101,6 +102,7 @@ test-docs = "make -C doc doctest" [tool.pylint.master] output-format = "colorized" disable = ["E1123", "E1120", "C0301"] +generated-members = ["qibolab.native.RxyFactory", "pydantic.fields.FieldInfo"] [tool.pytest.ini_options] testpaths = ['tests/'] diff --git a/src/qibolab/compilers/compiler.py b/src/qibolab/compilers/compiler.py index 06bbba2636..833057ecfb 100644 --- a/src/qibolab/compilers/compiler.py +++ b/src/qibolab/compilers/compiler.py @@ -1,8 +1,8 @@ from collections import defaultdict +from collections.abc import Callable from dataclasses import dataclass, field from qibo import Circuit, gates -from qibo.config import raise_error from qibolab.compilers.default import ( align_rule, @@ -19,6 +19,9 @@ from qibolab.pulses import Delay, PulseSequence from qibolab.qubits import QubitId +Rule = Callable[..., PulseSequence] +"""Compiler rule.""" + @dataclass class Compiler: @@ -41,7 +44,7 @@ class Compiler: See :class:`qibolab.compilers.default` for an example of a compiler implementation. """ - rules: dict = field(default_factory=dict) + rules: dict[type[gates.Gate], Rule] = field(default_factory=dict) """Map from gates to compilation rules.""" @classmethod @@ -60,31 +63,7 @@ def default(cls): } ) - def __setitem__(self, key, rule): - """Sets a new rule to the compiler. - - If a rule already exists for the gate, it will be overwritten. - """ - self.rules[key] = rule - - def __getitem__(self, item): - """Get an existing rule for a given gate.""" - try: - return self.rules[item] - except KeyError: - raise_error(KeyError, f"Compiler rule not available for {item}.") - - def __delitem__(self, item): - """Remove rule for the given gate.""" - try: - del self.rules[item] - except KeyError: - raise_error( - KeyError, - f"Cannot remove {item} from compiler because it does not exist.", - ) - - def register(self, gate_cls): + def register(self, gate_cls: type[gates.Gate]) -> Callable[[Rule], Rule]: """Decorator for registering a function as a rule in the compiler. Using this decorator is optional. Alternatively the user can set the rules directly @@ -94,13 +73,13 @@ def register(self, gate_cls): gate_cls: Qibo gate object that the rule will be assigned to. """ - def inner(func): - self[gate_cls] = func + def inner(func: Rule) -> Rule: + self.rules[gate_cls] = func return func return inner - def get_sequence(self, gate, platform): + def get_sequence(self, gate: gates.Gate, platform: Platform) -> PulseSequence: """Get pulse sequence implementing the given gate using the registered rules. @@ -109,26 +88,39 @@ def get_sequence(self, gate, platform): platform (:class:`qibolab.platform.Platform`): Qibolab platform to read the native gates from. """ # get local sequence for the current gate - rule = self[type(gate)] - if isinstance(gate, (gates.M, gates.Align)): + rule = self.rules[type(gate)] + + natives = platform.natives + + if isinstance(gate, (gates.M)): + qubits = [ + natives.single_qubit[platform.get_qubit(q).name] for q in gate.qubits + ] + return rule(gate, qubits) + + if isinstance(gate, (gates.Align)): qubits = [platform.get_qubit(q) for q in gate.qubits] - gate_sequence = rule(gate, qubits) - elif len(gate.qubits) == 1: + return rule(gate, qubits) + + if isinstance(gate, (gates.Z, gates.RZ)): qubit = platform.get_qubit(gate.target_qubits[0]) - gate_sequence = rule(gate, qubit) - elif len(gate.qubits) == 2: - pair = platform.pairs[ - tuple(platform.get_qubit(q).name for q in gate.qubits) - ] - gate_sequence = rule(gate, pair) - else: - raise NotImplementedError(f"{type(gate)} is not a native gate.") - return gate_sequence + return rule(gate, qubit) + + if len(gate.qubits) == 1: + qubit = platform.get_qubit(gate.target_qubits[0]) + return rule(gate, natives.single_qubit[qubit.name]) + + if len(gate.qubits) == 2: + pair = tuple(platform.get_qubit(q).name for q in gate.qubits) + assert len(pair) == 2 + return rule(gate, natives.two_qubit[pair]) + + raise NotImplementedError(f"{type(gate)} is not a native gate.") # FIXME: pulse.qubit and pulse.channel do not exist anymore def compile( self, circuit: Circuit, platform: Platform - ) -> tuple[PulseSequence, dict]: + ) -> tuple[PulseSequence, dict[gates.M, PulseSequence]]: """Transforms a circuit to pulse sequence. Args: diff --git a/src/qibolab/compilers/default.py b/src/qibolab/compilers/default.py index de1fb8f1c1..5884ed038c 100644 --- a/src/qibolab/compilers/default.py +++ b/src/qibolab/compilers/default.py @@ -8,13 +8,9 @@ import numpy as np from qibo.gates import Align, Gate +from qibolab.native import SingleQubitNatives, TwoQubitNatives from qibolab.pulses import Delay, PulseSequence, VirtualZ -from qibolab.qubits import Qubit, QubitPair - - -def identity_rule(gate: Gate, qubit: Qubit) -> PulseSequence: - """Identity gate skipped.""" - return PulseSequence() +from qibolab.qubits import Qubit def z_rule(gate: Gate, qubit: Qubit) -> PulseSequence: @@ -27,41 +23,44 @@ def rz_rule(gate: Gate, qubit: Qubit) -> PulseSequence: return PulseSequence([(qubit.drive.name, VirtualZ(phase=gate.parameters[0]))]) -def gpi2_rule(gate: Gate, qubit: Qubit) -> PulseSequence: +def identity_rule(gate: Gate, natives: SingleQubitNatives) -> PulseSequence: + """Identity gate skipped.""" + return PulseSequence() + + +def gpi2_rule(gate: Gate, natives: SingleQubitNatives) -> PulseSequence: """Rule for GPI2.""" - return qubit.native_gates.RX.create_sequence( - theta=np.pi / 2, phi=gate.parameters[0] - ) + return natives.ensure("RX").create_sequence(theta=np.pi / 2, phi=gate.parameters[0]) -def gpi_rule(gate: Gate, qubit: Qubit) -> PulseSequence: +def gpi_rule(gate: Gate, natives: SingleQubitNatives) -> PulseSequence: """Rule for GPI.""" # 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. - return qubit.native_gates.RX.create_sequence(theta=np.pi, phi=gate.parameters[0]) + return natives.ensure("RX").create_sequence(theta=np.pi, phi=gate.parameters[0]) -def cz_rule(gate: Gate, pair: QubitPair) -> PulseSequence: +def cz_rule(gate: Gate, natives: TwoQubitNatives) -> PulseSequence: """CZ applied as defined in the platform runcard. Applying the CZ gate may involve sending pulses on qubits that the gate is not directly acting on. """ - return pair.native_gates.CZ.create_sequence() + return natives.ensure("CZ").create_sequence() -def cnot_rule(gate: Gate, pair: QubitPair) -> PulseSequence: +def cnot_rule(gate: Gate, natives: TwoQubitNatives) -> PulseSequence: """CNOT applied as defined in the platform runcard.""" - return pair.native_gates.CNOT.create_sequence() + return natives.ensure("CNOT").create_sequence() -def measurement_rule(gate: Gate, qubits: list[Qubit]) -> PulseSequence: +def measurement_rule(gate: Gate, natives: list[SingleQubitNatives]) -> PulseSequence: """Measurement gate applied using the platform readout pulse.""" seq = PulseSequence() - for qubit in qubits: - seq.concatenate(qubit.native_gates.MZ.create_sequence()) + for qubit in natives: + seq.concatenate(qubit.ensure("MZ").create_sequence()) return seq diff --git a/src/qibolab/components/configs.py b/src/qibolab/components/configs.py index 72f63a39ea..12e719b259 100644 --- a/src/qibolab/components/configs.py +++ b/src/qibolab/components/configs.py @@ -7,10 +7,11 @@ configuration defined by these classes. """ -from dataclasses import dataclass, field -from typing import Optional, Union +from typing import Annotated, Literal, Optional, Union -import numpy.typing as npt +from pydantic import Field + +from qibolab.serialize import Model, NdArray __all__ = [ "DcConfig", @@ -22,25 +23,26 @@ ] -@dataclass(frozen=True) -class DcConfig: +class DcConfig(Model): """Configuration for a channel that can be used to send DC pulses (i.e. just envelopes without modulation).""" + kind: Literal["dc"] = "dc" + offset: float """DC offset/bias of the channel.""" -@dataclass(frozen=True) -class OscillatorConfig: +class OscillatorConfig(Model): """Configuration for an oscillator.""" + kind: Literal["oscillator"] = "oscillator" + frequency: float power: float -@dataclass(frozen=True) -class IqMixerConfig: +class IqMixerConfig(Model): """Configuration for IQ mixer. Mixers usually have various imperfections, and one needs to @@ -48,6 +50,8 @@ class IqMixerConfig: configuration. """ + kind: Literal["iq-mixer"] = "iq-mixer" + offset_i: float = 0.0 """DC offset for the I component.""" offset_q: float = 0.0 @@ -60,21 +64,23 @@ class IqMixerConfig: imbalance.""" -@dataclass(frozen=True) -class IqConfig: +class IqConfig(Model): """Configuration for an IQ channel.""" + kind: Literal["iq"] = "iq" + frequency: float """The carrier frequency of the channel.""" -@dataclass(frozen=True) -class AcquisitionConfig: +class AcquisitionConfig(Model): """Configuration for acquisition channel. Currently, in qibolab, acquisition channels are FIXME: """ + kind: Literal["acquisition"] = "acquisition" + delay: float """Delay between readout pulse start and acquisition start.""" smearing: float @@ -88,9 +94,48 @@ class AcquisitionConfig: iq_angle: Optional[float] = None """Signal angle in the IQ-plane for disciminating ground and excited states.""" - kernel: Optional[npt.NDArray] = field(default=None, repr=False) + kernel: Annotated[Optional[NdArray], Field(repr=False)] = None """Integration weights to be used when post-processing the acquired signal.""" - -Config = Union[DcConfig, IqMixerConfig, OscillatorConfig, IqConfig, AcquisitionConfig] + def __eq__(self, other) -> bool: + """Explicit configuration equality. + + .. note:: + + the expliciti definition is required in order to solve the ambiguity about + the arrays equality + """ + return ( + (self.delay == other.delay) + and (self.smearing == other.smearing) + and (self.threshold == other.threshold) + and (self.iq_angle == other.iq_angle) + and (self.kernel == other.kernel).all() + ) + + +class BoundsConfig(Model): + """Instument memory limitations proxies.""" + + kind: Literal["bounds"] = "bounds" + + waveforms: int + """Waveforms estimated size.""" + readout: int + """Number of readouts.""" + instructions: int + """Instructions estimated size.""" + + +Config = Annotated[ + Union[ + DcConfig, + IqMixerConfig, + OscillatorConfig, + IqConfig, + AcquisitionConfig, + BoundsConfig, + ], + Field(discriminator="kind"), +] diff --git a/src/qibolab/dummy/kernels.npz b/src/qibolab/dummy/kernels.npz deleted file mode 100644 index 15cb0ecb3e..0000000000 Binary files a/src/qibolab/dummy/kernels.npz and /dev/null differ diff --git a/src/qibolab/dummy/parameters.json b/src/qibolab/dummy/parameters.json index d894f20dbd..9c17c54350 100644 --- a/src/qibolab/dummy/parameters.json +++ b/src/qibolab/dummy/parameters.json @@ -1,668 +1,794 @@ { - "nqubits": 5, - "settings": { - "nshots": 1024, - "relaxation_time": 0 - }, - "instruments": { - "dummy": { - "bounds": { - "waveforms": 0, - "readout": 0, - "instructions": 0 - } + "settings": { + "nshots": 1024, + "relaxation_time": 0 }, - "twpa_pump": { - "power": 10, - "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, - "threshold": 0.0, - "iq_angle": 0.0 - }, - "qubit_1/acquire": { - "delay": 0, - "smearing": 0, - "threshold": 0.0, - "iq_angle": 0.0 - }, - "qubit_2/acquire": { - "delay": 0, - "smearing": 0, - "threshold": 0.0, - "iq_angle": 0.0 - }, - "qubit_3/acquire": { - "delay": 0, - "smearing": 0, - "threshold": 0.0, - "iq_angle": 0.0 - }, - "qubit_4/acquire": { - "delay": 0, - "smearing": 0, - "threshold": 0.0, - "iq_angle": 0.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": [ - [ - "qubit_0/drive", - { - "duration": 40.0, - "amplitude": 0.1, - "envelope": { "kind": "gaussian", "rel_sigma": 5.0 } - } - ] - ], - "RX12": [ - [ - "qubit_0/drive12", - { - "duration": 40.0, - "amplitude": 0.005, - "envelope": { "kind": "gaussian", "rel_sigma": 5.0 } - } - ] - ], - "MZ": [ - [ - "qubit_0/probe", - { - "duration": 2000.0, - "amplitude": 0.1, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - } - } - ] - ] - }, - "1": { - "RX": [ - [ - "qubit_1/drive", - { - "duration": 40.0, - "amplitude": 0.3, - "envelope": { "kind": "drag", "rel_sigma": 5.0, "beta": 0.02 } - } - ] - ], - "RX12": [ - [ - "qubit_1/drive12", - { - "duration": 40.0, - "amplitude": 0.0484, - "envelope": { "kind": "drag", "rel_sigma": 5.0, "beta": 0.02 } - } - ] - ], - "MZ": [ - [ - "qubit_1/probe", - { - "duration": 2000.0, - "amplitude": 0.1, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - } - } - ] - ] - }, - "2": { - "RX": [ - [ - "qubit_2/drive", - { - "duration": 40.0, - "amplitude": 0.3, - "envelope": { "kind": "drag", "rel_sigma": 5.0, "beta": 0.02 } - } - ] - ], - "RX12": [ - [ - "qubit_2/drive12", - { - "duration": 40.0, - "amplitude": 0.005, - "envelope": { "kind": "gaussian", "rel_sigma": 5.0 } - } - ] - ], - "MZ": [ - [ - "qubit_2/probe", - { - "duration": 2000.0, - "amplitude": 0.1, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - } - } - ] - ] - }, - "3": { - "RX": [ - [ - "qubit_3/drive", - { - "duration": 40.0, - "amplitude": 0.3, - "envelope": { "kind": "drag", "rel_sigma": 5.0, "beta": 0.02 } - } - ] - ], - "RX12": [ - [ - "qubit_3/drive12", - { - "duration": 40.0, - "amplitude": 0.0484, - "envelope": { "kind": "drag", "rel_sigma": 5.0, "beta": 0.02 } - } - ] - ], - "MZ": [ - [ - "qubit_3/probe", - { - "duration": 2000.0, - "amplitude": 0.1, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - } - } - ] - ] - }, - "4": { - "RX": [ - [ - "qubit_4/drive", - { - "duration": 40.0, - "amplitude": 0.3, - "envelope": { "kind": "drag", "rel_sigma": 5.0, "beta": 0.02 } - } - ] - ], - "RX12": [ - [ - "qubit_4/drive12", - { - "duration": 40.0, - "amplitude": 0.0484, - "envelope": { "kind": "drag", "rel_sigma": 5.0, "beta": 0.02 } - } - ] - ], - "MZ": [ - [ - "qubit_4/probe", - { - "duration": 2000.0, - "amplitude": 0.1, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - } - } - ] - ] - } + "configs": { + "dummy/bounds": { + "kind": "bounds", + "waveforms": 0, + "readout": 0, + "instructions": 0 + }, + "qubit_0/drive": { + "kind": "iq", + "frequency": 4000000000.0 + }, + "qubit_1/drive": { + "kind": "iq", + "frequency": 4200000000.0 + }, + "qubit_2/drive": { + "kind": "iq", + "frequency": 4500000000.0 + }, + "qubit_3/drive": { + "kind": "iq", + "frequency": 4150000000.0 + }, + "qubit_4/drive": { + "kind": "iq", + "frequency": 4155663000.0 + }, + "qubit_0/drive12": { + "kind": "iq", + "frequency": 4700000000.0 + }, + "qubit_1/drive12": { + "kind": "iq", + "frequency": 4855663000.0 + }, + "qubit_2/drive12": { + "kind": "iq", + "frequency": 2700000000.0 + }, + "qubit_3/drive12": { + "kind": "iq", + "frequency": 5855663000.0 + }, + "qubit_4/drive12": { + "kind": "iq", + "frequency": 5855663000.0 + }, + "qubit_0/flux": { + "kind": "dc", + "offset": -0.1 + }, + "qubit_1/flux": { + "kind": "dc", + "offset": 0.0 + }, + "qubit_2/flux": { + "kind": "dc", + "offset": 0.1 + }, + "qubit_3/flux": { + "kind": "dc", + "offset": 0.2 + }, + "qubit_4/flux": { + "kind": "dc", + "offset": 0.15 + }, + "qubit_0/probe": { + "kind": "iq", + "frequency": 5200000000.0 + }, + "qubit_1/probe": { + "kind": "iq", + "frequency": 4900000000.0 + }, + "qubit_2/probe": { + "kind": "iq", + "frequency": 6100000000.0 + }, + "qubit_3/probe": { + "kind": "iq", + "frequency": 5800000000.0 + }, + "qubit_4/probe": { + "kind": "iq", + "frequency": 5500000000.0 + }, + "qubit_0/acquire": { + "kind": "acquisition", + "delay": 0.0, + "smearing": 0.0, + "threshold": 0.0, + "iq_angle": 0.0, + "kernel": "k05VTVBZAQB2AHsnZGVzY3InOiAnPGY4JywgJ2ZvcnRyYW5fb3JkZXInOiBGYWxzZSwgJ3NoYXBlJzogKDEwLCksIH0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAp5sDfS7uHlP2DIMKNvnKc/gCqN8KV/pT94FQCYYJC3PzSbwfi/894/APwg6C61rj8MSN3blizAP2ha9unQYsM/+BFjHTxcwT+gXaJazvbpPw==" + }, + "qubit_1/acquire": { + "kind": "acquisition", + "delay": 0.0, + "smearing": 0.0, + "threshold": 0.0, + "iq_angle": 0.0, + "kernel": "k05VTVBZAQB2AHsnZGVzY3InOiAnPGY4JywgJ2ZvcnRyYW5fb3JkZXInOiBGYWxzZSwgJ3NoYXBlJzogKDEwLCksIH0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAr4dT6V5tHrP1w+JhHImN8/sPePZeSUuj/4yTKrD5fRP/ysonZip98/6GJMAPV9xD/LTiJo4k7oP96aWXpxduU/6fUxETe/7z9GXEBNGebWPw==" + }, + "qubit_2/acquire": { + "kind": "acquisition", + "delay": 0.0, + "smearing": 0.0, + "threshold": 0.0, + "iq_angle": 0.0, + "kernel": "k05VTVBZAQB2AHsnZGVzY3InOiAnPGY4JywgJ2ZvcnRyYW5fb3JkZXInOiBGYWxzZSwgJ3NoYXBlJzogKDEwLCksIH0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIApUtcFdBpTsPzzwG8Xkbts/OAvkS0qo4z+OUcJpZ8HlP/jsO9cUwso/s6DVM7e/4T/NL4JYzUXvP9CibqEg98M/AENJ8QPkcD8wAOtI4pHNPw==" + }, + "qubit_3/acquire": { + "kind": "acquisition", + "delay": 0.0, + "smearing": 0.0, + "threshold": 0.0, + "iq_angle": 0.0, + "kernel": "k05VTVBZAQB2AHsnZGVzY3InOiAnPGY4JywgJ2ZvcnRyYW5fb3JkZXInOiBGYWxzZSwgJ3NoYXBlJzogKDEwLCksIH0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogk3D0+DqsP1ry/LSlMuM/ydJYpPk/6D8BJMdYs+zsP1CqhVqYz+o/1A3srA5z7j8CUqCE6lvqPwjNySZ1DuA/YHGvVmuFsz+XwaQKz/bqPw==" + }, + "qubit_4/acquire": { + "kind": "acquisition", + "delay": 0.0, + "smearing": 0.0, + "threshold": 0.0, + "iq_angle": 0.0, + "kernel": "k05VTVBZAQB2AHsnZGVzY3InOiAnPGY4JywgJ2ZvcnRyYW5fb3JkZXInOiBGYWxzZSwgJ3NoYXBlJzogKDEwLCksIH0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIArIY+/UbVjMP+yH0UH5xOU/YDXeTaxK4j/Au9mGjGjBPx1R4v/zy+Q/+OxfZT0Vzj/5ng1s2GzkP64nr91FXOk/9/ggMXmG4D+6NjR7dMHtPw==" + }, + "coupler_0/flux": { + "kind": "dc", + "offset": 0.0 + }, + "coupler_1/flux": { + "kind": "dc", + "offset": 0.0 + }, + "coupler_3/flux": { + "kind": "dc", + "offset": 0.0 + }, + "coupler_4/flux": { + "kind": "dc", + "offset": 0.0 + }, + "twpa_pump": { + "kind": "oscillator", + "frequency": 1000000000.0, + "power": 10.0 + } }, - "coupler": { - "0": { - "CP": [ - ["coupler_0/flux", - { - "duration": 30.0, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - } - } - ] - ] - }, - "1": { - "CP": [ - ["coupler_1/flux", - { - "duration": 30.0, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - } - } - ] - ] - }, - "3": { - "CP": [ - ["coupler_3/flux", - { - "duration": 30.0, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - } - } - ] - ] - }, - "4": { - "CP": [ - ["coupler_4/flux", - { - "duration": 30.0, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - } - } - ] - ] - } - }, - "two_qubit": { - "0-2": { - "CZ": [ - [ - "qubit_2/flux", - { - "duration": 30.0, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - } - } - ], - [ - "qubit_0/drive", - { - "phase": 0.0 - } - ], - [ - "qubit_2/drive", - { - "phase": 0.0 - } - ], - [ - "coupler_0/flux", - { - "duration": 30.0, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - } - } - ] - ], - "iSWAP": [ - [ - "qubit_2/flux", - { - "duration": 30.0, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - } - } - ], - [ - "qubit_0/drive", - { - "phase": 0.0 - } - ], - [ - "qubit_2/drive", - { - "phase": 0.0 - } - ], - [ - "coupler_0/flux", - { - "duration": 30.0, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - } - } - ] - ] - }, - "1-2": { - "CZ": [ - [ - "qubit_2/flux", - { - "duration": 30.0, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - } - } - ], - [ - "qubit_1/drive", - { - "phase": 0.0 - } - ], - [ - "qubit_2/drive", - { - "phase": 0.0 - } - ], - [ - "coupler_1/flux", - { - "duration": 30.0, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - } - } - ] - ], - "iSWAP": [ - [ - "qubit_2/flux", - { - "duration": 30.0, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - } - } - ], - [ - "qubit_1/drive", - { - "phase": 0.0 - } - ], - [ - "qubit_2/drive", - { - "phase": 0.0 - } - ], - [ - "coupler_1/flux", - { - "duration": 30.0, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - } - } - ] - ] - }, - "2-3": { - "CZ": [ - [ - "qubit_2/flux", - { - "duration": 30.0, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - } - } - ], - [ - "qubit_2/drive", - { - "phase": 0.0 - } - ], - [ - "qubit_3/drive", - { - "phase": 0.0 - } - ] - ], - "iSWAP": [ - [ - "qubit_2/flux", - { - "duration": 30.0, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - } - } - ], - [ - "qubit_2/drive", - { - "phase": 0.0 - } - ], - [ - "qubit_3/drive", - { - "phase": 0.0 - } - ] - ], - "CNOT": [ - [ - "qubit_2/drive", - { - "duration": 40.0, - "amplitude": 0.3, - "envelope": { "kind": "drag", "rel_sigma": 5.0, "beta": 0.02 } - } - ] - ] - }, - "2-4": { - "CZ": [ - [ - "qubit_2/flux", - { - "duration": 30.0, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - } - } - ], - [ - "qubit_4/drive", - { - "phase": 0.0 - } - ], - [ - "coupler_4/flux", - { - "duration": 30.0, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - } - } - ] - ], - "iSWAP": [ - [ - "qubit_2/flux", - { - "duration": 30.0, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - } - } - ], - [ - "qubit_2/drive", - { - "phase": 0.0 - } - ], - [ - "qubit_4/drive", - { - "phase": 0.0 - } - ], - [ - "coupler_4/flux", - { - "duration": 30.0, - "amplitude": 0.05, - "envelope": { - "kind": "gaussian_square", - "rel_sigma": 5.0, - "width": 0.75 - } - } - ] - ] - } + "native_gates": { + "single_qubit": { + "0": { + "RX": [ + [ + "qubit_0/drive", + { + "duration": 40.0, + "amplitude": 0.1, + "envelope": { + "kind": "gaussian", + "rel_sigma": 5.0 + }, + "relative_phase": 0.0 + } + ] + ], + "RX12": [ + [ + "qubit_0/drive12", + { + "duration": 40.0, + "amplitude": 0.005, + "envelope": { + "kind": "gaussian", + "rel_sigma": 5.0 + }, + "relative_phase": 0.0 + } + ] + ], + "MZ": [ + [ + "qubit_0/probe", + { + "duration": 2000.0, + "amplitude": 0.1, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0 + } + ] + ], + "CP": null + }, + "1": { + "RX": [ + [ + "qubit_1/drive", + { + "duration": 40.0, + "amplitude": 0.3, + "envelope": { + "kind": "drag", + "rel_sigma": 5.0, + "beta": 0.02 + }, + "relative_phase": 0.0 + } + ] + ], + "RX12": [ + [ + "qubit_1/drive12", + { + "duration": 40.0, + "amplitude": 0.0484, + "envelope": { + "kind": "drag", + "rel_sigma": 5.0, + "beta": 0.02 + }, + "relative_phase": 0.0 + } + ] + ], + "MZ": [ + [ + "qubit_1/probe", + { + "duration": 2000.0, + "amplitude": 0.1, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0 + } + ] + ], + "CP": null + }, + "2": { + "RX": [ + [ + "qubit_2/drive", + { + "duration": 40.0, + "amplitude": 0.3, + "envelope": { + "kind": "drag", + "rel_sigma": 5.0, + "beta": 0.02 + }, + "relative_phase": 0.0 + } + ] + ], + "RX12": [ + [ + "qubit_2/drive12", + { + "duration": 40.0, + "amplitude": 0.005, + "envelope": { + "kind": "gaussian", + "rel_sigma": 5.0 + }, + "relative_phase": 0.0 + } + ] + ], + "MZ": [ + [ + "qubit_2/probe", + { + "duration": 2000.0, + "amplitude": 0.1, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0 + } + ] + ], + "CP": null + }, + "3": { + "RX": [ + [ + "qubit_3/drive", + { + "duration": 40.0, + "amplitude": 0.3, + "envelope": { + "kind": "drag", + "rel_sigma": 5.0, + "beta": 0.02 + }, + "relative_phase": 0.0 + } + ] + ], + "RX12": [ + [ + "qubit_3/drive12", + { + "duration": 40.0, + "amplitude": 0.0484, + "envelope": { + "kind": "drag", + "rel_sigma": 5.0, + "beta": 0.02 + }, + "relative_phase": 0.0 + } + ] + ], + "MZ": [ + [ + "qubit_3/probe", + { + "duration": 2000.0, + "amplitude": 0.1, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0 + } + ] + ], + "CP": null + }, + "4": { + "RX": [ + [ + "qubit_4/drive", + { + "duration": 40.0, + "amplitude": 0.3, + "envelope": { + "kind": "drag", + "rel_sigma": 5.0, + "beta": 0.02 + }, + "relative_phase": 0.0 + } + ] + ], + "RX12": [ + [ + "qubit_4/drive12", + { + "duration": 40.0, + "amplitude": 0.0484, + "envelope": { + "kind": "drag", + "rel_sigma": 5.0, + "beta": 0.02 + }, + "relative_phase": 0.0 + } + ] + ], + "MZ": [ + [ + "qubit_4/probe", + { + "duration": 2000.0, + "amplitude": 0.1, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0 + } + ] + ], + "CP": null + } + }, + "coupler": { + "0": { + "RX": null, + "RX12": null, + "MZ": null, + "CP": [ + [ + "coupler_0/flux", + { + "duration": 30.0, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0 + } + ] + ] + }, + "1": { + "RX": null, + "RX12": null, + "MZ": null, + "CP": [ + [ + "coupler_1/flux", + { + "duration": 30.0, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0 + } + ] + ] + }, + "3": { + "RX": null, + "RX12": null, + "MZ": null, + "CP": [ + [ + "coupler_3/flux", + { + "duration": 30.0, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0 + } + ] + ] + }, + "4": { + "RX": null, + "RX12": null, + "MZ": null, + "CP": [ + [ + "coupler_4/flux", + { + "duration": 30.0, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0 + } + ] + ] + } + }, + "two_qubit": { + "0-2": { + "CZ": [ + [ + "qubit_2/flux", + { + "duration": 30.0, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0 + } + ], + [ + "qubit_0/drive", + { + "phase": 0.0 + } + ], + [ + "qubit_2/drive", + { + "phase": 0.0 + } + ], + [ + "coupler_0/flux", + { + "duration": 30.0, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0 + } + ] + ], + "CNOT": null, + "iSWAP": [ + [ + "qubit_2/flux", + { + "duration": 30.0, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0 + } + ], + [ + "qubit_0/drive", + { + "phase": 0.0 + } + ], + [ + "qubit_2/drive", + { + "phase": 0.0 + } + ], + [ + "coupler_0/flux", + { + "duration": 30.0, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0 + } + ] + ] + }, + "1-2": { + "CZ": [ + [ + "qubit_2/flux", + { + "duration": 30.0, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0 + } + ], + [ + "qubit_1/drive", + { + "phase": 0.0 + } + ], + [ + "qubit_2/drive", + { + "phase": 0.0 + } + ], + [ + "coupler_1/flux", + { + "duration": 30.0, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0 + } + ] + ], + "CNOT": null, + "iSWAP": [ + [ + "qubit_2/flux", + { + "duration": 30.0, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0 + } + ], + [ + "qubit_1/drive", + { + "phase": 0.0 + } + ], + [ + "qubit_2/drive", + { + "phase": 0.0 + } + ], + [ + "coupler_1/flux", + { + "duration": 30.0, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0 + } + ] + ] + }, + "2-3": { + "CZ": [ + [ + "qubit_2/flux", + { + "duration": 30.0, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0 + } + ], + [ + "qubit_2/drive", + { + "phase": 0.0 + } + ], + [ + "qubit_3/drive", + { + "phase": 0.0 + } + ] + ], + "CNOT": [ + [ + "qubit_2/drive", + { + "duration": 40.0, + "amplitude": 0.3, + "envelope": { + "kind": "drag", + "rel_sigma": 5.0, + "beta": 0.02 + }, + "relative_phase": 0.0 + } + ] + ], + "iSWAP": [ + [ + "qubit_2/flux", + { + "duration": 30.0, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0 + } + ], + [ + "qubit_2/drive", + { + "phase": 0.0 + } + ], + [ + "qubit_3/drive", + { + "phase": 0.0 + } + ] + ] + }, + "2-4": { + "CZ": [ + [ + "qubit_2/flux", + { + "duration": 30.0, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0 + } + ], + [ + "qubit_4/drive", + { + "phase": 0.0 + } + ], + [ + "coupler_4/flux", + { + "duration": 30.0, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0 + } + ] + ], + "CNOT": null, + "iSWAP": [ + [ + "qubit_2/flux", + { + "duration": 30.0, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0 + } + ], + [ + "qubit_2/drive", + { + "phase": 0.0 + } + ], + [ + "qubit_4/drive", + { + "phase": 0.0 + } + ], + [ + "coupler_4/flux", + { + "duration": 30.0, + "amplitude": 0.05, + "envelope": { + "kind": "gaussian_square", + "rel_sigma": 5.0, + "width": 0.75 + }, + "relative_phase": 0.0 + } + ] + ] + } + } } - } } diff --git a/src/qibolab/dummy/platform.py b/src/qibolab/dummy/platform.py index 05ef378dd4..c725a3c9ad 100644 --- a/src/qibolab/dummy/platform.py +++ b/src/qibolab/dummy/platform.py @@ -1,83 +1,36 @@ import pathlib -from qibolab.components import ( - AcquireChannel, - AcquisitionConfig, - DcChannel, - DcConfig, - IqChannel, - IqConfig, - OscillatorConfig, -) +from qibolab.components import AcquireChannel, DcChannel, IqChannel from qibolab.instruments.dummy import DummyInstrument, DummyLocalOscillator -from qibolab.kernels import Kernels from qibolab.platform import Platform -from qibolab.serialize import ( - load_instrument_settings, - load_qubits, - load_runcard, - load_settings, -) +from qibolab.qubits import Qubit FOLDER = pathlib.Path(__file__).parent -def create_dummy(): +def create_dummy() -> Platform: """Create a dummy platform using the dummy instrument.""" + # register the instruments instrument = DummyInstrument("dummy", "0.0.0.0") - - twpa_pump_name = "twpa_pump" - twpa_pump = DummyLocalOscillator(twpa_pump_name, "0.0.0.0") - - runcard = load_runcard(FOLDER) - kernels = Kernels.load(FOLDER) - - qubits, couplers, pairs = load_qubits(runcard) - settings = load_settings(runcard) - - configs = {} - component_params = runcard["components"] - configs[twpa_pump_name] = OscillatorConfig(**component_params[twpa_pump_name]) - for q, qubit in qubits.items(): - 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 + pump = DummyLocalOscillator("twpa_pump", "0.0.0.0") + + qubits = {} + # attach the channels + for q in range(5): + probe, acquisition = f"qubit_{q}/probe", f"qubit_{q}/acquire" + qubits[q] = Qubit( + name=q, + probe=IqChannel(probe, mixer=None, lo=None, acquisition=acquisition), + acquisition=AcquireChannel(acquisition, twpa_pump=pump.name, probe=probe), + drive=IqChannel(f"qubit_{q}/drive", mixer=None, lo=None), + drive12=IqChannel(f"qubit_{q}/drive12", mixer=None, lo=None), + flux=DcChannel(f"qubit_{q}/flux"), ) - configs[probe_name] = IqConfig(**component_params[probe_name]) - configs[acquisition_name] = AcquisitionConfig( - **component_params[acquisition_name], kernel=kernels.get(q) - ) - - 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]) - for c, coupler in couplers.items(): - flux_name = f"coupler_{c}/flux" - coupler.flux = DcChannel(flux_name) - configs[flux_name] = DcConfig(**component_params[flux_name]) + couplers = {} + for c in (0, 1, 3, 4): + couplers[c] = Qubit(name=c, flux=DcChannel(f"coupler_{c}/flux")) - instruments = {instrument.name: instrument, twpa_pump.name: twpa_pump} - instruments = load_instrument_settings(runcard, instruments) - return Platform( - FOLDER.name, - qubits, - pairs, - configs, - instruments, - settings, - resonator_type="2D", - couplers=couplers, + return Platform.load( + path=FOLDER, instruments=[instrument, pump], qubits=qubits, couplers=couplers ) diff --git a/src/qibolab/execution_parameters.py b/src/qibolab/execution_parameters.py index f9117f32b5..646c806aa9 100644 --- a/src/qibolab/execution_parameters.py +++ b/src/qibolab/execution_parameters.py @@ -1,7 +1,7 @@ from enum import Enum, auto from typing import Any, Optional -from qibolab.serialize_ import Model +from qibolab.serialize import Model from qibolab.sweeper import ParallelSweepers diff --git a/src/qibolab/instruments/abstract.py b/src/qibolab/instruments/abstract.py index 1824f58fb2..bee8f98cac 100644 --- a/src/qibolab/instruments/abstract.py +++ b/src/qibolab/instruments/abstract.py @@ -8,7 +8,6 @@ from qibolab.execution_parameters import ExecutionParameters from qibolab.pulses.sequence import PulseSequence from qibolab.sweeper import ParallelSweepers -from qibolab.unrolling import Bounds InstrumentId = str @@ -61,17 +60,9 @@ class Controller(Instrument): def __init__(self, name, address): super().__init__(name, address) - self.bounds: Bounds = Bounds(0, 0, 0) + self.bounds: str """Estimated limitations of the device memory.""" - def setup(self, bounds): - """Set unrolling batch bounds.""" - self.bounds = Bounds(**bounds) - - def dump(self): - """Dump unrolling batch bounds.""" - return {"bounds": asdict(self.bounds)} - @property @abstractmethod def sampling_rate(self) -> int: diff --git a/src/qibolab/instruments/dummy.py b/src/qibolab/instruments/dummy.py index 30f38d8c33..5e2774b4b1 100644 --- a/src/qibolab/instruments/dummy.py +++ b/src/qibolab/instruments/dummy.py @@ -1,3 +1,5 @@ +from dataclasses import dataclass + import numpy as np from qibo.config import log @@ -12,6 +14,7 @@ from .oscillator import LocalOscillator SAMPLING_RATE = 1 +BOUNDS = Bounds(waveforms=1, readout=1, instructions=1) class DummyDevice: @@ -47,6 +50,7 @@ def create(self): return DummyDevice() +@dataclass class DummyInstrument(Controller): """Dummy instrument that returns random voltage values. @@ -60,7 +64,9 @@ class DummyInstrument(Controller): instruments. """ - BOUNDS = Bounds(1, 1, 1) + name: str + address: str + bounds: str = "dummy/bounds" @property def sampling_rate(self) -> int: diff --git a/src/qibolab/instruments/emulator/pulse_simulator.py b/src/qibolab/instruments/emulator/pulse_simulator.py index 851177557a..f985f0fb5d 100644 --- a/src/qibolab/instruments/emulator/pulse_simulator.py +++ b/src/qibolab/instruments/emulator/pulse_simulator.py @@ -93,7 +93,7 @@ def disconnect(self): pass def dump(self): - return self.settings | super().dump() + return self.settings def run_pulse_simulation( self, diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index 5bafbf8153..4f63f643d8 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -30,6 +30,12 @@ __all__ = ["QmController", "Octave"] +BOUNDS = Bounds( + waveforms=int(4e4), + readout=30, + instructions=int(1e6), +) + @dataclass(frozen=True) class Octave: @@ -132,7 +138,7 @@ class QmController(Controller): used.""" channels: dict[str, QmChannel] - bounds: Bounds = Bounds(0, 0, 0) + bounds: str = "qm/bounds" """Maximum bounds used for batching in sequence unrolling.""" calibration_path: Optional[str] = None """Path to the JSON file that contains the mixer calibration.""" @@ -182,13 +188,6 @@ def __post_init__(self): self.channels = { channel.logical_channel.name: channel for channel in self.channels } - # redefine bounds because abstract instrument overwrites them - # FIXME: This overwrites the ``bounds`` given in the runcard! - self.bounds = Bounds( - waveforms=int(4e4), - readout=30, - instructions=int(1e6), - ) if self.simulation_duration is not None: # convert simulation duration from ns to clock cycles diff --git a/src/qibolab/instruments/zhinst/components/configs.py b/src/qibolab/instruments/zhinst/components/configs.py index d65a28dd8a..e85524bb8e 100644 --- a/src/qibolab/instruments/zhinst/components/configs.py +++ b/src/qibolab/instruments/zhinst/components/configs.py @@ -1,5 +1,3 @@ -from dataclasses import dataclass - from qibolab.components import AcquisitionConfig, DcConfig, IqConfig __all__ = [ @@ -9,7 +7,6 @@ ] -@dataclass(frozen=True) class ZiDcConfig(DcConfig): """DC channel config using ZI HDAWG.""" @@ -20,7 +17,6 @@ class ZiDcConfig(DcConfig): """ -@dataclass(frozen=True) class ZiIqConfig(IqConfig): """IQ channel config for ZI SHF* line instrument.""" @@ -31,7 +27,6 @@ class ZiIqConfig(IqConfig): """ -@dataclass(frozen=True) class ZiAcquisitionConfig(AcquisitionConfig): """Acquisition config for ZI SHF* line instrument.""" diff --git a/src/qibolab/kernels.py b/src/qibolab/kernels.py deleted file mode 100644 index 9ee1ae60d8..0000000000 --- a/src/qibolab/kernels.py +++ /dev/null @@ -1,39 +0,0 @@ -import json -from pathlib import Path - -import numpy as np - -from qibolab.qubits import QubitId - -KERNELS = "kernels.npz" - - -class Kernels(dict[QubitId, np.ndarray]): - """A dictionary subclass for handling Qubit Kernels. - - This class extends the built-in dict class and maps QubitId to numpy - arrays. It provides methods to load and dump the kernels from and to - a file. - """ - - @classmethod - def load(cls, path: Path): - """Class method to load kernels from a file. - - The file should contain a serialized dictionary where keys are - serialized QubitId and values are numpy arrays. - """ - return cls( - {json.loads(key): value for key, value in np.load(path / KERNELS).items()} - ) - - def dump(self, path: Path): - """Instance method to dump the kernels to a file. - - The keys (QubitId) are serialized to strings and the values - (numpy arrays) are kept as is. - """ - np.savez( - path / KERNELS, - **{json.dumps(qubit_id): value for qubit_id, value in self.items()} - ) diff --git a/src/qibolab/native.py b/src/qibolab/native.py index 753c569108..ded928963d 100644 --- a/src/qibolab/native.py +++ b/src/qibolab/native.py @@ -1,10 +1,10 @@ -from dataclasses import dataclass, field, fields -from typing import Optional +from abc import ABC, abstractmethod +from typing import Annotated, Optional import numpy as np from .pulses import Drag, Gaussian, Pulse, PulseSequence -from .serialize_ import replace +from .serialize import Model, replace def _normalize_angles(theta, phi): @@ -15,7 +15,13 @@ def _normalize_angles(theta, phi): return theta, phi -class RxyFactory: +class Native(ABC, PulseSequence): + @abstractmethod + def create_sequence(self, *args, **kwargs) -> PulseSequence: + """Create a sequence for single-qubit rotation.""" + + +class RxyFactory(Native): """Factory for pulse sequences that generate single-qubit rotations around an axis in xy plane. @@ -27,30 +33,30 @@ class RxyFactory: sequence: The base sequence for the factory. """ - def __init__(self, sequence: PulseSequence): - if len(sequence.channels) != 1: + def __init__(self, iterable): + super().__init__(iterable) + cls = type(self) + if len(self.channels) != 1: raise ValueError( - f"Incompatible number of channels: {len(sequence.channels)}. " - f"{self.__class__} expects a sequence on exactly one channel." + f"Incompatible number of channels: {len(self.channels)}. " + f"{cls} expects a sequence on exactly one channel." ) - if len(sequence) != 1: + if len(self) != 1: raise ValueError( - f"Incompatible number of pulses: {len(sequence)}. " - f"{self.__class__} expects a sequence with exactly one pulse." + f"Incompatible number of pulses: {len(self)}. " + f"{cls} expects a sequence with exactly one pulse." ) - pulse = sequence[0][1] + pulse = self[0][1] assert isinstance(pulse, Pulse) 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." + f"{cls} 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. @@ -59,7 +65,7 @@ def create_sequence(self, theta: float = np.pi, phi: float = 0.0) -> PulseSequen phi: the angle that rotation axis forms with x axis. """ theta, phi = _normalize_angles(theta, phi) - ch, pulse = self._seq[0] + ch, pulse = self[0] assert isinstance(pulse, Pulse) new_amplitude = pulse.amplitude * theta / np.pi return PulseSequence( @@ -67,18 +73,29 @@ def create_sequence(self, theta: float = np.pi, phi: float = 0.0) -> PulseSequen ) -class FixedSequenceFactory: +class FixedSequenceFactory(Native): """Simple factory for a fixed arbitrary sequence.""" - def __init__(self, sequence: PulseSequence): - self._seq = sequence - def create_sequence(self) -> PulseSequence: - return self._seq.copy() + return self.copy() + + +class MissingNative(RuntimeError): + """Missing native gate.""" + + def __init__(self, gate: str): + super().__init__(f"Native gate definition not found, for gate {gate}") + + +class NativeContainer(Model): + def ensure(self, name: str) -> Native: + value = getattr(self, name) + if value is None: + raise MissingNative(value) + return value -@dataclass -class SingleQubitNatives: +class SingleQubitNatives(NativeContainer): """Container with the native single-qubit gates acting on a specific qubit.""" @@ -92,26 +109,19 @@ class SingleQubitNatives: """Pulse to activate coupler.""" -@dataclass -class TwoQubitNatives: +class TwoQubitNatives(NativeContainer): """Container with the native two-qubit gates acting on a specific pair of qubits.""" - CZ: Optional[FixedSequenceFactory] = field( - default=None, metadata={"symmetric": True} - ) - CNOT: Optional[FixedSequenceFactory] = field( - default=None, metadata={"symmetric": False} - ) - iSWAP: Optional[FixedSequenceFactory] = field( - default=None, metadata={"symmetric": True} - ) + CZ: Annotated[Optional[FixedSequenceFactory], {"symmetric": True}] = None + CNOT: Annotated[Optional[FixedSequenceFactory], {"symmetric": False}] = None + iSWAP: Annotated[Optional[FixedSequenceFactory], {"symmetric": True}] = None @property def symmetric(self): """Check if the defined two-qubit gates are symmetric between target and control qubits.""" return all( - fld.metadata["symmetric"] or getattr(self, fld.name) is None - for fld in fields(self) + info.metadata[0]["symmetric"] or getattr(self, fld) is None + for fld, info in self.model_fields.items() ) diff --git a/src/qibolab/parameters.py b/src/qibolab/parameters.py new file mode 100644 index 0000000000..288a8f6960 --- /dev/null +++ b/src/qibolab/parameters.py @@ -0,0 +1,108 @@ +"""Helper methods for (de)serializing parameters. + +The format is explained in the :ref:`Using parameters ` +example. +""" + +from collections.abc import Callable +from typing import Any + +from pydantic import Field, TypeAdapter +from pydantic_core import core_schema + +from qibolab.components import Config +from qibolab.execution_parameters import ConfigUpdate, ExecutionParameters +from qibolab.native import SingleQubitNatives, TwoQubitNatives +from qibolab.qubits import QubitId, QubitPairId +from qibolab.serialize import Model, replace + + +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] = replace(configs[name], **changes) + + +class Settings(Model): + """Default platform execution settings.""" + + nshots: int = 1000 + """Default number of repetitions when executing a pulse sequence.""" + relaxation_time: int = int(1e5) + """Time in ns to wait for the qubit to relax to its ground state between + shots.""" + + def fill(self, options: ExecutionParameters): + """Use default values for missing execution options.""" + if options.nshots is None: + options = replace(options, nshots=self.nshots) + + if options.relaxation_time is None: + options = replace(options, relaxation_time=self.relaxation_time) + + return options + + +class TwoQubitContainer(dict[QubitPairId, TwoQubitNatives]): + @classmethod + def __get_pydantic_core_schema__( + cls, source_type: Any, handler: Callable[[Any], core_schema.CoreSchema] + ) -> core_schema.CoreSchema: + schema = handler(dict[QubitPairId, TwoQubitNatives]) + return core_schema.no_info_after_validator_function( + cls._validate, + schema, + serialization=core_schema.plain_serializer_function_ser_schema( + cls._serialize, info_arg=False + ), + ) + + @classmethod + def _validate(cls, value): + return cls(value) + + @staticmethod + def _serialize(value): + return TypeAdapter(dict[QubitPairId, TwoQubitNatives]).dump_python(value) + + def __getitem__(self, key: QubitPairId): + try: + return super().__getitem__(key) + except KeyError: + value = super().__getitem__((key[1], key[0])) + if value.symmetric: + return value + raise + + +class NativeGates(Model): + """Native gates parameters. + + This is a container for the parameters of the whole platform. + """ + + single_qubit: dict[QubitId, SingleQubitNatives] = Field(default_factory=dict) + coupler: dict[QubitId, SingleQubitNatives] = Field(default_factory=dict) + two_qubit: TwoQubitContainer = Field(default_factory=dict) + + +ComponentId = str + + +class Parameters(Model): + """Serializable parameters.""" + + settings: Settings = Field(default_factory=Settings) + configs: dict[ComponentId, Config] = Field(default_factory=dict) + native_gates: NativeGates = Field(default_factory=NativeGates) diff --git a/src/qibolab/platform/load.py b/src/qibolab/platform/load.py index d5b514fb85..c9b95aa587 100644 --- a/src/qibolab/platform/load.py +++ b/src/qibolab/platform/load.py @@ -4,10 +4,9 @@ from qibo.config import raise_error -from qibolab.serialize import PLATFORM - from .platform import Platform +PLATFORM = "platform.py" PLATFORMS = "QIBOLAB_PLATFORMS" diff --git a/src/qibolab/platform/platform.py b/src/qibolab/platform/platform.py index 8e2072b994..e9b82330e7 100644 --- a/src/qibolab/platform/platform.py +++ b/src/qibolab/platform/platform.py @@ -1,28 +1,30 @@ """A platform for executing quantum algorithms.""" -import dataclasses +import json from collections import defaultdict -from dataclasses import asdict, dataclass, field +from collections.abc import Iterable +from dataclasses import dataclass, field from math import prod -from typing import Any, Optional, TypeVar, Union +from pathlib import Path +from typing import Any, Literal, Optional, TypeVar, Union from qibo.config import log, raise_error from qibolab.components import Config -from qibolab.execution_parameters import ConfigUpdate, ExecutionParameters +from qibolab.execution_parameters import ExecutionParameters from qibolab.instruments.abstract import Controller, Instrument, InstrumentId +from qibolab.parameters import NativeGates, Parameters, Settings, update_configs from qibolab.pulses import Delay, PulseSequence -from qibolab.qubits import Qubit, QubitId, QubitPair, QubitPairId -from qibolab.serialize_ import replace +from qibolab.qubits import Qubit, QubitId, QubitPairId from qibolab.sweeper import ParallelSweepers -from qibolab.unrolling import batch +from qibolab.unrolling import Bounds, batch -InstrumentMap = dict[InstrumentId, Instrument] QubitMap = dict[QubitId, Qubit] -CouplerMap = dict[QubitId, Qubit] -QubitPairMap = dict[QubitPairId, QubitPair] +QubitPairMap = list[QubitPairId] +InstrumentMap = dict[InstrumentId, Instrument] NS_TO_SEC = 1e-9 +PARAMETERS = "parameters.json" # TODO: replace with https://docs.python.org/3/reference/compound_stmts.html#type-params T = TypeVar("T") @@ -64,23 +66,6 @@ def unroll_sequences( 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, @@ -98,28 +83,7 @@ def estimate_duration( ) -@dataclass -class Settings: - """Default execution settings read from the runcard.""" - - nshots: int = 1024 - """Default number of repetitions when executing a pulse sequence.""" - relaxation_time: int = int(1e5) - """Time in ns to wait for the qubit to relax to its ground state between - shots.""" - - def fill(self, options: ExecutionParameters): - """Use default values for missing execution options.""" - if options.nshots is None: - options = replace(options, nshots=self.nshots) - - if options.relaxation_time is None: - options = replace(options, relaxation_time=self.relaxation_time) - - return options - - -def _channels_map(elements: Union[QubitMap, CouplerMap]): +def _channels_map(elements: QubitMap): """Map channel names to element (qubit or coupler).""" return {ch.name: id for id, el in elements.items() for ch in el.channels} @@ -130,28 +94,25 @@ class Platform: name: str """Name of the platform.""" - qubits: QubitMap - """Mapping qubit names to :class:`qibolab.qubits.Qubit` objects.""" - pairs: QubitPairMap - """Mapping tuples of qubit names to :class:`qibolab.qubits.QubitPair` - objects.""" - configs: dict[str, Config] - """Mapping name of component to its default config.""" + parameters: Parameters + """...""" instruments: InstrumentMap """Mapping instrument names to :class:`qibolab.instruments.abstract.Instrument` objects.""" + qubits: QubitMap + """Qubit controllers. - settings: Settings = field(default_factory=Settings) - """Container with default execution settings.""" - resonator_type: Optional[str] = None - """Type of resonator (2D or 3D) in the used QPU. - - Default is 3D for single-qubit chips and 2D for multi-qubit. + The mapped objects hold the :class:`qubit.components.channels.Channel` instances + required to send pulses addressing the desired qubits. """ + couplers: QubitMap = field(default_factory=dict) + """Coupler controllers. - couplers: CouplerMap = field(default_factory=dict) - """Mapping coupler names to :class:`qibolab.qubits.Qubit` objects.""" - + Fully analogue to :attr:`qubits`. Only the flux channel is expected to be populated + in the mapped objects. + """ + resonator_type: Literal["2D", "3D"] = "2D" + """Type of resonator (2D or 3D) in the used QPU.""" is_connected: bool = False """Flag for whether we are connected to the physical instruments.""" @@ -168,15 +129,25 @@ def nqubits(self) -> int: """Total number of usable qubits in the QPU.""" return len(self.qubits) + @property + def pairs(self) -> list[QubitPairId]: + """Available pairs in thee platform.""" + return list(self.parameters.native_gates.two_qubit) + @property def ordered_pairs(self): """List of qubit pairs that are connected in the QPU.""" return sorted({tuple(sorted(pair)) for pair in self.pairs}) @property - def topology(self) -> list[QubitPairId]: - """Graph representing the qubit connectivity in the quantum chip.""" - return list(self.pairs) + def settings(self) -> Settings: + """Container with default execution settings.""" + return self.parameters.settings + + @property + def natives(self) -> NativeGates: + """Native gates containers.""" + return self.parameters.native_gates @property def sampling_rate(self): @@ -189,7 +160,7 @@ def sampling_rate(self): @property def components(self) -> set[str]: """Names of all components available in the platform.""" - return set(self.configs.keys()) + return set(self.parameters.configs.keys()) @property def channels(self) -> list[str]: @@ -203,7 +174,8 @@ def channels_map(self) -> dict[str, QubitId]: def config(self, name: str) -> Config: """Returns configuration of given component.""" - return self.configs[name] + # pylint: disable=unsubscriptable-object + return self.parameters.configs[name] def connect(self): """Connect to all instruments.""" @@ -286,7 +258,8 @@ def execute( platform = create_dummy() qubit = platform.qubits[0] - sequence = qubit.native_gates.MZ.create_sequence() + natives = platform.natives.single_qubit[0] + sequence = natives.MZ.create_sequence() parameter = Parameter.frequency parameter_range = np.random.randint(10, size=10) sweeper = [Sweeper(parameter, parameter_range, channels=[qubit.probe.name])] @@ -300,24 +273,59 @@ def execute( time = estimate_duration(sequences, options, sweepers) log.info(f"Minimal execution time: {time}") - configs = self.configs.copy() + configs = self.parameters.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)) + self.instruments[name].setup(**cfg.model_dump(exclude={"kind"})) results = defaultdict(list) - for b in batch(sequences, self._controller.bounds): + # pylint: disable=unsubscriptable-object + bounds = Bounds.from_config(self.parameters.configs[self._controller.bounds]) + for b in batch(sequences, bounds): result = self._execute(b, options, configs, sweepers) for serial, data in result.items(): results[serial].append(data) return results - def get_qubit(self, qubit): + @classmethod + def load( + cls, + path: Path, + instruments: Union[InstrumentMap, Iterable[Instrument]], + qubits: QubitMap, + couplers: Optional[QubitMap] = None, + name: Optional[str] = None, + ) -> "Platform": + """Dump platform.""" + if name is None: + name = path.name + if not isinstance(instruments, dict): + instruments = {i.name: i for i in instruments} + if couplers is None: + couplers = {} + + return cls( + name=name, + parameters=Parameters.model_validate( + json.loads((path / PARAMETERS).read_text()) + ), + instruments=instruments, + qubits=qubits, + couplers=couplers, + ) + + def dump(self, path: Path): + """Dump platform.""" + (path / PARAMETERS).write_text( + json.dumps(self.parameters.model_dump(), sort_keys=False, indent=4) + ) + + def get_qubit(self, qubit: QubitId) -> Qubit: """Return the name of the physical qubit corresponding to a logical qubit. @@ -329,7 +337,7 @@ def get_qubit(self, qubit): except KeyError: return list(self.qubits.values())[qubit] - def get_coupler(self, coupler): + def get_coupler(self, coupler: QubitId) -> Qubit: """Return the name of the physical coupler corresponding to a logical coupler. diff --git a/src/qibolab/pulses/envelope.py b/src/qibolab/pulses/envelope.py index cda3581bec..2a1eff6203 100644 --- a/src/qibolab/pulses/envelope.py +++ b/src/qibolab/pulses/envelope.py @@ -9,7 +9,7 @@ from scipy.signal import lfilter from scipy.signal.windows import gaussian -from qibolab.serialize_ import Model, NdArray, eq +from qibolab.serialize import Model, NdArray, eq __all__ = [ "Waveform", diff --git a/src/qibolab/pulses/pulse.py b/src/qibolab/pulses/pulse.py index 45d7b12f25..e380f6fdaf 100644 --- a/src/qibolab/pulses/pulse.py +++ b/src/qibolab/pulses/pulse.py @@ -4,7 +4,7 @@ import numpy as np -from qibolab.serialize_ import Model +from qibolab.serialize import Model from .envelope import Envelope, IqWaveform, Waveform diff --git a/src/qibolab/pulses/sequence.py b/src/qibolab/pulses/sequence.py index 6ad5b4144a..27122f13df 100644 --- a/src/qibolab/pulses/sequence.py +++ b/src/qibolab/pulses/sequence.py @@ -1,7 +1,11 @@ """PulseSequence class.""" from collections import UserList -from collections.abc import Iterable +from collections.abc import Callable, Iterable +from typing import Any + +from pydantic import TypeAdapter +from pydantic_core import core_schema from qibolab.components import ChannelId @@ -22,6 +26,27 @@ class PulseSequence(UserList[_Element]): the action, and the channel on which it should be performed. """ + @classmethod + def __get_pydantic_core_schema__( + cls, source_type: Any, handler: Callable[[Any], core_schema.CoreSchema] + ) -> core_schema.CoreSchema: + schema = handler(list[_Element]) + return core_schema.no_info_after_validator_function( + cls._validate, + schema, + serialization=core_schema.plain_serializer_function_ser_schema( + cls._serialize, info_arg=False + ), + ) + + @classmethod + def _validate(cls, value): + return cls(value) + + @staticmethod + def _serialize(value): + return TypeAdapter(list[_Element]).dump_python(list(value)) + @property def duration(self) -> float: """Duration of the entire sequence.""" diff --git a/src/qibolab/qubits.py b/src/qibolab/qubits.py index 645a1f3c1a..c604ecf1b6 100644 --- a/src/qibolab/qubits.py +++ b/src/qibolab/qubits.py @@ -1,10 +1,11 @@ -from dataclasses import dataclass, field, fields -from typing import Optional, Union +from typing import Annotated, Optional, Union + +from pydantic import BeforeValidator, ConfigDict, Field, PlainSerializer from qibolab.components import AcquireChannel, DcChannel, IqChannel -from qibolab.native import SingleQubitNatives, TwoQubitNatives +from qibolab.serialize import Model -QubitId = Union[str, int] +QubitId = Annotated[Union[int, str], Field(union_mode="left_to_right")] """Type for qubit names.""" CHANNEL_NAMES = ("probe", "acquisition", "drive", "drive12", "drive_cross", "flux") @@ -14,8 +15,7 @@ """ -@dataclass -class Qubit: +class Qubit(Model): """Representation of a physical qubit. Qubit objects are instantiated by :class:`qibolab.platforms.platform.Platform` @@ -31,9 +31,9 @@ class Qubit: send flux pulses to the qubit. """ - name: QubitId + model_config = ConfigDict(frozen=False) - native_gates: SingleQubitNatives = field(default_factory=SingleQubitNatives) + name: QubitId probe: Optional[IqChannel] = None acquisition: Optional[AcquireChannel] = None @@ -56,38 +56,19 @@ def mixer_frequencies(self): Assumes RF = LO + IF. """ freqs = {} - for gate in fields(self.native_gates): - native = getattr(self.native_gates, gate.name) + for name in self.native_gates.model_fields: + native = getattr(self.native_gates, name) if native is not None: channel_type = native.pulse_type.name.lower() _lo = getattr(self, channel_type).lo_frequency _if = native.frequency - _lo - freqs[gate.name] = _lo, _if + freqs[name] = _lo, _if return freqs -QubitPairId = tuple[QubitId, QubitId] +QubitPairId = Annotated[ + tuple[QubitId, QubitId], + BeforeValidator(lambda p: tuple(p.split("-")) if isinstance(p, str) else p), + PlainSerializer(lambda p: f"{p[0]}-{p[1]}"), +] """Type for holding ``QubitPair``s in the ``platform.pairs`` dictionary.""" - - -@dataclass -class QubitPair: - """Data structure for holding the native two-qubit gates acting on a pair - of qubits. - - This is needed for symmetry to the single-qubit gates which are storred in the - :class:`qibolab.platforms.abstract.Qubit`. - """ - - qubit1: QubitId - """First qubit of the pair. - - Acts as control on two-qubit gates. - """ - qubit2: QubitId - """Second qubit of the pair. - - Acts as target on two-qubit gates. - """ - - native_gates: TwoQubitNatives = field(default_factory=TwoQubitNatives) diff --git a/src/qibolab/serialize.py b/src/qibolab/serialize.py index e72baed208..bab6707e17 100644 --- a/src/qibolab/serialize.py +++ b/src/qibolab/serialize.py @@ -1,293 +1,72 @@ -"""Helper methods for loading and saving to runcards. +"""Serialization utilities.""" -The format of runcards in the ``qiboteam/qibolab_platforms_qrc`` -repository is assumed here. See :ref:`Using runcards ` -example for more details. -""" +import base64 +import io +from typing import Annotated, TypeVar, Union -import json -from dataclasses import asdict, fields -from pathlib import Path -from typing import Optional, Union +import numpy as np +import numpy.typing as npt +from pydantic import BaseModel, ConfigDict, PlainSerializer, PlainValidator -from pydantic import ConfigDict, TypeAdapter -from qibolab.components import AcquisitionConfig -from qibolab.execution_parameters import ConfigUpdate -from qibolab.kernels import Kernels -from qibolab.native import ( - FixedSequenceFactory, - RxyFactory, - SingleQubitNatives, - TwoQubitNatives, -) -from qibolab.platform.platform import ( - InstrumentMap, - Platform, - QubitMap, - QubitPairMap, - Settings, - update_configs, -) -from qibolab.pulses import PulseSequence -from qibolab.pulses.pulse import PulseLike -from qibolab.qubits import Qubit, QubitId, QubitPair, QubitPairId +def ndarray_serialize(ar: npt.NDArray) -> str: + """Serialize array to string.""" + buffer = io.BytesIO() + np.save(buffer, ar) + buffer.seek(0) + return base64.standard_b64encode(buffer.read()).decode() -RUNCARD = "parameters.json" -PLATFORM = "platform.py" +def ndarray_deserialize(x: Union[str, npt.NDArray]) -> npt.NDArray: + """Deserialize array.""" + if isinstance(x, np.ndarray): + return x -def load_runcard(path: Path) -> dict: - """Load runcard JSON to a dictionary.""" - return json.loads((path / RUNCARD).read_text()) + buffer = io.BytesIO() + buffer.write(base64.standard_b64decode(x)) + buffer.seek(0) + return np.load(buffer) -def load_settings(runcard: dict) -> Settings: - """Load platform settings section from the runcard.""" - return Settings(**runcard["settings"]) +NdArray = Annotated[ + npt.NDArray, + PlainValidator(ndarray_deserialize), + PlainSerializer(ndarray_serialize, return_type=str), +] +"""Pydantic-compatible array representation.""" -def load_qubit_name(name: str) -> QubitId: - """Convert qubit name from string to integer or string.""" - try: - return int(name) - except ValueError: - return name +def eq(obj1: BaseModel, obj2: BaseModel) -> bool: + """Compare two models with non-default equality. - -_PulseLike = TypeAdapter(PulseLike, config=ConfigDict(extra="ignore")) -"""Parse a pulse-like object. - -.. note:: - - Extra arguments are ignored, in order to standardize the qubit handling, since the - :cls:`Delay` object has no `qubit` field. - This will be removed once there won't be any need for dedicated couplers handling. -""" - - -def _load_pulse(pulse_kwargs: dict): - return _PulseLike.validate_python(pulse_kwargs) - - -def _load_sequence(raw_sequence): - return PulseSequence([(ch, _load_pulse(pulse)) for ch, pulse in raw_sequence]) - - -def _load_single_qubit_natives(gates: dict) -> dict[QubitId, Qubit]: - """Parse native gates from the runcard. - - Args: - gates (dict): Dictionary with native gate pulse parameters as loaded - from the runcard. - """ - qubits = {} - for q, gatedict in gates.items(): - name = load_qubit_name(q) - native_gates = SingleQubitNatives( - **{ - gate_name: ( - RxyFactory(_load_sequence(raw_sequence)) - if gate_name == "RX" - else FixedSequenceFactory(_load_sequence(raw_sequence)) - ) - for gate_name, raw_sequence in gatedict.items() - } - ) - qubits[name] = Qubit(load_qubit_name(q), native_gates=native_gates) - return qubits - - -def _load_two_qubit_natives( - gates: dict, qubits: dict[QubitId, Qubit] -) -> dict[QubitPairId, QubitPair]: - pairs = {} - for pair, gatedict in gates.items(): - q0, q1 = (load_qubit_name(q) for q in pair.split("-")) - native_gates = TwoQubitNatives( - **{ - gate_name: FixedSequenceFactory(_load_sequence(raw_sequence)) - for gate_name, raw_sequence in gatedict.items() - } - ) - pairs[(q0, q1)] = QubitPair(q0, q1, native_gates=native_gates) - if native_gates.symmetric: - pairs[(q1, q0)] = pairs[(q0, q1)] - return pairs - - -def load_qubits(runcard: dict) -> tuple[QubitMap, QubitMap, QubitPairMap]: - """Load qubits, couplers and pairs from the runcard. - - Uses the native gate section of the runcard to parse the - corresponding :class: `qibolab.qubits.Qubit` and - :class: `qibolab.qubits.QubitPair` objects. - """ - native_gates = runcard.get("native_gates", {}) - qubits = _load_single_qubit_natives(native_gates.get("single_qubit", {})) - couplers = _load_single_qubit_natives(native_gates.get("coupler", {})) - pairs = _load_two_qubit_natives(native_gates.get("two_qubit", {}), qubits) - return qubits, couplers, pairs - - -def load_instrument_settings( - runcard: dict, instruments: InstrumentMap -) -> InstrumentMap: - """Setup instruments according to the settings given in the runcard.""" - for name, settings in runcard.get("instruments", {}).items(): - instruments[name].setup(**settings) - return instruments - - -def dump_qubit_name(name: QubitId) -> str: - """Convert qubit name from integer or string to string.""" - if isinstance(name, int): - return str(name) - return name - - -def _dump_pulse(pulse: PulseLike): - data = pulse.model_dump() - if "channel" in data: - del data["channel"] - if "relative_phase" in data: - del data["relative_phase"] - return data - - -def _dump_sequence(sequence: PulseSequence): - return [(ch, _dump_pulse(p)) for ch, p in sequence] - - -def _dump_natives(natives: Union[SingleQubitNatives, TwoQubitNatives]): - data = {} - for fld in fields(natives): - factory = getattr(natives, fld.name) - if factory is not None: - data[fld.name] = _dump_sequence(factory._seq) - return data - - -def dump_native_gates( - qubits: QubitMap, pairs: QubitPairMap, couplers: Optional[QubitMap] = None -) -> dict: - """Dump native gates section to dictionary following the runcard format, - using qubit and pair objects.""" - # single-qubit native gates - native_gates = { - "single_qubit": { - dump_qubit_name(q): _dump_natives(qubit.native_gates) - for q, qubit in qubits.items() - } - } - - # two-qubit native gates - native_gates["two_qubit"] = {} - for pair in pairs.values(): - natives = _dump_natives(pair.native_gates) - if len(natives) > 0: - pair_name = f"{pair.qubit1}-{pair.qubit2}" - native_gates["two_qubit"][pair_name] = natives - - return native_gates - - -def dump_instruments(instruments: InstrumentMap) -> dict: - """Dump instrument settings to a dictionary following the runcard - format.""" - # Qblox modules settings are dictionaries and not dataclasses - data = {} - for name, instrument in instruments.items(): - try: - # TODO: Migrate all instruments to this approach - # (I think it is also useful for qblox) - settings = instrument.dump() - if len(settings) > 0: - data[name] = settings - except AttributeError: - settings = instrument.settings - if settings is not None: - if isinstance(settings, dict): - data[name] = settings - else: - data[name] = settings.dump() - - return data - - -def dump_component_configs(component_configs) -> dict: - """Dump channel configs.""" - components = {} - for name, cfg in component_configs.items(): - components[name] = asdict(cfg) - if isinstance(cfg, AcquisitionConfig): - del components[name]["kernel"] - return components - - -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 `. - - 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). + Currently, defines custom equality for NumPy arrays. """ + obj2d = obj2.model_dump() + comparisons = [] + for field, value1 in obj1.model_dump().items(): + value2 = obj2d[field] + if isinstance(value1, np.ndarray): + comparisons.append( + (value1.shape == value2.shape) and (value1 == value2).all() + ) - configs = platform.configs.copy() - update_configs(configs, updates or []) - - settings = { - "nqubits": platform.nqubits, - "settings": asdict(platform.settings), - "qubits": list(platform.qubits), - "instruments": dump_instruments(platform.instruments), - "components": dump_component_configs(configs), - } + comparisons.append(value1 == value2) - settings["native_gates"] = dump_native_gates( - platform.qubits, platform.pairs, platform.couplers - ) + return all(comparisons) - (path / RUNCARD).write_text(json.dumps(settings, sort_keys=False, indent=4)) +class Model(BaseModel): + """Global qibolab model, holding common configurations.""" -def dump_kernels(platform: Platform, path: Path): - """Creates Kernels instance from platform and dumps as npz. + model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid", frozen=True) - Args: - platform (qibolab.platform.Platform): The platform to be serialized. - path (pathlib.Path): Path that the kernels file will be saved. - """ - # create kernels - kernels = Kernels() - for qubit in platform.qubits.values(): - kernel = platform.configs[qubit.acquisition.name].kernel - if kernel is not None: - kernels[qubit.name] = kernel +M = TypeVar("M", bound=BaseModel) - # dump only if not None - if len(kernels) > 0: - kernels.dump(path) +def replace(model: M, **update) -> M: + """Replace interface for pydantic models. -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). + To have the same familiar syntax of :func:`dataclasses.replace`. """ - - dump_kernels(platform=platform, path=path) - dump_runcard(platform=platform, path=path, updates=updates) + return model.model_copy(update=update) diff --git a/src/qibolab/serialize_.py b/src/qibolab/serialize_.py deleted file mode 100644 index eb8cceb0ab..0000000000 --- a/src/qibolab/serialize_.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Serialization utilities.""" - -import base64 -import io -from typing import Annotated, TypeVar, Union - -import numpy as np -import numpy.typing as npt -from pydantic import BaseModel, ConfigDict, PlainSerializer, PlainValidator - - -def ndarray_serialize(ar: npt.NDArray) -> str: - """Serialize array to string.""" - buffer = io.BytesIO() - np.save(buffer, ar) - buffer.seek(0) - return base64.standard_b64encode(buffer.read()).decode() - - -def ndarray_deserialize(x: Union[str, npt.NDArray]) -> npt.NDArray: - """Deserialize array.""" - if isinstance(x, np.ndarray): - return x - - buffer = io.BytesIO() - buffer.write(base64.standard_b64decode(x)) - buffer.seek(0) - return np.load(buffer) - - -NdArray = Annotated[ - npt.NDArray, - PlainValidator(ndarray_deserialize), - PlainSerializer(ndarray_serialize, return_type=str), -] -"""Pydantic-compatible array representation.""" - - -def eq(obj1: BaseModel, obj2: BaseModel) -> bool: - """Compare two models with non-default equality. - - Currently, defines custom equality for NumPy arrays. - """ - obj2d = obj2.model_dump() - comparisons = [] - for field, value1 in obj1.model_dump().items(): - value2 = obj2d[field] - if isinstance(value1, np.ndarray): - comparisons.append( - (value1.shape == value2.shape) and (value1 == value2).all() - ) - - comparisons.append(value1 == value2) - - return all(comparisons) - - -class Model(BaseModel): - """Global qibolab model, holding common configurations.""" - - model_config = ConfigDict(arbitrary_types_allowed=True, frozen=True) - - -M = TypeVar("M", bound=BaseModel) - - -def replace(model: M, **update) -> M: - """Replace interface for pydantic models. - - To have the same familiar syntax of :func:`dataclasses.replace`. - """ - return model.model_copy(update=update) diff --git a/src/qibolab/sweeper.py b/src/qibolab/sweeper.py index 91107aef6c..4db5b2b6c2 100644 --- a/src/qibolab/sweeper.py +++ b/src/qibolab/sweeper.py @@ -66,7 +66,8 @@ class Sweeper: platform = create_dummy() qubit = platform.qubits[0] - sequence = qubit.native_gates.MZ.create_sequence() + natives = platform.natives.single_qubit[0] + sequence = natives.MZ.create_sequence() parameter = Parameter.frequency parameter_range = np.random.randint(10, size=10) sweeper = Sweeper(parameter, parameter_range, channels=[qubit.probe.name]) diff --git a/src/qibolab/unrolling.py b/src/qibolab/unrolling.py index c8eb888f80..56d33bad4e 100644 --- a/src/qibolab/unrolling.py +++ b/src/qibolab/unrolling.py @@ -3,8 +3,11 @@ May be reused by different instruments. """ -from dataclasses import asdict, dataclass, field, fields from functools import total_ordering +from typing import Annotated + +from qibolab.components.configs import BoundsConfig +from qibolab.serialize import Model from .pulses import Pulse, PulseSequence from .pulses.envelope import Rectangular @@ -35,36 +38,43 @@ def _instructions(sequence: PulseSequence): @total_ordering -@dataclass(frozen=True, eq=True) -class Bounds: +class Bounds(Model): """Instument memory limitations proxies.""" - waveforms: int = field(metadata={"count": _waveform}) + waveforms: Annotated[int, {"count": _waveform}] """Waveforms estimated size.""" - readout: int = field(metadata={"count": _readout}) + readout: Annotated[int, {"count": _readout}] """Number of readouts.""" - instructions: int = field(metadata={"count": _instructions}) + instructions: Annotated[int, {"count": _instructions}] """Instructions estimated size.""" + @classmethod + def from_config(cls, config: BoundsConfig): + d = config.model_dump() + del d["kind"] + return cls(**d) + @classmethod def update(cls, sequence: PulseSequence): up = {} - for f in fields(cls): - up[f.name] = f.metadata["count"](sequence) + for name, info in cls.model_fields.items(): + up[name] = info.metadata[0]["count"](sequence) return cls(**up) def __add__(self, other: "Bounds") -> "Bounds": """Sum bounds element by element.""" new = {} - for (k, x), (_, y) in zip(asdict(self).items(), asdict(other).items()): + for (k, x), (_, y) in zip( + self.model_dump().items(), other.model_dump().items() + ): new[k] = x + y return type(self)(**new) def __gt__(self, other: "Bounds") -> bool: """Define ordering as exceeding any bound.""" - return any(getattr(self, f.name) > getattr(other, f.name) for f in fields(self)) + return any(getattr(self, f) > getattr(other, f) for f in self.model_fields) def batch(sequences: list[PulseSequence], bounds: Bounds): @@ -73,7 +83,7 @@ def batch(sequences: list[PulseSequence], bounds: Bounds): Takes into account the various limitations throught the mechanics defined in :cls:`Bounds`, and the numerical limitations specified by the `bounds` argument. """ - counters = Bounds(0, 0, 0) + counters = Bounds(waveforms=0, readout=0, instructions=0) batch = [] for sequence in sequences: update = Bounds.update(sequence) diff --git a/tests/conftest.py b/tests/conftest.py index 81d031c540..2fb51d3079 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,7 +25,7 @@ # "qm_octave", # "qblox", # "rfsoc", - "zurich", + # "zurich", ] """Platforms used for testing without access to real instruments.""" @@ -149,10 +149,11 @@ def wrapped( ) qubit = next(iter(connected_platform.qubits.values())) + natives = connected_platform.natives.single_qubit[0] if sequence is None: - qd_seq = qubit.native_gates.RX.create_sequence() - probe_seq = qubit.native_gates.MZ.create_sequence() + qd_seq = natives.RX.create_sequence() + probe_seq = natives.MZ.create_sequence() probe_pulse = probe_seq[0][1] sequence = PulseSequence() sequence.concatenate(qd_seq) diff --git a/tests/dummy_qrc/qblox/platform.py b/tests/dummy_qrc/qblox/platform.py index 10aa6c0360..1e70f50c4f 100644 --- a/tests/dummy_qrc/qblox/platform.py +++ b/tests/dummy_qrc/qblox/platform.py @@ -7,12 +7,6 @@ from qibolab.instruments.qblox.controller import QbloxController from qibolab.instruments.rohde_schwarz import SGS100A from qibolab.platform import Platform -from qibolab.serialize import ( - load_instrument_settings, - load_qubits, - load_runcard, - load_settings, -) ADDRESS = "192.168.0.6" TIME_OF_FLIGHT = 500 @@ -45,7 +39,6 @@ def create(): twpa_pump.name: twpa_pump, } instruments.update(modules) - instruments = load_instrument_settings(runcard, instruments) # Create channel objects channels = ChannelMap() diff --git a/tests/dummy_qrc/qm/platform.py b/tests/dummy_qrc/qm/platform.py index d0a4311433..2e715e7660 100644 --- a/tests/dummy_qrc/qm/platform.py +++ b/tests/dummy_qrc/qm/platform.py @@ -4,12 +4,6 @@ from qibolab.instruments.dummy import DummyLocalOscillator as LocalOscillator from qibolab.instruments.qm import OPXplus, QMController from qibolab.platform import Platform -from qibolab.serialize import ( - load_instrument_settings, - load_qubits, - load_runcard, - load_settings, -) FOLDER = pathlib.Path(__file__).parent @@ -91,5 +85,4 @@ def create(): instruments.update(controller.opxs) instruments.update({lo.name: lo for lo in local_oscillators}) settings = load_settings(runcard) - instruments = load_instrument_settings(runcard, instruments) return Platform("qm", qubits, pairs, instruments, settings, resonator_type="2D") diff --git a/tests/dummy_qrc/qm_octave/platform.py b/tests/dummy_qrc/qm_octave/platform.py index ac11a5fe8a..296381c807 100644 --- a/tests/dummy_qrc/qm_octave/platform.py +++ b/tests/dummy_qrc/qm_octave/platform.py @@ -4,12 +4,6 @@ from qibolab.instruments.dummy import DummyLocalOscillator as LocalOscillator from qibolab.instruments.qm import Octave, OPXplus, QMController from qibolab.platform import Platform -from qibolab.serialize import ( - load_instrument_settings, - load_qubits, - load_runcard, - load_settings, -) RUNCARD = pathlib.Path(__file__).parent @@ -76,7 +70,6 @@ def create(runcard_path=RUNCARD): instruments.update(controller.opxs) instruments.update(controller.octaves) settings = load_settings(runcard) - instruments = load_instrument_settings(runcard, instruments) return Platform( "qm_octave", qubits, pairs, instruments, settings, resonator_type="2D" ) diff --git a/tests/dummy_qrc/rfsoc/platform.py b/tests/dummy_qrc/rfsoc/platform.py index ef6f5da36c..b2311f6cac 100644 --- a/tests/dummy_qrc/rfsoc/platform.py +++ b/tests/dummy_qrc/rfsoc/platform.py @@ -5,12 +5,6 @@ from qibolab.instruments.rfsoc import RFSoC from qibolab.instruments.rohde_schwarz import SGS100A from qibolab.platform import Platform -from qibolab.serialize import ( - load_instrument_settings, - load_qubits, - load_runcard, - load_settings, -) FOLDER = pathlib.Path(__file__).parent @@ -45,7 +39,6 @@ def create(): instruments = {inst.name: inst for inst in [controller, lo_twpa, lo_era]} settings = load_settings(runcard) - instruments = load_instrument_settings(runcard, instruments) return Platform( str(FOLDER), qubits, pairs, instruments, settings, resonator_type="3D" ) diff --git a/tests/dummy_qrc/zurich/parameters.json b/tests/dummy_qrc/zurich/parameters.json index 5bcd3dec7f..ccf64499e9 100644 --- a/tests/dummy_qrc/zurich/parameters.json +++ b/tests/dummy_qrc/zurich/parameters.json @@ -15,119 +15,157 @@ }, "components": { "qubit_0/drive": { + "kind": "iq", "frequency": 4000000000, "power_range": 5 }, "qubit_1/drive": { + "kind": "iq", "frequency": 4200000000, "power_range": 0 }, "qubit_2/drive": { + "kind": "iq", "frequency": 4500000000, "power_range": -5 }, "qubit_3/drive": { + "kind": "iq", "frequency": 4150000000, "power_range": -10 }, "qubit_4/drive": { + "kind": "iq", "frequency": 4155663000, "power_range": 5 }, "qubit_0/flux": { + "kind": "dc", "offset": -0.1, "power_range": 0.2 }, "qubit_1/flux": { + "kind": "dc", "offset": 0.0, "power_range": 0.6 }, "qubit_2/flux": { + "kind": "dc", "offset": 0.1, "power_range": 0.4 }, "qubit_3/flux": { + "kind": "dc", "offset": 0.2, "power_range": 1 }, "qubit_4/flux": { + "kind": "dc", "offset": 0.15, "power_range": 5 }, "qubit_0/probe": { + "kind": "iq", "frequency": 5200000000, "power_range": -10 }, "qubit_1/probe": { + "kind": "iq", "frequency": 4900000000, "power_range": -10 }, "qubit_2/probe": { + "kind": "iq", "frequency": 6100000000, "power_range": -10 }, "qubit_3/probe": { + "kind": "iq", "frequency": 5800000000, "power_range": -10 }, "qubit_4/probe": { + "kind": "iq", "frequency": 5500000000, "power_range": -10 }, "qubit_0/acquire": { + "kind": "acquisition", "delay": 0, + "iq_angle": null, "smearing": 0, - "power_range": 10 + "power_range": 10, + "threshold": null }, "qubit_1/acquire": { + "kind": "acquisition", "delay": 0, + "iq_angle": null, "smearing": 0, - "power_range": 10 + "power_range": 10, + "threshold": null }, "qubit_2/acquire": { + "kind": "acquisition", "delay": 0, + "iq_angle": null, "smearing": 0, - "power_range": 10 + "power_range": 10, + "threshold": null }, "qubit_3/acquire": { + "kind": "acquisition", "delay": 0, + "iq_angle": null, "smearing": 0, - "power_range": 10 + "power_range": 10, + "threshold": null }, "qubit_4/acquire": { + "kind": "acquisition", "delay": 0, + "iq_angle": null, "smearing": 0, - "power_range": 10 + "power_range": 10, + "threshold": null }, "coupler_0/flux": { + "kind": "dc", "offset": 0.0, "power_range": 3 }, "coupler_1/flux": { + "kind": "dc", "offset": 0.0, "power_range": 1 }, "coupler_3/flux": { + "kind": "dc", "offset": 0.0, "power_range": 0.4 }, "coupler_4/flux": { + "kind": "dc", "offset": 0.0, "power_range": 0.4 }, "readout/lo": { + "kind": "oscillator", "power": 10, "frequency": 6000000000.0 }, "qubit_0_1/drive/lo": { + "kind": "oscillator", "power": 10, "frequency": 3000000000.0 }, "qubit_2_3/drive/lo": { + "kind": "oscillator", "power": 10, "frequency": 3500000000.0 }, "qubit_4/drive/lo": { + "kind": "oscillator", "power": 10, "frequency": 4000000000.0 } @@ -273,7 +311,8 @@ "coupler": { "0": { "CP": [ - ["coupler_0/flux", + [ + "coupler_0/flux", { "duration": 30.0, "amplitude": 0.05, @@ -288,7 +327,8 @@ }, "1": { "CP": [ - ["coupler_1/flux", + [ + "coupler_1/flux", { "duration": 30.0, "amplitude": 0.05, @@ -303,7 +343,8 @@ }, "3": { "CP": [ - ["coupler_3/flux", + [ + "coupler_3/flux", { "duration": 30.0, "amplitude": 0.05, @@ -313,12 +354,13 @@ "width": 0.75 } } - ] ] + ] }, "4": { "CP": [ - ["coupler_4/flux", + [ + "coupler_4/flux", { "duration": 30.0, "amplitude": 0.05, diff --git a/tests/dummy_qrc/zurich/platform.py b/tests/dummy_qrc/zurich/platform.py index a98c6859b1..f391e66634 100644 --- a/tests/dummy_qrc/zurich/platform.py +++ b/tests/dummy_qrc/zurich/platform.py @@ -14,12 +14,7 @@ Zurich, ) from qibolab.kernels import Kernels -from qibolab.serialize import ( - load_instrument_settings, - load_qubits, - load_runcard, - load_settings, -) +from qibolab.parameters import Parameters FOLDER = pathlib.Path(__file__).parent @@ -43,13 +38,15 @@ def create(): create_connection(to_instrument="device_shfqc", ports="ZSYNCS/2"), ) - runcard = load_runcard(FOLDER) + parameters = Parameters.load(FOLDER) kernels = Kernels.load(FOLDER) - qubits, couplers, pairs = load_qubits(runcard) - settings = load_settings(runcard) + qubits, couplers, pairs = ( + parameters.native_gates.single_qubit, + parameters.native_gates.coupler, + parameters.native_gates.two_qubit, + ) - configs = {} - component_params = runcard["components"] + configs = parameters.configs readout_lo = "readout/lo" drive_los = { 0: "qubit_0_1/drive/lo", @@ -126,15 +123,10 @@ def create(): smearing=50, ) - instruments = {controller.name: controller} - instruments = load_instrument_settings(runcard, instruments) return Platform( - str(FOLDER), - qubits, - pairs, - configs, - instruments, - settings, + name=str(FOLDER), + configs=configs, + parameters=parameters, + instruments={controller.name: controller}, resonator_type="3D", - couplers=couplers, ) diff --git a/tests/pulses/test_envelope.py b/tests/pulses/test_envelope.py index 72ca1d08fa..c29299e98d 100644 --- a/tests/pulses/test_envelope.py +++ b/tests/pulses/test_envelope.py @@ -26,7 +26,6 @@ def test_sampling_rate(shape): pulse = Pulse( duration=40, amplitude=0.9, - frequency=int(100e6), envelope=shape, relative_phase=0, ) @@ -38,7 +37,6 @@ def test_drag_shape(): pulse = Pulse( duration=2, amplitude=1, - frequency=int(4e9), envelope=Drag(rel_sigma=0.5, beta=1), relative_phase=0, ) @@ -75,11 +73,8 @@ def test_rectangular(): pulse = Pulse( duration=50, amplitude=1, - frequency=200_000_000, relative_phase=0, envelope=Rectangular(), - channel="1", - qubit=0, ) assert pulse.duration == 50 @@ -100,11 +95,8 @@ def test_gaussian(): pulse = Pulse( duration=50, amplitude=1, - frequency=200_000_000, relative_phase=0, envelope=Gaussian(rel_sigma=5), - channel="1", - qubit=0, ) assert pulse.duration == 50 @@ -130,10 +122,8 @@ def test_drag(): pulse = Pulse( duration=50, amplitude=1, - frequency=200_000_000, relative_phase=0, envelope=Drag(rel_sigma=0.2, beta=0.2), - qubit=0, ) assert pulse.duration == 50 diff --git a/tests/test_backends.py b/tests/test_backends.py index c7d7b90b3a..48e82905d5 100644 --- a/tests/test_backends.py +++ b/tests/test_backends.py @@ -106,12 +106,22 @@ def test_multiple_measurements(): def dummy_string_qubit_names(): """Create dummy platform with string-named qubits.""" platform = create_platform("dummy") - for q, qubit in platform.qubits.items(): - qubit.name = f"A{q}" - platform.qubits = {qubit.name: qubit for qubit in platform.qubits.values()} - platform.pairs = { - (f"A{q0}", f"A{q1}"): pair for (q0, q1), pair in platform.pairs.items() - } + for q, qubit in platform.qubits.copy().items(): + name = f"A{q}" + qubit.name = name + platform.qubits[name] = qubit + del platform.qubits[q] + platform.natives.single_qubit[name] = platform.natives.single_qubit[q] + del platform.natives.single_qubit[q] + for q0, q1 in platform.pairs: + name = (f"A{q0}", f"A{q1}") + try: + platform.natives.two_qubit[name] = platform.natives.two_qubit[(q0, q1)] + del platform.natives.two_qubit[(q0, q1)] + except KeyError: + # the symmetrized pair is only present in pairs, not in the natives + pass + return platform diff --git a/tests/test_compilers_default.py b/tests/test_compilers_default.py index 5bd41393ed..0a9e0e106d 100644 --- a/tests/test_compilers_default.py +++ b/tests/test_compilers_default.py @@ -6,6 +6,7 @@ from qibolab import create_platform from qibolab.compilers import Compiler +from qibolab.platform import Platform from qibolab.pulses import Delay, PulseSequence @@ -86,26 +87,28 @@ def test_rz_to_sequence(platform): assert len(sequence) == 2 -def test_gpi_to_sequence(platform): +def test_gpi_to_sequence(platform: Platform): + natives = platform.natives + circuit = Circuit(1) circuit.add(gates.GPI(0, phi=0.2)) sequence = compile_circuit(circuit, platform) assert len(sequence.channels) == 1 - rx_seq = platform.qubits[0].native_gates.RX.create_sequence(phi=0.2) + rx_seq = natives.single_qubit[0].RX.create_sequence(phi=0.2) np.testing.assert_allclose(sequence.duration, rx_seq.duration) def test_gpi2_to_sequence(platform): + natives = platform.natives + circuit = Circuit(1) circuit.add(gates.GPI2(0, phi=0.2)) sequence = compile_circuit(circuit, platform) assert len(sequence.channels) == 1 - rx90_seq = platform.qubits[0].native_gates.RX.create_sequence( - theta=np.pi / 2, phi=0.2 - ) + rx90_seq = natives.single_qubit[0].RX.create_sequence(theta=np.pi / 2, phi=0.2) np.testing.assert_allclose(sequence.duration, rx90_seq.duration) assert sequence == rx90_seq @@ -113,25 +116,31 @@ def test_gpi2_to_sequence(platform): def test_cz_to_sequence(): platform = create_platform("dummy") + natives = platform.natives + circuit = Circuit(3) circuit.add(gates.CZ(1, 2)) sequence = compile_circuit(circuit, platform) - test_sequence = platform.pairs[(2, 1)].native_gates.CZ.create_sequence() + test_sequence = natives.two_qubit[(2, 1)].CZ.create_sequence() assert sequence == test_sequence def test_cnot_to_sequence(): platform = create_platform("dummy") + natives = platform.natives + circuit = Circuit(4) circuit.add(gates.CNOT(2, 3)) sequence = compile_circuit(circuit, platform) - test_sequence = platform.pairs[(2, 3)].native_gates.CNOT.create_sequence() + test_sequence = natives.two_qubit[(2, 3)].CNOT.create_sequence() assert sequence == test_sequence -def test_add_measurement_to_sequence(platform): +def test_add_measurement_to_sequence(platform: Platform): + natives = platform.natives + circuit = Circuit(1) circuit.add(gates.GPI2(0, 0.1)) circuit.add(gates.GPI2(0, 0.2)) @@ -144,16 +153,18 @@ def test_add_measurement_to_sequence(platform): assert len(list(sequence.channel(qubit.probe.name))) == 2 # include delay s = PulseSequence() - s.concatenate(qubit.native_gates.RX.create_sequence(theta=np.pi / 2, phi=0.1)) - s.concatenate(qubit.native_gates.RX.create_sequence(theta=np.pi / 2, phi=0.2)) + s.concatenate(natives.single_qubit[0].RX.create_sequence(theta=np.pi / 2, phi=0.1)) + s.concatenate(natives.single_qubit[0].RX.create_sequence(theta=np.pi / 2, phi=0.2)) s.append((qubit.probe.name, Delay(duration=s.duration))) - s.concatenate(qubit.native_gates.MZ.create_sequence()) + s.concatenate(natives.single_qubit[0].MZ.create_sequence()) assert sequence == s @pytest.mark.parametrize("delay", [0, 100]) -def test_align_delay_measurement(platform, delay): +def test_align_delay_measurement(platform: Platform, delay): + natives = platform.natives + circuit = Circuit(1) circuit.add(gates.Align(0, delay=delay)) circuit.add(gates.M(0)) @@ -162,12 +173,12 @@ def test_align_delay_measurement(platform, delay): target_sequence = PulseSequence() if delay > 0: target_sequence.append((platform.qubits[0].probe.name, Delay(duration=delay))) - target_sequence.concatenate(platform.qubits[0].native_gates.MZ.create_sequence()) + target_sequence.concatenate(natives.single_qubit[0].MZ.create_sequence()) assert sequence == target_sequence assert len(sequence.probe_pulses) == 1 -def test_align_multiqubit(platform): +def test_align_multiqubit(platform: Platform): main, coupled = 0, 2 circuit = Circuit(3) circuit.add(gates.GPI2(main, phi=0.2)) diff --git a/tests/test_dummy.py b/tests/test_dummy.py index 1a3b5265cc..7aa8782fdd 100644 --- a/tests/test_dummy.py +++ b/tests/test_dummy.py @@ -24,11 +24,12 @@ def test_dummy_initialization(platform: Platform): ) def test_dummy_execute_pulse_sequence(platform: Platform, acquisition): nshots = 100 - probe_seq = platform.qubits[0].native_gates.MZ.create_sequence() + natives = platform.natives.single_qubit[0] + probe_seq = natives.MZ.create_sequence() probe_pulse = probe_seq[0][1] sequence = PulseSequence() sequence.concatenate(probe_seq) - sequence.concatenate(platform.qubits[0].native_gates.RX12.create_sequence()) + sequence.concatenate(natives.RX12.create_sequence()) options = ExecutionParameters(nshots=100, acquisition_type=acquisition) result = platform.execute([sequence], options) if acquisition is AcquisitionType.INTEGRATION: @@ -56,20 +57,22 @@ def test_dummy_execute_pulse_sequence_couplers(): platform = create_platform("dummy") sequence = PulseSequence() - cz = platform.pairs[(1, 2)].native_gates.CZ.create_sequence() + natives = platform.natives + cz = natives.two_qubit[(1, 2)].CZ.create_sequence() sequence.concatenate(cz) sequence.append((platform.qubits[0].probe.name, Delay(duration=40))) sequence.append((platform.qubits[2].probe.name, Delay(duration=40))) - sequence.concatenate(platform.qubits[0].native_gates.MZ.create_sequence()) - sequence.concatenate(platform.qubits[2].native_gates.MZ.create_sequence()) + sequence.concatenate(natives.single_qubit[0].MZ.create_sequence()) + sequence.concatenate(natives.single_qubit[2].MZ.create_sequence()) options = ExecutionParameters(nshots=None) _ = platform.execute([sequence], options) def test_dummy_execute_pulse_sequence_fast_reset(platform: Platform): + natives = platform.natives sequence = PulseSequence() - sequence.concatenate(platform.qubits[0].native_gates.MZ.create_sequence()) + sequence.concatenate(natives.single_qubit[0].MZ.create_sequence()) options = ExecutionParameters(nshots=None, fast_reset=True) _ = platform.execute([sequence], options) @@ -84,9 +87,10 @@ def test_dummy_execute_pulse_sequence_unrolling( nshots = 100 nsequences = 10 platform.instruments["dummy"].UNROLLING_BATCH_SIZE = batch_size + natives = platform.natives sequences = [] sequence = PulseSequence() - sequence.concatenate(platform.qubits[0].native_gates.MZ.create_sequence()) + sequence.concatenate(natives.single_qubit[0].MZ.create_sequence()) for _ in range(nsequences): sequences.append(sequence) options = ExecutionParameters(nshots=nshots, acquisition_type=acquisition) @@ -101,7 +105,8 @@ def test_dummy_execute_pulse_sequence_unrolling( def test_dummy_single_sweep_raw(platform: Platform): sequence = PulseSequence() - probe_seq = platform.qubits[0].native_gates.MZ.create_sequence() + natives = platform.natives + probe_seq = natives.single_qubit[0].MZ.create_sequence() pulse = probe_seq[0][1] parameter_range = np.random.randint(SWEPT_POINTS, size=SWEPT_POINTS) @@ -136,7 +141,8 @@ def test_dummy_single_sweep_coupler( ): platform = create_platform("dummy") sequence = PulseSequence() - probe_seq = platform.qubits[0].native_gates.MZ.create_sequence() + natives = platform.natives + probe_seq = natives.single_qubit[0].MZ.create_sequence() probe_pulse = probe_seq[0][1] coupler_pulse = Pulse.flux( duration=40, @@ -196,7 +202,8 @@ def test_dummy_single_sweep( platform: Platform, fast_reset, parameter, average, acquisition, nshots ): sequence = PulseSequence() - probe_seq = platform.qubits[0].native_gates.MZ.create_sequence() + natives = platform.natives + probe_seq = natives.single_qubit[0].MZ.create_sequence() pulse = probe_seq[0][1] if parameter is Parameter.amplitude: parameter_range = np.random.rand(SWEPT_POINTS) @@ -254,7 +261,8 @@ def test_dummy_double_sweep( ): sequence = PulseSequence() pulse = Pulse(duration=40, amplitude=0.1, envelope=Gaussian(rel_sigma=5)) - probe_seq = platform.qubits[0].native_gates.MZ.create_sequence() + natives = platform.natives + probe_seq = natives.single_qubit[0].MZ.create_sequence() probe_pulse = probe_seq[0][1] sequence.append((platform.get_qubit(0).drive.name, pulse)) sequence.append((platform.qubits[0].probe.name, Delay(duration=pulse.duration))) @@ -327,8 +335,9 @@ def test_dummy_single_sweep_multiplex( ): sequence = PulseSequence() probe_pulses = {} + natives = platform.natives for qubit in platform.qubits: - probe_seq = platform.qubits[qubit].native_gates.MZ.create_sequence() + probe_seq = natives.single_qubit[qubit].MZ.create_sequence() probe_pulses[qubit] = probe_seq[0][1] sequence.concatenate(probe_seq) parameter_range = ( diff --git a/tests/test_instruments_zhinst.py b/tests/test_instruments_zhinst.py index 9dc5536e9b..002d39a0af 100644 --- a/tests/test_instruments_zhinst.py +++ b/tests/test_instruments_zhinst.py @@ -28,58 +28,40 @@ Pulse( duration=40, amplitude=0.05, - frequency=int(3e9), relative_phase=0.0, envelope=Rectangular(), - channel="ch0", - qubit=0, ), Pulse( duration=40, amplitude=0.05, - frequency=int(3e9), relative_phase=0.0, envelope=Gaussian(rel_sigma=5), - channel="ch0", - qubit=0, ), Pulse( duration=40, amplitude=0.05, - frequency=int(3e9), relative_phase=0.0, envelope=Gaussian(rel_sigma=5), - channel="ch0", - qubit=0, ), Pulse( duration=40, amplitude=0.05, - frequency=int(3e9), relative_phase=0.0, envelope=Drag(rel_sigma=5, beta=0.4), - channel="ch0", - qubit=0, ), Pulse( duration=40, amplitude=0.05, - frequency=int(3e9), relative_phase=0.0, envelope=Snz(t_idling=10, b_amplitude=0.01), - channel="ch0", - qubit=0, ), Pulse( duration=40, amplitude=0.05, - frequency=int(3e9), relative_phase=0.0, envelope=Iir( a=np.array([10, 1]), b=np.array([0.4, 1]), target=Gaussian(rel_sigma=5) ), - channel="ch0", - qubit=0, ), ], ) @@ -100,13 +82,11 @@ def test_classify_sweepers(dummy_qrc): duration=40, amplitude=0.05, envelope=Gaussian(rel_sigma=5), - type=PulseType.DRIVE, ) pulse_2 = Pulse( 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( @@ -133,13 +113,11 @@ def test_processed_sweeps_pulse_properties(dummy_qrc): duration=40, amplitude=0.05, envelope=Gaussian(rel_sigma=5), - type=PulseType.DRIVE, ) pulse_2 = Pulse( 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] @@ -323,7 +301,6 @@ def test_experiment_flow_coupler(dummy_qrc): duration=500, amplitude=1, envelope=Rectangular(), - type=PulseType.COUPLERFLUX, ) ) @@ -455,7 +432,6 @@ def test_experiment_sweep_single_coupler(dummy_qrc, parameter1): duration=500, amplitude=1, envelope=Rectangular(), - type=PulseType.COUPLERFLUX, ) ) diff --git a/tests/test_native.py b/tests/test_native.py index 160b24d24f..b852f16ef1 100644 --- a/tests/test_native.py +++ b/tests/test_native.py @@ -2,6 +2,7 @@ import numpy as np import pytest +from pydantic import TypeAdapter from qibolab.native import FixedSequenceFactory, RxyFactory, TwoQubitNatives from qibolab.pulses import ( @@ -141,7 +142,7 @@ def test_rxy_rotation_factory_envelopes(envelope): context = pytest.raises(ValueError, match="Incompatible pulse envelope") with context: - _ = RxyFactory(seq) + _ = TypeAdapter(RxyFactory).validate_python(seq) def test_two_qubit_natives_symmetric(): diff --git a/tests/test_platform.py b/tests/test_platform.py index dd216c7033..7e30fc0878 100644 --- a/tests/test_platform.py +++ b/tests/test_platform.py @@ -5,7 +5,6 @@ import os import pathlib import warnings -from dataclasses import replace from pathlib import Path import numpy as np @@ -20,32 +19,26 @@ from qibolab.dummy.platform import FOLDER from qibolab.execution_parameters import ExecutionParameters from qibolab.instruments.qblox.controller import QbloxController -from qibolab.kernels import Kernels +from qibolab.native import SingleQubitNatives, TwoQubitNatives +from qibolab.parameters import NativeGates, Parameters, update_configs from qibolab.platform import Platform, unroll_sequences -from qibolab.platform.load import PLATFORMS -from qibolab.platform.platform import update_configs +from qibolab.platform.load import PLATFORM, PLATFORMS +from qibolab.platform.platform import PARAMETERS from qibolab.pulses import Delay, Gaussian, Pulse, PulseSequence, Rectangular -from qibolab.qubits import Qubit, QubitPair -from qibolab.serialize import ( - PLATFORM, - dump_kernels, - dump_platform, - dump_runcard, - load_runcard, - load_settings, -) +from qibolab.serialize import replace from .conftest import find_instrument nshots = 1024 -def test_unroll_sequences(platform): +def test_unroll_sequences(platform: Platform): qubit = next(iter(platform.qubits.values())) + natives = platform.natives.single_qubit[0] sequence = PulseSequence() - sequence.concatenate(qubit.native_gates.RX.create_sequence()) + sequence.concatenate(natives.RX.create_sequence()) sequence.append((qubit.probe.name, Delay(duration=sequence.duration))) - sequence.concatenate(qubit.native_gates.MZ.create_sequence()) + sequence.concatenate(natives.MZ.create_sequence()) total_sequence, readouts = unroll_sequences(10 * [sequence], relaxation_time=10000) assert len(total_sequence.probe_pulses) == 10 assert len(readouts) == 1 @@ -62,20 +55,31 @@ def test_create_platform_error(): def test_platform_basics(): - platform = Platform("ciao", {}, {}, {}, {}) + platform = Platform( + name="ciao", + parameters=Parameters(native_gates=NativeGates()), + instruments={}, + qubits={}, + ) assert str(platform) == "ciao" - assert platform.topology == [] + assert platform.pairs == [] - qs = {q: Qubit(q) for q in range(10)} + qs = {q: SingleQubitNatives() for q in range(10)} + ts = {(q1, q2): TwoQubitNatives() for q1 in range(3) for q2 in range(4, 8)} platform2 = Platform( - "come va?", - qs, - {(q1, q2): QubitPair(q1, q2) for q1 in range(3) for q2 in range(4, 8)}, - {}, - {}, + name="come va?", + parameters=Parameters( + native_gates=NativeGates( + single_qubit=qs, + two_qubit=ts, + coupler={}, + ) + ), + instruments={}, + qubits=qs, ) assert str(platform2) == "come va?" - assert (1, 6) in platform2.topology + assert (1, 6) in platform2.pairs def test_create_platform_multipath(tmp_path: Path): @@ -122,8 +126,8 @@ def test_update_configs(platform): drive_name = "q0/drive" pump_name = "twpa_pump" configs = { - drive_name: IqConfig(4.1e9), - pump_name: OscillatorConfig(3e9, -5), + drive_name: IqConfig(frequency=4.1e9), + pump_name: OscillatorConfig(frequency=3e9, power=-5), } updated = update_configs(configs, [{drive_name: {"frequency": 4.2e9}}]) @@ -146,26 +150,21 @@ def test_update_configs(platform): 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) +def test_dump_parameters(platform: Platform, tmp_path: Path): + (tmp_path / PARAMETERS).write_text(platform.parameters.model_dump_json()) + final = Parameters.model_validate_json((tmp_path / PARAMETERS).read_text()) if platform.name == "dummy": - target_runcard = load_runcard(FOLDER) + target = Parameters.model_validate_json((FOLDER / PARAMETERS).read_text()) else: target_path = pathlib.Path(__file__).parent / "dummy_qrc" / f"{platform.name}" - target_runcard = load_runcard(target_path) + target = Parameters.model_validate_json((target_path / PARAMETERS).read_text()) - # assert instrument section is dumped properly in the runcard - target_instruments = target_runcard.pop("instruments") - final_instruments = final_runcard.pop("instruments") - assert final_instruments == target_instruments + # assert configs section is dumped properly in the parameters + assert final.configs == target.configs -def test_dump_runcard_with_updates(platform, tmp_path): +def test_dump_parameters_with_updates(platform: Platform, tmp_path: Path): qubit = next(iter(platform.qubits.values())) frequency = platform.config(qubit.drive.name).frequency + 1.5e9 smearing = platform.config(qubit.acquisition.name).smearing + 10 @@ -173,52 +172,47 @@ def test_dump_runcard_with_updates(platform, tmp_path): 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 + update_configs(platform.parameters.configs, [update]) + (tmp_path / PARAMETERS).write_text(platform.parameters.model_dump_json()) + final = Parameters.model_validate_json((tmp_path / PARAMETERS).read_text()) + assert final.configs[qubit.drive.name].frequency == frequency + assert final.configs[qubit.acquisition.name].smearing == smearing -@pytest.mark.parametrize("has_kernels", [False, True]) -def test_kernels(tmp_path, has_kernels): +def test_kernels(tmp_path: Path): """Test dumping and loading of `Kernels`.""" platform = create_dummy() - if has_kernels: - for name, config in platform.configs.items(): - if isinstance(config, AcquisitionConfig): - platform.configs[name] = replace(config, kernel=np.random.rand(10)) - - dump_kernels(platform, tmp_path) - - if has_kernels: - kernels = Kernels.load(tmp_path) - for qubit in platform.qubits.values(): - kernel = platform.configs[qubit.acquisition.name].kernel - np.testing.assert_array_equal(kernel, kernels[qubit.name]) - else: - with pytest.raises(FileNotFoundError): - Kernels.load(tmp_path) + for name, config in platform.parameters.configs.items(): + if isinstance(config, AcquisitionConfig): + platform.parameters.configs[name] = replace( + config, kernel=np.random.rand(10) + ) + + platform.dump(tmp_path) + reloaded = Platform.load( + tmp_path, + instruments=platform.instruments, + qubits=platform.qubits, + couplers=platform.couplers, + ) + + for qubit in platform.qubits.values(): + orig = platform.parameters.configs[qubit.acquisition.name].kernel + load = reloaded.parameters.configs[qubit.acquisition.name].kernel + np.testing.assert_array_equal(orig, load) -@pytest.mark.parametrize("has_kernels", [False, True]) -def test_dump_platform(tmp_path, has_kernels): - """Test platform dump and loading runcard and kernels.""" +def test_dump_platform(tmp_path): + """Test platform dump and loading parameters and kernels.""" platform = create_dummy() - if has_kernels: - for name, config in platform.configs.items(): - if isinstance(config, AcquisitionConfig): - platform.configs[name] = replace(config, kernel=np.random.rand(10)) - - dump_platform(platform, tmp_path) - - settings = load_settings(load_runcard(tmp_path)) - if has_kernels: - kernels = Kernels.load(tmp_path) - for qubit in platform.qubits.values(): - kernel = platform.configs[qubit.acquisition.name].kernel - np.testing.assert_array_equal(kernel, kernels[qubit.name]) + + platform.dump(tmp_path) + + settings = Parameters.model_validate_json( + (tmp_path / PARAMETERS).read_text() + ).settings assert settings == platform.settings diff --git a/tests/test_serialize.py b/tests/test_serialize.py index 6fcccc3ef1..8a6d4bd7ee 100644 --- a/tests/test_serialize.py +++ b/tests/test_serialize.py @@ -1,7 +1,7 @@ import numpy as np from pydantic import BaseModel, ConfigDict -from qibolab.serialize_ import NdArray, eq +from qibolab.serialize import NdArray, eq class ArrayModel(BaseModel): diff --git a/tests/test_unrolling.py b/tests/test_unrolling.py index 6d16404d77..2d36f770f3 100644 --- a/tests/test_unrolling.py +++ b/tests/test_unrolling.py @@ -44,8 +44,8 @@ def test_bounds_update(): def test_bounds_add(): - bounds1 = Bounds(2, 1, 3) - bounds2 = Bounds(1, 2, 1) + bounds1 = Bounds(waveforms=2, readout=1, instructions=3) + bounds2 = Bounds(waveforms=1, readout=2, instructions=1) bounds_sum = bounds1 + bounds2 @@ -55,8 +55,8 @@ def test_bounds_add(): def test_bounds_comparison(): - bounds1 = Bounds(2, 1, 3) - bounds2 = Bounds(1, 2, 1) + bounds1 = Bounds(waveforms=2, readout=1, instructions=3) + bounds2 = Bounds(waveforms=1, readout=2, instructions=1) assert bounds1 > bounds2 assert not bounds2 < bounds1 @@ -65,9 +65,9 @@ def test_bounds_comparison(): @pytest.mark.parametrize( "bounds", [ - Bounds(150, int(10e6), int(10e6)), - Bounds(int(10e6), 10, int(10e6)), - Bounds(int(10e6), int(10e6), 20), + Bounds(waveforms=150, readout=int(10e6), instructions=int(10e6)), + Bounds(waveforms=int(10e6), readout=10, instructions=int(10e6)), + Bounds(waveforms=int(10e6), readout=int(10e6), instructions=20), ], ) def test_batch(bounds):