dftmodels 1.0.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.
- dftmodels-1.0.0/LICENSE +21 -0
- dftmodels-1.0.0/PKG-INFO +196 -0
- dftmodels-1.0.0/README.md +183 -0
- dftmodels-1.0.0/dftmodels/__init__.py +42 -0
- dftmodels-1.0.0/dftmodels/dft/__init__.py +15 -0
- dftmodels-1.0.0/dftmodels/dft/config.py +170 -0
- dftmodels-1.0.0/dftmodels/dft/correction.py +19 -0
- dftmodels-1.0.0/dftmodels/dft/series.py +252 -0
- dftmodels-1.0.0/dftmodels/models/__init__.py +11 -0
- dftmodels-1.0.0/dftmodels/models/base.py +198 -0
- dftmodels-1.0.0/dftmodels/models/composite.py +123 -0
- dftmodels-1.0.0/dftmodels/models/sinusoid.py +790 -0
- dftmodels-1.0.0/dftmodels/py.typed +0 -0
- dftmodels-1.0.0/dftmodels/stats.py +86 -0
- dftmodels-1.0.0/dftmodels/utils/__init__.py +11 -0
- dftmodels-1.0.0/dftmodels/utils/math.py +38 -0
- dftmodels-1.0.0/dftmodels.egg-info/PKG-INFO +196 -0
- dftmodels-1.0.0/dftmodels.egg-info/SOURCES.txt +21 -0
- dftmodels-1.0.0/dftmodels.egg-info/dependency_links.txt +1 -0
- dftmodels-1.0.0/dftmodels.egg-info/requires.txt +3 -0
- dftmodels-1.0.0/dftmodels.egg-info/top_level.txt +1 -0
- dftmodels-1.0.0/pyproject.toml +32 -0
- dftmodels-1.0.0/setup.cfg +4 -0
dftmodels-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Sven Bodenstedt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
dftmodels-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dftmodels
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Analytical DFT models with closed-form correction terms for bias-free spectral parameter estimation.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: numpy>=2.0
|
|
10
|
+
Requires-Dist: scipy>=1.15
|
|
11
|
+
Requires-Dist: lmfit>=1.0
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# dftmodels
|
|
15
|
+
|
|
16
|
+
Analytical DFT models for spectral parameter estimation.
|
|
17
|
+
|
|
18
|
+
## Overview
|
|
19
|
+
|
|
20
|
+
Fitting peaks in DFT spectra with continuous Fourier transform (CFT) models (sinc functions, Lorentzians, and their window-convolved variants) introduces systematic errors in the estimated amplitude, frequency, phase, and linewidth. Three effects are responsible: the DFT sum gives full weight to the boundary samples where the trapezoidal-rule approximation gives half weight (boundary mismatch); sampling makes the spectrum periodic, introducing aliased copies at f ± k·f_s (aliasing); and the finite window modifies the spectral shape in a way that depends on both the window coefficients and the signal frequency (windowing). Each effect produces a deterministic, structured residual in the DFT.
|
|
21
|
+
|
|
22
|
+
`dftmodels` provides closed-form analytical models that include these effects as additive correction terms: WINDOW (the CFT of the windowed signal), BASELINE (the boundary weight mismatch), and SAMPLING (aliased spectral copies up to a configurable order). Combined with least-squares fitting, these models form estimators whose variance can approach the Cramér–Rao bound (CRB).
|
|
23
|
+
|
|
24
|
+
**When corrections matter.** Systematic bias is only the limiting factor when SNR is high and records are short. The SAMPLING correction becomes negligible above a crossover acquisition length that depends on window and noise level (see [Notebook 02](examples/02_dft_corrections.ipynb)). For long records or when noise dominates, a CFT-based sinc or Lorentzian model is adequate. The gains shown in the examples below are large by design — the signal parameters were chosen so that systematic bias is the limiting factor, not noise. The package is most relevant when that condition holds.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install dftmodels
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Dependencies: Python ≥ 3.12, `numpy` ≥ 2.0, `scipy` ≥ 1.15, `lmfit` ≥ 1.0.
|
|
33
|
+
|
|
34
|
+
## Examples
|
|
35
|
+
|
|
36
|
+
Six notebooks in [examples/](examples/) work through the main topics in sequence.
|
|
37
|
+
|
|
38
|
+
| Notebook | Topic |
|
|
39
|
+
|---|---|
|
|
40
|
+
| [01 — Normalizations](examples/01_dft_normalization.ipynb) | ASD, PSD, and CFT normalizations; Parseval's theorem |
|
|
41
|
+
| [02 — DFT corrections](examples/02_dft_corrections.ipynb) | Origin and convergence of the three correction terms |
|
|
42
|
+
| [03 — Precision fitting](examples/03_precision_fitting.ipynb) | Sinusoid parameter estimation; bias elimination; Cramér–Rao bound |
|
|
43
|
+
| [04 — Decaying signals](examples/04_decaying_signals.ipynb) | Lorentzian fitting; exponential windows; line broadening |
|
|
44
|
+
| [05 — Composite models](examples/05_composite_model.ipynb) | Simultaneous multi-peak fitting; spectral background |
|
|
45
|
+
| [06 — Window comparison](examples/06_window_comparison.ipynb) | Window choice: Fisher information loss vs. sidelobe suppression |
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
### Normalizations
|
|
50
|
+
|
|
51
|
+
Three normalizations are provided. **ASD** and **PSD** satisfy Parseval's theorem — ∫|ASD(f)|² df = ∫PSD(f) df = RMS² — and are appropriate for stationary signals. The normalization factor includes the window RMS (w_rms) to compensate for amplitude attenuation; this correction is exact in the limit N → ∞ and introduces a residual error of order 1/N. **CFT** normalization satisfies ∫X_CFT(f) df = x(0) and is appropriate for decaying signals, where total power depends on acquisition length and ASD peak height grows with T.
|
|
52
|
+
|
|
53
|
+

