waveforms 3.6.1__tar.gz → 3.7.0__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.6.1/waveforms.egg-info → waveforms-3.7.0}/PKG-INFO +29 -6
- {waveforms-3.6.1 → waveforms-3.7.0}/README.md +28 -5
- waveforms-3.7.0/tests/test_filter_parameters.py +225 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/tests/test_native_sos.py +14 -6
- {waveforms-3.6.1 → waveforms-3.7.0}/tests/test_nonlinear.py +3 -4
- {waveforms-3.6.1 → waveforms-3.7.0}/tests/test_output_limits.py +15 -9
- {waveforms-3.6.1 → waveforms-3.7.0}/tests/test_waveform.py +5 -7
- {waveforms-3.6.1 → waveforms-3.7.0}/tests/test_wavevstack.py +5 -8
- {waveforms-3.6.1 → waveforms-3.7.0}/waveforms/version.py +1 -1
- {waveforms-3.6.1 → waveforms-3.7.0}/waveforms/waveform.py +104 -42
- {waveforms-3.6.1 → waveforms-3.7.0/waveforms.egg-info}/PKG-INFO +29 -6
- {waveforms-3.6.1 → waveforms-3.7.0}/waveforms.egg-info/SOURCES.txt +1 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/LICENSE +0 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/MANIFEST.in +0 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/pyproject.toml +0 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/setup.cfg +0 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/setup.py +0 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/tests/test_common_pipeline.py +0 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/tests/test_core.py +0 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/tests/test_simd_sampling.py +0 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/waveforms/Waveform.g4 +0 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/waveforms/WaveformLexer.py +0 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/waveforms/WaveformListener.py +0 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/waveforms/WaveformParser.py +0 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/waveforms/__init__.py +0 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/waveforms/__main__.py +0 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/waveforms/_cwaveform.c +0 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/waveforms/_cwaveform.h +0 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/waveforms/_cwaveform.md +0 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/waveforms/_waveform.pyi +0 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/waveforms/_waveform.pyx +0 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/waveforms/distortion.py +0 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/waveforms/nonlinear.py +0 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/waveforms/utils.py +0 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/waveforms/waveform_parser.py +0 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/waveforms.egg-info/dependency_links.txt +0 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/waveforms.egg-info/entry_points.txt +0 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/waveforms.egg-info/requires.txt +0 -0
- {waveforms-3.6.1 → waveforms-3.7.0}/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.7.0
|
|
4
4
|
Summary: Edit waveforms used in experiment
|
|
5
5
|
Author-email: feihoo87 <feihoo87@gmail.com>
|
|
6
6
|
Maintainer-email: feihoo87 <feihoo87@gmail.com>
|
|
@@ -202,14 +202,37 @@ uses the same final limits. The filter state continues from the unclipped
|
|
|
202
202
|
filtered signal across chunks. DAC `full_scale` controls integer conversion
|
|
203
203
|
and saturation separately from these amplitude limits.
|
|
204
204
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
205
|
+
`filters` is a `defaultdict(float)` mapping exponential cascade time constants
|
|
206
|
+
`tau` (in seconds) to amplitudes `amp`, independent of the sampling clock:
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
stack.filters[100e-9] += 0.12
|
|
210
|
+
stack.filters[2e-6] += -0.04
|
|
211
|
+
samples = stack.sample(sample_rate=2_400_000_000)
|
|
212
|
+
unfiltered = stack.sample(filters={}) # bypass predistortion for this call
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The parameters describe cascade stages, not a parallel sum of exponential
|
|
216
|
+
terms. Sampling uses `exp_decay_filter_from_cascade` followed by
|
|
217
|
+
`exp_decay_filter(..., inv=True, output="sos")` at the actual sample rate:
|
|
218
|
+
filtering defaults to **predistortion**. Coefficients are cached by parameter
|
|
219
|
+
values and sample rate; changing an amplitude or the clock takes effect on the
|
|
220
|
+
next sampling call. Only the parameters are saved by pickle, not cached SOS
|
|
221
|
+
coefficients. Assign a mapping to replace them, or `None` to clear them. The old
|
|
222
|
+
`(sos, initial)` representation is no longer accepted.
|
|
223
|
+
|
|
224
|
+
The initial level is the first sample **after nonlinear mapping**, before
|
|
225
|
+
output limiting: the system is assumed to have held that level indefinitely
|
|
226
|
+
before playback. This stage subtracts that baseline, applies the inverse
|
|
227
|
+
filter, restores the baseline, and applies the final limits. Chunked sampling
|
|
228
|
+
retains both the initial level and the unclipped filter state across chunks;
|
|
229
|
+
complex signals use the first I and Q values independently.
|
|
230
|
+
|
|
231
|
+
SOS filtering runs in the C core for float64 and complex128 signals.
|
|
208
232
|
Real int16/int32 output is quantized in the same stage, without allocating a
|
|
209
233
|
full filtered floating-point buffer. Float output can reuse the sampled
|
|
210
234
|
buffer. A small fixed scratch buffer keeps filtering and conversion local;
|
|
211
|
-
waveform evaluation and nonlinear mapping still precede this stage.
|
|
212
|
-
coefficients and extended precision retain the SciPy implementation.
|
|
235
|
+
waveform evaluation and nonlinear mapping still precede this stage.
|
|
213
236
|
`sample_iq()` still converts the filtered complex result into separate I/Q
|
|
214
237
|
output arrays. See [the SOS benchmark](benchmarks/sos_filter.md).
|
|
215
238
|
|
|
@@ -158,14 +158,37 @@ uses the same final limits. The filter state continues from the unclipped
|
|
|
158
158
|
filtered signal across chunks. DAC `full_scale` controls integer conversion
|
|
159
159
|
and saturation separately from these amplitude limits.
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
`filters` is a `defaultdict(float)` mapping exponential cascade time constants
|
|
162
|
+
`tau` (in seconds) to amplitudes `amp`, independent of the sampling clock:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
stack.filters[100e-9] += 0.12
|
|
166
|
+
stack.filters[2e-6] += -0.04
|
|
167
|
+
samples = stack.sample(sample_rate=2_400_000_000)
|
|
168
|
+
unfiltered = stack.sample(filters={}) # bypass predistortion for this call
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The parameters describe cascade stages, not a parallel sum of exponential
|
|
172
|
+
terms. Sampling uses `exp_decay_filter_from_cascade` followed by
|
|
173
|
+
`exp_decay_filter(..., inv=True, output="sos")` at the actual sample rate:
|
|
174
|
+
filtering defaults to **predistortion**. Coefficients are cached by parameter
|
|
175
|
+
values and sample rate; changing an amplitude or the clock takes effect on the
|
|
176
|
+
next sampling call. Only the parameters are saved by pickle, not cached SOS
|
|
177
|
+
coefficients. Assign a mapping to replace them, or `None` to clear them. The old
|
|
178
|
+
`(sos, initial)` representation is no longer accepted.
|
|
179
|
+
|
|
180
|
+
The initial level is the first sample **after nonlinear mapping**, before
|
|
181
|
+
output limiting: the system is assumed to have held that level indefinitely
|
|
182
|
+
before playback. This stage subtracts that baseline, applies the inverse
|
|
183
|
+
filter, restores the baseline, and applies the final limits. Chunked sampling
|
|
184
|
+
retains both the initial level and the unclipped filter state across chunks;
|
|
185
|
+
complex signals use the first I and Q values independently.
|
|
186
|
+
|
|
187
|
+
SOS filtering runs in the C core for float64 and complex128 signals.
|
|
164
188
|
Real int16/int32 output is quantized in the same stage, without allocating a
|
|
165
189
|
full filtered floating-point buffer. Float output can reuse the sampled
|
|
166
190
|
buffer. A small fixed scratch buffer keeps filtering and conversion local;
|
|
167
|
-
waveform evaluation and nonlinear mapping still precede this stage.
|
|
168
|
-
coefficients and extended precision retain the SciPy implementation.
|
|
191
|
+
waveform evaluation and nonlinear mapping still precede this stage.
|
|
169
192
|
`sample_iq()` still converts the filtered complex result into separate I/Q
|
|
170
193
|
output arrays. See [the SOS benchmark](benchmarks/sos_filter.md).
|
|
171
194
|
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Clock-independent predistortion parameters and first-sample prehistory."""
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
import copy
|
|
5
|
+
import pickle
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pytest
|
|
9
|
+
from scipy.signal import sosfilt
|
|
10
|
+
|
|
11
|
+
import waveforms as wf
|
|
12
|
+
import waveforms.waveform as impl
|
|
13
|
+
from waveforms._waveform import quantize_samples
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
KINDS = ("real", "stack", "complex", "complex_stack")
|
|
17
|
+
STAGES = {50e-9: .12, 200e-9: -.04, 1e-6: .08}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _wave(kind, constant=False):
|
|
21
|
+
real = wf.const(.375) if constant else .125 + .25 * wf.cos(2 * np.pi * 7e6)
|
|
22
|
+
imag = wf.const(-.125) if constant else -.125 + .125 * wf.sin(2 * np.pi * 11e6)
|
|
23
|
+
if "stack" in kind:
|
|
24
|
+
real = wf.WaveVStack([.5 * real, .5 * real])
|
|
25
|
+
imag = wf.WaveVStack([.25 * imag, .75 * imag])
|
|
26
|
+
if kind == "complex":
|
|
27
|
+
real = wf.ComplexWaveform(real, imag)
|
|
28
|
+
elif kind == "complex_stack":
|
|
29
|
+
real = wf.ComplexWaveVStack(real, imag)
|
|
30
|
+
real.start, real.stop, real.sample_rate = 0., 256e-9, 1_000_000_000
|
|
31
|
+
return real
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _reference(raw, stages, rate):
|
|
35
|
+
amp, tau = wf.exp_decay_filter_from_cascade(
|
|
36
|
+
[(amp, tau) for tau, amp in sorted(stages.items()) if amp != 0])
|
|
37
|
+
sos = wf.exp_decay_filter(amp, tau, rate, inv=True, output="sos")
|
|
38
|
+
return sosfilt(sos, raw - raw[0]) + raw[0]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.mark.parametrize("kind", KINDS)
|
|
42
|
+
def test_default_mapping_assignment_and_independent_copies(kind):
|
|
43
|
+
wave = _wave(kind)
|
|
44
|
+
other = _wave(kind)
|
|
45
|
+
assert isinstance(wave.filters, defaultdict)
|
|
46
|
+
assert wave.filters.default_factory is float
|
|
47
|
+
assert wave.filters[50e-9] == 0
|
|
48
|
+
wave.filters[50e-9] += .12
|
|
49
|
+
assert other.filters == {}
|
|
50
|
+
params = defaultdict(lambda: 99, STAGES)
|
|
51
|
+
wave.filters = params
|
|
52
|
+
params[50e-9] = .9
|
|
53
|
+
assert wave.filters == STAGES
|
|
54
|
+
assert wave.filters.default_factory is float
|
|
55
|
+
for duplicate in (copy.copy(wave), copy.deepcopy(wave),
|
|
56
|
+
pickle.loads(pickle.dumps(wave))):
|
|
57
|
+
assert isinstance(duplicate.filters, defaultdict)
|
|
58
|
+
assert duplicate.filters == STAGES
|
|
59
|
+
assert duplicate.filters is not wave.filters
|
|
60
|
+
duplicate.filters[50e-9] += .05
|
|
61
|
+
assert wave.filters == STAGES
|
|
62
|
+
wave.filters = None
|
|
63
|
+
assert isinstance(wave.filters, defaultdict)
|
|
64
|
+
assert wave.filters == {}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pytest.mark.parametrize("kind", ["stack", "complex_stack"])
|
|
68
|
+
def test_delay_calibration_preserves_but_does_not_alias_filters(kind):
|
|
69
|
+
wave = _wave(kind)
|
|
70
|
+
wave.filters = STAGES
|
|
71
|
+
restored = pickle.loads(pickle.dumps(wave))
|
|
72
|
+
shifted = restored >> 2e-9
|
|
73
|
+
assert shifted.filters == STAGES
|
|
74
|
+
raw = shifted.sample(filters={})
|
|
75
|
+
np.testing.assert_allclose(shifted.sample(),
|
|
76
|
+
_reference(raw, STAGES, shifted.sample_rate),
|
|
77
|
+
rtol=3e-12, atol=3e-13)
|
|
78
|
+
shifted.filters[50e-9] += .01
|
|
79
|
+
assert restored.filters == STAGES
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@pytest.mark.parametrize("kind", KINDS)
|
|
83
|
+
@pytest.mark.parametrize("rate", [500_000_000, 1_200_000_000, 2_400_000_000,
|
|
84
|
+
7_000_000_000, 1_234_567_890.5])
|
|
85
|
+
def test_rate_override_inverse_filter_and_chunked_first_sample(kind, rate):
|
|
86
|
+
wave = _wave(kind)
|
|
87
|
+
wave.filters = STAGES
|
|
88
|
+
raw = wave.sample(rate, filters={})
|
|
89
|
+
expected = _reference(raw, STAGES, rate)
|
|
90
|
+
actual = wave.sample(rate)
|
|
91
|
+
assert actual[0] == raw[0]
|
|
92
|
+
assert not np.allclose(actual, raw)
|
|
93
|
+
# Native real/imag recurrences and SciPy complex arithmetic round slightly
|
|
94
|
+
# differently for poles this close to one (small accumulated roundoff).
|
|
95
|
+
np.testing.assert_allclose(actual, expected, rtol=3e-12, atol=2e-12)
|
|
96
|
+
for chunk_size in (1, 31, 256, len(raw) + 1):
|
|
97
|
+
target = np.empty_like(expected)
|
|
98
|
+
chunks = list(wave.sample(rate, chunk_size=chunk_size, out=target))
|
|
99
|
+
np.testing.assert_allclose(np.concatenate(chunks), expected,
|
|
100
|
+
rtol=3e-12, atol=2e-12)
|
|
101
|
+
np.testing.assert_allclose(target, expected, rtol=3e-12, atol=2e-12)
|
|
102
|
+
# Changing the device clock must not modify the serialized calibration.
|
|
103
|
+
assert wave.sample_rate == 1_000_000_000
|
|
104
|
+
assert wave.filters == STAGES
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@pytest.mark.parametrize("kind", KINDS)
|
|
108
|
+
def test_constant_prehistory_has_no_startup_transient_after_mapping(kind):
|
|
109
|
+
wave = _wave(kind, constant=True)
|
|
110
|
+
wave.filters = STAGES
|
|
111
|
+
mapping = wf.NonlinearMap.from_samples(
|
|
112
|
+
[-1., 1.], [-.5, 1.5], method="linear", table_size=2)
|
|
113
|
+
wave.nonlinear = (mapping, mapping) if "complex" in kind else mapping
|
|
114
|
+
expected = .875 + .375j if "complex" in kind else .875
|
|
115
|
+
np.testing.assert_array_equal(wave.sample(), np.full(256, expected))
|
|
116
|
+
chunks = np.concatenate(list(wave.sample(chunk_size=7)))
|
|
117
|
+
np.testing.assert_array_equal(chunks, np.full(256, expected))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@pytest.mark.parametrize("kind", KINDS)
|
|
121
|
+
@pytest.mark.parametrize("dtype", [np.int16, np.int32])
|
|
122
|
+
def test_initial_level_precedes_clipping_and_quantization(kind, dtype):
|
|
123
|
+
wave = _wave(kind)
|
|
124
|
+
wave.filters = STAGES
|
|
125
|
+
mapping = wf.NonlinearMap.from_samples(
|
|
126
|
+
[-1., 1.], [-.75, 1.25], method="linear", table_size=2)
|
|
127
|
+
wave.nonlinear = (mapping, mapping) if "complex" in kind else mapping
|
|
128
|
+
raw = wave.sample(filters={})
|
|
129
|
+
filtered = _reference(raw, STAGES, wave.sample_rate)
|
|
130
|
+
wave.min, wave.max = -.1, .2
|
|
131
|
+
bits = np.dtype(dtype).itemsize * 8
|
|
132
|
+
if "complex" in kind:
|
|
133
|
+
expected = tuple(quantize_samples(np.clip(v, -.1, .2), bits)
|
|
134
|
+
for v in (filtered.real, filtered.imag))
|
|
135
|
+
actual = wave.sample_iq(dtype=dtype)
|
|
136
|
+
for component, reference in zip(actual, expected):
|
|
137
|
+
np.testing.assert_array_equal(component, reference)
|
|
138
|
+
chunks = list(wave.sample_iq(dtype=dtype, chunk_size=1))
|
|
139
|
+
for index in (0, 1):
|
|
140
|
+
np.testing.assert_array_equal(
|
|
141
|
+
np.concatenate([chunk[index] for chunk in chunks]), expected[index])
|
|
142
|
+
else:
|
|
143
|
+
expected = quantize_samples(np.clip(filtered, -.1, .2), bits)
|
|
144
|
+
np.testing.assert_array_equal(wave.sample(dtype=dtype), expected)
|
|
145
|
+
np.testing.assert_array_equal(
|
|
146
|
+
np.concatenate(list(wave.sample(dtype=dtype, chunk_size=1))), expected)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@pytest.mark.parametrize("kind", KINDS)
|
|
150
|
+
def test_empty_and_single_sample(kind):
|
|
151
|
+
wave = _wave(kind)
|
|
152
|
+
wave.filters = STAGES
|
|
153
|
+
wave.stop = wave.start
|
|
154
|
+
assert wave.sample().size == 0
|
|
155
|
+
assert list(wave.sample(chunk_size=1)) == []
|
|
156
|
+
wave.stop = 1e-9
|
|
157
|
+
expected = wave.sample(filters={})
|
|
158
|
+
assert len(expected) == 1
|
|
159
|
+
np.testing.assert_array_equal(wave.sample(), expected)
|
|
160
|
+
np.testing.assert_array_equal(np.concatenate(list(wave.sample(chunk_size=1))), expected)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_cache_tracks_values_and_rate_but_not_initial_or_filter_state():
|
|
164
|
+
impl._exp_decay_sos.cache_clear()
|
|
165
|
+
wave = _wave("real")
|
|
166
|
+
wave.filters = STAGES
|
|
167
|
+
before = pickle.dumps(wave)
|
|
168
|
+
initial_result = wave.sample()
|
|
169
|
+
assert impl._exp_decay_sos.cache_info().misses == 1
|
|
170
|
+
assert pickle.dumps(wave) == before
|
|
171
|
+
np.testing.assert_array_equal(wave.sample(), initial_result)
|
|
172
|
+
wave.filters = dict(reversed(list(STAGES.items())))
|
|
173
|
+
wave.filters[2e-6] # Reading a missing tau must not invalidate the cache.
|
|
174
|
+
np.testing.assert_array_equal(wave.sample(), initial_result)
|
|
175
|
+
assert impl._exp_decay_sos.cache_info().misses == 1
|
|
176
|
+
# A different signal shares coefficients, not its first point or state.
|
|
177
|
+
other = _wave("complex")
|
|
178
|
+
other.filters = STAGES
|
|
179
|
+
other.sample()
|
|
180
|
+
assert impl._exp_decay_sos.cache_info().misses == 1
|
|
181
|
+
wave.filters[50e-9] += .1
|
|
182
|
+
assert not np.allclose(wave.sample(), initial_result)
|
|
183
|
+
assert impl._exp_decay_sos.cache_info().misses == 2
|
|
184
|
+
wave.sample(2_400_000_000)
|
|
185
|
+
assert impl._exp_decay_sos.cache_info().misses == 3
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_explicit_mapping_override_and_snapshot_during_stream():
|
|
189
|
+
wave = _wave("real")
|
|
190
|
+
wave.filters = STAGES
|
|
191
|
+
raw = wave.sample(filters={})
|
|
192
|
+
override = {75e-9: .2}
|
|
193
|
+
expected = _reference(raw, override, wave.sample_rate)
|
|
194
|
+
stream = wave.sample(filters=override, chunk_size=31)
|
|
195
|
+
chunks = [next(stream)]
|
|
196
|
+
override[75e-9] = .4
|
|
197
|
+
wave.filters[50e-9] = .5
|
|
198
|
+
chunks.extend(stream)
|
|
199
|
+
np.testing.assert_allclose(np.concatenate(chunks), expected,
|
|
200
|
+
rtol=3e-12, atol=3e-13)
|
|
201
|
+
np.testing.assert_array_equal(wave.sample(filters={75e-9: 0}), raw)
|
|
202
|
+
np.testing.assert_array_equal(wave.sample(filters={}), raw)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@pytest.mark.parametrize("params,message", [
|
|
206
|
+
({0.: .1}, "positive"), ({-1.: .1}, "positive"),
|
|
207
|
+
({np.inf: .1}, "finite"), ({np.nan: .1}, "finite"),
|
|
208
|
+
({1e-6: np.nan}, "finite"), ({1e-6: np.inf}, "finite"),
|
|
209
|
+
])
|
|
210
|
+
def test_invalid_active_parameters(params, message):
|
|
211
|
+
wave = _wave("real")
|
|
212
|
+
wave.filters = params
|
|
213
|
+
with pytest.raises(ValueError, match=message):
|
|
214
|
+
wave.sample()
|
|
215
|
+
with pytest.raises(ValueError, match=message):
|
|
216
|
+
list(wave.sample(chunk_size=7))
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_old_sos_tuple_is_not_a_parameter_mapping():
|
|
220
|
+
wave = _wave("real")
|
|
221
|
+
old = (np.array([[1., 0., 0., 1., 0., 0.]]), 0.)
|
|
222
|
+
with pytest.raises(TypeError, match="mapping"):
|
|
223
|
+
wave.filters = old
|
|
224
|
+
with pytest.raises(TypeError, match="mapping"):
|
|
225
|
+
wave.sample(filters=old)
|
|
@@ -6,6 +6,7 @@ from scipy.signal import butter, sosfilt
|
|
|
6
6
|
|
|
7
7
|
import waveforms as wf
|
|
8
8
|
from waveforms._waveform import quantize_samples, sosfilt_samples
|
|
9
|
+
from waveforms.waveform import _filter_and_finish
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def _clip(values, lower, upper):
|
|
@@ -148,17 +149,24 @@ def test_scipy_fallback(coefficient_dtype):
|
|
|
148
149
|
sos = butter(4, .3, output="sos").astype(coefficient_dtype)
|
|
149
150
|
if coefficient_dtype == np.complex128:
|
|
150
151
|
sos[0, 0] += .1j
|
|
151
|
-
wave.filters = sos, .03
|
|
152
152
|
raw = .4 + .5 * np.cos(6 * np.pi * np.arange(1024) / 1024)
|
|
153
153
|
expected = _clip(sosfilt(sos, raw - .03) + .03, wave.min, wave.max)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
154
|
+
result, _ = _filter_and_finish(
|
|
155
|
+
raw.copy(), sos, None, 1., None, wave.min, wave.max, initial=.03)
|
|
156
|
+
np.testing.assert_allclose(result, expected, rtol=3e-13, atol=3e-14)
|
|
157
|
+
chunks, state = [], None
|
|
158
|
+
for start in range(0, len(raw), 73):
|
|
159
|
+
result, state = _filter_and_finish(
|
|
160
|
+
raw[start:start + 73].copy(), sos, None, 1., None,
|
|
161
|
+
wave.min, wave.max, state, initial=.03)
|
|
162
|
+
chunks.append(result)
|
|
163
|
+
np.testing.assert_allclose(np.concatenate(chunks), expected,
|
|
164
|
+
rtol=3e-13, atol=3e-14)
|
|
157
165
|
|
|
158
166
|
|
|
159
|
-
def
|
|
167
|
+
def test_pipeline_output_casting_and_single_section():
|
|
160
168
|
wave = _wave()
|
|
161
|
-
wave.filters =
|
|
169
|
+
wave.filters = {.04: .2}
|
|
162
170
|
expected = wave.sample()
|
|
163
171
|
for dtype in (None, np.float32, np.float64):
|
|
164
172
|
for stride in (1, 2):
|
|
@@ -3,7 +3,7 @@ import pickle
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
import pytest
|
|
5
5
|
from scipy.interpolate import PchipInterpolator
|
|
6
|
-
from scipy.signal import
|
|
6
|
+
from scipy.signal import sosfilt
|
|
7
7
|
|
|
8
8
|
import waveforms as wf
|
|
9
9
|
from waveforms._waveform import quantize_samples
|
|
@@ -129,14 +129,13 @@ def test_waveform_sampling_order_chunking_quantization_and_pickle():
|
|
|
129
129
|
[0.0, 0.0625, 0.25, 0.5625, 1.0],
|
|
130
130
|
table_size=257,
|
|
131
131
|
)
|
|
132
|
-
|
|
133
|
-
sos = tf2sos(b, a)
|
|
132
|
+
sos = wf.exp_decay_filter(.2, .04, sample_rate, inv=True, output="sos")
|
|
134
133
|
waveform = wf.t()
|
|
135
134
|
waveform.start = 0.0
|
|
136
135
|
waveform.stop = 1.0
|
|
137
136
|
waveform.sample_rate = sample_rate
|
|
138
137
|
waveform.nonlinear = mapping
|
|
139
|
-
waveform.filters =
|
|
138
|
+
waveform.filters = {.04: .2}
|
|
140
139
|
|
|
141
140
|
raw = np.arange(sample_rate, dtype=np.float64) / sample_rate
|
|
142
141
|
expected = sosfilt(sos, mapping(raw))
|
|
@@ -49,14 +49,13 @@ def _clip(values, lower, upper):
|
|
|
49
49
|
|
|
50
50
|
@pytest.mark.parametrize("kind", ["real", "stack", "complex", "complex_stack"])
|
|
51
51
|
@pytest.mark.parametrize("rate", [128, 127.5])
|
|
52
|
-
@pytest.mark.parametrize("
|
|
53
|
-
def test_output_limits_follow_mapping_and_filter_with_chunk_state(kind, rate,
|
|
52
|
+
@pytest.mark.parametrize("amp", [0.25, -0.5])
|
|
53
|
+
def test_output_limits_follow_mapping_and_filter_with_chunk_state(kind, rate, amp):
|
|
54
54
|
wave = _configure(_signal(kind), rate)
|
|
55
55
|
wave.min, wave.max = -0.22, 0.27
|
|
56
56
|
# Stateful filter exercises overshoot and continuing with the unclipped zf.
|
|
57
|
-
sos =
|
|
58
|
-
|
|
59
|
-
wave.filters = (sos, initial)
|
|
57
|
+
sos = wf.exp_decay_filter(amp, .04, rate, inv=True, output="sos")
|
|
58
|
+
wave.filters = {.04: amp}
|
|
60
59
|
mapping = wf.NonlinearMap.from_samples(
|
|
61
60
|
[-3., 3.], [-6., 6.], method="linear", table_size=2)
|
|
62
61
|
wave.nonlinear = (mapping, mapping) if "complex" in kind else mapping
|
|
@@ -65,6 +64,7 @@ def test_output_limits_follow_mapping_and_filter_with_chunk_state(kind, rate, ga
|
|
|
65
64
|
raw = 0.45 + 0.35 * np.cos(6 * np.pi * positions)
|
|
66
65
|
if "complex" in kind:
|
|
67
66
|
raw = raw + 1j * (-0.3 + 0.2 * np.sin(4 * np.pi * positions))
|
|
67
|
+
initial = 2 * raw[0]
|
|
68
68
|
filtered = sosfilt(sos, 2 * raw - initial) + initial
|
|
69
69
|
expected = _clip(filtered, wave.min, wave.max)
|
|
70
70
|
|
|
@@ -103,11 +103,17 @@ def test_output_limits_follow_mapping_and_filter_with_chunk_state(kind, rate, ga
|
|
|
103
103
|
np.testing.assert_array_equal(np.concatenate(list(chunks)), expected_integer)
|
|
104
104
|
|
|
105
105
|
|
|
106
|
-
@pytest.mark.parametrize("
|
|
107
|
-
def
|
|
108
|
-
wave = _configure(wf.
|
|
106
|
+
@pytest.mark.parametrize("amp", [.5, -.5])
|
|
107
|
+
def test_filter_never_sees_prematurely_limited_input(amp):
|
|
108
|
+
wave = _configure(.4 - .35 * (wf.step(0) >> .25))
|
|
109
109
|
wave.min, wave.max = -.2, .2
|
|
110
|
-
wave.filters =
|
|
110
|
+
wave.filters = {.03: amp}
|
|
111
|
+
sos = wf.exp_decay_filter(amp, .03, wave.sample_rate, inv=True, output="sos")
|
|
112
|
+
raw = np.where(np.arange(128) < 32, .4, .05)
|
|
113
|
+
expected = np.clip(sosfilt(sos, raw - raw[0]) + raw[0], -.2, .2)
|
|
114
|
+
limited = np.clip(raw, -.2, .2)
|
|
115
|
+
premature = np.clip(sosfilt(sos, limited - limited[0]) + limited[0], -.2, .2)
|
|
116
|
+
assert not np.allclose(expected, premature)
|
|
111
117
|
np.testing.assert_allclose(wave.sample(), expected)
|
|
112
118
|
np.testing.assert_allclose(np.concatenate(list(wave.sample(chunk_size=7))), expected)
|
|
113
119
|
|
|
@@ -6,7 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
import pytest
|
|
8
8
|
import scipy.special as special
|
|
9
|
-
from scipy.signal import
|
|
9
|
+
from scipy.signal import sosfilt
|
|
10
10
|
|
|
11
11
|
import waveforms as wf
|
|
12
12
|
from waveforms._waveform import (
|
|
@@ -252,16 +252,15 @@ def test_multi_frequency_drag_matches_120ghz_tick_behavior():
|
|
|
252
252
|
|
|
253
253
|
def test_filters_and_chunked_sampling():
|
|
254
254
|
sample_rate = 1000
|
|
255
|
-
|
|
256
|
-
zi = lfiltic(b, a, [0])
|
|
255
|
+
sos = wf.exp_decay_filter(.2, .04, sample_rate, inv=True, output="sos")
|
|
257
256
|
x = np.linspace(-1, 1, 2000, endpoint=False)
|
|
258
257
|
|
|
259
258
|
wav = wf.step(0)
|
|
260
259
|
wav.start = -1
|
|
261
260
|
wav.stop = 1
|
|
262
261
|
wav.sample_rate = sample_rate
|
|
263
|
-
wav.filters =
|
|
264
|
-
expected =
|
|
262
|
+
wav.filters = {.04: .2}
|
|
263
|
+
expected = sosfilt(sos, np.heaviside(x, 1))
|
|
265
264
|
assert np.allclose(wav.sample(), expected)
|
|
266
265
|
assert np.allclose(np.concatenate(list(wav.sample(chunk_size=137))), expected)
|
|
267
266
|
|
|
@@ -575,8 +574,7 @@ def test_integer_sampling_filtered_and_overlapping_fallbacks_are_exact():
|
|
|
575
574
|
quantize_samples(overlapping.sample(rate), 16),
|
|
576
575
|
)
|
|
577
576
|
|
|
578
|
-
|
|
579
|
-
overlapping.filters = (tf2sos(b, a), 0)
|
|
577
|
+
overlapping.filters = {20e-9: .2}
|
|
580
578
|
assert np.array_equal(
|
|
581
579
|
overlapping.sample(rate, dtype=np.int16),
|
|
582
580
|
quantize_samples(overlapping.sample(rate), 16),
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import pickle
|
|
2
2
|
|
|
3
3
|
import numpy as np
|
|
4
|
-
from scipy.signal import
|
|
4
|
+
from scipy.signal import sosfilt
|
|
5
5
|
|
|
6
6
|
from waveforms import (
|
|
7
|
-
Waveform, WaveVStack, cos, gaussian, pi, poly, sin, step,
|
|
7
|
+
Waveform, WaveVStack, cos, exp_decay_filter, gaussian, pi, poly, sin, step,
|
|
8
8
|
time_to_tick, zero,
|
|
9
9
|
)
|
|
10
10
|
|
|
@@ -35,18 +35,15 @@ def test_wavevstack_sampling_algebra_and_shifts():
|
|
|
35
35
|
|
|
36
36
|
def test_wavevstack_filtering_and_chunked_sampling():
|
|
37
37
|
sample_rate = 1000
|
|
38
|
-
|
|
39
|
-
zi = lfiltic(b, a, [0])
|
|
38
|
+
sos = exp_decay_filter(.2, .04, sample_rate, inv=True, output="sos")
|
|
40
39
|
x = np.linspace(-1, 1, 2000, endpoint=False)
|
|
41
40
|
|
|
42
41
|
stack = WaveVStack([step(0) << 0.5, -step(0)])
|
|
43
42
|
stack.sample_rate = sample_rate
|
|
44
43
|
stack.start = -1
|
|
45
44
|
stack.stop = 1
|
|
46
|
-
stack.filters =
|
|
47
|
-
expected =
|
|
48
|
-
b, a, np.heaviside(x + 0.5, 1) - np.heaviside(x, 1), zi=zi
|
|
49
|
-
)[0]
|
|
45
|
+
stack.filters = {.04: .2}
|
|
46
|
+
expected = sosfilt(sos, np.heaviside(x + 0.5, 1) - np.heaviside(x, 1))
|
|
50
47
|
|
|
51
48
|
assert np.allclose(stack.sample(), expected, atol=1e-6)
|
|
52
49
|
chunks = np.concatenate(list(stack.sample(chunk_size=137)))
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""Define version number here and read it from setup.py automatically"""
|
|
2
|
-
__version__ = "3.
|
|
2
|
+
__version__ = "3.7.0"
|
|
@@ -9,6 +9,8 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import io
|
|
11
11
|
import struct
|
|
12
|
+
from collections import defaultdict
|
|
13
|
+
from collections.abc import Mapping
|
|
12
14
|
from functools import lru_cache
|
|
13
15
|
from typing import Iterable, cast
|
|
14
16
|
|
|
@@ -16,6 +18,7 @@ import numpy as np
|
|
|
16
18
|
from numpy import e, inf, pi
|
|
17
19
|
from scipy.signal import sosfilt
|
|
18
20
|
|
|
21
|
+
from .distortion import exp_decay_filter, exp_decay_filter_from_cascade
|
|
19
22
|
from .nonlinear import NonlinearMap
|
|
20
23
|
|
|
21
24
|
from ._waveform import (
|
|
@@ -102,7 +105,7 @@ def _copy_sampling_metadata(source, target):
|
|
|
102
105
|
target.start = source.start
|
|
103
106
|
target.stop = source.stop
|
|
104
107
|
target.sample_rate = source.sample_rate
|
|
105
|
-
target.filters = source.
|
|
108
|
+
target.filters = source._filters
|
|
106
109
|
target.nonlinear = source.nonlinear
|
|
107
110
|
target.label = source.label
|
|
108
111
|
return target
|
|
@@ -332,10 +335,38 @@ def _real_sampling_output(dtype, bits, out):
|
|
|
332
335
|
return None
|
|
333
336
|
|
|
334
337
|
|
|
335
|
-
|
|
338
|
+
@lru_cache(maxsize=128)
|
|
339
|
+
def _exp_decay_sos(stages, sample_rate):
|
|
340
|
+
"""Compile a snapshot of cascade parameters for one sampling clock."""
|
|
341
|
+
if not np.isfinite(sample_rate) or sample_rate <= 0:
|
|
342
|
+
raise ValueError("sample_rate must be a finite positive number")
|
|
343
|
+
amp, tau = exp_decay_filter_from_cascade(
|
|
344
|
+
[(amp, tau) for tau, amp in stages])
|
|
345
|
+
sos = np.ascontiguousarray(
|
|
346
|
+
exp_decay_filter(amp, tau, sample_rate, inv=True, output='sos'),
|
|
347
|
+
dtype=np.float64)
|
|
348
|
+
if not np.all(np.isfinite(sos)):
|
|
349
|
+
raise ValueError("filters produce non-finite SOS coefficients")
|
|
350
|
+
# Cache coefficients, never mutable streaming state or a signal's baseline.
|
|
351
|
+
sos.flags.writeable = False
|
|
352
|
+
return sos
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _prepare_filters(filters, sample_rate):
|
|
336
356
|
if filters is None:
|
|
337
|
-
return
|
|
338
|
-
|
|
357
|
+
return None
|
|
358
|
+
if not isinstance(filters, Mapping):
|
|
359
|
+
raise TypeError("filters must be a mapping from tau (seconds) to amp")
|
|
360
|
+
if not filters:
|
|
361
|
+
return None
|
|
362
|
+
# Ignore missing-key/default-zero entries and key the cache by values, so
|
|
363
|
+
# edits through filters[tau] += amp take effect on the next sampling call.
|
|
364
|
+
stages = tuple(sorted((float(tau), float(amp))
|
|
365
|
+
for tau, amp in filters.items() if amp != 0))
|
|
366
|
+
return _exp_decay_sos(stages, float(sample_rate)) if stages else None
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _filter_samples(sig, sos, initial, zi=None):
|
|
339
370
|
sos = np.asarray(sos)
|
|
340
371
|
if not sos.flags.writeable:
|
|
341
372
|
sos = sos.copy()
|
|
@@ -349,20 +380,20 @@ def _filter_samples(sig, filters, zi=None):
|
|
|
349
380
|
return filtered, zi
|
|
350
381
|
|
|
351
382
|
|
|
352
|
-
def _filter_and_finish(sig,
|
|
353
|
-
minimum, maximum, zi=None):
|
|
354
|
-
if
|
|
383
|
+
def _filter_and_finish(sig, sos, dtype, full_scale, out,
|
|
384
|
+
minimum, maximum, zi=None, initial=None):
|
|
385
|
+
if sos is None:
|
|
355
386
|
return _finish_samples(
|
|
356
387
|
sig, dtype, full_scale, out, minimum, maximum), zi
|
|
357
|
-
sos, initial = filters
|
|
358
388
|
sos = np.asarray(sos)
|
|
359
|
-
|
|
389
|
+
if initial is None:
|
|
390
|
+
initial = sig[0] if len(sig) else 0.0
|
|
360
391
|
values_dtype = np.result_type(sig, np.asarray(initial), sos,
|
|
361
392
|
zi if zi is not None else np.float64)
|
|
362
393
|
# Preserve SciPy's complex-coefficient and extended-precision behavior.
|
|
363
394
|
if (sos.dtype.kind not in 'biuf' or sos.dtype.itemsize > 8
|
|
364
395
|
or values_dtype not in (np.dtype(np.float64), np.dtype(np.complex128))):
|
|
365
|
-
sig, zi = _filter_samples(sig,
|
|
396
|
+
sig, zi = _filter_samples(sig, sos, initial, zi)
|
|
366
397
|
return _finish_samples(
|
|
367
398
|
sig, dtype, full_scale, out, minimum, maximum), zi
|
|
368
399
|
dtype, bits = _quantization_bits(dtype, out)
|
|
@@ -479,11 +510,32 @@ class Waveform(metaclass=_WaveformMeta):
|
|
|
479
510
|
start: float | None
|
|
480
511
|
stop: float | None
|
|
481
512
|
sample_rate: float | None
|
|
482
|
-
|
|
513
|
+
_filters: defaultdict[float, float] | None
|
|
483
514
|
nonlinear: NonlinearMap | tuple[NonlinearMap | None, NonlinearMap | None] | None
|
|
484
515
|
min: float
|
|
485
516
|
max: float
|
|
486
517
|
|
|
518
|
+
@property
|
|
519
|
+
def filters(self) -> defaultdict[float, float]:
|
|
520
|
+
"""Exponential cascade stages: time constant (seconds) -> amplitude.
|
|
521
|
+
|
|
522
|
+
Missing amplitudes default to zero. SOS coefficients are derived from
|
|
523
|
+
these parameters at sampling time using the actual sample rate and
|
|
524
|
+
``inv=True`` (predistortion).
|
|
525
|
+
"""
|
|
526
|
+
# Most temporary expression nodes never use filters. Allocate only on
|
|
527
|
+
# access, keeping pulse construction and unfiltered sampling cheap.
|
|
528
|
+
if self._filters is None:
|
|
529
|
+
self._filters = defaultdict(float)
|
|
530
|
+
return self._filters
|
|
531
|
+
|
|
532
|
+
@filters.setter
|
|
533
|
+
def filters(self, value: Mapping[float, float] | None):
|
|
534
|
+
if value is not None and not isinstance(value, Mapping):
|
|
535
|
+
raise TypeError("filters must be a mapping from tau (seconds) to amp")
|
|
536
|
+
# Copy on assignment: metadata on shifted/copied objects is independent.
|
|
537
|
+
self._filters = None if not value else defaultdict(float, value)
|
|
538
|
+
|
|
487
539
|
def _evaluate_raw(self, x):
|
|
488
540
|
"""Evaluate before output limits, nonlinear calibration or filtering."""
|
|
489
541
|
return self(x, _raw=True)
|
|
@@ -508,8 +560,15 @@ class Waveform(metaclass=_WaveformMeta):
|
|
|
508
560
|
|
|
509
561
|
def sample(self, sample_rate=None, out: np.ndarray | None = None,
|
|
510
562
|
chunk_size=None, function_lib=None,
|
|
511
|
-
filters:
|
|
563
|
+
filters: Mapping[float, float] | None = None,
|
|
512
564
|
dtype=None, full_scale=1.0, nonlinear=None):
|
|
565
|
+
"""Sample, map, filter, limit, then convert to the requested dtype.
|
|
566
|
+
|
|
567
|
+
``filters=None`` uses this object's tau-to-amp cascade; ``filters={}``
|
|
568
|
+
disables it for this call. Filtering assumes the mapped first sample
|
|
569
|
+
was held indefinitely before playback. The baseline and filter state
|
|
570
|
+
persist across chunks of this sampling call.
|
|
571
|
+
"""
|
|
513
572
|
minimum, maximum = _amplitude_limits(self)
|
|
514
573
|
if function_lib is not None:
|
|
515
574
|
raise NotImplementedError("custom waveform functions are not supported")
|
|
@@ -520,14 +579,14 @@ class Waveform(metaclass=_WaveformMeta):
|
|
|
520
579
|
f"Waveform is not initialized. {self.start=}, {self.stop=}, "
|
|
521
580
|
f"{sample_rate=}"
|
|
522
581
|
)
|
|
523
|
-
if filters is None
|
|
524
|
-
|
|
582
|
+
sos = _prepare_filters(self._filters if filters is None else filters,
|
|
583
|
+
sample_rate)
|
|
525
584
|
if nonlinear is None:
|
|
526
585
|
nonlinear = self.nonlinear
|
|
527
586
|
dtype, bits = _quantization_bits(dtype, out)
|
|
528
587
|
if chunk_size is not None:
|
|
529
588
|
return self._sample_iter(
|
|
530
|
-
sample_rate, int(chunk_size), out,
|
|
589
|
+
sample_rate, int(chunk_size), out, sos, dtype, full_scale,
|
|
531
590
|
nonlinear,
|
|
532
591
|
)
|
|
533
592
|
|
|
@@ -536,7 +595,7 @@ class Waveform(metaclass=_WaveformMeta):
|
|
|
536
595
|
x = np.arange(self.start, self.stop, 1 / float(sample_rate))
|
|
537
596
|
sig = cast(np.ndarray, self._evaluate_raw(x))
|
|
538
597
|
else:
|
|
539
|
-
if bits is not None and
|
|
598
|
+
if bits is not None and sos is None and nonlinear is None:
|
|
540
599
|
specialized = getattr(
|
|
541
600
|
self, "_sample_supported_quantized", None
|
|
542
601
|
)
|
|
@@ -549,7 +608,7 @@ class Waveform(metaclass=_WaveformMeta):
|
|
|
549
608
|
sig = self._sample_supported(*plan)
|
|
550
609
|
sig = _apply_nonlinear(sig, nonlinear)
|
|
551
610
|
result, _ = _filter_and_finish(
|
|
552
|
-
sig,
|
|
611
|
+
sig, sos, dtype, full_scale, out, minimum, maximum)
|
|
553
612
|
return cast(np.ndarray, result)
|
|
554
613
|
|
|
555
614
|
def _sample_supported(self, start_tick, count, step_numerator,
|
|
@@ -558,13 +617,14 @@ class Waveform(metaclass=_WaveformMeta):
|
|
|
558
617
|
index_offset)
|
|
559
618
|
return cast(np.ndarray, self._evaluate_raw(x))
|
|
560
619
|
|
|
561
|
-
def _sample_iter(self, sample_rate, chunk_size, out,
|
|
620
|
+
def _sample_iter(self, sample_rate, chunk_size, out, sos, dtype,
|
|
562
621
|
full_scale, nonlinear):
|
|
563
622
|
minimum, maximum = _amplitude_limits(self)
|
|
564
623
|
start = cast(float, self.start)
|
|
565
624
|
stop = cast(float, self.stop)
|
|
566
625
|
output_index = 0
|
|
567
626
|
zi = None
|
|
627
|
+
initial = None
|
|
568
628
|
if chunk_size <= 0:
|
|
569
629
|
raise ValueError("chunk_size must be positive")
|
|
570
630
|
plan = _sampling_plan(start, stop, sample_rate)
|
|
@@ -596,8 +656,10 @@ class Waveform(metaclass=_WaveformMeta):
|
|
|
596
656
|
target = (None if output is None
|
|
597
657
|
else output[output_index:output_index + size])
|
|
598
658
|
sig = _apply_nonlinear(sig, nonlinear)
|
|
659
|
+
if initial is None and sos is not None:
|
|
660
|
+
initial = sig[0]
|
|
599
661
|
sig, zi = _filter_and_finish(
|
|
600
|
-
sig,
|
|
662
|
+
sig, sos, dtype, full_scale, target, minimum, maximum, zi, initial)
|
|
601
663
|
yield sig
|
|
602
664
|
output_index += size
|
|
603
665
|
|
|
@@ -638,7 +700,7 @@ class _RealWaveformBase(Waveform):
|
|
|
638
700
|
|
|
639
701
|
__slots__ = (
|
|
640
702
|
"_core", "max", "min", "start", "stop", "sample_rate",
|
|
641
|
-
"
|
|
703
|
+
"_filters", "nonlinear", "label",
|
|
642
704
|
)
|
|
643
705
|
|
|
644
706
|
|
|
@@ -647,7 +709,7 @@ class ComplexWaveform(Waveform):
|
|
|
647
709
|
|
|
648
710
|
__slots__ = (
|
|
649
711
|
"_real", "_imag", "max", "min", "start", "stop", "sample_rate",
|
|
650
|
-
"
|
|
712
|
+
"_filters", "nonlinear", "label",
|
|
651
713
|
)
|
|
652
714
|
|
|
653
715
|
def __init__(self, real=0.0, imag=0.0):
|
|
@@ -681,7 +743,7 @@ class ComplexWaveform(Waveform):
|
|
|
681
743
|
self.start = None
|
|
682
744
|
self.stop = None
|
|
683
745
|
self.sample_rate = None
|
|
684
|
-
self.
|
|
746
|
+
self._filters = None
|
|
685
747
|
self.nonlinear = None
|
|
686
748
|
self.label = None
|
|
687
749
|
|
|
@@ -913,7 +975,7 @@ class ComplexWaveform(Waveform):
|
|
|
913
975
|
|
|
914
976
|
def __getstate__(self):
|
|
915
977
|
return (self.to_bytes(), self.max, self.min, self.start, self.stop,
|
|
916
|
-
self.sample_rate, self.
|
|
978
|
+
self.sample_rate, self._filters, self.nonlinear, self.label)
|
|
917
979
|
|
|
918
980
|
def __setstate__(self, state):
|
|
919
981
|
if len(state) == 8:
|
|
@@ -965,7 +1027,7 @@ class _RealWaveVStackBase(WaveVStack):
|
|
|
965
1027
|
"""Common real-stack type; concrete storage is provided by the C core."""
|
|
966
1028
|
|
|
967
1029
|
__slots__ = (
|
|
968
|
-
"start", "stop", "sample_rate", "offset", "
|
|
1030
|
+
"start", "stop", "sample_rate", "offset", "_filters", "nonlinear", "label",
|
|
969
1031
|
"function_lib", "_sample_plan_cache", "min", "max",
|
|
970
1032
|
)
|
|
971
1033
|
|
|
@@ -975,7 +1037,7 @@ class ComplexWaveVStack(WaveVStack):
|
|
|
975
1037
|
|
|
976
1038
|
__slots__ = (
|
|
977
1039
|
"_real_stack", "_imag_stack", "start", "stop", "sample_rate",
|
|
978
|
-
"offset", "shift", "
|
|
1040
|
+
"offset", "shift", "_filters", "nonlinear", "label", "function_lib",
|
|
979
1041
|
"min", "max",
|
|
980
1042
|
)
|
|
981
1043
|
|
|
@@ -1021,7 +1083,7 @@ class ComplexWaveVStack(WaveVStack):
|
|
|
1021
1083
|
self.start = None
|
|
1022
1084
|
self.stop = None
|
|
1023
1085
|
self.sample_rate = None
|
|
1024
|
-
self.
|
|
1086
|
+
self._filters = None
|
|
1025
1087
|
self.nonlinear = None
|
|
1026
1088
|
self.label = None
|
|
1027
1089
|
self.function_lib = None
|
|
@@ -1177,7 +1239,7 @@ class ComplexWaveVStack(WaveVStack):
|
|
|
1177
1239
|
else:
|
|
1178
1240
|
real, imag = _number_parts(other)
|
|
1179
1241
|
result = ComplexWaveVStack(self.real + real, self.imag + imag)
|
|
1180
|
-
result.filters = self.
|
|
1242
|
+
result.filters = self._filters
|
|
1181
1243
|
result.label = self.label
|
|
1182
1244
|
return result
|
|
1183
1245
|
|
|
@@ -1209,7 +1271,7 @@ class ComplexWaveVStack(WaveVStack):
|
|
|
1209
1271
|
self.real * real - self.imag * imag,
|
|
1210
1272
|
self.real * imag + self.imag * real,
|
|
1211
1273
|
)
|
|
1212
|
-
result.filters = self.
|
|
1274
|
+
result.filters = self._filters
|
|
1213
1275
|
result.label = self.label
|
|
1214
1276
|
return result
|
|
1215
1277
|
|
|
@@ -1265,7 +1327,7 @@ class ComplexWaveVStack(WaveVStack):
|
|
|
1265
1327
|
|
|
1266
1328
|
def __getstate__(self):
|
|
1267
1329
|
return (self.to_bytes(), self.start, self.stop, self.sample_rate,
|
|
1268
|
-
self.
|
|
1330
|
+
self._filters, self.nonlinear, self.label, self.min, self.max)
|
|
1269
1331
|
|
|
1270
1332
|
def __setstate__(self, state):
|
|
1271
1333
|
self.min, self.max = state[7:] if len(state) == 9 else (-inf, inf)
|
|
@@ -1333,7 +1395,7 @@ class RealWaveform(_RealWaveformBase):
|
|
|
1333
1395
|
self.start = None
|
|
1334
1396
|
self.stop = None
|
|
1335
1397
|
self.sample_rate = None
|
|
1336
|
-
self.
|
|
1398
|
+
self._filters = None
|
|
1337
1399
|
self.nonlinear = None
|
|
1338
1400
|
self.label = None
|
|
1339
1401
|
|
|
@@ -1447,8 +1509,8 @@ class RealWaveform(_RealWaveformBase):
|
|
|
1447
1509
|
sample_rate = self.sample_rate
|
|
1448
1510
|
if self.start is None or self.stop is None or sample_rate is None:
|
|
1449
1511
|
raise ValueError("RealWaveform sampling metadata is incomplete")
|
|
1450
|
-
if filters is None
|
|
1451
|
-
|
|
1512
|
+
sos = _prepare_filters(self._filters if filters is None else filters,
|
|
1513
|
+
sample_rate)
|
|
1452
1514
|
if nonlinear is None:
|
|
1453
1515
|
nonlinear = self.nonlinear
|
|
1454
1516
|
plan = _sampling_plan(self.start, self.stop, sample_rate)
|
|
@@ -1459,7 +1521,7 @@ class RealWaveform(_RealWaveformBase):
|
|
|
1459
1521
|
values = self._evaluate_raw(positions)
|
|
1460
1522
|
else:
|
|
1461
1523
|
start_tick, count, step_numerator, step_denominator = plan
|
|
1462
|
-
if
|
|
1524
|
+
if sos is None and nonlinear is None:
|
|
1463
1525
|
values = self._core.sample(
|
|
1464
1526
|
start_tick, count, step_numerator, step_denominator,
|
|
1465
1527
|
self._delay_tick, self._scale, minimum, maximum,
|
|
@@ -1473,7 +1535,7 @@ class RealWaveform(_RealWaveformBase):
|
|
|
1473
1535
|
)
|
|
1474
1536
|
values = _apply_nonlinear(values, nonlinear)
|
|
1475
1537
|
result, _ = _filter_and_finish(
|
|
1476
|
-
values,
|
|
1538
|
+
values, sos, dtype, full_scale, out, minimum, maximum)
|
|
1477
1539
|
return result
|
|
1478
1540
|
|
|
1479
1541
|
def to_bytes(self):
|
|
@@ -1624,7 +1686,7 @@ class RealWaveform(_RealWaveformBase):
|
|
|
1624
1686
|
|
|
1625
1687
|
def __getstate__(self):
|
|
1626
1688
|
return (self.to_bytes(), self.max, self.min, self.start, self.stop,
|
|
1627
|
-
self.sample_rate, self.
|
|
1689
|
+
self.sample_rate, self._filters, self.nonlinear, self.label)
|
|
1628
1690
|
|
|
1629
1691
|
def __setstate__(self, state):
|
|
1630
1692
|
_ensure_c_clock_locked()
|
|
@@ -1716,7 +1778,7 @@ class RealWaveVStack(_RealWaveVStackBase):
|
|
|
1716
1778
|
self.sample_rate = None
|
|
1717
1779
|
self.offset = 0.0
|
|
1718
1780
|
self._shift_tick = 0
|
|
1719
|
-
self.
|
|
1781
|
+
self._filters = None
|
|
1720
1782
|
self.nonlinear = None
|
|
1721
1783
|
self.label = None
|
|
1722
1784
|
self.function_lib = None
|
|
@@ -1831,8 +1893,8 @@ class RealWaveVStack(_RealWaveVStackBase):
|
|
|
1831
1893
|
sample_rate = self.sample_rate
|
|
1832
1894
|
if self.start is None or self.stop is None or sample_rate is None:
|
|
1833
1895
|
raise ValueError("RealWaveVStack sampling metadata is incomplete")
|
|
1834
|
-
if filters is None
|
|
1835
|
-
|
|
1896
|
+
sos = _prepare_filters(self._filters if filters is None else filters,
|
|
1897
|
+
sample_rate)
|
|
1836
1898
|
if nonlinear is None:
|
|
1837
1899
|
nonlinear = self.nonlinear
|
|
1838
1900
|
plan = _sampling_plan(self.start, self.stop, sample_rate)
|
|
@@ -1856,7 +1918,7 @@ class RealWaveVStack(_RealWaveVStackBase):
|
|
|
1856
1918
|
)
|
|
1857
1919
|
self._sample_plan_cache = cache_key, sample_plan
|
|
1858
1920
|
if sample_plan is not None:
|
|
1859
|
-
if
|
|
1921
|
+
if sos is None and nonlinear is None:
|
|
1860
1922
|
values = sample_plan.sample(
|
|
1861
1923
|
self.offset, bits or 0, full_scale,
|
|
1862
1924
|
_real_sampling_output(dtype, bits, out), minimum, maximum,
|
|
@@ -1865,7 +1927,7 @@ class RealWaveVStack(_RealWaveVStackBase):
|
|
|
1865
1927
|
_finish_samples(values, dtype, full_scale, out))
|
|
1866
1928
|
values = sample_plan.sample(self.offset)
|
|
1867
1929
|
else:
|
|
1868
|
-
if
|
|
1930
|
+
if sos is None and nonlinear is None:
|
|
1869
1931
|
values = self._core.sample(
|
|
1870
1932
|
start_tick, count, step_numerator, step_denominator,
|
|
1871
1933
|
self._shift_tick, self.offset, bits or 0, full_scale,
|
|
@@ -1882,7 +1944,7 @@ class RealWaveVStack(_RealWaveVStackBase):
|
|
|
1882
1944
|
)
|
|
1883
1945
|
values = _apply_nonlinear(values, nonlinear)
|
|
1884
1946
|
result, _ = _filter_and_finish(
|
|
1885
|
-
values,
|
|
1947
|
+
values, sos, dtype, full_scale, out, minimum, maximum)
|
|
1886
1948
|
return result
|
|
1887
1949
|
|
|
1888
1950
|
def simplify(self, eps=1e-15):
|
|
@@ -2023,7 +2085,7 @@ class RealWaveVStack(_RealWaveVStackBase):
|
|
|
2023
2085
|
def __getstate__(self):
|
|
2024
2086
|
return (self._core.to_bytes(), self.start, self.stop,
|
|
2025
2087
|
self.sample_rate, self.offset, self._shift_tick,
|
|
2026
|
-
self.
|
|
2088
|
+
self._filters, self.nonlinear, self.label, self.min, self.max)
|
|
2027
2089
|
|
|
2028
2090
|
def __setstate__(self, state):
|
|
2029
2091
|
_ensure_c_clock_locked()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: waveforms
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.7.0
|
|
4
4
|
Summary: Edit waveforms used in experiment
|
|
5
5
|
Author-email: feihoo87 <feihoo87@gmail.com>
|
|
6
6
|
Maintainer-email: feihoo87 <feihoo87@gmail.com>
|
|
@@ -202,14 +202,37 @@ uses the same final limits. The filter state continues from the unclipped
|
|
|
202
202
|
filtered signal across chunks. DAC `full_scale` controls integer conversion
|
|
203
203
|
and saturation separately from these amplitude limits.
|
|
204
204
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
205
|
+
`filters` is a `defaultdict(float)` mapping exponential cascade time constants
|
|
206
|
+
`tau` (in seconds) to amplitudes `amp`, independent of the sampling clock:
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
stack.filters[100e-9] += 0.12
|
|
210
|
+
stack.filters[2e-6] += -0.04
|
|
211
|
+
samples = stack.sample(sample_rate=2_400_000_000)
|
|
212
|
+
unfiltered = stack.sample(filters={}) # bypass predistortion for this call
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The parameters describe cascade stages, not a parallel sum of exponential
|
|
216
|
+
terms. Sampling uses `exp_decay_filter_from_cascade` followed by
|
|
217
|
+
`exp_decay_filter(..., inv=True, output="sos")` at the actual sample rate:
|
|
218
|
+
filtering defaults to **predistortion**. Coefficients are cached by parameter
|
|
219
|
+
values and sample rate; changing an amplitude or the clock takes effect on the
|
|
220
|
+
next sampling call. Only the parameters are saved by pickle, not cached SOS
|
|
221
|
+
coefficients. Assign a mapping to replace them, or `None` to clear them. The old
|
|
222
|
+
`(sos, initial)` representation is no longer accepted.
|
|
223
|
+
|
|
224
|
+
The initial level is the first sample **after nonlinear mapping**, before
|
|
225
|
+
output limiting: the system is assumed to have held that level indefinitely
|
|
226
|
+
before playback. This stage subtracts that baseline, applies the inverse
|
|
227
|
+
filter, restores the baseline, and applies the final limits. Chunked sampling
|
|
228
|
+
retains both the initial level and the unclipped filter state across chunks;
|
|
229
|
+
complex signals use the first I and Q values independently.
|
|
230
|
+
|
|
231
|
+
SOS filtering runs in the C core for float64 and complex128 signals.
|
|
208
232
|
Real int16/int32 output is quantized in the same stage, without allocating a
|
|
209
233
|
full filtered floating-point buffer. Float output can reuse the sampled
|
|
210
234
|
buffer. A small fixed scratch buffer keeps filtering and conversion local;
|
|
211
|
-
waveform evaluation and nonlinear mapping still precede this stage.
|
|
212
|
-
coefficients and extended precision retain the SciPy implementation.
|
|
235
|
+
waveform evaluation and nonlinear mapping still precede this stage.
|
|
213
236
|
`sample_iq()` still converts the filtered complex result into separate I/Q
|
|
214
237
|
output arrays. See [the SOS benchmark](benchmarks/sos_filter.md).
|
|
215
238
|
|
|
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
|
|
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
|