1use 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#[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 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 pub fn window_size(mut self, window_size: usize) -> Self {
88 self.window_size = window_size;
89 self
90 }
91
92 pub fn lambda_sparse(mut self, lambda_sparse: f64) -> Self {
94 self.lambda_sparse = lambda_sparse;
95 self
96 }
97
98 pub fn alpha_decay(mut self, alpha_decay: f64) -> Self {
100 self.alpha_decay = alpha_decay;
101 self
102 }
103
104 pub fn update_frequency(mut self, frequency: usize) -> Self {
106 self.update_frequency = frequency;
107 self
108 }
109
110 pub fn forgetting_factor(mut self, factor: f64) -> Self {
112 self.forgetting_factor = factor;
113 self
114 }
115
116 pub fn adaptive_threshold(mut self, adaptive: bool) -> Self {
118 self.adaptive_threshold = adaptive;
119 self
120 }
121
122 pub fn min_samples_update(mut self, min_samples: usize) -> Self {
124 self.min_samples_update = min_samples;
125 self
126 }
127
128 pub fn k_neighbors(mut self, k: usize) -> Self {
130 self.k_neighbors = k;
131 self
132 }
133
134 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)] 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 similarities.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("operation should succeed"));
163
164 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; }
170 }
171 }
172
173 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 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 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 for _iter in 0..30 {
206 let prev_Y = Y.clone();
207 Y = 0.8 * P.dot(&Y) + 0.2 * &Y_static;
208
209 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 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 let W = self.build_initial_graph(&X);
267
268 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 let Y_final = self.propagate_labels(&W, &Y)?;
278
279 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)] 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 similarities.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("operation should succeed"));
338
339 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; }
345 }
346 }
347
348 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 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 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 for _iter in 0..30 {
381 let prev_Y = Y.clone();
382 Y = 0.8 * P.dot(&Y) + 0.2 * &Y_static;
383
384 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 #[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 for i in 0..n_new {
406 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 self.state
420 .current_graph
421 .mapv_inplace(|x| x * self.alpha_decay);
422
423 if self.adaptive_threshold {
425 self.update_adaptive_threshold();
426 }
427
428 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 self.incremental_graph_update(&X_new, &y_new)?;
437
438 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(¤t_data[i].view(), ¤t_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)] 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 let mut new_graph = Array2::zeros((n_current, n_current));
484
485 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 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(¤t_data[i].view(), ¤t_data[j].view());
502 similarities.push((j, sim));
503 }
504 }
505
506 similarities.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("operation should succeed"));
508
509 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; self.state.edge_ages.insert((i, j), 0);
523 self.state.edge_ages.insert((j, i), 0);
524 }
525 }
526 }
527
528 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 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 for i in 0..n_current {
543 new_graph[[i, i]] = 0.0;
544 }
545
546 self.state.current_graph = new_graph;
547
548 self.update_label_propagation(¤t_data, ¤t_labels)?;
550
551 Ok(())
552 }
553
554 #[allow(non_snake_case)] 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 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 self.state.current_graph = self.build_initial_graph(&X);
573
574 self.state.edge_ages.clear();
576
577 self.update_label_propagation(¤t_data, ¤t_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 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 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 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 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 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 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#[derive(Debug, Clone)]
695#[allow(non_snake_case)] pub struct StreamingGraphLearningTrained {
697 pub X_train: Array2<f64>,
699 pub y_train: Array1<i32>,
701 pub classes: Array1<i32>,
703 pub current_graph: Array2<f64>,
705 pub label_distributions: Array2<f64>,
707 pub data_window: VecDeque<Array1<f64>>,
709 pub label_window: VecDeque<i32>,
711 pub update_count: usize,
713 pub edge_ages: HashMap<(usize, usize), usize>,
715 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]; 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 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 let initial_graph_size = fitted.state.current_graph.dim();
769 assert_eq!(initial_graph_size, (4, 4));
770
771 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 assert_eq!(fitted.state.data_window.len(), 6);
780 assert_eq!(fitted.state.label_window.len(), 6);
781
782 let updated_graph_size = fitted.state.current_graph.dim();
784 assert_eq!(updated_graph_size, (6, 6));
785
786 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) .update_frequency(2);
802 let mut fitted = sgl
803 .fit(&X.view(), &y.view())
804 .expect("operation should succeed");
805
806 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 assert_eq!(fitted.state.data_window.len(), 3);
821 assert_eq!(fitted.state.label_window.len(), 3);
822
823 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 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 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 assert_eq!(fitted.state.update_count, 0);
874
875 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 assert_eq!(fitted.state.update_count, 3);
886
887 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); let mut fitted = sgl
901 .fit(&X.view(), &y.view())
902 .expect("operation should succeed");
903
904 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 assert!(
920 fitted.state.edge_ages.is_empty()
921 || fitted.state.edge_ages.values().all(|&age| age == 0)
922 );
923
924 let predictions = fitted
926 .predict(&X_new2.view())
927 .expect("operation should succeed");
928 assert_eq!(predictions.len(), 1);
929 }
930}