From 193885078bba212c7f650b9eb8b1728e08b9d4f7 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:36:58 +0400 Subject: [PATCH 1/4] feat: implement ParametersBuilder --- src/qibolab/_core/components/__init__.py | 1 + src/qibolab/_core/components/default.py | 19 ++++++++ src/qibolab/_core/parameters.py | 57 ++++++++++++++++++++++-- src/qibolab/_core/platform/load.py | 13 +++++- src/qibolab/_core/platform/platform.py | 16 ++++--- tests/test_parameters.py | 31 ++++++++++++- 6 files changed, 125 insertions(+), 12 deletions(-) create mode 100644 src/qibolab/_core/components/default.py diff --git a/src/qibolab/_core/components/__init__.py b/src/qibolab/_core/components/__init__.py index 1b9fab688..5598bdf56 100644 --- a/src/qibolab/_core/components/__init__.py +++ b/src/qibolab/_core/components/__init__.py @@ -18,6 +18,7 @@ from . import channels from .channels import * from .configs import * +from .default import * __all__ = [] __all__ += channels.__all__ diff --git a/src/qibolab/_core/components/default.py b/src/qibolab/_core/components/default.py new file mode 100644 index 000000000..d219e8115 --- /dev/null +++ b/src/qibolab/_core/components/default.py @@ -0,0 +1,19 @@ +from .channels import AcquisitionChannel, Channel, DcChannel, IqChannel +from .configs import AcquisitionConfig, Config, DcConfig, IqConfig + +__all__ = ["channel_to_config"] + +CHANNEL_TO_CONFIG_MAP = { + Channel: Config, + DcChannel: lambda: DcConfig(offset=0), + IqChannel: lambda: IqConfig(frequency=0), + AcquisitionChannel: lambda: AcquisitionConfig(delay=0, smearing=0), +} + + +def channel_to_config(channel: Channel) -> Config: + """Create a default config for a given channel. + + The config type depends on the channel type. + """ + return CHANNEL_TO_CONFIG_MAP[type(channel)]() diff --git a/src/qibolab/_core/parameters.py b/src/qibolab/_core/parameters.py index dba5aa3db..151563862 100644 --- a/src/qibolab/_core/parameters.py +++ b/src/qibolab/_core/parameters.py @@ -9,15 +9,18 @@ from pydantic import BeforeValidator, Field, PlainSerializer, TypeAdapter from pydantic_core import core_schema +from typing_extensions import NotRequired, TypedDict -from .components import ChannelConfig, Config +from .components import ChannelConfig, Config, channel_to_config from .execution_parameters import ConfigUpdate, ExecutionParameters from .identifier import QubitId, QubitPairId -from .native import SingleQubitNatives, TwoQubitNatives +from .instruments.abstract import Instrument, InstrumentId +from .native import Native, NativeContainer, SingleQubitNatives, TwoQubitNatives +from .qubits import Qubit from .serialize import Model, replace from .unrolling import Bounds -__all__ = ["ConfigKinds"] +__all__ = ["ConfigKinds", "QubitMap", "InstrumentMap", "Hardware", "ParametersBuilder"] def update_configs(configs: dict[str, Config], updates: list[ConfigUpdate]): @@ -202,3 +205,51 @@ 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(TypedDict): + instruments: InstrumentMap + qubits: QubitMap + couplers: NotRequired[QubitMap] + + +def _native_builder(cls, natives: set[str]) -> NativeContainer: + return cls(**{gate: Native() for gate in cls.model_fields.keys() & natives}) + + +class ParametersBuilder(Model): + hardware: Hardware + natives: set[str] = Field(default_factory=set) + pairs: list[str] = Field(default_factory=list) + + def build(self): + settings = Settings() + + configs = {} + for instrument in self.hardware.get("instruments", {}).values(): + if hasattr(instrument, "channels"): + configs |= { + id: channel_to_config(channel) + for id, channel in instrument.channels.items() + } + + single_qubit = { + q: _native_builder(SingleQubitNatives, self.natives - {"CP"}) + for q in self.hardware.get("qubits", {}) + } + coupler = { + q: _native_builder(SingleQubitNatives, self.natives & {"CP"}) + for q in self.hardware.get("couplers", {}) + } + two_qubit = { + p: _native_builder(TwoQubitNatives, self.natives) for p in self.pairs + } + 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..a79e7adf9 100644 --- a/src/qibolab/_core/platform/load.py +++ b/src/qibolab/_core/platform/load.py @@ -5,6 +5,7 @@ from qibo.config import raise_error +from ..parameters import Parameters from .platform import Platform __all__ = ["create_platform", "locate_platform"] @@ -61,7 +62,7 @@ def locate_platform(name: str, paths: Optional[list[Path]] = None) -> Path: return _search(name, paths) -def create_platform(name: str) -> Platform: +def create_platform(name: str, params: Optional[dict] = None) -> Platform: """A platform for executing quantum algorithms. It consists of a quantum processor QPU and a set of controlling instruments. @@ -77,7 +78,15 @@ 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 + + if params is None: + return Platform.load(path, **hardware) + + return Platform(**hardware, parameters=Parameters(**params)) def available_platforms() -> list[str]: diff --git a/src/qibolab/_core/platform/platform.py b/src/qibolab/_core/platform/platform.py index ff1bbe73d..4ef4d292a 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" diff --git a/tests/test_parameters.py b/tests/test_parameters.py index 5cc461c1f..a71a15164 100644 --- a/tests/test_parameters.py +++ b/tests/test_parameters.py @@ -4,7 +4,12 @@ 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, + Parameters, + ParametersBuilder, + TwoQubitContainer, +) from qibolab._core.platform.load import create_platform from qibolab._core.pulses.pulse import Pulse @@ -104,3 +109,27 @@ 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 = { + "instruments": dummy.instruments, + "qubits": dummy.qubits, + "couplers": dummy.couplers, + } + builder = ParametersBuilder(hardware=hardware, pairs=["0-2"]) + parameters = builder.build() + + 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)] From 701093d5b14338c99ddf11d82ac756a7980549ce Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:31:54 +0400 Subject: [PATCH 2/4] feat: default native gate sequences --- src/qibolab/_core/parameters.py | 70 +++++++++++++++++++++++++++++---- tests/test_parameters.py | 15 ++++++- 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/src/qibolab/_core/parameters.py b/src/qibolab/_core/parameters.py index 151563862..0e1aad989 100644 --- a/src/qibolab/_core/parameters.py +++ b/src/qibolab/_core/parameters.py @@ -16,6 +16,7 @@ from .identifier import 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 @@ -212,21 +213,70 @@ def replace(self, update: Update) -> "Parameters": class Hardware(TypedDict): + """Part of the platform that specifies the hardware configuration.""" + instruments: InstrumentMap qubits: QubitMap couplers: NotRequired[QubitMap] -def _native_builder(cls, natives: set[str]) -> NativeContainer: - return cls(**{gate: Native() for gate in cls.model_fields.keys() & natives}) +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 + } + ) class ParametersBuilder(Model): + """Generates default ``Parameters`` for a given platform hardware + configuration.""" + hardware: Hardware natives: set[str] = Field(default_factory=set) pairs: list[str] = Field(default_factory=list) - def build(self): + def build(self) -> Parameters: settings = Settings() configs = {} @@ -237,16 +287,20 @@ def build(self): for id, channel in instrument.channels.items() } + qubits = self.hardware.get("qubits", {}) single_qubit = { - q: _native_builder(SingleQubitNatives, self.natives - {"CP"}) - for q in self.hardware.get("qubits", {}) + q: _native_builder(SingleQubitNatives, qubit, self.natives - {"CP"}) + for q, qubit in qubits.items() } coupler = { - q: _native_builder(SingleQubitNatives, self.natives & {"CP"}) - for q in self.hardware.get("couplers", {}) + q: _native_builder(SingleQubitNatives, qubit, self.natives & {"CP"}) + for q, qubit in self.hardware.get("couplers", {}).items() } two_qubit = { - p: _native_builder(TwoQubitNatives, self.natives) for p in self.pairs + pair: _native_builder( + TwoQubitNatives, _pair_to_qubit(pair, qubits), self.natives + ) + for pair in self.pairs } native_gates = NativeGates( single_qubit=single_qubit, coupler=coupler, two_qubit=two_qubit diff --git a/tests/test_parameters.py b/tests/test_parameters.py index a71a15164..963090953 100644 --- a/tests/test_parameters.py +++ b/tests/test_parameters.py @@ -11,7 +11,7 @@ TwoQubitContainer, ) 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(): @@ -119,7 +119,9 @@ def test_builder(): "qubits": dummy.qubits, "couplers": dummy.couplers, } - builder = ParametersBuilder(hardware=hardware, pairs=["0-2"]) + builder = ParametersBuilder( + hardware=hardware, natives=["RX", "MZ", "CZ"], pairs=["0-2"] + ) parameters = builder.build() for q in dummy.qubits: @@ -133,3 +135,12 @@ def test_builder(): 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) From 978034732f7059e4183506f8ac4840105695e7f2 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:45:41 +0400 Subject: [PATCH 3/4] feat: instantiate platform without parameters --- src/qibolab/_core/platform/load.py | 6 +++--- src/qibolab/_core/platform/platform.py | 15 ++++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/qibolab/_core/platform/load.py b/src/qibolab/_core/platform/load.py index a79e7adf9..034d8763e 100644 --- a/src/qibolab/_core/platform/load.py +++ b/src/qibolab/_core/platform/load.py @@ -62,7 +62,7 @@ def locate_platform(name: str, paths: Optional[list[Path]] = None) -> Path: return _search(name, paths) -def create_platform(name: str, params: Optional[dict] = None) -> Platform: +def create_platform(name: str, parameters: Optional[Parameters] = None) -> Platform: """A platform for executing quantum algorithms. It consists of a quantum processor QPU and a set of controlling instruments. @@ -83,10 +83,10 @@ def create_platform(name: str, params: Optional[dict] = None) -> Platform: if isinstance(hardware, Platform): return hardware - if params is None: + if parameters is None: return Platform.load(path, **hardware) - return Platform(**hardware, parameters=Parameters(**params)) + return Platform(**hardware, parameters=parameters) def available_platforms() -> list[str]: diff --git a/src/qibolab/_core/platform/platform.py b/src/qibolab/_core/platform/platform.py index 4ef4d292a..e5f61bc25 100644 --- a/src/qibolab/_core/platform/platform.py +++ b/src/qibolab/_core/platform/platform.py @@ -16,6 +16,7 @@ InstrumentMap, NativeGates, Parameters, + ParametersBuilder, QubitMap, Settings, Update, @@ -310,13 +311,13 @@ def load( if couplers is None: couplers = {} - return cls( - name=name, - parameters=Parameters.model_validate_json((path / PARAMETERS).read_text()), - instruments=instruments, - qubits=qubits, - couplers=couplers, - ) + hardware = {"instruments": instruments, "qubits": qubits, "couplers": couplers} + try: + parameters = Parameters.model_validate_json((path / PARAMETERS).read_text()) + except FileNotFoundError: + parameters = ParametersBuilder(hardware=hardware).build() + + return cls(name=name, parameters=parameters, **hardware) def dump(self, path: Path): """Dump platform.""" From 8725616e32b4a31a0fb53623e504300a693ab560 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 17 Dec 2024 18:08:10 +0400 Subject: [PATCH 4/4] fix: include LO and mixer configs --- src/qibolab/_core/components/__init__.py | 1 - src/qibolab/_core/components/default.py | 19 ------------ src/qibolab/_core/parameters.py | 38 ++++++++++++++++++++---- 3 files changed, 32 insertions(+), 26 deletions(-) delete mode 100644 src/qibolab/_core/components/default.py diff --git a/src/qibolab/_core/components/__init__.py b/src/qibolab/_core/components/__init__.py index 5598bdf56..1b9fab688 100644 --- a/src/qibolab/_core/components/__init__.py +++ b/src/qibolab/_core/components/__init__.py @@ -18,7 +18,6 @@ from . import channels from .channels import * from .configs import * -from .default import * __all__ = [] __all__ += channels.__all__ diff --git a/src/qibolab/_core/components/default.py b/src/qibolab/_core/components/default.py deleted file mode 100644 index d219e8115..000000000 --- a/src/qibolab/_core/components/default.py +++ /dev/null @@ -1,19 +0,0 @@ -from .channels import AcquisitionChannel, Channel, DcChannel, IqChannel -from .configs import AcquisitionConfig, Config, DcConfig, IqConfig - -__all__ = ["channel_to_config"] - -CHANNEL_TO_CONFIG_MAP = { - Channel: Config, - DcChannel: lambda: DcConfig(offset=0), - IqChannel: lambda: IqConfig(frequency=0), - AcquisitionChannel: lambda: AcquisitionConfig(delay=0, smearing=0), -} - - -def channel_to_config(channel: Channel) -> Config: - """Create a default config for a given channel. - - The config type depends on the channel type. - """ - return CHANNEL_TO_CONFIG_MAP[type(channel)]() diff --git a/src/qibolab/_core/parameters.py b/src/qibolab/_core/parameters.py index 0e1aad989..bf0d3a321 100644 --- a/src/qibolab/_core/parameters.py +++ b/src/qibolab/_core/parameters.py @@ -11,9 +11,21 @@ from pydantic_core import core_schema from typing_extensions import NotRequired, TypedDict -from .components import ChannelConfig, Config, channel_to_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 .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 @@ -268,6 +280,22 @@ def _native_builder(cls, qubit: Qubit, natives: set[str]) -> NativeContainer: ) +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()} + + class ParametersBuilder(Model): """Generates default ``Parameters`` for a given platform hardware configuration.""" @@ -282,10 +310,8 @@ def build(self) -> Parameters: configs = {} for instrument in self.hardware.get("instruments", {}).values(): if hasattr(instrument, "channels"): - configs |= { - id: channel_to_config(channel) - for id, channel in instrument.channels.items() - } + for id, channel in instrument.channels.items(): + configs |= _channel_config(id, channel) qubits = self.hardware.get("qubits", {}) single_qubit = {