iqm-pulse 12.4.0__py3-none-any.whl → 12.6.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.
@@ -18,10 +18,10 @@ import numpy as np
18
18
  from exa.common.data.parameter import CollectionType, Parameter
19
19
  from iqm.pulse.gate_implementation import CompositeGate
20
20
  from iqm.pulse.gates.measure import FEEDBACK_KEY
21
- from iqm.pulse.gates.prx import PRX_SinglePulse_GateImplementation
22
- from iqm.pulse.playlist.instructions import Block, ConditionalInstruction, Wait
21
+ from iqm.pulse.playlist.instructions import Block, ConditionalInstruction, IQPulse, Wait
23
22
  from iqm.pulse.playlist.schedule import Schedule
24
23
  from iqm.pulse.timebox import TimeBox
24
+ from iqm.pulse.utils import fuse_iq_pulses
25
25
 
26
26
 
27
27
  class CCPRX_Composite(CompositeGate):
@@ -30,6 +30,12 @@ class CCPRX_Composite(CompositeGate):
30
30
  Applies a PRX gate conditioned on a discriminated readout result obtained in the same segment (active feedback).
31
31
  Applies a PRX gate if the result is 1, and a Wait of equal duration if the result is 0.
32
32
  Uses the default implementation of PRX underneath, so no extra calibration is needed.
33
+
34
+ .. note::
35
+
36
+ Assumes that the PRX gate implementation used only consists of IQPulse instructions on the drive channel
37
+ of its locus qubit.
38
+
33
39
  """
34
40
 
35
41
  registered_gates = ("prx",)
@@ -70,30 +76,43 @@ class CCPRX_Composite(CompositeGate):
70
76
 
71
77
  """
72
78
  qubit = self.locus[0]
73
- awg_name = self.builder.get_drive_channel(qubit)
79
+ drive_channel = self.builder.get_drive_channel(qubit)
80
+ prx_gate = self.build("prx", self.locus)
81
+
82
+ # NOTE assumes that the PRX gate only has IQPulse instructions on drive_channel
83
+ timebox: TimeBox = prx_gate(angle, phase) # type: ignore[assignment]
84
+ if not timebox.atom:
85
+ raise RuntimeError("Received non-atomic PRX timebox.")
86
+ prx_instructions = timebox.atom[drive_channel]
87
+ iq_pulses = [inst for inst in prx_instructions if isinstance(inst, IQPulse)]
88
+ if len(iq_pulses) != len(prx_instructions):
89
+ raise RuntimeError(f"PRX drive channel has non-IQPulse instructions: {prx_instructions}")
90
+
91
+ if len(iq_pulses) > 1:
92
+ iq_pulse = fuse_iq_pulses(iq_pulses)
93
+ else:
94
+ iq_pulse = iq_pulses[0]
74
95
 
75
- prx_gate: PRX_SinglePulse_GateImplementation = self.build("prx", self.locus) # type: ignore[assignment]
76
- # FIXME assumes PRX gates only use this one implementation as the default,
77
- # with just a drive channel and a single IQPulse.
78
- pulse = prx_gate(angle, phase).atom[prx_gate.channel][0] # type: ignore[union-attr,index]
79
- wait = Wait(pulse.duration) # idling, can be replaced with a DD sequence later on
96
+ wait = Wait(iq_pulse.duration) # idling, can be replaced with a DD sequence later on
80
97
 
81
98
  # TODO: use the actual inputted label when the HW supports many labels per drive channel
82
99
  default_label = f"{feedback_qubit}__{FEEDBACK_KEY}"
