boulder-opal-scale-up-sdk 1.0.5__py3-none-any.whl → 1.0.7__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 (40) hide show
  1. {boulder_opal_scale_up_sdk-1.0.5.dist-info → boulder_opal_scale_up_sdk-1.0.7.dist-info}/METADATA +2 -2
  2. {boulder_opal_scale_up_sdk-1.0.5.dist-info → boulder_opal_scale_up_sdk-1.0.7.dist-info}/RECORD +38 -28
  3. boulderopalscaleupsdk/agent/worker.py +15 -2
  4. boulderopalscaleupsdk/common/dtypes.py +48 -4
  5. boulderopalscaleupsdk/{stubs/__init__.py → constants.py} +3 -0
  6. boulderopalscaleupsdk/device/controller/qblox.py +10 -2
  7. boulderopalscaleupsdk/device/controller/quantum_machines.py +86 -17
  8. boulderopalscaleupsdk/device/processor/common.py +3 -3
  9. boulderopalscaleupsdk/errors.py +21 -0
  10. boulderopalscaleupsdk/experiments/__init__.py +8 -2
  11. boulderopalscaleupsdk/experiments/cz_spectroscopy_by_bias.py +84 -0
  12. boulderopalscaleupsdk/experiments/power_rabi.py +1 -1
  13. boulderopalscaleupsdk/experiments/power_rabi_ef.py +1 -1
  14. boulderopalscaleupsdk/experiments/ramsey_ef.py +62 -0
  15. boulderopalscaleupsdk/experiments/{readout_classifier_calibration.py → readout_classifier.py} +7 -3
  16. boulderopalscaleupsdk/experiments/readout_optimization.py +57 -0
  17. boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_bias.py +2 -4
  18. boulderopalscaleupsdk/experiments/t2.py +1 -1
  19. boulderopalscaleupsdk/experiments/transmon_anharmonicity.py +0 -2
  20. boulderopalscaleupsdk/experiments/waveforms.py +15 -0
  21. boulderopalscaleupsdk/grpc_interceptors/error.py +318 -0
  22. boulderopalscaleupsdk/plotting/dtypes.py +5 -5
  23. boulderopalscaleupsdk/protobuf/v1/device_pb2.py +57 -49
  24. boulderopalscaleupsdk/protobuf/v1/device_pb2.pyi +76 -46
  25. boulderopalscaleupsdk/protobuf/v1/device_pb2_grpc.py +100 -66
  26. boulderopalscaleupsdk/protobuf/v1/job_pb2.py +47 -0
  27. boulderopalscaleupsdk/protobuf/v1/job_pb2.pyi +54 -0
  28. boulderopalscaleupsdk/protobuf/v1/job_pb2_grpc.py +138 -0
  29. boulderopalscaleupsdk/routines/__init__.py +2 -0
  30. boulderopalscaleupsdk/routines/coupler_discovery.py +37 -0
  31. boulderopalscaleupsdk/routines/transmon_retuning.py +0 -4
  32. boulderopalscaleupsdk/solutions/__init__.py +22 -0
  33. boulderopalscaleupsdk/solutions/common.py +23 -0
  34. boulderopalscaleupsdk/solutions/placeholder_solution.py +28 -0
  35. boulderopalscaleupsdk/third_party/quantum_machines/__init__.py +16 -1
  36. boulderopalscaleupsdk/third_party/quantum_machines/config.py +50 -50
  37. boulderopalscaleupsdk/stubs/dtypes.py +0 -47
  38. boulderopalscaleupsdk/stubs/maps.py +0 -18
  39. {boulder_opal_scale_up_sdk-1.0.5.dist-info → boulder_opal_scale_up_sdk-1.0.7.dist-info}/LICENSE +0 -0
  40. {boulder_opal_scale_up_sdk-1.0.5.dist-info → boulder_opal_scale_up_sdk-1.0.7.dist-info}/WHEEL +0 -0
