iqm-pulse 12.6.1__py3-none-any.whl → 12.7.1__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
@@ -15,8 +15,7 @@
15
15
 
16
16
  from __future__ import annotations
17
17
 
18
- from collections import defaultdict
19
- from collections.abc import Iterable
18
+ from collections.abc import Iterable, Sequence
20
19
  import copy
21
20
  from dataclasses import dataclass, field, replace
22
21
  import itertools
@@ -56,6 +55,7 @@ from iqm.pulse.playlist.instructions import (
56
55
  Instruction,
57
56
  IQPulse,
58
57
  MultiplexedIQPulse,
58
+ ReadoutMetrics,
59
59
  ReadoutTrigger,
60
60
  RealPulse,
61
61
  ThresholdStateDiscrimination,
@@ -492,6 +492,10 @@ class ScheduleBuilder:
492
492
  """Probe line channel for the probe line ``component`` belongs to.
493
493
 
494
494
  See :meth:`.get_drive_channel`.
495
+
496
+ Args:
497
+ component: name of a QPU component (typically qubit) to probe
498
+
495
499
  """
496
500
  probe_line = self.chip_topology.component_to_probe_line.get(component, None)
497
501
  if probe_line is None:
@@ -1038,7 +1042,7 @@ class ScheduleBuilder:
1038
1042
 
1039
1043
  def timeboxes_to_front_padded_playlist(
1040
1044
  self, boxes: Iterable[TimeBox], *, neighborhood: int = 0
1041
- ) -> tuple[Playlist, dict[str, set[str]]]:
1045
+ ) -> tuple[Playlist, ReadoutMetrics]:
1042
1046
  """Temporary helper function, for converting a sequence of TimeBoxes to a Playlist.
1043
1047
 
1044
1048
  Each individual TimeBox in ``boxes`` is resolved into a Schedule, and then
@@ -1057,8 +1061,7 @@ class ScheduleBuilder:
1057
1061
  blocks only the defined locus components and any other components which have occupied channels.
1058
1062
 
1059
1063
  Returns:
1060
- playlist that implements ``boxes`` and the mapping from readout labels to the set of used measurement
1061
- implementations (of the format ``<QuantumOp name>.<impl name>``).
1064
+ playlist that implements ``boxes`` and the readout metrics for that playlist.
1062
1065
 
1063
1066
  """
1064
1067
  schedules = [self.resolve_timebox(box, neighborhood=neighborhood).cleanup() for box in boxes]
@@ -1102,7 +1105,7 @@ class ScheduleBuilder:
1102
1105
  playlist that implements ``boxes``
1103
1106
 
1104
1107
  """
1105
- return self.build_playlist(self.timebox_to_schedule(box, neighborhood=neighborhood) for box in boxes)[0]
1108
+ return self.build_playlist([self.timebox_to_schedule(box, neighborhood=neighborhood) for box in boxes])[0]
1106
1109
 
1107
1110
  def timebox_to_schedule(
1108
1111
  self,
@@ -1453,8 +1456,8 @@ class ScheduleBuilder:
1453
1456
  schedule.append(ch, Block(T))
1454
1457
 
1455
1458
  def build_playlist( # noqa: PLR0915
1456
- self, schedules: Iterable[Schedule], finish_schedules: bool = True
1457
- ) -> tuple[Playlist, dict[str, set[str]]]:
1459
+ self, schedules: Sequence[Schedule], finish_schedules: bool = True
1460
+ ) -> tuple[Playlist, ReadoutMetrics]:
1458
1461
  """Build a playlist from a number of instruction schedules.
1459
1462
 
1460
1463
  This involves compressing the schedules so that no duplicate information
@@ -1468,8 +1471,7 @@ class ScheduleBuilder:
1468
1471
  unless some process has already finalised them before calling this function.
1469
1472
 
1470
1473
  Returns:
1471
- playlist containing the schedules and the mapping from readout labels to the set of used measurement
1472
- implementations (of the format ``<QuantumOp name>.<impl name>``).
1474
+ playlist containing the schedules and the readout metrics for this playlist.
1473
1475
 
1474
1476
  Raises:
1475
1477
  ValueError: if the schedules contain channels with non-uniform sampling rates
@@ -1492,6 +1494,7 @@ class ScheduleBuilder:
1492
1494
 
1493
1495
  pl = Playlist()
1494
1496
  mapped_instructions: dict[str, dict[int | Instruction, Any]] = {}
1497
+ readout_metrics = ReadoutMetrics(num_segments=len(schedules))
1495
1498
 
1496
1499
  def _append_to_schedule(sc_schedule: SC_Schedule, channel_name: str, instr: Instruction) -> None:
1497
1500
  """Append ``instr`` to ``sc_schedule`` into the channel``channel_name``."""
@@ -1517,15 +1520,15 @@ class ScheduleBuilder:
1517
1520
 
1518
1521
  # add the schedules in the playlist
1519
1522
 
1520
- label_to_implementation: dict[str, set[str]] = defaultdict(set)
1521
-
1522
1523
  def _map_instruction(inst: Instruction) -> sc_instructions.Instruction:
1523
1524
  """TODO only necessary until SC has been updated to use the iqm.pulse Instruction class."""
1524
1525
  operation: Any
1525
1526
 
1526
1527
  def _map_acquisition(acq: AcquisitionMethod) -> sc_instructions.AcquisitionMethod:
1527
- if acq.implementation is not None:
1528
- label_to_implementation[acq.label].add(acq.implementation)
1528
+ if isinstance(acq, TimeTrace):
1529
+ return sc_instructions.TimeTrace(
1530
+ label=acq.label, delay_samples=acq.delay_samples, duration_samples=acq.duration_samples
1531
+ )
1529
1532
  if isinstance(acq, ThresholdStateDiscrimination):
