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
use crate::evaluator::*;
use crate::straight_line_fit::fit_straight_line;

macro_const! {
    const DOC: &str = r"
The slope, its error and noise level of the light curve in the linear fit

Least squares fit of the linear stochastic model with constant Gaussian noise $\Sigma$ assuming
observation errors to be zero:
$$
m_i = c + \mathrm{slope} t_i + \Sigma \varepsilon_i,
$$
where $c$ is a constant,
$\{\varepsilon_i\}$ are standard distributed random variables. $\mathrm{slope}$,
$\sigma_\mathrm{slope}$ and $\Sigma$ are returned.

- Depends on: **time**, **magnitude**
- Minimum number of observations: **3**
- Number of features: **3**
";
}

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

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

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

lazy_info!(
    LINEAR_TREND_INFO,
    LinearTrend,
    size: 3,
    min_ts_length: 3,
    t_required: true,
    m_required: true,
    w_required: false,
    sorting_required: true,
);

impl FeatureNamesDescriptionsTrait for LinearTrend {
    fn get_names(&self) -> Vec<&str> {
        vec!["linear_trend", "linear_trend_sigma", "linear_trend_noise"]
    }

    fn get_descriptions(&self) -> Vec<&str> {
        vec![
            "linear trend without respect to observation errors",
            "error of slope of linear fit without respect to observation errors",
            "standard deviation of noise for linear fit without respect to observation errors",
        ]
    }
}

impl<T> FeatureEvaluator<T> for LinearTrend
where
    T: Float,
{
    fn eval(&self, ts: &mut TimeSeries<T>) -> Result<Vec<T>, EvaluatorError> {
        self.check_ts_length(ts)?;
        let result = fit_straight_line(ts, false);
        Ok(vec![
            result.slope,
            T::sqrt(result.slope_sigma2),
            T::sqrt(result.reduced_chi2),
        ])
    }
}

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

    check_feature!(LinearTrend);

    feature_test!(
        linear_trend,
        [LinearTrend::new()],
        [1.38198758, 0.24532195657979344, 2.54157969],
        [1.0_f32, 3.0, 5.0, 7.0, 11.0, 13.0],
        [1.0_f32, 2.0, 3.0, 8.0, 10.0, 19.0],
    );

    /// See [Issue #3](https://github.com/hombit/light-curve/issues/3)
    fn linear_trend_finite(path: &str) {
        let eval = LinearTrend::default();
        let (t, m, _) =
            light_curve_feature_test_util::issue_light_curve_mag::<f32, _>(path).into_triple(None);
        let mut ts = TimeSeries::new_without_weight(t, m);
        let actual = eval.eval(&mut ts).unwrap();
        assert!(actual.iter().all(|x| x.is_finite()));
    }

    #[test]
    fn linear_trend_finite_1() {
        linear_trend_finite("light-curve-3/1.csv");
    }

    #[test]
    fn linear_trend_finite_2() {
        linear_trend_finite("light-curve-3/2.csv");
    }

    #[test]
    fn linear_trend_finite_3() {
        linear_trend_finite("light-curve-3/640202200001881.csv");
    }

    #[test]
    fn linear_trend_finite_4() {
        linear_trend_finite("light-curve-3/742201400001054.csv");
    }

    #[test]
    fn linear_trend_finite_5() {
        linear_trend_finite("light-curve-3/742201400001066.csv");
    }
}