|
|
54
|
+
|
|
55
|
+
*Top: CFT normalization — the spectral integral converges to x(0) = A for both decaying (left cluster, 8–12 Hz) and stationary sinusoids (right cluster, 18–22 Hz), independent of T. Bottom: ASD normalization — for stationary sinusoids √∫|ASD|² = RMS is stable; for decaying sinusoids the integral grows with T.*
|
|
56
|
+
|
|
57
|
+
*[Notebook 01](examples/01_dft_normalization.ipynb). Implementation: `NormType`, `DFTConfig.norm_factor` in [dft/config.py](dftmodels/dft/config.py).*
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
### DFT corrections
|
|
62
|
+
|
|
63
|
+
An off-bin sinusoid produces a deterministic, structured residual in the DFT that is not captured by the WINDOW-only model. The figure below shows the residual reduction as corrections are applied in sequence. Without corrections, the RMS residual for a rectangular window at N = 100 samples is ~7×10⁻² V/√Hz; with all corrections at order 100 it drops to ~3×10⁻⁵ V/√Hz. For Hann and Nuttall windows, which taper to zero at both boundaries, the BASELINE correction is zero and the SAMPLING correction converges in a few orders; for rectangular and Bartlett windows, higher orders are required (see window sweep table in Notebook 02).
|
|
64
|
+
|
|
65
|
+

|
|
66
|
+
|
|
67
|
+
The corrections become irrelevant above a crossover acquisition length where the noise floor dominates. The figure below sweeps N at fixed f_s and shows the normalized model error for each correction level alongside reference noise floors at fixed σ/A ratios. Below the crossover, the fit is model-limited; above it, noise-limited.
|
|
68
|
+
|
|
69
|
+

|
|
70
|
+
|
|
71
|
+
*[Notebook 02](examples/02_dft_corrections.ipynb). Implementation: `DFTCorrection`, `DFTCorrectionMode` in [dft/correction.py](dftmodels/dft/correction.py).*
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
### Precision fitting — sinusoid
|
|
76
|
+
|
|
77
|
+
300 noise realizations of a rectangular-windowed sinusoid at f₀ = 10.42 Hz (0.42 bins from the nearest bin), T = 1 s, σ = 0.01 V. The table below compares four correction levels; the violin plots show the full error distribution.
|
|
78
|
+
|
|
79
|
+
| | freq RMSE (mHz) | amp RMSE (mV) | phase RMSE (mrad) |
|
|
80
|
+
|---|---|---|---|
|
|
81
|
+
| CRB | 0.39 | 1.41 | 0.98 |
|
|
82
|
+
| ★ None | 9.90 | 117.4 | 27.18 |
|
|
83
|
+
| ★ All (order 10) | 0.41 | 1.47 | 1.43 |
|
|
84
|
+
|
|
85
|
+
Frequency and amplitude RMSE approach the CRB once corrections eliminate the systematic bias.
|
|
86
|
+
|
|
87
|
+

|
|
88
|
+
|
|
89
|
+
*[Notebook 03](examples/03_precision_fitting.ipynb). Implementation: `SineFourier` in [models/sinusoid.py](dftmodels/models/sinusoid.py).*
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
### Decaying signals
|
|
94
|
+
|
|
95
|
+
For a decaying sinusoid x(t) = A·cos(2πf₀t + φ)·e^{−γt}, the CFT near f₀ is a complex Lorentzian: X(f) ≈ (Ae^{jφ}/2) / (γ + 2πj(f − f₀)). `SineFourier` with a nonzero `decay` parameter implements the exact analytical DFT expression and fits four parameters (frequency f₀, quadrature amplitudes Aᵢ = A cos φ and A_q = A sin φ, and decay rate γ); amplitude A, phase φ, and FWHM = γ/π are derived quantities. The Fisher information matrix for a decaying sinusoid has non-zero off-diagonal coupling between f and γ — the two parameters are not decoupled, unlike the pure sinusoid case.
|
|
96
|
+
|
|
97
|
+
300 realizations at σ = 0.05 V, T = 5 s, γ = 0.5 s⁻¹:
|
|
98
|
+
|
|
99
|
+
| | amp RMSE (mV) | phase RMSE (mrad) | freq RMSE (mHz) | decay RMSE (ms⁻¹) |
|
|
100
|
+
|---|---|---|---|---|
|
|
101
|
+
| CRB | 10.20 | 5.13 | 0.62 | 3.87 |
|
|
102
|
+
| ★ None | 59.98 | 5.93 | 0.67 | 34.41 |
|
|
103
|
+
| ★ All (order 10) | 10.03 | 5.14 | 0.61 | 3.83 |
|
|
104
|
+
|
|
105
|
+

|
|
106
|
+
|
|
107
|
+
Applying an exponential window w(t_n) = e^{−α·t_n/T} before the DFT shifts the effective decay to γ_eff = γ + α/T (line broadening). `LorentzianComplex` with `WindowType.EXPONENTIAL_ASYM` corrects for the window-induced decay shift and recovers the true γ from the broadened spectrum. A bare fit converges to γ_eff, biased by α/T.
|
|
108
|
+
|
|
109
|
+
*[Notebook 04](examples/04_decaying_signals.ipynb). Implementation: `LorentzianComplex` in [models/lorentzian.py](dftmodels/models/lorentzian.py).*
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
### Composite models
|
|
114
|
+
|
|
115
|
+
`CompositeModel` fits a weighted sum of named components simultaneously, with all parameters coupled through a shared covariance matrix. For overlapping spectral lines, sequential peak-by-peak fitting propagates subtraction errors; joint fitting avoids this. Each component's parameters are prefixed by name (`p1_frequency`, `p2_decay`, ...).
|
|
116
|
+
|
|
117
|
+
A J-coupled quartet — four Lorentzian lines with 1:3:3:1 binomial amplitudes at spacing J = 2.5 Hz — illustrates the setup. A linear complex background b₀ + b₁f is included as a fifth component; fitting without it forces the peaks to absorb the baseline, distorting amplitude ratios.
|
|
118
|
+
|
|
119
|
+

