diff --git a/src/qibolab/_core/parameters.py b/src/qibolab/_core/parameters.py index dba5aa3db..4cfd72c9c 100644 --- a/src/qibolab/_core/parameters.py +++ b/src/qibolab/_core/parameters.py @@ -5,19 +5,40 @@ """ from collections.abc import Callable, Iterable -from typing import Annotated, Any, Union +from typing import Annotated, Any, Optional, Union from pydantic import BeforeValidator, Field, PlainSerializer, TypeAdapter from pydantic_core import core_schema -from .components import ChannelConfig, Config +from .components import ( + AcquisitionChannel, + AcquisitionConfig, + Channel, + ChannelConfig, + Config, + DcChannel, + DcConfig, + IqChannel, + IqConfig, + IqMixerConfig, + OscillatorConfig, +) from .execution_parameters import ConfigUpdate, ExecutionParameters -from .identifier import QubitId, QubitPairId -from .native import SingleQubitNatives, TwoQubitNatives +from .identifier import ChannelId, QubitId, QubitPairId +from .instruments.abstract import Instrument, InstrumentId +from .native import Native, NativeContainer, SingleQubitNatives, TwoQubitNatives +from .pulses import Acquisition, Pulse, Readout, Rectangular +from .qubits import Qubit from .serialize import Model, replace from .unrolling import Bounds -__all__ = ["ConfigKinds"] +__all__ = [ + "ConfigKinds", + "QubitMap", + "InstrumentMap", + "Hardware", + "initialize_parameters", +] def update_configs(configs: dict[str, Config], updates: list[ConfigUpdate]): @@ -202,3 +223,121 @@ def replace(self, update: Update) -> "Parameters": _setvalue(d, path, val) return self.model_validate(d) + + +QubitMap = dict[QubitId, Qubit] +InstrumentMap = dict[InstrumentId, Instrument] + + +class Hardware(Model): + """Part of the platform that specifies the hardware configuration.""" + + instruments: InstrumentMap + qubits: QubitMap + couplers: QubitMap = Field(default_factory=dict) + + +def _gate_channel(qubit: Qubit, gate: str) -> str: + """Default channel that a native gate plays on.""" + if gate in ("RX", "RX90", "CNOT"): + return qubit.drive + if gate == "RX12": + return qubit.drive_qudits[(1, 2)] + if gate == "MZ": + return qubit.acquisition + if gate in ("CP", "CZ", "iSWAP"): + return qubit.flux + + +def _gate_sequence(qubit: Qubit, gate: str) -> Native: + """Default sequence corresponding to a native gate.""" + channel = _gate_channel(qubit, gate) + pulse = Pulse(duration=0, amplitude=0, envelope=Rectangular()) + if gate != "MZ": + return Native([(channel, pulse)]) + + return Native( + [(channel, Readout(acquisition=Acquisition(duration=0), probe=pulse))] + ) + + +def _pair_to_qubit(pair: str, qubits: QubitMap) -> Qubit: + """Get first qubit of a pair given in ``{q0}-{q1}`` format.""" + q = tuple(pair.split("-"))[0] + try: + return qubits[q] + except KeyError: + return qubits[int(q)] + + +def _native_builder(cls, qubit: Qubit, natives: set[str]) -> NativeContainer: + """Build default native gates for a given qubit or pair. + + In case of pair, ``qubit`` is assumed to be the first qubit of the pair, + and a default pulse is added on that qubit, because at this stage we don't + know which qubit is the high frequency one. + """ + return cls( + **{ + gate: _gate_sequence(qubit, gate) + for gate in cls.model_fields.keys() & natives + } + ) + + +def _channel_config(id: ChannelId, channel: Channel) -> dict[ChannelId, Config]: + """Default configs correspondign to a channel.""" + if isinstance(channel, DcChannel): + return {id: DcConfig(offset=0)} + if isinstance(channel, AcquisitionChannel): + return {id: AcquisitionConfig(delay=0, smearing=0)} + if isinstance(channel, IqChannel): + configs = {id: IqConfig(frequency=0)} + if channel.lo is not None: + configs[channel.lo] = OscillatorConfig(frequency=0, power=0) + if channel.mixer is not None: + configs[channel.mixer] = IqMixerConfig(frequency=0, power=0) + return configs + return {id: Config()} + + +def initialize_parameters( + hardware: Hardware, + natives: Optional[set[str]] = None, + pairs: Optional[list[str]] = None, +) -> Parameters: + """Generates default ``Parameters`` for a given hardware configuration.""" + if natives is None: + natives = set() + else: + natives = set(natives) + + configs = {} + for instrument in hardware.instruments.values(): + if hasattr(instrument, "channels"): + for id, channel in instrument.channels.items(): + configs |= _channel_config(id, channel) + + single_qubit = { + q: _native_builder(SingleQubitNatives, qubit, natives - {"CP"}) + for q, qubit in hardware.qubits.items() + } + coupler = { + q: _native_builder(SingleQubitNatives, qubit, natives & {"CP"}) + for q, qubit in hardware.couplers.items() + } + if pairs is not None: + two_qubit = { + pair: _native_builder( + TwoQubitNatives, _pair_to_qubit(pair, hardware.qubits), natives + ) + for pair in pairs + } + else: + two_qubit = {} + + native_gates = NativeGates( + single_qubit=single_qubit, coupler=coupler, two_qubit=two_qubit + ) + + return Parameters(settings=Settings(), configs=configs, native_gates=native_gates) diff --git a/src/qibolab/_core/platform/load.py b/src/qibolab/_core/platform/load.py index effe85e5d..319a09cb0 100644 --- a/src/qibolab/_core/platform/load.py +++ b/src/qibolab/_core/platform/load.py @@ -1,10 +1,11 @@ import importlib.util import os from pathlib import Path -from typing import Optional +from typing import Optional, Union from qibo.config import raise_error +from ..parameters import Hardware from .platform import Platform __all__ = ["create_platform", "locate_platform"] @@ -38,7 +39,7 @@ def _search(name: str, paths: list[Path]) -> Path: ) -def _load(platform: Path) -> Platform: +def _load(platform: Path) -> Union[Platform, Hardware]: """Load the platform module.""" module_name = "platform" spec = importlib.util.spec_from_file_location(module_name, platform / PLATFORM) @@ -68,7 +69,6 @@ def create_platform(name: str) -> Platform: Args: name (str): name of the platform. - path (pathlib.Path): path with platform serialization Returns: The plaform class. """ @@ -77,7 +77,13 @@ def create_platform(name: str) -> Platform: return create_dummy() - return _load(_search(name, _platforms_paths())) + path = _search(name, _platforms_paths()) + + hardware = _load(path) + if isinstance(hardware, Platform): + return hardware + + return Platform.load(path, **hardware.model_dump()) def available_platforms() -> list[str]: diff --git a/src/qibolab/_core/platform/platform.py b/src/qibolab/_core/platform/platform.py index ff1bbe73d..08c1967e9 100644 --- a/src/qibolab/_core/platform/platform.py +++ b/src/qibolab/_core/platform/platform.py @@ -11,8 +11,16 @@ from ..components.channels import Channel from ..execution_parameters import ExecutionParameters from ..identifier import ChannelId, QubitId, QubitPairId, Result -from ..instruments.abstract import Controller, Instrument, InstrumentId -from ..parameters import NativeGates, Parameters, Settings, Update, update_configs +from ..instruments.abstract import Controller +from ..parameters import ( + InstrumentMap, + NativeGates, + Parameters, + QubitMap, + Settings, + Update, + update_configs, +) from ..pulses import PulseId from ..qubits import Qubit from ..sequence import PulseSequence @@ -21,10 +29,6 @@ __all__ = ["Platform"] -QubitMap = dict[QubitId, Qubit] -QubitPairMap = list[QubitPairId] -InstrumentMap = dict[InstrumentId, Instrument] - NS_TO_SEC = 1e-9 PARAMETERS = "parameters.json" @@ -301,17 +305,13 @@ def load( name: Optional[str] = None, ) -> "Platform": """Dump platform.""" - if name is None: - name = path.name - if couplers is None: - couplers = {} - + parameters = Parameters.model_validate_json((path / PARAMETERS).read_text()) return cls( - name=name, - parameters=Parameters.model_validate_json((path / PARAMETERS).read_text()), + name=name if name is not None else path.name, + parameters=parameters, instruments=instruments, qubits=qubits, - couplers=couplers, + couplers=couplers if couplers is not None else {}, ) def dump(self, path: Path): diff --git a/tests/test_parameters.py b/tests/test_parameters.py index 5cc461c1f..e7d2bac99 100644 --- a/tests/test_parameters.py +++ b/tests/test_parameters.py @@ -4,9 +4,15 @@ from qibolab._core.components.configs import Config from qibolab._core.native import Native, TwoQubitNatives -from qibolab._core.parameters import ConfigKinds, Parameters, TwoQubitContainer +from qibolab._core.parameters import ( + ConfigKinds, + Hardware, + Parameters, + TwoQubitContainer, + initialize_parameters, +) from qibolab._core.platform.load import create_platform -from qibolab._core.pulses.pulse import Pulse +from qibolab._core.pulses.pulse import Pulse, Readout def test_two_qubit_container(): @@ -104,3 +110,37 @@ def test_update(): assert dummy.settings.nshots == 42 assert dummy.natives.single_qubit[1].RX[0][1].amplitude == -0.123 assert dummy.natives.single_qubit[1].RX[0][1].duration == 456.7 + + +def test_builder(): + dummy = create_platform("dummy") + + hardware = Hardware( + instruments=dummy.instruments, + qubits=dummy.qubits, + couplers=dummy.couplers, + ) + parameters = initialize_parameters( + hardware=hardware, natives=["RX", "MZ", "CZ"], pairs=["0-2"] + ) + + for q in dummy.qubits: + assert f"{q}/drive" in parameters.configs + assert f"{q}/probe" in parameters.configs + assert f"{q}/acquisition" in parameters.configs + assert f"{q}/drive12" in parameters.configs + assert q in parameters.native_gates.single_qubit + for c in dummy.couplers: + assert f"coupler_{c}/flux" in parameters.configs + assert c in parameters.native_gates.coupler + + assert list(parameters.native_gates.two_qubit) == [(0, 2)] + sequence = parameters.native_gates.two_qubit[(0, 2)].CZ + assert sequence[0][0] == "0/flux" + assert isinstance(sequence[0][1], Pulse) + sequence = parameters.native_gates.single_qubit[0].RX + assert sequence[0][0] == "0/drive" + assert isinstance(sequence[0][1], Pulse) + sequence = parameters.native_gates.single_qubit[2].MZ + assert sequence[0][0] == "2/acquisition" + assert isinstance(sequence[0][1], Readout)