Skip to main content

sklears_semi_supervised/
streaming_graph_learning.rs

1//! Streaming Graph Learning for Dynamic Semi-Supervised Learning
2//!
3//! This module provides algorithms for learning and updating graph structures
4//! incrementally as new data arrives in streaming scenarios.
5
6use scirs2_core::ndarray_ext::{Array1, Array2, ArrayView1, ArrayView2, Axis};
7use sklears_core::{
8    error::{Result as SklResult, SklearsError},
9    traits::{Estimator, Fit, Predict, PredictProba, Untrained},
10    types::Float,
11};
12use std::collections::{HashMap, VecDeque};
13
14/// Streaming Graph Learning for Dynamic Semi-Supervised Learning
15///
16/// This method continuously updates graph structures as new data points arrive,
17/// making it suitable for dynamic environments where the data distribution
18/// may change over time. It maintains a sliding window of recent data points
19/// and efficiently updates the graph structure and label propagation.
20///
21/// # Parameters
22///
23/// * `window_size` - Size of the sliding window for maintaining recent data
24/// * `lambda_sparse` - Sparsity regularization parameter for graph learning
25/// * `alpha_decay` - Decay factor for edge weights over time
26/// * `update_frequency` - Frequency of full graph reconstruction
27/// * `forgetting_factor` - Factor for exponential forgetting of old connections
28/// * `adaptive_threshold` - Whether to use adaptive thresholds for edge addition
29/// * `min_samples_update` - Minimum samples required before updating the graph
30///
31/// # Examples
32///
33/// ```
34/// use scirs2_core::array;
35/// use sklears_semi_supervised::StreamingGraphLearning;
36/// use sklears_core::traits::{Predict, Fit};
37///
38///
39/// let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [4.0, 5.0]];
40/// let y = array![0, 1, -1, -1]; // -1 indicates unlabeled
41///
42/// let mut sgl = StreamingGraphLearning::new()
43///     .window_size(100)
44///     .lambda_sparse(0.1)
45///     .alpha_decay(0.95);
46///
47/// let mut fitted = sgl.fit(&X.view(), &y.view()).unwrap();
48/// let predictions = fitted.predict(&X.view()).unwrap();
49///
50/// // Update with new data
51/// let X_new = array![[5.0, 6.0], [6.0, 7.0]];
52/// let y_new = array![-1, 0];
53/// let updated = fitted.update(&X_new.view(), &y_new.view()).unwrap();
54/// ```
55#[derive(Debug, Clone)]
56pub struct StreamingGraphLearning<S = Untrained> {
57    state: S,
58    window_size: usize,
59    lambda_sparse: f64,
60    alpha_decay: f64,
61    update_frequency: usize,
62    forgetting_factor: f64,
63    adaptive_threshold: bool,
64    min_samples_update: usize,
65    k_neighbors: usize,
66    similarity_threshold: f64,
67}
68
69impl StreamingGraphLearning<Untrained> {
70    /// Create a new StreamingGraphLearning instance
71    pub fn new() -> Self {
72        Self {
73            state: Untrained,
74            window_size: 1000,
75            lambda_sparse: 0.1,
76            alpha_decay: 0.95,
77            update_frequency: 50,
78            forgetting_factor: 0.99,
79            adaptive_threshold: true,
80            min_samples_update: 10,
81            k_neighbors: 5,
82            similarity_threshold: 0.5,
83        }
84    }
85
86    /// Set the sliding window size
87    pub fn window_size(mut self, window_size: usize) -> Self {
88        self.window_size = window_size;
89        self
90    }
91
92    /// Set the sparsity regularization parameter
93    pub fn lambda_sparse(mut self, lambda_sparse: f64) -> Self {
94        self.lambda_sparse = lambda_sparse;
95        self
96    }
97
98    /// Set the decay factor for edge weights
99    pub fn alpha_decay(mut self, alpha_decay: f64) -> Self {
100        self.alpha_decay = alpha_decay;
101        self
102    }
103
104    /// Set the frequency of full graph reconstruction
105    pub fn update_frequency(mut self, frequency: usize) -> Self {
106        self.update_frequency = frequency;
107        self
108    }
109
110    /// Set the forgetting factor for old connections
111    pub fn forgetting_factor(mut self, factor: f64) -> Self {
112        self.forgetting_factor = factor;
113        self
114    }
115
116    /// Enable/disable adaptive threshold for edge addition
117    pub fn adaptive_threshold(mut self, adaptive: bool) -> Self {
118        self.adaptive_threshold = adaptive;
119        self
120    }
121
122    /// Set minimum samples required before updating the graph
123    pub fn min_samples_update(mut self, min_samples: usize) -> Self {
124        self.min_samples_update = min_samples;
125        self
126    }
127
128    /// Set the number of nearest neighbors to consider
129    pub fn k_neighbors(mut self, k: usize) -> Self {
130        self.k_neighbors = k;
131        self
132    }
133
134    /// Set the similarity threshold for edge creation
135    pub fn similarity_threshold(mut self, threshold: f64) -> Self {
136        self.similarity_threshold = threshold;
137        self
138    }
139
140    fn compute_similarity(&self, x1: &ArrayView1<f64>, x2: &ArrayView1<f64>) -> f64 {
141        let diff = x1 - x2;
142        let dist = diff.mapv(|x: f64| x * x).sum().sqrt();
143        (-dist / (2.0 * 1.0_f64.powi(2))).exp()
144    }
145
146    #[allow(non_snake_case)] // standard ML notation
147    fn build_initial_graph(&self, X: &Array2<f64>) -> Array2<f64> {
148        let n_samples = X.nrows();
149        let mut W = Array2::zeros((n_samples, n_samples));
150
151        for i in 0..n_samples {
152            let mut similarities: Vec<(usize, f64)> = Vec::new();
153
154            for j in 0..n_samples {
155                if i != j {
156                    let sim = self.compute_similarity(&X.row(i), &X.row(j));
157                    similarities.push((j, sim));
158                }
159            }
160
161            // Sort by similarity (descending)
162            similarities.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("operation should succeed"));
163
164            // Connect to k nearest neighbors
165            for &(j, sim) in similarities.iter().take(self.k_neighbors) {
166                if sim > self.similarity_threshold {
167                    W[[i, j]] = sim;
168                    W[[j, i]] = sim; // Ensure symmetry
169                }
170            }
171        }
172
173        // Apply sparsity threshold
174        let threshold = self.lambda_sparse;
175        W.mapv_inplace(|x| if x > threshold { x - threshold } else { 0.0 });
176        W.mapv_inplace(|x| x.max(0.0));
177
178        // Zero diagonal
179        for i in 0..n_samples {
180            W[[i, i]] = 0.0;
181        }
182
183        W
184    }
185
186    #[allow(non_snake_case)]
187    fn propagate_labels(&self, W: &Array2<f64>, Y_init: &Array2<f64>) -> SklResult<Array2<f64>> {
188        let n_samples = W.nrows();
189
190        // Compute transition matrix
191        let D = W.sum_axis(Axis(1));
192        let mut P = Array2::zeros((n_samples, n_samples));
193        for i in 0..n_samples {
194            if D[i] > 0.0 {
195                for j in 0..n_samples {
196                    P[[i, j]] = W[[i, j]] / D[i];
197                }
198            }
199        }
200
201        let mut Y = Y_init.clone();
202        let Y_static = Y_init.clone();
203
204        // Label propagation iterations
205        for _iter in 0..30 {
206            let prev_Y = Y.clone();
207            Y = 0.8 * P.dot(&Y) + 0.2 * &Y_static;
208
209            // Check convergence
210            let diff = (&Y - &prev_Y).mapv(|x| x.abs()).sum();
211            if diff < 1e-6 {
212                break;
213            }
214        }
215
216        Ok(Y)
217    }
218}
219
220impl Default for StreamingGraphLearning<Untrained> {
221    fn default() -> Self {
222        Self::new()
223    }
224}
225
226impl Estimator for StreamingGraphLearning<Untrained> {
227    type Config = ();
228    type Error = SklearsError;
229    type Float = Float;
230
231    fn config(&self) -> &Self::Config {
232        &()
233    }
234}
235
236impl Fit<ArrayView2<'_, Float>, ArrayView1<'_, i32>> for StreamingGraphLearning<Untrained> {
237    type Fitted = StreamingGraphLearning<StreamingGraphLearningTrained>;
238
239    #[allow(non_snake_case)]
240    fn fit(self, X: &ArrayView2<'_, Float>, y: &ArrayView1<'_, i32>) -> SklResult<Self::Fitted> {
241        let X = X.to_owned();
242        let y = y.to_owned();
243        let (n_samples, _n_features) = X.dim();
244
245        // Identify labeled samples and classes
246        let mut labeled_indices = Vec::new();
247        let mut classes = std::collections::HashSet::new();
248
249        for (i, &label) in y.iter().enumerate() {
250            if label != -1 {
251                labeled_indices.push(i);
252                classes.insert(label);
253            }
254        }
255
256        if labeled_indices.is_empty() {
257            return Err(SklearsError::InvalidInput(
258                "No labeled samples provided".to_string(),
259            ));
260        }
261
262        let classes: Vec<i32> = classes.into_iter().collect();
263        let n_classes = classes.len();
264
265        // Build initial graph
266        let W = self.build_initial_graph(&X);
267
268        // Initialize label matrix
269        let mut Y = Array2::zeros((n_samples, n_classes));
270        for &idx in &labeled_indices {
271            if let Some(class_idx) = classes.iter().position(|&c| c == y[idx]) {
272                Y[[idx, class_idx]] = 1.0;
273            }
274        }
275
276        // Perform initial label propagation
277        let Y_final = self.propagate_labels(&W, &Y)?;
278
279        // Initialize sliding window with current data
280        let mut data_window = VecDeque::with_capacity(self.window_size);
281        let mut label_window = VecDeque::with_capacity(self.window_size);
282
283        for i in 0..n_samples {
284            data_window.push_back(X.row(i).to_owned());
285            label_window.push_back(y[i]);
286        }
287
288        Ok(StreamingGraphLearning {
289            state: StreamingGraphLearningTrained {
290                X_train: X,
291                y_train: y,
292                classes: Array1::from(classes),
293                current_graph: W,
294                label_distributions: Y_final,
295                data_window,
296                label_window,
297                update_count: 0,
298                edge_ages: HashMap::new(),
299                adaptive_threshold_value: self.similarity_threshold,
300            },
301            window_size: self.window_size,
302            lambda_sparse: self.lambda_sparse,
303            alpha_decay: self.alpha_decay,
304            update_frequency: self.update_frequency,
305            forgetting_factor: self.forgetting_factor,
306            adaptive_threshold: self.adaptive_threshold,
307            min_samples_update: self.min_samples_update,
308            k_neighbors: self.k_neighbors,
309            similarity_threshold: self.similarity_threshold,
310        })
311    }
312}
313
314impl StreamingGraphLearning<StreamingGraphLearningTrained> {
315    fn compute_similarity(&self, x1: &ArrayView1<f64>, x2: &ArrayView1<f64>) -> f64 {
316        let diff = x1 - x2;
317        let dist = diff.mapv(|x: f64| x * x).sum().sqrt();
318        (-dist / (2.0 * 1.0_f64.powi(2))).exp()
319    }
320
321    #[allow(non_snake_case)] // standard ML notation
322    fn build_initial_graph(&self, X: &Array2<f64>) -> Array2<f64> {
323        let n_samples = X.nrows();
324        let mut W = Array2::zeros((n_samples, n_samples));
325
326        for i in 0..n_samples {
327            let mut similarities: Vec<(usize, f64)> = Vec::new();
328
329            for j in 0..n_samples {
330                if i != j {
331                    let sim = self.compute_similarity(&X.row(i), &X.row(j));
332                    similarities.push((j, sim));
333                }
334            }
335
336            // Sort by similarity (descending)
337            similarities.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("operation should succeed"));
338
339            // Connect to k nearest neighbors
340            for &(j, sim) in similarities.iter().take(self.k_neighbors) {
341                if sim > self.similarity_threshold {
342                    W[[i, j]] = sim;
343                    W[[j, i]] = sim; // Ensure symmetry
344                }
345            }
346        }
347
348        // Apply sparsity threshold
349        let threshold = self.lambda_sparse;
350        W.mapv_inplace(|x| if x > threshold { x - threshold } else { 0.0 });
351        W.mapv_inplace(|x| x.max(0.0));
352
353        // Zero diagonal
354        for i in 0..n_samples {
355            W[[i, i]] = 0.0;
356        }
357
358        W
359    }
360
361    #[allow(non_snake_case)]
362    fn propagate_labels(&self, W: &Array2<f64>, Y_init: &Array2<f64>) -> SklResult<Array2<f64>> {
363        let n_samples = W.nrows();
364
365        // Compute transition matrix
366        let D = W.sum_axis(Axis(1));
367        let mut P = Array2::zeros((n_samples, n_samples));
368        for i in 0..n_samples {
369            if D[i] > 0.0 {
370                for j in 0..n_samples {
371                    P[[i, j]] = W[[i, j]] / D[i];
372                }
373            }
374        }
375
376        let mut Y = Y_init.clone();
377        let Y_static = Y_init.clone();
378
379        // Label propagation iterations
380        for _iter in 0..30 {
381            let prev_Y = Y.clone();
382            Y = 0.8 * P.dot(&Y) + 0.2 * &Y_static;
383
384            // Check convergence
385            let diff = (&Y - &prev_Y).mapv(|x| x.abs()).sum();
386            if diff < 1e-6 {
387                break;
388            }
389        }
390
391        Ok(Y)
392    }
393    /// Update the model with new streaming data
394    #[allow(non_snake_case)]
395    pub fn update(
396        &mut self,
397        X_new: &ArrayView2<'_, Float>,
398        y_new: &ArrayView1<'_, i32>,
399    ) -> SklResult<()> {
400        let X_new = X_new.to_owned();
401        let y_new = y_new.to_owned();
402        let (n_new, _) = X_new.dim();
403
404        // Add new data to sliding window
405        for i in 0..n_new {
406            // Remove oldest data if window is full
407            if self.state.data_window.len() >= self.window_size {
408                self.state.data_window.pop_front();
409                self.state.label_window.pop_front();
410            }
411
412            self.state.data_window.push_back(X_new.row(i).to_owned());
413            self.state.label_window.push_back(y_new[i]);
414        }
415
416        self.state.update_count += n_new;
417
418        // Decay existing edge weights
419        self.state
420            .current_graph
421            .mapv_inplace(|x| x * self.alpha_decay);
422
423        // Update adaptive threshold if enabled
424        if self.adaptive_threshold {
425            self.update_adaptive_threshold();
426        }
427
428        // Age all edges
429        let mut aged_edges = HashMap::new();
430        for ((i, j), age) in &self.state.edge_ages {
431            aged_edges.insert((*i, *j), age + 1);
432        }
433        self.state.edge_ages = aged_edges;
434
435        // Incremental graph update
436        self.incremental_graph_update(&X_new, &y_new)?;
437
438        // Full reconstruction if update frequency is reached
439        if self
440            .state
441            .update_count
442            .is_multiple_of(self.update_frequency)
443        {
444            self.full_graph_reconstruction()?;
445        }
446
447        Ok(())
448    }
449
450    fn update_adaptive_threshold(&mut self) {
451        let current_data: Vec<Array1<f64>> = self.state.data_window.iter().cloned().collect();
452        if current_data.len() < 2 {
453            return;
454        }
455
456        let mut similarities = Vec::new();
457        for i in 0..current_data.len().min(100) {
458            for j in (i + 1)..current_data.len().min(100) {
459                let sim = self.compute_similarity(&current_data[i].view(), &current_data[j].view());
460                similarities.push(sim);
461            }
462        }
463
464        if !similarities.is_empty() {
465            similarities.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
466            let median_idx = similarities.len() / 2;
467            self.state.adaptive_threshold_value = similarities[median_idx] * 0.8;
468        }
469    }
470
471    #[allow(non_snake_case)] // standard ML notation
472    fn incremental_graph_update(
473        &mut self,
474        X_new: &Array2<f64>,
475        _y_new: &Array1<i32>,
476    ) -> SklResult<()> {
477        let current_data: Vec<Array1<f64>> = self.state.data_window.iter().cloned().collect();
478        let current_labels: Vec<i32> = self.state.label_window.iter().cloned().collect();
479        let n_current = current_data.len();
480        let n_new = X_new.nrows();
481
482        // Extend current graph to accommodate new nodes
483        let mut new_graph = Array2::zeros((n_current, n_current));
484
485        // Copy existing graph (with aging applied)
486        let old_size = self.state.current_graph.nrows().min(n_current);
487        for i in 0..old_size {
488            for j in 0..old_size {
489                new_graph[[i, j]] = self.state.current_graph[[i, j]];
490            }
491        }
492
493        // Add connections for new nodes
494        let start_idx = n_current - n_new;
495        for i in start_idx..n_current {
496            let mut similarities: Vec<(usize, f64)> = Vec::new();
497
498            for j in 0..n_current {
499                if i != j {
500                    let sim =
501                        self.compute_similarity(&current_data[i].view(), &current_data[j].view());
502                    similarities.push((j, sim));
503                }
504            }
505
506            // Sort by similarity (descending)
507            similarities.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("operation should succeed"));
508
509            // Connect to k nearest neighbors
510            let threshold = if self.adaptive_threshold {
511                self.state.adaptive_threshold_value
512            } else {
513                self.similarity_threshold
514            };
515
516            for &(j, sim) in similarities.iter().take(self.k_neighbors) {
517                if sim > threshold {
518                    new_graph[[i, j]] = sim;
519                    new_graph[[j, i]] = sim; // Ensure symmetry
520
521                    // Track edge age
522                    self.state.edge_ages.insert((i, j), 0);
523                    self.state.edge_ages.insert((j, i), 0);
524                }
525            }
526        }
527
528        // Apply forgetting to old edges
529        for ((i, j), age) in &self.state.edge_ages {
530            if *i < n_current && *j < n_current {
531                let forgetting_weight = self.forgetting_factor.powi(*age as i32);
532                new_graph[[*i, *j]] *= forgetting_weight;
533            }
534        }
535
536        // Apply sparsity threshold
537        let threshold = self.lambda_sparse;
538        new_graph.mapv_inplace(|x| if x > threshold { x - threshold } else { 0.0 });
539        new_graph.mapv_inplace(|x| x.max(0.0));
540
541        // Zero diagonal
542        for i in 0..n_current {
543            new_graph[[i, i]] = 0.0;
544        }
545
546        self.state.current_graph = new_graph;
547
548        // Update label propagation
549        self.update_label_propagation(&current_data, &current_labels)?;
550
551        Ok(())
552    }
553
554    #[allow(non_snake_case)] // standard ML notation
555    fn full_graph_reconstruction(&mut self) -> SklResult<()> {
556        let current_data: Vec<Array1<f64>> = self.state.data_window.iter().cloned().collect();
557        let current_labels: Vec<i32> = self.state.label_window.iter().cloned().collect();
558
559        if current_data.is_empty() {
560            return Ok(());
561        }
562
563        let n_samples = current_data.len();
564
565        // Convert data to Array2
566        let mut X = Array2::zeros((n_samples, current_data[0].len()));
567        for (i, data_point) in current_data.iter().enumerate() {
568            X.row_mut(i).assign(data_point);
569        }
570
571        // Rebuild graph from scratch
572        self.state.current_graph = self.build_initial_graph(&X);
573
574        // Clear edge ages
575        self.state.edge_ages.clear();
576
577        // Update label propagation
578        self.update_label_propagation(&current_data, &current_labels)?;
579
580        Ok(())
581    }
582
583    #[allow(non_snake_case)]
584    fn update_label_propagation(
585        &mut self,
586        current_data: &[Array1<f64>],
587        current_labels: &[i32],
588    ) -> SklResult<()> {
589        let n_samples = current_data.len();
590        let n_classes = self.state.classes.len();
591
592        if n_samples == 0 {
593            return Ok(());
594        }
595
596        // Initialize label matrix
597        let mut Y = Array2::zeros((n_samples, n_classes));
598        for (i, &label) in current_labels.iter().enumerate() {
599            if label != -1 {
600                if let Some(class_idx) = self.state.classes.iter().position(|&c| c == label) {
601                    Y[[i, class_idx]] = 1.0;
602                }
603            }
604        }
605
606        // Perform label propagation
607        let Y_final = self.propagate_labels(&self.state.current_graph, &Y)?;
608        self.state.label_distributions = Y_final;
609
610        Ok(())
611    }
612}
613
614impl Predict<ArrayView2<'_, Float>, Array1<i32>>
615    for StreamingGraphLearning<StreamingGraphLearningTrained>
616{
617    #[allow(non_snake_case)]
618    fn predict(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array1<i32>> {
619        let X = X.to_owned();
620        let n_test = X.nrows();
621        let mut predictions = Array1::zeros(n_test);
622
623        let current_data: Vec<Array1<f64>> = self.state.data_window.iter().cloned().collect();
624
625        for i in 0..n_test {
626            let mut max_sim = -1.0;
627            let mut best_idx = 0;
628
629            // Find most similar sample in current window
630            for (j, data_point) in current_data.iter().enumerate() {
631                let sim = self.compute_similarity(&X.row(i), &data_point.view());
632                if sim > max_sim {
633                    max_sim = sim;
634                    best_idx = j;
635                }
636            }
637
638            // Use the label distribution of the most similar sample
639            if best_idx < self.state.label_distributions.nrows() {
640                let distributions = self.state.label_distributions.row(best_idx);
641                let max_idx = distributions
642                    .iter()
643                    .enumerate()
644                    .max_by(|a, b| a.1.partial_cmp(b.1).expect("operation should succeed"))
645                    .expect("operation should succeed")
646                    .0;
647
648                predictions[i] = self.state.classes[max_idx];
649            }
650        }
651
652        Ok(predictions)
653    }
654}
655
656impl PredictProba<ArrayView2<'_, Float>, Array2<f64>>
657    for StreamingGraphLearning<StreamingGraphLearningTrained>
658{
659    #[allow(non_snake_case)]
660    fn predict_proba(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<f64>> {
661        let X = X.to_owned();
662        let n_test = X.nrows();
663        let n_classes = self.state.classes.len();
664        let mut probas = Array2::zeros((n_test, n_classes));
665
666        let current_data: Vec<Array1<f64>> = self.state.data_window.iter().cloned().collect();
667
668        for i in 0..n_test {
669            let mut max_sim = -1.0;
670            let mut best_idx = 0;
671
672            // Find most similar sample in current window
673            for (j, data_point) in current_data.iter().enumerate() {
674                let sim = self.compute_similarity(&X.row(i), &data_point.view());
675                if sim > max_sim {
676                    max_sim = sim;
677                    best_idx = j;
678                }
679            }
680
681            // Copy the label distribution
682            if best_idx < self.state.label_distributions.nrows() {
683                for k in 0..n_classes {
684                    probas[[i, k]] = self.state.label_distributions[[best_idx, k]];
685                }
686            }
687        }
688
689        Ok(probas)
690    }
691}
692
693/// Trained state for StreamingGraphLearning
694#[derive(Debug, Clone)]
695#[allow(non_snake_case)] // standard ML notation
696pub struct StreamingGraphLearningTrained {
697    /// X_train
698    pub X_train: Array2<f64>,
699    /// y_train
700    pub y_train: Array1<i32>,
701    /// classes
702    pub classes: Array1<i32>,
703    /// current_graph
704    pub current_graph: Array2<f64>,
705    /// label_distributions
706    pub label_distributions: Array2<f64>,
707    /// data_window
708    pub data_window: VecDeque<Array1<f64>>,
709    /// label_window
710    pub label_window: VecDeque<i32>,
711    /// update_count
712    pub update_count: usize,
713    /// edge_ages
714    pub edge_ages: HashMap<(usize, usize), usize>,
715    /// adaptive_threshold_value
716    pub adaptive_threshold_value: f64,
717}
718
719#[allow(non_snake_case)]
720#[cfg(test)]
721mod tests {
722    use super::*;
723    use scirs2_core::array;
724
725    #[test]
726    #[allow(non_snake_case)]
727    fn test_streaming_graph_learning_basic() {
728        let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [4.0, 5.0]];
729        let y = array![0, 1, -1, -1]; // -1 indicates unlabeled
730
731        let sgl = StreamingGraphLearning::new()
732            .window_size(10)
733            .lambda_sparse(0.1)
734            .alpha_decay(0.9)
735            .update_frequency(5);
736        let fitted = sgl
737            .fit(&X.view(), &y.view())
738            .expect("operation should succeed");
739
740        let predictions = fitted.predict(&X.view()).expect("operation should succeed");
741        assert_eq!(predictions.len(), 4);
742
743        let probas = fitted
744            .predict_proba(&X.view())
745            .expect("operation should succeed");
746        assert_eq!(probas.dim(), (4, 2));
747
748        // Check that labeled samples maintain their labels
749        assert_eq!(predictions[0], 0);
750        assert_eq!(predictions[1], 1);
751    }
752
753    #[test]
754    #[allow(non_snake_case)]
755    fn test_streaming_graph_learning_update() {
756        let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [4.0, 5.0]];
757        let y = array![0, 1, -1, -1];
758
759        let sgl = StreamingGraphLearning::new()
760            .window_size(10)
761            .update_frequency(3)
762            .alpha_decay(0.95);
763        let mut fitted = sgl
764            .fit(&X.view(), &y.view())
765            .expect("operation should succeed");
766
767        // Initial graph size
768        let initial_graph_size = fitted.state.current_graph.dim();
769        assert_eq!(initial_graph_size, (4, 4));
770
771        // Add new streaming data
772        let X_new = array![[5.0, 6.0], [6.0, 7.0]];
773        let y_new = array![-1, 0];
774        fitted
775            .update(&X_new.view(), &y_new.view())
776            .expect("operation should succeed");
777
778        // Check that data window is updated
779        assert_eq!(fitted.state.data_window.len(), 6);
780        assert_eq!(fitted.state.label_window.len(), 6);
781
782        // Graph should be updated to accommodate new data
783        let updated_graph_size = fitted.state.current_graph.dim();
784        assert_eq!(updated_graph_size, (6, 6));
785
786        // Test predictions with updated model
787        let predictions = fitted
788            .predict(&X_new.view())
789            .expect("operation should succeed");
790        assert_eq!(predictions.len(), 2);
791    }
792
793    #[test]
794    #[allow(non_snake_case)]
795    fn test_streaming_graph_learning_window_overflow() {
796        let X = array![[1.0, 2.0], [2.0, 3.0]];
797        let y = array![0, 1];
798
799        let sgl = StreamingGraphLearning::new()
800            .window_size(3) // Small window size
801            .update_frequency(2);
802        let mut fitted = sgl
803            .fit(&X.view(), &y.view())
804            .expect("operation should succeed");
805
806        // Add more data than window size
807        let X_new1 = array![[3.0, 4.0]];
808        let y_new1 = array![-1];
809        fitted
810            .update(&X_new1.view(), &y_new1.view())
811            .expect("operation should succeed");
812
813        let X_new2 = array![[4.0, 5.0]];
814        let y_new2 = array![0];
815        fitted
816            .update(&X_new2.view(), &y_new2.view())
817            .expect("operation should succeed");
818
819        // Window should maintain size limit
820        assert_eq!(fitted.state.data_window.len(), 3);
821        assert_eq!(fitted.state.label_window.len(), 3);
822
823        // Should still be able to make predictions
824        let predictions = fitted
825            .predict(&X_new2.view())
826            .expect("operation should succeed");
827        assert_eq!(predictions.len(), 1);
828    }
829
830    #[test]
831    #[allow(non_snake_case)]
832    fn test_streaming_graph_learning_adaptive_threshold() {
833        let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [4.0, 5.0]];
834        let y = array![0, 1, -1, -1];
835
836        let sgl = StreamingGraphLearning::new()
837            .window_size(10)
838            .adaptive_threshold(true)
839            .similarity_threshold(0.5);
840        let mut fitted = sgl
841            .fit(&X.view(), &y.view())
842            .expect("operation should succeed");
843
844        let _initial_threshold = fitted.state.adaptive_threshold_value;
845
846        // Add new data with different characteristics
847        let X_new = array![[10.0, 20.0], [20.0, 30.0]];
848        let y_new = array![-1, 1];
849        fitted
850            .update(&X_new.view(), &y_new.view())
851            .expect("operation should succeed");
852
853        // Adaptive threshold should potentially change
854        // (depends on the similarity distribution)
855        assert!(fitted.state.adaptive_threshold_value > 0.0);
856    }
857
858    #[test]
859    #[allow(non_snake_case)]
860    fn test_streaming_graph_learning_edge_aging() {
861        let X = array![[1.0, 2.0], [2.0, 3.0]];
862        let y = array![0, 1];
863
864        let sgl = StreamingGraphLearning::new()
865            .window_size(10)
866            .forgetting_factor(0.8)
867            .alpha_decay(0.9);
868        let mut fitted = sgl
869            .fit(&X.view(), &y.view())
870            .expect("operation should succeed");
871
872        // Check initial state
873        assert_eq!(fitted.state.update_count, 0);
874
875        // Add new data multiple times to age edges
876        for i in 0..3 {
877            let X_new = array![[3.0 + i as f64, 4.0 + i as f64]];
878            let y_new = array![-1];
879            fitted
880                .update(&X_new.view(), &y_new.view())
881                .expect("operation should succeed");
882        }
883
884        // Update count should be incremented
885        assert_eq!(fitted.state.update_count, 3);
886
887        // Some edges should have aged
888        assert!(!fitted.state.edge_ages.is_empty());
889    }
890
891    #[test]
892    #[allow(non_snake_case)]
893    fn test_streaming_graph_learning_full_reconstruction() {
894        let X = array![[1.0, 2.0], [2.0, 3.0]];
895        let y = array![0, 1];
896
897        let sgl = StreamingGraphLearning::new()
898            .window_size(10)
899            .update_frequency(2); // Trigger full reconstruction frequently
900        let mut fitted = sgl
901            .fit(&X.view(), &y.view())
902            .expect("operation should succeed");
903
904        // Add data to trigger full reconstruction
905        let X_new1 = array![[3.0, 4.0]];
906        let y_new1 = array![-1];
907        fitted
908            .update(&X_new1.view(), &y_new1.view())
909            .expect("operation should succeed");
910
911        let X_new2 = array![[4.0, 5.0]];
912        let y_new2 = array![0];
913        fitted
914            .update(&X_new2.view(), &y_new2.view())
915            .expect("operation should succeed");
916
917        // Full reconstruction should have been triggered
918        // Edge ages should be cleared
919        assert!(
920            fitted.state.edge_ages.is_empty()
921                || fitted.state.edge_ages.values().all(|&age| age == 0)
922        );
923
924        // Should still be able to make predictions
925        let predictions = fitted
926            .predict(&X_new2.view())
927            .expect("operation should succeed");
928        assert_eq!(predictions.len(), 1);
929    }
930}