Skip to main content

torsh_graph/
temporal.rs

1//! Temporal Graph Neural Networks
2//!
3//! Advanced implementation of temporal graph neural networks for continuous-time dynamic graphs.
4//! Handles evolving graph structures and node/edge features over time with sophisticated
5//! temporal modeling capabilities.
6//!
7//! # Features:
8//! - Continuous-time temporal graphs with event-based modeling
9//! - Temporal graph convolution layers (TGN, DyRep, TGAT)
10//! - Memory-augmented temporal networks
11//! - Time-aware graph attention mechanisms
12//! - Temporal pooling and aggregation operations
13//! - Causal temporal modeling with proper time ordering
14// Framework infrastructure - components designed for future use
15#![allow(dead_code)]
16/// Crate-local result alias: the error type defaults to [`TorshError`],
17/// so both `Result<T>` and `Result<T, OtherError>` stay valid.
18type Result<T, E = torsh_core::error::TorshError> = std::result::Result<T, E>;
19
20use crate::parameter::Parameter;
21use crate::{GraphData, GraphLayer};
22use std::collections::{BTreeMap, HashMap};
23use torsh_tensor::{
24    creation::{from_vec, randn, zeros},
25    Tensor,
26};
27
28/// Temporal event representing a graph change at a specific time
29#[derive(Debug, Clone)]
30pub struct TemporalEvent {
31    /// Time of the event (continuous time)
32    pub time: f64,
33    /// Type of event (node addition, edge addition, feature update, etc.)
34    pub event_type: EventType,
35    /// Source node ID (for edge events)
36    pub source: Option<usize>,
37    /// Target node ID (for edge events)
38    pub target: Option<usize>,
39    /// Node ID (for node events)
40    pub node: Option<usize>,
41    /// Feature vector associated with the event
42    pub features: Option<Tensor>,
43    /// Edge weight (for edge events)
44    pub weight: Option<f32>,
45}
46
47/// Types of temporal events
48#[derive(Debug, Clone, PartialEq)]
49pub enum EventType {
50    NodeAddition,
51    NodeDeletion,
52    NodeFeatureUpdate,
53    EdgeAddition,
54    EdgeDeletion,
55    EdgeFeatureUpdate,
56    GraphSnapshot,
57}
58
59/// Temporal graph data structure for continuous-time dynamic graphs
60#[derive(Debug, Clone)]
61pub struct TemporalGraphData {
62    /// Static graph structure at current time
63    pub current_graph: GraphData,
64    /// Sequence of temporal events ordered by time
65    pub events: BTreeMap<u64, Vec<TemporalEvent>>, // Using u64 timestamp for ordering
66    /// Time-indexed node features
67    pub node_features_history: HashMap<usize, BTreeMap<u64, Tensor>>,
68    /// Time-indexed edge features
69    pub edge_features_history: HashMap<(usize, usize), BTreeMap<u64, Tensor>>,
70    /// Current timestamp
71    pub current_time: f64,
72    /// Time window for temporal aggregation
73    pub time_window: f64,
74    /// Maximum number of events to keep in memory
75    pub max_events: usize,
76}
77
78impl TemporalGraphData {
79    /// Create a new temporal graph
80    pub fn new(initial_graph: GraphData, time_window: f64, max_events: usize) -> Self {
81        Self {
82            current_graph: initial_graph,
83            events: BTreeMap::new(),
84            node_features_history: HashMap::new(),
85            edge_features_history: HashMap::new(),
86            current_time: 0.0,
87            time_window,
88            max_events,
89        }
90    }
91
92    /// Add a temporal event to the graph
93    ///
94    /// # Errors
95    /// Returns an error when the event's feature tensor cannot be written into
96    /// the current graph.
97    pub fn add_event(&mut self, event: TemporalEvent) -> Result<()> {
98        let timestamp = (event.time * 1000.0) as u64; // Convert to milliseconds for ordering
99        self.events
100            .entry(timestamp)
101            .or_insert_with(Vec::new)
102            .push(event.clone());
103
104        // Update current time
105        self.current_time = self.current_time.max(event.time);
106
107        // Apply event to current graph structure
108        self.apply_event(&event)?;
109
110        // Clean up old events outside time window
111        self.cleanup_old_events();
112
113        Ok(())
114    }
115
116    /// Apply an event to the current graph structure
117    fn apply_event(&mut self, event: &TemporalEvent) -> Result<()> {
118        match event.event_type {
119            EventType::NodeFeatureUpdate => {
120                if let (Some(node), Some(ref features)) = (event.node, &event.features) {
121                    // Update node features in current graph
122                    self.update_node_features(node, features.clone())?;
123
124                    // Store in history
125                    let timestamp = (event.time * 1000.0) as u64;
126                    self.node_features_history
127                        .entry(node)
128                        .or_insert_with(BTreeMap::new)
129                        .insert(timestamp, features.clone());
130                }
131            }
132            EventType::EdgeFeatureUpdate => {
133                if let (Some(source), Some(target), Some(ref features)) =
134                    (event.source, event.target, &event.features)
135                {
136                    let timestamp = (event.time * 1000.0) as u64;
137                    self.edge_features_history
138                        .entry((source, target))
139                        .or_insert_with(BTreeMap::new)
140                        .insert(timestamp, features.clone());
141                }
142            }
143            _ => {
144                // For simplicity, other event types are not fully implemented
145                // In a complete implementation, these would modify the graph structure
146            }
147        }
148
149        Ok(())
150    }
151
152    /// Update node features in the current graph
153    fn update_node_features(&mut self, node_id: usize, features: Tensor) -> Result<()> {
154        // Simplified implementation - would need proper tensor slicing in practice
155        let current_features = self.current_graph.x.to_vec()?;
156        let feature_dim = self.current_graph.x.shape().dims()[1];
157        let new_features = features.to_vec()?;
158
159        let mut updated_features = current_features;
160        let start_idx = node_id * feature_dim;
161        let _end_idx = start_idx + feature_dim.min(new_features.len());
162
163        for (i, &value) in new_features.iter().take(feature_dim).enumerate() {
164            if start_idx + i < updated_features.len() {
165                updated_features[start_idx + i] = value;
166            }
167        }
168
169        self.current_graph.x = from_vec(
170            updated_features,
171            &[self.current_graph.num_nodes, feature_dim],
172            torsh_core::device::DeviceType::Cpu,
173        )?;
174
175        Ok(())
176    }
177
178    /// Clean up old events outside the time window
179    fn cleanup_old_events(&mut self) {
180        let cutoff_time = ((self.current_time - self.time_window) * 1000.0) as u64;
181
182        // Remove events older than time window
183        let old_keys: Vec<u64> = self
184            .events
185            .keys()
186            .filter(|&&timestamp| timestamp < cutoff_time)
187            .cloned()
188            .collect();
189
190        for key in old_keys {
191            self.events.remove(&key);
192        }
193
194        // Also limit total number of events
195        while self.events.len() > self.max_events {
196            if let Some(first_key) = self.events.keys().next().cloned() {
197                self.events.remove(&first_key);
198            } else {
199                break;
200            }
201        }
202    }
203
204    /// Get events within a specific time range
205    pub fn get_events_in_range(&self, start_time: f64, end_time: f64) -> Vec<&TemporalEvent> {
206        let start_timestamp = (start_time * 1000.0) as u64;
207        let end_timestamp = (end_time * 1000.0) as u64;
208
209        self.events
210            .range(start_timestamp..=end_timestamp)
211            .flat_map(|(_, events)| events.iter())
212            .collect()
213    }
214
215    /// Get node features at a specific time (with interpolation)
216    pub fn get_node_features_at_time(&self, node_id: usize, time: f64) -> Option<Tensor> {
217        let timestamp = (time * 1000.0) as u64;
218
219        if let Some(history) = self.node_features_history.get(&node_id) {
220            // Find the most recent features before or at the requested time
221            if let Some((_, features)) = history.range(..=timestamp).next_back() {
222                return Some(features.clone());
223            }
224        }
225
226        None
227    }
228
229    /// Create a snapshot of the graph at a specific time
230    pub fn snapshot_at_time(&self, _time: f64) -> GraphData {
231        // Simplified implementation - returns current graph
232        // In practice, this would reconstruct the graph state at the specified time
233        self.current_graph.clone()
234    }
235}
236
237/// Temporal Graph Convolutional Network (TGCN) layer
238#[derive(Debug)]
239pub struct TGCNConv {
240    in_features: usize,
241    out_features: usize,
242    temporal_dim: usize,
243    spatial_weight: Parameter,
244    temporal_weight: Parameter,
245    bias: Option<Parameter>,
246    memory_size: usize,
247    time_encoding_dim: usize,
248}
249
250impl TGCNConv {
251    /// Create a new TGCN layer
252    pub fn new(
253        in_features: usize,
254        out_features: usize,
255        temporal_dim: usize,
256        memory_size: usize,
257        bias: bool,
258    ) -> Result<Self> {
259        let spatial_weight = Parameter::new(randn(&[in_features, out_features])?);
260        let temporal_weight = Parameter::new(randn(&[temporal_dim, out_features])?);
261        let bias = if bias {
262            Some(Parameter::new(zeros(&[out_features])?))
263        } else {
264            None
265        };
266
267        Ok(Self {
268            in_features,
269            out_features,
270            temporal_dim,
271            spatial_weight,
272            temporal_weight,
273            bias,
274            memory_size,
275            time_encoding_dim: temporal_dim,
276        })
277    }
278
279    /// Forward pass through TGCN layer
280    pub fn forward(&self, temporal_graph: &TemporalGraphData) -> Result<TemporalGraphData> {
281        // Step 1: Spatial convolution on current graph
282        let spatial_features = temporal_graph
283            .current_graph
284            .x
285            .matmul(&self.spatial_weight.clone_data())?;
286
287        // Step 2: Temporal encoding based on recent events
288        let temporal_features = self.encode_temporal_context(temporal_graph)?;
289
290        // Step 3: Combine spatial and temporal features
291        let combined_features = spatial_features.add(&temporal_features)?;
292
293        // Step 4: Add bias if present
294        let output_features = if let Some(ref bias) = self.bias {
295            combined_features.add(&bias.clone_data())?
296        } else {
297            combined_features
298        };
299
300        // Create output temporal graph
301        let mut output_graph = temporal_graph.clone();
302        output_graph.current_graph.x = output_features;
303        Ok(output_graph)
304    }
305
306    /// Encode temporal context from recent events
307    fn encode_temporal_context(&self, temporal_graph: &TemporalGraphData) -> Result<Tensor> {
308        let num_nodes = temporal_graph.current_graph.num_nodes;
309        let current_time = temporal_graph.current_time;
310        let lookback_time = current_time - temporal_graph.time_window;
311
312        // Get recent events
313        let recent_events = temporal_graph.get_events_in_range(lookback_time, current_time);
314
315        // Initialize temporal encoding
316        let _temporal_encoding = zeros::<f32>(&[num_nodes, self.out_features])?;
317
318        // Simple temporal encoding based on event recency and frequency
319        let mut node_event_counts = vec![0.0; num_nodes];
320
321        for event in recent_events {
322            if let Some(node_id) = event.node {
323                if node_id < num_nodes {
324                    // Weight by recency (more recent events have higher weight)
325                    let recency_weight =
326                        1.0 - (current_time - event.time) / temporal_graph.time_window;
327                    node_event_counts[node_id] += recency_weight;
328                }
329            }
330        }
331
332        // Convert counts to temporal features
333        let temporal_data: Vec<f32> = node_event_counts
334            .iter()
335            .flat_map(|&count| {
336                // Simple encoding: repeat the count for each output feature
337                (0..self.out_features).map(move |_| count as f32)
338            })
339            .collect();
340
341        Ok(from_vec(
342            temporal_data,
343            &[num_nodes, self.out_features],
344            torsh_core::device::DeviceType::Cpu,
345        )?)
346    }
347}
348
349impl GraphLayer for TGCNConv {
350    fn forward(&self, graph: &GraphData) -> Result<GraphData> {
351        // Convert to temporal graph for processing
352        let temporal_graph = TemporalGraphData::new(graph.clone(), 1.0, 1000);
353        let output_temporal = TGCNConv::forward(self, &temporal_graph)?;
354        Ok(output_temporal.current_graph)
355    }
356
357    fn parameters(&self) -> Vec<Tensor> {
358        let mut params = vec![
359            self.spatial_weight.clone_data(),
360            self.temporal_weight.clone_data(),
361        ];
362        if let Some(ref bias) = self.bias {
363            params.push(bias.clone_data());
364        }
365        params
366    }
367}
368
369/// Temporal Graph Attention Network (TGAT) layer
370#[derive(Debug)]
371pub struct TGATConv {
372    in_features: usize,
373    out_features: usize,
374    heads: usize,
375    time_encoding_dim: usize,
376    query_weight: Parameter,
377    key_weight: Parameter,
378    value_weight: Parameter,
379    time_weight: Parameter,
380    output_weight: Parameter,
381    bias: Option<Parameter>,
382    dropout: f32,
383}
384
385impl TGATConv {
386    /// Create a new TGAT layer
387    pub fn new(
388        in_features: usize,
389        out_features: usize,
390        heads: usize,
391        time_encoding_dim: usize,
392        dropout: f32,
393        bias: bool,
394    ) -> Result<Self> {
395        let query_weight = Parameter::new(randn(&[in_features, out_features])?);
396        let key_weight = Parameter::new(randn(&[in_features, out_features])?);
397        let value_weight = Parameter::new(randn(&[in_features, out_features])?);
398        let time_weight = Parameter::new(randn(&[time_encoding_dim, out_features])?);
399        let output_weight = Parameter::new(randn(&[out_features, out_features])?);
400
401        let bias = if bias {
402            Some(Parameter::new(zeros(&[out_features])?))
403        } else {
404            None
405        };
406
407        Ok(Self {
408            in_features,
409            out_features,
410            heads,
411            time_encoding_dim,
412            query_weight,
413            key_weight,
414            value_weight,
415            time_weight,
416            output_weight,
417            bias,
418            dropout,
419        })
420    }
421
422    /// Forward pass through TGAT layer
423    pub fn forward(&self, temporal_graph: &TemporalGraphData) -> Result<TemporalGraphData> {
424        let num_nodes = temporal_graph.current_graph.num_nodes;
425        let head_dim = self.out_features / self.heads;
426
427        // Compute Q, K, V transformations
428        let queries = temporal_graph
429            .current_graph
430            .x
431            .matmul(&self.query_weight.clone_data())?;
432        let keys = temporal_graph
433            .current_graph
434            .x
435            .matmul(&self.key_weight.clone_data())?;
436        let values = temporal_graph
437            .current_graph
438            .x
439            .matmul(&self.value_weight.clone_data())?;
440
441        // Compute time encoding for each node based on recent activity
442        let time_encoding = self.compute_time_encoding(temporal_graph);
443        let time_transformed = time_encoding?.matmul(&self.time_weight.clone_data())?;
444
445        // Reshape for multi-head attention
446        let q = queries.view(&[num_nodes as i32, self.heads as i32, head_dim as i32])?;
447        let k = keys.view(&[num_nodes as i32, self.heads as i32, head_dim as i32])?;
448        let v = values.view(&[num_nodes as i32, self.heads as i32, head_dim as i32])?;
449
450        // Perform temporal attention
451        let attended_features =
452            self.temporal_attention(&q, &k, &v, &time_transformed, temporal_graph);
453
454        // Reshape and apply output transformation
455        let concatenated =
456            attended_features?.view(&[num_nodes as i32, self.out_features as i32])?;
457        let mut output = concatenated.matmul(&self.output_weight.clone_data())?;
458
459        // Add bias if present
460        if let Some(ref bias) = self.bias {
461            output = output.add(&bias.clone_data())?;
462        }
463
464        // Create output temporal graph
465        let mut output_graph = temporal_graph.clone();
466        output_graph.current_graph.x = output;
467        Ok(output_graph)
468    }
469
470    /// Compute time encoding for nodes based on recent events
471    fn compute_time_encoding(&self, temporal_graph: &TemporalGraphData) -> Result<Tensor> {
472        let num_nodes = temporal_graph.current_graph.num_nodes;
473        let current_time = temporal_graph.current_time;
474
475        // Simple time encoding: time since last event for each node
476        let mut time_features = vec![current_time as f32; num_nodes * self.time_encoding_dim];
477
478        // Update with actual last event times
479        for (node_id, history) in &temporal_graph.node_features_history {
480            if *node_id < num_nodes {
481                if let Some((timestamp, _)) = history.iter().next_back() {
482                    let last_event_time = (*timestamp as f64) / 1000.0;
483                    let time_diff = (current_time - last_event_time) as f32;
484
485                    // Encode time difference in multiple dimensions
486                    for dim in 0..self.time_encoding_dim {
487                        let freq = 2.0_f32.powf(dim as f32);
488                        let encoded = (time_diff * freq).sin();
489                        time_features[*node_id * self.time_encoding_dim + dim] = encoded;
490                    }
491                }
492            }
493        }
494
495        Ok(from_vec(
496            time_features,
497            &[num_nodes, self.time_encoding_dim],
498            torsh_core::device::DeviceType::Cpu,
499        )?)
500    }
501
502    /// Temporal attention mechanism
503    fn temporal_attention(
504        &self,
505        q: &Tensor,
506        k: &Tensor,
507        v: &Tensor,
508        _time_encoding: &Tensor,
509        temporal_graph: &TemporalGraphData,
510    ) -> Result<Tensor> {
511        let num_nodes = temporal_graph.current_graph.num_nodes;
512        let head_dim = self.out_features / self.heads;
513
514        // Simplified temporal attention
515        let mut output = zeros(&[num_nodes, self.heads, head_dim])?;
516
517        // For each head, compute attention with temporal bias
518        for head in 0..self.heads {
519            // Extract head-specific features
520            let _q_head = q.slice_tensor(1, head, head + 1)?;
521            let _k_head = k.slice_tensor(1, head, head + 1)?;
522            let v_head = v.slice_tensor(1, head, head + 1)?;
523
524            // Simplified attention computation (using dot product)
525            for i in 0..num_nodes {
526                let mut attended_value = zeros(&[head_dim])?;
527                let mut attention_sum = 0.0;
528
529                for j in 0..num_nodes {
530                    // Basic attention score computation
531                    let score = 1.0 / (1.0 + (i as f32 - j as f32).abs()); // Distance-based attention
532
533                    // Get value for node j
534                    let v_j = v_head
535                        .slice_tensor(0, j, j + 1)?
536                        .squeeze_tensor(0)?
537                        .squeeze_tensor(0)?;
538
539                    let weighted_value = v_j.mul_scalar(score)?;
540                    attended_value = attended_value.add(&weighted_value)?;
541                    attention_sum += score;
542                }
543
544                // Normalize
545                if attention_sum > 0.0 {
546                    attended_value = attended_value.div_scalar(attention_sum)?;
547                }
548
549                // Store in output (simplified assignment)
550                let attended_data = attended_value.to_vec()?;
551                for (dim, &val) in attended_data.iter().enumerate() {
552                    if dim < head_dim {
553                        output.set_item(&[i, head, dim], val)?;
554                    }
555                }
556            }
557        }
558
559        Ok(output)
560    }
561}
562
563impl GraphLayer for TGATConv {
564    fn forward(&self, graph: &GraphData) -> Result<GraphData> {
565        let temporal_graph = TemporalGraphData::new(graph.clone(), 1.0, 1000);
566        let output_temporal = TGATConv::forward(self, &temporal_graph)?;
567        Ok(output_temporal.current_graph)
568    }
569
570    fn parameters(&self) -> Vec<Tensor> {
571        let mut params = vec![
572            self.query_weight.clone_data(),
573            self.key_weight.clone_data(),
574            self.value_weight.clone_data(),
575            self.time_weight.clone_data(),
576            self.output_weight.clone_data(),
577        ];
578        if let Some(ref bias) = self.bias {
579            params.push(bias.clone_data());
580        }
581        params
582    }
583}
584
585/// Memory-augmented Temporal Graph Network (TGN) layer
586#[derive(Debug)]
587pub struct TGNConv {
588    in_features: usize,
589    out_features: usize,
590    memory_dim: usize,
591    time_encoding_dim: usize,
592    message_function: Parameter,
593    memory_updater: Parameter,
594    node_embedding: Parameter,
595    bias: Option<Parameter>,
596    node_memories: HashMap<usize, Tensor>,
597    last_update_times: HashMap<usize, f64>,
598}
599
600impl TGNConv {
601    /// Create a new TGN layer
602    pub fn new(
603        in_features: usize,
604        out_features: usize,
605        memory_dim: usize,
606        time_encoding_dim: usize,
607        bias: bool,
608    ) -> Result<Self> {
609        let message_function =
610            Parameter::new(randn(&[in_features + time_encoding_dim, memory_dim])?);
611        let memory_updater = Parameter::new(randn(&[memory_dim * 2, memory_dim])?);
612        let node_embedding = Parameter::new(randn(&[memory_dim, out_features])?);
613
614        let bias = if bias {
615            Some(Parameter::new(zeros(&[out_features])?))
616        } else {
617            None
618        };
619
620        Ok(Self {
621            in_features,
622            out_features,
623            memory_dim,
624            time_encoding_dim,
625            message_function,
626            memory_updater,
627            node_embedding,
628            bias,
629            node_memories: HashMap::new(),
630            last_update_times: HashMap::new(),
631        })
632    }
633
634    /// Forward pass through TGN layer
635    ///
636    /// # Errors
637    /// Propagates memory-update and embedding tensor-operation failures.
638    pub fn forward(&mut self, temporal_graph: &TemporalGraphData) -> Result<TemporalGraphData> {
639        // Update node memories based on recent events
640        self.update_memories(temporal_graph)?;
641
642        // Generate node embeddings from memories
643        let output_features = self.generate_embeddings(temporal_graph)?;
644
645        // Create output temporal graph
646        let mut output_graph = temporal_graph.clone();
647        output_graph.current_graph.x = output_features;
648        Ok(output_graph)
649    }
650
651    /// Update node memories based on temporal events
652    fn update_memories(&mut self, temporal_graph: &TemporalGraphData) -> Result<()> {
653        let current_time = temporal_graph.current_time;
654        let lookback_time = current_time - temporal_graph.time_window;
655
656        // Get recent events
657        let recent_events = temporal_graph.get_events_in_range(lookback_time, current_time);
658
659        for event in recent_events {
660            if let Some(node_id) = event.node {
661                // Generate message from event
662                let message = self.compute_message(event, current_time)?;
663
664                // Update node memory
665                self.update_node_memory(node_id, message, event.time)?;
666            }
667        }
668
669        Ok(())
670    }
671
672    /// Compute message from temporal event
673    fn compute_message(&self, event: &TemporalEvent, current_time: f64) -> Result<Tensor> {
674        // Time encoding
675        let time_diff = (current_time - event.time) as f32;
676        let mut time_encoding = Vec::new();
677
678        for i in 0..self.time_encoding_dim {
679            let freq = 2.0_f32.powf(i as f32);
680            time_encoding.push((time_diff * freq).sin());
681        }
682
683        // Combine event features with time encoding
684        let mut message_input = if let Some(ref features) = event.features {
685            features.to_vec()?
686        } else {
687            vec![1.0; self.in_features] // Default features
688        };
689
690        message_input.extend(time_encoding);
691
692        let input_tensor = from_vec(
693            message_input,
694            &[1, self.in_features + self.time_encoding_dim],
695            torsh_core::device::DeviceType::Cpu,
696        )?;
697
698        // Apply message function
699        Ok(input_tensor.matmul(&self.message_function.clone_data())?)
700    }
701
702    /// Update memory for a specific node
703    fn update_node_memory(
704        &mut self,
705        node_id: usize,
706        message: Tensor,
707        event_time: f64,
708    ) -> Result<()> {
709        // Get current memory or initialize
710        let current_memory = match self.node_memories.get(&node_id).cloned() {
711            Some(memory) => memory,
712            None => zeros(&[1, self.memory_dim])?,
713        };
714
715        // Concatenate current memory and message
716        let current_data = current_memory.to_vec()?;
717        let message_data = message.to_vec()?;
718        let mut combined_data = current_data;
719        combined_data.extend(message_data);
720
721        let combined_tensor = from_vec(
722            combined_data,
723            &[1, self.memory_dim * 2],
724            torsh_core::device::DeviceType::Cpu,
725        )?;
726
727        // Update memory using memory updater
728        let new_memory = combined_tensor.matmul(&self.memory_updater.clone_data())?;
729
730        self.node_memories.insert(node_id, new_memory);
731        self.last_update_times.insert(node_id, event_time);
732
733        Ok(())
734    }
735
736    /// Generate node embeddings from memories
737    fn generate_embeddings(&self, temporal_graph: &TemporalGraphData) -> Result<Tensor> {
738        let num_nodes = temporal_graph.current_graph.num_nodes;
739        let mut embeddings = Vec::new();
740
741        for node_id in 0..num_nodes {
742            let memory = match self.node_memories.get(&node_id).cloned() {
743                Some(memory) => memory,
744                None => zeros(&[1, self.memory_dim])?,
745            };
746
747            let embedding = memory.matmul(&self.node_embedding.clone_data())?;
748            let embedding_data = embedding.to_vec()?;
749            embeddings.extend(embedding_data);
750        }
751
752        let mut output = from_vec(
753            embeddings,
754            &[num_nodes, self.out_features],
755            torsh_core::device::DeviceType::Cpu,
756        )?;
757
758        // Add bias if present
759        if let Some(ref bias) = self.bias {
760            output = output.add(&bias.clone_data())?;
761        }
762
763        Ok(output)
764    }
765}
766
767/// Temporal graph pooling operations
768pub mod pooling {
769    use super::*;
770
771    /// Temporal pooling methods
772    #[derive(Debug, Clone, Copy)]
773    pub enum TemporalPoolingMethod {
774        MostRecent,
775        TimeWeightedMean,
776        ExponentialDecay,
777        AttentionBased,
778    }
779
780    /// Global temporal pooling
781    pub fn temporal_pool(
782        temporal_graph: &TemporalGraphData,
783        method: TemporalPoolingMethod,
784    ) -> Result<Tensor> {
785        match method {
786            TemporalPoolingMethod::MostRecent => {
787                // Use current graph features
788                Ok(temporal_graph.current_graph.x.mean(Some(&[0]), false)?)
789            }
790            TemporalPoolingMethod::TimeWeightedMean => time_weighted_pool(temporal_graph),
791            TemporalPoolingMethod::ExponentialDecay => exponential_decay_pool(temporal_graph),
792            TemporalPoolingMethod::AttentionBased => attention_temporal_pool(temporal_graph),
793        }
794    }
795
796    /// Time-weighted pooling based on event recency
797    fn time_weighted_pool(temporal_graph: &TemporalGraphData) -> Result<Tensor> {
798        let current_time = temporal_graph.current_time;
799        let lookback_time = current_time - temporal_graph.time_window;
800        let recent_events = temporal_graph.get_events_in_range(lookback_time, current_time);
801
802        if recent_events.is_empty() {
803            return Ok(temporal_graph.current_graph.x.mean(Some(&[0]), false)?);
804        }
805
806        // Weight events by recency
807        let mut weighted_sum = zeros(&[temporal_graph.current_graph.x.shape().dims()[1]])?;
808        let mut total_weight = 0.0;
809
810        for event in recent_events {
811            if let Some(ref features) = event.features {
812                let weight = 1.0 - (current_time - event.time) / temporal_graph.time_window;
813                let weighted_features = features.mul_scalar(weight as f32)?;
814
815                // Sum the features (simplified)
816                let features_data = weighted_features.to_vec()?;
817                let current_data = weighted_sum.to_vec()?;
818                let mut new_data = Vec::new();
819
820                for (_i, (&current, &new)) in
821                    current_data.iter().zip(features_data.iter()).enumerate()
822                {
823                    new_data.push(current + new);
824                }
825
826                weighted_sum = from_vec(
827                    new_data,
828                    &[weighted_sum.shape().dims()[0]],
829                    torsh_core::device::DeviceType::Cpu,
830                )?;
831
832                total_weight += weight;
833            }
834        }
835
836        if total_weight > 0.0 {
837            Ok(weighted_sum.div_scalar(total_weight as f32)?)
838        } else {
839            Ok(temporal_graph.current_graph.x.mean(Some(&[0]), false)?)
840        }
841    }
842
843    /// Exponential decay pooling
844    fn exponential_decay_pool(temporal_graph: &TemporalGraphData) -> Result<Tensor> {
845        let decay_rate = 0.1; // Decay parameter
846        let current_time = temporal_graph.current_time;
847
848        // Simple exponential decay - use current features
849        let decay_factor = (-decay_rate * current_time).exp() as f32;
850        Ok(temporal_graph
851            .current_graph
852            .x
853            .mul_scalar(decay_factor)?
854            .mean(Some(&[0]), false)?)
855    }
856
857    /// Attention-based temporal pooling
858    fn attention_temporal_pool(temporal_graph: &TemporalGraphData) -> Result<Tensor> {
859        // Simplified attention pooling
860        let features = &temporal_graph.current_graph.x;
861        let attention_scores = features.sum_dim(&[1], false)?;
862        let attention_weights = attention_scores.softmax(0)?;
863        let attention_expanded = attention_weights.unsqueeze(-1)?;
864
865        let weighted_features = features.mul(&attention_expanded)?;
866        Ok(weighted_features.sum_dim(&[0], false)?)
867    }
868}
869
870/// Temporal graph utilities
871pub mod utils {
872    use super::*;
873
874    /// Generate random temporal events
875    pub fn generate_random_events(
876        num_events: usize,
877        num_nodes: usize,
878        time_span: f64,
879        feature_dim: usize,
880    ) -> Result<Vec<TemporalEvent>> {
881        let mut rng = scirs2_core::random::thread_rng();
882        let mut events = Vec::new();
883
884        for _ in 0..num_events {
885            let time = rng.gen_range(0.0..time_span);
886            let event_type = if rng.gen_range(0.0..1.0) < 0.7 {
887                EventType::NodeFeatureUpdate
888            } else {
889                EventType::EdgeAddition
890            };
891
892            let node = if matches!(event_type, EventType::NodeFeatureUpdate) {
893                Some(rng.gen_range(0..num_nodes))
894            } else {
895                None
896            };
897
898            let (source, target) = if matches!(event_type, EventType::EdgeAddition) {
899                let s = rng.gen_range(0..num_nodes);
900                let t = rng.gen_range(0..num_nodes);
901                (Some(s), Some(t))
902            } else {
903                (None, None)
904            };
905
906            let features = if matches!(event_type, EventType::NodeFeatureUpdate) {
907                Some(randn(&[feature_dim])?)
908            } else {
909                None
910            };
911
912            events.push(TemporalEvent {
913                time,
914                event_type,
915                source,
916                target,
917                node,
918                features,
919                weight: Some(rng.gen_range(0.1..1.0)),
920            });
921        }
922
923        // Sort events by time
924        events.sort_by(|a, b| {
925            a.time
926                .partial_cmp(&b.time)
927                .unwrap_or(std::cmp::Ordering::Equal)
928        });
929        Ok(events)
930    }
931
932    /// Create temporal graph from event sequence
933    pub fn create_temporal_graph_from_events(
934        initial_graph: GraphData,
935        events: Vec<TemporalEvent>,
936        time_window: f64,
937    ) -> Result<TemporalGraphData> {
938        let mut temporal_graph = TemporalGraphData::new(initial_graph, time_window, 10000);
939
940        for event in events {
941            temporal_graph.add_event(event)?;
942        }
943
944        Ok(temporal_graph)
945    }
946
947    /// Compute temporal graph metrics
948    pub fn temporal_metrics(temporal_graph: &TemporalGraphData) -> TemporalMetrics {
949        let total_events = temporal_graph.events.values().map(|v| v.len()).sum();
950        let unique_nodes_with_events = temporal_graph.node_features_history.len();
951        let time_span = if let (Some(first), Some(last)) = (
952            temporal_graph.events.keys().next(),
953            temporal_graph.events.keys().next_back(),
954        ) {
955            (*last as f64 - *first as f64) / 1000.0
956        } else {
957            0.0
958        };
959
960        let event_rate = if time_span > 0.0 {
961            total_events as f64 / time_span
962        } else {
963            0.0
964        };
965
966        TemporalMetrics {
967            total_events,
968            unique_active_nodes: unique_nodes_with_events,
969            time_span,
970            event_rate,
971            current_time: temporal_graph.current_time,
972        }
973    }
974
975    /// Temporal graph metrics
976    #[derive(Debug, Clone)]
977    pub struct TemporalMetrics {
978        pub total_events: usize,
979        pub unique_active_nodes: usize,
980        pub time_span: f64,
981        pub event_rate: f64,
982        pub current_time: f64,
983    }
984}
985
986#[cfg(test)]
987mod tests {
988    use super::*;
989    use torsh_core::device::DeviceType;
990
991    #[test]
992    fn test_temporal_graph_creation() {
993        let features = randn(&[4, 3]).unwrap();
994        let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 0.0];
995        let edge_index = from_vec(edges, &[2, 4], DeviceType::Cpu).unwrap();
996        let graph = GraphData::new(features, edge_index);
997
998        let temporal_graph = TemporalGraphData::new(graph, 10.0, 1000);
999
1000        assert_eq!(temporal_graph.current_graph.num_nodes, 4);
1001        assert_eq!(temporal_graph.time_window, 10.0);
1002        assert_eq!(temporal_graph.max_events, 1000);
1003    }
1004
1005    #[test]
1006    fn test_temporal_event_addition() {
1007        let features = randn(&[3, 2]).unwrap();
1008        let edges = vec![0.0, 1.0, 1.0, 2.0];
1009        let edge_index = from_vec(edges, &[2, 2], DeviceType::Cpu).unwrap();
1010        let graph = GraphData::new(features, edge_index);
1011
1012        let mut temporal_graph = TemporalGraphData::new(graph, 5.0, 100);
1013
1014        let event = TemporalEvent {
1015            time: 1.0,
1016            event_type: EventType::NodeFeatureUpdate,
1017            source: None,
1018            target: None,
1019            node: Some(0),
1020            features: Some(randn(&[2]).unwrap()),
1021            weight: None,
1022        };
1023
1024        temporal_graph.add_event(event).expect("add event");
1025
1026        assert_eq!(temporal_graph.current_time, 1.0);
1027        assert!(!temporal_graph.events.is_empty());
1028    }
1029
1030    #[test]
1031    fn test_tgcn_layer() {
1032        let features = randn(&[3, 4]).unwrap();
1033        let edges = vec![0.0, 1.0, 1.0, 2.0];
1034        let edge_index = from_vec(edges, &[2, 2], DeviceType::Cpu).unwrap();
1035        let graph = GraphData::new(features, edge_index);
1036
1037        let temporal_graph = TemporalGraphData::new(graph, 1.0, 100);
1038        let tgcn = TGCNConv::new(4, 8, 16, 64, true).expect("operation should succeed");
1039
1040        let output = tgcn
1041            .forward(&temporal_graph)
1042            .expect("operation should succeed");
1043        assert_eq!(output.current_graph.x.shape().dims(), &[3, 8]);
1044    }
1045
1046    #[test]
1047    fn test_tgat_layer() {
1048        let features = randn(&[4, 6]).unwrap();
1049        let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0];
1050        let edge_index = from_vec(edges, &[2, 3], DeviceType::Cpu).unwrap();
1051        let graph = GraphData::new(features, edge_index);
1052
1053        let temporal_graph = TemporalGraphData::new(graph, 2.0, 200);
1054        let tgat = TGATConv::new(6, 12, 3, 8, 0.1, true).expect("operation should succeed");
1055
1056        let output = tgat
1057            .forward(&temporal_graph)
1058            .expect("operation should succeed");
1059        assert_eq!(output.current_graph.x.shape().dims(), &[4, 12]);
1060    }
1061
1062    #[test]
1063    fn test_temporal_pooling() {
1064        let features = randn(&[5, 4]).unwrap();
1065        let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0];
1066        let edge_index = from_vec(edges, &[2, 4], DeviceType::Cpu).unwrap();
1067        let graph = GraphData::new(features, edge_index);
1068
1069        let temporal_graph = TemporalGraphData::new(graph, 3.0, 150);
1070
1071        let pooled =
1072            pooling::temporal_pool(&temporal_graph, pooling::TemporalPoolingMethod::MostRecent)
1073                .expect("operation should succeed");
1074        assert_eq!(pooled.shape().dims(), &[4]);
1075
1076        let weighted_pooled = pooling::temporal_pool(
1077            &temporal_graph,
1078            pooling::TemporalPoolingMethod::TimeWeightedMean,
1079        )
1080        .expect("operation should succeed");
1081        assert_eq!(weighted_pooled.shape().dims(), &[4]);
1082    }
1083
1084    #[test]
1085    fn test_temporal_utils() {
1086        let events =
1087            utils::generate_random_events(10, 5, 10.0, 3).expect("operation should succeed");
1088        assert_eq!(events.len(), 10);
1089
1090        // Check that events are sorted by time
1091        for i in 1..events.len() {
1092            assert!(events[i].time >= events[i - 1].time);
1093        }
1094
1095        let features = randn(&[5, 3]).unwrap();
1096        let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0, 4.0, 0.0];
1097        let edge_index = from_vec(edges, &[2, 5], DeviceType::Cpu).unwrap();
1098        let graph = GraphData::new(features, edge_index);
1099
1100        let temporal_graph = utils::create_temporal_graph_from_events(graph, events, 5.0)
1101            .expect("operation should succeed");
1102        let metrics = utils::temporal_metrics(&temporal_graph);
1103
1104        assert!(metrics.total_events > 0);
1105        assert!(metrics.time_span >= 0.0);
1106    }
1107
1108    #[test]
1109    fn test_event_time_range_query() {
1110        let features = randn(&[3, 2]).unwrap();
1111        let edges = vec![0.0, 1.0, 1.0, 2.0];
1112        let edge_index = from_vec(edges, &[2, 2], DeviceType::Cpu).unwrap();
1113        let graph = GraphData::new(features, edge_index);
1114
1115        let mut temporal_graph = TemporalGraphData::new(graph, 10.0, 100);
1116
1117        // Add events at different times
1118        for i in 0..5 {
1119            let event = TemporalEvent {
1120                time: i as f64,
1121                event_type: EventType::NodeFeatureUpdate,
1122                source: None,
1123                target: None,
1124                node: Some(i % 3),
1125                features: Some(randn(&[2]).unwrap()),
1126                weight: None,
1127            };
1128            temporal_graph.add_event(event).expect("add event");
1129        }
1130
1131        let events_in_range = temporal_graph.get_events_in_range(1.0, 3.0);
1132        assert_eq!(events_in_range.len(), 3); // Events at times 1, 2, 3
1133    }
1134}