1530
1533
  return sc_instructions.ThresholdStateDiscrimination(
1531
1534
  label=acq.label,
@@ -1540,10 +1543,7 @@ class ScheduleBuilder:
1540
1543
  delay_samples=acq.delay_samples,
1541
1544
  weights=_map_instruction(acq.weights).operation,
1542
1545
  )
1543
- if isinstance(acq, TimeTrace):
1544
- return sc_instructions.TimeTrace(
1545
- label=acq.label, delay_samples=acq.delay_samples, duration_samples=acq.duration_samples
1546
- )
1546
+
1547
1547
  raise ValueError(f"Unknown AcquisitionMethod {acq}")
1548
1548
 
1549
1549
  if isinstance(inst, Wait):
@@ -1587,7 +1587,7 @@ class ScheduleBuilder:
1587
1587
  return sc_instructions.Instruction(duration_samples=int(inst.duration), operation=operation)
1588
1588
 
1589
1589
  # NOTE that there is no implicit right-alignment or equal duration for schedules, unlike in old-style playlists!
1590
- for schedule in schedules:
1590
+ for seg_idx, schedule in enumerate(schedules):
1591
1591
  finished_schedule = self._finish_schedule(schedule) if finish_schedules else schedule
1592
1592
  sc_schedule = SC_Schedule()
1593
1593
  for channel_name, segment in finished_schedule.items():
@@ -1596,6 +1596,8 @@ class ScheduleBuilder:
1596
1596
  pl.add_channel(channel)
1597
1597
  prev_wait = None
1598
1598
  for instruction in segment:
1599
+ if isinstance(instruction, ReadoutTrigger):
1600
+ readout_metrics.extend(instruction, seg_idx)
1599
1601
  # convert all NONSOLID instructions into Waits
1600
1602
  if finish_schedules and (isinstance(instruction, NONSOLID) or isinstance(instruction, Wait)):
1601
1603
  if instruction.duration > 0:
@@ -1614,7 +1616,7 @@ class ScheduleBuilder:
1614
1616
  if prev_wait:
1615
1617
  _append_to_schedule(sc_schedule, channel_name, prev_wait)
1616
1618
  pl.segments.append(sc_schedule)
1617
- return pl, label_to_implementation
1619
+ return pl, readout_metrics
1618
1620
 
1619
1621
  def _set_gate_implementation_shortcut(self, op_name: str) -> None:
