waveforms 3.2.0__tar.gz → 3.3.2__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 (36) hide show
  1. {waveforms-3.2.0 → waveforms-3.3.2}/MANIFEST.in +0 -2
  2. {waveforms-3.2.0/waveforms.egg-info → waveforms-3.3.2}/PKG-INFO +18 -11
  3. {waveforms-3.2.0 → waveforms-3.3.2}/README.md +17 -10
  4. {waveforms-3.2.0 → waveforms-3.3.2}/setup.py +4 -0
  5. waveforms-3.3.2/tests/test_core.py +322 -0
  6. {waveforms-3.2.0 → waveforms-3.3.2}/tests/test_waveform.py +49 -36
  7. {waveforms-3.2.0 → waveforms-3.3.2}/waveforms/__init__.py +4 -15
  8. waveforms-3.3.2/waveforms/_cwaveform.c +4563 -0
  9. {waveforms-3.2.0 → waveforms-3.3.2}/waveforms/_cwaveform.h +50 -0
  10. waveforms-3.3.2/waveforms/_cwaveform.md +85 -0
  11. waveforms-3.3.2/waveforms/_waveform.pyi +131 -0
  12. waveforms-3.3.2/waveforms/_waveform.pyx +1125 -0
  13. {waveforms-3.2.0 → waveforms-3.3.2}/waveforms/version.py +1 -1
  14. {waveforms-3.2.0 → waveforms-3.3.2}/waveforms/waveform.py +662 -1002
  15. {waveforms-3.2.0 → waveforms-3.3.2/waveforms.egg-info}/PKG-INFO +18 -11
  16. {waveforms-3.2.0 → waveforms-3.3.2}/waveforms.egg-info/SOURCES.txt +1 -3
  17. waveforms-3.2.0/src/waveform.c +0 -248
  18. waveforms-3.2.0/src/waveform.h +0 -171
  19. waveforms-3.2.0/tests/test_native.py +0 -242
  20. waveforms-3.2.0/waveforms/_cwaveform.c +0 -2034
  21. waveforms-3.2.0/waveforms/_cwaveform.md +0 -74
  22. waveforms-3.2.0/waveforms/_waveform.pyi +0 -171
  23. waveforms-3.2.0/waveforms/_waveform.pyx +0 -2823
  24. {waveforms-3.2.0 → waveforms-3.3.2}/LICENSE +0 -0
  25. {waveforms-3.2.0 → waveforms-3.3.2}/pyproject.toml +0 -0
  26. {waveforms-3.2.0 → waveforms-3.3.2}/setup.cfg +0 -0
  27. {waveforms-3.2.0 → waveforms-3.3.2}/tests/test_wavevstack.py +0 -0
  28. {waveforms-3.2.0 → waveforms-3.3.2}/waveforms/Waveform.g4 +0 -0
  29. {waveforms-3.2.0 → waveforms-3.3.2}/waveforms/__main__.py +0 -0
  30. {waveforms-3.2.0 → waveforms-3.3.2}/waveforms/distortion.py +0 -0
  31. {waveforms-3.2.0 → waveforms-3.3.2}/waveforms/utils.py +0 -0
  32. {waveforms-3.2.0 → waveforms-3.3.2}/waveforms/waveform_parser.py +0 -0
  33. {waveforms-3.2.0 → waveforms-3.3.2}/waveforms.egg-info/dependency_links.txt +0 -0
  34. {waveforms-3.2.0 → waveforms-3.3.2}/waveforms.egg-info/entry_points.txt +0 -0
  35. {waveforms-3.2.0 → waveforms-3.3.2}/waveforms.egg-info/requires.txt +0 -0
  36. {waveforms-3.2.0 → waveforms-3.3.2}/waveforms.egg-info/top_level.txt +0 -0
@@ -1,5 +1,3 @@
1
- include src/*.h
2
- include src/*.c
3
1
  include waveforms/_cwaveform.h
4
2
  include waveforms/_cwaveform.c
5
3
  include waveforms/_cwaveform.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: waveforms
3
- Version: 3.2.0
3
+ Version: 3.3.2
4
4
  Summary: Edit waveforms used in experiment
5
5
  Author-email: feihoo87 <feihoo87@gmail.com>
6
6
  Maintainer-email: feihoo87 <feihoo87@gmail.com>
@@ -98,11 +98,13 @@ plt.plot(t, y_wav(t))
98
98
  plt.show()
99
99
  ```
100
100
 
101
- ### Packed binary representation
101
+ ### Unified binary representation
102
102
 
103
- Starting with version 3, `Waveform` and `WaveVStack` use a packed-binary backend.
104
- It is intended for workloads with many small pulses and for low-overhead
105
- serialization.
103
+ `Waveform` is the common base class for every signal object. `RealWaveform`
104
+ and `ComplexWaveform` are concrete waveforms; `WaveVStack` is the common stack
105
+ base, with `RealWaveVStack` and `ComplexWaveVStack` as its concrete forms.
106
+ The original construction style remains unchanged and automatically selects
107
+ the compact C core for common pulses and repeated-pulse stacks:
106
108
 
107
109
  ```python
108
110
  import waveforms as wf
@@ -114,12 +116,17 @@ import waveforms as wf
114
116
  pulse = (wf.gaussian(12e-9) >> 20e-9) * wf.cos(2 * wf.pi * 5e9)
115
117
  data = pulse.to_bytes()
116
118
  restored = wf.Waveform.from_bytes(data)
119
+
120
+ assert isinstance(pulse, wf.Waveform)
121
+ stack = wf.WaveVStack([pulse, pulse >> 40e-9])
122
+ assert isinstance(stack, wf.RealWaveVStack)
123
+ assert isinstance(stack, wf.WaveVStack)
117
124
  ```
118
125
 
119
- Time resolution is process-wide and is not stored in each binary block. A
120
- process loading a block must therefore use the same resolution as the process
121
- that created it. The setting is locked when the first waveform object is
122
- created or loaded.
126
+ The C block format stores signed 64-bit ticks using a process-wide clock. The
127
+ default is 120 GHz. A different process-wide time resolution may be selected
128
+ before the first waveform is constructed; the same C representation and
129
+ evaluator continue to be used. The setting is locked by the first object.
123
130
 
124
131
  `sample()` has integer-grid fast paths for 500 MHz, 1 GHz, 1.2 GHz, 2 GHz,
125
132
  2.4 GHz, 2.5 GHz, 4 GHz, 6 GHz, 8 GHz, and 10 GHz. Real waveforms can be
@@ -131,7 +138,7 @@ pulse.stop = 100e-9
131
138
  dac16 = pulse.sample(2_400_000_000, dtype=np.int16, full_scale=1.0)
132
139
  ```
133
140
 
134
- The packed core stores and evaluates real-valued signals only. Complex signals
141
+ The low-level cores store and evaluate real-valued signals only. Complex signals
135
142
  are represented in Python as independent real and imaginary channels:
136
143
 
137
144
  ```python
@@ -142,7 +149,7 @@ stack = wf.ComplexWaveVStack([z, z >> 20e-9])
142
149
  samples = stack(t) # complex NumPy array
143
150
  ```
144
151
 
145
- `Waveform` and `WaveVStack` therefore avoid complex storage and arithmetic for
152
+ Real waveforms and stacks therefore avoid complex storage and arithmetic for
146
153
  the common real-valued case. `ComplexWaveform.real` and `.imag` expose the two
147
154
  real channel waveforms.
148
155
 
@@ -54,11 +54,13 @@ plt.plot(t, y_wav(t))
54
54
  plt.show()
55
55
  ```
56
56
 
57
- ### Packed binary representation
57
+ ### Unified binary representation
58
58
 
59
- Starting with version 3, `Waveform` and `WaveVStack` use a packed-binary backend.
60
- It is intended for workloads with many small pulses and for low-overhead
61
- serialization.
59
+ `Waveform` is the common base class for every signal object. `RealWaveform`
60
+ and `ComplexWaveform` are concrete waveforms; `WaveVStack` is the common stack
61
+ base, with `RealWaveVStack` and `ComplexWaveVStack` as its concrete forms.
62
+ The original construction style remains unchanged and automatically selects
63
+ the compact C core for common pulses and repeated-pulse stacks:
62
64
 
63
65
  ```python
64
66
  import waveforms as wf
@@ -70,12 +72,17 @@ import waveforms as wf
70
72
  pulse = (wf.gaussian(12e-9) >> 20e-9) * wf.cos(2 * wf.pi * 5e9)
71
73
  data = pulse.to_bytes()
72
74
  restored = wf.Waveform.from_bytes(data)
75
+
76
+ assert isinstance(pulse, wf.Waveform)
77
+ stack = wf.WaveVStack([pulse, pulse >> 40e-9])
78
+ assert isinstance(stack, wf.RealWaveVStack)
79
+ assert isinstance(stack, wf.WaveVStack)
73
80
  ```
74
81
 
75
- Time resolution is process-wide and is not stored in each binary block. A
76
- process loading a block must therefore use the same resolution as the process
77
- that created it. The setting is locked when the first waveform object is
78
- created or loaded.
82
+ The C block format stores signed 64-bit ticks using a process-wide clock. The
83
+ default is 120 GHz. A different process-wide time resolution may be selected
84
+ before the first waveform is constructed; the same C representation and
85
+ evaluator continue to be used. The setting is locked by the first object.
79
86
 
80
87
  `sample()` has integer-grid fast paths for 500 MHz, 1 GHz, 1.2 GHz, 2 GHz,
81
88
  2.4 GHz, 2.5 GHz, 4 GHz, 6 GHz, 8 GHz, and 10 GHz. Real waveforms can be
@@ -87,7 +94,7 @@ pulse.stop = 100e-9
87
94
  dac16 = pulse.sample(2_400_000_000, dtype=np.int16, full_scale=1.0)
88
95
  ```
89
96
 
90
- The packed core stores and evaluates real-valued signals only. Complex signals
97
+ The low-level cores store and evaluate real-valued signals only. Complex signals
91
98
  are represented in Python as independent real and imaginary channels:
92
99
 
93
100
  ```python
@@ -98,7 +105,7 @@ stack = wf.ComplexWaveVStack([z, z >> 20e-9])
98
105
  samples = stack(t) # complex NumPy array
99
106
  ```
100
107
 
101
- `Waveform` and `WaveVStack` therefore avoid complex storage and arithmetic for
108
+ Real waveforms and stacks therefore avoid complex storage and arithmetic for
102
109
  the common real-valued case. `ComplexWaveform.real` and `.imag` expose the two
103
110
  real channel waveforms.
104
111
 
@@ -39,15 +39,19 @@ def get_extensions():
39
39
  sources = [os.path.join(dirpath, filename)]
40
40
  include_dirs = []
41
41
  extra_link_args = []
42
+ libraries = []
42
43
  if filename == '_waveform.pyx':
43
44
  sources.append(os.path.join(
44
45
  'waveforms', '_cwaveform.c'))
45
46
  include_dirs.append('waveforms')
46
47
  if sys.platform == 'darwin':
47
48
  extra_link_args.extend(['-framework', 'Accelerate'])
49
+ elif sys.platform.startswith('linux'):
50
+ libraries.append('m')
48
51
  extensions.append(
49
52
  Extension(module_name(dirpath, filename), sources,
50
53
  include_dirs=include_dirs,
54
+ libraries=libraries,
51
55
  extra_link_args=extra_link_args))
52
56
 
53
57
  return extensions
@@ -0,0 +1,322 @@
1
+ import pickle
2
+ from pathlib import Path
3
+
4
+ import numpy as np
5
+ import pytest
6
+
7
+ import waveforms as wf
8
+ import waveforms._waveform as core
9
+ from waveforms._waveform import quantize_samples
10
+
11
+
12
+ RATE = 2_400_000_000
13
+
14
+
15
+ def _pulse():
16
+ return (
17
+ 0.8 * wf.gaussian(20e-9) * wf.cos(2 * np.pi * 100e6)
18
+ + 0.15 * wf.gaussian(12e-9) * wf.sin(2 * np.pi * 180e6, 0.2)
19
+ + 0.05 * wf.square(6e-9)
20
+ )
21
+
22
+
23
+ def _stack(count, spacing=40e-9):
24
+ pulse = 0.8 * wf.gaussian(20e-9) * wf.cos(2 * np.pi * 100e6)
25
+ stack = wf.WaveVStack([
26
+ pulse >> (index * spacing) for index in range(count)
27
+ ])
28
+ stack.start = -20e-9
29
+ stack.stop = count * spacing
30
+ stack.sample_rate = RATE
31
+ return stack
32
+
33
+
34
+ def test_unified_type_hierarchy_and_removed_legacy_api():
35
+ real = wf.gaussian(20e-9)
36
+ complex_wave = real + 0.5j * real
37
+ real_stack = wf.WaveVStack([real])
38
+ complex_stack = wf.ComplexWaveVStack([complex_wave])
39
+
40
+ assert isinstance(real, wf.RealWaveform)
41
+ assert isinstance(real_stack, wf.RealWaveVStack)
42
+ assert isinstance(real_stack, wf.WaveVStack)
43
+ assert isinstance(complex_stack, wf.WaveVStack)
44
+ assert all(isinstance(value, wf.Waveform) for value in (
45
+ real, complex_wave, real_stack, complex_stack
46
+ ))
47
+ assert not any(name.startswith("native_") for name in dir(wf))
48
+ assert not any(name.startswith("Native") for name in dir(wf))
49
+ assert not hasattr(core, "PackedWaveform")
50
+ assert not hasattr(core, "PackedStack")
51
+
52
+
53
+ def test_canonical_real_waveform_equality_and_hash():
54
+ a = wf.gaussian(12e-9)
55
+ b = wf.cos(2 * np.pi * 100e6)
56
+
57
+ assert a + b == b + a
58
+ assert hash(a + b) == hash(b + a)
59
+ assert a + a == 2 * a
60
+ assert a * a == a ** 2
61
+ assert (a + b).simplify().to_bytes() == (b + a).simplify().to_bytes()
62
+ assert wf.WaveVStack([a, b]) == wf.WaveVStack([b, a])
63
+
64
+
65
+ def test_c_symbolic_frequency_filter():
66
+ envelope = wf.gaussian(20e-9)
67
+ low = envelope * wf.cos(2 * np.pi * 100e6)
68
+ high = 0.25 * envelope * wf.sin(2 * np.pi * 300e6)
69
+ baseband = 0.1 * envelope
70
+ signal = low + high + baseband
71
+ x = np.linspace(-20e-9, 20e-9, 4097)
72
+
73
+ assert np.allclose(
74
+ signal.filter(2 * np.pi * 200e6, 2 * np.pi * 400e6)(x),
75
+ high(x),
76
+ )
77
+ assert np.allclose(
78
+ signal.filter(0, 2 * np.pi * 200e6)(x),
79
+ (low + baseband)(x),
80
+ )
81
+
82
+
83
+ def test_c_symbolic_filter_reduces_trigonometric_products_and_powers():
84
+ x = np.linspace(-1.0, 1.0, 4097)
85
+ envelope = wf.gaussian(2.0)
86
+ first_frequency = 9.0
87
+ second_frequency = 7.0
88
+ first_phase = 0.31
89
+ second_phase = -0.27
90
+ cutoff = 5.0
91
+
92
+ first_cos = wf.cos(first_frequency, first_phase)
93
+ first_sin = wf.sin(first_frequency, first_phase)
94
+ second_cos = wf.cos(second_frequency, second_phase)
95
+ second_sin = wf.sin(second_frequency, second_phase)
96
+ difference = first_frequency - second_frequency
97
+ phase_difference = first_phase - second_phase
98
+
99
+ assert np.allclose(
100
+ (2 * envelope * first_cos * second_cos).filter(high=cutoff)(x),
101
+ (envelope * wf.cos(difference, phase_difference))(x),
102
+ atol=2e-11,
103
+ )
104
+ assert np.allclose(
105
+ (2 * envelope * first_cos * second_sin).filter(high=cutoff)(x),
106
+ (-envelope * wf.sin(difference, phase_difference))(x),
107
+ atol=2e-11,
108
+ )
109
+ assert np.allclose(
110
+ (2 * envelope * first_sin * second_sin).filter(high=cutoff)(x),
111
+ (envelope * wf.cos(difference, phase_difference))(x),
112
+ atol=2e-11,
113
+ )
114
+
115
+ powered = wf.cos(first_frequency, first_phase) ** 2
116
+ assert np.allclose(powered.filter(high=cutoff)(x), 0.5, atol=2e-11)
117
+ assert np.allclose(
118
+ powered.filter(2 * first_frequency, np.inf)(x),
119
+ (0.5 * wf.cos(2 * first_frequency, 2 * first_phase))(x),
120
+ atol=2e-11,
121
+ )
122
+
123
+ retained = (1.5e-15 * first_cos).filter(eps=1e-15)
124
+ discarded = (0.5e-15 * first_cos).filter(eps=1e-15)
125
+ assert np.max(np.abs(retained(x))) > 1e-15
126
+ assert np.array_equal(discarded(x), np.zeros_like(x))
127
+
128
+
129
+ def test_down_conversion_filter_matches_historic_symbolic_behavior():
130
+ x = np.linspace(-100.0, 100.0, 10001)
131
+ envelope = wf.gaussian(100.0)
132
+ radio_frequency = 92.0451
133
+ local_frequency = 92.0
134
+ phase = 0.32
135
+ rf, _ = wf.mixing(
136
+ envelope, freq=radio_frequency, phase=phase, DRAGScaling=0.0,
137
+ )
138
+
139
+ i = (2 * rf * wf.cos(-2 * np.pi * local_frequency)).filter(
140
+ high=2 * np.pi * local_frequency,
141
+ )
142
+ q = (2 * rf * wf.sin(-2 * np.pi * local_frequency)).filter(
143
+ high=2 * np.pi * local_frequency,
144
+ )
145
+ difference = 2 * np.pi * (radio_frequency - local_frequency)
146
+
147
+ assert np.allclose(
148
+ i(x), (envelope * wf.cos(difference, -phase))(x), atol=2e-9,
149
+ )
150
+ assert np.allclose(
151
+ q(x), (envelope * wf.sin(difference, -phase))(x), atol=2e-9,
152
+ )
153
+
154
+
155
+ def test_wave_block_roundtrip_pickle_and_numerics():
156
+ actual = _pulse()
157
+ actual.start = -20e-9
158
+ actual.stop = 20e-9
159
+ actual.sample_rate = RATE
160
+ actual.label = "pulse"
161
+ data = actual.to_bytes()
162
+ restored = wf.Waveform.from_bytes(data)
163
+ x = np.linspace(-20e-9, 20e-9, 200_000, endpoint=False)
164
+
165
+ assert data[:4] == b"WNF4"
166
+ assert actual.to_bytes() is data
167
+ assert restored.to_bytes() is data
168
+ assert restored == wf.Waveform.from_bytes(data)
169
+ assert wf.RealWaveform.from_bytes(data) == restored
170
+ assert np.array_equal(restored(x), actual(x))
171
+
172
+ unpickled = pickle.loads(pickle.dumps(actual, protocol=5))
173
+ assert unpickled == actual
174
+ assert unpickled.start == actual.start
175
+ assert unpickled.stop == actual.stop
176
+ assert unpickled.sample_rate == actual.sample_rate
177
+ assert unpickled.label == actual.label
178
+
179
+
180
+ @pytest.mark.parametrize("dtype", [np.float64, np.int16, np.int32])
181
+ def test_device_grid_sampling_and_output_buffer(dtype):
182
+ actual = _pulse()
183
+ actual.start = -20e-9
184
+ actual.stop = 20e-9
185
+ actual.sample_rate = RATE
186
+ result = actual.sample(dtype=dtype)
187
+ target = np.empty_like(result)
188
+ assert actual.sample(dtype=dtype, out=target) is target
189
+ assert np.array_equal(target, result)
190
+
191
+
192
+ def test_complex_block_roundtrip_and_pickle():
193
+ pulse = _pulse()
194
+ complex_wave = pulse + 0.5j * (pulse >> 2e-9)
195
+ complex_wave.start = -20e-9
196
+ complex_wave.stop = 20e-9
197
+ complex_wave.sample_rate = RATE
198
+ complex_wave.label = "iq"
199
+ x = np.linspace(-20e-9, 20e-9, 8192, endpoint=False)
200
+ expected = pulse(x) + 0.5j * (pulse >> 2e-9)(x)
201
+
202
+ data = complex_wave.to_bytes()
203
+ restored = wf.Waveform.from_bytes(data)
204
+ assert data[:4] == b"CWF1"
205
+ assert isinstance(restored, wf.ComplexWaveform)
206
+ assert np.allclose(restored(x), expected, rtol=2e-15, atol=2e-15)
207
+ unpickled = pickle.loads(pickle.dumps(complex_wave, protocol=5))
208
+ assert np.allclose(unpickled(x), expected, rtol=2e-15, atol=2e-15)
209
+ assert unpickled.label == complex_wave.label
210
+
211
+
212
+ def test_stack_sampling_roundtrip_shift_offset_and_pickle():
213
+ actual = _stack(100)
214
+ data = actual.to_bytes()
215
+ restored = wf.WaveVStack.from_bytes(data)
216
+ restored.start = actual.start
217
+ restored.stop = actual.stop
218
+ restored.sample_rate = actual.sample_rate
219
+
220
+ assert data[:4] == b"WNS4"
221
+ assert restored.to_bytes() is data
222
+ assert isinstance(wf.RealWaveVStack.from_bytes(data),
223
+ wf.RealWaveVStack)
224
+ assert np.array_equal(restored.sample(), actual.sample())
225
+ assert np.array_equal(restored.sample(dtype=np.int16),
226
+ actual.sample(dtype=np.int16))
227
+
228
+ transformed = (actual >> 5e-9) + 0.125
229
+ transformed_restored = wf.WaveVStack.from_bytes(transformed.to_bytes())
230
+ x = np.linspace(-20e-9, 4e-6, 10_000, endpoint=False)
231
+ assert np.allclose(transformed_restored(x), transformed(x),
232
+ rtol=2e-15, atol=2e-15)
233
+
234
+ actual.label = "events"
235
+ unpickled = pickle.loads(pickle.dumps(actual, protocol=5))
236
+ assert unpickled == actual
237
+ assert unpickled.sample_rate == actual.sample_rate
238
+ assert unpickled.label == actual.label
239
+
240
+
241
+ @pytest.mark.parametrize("dtype,bits", [
242
+ (np.float64, 0),
243
+ (np.int16, 16),
244
+ (np.int32, 32),
245
+ ])
246
+ def test_stack_sample_plan_matches_direct_sampler(dtype, bits):
247
+ pulse = wf.gaussian(20e-9) * wf.cos(2 * np.pi * 100e6)
248
+ scales = (0.25, 0.5, 1.0)
249
+ actual = wf.WaveVStack([
250
+ scales[index % len(scales)] * pulse >> (index * 40e-9)
251
+ for index in range(75)
252
+ ])
253
+ actual.start = -20e-9
254
+ actual.stop = 75 * 40e-9
255
+ actual.sample_rate = RATE
256
+ actual.offset = 0.071
257
+
258
+ first = actual.sample(dtype=dtype)
259
+ cache_key, sample_plan = actual._sample_plan_cache
260
+ target = np.empty_like(first)
261
+ result = actual.sample(dtype=dtype, out=target)
262
+ direct = actual._core.sample(
263
+ cache_key[0], cache_key[1], cache_key[2], cache_key[3],
264
+ cache_key[4], actual.offset, bits, 1.0,
265
+ )
266
+
267
+ assert result is target
268
+ assert sample_plan.count == len(result)
269
+ assert sample_plan.group_count == 1
270
+ assert sample_plan.non_overlapping
271
+ assert np.array_equal(result, direct)
272
+ assert actual._sample_plan_cache[1] is sample_plan
273
+
274
+
275
+ def test_overlapping_stack_quantization_and_empty_stack():
276
+ actual = _stack(50, spacing=10e-9)
277
+ for dtype, bits in ((np.int16, 16), (np.int32, 32)):
278
+ expected = quantize_samples(actual.sample(), bits)
279
+ assert np.array_equal(actual.sample(dtype=dtype), expected)
280
+
281
+ empty = wf.WaveVStack([])
282
+ empty.start = -1e-9
283
+ empty.stop = 1e-9
284
+ empty.sample_rate = RATE
285
+ assert empty.to_bytes() == b"WNS4\x02\x00\x00\x00" + b"\x00" * 8
286
+ assert not np.any(empty.sample())
287
+ assert not np.any(wf.WaveVStack.from_bytes(empty.to_bytes())(
288
+ np.linspace(-1e-9, 1e-9, 17)
289
+ ))
290
+
291
+
292
+ def test_blocks_reject_malformed_layouts():
293
+ wave_data = bytearray(_pulse().to_bytes())
294
+ with pytest.raises(ValueError):
295
+ wf.Waveform.from_bytes(wave_data[:20])
296
+ wave_data[0] = 0
297
+ with pytest.raises(ValueError):
298
+ wf.Waveform.from_bytes(wave_data)
299
+
300
+ wave_data = bytearray(_pulse().to_bytes())
301
+ wave_data[24] = 255
302
+ with pytest.raises(ValueError):
303
+ wf.Waveform.from_bytes(wave_data)
304
+
305
+ stack_data = bytearray(_stack(2).to_bytes())
306
+ with pytest.raises(ValueError):
307
+ wf.WaveVStack.from_bytes(stack_data[:-1])
308
+ event_offset = len(stack_data) - 40
309
+ stack_data[event_offset:event_offset + 4] = (99).to_bytes(4, "little")
310
+ malformed = wf.WaveVStack.from_bytes(stack_data)
311
+ with pytest.raises(ValueError):
312
+ _ = malformed._core.event_count
313
+
314
+
315
+ def test_format_is_language_neutral_and_uses_the_global_clock():
316
+ data = _pulse().to_bytes()
317
+ assert data[:4] == b"WNF4"
318
+ assert int.from_bytes(data[4:6], "little") == 2
319
+ assert wf.get_time_resolution() == 1 / 120_000_000_000
320
+ package_dir = Path(wf.__file__).resolve().parent
321
+ assert (package_dir / "_cwaveform.c").is_file()
322
+ assert (package_dir / "_cwaveform.h").is_file()
@@ -15,7 +15,8 @@ from waveforms._waveform import (
15
15
 
16
16
 
17
17
  PUBLIC_NAMES = {
18
- "ComplexWaveform", "ComplexWaveVStack", "D", "Waveform", "WaveVStack",
18
+ "ComplexWaveform", "ComplexWaveVStack", "D", "RealWaveform",
19
+ "RealWaveVStack", "Waveform", "WaveVStack",
19
20
  "chirp", "const", "cos", "cosh",
20
21
  "coshPulse", "cosPulse", "cut", "drag", "drag_sin", "drag_sinx",
21
22
  "exp", "function",
@@ -51,19 +52,13 @@ def test_affine_algebra_matches_eager_materialization():
51
52
  right = -1.25 * (wf.cos(2.3, 0.4) << 0.35)
52
53
 
53
54
  actual_add = left + right
54
- eager_add = wf.Waveform._from_core(
55
- left._materialized_core().add(right._materialized_core())
56
- )
57
55
  actual_mul = left * right
58
- eager_mul = wf.Waveform._from_core(
59
- left._materialized_core().mul(right._materialized_core())
60
- )
61
56
 
62
57
  x = np.linspace(-3, 4, 8193)
63
- assert actual_add.to_bytes() == eager_add.to_bytes()
64
- assert actual_mul.to_bytes() == eager_mul.to_bytes()
65
- assert np.array_equal(actual_add(x), eager_add(x))
66
- assert np.array_equal(actual_mul(x), eager_mul(x))
58
+ assert np.allclose(actual_add(x), left(x) + right(x),
59
+ rtol=2e-15, atol=2e-15)
60
+ assert np.allclose(actual_mul(x), left(x) * right(x),
61
+ rtol=2e-15, atol=2e-15)
67
62
  assert wf.Waveform.from_bytes(actual_add.to_bytes()) == actual_add
68
63
  assert wf.Waveform.from_bytes(actual_mul.to_bytes()) == actual_mul
69
64
 
@@ -272,7 +267,7 @@ def test_wavevstack_template_sharing_operations_and_roundtrip():
272
267
  waves = [template >> (i * 80e-9) for i in range(1000)]
273
268
  stack = wf.WaveVStack(waves)
274
269
 
275
- # One template plus three packed event arrays; substantially smaller than
270
+ # One template plus three contiguous event arrays; substantially smaller than
276
271
  # serializing 1000 complete shifted expression trees.
277
272
  assert len(stack.to_bytes()) < 32_000
278
273
  x = np.linspace(0, 80e-6, 16_000, endpoint=False)
@@ -410,30 +405,26 @@ def test_complex_interpolation_and_stack_roundtrip():
410
405
  wf.WaveVStack(waves)
411
406
 
412
407
 
413
- def test_packed_backend_rejects_complex_coefficients_and_scales():
408
+ def test_real_backend_promotes_complex_coefficients_and_scales():
414
409
  wav = wf.gaussian(1.0)
415
410
  stack = wf.WaveVStack([wav, 2 * (wav >> 3)])
416
- assert wav.to_bytes()[:4] == b"WFM3"
417
- assert stack.to_bytes()[:4] == b"WVS3"
418
- assert all(isinstance(scale, float)
419
- for _, _, scale in stack._stack.events())
411
+ assert wav.to_bytes()[:4] == b"WNF4"
412
+ assert stack.to_bytes()[:4] == b"WNS4"
420
413
  assert stack(np.linspace(-1, 4, 101)).dtype == np.float64
421
414
 
422
- with pytest.raises(TypeError, match="must be real"):
423
- wav._core.scaled(1j)
424
- with pytest.raises(TypeError, match="must be real"):
425
- stack._stack.scaled(1j)
426
- with pytest.raises(TypeError, match="positions must be real"):
415
+ assert isinstance(wav * 1j, wf.ComplexWaveform)
416
+ assert isinstance(stack * 1j, wf.ComplexWaveVStack)
417
+ with pytest.raises((TypeError, ValueError)):
427
418
  wav(np.array([0.0 + 0.0j]))
428
419
 
429
420
 
430
421
  def test_time_is_global_configuration_and_blocks_store_integer_ticks():
431
422
  assert wf.get_time_resolution() == 1 / 120_000_000_000
432
423
  wav = wf.square(4e-9) >> 11e-9
433
- ticks = wav._core.get_bound_ticks()
434
- assert ticks.dtype == np.dtype("<i8")
435
- assert np.array_equal(ticks[:-1], [-240, 240])
436
- assert wav._delay == 11e-9
424
+ data = wav.to_bytes()
425
+ assert data[:4] == b"WNF4"
426
+ assert wav.begin == 9e-9
427
+ assert wav.end == 13e-9
437
428
  with pytest.raises(RuntimeError):
438
429
  wf.set_time_resolution(1e-15)
439
430
 
@@ -441,7 +432,7 @@ def test_time_is_global_configuration_and_blocks_store_integer_ticks():
441
432
  "import waveforms as w; "
442
433
  "w.set_time_resolution(1e-15); "
443
434
  "x=w.square(4e-12); "
444
- "print(w.get_time_resolution(), x._core.get_bound_ticks()[0])"
435
+ "print(w.get_time_resolution(), round(x.begin / 1e-15))"
445
436
  )
446
437
  result = subprocess.run([sys.executable, "-c", code], check=True,
447
438
  capture_output=True, text=True)
@@ -482,6 +473,15 @@ def test_fixed_width_quantization_supported_sampling_and_fallback():
482
473
  1073741824, 2147483647, 2147483647],
483
474
  )
484
475
 
476
+ # Exercise the fused SIMD path, including its historic half-away-from-zero
477
+ # rounding rule and multidimensional ``out`` handling.
478
+ half_steps = np.array([0.5, -0.5, 1.5, -1.5] * 8) / 32768.0
479
+ target = np.empty((8, 4), dtype=np.int16)
480
+ assert quantize_samples(half_steps.reshape(8, 4), 16, out=target) is target
481
+ assert np.array_equal(
482
+ target.reshape(-1), np.array([1, -1, 2, -2] * 8, dtype=np.int16)
483
+ )
484
+
485
485
  wav = 0.8 * wf.gaussian(20e-9)
486
486
  wav.start = -20e-9
487
487
  wav.stop = 20e-9
@@ -502,6 +502,21 @@ def test_fixed_width_quantization_supported_sampling_and_fallback():
502
502
  assert np.array_equal(wav.sample(odd_rate), wav(legacy_grid))
503
503
 
504
504
 
505
+ def test_simd_node_evaluation_matches_scalar_boundaries():
506
+ positions = np.linspace(-2.0, 2.0, 513)
507
+ waves = (
508
+ wf.t(),
509
+ wf.exp(0.2),
510
+ wf.sinc(1.3),
511
+ wf.cosh(0.3),
512
+ wf.sinh(0.3),
513
+ (wf.gaussian(4.0, 1.5) * wf.cos(2.2)) ** 2,
514
+ )
515
+ for wave in waves:
516
+ expected = np.array([wave(float(position)) for position in positions])
517
+ assert np.allclose(wave(positions), expected, rtol=1e-12, atol=1e-13)
518
+
519
+
505
520
  def test_integer_sampling_fast_paths_are_bit_exact_and_pickle_safe():
506
521
  rate = 2_400_000_000
507
522
  pulse = 0.8 * wf.gaussian(20e-9) * wf.cos(2 * np.pi * 100e6)
@@ -576,14 +591,8 @@ def test_wavevstack_integer_grid_template_sampling_and_complex_iq():
576
591
  stack.start = -10e-9
577
592
  stack.stop = 110e-9
578
593
  actual = stack.sample(rate)
579
- start_tick = time_to_tick(stack.start)
580
- expected = np.full(len(actual), stack.offset)
581
- for core, delay, scale in stack._stack.events():
582
- delay_tick = time_to_tick(delay + stack.shift)
583
- local_grid = sample_grid(
584
- start_tick - delay_tick, len(actual), *step
585
- )
586
- expected += scale * core.evaluate(local_grid)
594
+ x = sample_grid(time_to_tick(stack.start), len(actual), *step)
595
+ expected = stack(x)
587
596
  assert np.allclose(actual, expected, rtol=2e-15, atol=2e-15)
588
597
 
589
598
  complex_stack = wf.ComplexWaveVStack(stack, -0.5 * stack)
@@ -595,12 +604,16 @@ def test_wavevstack_integer_grid_template_sampling_and_complex_iq():
595
604
  assert np.array_equal(q_data, quantize_samples(complex_samples.imag, 16))
596
605
 
597
606
 
598
- def test_single_packed_backend_has_no_waveform2_modules():
607
+ def test_single_c_backend_has_no_waveform2_or_packed_core():
599
608
  package = Path(wf.__file__).parent
600
609
  python_source = (package / "waveform.py").read_text()
601
610
  cython_source = (package / "_waveform.pyx").read_text()
602
611
  assert "waveform2" not in python_source
603
612
  assert "_waveform2" not in cython_source
613
+ assert "PackedWaveform" not in python_source + cython_source
614
+ assert "PackedStack" not in python_source + cython_source
615
+ assert "WFM3" not in python_source + cython_source
616
+ assert "WVS3" not in python_source + cython_source
604
617
  assert not (package / "waveform2.py").exists()
605
618
  assert not (package / "_waveform2.pyx").exists()
606
619
 
@@ -5,11 +5,10 @@ from .waveform import (
5
5
  ComplexWaveform,
6
6
  ComplexWaveVStack,
7
7
  D,
8
+ RealWaveform,
9
+ RealWaveVStack,
8
10
  Waveform,
9
11
  WaveVStack,
10
- NativeWaveform,
11
- NativeWaveVStack,
12
- NativeComplexWaveform,
13
12
  chirp,
14
13
  const,
15
14
  cos,
@@ -44,19 +43,11 @@ from .waveform import (
44
43
  t,
45
44
  wave_eval,
46
45
  zero,
47
- native_zero,
48
- native_one,
49
- native_const,
50
- native_gaussian,
51
- native_cos,
52
- native_sin,
53
- native_square,
54
- native_format_description,
55
46
  )
56
47
 
57
48
  __all__ = [
58
- "ComplexWaveform", "ComplexWaveVStack", "D", "Waveform", "WaveVStack",
59
- "NativeWaveform", "NativeWaveVStack", "NativeComplexWaveform",
49
+ "ComplexWaveform", "ComplexWaveVStack", "D", "RealWaveform",
50
+ "RealWaveVStack", "Waveform", "WaveVStack",
60
51
  "chirp", "const", "cos", "cosh",
61
52
  "coshPulse", "cosPulse", "cut", "drag", "drag_sin", "drag_sinx",
62
53
  "e", "exp", "function",
@@ -65,6 +56,4 @@ __all__ = [
65
56
  "registerBaseFunc", "registerDerivative", "samplingPoints",
66
57
  "set_time_resolution", "sign", "sin", "sinc", "sinh", "square",
67
58
  "step", "t", "wave_eval", "zero", "__version__",
68
- "native_zero", "native_one", "native_const", "native_gaussian",
69
- "native_cos", "native_sin", "native_square", "native_format_description",
70
59
  ]