iqm-pulse 12.5.0__tar.gz → 12.6.1__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 (83) hide show
  1. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/CHANGELOG.rst +14 -0
  2. {iqm_pulse-12.5.0/src/iqm_pulse.egg-info → iqm_pulse-12.6.1}/PKG-INFO +1 -1
  3. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/gates/conditional.py +32 -13
  4. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/playlist/waveforms.py +3 -6
  5. iqm_pulse-12.6.1/src/iqm/pulse/utils.py +195 -0
  6. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1/src/iqm_pulse.egg-info}/PKG-INFO +1 -1
  7. iqm_pulse-12.6.1/version.txt +1 -0
  8. iqm_pulse-12.5.0/src/iqm/pulse/utils.py +0 -109
  9. iqm_pulse-12.5.0/version.txt +0 -1
  10. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/LICENSE.txt +0 -0
  11. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/MANIFEST.in +0 -0
  12. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/README.rst +0 -0
  13. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/docs/API.rst +0 -0
  14. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/docs/Makefile +0 -0
  15. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/docs/_static/.gitignore +0 -0
  16. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/docs/_static/css/custom.css +0 -0
  17. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/docs/_static/images/favicon.ico +0 -0
  18. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/docs/_static/images/feedback_timing.svg +0 -0
  19. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/docs/_static/images/logo.png +0 -0
  20. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/docs/_static/images/playlist_breakdown.svg +0 -0
  21. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/docs/_static/images/pulse_timing.svg +0 -0
  22. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/docs/_static/images/readout_timing.svg +0 -0
  23. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/docs/_templates/autosummary-class-template.rst +0 -0
  24. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/docs/_templates/autosummary-module-template.rst +0 -0
  25. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/docs/changelog.rst +0 -0
  26. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/docs/concepts.rst +0 -0
  27. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/docs/conf.py +0 -0
  28. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/docs/custom_gates.rst +0 -0
  29. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/docs/index.rst +0 -0
  30. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/docs/license.rst +0 -0
  31. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/docs/pulse_timing.rst +0 -0
  32. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/docs/references.bib +0 -0
  33. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/docs/references.rst +0 -0
  34. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/docs/using_builder.rst +0 -0
  35. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/pyproject.toml +0 -0
  36. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/requirements/base.in +0 -0
  37. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/requirements/base.txt +0 -0
  38. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/setup.cfg +0 -0
  39. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/setup.py +0 -0
  40. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/__init__.py +0 -0
  41. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/base_utils.py +0 -0
  42. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/builder.py +0 -0
  43. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/circuit_operations.py +0 -0
  44. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/gate_implementation.py +0 -0
  45. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/gates/__init__.py +0 -0
  46. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/gates/barrier.py +0 -0
  47. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/gates/cz.py +0 -0
  48. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/gates/default_gates.py +0 -0
  49. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/gates/delay.py +0 -0
  50. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/gates/enums.py +0 -0
  51. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/gates/flux_multiplexer.py +0 -0
  52. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/gates/measure.py +0 -0
  53. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/gates/move.py +0 -0
  54. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/gates/prx.py +0 -0
  55. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/gates/reset.py +0 -0
  56. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/gates/rz.py +0 -0
  57. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/gates/sx.py +0 -0
  58. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/gates/u.py +0 -0
  59. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/playlist/__init__.py +0 -0
  60. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/playlist/channel.py +0 -0
  61. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/playlist/fast_drag.py +0 -0
  62. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/playlist/hd_drag.py +0 -0
  63. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/playlist/instructions.py +0 -0
  64. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/playlist/playlist.py +0 -0
  65. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/playlist/schedule.py +0 -0
  66. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/playlist/visualisation/__init__.py +0 -0
  67. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/playlist/visualisation/base.py +0 -0
  68. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/playlist/visualisation/templates/playlist_inspection.jinja2 +0 -0
  69. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/playlist/visualisation/templates/static/logo.png +0 -0
  70. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/playlist/visualisation/templates/static/moment.min.js +0 -0
  71. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/playlist/visualisation/templates/static/vis-timeline-graph2d.min.css +0 -0
  72. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/playlist/visualisation/templates/static/vis-timeline-graph2d.min.js +0 -0
  73. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/py.typed +0 -0
  74. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/quantum_ops.py +0 -0
  75. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/scheduler.py +0 -0
  76. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/timebox.py +0 -0
  77. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm/pulse/validation.py +0 -0
  78. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm_pulse.egg-info/SOURCES.txt +0 -0
  79. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm_pulse.egg-info/dependency_links.txt +0 -0
  80. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm_pulse.egg-info/requires.txt +0 -0
  81. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/src/iqm_pulse.egg-info/top_level.txt +0 -0
  82. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/tests/.pylintrc +0 -0
  83. {iqm_pulse-12.5.0 → iqm_pulse-12.6.1}/tests/__init__.py +0 -0
