boulder-opal-scale-up-sdk 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. boulder_opal_scale_up_sdk-1.0.0.dist-info/METADATA +38 -0
  2. boulder_opal_scale_up_sdk-1.0.0.dist-info/RECORD +50 -0
  3. boulder_opal_scale_up_sdk-1.0.0.dist-info/WHEEL +4 -0
  4. boulderopalscaleupsdk/__init__.py +14 -0
  5. boulderopalscaleupsdk/agent/__init__.py +29 -0
  6. boulderopalscaleupsdk/agent/worker.py +244 -0
  7. boulderopalscaleupsdk/common/__init__.py +12 -0
  8. boulderopalscaleupsdk/common/dtypes.py +353 -0
  9. boulderopalscaleupsdk/common/typeclasses.py +85 -0
  10. boulderopalscaleupsdk/device/__init__.py +16 -0
  11. boulderopalscaleupsdk/device/common.py +58 -0
  12. boulderopalscaleupsdk/device/config_loader.py +88 -0
  13. boulderopalscaleupsdk/device/controller/__init__.py +32 -0
  14. boulderopalscaleupsdk/device/controller/base.py +18 -0
  15. boulderopalscaleupsdk/device/controller/qblox.py +664 -0
  16. boulderopalscaleupsdk/device/controller/quantum_machines.py +139 -0
  17. boulderopalscaleupsdk/device/device.py +35 -0
  18. boulderopalscaleupsdk/device/processor/__init__.py +23 -0
  19. boulderopalscaleupsdk/device/processor/common.py +148 -0
  20. boulderopalscaleupsdk/device/processor/superconducting_processor.py +291 -0
  21. boulderopalscaleupsdk/experiments/__init__.py +44 -0
  22. boulderopalscaleupsdk/experiments/common.py +96 -0
  23. boulderopalscaleupsdk/experiments/power_rabi.py +60 -0
  24. boulderopalscaleupsdk/experiments/ramsey.py +55 -0
  25. boulderopalscaleupsdk/experiments/resonator_spectroscopy.py +64 -0
  26. boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_bias.py +76 -0
  27. boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_power.py +64 -0
  28. boulderopalscaleupsdk/grpc_interceptors/__init__.py +16 -0
  29. boulderopalscaleupsdk/grpc_interceptors/auth.py +101 -0
  30. boulderopalscaleupsdk/plotting/__init__.py +24 -0
  31. boulderopalscaleupsdk/plotting/dtypes.py +221 -0
  32. boulderopalscaleupsdk/protobuf/v1/agent_pb2.py +48 -0
  33. boulderopalscaleupsdk/protobuf/v1/agent_pb2.pyi +53 -0
  34. boulderopalscaleupsdk/protobuf/v1/agent_pb2_grpc.py +138 -0
  35. boulderopalscaleupsdk/protobuf/v1/device_pb2.py +71 -0
  36. boulderopalscaleupsdk/protobuf/v1/device_pb2.pyi +110 -0
  37. boulderopalscaleupsdk/protobuf/v1/device_pb2_grpc.py +274 -0
  38. boulderopalscaleupsdk/protobuf/v1/task_pb2.py +53 -0
  39. boulderopalscaleupsdk/protobuf/v1/task_pb2.pyi +118 -0
  40. boulderopalscaleupsdk/protobuf/v1/task_pb2_grpc.py +119 -0
  41. boulderopalscaleupsdk/py.typed +0 -0
  42. boulderopalscaleupsdk/routines/__init__.py +9 -0
  43. boulderopalscaleupsdk/routines/common.py +10 -0
  44. boulderopalscaleupsdk/routines/resonator_mapping.py +13 -0
  45. boulderopalscaleupsdk/third_party/__init__.py +14 -0
  46. boulderopalscaleupsdk/third_party/quantum_machines/__init__.py +51 -0
  47. boulderopalscaleupsdk/third_party/quantum_machines/config.py +597 -0
  48. boulderopalscaleupsdk/third_party/quantum_machines/constants.py +20 -0
  49. boulderopalscaleupsdk/utils/__init__.py +12 -0
  50. boulderopalscaleupsdk/utils/serial_utils.py +62 -0
