waveforms 3.0.0__tar.gz → 3.2.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 (38) hide show
  1. {waveforms-3.0.0 → waveforms-3.2.0}/MANIFEST.in +4 -0
  2. {waveforms-3.0.0 → waveforms-3.2.0}/PKG-INFO +29 -3
  3. {waveforms-3.0.0 → waveforms-3.2.0}/README.md +28 -2
  4. {waveforms-3.0.0 → waveforms-3.2.0}/pyproject.toml +9 -1
  5. {waveforms-3.0.0 → waveforms-3.2.0}/setup.py +13 -2
  6. waveforms-3.2.0/src/waveform.c +248 -0
  7. waveforms-3.2.0/tests/test_native.py +242 -0
  8. waveforms-3.2.0/tests/test_waveform.py +621 -0
  9. {waveforms-3.0.0 → waveforms-3.2.0}/waveforms/__init__.py +22 -2
  10. waveforms-3.2.0/waveforms/_cwaveform.c +2034 -0
  11. waveforms-3.2.0/waveforms/_cwaveform.h +129 -0
  12. waveforms-3.2.0/waveforms/_cwaveform.md +74 -0
  13. waveforms-3.2.0/waveforms/_waveform.pyi +171 -0
  14. waveforms-3.2.0/waveforms/_waveform.pyx +2823 -0
  15. {waveforms-3.0.0 → waveforms-3.2.0}/waveforms/distortion.py +2 -2
  16. {waveforms-3.0.0 → waveforms-3.2.0}/waveforms/version.py +1 -1
  17. waveforms-3.2.0/waveforms/waveform.py +2581 -0
  18. {waveforms-3.0.0 → waveforms-3.2.0}/waveforms/waveform_parser.py +1 -1
  19. {waveforms-3.0.0 → waveforms-3.2.0}/waveforms.egg-info/PKG-INFO +29 -3
  20. {waveforms-3.0.0 → waveforms-3.2.0}/waveforms.egg-info/SOURCES.txt +5 -3
  21. waveforms-3.0.0/tests/test_waveform.py +0 -254
  22. waveforms-3.0.0/waveforms/WaveformLexer.py +0 -134
  23. waveforms-3.0.0/waveforms/WaveformListener.py +0 -228
  24. waveforms-3.0.0/waveforms/WaveformParser.py +0 -1241
  25. waveforms-3.0.0/waveforms/_waveform.pyi +0 -73
  26. waveforms-3.0.0/waveforms/_waveform.pyx +0 -1354
  27. waveforms-3.0.0/waveforms/waveform.py +0 -950
  28. {waveforms-3.0.0 → waveforms-3.2.0}/LICENSE +0 -0
  29. {waveforms-3.0.0 → waveforms-3.2.0}/setup.cfg +0 -0
  30. {waveforms-3.0.0 → waveforms-3.2.0}/src/waveform.h +0 -0
  31. {waveforms-3.0.0 → waveforms-3.2.0}/tests/test_wavevstack.py +0 -0
  32. {waveforms-3.0.0 → waveforms-3.2.0}/waveforms/Waveform.g4 +0 -0
  33. {waveforms-3.0.0 → waveforms-3.2.0}/waveforms/__main__.py +0 -0
  34. {waveforms-3.0.0 → waveforms-3.2.0}/waveforms/utils.py +0 -0
  35. {waveforms-3.0.0 → waveforms-3.2.0}/waveforms.egg-info/dependency_links.txt +0 -0
  36. {waveforms-3.0.0 → waveforms-3.2.0}/waveforms.egg-info/entry_points.txt +0 -0
  37. {waveforms-3.0.0 → waveforms-3.2.0}/waveforms.egg-info/requires.txt +0 -0
  38. {waveforms-3.0.0 → waveforms-3.2.0}/waveforms.egg-info/top_level.txt +0 -0