@@ -2,6 +2,20 @@
2
2
  Changelog
3
3
  =========
4
4
 
5
+ Version 12.6.1 (2025-10-27)
6
+ ===========================
7
+
8
+ - Bump version for 4.3.1 release. No functional changes.
9
+
10
+ Version 12.6.0 (2025-10-23)
11
+ ===========================
12
+
13
+ Features
14
+ --------
15
+
16
+ - ``cc_prx`` gate now supports ``prx`` gate implementations that consists of multiple drive IQ pulses,
17
+ e.g. ones that use the ``sx`` gate. As a result, the ``reset`` operation now also works with them.
18
+
5
19
  Version 12.5.0 (2025-10-09)
6
20
  ===========================
7
21
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: iqm-pulse
3
- Version: 12.5.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
@@ -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
  )
@@ -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.
@@ -0,0 +1,195 @@
1
+ # ********************************************************************************
2
+ #
3
+ # Copyright 2024 IQM
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ """Utility functions."""
17
+
18
+ from __future__ import annotations
19
+
20
+ from collections.abc import Iterable
21
+ from dataclasses import replace
22
+ from typing import get_args, get_origin
23
+
24
+ import numpy as np
25
+
26
+ from exa.common.data.parameter import CollectionType, DataType
27
+ from iqm.pulse.playlist import IQPulse
28
+ from iqm.pulse.playlist.waveforms import Samples
29
+
30
+
31
+ def map_waveform_param_types(type_hint: type) -> tuple[DataType, CollectionType]:
32
+ """Map a python typehint into EXA Parameter's `(DataType, CollectionType)` tuple.
33
+
34
+ Args:
35
+ type: python typehint.
36
+
37
+ Returns:
38
+ A `(DataType, CollectionType)` tuple
39
+
40
+ Raises:
41
+ ValueError: for a non-supported type.
42
+
43
+ """
44
+ value_error = ValueError(f"Nonsupported datatype for a waveform parameter: {type_hint}")
45
+ if hasattr(type_hint, "__iter__") and type_hint is not str:
46
+ if type_hint == np.ndarray:
47
+ data_type = DataType.COMPLEX # due to np.ndarray not being generic we assume complex numbers
48
+ collection_type = CollectionType.NDARRAY
49
+ return (data_type, collection_type)
50
+ if get_origin(type_hint) is list:
51
+ collection_type = CollectionType.LIST
52
+ type_hint = get_args(type_hint)[0]
53
+ else:
54
+ raise value_error
55
+ else:
56
+ collection_type = CollectionType.SCALAR
57
+
58
+ if type_hint is float:
59
+ data_type = DataType.FLOAT
60
+ elif type_hint is int:
61
+ data_type = DataType.INT
62
+ elif type_hint is str:
63
+ data_type = DataType.STRING
64
+ elif type_hint is complex:
65
+ data_type = DataType.COMPLEX
66
+ elif type_hint is bool:
67
+ data_type = DataType.BOOLEAN
68
+ else:
69
+ raise value_error
70
+ return (data_type, collection_type)
71
+
72
+
73
+ def normalize_angle(angle: float) -> float:
74
+ """Normalize the given angle to (-pi, pi].
75
+
76
+ Args:
77
+ angle: angle to normalize (in radians)
78
+
79
+ Returns:
80
+ ``angle`` normalized to (-pi, pi]
81
+
82
+ """
83
+ half_turn = np.pi
84
+ full_turn = 2 * half_turn
85
+ return (angle - half_turn) % -full_turn + half_turn
86
+
87
+
88
+ def phase_transformation(psi_1: float = 0.0, psi_2: float = 0.0) -> tuple[float, float]:
89
+ r"""Implement an arbitrary (RZ, PRX, RZ) gate sequence by modifying the parameters of the
90
+ IQ pulse implementing the PRX.
91
+
92
+ By commutation rules we have
93
+
94
+ .. math::
95
+ RZ(\psi_2) \: PRX(\theta, \phi) \: RZ(\psi_1) = PRX(\theta, \phi+\psi_2) \: RZ(\psi_1 + \psi_2).
96
+
97
+ Hence an arbitrary (RZ, PRX, RZ) gate sequence is equivalent to (RZ, PRX) with adjusted angles.
98
+
99
+ Use case: with resonant driving, the PRX gate can be implemented using an :class:`IQPulse` instance,
100
+ and the preceding RZ can be handled by decrementing the local oscillator phase beforehand (something
101
+ the IQPulse instruction can also do), which is equivalent to rotating the local computational frame
102
+ around the z axis in the opposite direction of the required quantum state rotation.
103
+
104
+ Args:
105
+ psi_1: RZ angle before the PRX (in rad)
106
+ psi_2: RZ angle after the PRX (in rad)
107
+
108
+ Returns:
109
+ change to the PRX phase angle (in rad),
110
+ phase increment for the IQ pulse that implements the remaining RZ (in rad)
111
+
112
+ """
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.5.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
@@ -0,0 +1 @@
1
+ 12.6.1
@@ -1,109 +0,0 @@
1
- # ********************************************************************************
2
- #
3
- # Copyright 2024 IQM
4
- #
5
- # Licensed under the Apache License, Version 2.0 (the "License");
6
- # you may not use this file except in compliance with the License.
7
- # You may obtain a copy of the License at
8
- #
9
- # http://www.apache.org/licenses/LICENSE-2.0
10
- #
11
- # Unless required by applicable law or agreed to in writing, software
12
- # distributed under the License is distributed on an "AS IS" BASIS,
13
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
- # See the License for the specific language governing permissions and
15
- # limitations under the License.
16
- """Utility functions."""
17
-
18
- from __future__ import annotations
19
-
20
- from typing import get_args, get_origin
21
-
22
- import numpy as np
23
-
24
- from exa.common.data.parameter import CollectionType, DataType
25
-
26
-
27
- def map_waveform_param_types(type_hint: type) -> tuple[DataType, CollectionType]:
28
- """Map a python typehint into EXA Parameter's `(DataType, CollectionType)` tuple.
29
-
30
- Args:
31
- type: python typehint.
32
-
33
- Returns:
34
- A `(DataType, CollectionType)` tuple
35
-
36
- Raises:
37
- ValueError: for a non-supported type.
38
-
39
- """
40
- value_error = ValueError(f"Nonsupported datatype for a waveform parameter: {type_hint}")
41
- if hasattr(type_hint, "__iter__") and type_hint is not str:
42
- if type_hint == np.ndarray:
43
- data_type = DataType.COMPLEX # due to np.ndarray not being generic we assume complex numbers
44
- collection_type = CollectionType.NDARRAY
45
- return (data_type, collection_type)
46
- if get_origin(type_hint) is list:
47
- collection_type = CollectionType.LIST
48
- type_hint = get_args(type_hint)[0]
49
- else:
50
- raise value_error
51
- else:
52
- collection_type = CollectionType.SCALAR
53
-
54
- if type_hint is float:
55
- data_type = DataType.FLOAT
56
- elif type_hint is int:
57
- data_type = DataType.INT
58
- elif type_hint is str:
59
- data_type = DataType.STRING
60
- elif type_hint is complex:
61
- data_type = DataType.COMPLEX
62
- elif type_hint is bool:
63
- data_type = DataType.BOOLEAN
64
- else:
65
- raise value_error
66
- return (data_type, collection_type)
67
-
68
-
69
- def normalize_angle(angle: float) -> float:
70
- """Normalize the given angle to (-pi, pi].
71
-
72
- Args:
73
- angle: angle to normalize (in radians)
74
-
75
- Returns:
76
- ``angle`` normalized to (-pi, pi]
77
-
78
- """
79
- half_turn = np.pi
80
- full_turn = 2 * half_turn
81
- return (angle - half_turn) % -full_turn + half_turn
82
-
83
-
84
- def phase_transformation(psi_1: float = 0.0, psi_2: float = 0.0) -> tuple[float, float]:
85
- r"""Implement an arbitrary (RZ, PRX, RZ) gate sequence by modifying the parameters of the
86
- IQ pulse implementing the PRX.
87
-
88
- By commutation rules we have
89
-
90
- .. math::
91
- RZ(\psi_2) \: PRX(\theta, \phi) \: RZ(\psi_1) = PRX(\theta, \phi+\psi_2) \: RZ(\psi_1 + \psi_2).
92
-
93
- Hence an arbitrary (RZ, PRX, RZ) gate sequence is equivalent to (RZ, PRX) with adjusted angles.
94
-
95
- Use case: with resonant driving, the PRX gate can be implemented using an :class:`IQPulse` instance,
96
- and the preceding RZ can be handled by decrementing the local oscillator phase beforehand (something
97
- the IQPulse instruction can also do), which is equivalent to rotating the local computational frame
98
- around the z axis in the opposite direction of the required quantum state rotation.
99
-
100
- Args:
101
- psi_1: RZ angle before the PRX (in rad)
102
- psi_2: RZ angle after the PRX (in rad)
103
-
104
- Returns:
105
- change to the PRX phase angle (in rad),
106
- phase increment for the IQ pulse that implements the remaining RZ (in rad)
107
-
108
- """
109
- return psi_2, -(psi_1 + psi_2)
@@ -1 +0,0 @@
1
- 12.5.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