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
use crate::evaluator::*;
use itertools::Itertools;

macro_const! {
    const DOC: &'static str = r"
$\eta^e$ — modification of [Eta](crate::Eta) for unevenly time series

$$
\eta^e \equiv \frac{(t_{N-1} - t_0)^2}{(N - 1)^3} \frac{\sum_{i=0}^{N-2} \left(\frac{m_{i+1} - m_i}{t_{i+1} - t_i}\right)^2}{\sigma_m^2}
$$
where $N$ is the number of observations,
$\sigma_m = \sqrt{\sum_i (m_i - \langle m \rangle)^2 / (N-1)}$ is the magnitude standard deviation.
Note that this definition is a bit different from both Kim et al. 2014 and
[feets](https://feets.readthedocs.io/en/latest/)

Note that this feature can have very high values and be highly cadence-dependent in the case of large range of time
lags. In this case consider to use this feature with [Bins](crate::Bins).

- Depends on: **time**, **magnitude**
- Minimum number of observations: **2**
- Number of features: **1**

Kim et al. 2014, [DOI:10.1051/0004-6361/201323252](https://doi.org/10.1051/0004-6361/201323252)
";
}

#[doc = DOC!()]
#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
pub struct EtaE {}

impl EtaE {
    pub fn new() -> Self {
        Self {}
    }

    pub const fn doc() -> &'static str {
        DOC
    }
}

lazy_info!(
    ETA_E_INFO,
    EtaE,
    size: 1,
    min_ts_length: 2,
    t_required: true,
    m_required: true,
    w_required: false,
    sorting_required: true,
);

impl FeatureNamesDescriptionsTrait for EtaE {
    fn get_names(&self) -> Vec<&str> {
        vec!["eta_e"]
    }

    fn get_descriptions(&self) -> Vec<&str> {
        vec!["generalised Von Neummann eta for irregular time-series"]
    }
}

impl<T> FeatureEvaluator<T> for EtaE
where
    T: Float,
{
    fn eval(&self, ts: &mut TimeSeries<T>) -> Result<Vec<T>, EvaluatorError> {
        self.check_ts_length(ts)?;
        let m_std2 = get_nonzero_m_std2(ts)?;
        let sq_slope_sum =
            ts.t.as_slice()
                .iter()
                .zip(ts.m.as_slice().iter())
                .tuple_windows()
                .map(|((&t1, &m1), (&t2, &m2))| ((m2 - m1) / (t2 - t1)).powi(2))
                .filter(|&x| x.is_finite())
                .sum::<T>();
        let value = (ts.t.sample[ts.lenu() - 1] - ts.t.sample[0]).powi(2) * sq_slope_sum
            / m_std2
            / (ts.lenf() - T::one()).powi(3);
        Ok(vec![value])
    }
}

#[cfg(test)]
#[allow(clippy::unreadable_literal)]
#[allow(clippy::excessive_precision)]
mod tests {
    use super::*;
    use crate::extractor::FeatureExtractor;
    use crate::features::Eta;
    use crate::tests::*;

    check_feature!(EtaE);

    feature_test!(
        eta_e,
        [EtaE::new()],
        [0.6957894],
        [1.0_f32, 2.0, 5.0, 10.0],
        [1.0_f32, 1.0, 6.0, 8.0],
    );

    #[test]
    fn eta_is_eta_e_for_even_grid() {
        let fe = FeatureExtractor::<_, Feature<_>>::new(vec![
            Eta::default().into(),
            EtaE::default().into(),
        ]);
        let x = linspace(0.0_f64, 1.0, 11);
        let y: Vec<_> = x.iter().map(|&t| 3.0 + t.powi(2)).collect();
        let mut ts = TimeSeries::new_without_weight(&x, &y);
        let values = fe.eval(&mut ts).unwrap();
        all_close(&values[0..1], &values[1..2], 1e-10);
    }

    /// See [Issue #2](https://github.com/hombit/light-curve/issues/2)
    #[test]
    fn eta_e_finite() {
        let eval = EtaE::default();
        let (t, m, _) = light_curve_feature_test_util::issue_light_curve_mag("light-curve-2/1.csv")
            .into_triple(None);
        let mut ts = TimeSeries::new_without_weight(t, m);
        let actual: f32 = eval.eval(&mut ts).unwrap()[0];
        assert!(actual.is_finite());
    }
}