iqm-pulse 9.20.0__py3-none-any.whl → 10.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -61,11 +61,11 @@ class Measure_CustomWaveforms(CustomIQWaveforms):
61
61
  ``class MyGate(Measure_CustomWaveforms, i_wave=Something, q_wave=SomethingElse)``.
62
62
 
63
63
  The ``measure`` operation is factorizable, and its :attr:`arity` is 0, which together mean that it can operate
64
- on loci of any dimensionality, but is calibrated only on single component loci. When the gate is constructed in the
65
- ``len(locus) > 1``, case (e.g. ``builder.get_implementation('measure', ('QB1', 'QB2', 'QB3'))()``) the resulting
66
- :class:`.TimeBox` is constructed from the calibrated single-component gates.
64
+ on loci of any length, but is calibrated only on single component loci. When the gate is constructed in the
65
+ ``len(locus) > 1`` case (e.g. ``builder.get_implementation('measure', ('QB1', 'QB2', 'QB3'))()``), the resulting
66
+ :class:`.TimeBox` is constructed from the calibrated single-component implementations.
67
67
 
68
- For each measured component, the readout ``IQPulse`` will be modulated with the
68
+ For each measured component, the readout :class:`.IQPulse` will be modulated with the
69
69
  intermediate frequency (IF), computed as the difference between the readout
70
70
  frequency of that component and the probe line center frequency, and offset in phase
71
71
  by the readout phase of the component.
@@ -80,6 +80,7 @@ class Measure_CustomWaveforms(CustomIQWaveforms):
80
80
  "frequency": Parameter("", "Readout pulse frequency", "Hz"),
81
81
  "phase": Parameter("", "Readout pulse phase", "rad"),
82
82
  "amplitude_i": Parameter("", "Readout channel I amplitude", ""),
83
+ # TODO do we really need these defaults? are they used anywhere?
83
84
  "amplitude_q": Setting(Parameter("", "Readout channel Q amplitude", ""), 0.0),
84
85
  "integration_length": Parameter("", "Integration length", "s"),