@@ -0,0 +1,62 @@
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 typing import Literal
15
+
16
+ from pydantic import PrivateAttr
17
+
18
+ from .common import Experiment
19
+ from .waveforms import ConstantWaveform
20
+
21
+
22
+ class RamseyEF(Experiment):
23
+ """
24
+ Parameters for running a EF Ramsey experiment.
25
+
26
+ Attributes
27
+ ----------
28
+ transmon : str
29
+ The reference for the transmon to target.
30
+ min_delay_ns : int
31
+ The minimum delay time, in nanoseconds.
32
+ max_delay_ns : int
33
+ The maximum delay time, in nanoseconds.
34
+ delay_step_ns : int
35
+ The step for generating the list of delays, in nanoseconds.
36
+ virtual_detuning : float
37
+ The virtual detuning added between sx_ef pulses, in Hz.
38
+ recycle_delay_ns : int, optional
39
+ The delay between consecutive shots, in nanoseconds. Defaults to 200,000 ns.
40
+ shot_count : int, optional
41
+ The number of shots to take. Defaults to 400.
42
+ measure_waveform : ConstantWaveform or None, optional
43
+ The waveform to use for the measurement pulse.
44
+ Defaults to the measurement defcal.
45
+ run_mixer_calibration: bool
46
+ Whether to run mixer calibrations before running a program. Defaults to False.
47
+ update : "auto" or "off" or "prompt", optional
48
+ How the device should be updated after an experiment run. Defaults to auto.
49
+ """
50
+
51
+ _experiment_name: str = PrivateAttr("ramsey_ef")
52
+
53
+ transmon: str
54
+ min_delay_ns: int
55
+ max_delay_ns: int
56
+ delay_step_ns: int
57
+ virtual_detuning: float
58
+ recycle_delay_ns: int = 200_000
59
+ shot_count: int = 400
60
+ measure_waveform: ConstantWaveform | None = None
61
+ run_mixer_calibration: bool = False
62
+ update: Literal["auto", "off", "prompt"] = "auto"
@@ -19,9 +19,13 @@ from .common import Experiment
19
19
  from .waveforms import ConstantWaveform
20
20
 
21
21
 
22
- class ReadoutClassifierCalibration(Experiment):
22
+ class ReadoutClassifier(Experiment):
23
23
  """
24
- Parameters for running calibration of readout classifier for a transmon.
24
+ Parameters for training a readout classifier.
25
+
26
+ This does not optimize the readout pulse itself.
27
+ The measure waveform is fixed (provided or using the device's default).
28
+ To optimize the readout pulse use ReadoutOptimization.
25
29
 
26
30
  Attributes
27
31
  ----------
@@ -40,7 +44,7 @@ class ReadoutClassifierCalibration(Experiment):
40
44
  How the device should be updated after an experiment run. Defaults to auto.
41
45
  """
42
46
 
43
- _experiment_name: str = PrivateAttr("readout_classifier_calibration")
47
+ _experiment_name: str = PrivateAttr("readout_classifier")
44
48
 
45
49
  transmon: str
46
50
  recycle_delay_ns: int = 200_000
@@ -0,0 +1,57 @@
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 typing import Literal
15
+
16
+ from pydantic import PrivateAttr
17
+
18
+ from .common import (
19
+ CWSIterable,
20
+ Experiment,
21
+ HypIterable,
22
+ LinspaceIterable,
23
+ RangeIterable,
24
+ )
25
+
26
+
27
+ class ReadoutOptimization(Experiment):
28
+ """
29
+ Parameters for optimizing the readout classifier.
30
+
31
+ Parameters
32
+ ----------
33
+ transmon : str
34
+ The reference for the transmon to target.
35
+ frequencies : list[int] or LinspaceIterable or RangeIterable or CWSIterable or HypIterable
36
+ The readout frequencies to sweep, in Hz.
37
+ amplitudes : list[float]
38
+ The readout amplitudes to sweep.
39
+ recycle_delay_ns : int
40
+ The delay between consecutive shots, in nanoseconds. Defaults to 200,000 ns.
41
+ shot_count : int, optional
42
+ The number of shots to take. Defaults to 5,000.
43
+ run_mixer_calibration: bool
44
+ Whether to run mixer calibrations before running a program. Defaults to False.
45
+ update : "auto" or "off", optional
46
+ How the device should be updated after an experiment run. Defaults to auto.
47
+ """
48
+
49
+ _experiment_name: str = PrivateAttr("readout_optimization")
50
+
51
+ transmon: str
52
+ frequencies: list[int] | LinspaceIterable | RangeIterable | CWSIterable | HypIterable
53
+ amplitudes: list[float]
54
+ recycle_delay_ns: int = 200_000
55
+ shot_count: int = 5000
56
+ run_mixer_calibration: bool = False
57
+ update: Literal["auto", "off"] = "auto"
@@ -24,8 +24,6 @@ from .common import (
24
24
  )
