Skip to main content

torsh_graph/
neural_operators.rs

1//! Graph Neural Operators
2//!
3//! Advanced implementation of graph neural operators for learning continuous
4//! functions on graphs. Inspired by Neural Operator Theory and Physics-Informed
5//! Neural Networks (PINNs) for graph-structured data.
6//!
7//! # Features:
8//! - Graph Fourier Neural Operators (GraphFNO)
9//! - Graph DeepONet for operator learning
10//! - Physics-informed graph neural networks
11//! - Multi-scale graph operators
12//! - Spectral graph convolutions with learnable kernels
13//! - Graph wavelet neural operators
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 torsh_tensor::{
23    creation::{from_vec, randn, zeros},
24    Tensor,
25};
26
27/// Graph Fourier Neural Operator (GraphFNO)
28/// Learns operators in the spectral domain of graphs
29#[derive(Debug)]
30pub struct GraphFNO {
31    in_features: usize,
32    out_features: usize,
33    hidden_features: usize,
34    num_modes: usize,
35    num_layers: usize,
36
37    // Fourier layers
38    fourier_weights: Vec<Parameter>,
39    conv_weights: Vec<Parameter>,
40
41    // Input/output projections
42    input_projection: Parameter,
43    output_projection: Parameter,
44
45    // Bias terms
46    bias: Option<Parameter>,
47}
48
49impl GraphFNO {
50    /// Create a new Graph Fourier Neural Operator
51    pub fn new(
52        in_features: usize,
53        out_features: usize,
54        hidden_features: usize,
55        num_modes: usize,
56        num_layers: usize,
57        bias: bool,
58    ) -> Result<Self> {
59        let mut fourier_weights = Vec::new();
60        let mut conv_weights = Vec::new();
61
62        // Initialize Fourier weights for each layer
63        for _ in 0..num_layers {
64            fourier_weights.push(Parameter::new(randn(&[
65                hidden_features,
66                hidden_features,
67                num_modes,
68            ])?));
69            conv_weights.push(Parameter::new(randn(&[hidden_features, hidden_features])?));
70        }
71
72        let input_projection = Parameter::new(randn(&[in_features, hidden_features])?);
73        let output_projection = Parameter::new(randn(&[hidden_features, out_features])?);
74
75        let bias = if bias {
76            Some(Parameter::new(zeros::<f32>(&[out_features])?))
77        } else {
78            None
79        };
80
81        Ok(Self {
82            in_features,
83            out_features,
84            hidden_features,
85            num_modes,
86            num_layers,
87            fourier_weights,
88            conv_weights,
89            input_projection,
90            output_projection,
91            bias,
92        })
93    }
94
95    /// Forward pass through GraphFNO
96    pub fn forward(&self, graph: &GraphData) -> Result<GraphData> {
97        let _num_nodes = graph.num_nodes;
98
99        // Input projection
100        let mut x = graph.x.matmul(&self.input_projection.clone_data())?;
101
102        // Apply Fourier layers
103        for layer in 0..self.num_layers {
104            x = self.fourier_layer(&x, layer, graph)?;
105        }
106
107        // Output projection
108        let mut output = x.matmul(&self.output_projection.clone_data())?;
109
110        // Add bias if present
111        if let Some(ref bias) = self.bias {
112            output = output.add(&bias.clone_data())?;
113        }
114
115        // Create output graph
116        let mut output_graph = graph.clone();
117        output_graph.x = output;
118        Ok(output_graph)
119    }
120
121    /// Apply a single Fourier layer
122    fn fourier_layer(&self, x: &Tensor, layer: usize, graph: &GraphData) -> Result<Tensor> {
123        // Step 1: Apply graph Fourier transform (simplified)
124        let fourier_x = self.graph_fourier_transform(x, graph)?;
125
126        // Step 2: Apply learnable Fourier weights
127        let fourier_weights = &self.fourier_weights[layer];
128        let spectral_conv = self.spectral_convolution(&fourier_x, fourier_weights)?;
129
130        // Step 3: Inverse Fourier transform
131        let spatial_features = self.inverse_graph_fourier_transform(&spectral_conv, graph)?;
132
133        // Step 4: Apply spatial convolution
134        let conv_weights = &self.conv_weights[layer];
135        let conv_output = spatial_features.matmul(&conv_weights.clone_data())?;
136
137        // Step 5: Residual connection and activation
138        let residual = x.add(&conv_output)?;
139
140        // Apply ReLU activation (simplified)
141        self.relu(&residual)
142    }
143
144    /// Graph Fourier Transform (simplified eigendecomposition)
145    fn graph_fourier_transform(&self, x: &Tensor, graph: &GraphData) -> Result<Tensor> {
146        // For simplicity, we'll use a learned transformation matrix
147        // In practice, this would use graph Laplacian eigendecomposition
148        let num_nodes = graph.num_nodes;
149
150        // Create a simple transformation that captures spectral properties
151        let mut transform_data = Vec::new();
152        for i in 0..num_nodes {
153            for j in 0..self.num_modes {
154                let freq = (j as f32 + 1.0) * std::f32::consts::PI / num_nodes as f32;
155                let basis = (freq * i as f32).cos();
156                transform_data.push(basis);
157            }
158        }
159
160        let transform_matrix = from_vec(
161            transform_data,
162            &[num_nodes, self.num_modes],
163            torsh_core::device::DeviceType::Cpu,
164        )?;
165
166        // Project to spectral domain
167        Ok(transform_matrix.t()?.matmul(x)?)
168    }
169
170    /// Inverse Graph Fourier Transform
171    fn inverse_graph_fourier_transform(
172        &self,
173        fourier_x: &Tensor,
174        graph: &GraphData,
175    ) -> Result<Tensor> {
176        let num_nodes = graph.num_nodes;
177
178        // Create inverse transformation matrix
179        let mut inv_transform_data = Vec::new();
180        for i in 0..num_nodes {
181            for j in 0..self.num_modes {
182                let freq = (j as f32 + 1.0) * std::f32::consts::PI / num_nodes as f32;
183                let basis = (freq * i as f32).cos();
184                inv_transform_data.push(basis);
185            }
186        }
187
188        let inv_transform_matrix = from_vec(
189            inv_transform_data,
190            &[num_nodes, self.num_modes],
191            torsh_core::device::DeviceType::Cpu,
192        )?;
193
194        // Project back to spatial domain
195        Ok(inv_transform_matrix.matmul(fourier_x)?)
196    }
197
198    /// Spectral convolution in Fourier domain
199    fn spectral_convolution(&self, fourier_x: &Tensor, weights: &Parameter) -> Result<Tensor> {
200        // Apply Fourier weights (simplified)
201        let weight_data = weights.clone_data();
202
203        // For simplicity, use only the first mode slice
204        // In practice, this would involve complex multiplication across all modes
205        let weight_2d = weight_data.slice_tensor(2, 0, 1)?.squeeze_tensor(2)?;
206
207        Ok(fourier_x.matmul(&weight_2d)?)
208    }
209
210    /// ReLU activation function
211    fn relu(&self, x: &Tensor) -> Result<Tensor> {
212        // Simplified ReLU - clamp negative values to 0
213        let data = x.to_vec()?;
214        let activated_data: Vec<f32> = data.iter().map(|&val| val.max(0.0)).collect();
215
216        Ok(from_vec(
217            activated_data,
218            x.shape().dims(),
219            torsh_core::device::DeviceType::Cpu,
220        )?)
221    }
222}
223
224impl GraphLayer for GraphFNO {
225    fn forward(&self, graph: &GraphData) -> Result<GraphData> {
226        self.forward(graph)
227    }
228
229    fn parameters(&self) -> Vec<Tensor> {
230        let mut params = vec![
231            self.input_projection.clone_data(),
232            self.output_projection.clone_data(),
233        ];
234
235        for weight in &self.fourier_weights {
236            params.push(weight.clone_data());
237        }
238
239        for weight in &self.conv_weights {
240            params.push(weight.clone_data());
241        }
242
243        if let Some(ref bias) = self.bias {
244            params.push(bias.clone_data());
245        }
246
247        params
248    }
249}
250
251/// Graph DeepONet for operator learning on graphs
252#[derive(Debug)]
253pub struct GraphDeepONet {
254    trunk_net_features: usize,
255    branch_net_features: usize,
256    hidden_features: usize,
257    output_features: usize,
258    num_sensors: usize,
259
260    // Branch network (processes input functions)
261    branch_layers: Vec<Parameter>,
262
263    // Trunk network (processes locations/coordinates)
264    trunk_layers: Vec<Parameter>,
265
266    // Output bias
267    bias: Option<Parameter>,
268}
269
270impl GraphDeepONet {
271    /// Create a new Graph DeepONet
272    pub fn new(
273        trunk_net_features: usize,
274        branch_net_features: usize,
275        hidden_features: usize,
276        output_features: usize,
277        num_sensors: usize,
278        num_layers: usize,
279        bias: bool,
280    ) -> Result<Self> {
281        let mut branch_layers = Vec::new();
282        let mut trunk_layers = Vec::new();
283
284        // Initialize branch network layers
285        for i in 0..num_layers {
286            let in_dim = if i == 0 { num_sensors } else { hidden_features };
287            let out_dim = if i == num_layers - 1 {
288                output_features
289            } else {
290                hidden_features
291            };
292            branch_layers.push(Parameter::new(randn(&[in_dim, out_dim])?));
293        }
294
295        // Initialize trunk network layers
296        for i in 0..num_layers {
297            let in_dim = if i == 0 {
298                trunk_net_features
299            } else {
300                hidden_features
301            };
302            let out_dim = if i == num_layers - 1 {
303                output_features
304            } else {
305                hidden_features
306            };
307            trunk_layers.push(Parameter::new(randn(&[in_dim, out_dim])?));
308        }
309
310        let bias = if bias {
311            Some(Parameter::new(zeros::<f32>(&[output_features])?))
312        } else {
313            None
314        };
315
316        Ok(Self {
317            trunk_net_features,
318            branch_net_features,
319            hidden_features,
320            output_features,
321            num_sensors,
322            branch_layers,
323            trunk_layers,
324            bias,
325        })
326    }
327
328    /// Forward pass through Graph DeepONet
329    pub fn forward(
330        &self,
331        graph: &GraphData,
332        sensor_data: &Tensor,
333        locations: &Tensor,
334    ) -> Result<GraphData> {
335        // Process sensor data through branch network
336        let branch_output = self.forward_branch_net(sensor_data)?;
337
338        // Process locations through trunk network
339        let trunk_output = self.forward_trunk_net(locations)?;
340
341        // Combine branch and trunk outputs (dot product)
342        let combined = self.combine_outputs(&branch_output, &trunk_output)?;
343
344        // Add bias if present
345        let mut output = combined;
346        if let Some(ref bias) = self.bias {
347            output = output.add(&bias.clone_data())?;
348        }
349
350        // Create output graph
351        let mut output_graph = graph.clone();
352        output_graph.x = output;
353        Ok(output_graph)
354    }
355
356    /// Forward pass through branch network
357    fn forward_branch_net(&self, sensor_data: &Tensor) -> Result<Tensor> {
358        let mut x = sensor_data.clone();
359
360        for (i, layer) in self.branch_layers.iter().enumerate() {
361            x = x.matmul(&layer.clone_data())?;
362
363            // Apply activation function except for last layer
364            if i < self.branch_layers.len() - 1 {
365                x = self.tanh(&x)?;
366            }
367        }
368
369        Ok(x)
370    }
371
372    /// Forward pass through trunk network
373    fn forward_trunk_net(&self, locations: &Tensor) -> Result<Tensor> {
374        let mut x = locations.clone();
375
376        for (i, layer) in self.trunk_layers.iter().enumerate() {
377            x = x.matmul(&layer.clone_data())?;
378
379            // Apply activation function except for last layer
380            if i < self.trunk_layers.len() - 1 {
381                x = self.tanh(&x)?;
382            }
383        }
384
385        Ok(x)
386    }
387
388    /// Combine branch and trunk network outputs
389    fn combine_outputs(&self, branch_output: &Tensor, trunk_output: &Tensor) -> Result<Tensor> {
390        // Element-wise multiplication and sum
391        Ok(branch_output.mul(trunk_output)?)
392    }
393
394    /// Tanh activation function
395    fn tanh(&self, x: &Tensor) -> Result<Tensor> {
396        let data = x.to_vec()?;
397        let activated_data: Vec<f32> = data.iter().map(|&val| val.tanh()).collect();
398
399        Ok(from_vec(
400            activated_data,
401            x.shape().dims(),
402            torsh_core::device::DeviceType::Cpu,
403        )?)
404    }
405}
406
407impl GraphLayer for GraphDeepONet {
408    fn forward(&self, graph: &GraphData) -> Result<GraphData> {
409        // Default forward using graph features as both sensor data and locations
410        let sensor_data =
411            graph
412                .x
413                .slice_tensor(1, 0, self.num_sensors.min(graph.x.shape().dims()[1]))?;
414        let locations = graph.x.clone();
415
416        self.forward(graph, &sensor_data, &locations)
417    }
418
419    fn parameters(&self) -> Vec<Tensor> {
420        let mut params = Vec::new();
421
422        for layer in &self.branch_layers {
423            params.push(layer.clone_data());
424        }
425
426        for layer in &self.trunk_layers {
427            params.push(layer.clone_data());
428        }
429
430        if let Some(ref bias) = self.bias {
431            params.push(bias.clone_data());
432        }
433
434        params
435    }
436}
437
438/// Physics-Informed Graph Neural Network
439#[derive(Debug)]
440pub struct PhysicsInformedGNN {
441    in_features: usize,
442    out_features: usize,
443    hidden_features: usize,
444
445    // Neural network layers
446    layers: Vec<Parameter>,
447
448    // Physics constraints
449    diffusion_coefficient: f32,
450    reaction_rate: f32,
451
452    // Bias
453    bias: Option<Parameter>,
454}
455
456impl PhysicsInformedGNN {
457    /// Create a new Physics-Informed GNN
458    pub fn new(
459        in_features: usize,
460        out_features: usize,
461        hidden_features: usize,
462        num_layers: usize,
463        diffusion_coefficient: f32,
464        reaction_rate: f32,
465        bias: bool,
466    ) -> Result<Self> {
467        let mut layers = Vec::new();
468
469        for i in 0..num_layers {
470            let in_dim = if i == 0 { in_features } else { hidden_features };
471            let out_dim = if i == num_layers - 1 {
472                out_features
473            } else {
474                hidden_features
475            };
476            layers.push(Parameter::new(randn(&[in_dim, out_dim])?));
477        }
478
479        let bias = if bias {
480            Some(Parameter::new(zeros::<f32>(&[out_features])?))
481        } else {
482            None
483        };
484
485        Ok(Self {
486            in_features,
487            out_features,
488            hidden_features,
489            layers,
490            diffusion_coefficient,
491            reaction_rate,
492            bias,
493        })
494    }
495
496    /// Forward pass with physics constraints
497    pub fn forward(&self, graph: &GraphData) -> Result<GraphData> {
498        // Neural network forward pass
499        let mut x = graph.x.clone();
500
501        for (i, layer) in self.layers.iter().enumerate() {
502            x = x.matmul(&layer.clone_data())?;
503
504            // Apply activation except for last layer
505            if i < self.layers.len() - 1 {
506                x = self.swish(&x)?;
507            }
508        }
509
510        // Apply physics constraints
511        let physics_constrained = self.apply_physics_constraints(&x, graph);
512
513        // Add bias if present
514        let mut output = physics_constrained;
515        if let Some(ref bias) = self.bias {
516            output = Ok(output?.add(&bias.clone_data())?);
517        }
518
519        // Create output graph
520        let mut output_graph = graph.clone();
521        output_graph.x = output?;
522        Ok(output_graph)
523    }
524
525    /// Apply physics constraints (diffusion-reaction equation)
526    fn apply_physics_constraints(&self, prediction: &Tensor, graph: &GraphData) -> Result<Tensor> {
527        // Compute graph Laplacian for diffusion term
528        let laplacian = self.compute_graph_laplacian(graph);
529
530        // Diffusion term: D * L * u
531        let diffusion_term = laplacian?
532            .matmul(prediction)?
533            .mul_scalar(self.diffusion_coefficient)?;
534
535        // Reaction term: r * u
536        let reaction_term = prediction.mul_scalar(self.reaction_rate)?;
537
538        // Combine terms (simplified physics equation)
539        Ok(prediction.add(&diffusion_term)?.add(&reaction_term)?)
540    }
541
542    /// Compute graph Laplacian matrix
543    fn compute_graph_laplacian(&self, graph: &GraphData) -> Result<Tensor> {
544        let num_nodes = graph.num_nodes;
545        let _num_edges = graph.num_edges;
546
547        // Initialize adjacency matrix
548        let mut adj_data = vec![0.0f32; num_nodes * num_nodes];
549
550        // Fill adjacency matrix from edge_index
551        let edge_data = graph.edge_index.to_vec()?;
552        for i in (0..edge_data.len()).step_by(2) {
553            if i + 1 < edge_data.len() {
554                let src = edge_data[i] as usize;
555                let dst = edge_data[i + 1] as usize;
556
557                if src < num_nodes && dst < num_nodes {
558                    adj_data[src * num_nodes + dst] = 1.0;
559                    adj_data[dst * num_nodes + src] = 1.0; // Undirected graph
560                }
561            }
562        }
563
564        // Compute degree matrix
565        let mut degree_data = vec![0.0f32; num_nodes * num_nodes];
566        for i in 0..num_nodes {
567            let mut degree = 0.0;
568            for j in 0..num_nodes {
569                degree += adj_data[i * num_nodes + j];
570            }
571            degree_data[i * num_nodes + i] = degree;
572        }
573
574        // Laplacian = Degree - Adjacency
575        let mut laplacian_data = Vec::new();
576        for i in 0..num_nodes * num_nodes {
577            laplacian_data.push(degree_data[i] - adj_data[i]);
578        }
579
580        Ok(from_vec(
581            laplacian_data,
582            &[num_nodes, num_nodes],
583            torsh_core::device::DeviceType::Cpu,
584        )?)
585    }
586
587    /// Swish activation function (x * sigmoid(x))
588    fn swish(&self, x: &Tensor) -> Result<Tensor> {
589        let data = x.to_vec()?;
590        let activated_data: Vec<f32> = data
591            .iter()
592            .map(|&val| val * (1.0 / (1.0 + (-val).exp())))
593            .collect();
594
595        Ok(from_vec(
596            activated_data,
597            x.shape().dims(),
598            torsh_core::device::DeviceType::Cpu,
599        )?)
600    }
601}
602
603impl GraphLayer for PhysicsInformedGNN {
604    fn forward(&self, graph: &GraphData) -> Result<GraphData> {
605        self.forward(graph)
606    }
607
608    fn parameters(&self) -> Vec<Tensor> {
609        let mut params = Vec::new();
610
611        for layer in &self.layers {
612            params.push(layer.clone_data());
613        }
614
615        if let Some(ref bias) = self.bias {
616            params.push(bias.clone_data());
617        }
618
619        params
620    }
621}
622
623/// Multi-scale Graph Neural Operator
624#[derive(Debug)]
625pub struct MultiScaleGNO {
626    in_features: usize,
627    out_features: usize,
628    num_scales: usize,
629    hidden_features: usize,
630
631    // Scale-specific operators
632    scale_operators: Vec<Parameter>,
633
634    // Cross-scale fusion
635    fusion_weights: Parameter,
636
637    // Output projection
638    output_projection: Parameter,
639
640    bias: Option<Parameter>,
641}
642
643impl MultiScaleGNO {
644    /// Create a new Multi-scale Graph Neural Operator
645    pub fn new(
646        in_features: usize,
647        out_features: usize,
648        num_scales: usize,
649        hidden_features: usize,
650        bias: bool,
651    ) -> Result<Self> {
652        let mut scale_operators = Vec::new();
653
654        // Initialize scale-specific operators
655        for _ in 0..num_scales {
656            scale_operators.push(Parameter::new(randn(&[in_features, hidden_features])?));
657        }
658
659        let fusion_weights =
660            Parameter::new(randn(&[num_scales * hidden_features, hidden_features])?);
661
662        let output_projection = Parameter::new(randn(&[hidden_features, out_features])?);
663
664        let bias = if bias {
665            Some(Parameter::new(zeros::<f32>(&[out_features])?))
666        } else {
667            None
668        };
669
670        Ok(Self {
671            in_features,
672            out_features,
673            num_scales,
674            hidden_features,
675            scale_operators,
676            fusion_weights,
677            output_projection,
678            bias,
679        })
680    }
681
682    /// Forward pass through multi-scale operator
683    pub fn forward(&self, graph: &GraphData) -> Result<GraphData> {
684        let mut scale_features = Vec::new();
685
686        // Process each scale
687        for scale in 0..self.num_scales {
688            let scale_graph = self.coarsen_graph(graph, scale)?;
689            let features = self.process_scale(&scale_graph, scale)?;
690            let upsampled = self.upsample_features(&features, graph.num_nodes)?;
691            scale_features.push(upsampled);
692        }
693
694        // Fuse multi-scale features
695        let fused_features = self.fuse_scales(&scale_features)?;
696
697        // Output projection
698        let mut output = fused_features.matmul(&self.output_projection.clone_data())?;
699
700        // Add bias if present
701        if let Some(ref bias) = self.bias {
702            output = output.add(&bias.clone_data())?;
703        }
704
705        // Create output graph
706        let mut output_graph = graph.clone();
707        output_graph.x = output;
708        Ok(output_graph)
709    }
710
711    /// Coarsen graph for multi-scale processing
712    fn coarsen_graph(&self, graph: &GraphData, scale: usize) -> Result<GraphData> {
713        let coarsening_factor = 2_usize.pow(scale as u32);
714        let coarse_nodes = (graph.num_nodes + coarsening_factor - 1) / coarsening_factor;
715
716        // Simple node pooling - average features of neighboring nodes
717        let mut coarse_features = Vec::new();
718
719        for coarse_id in 0..coarse_nodes {
720            let start_node = coarse_id * coarsening_factor;
721            let end_node = ((coarse_id + 1) * coarsening_factor).min(graph.num_nodes);
722
723            // Average features of nodes in this coarse group
724            let mut sum_features = vec![0.0f32; graph.x.shape().dims()[1]];
725            let mut count = 0;
726
727            for node_id in start_node..end_node {
728                let features = graph.x.slice_tensor(0, node_id, node_id + 1)?;
729                let feature_data = features.to_vec()?;
730
731                for (i, &val) in feature_data.iter().enumerate() {
732                    if i < sum_features.len() {
733                        sum_features[i] += val;
734                    }
735                }
736                count += 1;
737            }
738
739            // Normalize
740            if count > 0 {
741                for val in &mut sum_features {
742                    *val /= count as f32;
743                }
744            }
745
746            coarse_features.extend(sum_features);
747        }
748
749        let coarse_x = from_vec(
750            coarse_features,
751            &[coarse_nodes, graph.x.shape().dims()[1]],
752            torsh_core::device::DeviceType::Cpu,
753        )?;
754
755        // Simplified edge index (connect sequential nodes)
756        let mut coarse_edges = Vec::new();
757        for i in 0..coarse_nodes.saturating_sub(1) {
758            coarse_edges.push(i as f32);
759            coarse_edges.push((i + 1) as f32);
760        }
761
762        let coarse_edge_index = from_vec(
763            coarse_edges,
764            &[2, coarse_nodes.saturating_sub(1)],
765            torsh_core::device::DeviceType::Cpu,
766        )?;
767
768        Ok(GraphData::new(coarse_x, coarse_edge_index))
769    }
770
771    /// Process features at a specific scale
772    fn process_scale(&self, graph: &GraphData, scale: usize) -> Result<Tensor> {
773        let operator = &self.scale_operators[scale];
774        Ok(graph.x.matmul(&operator.clone_data())?)
775    }
776
777    /// Upsample features to original graph size
778    fn upsample_features(&self, features: &Tensor, target_nodes: usize) -> Result<Tensor> {
779        let current_nodes = features.shape().dims()[0];
780        let feature_dim = features.shape().dims()[1];
781
782        if current_nodes >= target_nodes {
783            // Truncate if necessary
784            return Ok(features.slice_tensor(0, 0, target_nodes)?);
785        }
786
787        // Simple upsampling by repetition
788        let feature_data = features.to_vec()?;
789        let mut upsampled_data = Vec::new();
790
791        for target_id in 0..target_nodes {
792            let source_id = (target_id * current_nodes) / target_nodes;
793            let start_idx = source_id * feature_dim;
794            let end_idx = start_idx + feature_dim;
795
796            if end_idx <= feature_data.len() {
797                upsampled_data.extend(&feature_data[start_idx..end_idx]);
798            } else {
799                // Pad with zeros if needed
800                upsampled_data.extend(vec![0.0f32; feature_dim]);
801            }
802        }
803
804        Ok(from_vec(
805            upsampled_data,
806            &[target_nodes, feature_dim],
807            torsh_core::device::DeviceType::Cpu,
808        )?)
809    }
810
811    /// Fuse multi-scale features
812    fn fuse_scales(&self, scale_features: &[Tensor]) -> Result<Tensor> {
813        // Concatenate features from all scales
814        let mut concatenated_data = Vec::new();
815        let num_nodes = scale_features[0].shape().dims()[0];
816
817        for node_id in 0..num_nodes {
818            for scale_feature in scale_features {
819                let node_features = scale_feature.slice_tensor(0, node_id, node_id + 1)?;
820                let feature_data = node_features.to_vec()?;
821                concatenated_data.extend(feature_data);
822            }
823        }
824
825        let concatenated = from_vec(
826            concatenated_data,
827            &[num_nodes, self.num_scales * self.hidden_features],
828            torsh_core::device::DeviceType::Cpu,
829        )?;
830
831        // Apply fusion weights
832        Ok(concatenated.matmul(&self.fusion_weights.clone_data())?)
833    }
834}
835
836impl GraphLayer for MultiScaleGNO {
837    fn forward(&self, graph: &GraphData) -> Result<GraphData> {
838        self.forward(graph)
839    }
840
841    fn parameters(&self) -> Vec<Tensor> {
842        let mut params = vec![
843            self.fusion_weights.clone_data(),
844            self.output_projection.clone_data(),
845        ];
846
847        for operator in &self.scale_operators {
848            params.push(operator.clone_data());
849        }
850
851        if let Some(ref bias) = self.bias {
852            params.push(bias.clone_data());
853        }
854
855        params
856    }
857}
858
859/// Graph Neural Operator utilities
860pub mod utils {
861    use super::*;
862
863    /// Compute spectral features of a graph
864    pub fn compute_spectral_features(graph: &GraphData, num_eigenvalues: usize) -> Result<Tensor> {
865        // Simplified spectral computation
866        let num_nodes = graph.num_nodes;
867        let mut spectral_data = Vec::new();
868
869        for i in 0..num_nodes {
870            for j in 0..num_eigenvalues {
871                let eigenvalue = (j as f32 + 1.0) / num_eigenvalues as f32;
872                let eigenvector_val = (std::f32::consts::PI * (i as f32 + 1.0) * (j as f32 + 1.0)
873                    / num_nodes as f32)
874                    .sin();
875                spectral_data.push(eigenvalue * eigenvector_val);
876            }
877        }
878
879        Ok(from_vec(
880            spectral_data,
881            &[num_nodes, num_eigenvalues],
882            torsh_core::device::DeviceType::Cpu,
883        )?)
884    }
885
886    /// Generate synthetic operator learning data
887    pub fn generate_operator_data(
888        num_graphs: usize,
889        num_nodes: usize,
890        feature_dim: usize,
891    ) -> Result<Vec<(GraphData, GraphData)>> {
892        let mut rng = scirs2_core::random::thread_rng();
893        let mut data_pairs = Vec::new();
894
895        for _ in 0..num_graphs {
896            // Generate input graph
897            let input_features = randn(&[num_nodes, feature_dim])?;
898            let mut edge_data = Vec::new();
899
900            // Create random edges
901            for _ in 0..(num_nodes * 2) {
902                let src = rng.gen_range(0..num_nodes) as f32;
903                let dst = rng.gen_range(0..num_nodes) as f32;
904                edge_data.push(src);
905                edge_data.push(dst);
906            }
907
908            let edge_index = from_vec(
909                edge_data,
910                &[2, num_nodes * 2],
911                torsh_core::device::DeviceType::Cpu,
912            )?;
913
914            let input_graph = GraphData::new(input_features, edge_index);
915
916            // Generate corresponding output (apply some transformation)
917            let output_features = input_graph.x.mul_scalar(2.0)?;
918            let output_graph = GraphData::new(output_features, input_graph.edge_index.clone());
919
920            data_pairs.push((input_graph, output_graph));
921        }
922
923        Ok(data_pairs)
924    }
925
926    /// Evaluate operator approximation error
927    pub fn compute_operator_error(predicted: &GraphData, target: &GraphData) -> Result<f32> {
928        let pred_data = predicted.x.to_vec()?;
929        let target_data = target.x.to_vec()?;
930
931        let mut mse = 0.0;
932        let mut count = 0;
933
934        for (pred, target) in pred_data.iter().zip(target_data.iter()) {
935            mse += (pred - target).powi(2);
936            count += 1;
937        }
938
939        if count > 0 {
940            Ok(mse / count as f32)
941        } else {
942            Ok(0.0)
943        }
944    }
945}
946
947#[cfg(test)]
948mod tests {
949    use super::*;
950    use torsh_core::device::DeviceType;
951
952    #[test]
953    fn test_graph_fno_creation() {
954        let fno = GraphFNO::new(4, 8, 16, 10, 3, true).expect("operation should succeed");
955        assert_eq!(fno.in_features, 4);
956        assert_eq!(fno.out_features, 8);
957        assert_eq!(fno.hidden_features, 16);
958        assert_eq!(fno.num_modes, 10);
959        assert_eq!(fno.num_layers, 3);
960    }
961
962    #[test]
963    fn test_graph_fno_forward() {
964        let features = randn(&[5, 4]).unwrap();
965        let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0];
966        let edge_index = from_vec(edges, &[2, 4], DeviceType::Cpu).unwrap();
967        let graph = GraphData::new(features, edge_index);
968
969        let fno = GraphFNO::new(4, 8, 16, 10, 3, true).expect("operation should succeed");
970        let output = fno.forward(&graph).expect("operation should succeed");
971
972        assert_eq!(output.x.shape().dims(), &[5, 8]);
973    }
974
975    #[test]
976    fn test_graph_deeponet_creation() {
977        let deeponet =
978            GraphDeepONet::new(3, 4, 16, 8, 10, 3, true).expect("operation should succeed");
979        assert_eq!(deeponet.trunk_net_features, 3);
980        assert_eq!(deeponet.branch_net_features, 4);
981        assert_eq!(deeponet.output_features, 8);
982        assert_eq!(deeponet.num_sensors, 10);
983    }
984
985    #[test]
986    fn test_physics_informed_gnn() {
987        let features = randn(&[4, 3]).unwrap();
988        let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0];
989        let edge_index = from_vec(edges, &[2, 3], DeviceType::Cpu).unwrap();
990        let graph = GraphData::new(features, edge_index);
991
992        let pignn = PhysicsInformedGNN::new(3, 6, 12, 2, 0.1, 0.05, true)
993            .expect("operation should succeed");
994        let output = pignn.forward(&graph).expect("operation should succeed");
995
996        assert_eq!(output.x.shape().dims(), &[4, 6]);
997    }
998
999    #[test]
1000    fn test_multi_scale_gno() {
1001        let features = randn(&[8, 4]).unwrap();
1002        let edges = vec![
1003            0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0, 4.0, 5.0, 5.0, 6.0, 6.0, 7.0,
1004        ];
1005        let edge_index = from_vec(edges, &[2, 7], DeviceType::Cpu).unwrap();
1006        let graph = GraphData::new(features, edge_index);
1007
1008        let ms_gno = MultiScaleGNO::new(4, 6, 3, 8, true).expect("operation should succeed");
1009        let output = ms_gno.forward(&graph).expect("operation should succeed");
1010
1011        assert_eq!(output.x.shape().dims(), &[8, 6]);
1012    }
1013
1014    #[test]
1015    fn test_spectral_features() {
1016        let features = randn(&[6, 3]).unwrap();
1017        let edges = vec![0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0, 4.0, 5.0];
1018        let edge_index = from_vec(edges, &[2, 5], DeviceType::Cpu).unwrap();
1019        let graph = GraphData::new(features, edge_index);
1020
1021        let spectral_features =
1022            utils::compute_spectral_features(&graph, 4).expect("operation should succeed");
1023        assert_eq!(spectral_features.shape().dims(), &[6, 4]);
1024    }
1025
1026    #[test]
1027    fn test_operator_data_generation() {
1028        let data_pairs = utils::generate_operator_data(3, 5, 4).expect("operation should succeed");
1029        assert_eq!(data_pairs.len(), 3);
1030
1031        for (input, output) in &data_pairs {
1032            assert_eq!(input.num_nodes, 5);
1033            assert_eq!(output.num_nodes, 5);
1034            assert_eq!(input.x.shape().dims()[1], 4);
1035            assert_eq!(output.x.shape().dims()[1], 4);
1036        }
1037    }
1038
1039    #[test]
1040    fn test_operator_error_computation() {
1041        let features1 = from_vec(vec![1.0, 2.0, 3.0, 4.0], &[2, 2], DeviceType::Cpu).unwrap();
1042        let features2 = from_vec(vec![1.1, 2.1, 3.1, 4.1], &[2, 2], DeviceType::Cpu).unwrap();
1043        let edges = vec![0.0, 1.0];
1044        let edge_index = from_vec(edges, &[2, 1], DeviceType::Cpu).unwrap();
1045
1046        let graph1 = GraphData::new(features1, edge_index.clone());
1047        let graph2 = GraphData::new(features2, edge_index);
1048
1049        let error =
1050            utils::compute_operator_error(&graph1, &graph2).expect("operation should succeed");
1051        assert!(error > 0.0);
1052        assert!(error < 1.0); // Should be small for similar graphs
1053    }
1054}