boulder-opal-scale-up-sdk 1.0.2__tar.gz → 1.0.4__tar.gz

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 (73) hide show
  1. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/PKG-INFO +1 -1
  2. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/agent/worker.py +10 -5
  3. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/common/dtypes.py +14 -1
  4. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/common/typeclasses.py +22 -0
  5. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/device/common.py +0 -3
  6. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/device/config_loader.py +1 -0
  7. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/device/controller/qblox.py +129 -0
  8. boulder_opal_scale_up_sdk-1.0.2/boulderopalscaleupsdk/device/__init__.py → boulder_opal_scale_up_sdk-1.0.4/boulderopalscaleupsdk/device/defcal.py +9 -6
  9. boulder_opal_scale_up_sdk-1.0.4/boulderopalscaleupsdk/device/device.py +63 -0
  10. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/device/processor/__init__.py +0 -2
  11. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/device/processor/common.py +37 -20
  12. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/device/processor/superconducting_processor.py +28 -22
  13. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/experiments/__init__.py +20 -2
  14. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/experiments/chi01_scan.py +14 -8
  15. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/experiments/common.py +15 -16
  16. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/experiments/power_rabi.py +16 -12
  17. boulder_opal_scale_up_sdk-1.0.4/boulderopalscaleupsdk/experiments/power_rabi_ef.py +66 -0
  18. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/experiments/ramsey.py +15 -17
  19. boulder_opal_scale_up_sdk-1.0.4/boulderopalscaleupsdk/experiments/readout_classifier_calibration.py +44 -0
  20. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/experiments/resonator_spectroscopy.py +14 -17
  21. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_bias.py +17 -20
  22. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_power.py +12 -12
  23. boulder_opal_scale_up_sdk-1.0.4/boulderopalscaleupsdk/experiments/t1.py +46 -0
  24. boulder_opal_scale_up_sdk-1.0.4/boulderopalscaleupsdk/experiments/t2.py +49 -0
  25. boulder_opal_scale_up_sdk-1.0.4/boulderopalscaleupsdk/experiments/t2_echo.py +49 -0
  26. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/experiments/transmon_anharmonicity.py +29 -26
  27. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/experiments/transmon_spectroscopy.py +16 -21
  28. boulder_opal_scale_up_sdk-1.0.4/boulderopalscaleupsdk/experiments/waveforms.py +63 -0
  29. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/plotting/dtypes.py +1 -0
  30. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/protobuf/v1/device_pb2.py +20 -20
  31. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/protobuf/v1/device_pb2.pyi +4 -2
  32. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/protobuf/v1/task_pb2.py +16 -16
  33. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/protobuf/v1/task_pb2.pyi +4 -2
  34. boulder_opal_scale_up_sdk-1.0.4/boulderopalscaleupsdk/routines/__init__.py +21 -0
  35. boulder_opal_scale_up_sdk-1.0.4/boulderopalscaleupsdk/routines/common.py +23 -0
  36. boulder_opal_scale_up_sdk-1.0.4/boulderopalscaleupsdk/routines/resonator_mapping.py +35 -0
  37. boulder_opal_scale_up_sdk-1.0.4/boulderopalscaleupsdk/routines/transmon_discovery.py +45 -0
  38. boulder_opal_scale_up_sdk-1.0.4/boulderopalscaleupsdk/routines/transmon_retuning.py +31 -0
  39. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/stubs/maps.py +11 -2
  40. boulder_opal_scale_up_sdk-1.0.4/boulderopalscaleupsdk/utils/__init__.py +12 -0
  41. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/pyproject.toml +1 -1
  42. boulder_opal_scale_up_sdk-1.0.2/boulderopalscaleupsdk/device/defcal.py +0 -62
  43. boulder_opal_scale_up_sdk-1.0.2/boulderopalscaleupsdk/device/device.py +0 -41
  44. boulder_opal_scale_up_sdk-1.0.2/boulderopalscaleupsdk/experiments/readout_classifier_calibration.py +0 -24
  45. boulder_opal_scale_up_sdk-1.0.2/boulderopalscaleupsdk/routines/__init__.py +0 -6
  46. boulder_opal_scale_up_sdk-1.0.2/boulderopalscaleupsdk/routines/common.py +0 -10
  47. boulder_opal_scale_up_sdk-1.0.2/boulderopalscaleupsdk/routines/resonator_mapping.py +0 -19
  48. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/LICENSE +0 -0
  49. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/README.md +0 -0
  50. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/__init__.py +0 -0
  51. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/agent/__init__.py +0 -0
  52. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/common/__init__.py +0 -0
  53. {boulder_opal_scale_up_sdk-1.0.2/boulderopalscaleupsdk/stubs → boulder_opal_scale_up_sdk-1.0.4/boulderopalscaleupsdk/device}/__init__.py +0 -0
  54. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/device/controller/__init__.py +0 -0
  55. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/device/controller/base.py +0 -0
  56. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/device/controller/quantum_machines.py +0 -0
  57. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/device/controller/resolver.py +0 -0
  58. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/grpc_interceptors/__init__.py +0 -0
  59. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/grpc_interceptors/auth.py +0 -0
  60. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/plotting/__init__.py +0 -0
  61. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/protobuf/v1/agent_pb2.py +0 -0
  62. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/protobuf/v1/agent_pb2.pyi +0 -0
  63. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/protobuf/v1/agent_pb2_grpc.py +0 -0
  64. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/protobuf/v1/device_pb2_grpc.py +0 -0
  65. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/protobuf/v1/task_pb2_grpc.py +0 -0
  66. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/py.typed +0 -0
  67. {boulder_opal_scale_up_sdk-1.0.2/boulderopalscaleupsdk/utils → boulder_opal_scale_up_sdk-1.0.4/boulderopalscaleupsdk/stubs}/__init__.py +0 -0
  68. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/stubs/dtypes.py +0 -0
  69. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/third_party/__init__.py +0 -0
  70. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/third_party/quantum_machines/__init__.py +0 -0
  71. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/third_party/quantum_machines/config.py +0 -0
  72. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/third_party/quantum_machines/constants.py +0 -0
  73. {boulder_opal_scale_up_sdk-1.0.2 → boulder_opal_scale_up_sdk-1.0.4}/boulderopalscaleupsdk/utils/serial_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: boulder-opal-scale-up-sdk
