iqm-pulse 9.21.0__py3-none-any.whl → 10.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.
@@ -25,10 +25,10 @@
25
25
  from __future__ import annotations
26
26
 
27
27
  import abc
28
- from collections.abc import Iterable
28
+ from collections.abc import Iterable, Mapping
29
29
  import dataclasses
30
30
  import re
31
- from typing import TYPE_CHECKING, Any, TypeAlias, get_type_hints
31
+ from typing import TYPE_CHECKING, Any, TypeAlias, final, get_type_hints
32
32
  from uuid import uuid4
33
33
 
34
34
  import numpy as np
@@ -36,7 +36,7 @@ import numpy as np
36
36
  from exa.common.data.parameter import Parameter, Setting
37
37
  from exa.common.data.setting_node import SettingNode
38
38
  from exa.common.qcm_data.chip_topology import ChipTopology
39
- from iqm.pulse.playlist.instructions import Instruction
39
+ from iqm.pulse.playlist.instructions import IQPulse
40
40
  from iqm.pulse.playlist.schedule import Schedule
41
41
  from iqm.pulse.timebox import TimeBox
42
42
  from iqm.pulse.utils import map_waveform_param_types
@@ -81,23 +81,31 @@ PROBE_LINES_LOCUS_MAPPING = "probe_lines"
81
81
  class GateImplementation(abc.ABC):
82
82
  """ABC for implementing quantum gates and other quantum operations using instruction schedules.
83
83
 
84
- There is a separate GateImplementation subclass for every implementation of every operation type.
84
+ Every implementation of every operation type can have its own GateImplementation subclass.
85
85
  Each GateImplementation instance represents a particular locus for that implementation, and encapsulates
86
86
  the calibration data it requires.
87
87
 
88
- All GateImplementation subclasses ``__init__`` must have exactly the below arguments in order to be
88
+ All GateImplementation subclasses :meth:`__init__` must have exactly the below arguments in order to be
89
89
  usable via :meth:`.ScheduleBuilder.get_implementation`.
90
90
 
91
- GateImplementations also have the :meth:`~GateImplementation.__call__` method, which takes the operation parameters
92
- (e.g. rotation angles) as input, and returns a :class:`.TimeBox` instance which implements
93
- an instance of the operation at that locus.
91
+ GateImplementation also has the :meth:`__call__` method, which takes the operation parameters
92
+ (e.g. rotation angles) as input, and builds, caches and then returns a :class:`.TimeBox` instance which
93
+ implements an instance of the operation at that locus. :meth:`__call__` normally should not be
94
+ reimplemented by the subclasses, instead they should define :meth:`_call` which contains the actual
95
+ implementation without the caching logic.
96
+
97
+ Even though it is possible, GateImplementations *should not* use other gates to implement themselves,
98
+ unless they are subclasses of :class:`CompositeGate`. This is to encapsulate the calibration data better,
99
+ and avoid unpredictable dependencies between gates.
94
100
 
95
101
  Args:
96
102
  parent: Quantum operation this instance implements.
97
103
  name: Name of the implementation provided by this instance.
98
104
  locus: Locus the operation acts on.
99
- calibration_data: (Raw) calibration data for the (operation, implementation, locus) represented by this instance
100
- builder: Schedule builder.
105
+ calibration_data: (Raw) calibration data for the (operation, implementation, locus) represented
106
+ by this instance.
107
+ builder: Schedule builder instance that can be used to access system properties (and in the
108
+ case of :class:`CompositeGate`, other gates).
101
109
 
102
110
  """
103
111
 
@@ -120,8 +128,13 @@ class GateImplementation(abc.ABC):
120
128
  self.locus = locus
121
129
  self.calibration_data = calibration_data
122
130
  self.builder = builder
123
- self.id = str(uuid4()) # unique str identifier, needed for certain caching properties
131
+ self.id = str(uuid4())
132
+ """Unique str identifier, needed for certain caching properties."""
133
+ self.sub_implementations: dict[str, GateImplementation] = {}
134
+ """Single-component sub-implementations for factorizable gates with len(locus) > 1, otherwise empty.
135
+ At least one of self.sub_implementations and self.calibration_data is always empty."""
124
136
  self._timebox_cache: dict[tuple[Any, ...], TimeBox | list[TimeBox]] = {}
137
+ """Cache for :meth:`__call__` results."""
125
138
 
126
139
  @property
127
140
  def qualified_name(self) -> str:
@@ -130,13 +143,38 @@ class GateImplementation(abc.ABC):
130
143
 
