waveforms 3.5.0__tar.gz → 3.5.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 (36) hide show
  1. {waveforms-3.5.0/waveforms.egg-info → waveforms-3.5.1}/PKG-INFO +34 -2
  2. {waveforms-3.5.0 → waveforms-3.5.1}/README.md +33 -1
  3. waveforms-3.5.1/tests/test_native_sos.py +176 -0
  4. waveforms-3.5.1/tests/test_output_limits.py +252 -0
  5. waveforms-3.5.1/waveforms/WaveformLexer.py +134 -0
  6. waveforms-3.5.1/waveforms/WaveformListener.py +228 -0
  7. waveforms-3.5.1/waveforms/WaveformParser.py +1241 -0
  8. {waveforms-3.5.0 → waveforms-3.5.1}/waveforms/_cwaveform.c +181 -18
  9. {waveforms-3.5.0 → waveforms-3.5.1}/waveforms/_cwaveform.h +18 -0
  10. {waveforms-3.5.0 → waveforms-3.5.1}/waveforms/_cwaveform.md +38 -0
  11. {waveforms-3.5.0 → waveforms-3.5.1}/waveforms/_waveform.pyi +8 -1
  12. {waveforms-3.5.0 → waveforms-3.5.1}/waveforms/_waveform.pyx +107 -2
  13. {waveforms-3.5.0 → waveforms-3.5.1}/waveforms/version.py +1 -1
  14. {waveforms-3.5.0 → waveforms-3.5.1}/waveforms/waveform.py +172 -55
  15. {waveforms-3.5.0 → waveforms-3.5.1/waveforms.egg-info}/PKG-INFO +34 -2
  16. {waveforms-3.5.0 → waveforms-3.5.1}/waveforms.egg-info/SOURCES.txt +5 -0
  17. {waveforms-3.5.0 → waveforms-3.5.1}/LICENSE +0 -0
  18. {waveforms-3.5.0 → waveforms-3.5.1}/MANIFEST.in +0 -0
  19. {waveforms-3.5.0 → waveforms-3.5.1}/pyproject.toml +0 -0
  20. {waveforms-3.5.0 → waveforms-3.5.1}/setup.cfg +0 -0
  21. {waveforms-3.5.0 → waveforms-3.5.1}/setup.py +0 -0
  22. {waveforms-3.5.0 → waveforms-3.5.1}/tests/test_core.py +0 -0
  23. {waveforms-3.5.0 → waveforms-3.5.1}/tests/test_nonlinear.py +0 -0
  24. {waveforms-3.5.0 → waveforms-3.5.1}/tests/test_waveform.py +0 -0
  25. {waveforms-3.5.0 → waveforms-3.5.1}/tests/test_wavevstack.py +0 -0
  26. {waveforms-3.5.0 → waveforms-3.5.1}/waveforms/Waveform.g4 +0 -0
  27. {waveforms-3.5.0 → waveforms-3.5.1}/waveforms/__init__.py +0 -0
  28. {waveforms-3.5.0 → waveforms-3.5.1}/waveforms/__main__.py +0 -0
  29. {waveforms-3.5.0 → waveforms-3.5.1}/waveforms/distortion.py +0 -0
  30. {waveforms-3.5.0 → waveforms-3.5.1}/waveforms/nonlinear.py +0 -0
  31. {waveforms-3.5.0 → waveforms-3.5.1}/waveforms/utils.py +0 -0
  32. {waveforms-3.5.0 → waveforms-3.5.1}/waveforms/waveform_parser.py +0 -0
  33. {waveforms-3.5.0 → waveforms-3.5.1}/waveforms.egg-info/dependency_links.txt +0 -0
  34. {waveforms-3.5.0 → waveforms-3.5.1}/waveforms.egg-info/entry_points.txt +0 -0
  35. {waveforms-3.5.0 → waveforms-3.5.1}/waveforms.egg-info/requires.txt +0 -0
  36. {waveforms-3.5.0 → waveforms-3.5.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.0
3
+ Version: 3.5.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>
@@ -180,12 +180,44 @@ flux_samples = trajectory.sample(dtype=np.int16)
180
180
  ```
181
181
 
182
182
  The sampling order is waveform accumulation, nonlinear mapping, optional SOS
183
- filtering/predistortion, and finally integer quantization. This is important for
183
+ filtering/predistortion, output amplitude limits (`min`/`max`), and finally
184
+ integer quantization or floating-point output. This is important for
184
185
  `WaveVStack`: the map is applied to the accumulated trajectory rather than to
185
186
  each pulse event independently. `method="linear"` selects the smaller and
186
187
  fastest two-point interpolation path. Maps serialize independently through
187
188
  `to_bytes()`/`from_bytes()` using the language-neutral `NLM1` format.
188
189
 
190
+ Every waveform and stack has output limits, defaulting to `min=-np.inf` and
191
+ `max=np.inf`. A stack ignores its child waveforms' limits and uses only its own:
192
+
193
+ ```python
194
+ stack.min = -0.2
195
+ stack.max = 0.3
196
+ samples = stack.sample() # limit after the complete calibration/filter chain
197
+ ```
198
+
199
+ Limits apply to whole and chunked sampling, including `out=` buffers and
200
+ integer output. Complex signals limit I and Q independently; `sample_iq()`
201
+ uses the same final limits. The filter state continues from the unclipped
202
+ filtered signal across chunks. DAC `full_scale` controls integer conversion
203
+ and saturation separately from these amplitude limits.
204
+
205
+ Real-coefficient SOS filters (`filters=(sos, initial)`) run in the C core for
206
+ float64 and complex128 signals. This stage subtracts the baseline `initial`,
207
+ applies the cascade, restores the baseline, and applies the final limits.
208
+ Real int16/int32 output is quantized in the same stage, without allocating a
209
+ full filtered floating-point buffer. Float output can reuse the sampled
210
+ buffer. A small fixed scratch buffer keeps filtering and conversion local;
211
+ waveform evaluation and nonlinear mapping still precede this stage. Complex
212
+ coefficients and extended precision retain the SciPy implementation.
213
+ `sample_iq()` still converts the filtered complex result into separate I/Q
214
+ output arrays. See [the SOS benchmark](benchmarks/sos_filter.md).
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.
220
+
189
221
  ## Reporting Issues
190
222
  Please report all issues [on github](https://github.com/feihoo87/waveforms/issues).
191
223
 
@@ -136,12 +136,44 @@ flux_samples = trajectory.sample(dtype=np.int16)
136
136
  ```
137
137
 
138
138
  The sampling order is waveform accumulation, nonlinear mapping, optional SOS
139
- filtering/predistortion, and finally integer quantization. This is important for
139
+ filtering/predistortion, output amplitude limits (`min`/`max`), and finally
140
+ integer quantization or floating-point output. This is important for
140
141
  `WaveVStack`: the map is applied to the accumulated trajectory rather than to
141
142
  each pulse event independently. `method="linear"` selects the smaller and
142
143
  fastest two-point interpolation path. Maps serialize independently through
143
144
  `to_bytes()`/`from_bytes()` using the language-neutral `NLM1` format.
144
145
 
146
+ Every waveform and stack has output limits, defaulting to `min=-np.inf` and
147
+ `max=np.inf`. A stack ignores its child waveforms' limits and uses only its own:
148
+
149
+ ```python
150
+ stack.min = -0.2
151
+ stack.max = 0.3
152
+ samples = stack.sample() # limit after the complete calibration/filter chain
153
+ ```
154
+
155
+ Limits apply to whole and chunked sampling, including `out=` buffers and
156
+ integer output. Complex signals limit I and Q independently; `sample_iq()`
157
+ uses the same final limits. The filter state continues from the unclipped
158
+ filtered signal across chunks. DAC `full_scale` controls integer conversion
159
+ and saturation separately from these amplitude limits.
160
+
161
+ Real-coefficient SOS filters (`filters=(sos, initial)`) run in the C core for
162
+ float64 and complex128 signals. This stage subtracts the baseline `initial`,
163
+ applies the cascade, restores the baseline, and applies the final limits.
164
+ Real int16/int32 output is quantized in the same stage, without allocating a
165
+ full filtered floating-point buffer. Float output can reuse the sampled
166
+ buffer. A small fixed scratch buffer keeps filtering and conversion local;
167
+ waveform evaluation and nonlinear mapping still precede this stage. Complex
168
+ coefficients and extended precision retain the SciPy implementation.
169
+ `sample_iq()` still converts the filtered complex result into separate I/Q
170
+ output arrays. See [the SOS benchmark](benchmarks/sos_filter.md).
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.
176
+
145
177
  ## Reporting Issues
146
178
  Please report all issues [on github](https://github.com/feihoo87/waveforms/issues).
147
179
 
@@ -0,0 +1,176 @@
1
+ """Native SOS processing agrees with SciPy and keeps output limits out of state."""
2
+
3
+ import numpy as np
4
+ import pytest
5
+ from scipy.signal import butter, sosfilt
6
+
7
+ import waveforms as wf
8
+ from waveforms._waveform import quantize_samples, sosfilt_samples
9
+
10
+
11
+ def _clip(values, lower, upper):
12
+ if np.iscomplexobj(values):
13
+ return (np.clip(values.real, lower, upper)
14
+ + 1j * np.clip(values.imag, lower, upper))
15
+ return np.clip(values, lower, upper)
16
+
17
+
18
+ @pytest.mark.parametrize("sections", [1, 2, 4, 8, 16])
19
+ @pytest.mark.parametrize("complex_signal", [False, True])
20
+ def test_scipy_equivalence_and_streaming(sections, complex_signal):
21
+ rng = np.random.default_rng(832)
22
+ sos = butter(2 * sections, .2, output="sos")
23
+ values = rng.normal(size=1031)
24
+ zi = rng.normal(size=(sections, 2)) * .001
25
+ initial = .07
26
+ if complex_signal:
27
+ values = values + 1j * rng.normal(size=len(values))
28
+ zi = zi + 1j * rng.normal(size=zi.shape) * .001
29
+ initial += .04j
30
+ original = values.copy()
31
+ initial_state = zi.copy()
32
+ filtered, expected_state = sosfilt(sos, values - initial, zi=zi)
33
+ expected = _clip(filtered + initial, -.17, .23)
34
+ result, state = sosfilt_samples(values, sos, initial, zi,
35
+ lower=-.17, upper=.23)
36
+ np.testing.assert_allclose(result, expected, rtol=3e-13, atol=3e-14)
37
+ np.testing.assert_allclose(state, expected_state, rtol=3e-13, atol=3e-14)
38
+ np.testing.assert_array_equal(zi, initial_state)
39
+ np.testing.assert_array_equal(values, original)
40
+
41
+ # Boundaries straddle the internal block size and include a one-sample chunk.
42
+ output = np.empty_like(values)
43
+ state = zi
44
+ start = 0
45
+ for stop in (1, 254, 511, 512, 1024, len(values)):
46
+ part, state = sosfilt_samples(values[start:stop], sos, initial, state,
47
+ lower=-.17, upper=.23,
48
+ out=output[start:stop])
49
+ assert np.shares_memory(part, output)
50
+ start = stop
51
+ np.testing.assert_array_equal(output, result)
52
+ np.testing.assert_allclose(state, expected_state, rtol=3e-13, atol=3e-14)
53
+
54
+
55
+ @pytest.mark.parametrize("bits", [16, 32])
56
+ @pytest.mark.parametrize("sections", [1, 4, 8])
57
+ def test_direct_quantization_matches_scipy(bits, sections):
58
+ rng = np.random.default_rng(337)
59
+ values = rng.uniform(-2, 2, 5003)
60
+ sos = butter(sections * 2, .25, output="sos")
61
+ expected = quantize_samples(
62
+ np.clip(sosfilt(sos, values - .1) + .1, -.25, .35), bits, .7)
63
+ output = np.empty_like(expected)
64
+ result, _ = sosfilt_samples(values, sos, .1, bits=bits, full_scale=.7,
65
+ lower=-.25, upper=.35, out=output)
66
+ assert result is output
67
+ np.testing.assert_array_equal(result, expected)
68
+
69
+
70
+ @pytest.mark.parametrize("complex_signal", [False, True])
71
+ @pytest.mark.parametrize("overlap", [False, True])
72
+ def test_aliasing_and_readonly_inputs(complex_signal, overlap):
73
+ rng = np.random.default_rng(4)
74
+ storage = rng.normal(size=601)
75
+ if complex_signal:
76
+ storage = storage + 1j * rng.normal(size=len(storage))
77
+ values = storage[:-1]
78
+ output = storage[1:] if overlap else values
79
+ sos = butter(6, .3, output="sos")
80
+ sos.flags.writeable = False
81
+ # SciPy 1.13 requires a writable coefficient buffer.
82
+ expected = sosfilt(sos.copy(), values.copy())
83
+ result, _ = sosfilt_samples(values, sos, out=output)
84
+ assert result is output
85
+ np.testing.assert_allclose(result, expected, rtol=3e-13, atol=3e-14)
86
+ values.flags.writeable = False
87
+ expected = sosfilt(sos.copy(), values.copy())
88
+ result, _ = sosfilt_samples(values, sos)
89
+ np.testing.assert_allclose(result, expected, rtol=3e-13, atol=3e-14)
90
+
91
+
92
+ def test_coefficients_may_overlap_output():
93
+ sos = butter(8, .3, output="sos")
94
+ values = np.linspace(-1, 1, sos.size)
95
+ expected = sosfilt(sos.copy(), values)
96
+ result, _ = sosfilt_samples(values, sos, out=sos.reshape(-1))
97
+ np.testing.assert_allclose(result, expected, rtol=3e-13, atol=3e-14)
98
+
99
+
100
+ @pytest.mark.parametrize("dtype", [np.float64, np.complex128])
101
+ def test_empty_input_preserves_state(dtype):
102
+ sos = butter(4, .3, output="sos")
103
+ zi = np.ones((2, 2), dtype=dtype)
104
+ result, state = sosfilt_samples(np.empty(0, dtype=dtype), sos, zi=zi)
105
+ assert result.shape == (0,)
106
+ assert result.dtype == dtype
107
+ np.testing.assert_array_equal(state, zi)
108
+
109
+
110
+ @pytest.mark.parametrize("sos", [np.ones((2, 5)), np.ones((1, 2, 6)),
111
+ [[1, 0, 0, 2, 0, 0]]])
112
+ def test_invalid_coefficients(sos):
113
+ with pytest.raises(ValueError, match="sos"):
114
+ sosfilt_samples(np.ones(3), sos)
115
+
116
+
117
+ def test_invalid_state_and_output():
118
+ sos = butter(4, .3, output="sos")
119
+ values = np.ones(10)
120
+ with pytest.raises(ValueError, match="zi"):
121
+ sosfilt_samples(values, sos, zi=np.zeros((1, 2)))
122
+ with pytest.raises(ValueError, match="shape"):
123
+ sosfilt_samples(values, sos, out=np.empty(9))
124
+ with pytest.raises(TypeError, match="dtype"):
125
+ sosfilt_samples(values, sos, bits=16, out=np.empty(10))
126
+ with pytest.raises(ValueError, match="contiguous"):
127
+ sosfilt_samples(values, sos, bits=16, out=np.empty(20, dtype=np.int16)[::2])
128
+ with pytest.raises(TypeError, match="real signal"):
129
+ sosfilt_samples(values + 1j, sos, bits=16)
130
+ with pytest.raises(ValueError, match="full_scale"):
131
+ sosfilt_samples(values, sos, bits=16, full_scale=0)
132
+ with pytest.raises(ValueError, match="min and max"):
133
+ sosfilt_samples(values, sos, lower=np.nan)
134
+ with pytest.raises(ValueError, match="non-finite"):
135
+ sosfilt_samples(np.array([np.nan]), sos, bits=16)
136
+
137
+
138
+ def _wave():
139
+ wave = .4 + .5 * wf.cos(6 * np.pi)
140
+ wave.start, wave.stop, wave.sample_rate = 0., 1., 1024
141
+ wave.min, wave.max = -.15, .27
142
+ return wave
143
+
144
+
145
+ @pytest.mark.parametrize("coefficient_dtype", [np.complex128, np.longdouble])
146
+ def test_scipy_fallback(coefficient_dtype):
147
+ wave = _wave()
148
+ sos = butter(4, .3, output="sos").astype(coefficient_dtype)
149
+ if coefficient_dtype == np.complex128:
150
+ sos[0, 0] += .1j
151
+ wave.filters = sos, .03
152
+ raw = .4 + .5 * np.cos(6 * np.pi * np.arange(1024) / 1024)
153
+ expected = _clip(sosfilt(sos, raw - .03) + .03, wave.min, wave.max)
154
+ np.testing.assert_allclose(wave.sample(), expected, rtol=3e-13, atol=3e-14)
155
+ np.testing.assert_allclose(np.concatenate(list(wave.sample(chunk_size=73))),
156
+ expected, rtol=3e-13, atol=3e-14)
157
+
158
+
159
+ def test_pipeline_output_casting_and_single_section_vector():
160
+ wave = _wave()
161
+ wave.filters = butter(2, .3, output="sos")[0], .03
162
+ expected = wave.sample()
163
+ for dtype in (None, np.float32, np.float64):
164
+ for stride in (1, 2):
165
+ output = np.empty(1024 * stride)[::stride]
166
+ result = wave.sample(dtype=dtype, out=output)
167
+ assert result is output
168
+ np.testing.assert_array_equal(result, expected.astype(dtype or np.float64))
169
+ expected_i16 = quantize_samples(expected, 16)
170
+ np.testing.assert_array_equal(wave.sample(dtype=np.int16), expected_i16)
171
+ np.testing.assert_allclose(np.concatenate(list(wave.sample(chunk_size=31))),
172
+ expected, rtol=3e-13, atol=3e-14)
173
+ with pytest.raises(TypeError, match="dtype"):
174
+ wave.sample(dtype=np.int16, out=np.empty(1024))
175
+ with pytest.raises(ValueError, match="contiguous"):
176
+ wave.sample(out=np.empty(2048, dtype=np.int16)[::2])
@@ -0,0 +1,252 @@
1
+ """Output limits belong after calibration/filtering, before DAC conversion."""
2
+
3
+ import pickle
4
+
5
+ import numpy as np
6
+ import pytest
7
+ from scipy.signal import sosfilt
8
+
9
+ import waveforms as wf
10
+ from waveforms._waveform import quantize_samples
11
+
12
+
13
+ def _configure(wave, rate=128):
14
+ wave.start = 0.0
15
+ wave.stop = 1.0
16
+ wave.sample_rate = rate
17
+ return wave
18
+
19
+
20
+ def _signal(kind):
21
+ real = 0.45 + 0.35 * wf.cos(6 * np.pi)
22
+ imag = -0.3 + 0.2 * wf.sin(4 * np.pi)
23
+ if kind == "real":
24
+ return real
25
+ if kind == "complex":
26
+ # Component limits must not truncate a later calibration/filter input.
27
+ real.min, real.max = -0.01, 0.01
28
+ imag.min, imag.max = -0.01, 0.01
29
+ return wf.ComplexWaveform(real, imag)
30
+ real_children = [0.6 * real, 0.4 * real]
31
+ imag_children = [0.3 * imag, 0.7 * imag]
32
+ for child in real_children + imag_children:
33
+ child.min, child.max = -0.01, 0.01
34
+ real_stack = wf.WaveVStack(real_children)
35
+ if kind == "stack":
36
+ return real_stack
37
+ imag_stack = wf.WaveVStack(imag_children)
38
+ real_stack.min, real_stack.max = -0.02, 0.02
39
+ imag_stack.min, imag_stack.max = -0.02, 0.02
40
+ return wf.ComplexWaveVStack(real_stack, imag_stack)
41
+
42
+
43
+ def _clip(values, lower, upper):
44
+ if np.iscomplexobj(values):
45
+ return (np.clip(values.real, lower, upper)
46
+ + 1j * np.clip(values.imag, lower, upper))
47
+ return np.clip(values, lower, upper)
48
+
49
+
50
+ @pytest.mark.parametrize("kind", ["real", "stack", "complex", "complex_stack"])
51
+ @pytest.mark.parametrize("rate", [128, 127.5])
52
+ @pytest.mark.parametrize("gain", [0.25, 2.0])
53
+ def test_output_limits_follow_mapping_and_filter_with_chunk_state(kind, rate, gain):
54
+ wave = _configure(_signal(kind), rate)
55
+ wave.min, wave.max = -0.22, 0.27
56
+ # Stateful filter exercises overshoot and continuing with the unclipped zf.
57
+ sos = np.array([[gain * 0.5, 0., 0., 1., -0.5, 0.]])
58
+ initial = 0.03
59
+ wave.filters = (sos, initial)
60
+ mapping = wf.NonlinearMap.from_samples(
61
+ [-3., 3.], [-6., 6.], method="linear", table_size=2)
62
+ wave.nonlinear = (mapping, mapping) if "complex" in kind else mapping
63
+
64
+ positions = np.arange(0., 1., 1 / rate)
65
+ raw = 0.45 + 0.35 * np.cos(6 * np.pi * positions)
66
+ if "complex" in kind:
67
+ raw = raw + 1j * (-0.3 + 0.2 * np.sin(4 * np.pi * positions))
68
+ filtered = sosfilt(sos, 2 * raw - initial) + initial
69
+ expected = _clip(filtered, wave.min, wave.max)
70
+
71
+ np.testing.assert_allclose(wave.sample(), expected, rtol=2e-13, atol=2e-14)
72
+ chunks = list(wave.sample(chunk_size=13))
73
+ np.testing.assert_allclose(np.concatenate(chunks), expected,
74
+ rtol=2e-13, atol=2e-14)
75
+ target = np.empty_like(expected)
76
+ assert wave.sample(out=target) is target
77
+ np.testing.assert_allclose(target, expected, rtol=2e-13, atol=2e-14)
78
+ restored = pickle.loads(pickle.dumps(wave))
79
+ np.testing.assert_array_equal(restored.sample(), wave.sample())
80
+
81
+ if "complex" in kind:
82
+ for dtype, bits in ((np.int16, 16), (np.int32, 32)):
83
+ targets = tuple(np.empty(len(expected), dtype=dtype) for _ in range(2))
84
+ actual = wave.sample_iq(dtype=dtype, full_scale=0.7, out=targets)
85
+ for result, output, component in zip(actual, targets,
86
+ (expected.real, expected.imag)):
87
+ assert result is output
88
+ np.testing.assert_array_equal(
89
+ result, quantize_samples(component, bits, 0.7))
90
+ iq_chunks = list(wave.sample_iq(
91
+ dtype=dtype, full_scale=0.7, chunk_size=13, out=targets))
92
+ for index, component in enumerate((expected.real, expected.imag)):
93
+ np.testing.assert_array_equal(
94
+ np.concatenate([chunk[index] for chunk in iq_chunks]),
95
+ quantize_samples(component, bits, 0.7))
96
+ else:
97
+ for dtype, bits in ((np.int16, 16), (np.int32, 32)):
98
+ expected_integer = quantize_samples(expected, bits, 0.7)
99
+ target = np.empty(len(expected), dtype=dtype)
100
+ assert wave.sample(out=target, full_scale=0.7) is target
101
+ np.testing.assert_array_equal(target, expected_integer)
102
+ chunks = wave.sample(out=target, chunk_size=13, full_scale=0.7)
103
+ np.testing.assert_array_equal(np.concatenate(list(chunks)), expected_integer)
104
+
105
+
106
+ @pytest.mark.parametrize("gain,expected", [(2., .2), (.25, .1)])
107
+ def test_filter_gain_never_sees_prematurely_limited_input(gain, expected):
108
+ wave = _configure(wf.const(.4))
109
+ wave.min, wave.max = -.2, .2
110
+ wave.filters = (np.array([[gain, 0., 0., 1., 0., 0.]]), 0.)
111
+ np.testing.assert_allclose(wave.sample(), expected)
112
+ np.testing.assert_allclose(np.concatenate(list(wave.sample(chunk_size=7))), expected)
113
+
114
+
115
+ @pytest.mark.parametrize("kind", ["real", "stack", "complex", "complex_stack"])
116
+ def test_limits_cannot_hide_nonlinear_domain_errors(kind):
117
+ wave = _configure(_signal(kind))
118
+ wave.min, wave.max = -.1, .1
119
+ mapping = wf.NonlinearMap.from_samples([-.2, .2], [-.2, .2], method="linear")
120
+ wave.nonlinear = (mapping, mapping) if "complex" in kind else mapping
121
+ with pytest.raises(ValueError, match="outside its domain"):
122
+ wave.sample()
123
+ with pytest.raises(ValueError, match="outside its domain"):
124
+ list(wave.sample(chunk_size=7))
125
+
126
+
127
+ @pytest.mark.parametrize("rate", [2_400_000_000, 7_000_000_000])
128
+ @pytest.mark.parametrize("spacing", [2e-9, 40e-9])
129
+ @pytest.mark.parametrize("many_scales", [False, True])
130
+ @pytest.mark.parametrize("limits", [(-.18, .26), (.08, .26), (-.3, -.1)])
131
+ def test_stack_native_limits_after_overlap_and_before_quantization(
132
+ rate, spacing, many_scales, limits):
133
+ pulse = wf.gaussian(20e-9)
134
+ pulse.min, pulse.max = -.001, .001
135
+ scales = np.linspace(-.9, .9, 80) if many_scales else np.resize([-.6, .4, .8], 80)
136
+ stack = wf.WaveVStack.from_events(
137
+ [pulse], np.zeros(80, dtype=np.uint32),
138
+ np.arange(80, dtype=np.int64) * wf.time_to_tick(spacing), scales)
139
+ stack.offset = .03
140
+ stack.start, stack.stop = -30e-9, 80 * spacing + 30e-9
141
+ stack.sample_rate = rate
142
+ raw = stack.sample()
143
+ plan = stack._sample_plan_cache[1]
144
+ stack.min, stack.max = limits
145
+ expected = np.clip(raw, *limits)
146
+ np.testing.assert_array_equal(stack.sample(), expected)
147
+ # Limits are execution parameters: changing them must not rebuild the plan.
148
+ assert stack._sample_plan_cache[1] is plan
149
+ np.testing.assert_allclose(np.concatenate(list(stack.sample(chunk_size=137))),
150
+ expected, rtol=2e-13, atol=2e-14)
151
+ for dtype, bits in ((np.int16, 16), (np.int32, 32)):
152
+ expected_integer = quantize_samples(expected, bits, .7)
153
+ out = np.empty(len(raw), dtype=dtype)
154
+ assert stack.sample(dtype=dtype, out=out, full_scale=.7) is out
155
+ np.testing.assert_array_equal(out, expected_integer)
156
+ stack.min, stack.max = -np.inf, np.inf
157
+ np.testing.assert_array_equal(stack.sample(), raw)
158
+
159
+
160
+ @pytest.mark.parametrize("kind", ["stack", "complex_stack"])
161
+ def test_stack_owns_limits_and_preserves_them_in_pickle_and_shift(kind):
162
+ stack = _configure(_signal(kind))
163
+ assert stack.min == -np.inf and stack.max == np.inf
164
+ raw = stack.sample()
165
+ stack.min, stack.max = -.17, .23
166
+ expected = _clip(raw, stack.min, stack.max)
167
+ np.testing.assert_array_equal(stack.sample(), expected)
168
+ shifted = stack >> 0.0
169
+ assert (shifted.min, shifted.max) == (-.17, .23)
170
+ np.testing.assert_array_equal(shifted.sample(), expected)
171
+ restored = pickle.loads(pickle.dumps(stack))
172
+ assert (restored.min, restored.max) == (-.17, .23)
173
+ np.testing.assert_array_equal(restored.sample(), expected)
174
+ # Legacy states omit limits, and the oldest also omit the nonlinear map.
175
+ state = stack.__getstate__()[:-2]
176
+ for legacy in (state, state[:-2] + state[-1:]):
177
+ restored = type(stack).__new__(type(stack))
178
+ restored.__setstate__(legacy)
179
+ assert restored.min == -np.inf and restored.max == np.inf
180
+ np.testing.assert_array_equal(restored.sample(), raw)
181
+
182
+
183
+ @pytest.mark.parametrize("kind", ["stack", "complex_stack"])
184
+ def test_stack_direct_evaluation_limits_only_its_own_contribution(kind):
185
+ stack = _signal(kind)
186
+ positions = np.array([.013, .129, .44])
187
+ raw = stack(positions)
188
+ stack.min, stack.max = -.17, .23
189
+ expected = _clip(raw, stack.min, stack.max)
190
+ output = np.full_like(raw, 3)
191
+ assert stack(positions, out=output, accumulate=True) is output
192
+ np.testing.assert_allclose(output, 3 + expected)
193
+ np.testing.assert_allclose(stack(float(positions[0])), expected[0])
194
+
195
+
196
+ def test_empty_stack_limits_apply_to_idle_and_float_output_buffers():
197
+ stack = _configure(wf.WaveVStack())
198
+ stack.min, stack.max = .13, .27
199
+ for dtype in (np.float32, np.float64, np.int16, np.int32):
200
+ target = np.empty(128, dtype=dtype)
201
+ result = stack.sample(dtype=dtype, out=target)
202
+ assert result is target
203
+ if np.issubdtype(dtype, np.integer):
204
+ expected = quantize_samples(np.full(128, .13), np.iinfo(dtype).bits)
205
+ else:
206
+ expected = np.full(128, .13, dtype=dtype)
207
+ np.testing.assert_array_equal(result, expected)
208
+ stack.stop = stack.start
209
+ assert stack.sample(dtype=np.int16).size == 0
210
+
211
+
212
+ @pytest.mark.parametrize("kind", ["real", "stack", "complex", "complex_stack"])
213
+ @pytest.mark.parametrize("bounds", [
214
+ (1., -1.), (np.nan, 1.), (-1., np.nan), (np.inf, np.inf), (-np.inf, -np.inf),
215
+ ])
216
+ def test_invalid_output_limits_are_rejected(kind, bounds):
217
+ wave = _configure(_signal(kind))
218
+ wave.min, wave.max = bounds
219
+ with pytest.raises(ValueError, match="min and max"):
220
+ wave.sample()
221
+
222
+
223
+ @pytest.mark.parametrize("rate", [2_400_000_000, 7_000_000_000])
224
+ @pytest.mark.parametrize("bits", [16, 32])
225
+ def test_integer_limit_rounding_at_half_codes_and_beyond_full_scale(rate, bits):
226
+ wave = wf.WaveVStack([wf.square(20e-9), -wf.square(20e-9) >> 40e-9])
227
+ wave.start, wave.stop, wave.sample_rate = -30e-9, 70e-9, rate
228
+ raw = wave.sample()
229
+ full_scale = .7
230
+ code = full_scale / (2 ** (bits - 1))
231
+ dtype = np.int16 if bits == 16 else np.int32
232
+ for lower, upper in ((-.5 * code, 2.5 * code), (.8, 1.1), (-1.1, -.8),
233
+ (-np.inf, .2), (-.2, np.inf), (.1, .1)):
234
+ wave.min, wave.max = lower, upper
235
+ np.testing.assert_array_equal(
236
+ wave.sample(dtype=dtype, full_scale=full_scale),
237
+ quantize_samples(np.clip(raw, lower, upper), bits, full_scale))
238
+
239
+
240
+ def test_many_scale_native_limits_span_scratch_blocks_and_partial_placements():
241
+ wave = wf.WaveVStack.from_events(
242
+ [wf.gaussian(300e-9)], np.zeros(80, dtype=np.uint32),
243
+ np.arange(80, dtype=np.int64) * wf.time_to_tick(600e-9),
244
+ np.linspace(-.9, .9, 80))
245
+ wave.start, wave.stop = 47e-9, 79 * 600e-9 + 73e-9
246
+ wave.sample_rate = 2_400_000_000
247
+ raw = wave.sample()
248
+ wave.min, wave.max = -.17, .23
249
+ for dtype, bits in ((np.int16, 16), (np.int32, 32)):
250
+ np.testing.assert_array_equal(
251
+ wave.sample(dtype=dtype, full_scale=.7),
252
+ quantize_samples(np.clip(raw, -.17, .23), bits, .7))
@@ -0,0 +1,134 @@
1
+ # Generated from Waveform.g4 by ANTLR 4.13.2
2
+ from antlr4 import *
3
+ from io import StringIO
4
+ import sys
5
+ if sys.version_info[1] > 5:
6
+ from typing import TextIO
7
+ else:
8
+ from typing.io import TextIO
9
+
10
+
11
+ def serializedATN():
12
+ return [
13
+ 4,0,22,169,6,-1,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,
14
+ 2,6,7,6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,
15
+ 13,7,13,2,14,7,14,2,15,7,15,2,16,7,16,2,17,7,17,2,18,7,18,2,19,7,
16
+ 19,2,20,7,20,2,21,7,21,2,22,7,22,1,0,1,0,1,1,1,1,1,2,1,2,1,3,1,3,
17
+ 1,4,1,4,1,5,1,5,1,6,1,6,1,7,1,7,1,8,1,8,1,9,1,9,1,10,1,10,1,11,1,
18
+ 11,1,11,3,11,73,8,11,1,12,4,12,76,8,12,11,12,12,12,77,1,12,1,12,
19
+ 5,12,82,8,12,10,12,12,12,85,9,12,3,12,87,8,12,1,12,1,12,4,12,91,
20
+ 8,12,11,12,12,12,92,3,12,95,8,12,1,12,1,12,3,12,99,8,12,1,12,4,12,
21
+ 102,8,12,11,12,12,12,103,3,12,106,8,12,1,13,4,13,109,8,13,11,13,
22
+ 12,13,110,1,14,1,14,3,14,115,8,14,1,14,1,14,1,15,1,15,5,15,121,8,
23
+ 15,10,15,12,15,124,9,15,1,15,1,15,1,15,5,15,129,8,15,10,15,12,15,
24
+ 132,9,15,1,15,3,15,135,8,15,1,16,1,16,1,16,1,16,1,16,1,16,3,16,143,
25
+ 8,16,1,17,1,17,5,17,147,8,17,10,17,12,17,150,9,17,1,18,1,18,1,18,
26
+ 1,19,1,19,1,19,1,20,1,20,1,20,1,21,4,21,162,8,21,11,21,12,21,163,
27
+ 1,21,1,21,1,22,1,22,0,0,23,1,1,3,2,5,3,7,4,9,5,11,6,13,7,15,8,17,
28
+ 9,19,10,21,11,23,12,25,13,27,14,29,15,31,16,33,17,35,18,37,19,39,
29
+ 20,41,21,43,22,45,0,1,0,8,2,0,69,69,101,101,2,0,43,43,45,45,3,0,
30
+ 10,10,13,13,34,34,3,0,10,10,13,13,39,39,3,0,65,90,95,95,97,122,4,
31
+ 0,48,57,65,90,95,95,97,122,3,0,9,10,13,13,32,32,1,0,48,57,186,0,
32
+ 1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,9,1,0,0,0,0,11,1,
33
+ 0,0,0,0,13,1,0,0,0,0,15,1,0,0,0,0,17,1,0,0,0,0,19,1,0,0,0,0,21,1,
34
+ 0,0,0,0,23,1,0,0,0,0,25,1,0,0,0,0,27,1,0,0,0,0,29,1,0,0,0,0,31,1,
35
+ 0,0,0,0,33,1,0,0,0,0,35,1,0,0,0,0,37,1,0,0,0,0,39,1,0,0,0,0,41,1,
36
+ 0,0,0,0,43,1,0,0,0,1,47,1,0,0,0,3,49,1,0,0,0,5,51,1,0,0,0,7,53,1,
37
+ 0,0,0,9,55,1,0,0,0,11,57,1,0,0,0,13,59,1,0,0,0,15,61,1,0,0,0,17,
38
+ 63,1,0,0,0,19,65,1,0,0,0,21,67,1,0,0,0,23,72,1,0,0,0,25,94,1,0,0,
39
+ 0,27,108,1,0,0,0,29,114,1,0,0,0,31,134,1,0,0,0,33,142,1,0,0,0,35,
40
+ 144,1,0,0,0,37,151,1,0,0,0,39,154,1,0,0,0,41,157,1,0,0,0,43,161,
41
+ 1,0,0,0,45,167,1,0,0,0,47,48,5,61,0,0,48,2,1,0,0,0,49,50,5,94,0,
42
+ 0,50,4,1,0,0,0,51,52,5,42,0,0,52,6,1,0,0,0,53,54,5,47,0,0,54,8,1,
43
+ 0,0,0,55,56,5,43,0,0,56,10,1,0,0,0,57,58,5,45,0,0,58,12,1,0,0,0,
44
+ 59,60,5,40,0,0,60,14,1,0,0,0,61,62,5,41,0,0,62,16,1,0,0,0,63,64,
45
+ 5,44,0,0,64,18,1,0,0,0,65,66,5,91,0,0,66,20,1,0,0,0,67,68,5,93,0,
46
+ 0,68,22,1,0,0,0,69,73,3,25,12,0,70,73,3,27,13,0,71,73,3,29,14,0,
47
+ 72,69,1,0,0,0,72,70,1,0,0,0,72,71,1,0,0,0,73,24,1,0,0,0,74,76,3,
48
+ 45,22,0,75,74,1,0,0,0,76,77,1,0,0,0,77,75,1,0,0,0,77,78,1,0,0,0,
49
+ 78,86,1,0,0,0,79,83,5,46,0,0,80,82,3,45,22,0,81,80,1,0,0,0,82,85,
50
+ 1,0,0,0,83,81,1,0,0,0,83,84,1,0,0,0,84,87,1,0,0,0,85,83,1,0,0,0,
51
+ 86,79,1,0,0,0,86,87,1,0,0,0,87,95,1,0,0,0,88,90,5,46,0,0,89,91,3,
52
+ 45,22,0,90,89,1,0,0,0,91,92,1,0,0,0,92,90,1,0,0,0,92,93,1,0,0,0,
53
+ 93,95,1,0,0,0,94,75,1,0,0,0,94,88,1,0,0,0,95,105,1,0,0,0,96,98,7,
54
+ 0,0,0,97,99,7,1,0,0,98,97,1,0,0,0,98,99,1,0,0,0,99,101,1,0,0,0,100,
55
+ 102,3,45,22,0,101,100,1,0,0,0,102,103,1,0,0,0,103,101,1,0,0,0,103,
56
+ 104,1,0,0,0,104,106,1,0,0,0,105,96,1,0,0,0,105,106,1,0,0,0,106,26,
57
+ 1,0,0,0,107,109,3,45,22,0,108,107,1,0,0,0,109,110,1,0,0,0,110,108,
58
+ 1,0,0,0,110,111,1,0,0,0,111,28,1,0,0,0,112,115,3,25,12,0,113,115,
59
+ 3,27,13,0,114,112,1,0,0,0,114,113,1,0,0,0,115,116,1,0,0,0,116,117,
60
+ 5,106,0,0,117,30,1,0,0,0,118,122,5,34,0,0,119,121,8,2,0,0,120,119,
61
+ 1,0,0,0,121,124,1,0,0,0,122,120,1,0,0,0,122,123,1,0,0,0,123,125,
62
+ 1,0,0,0,124,122,1,0,0,0,125,135,5,34,0,0,126,130,5,39,0,0,127,129,
63
+ 8,3,0,0,128,127,1,0,0,0,129,132,1,0,0,0,130,128,1,0,0,0,130,131,
64
+ 1,0,0,0,131,133,1,0,0,0,132,130,1,0,0,0,133,135,5,39,0,0,134,118,
65
+ 1,0,0,0,134,126,1,0,0,0,135,32,1,0,0,0,136,137,5,112,0,0,137,143,
66
+ 5,105,0,0,138,143,5,101,0,0,139,140,5,105,0,0,140,141,5,110,0,0,
67
+ 141,143,5,102,0,0,142,136,1,0,0,0,142,138,1,0,0,0,142,139,1,0,0,
68
+ 0,143,34,1,0,0,0,144,148,7,4,0,0,145,147,7,5,0,0,146,145,1,0,0,0,
69
+ 147,150,1,0,0,0,148,146,1,0,0,0,148,149,1,0,0,0,149,36,1,0,0,0,150,
70
+ 148,1,0,0,0,151,152,5,42,0,0,152,153,5,42,0,0,153,38,1,0,0,0,154,
71
+ 155,5,60,0,0,155,156,5,60,0,0,156,40,1,0,0,0,157,158,5,62,0,0,158,
72
+ 159,5,62,0,0,159,42,1,0,0,0,160,162,7,6,0,0,161,160,1,0,0,0,162,
73
+ 163,1,0,0,0,163,161,1,0,0,0,163,164,1,0,0,0,164,165,1,0,0,0,165,
74
+ 166,6,21,0,0,166,44,1,0,0,0,167,168,7,7,0,0,168,46,1,0,0,0,18,0,
75
+ 72,77,83,86,92,94,98,103,105,110,114,122,130,134,142,148,163,1,6,
76
+ 0,0
77
+ ]
78
+
79
+ class WaveformLexer(Lexer):
80
+
81
+ atn = ATNDeserializer().deserialize(serializedATN())
82
+
83
+ decisionsToDFA = [ DFA(ds, i) for i, ds in enumerate(atn.decisionToState) ]
84
+
85
+ T__0 = 1
86
+ T__1 = 2
87
+ T__2 = 3
88
+ T__3 = 4
89
+ T__4 = 5
90
+ T__5 = 6
91
+ T__6 = 7
92
+ T__7 = 8
93
+ T__8 = 9
94
+ T__9 = 10
95
+ T__10 = 11
96
+ NUMBER = 12
97
+ REAL = 13
98
+ INT = 14
99
+ IMAG = 15
100
+ STRING = 16
101
+ CONSTANT = 17
102
+ ID = 18
103
+ POW = 19
104
+ LSHIFT = 20
105
+ RSHIFT = 21
106
+ WS = 22
107
+
108
+ channelNames = [ u"DEFAULT_TOKEN_CHANNEL", u"HIDDEN" ]
109
+
110
+ modeNames = [ "DEFAULT_MODE" ]
111
+
112
+ literalNames = [ "<INVALID>",
113
+ "'='", "'^'", "'*'", "'/'", "'+'", "'-'", "'('", "')'", "','",
114
+ "'['", "']'", "'**'", "'<<'", "'>>'" ]
115
+
116
+ symbolicNames = [ "<INVALID>",
117
+ "NUMBER", "REAL", "INT", "IMAG", "STRING", "CONSTANT", "ID",
118
+ "POW", "LSHIFT", "RSHIFT", "WS" ]
119
+
120
+ ruleNames = [ "T__0", "T__1", "T__2", "T__3", "T__4", "T__5", "T__6",
121
+ "T__7", "T__8", "T__9", "T__10", "NUMBER", "REAL", "INT",
122
+ "IMAG", "STRING", "CONSTANT", "ID", "POW", "LSHIFT", "RSHIFT",
123
+ "WS", "DIGIT" ]
124
+
125
+ grammarFileName = "Waveform.g4"
126
+
127
+ def __init__(self, input=None, output:TextIO = sys.stdout):
128
+ super().__init__(input, output)
129
+ self.checkVersion("4.13.2")
130
+ self._interp = LexerATNSimulator(self, self.atn, self.decisionsToDFA, PredictionContextCache())
131
+ self._actions = None
132
+ self._predicates = None
133
+
134
+