1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
//! Public plans and reports shared by classical spectral estimators.
use crate::{SignalError, WindowMetrics, WindowSpec};
/// Whether reported non-DC power represents one or both frequency signs.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SpectrumSide {
/// Report non-negative frequencies and fold negative-frequency power into
/// interior bins.
#[default]
OneSided,
/// Report the complete signed spectrum without mirror folding.
TwoSided,
}
/// Policy that constructs the frequencies evaluated by an estimator.
#[derive(Clone, Debug, PartialEq)]
pub enum FrequencyGridPolicy {
/// Use the exact DFT bins implied by the plan's transform length.
FftBins {
/// One-sided folded bins or the complete signed grid.
side: SpectrumSide,
},
/// Evaluate an inclusive, evenly spaced grid by direct Fourier sums.
Linear {
/// First frequency in hertz.
start_hz: f64,
/// Last frequency in hertz.
end_hz: f64,
/// Number of frequencies, including both endpoints.
bins: usize,
/// Whether negative-frequency power is folded into the result.
side: SpectrumSide,
},
/// Evaluate caller-supplied, strictly increasing frequencies.
Explicit {
/// Frequencies in hertz.
frequencies_hz: Vec<f64>,
/// Whether negative-frequency power is folded into the result.
side: SpectrumSide,
},
}
impl Default for FrequencyGridPolicy {
fn default() -> Self {
Self::FftBins {
side: SpectrumSide::OneSided,
}
}
}
/// Output units for a Fourier power estimate.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SpectrumScalingKind {
/// Squared signal units, corrected for coherent window gain.
Power,
/// Squared signal units per hertz, corrected for window energy.
#[default]
Density,
/// Fraction of centered variance explained by a sinusoidal least-squares
/// fit. This is the generalized Lomb-Scargle normalization.
LombScargleNormalized,
}
/// Exact denominator and folding convention used to reconstruct output scale.
#[derive(Clone, Debug, PartialEq)]
pub struct SpectrumScaling {
/// Semantic output units.
pub kind: SpectrumScalingKind,
/// Sample rate used by Fourier estimators, when applicable.
pub sample_rate_hz: Option<f64>,
/// Divisor applied to squared Fourier magnitude or variance reduction.
pub normalization_denominator: f64,
/// Whether negative-frequency power is folded into positive frequencies.
pub one_sided: bool,
/// Multiplier for non-DC, non-Nyquist bins under one-sided folding.
pub interior_bin_multiplier: f64,
}
/// Explicit resource ceilings applied before estimator work begins.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct EstimatorLimits {
/// Maximum transform length.
pub max_fft_len: usize,
/// Maximum number of Welch segments.
pub max_segments: usize,
/// Maximum number of Slepian tapers.
pub max_tapers: usize,
/// Maximum number of requested output frequencies.
pub max_frequency_bins: usize,
/// Conservative deterministic work-unit ceiling.
pub max_work: u64,
}
impl Default for EstimatorLimits {
fn default() -> Self {
Self {
max_fft_len: 16_384,
max_segments: 4_096,
max_tapers: 16,
max_frequency_bins: 16_385,
max_work: 100_000_000,
}
}
}
/// Estimator that produced a report.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EstimatorKind {
/// Single-window periodogram.
Periodogram,
/// Averaged, overlapping Welch periodogram.
Welch,
/// Averaged auto/cross spectrum and coherence.
CrossSpectrum,
/// Slepian discrete-prolate multitaper estimate.
SlepianMultitaper,
/// Uneven-sample generalized Lomb-Scargle estimate.
LombScargle,
/// Burg autoregressive maximum-entropy estimate.
MaximumEntropy,
}
/// Work, averaging, grid, and window facts retained beside an estimate.
#[derive(Clone, Debug, PartialEq)]
pub struct EstimatorEvidence {
/// Estimator family.
pub estimator: EstimatorKind,
/// Number of input observations admitted by the plan.
pub input_len: usize,
/// Transform length or reference grid length.
pub fft_len: usize,
/// Samples per segment or taper.
pub segment_len: usize,
/// Number of admitted data segments.
pub segment_count: usize,
/// Number of Slepian tapers.
pub taper_count: usize,
/// Number of output frequencies.
pub frequency_bins: usize,
/// Conservative work charged before execution.
pub work_units: u64,
/// Work ceiling that admitted this execution.
pub work_limit: u64,
/// Nominal independent chi-square or residual degrees of freedom.
pub degrees_of_freedom: f64,
/// Resolved frequency-grid policy.
pub frequency_grid: FrequencyGridPolicy,
/// Generated window metrics for periodogram-family estimates.
pub window: Option<WindowMetrics>,
/// Spectral concentration of each selected Slepian taper.
pub taper_concentrations: Vec<f64>,
}
/// Power values and all policy/evidence needed to interpret their scale.
#[derive(Clone, Debug, PartialEq)]
pub struct SpectrumEstimate {
/// Evaluated frequencies in hertz.
pub frequency: Vec<f64>,
/// Power, density, or normalized variance reduction at each frequency.
pub power: Vec<f64>,
/// Exact scaling convention.
pub scaling: SpectrumScaling,
/// Bounded-work and averaging evidence.
pub evidence: EstimatorEvidence,
}
/// Complex cross power, auto power, and magnitude-squared coherence.
#[derive(Clone, Debug, PartialEq)]
pub struct CrossSpectrumEstimate {
/// Evaluated frequencies in hertz.
pub frequency: Vec<f64>,
/// Complex `X * conjugate(Y)` values as `(real, imaginary)` pairs.
pub cross_power: Vec<(f64, f64)>,
/// Auto power for the first signal.
pub x_power: Vec<f64>,
/// Auto power for the second signal.
pub y_power: Vec<f64>,
/// Magnitude-squared coherence, clamped to `[0, 1]`.
pub coherence: Vec<f64>,
/// Exact scaling convention shared by cross and auto power.
pub scaling: SpectrumScaling,
/// Bounded-work and averaging evidence.
pub evidence: EstimatorEvidence,
}
/// Single-record periodogram policy.
#[derive(Clone, Debug, PartialEq)]
pub struct PeriodogramPlan {
/// Sample rate in hertz.
pub sample_rate_hz: f64,
/// Transform length, including any zero padding.
pub fft_len: usize,
/// Analysis-window policy.
pub window: WindowSpec,
/// Frequency-grid policy.
pub grid: FrequencyGridPolicy,
/// Power or power-density output.
pub scaling: SpectrumScalingKind,
/// Resource ceilings.
pub limits: EstimatorLimits,
}
impl PeriodogramPlan {
/// Creates a one-sided density periodogram with a symmetric Hann window.
pub fn new(sample_rate_hz: f64, fft_len: usize) -> Self {
Self {
sample_rate_hz,
fft_len,
window: WindowSpec::default(),
grid: FrequencyGridPolicy::default(),
scaling: SpectrumScalingKind::Density,
limits: EstimatorLimits::default(),
}
}
}
/// Segment, overlap, and scaling policy shared by Welch and cross spectra.
#[derive(Clone, Debug, PartialEq)]
pub struct WelchPlan {
/// Sample rate in hertz.
pub sample_rate_hz: f64,
/// Samples in each complete segment.
pub segment_len: usize,
/// Reused samples between adjacent segments.
pub overlap: usize,
/// Transform length, including segment zero padding.
pub fft_len: usize,
/// Analysis-window policy.
pub window: WindowSpec,
/// Frequency-grid policy.
pub grid: FrequencyGridPolicy,
/// Power or power-density output.
pub scaling: SpectrumScalingKind,
/// Resource ceilings.
pub limits: EstimatorLimits,
}
impl WelchPlan {
/// Creates a 50%-overlapped, one-sided Hann density plan.
pub fn new(sample_rate_hz: f64, segment_len: usize) -> Self {
Self {
sample_rate_hz,
segment_len,
overlap: segment_len / 2,
fft_len: segment_len,
window: WindowSpec::default(),
grid: FrequencyGridPolicy::default(),
scaling: SpectrumScalingKind::Density,
limits: EstimatorLimits::default(),
}
}
}
/// Slepian multitaper policy with explicit bandwidth and taper count.
#[derive(Clone, Debug, PartialEq)]
pub struct MultitaperPlan {
/// Sample rate in hertz.
pub sample_rate_hz: f64,
/// Transform length, including zero padding.
pub fft_len: usize,
/// Time-half-bandwidth product `N * W`, strictly between zero and `N / 2`.
pub time_bandwidth: f64,
/// Number of leading discrete prolate tapers to average.
pub taper_count: usize,
/// Frequency-grid policy.
pub grid: FrequencyGridPolicy,
/// Resource ceilings.
pub limits: EstimatorLimits,
}
impl MultitaperPlan {
/// Creates a one-sided density plan.
pub fn new(
sample_rate_hz: f64,
fft_len: usize,
time_bandwidth: f64,
taper_count: usize,
) -> Self {
Self {
sample_rate_hz,
fft_len,
time_bandwidth,
taper_count,
grid: FrequencyGridPolicy::default(),
limits: EstimatorLimits::default(),
}
}
}
/// Uneven-sample generalized Lomb-Scargle policy.
#[derive(Clone, Debug, PartialEq)]
pub struct LombScarglePlan {
/// Reference sample rate used to validate the frequency grid.
pub sample_rate_hz: f64,
/// Reference DFT length used by [`FrequencyGridPolicy::FftBins`].
pub fft_len: usize,
/// Frequency-grid policy. A one-sided positive linear or explicit grid is
/// usually clearest for uneven samples.
pub grid: FrequencyGridPolicy,
/// Resource ceilings.
pub limits: EstimatorLimits,
}
impl LombScarglePlan {
/// Creates a one-sided FFT-bin grid under the supplied reference rate.
pub fn new(sample_rate_hz: f64, fft_len: usize) -> Self {
Self {
sample_rate_hz,
fft_len,
grid: FrequencyGridPolicy::default(),
limits: EstimatorLimits::default(),
}
}
}
pub(crate) fn validate_common(
sample_rate_hz: f64,
fft_len: usize,
segment_len: usize,
limits: EstimatorLimits,
) -> Result<(), SignalError> {
if !sample_rate_hz.is_finite() || sample_rate_hz <= 0.0 {
return Err(SignalError::InvalidPolicy {
policy: "sample rate",
reason: "sample rate must be finite and positive",
});
}
if segment_len == 0 || fft_len < segment_len {
return Err(SignalError::InvalidLength {
len: fft_len,
reason: "spectral transform length must contain the non-empty segment",
});
}
if fft_len > limits.max_fft_len {
return Err(SignalError::InvalidPolicy {
policy: "FFT length limit",
reason: "transform length exceeds the estimator limit",
});
}
Ok(())
}
pub(crate) fn admit_work(required: u64, limits: EstimatorLimits) -> Result<(), SignalError> {
if required > limits.max_work {
Err(SignalError::WorkLimit {
required,
maximum: limits.max_work,
})
} else {
Ok(())
}
}