131
144
  @classmethod
132
145
  def needs_calibration(cls) -> bool:
133
- """Whether the implementation needs calibration data
146
+ """True iff the implementation needs calibration data.
147
+
148
+ Returns:
149
+ True iff :attr:`OpCalibrationDataTree` must contain a node ``f"{self.parent.name}.{self.name}.{self.locus}``
150
+ for the calibration data this instance needs.
134
151
 
135
- Returns True if the calibration dict must contain a node with keyed with
136
- <operation name>: <implementation name>: <appropriate locus> in order to use this implementation.
137
152
  """
138
153
  return bool(cls.parameters)
139
154
 
155
+ @classmethod
156
+ def optional_calibration_keys(cls) -> tuple[str, ...]:
157
+ """Optional calibration data keys for this class, in addition to the required items in :attr:`parameters`.
158
+
159
+ These keys are not required to be present in :attr:`OILCalibrationData` when validating it.
160
+
161
+ Returns:
162
+ Optional top-level calibration data keys.
163
+
164
+ """
165
+ return ()
166
+
167
+ @classmethod
168
+ def may_have_calibration(cls) -> bool:
169
+ """True iff the implementation may have calibration data.
170
+
171
+ Returns:
172
+ True iff :attr:`OpCalibrationDataTree` may contain a node ``"{self.parent.name}.{self.name}.{self.locus}``
173
+ for the calibration data of this instance.
174
+
175
+ """
176
+ return cls.needs_calibration() or bool(cls.optional_calibration_keys())
177
+
140
178
  def __call__(self, *args, **kwargs) -> TimeBox | list[TimeBox]:
141
179
  """TimeBox that implements a quantum operation.
142
180
 
@@ -171,26 +209,32 @@ class GateImplementation(abc.ABC):
171
209
  """
172
210
  return NotImplementedError # type: ignore[return-value]
173
211
 
174
- def build(
175
- self, op_name: str, locus: Locus, impl_name: str | None = None, strict_locus: bool = False
212
+ @final
213
+ @classmethod
214
+ def construct_factorizable(
215
+ cls,
216
+ parent: QuantumOp,
217
+ name: str,
218
+ locus: Locus,
219
+ builder: ScheduleBuilder,
220
+ sub_implementations: dict[str, GateImplementation],
176
221
  ) -> GateImplementation:
177
- """Utility method for constructing a GateImplementation with ``self.builder``.
178
-
179
- Inheriting classes may override this in order to add additional logic.
180
-
181
- Args:
182
- op_name: operation name
183
- locus: locus the operation acts on
184
- impl_name: implementation name. Uses the assigned default implementation if not specified.
185
- strict_locus: iff False, for non-symmetric implementations of symmetric ops the locus order may
186
- be changed if no calibration data is available for the requested locus order
187
-
188
- Returns:
189
- Calibrated gate implementation.
222
+ """Construct an implementation for a factorizable operation.
190
223
 
224
+ Instead of calibration data this method is given ``sub_implementations``, which contains single-qubit
225
+ implementations for all the components in ``locus``.
191
226
  """
192
- return self.builder.get_implementation(op_name, locus, impl_name, strict_locus=strict_locus)
227
+ impl = cls(
228
+ parent=parent,
229
+ name=name,
230
+ locus=locus,
231
+ calibration_data={},
232
+ builder=builder,
233
+ )
234
+ impl.sub_implementations = sub_implementations
235
+ return impl
193
236
 
237
+ @final
194
238
  def to_timebox(self, schedule: Schedule) -> TimeBox:
195
239
  """Wraps the given instruction schedule into an atomic/resolved timebox."""
