1use crate::matrix::FdMatrix;
2
3use super::{
4 autocorrelation, compute_mean_curve, find_consensus_period, find_first_acf_peak, find_peaks_1d,
5 periodogram, validate_sazed_component,
6};
7
8#[derive(Debug, Clone, PartialEq)]
10pub struct SazedResult {
11 pub period: f64,
13 pub confidence: f64,
15 pub component_periods: SazedComponents,
17 pub agreeing_components: usize,
19}
20
21#[derive(Debug, Clone, PartialEq)]
23pub struct SazedComponents {
24 pub spectral: f64,
26 pub acf_peak: f64,
28 pub acf_average: f64,
30 pub zero_crossing: f64,
32 pub spectral_diff: f64,
34}
35
36pub fn sazed(data: &[f64], argvals: &[f64], tolerance: Option<f64>) -> SazedResult {
58 let n = data.len();
59 let tol = tolerance.unwrap_or(0.05); if n < 8 || argvals.len() != n {
62 return SazedResult {
63 period: f64::NAN,
64 confidence: 0.0,
65 component_periods: SazedComponents {
66 spectral: f64::NAN,
67 acf_peak: f64::NAN,
68 acf_average: f64::NAN,
69 zero_crossing: f64::NAN,
70 spectral_diff: f64::NAN,
71 },
72 agreeing_components: 0,
73 };
74 }
75
76 let dt = (argvals[n - 1] - argvals[0]) / (n - 1) as f64;
77 let max_lag = (n / 2).max(4);
78 let signal_range = argvals[n - 1] - argvals[0];
79
80 let min_period = signal_range / (n as f64 / 3.0);
82 let max_period = signal_range / 2.0;
84
85 let (spectral_period, spectral_conf) = sazed_spectral_with_confidence(data, argvals);
87
88 let (acf_peak_period, acf_peak_conf) = sazed_acf_peak_with_confidence(data, dt, max_lag);
90
91 let acf_average_period = sazed_acf_average(data, dt, max_lag);
93
94 let (zero_crossing_period, zero_crossing_conf) =
96 sazed_zero_crossing_with_confidence(data, dt, max_lag);
97
98 let (spectral_diff_period, spectral_diff_conf) =
100 sazed_spectral_diff_with_confidence(data, argvals);
101
102 let components = SazedComponents {
103 spectral: spectral_period,
104 acf_peak: acf_peak_period,
105 acf_average: acf_average_period,
106 zero_crossing: zero_crossing_period,
107 spectral_diff: spectral_diff_period,
108 };
109
110 let spectral_thresh = 8.0; let acf_thresh = 0.3; let zero_crossing_thresh = 0.9; let spectral_diff_thresh = 6.0; let min_confident_components = 2;
119
120 let confident_periods: Vec<f64> = [
122 validate_sazed_component(
123 spectral_period,
124 spectral_conf,
125 min_period,
126 max_period,
127 spectral_thresh,
128 ),
129 validate_sazed_component(
130 acf_peak_period,
131 acf_peak_conf,
132 min_period,
133 max_period,
134 acf_thresh,
135 ),
136 validate_sazed_component(
137 acf_average_period,
138 acf_peak_conf,
139 min_period,
140 max_period,
141 acf_thresh,
142 ),
143 validate_sazed_component(
144 zero_crossing_period,
145 zero_crossing_conf,
146 min_period,
147 max_period,
148 zero_crossing_thresh,
149 ),
150 validate_sazed_component(
151 spectral_diff_period,
152 spectral_diff_conf,
153 min_period,
154 max_period,
155 spectral_diff_thresh,
156 ),
157 ]
158 .into_iter()
159 .flatten()
160 .collect();
161
162 if confident_periods.len() < min_confident_components {
164 return SazedResult {
165 period: f64::NAN,
166 confidence: 0.0,
167 component_periods: components,
168 agreeing_components: confident_periods.len(),
169 };
170 }
171
172 let (consensus_period, agreeing_count) = find_consensus_period(&confident_periods, tol);
174 let confidence = agreeing_count as f64 / 5.0;
175
176 SazedResult {
177 period: consensus_period,
178 confidence,
179 component_periods: components,
180 agreeing_components: agreeing_count,
181 }
182}
183
184pub fn sazed_fdata(data: &FdMatrix, argvals: &[f64], tolerance: Option<f64>) -> SazedResult {
193 let (n, m) = data.shape();
194 if n == 0 || m < 8 || argvals.len() != m {
195 return SazedResult {
196 period: f64::NAN,
197 confidence: 0.0,
198 component_periods: SazedComponents {
199 spectral: f64::NAN,
200 acf_peak: f64::NAN,
201 acf_average: f64::NAN,
202 zero_crossing: f64::NAN,
203 spectral_diff: f64::NAN,
204 },
205 agreeing_components: 0,
206 };
207 }
208
209 let mean_curve = compute_mean_curve(data);
210 sazed(&mean_curve, argvals, tolerance)
211}
212
213fn sazed_spectral_with_confidence(data: &[f64], argvals: &[f64]) -> (f64, f64) {
215 let (frequencies, power) = periodogram(data, argvals);
216
217 if frequencies.len() < 3 {
218 return (f64::NAN, 0.0);
219 }
220
221 let power_no_dc: Vec<f64> = power.iter().skip(1).copied().collect();
223
224 if power_no_dc.is_empty() {
225 return (f64::NAN, 0.0);
226 }
227
228 let mut sorted_power = power_no_dc.clone();
230 crate::helpers::sort_nan_safe(&mut sorted_power);
231 let noise_floor = sorted_power[sorted_power.len() / 2].max(1e-15);
232
233 let mut max_idx = 0;
235 let mut max_val = 0.0;
236 for (i, &p) in power_no_dc.iter().enumerate() {
237 if p > max_val {
238 max_val = p;
239 max_idx = i;
240 }
241 }
242
243 let power_ratio = max_val / noise_floor;
244 let freq = frequencies[max_idx + 1];
245
246 if freq > 1e-15 {
247 (1.0 / freq, power_ratio)
248 } else {
249 (f64::NAN, 0.0)
250 }
251}
252
253fn sazed_acf_peak_with_confidence(data: &[f64], dt: f64, max_lag: usize) -> (f64, f64) {
255 let acf = autocorrelation(data, max_lag);
256
257 match find_first_acf_peak(&acf) {
258 Some((peak_lag, acf_value)) => (peak_lag as f64 * dt, acf_value),
259 None => (f64::NAN, 0.0),
260 }
261}
262
263fn sazed_acf_average(data: &[f64], dt: f64, max_lag: usize) -> f64 {
265 let acf = autocorrelation(data, max_lag);
266
267 if acf.len() < 4 {
268 return f64::NAN;
269 }
270
271 let peaks = find_peaks_1d(&acf[1..], 1);
273
274 if peaks.is_empty() {
275 return f64::NAN;
276 }
277
278 let mut weighted_sum = 0.0;
280 let mut weight_sum = 0.0;
281
282 for (i, &peak_idx) in peaks.iter().enumerate() {
283 let lag = peak_idx + 1;
284 let weight = acf[lag].max(0.0);
285
286 if i == 0 {
287 weighted_sum += lag as f64 * weight;
289 weight_sum += weight;
290 } else {
291 let expected_fundamental = peaks[0] + 1;
293 let harmonic = ((lag as f64 / expected_fundamental as f64) + 0.5) as usize;
294 if harmonic > 0 {
295 let fundamental_est = lag as f64 / harmonic as f64;
296 weighted_sum += fundamental_est * weight;
297 weight_sum += weight;
298 }
299 }
300 }
301
302 if weight_sum > 1e-15 {
303 weighted_sum / weight_sum * dt
304 } else {
305 f64::NAN
306 }
307}
308
309fn sazed_zero_crossing_with_confidence(data: &[f64], dt: f64, max_lag: usize) -> (f64, f64) {
312 let acf = autocorrelation(data, max_lag);
313
314 if acf.len() < 4 {
315 return (f64::NAN, 0.0);
316 }
317
318 let mut crossings = Vec::new();
320 for i in 1..acf.len() {
321 if acf[i - 1] * acf[i] < 0.0 {
322 let frac = acf[i - 1].abs() / (acf[i - 1].abs() + acf[i].abs());
324 crossings.push((i - 1) as f64 + frac);
325 }
326 }
327
328 if crossings.len() < 2 {
329 return (f64::NAN, 0.0);
330 }
331
332 let mut half_periods = Vec::new();
335 for i in 1..crossings.len() {
336 half_periods.push(crossings[i] - crossings[i - 1]);
337 }
338
339 if half_periods.is_empty() {
340 return (f64::NAN, 0.0);
341 }
342
343 let mean: f64 = half_periods.iter().sum::<f64>() / half_periods.len() as f64;
346 let variance: f64 =
347 half_periods.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / half_periods.len() as f64;
348 let std = variance.sqrt();
349 let consistency = (1.0 - std / mean.max(1e-15)).clamp(0.0, 1.0);
350
351 crate::helpers::sort_nan_safe(&mut half_periods);
353 let median_half = half_periods[half_periods.len() / 2];
354
355 (2.0 * median_half * dt, consistency)
356}
357
358fn sazed_spectral_diff_with_confidence(data: &[f64], argvals: &[f64]) -> (f64, f64) {
360 if data.len() < 4 {
361 return (f64::NAN, 0.0);
362 }
363
364 let diff: Vec<f64> = data.windows(2).map(|w| w[1] - w[0]).collect();
366 let diff_argvals: Vec<f64> = argvals.windows(2).map(|w| (w[0] + w[1]) / 2.0).collect();
367
368 sazed_spectral_with_confidence(&diff, &diff_argvals)
369}