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 +92 -0
- pynaff-1.2.0/PyNAFF/PyNAFF.py +445 -0
- pynaff-1.2.0/PyNAFF/__init__.py +4 -0
- pynaff-1.2.0/PyNAFF/_version.py +1 -0
- pynaff-1.2.0/PyNAFF.egg-info/PKG-INFO +92 -0
- {pynaff-1.1.7 → pynaff-1.2.0}/PyNAFF.egg-info/SOURCES.txt +5 -2
- pynaff-1.2.0/PyNAFF.egg-info/requires.txt +1 -0
- pynaff-1.2.0/README.md +66 -0
- pynaff-1.2.0/README.rst +71 -0
- pynaff-1.2.0/pyproject.toml +41 -0
- pynaff-1.2.0/setup.cfg +4 -0
- pynaff-1.2.0/setup.py +4 -0
- pynaff-1.2.0/tests/test_pynaff.py +175 -0
- pynaff-1.1.7/PKG-INFO +0 -89
- pynaff-1.1.7/PyNAFF/PyNAFF.py +0 -285
- pynaff-1.1.7/PyNAFF/__init__.py +0 -2
- pynaff-1.1.7/PyNAFF.egg-info/PKG-INFO +0 -89
- pynaff-1.1.7/PyNAFF.egg-info/requires.txt +0 -1
- pynaff-1.1.7/README.md +0 -61
- pynaff-1.1.7/setup.cfg +0 -9
- pynaff-1.1.7/setup.py +0 -25
- {pynaff-1.1.7 → pynaff-1.2.0}/LICENSE.txt +0 -0
- {pynaff-1.1.7 → pynaff-1.2.0}/PyNAFF.egg-info/dependency_links.txt +0 -0
- {pynaff-1.1.7 → pynaff-1.2.0}/PyNAFF.egg-info/top_level.txt +0 -0
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 @@
|
|
|
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
|
-
|
|
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
|