1620
1622
  """Create shortcut for `self.get_implementation(<op_name>, ...)` as `self.<op_name>(...)`.
@@ -602,7 +602,7 @@ class SinglePulseGate(GateImplementation):
602
602
  return self.builder.channels[self.channel].duration_to_seconds(self.pulse.duration)
603
603
 
604
604
 
605
- def init_subclass_composite(gate_class: type[CompositeGate]) -> None:
605
+ def init_subclass_composite(gate_class: type[CompositeGate]) -> None: # noqa: D103
606
606
  if not gate_class.registered_gates:
607
607
  # this would be pointless
608
608
  raise ValueError(f"CompositeGate {gate_class.__name__} has no registered gates.")
@@ -49,7 +49,12 @@ from iqm.pulse.gates.cz import (
49
49
  from iqm.pulse.gates.default_gates import _implementation_library, _quantum_ops_library
50
50
  from iqm.pulse.gates.delay import Delay
51
51
  from iqm.pulse.gates.flux_multiplexer import FluxMultiplexer_SampleLinear
52
- from iqm.pulse.gates.measure import Measure_Constant, Measure_Constant_Qnd, Shelved_Measure_Constant
52
+ from iqm.pulse.gates.measure import (
53
+ Fast_Measure_Constant,
54
+ Measure_Constant,
55
+ Measure_Constant_Qnd,
56
+ Shelved_Measure_Constant,
57
+ )
53
58
  from iqm.pulse.gates.move import MOVE_CRF_CRF, MOVE_SLEPIAN_CRF, MOVE_TGSS_CRF
54
59
  from iqm.pulse.gates.prx import (
55
60
  Constant_PRX_with_smooth_rise_fall,
@@ -105,6 +110,7 @@ _exposed_implementations: dict[str, type[GateImplementation]] = {
105
110
  FluxPulseGate_CRF_CRF,
106
111
  Measure_Constant,
107
112
  Measure_Constant_Qnd,
113
+ Fast_Measure_Constant,
108
114
  Shelved_Measure_Constant,
109
115
  PRX_ModulatedDRAGCosineRiseFall,
110
116
  MOVE_CRF_CRF,
@@ -38,7 +38,11 @@ from iqm.pulse.gates.cz import (
38
38
  )
39
39
  from iqm.pulse.gates.delay import Delay
40
40
  from iqm.pulse.gates.flux_multiplexer import FluxMultiplexer_SampleLinear
41
- from iqm.pulse.gates.measure import Measure_Constant, Shelved_Measure_Constant
41
+ from iqm.pulse.gates.measure import (
42
+ Fast_Measure_Constant,
43
+ Measure_Constant,
44
+ Shelved_Measure_Constant,
45
+ )
42
46
  from iqm.pulse.gates.move import MOVE_CRF_CRF, MOVE_SLEPIAN_CRF, MOVE_TGSS_CRF
43
47
  from iqm.pulse.gates.prx import (
44
48
  PRX_DRAGCosineRiseFall,
@@ -68,6 +72,7 @@ _implementation_library: dict[str, dict[str, type[GateImplementation]]] = {
68
72
  "delay": {"wait": Delay},
69
73
  "measure": {
70
74
  "constant": Measure_Constant,
75
+ "fast_constant": Fast_Measure_Constant,
71
76
  },
72
77
  "measure_fidelity": {
73
78
  "constant": Measure_Constant,
@@ -32,6 +32,7 @@ from iqm.pulse.gate_implementation import (
32
32
  Locus,
33
33
  OILCalibrationData,
34
34
  )
35
+ from iqm.pulse.playlist import Schedule, Segment
35
36
  from iqm.pulse.playlist.channel import ProbeChannelProperties
36
37
  from iqm.pulse.playlist.instructions import (
37
38
  AcquisitionMethod,
@@ -44,7 +45,7 @@ from iqm.pulse.playlist.instructions import (
44
45
  TimeTrace,
45
46
  )
46
47
  from iqm.pulse.playlist.waveforms import Constant, Samples
47
- from iqm.pulse.timebox import MultiplexedProbeTimeBox, SchedulingStrategy, TimeBox
48
+ from iqm.pulse.timebox import MultiplexedProbeTimeBox, ProbeTimeBoxes, SchedulingStrategy, TimeBox
48
49
 
49
50
  if TYPE_CHECKING: # pragma: no cover
50
51
  from iqm.pulse.builder import ScheduleBuilder
@@ -53,6 +54,7 @@ if TYPE_CHECKING: # pragma: no cover
53
54
  DEFAULT_INTEGRATION_KEY = "readout.result"
54
55
  DEFAULT_TIME_TRACE_KEY = "readout.time_trace"
55
56
  FEEDBACK_KEY = "feedback"
57
+ TIMING_TOLERANCE = 1e-12
56
58
 
57
59
 
58
60
  class Measure_CustomWaveforms(CustomIQWaveforms):
@@ -301,16 +303,17 @@ class Measure_CustomWaveforms(CustomIQWaveforms):
301
303
  for c in self.locus
302
304
  ]
303
305
  probe_timebox = functools.reduce(lambda x, y: x + y, probe_timeboxes)
304
- probe_timebox.neighborhood_components[0] = copy(self._neighborhood_components)
305
- if feedback_key:
306
- # Block all the virtual channels from the probes involved in self.locus as we cannot know what AWG
307
- # might be listening to the sent bits. NOTE: No Waits are added, the channels are just blocked in
308
- # scheduling, so the impact to performance is negligible
309
- probelines = {self.builder.chip_topology.component_to_probe_line[q] for q in self.locus}
310
- for probe in probelines:
311
- probe_timebox.neighborhood_components[0].update(
312
- set(self.builder.get_virtual_feedback_channels(probe))
313
- )
306
+ if isinstance(probe_timebox, TimeBox): # FIXME: not needed once the measure implementations are cleaned up
307
+ probe_timebox.neighborhood_components[0] = copy(self._neighborhood_components)
308
+ if feedback_key:
309
+ # Block all the virtual channels from the probes involved in self.locus as we cannot know what AWG
310
+ # might be listening to the sent bits. NOTE: No Waits are added, the channels are just blocked in
311
+ # scheduling, so the impact to performance is negligible
312
+ probelines = {self.builder.chip_topology.component_to_probe_line[q] for q in self.locus}
313
+ for probe in probelines:
314
+ probe_timebox.neighborhood_components[0].update(
315
+ set(self.builder.get_virtual_feedback_channels(probe))
316
+ )
314
317
  self._multiplexed_timeboxes[args] = probe_timebox
315
318
  return self._multiplexed_timeboxes[args]
316
319
 
@@ -876,11 +879,6 @@ class ShelvedMeasureTimeBox(TimeBox):
876
879
  return super().__radd__(other)
877
880
 
878
881
 
879
- SHELVED_OFFSET_TOLERANCE = 1e-12
880
- """Tolerance for the absolute value of shelved measure ``second_prx_12_offset`` calibration value
881
- being considered zero."""
882
-
883
-
884
882
  class Shelved_Measure_CustomWaveforms(Measure_CustomWaveforms, CompositeGate):
885
883
  """Base class for shelved readout.
886
884
 
@@ -945,7 +943,7 @@ class Shelved_Measure_CustomWaveforms(Measure_CustomWaveforms, CompositeGate):
945
943
  # schedule the shelved box to get an atomic schedule
946
944
  shelved_atom = deepcopy(self.builder.resolve_timebox(shelved_box, neighborhood=0))
947
945
  offset = self.calibration_data["second_prx_12_offset"]
948
- if self.calibration_data["do_prx_12"] and abs(offset) > SHELVED_OFFSET_TOLERANCE:
946
+ if self.calibration_data["do_prx_12"] and abs(offset) > TIMING_TOLERANCE:
949
947
  drive_channel_name = self.builder.get_drive_channel(self.locus[0])
950
948
  drive_channel = self.builder.channels[drive_channel_name]
951
949
  offset_sign = offset / abs(offset)
