1use scirs2_core::ndarray_ext::{Array2, ArrayView1, ArrayView2};
8use scirs2_core::random::Random;
9use sklears_core::error::SklearsError;
10use std::collections::HashMap;
11
12#[derive(Clone)]
14pub struct MultiViewGraphLearning {
15 pub k_neighbors: usize,
17 pub view_weights: Vec<f64>,
19 pub combination_method: String,
21 pub regularization: f64,
23 pub max_iter: usize,
25 pub tolerance: f64,
27 pub random_state: Option<u64>,
29}
30
31impl MultiViewGraphLearning {
32 pub fn new() -> Self {
34 Self {
35 k_neighbors: 5,
36 view_weights: vec![],
37 combination_method: "weighted".to_string(),
38 regularization: 0.1,
39 max_iter: 100,
40 tolerance: 1e-6,
41 random_state: None,
42 }
43 }
44
45 pub fn k_neighbors(mut self, k: usize) -> Self {
47 self.k_neighbors = k;
48 self
49 }
50
51 pub fn view_weights(mut self, weights: Vec<f64>) -> Self {
53 self.view_weights = weights;
54 self
55 }
56
57 pub fn combination_method(mut self, method: String) -> Self {
59 self.combination_method = method;
60 self
61 }
62
63 pub fn regularization(mut self, reg: f64) -> Self {
65 self.regularization = reg;
66 self
67 }
68
69 pub fn max_iter(mut self, max_iter: usize) -> Self {
71 self.max_iter = max_iter;
72 self
73 }
74
75 pub fn tolerance(mut self, tol: f64) -> Self {
77 self.tolerance = tol;
78 self
79 }
80
81 pub fn random_state(mut self, seed: u64) -> Self {
83 self.random_state = Some(seed);
84 self
85 }
86
87 pub fn fit(&self, views: &[ArrayView2<f64>]) -> Result<Array2<f64>, SklearsError> {
89 if views.is_empty() {
90 return Err(SklearsError::InvalidInput("No views provided".to_string()));
91 }
92
93 let n_samples = views[0].nrows();
94
95 for view in views.iter() {
97 if view.nrows() != n_samples {
98 return Err(SklearsError::ShapeMismatch {
99 expected: format!("All views should have {} samples", n_samples),
100 actual: format!("View has {} samples", view.nrows()),
101 });
102 }
103 }
104
105 let view_graphs = self.construct_view_graphs(views)?;
107
108 let combined_graph = self.combine_graphs(&view_graphs)?;
110
111 Ok(combined_graph)
112 }
113
114 fn construct_view_graphs(
116 &self,
117 views: &[ArrayView2<f64>],
118 ) -> Result<Vec<Array2<f64>>, SklearsError> {
119 let mut graphs = Vec::new();
120
121 for view in views.iter() {
122 let graph = self.construct_knn_graph(view)?;
123 graphs.push(graph);
124 }
125
126 Ok(graphs)
127 }
128
129 #[allow(non_snake_case)] fn construct_knn_graph(&self, X: &ArrayView2<f64>) -> Result<Array2<f64>, SklearsError> {
132 let n_samples = X.nrows();
133 let mut graph = Array2::<f64>::zeros((n_samples, n_samples));
134
135 for i in 0..n_samples {
136 let mut distances: Vec<(f64, usize)> = Vec::new();
137
138 for j in 0..n_samples {
139 if i != j {
140 let dist = self.euclidean_distance(&X.row(i), &X.row(j));
141 distances.push((dist, j));
142 }
143 }
144
145 distances.sort_by(|a, b| a.0.partial_cmp(&b.0).expect("operation should succeed"));
147
148 for (dist, j) in distances.iter().take(self.k_neighbors.min(distances.len())) {
149 let weight = (-dist.powi(2) / 2.0).exp(); graph[[i, *j]] = weight;
151 }
152 }
153
154 for i in 0..n_samples {
156 for j in i + 1..n_samples {
157 let avg_weight = (graph[[i, j]] + graph[[j, i]]) / 2.0;
158 graph[[i, j]] = avg_weight;
159 graph[[j, i]] = avg_weight;
160 }
161 }
162
163 Ok(graph)
164 }
165
166 fn combine_graphs(&self, graphs: &[Array2<f64>]) -> Result<Array2<f64>, SklearsError> {
168 if graphs.is_empty() {
169 return Err(SklearsError::InvalidInput(
170 "No graphs to combine".to_string(),
171 ));
172 }
173
174 let n_samples = graphs[0].nrows();
175 let mut combined = Array2::<f64>::zeros((n_samples, n_samples));
176
177 match self.combination_method.as_str() {
178 "weighted" => {
179 let weights = if self.view_weights.is_empty() {
180 vec![1.0 / graphs.len() as f64; graphs.len()]
181 } else {
182 self.view_weights.clone()
183 };
184
185 if weights.len() != graphs.len() {
186 return Err(SklearsError::InvalidInput(
187 "Number of weights must match number of views".to_string(),
188 ));
189 }
190
191 for (i, graph) in graphs.iter().enumerate() {
192 combined += &(graph * weights[i]);
193 }
194 }
195 "union" => {
196 for graph in graphs.iter() {
197 for i in 0..n_samples {
198 for j in 0..n_samples {
199 combined[[i, j]] = combined[[i, j]].max(graph[[i, j]]);
200 }
201 }
202 }
203 }
204 "intersection" => {
205 combined = graphs[0].clone();
206 for graph in graphs.iter().skip(1) {
207 for i in 0..n_samples {
208 for j in 0..n_samples {
209 combined[[i, j]] = combined[[i, j]].min(graph[[i, j]]);
210 }
211 }
212 }
213 }
214 "adaptive" => {
215 combined = self.adaptive_combination(graphs)?;
216 }
217 _ => {
218 return Err(SklearsError::InvalidInput(format!(
219 "Unknown combination method: {}",
220 self.combination_method
221 )));
222 }
223 }
224
225 Ok(combined)
226 }
227
228 fn adaptive_combination(&self, graphs: &[Array2<f64>]) -> Result<Array2<f64>, SklearsError> {
230 let n_views = graphs.len();
231 let n_samples = graphs[0].nrows();
232
233 let mut weights = vec![1.0 / n_views as f64; n_views];
235
236 for _iter in 0..self.max_iter {
237 let old_weights = weights.clone();
238
239 let mut combined = Array2::<f64>::zeros((n_samples, n_samples));
241 for (i, graph) in graphs.iter().enumerate() {
242 combined += &(graph * weights[i]);
243 }
244
245 for i in 0..n_views {
247 let agreement = self.compute_graph_agreement(&graphs[i], &combined);
248 weights[i] = agreement;
249 }
250
251 let weight_sum: f64 = weights.iter().sum();
253 if weight_sum > 0.0 {
254 for w in weights.iter_mut() {
255 *w /= weight_sum;
256 }
257 }
258
259 let weight_change: f64 = weights
261 .iter()
262 .zip(old_weights.iter())
263 .map(|(w1, w2)| (w1 - w2).abs())
264 .sum();
265
266 if weight_change < self.tolerance {
267 break;
268 }
269 }
270
271 let mut combined = Array2::<f64>::zeros((n_samples, n_samples));
273 for (i, graph) in graphs.iter().enumerate() {
274 combined += &(graph * weights[i]);
275 }
276
277 Ok(combined)
278 }
279
280 fn compute_graph_agreement(&self, graph1: &Array2<f64>, graph2: &Array2<f64>) -> f64 {
282 let mut agreement = 0.0;
283 let mut total = 0.0;
284
285 for i in 0..graph1.nrows() {
286 for j in 0..graph1.ncols() {
287 let diff = (graph1[[i, j]] - graph2[[i, j]]).abs();
288 agreement += 1.0 / (1.0 + diff);
289 total += 1.0;
290 }
291 }
292
293 if total > 0.0 {
294 agreement / total
295 } else {
296 0.0
297 }
298 }
299
300 fn euclidean_distance(&self, x1: &ArrayView1<f64>, x2: &ArrayView1<f64>) -> f64 {
302 x1.iter()
303 .zip(x2.iter())
304 .map(|(a, b)| (a - b).powi(2))
305 .sum::<f64>()
306 .sqrt()
307 }
308}
309
310impl Default for MultiViewGraphLearning {
311 fn default() -> Self {
312 Self::new()
313 }
314}
315
316#[derive(Clone)]
318pub struct HeterogeneousGraphLearning {
319 pub node_types: Vec<String>,
321 pub edge_types: Vec<(String, String)>,
323 pub edge_weights: HashMap<(String, String), f64>,
325 pub embedding_dims: HashMap<String, usize>,
327 pub k_neighbors: HashMap<(String, String), usize>,
329 pub random_state: Option<u64>,
331}
332
333impl HeterogeneousGraphLearning {
334 pub fn new() -> Self {
336 Self {
337 node_types: vec![],
338 edge_types: vec![],
339 edge_weights: HashMap::new(),
340 embedding_dims: HashMap::new(),
341 k_neighbors: HashMap::new(),
342 random_state: None,
343 }
344 }
345
346 pub fn node_types(mut self, types: Vec<String>) -> Self {
348 self.node_types = types;
349 self
350 }
351
352 pub fn edge_types(mut self, types: Vec<(String, String)>) -> Self {
354 self.edge_types = types;
355 self
356 }
357
358 pub fn edge_weights(mut self, weights: HashMap<(String, String), f64>) -> Self {
360 self.edge_weights = weights;
361 self
362 }
363
364 pub fn embedding_dims(mut self, dims: HashMap<String, usize>) -> Self {
366 self.embedding_dims = dims;
367 self
368 }
369
370 pub fn random_state(mut self, seed: u64) -> Self {
372 self.random_state = Some(seed);
373 self
374 }
375
376 pub fn fit(
378 &self,
379 data: &HashMap<String, ArrayView2<f64>>,
380 ) -> Result<HashMap<String, Array2<f64>>, SklearsError> {
381 if data.is_empty() {
382 return Err(SklearsError::InvalidInput("No data provided".to_string()));
383 }
384
385 let mut embeddings = HashMap::new();
386 let mut rng = if let Some(_seed) = self.random_state {
387 Random::seed(42)
388 } else {
389 Random::seed(42) };
391
392 for (node_type, node_data) in data.iter() {
394 let embed_dim = self.embedding_dims.get(node_type).unwrap_or(&64);
395 let n_nodes = node_data.nrows();
396
397 let mut embedding = Array2::<f64>::zeros((n_nodes, *embed_dim));
399 for i in 0..n_nodes {
400 for j in 0..*embed_dim {
401 embedding[[i, j]] = rng.random_range(-1.0..1.0);
402 }
403 }
404
405 embeddings.insert(node_type.clone(), embedding);
406 }
407
408 for (node_type, node_data) in data.iter() {
411 let features = node_data.to_owned();
412 embeddings.insert(node_type.clone(), features);
413 }
414
415 Ok(embeddings)
416 }
417}
418
419impl Default for HeterogeneousGraphLearning {
420 fn default() -> Self {
421 Self::new()
422 }
423}
424
425#[derive(Clone)]
427pub struct TemporalGraphLearning {
428 pub window_size: usize,
430 pub temporal_decay: f64,
432 pub aggregation_method: String,
434 pub k_neighbors: usize,
436 pub random_state: Option<u64>,
438}
439
440impl TemporalGraphLearning {
441 pub fn new() -> Self {
443 Self {
444 window_size: 5,
445 temporal_decay: 0.9,
446 aggregation_method: "weighted".to_string(),
447 k_neighbors: 5,
448 random_state: None,
449 }
450 }
451
452 pub fn window_size(mut self, size: usize) -> Self {
454 self.window_size = size;
455 self
456 }
457
458 pub fn temporal_decay(mut self, decay: f64) -> Self {
460 self.temporal_decay = decay;
461 self
462 }
463
464 pub fn aggregation_method(mut self, method: String) -> Self {
466 self.aggregation_method = method;
467 self
468 }
469
470 pub fn k_neighbors(mut self, k: usize) -> Self {
472 self.k_neighbors = k;
473 self
474 }
475
476 pub fn random_state(mut self, seed: u64) -> Self {
478 self.random_state = Some(seed);
479 self
480 }
481
482 pub fn fit(&self, snapshots: &[ArrayView2<f64>]) -> Result<Array2<f64>, SklearsError> {
484 if snapshots.is_empty() {
485 return Err(SklearsError::InvalidInput(
486 "No snapshots provided".to_string(),
487 ));
488 }
489
490 let n_samples = snapshots[0].nrows();
491
492 for snapshot in snapshots.iter() {
494 if snapshot.nrows() != n_samples {
495 return Err(SklearsError::ShapeMismatch {
496 expected: format!("All snapshots should have {} samples", n_samples),
497 actual: format!("Snapshot has {} samples", snapshot.nrows()),
498 });
499 }
500 }
501
502 let graphs = self.construct_temporal_graphs(snapshots)?;
504
505 let aggregated_graph = self.aggregate_temporal_graphs(&graphs)?;
507
508 Ok(aggregated_graph)
509 }
510
511 fn construct_temporal_graphs(
513 &self,
514 snapshots: &[ArrayView2<f64>],
515 ) -> Result<Vec<Array2<f64>>, SklearsError> {
516 let mut graphs = Vec::new();
517
518 for snapshot in snapshots.iter() {
519 let graph = self.construct_knn_graph(snapshot)?;
520 graphs.push(graph);
521 }
522
523 Ok(graphs)
524 }
525
526 #[allow(non_snake_case)] fn construct_knn_graph(&self, X: &ArrayView2<f64>) -> Result<Array2<f64>, SklearsError> {
529 let n_samples = X.nrows();
530 let mut graph = Array2::<f64>::zeros((n_samples, n_samples));
531
532 for i in 0..n_samples {
533 let mut distances: Vec<(f64, usize)> = Vec::new();
534
535 for j in 0..n_samples {
536 if i != j {
537 let dist = self.euclidean_distance(&X.row(i), &X.row(j));
538 distances.push((dist, j));
539 }
540 }
541
542 distances.sort_by(|a, b| a.0.partial_cmp(&b.0).expect("operation should succeed"));
544
545 for (dist, j) in distances.iter().take(self.k_neighbors.min(distances.len())) {
546 let weight = (-dist.powi(2) / 2.0).exp(); graph[[i, *j]] = weight;
548 }
549 }
550
551 for i in 0..n_samples {
553 for j in i + 1..n_samples {
554 let avg_weight = (graph[[i, j]] + graph[[j, i]]) / 2.0;
555 graph[[i, j]] = avg_weight;
556 graph[[j, i]] = avg_weight;
557 }
558 }
559
560 Ok(graph)
561 }
562
563 fn aggregate_temporal_graphs(
565 &self,
566 graphs: &[Array2<f64>],
567 ) -> Result<Array2<f64>, SklearsError> {
568 if graphs.is_empty() {
569 return Err(SklearsError::InvalidInput(
570 "No graphs to aggregate".to_string(),
571 ));
572 }
573
574 let n_samples = graphs[0].nrows();
575 let mut aggregated = Array2::<f64>::zeros((n_samples, n_samples));
576
577 match self.aggregation_method.as_str() {
578 "mean" => {
579 for graph in graphs.iter() {
580 aggregated += graph;
581 }
582 aggregated /= graphs.len() as f64;
583 }
584 "weighted" => {
585 let total_weight: f64 = (0..graphs.len())
586 .map(|i| self.temporal_decay.powi(i as i32))
587 .sum();
588
589 for (i, graph) in graphs.iter().enumerate() {
590 let weight = self.temporal_decay.powi(i as i32) / total_weight;
591 aggregated += &(graph * weight);
592 }
593 }
594 "attention" => {
595 let weights = self.compute_attention_weights(graphs)?;
597 for (i, graph) in graphs.iter().enumerate() {
598 aggregated += &(graph * weights[i]);
599 }
600 }
601 _ => {
602 return Err(SklearsError::InvalidInput(format!(
603 "Unknown aggregation method: {}",
604 self.aggregation_method
605 )));
606 }
607 }
608
609 Ok(aggregated)
610 }
611
612 fn compute_attention_weights(&self, graphs: &[Array2<f64>]) -> Result<Vec<f64>, SklearsError> {
614 let n_graphs = graphs.len();
615 let mut weights = vec![1.0 / n_graphs as f64; n_graphs];
616
617 let mut densities = Vec::new();
619 for graph in graphs.iter() {
620 let density = graph.iter().filter(|&&x| x > 0.0).count() as f64 / (graph.len() as f64);
621 densities.push(density);
622 }
623
624 let total_density: f64 = densities.iter().sum();
625 if total_density > 0.0 {
626 for (i, density) in densities.iter().enumerate() {
627 weights[i] = density / total_density;
628 }
629 }
630
631 Ok(weights)
632 }
633
634 fn euclidean_distance(&self, x1: &ArrayView1<f64>, x2: &ArrayView1<f64>) -> f64 {
636 x1.iter()
637 .zip(x2.iter())
638 .map(|(a, b)| (a - b).powi(2))
639 .sum::<f64>()
640 .sqrt()
641 }
642}
643
644impl Default for TemporalGraphLearning {
645 fn default() -> Self {
646 Self::new()
647 }
648}
649
650#[allow(non_snake_case)]
651#[cfg(test)]
652mod tests {
653 use super::*;
654 use approx::assert_abs_diff_eq;
655 use scirs2_core::array;
656
657 #[test]
658 fn test_multi_view_graph_learning() {
659 let view1 = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0]];
660 let view2 = array![[2.0, 1.0], [3.0, 2.0], [4.0, 3.0]];
661 let views = vec![view1.view(), view2.view()];
662
663 let mvgl = MultiViewGraphLearning::new()
664 .k_neighbors(2)
665 .combination_method("weighted".to_string());
666
667 let result = mvgl.fit(&views);
668 assert!(result.is_ok());
669
670 let graph = result.expect("operation should succeed");
671 assert_eq!(graph.dim(), (3, 3));
672
673 assert_eq!(graph[[0, 0]], 0.0);
675 assert_eq!(graph[[1, 1]], 0.0);
676 assert_eq!(graph[[2, 2]], 0.0);
677
678 assert_abs_diff_eq!(graph[[0, 1]], graph[[1, 0]], epsilon = 1e-10);
680 assert_abs_diff_eq!(graph[[0, 2]], graph[[2, 0]], epsilon = 1e-10);
681 assert_abs_diff_eq!(graph[[1, 2]], graph[[2, 1]], epsilon = 1e-10);
682 }
683
684 #[test]
685 fn test_multi_view_graph_union() {
686 let view1 = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0]];
687 let view2 = array![[2.0, 1.0], [3.0, 2.0], [4.0, 3.0]];
688 let views = vec![view1.view(), view2.view()];
689
690 let mvgl = MultiViewGraphLearning::new()
691 .k_neighbors(2)
692 .combination_method("union".to_string());
693
694 let result = mvgl.fit(&views);
695 assert!(result.is_ok());
696
697 let graph = result.expect("operation should succeed");
698 assert_eq!(graph.dim(), (3, 3));
699 }
700
701 #[test]
702 fn test_multi_view_graph_adaptive() {
703 let view1 = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0]];
704 let view2 = array![[2.0, 1.0], [3.0, 2.0], [4.0, 3.0]];
705 let views = vec![view1.view(), view2.view()];
706
707 let mvgl = MultiViewGraphLearning::new()
708 .k_neighbors(2)
709 .combination_method("adaptive".to_string())
710 .max_iter(10)
711 .tolerance(1e-4);
712
713 let result = mvgl.fit(&views);
714 assert!(result.is_ok());
715
716 let graph = result.expect("operation should succeed");
717 assert_eq!(graph.dim(), (3, 3));
718 }
719
720 #[test]
721 fn test_heterogeneous_graph_learning() {
722 let type1_data = array![[1.0, 2.0], [2.0, 3.0]];
723 let type2_data = array![[3.0, 4.0], [4.0, 5.0]];
724 let mut data = HashMap::new();
725 data.insert("type1".to_string(), type1_data.view());
726 data.insert("type2".to_string(), type2_data.view());
727
728 let hgl = HeterogeneousGraphLearning::new()
729 .node_types(vec!["type1".to_string(), "type2".to_string()]);
730
731 let result = hgl.fit(&data);
732 assert!(result.is_ok());
733
734 let embeddings = result.expect("operation should succeed");
735 assert!(embeddings.contains_key("type1"));
736 assert!(embeddings.contains_key("type2"));
737 assert_eq!(embeddings["type1"].dim(), (2, 2));
738 assert_eq!(embeddings["type2"].dim(), (2, 2));
739 }
740
741 #[test]
742 fn test_temporal_graph_learning() {
743 let snapshot1 = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0]];
744 let snapshot2 = array![[1.1, 2.1], [2.1, 3.1], [3.1, 4.1]];
745 let snapshot3 = array![[1.2, 2.2], [2.2, 3.2], [3.2, 4.2]];
746 let snapshots = vec![snapshot1.view(), snapshot2.view(), snapshot3.view()];
747
748 let tgl = TemporalGraphLearning::new()
749 .window_size(3)
750 .temporal_decay(0.9)
751 .aggregation_method("weighted".to_string())
752 .k_neighbors(2);
753
754 let result = tgl.fit(&snapshots);
755 assert!(result.is_ok());
756
757 let graph = result.expect("operation should succeed");
758 assert_eq!(graph.dim(), (3, 3));
759
760 assert_eq!(graph[[0, 0]], 0.0);
762 assert_eq!(graph[[1, 1]], 0.0);
763 assert_eq!(graph[[2, 2]], 0.0);
764 }
765
766 #[test]
767 fn test_temporal_graph_attention() {
768 let snapshot1 = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0]];
769 let snapshot2 = array![[1.1, 2.1], [2.1, 3.1], [3.1, 4.1]];
770 let snapshots = vec![snapshot1.view(), snapshot2.view()];
771
772 let tgl = TemporalGraphLearning::new()
773 .aggregation_method("attention".to_string())
774 .k_neighbors(2);
775
776 let result = tgl.fit(&snapshots);
777 assert!(result.is_ok());
778
779 let graph = result.expect("operation should succeed");
780 assert_eq!(graph.dim(), (3, 3));
781 }
782
783 #[test]
784 fn test_multi_view_graph_error_cases() {
785 let mvgl = MultiViewGraphLearning::new();
786
787 let result = mvgl.fit(&[]);
789 assert!(result.is_err());
790
791 let view1 = array![[1.0, 2.0], [2.0, 3.0]];
793 let view2 = array![[3.0, 4.0], [4.0, 5.0], [5.0, 6.0]];
794 let views = vec![view1.view(), view2.view()];
795
796 let result = mvgl.fit(&views);
797 assert!(result.is_err());
798 }
799
800 #[test]
801 fn test_temporal_graph_error_cases() {
802 let tgl = TemporalGraphLearning::new();
803
804 let result = tgl.fit(&[]);
806 assert!(result.is_err());
807
808 let snapshot1 = array![[1.0, 2.0], [2.0, 3.0]];
810 let snapshot2 = array![[3.0, 4.0], [4.0, 5.0], [5.0, 6.0]];
811 let snapshots = vec![snapshot1.view(), snapshot2.view()];
812
813 let result = tgl.fit(&snapshots);
814 assert!(result.is_err());
815 }
816}