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.
- {waveforms-3.5.1/waveforms.egg-info → waveforms-3.6.1}/PKG-INFO +12 -5
- {waveforms-3.5.1 → waveforms-3.6.1}/README.md +11 -4
- waveforms-3.6.1/tests/test_common_pipeline.py +242 -0
- waveforms-3.6.1/tests/test_simd_sampling.py +136 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/__init__.py +2 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/_cwaveform.c +374 -175
- {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/_cwaveform.h +2 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/_cwaveform.md +12 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/distortion.py +26 -1
- {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/version.py +1 -1
- {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/waveform.py +119 -75
- {waveforms-3.5.1 → waveforms-3.6.1/waveforms.egg-info}/PKG-INFO +12 -5
- {waveforms-3.5.1 → waveforms-3.6.1}/waveforms.egg-info/SOURCES.txt +2 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/LICENSE +0 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/MANIFEST.in +0 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/pyproject.toml +0 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/setup.cfg +0 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/setup.py +0 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/tests/test_core.py +0 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/tests/test_native_sos.py +0 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/tests/test_nonlinear.py +0 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/tests/test_output_limits.py +0 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/tests/test_waveform.py +0 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/tests/test_wavevstack.py +0 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/Waveform.g4 +0 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/WaveformLexer.py +0 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/WaveformListener.py +0 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/WaveformParser.py +0 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/__main__.py +0 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/_waveform.pyi +0 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/_waveform.pyx +0 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/nonlinear.py +0 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/utils.py +0 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/waveforms/waveform_parser.py +0 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/waveforms.egg-info/dependency_links.txt +0 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/waveforms.egg-info/entry_points.txt +0 -0
- {waveforms-3.5.1 → waveforms-3.6.1}/waveforms.egg-info/requires.txt +0 -0
- {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.
|
|
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)`
|
|
217
|
-
nonlinear
|
|
218
|
-
|
|
219
|
-
|
|
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)`
|
|
173
|
-
nonlinear
|
|
174
|
-
|
|
175
|
-
|
|
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",
|