11use crate :: CoreError ;
2+ use nalgebra:: { DMatrix , DVector } ;
23use ndarray:: { Axis , IxDyn , Zip } ;
34use numpy:: { IntoPyArray , PyArrayDyn , PyReadonlyArrayDyn } ;
45use pyo3:: prelude:: * ;
56use rayon:: prelude:: * ;
67
8+ const TWO_PI : f64 = 2.0 * std:: f64:: consts:: PI ;
9+
710// --- 1. BFAST Monitor Workflow ---
811
9- // Placeholder struct for model parameters
12+ /// Represents the fitted harmonic model.
1013struct HarmonicModel {
11- mean : f64 ,
14+ coefficients : DVector < f64 > ,
15+ sigma : f64 ,
1216}
1317
14- // Placeholder for fitting a harmonic model to the stable history period.
15- // In a real implementation, this would involve solving for harmonic coefficients.
16- fn fit_harmonic_model ( y : & [ f64 ] ) -> HarmonicModel {
17- if y. is_empty ( ) {
18- return HarmonicModel { mean : 0.0 } ;
18+ /// Constructs the design matrix for a harmonic model.
19+ ///
20+ /// # Arguments
21+ ///
22+ /// * `dates` - A slice of fractional years.
23+ /// * `order` - The order of the harmonic model (e.g., 1 for one sine/cosine pair).
24+ ///
25+ /// # Returns
26+ ///
27+ /// A 2D array representing the design matrix `X`.
28+ fn build_design_matrix ( dates : & [ f64 ] , order : usize ) -> DMatrix < f64 > {
29+ let n = dates. len ( ) ;
30+ let num_coeffs = 2 * order + 2 ; // intercept, trend, and sin/cos pairs
31+ let mut x = DMatrix :: < f64 > :: zeros ( n, num_coeffs) ;
32+
33+ for i in 0 ..n {
34+ let t = dates[ i] ;
35+ x[ ( i, 0 ) ] = 1.0 ; // Intercept
36+ x[ ( i, 1 ) ] = t; // Trend
37+ for j in 1 ..=order {
38+ let freq = TWO_PI * j as f64 * t;
39+ x[ ( i, 2 * j) ] = freq. cos ( ) ;
40+ x[ ( i, 2 * j + 1 ) ] = freq. sin ( ) ;
41+ }
1942 }
20- let sum: f64 = y. iter ( ) . sum ( ) ;
21- HarmonicModel {
22- mean : sum / y. len ( ) as f64 ,
43+ x
44+ }
45+
46+ /// Fits a harmonic model to the stable history period using Ordinary Least Squares (OLS).
47+ fn fit_harmonic_model ( y : & [ f64 ] , dates : & [ f64 ] , order : usize ) -> Result < HarmonicModel , CoreError > {
48+ if y. len ( ) < ( 2 * order + 2 ) {
49+ return Err ( CoreError :: NotEnoughData (
50+ "Not enough historical data to fit model" . to_string ( ) ,
51+ ) ) ;
2352 }
53+
54+ let y_vec = DVector :: from_vec ( y. to_vec ( ) ) ;
55+ let x = build_design_matrix ( dates, order) ;
56+
57+ let decomp = x. clone ( ) . svd ( true , true ) ;
58+ let coeffs = decomp. solve ( & y_vec, 1e-10 ) . map_err ( |e| {
59+ CoreError :: ComputationError ( format ! ( "Failed to solve OLS with nalgebra: {}" , e) )
60+ } ) ?;
61+
62+ let y_pred = & x * & coeffs;
63+ let residuals = & y_vec - & y_pred;
64+ let sum_sq_err = residuals. iter ( ) . map ( |& r| r * r) . sum :: < f64 > ( ) ;
65+ let df = ( y. len ( ) - ( 2 * order + 2 ) ) as f64 ;
66+ if df <= 0.0 {
67+ return Err ( CoreError :: ComputationError (
68+ "Degrees of freedom is non-positive" . to_string ( ) ,
69+ ) ) ;
70+ }
71+ let sigma = ( sum_sq_err / df) . sqrt ( ) ;
72+
73+ Ok ( HarmonicModel {
74+ coefficients : coeffs,
75+ sigma,
76+ } )
2477}
2578
26- // Placeholder for predicting values based on the fitted model.
27- fn predict_harmonic_model ( _model : & HarmonicModel , dates : & [ i64 ] ) -> Vec < f64 > {
28- // For now, just return a constant prediction (the historical mean)
29- vec ! [ _model . mean ; dates . len ( ) ]
79+ /// Predicts values for the monitoring period based on the fitted model.
80+ fn predict_harmonic_model ( model : & HarmonicModel , dates : & [ f64 ] , order : usize ) -> DVector < f64 > {
81+ let x_mon = build_design_matrix ( dates , order ) ;
82+ & x_mon * & model . coefficients
3083}
3184
32- // Placeholder for the MOSUM process to detect a break.
33- // Returns (break_date, magnitude)
85+ /// Detects a break using the OLS-MOSUM process.
3486fn detect_mosum_break (
3587 y_monitor : & [ f64 ] ,
36- y_pred : & [ f64 ] ,
37- monitor_dates : & [ i64 ] ,
38- level : f64 ,
88+ y_pred : & DVector < f64 > ,
89+ monitor_dates : & [ f64 ] ,
90+ hist_len : usize ,
91+ sigma : f64 ,
92+ h : f64 ,
93+ alpha : f64 ,
3994) -> ( f64 , f64 ) {
40- if y_monitor. is_empty ( ) || y_monitor . len ( ) != y_pred . len ( ) {
95+ if y_monitor. is_empty ( ) {
4196 return ( 0.0 , 0.0 ) ;
4297 }
4398
99+ let n_hist = hist_len as f64 ;
100+ let window_size = ( h * n_hist) . floor ( ) as usize ;
101+
44102 let residuals: Vec < f64 > = y_monitor
45103 . iter ( )
46104 . zip ( y_pred. iter ( ) )
47105 . map ( |( obs, pred) | obs - pred)
48106 . collect ( ) ;
49107
50- let mean_residual: f64 = residuals. iter ( ) . sum :: < f64 > ( ) / residuals. len ( ) as f64 ;
108+ let mut cusum = vec ! [ 0.0 ; residuals. len( ) + 1 ] ;
109+ for i in 0 ..residuals. len ( ) {
110+ cusum[ i + 1 ] = cusum[ i] + residuals[ i] ;
111+ }
112+
113+ // We can only start calculating MOSUM after `window_size` observations
114+ if residuals. len ( ) < window_size {
115+ return ( 0.0 , 0.0 ) ;
116+ }
117+
118+ let mosum_process: Vec < f64 > = ( window_size..residuals. len ( ) )
119+ . map ( |i| cusum[ i] - cusum[ i - window_size] )
120+ . collect ( ) ;
121+
122+ let standardizer = sigma * n_hist. sqrt ( ) ;
123+ let standardized_mosum: Vec < f64 > = mosum_process
124+ . iter ( )
125+ . map ( |& m| ( m / standardizer) . abs ( ) )
126+ . collect ( ) ;
127+
128+ // Simplified critical boundary based on a lookup for alpha=0.05 and h=0.25
129+ // A full implementation would use a precomputed table or a more complex calculation.
130+ let critical_value = if alpha <= 0.05 { 1.36 } else { 1.63 } ; // Approximations
51131
52- // Simplified break detection: if the average residual in the monitoring period
53- // exceeds the significance level, flag the start of the period as a break.
54- if mean_residual. abs ( ) > level {
55- ( monitor_dates[ 0 ] as f64 , mean_residual. abs ( ) )
56- } else {
57- ( 0.0 , 0.0 ) // No break detected
132+ for ( i, & mosum_val) in standardized_mosum. iter ( ) . enumerate ( ) {
133+ // The index k starts from 1 for the monitoring period
134+ let k = ( i + 1 ) as f64 ;
135+ let boundary = critical_value * ( 1.0 + k / n_hist) . sqrt ( ) ;
136+
137+ if mosum_val > boundary {
138+ let break_idx = i + window_size;
139+ let magnitude = ( y_monitor[ break_idx] - y_pred[ break_idx] ) . abs ( ) ;
140+ return ( monitor_dates[ break_idx] , magnitude) ;
141+ }
58142 }
143+
144+ ( 0.0 , 0.0 ) // No break detected
145+ }
146+
147+ /// Converts integer dates (YYYYMMDD) to fractional years.
148+ fn dates_to_frac_years ( dates : & [ i64 ] ) -> Vec < f64 > {
149+ dates
150+ . iter ( )
151+ . map ( |& date| {
152+ let year = ( date / 10000 ) as f64 ;
153+ let month = ( ( date % 10000 ) / 100 ) as f64 ;
154+ let day = ( date % 100 ) as f64 ;
155+ // Simple approximation
156+ year + ( month - 1.0 ) / 12.0 + ( day - 1.0 ) / 365.25
157+ } )
158+ . collect ( )
59159}
60160
61161// This is the main logic function that runs for each pixel.
62162fn run_bfast_monitor_per_pixel (
63163 pixel_ts : & [ f64 ] ,
64- dates : & [ i64 ] ,
65- history_start : i64 ,
66- monitor_start : i64 ,
67- level : f64 ,
164+ dates : & [ f64 ] ,
165+ history_start : f64 ,
166+ monitor_start : f64 ,
167+ order : usize ,
168+ h : f64 , // h parameter for MOSUM window size
169+ alpha : f64 , // Significance level
68170) -> ( f64 , f64 ) {
69171 // 1. Find the indices for the history and monitoring periods
70172 let history_indices: Vec < usize > = dates
@@ -82,32 +184,48 @@ fn run_bfast_monitor_per_pixel(
82184 . collect ( ) ;
83185
84186 if history_indices. is_empty ( ) || monitor_indices. is_empty ( ) {
85- return ( 0.0 , 0.0 ) ; // Not enough data
187+ return ( 0.0 , 0.0 ) ;
86188 }
87189
88190 // 2. Extract the data for these periods
89191 let history_ts: Vec < f64 > = history_indices. iter ( ) . map ( |& i| pixel_ts[ i] ) . collect ( ) ;
192+ let history_dates: Vec < f64 > = history_indices. iter ( ) . map ( |& i| dates[ i] ) . collect ( ) ;
90193 let monitor_ts: Vec < f64 > = monitor_indices. iter ( ) . map ( |& i| pixel_ts[ i] ) . collect ( ) ;
91- let monitor_dates: Vec < i64 > = monitor_indices. iter ( ) . map ( |& i| dates[ i] ) . collect ( ) ;
194+ let monitor_dates: Vec < f64 > = monitor_indices. iter ( ) . map ( |& i| dates[ i] ) . collect ( ) ;
92195
93196 // 3. Fit model on the historical period
94- let model = fit_harmonic_model ( & history_ts) ;
197+ let model_result = fit_harmonic_model ( & history_ts, & history_dates, order) ;
198+ let model = match model_result {
199+ Ok ( m) => m,
200+ Err ( _) => return ( 0.0 , 0.0 ) , // Return no-break if model fails
201+ } ;
95202
96203 // 4. Predict for the monitoring period
97- let predicted_ts = predict_harmonic_model ( & model, & monitor_dates) ;
204+ let predicted_ts = predict_harmonic_model ( & model, & monitor_dates, order ) ;
98205
99206 // 5. Detect break using MOSUM process on residuals
100- detect_mosum_break ( & monitor_ts, & predicted_ts, & monitor_dates, level)
207+ detect_mosum_break (
208+ & monitor_ts,
209+ & predicted_ts,
210+ & monitor_dates,
211+ history_ts. len ( ) ,
212+ model. sigma ,
213+ h,
214+ alpha,
215+ )
101216}
102217
103218#[ pyfunction]
219+ #[ allow( clippy:: too_many_arguments) ]
104220pub fn bfast_monitor (
105221 py : Python ,
106222 stack : PyReadonlyArrayDyn < f64 > ,
107223 dates : Vec < i64 > ,
108224 history_start_date : i64 ,
109225 monitor_start_date : i64 ,
110- level : f64 , // Significance level
226+ order : usize ,
227+ h : f64 ,
228+ alpha : f64 ,
111229) -> PyResult < Py < PyArrayDyn < f64 > > > {
112230 let stack_arr = stack. as_array ( ) ;
113231
@@ -132,6 +250,11 @@ pub fn bfast_monitor(
132250 . into ( ) ) ;
133251 }
134252
253+ // Convert integer dates to fractional years for modeling
254+ let frac_dates = dates_to_frac_years ( & dates) ;
255+ let history_start_frac = dates_to_frac_years ( & [ history_start_date] ) [ 0 ] ;
256+ let monitor_start_frac = dates_to_frac_years ( & [ monitor_start_date] ) [ 0 ] ;
257+
135258 // Output channels: [break_date, magnitude]
136259 let mut out_array = ndarray:: ArrayD :: < f64 > :: zeros ( IxDyn ( & [ 2 , height, width] ) ) ;
137260
@@ -158,10 +281,12 @@ pub fn bfast_monitor(
158281 . par_for_each ( |break_date, magnitude, pixel_ts| {
159282 let ( bk_date, mag) = run_bfast_monitor_per_pixel (
160283 pixel_ts. as_slice ( ) . unwrap ( ) ,
161- & dates,
162- history_start_date,
163- monitor_start_date,
164- level,
284+ & frac_dates,
285+ history_start_frac,
286+ monitor_start_frac,
287+ order,
288+ h,
289+ alpha,
165290 ) ;
166291 * break_date = bk_date;
167292 * magnitude = mag;
@@ -194,16 +319,18 @@ pub fn complex_classification(
194319
195320 let mut out = ndarray:: ArrayD :: < u8 > :: zeros ( blue_arr. raw_dim ( ) ) ;
196321
197- out. indexed_iter_mut ( ) . par_bridge ( ) . for_each ( |( idx, res) | {
198- let b = blue_arr[ & idx] ;
199- let g = green_arr[ & idx] ;
200- let r = red_arr[ & idx] ;
201- let n = nir_arr[ & idx] ;
202- let s1 = swir1_arr[ & idx] ;
203- let s2 = swir2_arr[ & idx] ;
204- let t = temp_arr[ & idx] ;
205- * res = classify_pixel ( b, g, r, n, s1, s2, t) ;
206- } ) ;
322+ out. indexed_iter_mut ( )
323+ . par_bridge ( )
324+ . for_each ( |( idx, res) | {
325+ let b = blue_arr[ & idx] ;
326+ let g = green_arr[ & idx] ;
327+ let r = red_arr[ & idx] ;
328+ let n = nir_arr[ & idx] ;
329+ let s1 = swir1_arr[ & idx] ;
330+ let s2 = swir2_arr[ & idx] ;
331+ let t = temp_arr[ & idx] ;
332+ * res = classify_pixel ( b, g, r, n, s1, s2, t) ;
333+ } ) ;
207334
208335 Ok ( out. into_pyarray ( py) . to_owned ( ) )
209336}
0 commit comments