25
25
  from .waveforms import ConstantWaveform
26
26
 
27
- DEFAULT_BIASES = LinspaceIterable(start=-0.49, stop=0.49, count=21)
28
-
29
27
 
30
28
  class ResonatorSpectroscopyByBias(Experiment):
31
29
  """
@@ -44,7 +42,7 @@ class ResonatorSpectroscopyByBias(Experiment):
44
42
  Defaults to a scan around the readout frequency.
45
43
  biases : list[int] or LinspaceIterable or RangeIterable or CWSIterable \
46
44
  or HypIterable, optional
47
- The biases at which to scan, in Volts.
45
+ The biases at which to scan, in volts.
48
46
  Defaults to 21 points between -0.49 and 0.49.
49
47
  recycle_delay_ns : int, optional
50
48
  The delay between consecutive shots, in nanoseconds. Defaults to 1,000 ns.
@@ -67,7 +65,7 @@ class ResonatorSpectroscopyByBias(Experiment):
67
65
  None
68
66
  )
69
67
  biases: list[float] | LinspaceIterable | RangeIterable | CWSIterable | HypIterable = (
70
- DEFAULT_BIASES
68
+ LinspaceIterable(start=-0.49, stop=0.49, count=21)
71
69
  )
72
70
  recycle_delay_ns: int = 1_000
73
71
  shot_count: int = 100
@@ -22,7 +22,7 @@ class T2(Experiment):
22
22
  """
23
23
  Parameters for running a T2 experiment.
24
24
 
25
- Parameters
25
+ Attributes
26
26
  ----------
27
27
  transmon : str
28
28
  The reference for the transmon to target.
@@ -24,8 +24,6 @@ from .common import (
24
24
  )
25
25
  from .waveforms import ConstantWaveform
26
26
 
27
- DEFAULT_RECYCLE_DELAY_NS = 1_000 # ns
28
-
29
27
 
30
28
  class TransmonAnharmonicity(Experiment):