@@ -998,3 +996,123 @@ class Shelved_Measure_Constant(Shelved_Measure_CustomWaveforms, wave_i=Constant,
998
996
 
999
997
  A measure gate implemented as a constant waveform is surrounded by two `prx_12` gates.
1000
998
  """
999
+
1000
+
1001
+ class Fast_Measure_CustomWaveforms(Measure_CustomWaveforms):
1002
+ """Measure implementation that blocks locus qubits for a shorter duration than the probes.
1003
+
1004
+ The locus qubits are blocked only for the physical probe pulse duration plus (calibratable) extra dead time that
1005
+ can be used to take into account e.g. ring down delay of waiting the readout resonator to empty itself. The probe
1006
+ channels are still blocked as in ``Measure_CustomWaveforms``, i.e. for the duration of
1007
+ ``acquisition_delay + integration_length + integration_dead_time``.
1008
+ """
1009
+
1010
+ root_parameters = Measure_CustomWaveforms.root_parameters | {
1011
+ "locus_deadtime": Setting(
1012
+ Parameter("locus_deadtime", "Locus dead time after the probe pulse", unit="s"),
1013
+ 0.0,
1014
+ ),
1015
+ }
1016
+
1017
+ def probe_timebox( # type: ignore[override]
1018
+ self, key: str = "", feedback_key: str = "", do_acquisition: bool = True, **kwargs
1019
+ ) -> ProbeTimeBoxes:
1020
+ """Otherwise the same as ``Measure_CustomWaveforms.probe_timebox``, but returns two TimeBoxes, the
1021
+ actual MultiplexedProbeTimeBox and the rest of the probe-blocking wait time in its own TimeBox. This
1022
+ allows the "tetris logic" in scheduling to block the locus qubits for a shorter duration.
1023
+ """
1024
+ args = (key, feedback_key, do_acquisition)
1025
+ if args not in self._multiplexed_timeboxes:
1026
+ if len(self.locus) == 1:
1027
+ probe = self.builder.chip_topology.component_to_probe_line[self.locus[0]]
1028
+ probe_channel = self.builder.get_probe_channel(self.locus[0])
1029
+ try:
1030
+ drive_channel = self.builder.get_drive_channel(self.locus[0])
1031
+ except KeyError:
1032
+ drive_channel = ""
1033
+
1034
+ combined_probe_timebox = super().probe_timebox(
1035
+ key, feedback_key, do_acquisition=do_acquisition, **kwargs
1036
+ )
1037
+ readout_trigger = combined_probe_timebox.atom[probe_channel][0] # type: ignore[index]
1038
+ actual_probe_duration = readout_trigger.probe_pulse.duration
1039
+
1040
+ # MultiplexedProbeTimeBox that has the minimum possible duration
1041
+ if self.calibration_data["locus_deadtime"] > TIMING_TOLERANCE:
1042
+ deadtime = self.builder.channels[probe_channel].duration_to_int_samples(
1043
+ self.calibration_data["locus_deadtime"]
1044
+ )
1045
+ else:
1046
+ deadtime = 0
1047
+ # Must be: ReadoutTrigger.duration > ReadoutTrigger.probe_pulse.duration so if they would match, we
1048
+ # need to add a minimum offset to make it hold, i.e. the smallest granularity allowed by the probe
1049
+ # channel
1050
+ probe_granularity = self.builder.channels[probe_channel].instruction_duration_granularity
1051
+ offset = max(deadtime, probe_granularity)
1052
+
1053
+ truncated_readout_trigger = replace(readout_trigger, duration=actual_probe_duration + offset)
1054
+ physical_probe_box = MultiplexedProbeTimeBox.from_readout_trigger(
1055
+ truncated_readout_trigger,
1056
+ probe_channel,
1057
+ locus_components=self.locus,
1058
+ label=f"Physical probe box of {self.__class__.__name__} on {self.locus}",
1059
+ block_channels=[drive_channel] if drive_channel else [],
1060
+ block_duration=truncated_readout_trigger.duration if drive_channel else 0,
1061
+ )
1062
+ physical_probe_box.neighborhood_components[0] = combined_probe_timebox.neighborhood_components[0].copy()
1063
+ # extra Blocks (integration etc) for the probe channel
1064
+ extra_block_duration = max(combined_probe_timebox.atom.duration - truncated_readout_trigger.duration, 0) # type:ignore[union-attr]
1065
+ virtual_extra_wait_box = TimeBox.atomic(
1066
+ Schedule({probe_channel: Segment([Block(extra_block_duration)])}),
1067
+ locus_components=[probe],
1068
+ label=f"Virtual probe extra wait box of {self.__class__.__name__} on {self.locus}",
1069
+ )
1070
+ virtual_extra_wait_box.neighborhood_components[0] = {probe}
1071
+ final_boxes = ProbeTimeBoxes([physical_probe_box, virtual_extra_wait_box])
1072
+ else:
1073
+ # FIXME: all the return types in the measure impls need to be cleaned up
1074
+ final_boxes = super().probe_timebox(key, feedback_key, do_acquisition=do_acquisition, **kwargs) # type:ignore[assignment]
1075
+ if feedback_key:
1076
+ probelines = {self.builder.chip_topology.component_to_probe_line[q] for q in self.locus}
1077
+ for probe in probelines:
1078
+ final_boxes[1].neighborhood_components[0].update(
1079
+ set(self.builder.get_virtual_feedback_channels(probe))
1080
+ )
1081
+ self._multiplexed_timeboxes[args] = final_boxes # type:ignore[assignment]
1082
+ return final_boxes # type:ignore[return-value]
1083
+ return self._multiplexed_timeboxes[args] # type:ignore[return-value]
1084
+
1085
+ def _call(self, key: str = "", feedback_key: str = "") -> list[TimeBox]: # type:ignore[override]
1086
+ """The same as ``Measure_CustomWaveforms._call``, i.e. wrap the "naked" multiplexable probe_timeboxes
1087
+ into a composite TimeBox.
1088
+ """
1089
+ probe_box, wait_box = self.probe_timebox(key, feedback_key, do_acquisition=True)
1090
+ final_box = TimeBox.composite([probe_box], label=f"Readout on {self.locus}")
1091
+ final_box.neighborhood_components[0] = final_box.children[0].neighborhood_components[0]
1092
+ return [final_box, wait_box]
1093
+
1094
+ def _get_probe_timebox_for_time_trace(self, key: str = "", feedback_key: str = "") -> TimeBox:
1095
+ """Get the probe TimeBox for TimeTrace. This is just a single MultiplexedProbeTimeBox which has the full
1096
+ probe duration (including integration). The faster logic wrt. qubit blocking is not important in the context of
1097
+ TimeTraces.
1098
+ """
1099
+ # FIXME: not needed once we align the return types of all these measure gates
1100
+ # Then we can make this method also shorter for the locus qubits
1101
+ probe_boxes = self.probe_timebox(key, feedback_key, do_acquisition=True)
1102
+ total_box = TimeBox.composite(self.probe_timebox(key, feedback_key, do_acquisition=True))
1103
+ probe_schedule = self.builder.resolve_timebox(total_box, neighborhood=0)
1104
+ atomic_probe_box = MultiplexedProbeTimeBox.atomic(
1105
+ probe_schedule,
1106
+ label=f"Time Trace atomic probe box of {self.__class__.__name__} on {self.locus}",
1107
+ locus_components=probe_boxes[0].locus_components,
1108
+ )
1109
+ atomic_probe_box.neighborhood_components[0] = probe_boxes[0].neighborhood_components[0]
1110
+ return atomic_probe_box # type:ignore[return-value]
1111
+
1112
+
1113
+ class Fast_Measure_Constant(Fast_Measure_CustomWaveforms, wave_i=Constant, wave_q=Constant): # type:ignore[call-arg]
1114
+ """Implementation of a faster measure with constant i and q waveforms.
1115
+
1116
+ Does not block the drive and flux channels of the locus qubits during the integration, but just during the probe
1117
+ pulse and extra calibrated dead time after it.
1118
+ """
@@ -15,9 +15,11 @@
15
15
 
16
16
  from __future__ import annotations
17
17
 
18
+ from collections.abc import Callable
18
19
  import dataclasses
19
20
  from dataclasses import dataclass, field
20
21
  import random
22
+ import re
21
23
 
22
24
  from iqm.pulse.playlist.waveforms import Waveform
23
25
 
@@ -271,3 +273,122 @@ class ReadoutTrigger(Instruction):
271
273
  ),
272
274
  acquisitions=self.acquisitions + other.acquisitions,
273
275
  )
276
+
277
+
278
+ @dataclass
279
+ class ReadoutMetrics:
280
+ """Aggregates the necessary readout metrics needed for return data processing."""
281
+
282
+ num_segments: int
283
+ """Number of segments in the Playlist this ReadoutMetrics object represents."""
284
+ integration_occurrences: dict[str, list[int]] = field(default_factory=dict)
285
+ """Map each integration readout label (of the format ``"<component>__<readout key>"``) to its number of occurrences
286
+ in each Playlist segment."""
287
+ timetrace_occurrences: dict[str, list[int]] = field(default_factory=dict)
288
+ """Map each time trace label (of the format ``"<component>__<readout key>"``) to its number of occurrences in each
289
+ Playlist segment."""
290
+ timetrace_lengths: dict[str, int] = field(default_factory=dict)
291
+ """Map each time trace label (of the format ``"<component>__<readout key>"``) to its number of time trace samples.
292
+ """
293
+ implementations: dict[str, set[str]] = field(default_factory=dict)
294
+ """Map each integration or time trace readout label to its implementations (of the format
295
+ ``"<measure gate name>.<implementation name>"``)."""
296
+
297
+ def extend(self, trigger: ReadoutTrigger, seg_idx: int) -> None:
298
+ """Extend the metrics with a ReadoutTrigger in a given Playlist Segment.
299
+
300
+ Args:
301
+ trigger: The readout trigger.
302
+ seg_idx: The index of the Playlist segment ``trigger`` belongs to.
303
+
304
+ """
305
+ for acq in trigger.acquisitions:
306
+ if isinstance(acq, TimeTrace):
307
+ occurrences = self.timetrace_occurrences
308
+ if acq.label in self.timetrace_lengths:
309
+ if self.timetrace_lengths[acq.label] != acq.duration_samples:
310
+ raise ValueError(f"Time trace length mismatch in label {acq.label}")
311
+ else:
312
+ self.timetrace_lengths[acq.label] = acq.duration_samples
313
+ else:
314
+ occurrences = self.integration_occurrences
315
+ if acq.label not in occurrences:
316
+ occurrences[acq.label] = [0] * self.num_segments
317
+ occurrences[acq.label][seg_idx] += 1
318
+ if acq.implementation is not None:
319
+ if acq.label not in self.implementations:
320
+ self.implementations[acq.label] = set()
321
+ self.implementations[acq.label].add(acq.implementation)
322
+
323
+ def filter_out(
324
+ self,
325
+ labels: list[str] | None = None,
326
+ components: list[str] | None = None,
327
+ keys: list[str] | None = None,
328
+ ) -> None:
329
+ """Filter out labels from the contents in self.
330
+
331
+ Args:
332
+ labels: The labels to filter out.
333
+ components: The components to filter out (all labels for these components will be removed).
334
+ keys: The keys to filter out (all components with these keys will be removed).
335
+
336
+ """
337
+ labels = labels if labels else []
338
+ components = components if components else []
339
+ keys = keys if keys else []
340
+ for field_name in ["integration_occurrences", "timetrace_occurrences", "timetrace_lengths", "implementations"]:
341
+ field = getattr(self, field_name)
342
+ field_copy = field.copy()
343
+ for label, contents in field.items():
344
+ component, key = re.split("__[_]*", label)
345
+ if component in components or key in keys or label in labels:
346
+ del field_copy[label]
347
+ setattr(self, field_name, field_copy)
348
+
349
+ def get_labels(self, integration: bool = True, timetrace: bool = True) -> set[str]:
350
+ """Get all the labels in self
351
+
352
+ Args:
353
+ integration: Whether to get integration labels.
354
+ timetrace: Whether to get time-trace labels.
355
+
356
+ Returns:
357
+ The applicable labels in self.
358
+
359
+ """
360
+ return self._get(lambda label: label, integration=integration, timetrace=timetrace)
361
+
362
+ def get_components(self, integration: bool = True, timetrace: bool = True) -> set[str]:
363
+ """Get all components in the labels in self
364
+
365
+ Args:
366
+ integration: Whether to get components from the integration labels.
367
+ timetrace: Whether to get components from the time-trace labels.
368
+
369
+ Returns:
370
+ The applicable components in self.
371
+
372
+ """
373
+ return self._get(lambda label: re.split("__[_]*", label)[0], integration=integration, timetrace=timetrace)
374
+
375
+ def get_keys(self, integration: bool = True, timetrace: bool = True) -> set[str]:
376
+ """Get all readout keys in the labels in self
377
+
378
+ Args:
379
+ integration: Whether to get readout keys from the integration labels.
380
+ timetrace: Whether to get readout keys from the time-trace labels.
381
+
382
+ Returns:
383
+ The applicable readout keys in self.
384
+
385
+ """
386
+ return self._get(lambda label: re.split("__[_]*", label)[1], integration=integration, timetrace=timetrace)
387
+
388
+ def _get(self, func: Callable, integration: bool = True, timetrace: bool = True) -> set[str]:
389
+ contents = []
390
+ if integration:
391
+ contents += [func(label) for label in self.integration_occurrences]
392
+ if timetrace:
393
+ contents += [func(label) for label in self.timetrace_occurrences]
394
+ return set(contents)
iqm/pulse/timebox.py CHANGED
@@ -122,8 +122,9 @@ class TimeBox:
122
122
  this ``TimeBox`` for any reason.
123
123
  """
124
124
 
125
- @staticmethod
125
+ @classmethod
126
126
  def composite(
127
+ cls,
127
128
  boxes: Iterable[TimeBox | Iterable[TimeBox]],
128
129
  *,
129
130
  label: str = "",
@@ -155,7 +156,7 @@ class TimeBox:
155
156
  locus_components = set.union(*(box.locus_components for box in children))
156
157
  else:
157
158
  locus_components = set()
158
- return TimeBox(
159
+ return cls(
159
160
  label=label,
160
161
  locus_components=locus_components,
161
162
  atom=None,
@@ -363,6 +364,8 @@ class MultiplexedProbeTimeBox(TimeBox):
363
364
  scheduling_algorithm=self.scheduling_algorithm,
364
365
  neighborhood_components=neighborhood_components,
365
366
  )
367
+ if isinstance(other, ProbeTimeBoxes):
368
+ return self + other[0]
366
369
  return super().__add__(other)
367
370
 
368
371
  @staticmethod
@@ -401,3 +404,61 @@ class MultiplexedProbeTimeBox(TimeBox):
401
404
  scheduling_algorithm=SchedulingAlgorithm.HARD_BOUNDARY,
402
405
  )
403
406
  return box
407
+
408
+
409
+ class ProbeTimeBoxes(list):
410
+ """Allows multiplexing lists of probe TimeBoxes with each other and ``MultiplexedProbeTimeBox``es.
411
+
412
+ The first element is a ``MultiplexedProbeTimeBox`` and the second an atomic ``TimeBox`` containing ``Block``
413
+ instructions on probe channels (corresponding to e.g. the integration time).
414
+
415
+ If ``A`` is a ``ProbeTimeBoxes`` and ``B`` is a ``ProbeTimeBoxes``, then ``A+B`` is also a ProbeTimeBoxes instance
416
+ with the timings adjusted correctly and the ``MultiplexedProbeTimeBox``es in each multiplexed together.
417
+ If ``A`` is a ``ProbeTimeBoxes`` and ``B`` is a ``MultiplexedProbeTimeBox`` where the probe instructions are
418
+ multiplexed together and the duration is ``max(A[0].duration, B.duration)`` (the addition is symmetric).
419
+ Otherwise (if ``B`` is a list of ``Timebox``es or a non-probe ``TimeBox``), ``A+B`` works like
420
+ ``TimeBox.__add__``.
421
+ """
422
+
423
+ # FIXME: does not yet work with ShelvedProbeTimeBox
424
+ # All the measure gates should probably eventually return lists of TimeBoxes (that's the only way to "tetris"
425
+ # schedule stuff) and we should extend that to cover all the use cases, including Shelved.
426
+
427
+ def __init__(self, timeboxes: list[TimeBox]):
428
+ super().__init__(timeboxes)
429
+ if len(self) != 2:
430
+ raise ValueError(
431
+ "ProbeTimeBoxes instances must have exactly two elements, the first being the "
432
+ "MultiplexedProbeTimebox of the physical probe channels and the second the "
433
+ " extra Blocks in the probe channels."
434
+ )
435
+ if not isinstance(self[0], TimeBox) or self[0].atom is None or not isinstance(self[0], MultiplexedProbeTimeBox):
436
+ raise ValueError("The first child must be an atomic MultiplexedProbeTimeBox.")
437
+ if not isinstance(self[1], TimeBox) or self[1].atom is None:
438
+ raise ValueError(
439
+ "The second child must be an atomic TimeBox containing the extra Waits in the probe channels."
440
+ )
441
+
442
+ def __add__(self, other: TimeBox | list[TimeBox]) -> TimeBox | list[TimeBox]: # type: ignore[override]
443
+ """Multiplex ``self`` with other ``ProbeTimeBoxes`` instances or with ``MultiplexedProbeTimeBox`` instances."""
444
+ if isinstance(other, ProbeTimeBoxes):
445
+ duration_self = sum(child.atom.duration for child in self)
446
+ duration_probe_self = self[0].atom.duration
447
+ duration_other = sum(child.atom.duration for child in other)
448
+ duration_probe_other = other[0].atom.duration
449
+ extra_wait_duration = max(duration_self, duration_other) - max(duration_probe_self, duration_probe_other)
450
+ extra_wait_channels = [*self[1].atom.channels(), *other[1].atom.channels()]
451
+ probe_box = self[0] + other[0]
452
+ extra_wait_box = TimeBox.atomic(
453
+ Schedule({ch: Segment([Block(extra_wait_duration)]) for ch in extra_wait_channels}),
454
+ locus_components=set(self[1].locus_components).union(other[1].locus_components),
455
+ label="Virtual probe channel Block",
456
+ )
457
+ if 0 in self[1].neighborhood_components and 0 in other[1].neighborhood_components:
458
+ extra_wait_box.neighborhood_components[0] = (
459
+ self[1].neighborhood_components[0].union(other[1].neighborhood_components[0])
460
+ )
461
+ return ProbeTimeBoxes([probe_box, extra_wait_box])
462
+ if isinstance(other, MultiplexedProbeTimeBox):
463
+ return self[0] + other
464
+ return list(self) + other
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: iqm-pulse
3
- Version: 12.6.1
3
+ Version: 12.7.1
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
@@ -204,7 +204,7 @@ License: Apache License
204
204
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
205
205
  See the License for the specific language governing permissions and
206
206
  limitations under the License.
207
- Project-URL: Documentation, https://iqm-finland.github.io/docs/iqm-pulse/
207
+ Project-URL: Documentation, https://docs.meetiqm.com/iqm-pulse/
208
208
  Project-URL: Homepage, https://pypi.org/project/iqm-pulse/
209
209
  Classifier: Development Status :: 4 - Beta
210
210
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -213,7 +213,6 @@ 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 <28,>=27
217
216
  Requires-Dist: iqm-data-definitions <3.0,>=2.18
218
217
  Requires-Dist: python-rapidjson ==1.20
219
218
  Requires-Dist: jinja2 ==3.0.3
@@ -226,7 +225,7 @@ IQM Pulse library
226
225
 
227
226
  A Python-based project for providing interface and implementations for control pulses.
228
227
 
229
- See `API documentation <https://iqm-finland.github.io/docs/iqm-pulse/>`_.
228
+ See `API documentation <https://docs.meetiqm.com/iqm-pulse/>`_.
230
229
 
231
230
  Copyright
232
231
  ---------
@@ -1,23 +1,23 @@
1
1
  iqm/pulse/__init__.py,sha256=aQhtfbLQgr_1IvbZLH1MCuf0aFomokeu7DSjxuDWZC4,978
2
2
  iqm/pulse/base_utils.py,sha256=ll3k8wbVxjHi6XcVo3PEWIjHx5QjG1C-yH218zTG8jU,2657
3
- iqm/pulse/builder.py,sha256=uL2MsAvRs1dJfe2mVNrrHEqUoBBTJlABt0KmBtB6hl0,78294
3
+ iqm/pulse/builder.py,sha256=TByM0GSq13eim2vL3I1uBMU7otzheHAIzsVYxyZXSUc,78179
4
4
  iqm/pulse/circuit_operations.py,sha256=P83gevhGcaY4DaopGKO3CHN_CnWAPfEu-MHoLkqhu5Y,24847
5
- iqm/pulse/gate_implementation.py,sha256=nyoP54-VWKx9rO8MW5ZqCRPjsMTKfJFu_6cgabxJSj4,39843
5
+ iqm/pulse/gate_implementation.py,sha256=LuQuzv45X1iAOAe2UGegu--oFdrudVPCAwvXGgt7sAE,39857
6
6
  iqm/pulse/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  iqm/pulse/quantum_ops.py,sha256=cV13V1t2rj9lgNKXgGZXMcNgEPJ7oraHL6eJJTlLvWk,14825
8
8
  iqm/pulse/scheduler.py,sha256=62gH3AcxmsgmShcmtNaiCqFLRl1Wll6Q24zIkhw_-XM,22768
9
- iqm/pulse/timebox.py,sha256=VbNgxYsXP-Plx4FAX9b2v_CThJJh9dBzFun3E5jzP7o,17249
9
+ iqm/pulse/timebox.py,sha256=qk3QkNKukbys0K8bEEP1fHGMQswCTudsagg-a9WBQhM,20904
10
10
  iqm/pulse/utils.py,sha256=UjB5AHgDOj_5nm-Rt6Ql9GJpjV3OrtwpEVzysXhQghY,7466
11
11
  iqm/pulse/validation.py,sha256=U392B1QtX2nRNAD2j1eThrim1VH18SNKGYAYUVR9_q4,10781
12
- iqm/pulse/gates/__init__.py,sha256=l07iD52FiKKQyVFwswRapZMqEE0XfILSc4aZ5sXGJo4,8811
12
+ iqm/pulse/gates/__init__.py,sha256=Rjynol8PAJ5QWeAFCaGQRfw0w3_p3MFlWE898lojWNc,8886
13
13
  iqm/pulse/gates/barrier.py,sha256=WhYV70lf4lh4Wa9UZuMk2lp9JbUQIu8lzewRC2P7pNE,2546
14
14
  iqm/pulse/gates/conditional.py,sha256=mY7b4HmznFF5VUAXrbK7tDxCP4n2pASINa3TJHjHA6o,6872
15
15
  iqm/pulse/gates/cz.py,sha256=2-LqAMG08dOReQp8K517zTkMmhDfwXhEXPqTPyjppDk,33519
16
- iqm/pulse/gates/default_gates.py,sha256=JwdrCh_Blee4mMWZ8v8ymw3Ep0fZBvw9wuaLR3rjDXA,8073
16
+ iqm/pulse/gates/default_gates.py,sha256=bmlHWWSKvzdl35YBJlYHaqLagqaYbgJSmqdspuwZpjs,8161
17
17
  iqm/pulse/gates/delay.py,sha256=nST9dY2JFp_mpKhiSfsYa5yL4hFKcNJSAyCzXjhauQg,3767
18
18
  iqm/pulse/gates/enums.py,sha256=ATwb6vZYpfgQ1gQyFPW53JyIKrdAP3FPHm6jV-t9OAk,2532
19
19
  iqm/pulse/gates/flux_multiplexer.py,sha256=sk84ItEvkx7Z0pCHt8MCCczBe7_BHNvqS4_oeNghZw8,9757
20
- iqm/pulse/gates/measure.py,sha256=3gxMjUvQrum9ZVnm8rvRHqPUQYqOCnD3IsgnrP-vECY,51968
20
+ iqm/pulse/gates/measure.py,sha256=0vD-GEcS5CUOIl7HR42ekFO2ILHT0TBcNpBB2NFKHK8,59354
21
21
  iqm/pulse/gates/move.py,sha256=siy9SBIFohknYV6zEJuUZv03cDEOiS53p9jFYm-5WAA,17470
22
22
  iqm/pulse/gates/prx.py,sha256=gWT4vEL6BZvRNOkwlxSg_V8nq7baFgh2K-V8q3bqLjI,25203
23
23
  iqm/pulse/gates/reset.py,sha256=JzUurqhncTQ3Ti9tJyQHSybSXZsGslV0K-Dz5GRliPI,7066
@@ -28,7 +28,7 @@ iqm/pulse/playlist/__init__.py,sha256=XW-7Po_vCzhkk1Js06K8P-5srPpBuPHkqpc1yhL_Zx
28
28
  iqm/pulse/playlist/channel.py,sha256=RKFaLfdL436LOw-sNvcNVhNEQrQDoCWJsN8k2p479OQ,17180
29
29
  iqm/pulse/playlist/fast_drag.py,sha256=80Knv0Q9IaGiVdc0TrHlxNi1zJtTUZvWIVO38reYcA0,19600
30
30
  iqm/pulse/playlist/hd_drag.py,sha256=JYRy3aPu0LD9y0H3UncOiWI39eV9-TARvgLonNUsaO0,13562
31
- iqm/pulse/playlist/instructions.py,sha256=z9umJqFLTsSd26EuzW-X5raWVHGpSLds9s4m8tDAV-E,10249
31
+ iqm/pulse/playlist/instructions.py,sha256=uCZX5uKO5XV8fnQQO-_GwMXKcRFsHCu7hdL3YQwBI_4,15559
32
32
  iqm/pulse/playlist/playlist.py,sha256=7MIX4iRfh2Nit8AnpNyqv5A5ZmK7JR-v201DXVL6kUk,685
33
33
  iqm/pulse/playlist/schedule.py,sha256=ToaHKgDxNxLUQalI2TAMMld-EH5Im9rZNktQkXhOyBk,18037
34
34
  iqm/pulse/playlist/waveforms.py,sha256=w5Kgpo7VJwsaCUUKOu3rf5pNmN7Jw2jVLTn3hH-k0R4,23973
@@ -39,8 +39,8 @@ iqm/pulse/playlist/visualisation/templates/static/logo.png,sha256=rbfQZ6_UEaztpY
39
39
  iqm/pulse/playlist/visualisation/templates/static/moment.min.js,sha256=4iQZ6BVL4qNKlQ27TExEhBN1HFPvAvAMbFavKKosSWQ,53324
40
40
  iqm/pulse/playlist/visualisation/templates/static/vis-timeline-graph2d.min.css,sha256=svzNasPg1yR5gvEaRei2jg-n4Pc3sVyMUWeS6xRAh6U,19837
41
41
  iqm/pulse/playlist/visualisation/templates/static/vis-timeline-graph2d.min.js,sha256=OqCqCyA6JnxPz3PGXq_P_9VuSqWptgNt5Ev3T-xjefQ,570288
42
- iqm_pulse-12.6.1.dist-info/LICENSE.txt,sha256=R6Q7eUrLyoCQgWYorQ8WJmVmWKYU3dxA3jYUp0wwQAw,11332
43
- iqm_pulse-12.6.1.dist-info/METADATA,sha256=YFmqgzW0EOBqKVCAYvmretZ9LwjM8Mq-RHJk7DrivK8,14551
44
- iqm_pulse-12.6.1.dist-info/WHEEL,sha256=y4mX-SOX4fYIkonsAGA5N0Oy-8_gI4FXw5HNI1xqvWg,91
45
- iqm_pulse-12.6.1.dist-info/top_level.txt,sha256=NB4XRfyDS6_wG9gMsyX-9LTU7kWnTQxNvkbzIxGv3-c,4
46
- iqm_pulse-12.6.1.dist-info/RECORD,,
42
+ iqm_pulse-12.7.1.dist-info/LICENSE.txt,sha256=R6Q7eUrLyoCQgWYorQ8WJmVmWKYU3dxA3jYUp0wwQAw,11332
43
+ iqm_pulse-12.7.1.dist-info/METADATA,sha256=TpcZsymL50pnJKrIgF86AZW_IZm0IiD02PD1f8V9zvI,14492
44
+ iqm_pulse-12.7.1.dist-info/WHEEL,sha256=y4mX-SOX4fYIkonsAGA5N0Oy-8_gI4FXw5HNI1xqvWg,91
45
+ iqm_pulse-12.7.1.dist-info/top_level.txt,sha256=NB4XRfyDS6_wG9gMsyX-9LTU7kWnTQxNvkbzIxGv3-c,4
46
+ iqm_pulse-12.7.1.dist-info/RECORD,,