@@ -1,4 +1,8 @@
1
1
  include src/*.h
2
+ include src/*.c
3
+ include waveforms/_cwaveform.h
4
+ include waveforms/_cwaveform.c
5
+ include waveforms/_cwaveform.md
2
6
  include waveforms/_waveform.pyx
3
7
  include waveforms/_waveform.pyi
4
8
  include waveforms/Waveform.g4
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: waveforms
3
- Version: 3.0.0
3
+ Version: 3.2.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>
@@ -107,8 +107,9 @@ serialization.
107
107
  ```python
108
108
  import waveforms as wf
109
109
 
110
- # Optional; call this once, before constructing or loading waveform objects.
111
- wf.set_time_resolution(1e-12)
110
+ # The default tick is already the period of 120 GHz (1 / 120e9 seconds).
111
+ # To use another global tick, override it before constructing/loading waveforms:
112
+ # wf.set_time_resolution(1e-12)
112
113
 
113
114
  pulse = (wf.gaussian(12e-9) >> 20e-9) * wf.cos(2 * wf.pi * 5e9)
114
115
  data = pulse.to_bytes()
@@ -120,6 +121,31 @@ process loading a block must therefore use the same resolution as the process
120
121
  that created it. The setting is locked when the first waveform object is
121
122
  created or loaded.
122
123
 
124
+ `sample()` has integer-grid fast paths for 500 MHz, 1 GHz, 1.2 GHz, 2 GHz,
125
+ 2.4 GHz, 2.5 GHz, 4 GHz, 6 GHz, 8 GHz, and 10 GHz. Real waveforms can be
126
+ quantized directly to signed DAC buffers:
127
+
128
+ ```python
129
+ pulse.start = 0
130
+ pulse.stop = 100e-9
131
+ dac16 = pulse.sample(2_400_000_000, dtype=np.int16, full_scale=1.0)
132
+ ```
133
+
134
+ The packed core stores and evaluates real-valued signals only. Complex signals
135
+ are represented in Python as independent real and imaginary channels:
136
+
137
+ ```python
138
+ z = (1 + 0.25j) * wf.gaussian(12e-9)
139
+ assert isinstance(z, wf.ComplexWaveform)
140
+
141
+ stack = wf.ComplexWaveVStack([z, z >> 20e-9])
142
+ samples = stack(t) # complex NumPy array
143
+ ```
144
+
145
+ `Waveform` and `WaveVStack` therefore avoid complex storage and arithmetic for
146
+ the common real-valued case. `ComplexWaveform.real` and `.imag` expose the two
147
+ real channel waveforms.
148
+
123
149
  ## Reporting Issues
124
150
  Please report all issues [on github](https://github.com/feihoo87/waveforms/issues).
125
151
 
@@ -63,8 +63,9 @@ serialization.
63
63
  ```python
64
64
  import waveforms as wf
65
65
 
66
- # Optional; call this once, before constructing or loading waveform objects.
67
- wf.set_time_resolution(1e-12)
66
+ # The default tick is already the period of 120 GHz (1 / 120e9 seconds).
67
+ # To use another global tick, override it before constructing/loading waveforms:
68
+ # wf.set_time_resolution(1e-12)
68
69
 
69
70
  pulse = (wf.gaussian(12e-9) >> 20e-9) * wf.cos(2 * wf.pi * 5e9)
70
71
  data = pulse.to_bytes()
@@ -76,6 +77,31 @@ process loading a block must therefore use the same resolution as the process
76
77
  that created it. The setting is locked when the first waveform object is
77
78
  created or loaded.
78
79
 
80
+ `sample()` has integer-grid fast paths for 500 MHz, 1 GHz, 1.2 GHz, 2 GHz,
81
+ 2.4 GHz, 2.5 GHz, 4 GHz, 6 GHz, 8 GHz, and 10 GHz. Real waveforms can be
82
+ quantized directly to signed DAC buffers:
83
+
84
+ ```python
85
+ pulse.start = 0
86
+ pulse.stop = 100e-9
87
+ dac16 = pulse.sample(2_400_000_000, dtype=np.int16, full_scale=1.0)
88
+ ```
89
+
90
+ The packed core stores and evaluates real-valued signals only. Complex signals
91
+ are represented in Python as independent real and imaginary channels:
92
+
93
+ ```python
94
+ z = (1 + 0.25j) * wf.gaussian(12e-9)
95
+ assert isinstance(z, wf.ComplexWaveform)
96
+
97
+ stack = wf.ComplexWaveVStack([z, z >> 20e-9])
98
+ samples = stack(t) # complex NumPy array
99
+ ```
100
+
101
+ `Waveform` and `WaveVStack` therefore avoid complex storage and arithmetic for
102
+ the common real-valued case. `ComplexWaveform.real` and `.imag` expose the two
103
+ real channel waveforms.
104
+
79
105
  ## Reporting Issues
80
106
  Please report all issues [on github](https://github.com/feihoo87/waveforms/issues).
81
107
 
@@ -71,7 +71,15 @@ license-files = ["LICENSE"]
71
71
  include-package-data = true
72
72
 
73
73
  [tool.setuptools.package-data]
74
- waveforms = ["WaveformLexer.py", "WaveformParser.py", "WaveformListener.py", "*.pyi"]
74
+ waveforms = [
75
+ "WaveformLexer.py",
76
+ "WaveformParser.py",
77
+ "WaveformListener.py",
78
+ "*.pyi",
79
+ "_cwaveform.h",
80
+ "_cwaveform.c",
81
+ "_cwaveform.md",
82
+ ]
75
83
 
76
84
  [tool.setuptools.dynamic]
77
85
  version = {attr = "waveforms.version.__version__"}
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import sys
2
3
 
3
4
  from Cython.Build import cythonize
4
5
  from setuptools import Extension, find_packages, setup
@@ -35,9 +36,19 @@ def get_extensions():
35
36
  for dirpath, dirnames, filenames in os.walk('waveforms'):
36
37
  for filename in filenames:
37
38
  if filename.endswith('.pyx'):
39
+ sources = [os.path.join(dirpath, filename)]
40
+ include_dirs = []
41
+ extra_link_args = []
42
+ if filename == '_waveform.pyx':
43
+ sources.append(os.path.join(
44
+ 'waveforms', '_cwaveform.c'))
45
+ include_dirs.append('waveforms')
46
+ if sys.platform == 'darwin':
47
+ extra_link_args.extend(['-framework', 'Accelerate'])
38
48
  extensions.append(
39
- Extension(module_name(dirpath, filename),
40
- [os.path.join(dirpath, filename)]))
49
+ Extension(module_name(dirpath, filename), sources,
50
+ include_dirs=include_dirs,
51
+ extra_link_args=extra_link_args))
41
52
 
42
53
  return extensions
43
54
 
@@ -0,0 +1,248 @@
1
+ #include <Python.h>
2
+ #include <stdlib.h>
3
+ #include <math.h>
4
+ #include <stdint.h>
5
+ #include "waveform.h"
6
+
7
+ #define FUNC_TAB_SIZE 128
8
+
9
+ static size_t wave_func_count = 0;
10
+ FuncPtr wave_function_table[FUNC_TAB_SIZE];
11
+
12
+ static inline size_t bisect_left(const void *array, const void *const value,
13
+ size_t lo, size_t hi,
14
+ size_t type_size, int (*cmp)(const void *, const void *))
15
+ {
16
+ while (lo < hi)
17
+ {
18
+ size_t mid = (lo + hi) / 2;
19
+ void *p = (uint8_t *)array + mid * type_size;
20
+ if (cmp(p, value) < 0)
21
+ {
22
+ lo = mid + 1;
23
+ }
24
+ else
25
+ {
26
+ hi = mid;
27
+ }
28
+ }
29
+ return lo;
30
+ }
31
+
32
+ static int cmp_basic_fun(uint32_t func, const Wave1 *a, const Wave1 *b)
33
+ {
34
+ return 1;
35
+ }
36
+
37
+ static int cmp_wav1(const void *a, const void *b)
38
+ {
39
+ Wave1 *wa = (Wave1 *)a;
40
+ Wave1 *wb = (Wave1 *)b;
41
+ if (wa->func > wb->func)
42
+ {
43
+ return 1;
44
+ }
45
+ else if (wa->func < wb->func)
46
+ {
47
+ return -1;
48
+ }
49
+ return cmp_basic_fun(wa->func, wa, wb);
50
+ }
51
+
52
+ static int cmp_wav2(const void *a, const void *b)
53
+ {
54
+ Wave2 *wa = (Wave2 *)a;
55
+ Wave2 *wb = (Wave2 *)b;
56
+ for (size_t i = 0; i < wa->size && i < wb->size; i++)
57
+ {
58
+ if (wa->waves[i].func > wb->waves[i].func)
59
+ {
60
+ return 1;
61
+ }
62
+ else if (wa->waves[i].func < wb->waves[i].func)
63
+ {
64
+ return -1;
65
+ }
66
+ }
67
+ return wa->size > wb->size ? 1 : wa->size < wb->size ? -1
68
+ : 0;
69
+ }
70
+
71
+ static int cmp_wav3(const void *a, const void *b)
72
+ {
73
+ Wave3 *wa = (Wave3 *)a;
74
+ Wave3 *wb = (Wave3 *)b;
75
+ return wa->upper_bound > wb->upper_bound ? 1 : wa->upper_bound < wb->upper_bound ? -1
76
+ : 0;
77
+ }
78
+
79
+ static double wave_basic_func_linear(double t, size_t argc, const Number *argv)
80
+ {
81
+ return t;
82
+ }
83
+
84
+ static double wave_basic_func_gaussian(double t, size_t argc, const Number *argv)
85
+ {
86
+ double x = t / argv[0].real;
87
+ return exp(-x * x);
88
+ }
89
+
90
+ static double wave_basic_func_erf(double t, size_t argc, const Number *argv)
91
+ {
92
+ double x = t / argv[0].real;
93
+ return erf(x);
94
+ }
95
+
96
+ static double wave_basic_func_cos(double t, size_t argc, const Number *argv)
97
+ {
98
+ double x = t * argv[0].real;
99
+ return cos(x);
100
+ }
101
+
102
+ static double wave_basic_func_sinc(double t, size_t argc, const Number *argv)
103
+ {
104
+ double x = t * argv[0].real;
105
+ if (x == 0.0)
106
+ {
107
+ return 0.0;
108
+ }
109
+ return sin(x) / x;
110
+ }
111
+
112
+ static double wave_basic_func_exp(double t, size_t argc, const Number *argv)
113
+ {
114
+ double x = t * argv[0].real;
115
+ return exp(x);
116
+ }
117
+
118
+ static double wave_apply_wave1(Time t, const Wave1 *wave)
119
+ {
120
+ double x = time_2_double(t - wave->shift);
121
+ double y = wave_function_table[wave->func](x, wave->argc, wave->argv);
122
+ if (wave->n != 1 || wave->d != 1)
123
+ {
124
+ y = pow(y, (double)wave->n / wave->d);
125
+ }
126
+ return y;
127
+ }
128
+
129
+ static double wave_apply_wave2(Time t, const Wave2 *wave)
130
+ {
131
+ double y = wave->amplitude;
132
+ for (size_t i = 0; i < wave->size; i++)
133
+ {
134
+ y *= wave_apply_wave1(t, wave->waves + i);
135
+ }
136
+ return y;
137
+ }
138
+
139
+ static double wave_apply_wave3(Time t, const Wave3 *wave)
140
+ {
141
+ double y = 0.0;
142
+ for (size_t i = 0; i < wave->size; i++)
143
+ {
144
+ y += wave_apply_wave2(t, wave->waves + i);
145
+ }
146
+ return y;
147
+ }
148
+
149
+ static double wave_apply_waveform(Time t, const Waveform *wave)
150
+ {
151
+ size_t i = bisect_left(wave->waves, &t, 0, wave->size, sizeof(Wave3), cmp_wav3);
152
+ if (i == wave->size)
153
+ {
154
+ return 0.0;
155
+ }
156
+ return wave_apply_wave3(t, wave->waves + i);
157
+ }
158
+
159
+ static void wave_sample_waveform(const Waveform *wave, double *samples, size_t size, Time start, Time dt)
160
+ {
161
+ Time t = start;
162
+ size_t i = 0;
163
+ for (size_t j = 0; j < size; j++)
164
+ {
165
+ i = bisect_left(wave->waves, &t, i, wave->size, sizeof(Wave3), cmp_wav3);
166
+ if (i == wave->size)
167
+ {
168
+ for (; j < size; j++)
169
+ {
170
+ samples[j] = 0.0;
171
+ }
172
+ break;
173
+ }
174
+ samples[j] = wave_apply_wave3(t, wave->waves + i);
175
+ t += dt;
176
+ }
177
+ }
178
+
179
+ static void wave_sample_waveform_tlist(const Waveform *wave, size_t size, const Time *tlist, double *samples)
180
+ {
181
+ Time t = tlist[0];
182
+ size_t i = 0;
183
+ for (size_t j = 0; j < size; j++)
184
+ {
185
+ i = bisect_left(wave->waves, &t, i, wave->size, sizeof(Wave3), cmp_wav3);
186
+ if (i == wave->size)
187
+ {
188
+ for (; j < size; j++)
189
+ {
190
+ samples[j] = 0.0;
191
+ }
192
+ break;
193
+ }
194
+ samples[j] = wave_apply_wave3(t, wave->waves + i);
195
+ t = tlist[j + 1];
196
+ }
197
+ }
198
+
199
+ static size_t wave_register_basic_func(FuncPtr func)
200
+ {
201
+ wave_function_table[wave_func_count] = func;
202
+ return wave_func_count++;
203
+ }
204
+
205
+ static void wave_init_basic_func_table()
206
+ {
207
+ wave_register_basic_func(wave_basic_func_linear);
208
+ wave_register_basic_func(wave_basic_func_gaussian);
209
+ wave_register_basic_func(wave_basic_func_erf);
210
+ wave_register_basic_func(wave_basic_func_cos);
211
+ wave_register_basic_func(wave_basic_func_sinc);
212
+ wave_register_basic_func(wave_basic_func_exp);
213
+ }
214
+
215
+ static PyObject *method_fputs(PyObject *self, PyObject *args)
216
+ {
217
+ char *str, *filename = NULL;
218
+ int bytes_copied = -1;
219
+
220
+ /* Parse arguments */
221
+ if (!PyArg_ParseTuple(args, "ss", &str, &filename))
222
+ {
223
+ return NULL;
224
+ }
225
+
226
+ // FILE *fp = fopen(filename, "w");
227
+ // bytes_copied = fputs(str, fp);
228
+ // fclose(fp);
229
+
230
+ return PyLong_FromLong(bytes_copied);
231
+ }
232
+
233
+ static PyMethodDef Methods[] = {
234
+ {"fputs", method_fputs, METH_VARARGS, "Python interface for fputs C library function"},
235
+ {NULL, NULL, 0, NULL}};
236
+
237
+ static struct PyModuleDef module = {
238
+ PyModuleDef_HEAD_INIT,
239
+ "_waveform",
240
+ "Python interface for the _waveform C library",
241
+ -1,
242
+ Methods};
243
+
244
+ PyMODINIT_FUNC PyInit__waveform(void)
245
+ {
246
+ wave_init_basic_func_table();
247
+ return PyModule_Create(&module);
248
+ }
@@ -0,0 +1,242 @@
1
+ import pickle
2
+ from pathlib import Path
3
+
4
+ import numpy as np
5
+ import pytest
6
+
7
+ import waveforms as wf
8
+
9
+
10
+ RATE = 2_400_000_000
11
+
12
+
13
+ def _pulse(native=False):
14
+ gaussian = wf.native_gaussian if native else wf.gaussian
15
+ cosine = wf.native_cos if native else wf.cos
16
+ sine = wf.native_sin if native else wf.sin
17
+ square = wf.native_square if native else wf.square
18
+ return (
19
+ 0.8 * gaussian(20e-9) * cosine(2 * np.pi * 100e6)
20
+ + 0.15 * gaussian(12e-9) * sine(2 * np.pi * 180e6, 0.2)
21
+ + 0.05 * square(6e-9)
22
+ )
23
+
24
+
25
+ def _stack(count, native=False, spacing=40e-9):
26
+ pulse = (0.8 * (wf.native_gaussian(20e-9)
27
+ if native else wf.gaussian(20e-9))
28
+ * (wf.native_cos(2 * np.pi * 100e6)
29
+ if native else wf.cos(2 * np.pi * 100e6)))
30
+ stack_type = wf.NativeWaveVStack if native else wf.WaveVStack
31
+ stack = stack_type([pulse >> (index * spacing) for index in range(count)])
32
+ stack.start = -20e-9
33
+ stack.stop = count * spacing
34
+ stack.sample_rate = RATE
35
+ return stack
36
+
37
+
38
+ def test_native_wave_block_roundtrip_pickle_and_numerics():
39
+ expected = _pulse()
40
+ actual = _pulse(native=True)
41
+ expected.start = actual.start = -20e-9
42
+ expected.stop = actual.stop = 20e-9
43
+ expected.sample_rate = actual.sample_rate = RATE
44
+ actual.label = "native-pulse"
45
+
46
+ data = actual.to_bytes()
47
+ restored = wf.NativeWaveform.from_bytes(data)
48
+ x = np.linspace(-20e-9, 20e-9, 200_000, endpoint=False)
49
+
50
+ assert data[:4] == b"WNF4"
51
+ assert actual.to_bytes() is data
52
+ assert restored.to_bytes() is data
53
+ assert restored == wf.NativeWaveform.from_bytes(data)
54
+ assert np.allclose(actual(x), expected(x), rtol=2e-15, atol=2e-15)
55
+ assert np.array_equal(restored(x), actual(x))
56
+ assert len(data) < len(expected.to_bytes())
57
+
58
+ unpickled = pickle.loads(pickle.dumps(actual, protocol=5))
59
+ assert unpickled == actual
60
+ assert unpickled.start == actual.start
61
+ assert unpickled.stop == actual.stop
62
+ assert unpickled.sample_rate == actual.sample_rate
63
+ assert unpickled.label == actual.label
64
+
65
+
66
+ @pytest.mark.parametrize("dtype", [np.float64, np.int16, np.int32])
67
+ def test_native_device_grid_sampling_matches_packed_backend(dtype):
68
+ expected = _pulse()
69
+ actual = _pulse(native=True)
70
+ expected.start = actual.start = -20e-9
71
+ expected.stop = actual.stop = 20e-9
72
+ expected.sample_rate = actual.sample_rate = RATE
73
+
74
+ reference = expected.sample(dtype=dtype)
75
+ result = actual.sample(dtype=dtype)
76
+ if dtype == np.float64:
77
+ assert np.allclose(result, reference, rtol=2e-15, atol=2e-15)
78
+ else:
79
+ assert np.array_equal(result, reference)
80
+
81
+ target = np.empty_like(result)
82
+ returned = actual.sample(dtype=dtype, out=target)
83
+ assert returned is target
84
+ assert np.array_equal(target, result)
85
+
86
+
87
+ def test_native_complex_block_roundtrip_and_pickle():
88
+ pulse = _pulse(native=True)
89
+ complex_wave = pulse + 0.5j * (pulse >> 2e-9)
90
+ complex_wave.start = -20e-9
91
+ complex_wave.stop = 20e-9
92
+ complex_wave.sample_rate = RATE
93
+ complex_wave.label = "iq"
94
+ x = np.linspace(-20e-9, 20e-9, 8192, endpoint=False)
95
+ expected = pulse(x) + 0.5j * (pulse >> 2e-9)(x)
96
+
97
+ data = complex_wave.to_bytes()
98
+ restored = wf.NativeComplexWaveform.from_bytes(data)
99
+ assert data[:4] == b"WNC4"
100
+ assert np.allclose(restored(x), expected, rtol=2e-15, atol=2e-15)
101
+ unpickled = pickle.loads(pickle.dumps(complex_wave, protocol=5))
102
+ assert np.allclose(unpickled(x), expected, rtol=2e-15, atol=2e-15)
103
+ assert unpickled.start == complex_wave.start
104
+ assert unpickled.stop == complex_wave.stop
105
+ assert unpickled.sample_rate == complex_wave.sample_rate
106
+ assert unpickled.label == complex_wave.label
107
+
108
+
109
+ def test_native_stack_sampling_lazy_roundtrip_shift_offset_and_pickle():
110
+ expected = _stack(100)
111
+ actual = _stack(100, native=True)
112
+
113
+ assert np.allclose(actual.sample(), expected.sample(),
114
+ rtol=2e-15, atol=2e-15)
115
+ assert np.array_equal(actual.sample(dtype=np.int16),
116
+ expected.sample(dtype=np.int16))
117
+
118
+ data = actual.to_bytes()
119
+ restored = wf.NativeWaveVStack.from_bytes(data)
120
+ restored.start = actual.start
121
+ restored.stop = actual.stop
122
+ restored.sample_rate = actual.sample_rate
123
+ assert data[:4] == b"WNS4"
124
+ assert restored.to_bytes() is data
125
+ assert np.array_equal(restored.sample(), actual.sample())
126
+
127
+ transformed = (actual >> 5e-9) + 0.125
128
+ transformed_data = transformed.to_bytes()
129
+ transformed_restored = wf.NativeWaveVStack.from_bytes(transformed_data)
130
+ x = np.linspace(-20e-9, 4e-6, 10_000, endpoint=False)
131
+ assert transformed_data[:4] == b"WNS4"
132
+ assert np.allclose(transformed_restored(x), transformed(x),
133
+ rtol=2e-15, atol=2e-15)
134
+
135
+ actual.label = "events"
136
+ unpickled = pickle.loads(pickle.dumps(actual, protocol=5))
137
+ assert unpickled == actual
138
+ assert unpickled.sample_rate == actual.sample_rate
139
+ assert unpickled.label == actual.label
140
+
141
+
142
+ @pytest.mark.parametrize("dtype,bits", [
143
+ (np.float64, 0),
144
+ (np.int16, 16),
145
+ (np.int32, 32),
146
+ ])
147
+ def test_native_stack_sample_plan_matches_direct_sampler(dtype, bits):
148
+ pulse = (wf.native_gaussian(20e-9)
149
+ * wf.native_cos(2 * np.pi * 100e6))
150
+ scales = (0.25, 0.5, 1.0)
151
+ actual = wf.NativeWaveVStack([
152
+ scales[index % len(scales)] * pulse >> (index * 40e-9)
153
+ for index in range(75)
154
+ ])
155
+ actual.start = -20e-9
156
+ actual.stop = 75 * 40e-9
157
+ actual.sample_rate = RATE
158
+ actual.offset = 0.071
159
+
160
+ assert actual._sample_plan_cache is None
161
+ first = actual.sample(dtype=dtype)
162
+ cache_key, sample_plan = actual._sample_plan_cache
163
+ target = np.empty_like(first)
164
+ result = actual.sample(dtype=dtype, out=target)
165
+ direct = actual._core.sample(
166
+ cache_key[0], cache_key[1], cache_key[2], cache_key[3],
167
+ cache_key[4], actual.offset, bits, 1.0,
168
+ )
169
+
170
+ assert result is target
171
+ assert sample_plan.count == len(result)
172
+ assert sample_plan.group_count == 1
173
+ assert sample_plan.non_overlapping
174
+ assert np.array_equal(result, direct)
175
+ assert actual._sample_plan_cache[1] is sample_plan
176
+
177
+
178
+ def test_native_sample_plan_overlaps_and_rational_fallback():
179
+ actual = _stack(50, native=True, spacing=10e-9)
180
+ result = actual.sample(dtype=np.int16)
181
+ cache_key, sample_plan = actual._sample_plan_cache
182
+ expected = actual._core.sample(
183
+ cache_key[0], cache_key[1], cache_key[2], cache_key[3],
184
+ cache_key[4], actual.offset, 16, 1.0,
185
+ )
186
+
187
+ assert np.array_equal(result, expected)
188
+ assert not sample_plan.non_overlapping
189
+ assert actual._core.prepare_sample(0, 64, 1200, 11, 0) is None
190
+
191
+
192
+ def test_native_overlapping_stack_quantization_and_empty_stack():
193
+ expected = _stack(50, spacing=10e-9)
194
+ actual = _stack(50, native=True, spacing=10e-9)
195
+ assert np.array_equal(actual.sample(dtype=np.int16),
196
+ expected.sample(dtype=np.int16))
197
+ assert np.array_equal(actual.sample(dtype=np.int32),
198
+ expected.sample(dtype=np.int32))
199
+
200
+ empty = wf.NativeWaveVStack([])
201
+ empty.start = -1e-9
202
+ empty.stop = 1e-9
203
+ empty.sample_rate = RATE
204
+ assert empty.to_bytes() == b"WNS4\x01\x00\x00\x00" + b"\x00" * 8
205
+ assert not np.any(empty.sample())
206
+ assert not np.any(wf.NativeWaveVStack.from_bytes(empty.to_bytes())(
207
+ np.linspace(-1e-9, 1e-9, 17)
208
+ ))
209
+
210
+
211
+ def test_native_blocks_reject_malformed_layouts():
212
+ wave_data = bytearray(_pulse(native=True).to_bytes())
213
+ with pytest.raises(ValueError):
214
+ wf.NativeWaveform.from_bytes(wave_data[:20])
215
+ wave_data[0] = 0
216
+ with pytest.raises(ValueError):
217
+ wf.NativeWaveform.from_bytes(wave_data)
218
+
219
+ wave_data = bytearray(_pulse(native=True).to_bytes())
220
+ wave_data[24] = 255
221
+ with pytest.raises(ValueError):
222
+ wf.NativeWaveform.from_bytes(wave_data)
223
+
224
+ stack_data = bytearray(_stack(2, native=True).to_bytes())
225
+ with pytest.raises(ValueError):
226
+ wf.NativeWaveVStack.from_bytes(stack_data[:-1])
227
+ event_offset = len(stack_data) - 40
228
+ stack_data[event_offset:event_offset + 4] = (99).to_bytes(4, "little")
229
+ malformed = wf.NativeWaveVStack.from_bytes(stack_data)
230
+ with pytest.raises(ValueError):
231
+ _ = malformed._core.event_count
232
+
233
+
234
+ def test_native_format_is_language_neutral_and_self_describing():
235
+ description = wf.native_format_description()
236
+ assert "little-endian" in description
237
+ assert "120 GHz" in description
238
+ assert "ABI 1" in description
239
+ assert type(_pulse(native=True)._core).__module__ == "waveforms._waveform"
240
+ package_dir = Path(wf.__file__).resolve().parent
241
+ assert (package_dir / "_cwaveform.c").is_file()
242
+ assert (package_dir / "_cwaveform.h").is_file()