85
86
  "integration_weights_I": Setting(
@@ -105,36 +106,38 @@ class Measure_CustomWaveforms(CustomIQWaveforms):
105
106
  self._time_traces: dict[tuple[str, float | None, float | None, str], TimeBox] = {}
106
107
  """Cache for :meth:`time_trace`."""
107
108
 
108
- if len(locus) == 1: # factorizable gates only need calibration on 1-loci
109
- self._probe_line: ProbeChannelProperties = self.builder.channels[ # type: ignore[assignment]
110
- self.builder.get_probe_channel(self.locus[0])
109
+ if len(locus) == 1:
110
+ # prepare the single-component measurement
111
+ probe_line: ProbeChannelProperties = builder.channels[ # type: ignore[assignment]
112
+ builder.get_probe_channel(locus[0])
111
113
  ]
112
- c_freq = self._probe_line.center_frequency
113
- if_freq = (calibration_data["frequency"] - c_freq) / self._probe_line.sample_rate
114
+ # readout duration is determined by the acquisition, probe pulses are truncated to fit this window
114
115
  self._duration = (
115
- self._probe_line.duration_to_int_samples(
116
- self._probe_line.round_duration_to_granularity(
116
+ probe_line.duration_to_int_samples(
117
+ probe_line.round_duration_to_granularity(
117
118
  calibration_data["acquisition_delay"] + calibration_data["integration_length"]
118
119
  )
119
120
  )
120
- + self._probe_line.integration_stop_dead_time
121
+ + probe_line.integration_stop_dead_time
121
122
  )
122
-
123
+ self._probe_offset = probe_line.integration_start_dead_time
124
+ # "duration" is only used by the probe pulse
123
125
  waveform_params = self.convert_calibration_data(
124
126
  calibration_data,
125
- {k: v for k, v in self.parameters.items() if k not in self.root_parameters or k == "duration"},
126
- self._probe_line,
127
+ {k: v for k, v in self.parameters.items() if k not in self.root_parameters},
128
+ probe_line,
127
129
  )
130
+ # unconverted cal data that corresponds to a root param (not duration)
128
131
  root_params = {k: v for k, v in calibration_data.items() if k in self.root_parameters and k != "duration"}
132
+ # do some conversions TODO are these consistent?
133
+ root_params["integration_length"] = probe_line.duration_to_int_samples(root_params["integration_length"])
134
+ root_params["acquisition_delay"] = round(probe_line.duration_to_samples(root_params["acquisition_delay"]))
129
135
 
130
- probe_instruction, acquisition_method = self._build_instructions(waveform_params, root_params, if_freq)
131
- self._probe_instruction = probe_instruction
132
- self._acquisition_method = acquisition_method
133
- self._prio_calibration: OILCalibrationData | None = None
134
- else:
135
- # we need to store the possible cal_data == priority calibration in order to propagate it to the factored
136
- # single-component measure calls in :meth:`probe_timebox`
137
- self._prio_calibration = calibration_data or None
136
+ if_freq = (calibration_data["frequency"] - probe_line.center_frequency) / probe_line.sample_rate
137
+
138
+ self._probe_instruction, self._acquisition_method = self._build_instructions(
139
+ waveform_params, root_params, if_freq
140
+ )
138
141
 
139
142
  def _build_instructions(
140
143
  self, waveform_params: OILCalibrationData, root_params: OILCalibrationData, if_freq: float
@@ -160,7 +163,7 @@ class Measure_CustomWaveforms(CustomIQWaveforms):
160
163
  modulation_frequency=if_freq,
161
164
  )
162
165
 
163
- integration_length = self._probe_line.duration_to_int_samples(root_params["integration_length"])
166
+ integration_length = root_params["integration_length"]
164
167
  weights_i = root_params.get("integration_weights_I")
165
168
  weights_q = root_params.get("integration_weights_Q")
166
169
  if weights_i is not None and weights_i.size and weights_q is not None and weights_q.size:
@@ -195,16 +198,15 @@ class Measure_CustomWaveforms(CustomIQWaveforms):
195
198
  )
196
199
 
197
200
  acquisition_type = root_params.get("acquisition_type", self.root_parameters["acquisition_type"].value) # type: ignore[union-attr]
198
- acquisition_delay = round(self._probe_line.duration_to_samples(root_params["acquisition_delay"]))
199
201
  acquisition_label = "TO_BE_REPLACED"
200
202
  if acquisition_type == "complex":
201
203
  acquisition_method = ComplexIntegration(
202
- label=acquisition_label, delay_samples=acquisition_delay, weights=weights
204
+ label=acquisition_label, delay_samples=root_params["acquisition_delay"], weights=weights
203
205
  )
204
206
  elif acquisition_type == "threshold":
205
207
  acquisition_method = ThresholdStateDiscrimination(
206
208
  label=acquisition_label,
207
- delay_samples=acquisition_delay,
209
+ delay_samples=root_params["acquisition_delay"],
208
210
  weights=weights,
209
211
  threshold=root_params["integration_threshold"],
210
212
  )
@@ -243,12 +245,11 @@ class Measure_CustomWaveforms(CustomIQWaveforms):
243
245
  replacements = {"label": f"{self.locus[0]}__{label_key}"}
244
246
  if feedback_key and isinstance(self._acquisition_method, ThresholdStateDiscrimination):
245
247
  # TODO: use the actual ``feedback_key`` when AWGs support multiple feedback labels
246
- feedback_bit = f"{self.locus[0]}__{FEEDBACK_KEY}"
247
- replacements["feedback_signal_label"] = feedback_bit
248
+ replacements["feedback_signal_label"] = f"{self.locus[0]}__{FEEDBACK_KEY}"
248
249
  acquisitions = (replace(self._acquisition_method, **replacements),) if do_acquisition else () # type: ignore[arg-type]
249
250
  multiplexed_iq = MultiplexedIQPulse(
250
- duration=self._probe_instruction.duration + self._probe_line.integration_start_dead_time,
251
- entries=((self._probe_instruction, self._probe_line.integration_start_dead_time),),
251
+ duration=self._probe_instruction.duration + self._probe_offset,
252
+ entries=((self._probe_instruction, self._probe_offset),),
252
253
  )
253
254
  readout_trigger = ReadoutTrigger(
254
255
  duration=self._duration, probe_pulse=multiplexed_iq, acquisitions=acquisitions
@@ -286,12 +287,12 @@ class Measure_CustomWaveforms(CustomIQWaveforms):
286
287
  label=f"{self.__class__.__name__} on {self.locus}",
287
288
  )
288
289
  else:
290
+ # factorizability: use the sub-implementations
289
291
  # _skip_override used for child classes build on `Measure_CustomWaveforms` to not call `.probe_timebox`
290
292
  # from the parent class, but from this class instead.
293
+ # TODO remove, make probe_timebox private or something.
291
294
  probe_timeboxes = [
292
- self.builder.get_implementation( # type: ignore[attr-defined]
293
- self.parent.name, (c,), impl_name=self.name, priority_calibration=self._prio_calibration
294
- ).probe_timebox(key, feedback_key, do_acquisition, _skip_override=True)
295
+ self.sub_implementations[c].probe_timebox(key, feedback_key, do_acquisition, _skip_override=True) # type: ignore[attr-defined]
295
296
  for c in self.locus
296
297
  ]
297
298
  probe_timebox = functools.reduce(lambda x, y: x + y, probe_timeboxes)
@@ -446,10 +447,14 @@ class ProbePulse_CustomWaveforms(CustomIQWaveforms):
446
447
  With given :class:`.Waveform` waveform definitions ``Something`` and ``SomethingElse``,
447
448
  you may define a measurement implementation that uses them as follows:
448
449
  ``class MyGate(ProbePulse_CustomWaveforms, i_wave=Something, q_wave=SomethingElse)``.
450
+ The measurement :class:`.IQPulse` instruction will not be automatically modulated
451
+ by any frequency, so any modulations should be included in the I and Q waveforms themselves.
452
+
453
+ Due to device limitations this implementation also has to integrate the readout signal
454
+ (using arbitrary weights), even though it does not make much sense.
449
455
 
450
456
  Contrary to the ``Measure_CustomWaveforms`` class, this implementation acts on proble lines directly (i.e. its
451
- ``locus`` is a single probe line). The measurement ``IQPulse`` instruction will not be automatically modulated
452
- by any frequency, so any modulations should be included in the I and Q waveforms themselves.
457
+ ``locus`` is a single probe line).
453
458
  """
454
459
 
455
460
  root_parameters = {
@@ -465,8 +470,8 @@ class ProbePulse_CustomWaveforms(CustomIQWaveforms):
465
470
  self, parent: QuantumOp, name: str, locus: Locus, calibration_data: OILCalibrationData, builder: ScheduleBuilder
466
471
  ):
467
472
  super().__init__(parent, name, locus, calibration_data, builder)
468
- self._probe_line: ProbeChannelProperties = self.builder.channels[ # type: ignore[assignment]
469
- self.builder.component_channels[self.locus[0]]["readout"]
473
+ self._probe_line: ProbeChannelProperties = builder.channels[ # type: ignore[assignment]
474
+ builder.component_channels[locus[0]]["readout"]
470
475
  ]
471
476
  self._duration = (
472
477
  self._probe_line.duration_to_int_samples(
@@ -479,7 +484,7 @@ class ProbePulse_CustomWaveforms(CustomIQWaveforms):
479
484
 
480
485
  waveform_params = self.convert_calibration_data(
481
486
  calibration_data,
482
- {k: v for k, v in self.parameters.items() if k not in self.root_parameters or k == "duration"},
487
+ {k: v for k, v in self.parameters.items() if k not in self.root_parameters},
483
488
  self._probe_line,
484
489
  )
485
490
  root_params = {k: v for k, v in calibration_data.items() if k in self.root_parameters and k != "duration"}
@@ -602,8 +607,8 @@ class ProbePulse_CustomWaveforms_noIntegration(CustomIQWaveforms):
602
607
  """Cache for :meth:`probe_timebox`."""
603
608
 
604
609
  if len(locus) == 1: # factorizable gates only need calibration on 1-loci
605
- self._probe_line: ProbeChannelProperties = self.builder.channels[ # type: ignore[assignment]
606
- self.builder.get_probe_channel(self.locus[0])
610
+ self._probe_line: ProbeChannelProperties = builder.channels[ # type: ignore[assignment]
611
+ builder.get_probe_channel(locus[0])
607
612
  ]
608
613
  c_freq = self._probe_line.center_frequency
609
614
  if_freq = (calibration_data["frequency"] - c_freq) / self._probe_line.sample_rate
iqm/pulse/gates/prx.py CHANGED
@@ -211,11 +211,6 @@ class PRX_SinglePulse_GateImplementation(SinglePulseGate, PrxGateImplementation)
211
211
  timebox.neighborhood_components[0] = set(self.locus)
212
212
  return timebox
213
213
 
214
- @property
215
- def iq_pulse(self) -> IQPulse:
216
- """Alias for ``self.pulse`` for backward compatibility"""
217
- return self.pulse # type: ignore[return-value]
218
-
219
214
 
220
215
  class PRX_CustomWaveforms(PRX_SinglePulse_GateImplementation, CustomIQWaveforms):
221
216
  """Base class for PRX gates implemented using a single IQ pulse and hot-swappable waveforms."""
iqm/pulse/gates/reset.py CHANGED
@@ -18,7 +18,7 @@ The reset operation is a non-unitary quantum channel that sets the state of a qu
18
18
 
19
19
  from __future__ import annotations
20
20
 
21
- from collections.abc import Iterable
21
+ from collections.abc import Iterable, Mapping
22
22
 
23
23
  from exa.common.data.parameter import Parameter
24
24
  from exa.common.qcm_data.chip_topology import ChipTopology
@@ -53,7 +53,7 @@ class Reset_Conditional(CompositeGate):
53
53
  blocked.
54
54
  """
55
55
 
56
- registered_gates = ["measure"]
56
+ registered_gates = ("measure", "cc_prx")
57
57
 
58
58
  def _call(self) -> TimeBox: # type: ignore[override]
59
59
  # find locus components that are resettable via conditional reset
@@ -120,15 +120,8 @@ class Reset_Wait(GateImplementation):
120
120
  if len(self.locus) == 1:
121
121
  waits_box = self.builder.wait(self.locus, self.calibration_data["duration"], rounding=True)
122
122
  else:
123
- prio_calibration = self.calibration_data if self.calibration_data else None
124
- waits_box = TimeBox.composite(
125
- [
126
- self.builder.get_implementation( # type: ignore[attr-defined]
127
- self.parent.name, (q,), impl_name=self.name, priority_calibration=prio_calibration
128
- ).wait_box()
129
- for q in self.locus
130
- ]
131
- )
123
+ # factorizability: use the sub-implementations
124
+ waits_box = TimeBox.composite([self.sub_implementations[c].wait_box() for c in self.locus]) # type: ignore[attr-defined]
132
125
  return waits_box
133
126
 
134
127
  def _call(self) -> TimeBox:
@@ -147,14 +140,13 @@ class Reset_Wait(GateImplementation):
147
140
  return TimeBox.composite([waits_box])
148
141
 
149
142
  def duration_in_seconds(self) -> float:
150
- return max(
151
- self.builder.get_implementation(self.parent.name, (q,), impl_name=self.name).calibration_data["duration"]
152
- for q in self.locus
153
- )
143
+ if len(self.locus) == 1:
144
+ return self.calibration_data["duration"]
145
+ return max(self.sub_implementations[c].calibration_data["duration"] for c in self.locus)
154
146
 
155
147
  @classmethod
156
148
  def get_custom_locus_mapping(
157
- cls, chip_topology: ChipTopology, component_to_channels: dict[str, Iterable[str]]
149
+ cls, chip_topology: ChipTopology, component_to_channels: Mapping[str, Iterable[str]]
158
150
  ) -> dict[tuple[str, ...] | frozenset[str], tuple[str, ...]] | None:
159
151
  """Supported loci: all components that have channels."""
160
152
  return {(c,): (c,) for c in chip_topology.all_components}
iqm/pulse/gates/rz.py CHANGED
@@ -251,7 +251,7 @@ class RZ_ACStarkShift_smoothConstant( # type: ignore[call-arg] # type: ignore[
251
251
  class RZ_PRX_Composite(CompositeGate):
252
252
  """RZ gate implemented as a sequence of PRX gates."""
253
253
 
254
- registered_gates = ["prx"]
254
+ registered_gates = ("prx",)
255
255
 
256
256
  def __init__(self, parent, name, locus, calibration_data, builder):
257
257
  super().__init__(parent, name, locus, calibration_data, builder)
iqm/pulse/gates/sx.py CHANGED
@@ -38,7 +38,7 @@ if TYPE_CHECKING: # pragma: no cover
38
38
  class SXGate(CompositeGate):
39
39
  """SX gate implementation based on PRX gate, by limiting the angle to pi / 2."""
40
40
 
41
- registered_gates = ["prx"]
41
+ registered_gates = ("prx",)
42
42
 
43
43
  def _call(self) -> TimeBox: # type: ignore[override]
44
44
  """Call PRX gate with angle equals to pi / 2."""
iqm/pulse/gates/u.py CHANGED
@@ -91,7 +91,7 @@ class UGate(CompositeGate):
91
91
  Assumes the chosen PRX implementation uses resonant driving, and that the virtual RZ technique can be used.
92
92
  """
93
93
 
94
- registered_gates = ["prx"]
94
+ registered_gates = ("prx",)
95
95
 
96
96
  def _call(self, theta: float, phi: float = 0.0, lam: float = 0.0) -> TimeBox: # type: ignore[override]
97
97
  r"""Convert pulses into timebox, via Euler decomposition.
@@ -136,17 +136,17 @@ class IQPulse(Instruction):
136
136
  scale_q: float = 0.0
137
137
  """Scaling factor for the Q quadrature."""
138
138
  phase: float = 0.0
139
- """Phase of the pulse relative to the channel frequency, in radians."""
139
+ """Phase of the pulse relative to the channel upconversion oscillator ("carrier wave"), in radians."""
140
140
  modulation_frequency: float = 0.0
141
141
  """Modulation frequency of the waveforms, in units of the sampling rate.
142
- This modulation is additional to the channel frequency.
142
+ This modulation is additional to the channel upconversion frequency.
143
143
  The default value of 0.0 does not modulate.
144
144
  Note that the phase of this modulation resets for every instruction, that is, successive instances of the same
145
145
  modulated pulse are not phase coherent.
146
146
  """
147
147
  phase_increment: float = 0.0
148
- """Relative phase increment to the phase in the carrier frequency of this pulse and all pulses that
149
- are played after it. Unit: rad.
148
+ """Phase increment for the channel upconversion oscillator ("carrier wave"), affecting this pulse and
149
+ all pulses that are played after it on the channel, in radians.
150
150
  """
151
151
 
152
152
  def validate(self):
@@ -165,9 +165,9 @@ class ConditionalInstruction(Instruction):
165
165
  """Choice between multiple Instructions, depending on a condition."""
166
166
 
167
167
  condition: str
168
- """can be evaluated to an integer >= 0"""
168
+ """Can be evaluated to an integer >= 0 representing an outcome."""
169
169
  outcomes: tuple[Instruction, ...]
170
- """maps possible outcomes of the condition to the corresponding instructions"""
170
+ """Maps possible outcomes of the condition to the corresponding instructions."""
171
171
 
172
172
  def validate(self):
173
173
  super().validate()
@@ -186,7 +186,7 @@ class ConditionalInstruction(Instruction):
186
186
 
187
187
  @dataclass(frozen=True)
188
188
  class MultiplexedIQPulse(Instruction):
189
- """Instruction to simultaneously play multiple IQ pulses.
189
+ """Play the sum of multiple IQ pulses.
190
190
 
191
191
  Each component pulse can have an arbitrary delay from the beginning of this instruction.
192
192
  Outside the interval of the MultiplexedIQPulse, the component pulses are truncated.
iqm/pulse/quantum_ops.py CHANGED
@@ -27,7 +27,7 @@ import numpy as np
27
27
  from iqm.pulse.base_utils import merge_dicts
28
28
 
29
29
  if TYPE_CHECKING: # pragma: no cover
30
- from iqm.pulse.gate_implementation import GateImplementation, OILCalibrationData, OpCalibrationDataTree
30
+ from iqm.pulse.gate_implementation import GateImplementation, Locus, OILCalibrationData, OpCalibrationDataTree
31
31
 
32
32
 
33
33
  @dataclass(frozen=True)
@@ -73,7 +73,7 @@ class QuantumOp:
73
73
 
74
74
  name: str
75
75
  """Unique name of the operation."""
76
- arity: int
76
+ arity: int = 1
77
77
  """Number of locus components the operation acts on.
78
78
  Each locus component corresponds to a quantum subsystem in the definition of the operation.
79
79
  The computational subspace always consists of the lowest two levels of the subsystem.
@@ -92,7 +92,7 @@ class QuantumOp:
92
92
  """True iff the operation is always factorizable to independent single-subsystem operations, which
93
93
  is also how it is implemented, for example parallel single-qubit measurements.
94
94
  In this case the operation calibration data is for individual subsystems as well."""
95
- defaults_for_locus: dict[tuple[str, ...], str] = field(default_factory=dict)
95
+ defaults_for_locus: dict[Locus, str] = field(default_factory=dict)
96
96
  """Optionally define the implementation default individually per each locus. Maps the locus to the default
97
97
  gate implementation name. If a locus is not found in this dict (by default, the dict is empty), falls back to the
98
98
  global order defined in ``implementations``. The implementations must be first registered in ``implementations``."""
@@ -140,7 +140,6 @@ class QuantumOp:
140
140
  ValueError: ``default`` is unknown or is a special implementation.
141
141
 
142
142
  """
143
- # breakpoint()
144
143
  if (impl := self.implementations.get(default)) is None:
145
144
  raise ValueError(f"Operation '{self.name}' has no implementation named '{default}'.")
146
145
 
@@ -233,7 +232,7 @@ def validate_op_calibration(calibration: OpCalibrationDataTree, ops: QuantumOpTa
233
232
 
234
233
 
235
234
  def validate_locus_calibration(
236
- cal_data: OILCalibrationData, impl: type[GateImplementation], op: QuantumOp, impl_name: str, locus: tuple[str, ...]
235
+ cal_data: OILCalibrationData, impl: type[GateImplementation], op: QuantumOp, impl_name: str, locus: Locus
237
236
  ) -> None:
238
237
  """Validates calibration for a particular gate implementation at particular locus.
239
238
 
@@ -250,17 +249,14 @@ def validate_locus_calibration(
250
249
  """
251
250
  if not locus:
252
251
  return # default cal data for all loci
253
- # Remove registered gates from calibration so that validation has a chance to succeed for composite gates.
254
- # This validation is done anyway when constructing the registered gates themselves, so it is not necessary
255
- # to do it twice.
256
- cal_data_copy = cal_data.copy()
257
- if hasattr(impl, "registered_gates"):
258
- registered_gates = impl.registered_gates
259
- for gate in registered_gates:
260
- if gate in cal_data_copy:
261
- del cal_data_copy[gate]
262
- # since OILCalibrationData can have nested dicts, we do a recursive diff
263
- _diff_dicts(cal_data_copy, impl.parameters, [], op.name, impl_name, locus)
252
+
253
+ # Some implementations have optional calibration parameters which we ignore here,
254
+ # e.g. customizable member gate cal data for CompositeGates.
255
+ # Since OILCalibrationData can have nested dicts, we do a recursive diff.
256
+ try:
257
+ _diff_dicts(cal_data, impl.parameters, set(impl.optional_calibration_keys()), [])
258
+ except ValueError as exc:
259
+ raise ValueError(f"{op.name}.{impl_name} at {locus}: {exc}") from exc
264
260
 
265
261
  n_components = len(locus)
266
262
  arity = op.arity
@@ -274,29 +270,31 @@ def validate_locus_calibration(
274
270
  raise ValueError(f"{op.name}.{impl_name} at {locus}: locus must have {arity} component(s)")
275
271
 
276
272
 
277
- def _diff_dicts(cal_data: dict[str, Any], impl_parameters: dict[str, Any], path, op_name, impl_name, locus) -> None:
278
- """Compare the calibration"""
279
- full = set(impl_parameters)
273
+ def _diff_dicts(
274
+ cal_data: dict[str, Any], impl_parameters: dict[str, Any], ignored_keys: set[str], path: list[str]
275
+ ) -> None:
276
+ """Compare calibration data to the expected parameters."""
277
+ all_parameters = set(impl_parameters)
278
+ # some gate params have default values
280
279
  defaults = {k: v.value for k, v in impl_parameters.items() if hasattr(v, "value")}
280
+ # FIXME what is the defaults logic below? is it correct? needs a comment
281
281
  have = {k for k, v in cal_data.items() if v is not None or (k in defaults and defaults[k] is None)}
282
- need = set(impl_parameters) - set(defaults)
282
+ need = all_parameters - set(defaults)
283
283
  if need == {"*"}:
284
- return
285
- if diff := have - full:
286
- raise ValueError(f"Unknown calibration data for {op_name}.{impl_name} at {locus}: {'.'.join(path)} {diff}")
284
+ return # wildcard parameters are optional at any level
285
+ if diff := have - all_parameters - ignored_keys:
286
+ raise ValueError(f"Unknown calibration data {'.'.join(path)}.{diff}")
287
287
  if diff := need - have:
288
- raise ValueError(f"Missing calibration data for {op_name}.{impl_name} at {locus}: {'.'.join(path)} {diff}")
288
+ raise ValueError(f"Missing calibration data {'.'.join(path)}.{diff}")
289
289
  for key, data in cal_data.items():
290
+ if key in ignored_keys:
291
+ continue
290
292
  required_value = impl_parameters[key]
291
293
  new_path = path + [key]
292
294
  if isinstance(required_value, dict):
293
295
  if isinstance(data, dict):
294
- _diff_dicts(data, required_value, new_path, op_name, impl_name, locus)
296
+ _diff_dicts(data, required_value, set(), new_path)
295
297
  else:
296
- raise ValueError(
297
- f"Calibration data for {op_name}.{impl_name} at {locus}: '{'.'.join(new_path)}' should be a dict"
298
- )
298
+ raise ValueError(f"Calibration data item '{'.'.join(new_path)}' should be a dict")
299
299
  elif isinstance(data, dict):
300
- raise ValueError(
301
- f"Calibration data for {op_name}.{impl_name} at {locus}: '{'.'.join(new_path)}' should be a scalar"
302
- )
300
+ raise ValueError(f"Calibration data item '{'.'.join(new_path)}' should be a scalar")
iqm/pulse/utils.py CHANGED
@@ -17,14 +17,11 @@
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
- from collections.abc import Iterable
21
- import copy
22
20
  from typing import get_args, get_origin
23
21
 
24
22
  import numpy as np
25
23
 
26
24
  from exa.common.data.parameter import CollectionType, DataType
27
- from iqm.pulse.quantum_ops import QuantumOpTable
28
25
 
29
26
 
30
27
  def map_waveform_param_types(type_hint: type) -> tuple[DataType, CollectionType]:
@@ -110,92 +107,3 @@ def phase_transformation(psi_1: float = 0.0, psi_2: float = 0.0) -> tuple[float,
110
107
 
111
108
  """
112
109
  return psi_2, -(psi_1 + psi_2)
113
-
114
-
115
- def _validate_locus_defaults(op_name: str, definition: dict, implementations: dict) -> None:
116
- """Validate that locus defaults reference valid implementations.
117
-
118
- Args:
119
- op_name: Name of the gate we want to validate
120
- definition: Dictionary containing the parameters of the new operation
121
- implementations: Available implementations for the operation
122
-
123
- Raises:
124
- ValueError: If locus default references invalid implementation
125
-
126
- """
127
- for locus, impl_name in definition["defaults_for_locus"].items():
128
- if impl_name not in implementations:
129
- raise ValueError(
130
- f"defaults_for_locus[{locus}] implementation '{impl_name}' does not "
131
- f"appear in the implementations field of {op_name}."
132
- )
133
-
134
-
135
- def _validate_op_attributes(op_name: str, definition: dict, op_table: QuantumOpTable) -> None:
136
- """Assert that the attributes of the operation (other than default implementation info) are not changed.
137
-
138
- Args:
139
- op_name: Name of the quantum operation to be added to table of quantum operations
140
- definition: Dictionary containing the parameters of the new operation
141
- op_table: Table of default or existing quantum operations
142
-
143
- Raises:
144
- ValueError: If attributes don't match defaults or are invalid
145
-
146
- """
147
- for attribute_name, value in definition.items():
148
- if not hasattr(op_table[op_name], attribute_name):
149
- raise ValueError(f"QuantumOp {op_name} has no attribute '{attribute_name}'.")
150
-
151
- default_value = getattr(op_table[op_name], attribute_name)
152
- if isinstance(value, Iterable):
153
- default_value = tuple(default_value)
154
- value = tuple(value) # noqa: PLW2901
155
- if attribute_name not in ("implementations", "defaults_for_locus") and default_value != value:
156
- raise ValueError(
157
- "The QuantumOp definition provided by the user has a different value in the"
158
- f" attribute {attribute_name} compared to the QuantumOp definition in iqm-pulse "
159
- f" ({value} != {default_value}). "
160
- "Changing the attributes of default QuantumOps is not allowed, other than"
161
- "implementations and defaults_for_locus."
162
- )
163
-
164
-
165
- def _process_implementations(
166
- op_name: str, new_implementations: dict, exposed_implementations: dict, _default_implementations: dict
167
- ) -> dict:
168
- """Processes implementation definitions into implementation classes.
169
-
170
- Args:
171
- op_name: Name of the quantum operation
172
- new_implementations: Dictionary mapping implementation names to class names
173
- exposed_implementations: Dictionary containing the exposed implementations
174
- _default_implementations: Dictionary containing the default mappings between implementations
175
- and implementation classes for different gates
176
-
177
- Returns:
178
- Dictionary mapping implementation names to implementation classes
179
-
180
- Raises:
181
- ValueError: If implementation class is not exposed
182
- ValueError: If implementation is overriding a default implementation
183
-
184
- """
185
- implementations = {}
186
- default_implementations = copy.deepcopy(_default_implementations).get(op_name, {})
187
- for new_impl_name, new_impl_cls in new_implementations.items():
188
- if (impl := exposed_implementations.get(new_impl_cls)) is None:
189
- raise ValueError(f"Implementation {new_impl_cls} has not been exposed.")
190
- # check if the implementation is overriding a default implementation
191
- for def_impl_name, def_impl_cls in default_implementations.items():
192
- if new_impl_name == def_impl_name:
193
- def_impl_class_name = def_impl_cls if isinstance(def_impl_cls, str) else def_impl_cls.__name__
194
- if new_impl_cls != def_impl_class_name:
195
- raise ValueError(
196
- f"'{def_impl_class_name}' is the default GateImplementation class for implementation '{def_impl_name}' and cannot be overridden. Consider adding a new implementation with a different name." # noqa: E501
197
- )
198
-
199
- implementations[new_impl_name] = impl
200
-
201
- return implementations
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: iqm-pulse
3
- Version: 9.20.0
3
+ Version: 10.0.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
@@ -1,34 +1,34 @@
1
1
  iqm/pulse/__init__.py,sha256=FxgHlv3bF8bQCREwOJ1yu_nsrfJq0g0HdovvT1wUnCI,881
2
2
  iqm/pulse/base_utils.py,sha256=tLbmnWGmHsjcA8QFCiqvHfgSJJNWS4AVTcD2y4M2ipU,2641
3
- iqm/pulse/builder.py,sha256=G9AydyZHy3YX6h-BMvPxqqcMieTRq1PoWJ_Bau9uWH4,67024
4
- iqm/pulse/circuit_operations.py,sha256=i6v6dW-F_YATSFwZWbi8OTkcS_HCAqfJdfZ7ZfVzcmU,22743
5
- iqm/pulse/gate_implementation.py,sha256=iD9qdXbcJVXqa8xU0gJ1KSkhe02NsXpbmuAiRvvAdWo,33956
3
+ iqm/pulse/builder.py,sha256=YB-oiCqOka32Xyyjydomzq_tIwaE_4LsHkyLI4zGWCk,72482
4
+ iqm/pulse/circuit_operations.py,sha256=i-sUx-JZOKAMKWgYq1c5KIR2Ons9Uyq_meEmppAmwI8,22387
5
+ iqm/pulse/gate_implementation.py,sha256=yyj2d1aDL0K_qouo0qLHnZ49_1qHsJEShKW14398t8Q,39546
6
6
  iqm/pulse/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- iqm/pulse/quantum_ops.py,sha256=-ufAogPszeh1z23Qr3BeDH6L-vJAWauUZLJ5Xoqd4cY,14758
7
+ iqm/pulse/quantum_ops.py,sha256=UTQ08Eo5vFpV_lOoQNtlwRTchES6BdGHQ5pzSdb1ToY,14584
8
8
  iqm/pulse/scheduler.py,sha256=62gH3AcxmsgmShcmtNaiCqFLRl1Wll6Q24zIkhw_-XM,22768
9
9
  iqm/pulse/timebox.py,sha256=Yi8I43KiSPs2s_l1r0rMVm4mw0EpSCrpq3BCUNk5vyE,16618
10
- iqm/pulse/utils.py,sha256=HyTkMD0er_rZnvDZWM1WscOFxaxGMyRAW_Aw36b0Hdc,7998
10
+ iqm/pulse/utils.py,sha256=0J4KQNvKtZahme7sP7vLa1FtENepC-eb9_A5WaghvVU,3710
11
11
  iqm/pulse/validation.py,sha256=-tZWrW201t4nbTQWeZ8M9DixzoN8B0Q63IP57BfDAz0,10733
12
- iqm/pulse/gates/__init__.py,sha256=uvEfNMEd7-jp-PxbZ8F-9IpocFUQ_T_JEPZrZZpPBxQ,13348
12
+ iqm/pulse/gates/__init__.py,sha256=Hd0mKWw55afvgOhDD_dgaPNCLZR6LSUoywwjBgB5zd4,8719
13
13
  iqm/pulse/gates/barrier.py,sha256=WhYV70lf4lh4Wa9UZuMk2lp9JbUQIu8lzewRC2P7pNE,2546
14
- iqm/pulse/gates/conditional.py,sha256=NULUlVv4UNYHHILFIxvjPtWE1bg_nQu8lIXmBkBDBcM,6304
15
- iqm/pulse/gates/cz.py,sha256=1v72BQu2KByYZ43sFnM9ookIj_0DyFGPdIR6RbtXzLQ,20294
16
- iqm/pulse/gates/default_gates.py,sha256=NY_fduPHVyelaRFhi9BGokescSPxpouB9LY5ISUZsyE,10421
14
+ iqm/pulse/gates/conditional.py,sha256=NE-1GYlFcfaCmRuhqGEIhtoJmBArhyY5KhVnO4y4zng,6289
15
+ iqm/pulse/gates/cz.py,sha256=CkTHqw6417cvZ7FDjx-e727tNpHG3PCdrpmaH7RtgOE,19855
16
+ iqm/pulse/gates/default_gates.py,sha256=KWcwWowkBlLkpqxcPiX6dxLpAb7DZr3FLskaba5__I4,7303
17
17
  iqm/pulse/gates/delay.py,sha256=nST9dY2JFp_mpKhiSfsYa5yL4hFKcNJSAyCzXjhauQg,3767
18
18
  iqm/pulse/gates/enums.py,sha256=ATwb6vZYpfgQ1gQyFPW53JyIKrdAP3FPHm6jV-t9OAk,2532
19
- iqm/pulse/gates/flux_multiplexer.py,sha256=fUAg-biUcl6buWlANd2pOrBibOlLqO-lOSC7ZwZ7_xA,9745
20
- iqm/pulse/gates/measure.py,sha256=rKMTS-X4xWhyzU9F64B-8G6IQPRP2W30mCkwUiG_43Q,44392
19
+ iqm/pulse/gates/flux_multiplexer.py,sha256=sk84ItEvkx7Z0pCHt8MCCczBe7_BHNvqS4_oeNghZw8,9757
20
+ iqm/pulse/gates/measure.py,sha256=5yG5dQeDljZewrPdy6vwejz5m3KU-9SlXQco9oAyssw,44488
21
21
  iqm/pulse/gates/move.py,sha256=L8wDcc7He43sUrwQs24lNydxdM9bKN27zlEdnDucDfc,17490
22
- iqm/pulse/gates/prx.py,sha256=vf6tb7A6EzYm8GGJdcX7m9sKmToPODty-vQ8F1Mmhqw,25150
23
- iqm/pulse/gates/reset.py,sha256=6rpAfboNlINylDNphePGotbex-PtMKL_PnDXr2J4nK8,7706
24
- iqm/pulse/gates/rz.py,sha256=UYVyMfJZR9fl29vCiFPGz4RxJt__v0VK35Whkjlhc0Q,9683
25
- iqm/pulse/gates/sx.py,sha256=kNEBtkUMrbSMkvbLN78XuNjTR00NLQeZqBGilIAI98E,1735
26
- iqm/pulse/gates/u.py,sha256=lpj0dT7ktkStUSD4roPBn-8OLHrJYyQAHTuU0bm5kJg,5402
22
+ iqm/pulse/gates/prx.py,sha256=Vl7t26mDRjj-6sC8jmpAgzTAtaTubVx6Ib_lpx_HmCk,24978
23
+ iqm/pulse/gates/reset.py,sha256=idsknRq_z2JKIq4zXc4LvdE2czE5-KgGTdLPmYVyWIg,7480
24
+ iqm/pulse/gates/rz.py,sha256=kr8Y5ap1MN81InCSmzGkxejvUcNszl1sxpG7yacvDM0,9684
25
+ iqm/pulse/gates/sx.py,sha256=3s7lo5ib2HZh-kzZd95-JqL41LxCX5GA4QZ0h7u3WCQ,1736
26
+ iqm/pulse/gates/u.py,sha256=EUX84XkawqLhwRaJ1MxSKB33VpC8XDsFu875uHNo0WI,5403
27
27
  iqm/pulse/playlist/__init__.py,sha256=XW-7Po_vCzhkk1Js06K8P-5srPpBuPHkqpc1yhL_Zx4,997
28
28
  iqm/pulse/playlist/channel.py,sha256=RAkGyjTU66MiF-ccf06FFZAXpUykWEltbfNXw4xJpYc,17164
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=K4c8TwC9FBbD_kdPD2OpjG1OZsDJALlwJon4SD9etk8,9845
31
+ iqm/pulse/playlist/instructions.py,sha256=PyIjJJN58rbfIFqgRaynsnsHXywy6xBpkAtQuifgoJY,9930
32
32
  iqm/pulse/playlist/playlist.py,sha256=7MIX4iRfh2Nit8AnpNyqv5A5ZmK7JR-v201DXVL6kUk,685
33
33
  iqm/pulse/playlist/schedule.py,sha256=VYrVyPlB04Rm8iOYBXu2uOpsLBqczJRx3L2VU53CxVU,17989
34
34
  iqm/pulse/playlist/waveforms.py,sha256=Xa7P3V0rRIn7sQIoLKqXaWwtAbyQWjdVGW6bKOGNcOQ,17898
@@ -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-9.20.0.dist-info/LICENSE.txt,sha256=R6Q7eUrLyoCQgWYorQ8WJmVmWKYU3dxA3jYUp0wwQAw,11332
43
- iqm_pulse-9.20.0.dist-info/METADATA,sha256=P35RKE4Ooe9mXe6ifYgmX2B3wCKo5p21MVAdDngKEU8,14551
44
- iqm_pulse-9.20.0.dist-info/WHEEL,sha256=y4mX-SOX4fYIkonsAGA5N0Oy-8_gI4FXw5HNI1xqvWg,91
45
- iqm_pulse-9.20.0.dist-info/top_level.txt,sha256=NB4XRfyDS6_wG9gMsyX-9LTU7kWnTQxNvkbzIxGv3-c,4
46
- iqm_pulse-9.20.0.dist-info/RECORD,,
42
+ iqm_pulse-10.0.0.dist-info/LICENSE.txt,sha256=R6Q7eUrLyoCQgWYorQ8WJmVmWKYU3dxA3jYUp0wwQAw,11332
43
+ iqm_pulse-10.0.0.dist-info/METADATA,sha256=5-Re6d58u6Ek98qU8ea3CyYKoH5kh2eGw7lkTvGZQXI,14551
44
+ iqm_pulse-10.0.0.dist-info/WHEEL,sha256=y4mX-SOX4fYIkonsAGA5N0Oy-8_gI4FXw5HNI1xqvWg,91
45
+ iqm_pulse-10.0.0.dist-info/top_level.txt,sha256=NB4XRfyDS6_wG9gMsyX-9LTU7kWnTQxNvkbzIxGv3-c,4
46
+ iqm_pulse-10.0.0.dist-info/RECORD,,