waveforms 3.4.0__tar.gz → 3.5.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.
Files changed (31) hide show
  1. {waveforms-3.4.0/waveforms.egg-info → waveforms-3.5.0}/PKG-INFO +34 -1
  2. {waveforms-3.4.0 → waveforms-3.5.0}/README.md +33 -0
  3. waveforms-3.5.0/tests/test_nonlinear.py +255 -0
  4. {waveforms-3.4.0 → waveforms-3.5.0}/tests/test_waveform.py +1 -1
  5. {waveforms-3.4.0 → waveforms-3.5.0}/waveforms/__init__.py +2 -1
  6. {waveforms-3.4.0 → waveforms-3.5.0}/waveforms/_cwaveform.c +789 -1
  7. {waveforms-3.4.0 → waveforms-3.5.0}/waveforms/_cwaveform.h +58 -1
  8. {waveforms-3.4.0 → waveforms-3.5.0}/waveforms/_cwaveform.md +35 -1
  9. {waveforms-3.4.0 → waveforms-3.5.0}/waveforms/_waveform.pyi +27 -0
  10. {waveforms-3.4.0 → waveforms-3.5.0}/waveforms/_waveform.pyx +207 -0
  11. {waveforms-3.4.0 → waveforms-3.5.0}/waveforms/distortion.py +166 -10
  12. waveforms-3.5.0/waveforms/nonlinear.py +353 -0
  13. {waveforms-3.4.0 → waveforms-3.5.0}/waveforms/version.py +1 -1
  14. {waveforms-3.4.0 → waveforms-3.5.0}/waveforms/waveform.py +117 -38
  15. {waveforms-3.4.0 → waveforms-3.5.0/waveforms.egg-info}/PKG-INFO +34 -1
  16. {waveforms-3.4.0 → waveforms-3.5.0}/waveforms.egg-info/SOURCES.txt +2 -0
  17. {waveforms-3.4.0 → waveforms-3.5.0}/LICENSE +0 -0
  18. {waveforms-3.4.0 → waveforms-3.5.0}/MANIFEST.in +0 -0
  19. {waveforms-3.4.0 → waveforms-3.5.0}/pyproject.toml +0 -0
  20. {waveforms-3.4.0 → waveforms-3.5.0}/setup.cfg +0 -0
  21. {waveforms-3.4.0 → waveforms-3.5.0}/setup.py +0 -0
  22. {waveforms-3.4.0 → waveforms-3.5.0}/tests/test_core.py +0 -0
  23. {waveforms-3.4.0 → waveforms-3.5.0}/tests/test_wavevstack.py +0 -0
  24. {waveforms-3.4.0 → waveforms-3.5.0}/waveforms/Waveform.g4 +0 -0
  25. {waveforms-3.4.0 → waveforms-3.5.0}/waveforms/__main__.py +0 -0
  26. {waveforms-3.4.0 → waveforms-3.5.0}/waveforms/utils.py +0 -0
  27. {waveforms-3.4.0 → waveforms-3.5.0}/waveforms/waveform_parser.py +0 -0
  28. {waveforms-3.4.0 → waveforms-3.5.0}/waveforms.egg-info/dependency_links.txt +0 -0
  29. {waveforms-3.4.0 → waveforms-3.5.0}/waveforms.egg-info/entry_points.txt +0 -0
  30. {waveforms-3.4.0 → waveforms-3.5.0}/waveforms.egg-info/requires.txt +0 -0
  31. {waveforms-3.4.0 → waveforms-3.5.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.4.0
3
+ Version: 3.5.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>
@@ -153,6 +153,39 @@ Real waveforms and stacks therefore avoid complex storage and arithmetic for
153
153
  the common real-valued case. `ComplexWaveform.real` and `.imag` expose the two
154
154
  real channel waveforms.
155
155
 
156
+ ### Post-sampling nonlinear calibration
157
+
158
+ `NonlinearMap` compiles one monotone branch of calibration samples to a compact
159
+ native lookup table. A centered map is useful when a waveform describes a
160
+ frequency excursion around an idle point:
161
+
162
+ ```python
163
+ frequency = np.array([4.0e9, 4.5e9, 5.0e9, 5.5e9, 6.0e9])
164
+ flux = np.array([0.31, 0.22, 0.08, -0.06, -0.16])
165
+
166
+ frequency_to_flux = wf.NonlinearMap.from_samples(
167
+ frequency,
168
+ flux,
169
+ method="monotone_cubic", # PCHIP compiled to uniform cubic segments
170
+ reference=5.0e9, # map(0) == 0 around the idle frequency
171
+ dtype=np.float32,
172
+ extrapolate="error",
173
+ )
174
+
175
+ trajectory.start = 0
176
+ trajectory.stop = 200e-9
177
+ trajectory.sample_rate = 2_400_000_000
178
+ trajectory.nonlinear = frequency_to_flux
179
+ flux_samples = trajectory.sample(dtype=np.int16)
180
+ ```
181
+
182
+ The sampling order is waveform accumulation, nonlinear mapping, optional SOS
183
+ filtering/predistortion, and finally integer quantization. This is important for
184
+ `WaveVStack`: the map is applied to the accumulated trajectory rather than to
185
+ each pulse event independently. `method="linear"` selects the smaller and
186
+ fastest two-point interpolation path. Maps serialize independently through
187
+ `to_bytes()`/`from_bytes()` using the language-neutral `NLM1` format.
188
+
156
189
  ## Reporting Issues
157
190
  Please report all issues [on github](https://github.com/feihoo87/waveforms/issues).
158
191
 
@@ -109,6 +109,39 @@ Real waveforms and stacks therefore avoid complex storage and arithmetic for
109
109
  the common real-valued case. `ComplexWaveform.real` and `.imag` expose the two
110
110
  real channel waveforms.
111
111
 
112
+ ### Post-sampling nonlinear calibration
113
+
114
+ `NonlinearMap` compiles one monotone branch of calibration samples to a compact
115
+ native lookup table. A centered map is useful when a waveform describes a
116
+ frequency excursion around an idle point:
117
+
118
+ ```python
119
+ frequency = np.array([4.0e9, 4.5e9, 5.0e9, 5.5e9, 6.0e9])
120
+ flux = np.array([0.31, 0.22, 0.08, -0.06, -0.16])
121
+
122
+ frequency_to_flux = wf.NonlinearMap.from_samples(
123
+ frequency,
124
+ flux,
125
+ method="monotone_cubic", # PCHIP compiled to uniform cubic segments
126
+ reference=5.0e9, # map(0) == 0 around the idle frequency
127
+ dtype=np.float32,
128
+ extrapolate="error",
129
+ )
130
+
131
+ trajectory.start = 0
132
+ trajectory.stop = 200e-9
133
+ trajectory.sample_rate = 2_400_000_000
134
+ trajectory.nonlinear = frequency_to_flux
135
+ flux_samples = trajectory.sample(dtype=np.int16)
136
+ ```
137
+
138
+ The sampling order is waveform accumulation, nonlinear mapping, optional SOS
139
+ filtering/predistortion, and finally integer quantization. This is important for
140
+ `WaveVStack`: the map is applied to the accumulated trajectory rather than to
141
+ each pulse event independently. `method="linear"` selects the smaller and
142
+ fastest two-point interpolation path. Maps serialize independently through
143
+ `to_bytes()`/`from_bytes()` using the language-neutral `NLM1` format.
144
+
112
145
  ## Reporting Issues
113
146
  Please report all issues [on github](https://github.com/feihoo87/waveforms/issues).
114
147
 
@@ -0,0 +1,255 @@
1
+ import pickle
2
+
3
+ import numpy as np
4
+ import pytest
5
+ from scipy.interpolate import PchipInterpolator
6
+ from scipy.signal import butter, sosfilt, tf2sos
7
+
8
+ import waveforms as wf
9
+ from waveforms._waveform import quantize_samples
10
+
11
+
12
+ def test_linear_map_binary_roundtrip_clip_and_output():
13
+ x = np.linspace(-2.0, 2.0, 9)
14
+ y = 0.5 + 1.75 * x
15
+ mapping = wf.NonlinearMap.from_samples(
16
+ x, y, method="linear", table_size=9,
17
+ )
18
+ points = np.linspace(-2.0, 2.0, 1001)
19
+ expected = 0.5 + 1.75 * points
20
+ assert np.allclose(mapping(points), expected, rtol=0, atol=2e-15)
21
+
22
+ target = np.empty_like(points)
23
+ assert mapping(points, out=target) is target
24
+ assert np.array_equal(target, mapping(points))
25
+ assert mapping(0.25) == pytest.approx(0.9375)
26
+ assert mapping.method == "linear"
27
+ assert mapping.dtype == np.dtype(np.float64)
28
+ assert mapping.extrapolate == "error"
29
+ assert mapping.point_count == 9
30
+ assert mapping.domain == (-2.0, 2.0)
31
+
32
+ data = mapping.to_bytes()
33
+ assert data[:4] == b"NLM1"
34
+ restored = wf.NonlinearMap.from_bytes(data)
35
+ assert restored.to_bytes() is data
36
+ assert restored == mapping
37
+ assert hash(restored) == hash(mapping)
38
+ assert pickle.loads(pickle.dumps(mapping)) == mapping
39
+ assert np.array_equal(
40
+ mapping._apply_quantized(points, 16),
41
+ quantize_samples(mapping(points), 16),
42
+ )
43
+
44
+ malformed = bytearray(data)
45
+ malformed[9] = 1
46
+ with pytest.raises(ValueError, match="NLM1"):
47
+ wf.NonlinearMap.from_bytes(malformed)
48
+ with pytest.raises(ValueError, match="NLM1"):
49
+ wf.NonlinearMap.from_bytes(data[:-1])
50
+
51
+ with pytest.raises(ValueError, match="outside its domain"):
52
+ mapping(np.array([-2.1, 0.0]))
53
+ clipped = wf.NonlinearMap.from_samples(
54
+ x, y, method="linear", table_size=9, extrapolate="clip",
55
+ )
56
+ assert np.array_equal(clipped([-3.0, 3.0]), [y[0], y[-1]])
57
+
58
+
59
+ def test_monotone_cubic_accuracy_storage_and_centering():
60
+ x = np.array([4.0, 4.15, 4.4, 4.9, 5.6, 6.0])
61
+ y = np.array([0.31, 0.27, 0.18, 0.02, -0.11, -0.16])
62
+ reference = 4.9
63
+ mapping = wf.NonlinearMap.from_samples(
64
+ x, y, table_size=1025, reference=reference,
65
+ )
66
+ relative = np.linspace(mapping.domain[0], mapping.domain[1], 10001)
67
+ reference_curve = PchipInterpolator(x, y)
68
+ expected = reference_curve(reference + relative) - reference_curve(reference)
69
+ assert np.max(np.abs(mapping(relative) - expected)) < 2e-7
70
+ assert mapping(0.0) == pytest.approx(0.0, abs=2e-15)
71
+ assert mapping.method == "monotone_cubic"
72
+ assert mapping.reference_input == reference
73
+ assert mapping.reference_output == pytest.approx(
74
+ reference_curve(reference), abs=2e-7)
75
+ assert np.all(np.diff(mapping(relative)) <= 0)
76
+
77
+ compact = wf.NonlinearMap.from_samples(
78
+ x, y, table_size=1025, reference=reference, dtype=np.float32,
79
+ )
80
+ assert compact.dtype == np.dtype(np.float32)
81
+ assert compact(0.0) == pytest.approx(0.0, abs=2e-7)
82
+ assert len(compact.to_bytes()) == 48 + 16 * (compact.point_count - 1)
83
+ assert len(mapping.to_bytes()) == 48 + 32 * (mapping.point_count - 1)
84
+ assert np.max(np.abs(compact(relative) - expected)) < 3e-7
85
+
86
+
87
+ @pytest.mark.parametrize("x,y,message", [
88
+ ([0, 0, 1], [0, 1, 2], "increase strictly"),
89
+ ([0, 1, 2], [0, 2, 1], "one inverse branch"),
90
+ ([0, 1], [0, np.nan], "finite"),
91
+ ])
92
+ def test_map_validation(x, y, message):
93
+ with pytest.raises(ValueError, match=message):
94
+ wf.NonlinearMap.from_samples(x, y)
95
+ with pytest.raises(ValueError, match="reference"):
96
+ wf.NonlinearMap.from_samples([0, 1], [0, 1], reference=2)
97
+ with pytest.raises(TypeError, match="float32 or float64"):
98
+ wf.NonlinearMap.from_samples([0, 1], [0, 1], dtype=np.int16)
99
+ with pytest.raises(ValueError, match="method"):
100
+ wf.NonlinearMap.from_samples([0, 1], [0, 1], method="bezier")
101
+ with pytest.raises(ValueError, match="NLM1"):
102
+ wf.NonlinearMap.from_bytes(b"not a map")
103
+
104
+
105
+ def test_error_controlled_table_compilation():
106
+ x = np.array([0.0, 0.13, 0.41, 1.0])
107
+ y = np.array([0.0, 0.2, 0.75, 1.0])
108
+ mapping = wf.NonlinearMap.from_samples(
109
+ x, y, method="linear", table_size=5,
110
+ max_error=1e-3, max_table_size=4097,
111
+ )
112
+ assert mapping.point_count > 5
113
+ points = np.linspace(0.0, 1.0, 100_001)
114
+ assert np.max(np.abs(mapping(points) - np.interp(points, x, y))) < 1e-3
115
+
116
+ with pytest.raises(ValueError, match="requires more than"):
117
+ wf.NonlinearMap.from_samples(
118
+ x, y, method="linear", table_size=5,
119
+ max_error=1e-6, max_table_size=9,
120
+ )
121
+ with pytest.raises(ValueError, match="max_error"):
122
+ wf.NonlinearMap.from_samples(x, y, max_error=0)
123
+
124
+
125
+ def test_waveform_sampling_order_chunking_quantization_and_pickle():
126
+ sample_rate = 1024
127
+ mapping = wf.NonlinearMap.from_samples(
128
+ [0.0, 0.25, 0.5, 0.75, 1.0],
129
+ [0.0, 0.0625, 0.25, 0.5625, 1.0],
130
+ table_size=257,
131
+ )
132
+ b, a = butter(3, 40.0, "lowpass", fs=sample_rate)
133
+ sos = tf2sos(b, a)
134
+ waveform = wf.t()
135
+ waveform.start = 0.0
136
+ waveform.stop = 1.0
137
+ waveform.sample_rate = sample_rate
138
+ waveform.nonlinear = mapping
139
+ waveform.filters = (sos, 0.0)
140
+
141
+ raw = np.arange(sample_rate, dtype=np.float64) / sample_rate
142
+ expected = sosfilt(sos, mapping(raw))
143
+ actual = waveform.sample()
144
+ assert np.allclose(actual, expected, rtol=3e-14, atol=3e-14)
145
+ chunks = np.concatenate(list(waveform.sample(chunk_size=73)))
146
+ assert np.allclose(chunks, expected, rtol=3e-14, atol=3e-14)
147
+
148
+ expected_int16 = quantize_samples(expected, 16)
149
+ output = np.empty(sample_rate, dtype=np.int16)
150
+ assert waveform.sample(dtype=np.int16, out=output) is output
151
+ assert np.array_equal(output, expected_int16)
152
+
153
+ restored = pickle.loads(pickle.dumps(waveform))
154
+ assert restored.nonlinear == mapping
155
+ assert np.array_equal(restored.sample(), actual)
156
+
157
+
158
+ def test_stack_mapping_happens_after_event_accumulation():
159
+ pulse = 0.4 * wf.square(1.0)
160
+ stack = wf.WaveVStack([pulse, pulse])
161
+ stack.start = -0.25
162
+ stack.stop = 0.25
163
+ stack.sample_rate = 1000
164
+ stack.nonlinear = wf.NonlinearMap.from_samples(
165
+ [0.0, 0.4, 0.8], [0.0, 0.16, 0.64], table_size=257,
166
+ )
167
+ samples = stack.sample()
168
+ assert np.allclose(samples, 0.64, atol=2e-14)
169
+ assert not np.allclose(samples, 0.32)
170
+
171
+
172
+ def test_complex_waveform_uses_explicit_component_maps():
173
+ real_map = wf.NonlinearMap.from_samples(
174
+ [0.0, 0.5, 1.0], [0.0, 0.25, 1.0], table_size=257,
175
+ )
176
+ imag_map = wf.NonlinearMap.from_samples(
177
+ [0.0, 0.5, 1.0], [0.0, 1.0, 2.0],
178
+ method="linear", table_size=257,
179
+ )
180
+ waveform = wf.ComplexWaveform(0.5, 0.25)
181
+ waveform.start = 0.0
182
+ waveform.stop = 1.0
183
+ waveform.sample_rate = 16
184
+ waveform.nonlinear = (real_map, imag_map)
185
+ expected = real_map(0.5) + 1j * imag_map(0.25)
186
+ assert np.allclose(waveform.sample(), expected)
187
+ i, q = waveform.sample_iq(dtype=np.int16)
188
+ assert np.array_equal(i, quantize_samples(
189
+ np.full(16, expected.real), 16))
190
+ assert np.array_equal(q, quantize_samples(
191
+ np.full(16, expected.imag), 16))
192
+
193
+ waveform.nonlinear = real_map
194
+ with pytest.raises(TypeError, match="complex waveforms require"):
195
+ waveform.sample()
196
+
197
+
198
+ @pytest.mark.parametrize("method", ["linear", "monotone_cubic"])
199
+ @pytest.mark.parametrize("storage", [np.float32, np.float64])
200
+ def test_simd_batches_match_scalar_edges_aliasing_and_quantization(
201
+ method, storage):
202
+ mapping = wf.NonlinearMap.from_samples(
203
+ [-1.0, -0.6, -0.1, 0.35, 1.0],
204
+ [-0.8, -0.5, 0.05, 0.4, 0.9],
205
+ method=method, table_size=257, dtype=storage, extrapolate="clip",
206
+ )
207
+ source = np.linspace(-1.2, 1.2, 65)
208
+ source[0] = -1.0
209
+ source[-1] = 1.0
210
+ for count in (1, 7, 8, 9, 15, 16, 17, 31, 32, 33, 65):
211
+ values = source[:count].copy()
212
+ expected = np.array([mapping(float(value)) for value in values])
213
+ actual = mapping(values)
214
+ assert np.allclose(actual, expected, rtol=2e-15, atol=2e-15)
215
+
216
+ in_place = values.copy()
217
+ assert mapping(in_place, out=in_place) is in_place
218
+ assert np.allclose(in_place, expected, rtol=2e-15, atol=2e-15)
219
+
220
+ assert np.array_equal(
221
+ mapping._apply_quantized(values, 16),
222
+ quantize_samples(expected, 16),
223
+ )
224
+ assert np.array_equal(
225
+ mapping._apply_quantized(values, 32),
226
+ quantize_samples(expected, 32),
227
+ )
228
+
229
+
230
+ def test_simd_batches_preserve_error_extrapolation_and_nonfinite_checks():
231
+ mapping = wf.NonlinearMap.from_samples(
232
+ [-1.0, 0.0, 1.0], [-0.5, 0.0, 0.5], table_size=257,
233
+ )
234
+ for bad_value in (-1.01, 1.01, np.nan, np.inf, -np.inf):
235
+ values = np.linspace(-0.9, 0.9, 32)
236
+ values[19] = bad_value
237
+ with pytest.raises(ValueError, match="outside its domain"):
238
+ mapping(values)
239
+
240
+
241
+ def test_simd_quantized_pipeline_spans_multiple_cache_blocks():
242
+ mapping = wf.NonlinearMap.from_samples(
243
+ [-1.0, -0.25, 0.3, 1.0], [-0.9, -0.2, 0.4, 0.95],
244
+ table_size=1025,
245
+ )
246
+ values = 0.99 * np.sin(np.linspace(-70.0, 70.0, 12_345))
247
+ mapped = mapping(values)
248
+ assert np.array_equal(
249
+ mapping._apply_quantized(values, 16),
250
+ quantize_samples(mapped, 16),
251
+ )
252
+ assert np.array_equal(
253
+ mapping._apply_quantized(values, 32),
254
+ quantize_samples(mapped, 32),
255
+ )
@@ -15,7 +15,7 @@ from waveforms._waveform import (
15
15
 
16
16
 
17
17
  PUBLIC_NAMES = {
18
- "ComplexWaveform", "ComplexWaveVStack", "D", "RealWaveform",
18
+ "ComplexWaveform", "ComplexWaveVStack", "D", "NonlinearMap", "RealWaveform",
19
19
  "RealWaveVStack", "Waveform", "WaveVStack",
20
20
  "chirp", "const", "cos", "cosh",
21
21
  "coshPulse", "cosPulse", "cut", "drag", "drag_sin", "drag_sinx",
@@ -1,6 +1,7 @@
1
1
  from numpy import e, pi
2
2
 
3
3
  from .version import __version__
4
+ from .nonlinear import NonlinearMap
4
5
  from .waveform import (
5
6
  ComplexWaveform,
6
7
  ComplexWaveVStack,
@@ -51,7 +52,7 @@ from .waveform import (
51
52
  )
52
53
 
53
54
  __all__ = [
54
- "ComplexWaveform", "ComplexWaveVStack", "D", "RealWaveform",
55
+ "ComplexWaveform", "ComplexWaveVStack", "D", "NonlinearMap", "RealWaveform",
55
56
  "RealWaveVStack", "Waveform", "WaveVStack",
56
57
  "chirp", "const", "cos", "cosh",
57
58
  "coshPulse", "cosPulse", "cut", "drag", "drag_sin", "drag_sinx",