Skip to main content

load_lpp/
utils.rs

1use chrono::prelude::*;
2use rayon::prelude::*;
3use std::cmp::PartialOrd;
4use std::fs::File;
5use std::io::{BufRead, BufReader};
6use std::path::Path;
7use std::{error::Error, fmt};
8use nalgebra::{DVector, DMatrix};
9use plotly::{Plot, Scatter};
10
11
12/// If longer than one week, keep year, month and day, drop hours;
13/// if not, but longer than one day, add hours.
14/// Otherwise, shorter than one day, keep also minutes.
15pub fn suitable_xfmt(d: chrono::Duration) -> &'static str {
16    let xfmt = if d > chrono::Duration::weeks(1) {
17        "%y-%m-%d"
18    } else if d > chrono::Duration::days(1) {
19        "%m-%d %H"
20    } else {
21        "%d %H:%M"
22    };
23    return xfmt;
24}
25
26
27
28/// Read a list of bad datetimes to skip, always from RFC 3339 - ISO 8601 format.
29pub fn read_bad_datetimes<P>(fin: P) -> Vec<DateTime<FixedOffset>>
30where
31    P: AsRef<Path>,
32{
33    let file = File::open(fin).unwrap();
34    let buf = BufReader::new(file);
35    let mut bad_datetimes: Vec<DateTime<FixedOffset>> = Vec::new();
36    for l in buf.lines() {
37        let l_unwrap = match l {
38            Ok(l_ok) => l_ok,
39            Err(l_err) => {
40                println!("Err, could not read/unwrap line {}", l_err);
41                continue;
42            }
43        };
44        bad_datetimes.push(DateTime::parse_from_rfc3339(&l_unwrap).unwrap());
45    }
46    return bad_datetimes;
47}
48
49pub fn min_and_max<'a, I, T>(mut s: I) -> (T, T)
50where
51    I: Iterator<Item = &'a T>,
52    T: 'a + std::cmp::PartialOrd + Clone,
53{
54    let (mut min, mut max) = match s.next() {
55        Some(v) => (v, v),
56        None => panic!("could not iterate over slice"),
57    };
58    for es in s {
59        if es > max {
60            max = es
61        } else if es < min {
62            min = es
63        }
64    }
65    return (min.clone(), max.clone());
66}
67
68pub fn make_window(w_central: f64, w_side: f64, side: usize) -> Vec<f64> {
69    let w_step = (w_central - w_side) / (side as f64);
70    let up = (0..side + 1).map(|n| w_side + (n as f64 * w_step));
71    let down = up.clone().rev().skip(1);
72    let updown = up.chain(down).collect();
73    updown
74}
75
76// Flexible Weighted Moving Average implementation with parameters to handle the maximum missing information.
77/// Roll the weighted moving window w over the data v,
78/// also filling the NAN values with the weighted average when possible:
79/// 1) sufficient number of data, i.e., number missing data under the window < max_missing_v;
80/// 2) the window weight associated with the present data is sufficient, i.e.,
81///     the percentage of missing weight is < than max_missing_wpct.
82pub fn mavg(v: &[f64], w: &[f64], max_missing_v: usize, max_missing_wpct: f64) -> Vec<f64> {
83    let len_v: i32 = v.len() as i32;
84    let len_w: i32 = w.len() as i32;
85    assert!(
86        len_w < len_v,
87        "length of moving average window > length of vector"
88    );
89    assert!(
90        len_w % 2 == 1,
91        "the moving average window has an even number of elements; \
92        it should be odd to have a central element"
93    );
94    let side: i32 = (len_w - 1) / 2;
95    let sum_all_w: f64 = w.iter().sum();
96    let max_missing_w: f64 = sum_all_w / 100. * max_missing_wpct;
97    let mut vout: Vec<f64> = Vec::with_capacity(len_v as usize);
98    for i in 0..len_v {
99        let mut missing_v = 0;
100        let mut missing_w = 0.;
101        let mut sum_ve_we = 0.;
102        let mut sum_we = 0.;
103        let mut ve: f64;
104        let vl = i - side;
105        let vr = i + side + 1;
106        for (j, we) in (vl..vr).zip(w.iter()) {
107            if (j < 0) || (j >= len_v) {
108                missing_v += 1;
109                missing_w += we;
110            } else {
111                ve = v[j as usize];
112                if ve.is_nan() {
113                    missing_v += 1;
114                    missing_w += we;
115                } else {
116                    sum_ve_we += ve * we;
117                    sum_we += we;
118                }
119            }
120            if (missing_v > max_missing_v) || (missing_w > max_missing_w) {
121                sum_ve_we = f64::NAN;
122                // println!(
123                //     "setting to NAN; {} missing data with limit {}, {} missing window weight with limit {}",
124                //     missing_v, max_missing_v, missing_w, max_missing_w,
125                // );
126                break;
127            }
128        }
129        vout.push(sum_ve_we / sum_we);
130    }
131    vout
132}
133
134// Weighted Moving Average implementation for long windows and
135// with limited number of expected missing values in the time series.
136// This is a parallel implementation of the moving average
137// that splits the multiplication step from the successive sum.
138// This allows SIMD parallelism, but requires second loop over the window for the sum.
139// The SIMD optimization, in addition to the multi-threading, has been confirmed by the assembly.
140pub fn mavg_parallel_simd(v: &[f64], w: &[f64]) -> Vec<f64> {
141    let len_v: usize = v.len();
142    let len_w: usize = w.len();
143    assert!(
144        len_w < len_v,
145        "length of moving average window > length of vector"
146    );
147    assert!(
148        len_w % 2 == 1,
149        "the moving average window has an even number of elements; \
150        it should be odd to have a central element"
151    );
152    let sum_all_w: f64 = w.iter().sum();
153    let side: usize = (len_w - 1) / 2;
154    let mut vout: Vec<f64> = vec![f64::NAN; len_v];
155    v.par_windows(len_w as usize)
156        .zip(vout[side as usize..].par_iter_mut())
157        .for_each(|(window, vout_e)| {
158            let product: Vec<f64> = window
159                .iter()
160                .zip(w)
161                .map(|(win_e, wt_e)| win_e * wt_e)
162                .collect();
163            let sum: f64 = product.iter().sum();
164            *vout_e = sum / sum_all_w;
165        });
166    vout
167}
168
169// Weighted Moving Average implementation for long windows,
170// for limited number of expected missing values and edge devices with limited memory.
171// This is a parallel implementation of the moving average that
172// allows the sum of the weighted loads to be directly executed,
173// i.e., pair-wise multiplication proceed together with the sum.
174pub fn mavg_parallel_fold(v: &[f64], w: &[f64]) -> Vec<f64> {
175    let len_v: usize = v.len();
176    let len_w: usize = w.len();
177    assert!(
178        len_w < len_v,
179        "length of moving average window > length of vector"
180    );
181    assert!(
182        len_w % 2 == 1,
183        "the moving average window has an even number of elements; \
184        it should be odd to have a central element"
185    );
186    let sum_all_w: f64 = w.iter().sum();
187    let side: usize = (len_w - 1) / 2;
188    let mut vout: Vec<f64> = vec![f64::NAN; len_v];
189    v.par_windows(len_w as usize)
190        .zip(vout[side as usize..].par_iter_mut())
191        .for_each(|(window, vout_e)| {
192            *vout_e = window
193                .iter()
194                .zip(w)
195                .map(|(win_e, wt_e)| win_e * wt_e)
196                .fold(0., |acc, x| acc + x)
197                / sum_all_w;
198        });
199    vout
200}
201
202
203pub fn awat_regression_plot(data: DVector<f64>, model:DVector<f64>) {
204    let l = data.column(0).len();
205    let base: Vec<i32> = (0..l).map(|e| e as i32).collect();
206    let mut plot = Plot::new();
207    let trace_data = Scatter::new(base.clone(), data.data.as_vec().to_vec()).name("data");
208    plot.add_trace(trace_data);
209    let trace_model = Scatter::new(base.clone(), model.data.as_vec().to_vec()).name("fit");
210    plot.add_trace(trace_model);
211    plot.show();
212}
213
214
215// Adaptive Window and Adaptive Threshold.
216// Peters et al., 2014. Hydrol. Earth Syst. Sci., 18, 1189–1198, 2014
217// This assumes either ET or P is active, not both at the same time.
218// d is the threshold, smaller changes are ignored.
219// w is the width of the moving-average window.
220// The variable moving average can address the variability of
221// the noise intensity (wind, temp, etc.) and signal strength (rain and strong ET).
222pub fn awat_regression(v: &[f64], len_w: usize) -> (u8, f64) {
223    let k_max = 6;  // this is standard
224    let plot = false;
225
226    let len_v: usize = v.len();
227    assert!(
228        len_w < len_v,
229        "length of moving average window > length of vector"
230    );
231    assert!(
232        len_w % 2 == 1,
233        "the moving average window has an even number of elements; \
234        it should be odd to have a central element"
235    );
236
237    let base_lower: i32 = - (len_w as i32 - 1) / 2;
238    let base_upper: i32 = (len_w as i32 + 1) / 2;
239    let base: Vec<i32> = (base_lower..base_upper).collect();
240
241    // init and populate g matrix
242    let mut g_all = DMatrix::from_element(len_w, k_max + 1, 1.);
243    for i in 1..7 {
244        let dv  = DVector::from_iterator(len_w, base.clone().iter().map(|e| e.pow(i) as f64));
245        g_all.set_column(i as usize, &dv);
246    }
247    // alternative initialization
248    // let g_all = DMatrix::from_columns(&[dv0, dv1, dv2, dv3, dv4, dv5, dv6]);
249
250    let d = DVector::from_column_slice(&v[0..len_w]);
251    let ssd = d.dot(&d);
252
253    let mut solutions = DMatrix::zeros(k_max + 1, k_max);
254    let mut models: DMatrix<f64> = DMatrix::zeros(len_w, k_max);
255    let mut ssrs: Vec<f64> = Vec::with_capacity(6);
256    let mut aics: Vec<f64> = Vec::with_capacity(6);
257
258    for k in 1..(k_max + 1) {
259
260        let g = g_all.columns(0, k + 1);
261
262        let s: DVector<f64> = (g.transpose() * &g).try_inverse().unwrap() * g.transpose() * d.clone();
263
264        for (i, &e) in s.iter().enumerate() {
265            solutions[(i, k - 1)] = e
266        }
267
268        let sol: DVector<f64> = solutions.column(k - 1).into();
269
270        let model = &g_all * sol;
271        models.set_column(k - 1, &model);
272
273        let res = &model - &d;
274        let ssr = res.dot(&res);
275        ssrs.push(ssr);
276
277        // Akaike's information criterion
278        let n = (k + 1) as f64;
279        let r = len_w as f64;
280        let aic = r * f64::ln(ssr / r) + (2. * n) + ((2. * n * (n + 1.)) / (r - n - 1.));
281        aics.push(aic);
282
283    }
284
285    // find best acis, and associated solution and order
286    let mut min_index = 0;
287    let mut min_value = aics[0];
288    for (i, &e) in aics.iter().enumerate() {
289        if e < min_value {
290            min_value = e;
291            min_index = i;
292        }
293    }
294    // let sol_best: DVector<f64> = solutions.column(min_index).into();
295    let mod_best: DVector<f64> = models.column(min_index).into();
296    let ssr_best = ssrs[min_index];
297    let k_best = (min_index + 1) as u8;
298
299    if plot {
300        awat_regression_plot(d, mod_best)
301    }
302
303    let b = ssr_best / ssd;
304
305    (k_best, b)
306}
307
308
309// A configurable and automatic detection of anomalous periods
310// based on the interquartile range (IQR).
311// Anomalies can be periods that have to be removed or the actual events of interest.
312// For example, the reported anomalies can be appended to the bad datetimes input
313// and thus be removed in the successive processing iteration.
314//
315// Run a rolling window of width `window_width` over the vector `v`.
316// Make sure that:
317// 1] `window_width` is larger than the minimum number of data required by the user `min_window_data`
318// 2] `min_window_data` is statistically sufficient for calculating the IQR, see `MIN_DATA_IQR`.
319//
320// Return unique values of the indices and loads that fell in an anomalous window.
321pub fn find_anomalies(
322    v: &[f64],
323    window_width: usize,
324    min_window_data: usize,
325    max_iqr: f64,
326) -> (Vec<usize>, Vec<f64>) {
327    // Initial length checks for consistent lengths
328    pub const MIN_DATA_IQR: usize = 6usize;
329    if min_window_data < MIN_DATA_IQR {
330        panic!(
331            "find_anomalies: more than {} data are required for the IQR calculation",
332            MIN_DATA_IQR
333        );
334    }
335    if min_window_data > window_width {
336        panic!("find_anomalies: impossible to proceed as window_width < min_window_data");
337    }
338    let mut anomalies_index: Vec<usize> = Vec::new();
339    let indices: Vec<usize> = (0..v.len()).collect();
340    for (wl, wi) in v.windows(window_width).zip(indices.windows(window_width)) {
341        let (_ql, _qu, iqr) = match calculate_iqr(wl, min_window_data) {
342            Ok(res) => res,
343            Err(_e) => {
344                continue;
345            }
346        };
347        if iqr > max_iqr {
348            anomalies_index.append(&mut wi.to_owned());
349        }
350    }
351    // Anomalous windows may give duplicates, keep only unique indices:
352    // first, order the indices so that multiple duplicates will be consecutive,
353    // then deduplicate more quickly and in-place.
354    anomalies_index.sort_by(|a, b| a.partial_cmp(b).unwrap());
355    let (anomalies_index_dedup, _) = anomalies_index.partition_dedup_by(|a, b| a == b);
356    let anomalies_index_dedup = anomalies_index_dedup.to_vec();
357    let mut anomalies_load: Vec<f64> = Vec::new();
358    for i in anomalies_index_dedup.iter() {
359        anomalies_load.push(v[*i]);
360    }
361    return (anomalies_index_dedup, anomalies_load);
362}
363
364// Calculate the lower and upper quartiles
365// using the linear method (R-7) to calculate the IQR.
366// Note, no + 1 here because of the zero-starting indexing, i.e.,
367// h = (N - 1) * q + 1  => (N - 1) * q
368// This is analogous to the default method chosen by NumPy.
369//
370// Use map to dereference the f64 and usize,
371// as they are cheap and implement copy.
372// This gives a Vec that owns its elements.
373pub fn calculate_iqr(s: &[f64], min_len: usize) -> Result<(f64, f64, f64), LenErr> {
374    let mut v: Vec<f64> = s.iter().filter(|n| n.is_finite()).map(|n| *n).collect();
375    let v_len = v.len();
376
377    if v_len < min_len {
378        let err = LenErr {
379            min_len: Some(min_len),
380            got_len: v_len,
381            max_len: None,
382        };
383        return Err(err);
384    }
385    v.sort_by(|a, b| a.partial_cmp(&b).unwrap());
386    let hl = (v_len as f64 - 1.) * 0.25;
387    let hu = (v_len as f64 - 1.) * 0.75;
388    let hl_int = hl.floor() as usize;
389    let hl_fract = hl.fract();
390    let hu_int = hu.floor() as usize;
391    let hu_fract = hu.fract();
392    let ql_int = v[hl_int];
393    let qu_int = v[hu_int];
394    let ql_fract = (v[hl_int + 1usize] - v[hl_int]) * hl_fract;
395    let qu_fract = (v[hu_int + 1usize] - v[hu_int]) * hu_fract;
396    let ql = ql_int + ql_fract;
397    let qu = qu_int + qu_fract;
398    let iqr = qu - ql;
399    return Ok((ql, qu, iqr));
400}
401
402pub fn mean_or_nan(v: &Vec<f64>) -> f64 {
403    
404    let mut contains_nan = false;
405    v.iter().for_each(|f| {
406          if f.is_nan() {
407              contains_nan = true
408          }
409    });
410
411    let mean = if contains_nan {
412        f64::NAN
413    } else if v.len() == 0usize {
414        f64::NAN
415    } else {
416        v.iter().sum::<f64>() / v.len() as f64
417    };
418
419    mean
420}
421
422
423pub fn compare_f64_exact(a: f64, b: f64) -> bool {
424    (a.is_nan() && b.is_nan()) || (a == b)
425}
426
427pub fn compare_vecf64_exact(va: &[f64], vb: &[f64]) -> bool {
428    (va.len() == vb.len()) && va.iter().zip(vb).all(|(a, b)| compare_f64_exact(*a, *b))
429}
430
431pub fn compare_f64_approx(a: f64, b: f64, max: f64) -> bool {
432    (a.is_nan() && b.is_nan()) || ((a - b).abs() < max)
433}
434
435pub fn compare_vecf64_approx(va: &[f64], vb: &[f64]) -> bool {
436    (va.len() == vb.len())
437        && va
438            .iter()
439            .zip(vb)
440            .all(|(a, b)| compare_f64_approx(*a, *b, 0.1f64))
441}
442
443/// Return a new vector without the elements associated with the given indices.
444/// The indices are expected to be sorted or partially sorted:
445/// sort them completely and then take advantage of that.
446/// This avoids random indexing-access the vector,
447/// which can reduce cache misses and the number of comparisons.
448/// This is used for removing bad datetimes and anomalies.
449pub fn discharge_by_index<T: Copy>(ve: &[T], vi: &[usize]) -> Vec<T> {
450    let mut vout: Vec<T> = Vec::with_capacity(ve.len());
451    let mut vi = vi.to_vec();
452    vi.sort();
453    assert!(vi[vi.len() - 1usize] < ve.len());
454    let mut vi_iter = vi.iter();
455    let mut ve_iter = ve.iter().enumerate();
456    while let Some(i) = vi_iter.next() {
457        loop {
458            let (vei, vee) = ve_iter.next().unwrap();
459            if vei < *i {
460                vout.push(*vee);
461            } else if vei == *i {
462                break;
463            } else {
464                panic!("indices error: {} > {}", vei, i);
465            }
466        }
467    }
468    while let Some((_, vee)) = ve_iter.next() {
469        vout.push(*vee);
470    }
471    return vout;
472}
473
474/// Set to NAN the elements of the given vector at the given indices.
475/// The indices are expected to be sorted or partially sorted:
476/// sort them completely and then take advantage of that.
477/// This avoids random indexing-access the vector,
478/// which can reduce cache misses and the number of comparisons.
479/// This is used for removing bad datetimes and anomalies.
480pub fn setnan_by_index(ve: &mut [f64], vi: &[usize]) {
481    if vi.len() == 0 {
482        return;
483    }
484    let mut vi = vi.to_vec();
485    vi.sort();
486    assert!(vi[vi.len() - 1usize] < ve.len());
487    let mut vi_iter = vi.iter();
488    let mut ve_iter = ve.iter_mut().enumerate();
489    while let Some(i) = vi_iter.next() {
490        loop {
491            let (vei, vee) = ve_iter.next().unwrap();
492            if vei < *i {
493                continue;
494            } else if vei == *i {
495                *vee = f64::NAN;
496                break;
497            } else {
498                panic!("indices error: {} > {}", vei, i);
499            }
500        }
501    }
502}
503
504// An Error type for empty TimeLoad
505#[derive(Debug)]
506pub struct EmptyTimeLoad();
507impl fmt::Display for EmptyTimeLoad {
508    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
509        write!(f, "Found an empty TimeLoad")
510    }
511}
512impl Error for EmptyTimeLoad {}
513
514// An Error type for handling length requirements,
515// often needed in time series and statistics.
516#[derive(Debug)]
517pub struct LenErr {
518    pub min_len: Option<usize>,
519    pub got_len: usize,
520    pub max_len: Option<usize>,
521}
522impl Error for LenErr {}
523impl fmt::Display for LenErr {
524    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
525        write!(
526            f,
527            "Invalid length, got {}, required is >= {:?} and <= {:?}",
528            self.got_len, self.min_len, self.max_len
529        )
530    }
531}