PyNAFF 1.1.7__tar.gz → 1.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.
pynaff-1.2.0/PKG-INFO ADDED
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: PyNAFF
3
+ Version: 1.2.0
4
+ Summary: Numerical Analysis of Fundamental Frequencies for one or more signals
5
+ Author: Foteini Asvesta, Panagiotis Zisopoulos
6
+ Author-email: Nikos Karastathis <nkarast@gmail.com>
7
+ License-Expression: GPL-3.0-only
8
+ Project-URL: Homepage, https://github.com/PZiso/PyNAFF
9
+ Project-URL: Repository, https://github.com/PZiso/PyNAFF
10
+ Project-URL: Issues, https://github.com/PZiso/PyNAFF/issues
11
+ Keywords: NAFF,frequency analysis,beam position monitor
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: Physics
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE.txt
24
+ Requires-Dist: numpy>=1.22
25
+ Dynamic: license-file
26
+
27
+ # PyNAFF
28
+
29
+ Authors:
30
+
31
+ * Foteini Asvesta (fasvesta .at. cern .dot. ch)
32
+ * Nikos Karastathis (nkarast .at. cern .dot. ch)
33
+ * Panagiotis Zisopoulos (pzisopou .at. cern .dot. ch)
34
+
35
+ A Python implementation of J. Laskar's Numerical Analysis of Fundamental
36
+ Frequencies (NAFF) method.
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ python -m pip install PyNAFF
42
+ ```
43
+
44
+ ## Single BPM
45
+
46
+ ```python
47
+ import numpy as np
48
+ import PyNAFF as pnf
49
+
50
+ t = np.arange(3001)
51
+ signal = np.sin(2.0 * np.pi * 0.12345 * t)
52
+ result = pnf.naff(signal, turns=500, nterms=1, window=1)
53
+
54
+ # Each row is:
55
+ # [order, frequency, amplitude, real amplitude, imaginary amplitude]
56
+ frequency = result[0, 1]
57
+ ```
58
+
59
+ `turns` is the number of integration intervals, so the input must contain at
60
+ least `turns + 1` observations. For real sinusoids, the reported amplitude is
61
+ the magnitude of one complex Fourier coefficient, equal to half the sinusoid's
62
+ peak amplitude.
63
+
64
+ ## Multiple BPMs
65
+
66
+ Place observations on axis 0 and BPMs on axis 1:
67
+
68
+ ```python
69
+ signals = np.column_stack([
70
+ np.sin(2.0 * np.pi * 0.12345 * t),
71
+ 2.0 * np.sin(2.0 * np.pi * 0.27123 * t),
72
+ ])
73
+ results = pnf.naff(signals, turns=500, nterms=1)
74
+
75
+ # results.shape == (2 BPMs, 1 term, 5 values)
76
+ frequencies = results[:, 0, 1]
77
+ amplitudes = results[:, 0, 2]
78
+ ```
79
+
80
+ For multi-BPM input, unused term rows are filled with `NaN` when extraction
81
+ for a BPM stops before `nterms`.
82
+
83
+ The `tol` option controls duplicate residual handling as a fraction of one FFT
84
+ bin. If NAFF stops early because a residual peak is very close to a previously
85
+ extracted frequency, increasing `tol` can let it remove that residual and
86
+ continue to weaker frequencies. The default is `1e-4`; large values can also
87
+ turn spectral leakage into spurious frequencies, so compare results across
88
+ several values. `nterms` is an upper bound, not a guaranteed result count.
89
+
90
+ For real input, prefer `getFullSpectrum=False`. A full spectrum contains both
91
+ positive and negative conjugate frequencies, and each one occupies a result
92
+ row.
@@ -0,0 +1,445 @@
1
+ """Numerical Analysis of Fundamental Frequencies (NAFF)."""
2
+
3
+ from math import lgamma, log
4
+ import warnings as warnings_module
5
+
6
+ import numpy as np
7
+
8
+ from ._version import __version__
9
+
10
+ __PyVersion = [3]
11
+ __authors__ = ["F. Asvesta", "N. Karastathis", "P. Zisopoulos"]
12
+ __contact__ = ["nkarast .at. cern .dot. ch"]
13
+ __all__ = ["naff"]
14
+
15
+
16
+ def _hardy_weights(turns):
17
+ """Return the composite Hardy quadrature weights used by NAFF."""
18
+ weights = np.empty(turns + 1, dtype=np.float64)
19
+ weights[0] = 41.0
20
+ weights[-1] = 41.0
21
+ pattern = np.array([216.0, 27.0, 272.0, 27.0, 216.0, 82.0])
22
+ weights[1:-1] = np.resize(pattern, turns - 1)
23
+ weights *= 6.0 / (840.0 * turns)
24
+ return weights
25
+
26
+
27
+ def _prepare_context(turns, window):
28
+ samples = np.arange(turns + 1, dtype=np.float64)
29
+ centered_time = 2.0 * np.pi * samples - np.pi * turns
30
+ scale = np.exp(
31
+ window * log(2.0)
32
+ + 2.0 * lgamma(window + 1)
33
+ - lgamma(2 * window + 1)
34
+ )
35
+ taper = scale * (1.0 + np.cos(centered_time / turns)) ** window
36
+ return samples, taper, _hardy_weights(turns)
37
+
38
+
39
+ def _integral(signal, frequency, samples, integration_weights):
40
+ phase = np.exp(-2.0j * np.pi * frequency * samples)
41
+ value = np.dot(signal * integration_weights, phase)
42
+ return abs(value), value.real, value.imag
43
+
44
+
45
+ def _refine_frequency(
46
+ signal,
47
+ frequency,
48
+ step,
49
+ tolerance,
50
+ samples,
51
+ integration_weights,
52
+ ):
53
+ """Maximize the norm of the NAFF scalar product near an FFT bin."""
54
+ epsilon = 1.0e-15
55
+ x2 = frequency
56
+ y2, a2, b2 = _integral(signal, x2, samples, integration_weights)
57
+ x1 = x2 - step
58
+ x3 = x2 + step
59
+ y1, a1, b1 = _integral(signal, x1, samples, integration_weights)
60
+ y3, a3, b3 = _integral(signal, x3, samples, integration_weights)
61
+
62
+ for _ in range(1000):
63
+ if step < tolerance or abs(y3 - y1) < epsilon:
64
+ break
65
+
66
+ if y1 < y2 and y3 < y2:
67
+ slope2 = (y1 - y2) / (x1 - x2)
68
+ slope3 = (y1 - y3) / (x1 - x3)
69
+ parabola = (slope2 - slope3) / (x2 - x3)
70
+ if parabola == 0.0:
71
+ break
72
+ linear = slope2 - parabola * (x1 + x2)
73
+ candidate = -linear / (2.0 * parabola)
74
+ new_step = abs(candidate - x2)
75
+
76
+ if candidate > x2:
77
+ x1, y1, a1, b1 = x2, y2, a2, b2
78
+ x2 = candidate
79
+ y2, a2, b2 = _integral(
80
+ signal, x2, samples, integration_weights
81
+ )
82
+ x3 = x2 + new_step
83
+ y3, a3, b3 = _integral(
84
+ signal, x3, samples, integration_weights
85
+ )
86
+ else:
87
+ x3, y3, a3, b3 = x2, y2, a2, b2
88
+ x2 = candidate
89
+ y2, a2, b2 = _integral(
90
+ signal, x2, samples, integration_weights
91
+ )
92
+ x1 = x2 - new_step
93
+ y1, a1, b1 = _integral(
94
+ signal, x1, samples, integration_weights
95
+ )
96
+ step = new_step
97
+ else:
98
+ if y1 > y3:
99
+ x2, y2, a2, b2 = x1, y1, a1, b1
100
+ else:
101
+ x2, y2, a2, b2 = x3, y3, a3, b3
102
+
103
+ x1 = x2 - step
104
+ x3 = x2 + step
105
+ y1, a1, b1 = _integral(
106
+ signal, x1, samples, integration_weights
107
+ )
108
+ y3, a3, b3 = _integral(
109
+ signal, x3, samples, integration_weights
110
+ )
111
+
112
+ return x2, y2, a2, b2
113
+
114
+
115
+ def _frequency_status(
116
+ frequency,
117
+ frequencies,
118
+ fundamental_resolution,
119
+ tolerance,
120
+ ):
121
+ if not frequencies:
122
+ return 1, 0
123
+
124
+ distances = np.abs(np.asarray(frequencies) - frequency)
125
+ nearby = np.flatnonzero(distances < abs(fundamental_resolution))
126
+ if nearby.size == 0:
127
+ return 1, 0
128
+
129
+ duplicate = nearby[
130
+ distances[nearby] / abs(fundamental_resolution) < tolerance
131
+ ]
132
+ if duplicate.size:
133
+ return -1, int(duplicate[0])
134
+ return 0, 0
135
+
136
+
137
+ def _basis_overlap(
138
+ frequency,
139
+ old_frequency,
140
+ samples,
141
+ integration_weights,
142
+ ):
143
+ phase = np.exp(
144
+ -2.0j * np.pi * (frequency - old_frequency) * samples
145
+ )
146
+ return np.dot(integration_weights, phase)
147
+
148
+
149
+ def _remove_component(
150
+ residual,
151
+ contribution,
152
+ frequency,
153
+ samples,
154
+ real_spectrum,
155
+ ):
156
+ component = contribution * np.exp(
157
+ 2.0j * np.pi * frequency * samples
158
+ )
159
+ is_unpaired_bin = np.isclose(frequency, 0.0) or np.isclose(
160
+ abs(frequency), 0.5
161
+ )
162
+ if real_spectrum and not is_unpaired_bin:
163
+ residual -= 2.0 * component.real
164
+ elif real_spectrum:
165
+ residual -= component.real
166
+ else:
167
+ residual -= component
168
+
169
+
170
+ def _add_frequency(
171
+ residual,
172
+ frequency,
173
+ integral,
174
+ frequencies,
175
+ amplitudes,
176
+ coefficients,
177
+ samples,
178
+ integration_weights,
179
+ real_spectrum,
180
+ ):
181
+ """Orthonormalize a new exponential and subtract its projection."""
182
+ old_count = len(frequencies)
183
+ overlaps = np.ones(old_count + 1, dtype=np.complex128)
184
+ for index, old_frequency in enumerate(frequencies):
185
+ overlaps[index] = _basis_overlap(
186
+ frequency,
187
+ old_frequency,
188
+ samples,
189
+ integration_weights,
190
+ )
191
+
192
+ row = np.zeros(old_count + 1, dtype=np.complex128)
193
+ for k in range(old_count):
194
+ for i in range(old_count):
195
+ row[k] -= (
196
+ np.dot(
197
+ np.conj(coefficients[i, : i + 1]),
198
+ overlaps[: i + 1],
199
+ )
200
+ * coefficients[i, k]
201
+ )
202
+ row[old_count] = 1.0
203
+
204
+ divisor = np.sqrt(abs(np.dot(np.conj(row), overlaps)))
205
+ if divisor == 0.0:
206
+ return False
207
+ row /= divisor
208
+
209
+ frequencies.append(frequency)
210
+ coefficients[old_count, : old_count + 1] = row
211
+ multiplier = integral / divisor
212
+
213
+ for index, old_frequency in enumerate(frequencies):
214
+ contribution = row[index] * multiplier
215
+ amplitudes[index] += contribution
216
+ _remove_component(
217
+ residual,
218
+ contribution,
219
+ old_frequency,
220
+ samples,
221
+ real_spectrum,
222
+ )
223
+ return True
224
+
225
+
226
+ def _naff_1d(
227
+ data,
228
+ turns,
229
+ nterms,
230
+ skip_turns,
231
+ get_full_spectrum,
232
+ samples,
233
+ taper,
234
+ quadrature_weights,
235
+ tolerance,
236
+ show_warnings,
237
+ ):
238
+ residual = np.asarray(
239
+ data[skip_turns : skip_turns + turns + 1],
240
+ dtype=np.complex128,
241
+ ).copy()
242
+ integration_weights = taper * quadrature_weights
243
+ frequencies = []
244
+ amplitudes = np.zeros(nterms, dtype=np.complex128)
245
+ coefficients = np.zeros((nterms, nterms), dtype=np.complex128)
246
+ resolution = 1.0 / turns
247
+ real_spectrum = not get_full_spectrum
248
+
249
+ dc_warned = False
250
+ for _ in range(nterms):
251
+ fft_input = residual[:-1] * taper[:-1]
252
+ if get_full_spectrum:
253
+ spectrum = np.fft.fft(fft_input)
254
+ frequency = np.fft.fftfreq(turns)[
255
+ int(np.argmax(np.abs(spectrum)))
256
+ ]
257
+ else:
258
+ spectrum = np.fft.rfft(fft_input.real)
259
+ frequency = np.fft.rfftfreq(turns)[
260
+ int(np.argmax(np.abs(spectrum)))
261
+ ]
262
+
263
+ if frequency == 0.0 and show_warnings and not dc_warned:
264
+ warnings_module.warn(
265
+ "PyNAFF found a DC component; subtract the signal mean "
266
+ "before analysis if DC is not of interest.",
267
+ UserWarning,
268
+ stacklevel=3,
269
+ )
270
+ dc_warned = True
271
+
272
+ frequency, _, real, imaginary = _refine_frequency(
273
+ residual,
274
+ frequency,
275
+ resolution / 3.0,
276
+ resolution / 1.0e8,
277
+ samples,
278
+ integration_weights,
279
+ )
280
+ status, existing_index = _frequency_status(
281
+ frequency, frequencies, resolution, tolerance
282
+ )
283
+
284
+ if status == 0:
285
+ if show_warnings:
286
+ warnings_module.warn(
287
+ "PyNAFF stopped because a residual peak is within one "
288
+ "FFT bin of an extracted frequency but outside tol. "
289
+ "A larger tol may continue extraction, but can also "
290
+ "accept leakage artifacts.",
291
+ UserWarning,
292
+ stacklevel=3,
293
+ )
294
+ break
295
+ if status == -1:
296
+ contribution = complex(real, imaginary)
297
+ amplitudes[existing_index] += contribution
298
+ _remove_component(
299
+ residual,
300
+ contribution,
301
+ frequency,
302
+ samples,
303
+ real_spectrum,
304
+ )
305
+ continue
306
+
307
+ if not _add_frequency(
308
+ residual,
309
+ frequency,
310
+ complex(real, imaginary),
311
+ frequencies,
312
+ amplitudes,
313
+ coefficients,
314
+ samples,
315
+ integration_weights,
316
+ real_spectrum,
317
+ ):
318
+ break
319
+
320
+ result = np.empty((len(frequencies), 5), dtype=np.float64)
321
+ for order, frequency in enumerate(frequencies):
322
+ amplitude = amplitudes[order]
323
+ result[order] = (
324
+ order,
325
+ frequency,
326
+ abs(amplitude),
327
+ amplitude.real,
328
+ amplitude.imag,
329
+ )
330
+ return result
331
+
332
+
333
+ def naff(
334
+ data,
335
+ turns=300,
336
+ nterms=1,
337
+ skipTurns=0,
338
+ getFullSpectrum=False,
339
+ window=1,
340
+ tol=1.0e-4,
341
+ warnings=True,
342
+ ):
343
+ """Extract the fundamental frequencies of one or more BPM signals.
344
+
345
+ A single signal has shape ``(observations,)``. Multiple BPM signals
346
+ have shape ``(observations, bpms)``.
347
+
348
+ The scalar result has shape ``(found_terms, 5)``. The multi-BPM result
349
+ has shape ``(bpms, nterms, 5)``, with unused rows filled with ``NaN``.
350
+ Each row contains ``[order, frequency, amplitude, real amplitude,
351
+ imaginary amplitude]``.
352
+
353
+ ``tol`` controls when a refined frequency is considered a duplicate of
354
+ one already found. Set ``warnings=False`` to suppress the DC warning.
355
+ """
356
+ values = np.asarray(data)
357
+ if values.ndim not in (1, 2):
358
+ raise ValueError(
359
+ "data must have shape (observations,) or (observations, bpms)"
360
+ )
361
+ if values.ndim == 2 and values.shape[1] == 0:
362
+ raise ValueError("data must contain at least one BPM")
363
+ if not np.issubdtype(values.dtype, np.number):
364
+ raise TypeError("data must contain numeric values")
365
+ if (
366
+ isinstance(turns, (bool, np.bool_))
367
+ or not isinstance(turns, (int, np.integer))
368
+ or turns < 6
369
+ ):
370
+ raise ValueError("turns must be an integer of at least 6")
371
+ if (
372
+ isinstance(nterms, (bool, np.bool_))
373
+ or not isinstance(nterms, (int, np.integer))
374
+ or nterms < 1
375
+ ):
376
+ raise ValueError("nterms must be a positive integer")
377
+ if (
378
+ isinstance(skipTurns, (bool, np.bool_))
379
+ or not isinstance(skipTurns, (int, np.integer))
380
+ or skipTurns < 0
381
+ ):
382
+ raise ValueError("skipTurns must be a non-negative integer")
383
+ if (
384
+ isinstance(window, (bool, np.bool_))
385
+ or not isinstance(window, (int, np.integer))
386
+ or window < 0
387
+ ):
388
+ raise ValueError("window must be a non-negative integer")
389
+ if (
390
+ isinstance(tol, (bool, np.bool_))
391
+ or
392
+ not isinstance(tol, (int, float, np.integer, np.floating))
393
+ or not np.isfinite(tol)
394
+ or tol <= 0
395
+ or tol > 1
396
+ ):
397
+ raise ValueError("tol must be a finite number in the interval (0, 1]")
398
+ if not isinstance(warnings, (bool, np.bool_)):
399
+ raise ValueError("warnings must be a boolean")
400
+ if not isinstance(getFullSpectrum, (bool, np.bool_)):
401
+ raise ValueError("getFullSpectrum must be a boolean")
402
+ if np.iscomplexobj(values) and not getFullSpectrum:
403
+ raise ValueError("getFullSpectrum must be True for complex input")
404
+
405
+ turns -= turns % 6
406
+ required_observations = skipTurns + turns + 1
407
+ if values.shape[0] < required_observations:
408
+ raise ValueError(
409
+ "data must contain at least skipTurns + turns + 1 observations"
410
+ )
411
+ analysis_values = values[:required_observations]
412
+ if not np.all(np.isfinite(analysis_values)):
413
+ raise ValueError("data must contain only finite values")
414
+
415
+ samples, taper, quadrature_weights = _prepare_context(turns, window)
416
+ if values.ndim == 1:
417
+ return _naff_1d(
418
+ values,
419
+ turns,
420
+ nterms,
421
+ skipTurns,
422
+ getFullSpectrum,
423
+ samples,
424
+ taper,
425
+ quadrature_weights,
426
+ float(tol),
427
+ bool(warnings),
428
+ )
429
+
430
+ result = np.full((values.shape[1], nterms, 5), np.nan)
431
+ for bpm in range(values.shape[1]):
432
+ bpm_result = _naff_1d(
433
+ values[:, bpm],
434
+ turns,
435
+ nterms,
436
+ skipTurns,
437
+ getFullSpectrum,
438
+ samples,
439
+ taper,
440
+ quadrature_weights,
441
+ float(tol),
442
+ bool(warnings),
443
+ )
444
+ result[bpm, : len(bpm_result)] = bpm_result
445
+ return result
@@ -0,0 +1,4 @@
1
+ from .PyNAFF import naff
2
+ from ._version import __version__
3
+
4
+ __all__ = ["naff", "__version__"]
@@ -0,0 +1 @@
1
+ __version__ = "1.2.0"
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: PyNAFF
3
+ Version: 1.2.0
4
+ Summary: Numerical Analysis of Fundamental Frequencies for one or more signals
5
+ Author: Foteini Asvesta, Panagiotis Zisopoulos
6
+ Author-email: Nikos Karastathis <nkarast@gmail.com>
7
+ License-Expression: GPL-3.0-only
8
+ Project-URL: Homepage, https://github.com/PZiso/PyNAFF
9
+ Project-URL: Repository, https://github.com/PZiso/PyNAFF
10
+ Project-URL: Issues, https://github.com/PZiso/PyNAFF/issues
11
+ Keywords: NAFF,frequency analysis,beam position monitor
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: Physics
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE.txt
24
+ Requires-Dist: numpy>=1.22
25
+ Dynamic: license-file
26
+
27
+ # PyNAFF
28
+
29
+ Authors:
30
+
31
+ * Foteini Asvesta (fasvesta .at. cern .dot. ch)
32
+ * Nikos Karastathis (nkarast .at. cern .dot. ch)
33
+ * Panagiotis Zisopoulos (pzisopou .at. cern .dot. ch)
34
+
35
+ A Python implementation of J. Laskar's Numerical Analysis of Fundamental
36
+ Frequencies (NAFF) method.
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ python -m pip install PyNAFF
42
+ ```
43
+
44
+ ## Single BPM
45
+
46
+ ```python
47
+ import numpy as np
48
+ import PyNAFF as pnf
49
+
50
+ t = np.arange(3001)
51
+ signal = np.sin(2.0 * np.pi * 0.12345 * t)
52
+ result = pnf.naff(signal, turns=500, nterms=1, window=1)
53
+
54
+ # Each row is:
55
+ # [order, frequency, amplitude, real amplitude, imaginary amplitude]
56
+ frequency = result[0, 1]
57
+ ```
58
+
59
+ `turns` is the number of integration intervals, so the input must contain at
60
+ least `turns + 1` observations. For real sinusoids, the reported amplitude is
61
+ the magnitude of one complex Fourier coefficient, equal to half the sinusoid's
62
+ peak amplitude.
63
+
64
+ ## Multiple BPMs
65
+
66
+ Place observations on axis 0 and BPMs on axis 1:
67
+
68
+ ```python
69
+ signals = np.column_stack([
70
+ np.sin(2.0 * np.pi * 0.12345 * t),
71
+ 2.0 * np.sin(2.0 * np.pi * 0.27123 * t),
72
+ ])
73
+ results = pnf.naff(signals, turns=500, nterms=1)
74
+
75
+ # results.shape == (2 BPMs, 1 term, 5 values)
76
+ frequencies = results[:, 0, 1]
77
+ amplitudes = results[:, 0, 2]
78
+ ```
79
+
80
+ For multi-BPM input, unused term rows are filled with `NaN` when extraction
81
+ for a BPM stops before `nterms`.
82
+
83
+ The `tol` option controls duplicate residual handling as a fraction of one FFT
84
+ bin. If NAFF stops early because a residual peak is very close to a previously
85
+ extracted frequency, increasing `tol` can let it remove that residual and
86
+ continue to weaker frequencies. The default is `1e-4`; large values can also
87
+ turn spectral leakage into spurious frequencies, so compare results across
88
+ several values. `nterms` is an upper bound, not a guaranteed result count.
89
+
90
+ For real input, prefer `getFullSpectrum=False`. A full spectrum contains both
91
+ positive and negative conjugate frequencies, and each one occupies a result
92
+ row.
@@ -1,11 +1,14 @@
1
1
  LICENSE.txt
2
2
  README.md
3
- setup.cfg
3
+ README.rst
4
+ pyproject.toml
4
5
  setup.py
5
6
  PyNAFF/PyNAFF.py
6
7
  PyNAFF/__init__.py
8
+ PyNAFF/_version.py
7
9
  PyNAFF.egg-info/PKG-INFO
8
10
  PyNAFF.egg-info/SOURCES.txt
9
11
  PyNAFF.egg-info/dependency_links.txt
10
12
  PyNAFF.egg-info/requires.txt
11
- PyNAFF.egg-info/top_level.txt
13
+ PyNAFF.egg-info/top_level.txt
14
+ tests/test_pynaff.py
@@ -0,0 +1 @@
1
+ numpy>=1.22