3
- Version: 1.0.2
3
+ Version: 1.0.4
4
4
  Summary: Q-CTRL Boulder Opal Scale Up Python SDK
5
5
  License: https://q-ctrl.com/terms
6
6
  Keywords: black opal,boulder opal,fire opal,nisq,open controls,q control,q ctrl,q-control,q-ctrl,qcontrol,qctrl,quantum,quantum algorithms,quantum circuits,quantum coding,quantum coding software,quantum computing,quantum control,quantum control software,quantum control theory,quantum engineering,quantum error correction,quantum firmware,quantum fundamentals,quantum sensing,qubit,qudit
@@ -27,7 +27,7 @@ from grpc import aio as grpc
27
27
  from grpc import ssl_channel_credentials
28
28
  from pydantic_settings import BaseSettings, SettingsConfigDict
29
29
 
30
- from boulderopalscaleupsdk.common.dtypes import GrpcMetadata
30
+ from boulderopalscaleupsdk.common.dtypes import GrpcMetadata, JobId
31
31
  from boulderopalscaleupsdk.protobuf.v1 import agent_pb2, task_pb2, task_pb2_grpc
32
32
 
33
33
  LOG = logging.getLogger(__name__)
@@ -178,7 +178,7 @@ class Agent:
178
178
  device_name: str,
179
179
  routine: str,
180
180
  data: Struct | None,
181
- ) -> str:
181
+ ) -> JobId | None:
182
182
  if not self._channel:
183
183
  raise RuntimeError("Cannot start session: agent is shutdown.")
184
184
  _data = any_pb2.Any()
@@ -196,16 +196,18 @@ class Agent:
196
196
  metadata=metadata,
197
197
  )
198
198
  self._state = response.target_state
199
- await self._resume(response, metadata)
199
+ job_id: JobId | None = await self._resume(response, metadata)
200
200
  await self.shutdown()
201
- return response.session_id
201
+
202
+ return job_id
202
203
 
203
204
  async def _resume(
204
205
  self,
205
206
  response: task_pb2.AgentTasksResponse,
206
207
  metadata: GrpcMetadata,
207
- ) -> None:
208
+ ) -> JobId | None:
208
209
  tasks = response.tasks