@@ -0,0 +1,139 @@
1
+ # Copyright 2025 Q-CTRL. All rights reserved.
2
+ #
3
+ # Licensed under the Q-CTRL Terms of service (the "License"). Unauthorized
4
+ # copying or use of this file, via any medium, is strictly prohibited.
5
+ # Proprietary and confidential. You may not use this file except in compliance
6
+ # with the License. You may obtain a copy of the License at
7
+ #
8
+ # https://q-ctrl.com/terms
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS. See the
12
+ # License for the specific language.
13
+
14
+ import logging
15
+ import os
16
+ from typing import Literal
17
+ from unittest.mock import patch
18
+
19
+ from pydantic import BaseModel, Field, model_validator
20
+
21
+ from boulderopalscaleupsdk.common.dtypes import Duration, DurationNsLike, Self, TimeUnit
22
+ from boulderopalscaleupsdk.third_party.quantum_machines.config import (
23
+ ControllerConfigType,
24
+ OctaveConfig121,
25
+ OPX1000ControllerConfigType,
26
+ )
27
+ from boulderopalscaleupsdk.third_party.quantum_machines.constants import (
28
+ MIN_TIME_OF_FLIGHT,
29
+ QUA_CLOCK_CYCLE,
30
+ )
31
+
32
+ from .base import BaseControllerInfo
33
+
34
+ # Disable QM logging and telemetry
35
+ os.environ["QM_DISABLE_STREAMOUTPUT"] = "True" # Used in 1.1.0
36
+ _qm_logger = logging.getLogger("qm")
37
+ _qm_logger.disabled = True
38
+
39
+ # Disable unwanted telemetry/logging modules in QM
40
+ _qm_patch_targets = [
41
+ "qm._loc._get_loc",
42
+ "qm.program.expressions._get_loc",
43
+ "qm.program.StatementsCollection._get_loc",
44
+ "qm.qua._get_loc",
45
+ "qm.qua._dsl._get_loc",
46
+ "qm.qua._expressions._get_loc",
47
+ "qm.qua.AnalogMeasureProcess._get_loc",
48
+ "qm.qua.DigitalMeasureProcess._get_loc",
49
+ "qm.datadog_api.DatadogHandler",
50
+ ]
51
+ for target in _qm_patch_targets:
52
+ try:
53
+ _m = patch(target).__enter__()
54
+ _m.return_value = ""
55
+ except (AttributeError, ModuleNotFoundError): # noqa: PERF203
56
+ pass
57
+
58
+ PortRef = str
59
+ OctaveRef = str
60
+ ControllerRef = str
61
+ PortNum = int
62
+ PortMapping = tuple[OctaveRef, PortNum]
63
+
64
+
65
+ class OctaveConfig(OctaveConfig121):
66
+ host: str | None = Field(default=None)
67
+ port: int | None = Field(default=None)
68
+
69
+ def to_qm_octave_config_121(self) -> OctaveConfig121:
70
+ return OctaveConfig121.model_validate(self.model_dump())
71
+
72
+
73
+ class DrivePortConfig(BaseModel):
74
+ port_type: Literal["drive"] = "drive"
75
+ port_mapping: PortMapping
76
+
77
+
78
+ class FluxPortConfig(BaseModel):
79
+ port_type: Literal["flux"] = "flux"
80
+ port_mapping: PortMapping
81
+
82
+
83
+ class ReadoutPortConfig(BaseModel):
84
+ port_type: Literal["readout"] = "readout"
85
+ port_mapping: PortMapping
86
+ time_of_flight: DurationNsLike
87
+ smearing: DurationNsLike = Field(default=Duration(0, TimeUnit.NS))
88
+
89
+ @model_validator(mode="after")
90
+ def _validate_readout_port_config(self) -> Self:
91
+ min_time_of_flight_ns = MIN_TIME_OF_FLIGHT.convert(TimeUnit.NS).value
92
+ time_of_flight_ns = self.time_of_flight.convert(TimeUnit.NS).value
93
+ smearing_ns = self.smearing.convert(TimeUnit.NS).value
94
+ qua_clock_cycle_ns = QUA_CLOCK_CYCLE.convert(TimeUnit.NS).value
95
+
96
+ if time_of_flight_ns < min_time_of_flight_ns:
97
+ raise ValueError(f"time_of_flight must be >= {MIN_TIME_OF_FLIGHT}")
98
+
99
+ if time_of_flight_ns % qua_clock_cycle_ns != 0:
100
+ raise ValueError(f"time_of_flight must be a multiple of {QUA_CLOCK_CYCLE}")
101
+
102
+ if smearing_ns > time_of_flight_ns - 8:
103
+ raise ValueError(f"smearing must be at most {time_of_flight_ns - 8} ns")
104
+
105
+ return self
106
+
107
+
108
+ OPXControllerConfig = ControllerConfigType
109
+ OPX1000ControllerConfig = OPX1000ControllerConfigType
110
+
111
+
112
+ class QuantumMachinesControllerInfo(BaseControllerInfo):
113
+ """
114
+ QuantumMachinesControllerInfo is a data model that represents the configuration
115
+ and port settings for quantum machine controllers.
116
+
117
+ NOTE: Interface must match OPX Config for first set of parameters, remainder are ours
118
+ https://docs.quantum-machines.co/1.2.1/assets/qua_config.html#/paths/~1/get
119
+
120
+ Attributes
121
+ ----------
122
+ controllers : dict[ControllerRef, OPXControllerConfig | OPX1000ControllerConfig]
123
+ A dictionary mapping controller references to their respective configurations.
124
+ The configurations can be either OPXControllerConfig or OPX1000ControllerConfig.
125
+ Derived from OPX Config.
126
+ octaves : dict[OctaveRef, OctaveConfig]
127
+ A dictionary mapping octave references to their respective configurations.
128
+ Derived from OPX Config.
129
+ port_config : dict[PortRef, DrivePortConfig | FluxPortConfig | ReadoutPortConfig]
130
+ A dictionary mapping port references to their respective port configurations.
131
+ The configurations can be DrivePortConfig, FluxPortConfig, or ReadoutPortConfig.
132
+ Not derived from OPX Config, this is our custom config.
133
+ """
134
+
135
+ controllers: dict[ControllerRef, OPXControllerConfig | OPX1000ControllerConfig] = Field(
136
+ default={},
137
+ )
138
+ octaves: dict[OctaveRef, OctaveConfig] = Field(default={})
139
+ port_config: dict[PortRef, DrivePortConfig | FluxPortConfig | ReadoutPortConfig]
@@ -0,0 +1,35 @@
1
+ # Copyright 2025 Q-CTRL. All rights reserved.
2
+ #
3
+ # Licensed under the Q-CTRL Terms of service (the "License"). Unauthorized
4
+ # copying or use of this file, via any medium, is strictly prohibited.
5
+ # Proprietary and confidential. You may not use this file except in compliance
6
+ # with the License. You may obtain a copy of the License at
7
+ #
8
+ # https://q-ctrl.com/terms
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS. See the
12
+ # License for the specific language.
13
+
14
+ from pydantic import BaseModel
15
+ from pydantic.dataclasses import dataclass
16
+
17
+ from boulderopalscaleupsdk.device.controller.quantum_machines import QuantumMachinesControllerInfo
18
+ from boulderopalscaleupsdk.device.processor import SuperconductingProcessor
19
+
20
+
21
+ class Device(BaseModel):
22
+ """Device specification."""
23
+
24
+ processor: SuperconductingProcessor # | OtherProcessorTypes
25
+ controller_info: QuantumMachinesControllerInfo # | OtherControllerInfoTypes
26
+
27
+
28
+ @dataclass
29
+ class InvalidDevice:
30
+ message: str
31
+
32
+
33
+ @dataclass
34
+ class InvalidDeviceComponent:
35
+ message: str
@@ -0,0 +1,23 @@
1
+ # Copyright 2025 Q-CTRL. All rights reserved.
2
+ #
3
+ # Licensed under the Q-CTRL Terms of service (the "License"). Unauthorized
4
+ # copying or use of this file, via any medium, is strictly prohibited.
5
+ # Proprietary and confidential. You may not use this file except in compliance
6
+ # with the License. You may obtain a copy of the License at
7
+ #
8
+ # https://q-ctrl.com/terms
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS. See the
12
+ # License for the specific language.
13
+
14
+ __all__ = [
15
+ "ComponentParameter",
16
+ "DurationComponentParameter",
17
+ "FloatComponentParameter",
18
+ "SuperconductingProcessor",
19
+ "SuperconductingProcessorTemplate",
20
+ ]
21
+
22
+ from .common import ComponentParameter, DurationComponentParameter, FloatComponentParameter
23
+ from .superconducting_processor import SuperconductingProcessor, SuperconductingProcessorTemplate
@@ -0,0 +1,148 @@
1
+ # Copyright 2025 Q-CTRL. All rights reserved.
2
+ #
3
+ # Licensed under the Q-CTRL Terms of service (the "License"). Unauthorized
4
+ # copying or use of this file, via any medium, is strictly prohibited.
5
+ # Proprietary and confidential. You may not use this file except in compliance
6
+ # with the License. You may obtain a copy of the License at
7
+ #
8
+ # https://q-ctrl.com/terms
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS. See the
12
+ # License for the specific language.
13
+
14
+ from collections.abc import Callable
15
+ from datetime import datetime, timezone
16
+ from typing import Annotated, Any, Generic, Literal, TypeVar
17
+
18
+ from pydantic import BaseModel, BeforeValidator, ConfigDict, TypeAdapter
19
+
20
+ from boulderopalscaleupsdk.common.dtypes import Duration, DurationNsLike, ISO8601DatetimeUTCLike
21
+ from boulderopalscaleupsdk.common.typeclasses import Combine
22
+
23
+ T = TypeVar("T")
24
+ T2 = TypeVar("T2")
25
+ CalibrationStatusT = Literal["approximate", "bad", "good", "stale", "unmeasured"]
26
+
27
+
28
+ class ComponentParameter(BaseModel, Generic[T]):
29
+ dtype: Literal["component-parameter"] = "component-parameter"
30
+ value: T
31
+ err_minus: T | None = None
32
+ err_plus: T | None = None
33
+ calibration_status: CalibrationStatusT = "unmeasured"
34
+ updated_at: ISO8601DatetimeUTCLike | None = None
35
+
36
+ model_config = ConfigDict(arbitrary_types_allowed=True)
37
+
38
+ @classmethod
39
+ def from_value(
40
+ cls,
41
+ value: Any,
42
+ target_type: type[float | int | Duration],
43
+ ) -> "ComponentParameter":
44
+ """
45
+ Create a ComponentParameter instance from a value, ensuring it matches the target type.
46
+
47
+ Parameters
48
+ ----------
49
+ value : Any
50
+ The input value to validate and convert.
51
+ target_type : type
52
+ The expected type of the value (e.g., float, int, Duration).
53
+
54
+ Returns
55
+ -------
56
+ ComponentParameter
57
+ A validated ComponentParameter instance.
58
+
59
+ Raises
60
+ ------
61
+ ValueError
62
+ If the value does not match the target type.
63
+ """
64
+ match value:
65
+ case value if isinstance(value, cls):
66
+ if not isinstance(value.value, target_type):
67
+ raise TypeError("invalid value type")
68
+ return value
69
+ case _ if target_type in [float, int]:
70
+ return cls(**value)
71
+ case _ if target_type is Duration:
72
+ value_copy = value.copy()
73
+ duration_ns = TypeAdapter(DurationNsLike).validate_python(value_copy.pop("value"))
74
+ return cls(value=duration_ns, **value_copy) # type: ignore[arg-type]
75
+ case _:
76
+ return cls.model_validate(value)
77
+
78
+ @staticmethod
79
+ def combine(
80
+ combine_instance: Combine[T],
81
+ ) -> "Combine[ComponentParameter[T]]":
82
+ def _combine(
83
+ first: ComponentParameter[T],
84
+ other: ComponentParameter[Any],
85
+ ) -> ComponentParameter[T]:
86
+ return ComponentParameter(
87
+ value=combine_instance.combine(first.value, other.value),
88
+ err_minus=combine_instance.combine_option(
89
+ first.err_minus,
90
+ other.err_minus,
91
+ ),
92
+ err_plus=combine_instance.combine_option(
93
+ first.err_plus,
94
+ other.err_plus,
95
+ ),
96
+ calibration_status=other.calibration_status,
97
+ updated_at=datetime.now(tz=timezone.utc),
98
+ )
99
+
100
+ return Combine[ComponentParameter[T]].create(_combine)
101
+
102
+ def map(self, fn: Callable[[T], T2]) -> "ComponentParameter[T2]":
103
+ return ComponentParameter(
104
+ value=fn(self.value),
105
+ err_minus=fn(self.err_minus) if self.err_minus is not None else None,
106
+ err_plus=fn(self.err_plus) if self.err_plus is not None else None,
107
+ calibration_status=self.calibration_status,
108
+ updated_at=self.updated_at,
109
+ )
110
+
111
+ def merge_with(self, other: "ComponentParameter[T]", combine_value: "Combine[T]"):
112
+ combined = ComponentParameter[T].combine(combine_value).combine(self, other)
113
+ self.value = combined.value
114
+ self.err_minus = combined.err_minus
115
+ self.err_plus = combined.err_plus
116
+ self.calibration_status = combined.calibration_status
117
+ self.updated_at = combined.updated_at
118
+
119
+
120
+ def get_calibration_status_from_thresholds(
121
+ value: float,
122
+ confidence_interval: float,
123
+ good_threshold: float,
124
+ approximate_threshold: float,
125
+ ) -> CalibrationStatusT:
126
+ if good_threshold <= 0 or approximate_threshold <= 0 or good_threshold >= approximate_threshold:
127
+ raise ValueError(
128
+ f"Invalid thresholds: good: {good_threshold}, approximate: {approximate_threshold}",
129
+ )
130
+
131
+ relative_uncertainty = abs(confidence_interval / value)
132
+
133
+ if relative_uncertainty < good_threshold:
134
+ return "good"
135
+ if relative_uncertainty < approximate_threshold:
136
+ return "approximate"
137
+ return "bad"
138
+
139
+
140
+ FloatComponentParameter = Annotated[
141
+ ComponentParameter[float],
142
+ BeforeValidator(lambda value: ComponentParameter.from_value(value, float)),
143
+ ]
144
+
145
+ DurationComponentParameter = Annotated[
146
+ ComponentParameter[Duration],
147
+ BeforeValidator(lambda value: ComponentParameter.from_value(value, Duration)),
148
+ ]
@@ -0,0 +1,291 @@
1
+ # Copyright 2025 Q-CTRL. All rights reserved.
2
+ #
3
+ # Licensed under the Q-CTRL Terms of service (the "License"). Unauthorized
4
+ # copying or use of this file, via any medium, is strictly prohibited.
5
+ # Proprietary and confidential. You may not use this file except in compliance
6
+ # with the License. You may obtain a copy of the License at
7
+ #
8
+ # https://q-ctrl.com/terms
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS. See the
12
+ # License for the specific language.
13
+
14
+ from copy import deepcopy
15
+ from typing import Annotated, Any, Literal, Union, get_args, get_origin
16
+
17
+ from pydantic import BaseModel, Field
18
+
19
+ from boulderopalscaleupsdk.common.dtypes import (
20
+ Duration,
21
+ TimeUnit,
22
+ )
23
+ from boulderopalscaleupsdk.device.common import (
24
+ Component,
25
+ ComponentRef,
26
+ Processor,
27
+ )
28
+ from boulderopalscaleupsdk.device.processor import (
29
+ ComponentParameter,
30
+ DurationComponentParameter,
31
+ FloatComponentParameter,
32
+ )
33
+
34
+
35
+ # Retrieval data models
36
+ class Transmon(Component[Literal["tunable"]]):
37
+ dtype: Literal["transmon"] = "transmon"
38
+ traits: list[Literal["tunable"]] = Field(default=["tunable"])
39
+
40
+ freq_01: FloatComponentParameter | None = Field(
41
+ default=None,
42
+ json_schema_extra={"display": {"label": "freq_01", "unit": "MHz", "scale": 1e-6}},
43
+ )
44
+ anharm: FloatComponentParameter | None = Field(
45
+ default=None,
46
+ json_schema_extra={"display": {"label": "anharm", "unit": "MHz", "scale": 1e-6}},
47
+ )
48
+ t1: DurationComponentParameter = Field(
49
+ default=ComponentParameter(value=Duration(0, TimeUnit.NS)),
50
+ json_schema_extra={"display": {"label": "t1", "unit": "µs", "scale": 1e6}},
51
+ )
52
+ t2: DurationComponentParameter = Field(
53
+ default=ComponentParameter(value=Duration(0, TimeUnit.NS)),
54
+ json_schema_extra={"display": {"label": "t2", "unit": "µs", "scale": 1e6}},
55
+ )
56
+ t2_echo: DurationComponentParameter = Field(
57
+ default=ComponentParameter(value=Duration(0, TimeUnit.NS)),
58
+ json_schema_extra={"display": {"label": "t2_echo", "unit": "µs", "scale": 1e6}},
59
+ )
60
+ x_vp: FloatComponentParameter = Field(
61
+ default=ComponentParameter(value=0.0),
62
+ json_schema_extra={"display": {"label": "x_vp", "unit": "V", "scale": 1}},
63
+ )
64
+ sx_vp: FloatComponentParameter = Field(
65
+ default=ComponentParameter(value=0.0),
66
+ json_schema_extra={"display": {"label": "sx_vp", "unit": "V", "scale": 1}},
67
+ )
68
+
69
+ # Tunable transmon parameters.
70
+ dc_bias: FloatComponentParameter = Field(
71
+ default=ComponentParameter(value=0.0),
72
+ json_schema_extra={"display": {"label": "dc_bias", "unit": "V", "scale": 1}},
73
+ )
74
+ fmax_bias: FloatComponentParameter = Field(
75
+ default=ComponentParameter(value=0.0),
76
+ json_schema_extra={"display": {"label": "fmax_bias", "unit": "V", "scale": 1}},
77
+ )
78
+ bias_period: FloatComponentParameter = Field(
79
+ default=ComponentParameter(value=0.0),
80
+ json_schema_extra={"display": {"label": "bias_period", "unit": "V", "scale": 1}},
81
+ )
82
+
83
+
84
+ class Resonator(Component[Literal["dispersive"]]):
85
+ dtype: Literal["resonator"] = "resonator"
86
+ traits: list[Literal["dispersive"]] = Field(default=["dispersive"])
87
+
88
+ frequency_high: FloatComponentParameter | None = Field(
89
+ default=None,
90
+ json_schema_extra={"display": {"label": "frequency_high", "unit": "MHz", "scale": 1e-6}},
91
+ )
92
+ frequency_low: FloatComponentParameter | None = Field(
93
+ default=None,
94
+ json_schema_extra={"display": {"label": "frequency_low", "unit": "MHz", "scale": 1e-6}},
95
+ )
96
+ kappa_low: FloatComponentParameter | None = Field(
97
+ default=None,
98
+ json_schema_extra={"display": {"label": "kappa_low", "unit": "MHz", "scale": 1e-6}},
99
+ )
100
+ kappa_high: FloatComponentParameter | None = Field(
101
+ default=None,
102
+ json_schema_extra={"display": {"label": "kappa_high", "unit": "MHz", "scale": 1e-6}},
103
+ )
104
+ vp_low: FloatComponentParameter = Field(
105
+ default=ComponentParameter(value=0.0),
106
+ json_schema_extra={"display": {"label": "vp_low", "unit": "V", "scale": 1}},
107
+ )
108
+ vp_high: FloatComponentParameter = Field(
109
+ default=ComponentParameter(value=0.0),
110
+ json_schema_extra={"display": {"label": "vp_high", "unit": "V", "scale": 1}},
111
+ )
112
+
113
+
114
+ class Port(Component[Literal["drive", "readout", "flux"]]):
115
+ dtype: Literal["port"] = "port"
116
+ traits: list[Literal["flux", "drive", "readout"]] = Field(
117
+ default=["drive", "readout", "flux"],
118
+ )
119
+
120
+
121
+ class Filter(Component[Literal["filter"]]):
122
+ dtype: Literal["filter"] = "filter"
123
+ traits: list = Field(default=[])
124
+
125
+
126
+ class Feedline(Component[Literal["feedline"]]):
127
+ dtype: Literal["feedline"] = "feedline"
128
+ traits: list = Field(default=[])
129
+
130
+
131
+ SuperconductingComponentType = Transmon | Resonator | Port | Feedline | Filter
132
+
133
+
134
+ class Edge(BaseModel):
135
+ dtype: Literal["capacitive-coupling", "inductive-coupling"]
136
+ u: str
137
+ v: str
138
+
139
+
140
+ class TemplateParam(BaseModel):
141
+ template: str
142
+ vars: dict[int, list[str]]
143
+
144
+
145
+ class ProcessorTemplate(BaseModel):
146
+ elements: dict[str, Transmon | Resonator | Port | Feedline | Filter]
147
+ edges: list[Edge]
148
+
149
+
150
+ class SuperconductingProcessorTemplate(BaseModel):
151
+ build: list[TemplateParam]
152
+ templates: dict[str, ProcessorTemplate]
153
+ device_parameters: dict[str, Transmon | Resonator | Port | Feedline | Filter] = Field(
154
+ default={},
155
+ )
156
+
157
+
158
+ class SuperconductingProcessor(Processor):
159
+ nodes: dict[ComponentRef, SuperconductingComponentType]
160
+ edges: list[Edge]
161
+
162
+ @staticmethod
163
+ def from_template(template: SuperconductingProcessorTemplate) -> "SuperconductingProcessor":
164
+ _qpu = SuperconductingProcessor(nodes={}, edges=[])
165
+ for build in template.build:
166
+ qpu_template = template.templates[build.template]
167
+ for idx, subs in build.vars.items():
168
+ for sub in subs:
169
+ for k, template_params in qpu_template.elements.items():
170
+ ref = k.replace(f"${idx}", sub)
171
+ params = template.device_parameters.get(ref)
172
+ p = deepcopy(params or template_params)
173
+ p.traits = template_params.traits # type: ignore[reportAttributeAccessIssue]
174
+ p = _coerce_component_params(p)
175
+ _qpu.nodes[ref] = p
176
+
177
+ for edge in qpu_template.edges:
178
+ _qpu.edges.append(
179
+ Edge(
180
+ u=edge.u.replace(f"${idx}", sub),
181
+ v=edge.v.replace(f"${idx}", sub),
182
+ dtype=edge.dtype,
183
+ ),
184
+ )
185
+ return _qpu
186
+
187
+ @staticmethod
188
+ def from_dict(data: dict[str, Any]) -> "SuperconductingProcessor":
189
+ """
190
+ Deserializes a SuperconductingProcessor instance from a dictionary.
191
+ """
192
+ return SuperconductingProcessor.model_validate(data)
193
+
194
+ def to_dict(self) -> dict[str, Any]:
195
+ """
196
+ Serializes the SuperconductingProcessor instance into a dictionary.
197
+ """
198
+ return self.model_dump()
199
+
200
+ def update_component(self, component_id: str, **params: Any) -> None:
201
+ """
202
+ Update the parameters of a specific component.
203
+
204
+ Parameters
205
+ ----------
206
+ component_id : str
207
+ The ID of the component to update.
208
+ **params : Any
209
+ The parameters to update, provided as keyword arguments.
210
+
211
+ Raises
212
+ ------
213
+ KeyError
214
+ If the component with the given ID does not exist.
215
+ ValueError
216
+ If the component does not support the provided parameters.
217
+ ValidationError
218
+ If the updated parameters do not conform to the expected schema.
219
+ """
220
+ if component_id not in self.nodes:
221
+ raise KeyError(f"Component with ID '{component_id}' does not exist.")
222
+
223
+ component = self.nodes[component_id]
224
+ replacement_component = {
225
+ k: ComponentParameter(value=v).model_dump()
226
+ if k not in ["dtype", "traits"] and v is not None
227
+ else v
228
+ for k, v in params.items()
229
+ }
230
+ self.nodes[component_id] = type(component).model_validate(replacement_component)
231
+
232
+ def get_component(self, component_id: str) -> SuperconductingComponentType:
233
+ """
234
+ Retrieve the parameters of a specific component.
235
+
236
+ Parameters
237
+ ----------
238
+ component_id : str
239
+ The ID of the component.
240
+
241
+ Returns
242
+ -------
243
+ dict
244
+ A dictionary of the component's parameters.
245
+ """
246
+ return self.nodes[component_id]
247
+
248
+
249
+ def _coerce_component_params(
250
+ component: SuperconductingComponentType,
251
+ ) -> SuperconductingComponentType:
252
+ annotations = component.__annotations__
253
+
254
+ for field_name in component.model_fields:
255
+ expected_type = annotations.get(field_name)
256
+ value = getattr(component, field_name)
257
+
258
+ if not isinstance(value, ComponentParameter):
259
+ continue
260
+
261
+ def _unwrap_annotated(tp):
262
+ """Unwrap Annotated[...] -> original type."""
263
+ if get_origin(tp) is Annotated:
264
+ return get_args(tp)[0]
265
+ return tp
266
+
267
+ def _unwrap_optional(tp):
268
+ """Unwrap Optional[T] -> T."""
269
+ if get_origin(tp) is Union:
270
+ args = [t for t in get_args(tp) if t is not type(None)]
271
+ if len(args) == 1:
272
+ return args[0]
273
+ return tp
274
+
275
+ expected_type = _unwrap_optional(expected_type)
276
+ expected_type = _unwrap_annotated(expected_type)
277
+
278
+ if not isinstance(expected_type, type) or not issubclass(expected_type, ComponentParameter):
279
+ continue
280
+
281
+ try:
282
+ dumped = value.model_dump()
283
+ coerced = expected_type(**dumped)
284
+ setattr(component, field_name, coerced)
285
+ except Exception as e:
286
+ raise ValueError(
287
+ f"Failed to coerce field '{field_name}' of type "
288
+ "'{expected_type}' with value '{value}': {e}",
289
+ ) from e
290
+
291
+ return component
@@ -0,0 +1,44 @@
1
+ # Copyright 2025 Q-CTRL. All rights reserved.
2
+ #
3
+ # Licensed under the Q-CTRL Terms of service (the "License"). Unauthorized
4
+ # copying or use of this file, via any medium, is strictly prohibited.
5
+ # Proprietary and confidential. You may not use this file except in compliance
6
+ # with the License. You may obtain a copy of the License at
7
+ #
8
+ # https://q-ctrl.com/terms
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS. See the
12
+ # License for the specific language.
13
+
14
+ """Experiment library."""
15
+
16
+ __all__ = [
17
+ "CWSIterable",
18
+ "ConstantWaveform",
19
+ "Experiment",
20
+ "GaussianWaveform",
21
+ "HypIterable",
22
+ "LinspaceIterable",
23
+ "PowerRabi",
24
+ "Ramsey",
25
+ "RangeIterable",
26
+ "ResonatorSpectroscopy",
27
+ "ResonatorSpectroscopyByBias",
28
+ "ResonatorSpectroscopyByPower",
29
+ ]
30
+
31
+ from .common import (
32
+ ConstantWaveform,
33
+ CWSIterable,
34
+ Experiment,
35
+ GaussianWaveform,
36
+ HypIterable,
37
+ LinspaceIterable,
38
+ RangeIterable,
39
+ )
40
+ from .power_rabi import PowerRabi
41
+ from .ramsey import Ramsey
42
+ from .resonator_spectroscopy import ResonatorSpectroscopy
43
+ from .resonator_spectroscopy_by_bias import ResonatorSpectroscopyByBias
44
+ from .resonator_spectroscopy_by_power import ResonatorSpectroscopyByPower