83
- pulse_instruction = ConditionalInstruction(
84
- duration=pulse.duration, condition=default_label, outcomes=(wait, pulse)
100
+ conditional_instruction = ConditionalInstruction(
101
+ duration=iq_pulse.duration,
102
+ condition=default_label,
103
+ outcomes=(wait, iq_pulse),
85
104
  )
86
105
  delays = self.calibration_data["control_delays"]
87
106
  if len(delays) == 0:
88
107
  raise ValueError(f"'control_delays' for '{self.name}' on {qubit} is empty (not calibrated).")
89
108
 
90
- possible_sources = [c for c in self.builder.get_virtual_feedback_channels(qubit) if awg_name in c]
109
+ possible_sources = [c for c in self.builder.get_virtual_feedback_channels(qubit) if drive_channel in c]
91
110
  if len(delays) != len(possible_sources):
92
111
  raise ValueError(
93
112
  f"Not the correct amount of calibration values for 'control_delays'. Need {len(possible_sources)}"
94
113
  f"values, got {delays}."
95
114
  )
96
- virtual_channel_name = self.builder.get_virtual_feedback_channel_for(awg_name, feedback_qubit)
115
+ virtual_channel_name = self.builder.get_virtual_feedback_channel_for(drive_channel, feedback_qubit)
97
116
  delay = delays[possible_sources.index(virtual_channel_name)]
98
117
  virtual_channel = self.builder.channels[virtual_channel_name]
99
118
  delay_samples = virtual_channel.duration_to_int_samples(
@@ -106,7 +125,7 @@ class CCPRX_Composite(CompositeGate):
106
125
  )
107
126
  delay_box.neighborhood_components = {0: {virtual_channel_name}}
108
127
  cond = TimeBox.atomic(
109
- Schedule({virtual_channel_name: [Block(0)], prx_gate.channel: [pulse_instruction]}),
128
+ Schedule({virtual_channel_name: [Block(0)], drive_channel: [conditional_instruction]}),
110
129
  locus_components=[qubit],
111
130
  label=f"Conditional PRX for {qubit}",
112
131
  )
@@ -871,7 +871,9 @@ class ShelvedMeasureTimeBox(TimeBox):
871
871
  return super().__add__(other)
872
872
 
873
873
  def __radd__(self, other: TimeBox | Iterable[TimeBox]) -> TimeBox:
874
- return self.__add__(other)
874
+ if isinstance(other, MultiplexedProbeTimeBox):
875
+ return self.__add__(other) # this commutes
876
+ return super().__radd__(other)
875
877
 
876
878
 
877
879
  SHELVED_OFFSET_TOLERANCE = 1e-12
@@ -515,16 +515,13 @@ class ModulatedCosineRiseFall(Waveform):
515
515
 
516
516
  @dataclass(frozen=True)
517
517
  class CosineRise(Waveform):
518
- r"""Cosine Rise waveform.
518
+ r"""Cosine rise waveform.
519
519
 
520
- This waveform assumes that during its duration, the only thing happening is signal occurring to the required
520
+ This waveform assumes that during its duration, the only thing happening is signal rising to the required
521
521
  amplitude.
522
522
  The waveform is made for pairing with 'Constant' waveform to enable arbitrarily long pulses with smooth rise part.
523
523
  The rise time is equal to pulse duration.
524
524
 
525
- Args:
526
- rise_time: Dummy parameter, used only as due to a bug. FIXME it is not used, placed for resolving exa bug
527
-
528
525
  """
529
526
 
530
527
  def _sample(self, sample_coords: np.ndarray) -> np.ndarray:
@@ -533,7 +530,7 @@ class CosineRise(Waveform):
533
530
 
534
531
  @dataclass(frozen=True)
535
532
  class CosineFall(Waveform):
536
- r"""Cosine Rise waveform.
533
+ r"""Cosine fall waveform.
537
534
 
538
535
  This waveform assumes that during its duration, the only thing occurring is signal falling to 0.
539
536
  The waveform is made for pairing with 'Constant' waveform to enable arbitrarily long pulses with smooth fall part.
iqm/pulse/timebox.py CHANGED
@@ -210,6 +210,19 @@ class TimeBox:
210
210
  raise ValueError(f"Tried to access a child of {self}, which is atomic.")
211
211
  return self.children[item]
212
212
 
213
+ def _add_children(self, other: TimeBox) -> TimeBox:
214
+ """Concat the children of self and other together."""
215
+ left = self.children if self.atom is None else (self,)
216
+ right = other.children if other.atom is None else (other,)
217
+ return TimeBox(
218
+ label=self.label,
219
+ locus_components=self.locus_components.union(other.locus_components),
220
+ atom=None,
221
+ children=left + right,
222
+ scheduling=self.scheduling,
223
+ scheduling_algorithm=self.scheduling_algorithm,
224
+ )
225
+
213
226
  def __add__(self, other: TimeBox | Iterable[TimeBox]) -> TimeBox:
214
227
  """Return a new TimeBox which has the contents of this and another TimeBox concatenated.
215
228
 
@@ -231,16 +244,7 @@ class TimeBox:
231
244
  # allow subclasses to override __add__ such that __radd__ also works consistent with that logic
232
245
  return other.__radd__(self) # type: ignore[union-attr]
233
246
  if isinstance(other, TimeBox):
234
- left = self.children if self.atom is None else (self,)
235
- right = other.children if other.atom is None else (other,)
236
- return TimeBox(
237
- label=self.label,
238
- locus_components=self.locus_components.union(other.locus_components),
239
- atom=None,
240
- children=left + right,
241
- scheduling=self.scheduling,
242
- scheduling_algorithm=self.scheduling_algorithm,
243
- )
247
+ return self._add_children(other)
244
248
  try:
245
249
  return reduce(lambda x, y: x + y, other, self)
246
250
  except TypeError as err:
@@ -248,7 +252,7 @@ class TimeBox:
248
252
 
249
253
  def __radd__(self, other: TimeBox | Iterable[TimeBox]) -> TimeBox:
250
254
  if isinstance(other, TimeBox):
251
- return self.__add__(other)
255
+ return other._add_children(self)
252
256
  it = iter(other)
253
257
  try:
254
258
  first = next(it)
@@ -397,6 +401,3 @@ class MultiplexedProbeTimeBox(TimeBox):
397
401
  scheduling_algorithm=SchedulingAlgorithm.HARD_BOUNDARY,
398
402
  )