31
29
  """
@@ -29,6 +29,9 @@ class ConstantWaveform(_Waveform): # pragma: no cover
29
29
  amplitude: float
30
30
  waveform_type: Literal["Constant"] = "Constant"
31
31
 
32
+ def __str__(self):
33
+ return f"ConstantWaveform(duration_ns={self.duration_ns}, amplitude={self.amplitude})"
34
+
32
35
 
33
36
  @dataclass
34
37
  class GaussianWaveform(_Waveform): # pragma: no cover
@@ -37,6 +40,12 @@ class GaussianWaveform(_Waveform): # pragma: no cover
37
40
  sigma: float
38
41
  waveform_type: Literal["Gaussian"] = "Gaussian"
39
42
 
43
+ def __str__(self):
44
+ return (
45
+ f"GaussianWaveform(duration_ns={self.duration_ns}, "
46
+ f"amplitude={self.amplitude}, sigma={self.sigma})"
47
+ )
48
+
40
49
 
41
50
  @dataclass
42
51
  class DragCosineWaveform(_Waveform): # pragma: no cover
@@ -47,6 +56,12 @@ class DragCosineWaveform(_Waveform): # pragma: no cover
47
56
  center: float
48
57
  waveform_type: Literal["DragCosineWaveform"] = "DragCosineWaveform"
49
58
 
59
+ def __str__(self):
60
+ return (
61
+ f"DragCosineWaveform(duration_ns={self.duration_ns}, amplitude={self.amplitude}, "
62
+ f"drag={self.drag}, buffer_ns={self.buffer_ns}, center={self.center})"
63
+ )
64
+
50
65
 
51
66
  Waveform = Annotated[
52
67
  ConstantWaveform | GaussianWaveform | DragCosineWaveform,
@@ -0,0 +1,318 @@
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 __future__ import annotations
15
+
16
+ import inspect
17
+ import re
18
+ from typing import TYPE_CHECKING, Any
19
+
20
+ import grpc
21
+ import grpc.aio
22
+
23
+ from boulderopalscaleupsdk.errors import ScaleUpServerError
24
+
25
+ if TYPE_CHECKING:
26
+ from collections.abc import AsyncIterable, AsyncIterator
27
+
28
+ # ------------------------
29
+ # Helpers
30
+ # ------------------------
31
+
32
+ _GRPC_MESSAGE_RE = re.compile(r'grpc_message:"([^"]+)"')
33
+
34
+
35
+ def _extract_peer_message(err: grpc.RpcError) -> str | None:
36
+ dbg = getattr(err, "debug_error_string", None)
37
+ if callable(dbg):
38
+ try:
39
+ s = dbg()
40
+ except (AttributeError, TypeError, RuntimeError):
41
+ return None
42
+ if isinstance(s, str):
43
+ m = _GRPC_MESSAGE_RE.search(s)
44
+ if m:
45
+ return m.group(1)
46
+ return None
47
+
48
+
49
+ def _concise_error(err: grpc.RpcError, *, include_code: bool = False) -> str:
50
+ msg = _extract_peer_message(err) or (err.details() or "An unknown error occurred.")
51
+ if include_code:
52
+ try:
53
+ return f"{err.code().name}: {msg}"
54
+ except (AttributeError, ValueError):
55
+ pass
56
+ return msg
57
+
58
+
59
+ # ------------------------
60
+ # SYNC wrappers
61
+ # ------------------------
62
+
63
+
64
+ class _UnaryUnaryCallWrapper:
65
+ """Proxy that prettifies errors when .result() is called (sync)."""
66
+
67
+ def __init__(self, inner: Any, include_code: bool) -> None:
68
+ self._inner = inner
69
+ self._include_code = include_code
70
+
71
+ def result(self, timeout: float | None = None) -> Any:
72
+ try:
73
+ return self._inner.result(timeout)
74
+ except grpc.RpcError as e:
75
+ raise ScaleUpServerError(_concise_error(e, include_code=self._include_code)) from None
76
+
77
+ def __getattr__(self, name: str) -> Any:
78
+ return getattr(self._inner, name)
79
+
80
+ def __bool__(self) -> bool:
81
+ return bool(self._inner)
82
+
83
+
84
+ class _SyncRespIterWrapper:
85
+ """Wrap response-stream iterator to prettify RpcError during iteration (sync)."""
86
+
87
+ def __init__(self, inner: Any, include_code: bool) -> None:
88
+ self._inner = inner
89
+ self._include_code = include_code
90
+
91
+ def __iter__(self):
92
+ try:
93
+ yield from self._inner
94
+ except grpc.RpcError as e:
95
+ raise ScaleUpServerError(_concise_error(e, include_code=self._include_code)) from None
96
+
97
+ def __getattr__(self, name: str) -> Any:
98
+ return getattr(self._inner, name)
99
+
100
+
101
+ class _SyncReqIterWrapper:
102
+ """Wrap request-stream iterator to prettify RpcError while producing items (sync)."""
103
+
104
+ def __init__(self, inner: Any, include_code: bool) -> None:
105
+ self._inner = inner
106
+ self._include_code = include_code
107
+
108
+ def __iter__(self):
109
+ try:
110
+ yield from self._inner
111
+ except grpc.RpcError as e:
112
+ raise ScaleUpServerError(_concise_error(e, include_code=self._include_code)) from None
113
+
114
+ def __getattr__(self, name: str) -> Any:
115
+ return getattr(self._inner, name)
116
+
117
+
118
+ # ------------------------
119
+ # ASYNC wrappers
120
+ # ------------------------
121
+
122
+
123
+ class _AioRespAsyncIterWrapper:
124
+ """
125
+ Async iterator proxy that prettifies errors from response streams (async).
126
+ Accepts the Call object (UnaryStreamCall or StreamStreamCall).
127
+ """
128
+
129
+ def __init__(self, inner: Any, include_code: bool) -> None:
130
+ self._inner = inner
131
+ self._include_code = include_code
132
+
133
+ def __aiter__(self) -> AsyncIterator[Any]:
134
+ async def gen() -> AsyncIterator[Any]:
135
+ try:
136
+ async for item in self._inner:
137
+ yield item
138
+ except grpc.RpcError as e:
139
+ raise ScaleUpServerError(
140
+ _concise_error(e, include_code=self._include_code),
141
+ ) from None
142
+
143
+ return gen()
144
+
145
+ async def initial_metadata(self) -> Any:
146
+ try:
147
+ return await self._inner.initial_metadata()
148
+ except grpc.RpcError as e:
149
+ raise ScaleUpServerError(_concise_error(e, include_code=self._include_code)) from None
150
+
151
+ async def trailing_metadata(self) -> Any:
152
+ try:
153
+ return await self._inner.trailing_metadata()
154
+ except grpc.RpcError as e:
155
+ raise ScaleUpServerError(_concise_error(e, include_code=self._include_code)) from None
156
+
157
+ def cancel(self) -> None:
158
+ return self._inner.cancel()
159
+
160
+ def code(self) -> Any:
161
+ return self._inner.code()
162
+
163
+ def details(self) -> str:
164
+ return self._inner.details()
165
+
166
+ def __getattr__(self, name: str) -> Any:
167
+ return getattr(self._inner, name)
168
+
169
+
170
+ class _AioReqAsyncIterWrapper:
171
+ """
172
+ Async request iterator wrapper that prettifies errors when producing request items.
173
+ Takes any AsyncIterable (not necessarily an AsyncIterator).
174
+ """
175
+
176
+ def __init__(self, inner: AsyncIterable[Any], include_code: bool) -> None:
177
+ self._inner = inner
178
+ self._include_code = include_code
179
+
180
+ def __aiter__(self) -> AsyncIterator[Any]:
181
+ async def gen() -> AsyncIterator[Any]:
182
+ try:
183
+ async for item in self._inner:
184
+ yield item
185
+ except grpc.RpcError as e:
186
+ raise ScaleUpServerError(
187
+ _concise_error(e, include_code=self._include_code),
188
+ ) from None
189
+
190
+ return gen()
191
+
192
+
193
+ # ------------------------
194
+ # Unified interceptor (sync + aio)
195
+ # ------------------------
196
+
197
+
198
+ class ErrorFormatterInterceptor(
199
+ # sync interfaces
200
+ grpc.UnaryUnaryClientInterceptor,
201
+ grpc.UnaryStreamClientInterceptor,
202
+ grpc.StreamUnaryClientInterceptor,
203
+ grpc.StreamStreamClientInterceptor,
204
+ # aio interfaces
205
+ grpc.aio.UnaryUnaryClientInterceptor,
206
+ grpc.aio.UnaryStreamClientInterceptor,
207
+ grpc.aio.StreamUnaryClientInterceptor,
208
+ grpc.aio.StreamStreamClientInterceptor,
209
+ ):
210
+ """
211
+ Prettifies gRPC errors raised by the server into ScaleUpServerError.
212
+
213
+ Works with both sync (grpc) and async (grpc.aio) channels.
214
+
215
+ - Detects whether `continuation` is a coroutine function.
216
+ - In async case, returns a coroutine that yields wrapped awaitable/async-iterable calls.
217
+ - In sync case, returns wrapped blocking call objects/iterators.
218
+ """
219
+
220
+ def __init__(self, include_code: bool = False) -> None:
221
+ self._include_code = include_code
222
+
223
+ # ---------- UNARY -> UNARY ----------
224
+
225
+ async def _async_unary_unary(self, continuation, client_call_details, request) -> Any:
226
+ try:
227
+ call = continuation(client_call_details, request) # awaitable call
228
+ # Await here so callers receive the *message*, mirroring AuthInterceptor.
229
+ return await call
230
+ except grpc.RpcError as e:
231
+ raise ScaleUpServerError(_concise_error(e, include_code=self._include_code)) from None
232
+
233
+ def _sync_unary_unary(self, continuation, client_call_details, request) -> Any:
234
+ try:
235
+ call = continuation(client_call_details, request) # blocking call
236
+ except grpc.RpcError as e:
237
+ raise ScaleUpServerError(_concise_error(e, include_code=self._include_code)) from None
238
+ return _UnaryUnaryCallWrapper(call, self._include_code)
239
+
240
+ def intercept_unary_unary(self, continuation, client_call_details, request): # type: ignore[override]
241
+ if inspect.iscoroutinefunction(continuation):
242
+ return self._async_unary_unary(continuation, client_call_details, request)
243
+ return self._sync_unary_unary(continuation, client_call_details, request)
244
+
245
+ # ---------- UNARY -> STREAM ----------
246
+
247
+ async def _async_unary_stream(self, continuation, client_call_details, request) -> Any:
248
+ try:
249
+ resp_call = continuation(client_call_details, request) # async-iterable call
250
+ # Do NOT await: return an iterator wrapper that prettifies stream errors.
251
+ return _AioRespAsyncIterWrapper(resp_call, self._include_code)
252
+ except grpc.RpcError as e:
253
+ raise ScaleUpServerError(_concise_error(e, include_code=self._include_code)) from None
254
+
255
+ def _sync_unary_stream(self, continuation, client_call_details, request) -> Any:
256
+ try:
257
+ resp_iter = continuation(client_call_details, request) # iterator
258
+ except grpc.RpcError as e:
259
+ raise ScaleUpServerError(_concise_error(e, include_code=self._include_code)) from None
260
+ return _SyncRespIterWrapper(resp_iter, self._include_code)
261
+
262
+ def intercept_unary_stream(self, continuation, client_call_details, request): # type: ignore[override]
263
+ if inspect.iscoroutinefunction(continuation):
264
+ return self._async_unary_stream(continuation, client_call_details, request)
265
+ return self._sync_unary_stream(continuation, client_call_details, request)
266
+
267
+ # ---------- STREAM -> UNARY ----------
268
+
269
+ async def _async_stream_unary(self, continuation, client_call_details, request_iterator) -> Any:
270
+ try:
271
+ wrapped_req = _AioReqAsyncIterWrapper(request_iterator, self._include_code)
272
+ call = continuation(client_call_details, wrapped_req) # awaitable call
273
+ # Await and return the *message*.
274
+ return await call
275
+ except grpc.RpcError as e:
276
+ raise ScaleUpServerError(_concise_error(e, include_code=self._include_code)) from None
277
+
278
+ def _sync_stream_unary(self, continuation, client_call_details, request_iterator) -> Any:
279
+ try:
280
+ wrapped_req = _SyncReqIterWrapper(request_iterator, self._include_code)
281
+ call = continuation(client_call_details, wrapped_req) # blocking call
282
+ except grpc.RpcError as e:
283
+ raise ScaleUpServerError(_concise_error(e, include_code=self._include_code)) from None
284
+ return _UnaryUnaryCallWrapper(call, self._include_code)
285
+
286
+ def intercept_stream_unary(self, continuation, client_call_details, request_iterator): # type: ignore[override]
287
+ if inspect.iscoroutinefunction(continuation):
288
+ return self._async_stream_unary(continuation, client_call_details, request_iterator)
289
+ return self._sync_stream_unary(continuation, client_call_details, request_iterator)
290
+
291
+ # ---------- STREAM -> STREAM ----------
292
+
293
+ async def _async_stream_stream(
294
+ self,
295
+ continuation,
296
+ client_call_details,
297
+ request_iterator,
298
+ ) -> Any:
299
+ try:
300
+ wrapped_req = _AioReqAsyncIterWrapper(request_iterator, self._include_code)
301
+ resp_call = continuation(client_call_details, wrapped_req) # async-iterable call
302
+ # Return iterator wrapper.
303
+ return _AioRespAsyncIterWrapper(resp_call, self._include_code)
304
+ except grpc.RpcError as e:
305
+ raise ScaleUpServerError(_concise_error(e, include_code=self._include_code)) from None
306
+
307
+ def _sync_stream_stream(self, continuation, client_call_details, request_iterator) -> Any:
308
+ try:
309
+ wrapped_req = _SyncReqIterWrapper(request_iterator, self._include_code)
310
+ resp_iter = continuation(client_call_details, wrapped_req) # iterator
311
+ except grpc.RpcError as e:
312
+ raise ScaleUpServerError(_concise_error(e, include_code=self._include_code)) from None
313
+ return _SyncRespIterWrapper(resp_iter, self._include_code)
314
+
315
+ def intercept_stream_stream(self, continuation, client_call_details, request_iterator): # type: ignore[override]
316
+ if inspect.iscoroutinefunction(continuation):
317
+ return self._async_stream_stream(continuation, client_call_details, request_iterator)
318
+ return self._sync_stream_stream(continuation, client_call_details, request_iterator)
@@ -19,7 +19,6 @@ from pydantic import (
19
19
  BaseModel,
20
20
  BeforeValidator,
21
21
  ConfigDict,
22
- Field,
23
22
  PlainSerializer,
24
23
  ValidationError,
25
24
  )
@@ -87,7 +86,7 @@ class PlotConfig(BaseModel):
87
86
  report: PlotReport | None = None
88
87
 
89
88
 
90
- @dataclass(config=ConfigDict(arbitrary_types_allowed=True))
89
+ @dataclass(config=ConfigDict(arbitrary_types_allowed=True, extra="forbid"))
91
90
  class PlotData1D:
92
91
  x: _SerializableArray
93
92
  y: _SerializableArray
@@ -113,7 +112,7 @@ class PlotData1D:
113
112
  raise ValueError("The shapes of y and y_error must match.")
114
113
 
115
114
 
116
- @dataclass(config=ConfigDict(arbitrary_types_allowed=True))
115
+ @dataclass(config=ConfigDict(arbitrary_types_allowed=True, extra="forbid"))
117
116
  class HeatmapData:
118
117
  x: _SerializableArray
119
118
  y: _SerializableArray
@@ -123,6 +122,7 @@ class HeatmapData:
123
122
 
124
123
  # Whether to display the heatmap values as text.
125
124
  heatmap_text: bool = False
125
+ color_map: Literal["sequential", "divergent"] = "sequential"
126
126
 
127
127
  def __post_init__(self):
128
128
  if self.x.ndim != 1:
@@ -158,12 +158,12 @@ class LinePlot(BaseModel):
158
158
 
159
159
  config: PlotConfig
160
160
  heatmap: HeatmapData | None = None
161
- lines: Annotated[list[PlotData1D], Field(min_length=1)]
161
+ lines: list[PlotData1D] = []
162
162
  markers: list[Marker] = []
163
163
  vlines: list[VLine] = []
164
164
 
165
165
 
166
- @dataclass(config=ConfigDict(arbitrary_types_allowed=True))
166
+ @dataclass(config=ConfigDict(arbitrary_types_allowed=True, extra="forbid"))
167
167
  class HistogramData:
168
168
  data: _SerializableArray
169
169
  label: str | None = None