|
|
120
|
+
|
|
121
|
+
*Top: magnitude spectrum (grey), total fit (dashed), individual peak contributions (dotted), and background (dash-dot). Bottom: residuals with and without a background component in the model.*
|
|
122
|
+
|
|
123
|
+
The corrected composite estimator approaches the CRB; outer peaks (A = 1 V) have proportionally larger uncertainty than inner peaks (A = 3 V).
|
|
124
|
+
|
|
125
|
+
*[Notebook 05](examples/05_composite_model.ipynb). Implementation: `CompositeModel` in [models/composite.py](dftmodels/models/composite.py).*
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
### Window choice
|
|
130
|
+
|
|
131
|
+
The CRB is determined by the raw, unweighted time-domain data and is independent of the window. Any non-rectangular window downweights samples near the record boundaries, discarding Fisher information. This loss cannot be recovered by corrections. For an isolated peak, the rectangular window is the only window whose RMSE approaches the CRB; all others inflate variance proportionally to their effective bandwidth.
|
|
132
|
+
|
|
133
|
+

|
|
134
|
+
|
|
135
|
+
*RMSE for frequency (left) and amplitude (right) estimation across six windows at two fractional bin offsets δ = 0 and δ = 0.5. The CRB (dotted line) is the same for both offsets and all windows. N = 1000, f_s = 1000 Hz, σ = 0.05 V, 300 realizations.*
|
|
136
|
+
|
|
137
|
+
The tradeoff reverses when a nearby peak is absent from the model. Its spectral sidelobes leak into the fit region; the rectangular window has the broadest sidelobe structure (∝ sinc), so its estimator is most sensitive to this. At worst-case interferer offset (δ = 0.484, +5 bins), the rectangular window has a frequency RMSE of 13.5 mHz vs. 2.3 mHz for Hamming.
|
|
138
|
+
|
|
139
|
+
Window choice therefore depends on context: isolated peaks favour rectangular; peaks with unmodeled nearby components favour windowed estimators.
|
|
140
|
+
|
|
141
|
+
*[Notebook 06](examples/06_window_comparison.ipynb).*
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Usage
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
import numpy as np
|
|
149
|
+
from dftmodels import (
|
|
150
|
+
SignalSeries, NormType, WindowType, DFTRange,
|
|
151
|
+
DFTCorrection, DFTCorrectionMode, SineFourier,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
t = np.arange(0, 1.0, 1 / 100.0)
|
|
155
|
+
y = 2.0 * np.cos(2 * np.pi * 10.42 * t + 0.5)
|
|
156
|
+
y += np.random.normal(scale=0.01, size=len(t))
|
|
157
|
+
|
|
158
|
+
fourier = SignalSeries(x=t, y=y).calculate_dft(
|
|
159
|
+
norm=NormType.ASD, window=WindowType.RECTANGULAR,
|
|
160
|
+
dft_range=DFTRange.SINGLE_SIDED, pad=10.0,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
model = SineFourier(fourier.dft_config, DFTCorrection(DFTCorrectionMode.ALL, order=10))
|
|
164
|
+
params = model.make_params(
|
|
165
|
+
amplitude_i=2.0, amplitude_q=0.0, frequency=10.0,
|
|
166
|
+
frequency_min=8.0, frequency_max=13.0,
|
|
167
|
+
)
|
|
168
|
+
result = model.fit(fourier, params, mask=(fourier.x >= 8.0) & (fourier.x <= 13.0))
|
|
169
|
+
|
|
170
|
+
print(f"Amplitude : {model.amplitude(result.params):.6f} V")
|
|
171
|
+
print(f"Frequency : {model.center(result.params):.6f} Hz")
|
|
172
|
+
print(f"Phase : {model.phase(result.params):.6f} rad")
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
For decaying signals use `LorentzianComplex` with `NormType.CFT`. For simultaneous multi-peak fitting use `CompositeModel`. See the notebooks for detailed examples of each.
|
|
176
|
+
|
|
177
|
+
## Models
|
|
178
|
+
|
|
179
|
+
| Signal type | Model class | Windows |
|
|
180
|
+
|---|---|---|
|
|
181
|
+
| Sinusoid | `SineFourier` | All cosine-sum windows, Bartlett |
|
|
182
|
+
| Decaying sinusoid | `LorentzianComplex` | Rectangular, Exponential |
|
|
183
|
+
| Time-domain (decaying) sinusoid | `Sinusoid` | N/A |
|
|
184
|
+
| Custom | `ModelBase.build_model(fn)` | N/A |
|
|
185
|
+
|
|
186
|
+
## Limitations
|
|
187
|
+
|
|
188
|
+
**Scope of corrections.** The correction terms reduce systematic bias from the finite, sampled, windowed DFT. An alternative that achieves the same result is to define the signal model in the time domain and compute the DFT numerically at each evaluation. The analytical corrections are a closed-form shortcut to that approach; they are most efficient when the signal model is simple and the number of evaluations is large. For short-duration signals with few samples, the time-domain approach may be simpler.
|
|
189
|
+
|
|
190
|
+
**Noise model.** The Cramér–Rao bounds in the examples assume additive white Gaussian noise. Colored noise or non-Gaussian distributions will produce different variance floors; the model residuals remain unbiased but the optimality guarantee does not carry over.
|
|
191
|
+
|
|
192
|
+
**Model vs. estimator.** The classes in this package are models, not estimators. Combined with a least-squares fitting procedure (`lmfit.Minimizer`), they form estimators. The CRB is a property of the estimator (model + fitting algorithm + noise model), not of the model alone.
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# dftmodels
|
|
2
|
+
|
|
3
|
+
Analytical DFT models for spectral parameter estimation.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Fitting peaks in DFT spectra with continuous Fourier transform (CFT) models (sinc functions, Lorentzians, and their window-convolved variants) introduces systematic errors in the estimated amplitude, frequency, phase, and linewidth. Three effects are responsible: the DFT sum gives full weight to the boundary samples where the trapezoidal-rule approximation gives half weight (boundary mismatch); sampling makes the spectrum periodic, introducing aliased copies at f ± k·f_s (aliasing); and the finite window modifies the spectral shape in a way that depends on both the window coefficients and the signal frequency (windowing). Each effect produces a deterministic, structured residual in the DFT.
|
|
8
|
+
|
|
9
|
+
`dftmodels` provides closed-form analytical models that include these effects as additive correction terms: WINDOW (the CFT of the windowed signal), BASELINE (the boundary weight mismatch), and SAMPLING (aliased spectral copies up to a configurable order). Combined with least-squares fitting, these models form estimators whose variance can approach the Cramér–Rao bound (CRB).
|
|
10
|
+
|
|
11
|
+
**When corrections matter.** Systematic bias is only the limiting factor when SNR is high and records are short. The SAMPLING correction becomes negligible above a crossover acquisition length that depends on window and noise level (see [Notebook 02](examples/02_dft_corrections.ipynb)). For long records or when noise dominates, a CFT-based sinc or Lorentzian model is adequate. The gains shown in the examples below are large by design — the signal parameters were chosen so that systematic bias is the limiting factor, not noise. The package is most relevant when that condition holds.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install dftmodels
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Dependencies: Python ≥ 3.12, `numpy` ≥ 2.0, `scipy` ≥ 1.15, `lmfit` ≥ 1.0.
|
|
20
|
+
|
|
21
|
+
## Examples
|
|
22
|
+
|
|
23
|
+
Six notebooks in [examples/](examples/) work through the main topics in sequence.
|
|
24
|
+
|
|
25
|
+
| Notebook | Topic |
|
|
26
|
+
|---|---|
|
|
27
|
+
| [01 — Normalizations](examples/01_dft_normalization.ipynb) | ASD, PSD, and CFT normalizations; Parseval's theorem |
|
|
28
|
+
| [02 — DFT corrections](examples/02_dft_corrections.ipynb) | Origin and convergence of the three correction terms |
|
|
29
|
+
| [03 — Precision fitting](examples/03_precision_fitting.ipynb) | Sinusoid parameter estimation; bias elimination; Cramér–Rao bound |
|
|
30
|
+
| [04 — Decaying signals](examples/04_decaying_signals.ipynb) | Lorentzian fitting; exponential windows; line broadening |
|
|
31
|
+
| [05 — Composite models](examples/05_composite_model.ipynb) | Simultaneous multi-peak fitting; spectral background |
|
|
32
|
+
| [06 — Window comparison](examples/06_window_comparison.ipynb) | Window choice: Fisher information loss vs. sidelobe suppression |
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
### Normalizations
|
|
37
|
+
|
|
38
|
+
Three normalizations are provided. **ASD** and **PSD** satisfy Parseval's theorem — ∫|ASD(f)|² df = ∫PSD(f) df = RMS² — and are appropriate for stationary signals. The normalization factor includes the window RMS (w_rms) to compensate for amplitude attenuation; this correction is exact in the limit N → ∞ and introduces a residual error of order 1/N. **CFT** normalization satisfies ∫X_CFT(f) df = x(0) and is appropriate for decaying signals, where total power depends on acquisition length and ASD peak height grows with T.
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
*Top: CFT normalization — the spectral integral converges to x(0) = A for both decaying (left cluster, 8–12 Hz) and stationary sinusoids (right cluster, 18–22 Hz), independent of T. Bottom: ASD normalization — for stationary sinusoids √∫|ASD|² = RMS is stable; for decaying sinusoids the integral grows with T.*
|
|
43
|
+
|
|
44
|
+
*[Notebook 01](examples/01_dft_normalization.ipynb). Implementation: `NormType`, `DFTConfig.norm_factor` in [dft/config.py](dftmodels/dft/config.py).*
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
### DFT corrections
|
|
49
|
+
|
|
50
|
+
An off-bin sinusoid produces a deterministic, structured residual in the DFT that is not captured by the WINDOW-only model. The figure below shows the residual reduction as corrections are applied in sequence. Without corrections, the RMS residual for a rectangular window at N = 100 samples is ~7×10⁻² V/√Hz; with all corrections at order 100 it drops to ~3×10⁻⁵ V/√Hz. For Hann and Nuttall windows, which taper to zero at both boundaries, the BASELINE correction is zero and the SAMPLING correction converges in a few orders; for rectangular and Bartlett windows, higher orders are required (see window sweep table in Notebook 02).
|
|
51
|
+
|
|
52
|
+

|
|
53
|
+
|
|
54
|
+
The corrections become irrelevant above a crossover acquisition length where the noise floor dominates. The figure below sweeps N at fixed f_s and shows the normalized model error for each correction level alongside reference noise floors at fixed σ/A ratios. Below the crossover, the fit is model-limited; above it, noise-limited.
|
|
55
|
+
|
|
56
|
+

|
|
57
|
+
|
|
58
|
+
*[Notebook 02](examples/02_dft_corrections.ipynb). Implementation: `DFTCorrection`, `DFTCorrectionMode` in [dft/correction.py](dftmodels/dft/correction.py).*
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
### Precision fitting — sinusoid
|
|
63
|
+
|
|
64
|
+
300 noise realizations of a rectangular-windowed sinusoid at f₀ = 10.42 Hz (0.42 bins from the nearest bin), T = 1 s, σ = 0.01 V. The table below compares four correction levels; the violin plots show the full error distribution.
|
|
65
|
+
|
|
66
|
+
| | freq RMSE (mHz) | amp RMSE (mV) | phase RMSE (mrad) |
|
|
67
|
+
|---|---|---|---|
|
|
68
|
+
| CRB | 0.39 | 1.41 | 0.98 |
|
|
69
|
+
| ★ None | 9.90 | 117.4 | 27.18 |
|
|
70
|
+
| ★ All (order 10) | 0.41 | 1.47 | 1.43 |
|
|
71
|
+
|
|
72
|
+
Frequency and amplitude RMSE approach the CRB once corrections eliminate the systematic bias.
|
|
73
|
+
|
|
74
|
+

|
|
75
|
+
|
|
76
|
+
*[Notebook 03](examples/03_precision_fitting.ipynb). Implementation: `SineFourier` in [models/sinusoid.py](dftmodels/models/sinusoid.py).*
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
### Decaying signals
|
|
81
|
+
|
|
82
|
+
For a decaying sinusoid x(t) = A·cos(2πf₀t + φ)·e^{−γt}, the CFT near f₀ is a complex Lorentzian: X(f) ≈ (Ae^{jφ}/2) / (γ + 2πj(f − f₀)). `SineFourier` with a nonzero `decay` parameter implements the exact analytical DFT expression and fits four parameters (frequency f₀, quadrature amplitudes Aᵢ = A cos φ and A_q = A sin φ, and decay rate γ); amplitude A, phase φ, and FWHM = γ/π are derived quantities. The Fisher information matrix for a decaying sinusoid has non-zero off-diagonal coupling between f and γ — the two parameters are not decoupled, unlike the pure sinusoid case.
|
|
83
|
+
|
|
84
|
+
300 realizations at σ = 0.05 V, T = 5 s, γ = 0.5 s⁻¹:
|
|
85
|
+
|
|
86
|
+
| | amp RMSE (mV) | phase RMSE (mrad) | freq RMSE (mHz) | decay RMSE (ms⁻¹) |
|
|
87
|
+
|---|---|---|---|---|
|
|
88
|
+
| CRB | 10.20 | 5.13 | 0.62 | 3.87 |
|
|
89
|
+
| ★ None | 59.98 | 5.93 | 0.67 | 34.41 |
|
|
90
|
+
| ★ All (order 10) | 10.03 | 5.14 | 0.61 | 3.83 |
|
|
91
|
+
|
|
92
|
+

|
|
93
|
+
|
|
94
|
+
Applying an exponential window w(t_n) = e^{−α·t_n/T} before the DFT shifts the effective decay to γ_eff = γ + α/T (line broadening). `LorentzianComplex` with `WindowType.EXPONENTIAL_ASYM` corrects for the window-induced decay shift and recovers the true γ from the broadened spectrum. A bare fit converges to γ_eff, biased by α/T.
|
|
95
|
+
|
|
96
|
+
*[Notebook 04](examples/04_decaying_signals.ipynb). Implementation: `LorentzianComplex` in [models/lorentzian.py](dftmodels/models/lorentzian.py).*
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
### Composite models
|
|
101
|
+
|
|
102
|
+
`CompositeModel` fits a weighted sum of named components simultaneously, with all parameters coupled through a shared covariance matrix. For overlapping spectral lines, sequential peak-by-peak fitting propagates subtraction errors; joint fitting avoids this. Each component's parameters are prefixed by name (`p1_frequency`, `p2_decay`, ...).
|
|
103
|
+
|
|
104
|
+
A J-coupled quartet — four Lorentzian lines with 1:3:3:1 binomial amplitudes at spacing J = 2.5 Hz — illustrates the setup. A linear complex background b₀ + b₁f is included as a fifth component; fitting without it forces the peaks to absorb the baseline, distorting amplitude ratios.
|
|
105
|
+
|
|
106
|
+

|
|
107
|
+
|
|
108
|
+
*Top: magnitude spectrum (grey), total fit (dashed), individual peak contributions (dotted), and background (dash-dot). Bottom: residuals with and without a background component in the model.*
|
|
109
|
+
|
|
110
|
+
The corrected composite estimator approaches the CRB; outer peaks (A = 1 V) have proportionally larger uncertainty than inner peaks (A = 3 V).
|
|
111
|
+
|
|
112
|
+
*[Notebook 05](examples/05_composite_model.ipynb). Implementation: `CompositeModel` in [models/composite.py](dftmodels/models/composite.py).*
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
### Window choice
|
|
117
|
+
|
|
118
|
+
The CRB is determined by the raw, unweighted time-domain data and is independent of the window. Any non-rectangular window downweights samples near the record boundaries, discarding Fisher information. This loss cannot be recovered by corrections. For an isolated peak, the rectangular window is the only window whose RMSE approaches the CRB; all others inflate variance proportionally to their effective bandwidth.
|
|
119
|
+
|
|
120
|
+

|
|
121
|
+
|
|
122
|
+
*RMSE for frequency (left) and amplitude (right) estimation across six windows at two fractional bin offsets δ = 0 and δ = 0.5. The CRB (dotted line) is the same for both offsets and all windows. N = 1000, f_s = 1000 Hz, σ = 0.05 V, 300 realizations.*
|
|
123
|
+
|
|
124
|
+
The tradeoff reverses when a nearby peak is absent from the model. Its spectral sidelobes leak into the fit region; the rectangular window has the broadest sidelobe structure (∝ sinc), so its estimator is most sensitive to this. At worst-case interferer offset (δ = 0.484, +5 bins), the rectangular window has a frequency RMSE of 13.5 mHz vs. 2.3 mHz for Hamming.
|
|
125
|
+
|
|
126
|
+
Window choice therefore depends on context: isolated peaks favour rectangular; peaks with unmodeled nearby components favour windowed estimators.
|
|
127
|
+
|
|
128
|
+
*[Notebook 06](examples/06_window_comparison.ipynb).*
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Usage
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
import numpy as np
|
|
136
|
+
from dftmodels import (
|
|
137
|
+
SignalSeries, NormType, WindowType, DFTRange,
|
|
138
|
+
DFTCorrection, DFTCorrectionMode, SineFourier,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
t = np.arange(0, 1.0, 1 / 100.0)
|
|
142
|
+
y = 2.0 * np.cos(2 * np.pi * 10.42 * t + 0.5)
|
|
143
|
+
y += np.random.normal(scale=0.01, size=len(t))
|
|
144
|
+
|
|
145
|
+
fourier = SignalSeries(x=t, y=y).calculate_dft(
|
|
146
|
+
norm=NormType.ASD, window=WindowType.RECTANGULAR,
|
|
147
|
+
dft_range=DFTRange.SINGLE_SIDED, pad=10.0,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
model = SineFourier(fourier.dft_config, DFTCorrection(DFTCorrectionMode.ALL, order=10))
|
|
151
|
+
params = model.make_params(
|
|
152
|
+
amplitude_i=2.0, amplitude_q=0.0, frequency=10.0,
|
|
153
|
+
frequency_min=8.0, frequency_max=13.0,
|
|
154
|
+
)
|
|
155
|
+
result = model.fit(fourier, params, mask=(fourier.x >= 8.0) & (fourier.x <= 13.0))
|
|
156
|
+
|
|
157
|
+
print(f"Amplitude : {model.amplitude(result.params):.6f} V")
|
|
158
|
+
print(f"Frequency : {model.center(result.params):.6f} Hz")
|
|
159
|
+
print(f"Phase : {model.phase(result.params):.6f} rad")
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
For decaying signals use `LorentzianComplex` with `NormType.CFT`. For simultaneous multi-peak fitting use `CompositeModel`. See the notebooks for detailed examples of each.
|
|
163
|
+
|
|
164
|
+
## Models
|
|
165
|
+
|
|
166
|
+
| Signal type | Model class | Windows |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| Sinusoid | `SineFourier` | All cosine-sum windows, Bartlett |
|
|
169
|
+
| Decaying sinusoid | `LorentzianComplex` | Rectangular, Exponential |
|
|
170
|
+
| Time-domain (decaying) sinusoid | `Sinusoid` | N/A |
|
|
171
|
+
| Custom | `ModelBase.build_model(fn)` | N/A |
|
|
172
|
+
|
|
173
|
+
## Limitations
|
|
174
|
+
|
|
175
|
+
**Scope of corrections.** The correction terms reduce systematic bias from the finite, sampled, windowed DFT. An alternative that achieves the same result is to define the signal model in the time domain and compute the DFT numerically at each evaluation. The analytical corrections are a closed-form shortcut to that approach; they are most efficient when the signal model is simple and the number of evaluations is large. For short-duration signals with few samples, the time-domain approach may be simpler.
|
|
176
|
+
|
|
177
|
+
**Noise model.** The Cramér–Rao bounds in the examples assume additive white Gaussian noise. Colored noise or non-Gaussian distributions will produce different variance floors; the model residuals remain unbiased but the optimality guarantee does not carry over.
|
|
178
|
+
|
|
179
|
+
**Model vs. estimator.** The classes in this package are models, not estimators. Combined with a least-squares fitting procedure (`lmfit.Minimizer`), they form estimators. The CRB is a property of the estimator (model + fitting algorithm + noise model), not of the model alone.
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from importlib.metadata import version
|
|
2
|
+
from .stats import cramer_rao_bound
|
|
3
|
+
|
|
4
|
+
__version__ = version("dftmodels")
|
|
5
|
+
from .dft import (
|
|
6
|
+
DFTConfig,
|
|
7
|
+
NormType,
|
|
8
|
+
DFTRange,
|
|
9
|
+
WindowType,
|
|
10
|
+
DFTCorrection,
|
|
11
|
+
DFTCorrectionMode,
|
|
12
|
+
SignalSeries,
|
|
13
|
+
FourierSeries,
|
|
14
|
+
)
|
|
15
|
+
from .models import (
|
|
16
|
+
FourierModelBase,
|
|
17
|
+
Sinusoid,
|
|
18
|
+
SineFourier,
|
|
19
|
+
CompositeModel,
|
|
20
|
+
ModelBase,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"__version__",
|
|
25
|
+
# Stats
|
|
26
|
+
"cramer_rao_bound",
|
|
27
|
+
# DFT
|
|
28
|
+
"DFTConfig",
|
|
29
|
+
"NormType",
|
|
30
|
+
"DFTRange",
|
|
31
|
+
"WindowType",
|
|
32
|
+
"DFTCorrection",
|
|
33
|
+
"DFTCorrectionMode",
|
|
34
|
+
"SignalSeries",
|
|
35
|
+
"FourierSeries",
|
|
36
|
+
# Models
|
|
37
|
+
"FourierModelBase",
|
|
38
|
+
"ModelBase",
|
|
39
|
+
"Sinusoid",
|
|
40
|
+
"SineFourier",
|
|
41
|
+
"CompositeModel",
|
|
42
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .config import DFTConfig, NormType, DFTRange, WindowType, get_window_array
|
|
2
|
+
from .correction import DFTCorrection, DFTCorrectionMode
|
|
3
|
+
from .series import SignalSeries, FourierSeries
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"DFTConfig",
|
|
7
|
+
"NormType",
|
|
8
|
+
"DFTRange",
|
|
9
|
+
"WindowType",
|
|
10
|
+
"get_window_array",
|
|
11
|
+
"DFTCorrection",
|
|
12
|
+
"DFTCorrectionMode",
|
|
13
|
+
"SignalSeries",
|
|
14
|
+
"FourierSeries",
|
|
15
|
+
]
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
from dataclasses import dataclass, replace, field
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from functools import cached_property
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from numpy.typing import NDArray
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WindowType(str, Enum):
|
|
10
|
+
# Cosine-sum windows
|
|
11
|
+
RECTANGULAR = "rectangular"
|
|
12
|
+
HAMMING = "hamming"
|
|
13
|
+
HANN = "hann"
|
|
14
|
+
DIRICHLET = "rectangular" # alias for RECTANGULAR
|
|
15
|
+
BLACKMAN = "blackman"
|
|
16
|
+
NUTTAL = "nuttal"
|
|
17
|
+
BLACKMAN_NUTTAL = "blackman-nuttal"
|
|
18
|
+
BLACKMAN_HARRIS = "blackman-harris"
|
|
19
|
+
FLAT_TOP = "flat-top"
|
|
20
|
+
|
|
21
|
+
# For Debug Only
|
|
22
|
+
# COS4N = "cos-4n"
|
|
23
|
+
# COS6N = "cos-6n"
|
|
24
|
+
# COS8N = "cos-8n"
|
|
25
|
+
|
|
26
|
+
# Other
|
|
27
|
+
EXPONENTIAL_ASYM = "exponential-asymmetric"
|
|
28
|
+
BARTLETT = "bartlett"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_window_array(window: WindowType, n: int, **params) -> NDArray[np.floating]:
|
|
33
|
+
match window:
|
|
34
|
+
case WindowType.RECTANGULAR:
|
|
35
|
+
return np.ones(n)
|
|
36
|
+
case WindowType.HAMMING:
|
|
37
|
+
return 25/46 - 21/46 * np.cos(2 * np.pi * np.arange(n) / (n - 1))
|
|
38
|
+
case WindowType.HANN:
|
|
39
|
+
return 0.5 - 0.5 * np.cos(2 * np.pi * np.arange(n) / (n - 1))
|
|
40
|
+
case WindowType.BLACKMAN:
|
|
41
|
+
return (7938
|
|
42
|
+
- 9240 * np.cos(2 * np.pi * np.arange(n) / (n - 1))
|
|
43
|
+
+ 1430 * np.cos(4 * np.pi * np.arange(n) / (n - 1))
|
|
44
|
+
) / 18608
|
|
45
|
+
case WindowType.NUTTAL:
|
|
46
|
+
return (355_768 \
|
|
47
|
+
- 487_396 * np.cos(2 * np.pi * np.arange(n) / (n - 1))
|
|
48
|
+
+ 144_232 * np.cos(4 * np.pi * np.arange(n) / (n - 1))
|
|
49
|
+
- 12_604 * np.cos(6 * np.pi * np.arange(n) / (n - 1))
|
|
50
|
+
) / 1_000_000
|
|
51
|
+
case WindowType.BLACKMAN_NUTTAL:
|
|
52
|
+
return (3_635_819 \
|
|
53
|
+
- 4_891_775 * np.cos(2 * np.pi * np.arange(n) / (n - 1))
|
|
54
|
+
+ 1_365_995 * np.cos(4 * np.pi * np.arange(n) / (n - 1))
|
|
55
|
+
- 106_411 * np.cos(6 * np.pi * np.arange(n) / (n - 1))
|
|
56
|
+
) / 10_000_000
|
|
57
|
+
case WindowType.BLACKMAN_HARRIS:
|
|
58
|
+
return (4_243_801 \
|
|
59
|
+
- 4_973_406 * np.cos(2 * np.pi * np.arange(n) / (n - 1))
|
|
60
|
+
+ 782_793 * np.cos(4 * np.pi * np.arange(n) / (n - 1))
|
|
61
|
+
) / 10_000_000
|
|
62
|
+
case WindowType.FLAT_TOP:
|
|
63
|
+
return (215_578_950 \
|
|
64
|
+
- 416_631_580 * np.cos(2 * np.pi * np.arange(n) / (n - 1))
|
|
65
|
+
+ 277_263_158 * np.cos(4 * np.pi * np.arange(n) / (n - 1))
|
|
66
|
+
- 83_578_947 * np.cos(6 * np.pi * np.arange(n) / (n - 1))
|
|
67
|
+
+ 6_947_368 * np.cos(8 * np.pi * np.arange(n) / (n - 1))
|
|
68
|
+
) / 1_000_000_000
|
|
69
|
+
case WindowType.BARTLETT:
|
|
70
|
+
return np.bartlett(n)
|
|
71
|
+
# case WindowType.COS6N:
|
|
72
|
+
# return .5 - .5 * np.cos(6 * np.pi * np.arange(n) / (n - 1))
|
|
73
|
+
# case WindowType.COS8N:
|
|
74
|
+
# return .5 - .5 * np.cos(8 * np.pi * np.arange(n) / (n - 1))
|
|
75
|
+
case WindowType.EXPONENTIAL_ASYM:
|
|
76
|
+
return np.exp(-np.arange(n) * params.get("alpha", 1.0) / (n - 1))
|
|
77
|
+
case _:
|
|
78
|
+
raise ValueError(f"Unsupported window type: {window}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class NormType(str, Enum):
|
|
82
|
+
CFT = "continuous_fourier_transform"
|
|
83
|
+
ASD = "amplitude_spectral_density"
|
|
84
|
+
ASD_ABS = "amplitude_spectral_density_absolute"
|
|
85
|
+
PSD = "power_spectral_density"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class DFTRange(str, Enum):
|
|
89
|
+
DOUBLE_SIDED = "double_sided"
|
|
90
|
+
SINGLE_SIDED = "single_sided"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class DFTConfig:
|
|
95
|
+
number_of_samples: int
|
|
96
|
+
sample_rate: float
|
|
97
|
+
pad: float = 1.0
|
|
98
|
+
window: WindowType = WindowType.RECTANGULAR
|
|
99
|
+
window_params: dict = field(default_factory=dict) # <--- NEW
|
|
100
|
+
norm_type: NormType = NormType.ASD
|
|
101
|
+
dft_range: DFTRange = DFTRange.DOUBLE_SIDED
|
|
102
|
+
|
|
103
|
+
def __post_init__(self):
|
|
104
|
+
if self.number_of_samples <= 0:
|
|
105
|
+
raise ValueError("number_of_samples must be a positive integer")
|
|
106
|
+
if self.sample_rate <= 0:
|
|
107
|
+
raise ValueError("sample_rate must be positive")
|
|
108
|
+
if self.pad < 1.0:
|
|
109
|
+
raise ValueError("pad cannot be less than 1.0")
|
|
110
|
+
if self.window not in WindowType:
|
|
111
|
+
raise ValueError(f"Unsupported window type: {self.window}")
|
|
112
|
+
if self.norm_type not in NormType:
|
|
113
|
+
raise ValueError(f"Unsupported norm type: {self.norm_type}")
|
|
114
|
+
if self.dft_range not in DFTRange:
|
|
115
|
+
raise ValueError(f"Unsupported DFT range: {self.dft_range}")
|
|
116
|
+
|
|
117
|
+
@cached_property
|
|
118
|
+
def number_of_samples_fft(self) -> int:
|
|
119
|
+
return round(self.number_of_samples * self.pad)
|
|
120
|
+
|
|
121
|
+
@cached_property
|
|
122
|
+
def window_array(self) -> NDArray[np.floating]:
|
|
123
|
+
return get_window_array(self.window, self.number_of_samples, **self.window_params)
|
|
124
|
+
|
|
125
|
+
@cached_property
|
|
126
|
+
def norm_factor(self) -> float:
|
|
127
|
+
if self.norm_type in (NormType.ASD, NormType.ASD_ABS):
|
|
128
|
+
return self._asd_norm()
|
|
129
|
+
elif self.norm_type == NormType.PSD:
|
|
130
|
+
return self._asd_norm() ** 2
|
|
131
|
+
elif self.norm_type == NormType.CFT:
|
|
132
|
+
return self._cft_norm()
|
|
133
|
+
raise ValueError(f"Unsupported norm type: {self.norm_type}")
|
|
134
|
+
|
|
135
|
+
def _cft_norm(self) -> float:
|
|
136
|
+
wa = self.window_array
|
|
137
|
+
if wa[0] == 0.0:
|
|
138
|
+
raise ValueError('Zero-valued window is incompatible with CFT normalization')
|
|
139
|
+
factor = 2.0 if self.dft_range == DFTRange.SINGLE_SIDED else 1.0
|
|
140
|
+
return factor / self.sample_rate / wa[0]
|
|
141
|
+
|
|
142
|
+
def _asd_norm(self) -> float:
|
|
143
|
+
wa = self.window_array
|
|
144
|
+
n = self.number_of_samples
|
|
145
|
+
window_rms = np.sqrt(np.sum(wa ** 2) / n)
|
|
146
|
+
factor = np.sqrt(2.0) if self.dft_range == DFTRange.SINGLE_SIDED else 1.0
|
|
147
|
+
return factor / np.sqrt(self.sample_rate * n) / window_rms
|
|
148
|
+
|
|
149
|
+
@cached_property
|
|
150
|
+
def frequency_step(self) -> float:
|
|
151
|
+
return self.sample_rate / self.number_of_samples_fft
|
|
152
|
+
|
|
153
|
+
@cached_property
|
|
154
|
+
def frequency_min(self) -> float:
|
|
155
|
+
if self.dft_range == DFTRange.SINGLE_SIDED:
|
|
156
|
+
return 0.0
|
|
157
|
+
n = self.number_of_samples_fft
|
|
158
|
+
df = self.frequency_step
|
|
159
|
+
return -(self.sample_rate + df) / 2 if n % 2 == 0 else -self.sample_rate / 2
|
|
160
|
+
|
|
161
|
+
@cached_property
|
|
162
|
+
def frequency_max(self) -> float:
|
|
163
|
+
if self.dft_range == DFTRange.SINGLE_SIDED:
|
|
164
|
+
return self.sample_rate / 2
|
|
165
|
+
n = self.number_of_samples_fft
|
|
166
|
+
df = self.frequency_step
|
|
167
|
+
return (self.sample_rate - df) / 2 if n % 2 == 0 else self.sample_rate / 2
|
|
168
|
+
|
|
169
|
+
def copy(self) -> 'DFTConfig':
|
|
170
|
+
return replace(self)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import IntFlag, auto
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class DFTCorrectionMode(IntFlag):
|
|
6
|
+
NONE = 0
|
|
7
|
+
BASELINE_ONLY = auto()
|
|
8
|
+
SAMPLING_ONLY = auto()
|
|
9
|
+
WINDOW_ONLY = auto()
|
|
10
|
+
|
|
11
|
+
WINDOW = WINDOW_ONLY
|
|
12
|
+
BASELINE = WINDOW_ONLY | BASELINE_ONLY
|
|
13
|
+
ALL = WINDOW_ONLY | BASELINE_ONLY | SAMPLING_ONLY
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class DFTCorrection:
|
|
18
|
+
mode: DFTCorrectionMode
|
|
19
|
+
order: int = 10
|