196
240
  return TimeBox.atomic(
@@ -207,22 +251,30 @@ class GateImplementation(abc.ABC):
207
251
  """
208
252
  raise NotImplementedError
209
253
 
254
+ @final
210
255
  @classmethod
211
256
  def convert_calibration_data(
212
257
  cls,
213
258
  calibration_data: OILCalibrationData,
214
259
  params: NestedParams,
215
260
  channel_props: ChannelProperties,
261
+ *,
216
262
  duration: float | None = None,
263
+ _top_level: bool = True,
217
264
  ) -> OILCalibrationData:
218
- """Convert time-like items in the calibration data to fractions of the time duration of the gate.
265
+ """Convert time- and frequency-like items in the calibration data to fractions of the time duration of the gate.
219
266
 
220
267
  This is a convenience method for converting calibration data items involving time
221
- durations measured in seconds into fractions of the duration of the gate.
268
+ durations measured in seconds and frequencies measured in Hz into fractions of the duration
269
+ of the gate, e.g. to be used to parameterize :class:`.Waveform` classes.
222
270
 
223
271
  * Values of items that are not measured in seconds or Hz are returned as is.
224
- * Additionally, converts ``duration`` to channel samples and adds it in the converted
225
- calibration data under the key ``"n_samples"``, while the original ``"duration"`` key is removed.
272
+ * Items named ``duration`` get special treatment for convenience.
273
+ They are not included in the converted data.
274
+ If the ``duration`` parameter is None, there must be a ``"duration"`` item at the top level in
275
+ ``calibration_data`` whose value will be used instead.
276
+ * The ``duration`` parameter is converted to channel samples and included in the converted
277
+ data under the top-level key ``"n_samples"``.
226
278
 
227
279
  Args:
228
280
  calibration_data: (subset of) calibration data for the gate/implementation/locus
@@ -243,7 +295,7 @@ class GateImplementation(abc.ABC):
243
295
  """
244
296
  if value is None:
245
297
  return None
246
- if (unit not in {"s", "Hz"}) or name == "duration":
298
+ if unit not in {"s", "Hz"}:
247
299
  return value
248
300
  if unit == "s":
249
301
 
@@ -261,27 +313,30 @@ class GateImplementation(abc.ABC):
261
313
  return conversion(value)
262
314
 
263
315
  if duration is None:
264
- # duration should be found in the outermost dict
316
+ # if not given, duration should be found in the outermost dict
265
317
  duration = calibration_data["duration"]
266
318
  if duration is None:
267
319
  raise ValueError(f"Duration for {cls.__name__} has not been set.")
268
- converted = {"n_samples": channel_props.duration_to_int_samples(duration) if duration > 0 else 0}
269
- else:
270
- converted = {}
320
+
321
+ # n_samples will only be included on the top level
322
+ converted = (
323
+ {"n_samples": channel_props.duration_to_int_samples(duration) if duration > 0 else 0} if _top_level else {}
324
+ )
271
325
 
272
326
  for p_name, p in params.items():
273
- if p_name in calibration_data:
327
+ if p_name in calibration_data and p_name != "duration":
328
+ # duration is not included in the converted data
274
329
  data = calibration_data[p_name]
275
330
  if isinstance(p, Setting | Parameter):
276
331
  value = convert(p_name, p.unit, data, duration)
277
332
  else:
278
333
  # recursion for nested parameter dicts
279
- value = cls.convert_calibration_data(data, p, channel_props, duration=duration)
280
- if p_name != "duration": # duration is converted to n_samples so we don't want it in seconds here
281
- converted[p_name] = value
334
+ value = cls.convert_calibration_data(data, p, channel_props, duration=duration, _top_level=False)
335
+ converted[p_name] = value
282
336
 
283
337
  return converted
284
338
 
339
+ @final
285
340
  @classmethod
286
341
  def get_parameters(cls, locus: Iterable[str], path: Iterable[str] = ()) -> SettingNode:
287
342
  """Calibration data tree the GateImplementation subclass expects for each locus.
@@ -342,7 +397,7 @@ class GateImplementation(abc.ABC):
342
397
 
343
398
  @classmethod
344
399
  def get_custom_locus_mapping(
345
- cls, chip_topology: ChipTopology, component_to_channels: dict[str, Iterable[str]]
400
+ cls, chip_topology: ChipTopology, component_to_channels: Mapping[str, Iterable[str]]
346
401
  ) -> dict[tuple[str, ...] | frozenset[str], tuple[str, ...]] | None:
347
402
  """Get custom locus mapping for this GateImplementation.
348
403
 
@@ -393,6 +448,7 @@ class CustomIQWaveforms(GateImplementation):
393
448
  self, parent: QuantumOp, name: str, locus: Locus, calibration_data: OILCalibrationData, builder: ScheduleBuilder
394
449
  ) -> None:
395
450
  super().__init__(parent, name, locus, calibration_data, builder)
451
+ # TODO why does not this check happen in __init_subclass__?
396
452
  if getattr(self, "wave_i", None) is None or getattr(self, "wave_q", None) is None:
