iqm-pulse 9.21.0__py3-none-any.whl → 10.1.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.
iqm/pulse/builder.py CHANGED
@@ -40,11 +40,12 @@ from iqm.pulse.gate_implementation import (
40
40
  CompositeCache,
41
41
  GateImplementation,
42
42
  Locus,
43
+ OICalibrationData,
43
44
  OILCalibrationData,
44
45
  OpCalibrationDataTree,
45
46
  )
46
- from iqm.pulse.gates import _exposed_implementations
47
- from iqm.pulse.gates.default_gates import _default_implementations, _quantum_ops_library
47
+ from iqm.pulse.gates import _validate_implementation, get_implementation_class
48
+ from iqm.pulse.gates.default_gates import _quantum_ops_library
48
49
  from iqm.pulse.playlist.channel import ChannelProperties, ProbeChannelProperties
49
50
  from iqm.pulse.playlist.instructions import (
50
51
  AcquisitionMethod,
@@ -72,11 +73,6 @@ from iqm.pulse.scheduler import (
72
73
  extend_schedule_new,
73
74
  )
74
75
  from iqm.pulse.timebox import SchedulingAlgorithm, SchedulingStrategy, TimeBox
75
- from iqm.pulse.utils import (
76
- _process_implementations,
77
- _validate_locus_defaults,
78
- _validate_op_attributes,
79
- )
80
76
 
81
77
  logger = logging.getLogger(__name__)
82
78
 
@@ -120,6 +116,9 @@ class CircuitOperation:
120
116
  f"but {len(self.locus)} were given: {self.locus}"
121
117
  )
122
118
 
119
+ if len(self.locus) != len(set(self.locus)):
120
+ raise ValueError(f"Repeated locus components: {self.locus}.")
121
+
123
122
  if not set(op_type.params).issubset(self.args):
