iqm-pulse 12.0.0__tar.gz → 12.2.0__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 (82) hide show
  1. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/CHANGELOG.rst +19 -0
  2. {iqm_pulse-12.0.0/src/iqm_pulse.egg-info → iqm_pulse-12.2.0}/PKG-INFO +2 -2
  3. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/requirements/base.txt +1 -1
  4. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/gate_implementation.py +18 -9
  5. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/gates/cz.py +10 -1
  6. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/gates/measure.py +188 -62
  7. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/timebox.py +27 -15
  8. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0/src/iqm_pulse.egg-info}/PKG-INFO +2 -2
  9. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm_pulse.egg-info/requires.txt +1 -1
  10. iqm_pulse-12.2.0/version.txt +1 -0
  11. iqm_pulse-12.0.0/version.txt +0 -1
  12. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/LICENSE.txt +0 -0
  13. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/MANIFEST.in +0 -0
  14. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/README.rst +0 -0
  15. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/docs/API.rst +0 -0
  16. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/docs/Makefile +0 -0
  17. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/docs/_static/.gitignore +0 -0
  18. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/docs/_static/css/custom.css +0 -0
  19. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/docs/_static/images/favicon.ico +0 -0
  20. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/docs/_static/images/feedback_timing.svg +0 -0
  21. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/docs/_static/images/logo.png +0 -0
  22. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/docs/_static/images/playlist_breakdown.svg +0 -0
  23. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/docs/_static/images/pulse_timing.svg +0 -0
  24. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/docs/_static/images/readout_timing.svg +0 -0
  25. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/docs/_templates/autosummary-class-template.rst +0 -0
  26. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/docs/_templates/autosummary-module-template.rst +0 -0
  27. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/docs/changelog.rst +0 -0
  28. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/docs/concepts.rst +0 -0
  29. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/docs/conf.py +0 -0
  30. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/docs/custom_gates.rst +0 -0
  31. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/docs/index.rst +0 -0
  32. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/docs/license.rst +0 -0
  33. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/docs/pulse_timing.rst +0 -0
  34. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/docs/references.bib +0 -0
  35. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/docs/references.rst +0 -0
  36. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/docs/using_builder.rst +0 -0
  37. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/pyproject.toml +0 -0
  38. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/requirements/base.in +0 -0
  39. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/setup.cfg +0 -0
  40. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/setup.py +0 -0
  41. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/__init__.py +0 -0
  42. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/base_utils.py +0 -0
  43. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/builder.py +0 -0
  44. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/circuit_operations.py +0 -0
  45. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/gates/__init__.py +0 -0
  46. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/gates/barrier.py +0 -0
  47. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/gates/conditional.py +0 -0
  48. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/gates/default_gates.py +0 -0
  49. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/gates/delay.py +0 -0
  50. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/gates/enums.py +0 -0
  51. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/gates/flux_multiplexer.py +0 -0
  52. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/gates/move.py +0 -0
  53. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/gates/prx.py +0 -0
  54. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/gates/reset.py +0 -0
  55. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/gates/rz.py +0 -0
  56. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/gates/sx.py +0 -0
  57. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/gates/u.py +0 -0
  58. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/playlist/__init__.py +0 -0
  59. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/playlist/channel.py +0 -0
  60. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/playlist/fast_drag.py +0 -0
  61. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/playlist/hd_drag.py +0 -0
  62. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/playlist/instructions.py +0 -0
  63. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/playlist/playlist.py +0 -0
  64. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/playlist/schedule.py +0 -0
  65. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/playlist/visualisation/__init__.py +0 -0
  66. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/playlist/visualisation/base.py +0 -0
  67. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/playlist/visualisation/templates/playlist_inspection.jinja2 +0 -0
  68. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/playlist/visualisation/templates/static/logo.png +0 -0
  69. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/playlist/visualisation/templates/static/moment.min.js +0 -0
  70. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/playlist/visualisation/templates/static/vis-timeline-graph2d.min.css +0 -0
  71. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/playlist/visualisation/templates/static/vis-timeline-graph2d.min.js +0 -0
  72. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/playlist/waveforms.py +0 -0
  73. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/py.typed +0 -0
  74. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/quantum_ops.py +0 -0
  75. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/scheduler.py +0 -0
  76. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/utils.py +0 -0
  77. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm/pulse/validation.py +0 -0
  78. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm_pulse.egg-info/SOURCES.txt +0 -0
  79. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm_pulse.egg-info/dependency_links.txt +0 -0
  80. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/src/iqm_pulse.egg-info/top_level.txt +0 -0
  81. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/tests/.pylintrc +0 -0
  82. {iqm_pulse-12.0.0 → iqm_pulse-12.2.0}/tests/__init__.py +0 -0
