1use crate::error::{Result, TemporalError};
7use crate::timeseries::TimeSeriesRaster;
8use scirs2_core::ndarray::Array3;
9use serde::{Deserialize, Serialize};
10use tracing::info;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14pub enum ForecastMethod {
15 LastValue,
17 Mean,
19 LinearExtrapolation,
21 ExponentialSmoothing,
23 MovingAverage,
25}
26
27#[derive(Debug, Clone)]
29pub struct ForecastResult {
30 pub forecast: Array3<f64>,
32 pub lower_bound: Option<Array3<f64>>,
34 pub upper_bound: Option<Array3<f64>>,
36 pub horizon: usize,
38}
39
40impl ForecastResult {
41 #[must_use]
43 pub fn new(forecast: Array3<f64>, horizon: usize) -> Self {
44 Self {
45 forecast,
46 lower_bound: None,
47 upper_bound: None,
48 horizon,
49 }
50 }
51
52 #[must_use]
54 pub fn with_confidence(mut self, lower_bound: Array3<f64>, upper_bound: Array3<f64>) -> Self {
55 self.lower_bound = Some(lower_bound);
56 self.upper_bound = Some(upper_bound);
57 self
58 }
59}
60
61pub struct Forecaster;
63
64impl Forecaster {
65 pub fn forecast(
70 ts: &TimeSeriesRaster,
71 method: ForecastMethod,
72 horizon: usize,
73 params: Option<ForecastParams>,
74 ) -> Result<ForecastResult> {
75 if horizon == 0 {
76 return Err(TemporalError::invalid_parameter(
77 "horizon",
78 "must be greater than 0",
79 ));
80 }
81
82 match method {
83 ForecastMethod::LastValue => Self::last_value_forecast(ts, horizon),
84 ForecastMethod::Mean => Self::mean_forecast(ts, horizon),
85 ForecastMethod::LinearExtrapolation => Self::linear_forecast(ts, horizon),
86 ForecastMethod::ExponentialSmoothing => {
87 let alpha = params.map_or(0.3, |p| p.alpha);
88 Self::exponential_smoothing_forecast(ts, horizon, alpha)
89 }
90 ForecastMethod::MovingAverage => {
91 let window = params.map_or(3, |p| p.window_size);
92 Self::moving_average_forecast(ts, horizon, window)
93 }
94 }
95 }
96
97 fn last_value_forecast(ts: &TimeSeriesRaster, horizon: usize) -> Result<ForecastResult> {
99 if ts.is_empty() {
100 return Err(TemporalError::insufficient_data("Empty time series"));
101 }
102
103 let (_height, _width, _n_bands) = ts
105 .expected_shape()
106 .ok_or_else(|| TemporalError::insufficient_data("No shape information"))?;
107
108 let last_entry = ts.get_by_index(ts.len() - 1)?;
109 let last_data = last_entry
110 .data
111 .as_ref()
112 .ok_or_else(|| TemporalError::invalid_input("Data not loaded"))?;
113
114 let forecast = last_data.clone();
115
116 info!("Generated last value forecast for {} steps", horizon);
117 Ok(ForecastResult::new(forecast, horizon))
118 }
119
120 fn mean_forecast(ts: &TimeSeriesRaster, horizon: usize) -> Result<ForecastResult> {
122 if ts.is_empty() {
123 return Err(TemporalError::insufficient_data("Empty time series"));
124 }
125
126 let (height, width, n_bands) = ts
127 .expected_shape()
128 .ok_or_else(|| TemporalError::insufficient_data("No shape information"))?;
129
130 let mut forecast = Array3::zeros((height, width, n_bands));
131
132 for i in 0..height {
133 for j in 0..width {
134 for k in 0..n_bands {
135 let values = ts.extract_pixel_timeseries(i, j, k)?;
136 let mean = values.iter().sum::<f64>() / values.len() as f64;
137 forecast[[i, j, k]] = mean;
138 }
139 }
140 }
141
142 info!("Generated mean forecast for {} steps", horizon);
143 Ok(ForecastResult::new(forecast, horizon))
144 }
145
146 fn linear_forecast(ts: &TimeSeriesRaster, horizon: usize) -> Result<ForecastResult> {
148 if ts.len() < 2 {
149 return Err(TemporalError::insufficient_data(
150 "Need at least 2 observations",
151 ));
152 }
153
154 let (height, width, n_bands) = ts
155 .expected_shape()
156 .ok_or_else(|| TemporalError::insufficient_data("No shape information"))?;
157
158 let mut forecast = Array3::zeros((height, width, n_bands));
159
160 for i in 0..height {
161 for j in 0..width {
162 for k in 0..n_bands {
163 let values = ts.extract_pixel_timeseries(i, j, k)?;
164
165 let n = values.len() as f64;
167 let times: Vec<f64> = (0..values.len()).map(|t| t as f64).collect();
168
169 let sum_t: f64 = times.iter().sum();
170 let sum_y: f64 = values.iter().sum();
171 let sum_t2: f64 = times.iter().map(|t| t * t).sum();
172 let sum_ty: f64 = times.iter().zip(values.iter()).map(|(t, y)| t * y).sum();
173
174 let slope = (n * sum_ty - sum_t * sum_y) / (n * sum_t2 - sum_t * sum_t);
175 let intercept = (sum_y - slope * sum_t) / n;
176
177 let forecast_time = (values.len() + horizon - 1) as f64;
179 forecast[[i, j, k]] = slope * forecast_time + intercept;
180 }
181 }
182 }
183
184 info!("Generated linear forecast for {} steps", horizon);
185 Ok(ForecastResult::new(forecast, horizon))
186 }
187
188 fn exponential_smoothing_forecast(
190 ts: &TimeSeriesRaster,
191 horizon: usize,
192 alpha: f64,
193 ) -> Result<ForecastResult> {
194 if ts.is_empty() {
195 return Err(TemporalError::insufficient_data("Empty time series"));
196 }
197
198 if !(0.0..=1.0).contains(&alpha) {
199 return Err(TemporalError::invalid_parameter(
200 "alpha",
201 "must be between 0 and 1",
202 ));
203 }
204
205 let (height, width, n_bands) = ts
206 .expected_shape()
207 .ok_or_else(|| TemporalError::insufficient_data("No shape information"))?;
208
209 let mut forecast = Array3::zeros((height, width, n_bands));
210
211 for i in 0..height {
212 for j in 0..width {
213 for k in 0..n_bands {
214 let values = ts.extract_pixel_timeseries(i, j, k)?;
215
216 let mut smoothed = values[0];
218
219 for &value in &values[1..] {
221 smoothed = alpha * value + (1.0 - alpha) * smoothed;
222 }
223
224 forecast[[i, j, k]] = smoothed;
225 }
226 }
227 }
228
229 info!(
230 "Generated exponential smoothing forecast (alpha={}) for {} steps",
231 alpha, horizon
232 );
233 Ok(ForecastResult::new(forecast, horizon))
234 }
235
236 fn moving_average_forecast(
238 ts: &TimeSeriesRaster,
239 horizon: usize,
240 window: usize,
241 ) -> Result<ForecastResult> {
242 if ts.len() < window {
243 return Err(TemporalError::insufficient_data(format!(
244 "Need at least {} observations for window size {}",
245 window, window
246 )));
247 }
248
249 let (height, width, n_bands) = ts
250 .expected_shape()
251 .ok_or_else(|| TemporalError::insufficient_data("No shape information"))?;
252
253 let mut forecast = Array3::zeros((height, width, n_bands));
254
255 for i in 0..height {
256 for j in 0..width {
257 for k in 0..n_bands {
258 let values = ts.extract_pixel_timeseries(i, j, k)?;
259
260 let start_idx = values.len().saturating_sub(window);
262 let sum: f64 = values[start_idx..].iter().sum();
263 let avg = sum / (values.len() - start_idx) as f64;
264
265 forecast[[i, j, k]] = avg;
266 }
267 }
268 }
269
270 info!(
271 "Generated moving average forecast (window={}) for {} steps",
272 window, horizon
273 );
274 Ok(ForecastResult::new(forecast, horizon))
275 }
276
277 pub fn calculate_confidence(
282 ts: &TimeSeriesRaster,
283 forecast: &Array3<f64>,
284 confidence_level: f64,
285 ) -> Result<(Array3<f64>, Array3<f64>)> {
286 if !(0.0..=1.0).contains(&confidence_level) {
287 return Err(TemporalError::invalid_parameter(
288 "confidence_level",
289 "must be between 0 and 1",
290 ));
291 }
292
293 let (height, width, n_bands) = ts
294 .expected_shape()
295 .ok_or_else(|| TemporalError::insufficient_data("No shape information"))?;
296
297 let mut lower = Array3::zeros((height, width, n_bands));
298 let mut upper = Array3::zeros((height, width, n_bands));
299
300 let z = match confidence_level {
302 x if x >= 0.99 => 2.576,
303 x if x >= 0.95 => 1.96,
304 x if x >= 0.90 => 1.645,
305 _ => 1.0,
306 };
307
308 for i in 0..height {
309 for j in 0..width {
310 for k in 0..n_bands {
311 let values = ts.extract_pixel_timeseries(i, j, k)?;
312
313 let mean = values.iter().sum::<f64>() / values.len() as f64;
315 let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>()
316 / values.len() as f64;
317 let std_error = variance.sqrt() / (values.len() as f64).sqrt();
318
319 let margin = z * std_error;
320 lower[[i, j, k]] = forecast[[i, j, k]] - margin;
321 upper[[i, j, k]] = forecast[[i, j, k]] + margin;
322 }
323 }
324 }
325
326 Ok((lower, upper))
327 }
328}
329
330#[derive(Debug, Clone, Copy)]
332pub struct ForecastParams {
333 pub alpha: f64,
335 pub window_size: usize,
337}
338
339impl Default for ForecastParams {
340 fn default() -> Self {
341 Self {
342 alpha: 0.3,
343 window_size: 3,
344 }
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351 use crate::timeseries::{TemporalMetadata, TimeSeriesRaster};
352 use chrono::{DateTime, NaiveDate};
353
354 #[test]
355 fn test_linear_forecast() {
356 let mut ts = TimeSeriesRaster::new();
357
358 for i in 0..10 {
359 let dt = DateTime::from_timestamp(1640995200 + i * 86400, 0).expect("valid");
360 let date = NaiveDate::from_ymd_opt(2022, 1, 1 + i as u32).expect("valid");
361 let metadata = TemporalMetadata::new(dt, date);
362 let data = Array3::from_elem((3, 3, 1), (i * 2) as f64);
363 ts.add_raster(metadata, data).expect("should add");
364 }
365
366 let result = Forecaster::forecast(&ts, ForecastMethod::LinearExtrapolation, 5, None)
367 .expect("should forecast");
368
369 assert_eq!(result.horizon, 5);
370 assert!(result.forecast[[0, 0, 0]] > 18.0);
372 }
373
374 #[test]
375 fn test_exponential_smoothing() {
376 let mut ts = TimeSeriesRaster::new();
377
378 for i in 0..10 {
379 let dt = DateTime::from_timestamp(1640995200 + i * 86400, 0).expect("valid");
380 let date = NaiveDate::from_ymd_opt(2022, 1, 1 + i as u32).expect("valid");
381 let metadata = TemporalMetadata::new(dt, date);
382 let data = Array3::from_elem((3, 3, 1), 10.0 + (i as f64));
383 ts.add_raster(metadata, data).expect("should add");
384 }
385
386 let params = ForecastParams {
387 alpha: 0.5,
388 window_size: 3,
389 };
390
391 let result =
392 Forecaster::forecast(&ts, ForecastMethod::ExponentialSmoothing, 3, Some(params))
393 .expect("should forecast");
394
395 assert_eq!(result.horizon, 3);
396 assert!(result.forecast[[0, 0, 0]] > 10.0);
397 }
398}