124
123
  raise ValueError(
125
124
  f"The '{self.name}' operation requires the arguments {op_type.params}, "
@@ -169,9 +168,9 @@ def validate_quantum_circuit(
169
168
  for op in operations:
170
169
  op.validate(op_table)
171
170
 
172
- if key := op.args.get("key"):
173
- # this is a measurement operation
174
- if key in measurement_keys:
171
+ # extra validation for specific operations
172
+ if op.name == "measure":
173
+ if (key := op.args["key"]) in measurement_keys:
175
174
  raise ValueError(f"Measurement key '{key}' is not unique.")
176
175
 
177
176
  measurement_keys.add(key)
@@ -182,36 +181,81 @@ def validate_quantum_circuit(
182
181
  raise ValueError("Circuit contains no measurements.")
183
182
 
184
183
 
185
- def build_quantum_ops(ops: dict[str, Any]) -> QuantumOpTable:
184
+ def build_quantum_ops(ops: dict[str, dict[str, Any]]) -> QuantumOpTable:
186
185
  """Builds the table of known quantum operations.
187
186
 
188
- Hardcoded default native ops table is extended by the ones in `ops`.
189
- In case of name collisions, the content of `ops` takes priority over the defaults.
187
+ Hardcoded default native ops table is extended by the ones in ``ops``.
188
+ In case of name collisions, the content of ``ops`` takes priority over the defaults.
190
189
 
191
190
  Args:
192
191
  ops: Contents of the ``gate_definitions`` section defining
193
- the quantum operations in the configuration YAML file.
194
- Modified by the function.
192
+ the quantum operations in the configuration YAML file.
193
+ Implementation names must be mapped to either exposed GateImplementation
194
+ class names, or actual GateImplementation classes.
195
+ NOTE: Modified by the function.
195
196
 
196
197
  Returns:
197
- Mapping from quantum operation name to its definition
198
+ Mapping from quantum operation names to their definitions.
199
+
200
+ Raises:
201
+ ValueError: Requested implementation class is not exposed.
202
+ ValueError: A canonical implementation name is being redefined.
203
+ ValueError: Locus default references an undefined implementation.
204
+ ValueError: Operation attributes don't match defaults or are invalid.
198
205
 
199
206
  """
200
207
  op_table = copy.deepcopy(_quantum_ops_library)
201
- for op_name, definition in ops.items():
202
- implementations_def = definition.pop("implementations", {})
203
- implementations = _process_implementations(
204
- op_name, implementations_def, _exposed_implementations, _default_implementations
205
- )
206
-
207
- if "defaults_for_locus" in definition:
208
- _validate_locus_defaults(op_name, definition, implementations)
208
+ for op_name, op_definition in ops.items():
209
+ # prepare the implementations
210
+ implementations: dict[str, type[GateImplementation]] = {}
211
+ for impl_name, impl_class_def in op_definition.pop("implementations", {}).items():
212
+ if isinstance(impl_class_def, str):
213
+ # check if the impl class name has been exposed
214
+ impl_class_name = impl_class_def
215
+ if (impl_class := get_implementation_class(impl_class_name)) is None:
216
+ raise ValueError(
217
+ f"'{op_name}': Requested implementation class '{impl_class_name}' has not been exposed."
218
+ )
219
+ elif issubclass(impl_class_def, GateImplementation):
220
+ # bit of a hack: also accept GateImplementation classes directly
221
+ impl_class = impl_class_def
222
+ impl_class_name = impl_class.__name__
223
+ else:
224
+ raise ValueError(f"{op_name}: {impl_class_def} is neither a str or a type[GateImplementation].")
225
+
226
+ # check if we are overriding a canonical implementation name for this op
227
+ _validate_implementation(op_name, impl_name, impl_class_name)
228
+ implementations[impl_name] = impl_class
229
+
230
+ # validate defaults_for_locus
231
+ defaults_for_locus: dict[Locus, str] = op_definition.pop("defaults_for_locus", {})
232
+ for locus, impl_name in defaults_for_locus.items():
233
+ if impl_name not in implementations:
234
+ raise ValueError(
235
+ f"'{op_name}': defaults_for_locus[{locus}] implementation '{impl_name}' does not "
236
+ f"appear in the implementations dict."
237
+ )
209
238
 
210
- if op_name in op_table:
211
- _validate_op_attributes(op_name, definition, op_table)
212
- op_table[op_name] = replace(op_table[op_name], implementations=implementations, **definition)
239
+ # prepare a new op, or modify the existing one
240
+ if (old_op := op_table.get(op_name)) is not None:
241
+ # known op: only some fields can be redefined, and they have been popped out already
242
+ if op_definition:
243
+ # TODO this should be an error, but there are so many old experiment.yml files in use
244
+ # that still have the old syntax that being strict about this would be disruptive.
245
+ # Now we just ignore the fields you cannot change.
246
+ logger.warning(
247
+ f"'{op_name}' is a canonical operation, which means the fields {set(op_definition)} "
248
+ "provided by the user may not be changed."
249
+ )
250
+ op_table[op_name] = replace(old_op, implementations=implementations, defaults_for_locus=defaults_for_locus)
213
251
  else:
214
- op_table[op_name] = QuantumOp(name=op_name, implementations=implementations, **definition)
252
+ # new op
253
+ op_table[op_name] = QuantumOp(
254
+ name=op_name,
255
+ implementations=implementations,
256
+ defaults_for_locus=defaults_for_locus,
257
+ **op_definition,
258
+ )
215
259
 
216
260
  return op_table
217
261
 
@@ -300,20 +344,22 @@ class ScheduleBuilder:
300
344
  raise ValueError(f"No operation found with the name {item} in ``self.op_table``.")
301
345
 
302
346
  def inject_calibration(self, partial_calibration: OpCalibrationDataTree) -> None:
303
- """Inject new calibration data, changing ``self.calibration`` after the ScheduleBuilder initialisation.
347
+ """Inject new calibration data, changing :attr:`calibration` after initialisation.
304
348
 
305
- Invalidates the gate_implementation cache for the affected operations/implementations/loci. Also invalidates
349
+ Invalidates the GateImplementation caches for the affected operations/implementations/loci. Also invalidates
306
350
  the cache for any factorizable gate implementation, if any of its locus components was affected.
307
351
 
308
352
  Args:
309
- partial_calibration: data to be injected. Must have the same structure as ``self.calibration`` but does not
353
+ partial_calibration: data to be injected. Must have the same structure as :attr:`calibration` but does not
310
354
  have to contain all operations/implementations/loci/values. Only the parts of the data that are
311
- found will be merged into ``self.calibration`` (including any ``None`` values). ``self._cache`` will
355
+ found will be merged into :attr:`calibration` (including any ``None`` values). :attr:`_cache` will
312
356
  be invalidated for the found operations/implementations/loci and only if the new calibration data
313
357
  actually differs from the previous.
314
358
 
315
359
  """
360
+ # composite gates are always flushed (though we could only flush the ones whose member gate cal is changed!)
316
361
  self.composite_cache.flush()
362
+ # merge the calibration changes
317
363
  for op, op_data in partial_calibration.items():
318
364
  for impl, impl_data in op_data.items():
319
365
  for locus, locus_data in impl_data.items():
@@ -326,13 +372,16 @@ class ScheduleBuilder:
326
372
  and locus in self._cache[op][impl]
327
373
  and _dicts_differ(prev_calibration, new_calibration)
328
374
  ):
375
+ # invalidate only the affected GateImplementations
329
376
  del self._cache[op][impl][locus]
330
377
  if self.op_table[op].factorizable:
331
- factorizable_cache = self._cache[op][impl].copy()
332
- set_locus = set(locus)
333
- for cache_locus in factorizable_cache:
334
- if set_locus.intersection(set(cache_locus)):
335
- del self._cache[op][impl][cache_locus]
378
+ # factorizable ops only have cal data for single-component loci,
379
+ # but we also need to flush all loci that include the single-component locus
380
+ locus_component = locus[0]
381
+ # dict size cannot change while you iterate over it, hence the list of keys
382
+ for cached_locus in list(self._cache[op][impl]):
383
+ if locus_component in set(cached_locus):
384
+ del self._cache[op][impl][cached_locus]
336
385
 
337
386
  def validate_calibration(self) -> None:
338
387
  """Check that the calibration data matches the known quantum operations.
@@ -568,7 +617,7 @@ class ScheduleBuilder:
568
617
  *,
569
618
  use_priority_order: bool = False,
570
619
  strict_locus: bool = False,
571
- priority_calibration: OILCalibrationData | None = None,
620
+ priority_calibration: OILCalibrationData | OICalibrationData | None = None,
572
621
  ) -> GateImplementation:
573
622
  """Provide an implementation for a quantum operation at a given locus.
574
623
 
@@ -585,10 +634,11 @@ class ScheduleBuilder:
585
634
  1. The locus-specific priority defined in ``QuantumOp.defaults_for_locus[locus]`` if any.
586
635
  2. The global priority order defined in :attr:`QuantumOp.implementations`.
587
636
  priority_calibration: Calibration data from which to load the calibration instead of the common calibration
588
- data in :attr:`calibration`. If no calibration is found for the given implementation or
589
- ``priority_calibration`` is ``None``, the common calibration is used. Any non-empty
590
- values found in ``priority_calibration`` will be merged to the common calibration. Note:
591
- using ``priority_calibration`` will prevent saving/loading via the cache.
637
+ data in :attr:`calibration`. Any non-None values found in ``priority_calibration``
638
+ will be merged to the common calibration.
639
+ For factorizable QuantumOps this is a mapping from single-qubit loci to their calibration data,
640
+ otherwise just the calibration data for a single locus.
641
+ Note: using ``priority_calibration`` will prevent caching.
592
642
 
593
643
  Returns:
594
644
  requested implementation
@@ -615,15 +665,15 @@ class ScheduleBuilder:
615
665
  strict_locus: bool = False,
616
666
  ) -> tuple[str, Locus]:
617
667
  """Find an implementation and locus for the given quantum operation instance compatible
618
- with the calibration data.
668
+ with both the calibration data and the implementation and locus requested by the caller.
619
669
 
620
670
  Args:
621
671
  op: quantum operation
622
- impl_name: Name of the implementation. ``None`` means use the highest-priority implementation for
623
- which we have calibration data.
624
- locus: locus of the operation
625
- strict_locus: iff False, for non-symmetric implementations of symmetric ops the locus order may
626
- be changed to an equivalent one if no calibration data is available for the requested locus order
672
+ impl_name: Name of the requested implementation. ``None`` means use the highest-priority
673
+ implementation for which we have calibration data.
674
+ locus: requested locus of the operation
675
+ strict_locus: Iff False, for non-symmetric implementations of symmetric ops the locus order may
676
+ be changed to an equivalent one if no calibration data is available for the requested locus order.
627
677
 
628
678
  Returns:
629
679
  chosen implementation name, locus
@@ -647,24 +697,26 @@ class ScheduleBuilder:
647
697
  calibration data available. If none can be found, returns None.
648
698
  """
649
699
  if not impl_class.needs_calibration():
700
+ # any locus is ok
701
+ # FIXME This is wrong for compositegates, see SW-1016
650
702
  return given_locus
651
703
  if op.factorizable and len(given_locus) > 1:
704
+ # check delegated to subimplementations
652
705
  return given_locus
653
706
  # find out which loci we need to check for cal data
654
707
  if op.symmetric:
708
+ # all locus orders are equivalent for perfectly calibrated symmetric ops, locus can be permuted
655
709
  if impl_class.symmetric:
656
710
  # Cal data for symmetric implementations uses always a sorted locus order.
657
711
  loci = [tuple(sort_components(given_locus))]
658
712
  elif strict_locus:
659
713
  # If the operation is symmetric but implementation is not (e.g. fast flux CZ)
660
- # the locus order can be meaningful.
714
+ # the locus order can be meaningful in practice.
661
715
  # Users must be able to request implementations for any order of the locus, which may have
662
716
  # independent cal data.
663
- # User requested this implementation, we assume they also want this particular locus.
664
717
  loci = [given_locus]
665
718
  else:
666
- # User did not request a specific implementation, so we assume they are fine
667
- # with any implementation and locus order. We pick the first locus order that has cal data.
719
+ # User did not request a strict locus order, pick the first one that has cal data.
668
720
  loci = list(itertools.permutations(given_locus))
669
721
  else:
670
722
  # For non-symmetric ops the locus is always strict.
@@ -716,12 +768,30 @@ class ScheduleBuilder:
716
768
  impl_name: str | None,
717
769
  locus: Locus,
718
770
  strict_locus: bool = False,
719
- priority_calibration: dict[str, Any] | None = None,
771
+ *,
772
+ priority_calibration: OILCalibrationData | OICalibrationData | None = None,
720
773
  ) -> GateImplementation:
721
774
  """Build a factory class for the given quantum operation, implementation and locus.
722
775
 
723
776
  The GateImplementations are built when they are first requested, and cached for later use.
724
777
 
778
+ The attributes :attr:`QuantumOp.factorizable`, :attr:`GateImplementation.needs_calibration` and whether
779
+ the implementation is a :class:`CompositeGate` interact in a nontrivial way, described in the table below.
780
+
781
+ .. list-table::
782
+ :header-rows: 1
783
+ :stub-columns: 1
784
+
785
+ * - composite / not composite
786
+ - factorizable
787
+ - not factorizable
788
+ * - needs_calibration
789
+ - not in use yet / ``measure.constant``
790
+ - ``cc_prx.prx_composite`` / ``prx.drag_crf``
791
+ * - not needs_calibration
792
+ - ``reset.conditional`` / not meaningful
793
+ - ``rz.prx_composite`` / ``rz.virtual``
794
+
725
795
  Args:
726
796
  op: quantum operation
727
797
  impl_name: Name of the implementation. ``None`` means use the highest-priority implementation for
@@ -729,10 +799,11 @@ class ScheduleBuilder:
729
799
  locus: locus of the operation
730
800
  strict_locus: iff False, for non-symmetric implementations of symmetric ops the locus order may
731
801
  be changed if no calibration data is available for the requested locus order
732
- priority_calibration: Calibration data from which to load the calibration instead of the common
733
- calibration data. Priority calibration should be either a dict of the type `OILCalibrationData`,
734
- i.e. containing the operation name, implementation name, and locus, or just a dict containing
735
- the calibration data for the locus implied by the args `op`, `impl_name` and `locus`.
802
+ priority_calibration: Calibration data node from which to load the calibration instead of the common
803
+ calibration data. Only overrides the given parameters. For this to work, ``impl_name`` should be given,
804
+ since ``priority_calibration`` is implementation-specific.
805
+ For factorizable QuantumOps this is a mapping from single-qubit loci to their calibration data,
806
+ otherwise just the calibration data for a single locus.
736
807
 
737
808
  Returns:
738
809
  requested implementation
@@ -742,45 +813,73 @@ class ScheduleBuilder:
742
813
 
743
814
  """
744
815
  new_impl_name, new_locus = self._find_implementation_and_locus(op, impl_name, locus, strict_locus=strict_locus)
745
-
746
- # use a cached factory if it exists and no priority calibration is used:
747
- if priority_calibration is None:
816
+ # use caching if no priority calibration is used
817
+ if not priority_calibration:
818
+ # use a cached factory if it exists
748
819
  op_cache = self._cache.setdefault(op.name, {})
749
820
  impl_cache = op_cache.setdefault(new_impl_name, {})
750
821
  if factory := impl_cache.get(new_locus):
751
822
  return factory
752
823
 
824
+ # find the calibration data
753
825
  impl_class = op.implementations[new_impl_name]
754
- if impl_class.needs_calibration() and (not op.factorizable or (op.factorizable and len(new_locus) == 1)):
755
- # find the calibration data
756
- cal_data = self.get_calibration(op.name, new_impl_name, new_locus)
757
- if priority_calibration:
758
- if op.name in priority_calibration:
759
- # first check if full OILCalibrationData structure was given
760
- op_data = priority_calibration.get(op.name, {})
761
- prio_data = op_data.get(new_impl_name, {}).get(new_locus, {})
762
- else:
763
- # if not assume just the data for this locus was given
764
- prio_data = priority_calibration
765
- cal_data = merge_dicts(cal_data, prio_data, merge_nones=False) if prio_data else cal_data
766
- validate_locus_calibration(cal_data, impl_class, op, new_impl_name, new_locus)
767
- elif op.factorizable and len(new_locus) > 1 and priority_calibration:
768
- # only calibration data a factorizable gate needs for a multi-qubit locus is possible prio calibration
769
- # we propagate it to the gate implementation which should take care of handling it correctly
770
- cal_data = priority_calibration
826
+ if op.factorizable and len(new_locus) > 1 and impl_class.may_have_calibration():
827
+ # E.g. measure.constant (needs_calibration), reset.conditional (composite, not needs_calibration)
828
+ # Currently there are no factorizable gates that are both composite and need calibration.
829
+ # For factorizable QuantumOps all the calibration data is for single-component loci,
830
+ # so priority_calibration is of the type OICalibrationData.
831
+ # Build (and possibly cache) the required single-component implementations, then
832
+ # use them to construct the full-locus implementation.
833
+ priority_calibration = priority_calibration or {}
834
+ factory = impl_class.construct_factorizable(
835
+ parent=op,
836
+ name=new_impl_name,
837
+ locus=new_locus,
838
+ sub_implementations={
839
+ c: self._get_implementation(
840
+ op,
841
+ new_impl_name,
842
+ (c,),
843
+ priority_calibration={(c,): c_cal} if (c_cal := priority_calibration.get((c,))) else None, # type: ignore
844
+ )
845
+ for c in new_locus
846
+ },
847
+ builder=self,
848
+ )
771
849
  else:
772
- cal_data = {}
773
-
774
- # construct the factory
775
- factory = impl_class(
776
- parent=op,
777
- name=new_impl_name,
778
- locus=new_locus,
779
- calibration_data=cal_data,
780
- builder=self,
781
- )
782
- # cache it if no priority_calibration was used
783
- if priority_calibration is None:
850
+ if impl_class.may_have_calibration():
851
+ # Either needs_calibration or is CompositeGate.
852
+ # Find the calibration data, which is all found under new_locus.
853
+ if impl_class.needs_calibration():
854
+ cal_data = self.get_calibration(op.name, new_impl_name, new_locus)
855
+ else:
856
+ # cal data optional
857
+ try:
858
+ cal_data = self.get_calibration(op.name, new_impl_name, new_locus)
859
+ except ValueError:
860
+ cal_data = {}
861
+
862
+ if priority_calibration:
863
+ if op.factorizable:
864
+ # pick out the single-component locus
865
+ priority_calibration = priority_calibration[new_locus] # type: ignore[index]
866
+ cal_data = merge_dicts(cal_data, priority_calibration, merge_nones=False)
867
+ validate_locus_calibration(cal_data, impl_class, op, new_impl_name, new_locus)
868
+ else:
869
+ # no cal data needed, e.g. rz.virtual
870
+ cal_data = {}
871
+
872
+ # construct the factory
873
+ factory = impl_class(
874
+ parent=op,
875
+ name=new_impl_name,
876
+ locus=new_locus,
877
+ calibration_data=cal_data,
878
+ builder=self,
879
+ )
880
+
881
+ # cache the factory if no priority_calibration was used
882
+ if not priority_calibration:
784
883
  impl_cache[new_locus] = factory
785
884
  return factory
786
885
 
@@ -350,11 +350,6 @@ class CircuitOperationList(list):
350
350
  qubit_names = self.qubits
351
351
  if name not in self.table:
352
352
  raise KeyError(f"QuantumOp with name {name} is not in the gate definitions table.")
353
- arity = self.table[name].arity
354
- if len(locus_indices) != len(set(locus_indices)):
355
- raise ValueError("Repeated locus indices.")
356
- if arity and arity != len(locus_indices): # arity = 0 is barrier and measure
357
- raise ValueError(f"Operation {name} has {arity=} but {len(locus_indices)} target qubits were provided.")
358
353
 
359
354
  try:
360
355
  locus = tuple(qubit_names[idx] for idx in locus_indices)