397
453
  raise ValueError(
398
454
  "You must provide valid Waveforms for both of the arguments `wave_i` and `wave_q` when inheriting"
@@ -532,7 +588,8 @@ class SinglePulseGate(GateImplementation):
532
588
  """
533
589
  return self.builder.get_drive_channel(self.locus[0])
534
590
 
535
- def _get_pulse(self, **kwargs) -> Instruction:
591
+ @classmethod
592
+ def _get_pulse(cls, **kwargs) -> IQPulse:
536
593
  """Return pulse based on the provided calibration data."""
537
594
  raise NotImplementedError
538
595
 
@@ -543,32 +600,70 @@ class SinglePulseGate(GateImplementation):
543
600
 
544
601
 
545
602
  class CompositeGate(GateImplementation):
546
- """Utility base class for creating gate implementations that are defined in terms of other gate implementations.
547
-
548
- Gates can be implemented using other pre-existing gate implementations by just utilizing the ScheduleBuilder in
549
- :attr:`~GateImplementation.builder` in the :meth:`__call__` method (e.g. by calling
550
- ``self.builder.get_implementation(<some gate>, <some locus>)``. In this way, any such "member gates" will use
551
- the common calibration that exists in :attr:`~GateImplementation.builder`. In order for a composite gate
552
- implementation to be able to calibrate its member gates with different calibration values from the common
553
- calibration, it needs to know what gates it considers as its "members". This is what the CompositeGate ABC is for.
554
-
555
- Inheriting from this class and defining e.g. ``registered_gates = ["prx", "cz"]`` allows one to calibrate the
556
- member operations (i.e. ``"prx"`` and ``"cz"`` in this example) inside this composite gate differently from the
557
- common calibration. However, if no specific calibration data is provided, the gate implementation will be calibrated
558
- with the common calibration.
603
+ """Base class for gate implementations that are defined in terms of other gate implementations.
604
+
605
+ Composite gates can be implemented using other pre-existing gate implementations (called its
606
+ *member gates*) by using :meth:`build` in the :meth:`_call` method. You *should not* call
607
+ :meth:`ScheduleBuilder.get_implementation` directly in composite gate code.
608
+
609
+ A CompositeGate subclass needs to declare what its member gates are, e.g. to be able to
610
+ verify that they are calibrated, using the :attr:`registered_gates` class attribute.
611
+
612
+ It is possibe to calibrate (some of) the member gates separately from the common calibration,
613
+ by listing their names in :attr:`customizable_gates` class attribute.
614
+ However, if no custom calibration data is provided, the composite gate will use
615
+ the common calibration for the member operations.
616
+
617
+ .. example::
618
+
619
+ Inheriting this class and defining ``registered_gates = ("prx", "cz")``, ``customizable_gates = ("prx",)``
620
+ allows one to use ``prx`` and ``cz`` gates as member operations, and calibrate ``prx`` independently of
621
+ the common calibration.
622
+
623
+ .. note::
624
+
625
+ :meth:`CompositeGate.needs_calibration` only tells whether the implementation class itself needs
626
+ calibration data, not whether the member gates need some.
627
+
628
+ """
629
+
630
+ registered_gates: tuple[str, ...] = ()
631
+ """Names of the member operations used by the composite gate.
632
+ There must be corresponding keys in :attr:`builder.op_table`.
633
+ """
634
+
635
+ customizable_gates: tuple[str, ...] | None = None
636
+ """These member operations can be calibrated separately from their common
637
+ calibration by adding :attr:`OCalibrationData` nodes for them under the
638
+ :attr:`OILCalibrationData` node of the composite gate.
639
+ Must be a subset of :attr:`registered_gates`.
640
+ By default all member operations are customizable.
559
641
  """
560
642
 
561
- registered_gates: list[str] = []
562
- """Gates that can be calibrated separately from their common calibration existing in ``self.builder``. The gate
563
- names should correspond to the keys in ``self.builder.op_table``. Other gates besides the ones given here can
564
- also be constructed via ``self.builder``, but these will always use the common calibration."""
565
643
  default_implementations: dict[str, str] = {}
566
- """Mapping from operation names to the designated default implementation of that operation. Filling this attribute
567
- allows one to define a different default implementation from the common default in ``self.builder.op_table`` to
568
- be used in he context of this composite gate. If an operation is not found in this dict as a key, this
569
- CompositeGate will use the common default as the default implementation for it.
644
+ """Mapping from member operation names to the designated default implementation of that
645
+ operation. Filling this attribute allows one to define a different default implementation from
646
+ the common default in :attr:`builder.op_table` to be used in the context of this composite
647
+ gate. If a member operation is not found in this dict as a key, the CompositeGate will use the
648
+ common default as its default implementation.
570
649
  """
571
650
 
651
+ def __init_subclass__(cls):
652
+ if not cls.registered_gates:
653
+ # this would be pointless
654
+ raise ValueError(f"CompositeGate {cls.__name__} has no registered gates.")
655
+ # TODO we should also check that customizable_gates may_have_calibration (otherwise it's pointless
656
+ # to call them customizable), but we don't currently have access to their implementation classes here...
657
+ if cls.customizable_gates is None:
658
+ cls.customizable_gates = cls.registered_gates
659
+ elif not set(cls.customizable_gates) <= set(cls.registered_gates):
660
+ raise ValueError(f"CompositeGate {cls.__name__}: customizable_gates must be a subset of registered_gates.")
661
+
662
+ @classmethod
663
+ def optional_calibration_keys(cls) -> tuple[str, ...]:
664
+ # Custom calibration data for member gates is optional. Affects may_have_calibration.
665
+ return cls.customizable_gates or ()
666
+
572
667
  def __init__(
573
668
  self,
574
669
  parent: QuantumOp,
@@ -577,14 +672,18 @@ class CompositeGate(GateImplementation):
577
672
  calibration_data: OILCalibrationData,
578
673
  builder: ScheduleBuilder,
579
674
  ) -> None:
675
+ # validate the registered gates
676
+ for op_name in self.registered_gates:
677
+ if (op := builder.op_table.get(op_name)) is None:
678
+ raise ValueError(f"Unknown registered gate '{op_name}'.")
679
+ if parent.factorizable:
680
+ if not (op.factorizable or op.arity == 1):
681
+ raise ValueError(
682
+ f"'{parent.name}' is factorizable, but registered gate '{op_name}'"
683
+ " is neither factorizable nor arity-1."
684
+ )
685
+
580
686
  super().__init__(parent, name, locus, calibration_data, builder)
581
- custom_defaults = {
582
- op: data.get("default_implementation") for op, data in calibration_data.items() if isinstance(data, dict)
583
- }
584
- self._default_implementations = self.default_implementations.copy()
585
- for op_name, impl in custom_defaults.items():
586
- if op_name in self.registered_gates and impl is not None:
587
- self._default_implementations[op_name] = impl
588
687
 
589
688
  def __call__(self, *args, **kwargs):
590
689
  default_cache_key = tuple(args) + tuple(kwargs.items())
@@ -601,46 +700,85 @@ class CompositeGate(GateImplementation):
601
700
  return box
602
701
 
603
702
  def build(
604
- self, op_name: str, locus: Locus, impl_name: str | None = None, strict_locus: bool = False
703
+ self,
704
+ op_name: str,
705
+ locus: Locus,
706
+ impl_name: str | None = None,
707
+ *,
708
+ strict_locus: bool = False,
709
+ priority_calibration: OILCalibrationData | None = None,
605
710
  ) -> GateImplementation:
606
- """Construct a member gate implementation.
711
+ """Construct an implementation for a member (registered) gate.
607
712
 
608
- If the gate ``op_name`` is registered, a specific calibration for it in the context of this CompositeGate
609
- will be sought for from ``self.builder.calibration``. If any (non-empty) calibration values are found in
610
- ``self.builder.calibration[self.name][op_name][<impl_name>]`` they will be merged to the common calibration
611
- (only non-empty values will be merged). If there are no values found, the
612
- common calibration will be used.
713
+ A custom calibration for ``op_name`` will be sought in :attr:`calibration_data`.
714
+ If any calibration parameters are found, they override the corresponding parameters
715
+ in the common calibration data.
613
716
 
614
717
  Args:
615
- op_name: operation name
718
+ op_name: member operation name
616
719
  locus: locus the operation acts on
617
720
  impl_name: Implementation name. If not given, uses the default implementation defined in the class instance
618
- if any, and otherwise the common default in ``self.builder.op_table``
721
+ if any, and otherwise the common default in :attr:`builder.op_table`.
619
722
  strict_locus: iff False, for non-symmetric implementations of symmetric ops the locus order may
620
723
  be changed if no calibration data is available for the requested locus order
724
+ priority_calibration: If given, overrides the custom calibration for the member gate. Deprecated,
725
+ should not be used.
621
726
 
622
727
  Returns:
623
- Calibrated gate implementation.
728
+ Calibrated member gate implementation.
624
729
 
625
730
  """
626
- impl_name = impl_name or self._default_implementations.get(op_name, None)
627
- composite_calibration = None
628
- if op_name in self.registered_gates:
629
- # if op is registered and needs calibration, find the cal data under the CompositeGate
630
- impl_class = self.builder.get_implementation_class(op_name, impl_name)
631
- if impl_class.needs_calibration() or (
632
- isinstance(impl_class, CompositeGate) and impl_class.registered_gates
633
- ):
634
- try:
635
- composite_calibration = self.builder.get_calibration(self.parent.name, self.name, self.locus)
636
- except ValueError:
637
- pass
731
+ # FIXME remove priority_calibration, it's a HACK to make a single current use case work
732
+ # (MOVE_NCZ_MOVE_Composite in exa-core).
733
+ if op_name not in self.registered_gates:
734
+ raise ValueError(f"'{op_name}' not found in registered_gates.")
735
+
736
+ op = self.builder.op_table[op_name]
737
+
738
+ # implementation to use: given or class default
739
+ impl_name = impl_name or self.default_implementations.get(op_name, None)
740
+ # or, finally, the global default
741
+ impl_name, locus = self.builder._find_implementation_and_locus(
742
+ op,
743
+ impl_name=impl_name,
744
+ locus=locus,
745
+ strict_locus=strict_locus,
746
+ )
747
+
748
+ def get_custom_oi(cal_impl: GateImplementation) -> OICalibrationData:
749
+ """Return the custom calibration data node for the requested member op/implementation in
750
+ the calibration data tree of the given implementation, or an empty dict if it does not exist.
751
+ """
752
+ return cal_impl.calibration_data.get(op_name, {}).get(impl_name, {})
753
+
754
+ if priority_calibration is None:
755
+ # Find the custom cal data for the member op (if allowed and present).
756
+ if op_name in (self.customizable_gates or ()):
757
+ impl_class = self.builder.get_implementation_class(op_name, impl_name)
758
+ if impl_class.may_have_calibration():
759
+ if self.sub_implementations:
760
+ # self is a len(locus) > 1 factorizable CompositeGate (e.g. reset).
761
+ # It has 1-qubit subimplementations that have their own cal data.
762
+ # It may only have factorizable or arity-1 member ops.
763
+ if op.factorizable:
764
+ # Combine the custom cal datas scattered in the sub_implementations.
765
+ # priority calibration for factorizable ops is OICalibrationData
766
+ priority_calibration = {}
767
+ for c in locus:
768
+ priority_calibration |= get_custom_oi(self.sub_implementations[c]) # type: ignore[arg-type]
769
+ else:
770
+ priority_calibration = get_custom_oi(self.sub_implementations[locus[0]]).get(locus)
771
+ else:
772
+ # self has normal cal data
773
+ oi = get_custom_oi(self)
774
+ priority_calibration = oi if op.factorizable else oi.get(locus) # type: ignore
775
+
638
776
  return self.builder.get_implementation(
639
777
  op_name,
640
778
  locus,
641
779
  impl_name=impl_name,
642
780
  strict_locus=strict_locus,
643
- priority_calibration=composite_calibration,
781
+ priority_calibration=priority_calibration,
644
782
  use_priority_order=True,
645
783
  )
646
784
 
@@ -652,17 +790,21 @@ class CompositeCache:
652
790
  cache ``GateImplementation._timebox_cache`` as composites can include any gates in their calls, and we cannot trust
653
791
  that the cache is flushed correctly just based on if the composite itself has its own calibration data changed
654
792
  (we would have to flush also when any of the composite's members get new calibration, and this cannot consistently
655
- be deduced). For this reason, CompositeCache will be flushed whenever ANY gate implementation gets new calibration
656
- data.
793
+ be deduced). For this reason, the CompositeCache is located in the ScheduleBuilder class, and will be flushed
794
+ whenever *any* gate implementation gets new calibration data.
657
795
  """
658
796
 
659
797
  def __init__(self) -> None:
660
798
  self._cache: dict[tuple[Any, ...], TimeBox] = {}
661
799
 
662
800
  def set(
663
- self, gate_implementation: GateImplementation, cache_key: tuple[Any, ...], timebox: TimeBox, extra_id: str = ""
801
+ self,
802
+ gate_implementation: GateImplementation,
803
+ cache_key: tuple[Any, ...],
804
+ timebox: TimeBox,
805
+ extra_id: str = "",
664
806
  ) -> None:
665
- """Set a TimeBox into the cache.
807
+ """Store a TimeBox in the cache.
666
808
 
667
809
  Args:
668
810
  gate_implementation: gate implementation that created the TimeBox.