@@ -2,6 +2,25 @@
2
2
  Changelog
3
3
  =========
4
4
 
5
+ Version 12.2.0 (2025-09-17)
6
+ ===========================
7
+
8
+ Features
9
+ --------
10
+
11
+ - Improvements to Shelved_Measure_CustomWaveforms
12
+ - Calibration parameter `second_prx_12_offset` that allows performing the last pre_12 operation during the ReadoutTrigger (defaults to 0s -- i.e. perform the pulse immediately after the ReadoutTrigger)
13
+ - Shelved_Measure_CustomWaveforms.probe_timebox now allows multiplexing. The results from this method are multiplexable with the ones coming from the base class
14
+ - Shelved_Measure_CustomWaveforms.time_trace works
15
+
16
+ Version 12.1.0 (2025-09-12)
17
+ ===========================
18
+
19
+ Features
20
+ --------
21
+
22
+ - Update dependency on exa-common
23
+
5
24
  Version 12.0.0 (2025-09-11)
6
25
  ===========================
7
26
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: iqm-pulse
3
- Version: 12.0.0
3
+ Version: 12.2.0
4
4
  Summary: A Python-based project for providing interface and implementations for control pulses.
5
5
  Author-email: IQM Finland Oy <info@meetiqm.com>
6
6
  License: Apache License
@@ -213,7 +213,7 @@ Classifier: Intended Audience :: Science/Research
213
213
  Requires-Python: >=3.11
214
214
  Description-Content-Type: text/x-rst
215
215
  License-File: LICENSE.txt
216
- Requires-Dist: iqm-exa-common<27,>=26
216
+ Requires-Dist: iqm-exa-common<28,>=27
217
217
  Requires-Dist: iqm-data-definitions<3.0,>=2.13
218
218
  Requires-Dist: python-rapidjson==1.20
219
219
  Requires-Dist: jinja2==3.0.3
@@ -1,4 +1,4 @@
1
- iqm-exa-common>=26,<27
1
+ iqm-exa-common>=27,<28
2
2
  iqm-data-definitions >= 2.13, < 3.0
3
3
  python-rapidjson == 1.20
4
4
  jinja2 == 3.0.3
@@ -516,6 +516,9 @@ class CustomIQWaveforms(GateImplementation):
516
516
  if k not in cls.excluded_parameters
517
517
  }
518
518
  cls.parameters = root_parameters | {"i": parameters_i, "q": parameters_q}
519
+ # allow Mixins of CustomIQWaveforms and CompositeGate
520
+ if issubclass(cls, CompositeGate):
521
+ init_subclass_composite(cls)
519
522
 
520
523
 
521
524
  def get_waveform_parameters(wave: type[Waveform], label_prefix: str = "") -> dict[str, Setting | Parameter]:
@@ -599,6 +602,20 @@ class SinglePulseGate(GateImplementation):
599
602
  return self.builder.channels[self.channel].duration_to_seconds(self.pulse.duration)
600
603
 
601
604
 
605
+ def init_subclass_composite(gate_class: type[CompositeGate]) -> None:
606
+ if not gate_class.registered_gates:
607
+ # this would be pointless
608
+ raise ValueError(f"CompositeGate {gate_class.__name__} has no registered gates.")
609
+ # TODO we should also check that customizable_gates may_have_calibration (otherwise it's pointless
610
+ # to call them customizable), but we don't currently have access to their implementation classes here...
611
+ if gate_class.customizable_gates is None:
612
+ gate_class.customizable_gates = gate_class.registered_gates
613
+ elif not set(gate_class.customizable_gates) <= set(gate_class.registered_gates):
614
+ raise ValueError(
615
+ f"CompositeGate {gate_class.__name__}: customizable_gates must be a subset of registered_gates."
616
+ )
617
+
618
+
602
619
  class CompositeGate(GateImplementation):
603
620
  """Base class for gate implementations that are defined in terms of other gate implementations.
604
621
 
@@ -649,15 +666,7 @@ class CompositeGate(GateImplementation):
649
666
  """
650
667
 
651
668
  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.")
669
+ init_subclass_composite(cls)
661
670
 
662
671
  @classmethod
663
672
  def optional_calibration_keys(cls) -> tuple[str, ...]:
@@ -29,7 +29,14 @@ import numpy as np
29
29
 
30
30
  from exa.common.data.parameter import Parameter, Setting
31
31
  from exa.common.qcm_data.chip_topology import DEFAULT_2QB_MAPPING