399
403
  return box
400
-
401
- def __radd__(self, other: TimeBox | Iterable[TimeBox]) -> TimeBox:
402
- return self.__add__(other)
iqm/pulse/utils.py CHANGED
@@ -17,11 +17,15 @@
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
+ from collections.abc import Iterable
21
+ from dataclasses import replace
20
22
  from typing import get_args, get_origin
21
23
 
22
24
  import numpy as np
23
25
 
24
26
  from exa.common.data.parameter import CollectionType, DataType
27
+ from iqm.pulse.playlist import IQPulse
28
+ from iqm.pulse.playlist.waveforms import Samples
25
29
 
26
30
 
27
31
  def map_waveform_param_types(type_hint: type) -> tuple[DataType, CollectionType]:
@@ -107,3 +111,85 @@ def phase_transformation(psi_1: float = 0.0, psi_2: float = 0.0) -> tuple[float,
107
111
 
108
112
  """
109
113
  return psi_2, -(psi_1 + psi_2)
114
+
115
+
116
+ def modulate_iq(pulse: IQPulse) -> np.ndarray:
117
+ """Sampled baseband waveform of an IQ pulse.
118
+
119
+ Note that :attr:`IQPulse.phase_increment` has no effect on the sampled waveform.
120
+ The upconversion oscillator phase incrementation is a separate action performed by the AWG
121
+ that also affects future IQPulses, and thus cannot be represented by an array of waveform samples.
122
+ To replicate the effect of ``pulse`` on an AWG, one should first perform the increment and then
123
+ play the returned samples.
124
+
125
+ Args:
126
+ pulse: IQ pulse.
127
+
128
+ Returns:
129
+ The waveform of ``pulse`` as an array of complex-valued samples.
130
+
131
+ """
132
+ # TODO: Could be an IQPulse method
133
+ wave = pulse.wave_i.sample() * pulse.scale_i + 1j * pulse.wave_q.sample() * pulse.scale_q
134
+ # starting times of the samples, in units of inverse sample rate
135
+ wave_sampletimes = np.arange(len(wave))
136
+ wave *= np.exp(2j * np.pi * pulse.modulation_frequency * wave_sampletimes + 1j * pulse.phase)
137
+ return wave
138
+
139
+
140
+ def fuse_iq_pulses(iq_pulses: Iterable[IQPulse]) -> IQPulse:
141
+ """Fuse multiple IQPulses into one by concatenating the sampled waveforms.
142
+
143
+ Works by flushing :attr:`IQPulse.phase_increment` s to the front, updating the :attr:`IQPulse.phase` s,
144
+ sampling the pulses, concatenating, normalizing the amplitudes, and putting the result
145
+ into a new IQPulse instruction with a ``phase_increment`` that is a sum of the individual ``phase_increment`` s.
146
+
147
+ Additionally, to conserve waveform memory on the AWGs, we normalize the waveform phase by setting
148
+ :attr:`IQPulse.phase` of the fused pulse to the flushed phase of the first pulse.
149
+
150
+ Args:
151
+ iq_pulses: IQPulse instructions to fuse.
152
+
153
+ Returns:
154
+ Fused IQPulse that behaves indentically to the sequence ``iq_pulses`` on an AWG.
155
+
156
+ """
157
+ # flush the phase increments to the start of the pulse sequence
158
+ phases = np.array([i.phase for i in iq_pulses])
159
+ phase_increments = np.array([i.phase_increment for i in iq_pulses])
160
+
161
+ # flushed_phases[k] == phases[k] - np.sum(phase_increments[k+1:])
162
+ flushed_phases = phases - np.cumsum(phase_increments[::-1])[::-1] + phase_increments
163
+
164
+ # Phase normalization of the samples to save waveform memory: There is an internal degree of freedom
165
+ # in the sampled IQPulse: IQPulse.phase can be represented in the global phase of the samples.
166
+ # Fix this d.o.f. by setting the phase of the fused IQ pulse to the phase of the first constituent IQ pulse.
167
+ fused_phase = flushed_phases[0]
168
+ flushed_phases -= fused_phase
169
+ flushed_iq_pulses = [
170
+ replace(instr, phase=phase, phase_increment=0.0) for instr, phase in zip(iq_pulses, flushed_phases)
171
+ ]
172
+ # sample and concatenate the IQ pulses
173
+ samples = np.hstack([modulate_iq(i) for i in flushed_iq_pulses])
174
+
175
+ # normalize the real and imaginary waveform components
176
+ def normalize(samples: np.ndarray) -> tuple[np.ndarray, float]:
177
+ """Normalize real-valued samples to [-1, 1]."""
178
+ scale = np.max(np.abs(samples))
179
+ # avoid division by zero
180
+ if scale > 0:
181
+ return samples / scale, scale
182
+ return samples, 1.0
183
+
184
+ samples.real, scale_i = normalize(samples.real)
185
+ samples.imag, scale_q = normalize(samples.imag)
186
+ return IQPulse(
187
+ duration=len(samples),
188
+ wave_i=Samples(samples.real),
189
+ wave_q=Samples(samples.imag),
190
+ scale_i=scale_i,
191
+ scale_q=scale_q,
192
+ phase=fused_phase,
193
+ phase_increment=np.sum(phase_increments),
194
+ modulation_frequency=0.0, # modulate_iq takes care of this
195
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: iqm-pulse
3
- Version: 12.4.0
3
+ Version: 12.6.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
@@ -6,18 +6,18 @@ iqm/pulse/gate_implementation.py,sha256=nyoP54-VWKx9rO8MW5ZqCRPjsMTKfJFu_6cgabxJ
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=FCwnQRQSrN_9O8obSJ16VXrSbSlg4CSdAPY8K2I8qCo,17226
10
- iqm/pulse/utils.py,sha256=0J4KQNvKtZahme7sP7vLa1FtENepC-eb9_A5WaghvVU,3710
9
+ iqm/pulse/timebox.py,sha256=VbNgxYsXP-Plx4FAX9b2v_CThJJh9dBzFun3E5jzP7o,17249
10
+ iqm/pulse/utils.py,sha256=UjB5AHgDOj_5nm-Rt6Ql9GJpjV3OrtwpEVzysXhQghY,7466
11
11
  iqm/pulse/validation.py,sha256=U392B1QtX2nRNAD2j1eThrim1VH18SNKGYAYUVR9_q4,10781
12
12
  iqm/pulse/gates/__init__.py,sha256=l07iD52FiKKQyVFwswRapZMqEE0XfILSc4aZ5sXGJo4,8811
13
13
  iqm/pulse/gates/barrier.py,sha256=WhYV70lf4lh4Wa9UZuMk2lp9JbUQIu8lzewRC2P7pNE,2546
14
- iqm/pulse/gates/conditional.py,sha256=NE-1GYlFcfaCmRuhqGEIhtoJmBArhyY5KhVnO4y4zng,6289
14
+ iqm/pulse/gates/conditional.py,sha256=mY7b4HmznFF5VUAXrbK7tDxCP4n2pASINa3TJHjHA6o,6872
15
15
  iqm/pulse/gates/cz.py,sha256=2-LqAMG08dOReQp8K517zTkMmhDfwXhEXPqTPyjppDk,33519
16
16
  iqm/pulse/gates/default_gates.py,sha256=JwdrCh_Blee4mMWZ8v8ymw3Ep0fZBvw9wuaLR3rjDXA,8073
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=_ujAxZb4b6NPXSMh7JPgJevMATHOc9oUUdwd0PTLios,51853
20
+ iqm/pulse/gates/measure.py,sha256=3gxMjUvQrum9ZVnm8rvRHqPUQYqOCnD3IsgnrP-vECY,51968
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
@@ -31,7 +31,7 @@ iqm/pulse/playlist/hd_drag.py,sha256=JYRy3aPu0LD9y0H3UncOiWI39eV9-TARvgLonNUsaO0
31
31
  iqm/pulse/playlist/instructions.py,sha256=z9umJqFLTsSd26EuzW-X5raWVHGpSLds9s4m8tDAV-E,10249
32
32
  iqm/pulse/playlist/playlist.py,sha256=7MIX4iRfh2Nit8AnpNyqv5A5ZmK7JR-v201DXVL6kUk,685
33
33
  iqm/pulse/playlist/schedule.py,sha256=ToaHKgDxNxLUQalI2TAMMld-EH5Im9rZNktQkXhOyBk,18037
34
- iqm/pulse/playlist/waveforms.py,sha256=rbP87uMjieKTYi6g4Twx_oPiVIDCTcTuWz5LaYhsaCA,24101
34
+ iqm/pulse/playlist/waveforms.py,sha256=w5Kgpo7VJwsaCUUKOu3rf5pNmN7Jw2jVLTn3hH-k0R4,23973
35
35
  iqm/pulse/playlist/visualisation/__init__.py,sha256=wCNfJHIR_bqG3ZBlgm55v90Rih7VCpfctoIMfwRMgjk,567
36
36
  iqm/pulse/playlist/visualisation/base.py,sha256=qk7oHvMFk7d3ybNwJ7TpZEX8ahxfzB-kVCzqgjKM71Y,8932
37
37
  iqm/pulse/playlist/visualisation/templates/playlist_inspection.jinja2,sha256=4XPmpISH5LLOf3YjmK3OKfvkAWj4TlzD2K4ClFTvZhI,11511
@@ -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.4.0.dist-info/LICENSE.txt,sha256=R6Q7eUrLyoCQgWYorQ8WJmVmWKYU3dxA3jYUp0wwQAw,11332
43
- iqm_pulse-12.4.0.dist-info/METADATA,sha256=q0rATe2XxoAOfBeOILxcAVWhoCOw_AbJ6VmmG7-gg2Y,14551
44
- iqm_pulse-12.4.0.dist-info/WHEEL,sha256=y4mX-SOX4fYIkonsAGA5N0Oy-8_gI4FXw5HNI1xqvWg,91
45
- iqm_pulse-12.4.0.dist-info/top_level.txt,sha256=NB4XRfyDS6_wG9gMsyX-9LTU7kWnTQxNvkbzIxGv3-c,4
46
- iqm_pulse-12.4.0.dist-info/RECORD,,
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,,