210
+ job_id = None
209
211
  while self._state not in [
210
212
  task_pb2.AGENT_STATE_SHUTDOWN_MANAGER_INITIATED,
211
213
  task_pb2.AGENT_STATE_SHUTDOWN_FAULT,
@@ -243,6 +245,9 @@ class Agent:
243
245
 
244
246
  tasks = _resp.tasks
245
247
  self._state = _resp.target_state
248
+ job_id = _resp.job_id
249
+
250
+ return job_id or response.job_id
246
251
 
247
252
  async def shutdown(
248
253
  self,
@@ -28,11 +28,13 @@ from typing import Annotated, Any, Literal, Self
28
28
 
29
29
  import numpy as np
30
30
  from dateutil.parser import isoparse
31
- from pydantic import BeforeValidator, ConfigDict, Field, PlainSerializer, TypeAdapter
31
+ from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, PlainSerializer, TypeAdapter
32
32
  from pydantic.dataclasses import dataclass
33
33
 
34
34
  GrpcMetadata = list[tuple[str, str | bytes]]
35
35
 
36
+ JobId = str
37
+
36
38
 
37
39
  class BaseType: ...
38
40
 
@@ -315,6 +317,17 @@ class JobHistorySortOrder(enum.Enum):
315
317
  CREATED_AT_ASC = 2
316
318
 
317
319
 
320
+ class JobSummary(BaseModel):
321
+ id: str
322
+ name: str
323
+ device_name: str
324
+ session_id: str
325
+ created_at: ISO8601DatetimeUTCLike
326
+
327
+ def __str__(self):
328
+ return f"JobSummary(name = {self.name}, id = {self.id})"
329
+
330
+
318
331
  DEFAULT_JOB_HISTORY_PAGE = 1
319
332
  DEFAULT_JOB_HISTORY_PAGE_SIZE = 10
320
333
  DEFAULT_JOB_HISTORY_SORT_ORDER = JobHistorySortOrder.CREATED_AT_DESC
@@ -83,3 +83,25 @@ class Combine(Protocol[T]):
83
83
  return destination
84
84
 
85
85
  return Combine.create(_combine)
86
+
87
+
88
+ class Eq(Protocol[T]):
89
+ @abstractmethod
90
+ def eq(self, first: T, second: T) -> bool: ...
91
+
92
+ def eq_option(self, first: T | None, second: T | None) -> bool:
93
+ match first, second:
94
+ case None, None:
95
+ return True
96
+ case _ if first is not None and second is not None:
97
+ return self.eq(first, second)
98
+ case _:
99
+ return False
100
+
101
+ @classmethod
102
+ def create(cls, eq_fn: Callable[[T, T], bool]) -> "Eq[T]":
103
+ class _Eq(Eq):
104
+ def eq(self, first: T, second: T) -> bool:
105
+ return eq_fn(first, second)
106
+
107
+ return _Eq()
@@ -23,9 +23,6 @@ ExtraType = JsonDict | Callable[[JsonDict], None] | None
23
23
  ComponentRef = str
24
24
 
25
25
 
26
- class Processor(BaseModel): ...
27
-
28
-
29
26
  class Component(BaseModel, Generic[TraitT]):
30
27
  traits: list[TraitT]
31
28
 
@@ -34,6 +34,7 @@ class ProcessorArchitecture(str, Enum):
34
34
  Superconducting = "superconducting"
35
35
 
36
36
 
37
+ # TODO: Remove this in the next release of SDK
37
38
  class DeviceInfo(BaseModel):
38
39
  controller_info: QBLOXControllerInfo | QuantumMachinesControllerInfo
39
40
  processor: SuperconductingProcessor # | OtherSDKProcessorType
@@ -18,6 +18,7 @@ QBLOX Quantum Control Stack
18
18
  __all__ = (
19
19
  "DEFAULT_MODULE_CONSTRAINTS",
20
20
  "AcquisitionConfig",
21
+ "BitStrideArrayEncoding",
21
22
  "ChannelType",
22
23
  "ComplexChannel",
23
24
  "IndexedData",
@@ -40,15 +41,19 @@ __all__ = (
40
41
  "SequenceProgram",
41
42
  "SequencerAddr",
42
43
  "SequencerAddrType",
44
+ "SequencerResults",
45
+ "process_sequencer_output",
43
46
  "validate_channel",
44
47
  )
45
48
 
46
49
  import dataclasses
47
50
  import enum
51
+ import math
48
52
  import re
49
53
  from dataclasses import dataclass
50
54
  from typing import Annotated, Any, ClassVar, Literal, Self, TypeVar
51
55
 
56
+ import numpy as np
52
57
  from pydantic import BaseModel, BeforeValidator, Field, PlainSerializer, model_validator
53
58
 
54
59
  from boulderopalscaleupsdk.device.controller.base import Backend, ControllerType
@@ -373,6 +378,7 @@ class SequenceProgram(BaseModel):
373
378
  weights: dict[str, IndexedData] = {}
374
379
  acquisitions: dict[str, AcquisitionConfig] = {}
375
380
  acquisition_scopes: list[str] = []
381
+ acquisition_shapes: dict[str, tuple[int, ...]] = {}
376
382
  params: SequencerParams = SequencerParams()
377
383
  params_only: bool = False
378
384
 
@@ -417,6 +423,14 @@ class PreparedProgram(BaseModel):
417
423
  modules: dict[ModuleAddrType, PreparedModule] # The set of modules this program will target.
418
424
  sequence_programs: dict[str, PreparedSequenceProgram] # The individual element programs.
419
425
 
426
+ @property
427
+ def sequencers(self) -> dict[SequencerAddr, str]:
428
+ return {psp.sequencer_addr: name for name, psp in self.sequence_programs.items()}
429
+
430
+ def get_sequencer_program(self, seq_addr: SequencerAddr) -> SequenceProgram:
431
+ prog_name = self.sequencers[seq_addr]
432
+ return self.sequence_programs[prog_name].sequence_program
433
+
420
434
  def dumps(self) -> str:
421
435
  return self.model_dump_json()
422
436
 
@@ -428,6 +442,9 @@ class PreparedProgram(BaseModel):
428
442
  # ==================================================================================================
429
443
  # Results
430
444
  # ==================================================================================================
445
+ MAX_ACQUISITION_BINS = 131072
446
+
447
+
431
448
  class OutputScopedAcquisitionData(BaseModel): # pragma: no cover
432
449
  """
433
450
  Scoped acquisition data for a single path in `OutputScopedAcquisition`.
@@ -544,6 +561,118 @@ class OutputIndexedAcquisition(BaseModel): # pragma: no cover
544
561
  OutputSequencerAcquisitions = dict[str, OutputIndexedAcquisition] # pragma: no cover
545
562
 
546
563
 
564
+ @dataclass
565
+ class SequencerResults:
566
+ """
567
+ Sequencer results formatted as a complex signal.
568
+
569
+ The real component corresponds to results on path0, whilst the imaginary component corresponds
570
+ to the results on path1.
571
+ """
572
+
573
+ scopes: dict[str, np.ndarray]
574
+ bins: dict[str, np.ndarray]
575
+
576
+
577
+ def process_sequencer_output(
578
+ program: SequenceProgram,
579
+ output: OutputSequencerAcquisitions,
580
+ ) -> SequencerResults:
581
+ """
582
+ Process the output from executing a sequencer into a simplified SequencerResults data structure.
583
+
584
+ Parameters
585
+ ----------
586
+ program: SequenceProgram
587
+ The corresponding program that was executed
588
+ output: OutputSequencerAcquisitions
589
+ The results of one sequencer's execution
590
+
591
+ Returns
592
+ -------
593
+ SequencerResults
594
+ """
595
+ bins = {}
596
+ scopes = {}
597
+ for acq_ref, acq_result in output.items():
598
+ acquisition = acq_result.acquisition
599
+
600
+ raw_bin = acquisition.bins.integration
601
+ shape = program.acquisition_shapes.get(acq_ref)
602
+ if shape is None or len(shape) == 1:
603
+ bins[acq_ref] = np.array(raw_bin.path0) + 1j * np.array(raw_bin.path1)
604
+ else:
605
+ bse = BitStrideArrayEncoding.from_desired(shape)
606
+ bins[acq_ref] = bse.decode(raw_bin.path0) + 1j * bse.decode(raw_bin.path1)
607
+
608
+ raw_scope = acquisition.scope
609
+ if acq_ref in program.acquisition_scopes:
610
+ scopes[acq_ref] = np.array(raw_scope.path0.data) + 1j * np.array(raw_scope.path1.data)
611
+
612
+ return SequencerResults(scopes=scopes, bins=bins)
613
+
614
+
615
+ @dataclass
616
+ class BitStrideArrayEncoding:
617
+ """
618
+ Encode a multi-dimensional array such that each dimensional index occupies an integer number of
619
+ bits (the bit-stride).
620
+
621
+ In this encoding, a linear index is calculated by left-shifting each element in the index by its
622
+ corresponding bit-stride:
623
+
624
+ linear_index = Σ (ii << bb) ∀ (ii, bb) ∈ zip(index, bit_strides)
625
+
626
+ Examples
627
+ --------
628
+ Given a desired_shape of (3, 5), the first index will have a bit-stride of 2 and the second
629
+ index will have a bit-stride of 3. To determine the linear index for a sample, we will use:
630
+
631
+ def linear_index(idx0: int, idx1: int) -> int:
632
+ return (idx0 << 2) + (idx1 << 3)
633
+
634
+ Note, the encoded data will occupy 2^2 * 2^3 samples and have (2^2 * 2^3 - 3 * 5) unused data
635
+ points.
636
+
637
+ >>> bse = BitStrideArrayEncoding.from_desired((3, 5))
638
+ >>> bse.encoded_shape
639
+ (4, 8)
640
+ >>> bse.bit_stride
641
+ (2, 3)
642
+ """
643
+
644
+ desired_shape: tuple[int, ...]
645
+ encoded_shape: tuple[int, ...]
646
+ bit_stride: tuple[int, ...]
647
+
648
+ @staticmethod
649
+ def _round_power2_32bit(val: int) -> int:
650
+ val -= 1
651
+ val |= val >> 1
652
+ val |= val >> 2
653
+ val |= val >> 4
654
+ val |= val >> 8
655
+ val |= val >> 16
656
+ val += 1
657
+ return val
658
+
659
+ @classmethod
660
+ def from_desired(cls, desired_shape: tuple[int, ...]) -> Self:
661
+ encoded_shape = tuple(
662
+ BitStrideArrayEncoding._round_power2_32bit(dim) for dim in desired_shape
663
+ )
664
+ bit_stride = tuple(int(math.log2(dim)) for dim in encoded_shape)
665
+ return cls(
666
+ desired_shape=desired_shape,
667
+ encoded_shape=encoded_shape,
668
+ bit_stride=bit_stride,
669
+ )
670
+
671
+ def decode(self, values: list[float]) -> np.ndarray:
672
+ decoded = np.reshape(values, self.encoded_shape)
673
+ return decoded[tuple(slice(0, dim) for dim in self.desired_shape)]
674
+
675
+
547
676
  # ==================================================================================================
548
677
  # Utilities
549
678
  # ==================================================================================================
@@ -11,10 +11,13 @@
11
11
  # distributed under the License is distributed on an "AS IS" BASIS. See the
12
12
  # License for the specific language.
13
13
 
14
- __all__ = ["Device", "InvalidDevice", "InvalidDeviceComponent"]
14
+ from typing import Literal
15
15
 
16
- from boulderopalscaleupsdk.device.device import (
17
- Device,
18
- InvalidDevice,
19
- InvalidDeviceComponent,
20
- )
16
+ from pydantic import BaseModel
17
+
18
+
19
+ class DefCalData(BaseModel):
20
+ gate: str
21
+ addr: list[str]
22
+ body: str
23
+ status: Literal["calibrated", "uncalibrated"]
@@ -0,0 +1,63 @@
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
+
15
+ from pydantic import BaseModel
16
+ from pydantic.dataclasses import dataclass
17
+
18
+ from boulderopalscaleupsdk.common.dtypes import ISO8601DatetimeUTCLike
19
+ from boulderopalscaleupsdk.device.controller import (
20
+ QBLOXControllerInfo,
21
+ QuantumMachinesControllerInfo,
22
+ )
23
+ from boulderopalscaleupsdk.device.defcal import DefCalData
24
+ from boulderopalscaleupsdk.device.processor import SuperconductingProcessor
25
+
26
+
27
+ @dataclass
28
+ class EmptyDefCalData:
29
+ message: str
30
+
31
+
32
+ @dataclass
33
+ class DeviceData:
34
+ # TODO: retire DeviceInfo the next SDK release
35
+ qpu: SuperconductingProcessor # | OtherSDKProcessorType
36
+ controller_info: QBLOXControllerInfo | QuantumMachinesControllerInfo
37
+ _defcals: dict[tuple[str, tuple[str, ...]], DefCalData]
38
+
39
+ def get_defcal(self, gate: str, addr: tuple[str, ...]) -> DefCalData | EmptyDefCalData:
40
+ """
41
+ Get the defcal data for a specific gate and address alias.
42
+ """
43
+ if self._defcals == {}:
44
+ return EmptyDefCalData(message="No defcal data available in a fresh device.")
45
+ _addr = tuple(i.lower() for i in sorted(addr))
46
+ defcal = self._defcals.get((gate, _addr))
47
+ if defcal is None:
48
+ return EmptyDefCalData(
49
+ message=f"No defcal data found for gate '{gate}' and address '{_addr}'.",
50
+ )
51
+ return defcal
52
+
53
+
54
+ class DeviceSummary(BaseModel):
55
+ id: str
56
+ organization_id: str
57
+ name: str
58
+ provider: str
59
+ updated_at: ISO8601DatetimeUTCLike
60
+ created_at: ISO8601DatetimeUTCLike
61
+
62
+ def __str__(self):
63
+ return f"DeviceSummary(name = {self.name}, id = {self.id})"
@@ -14,7 +14,6 @@
14
14
  __all__ = [
15
15
  "CalibrationThresholds",
16
16
  "ComponentParameter",
17
- "DurationComponentParameter",
18
17
  "FloatComponentParameter",
19
18
  "SuperconductingProcessor",
20
19
  "SuperconductingProcessorTemplate",
@@ -24,7 +23,6 @@ __all__ = [
24
23
  from .common import (
25
24
  CalibrationThresholds,
26
25
  ComponentParameter,
27
- DurationComponentParameter,
28
26
  FloatComponentParameter,
29
27
  update_parameter,
30
28
  )
@@ -25,7 +25,7 @@ from boulderopalscaleupsdk.common.dtypes import (
25
25
  Frequency,
26
26
  ISO8601DatetimeUTCLike,
27
27
  )
28
- from boulderopalscaleupsdk.common.typeclasses import Combine
28
+ from boulderopalscaleupsdk.common.typeclasses import Combine, Eq
29
29
 
30
30
  T = TypeVar("T")
31
31
  T2 = TypeVar("T2")
@@ -97,23 +97,40 @@ class ComponentParameter(Generic[T]):
97
97
  @staticmethod
98
98
  def combine(
99
99
  combine_instance: Combine[T],
100
+ eq_instance: Eq[T],
100
101
  ) -> "Combine[ComponentParameter[T]]":
101
102
  def _combine(
102
103
  first: ComponentParameter[T],
103
104
  other: ComponentParameter[Any],
104
105
  ) -> ComponentParameter[T]:
106
+ _value = combine_instance.combine(first.value, other.value)
107
+ _err_minus = combine_instance.combine_option(
108
+ first.err_minus,
109
+ other.err_minus,
110
+ )
111
+ _err_plus = combine_instance.combine_option(
112
+ first.err_plus,
113
+ other.err_plus,
114
+ )
115
+ _calibration_status = other.calibration_status
116
+
117
+ _updated_at: ISO8601DatetimeUTCLike | None
118
+ if (
119
+ not eq_instance.eq(_value, first.value)
120
+ or not eq_instance.eq_option(_err_minus, first.err_minus)
121
+ or not eq_instance.eq_option(_err_plus, first.err_plus)
122
+ or _calibration_status != first.calibration_status
123
+ ):
124
+ _updated_at = datetime.now(tz=UTC)
125
+ else:
126
+ _updated_at = first.updated_at
127
+
105
128
  return ComponentParameter(
106
- value=combine_instance.combine(first.value, other.value),
107
- err_minus=combine_instance.combine_option(
108
- first.err_minus,
109
- other.err_minus,
110
- ),
111
- err_plus=combine_instance.combine_option(
112
- first.err_plus,
113
- other.err_plus,
114
- ),
115
- calibration_status=other.calibration_status,
116
- updated_at=datetime.now(tz=UTC),
129
+ value=_value,
130
+ err_minus=_err_minus,
131
+ err_plus=_err_plus,
132
+ calibration_status=_calibration_status,
133
+ updated_at=_updated_at,
117
134
  )
118
135
 
119
136
  return Combine[ComponentParameter[T]].create(_combine)
@@ -127,8 +144,13 @@ class ComponentParameter(Generic[T]):
127
144
  updated_at=self.updated_at,
128
145
  )
129
146
 
130
- def merge_with(self, other: "ComponentParameter[T]", combine_value: "Combine[T]"):
131
- combined = ComponentParameter[T].combine(combine_value).combine(self, other)
147
+ def merge_with(
148
+ self,
149
+ other: "ComponentParameter[T]",
150
+ combine_value: "Combine[T]",
151
+ eq_value: Eq[T],
152
+ ):
153
+ combined = ComponentParameter[T].combine(combine_value, eq_value).combine(self, other)
132
154
  self.value = combined.value
133
155
  self.err_minus = combined.err_minus
134
156
  self.err_plus = combined.err_plus
@@ -178,7 +200,7 @@ class ComponentParameter(Generic[T]):
178
200
  ):
179
201
  self.calibration_status = _get_calibration_status_from_thresholds(
180
202
  value=to_float(self.value),
181
- confidence_interval=to_float(self.err_plus) - to_float(self.err_minus),
203
+ confidence_interval=to_float(self.err_plus) + to_float(self.err_minus),
182
204
  calibration_thresholds=calibration_thresholds,
183
205
  )
184
206
  self.updated_at = datetime.now(UTC)
@@ -250,8 +272,3 @@ FloatComponentParameter = Annotated[
250
272
  ComponentParameter[float],
251
273
  BeforeValidator(lambda value: ComponentParameter.from_value(value, float)),
252
274
  ]
253
-
254
- DurationComponentParameter = Annotated[
255
- ComponentParameter[Duration],
256
- BeforeValidator(lambda value: ComponentParameter.from_value(value, Duration)),
257
- ]
@@ -23,11 +23,9 @@ from boulderopalscaleupsdk.common.dtypes import (
23
23
  from boulderopalscaleupsdk.device.common import (
24
24
  Component,
25
25
  ComponentRef,
26
- Processor,
27
26
  )
28
27
  from boulderopalscaleupsdk.device.processor import (
29
28
  ComponentParameter,
30
- DurationComponentParameter,
31
29
  FloatComponentParameter,
32
30
  )
33
31
 
@@ -37,23 +35,23 @@ class Transmon(Component[Literal["tunable"]]):
37
35
  dtype: Literal["transmon"] = "transmon"
38
36
  traits: list[Literal["tunable"]] = Field(default=["tunable"])
39
37
 
40
- freq_01: FloatComponentParameter | None = Field(
41
- default=None,
38
+ freq_01: FloatComponentParameter = Field(
39
+ default=ComponentParameter(value=(0.0)),
42
40
  json_schema_extra={"display": {"label": "freq_01", "unit": "MHz", "scale": 1e-6}},
43
41
  )
44
- anharm: FloatComponentParameter | None = Field(
45
- default=None,
42
+ anharm: FloatComponentParameter = Field(
43
+ default=ComponentParameter(value=0.0),
46
44
  json_schema_extra={"display": {"label": "anharm", "unit": "MHz", "scale": 1e-6}},
47
45
  )
48
- t1: DurationComponentParameter = Field(
46
+ t1: ComponentParameter[Duration] = Field(
49
47
  default=ComponentParameter(value=Duration(0, TimeUnit.NS)),
50
48
  json_schema_extra={"display": {"label": "t1", "unit": "µs", "scale": 1e6}},
51
49
  )
52
- t2: DurationComponentParameter = Field(
50
+ t2: ComponentParameter[Duration] = Field(
53
51
  default=ComponentParameter(value=Duration(0, TimeUnit.NS)),
54
52
  json_schema_extra={"display": {"label": "t2", "unit": "µs", "scale": 1e6}},
55
53
  )
56
- t2_echo: DurationComponentParameter = Field(
54
+ t2_echo: ComponentParameter[Duration] = Field(
57
55
  default=ComponentParameter(value=Duration(0, TimeUnit.NS)),
58
56
  json_schema_extra={"display": {"label": "t2_echo", "unit": "µs", "scale": 1e6}},
59
57
  )
@@ -65,15 +63,23 @@ class Transmon(Component[Literal["tunable"]]):
65
63
  default=ComponentParameter(value=0.0),
66
64
  json_schema_extra={"display": {"label": "sx_vp", "unit": "V", "scale": 1}},
67
65
  )
66
+ x_ef_vp: FloatComponentParameter = Field(
67
+ default=ComponentParameter(value=0.0),
68
+ json_schema_extra={"display": {"label": "x_ef_vp", "unit": "V", "scale": 1}},
69
+ )
70
+ sx_ef_vp: FloatComponentParameter = Field(
71
+ default=ComponentParameter(value=0.0),
72
+ json_schema_extra={"display": {"label": "sx_ef_vp", "unit": "V", "scale": 1}},
73
+ )
68
74
 
69
75
  # Tunable transmon parameters.
70
76
  dc_bias: FloatComponentParameter = Field(
71
77
  default=ComponentParameter(value=0.0),
72
78
  json_schema_extra={"display": {"label": "dc_bias", "unit": "V", "scale": 1}},
73
79
  )
74
- fmax_bias: FloatComponentParameter = Field(
80
+ bias_offset: FloatComponentParameter = Field(
75
81
  default=ComponentParameter(value=0.0),
76
- json_schema_extra={"display": {"label": "fmax_bias", "unit": "V", "scale": 1}},
82
+ json_schema_extra={"display": {"label": "bias_offset", "unit": "V", "scale": 1}},
77
83
  )
78
84
  bias_period: FloatComponentParameter = Field(
79
85
  default=ComponentParameter(value=0.0),
@@ -85,20 +91,20 @@ class Resonator(Component[Literal["readout"]]):
85
91
  dtype: Literal["resonator"] = "resonator"
86
92
  traits: list[Literal["readout"]] = Field(default=["readout"])
87
93
 
88
- frequency_high: FloatComponentParameter | None = Field(
89
- default=None,
94
+ frequency_high: FloatComponentParameter = Field(
95
+ default=ComponentParameter(value=0.0),
90
96
  json_schema_extra={"display": {"label": "frequency_high", "unit": "MHz", "scale": 1e-6}},
91
97
  )
92
- frequency_low: FloatComponentParameter | None = Field(
93
- default=None,
98
+ frequency_low: FloatComponentParameter = Field(
99
+ default=ComponentParameter(value=0.0),
94
100
  json_schema_extra={"display": {"label": "frequency_low", "unit": "MHz", "scale": 1e-6}},
95
101
  )
96
- kappa_low: FloatComponentParameter | None = Field(
97
- default=None,
102
+ kappa_low: FloatComponentParameter = Field(
103
+ default=ComponentParameter(value=0.0),
98
104
  json_schema_extra={"display": {"label": "kappa_low", "unit": "MHz", "scale": 1e-6}},
99
105
  )
100
- kappa_high: FloatComponentParameter | None = Field(
101
- default=None,
106
+ kappa_high: FloatComponentParameter = Field(
107
+ default=ComponentParameter(value=0.0),
102
108
  json_schema_extra={"display": {"label": "kappa_high", "unit": "MHz", "scale": 1e-6}},
103
109
  )
104
110
  vp_low: FloatComponentParameter = Field(
@@ -128,9 +134,9 @@ class Coupler(Component[Literal["tunable"]]):
128
134
  default=ComponentParameter(value=0.0),
129
135
  json_schema_extra={"display": {"label": "dc_bias", "unit": "V", "scale": 1}},
130
136
  )
131
- fmax_bias: FloatComponentParameter = Field(
137
+ bias_offset: FloatComponentParameter = Field(
132
138
  default=ComponentParameter(value=0.0),
133
- json_schema_extra={"display": {"label": "fmax_bias", "unit": "V", "scale": 1}},
139
+ json_schema_extra={"display": {"label": "bias_offset", "unit": "V", "scale": 1}},
134
140
  )
135
141
  bias_period: FloatComponentParameter = Field(
136
142
  default=ComponentParameter(value=0.0),
@@ -182,7 +188,7 @@ class SuperconductingProcessorTemplate(BaseModel):
182
188
  )
183
189
 
184
190
 
185
- class SuperconductingProcessor(Processor):
191
+ class SuperconductingProcessor(BaseModel):
186
192
  nodes: dict[ComponentRef, SuperconductingComponentType]
187
193
  edges: list[Edge]
188
194