32
- from iqm.pulse.gate_implementation import GateImplementation, Locus, OILCalibrationData, get_waveform_parameters
32
+ from iqm.pulse.gate_implementation import (
33
+ CompositeGate,
34
+ GateImplementation,
35
+ Locus,
36
+ OILCalibrationData,
37
+ get_waveform_parameters,
38
+ init_subclass_composite,
39
+ )
33
40
  from iqm.pulse.playlist.instructions import Block, FluxPulse, Instruction, IQPulse, VirtualRZ
34
41
  from iqm.pulse.playlist.schedule import Schedule
35
42
  from iqm.pulse.playlist.waveforms import (
@@ -200,6 +207,8 @@ class FluxPulseGate(GateImplementation):
200
207
  parameters["qubit"]["amplitude"] = Parameter("", "Qubit flux pulse amplitude", "")
201
208
 
202
209
  cls.parameters = root_parameters | {k: v for k, v in parameters.items() if k not in cls.excluded_parameters}
210
+ if issubclass(cls, CompositeGate):
211
+ init_subclass_composite(cls)
203
212
 
204
213
  def _call(self) -> TimeBox:
205
214
  timebox = self.to_timebox(self._schedule)
@@ -15,7 +15,8 @@ r"""Projective measurement in the Z basis."""
15
15
 
16
16
  from __future__ import annotations
17
17
 
18
- from copy import deepcopy
18
+ from collections.abc import Iterable
19
+ from copy import copy, deepcopy
19
20
  from dataclasses import replace
20
21
  import functools
21
22
  from typing import TYPE_CHECKING
@@ -26,6 +27,7 @@ from exa.common.data.parameter import CollectionType, DataType, Parameter, Setti
26
27
  from iqm.pulse.gate_implementation import (
27
28
  PROBE_LINES_LOCUS_MAPPING,
28
29
  SINGLE_COMPONENTS_WITH_READOUT_LOCUS_MAPPING,
30
+ CompositeGate,
29
31
  CustomIQWaveforms,
30
32
  Locus,
31
33
  OILCalibrationData,
@@ -33,6 +35,7 @@ from iqm.pulse.gate_implementation import (
33
35
  from iqm.pulse.playlist.channel import ProbeChannelProperties
34
36
  from iqm.pulse.playlist.instructions import (
35
37
  AcquisitionMethod,
38
+ Block,
36
39
  ComplexIntegration,
37
40
  IQPulse,
38
41
  MultiplexedIQPulse,
@@ -41,7 +44,7 @@ from iqm.pulse.playlist.instructions import (
41
44
  TimeTrace,
42
45
  )
43
46
  from iqm.pulse.playlist.waveforms import Constant, Samples
44
- from iqm.pulse.timebox import MultiplexedProbeTimeBox, TimeBox
47
+ from iqm.pulse.timebox import MultiplexedProbeTimeBox, SchedulingStrategy, TimeBox
45
48
 
46
49
  if TYPE_CHECKING: # pragma: no cover
47
50
  from iqm.pulse.builder import ScheduleBuilder
@@ -101,10 +104,13 @@ class Measure_CustomWaveforms(CustomIQWaveforms):
101
104
  ):
102
105
  super().__init__(parent, name, locus, calibration_data, builder)
103
106
 
104
- self._multiplexed_timeboxes: dict[tuple[str, str, bool], MultiplexedProbeTimeBox] = {}
107
+ self._multiplexed_timeboxes: dict[tuple[str, str, bool], TimeBox] = {}
105
108
  """Cache for :meth:`probe_timebox`."""
106
109
  self._time_traces: dict[tuple[str, float | None, float | None, str], TimeBox] = {}
107
110
  """Cache for :meth:`time_trace`."""
111
+ self._neighborhood_components: set[str] = set(self.locus) | set(
112
+ self.builder.chip_topology.component_to_probe_line[q] for q in self.locus
113
+ )
108
114
 
109
115
  if len(locus) == 1:
110
116
  # prepare the single-component measurement
@@ -220,9 +226,7 @@ class Measure_CustomWaveforms(CustomIQWaveforms):
220
226
 
221
227
  return probe_pulse, acquisition_method
222
228
 
223
- def probe_timebox(
224
- self, key: str = "", feedback_key: str = "", do_acquisition: bool = True, **kwargs
225
- ) -> MultiplexedProbeTimeBox:
229
+ def probe_timebox(self, key: str = "", feedback_key: str = "", do_acquisition: bool = True, **kwargs) -> TimeBox:
226
230
  """Returns a "naked" probe timebox that supports convenient multiplexing through
227
231
  ``MultiplexedProbeTimeBox.__add__``.
228
232
 
@@ -292,18 +296,12 @@ class Measure_CustomWaveforms(CustomIQWaveforms):
292
296
  label=f"{self.__class__.__name__} on {self.locus}",
293
297
  )
294
298
  else:
295
- # factorizability: use the sub-implementations
296
- # _skip_override used for child classes build on `Measure_CustomWaveforms` to not call `.probe_timebox`
297
- # from the parent class, but from this class instead.
298
- # TODO remove, make probe_timebox private or something.
299
299
  probe_timeboxes = [
300
- self.sub_implementations[c].probe_timebox(key, feedback_key, do_acquisition, _skip_override=True) # type: ignore[attr-defined]
300
+ self.sub_implementations[c].probe_timebox(key, feedback_key, do_acquisition) # type: ignore[attr-defined]
301
301
  for c in self.locus
302
302
  ]
303
303
  probe_timebox = functools.reduce(lambda x, y: x + y, probe_timeboxes)
304
- probe_timebox.neighborhood_components[0] = set(
305
- self.locus + (self.builder.chip_topology.component_to_probe_line[self.locus[0]],)
306
- )
304
+ probe_timebox.neighborhood_components[0] = copy(self._neighborhood_components)
307
305
  if feedback_key:
308
306
  # Block all the virtual channels from the probes involved in self.locus as we cannot know what AWG
309
307
  # might be listening to the sent bits. NOTE: No Waits are added, the channels are just blocked in
@@ -352,6 +350,11 @@ class Measure_CustomWaveforms(CustomIQWaveforms):
352
350
  final_box.neighborhood_components[0] = final_box.children[0].neighborhood_components[0]
353
351
  return final_box
354
352
 
353
+ def _get_probe_timebox_for_time_trace(self, key: str = "", feedback_key: str = "") -> TimeBox:
354
+ """Utility method that can be overridden in subclasses if they have a return type `.probe_pulse`."""
355
+ # FIXME: not needed once we align the return types of all these measure gates
356
+ return self.probe_timebox(key=key, feedback_key=feedback_key)
357
+
355
358
  def time_trace(
356
359
  self,
357
360
  key: str = "",
@@ -385,12 +388,16 @@ class Measure_CustomWaveforms(CustomIQWaveforms):
385
388
  args = (key, acquisition_delay, acquisition_duration, feedback_key)
386
389
  # additional caching for time traces since the acquisitions differ from the ones in _call
387
390
  if args not in self._time_traces:
388
- probe_timebox = deepcopy(self.probe_timebox(key=key, feedback_key=feedback_key, _skip_override=True))
391
+ probe_timebox = deepcopy(self._get_probe_timebox_for_time_trace(key, feedback_key))
389
392
  for probe_channel, segment in probe_timebox.atom.items(): # type: ignore[union-attr]
390
- readout_trigger = segment[0]
393
+ readout_trigger = None
394
+ for inst in segment:
395
+ if isinstance(inst, ReadoutTrigger):
396
+ readout_trigger = inst
397
+ break
391
398
  # TODO instead of editing the probe_timebox output contents, we should make the function itself do this
392
399
  # so we would not need to blindly search through the channels
393
- if not isinstance(readout_trigger, ReadoutTrigger):
400
+ if readout_trigger is None:
394
401
  continue
395
402
 
396
403
  probe_line = self.builder.channels[probe_channel]
@@ -807,62 +814,181 @@ class Probe_Constant(ProbePulse_CustomWaveforms_noIntegration, wave_i=Constant,
807
814
  """
808
815
 
809
816
 
810
- class Shelved_Measure_CustomWaveforms(Measure_CustomWaveforms):
817
+ class ShelvedMeasureTimeBox(TimeBox):
818
+ """TimeBox representing a shelved measurement (ReadoutTrigger sandwiched between two PRX_12 operations).
819
+
820
+ ShelvedMeasureTimeBox is a composite TimeBox containing two children:
821
+ * first one being the first PRX_12 operation for the locus components of the measure
822
+ * second one being the ReadoutTrigger (MultiplexedProbeTimeBox) that includes the second PRX_12 operation.
823
+
824
+ Multiplexing is achieved so that ShelvedMeasureTimeBoxes support ``__add__`` and ``__radd__`` operations with other
825
+ boxes of the same type and MultiplexedProbeTimeBoxes. The multiplexing operation is defined such that the
826
+ initial PRX_12 boxes are added together (in case one of the multiplexed boxes is a MultiplexedProbeTimeBoxes, the
827
+ initial PRX_12 is considered empty), and the probe boxes are multiplexed together via the logic defined in
828
+ ``MultiplexedProbeTimeBoxes.__add__``. This behaviour results in the correct timings of the associated pulses
829
+ after the multiplexing.
830
+
831
+ """
832
+
833
+ def __post_init__(self):
834
+ if len(self.children) != 2:
835
+ raise ValueError(
836
+ "ShelvedMeasureTimeBox must have exactly two children: the first one corresponding to the "
837
+ "initial prx_12 operations, and the second one to the ReadoutTrigger and the final prx_12"
838
+ )
839
+ if self.children[1].atom is None or not isinstance(self.children[1], MultiplexedProbeTimeBox):
840
+ raise ValueError("The second child must be an atomic MultiplexedProbeTimeBox.")
841
+
842
+ @property
843
+ def prx_12_box(self) -> TimeBox:
844
+ return self.children[0]
845
+
846
+ @property
847
+ def trigger_box(self) -> TimeBox:
848
+ return self.children[1]
849
+
850
+ def __add__(self, other: TimeBox | Iterable[TimeBox]) -> TimeBox:
851
+ """Add the initial PRX_12 boxes together via the ``TimeBox``"""
852
+ if isinstance(other, (ShelvedMeasureTimeBox, MultiplexedProbeTimeBox)):
853
+ if isinstance(other, ShelvedMeasureTimeBox):
854
+ prx_12_box = self.prx_12_box + other.prx_12_box
855
+ trigger_box = self.trigger_box + other.trigger_box
856
+ else:
857
+ prx_12_box = self.prx_12_box
858
+ trigger_box = self.trigger_box + other
859
+ locus_components = self.locus_components.union(other.locus_components)
860
+ multiplexed = ShelvedMeasureTimeBox(
861
+ label=f"Shelved measure on {locus_components}",
862
+ locus_components=locus_components,
863
+ atom=None,
864
+ children=(prx_12_box, trigger_box),
865
+ scheduling=self.scheduling,
866
+ scheduling_algorithm=self.scheduling_algorithm,
867
+ )
868
+ # neighborhood components by the trigger_box
869
+ multiplexed.neighborhood_components[0] = trigger_box.neighborhood_components[0]
870
+ return multiplexed
871
+ return super().__add__(other)
872
+
873
+ def __radd__(self, other: TimeBox | Iterable[TimeBox]) -> TimeBox:
874
+ return self.__add__(other)
875
+
876
+
877
+ SHELVED_OFFSET_TOLERANCE = 1e-12
878
+ """Tolerance for the absolute value of shelved measure ``second_prx_12_offset`` calibration value
879
+ being considered zero."""
880
+
881
+
882
+ class Shelved_Measure_CustomWaveforms(Measure_CustomWaveforms, CompositeGate):
811
883
  """Base class for shelved readout.
812
884
 
813
885
  Shelved readout applies a ``prx_12(pi)`` gate before and after a standard dispersive readout on each qubit measured.
814
886
  The first ``prx_12(pi)`` swaps the amplitudes of the |1> and |2> states, and the second one swaps them back after
815
- the measurement has (roughtly) collapsed the state. If the discriminator of the readout is calibrated such that
887
+ the measurement has (roughly) collapsed the state. If the discriminator of the readout is calibrated such that
816
888
  the |0> state is on one side and the |1> and |2> states are on the other, the end result is equivalent to the
817
889
  standard readout operation but with the advantage that the population in the |2> state is less susceptible to
818
890
  :math:`T_1` decay during the readout than the population in the |1> state.
819
891
 
820
- .. note:: Mixed implementation multiplexing is not supported.
821
892
  """
822
893
 
823
- # Copied from `CompositeGate` to refresh caching after any calibration changes (in particular for the `prx_12`
824
- # calibration)
825
- def __call__(self, *args, **kwargs):
826
- default_cache_key = tuple(args) + tuple(kwargs.items())
827
- try:
828
- hash(default_cache_key)
829
- key_is_hashable = True
830
- except TypeError:
831
- key_is_hashable = False
832
- if key_is_hashable:
833
- if box := self.builder.composite_cache.get(self, default_cache_key):
834
- return box
835
- box = self._call(*args, **kwargs)
836
- if key_is_hashable:
837
- self.builder.composite_cache.set(self, default_cache_key, box)
838
- return box
839
-
840
- # `probe_timebox` is needed for making certain experiments work (e.g. `MeasurementQNDness`), since they call this
841
- # function explicitly. However, the main functionality of this method will not work: Enabling mixed
842
- # implementation multiplexing. This is because the method has to return time boxes due to the `prx_12` pulses,
843
- # instead of `MultiplexedProbeTimeBox`
844
- # TODO: Enable mixed implementation multiplexing for shelved readout
845
- def probe_timebox( # type: ignore[override]
846
- self, key: str = "", feedback_key: str = "", do_acquisition: bool = True, _skip_override: bool = False
847
- ) -> TimeBox:
848
- if _skip_override:
849
- return super().probe_timebox(key, feedback_key, do_acquisition)
850
- multiplexed_timeboxes = super().probe_timebox(key, feedback_key)
851
- prx_12_impl = [self.builder.get_implementation("prx_12", [q])(np.pi) for q in self.locus]
852
-
853
- boxes = prx_12_impl + multiplexed_timeboxes + prx_12_impl # type: ignore[operator]
854
- return boxes
855
-
856
- def _call(self, key: str = "", feedback_key: str = "") -> TimeBox: # type: ignore[override]
857
- shelved_measure_box = TimeBox.composite(
858
- self.probe_timebox(key=key, feedback_key=feedback_key), # type: ignore[arg-type]
859
- label=f"Readout on {self.locus}",
860
- )
861
- shelved_measure_box.neighborhood_components[0] = shelved_measure_box.children[
862
- len(self.locus)
863
- ].neighborhood_components[0]
894
+ root_parameters = Measure_CustomWaveforms.root_parameters | {
895
+ "second_prx_12_offset": Setting(
896
+ Parameter(
897
+ "second_prx_12_offset", "Offset of the second PRX_12 pulse from the end the ReadoutTrigger", unit="s"
898
+ ),
899
+ 0.0,
900
+ ),
901
+ "do_prx_12": Setting(
902
+ Parameter(
903
+ "do_prx_12",
904
+ "Whether to do the prx_12 flips in the measure operation",
905
+ unit="",
906
+ data_type=DataType.BOOLEAN,
907
+ ),
908
+ True,
909
+ ),
910
+ }
911
+ registered_gates = ("prx_12",)
864
912
 
865
- return shelved_measure_box
913
+ def probe_timebox(self, key: str = "", feedback_key: str = "", do_acquisition: bool = True, **kwargs) -> TimeBox: # type: ignore[union-attr, override]
914
+ """Returns a "naked" probe timebox that supports convenient multiplexing through
915
+ ``ShelvedMeasureTimeBox.__add__``.
916
+
917
+ This method can be used if the user wants to control the multiplexing explicitly. Supports adding together
918
+ boxes of type :class:`.ShelvedMeasureTimeBox` and/or :class:`.MultiplexedProbeTimeBox`. See
919
+ :meth:`.ShelvedMeasureTimeBox.__add__` for more information on the logic.
920
+
921
+ Args:
922
+ key: The readout results generated on this trigger will be assigned to
923
+ ``f"{qubit}__{key}"``, where ``qubit`` goes over the component names in ``self.locus``. If empty,
924
+ the key `"readout.result"` will be used to maintain backwards compatibility.
925
+ feedback_key: The signals generated by this measure operation are routed using this key for
926
+ fast feedback purposes. See :meth:`__call__`.
927
+ do_acquisition: if False, no acquisitions are added.
928
+
929
+ Returns:
930
+ ShelvedMeasureTimeBox containing the ReadoutTrigger instruction.
931
+
932
+ """
933
+ args = (key, feedback_key, do_acquisition)
934
+ if args not in self._multiplexed_timeboxes:
935
+ if len(self.locus) == 1:
936
+ probe_timebox = super().probe_timebox(key, feedback_key, do_acquisition, **kwargs)
937
+ shelved_box = probe_timebox
938
+ prx_12_box = TimeBox.composite(
939
+ [self.build("prx_12", self.locus)(np.pi)], scheduling=SchedulingStrategy.ALAP
940
+ )
941
+ if self.calibration_data["do_prx_12"]:
942
+ shelved_box = probe_timebox + prx_12_box # type: ignore[operator, assignment, override]
943
+ # schedule the shelved box to get an atomic schedule
944
+ shelved_atom = deepcopy(self.builder.resolve_timebox(shelved_box, neighborhood=0))
945
+ offset = self.calibration_data["second_prx_12_offset"]
946
+ if self.calibration_data["do_prx_12"] and abs(offset) > SHELVED_OFFSET_TOLERANCE:
947
+ drive_channel_name = self.builder.get_drive_channel(self.locus[0])
948
+ drive_channel = self.builder.channels[drive_channel_name]
949
+ offset_sign = offset / abs(offset)
950
+ offset_in_samples = offset_sign * drive_channel.duration_to_int_samples(abs(offset))
951
+ trigger_block = shelved_atom[drive_channel_name][0]
952
+ block_with_offset = Block(trigger_block.duration + offset_in_samples)
953
+ shelved_atom[drive_channel_name]._instructions[0] = block_with_offset
954
+ trigger_box = MultiplexedProbeTimeBox(
955
+ label=f"{self.__class__.__name__} on {self.locus}",
956
+ locus_components=probe_timebox.locus_components,
957
+ atom=shelved_atom,
958
+ )
959
+ trigger_box.neighborhood_components[0] = probe_timebox.neighborhood_components[0]
960
+ pre_box = prx_12_box if self.calibration_data["do_prx_12"] else TimeBox.composite([])
961
+ final_box = ShelvedMeasureTimeBox(
962
+ label=f"Shelved Measure on {self.locus}",
963
+ locus_components=set(self.locus),
964
+ atom=None,
965
+ children=(pre_box, trigger_box),
966
+ )
967
+ final_box.neighborhood_components[0] = probe_timebox.neighborhood_components[0]
968
+ else:
969
+ # NOTE: the super call can be a bit misleading; it is actually calling the `self.probe_timebox` of len 1
970
+ # in this class inside, via the factorizable gate's sub_implementations
971
+ final_box = super().probe_timebox(key, feedback_key) # type: ignore[assignment]
972
+ self._multiplexed_timeboxes[args] = final_box
973
+ return self._multiplexed_timeboxes[args]
974
+
975
+ def _get_probe_timebox_for_time_trace(self, key: str = "", feedback_key: str = "") -> TimeBox:
976
+ """Utility method that can be overridden in subclasses if they have a return type `.probe_pulse`.
977
+
978
+ The ``ShelvedMeasureTimeBox`` resulting from :meth:`.probe_timebox` is first scheduled to obtain an atomic
979
+ ``MultiplexedProbeTimeBox`` which is wrapped into a TimeBox.
980
+ """
981
+ # FIXME: not needed once we align the return types of all these measure gates
982
+ probe_timebox = self.probe_timebox(key=key, feedback_key=feedback_key)
983
+ # resolve the box to get an atomic time_box.
984
+ probe_schedule = self.builder.resolve_timebox(probe_timebox, neighborhood=0)
985
+ atomic_probe_box = MultiplexedProbeTimeBox.atomic(
986
+ probe_schedule,
987
+ label=f"Time Trace atomic probe box of {self.__class__.__name__} on {self.locus}",
988
+ locus_components=probe_timebox.locus_components,
989
+ )
990
+ atomic_probe_box.neighborhood_components[0] = probe_timebox.neighborhood_components[0]
991
+ return atomic_probe_box
866
992
 
867
993
 
868
994
  class Shelved_Measure_Constant(Shelved_Measure_CustomWaveforms, wave_i=Constant, wave_q=Constant): # type:ignore[call-arg]
@@ -23,7 +23,7 @@ import enum
23
23
  from functools import reduce
24
24
 
25
25
  from iqm.pulse.playlist.instructions import Block, ReadoutTrigger
26
- from iqm.pulse.playlist.schedule import Schedule
26
+ from iqm.pulse.playlist.schedule import Schedule, Segment
27
27
 
28
28
 
29
29
  class SchedulingAlgorithm(enum.Enum):
@@ -164,8 +164,8 @@ class TimeBox:
164
164
  scheduling_algorithm=scheduling_algorithm,
165
165
  )
166
166
 
167
- @staticmethod
168
- def atomic(schedule: Schedule, *, locus_components: Iterable[str], label: str) -> TimeBox:
167
+ @classmethod
168
+ def atomic(cls, schedule: Schedule, *, locus_components: Iterable[str], label: str) -> TimeBox:
169
169
  """Build an atomic timebox from a schedule.
170
170
 
171
171
  Args:
@@ -177,7 +177,7 @@ class TimeBox:
177
177
  atomic timebox containing ``schedule``
178
178
 
179
179
  """
180
- return TimeBox(label=label, locus_components=set(locus_components), atom=schedule, children=())
180
+ return cls(label=label, locus_components=set(locus_components), atom=schedule, children=())
181
181
 
182
182
  def validate(self, path: tuple[str, ...] = ()) -> None:
183
183
  """Validate the contents of the TimeBox.
@@ -227,6 +227,9 @@ class TimeBox:
227
227
  A new instance containing the children of both boxes.
228
228
 
229
229
  """
230
+ if issubclass(type(other), TimeBox) and type(other) is not TimeBox: # strict subclass
231
+ # allow subclasses to override __add__ such that __radd__ also works consistent with that logic
232
+ return other.__radd__(self) # type: ignore[union-attr]
230
233
  if isinstance(other, TimeBox):
231
234
  left = self.children if self.atom is None else (self,)
232
235
  right = other.children if other.atom is None else (other,)
@@ -243,7 +246,9 @@ class TimeBox:
243
246
  except TypeError as err:
244
247
  raise TypeError(f"Cannot add a TimeBox and a {type(other)}.") from err
245
248
 
246
- def __radd__(self, other: Iterable[TimeBox]) -> TimeBox:
249
+ def __radd__(self, other: TimeBox | Iterable[TimeBox]) -> TimeBox:
250
+ if isinstance(other, TimeBox):
251
+ return self.__add__(other)
247
252
  it = iter(other)
248
253
  try:
249
254
  first = next(it)
@@ -312,21 +317,25 @@ class MultiplexedProbeTimeBox(TimeBox):
312
317
  A ``MultiplexedProbeTimeBox``'s atom contains exactly one ``ReadoutTrigger`` for each probe channel.
313
318
  """
314
319
 
320
+ def _multiplex(self, other_atom: Schedule) -> dict[str, Segment]:
321
+ new_segments = dict(self.atom.copy().items()) # type: ignore[union-attr]
322
+ for channel, segment in other_atom.items():
323
+ if channel not in new_segments:
324
+ new_segments[channel] = segment
325
+ elif isinstance(segment[0], ReadoutTrigger) and isinstance(new_segments[channel][0], ReadoutTrigger):
326
+ # multiplex the readout triggers together
327
+ new_segments[channel]._instructions[0] = new_segments[channel][0] + segment[0]
328
+ else:
329
+ new_segments[channel].extend(iter(segment))
330
+ return new_segments
331
+
315
332
  def __add__(self, other: TimeBox | Iterable[TimeBox]) -> TimeBox:
316
333
  """Override ``__add__`` for two atomic ``MultiplexedProbeTimeBox`` instances such that ``ReadoutTrigger``s
317
334
  belonging to the same probe channel are multiplexed together. Otherwise, behaves exactly like
318
335
  ``TimeBox.__add__``, returning a normal ``TimeBox``.
319
336
  """
320
337
  if isinstance(other, MultiplexedProbeTimeBox) and self.atom is not None and other.atom is not None:
321
- new_segments = dict(self.atom.copy().items())
322
- for channel, segment in other.atom.items():
323
- if channel not in new_segments:
324
- new_segments[channel] = segment
325
- elif isinstance(segment[0], ReadoutTrigger) and isinstance(new_segments[channel][0], ReadoutTrigger):
326
- # multiplex the readout triggers together
327
- new_segments[channel]._instructions[0] = new_segments[channel][0] + segment[0]
328
- else:
329
- new_segments[channel].extend(iter(segment))
338
+ new_segments = self._multiplex(other.atom)
330
339
  locus_components = self.locus_components.union(other.locus_components)
331
340
  max_nb = max(
332
341
  max(self.neighborhood_components, default=-1),
@@ -341,7 +350,7 @@ class MultiplexedProbeTimeBox(TimeBox):
341
350
  neighborhood_components[nb] = self.neighborhood_components[nb].union(
342
351
  other.neighborhood_components[nb]
343
352
  )
344
- return MultiplexedProbeTimeBox(
353
+ return type(self)(
345
354
  label=f"MultiplexedProbeTimeBox on {locus_components}",
346
355
  locus_components=locus_components,
347
356
  atom=Schedule(new_segments),
@@ -388,3 +397,6 @@ class MultiplexedProbeTimeBox(TimeBox):
388
397
  scheduling_algorithm=SchedulingAlgorithm.HARD_BOUNDARY,
389
398
  )
390
399
  return box
400
+
401
+ def __radd__(self, other: TimeBox | Iterable[TimeBox]) -> TimeBox:
402
+ return self.__add__(other)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: iqm-pulse
3
- Version: 12.0.0
3
+ Version: 12.2.0
4
4
  Summary: A Python-based project for providing interface and implementations for control pulses.
5
5
  Author-email: IQM Finland Oy <info@meetiqm.com>
6
6
  License: Apache License
@@ -213,7 +213,7 @@ Classifier: Intended Audience :: Science/Research
213
213
  Requires-Python: >=3.11
214
214
  Description-Content-Type: text/x-rst
215
215
  License-File: LICENSE.txt
216
- Requires-Dist: iqm-exa-common<27,>=26
216
+ Requires-Dist: iqm-exa-common<28,>=27
217
217
  Requires-Dist: iqm-data-definitions<3.0,>=2.13
218
218
  Requires-Dist: python-rapidjson==1.20
219
219
  Requires-Dist: jinja2==3.0.3
@@ -1,4 +1,4 @@
1
- iqm-exa-common<27,>=26
1
+ iqm-exa-common<28,>=27
2
2
  iqm-data-definitions<3.0,>=2.13
3
3
  python-rapidjson==1.20
4
4
  jinja2==3.0.3
@@ -0,0 +1 @@
1
+ 12.2.0
@@ -1 +0,0 @@
1
- 12.0.0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes