waveforms 3.5.1__tar.gz → 3.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 (38) hide show
  1. {waveforms-3.5.1/waveforms.egg-info → waveforms-3.6.1}/PKG-INFO +12 -5
  2. {waveforms-3.5.1 → waveforms-3.6.1}/README.md +11 -4
  3. waveforms-3.6.1/tests/test_common_pipeline.py +242 -0
  4. waveforms-3.6.1/tests/test_simd_sampling.py +136 -0
  5. {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/__init__.py +2 -0
  6. {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/_cwaveform.c +374 -175
  7. {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/_cwaveform.h +2 -0
  8. {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/_cwaveform.md +12 -0
  9. {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/distortion.py +26 -1
  10. {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/version.py +1 -1
  11. {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/waveform.py +119 -75
  12. {waveforms-3.5.1 → waveforms-3.6.1/waveforms.egg-info}/PKG-INFO +12 -5
  13. {waveforms-3.5.1 → waveforms-3.6.1}/waveforms.egg-info/SOURCES.txt +2 -0
  14. {waveforms-3.5.1 → waveforms-3.6.1}/LICENSE +0 -0
  15. {waveforms-3.5.1 → waveforms-3.6.1}/MANIFEST.in +0 -0
  16. {waveforms-3.5.1 → waveforms-3.6.1}/pyproject.toml +0 -0
  17. {waveforms-3.5.1 → waveforms-3.6.1}/setup.cfg +0 -0
  18. {waveforms-3.5.1 → waveforms-3.6.1}/setup.py +0 -0
  19. {waveforms-3.5.1 → waveforms-3.6.1}/tests/test_core.py +0 -0
  20. {waveforms-3.5.1 → waveforms-3.6.1}/tests/test_native_sos.py +0 -0
  21. {waveforms-3.5.1 → waveforms-3.6.1}/tests/test_nonlinear.py +0 -0
  22. {waveforms-3.5.1 → waveforms-3.6.1}/tests/test_output_limits.py +0 -0
  23. {waveforms-3.5.1 → waveforms-3.6.1}/tests/test_waveform.py +0 -0
  24. {waveforms-3.5.1 → waveforms-3.6.1}/tests/test_wavevstack.py +0 -0
  25. {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/Waveform.g4 +0 -0
  26. {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/WaveformLexer.py +0 -0
  27. {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/WaveformListener.py +0 -0
  28. {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/WaveformParser.py +0 -0
  29. {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/__main__.py +0 -0
  30. {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/_waveform.pyi +0 -0
  31. {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/_waveform.pyx +0 -0
  32. {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/nonlinear.py +0 -0
  33. {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/utils.py +0 -0
  34. {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/waveform_parser.py +0 -0
  35. {waveforms-3.5.1 → waveforms-3.6.1}/waveforms.egg-info/dependency_links.txt +0 -0
  36. {waveforms-3.5.1 → waveforms-3.6.1}/waveforms.egg-info/entry_points.txt +0 -0
  37. {waveforms-3.5.1 → waveforms-3.6.1}/waveforms.egg-info/requires.txt +0 -0
  38. {waveforms-3.5.1 → waveforms-3.6.1}/waveforms.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: waveforms
3
- Version: 3.5.1
3
+ Version: 3.6.1
4
4
  Summary: Edit waveforms used in experiment
5
5
  Author-email: feihoo87 <feihoo87@gmail.com>
6
6
  Maintainer-email: feihoo87 <feihoo87@gmail.com>
@@ -213,10 +213,17 @@ coefficients and extended precision retain the SciPy implementation.
213
213
  `sample_iq()` still converts the filtered complex result into separate I/Q
214
214
  output arrays. See [the SOS benchmark](benchmarks/sos_filter.md).
215
215
 
216
- Calling `waveform(t)` directly evaluates with amplitude limits but without
217
- nonlinear calibration or filtering. Sampling evaluates the underlying signal
218
- without limits before applying its processing chain. Limits are sampling
219
- metadata preserved by pickle, not part of the raw WNF4/WNS4 binary blocks.
216
+ Calling `waveform(t)` applies waveform/event accumulation, the object's
217
+ nonlinear mapping, and then its own amplitude limits. It does not apply SOS
218
+ filters or DAC quantization. Stacks and complex wrappers use raw child values;
219
+ child maps and limits do not affect the parent's processing chain. Thus direct
220
+ evaluation and unfiltered floating-point sampling have the same processing
221
+ semantics on the same time grid, subject to floating-point rounding. With
222
+ `out=` and `accumulate=True`, the processed contribution is added to the
223
+ existing buffer without mapping or limiting its previous contents. Sampling
224
+ starts from raw values so mapping and limits are each applied only once, with
225
+ SOS filtering between them when configured. Maps and limits are metadata
226
+ preserved by pickle, not part of the raw WNF4/WNS4 binary blocks.
220
227
 
221
228
  ## Reporting Issues
222
229
  Please report all issues [on github](https://github.com/feihoo87/waveforms/issues).
@@ -169,10 +169,17 @@ coefficients and extended precision retain the SciPy implementation.
169
169
  `sample_iq()` still converts the filtered complex result into separate I/Q
170
170
  output arrays. See [the SOS benchmark](benchmarks/sos_filter.md).
171
171
 
172
- Calling `waveform(t)` directly evaluates with amplitude limits but without
173
- nonlinear calibration or filtering. Sampling evaluates the underlying signal
174
- without limits before applying its processing chain. Limits are sampling
175
- metadata preserved by pickle, not part of the raw WNF4/WNS4 binary blocks.
172
+ Calling `waveform(t)` applies waveform/event accumulation, the object's
173
+ nonlinear mapping, and then its own amplitude limits. It does not apply SOS
174
+ filters or DAC quantization. Stacks and complex wrappers use raw child values;
175
+ child maps and limits do not affect the parent's processing chain. Thus direct
176
+ evaluation and unfiltered floating-point sampling have the same processing
177
+ semantics on the same time grid, subject to floating-point rounding. With
178
+ `out=` and `accumulate=True`, the processed contribution is added to the
179
+ existing buffer without mapping or limiting its previous contents. Sampling
180
+ starts from raw values so mapping and limits are each applied only once, with
181
+ SOS filtering between them when configured. Maps and limits are metadata
182
+ preserved by pickle, not part of the raw WNF4/WNS4 binary blocks.
176
183
 
177
184
  ## Reporting Issues
178
185
  Please report all issues [on github](https://github.com/feihoo87/waveforms/issues).
@@ -0,0 +1,242 @@
1
+ """Regression coverage for direct native loading and output placement."""
2
+
3
+ import ctypes
4
+ from concurrent.futures import ThreadPoolExecutor
5
+ import importlib
6
+ import pickle
7
+ import struct
8
+
9
+ import numpy as np
10
+ import pytest
11
+
12
+ import waveforms as wf
13
+ import waveforms._waveform as core
14
+ from waveforms._waveform import quantize_samples
15
+
16
+
17
+ @pytest.fixture(scope='module')
18
+ def native():
19
+ library = ctypes.CDLL(core.__file__)
20
+ library.cwaveform_stack_from_bytes.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
21
+ library.cwaveform_stack_from_bytes.restype = ctypes.c_void_p
22
+ library.cwaveform_stack_release.argtypes = [ctypes.c_void_p]
23
+ library.cwaveform_stack_release.restype = None
24
+ library.cwaveform_stack_bytes.argtypes = [ctypes.c_void_p,
25
+ ctypes.POINTER(ctypes.c_size_t)]
26
+ library.cwaveform_stack_bytes.restype = ctypes.c_void_p
27
+ library.cwaveform_stack_hash.argtypes = [ctypes.c_void_p]
28
+ library.cwaveform_stack_hash.restype = ctypes.c_uint64
29
+ return library
30
+
31
+
32
+ def _stack(delays, *, scales=None):
33
+ delays = np.asarray(delays, dtype=np.int64)
34
+ if scales is None:
35
+ scales = np.resize([.5, -.75, 1.0], len(delays))
36
+ stack = wf.WaveVStack.from_events(
37
+ [.8 * wf.square(8 / 120e9), -.3 * wf.square(4 / 120e9)],
38
+ np.arange(len(delays), dtype=np.uint32) % 2, delays, scales)
39
+ stack.start, stack.stop, stack.sample_rate = 0, 120 / 120e9, 120_000_000_000
40
+ stack.offset = .07
41
+ return stack
42
+
43
+
44
+ def test_native_decode_owns_exact_bytes_and_preserves_hash(native):
45
+ original = _stack([-9, 0, 19, 33, 61, 118, 130])
46
+ data = original._core.to_bytes()
47
+ source = ctypes.create_string_buffer(data)
48
+ handle = native.cwaveform_stack_from_bytes(source, len(data))
49
+ assert handle
50
+ try:
51
+ # Mutating the transport buffer must not change the native handle.
52
+ ctypes.memset(source, 0, len(data))
53
+ size = ctypes.c_size_t()
54
+ pointer = native.cwaveform_stack_bytes(handle, ctypes.byref(size))
55
+ assert size.value == len(data)
56
+ assert ctypes.string_at(pointer, size.value) == data
57
+ assert native.cwaveform_stack_hash(handle) == original._core.hash64
58
+ finally:
59
+ native.cwaveform_stack_release(handle)
60
+
61
+
62
+ def test_lazy_native_hash_matches_format_and_is_safe_for_parallel_readers(native):
63
+ stack = _stack(np.arange(10000) * 13)
64
+ data = stack._core.to_bytes()
65
+ expected = 1469598103934665603
66
+ for byte in data:
67
+ expected = ((expected ^ byte) * 1099511628211) & ((1 << 64) - 1)
68
+ source = ctypes.create_string_buffer(data)
69
+ handle = native.cwaveform_stack_from_bytes(source, len(data))
70
+ assert handle
71
+ try:
72
+ with ThreadPoolExecutor(max_workers=8) as workers:
73
+ hashes = list(workers.map(
74
+ lambda _: native.cwaveform_stack_hash(handle), range(64)))
75
+ assert hashes == [expected] * len(hashes)
76
+ assert stack._core.hash64 == expected
77
+ finally:
78
+ native.cwaveform_stack_release(handle)
79
+
80
+
81
+ def test_native_decode_rejects_invalid_blocks_and_releases_partial_loads(native):
82
+ data = _stack([0, 16, 32])._core.to_bytes()
83
+ malformed = [data[:index] for index in range(len(data))]
84
+ event_offset = len(data) - 3 * 20
85
+ for offset, fmt, value in (
86
+ (0, '4s', b'BAD!'), (4, 'H', 65535), (6, 'H', 1),
87
+ (8, 'I', 2**32 - 1), (12, 'I', 2**32 - 1),
88
+ (16, 'I', 2**32 - 1),
89
+ (24 + 24, 'B', 255), # First template's first node.
90
+ (event_offset, 'I', 2),
91
+ (event_offset + 12 * 3, 'd', np.nan),
92
+ (event_offset + 12 * 3 + 8, 'd', np.inf),
93
+ (event_offset + 12 * 3 + 16, 'd', -np.inf),
94
+ ):
95
+ candidate = bytearray(data)
96
+ struct.pack_into('<' + fmt, candidate, offset, value)
97
+ malformed.append(bytes(candidate))
98
+ for candidate in malformed:
99
+ source = ctypes.create_string_buffer(candidate)
100
+ handle = native.cwaveform_stack_from_bytes(source, len(candidate))
101
+ try:
102
+ assert not handle
103
+ finally:
104
+ if handle:
105
+ native.cwaveform_stack_release(handle)
106
+
107
+
108
+ @pytest.mark.parametrize('delays', [[], [-30, -20], [140, 150], [0, 119],
109
+ [4, 10, 16, 22], [8, 30, 70],
110
+ [8, 30, 31], [80, 30, 8]])
111
+ @pytest.mark.parametrize('bits', [0, 16, 32])
112
+ @pytest.mark.parametrize('bounds', [(-np.inf, np.inf), (.1, .2)])
113
+ def test_gap_initialization_edges_overlap_and_out_of_order(delays, bits, bounds):
114
+ stack = _stack(delays)
115
+ stack.min, stack.max = bounds
116
+ restored = pickle.loads(pickle.dumps(stack)) >> (1 / 120e9)
117
+ expected = restored._core.sample(0, 120, 1, 1, 1, stack.offset)
118
+ np.clip(expected, *bounds, out=expected)
119
+ if bits:
120
+ expected = quantize_samples(expected, bits, .7)
121
+ output = np.full_like(expected, 17)
122
+ assert restored.sample(out=output, full_scale=.7) is output
123
+ if bits:
124
+ np.testing.assert_array_equal(output, expected)
125
+ else:
126
+ # The plan groups by template; overlapping additions can differ from
127
+ # the event-order evaluator by a rounding bit even before this change.
128
+ np.testing.assert_allclose(output, expected, rtol=1e-15, atol=1e-15)
129
+ first = output.copy()
130
+ output.fill(23)
131
+ restored.sample(out=output, full_scale=.7)
132
+ np.testing.assert_array_equal(output, first)
133
+ reference = stack >> (1 / 120e9)
134
+ assert restored.begin == reference.begin
135
+ assert restored.end == reference.end
136
+ assert restored._core.lower_tick == stack._core.lower_tick
137
+ assert restored._core.upper_tick == stack._core.upper_tick
138
+
139
+
140
+ def test_gap_initialization_skips_zero_scales_and_empty_templates():
141
+ stack = _stack([8, 30, 70], scales=[0, 1, 0])
142
+ expected = stack._core.sample(0, 120, 1, 1, 0, stack.offset)
143
+ np.testing.assert_array_equal(stack.sample(), expected)
144
+ empty = wf.WaveVStack.from_events([wf.zero()], [0], [0], [1.0])
145
+ empty.start, empty.stop, empty.sample_rate = 0, 1e-9, 10_000_000_000
146
+ np.testing.assert_array_equal(empty.sample(), np.zeros(10))
147
+
148
+
149
+ @pytest.mark.parametrize('bits', [0, 16, 32])
150
+ def test_many_scales_and_sparse_output(bits):
151
+ stack = _stack(np.arange(200) * 13, scales=np.linspace(-.8, .8, 200))
152
+ stack.stop = 2700 / 120e9
153
+ stack.min, stack.max = -.2, .25
154
+ expected = stack._core.sample(0, 2700, 1, 1, 0, stack.offset)
155
+ np.clip(expected, -.2, .25, out=expected)
156
+ if bits:
157
+ expected = quantize_samples(expected, bits, .7)
158
+ output = np.full_like(expected, 17)
159
+ stack.sample(out=output, full_scale=.7)
160
+ np.testing.assert_array_equal(output, expected)
161
+
162
+
163
+ @pytest.mark.parametrize('templates_count', [1, 8, 9, 16, 33, 129])
164
+ def test_plan_lookup_growth_repeated_keys_and_multiple_tick_phases(templates_count):
165
+ rng = np.random.default_rng(417)
166
+ templates = [wf.square((100 + index) / 120e9)
167
+ for index in range(templates_count)]
168
+ count = 2 * templates_count + 100
169
+ ids = np.arange(count, dtype=np.uint32) % templates_count
170
+ delays = np.arange(count, dtype=np.int64) * 500 + rng.integers(0, 50, count)
171
+ scales = np.resize([.125, -.25, .5], count)
172
+ stack = wf.WaveVStack.from_events(templates, ids, delays, scales)
173
+ stack.start, stack.stop, stack.sample_rate = 0, count * 500 / 120e9, 2_400_000_000
174
+ restored = pickle.loads(pickle.dumps(stack)) >> (7 / 120e9)
175
+ samples = count * 10
176
+ expected = restored._core.sample(0, samples, 50, 1, 7, 0)
177
+ np.testing.assert_array_equal(restored.sample(), expected)
178
+ plan = restored._sample_plan_cache[1]
179
+ assert plan.group_count == len(set(zip(ids, (-(delays + 7)) % 50)))
180
+ np.testing.assert_array_equal(restored.sample(), expected)
181
+ # Rebuilding with a different calibration changes phases, not the format.
182
+ restored.shift = 11 / 120e9
183
+ expected = restored._core.sample(0, samples, 50, 1, 11, 0)
184
+ np.testing.assert_array_equal(restored.sample(), expected)
185
+
186
+
187
+ @pytest.mark.parametrize('stack', [False, True])
188
+ @pytest.mark.parametrize('rate', [2_400_000_000, 1_234_567_891])
189
+ def test_float_output_is_written_directly(stack, rate, monkeypatch):
190
+ module = importlib.import_module('waveforms.waveform')
191
+ wave = wf.cos(2 * np.pi * 30e6)
192
+ if stack:
193
+ wave = wf.WaveVStack([wave * wf.square(2e-6)])
194
+ wave.start, wave.stop, wave.sample_rate = 0, 1e-6, rate
195
+ expected = wave.sample()
196
+ output = np.empty_like(expected)
197
+ finish = module._finish_samples
198
+
199
+ def check_direct(values, dtype, full_scale, out, *args):
200
+ assert values is output and out is output
201
+ return finish(values, dtype, full_scale, out, *args)
202
+
203
+ monkeypatch.setattr(module, '_finish_samples', check_direct)
204
+ assert wave.sample(out=output) is output
205
+ np.testing.assert_array_equal(output, expected)
206
+
207
+
208
+ @pytest.mark.parametrize('stack', [False, True])
209
+ @pytest.mark.parametrize('dtype', [None, np.float32, np.float64, np.complex128])
210
+ @pytest.mark.parametrize('layout', ['contiguous', 'strided', 'unaligned', 'big_endian'])
211
+ def test_output_casting_and_layout_fallbacks(stack, dtype, layout):
212
+ wave = .8 * wf.cos(2 * np.pi * 30e6)
213
+ if stack:
214
+ wave = wf.WaveVStack([wave * wf.square(2e-6)])
215
+ wave.start, wave.stop, wave.sample_rate = 0, 1e-6, 2_400_000_000
216
+ expected = wave.sample(dtype=dtype)
217
+ output_dtype = np.complex128 if dtype == np.complex128 else np.float64
218
+ if layout == 'strided':
219
+ output = np.empty(2 * len(expected), dtype=output_dtype)[::2]
220
+ elif layout == 'unaligned':
221
+ output = np.ndarray(expected.shape, dtype=output_dtype,
222
+ buffer=bytearray(expected.size * np.dtype(output_dtype).itemsize + 1),
223
+ offset=1)
224
+ else:
225
+ if layout == 'big_endian':
226
+ output_dtype = np.dtype(output_dtype).newbyteorder('>')
227
+ output = np.empty(expected.shape, dtype=output_dtype)
228
+ assert wave.sample(dtype=dtype, out=output) is output
229
+ np.testing.assert_array_equal(output, expected)
230
+
231
+
232
+ @pytest.mark.parametrize('stack', [False, True])
233
+ def test_float_output_validation(stack):
234
+ wave = _stack([10, 30]) if stack else wf.cos(1)
235
+ wave.start, wave.stop, wave.sample_rate = 0, 1e-6, 1_000_000_000
236
+ for output in (np.empty(999), np.empty((1, 1000))):
237
+ with pytest.raises(ValueError, match='shape'):
238
+ wave.sample(out=output)
239
+ output = np.empty(1000)
240
+ output.flags.writeable = False
241
+ with pytest.raises(ValueError, match='writ'):
242
+ wave.sample(out=output)
@@ -0,0 +1,136 @@
1
+ """Numerical boundaries for native SIMD quantization and table loads."""
2
+
3
+ import pickle
4
+
5
+ import numpy as np
6
+ import pytest
7
+
8
+ import waveforms as wf
9
+ from waveforms._waveform import quantize_samples
10
+
11
+
12
+ def reference_quantize(values, bits, full_scale):
13
+ limit = float(1 << (bits - 1))
14
+ scaled = np.clip(values * (limit / full_scale), -limit, limit - 1)
15
+ integral = np.trunc(scaled)
16
+ rounded = integral + np.copysign(
17
+ (np.abs(scaled - integral) >= .5).astype(float), scaled)
18
+ return rounded.astype(np.int16 if bits == 16 else np.int32)
19
+
20
+
21
+ @pytest.mark.parametrize('bits', [16, 32])
22
+ @pytest.mark.parametrize('full_scale', [1., .7, 2., 1e-100, 1e100])
23
+ def test_quantization_half_lsb_neighbors_tails_and_saturation(bits, full_scale):
24
+ limit = float(1 << (bits - 1))
25
+ halves = np.array([.5, 1.5, 2.5, 127.5, limit - 1.5])
26
+ steps = np.concatenate([np.nextafter(halves, 0), halves,
27
+ np.nextafter(halves, np.inf)])
28
+ values = np.concatenate([steps, -steps, [-2 * limit, 2 * limit, 0., -0.]])
29
+ values *= full_scale / limit
30
+ # Rotate the half-LSB neighbors through every SIMD lane and the tail.
31
+ for count in (0, 1, 3, 4, 7, 8, 9, 15, 16, 17, 31, 32, 33, 65, 257):
32
+ for shift in range(8):
33
+ source = np.resize(np.roll(values, shift), count)
34
+ expected = reference_quantize(source, bits, full_scale)
35
+ padded = np.full(count + 2, 123, dtype=expected.dtype)
36
+ output = padded[1:-1] # Not 16/32/64-byte aligned.
37
+ assert quantize_samples(source, bits, full_scale, out=output) is output
38
+ np.testing.assert_array_equal(output, expected)
39
+ assert padded[0] == padded[-1] == 123
40
+
41
+
42
+ @pytest.mark.parametrize('bits', [16, 32])
43
+ def test_nonfinite_quantization_in_each_vector_lane_and_tail(bits):
44
+ for count in (16, 33, 257):
45
+ for lane in range(count):
46
+ source = np.zeros(count)
47
+ source[lane] = [np.nan, np.inf, -np.inf][lane % 3]
48
+ with pytest.raises(ValueError):
49
+ quantize_samples(source, bits)
50
+
51
+
52
+ @pytest.mark.parametrize('storage', [np.float32, np.float64])
53
+ @pytest.mark.parametrize('method', ['linear', 'monotone_cubic'])
54
+ def test_table_loads_random_intervals_endpoints_and_aliasing(storage, method):
55
+ rng = np.random.default_rng(7428)
56
+ for table_size in (2, 3, 17, 4097):
57
+ mapping = wf.NonlinearMap.from_samples(
58
+ [-1., -.73, -.11, .29, 1.], [-.9, -.79, -.25, .42, .96],
59
+ method=method, dtype=storage, table_size=table_size,
60
+ extrapolate='clip')
61
+ for count in (7, 8, 9, 15, 16, 17, 31, 32, 33, 257):
62
+ source = rng.uniform(-1.2, 1.2, count)
63
+ source[::4] = -1.
64
+ source[1::4] = 1.
65
+ expected = np.array([mapping(float(value)) for value in source])
66
+ padded = np.full(count + 2, 123.)
67
+ output = padded[1:-1]
68
+ mapping(source, out=output)
69
+ np.testing.assert_allclose(output, expected, rtol=2e-15, atol=2e-15)
70
+ assert padded[0] == padded[-1] == 123.
71
+ mapping(source, out=source)
72
+ np.testing.assert_array_equal(source, output)
73
+
74
+
75
+ @pytest.mark.parametrize('bits', [16, 32])
76
+ @pytest.mark.parametrize('varied', [False, True])
77
+ @pytest.mark.parametrize('limited', [False, True])
78
+ @pytest.mark.parametrize('offset', [0., .07])
79
+ def test_stack_template_quantization_blocks_and_clipped_placements(
80
+ bits, varied, limited, offset):
81
+ events = 101
82
+ pulse = .9 * wf.gaussian(300e-9)
83
+ scales = (np.linspace(-.9, .9, events) if varied else
84
+ np.resize([.5, -.75, 1.], events))
85
+ stack = wf.WaveVStack.from_events(
86
+ [pulse], np.zeros(events, dtype=np.uint32),
87
+ np.arange(events, dtype=np.int64) * wf.time_to_tick(600e-9), scales)
88
+ stack.start, stack.stop = 0., (events - 1) * 600e-9
89
+ stack.sample_rate = 2_400_000_000
90
+ stack.offset = offset
91
+ if limited:
92
+ stack.min, stack.max = -.3, .35
93
+ restored = pickle.loads(pickle.dumps(stack)) >> (7 / 120e9)
94
+ expected = reference_quantize(restored.sample(), bits, .7)
95
+ np.testing.assert_array_equal(restored.sample(dtype=expected.dtype, full_scale=.7), expected)
96
+ output = np.empty_like(expected)
97
+ assert restored.sample(out=output, full_scale=.7) is output
98
+ np.testing.assert_array_equal(output, expected)
99
+
100
+
101
+ @pytest.mark.parametrize('block', [None, -200e6, 80e6])
102
+ @pytest.mark.parametrize('width', [12e-9, 20e-9, 1000e-9])
103
+ def test_drag_vector_evaluation_and_cold_template_plan(block, width):
104
+ templates = [.4 * wf.drag(5e9, width, delta=-200e6, block_freq=block,
105
+ phase=index * .371, t0=3e-9)
106
+ for index in range(9)]
107
+ pulse = templates[3] >> (7 / 120e9)
108
+ positions = np.linspace(-width, 2 * width, 1031)
109
+ scalar = np.array([pulse(float(position)) for position in positions])
110
+ np.testing.assert_allclose(pulse(positions), scalar, rtol=2e-14, atol=2e-14)
111
+ # Tick residues exercise different sampling phases and clipping at both
112
+ # ends; the old event evaluator provides an independent scalar reference.
113
+ delays = (np.arange(31, dtype=np.int64) * wf.time_to_tick(3 * width)
114
+ + np.arange(31) % 50)
115
+ stack = wf.WaveVStack.from_events(
116
+ templates, np.arange(31, dtype=np.uint32) % len(templates), delays,
117
+ np.resize([.5, -.75, 1.], 31))
118
+ stack.start, stack.stop = 0., 90 * width
119
+ stack.sample_rate = 2_400_000_000
120
+ restored = pickle.loads(pickle.dumps(stack)) >> (7 / 120e9)
121
+ block_before_sampling = restored.to_bytes()
122
+ count = len(restored.sample())
123
+ scalar = restored._core.sample(0, count, 50, 1, 7, 0.)
124
+ np.testing.assert_allclose(restored.sample(), scalar, rtol=2e-14, atol=2e-14)
125
+ for bits in (16, 32):
126
+ expected = reference_quantize(scalar, bits, 1.)
127
+ np.testing.assert_array_equal(restored.sample(dtype=expected.dtype), expected)
128
+ assert restored.to_bytes() == block_before_sampling
129
+
130
+
131
+ def test_drag_plan_falls_back_for_unsupported_companion_nodes():
132
+ pulse = wf.drag(5e9, 1e-6, delta=-200e6, block_freq=80e6) * wf.mollifier(1e-6)
133
+ stack = wf.WaveVStack.from_events([pulse], [0], [0], [.7])
134
+ stack.start, stack.stop, stack.sample_rate = 0., 1e-6, 2_400_000_000
135
+ expected = stack._core.sample(0, 2400, 50, 1, 0, 0.)
136
+ np.testing.assert_array_equal(stack.sample(), expected)
@@ -50,8 +50,10 @@ from .waveform import (
50
50
  wave_eval,
51
51
  zero,
52
52
  )
53
+ from .distortion import drag_fir, exp_decay_filter, exp_decay_filter_from_cascade, exp_decay_filter_to_cascade
53
54
 
54
55
  __all__ = [
56
+ "drag_fir", "exp_decay_filter", "exp_decay_filter_from_cascade", "exp_decay_filter_to_cascade",
55
57
  "ComplexWaveform", "ComplexWaveVStack", "D", "NonlinearMap", "RealWaveform",
56
58
  "RealWaveVStack", "Waveform", "WaveVStack",
57
59
  "chirp", "const", "cos", "cosh",