1use scirs2_core::ndarray_ext::{s, Array1, Array2, ArrayView1, ArrayView2};
8use sklears_core::error::SklearsError;
9use std::collections::{HashMap, VecDeque};
10
11#[derive(Clone)]
13pub struct DynamicGraphLearning {
14 pub learning_rate: f64,
16 pub forgetting_factor: f64,
18 pub k_neighbors: usize,
20 pub buffer_size: usize,
22 pub edge_threshold: f64,
24 pub max_nodes: Option<usize>,
26 pub random_state: Option<u64>,
28 adjacency_matrix: Option<Array2<f64>>,
30 node_features: Option<Array2<f64>>,
32 update_buffer: VecDeque<GraphUpdate>,
34}
35
36#[derive(Clone, Debug)]
38pub struct GraphUpdate {
39 pub update_type: String,
41 pub node_indices: Vec<usize>,
43 pub features: Option<Array1<f64>>,
45 pub edge_weight: Option<f64>,
47 pub timestamp: f64,
49}
50
51impl DynamicGraphLearning {
52 pub fn new() -> Self {
54 Self {
55 learning_rate: 0.01,
56 forgetting_factor: 0.95,
57 k_neighbors: 5,
58 buffer_size: 1000,
59 edge_threshold: 0.1,
60 max_nodes: None,
61 random_state: None,
62 adjacency_matrix: None,
63 node_features: None,
64 update_buffer: VecDeque::new(),
65 }
66 }
67
68 pub fn learning_rate(mut self, lr: f64) -> Self {
70 self.learning_rate = lr;
71 self
72 }
73
74 pub fn forgetting_factor(mut self, factor: f64) -> Self {
76 self.forgetting_factor = factor;
77 self
78 }
79
80 pub fn k_neighbors(mut self, k: usize) -> Self {
82 self.k_neighbors = k;
83 self
84 }
85
86 pub fn buffer_size(mut self, size: usize) -> Self {
88 self.buffer_size = size;
89 self
90 }
91
92 pub fn edge_threshold(mut self, threshold: f64) -> Self {
94 self.edge_threshold = threshold;
95 self
96 }
97
98 pub fn max_nodes(mut self, max_nodes: usize) -> Self {
100 self.max_nodes = Some(max_nodes);
101 self
102 }
103
104 pub fn random_state(mut self, seed: u64) -> Self {
106 self.random_state = Some(seed);
107 self
108 }
109
110 pub fn initialize(&mut self, initial_features: ArrayView2<f64>) -> Result<(), SklearsError> {
112 let n_samples = initial_features.nrows();
113
114 if n_samples == 0 {
115 return Err(SklearsError::InvalidInput(
116 "No initial data provided".to_string(),
117 ));
118 }
119
120 self.node_features = Some(initial_features.to_owned());
122
123 let mut adjacency = Array2::zeros((n_samples, n_samples));
125
126 for i in 0..n_samples {
127 let mut distances: Vec<(usize, f64)> = Vec::new();
128
129 for j in 0..n_samples {
130 if i != j {
131 let dist =
132 self.compute_distance(initial_features.row(i), initial_features.row(j));
133 distances.push((j, dist));
134 }
135 }
136
137 distances.sort_by(|a, b| a.1.partial_cmp(&b.1).expect("operation should succeed"));
139 for &(neighbor, dist) in distances.iter().take(self.k_neighbors) {
140 let weight = (-dist).exp(); adjacency[[i, neighbor]] = weight;
142 adjacency[[neighbor, i]] = weight; }
144 }
145
146 self.adjacency_matrix = Some(adjacency);
147 Ok(())
148 }
149
150 pub fn add_nodes(&mut self, new_features: ArrayView2<f64>) -> Result<(), SklearsError> {
152 if self.node_features.is_none() || self.adjacency_matrix.is_none() {
153 return Err(SklearsError::InvalidInput(
154 "Graph not initialized".to_string(),
155 ));
156 }
157
158 let new_n_nodes = new_features.nrows();
159
160 if let Some(max_nodes) = self.max_nodes {
162 let current_n_nodes = self
163 .node_features
164 .as_ref()
165 .expect("operation should succeed")
166 .nrows();
167 let total_nodes = current_n_nodes + new_n_nodes;
168 if total_nodes > max_nodes {
169 self.prune_old_nodes(max_nodes - new_n_nodes)?;
170 }
171 }
172
173 let current_features = self
175 .node_features
176 .as_ref()
177 .expect("operation should succeed");
178 let current_adjacency = self
179 .adjacency_matrix
180 .as_ref()
181 .expect("operation should succeed");
182
183 let old_n_nodes = current_features.nrows();
184 let total_nodes = old_n_nodes + new_n_nodes;
185
186 let mut extended_features = Array2::zeros((total_nodes, current_features.ncols()));
188 extended_features
189 .slice_mut(s![..old_n_nodes, ..])
190 .assign(current_features);
191 extended_features
192 .slice_mut(s![old_n_nodes.., ..])
193 .assign(&new_features);
194
195 let mut extended_adjacency = Array2::zeros((total_nodes, total_nodes));
197 extended_adjacency
198 .slice_mut(s![..old_n_nodes, ..old_n_nodes])
199 .assign(current_adjacency);
200
201 for i in old_n_nodes..total_nodes {
203 let mut distances: Vec<(usize, f64)> = Vec::new();
204
205 for j in 0..old_n_nodes {
206 let dist =
207 self.compute_distance(extended_features.row(i), extended_features.row(j));
208 distances.push((j, dist));
209 }
210
211 distances.sort_by(|a, b| a.1.partial_cmp(&b.1).expect("operation should succeed"));
213 for &(neighbor, dist) in distances.iter().take(self.k_neighbors) {
214 let weight = (-dist).exp();
215 extended_adjacency[[i, neighbor]] = weight;
216 extended_adjacency[[neighbor, i]] = weight;
217 }
218
219 for j in (old_n_nodes..total_nodes).filter(|&j| j != i) {
221 let dist =
222 self.compute_distance(extended_features.row(i), extended_features.row(j));
223 let weight = (-dist).exp();
224 if weight > self.edge_threshold {
225 extended_adjacency[[i, j]] = weight;
226 extended_adjacency[[j, i]] = weight;
227 }
228 }
229 }
230
231 self.node_features = Some(extended_features);
232 self.adjacency_matrix = Some(extended_adjacency);
233
234 for i in old_n_nodes..total_nodes {
236 self.record_update(GraphUpdate {
237 update_type: "add_node".to_string(),
238 node_indices: vec![i],
239 features: Some(new_features.row(i - old_n_nodes).to_owned()),
240 edge_weight: None,
241 timestamp: self.get_current_time(),
242 });
243 }
244
245 Ok(())
246 }
247
248 pub fn update_node_features(
250 &mut self,
251 node_idx: usize,
252 new_features: ArrayView1<f64>,
253 ) -> Result<(), SklearsError> {
254 if self.node_features.is_none() {
255 return Err(SklearsError::InvalidInput(
256 "Graph not initialized".to_string(),
257 ));
258 }
259
260 let features = self
261 .node_features
262 .as_mut()
263 .expect("operation should succeed");
264
265 if node_idx >= features.nrows() {
266 return Err(SklearsError::InvalidInput(
267 "Node index out of bounds".to_string(),
268 ));
269 }
270
271 let mut current_features = features.row_mut(node_idx);
273 for (i, &new_val) in new_features.iter().enumerate() {
274 current_features[i] =
275 (1.0 - self.learning_rate) * current_features[i] + self.learning_rate * new_val;
276 }
277
278 self.update_edges_for_node(node_idx)?;
280
281 self.record_update(GraphUpdate {
283 update_type: "update_features".to_string(),
284 node_indices: vec![node_idx],
285 features: Some(new_features.to_owned()),
286 edge_weight: None,
287 timestamp: self.get_current_time(),
288 });
289
290 Ok(())
291 }
292
293 fn update_edges_for_node(&mut self, node_idx: usize) -> Result<(), SklearsError> {
295 if self.node_features.is_none() || self.adjacency_matrix.is_none() {
296 return Ok(());
297 }
298
299 let features = self
301 .node_features
302 .as_ref()
303 .expect("operation should succeed")
304 .clone();
305 let n_nodes = features.nrows();
306 let forgetting_factor = self.forgetting_factor;
307 let edge_threshold = self.edge_threshold;
308
309 let adjacency = self
311 .adjacency_matrix
312 .as_mut()
313 .expect("operation should succeed");
314
315 for other_idx in 0..n_nodes {
317 if node_idx != other_idx {
318 let dist =
319 Self::compute_distance_static(features.row(node_idx), features.row(other_idx));
320 let new_weight = (-dist).exp();
321
322 let current_weight = adjacency[[node_idx, other_idx]];
324 let updated_weight =
325 forgetting_factor * current_weight + (1.0 - forgetting_factor) * new_weight;
326
327 let final_weight = if updated_weight > edge_threshold {
329 updated_weight
330 } else {
331 0.0
332 };
333
334 adjacency[[node_idx, other_idx]] = final_weight;
335 adjacency[[other_idx, node_idx]] = final_weight; }
337 }
338
339 Ok(())
340 }
341
342 fn prune_old_nodes(&mut self, target_nodes: usize) -> Result<(), SklearsError> {
344 if self.node_features.is_none() || self.adjacency_matrix.is_none() {
345 return Ok(());
346 }
347
348 let current_nodes = self
349 .node_features
350 .as_ref()
351 .expect("operation should succeed")
352 .nrows();
353 if current_nodes <= target_nodes {
354 return Ok(());
355 }
356
357 let nodes_to_remove = current_nodes - target_nodes;
358
359 let features = self
364 .node_features
365 .as_ref()
366 .expect("operation should succeed");
367 let adjacency = self
368 .adjacency_matrix
369 .as_ref()
370 .expect("operation should succeed");
371
372 let new_features = features.slice(s![nodes_to_remove.., ..]).to_owned();
374 let new_adjacency = adjacency
375 .slice(s![nodes_to_remove.., nodes_to_remove..])
376 .to_owned();
377
378 self.node_features = Some(new_features);
379 self.adjacency_matrix = Some(new_adjacency);
380
381 Ok(())
382 }
383
384 pub fn get_adjacency_matrix(&self) -> Option<&Array2<f64>> {
386 self.adjacency_matrix.as_ref()
387 }
388
389 pub fn get_node_features(&self) -> Option<&Array2<f64>> {
391 self.node_features.as_ref()
392 }
393
394 pub fn get_recent_updates(&self, n_updates: usize) -> Vec<&GraphUpdate> {
396 self.update_buffer.iter().rev().take(n_updates).collect()
397 }
398
399 fn compute_distance(&self, feat1: ArrayView1<f64>, feat2: ArrayView1<f64>) -> f64 {
401 Self::compute_distance_static(feat1, feat2)
402 }
403
404 fn compute_distance_static(feat1: ArrayView1<f64>, feat2: ArrayView1<f64>) -> f64 {
406 feat1
407 .iter()
408 .zip(feat2.iter())
409 .map(|(&a, &b)| (a - b).powi(2))
410 .sum::<f64>()
411 .sqrt()
412 }
413
414 fn record_update(&mut self, update: GraphUpdate) {
416 self.update_buffer.push_back(update);
417
418 while self.update_buffer.len() > self.buffer_size {
420 self.update_buffer.pop_front();
421 }
422 }
423
424 fn get_current_time(&self) -> f64 {
426 std::time::SystemTime::now()
427 .duration_since(std::time::UNIX_EPOCH)
428 .unwrap_or_default()
429 .as_secs_f64()
430 }
431
432 pub fn apply_temporal_decay(&mut self) -> Result<(), SklearsError> {
434 if let Some(adjacency) = self.adjacency_matrix.as_mut() {
435 *adjacency *= self.forgetting_factor;
436
437 adjacency.mapv_inplace(|x| if x < self.edge_threshold { 0.0 } else { x });
439 }
440 Ok(())
441 }
442
443 pub fn get_statistics(&self) -> HashMap<String, f64> {
445 let mut stats = HashMap::new();
446
447 if let Some(adjacency) = &self.adjacency_matrix {
448 let n_nodes = adjacency.nrows() as f64;
449 let total_edges = adjacency.iter().filter(|&&x| x > 0.0).count() as f64 / 2.0; let density = if n_nodes > 1.0 {
451 total_edges / (n_nodes * (n_nodes - 1.0) / 2.0)
452 } else {
453 0.0
454 };
455
456 stats.insert("n_nodes".to_string(), n_nodes);
457 stats.insert("n_edges".to_string(), total_edges);
458 stats.insert("density".to_string(), density);
459 stats.insert("avg_degree".to_string(), total_edges * 2.0 / n_nodes);
460 }
461
462 stats.insert("buffer_size".to_string(), self.update_buffer.len() as f64);
463 stats
464 }
465}
466
467impl Default for DynamicGraphLearning {
468 fn default() -> Self {
469 Self::new()
470 }
471}
472
473#[allow(non_snake_case)]
474#[cfg(test)]
475mod tests {
476 use super::*;
477 use scirs2_core::array;
478
479 #[test]
480 fn test_dynamic_graph_initialization() {
481 let mut dgl = DynamicGraphLearning::new().k_neighbors(2);
482
483 let initial_data = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0]];
484
485 let result = dgl.initialize(initial_data.view());
486 assert!(result.is_ok());
487
488 let adjacency = dgl
489 .get_adjacency_matrix()
490 .expect("operation should succeed");
491 assert_eq!(adjacency.dim(), (3, 3));
492
493 for i in 0..3 {
495 assert_eq!(adjacency[[i, i]], 0.0);
496 }
497 }
498
499 #[test]
500 fn test_add_nodes() {
501 let mut dgl = DynamicGraphLearning::new().k_neighbors(2);
502
503 let initial_data = array![[1.0, 2.0], [2.0, 3.0]];
504
505 dgl.initialize(initial_data.view())
506 .expect("operation should succeed");
507
508 let new_data = array![[3.0, 4.0], [4.0, 5.0]];
509
510 let result = dgl.add_nodes(new_data.view());
511 assert!(result.is_ok());
512
513 let adjacency = dgl
514 .get_adjacency_matrix()
515 .expect("operation should succeed");
516 assert_eq!(adjacency.dim(), (4, 4));
517
518 let features = dgl.get_node_features().expect("operation should succeed");
519 assert_eq!(features.dim(), (4, 2));
520 }
521
522 #[test]
523 fn test_update_node_features() {
524 let mut dgl = DynamicGraphLearning::new()
525 .k_neighbors(2)
526 .learning_rate(0.5);
527
528 let initial_data = array![[1.0, 2.0], [2.0, 3.0]];
529
530 dgl.initialize(initial_data.view())
531 .expect("operation should succeed");
532
533 let new_features = array![5.0, 6.0];
534 let result = dgl.update_node_features(0, new_features.view());
535 assert!(result.is_ok());
536
537 let features = dgl.get_node_features().expect("operation should succeed");
538 assert!(features[[0, 0]] > 1.0);
540 assert!(features[[0, 1]] > 2.0);
541 }
542
543 #[test]
544 fn test_temporal_decay() {
545 let mut dgl = DynamicGraphLearning::new()
546 .k_neighbors(2)
547 .forgetting_factor(0.5)
548 .edge_threshold(0.1);
549
550 let initial_data = array![[1.0, 2.0], [2.0, 3.0]];
551
552 dgl.initialize(initial_data.view())
553 .expect("operation should succeed");
554
555 let original_adjacency = dgl
556 .get_adjacency_matrix()
557 .expect("operation should succeed")
558 .clone();
559
560 dgl.apply_temporal_decay()
561 .expect("operation should succeed");
562
563 let decayed_adjacency = dgl
564 .get_adjacency_matrix()
565 .expect("operation should succeed");
566
567 for i in 0..2 {
569 for j in 0..2 {
570 if i != j && original_adjacency[[i, j]] > 0.0 {
571 assert!(decayed_adjacency[[i, j]] < original_adjacency[[i, j]]);
572 }
573 }
574 }
575 }
576
577 #[test]
578 fn test_max_nodes_constraint() {
579 let mut dgl = DynamicGraphLearning::new().k_neighbors(2).max_nodes(3);
580
581 let initial_data = array![[1.0, 2.0], [2.0, 3.0]];
582
583 dgl.initialize(initial_data.view())
584 .expect("operation should succeed");
585
586 let new_data = array![[3.0, 4.0], [4.0, 5.0], [5.0, 6.0]];
587
588 let result = dgl.add_nodes(new_data.view());
589 assert!(result.is_ok());
590
591 let adjacency = dgl
592 .get_adjacency_matrix()
593 .expect("operation should succeed");
594 assert_eq!(adjacency.nrows(), 3); }
596
597 #[test]
598 fn test_graph_statistics() {
599 let mut dgl = DynamicGraphLearning::new().k_neighbors(2);
600
601 let initial_data = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0]];
602
603 dgl.initialize(initial_data.view())
604 .expect("operation should succeed");
605
606 let stats = dgl.get_statistics();
607
608 assert!(stats.contains_key("n_nodes"));
609 assert!(stats.contains_key("n_edges"));
610 assert!(stats.contains_key("density"));
611 assert!(stats.contains_key("avg_degree"));
612
613 assert_eq!(stats["n_nodes"], 3.0);
614 assert!(stats["n_edges"] > 0.0);
615 }
616
617 #[test]
618 fn test_update_buffer() {
619 let mut dgl = DynamicGraphLearning::new().buffer_size(2);
620
621 let initial_data = array![[1.0, 2.0], [2.0, 3.0]];
622
623 dgl.initialize(initial_data.view())
624 .expect("operation should succeed");
625
626 let new_features = array![5.0, 6.0];
627 dgl.update_node_features(0, new_features.view())
628 .expect("operation should succeed");
629 dgl.update_node_features(1, new_features.view())
630 .expect("operation should succeed");
631 dgl.update_node_features(0, new_features.view())
632 .expect("operation should succeed");
633
634 let recent_updates = dgl.get_recent_updates(5);
635 assert!(recent_updates.len() <= 2); }
637
638 #[test]
639 fn test_error_cases() {
640 let mut dgl = DynamicGraphLearning::new();
641
642 let new_data = array![[1.0, 2.0]];
644 assert!(dgl.add_nodes(new_data.view()).is_err());
645
646 let new_features = array![5.0, 6.0];
647 assert!(dgl.update_node_features(0, new_features.view()).is_err());
648
649 let empty_data = Array2::<f64>::zeros((0, 2));
651 assert!(dgl.initialize(empty_data.view()).is_err());
652
653 let initial_data = array![[1.0, 2.0]];
655 dgl.initialize(initial_data.view())
656 .expect("operation should succeed");
657 assert!(dgl.update_node_features(10, new_features.view()).is_err());
658 }
659}