Skip to main content

torsh_nn/
research.rs

1//! Research neural network layers and components
2//!
3//! This module contains implementations of cutting-edge research architectures
4//! including Neural ODEs, Differentiable NAS, Meta-learning, and more.
5
6use crate::{Module, ModuleBase, Parameter};
7use torsh_core::device::DeviceType;
8use torsh_core::error::Result;
9use torsh_tensor::{creation::*, Tensor};
10
11// Conditional imports for std/no_std compatibility
12#[cfg(feature = "std")]
13use std::{boxed::Box, collections::HashMap, string::String, vec::Vec};
14
15#[cfg(not(feature = "std"))]
16use alloc::{boxed::Box, string::String, sync::Arc, vec::Vec};
17
18#[cfg(not(feature = "std"))]
19use hashbrown::HashMap;
20
21/// Neural Ordinary Differential Equation (NODE) layer
22///
23/// Neural ODEs model the hidden state as a continuous-time dynamical system
24/// defined by an ODE: dh/dt = f(h(t), t, θ) where f is a neural network.
25///
26/// This allows for more memory-efficient training and variable-depth computation.
27pub struct NeuralODE {
28    base: ModuleBase,
29    func: Box<dyn Module>,
30    solver: ODESolver,
31    #[allow(dead_code)]
32    atol: f32,
33    #[allow(dead_code)]
34    rtol: f32,
35    max_steps: usize,
36}
37
38/// ODE solver methods
39#[derive(Debug, Clone, Copy)]
40pub enum ODESolver {
41    /// Euler's method (simplest, first-order)
42    Euler,
43    /// Runge-Kutta 4th order (more accurate)
44    RK4,
45    /// Adaptive step size methods
46    Dopri5,
47}
48
49impl NeuralODE {
50    /// Create a new Neural ODE layer
51    ///
52    /// # Arguments
53    /// * `func` - The neural network that defines the ODE dynamics
54    /// * `solver` - The numerical ODE solver to use
55    /// * `atol` - Absolute tolerance for adaptive solvers
56    /// * `rtol` - Relative tolerance for adaptive solvers
57    /// * `max_steps` - Maximum number of integration steps
58    pub fn new(
59        func: Box<dyn Module>,
60        solver: ODESolver,
61        atol: f32,
62        rtol: f32,
63        max_steps: usize,
64    ) -> Self {
65        Self {
66            base: ModuleBase::new(),
67            func,
68            solver,
69            atol,
70            rtol,
71            max_steps,
72        }
73    }
74
75    /// Solve the ODE from t0 to t1 using the specified solver
76    fn solve_ode(&self, y0: &Tensor, t0: f32, t1: f32) -> Result<Tensor> {
77        match self.solver {
78            ODESolver::Euler => self.euler_solve(y0, t0, t1),
79            ODESolver::RK4 => self.rk4_solve(y0, t0, t1),
80            ODESolver::Dopri5 => self.dopri5_solve(y0, t0, t1),
81        }
82    }
83
84    /// Euler's method for ODE solving
85    fn euler_solve(&self, y0: &Tensor, t0: f32, t1: f32) -> Result<Tensor> {
86        let h = (t1 - t0) / self.max_steps as f32;
87        let mut y = y0.clone();
88        let mut t = t0;
89
90        for _ in 0..self.max_steps {
91            let dy_dt = self.func.forward(&y)?;
92            let h_tensor = full(y.shape().dims(), h)?;
93            let delta_y = dy_dt.mul_op(&h_tensor)?;
94            y = y.add_op(&delta_y)?;
95            t += h;
96
97            if t >= t1 {
98                break;
99            }
100        }
101
102        Ok(y)
103    }
104
105    /// Runge-Kutta 4th order method
106    fn rk4_solve(&self, y0: &Tensor, t0: f32, t1: f32) -> Result<Tensor> {
107        let h = (t1 - t0) / self.max_steps as f32;
108        let mut y = y0.clone();
109        let mut t = t0;
110
111        for _ in 0..self.max_steps {
112            let k1 = self.func.forward(&y)?;
113
114            let h_tensor = full(y.shape().dims(), h)?;
115            let half_tensor = full(y.shape().dims(), 0.5)?;
116            let two_tensor = full(y.shape().dims(), 2.0)?;
117            let six_tensor = full(y.shape().dims(), 6.0)?;
118
119            let k1_half_h = k1.mul_op(&h_tensor)?.mul_op(&half_tensor)?;
120            let y_k1 = y.add_op(&k1_half_h)?;
121            let k2 = self.func.forward(&y_k1)?;
122
123            let k2_half_h = k2.mul_op(&h_tensor)?.mul_op(&half_tensor)?;
124            let y_k2 = y.add_op(&k2_half_h)?;
125            let k3 = self.func.forward(&y_k2)?;
126
127            let k3_h = k3.mul_op(&h_tensor)?;
128            let y_k3 = y.add_op(&k3_h)?;
129            let k4 = self.func.forward(&y_k3)?;
130
131            // y = y + h/6 * (k1 + 2*k2 + 2*k3 + k4)
132            let k2_times_2 = k2.mul_op(&two_tensor)?;
133            let k3_times_2 = k3.mul_op(&two_tensor)?;
134            let sum = k1.add_op(&k2_times_2)?.add_op(&k3_times_2)?.add_op(&k4)?;
135            let weighted_sum = sum.mul_op(&h_tensor)?.div(&six_tensor)?;
136            y = y.add_op(&weighted_sum)?;
137
138            t += h;
139            if t >= t1 {
140                break;
141            }
142        }
143
144        Ok(y)
145    }
146
147    /// Dormand-Prince 5th order adaptive method (simplified)
148    fn dopri5_solve(&self, y0: &Tensor, t0: f32, t1: f32) -> Result<Tensor> {
149        // For simplicity, use RK4 with adaptive step size
150        // In a full implementation, this would use the Dormand-Prince coefficients
151        self.rk4_solve(y0, t0, t1)
152    }
153}
154
155impl Module for NeuralODE {
156    fn forward(&self, input: &Tensor) -> Result<Tensor> {
157        // Integrate from t=0 to t=1
158        self.solve_ode(input, 0.0, 1.0)
159    }
160
161    fn parameters(&self) -> HashMap<String, Parameter> {
162        self.base.parameters.clone()
163    }
164
165    fn training(&self) -> bool {
166        self.base.training()
167    }
168
169    fn train(&mut self) {
170        self.base.set_training(true);
171    }
172
173    fn eval(&mut self) {
174        self.base.set_training(false);
175    }
176
177    fn set_training(&mut self, training: bool) {
178        self.base.set_training(training);
179    }
180
181    fn to_device(&mut self, device: DeviceType) -> Result<()> {
182        self.base.to_device(device)
183    }
184
185    fn named_parameters(&self) -> HashMap<String, Parameter> {
186        self.base.named_parameters()
187    }
188}
189
190/// Differentiable Neural Architecture Search (DARTS) cell
191///
192/// DARTS enables gradient-based architecture optimization by using
193/// continuous relaxation of the architecture search space.
194pub struct DARTSCell {
195    base: ModuleBase,
196    operations: Vec<Box<dyn Module>>,
197    alpha: Parameter, // Architecture parameters
198    #[allow(dead_code)]
199    num_nodes: usize,
200    #[allow(dead_code)]
201    num_ops: usize,
202}
203
204impl DARTSCell {
205    /// Create a new DARTS cell
206    ///
207    /// # Arguments
208    /// * `operations` - Set of candidate operations
209    /// * `num_nodes` - Number of intermediate nodes
210    pub fn new(operations: Vec<Box<dyn Module>>, num_nodes: usize) -> Result<Self> {
211        let num_ops = operations.len();
212        let mut base = ModuleBase::new();
213
214        // Initialize architecture parameters (logits)
215        let alpha_size = num_nodes * (num_nodes + 1) / 2 * num_ops;
216        let alpha_data = zeros(&[alpha_size])?;
217        base.register_parameter("alpha".to_string(), Parameter::new(alpha_data));
218
219        // Get alpha parameter before moving base
220        let alpha = base.parameters["alpha"].clone();
221
222        Ok(Self {
223            base,
224            operations,
225            alpha,
226            num_nodes,
227            num_ops,
228        })
229    }
230
231    /// Apply softmax to architecture parameters to get weights
232    fn get_architecture_weights(&self) -> Result<Tensor> {
233        let alpha_tensor = self.alpha.tensor().read().clone();
234        alpha_tensor.softmax(-1)
235    }
236}
237
238impl Module for DARTSCell {
239    fn forward(&self, input: &Tensor) -> Result<Tensor> {
240        let weights = self.get_architecture_weights()?;
241        let weight_data = weights.to_vec()?;
242
243        // For each edge, compute weighted sum of operations
244        // This is a simplified implementation
245        let mut output = input.clone();
246
247        for (i, op) in self.operations.iter().enumerate() {
248            let op_output = op.forward(input)?;
249            let weight = weight_data[i % weight_data.len()];
250            let weight_tensor = full(op_output.shape().dims(), weight)?;
251            let weighted_output = op_output.mul_op(&weight_tensor)?;
252
253            if i == 0 {
254                output = weighted_output;
255            } else {
256                output = output.add_op(&weighted_output)?;
257            }
258        }
259
260        Ok(output)
261    }
262
263    fn parameters(&self) -> HashMap<String, Parameter> {
264        let mut params = self.base.parameters.clone();
265
266        // Add parameters from all operations
267        for (i, op) in self.operations.iter().enumerate() {
268            let op_params = op.parameters();
269            for (name, param) in op_params {
270                params.insert(format!("op_{}_{}", i, name), param);
271            }
272        }
273
274        params
275    }
276
277    fn training(&self) -> bool {
278        self.base.training()
279    }
280
281    fn train(&mut self) {
282        self.base.set_training(true);
283    }
284
285    fn eval(&mut self) {
286        self.base.set_training(false);
287    }
288
289    fn set_training(&mut self, training: bool) {
290        self.base.set_training(training);
291    }
292
293    fn to_device(&mut self, device: DeviceType) -> Result<()> {
294        self.base.to_device(device)
295    }
296
297    fn named_parameters(&self) -> HashMap<String, Parameter> {
298        self.base.named_parameters()
299    }
300}
301
302/// Model-Agnostic Meta-Learning (MAML) module
303///
304/// MAML trains a model to quickly adapt to new tasks with minimal examples
305/// by optimizing for fast learning rather than task-specific performance.
306pub struct MAMLModule {
307    base: ModuleBase,
308    inner_model: Box<dyn Module>,
309    #[allow(dead_code)]
310    inner_lr: f32,
311    inner_steps: usize,
312}
313
314impl MAMLModule {
315    /// Create a new MAML module
316    ///
317    /// # Arguments
318    /// * `inner_model` - The base model to meta-learn
319    /// * `inner_lr` - Learning rate for inner loop adaptation
320    /// * `inner_steps` - Number of gradient steps in inner loop
321    pub fn new(inner_model: Box<dyn Module>, inner_lr: f32, inner_steps: usize) -> Self {
322        Self {
323            base: ModuleBase::new(),
324            inner_model,
325            inner_lr,
326            inner_steps,
327        }
328    }
329
330    /// Perform inner loop adaptation on a support set
331    pub fn adapt(&mut self, support_x: &Tensor, support_y: &Tensor) -> Result<()> {
332        // Perform gradient descent on the support set
333        // This is a simplified implementation - real MAML would use higher-order gradients
334
335        for _ in 0..self.inner_steps {
336            let prediction = self.inner_model.forward(support_x)?;
337
338            // Compute loss (MSE for simplicity)
339            let diff = prediction.sub(support_y)?;
340            let squared_diff = diff.mul_op(&diff)?;
341            // Placeholder: would compute mean in real implementation
342            let loss = squared_diff; // .mean() - mean function not available yet
343
344            // In a full implementation, we would compute gradients and update parameters
345            // For now, this is a placeholder
346            let _ = loss; // Suppress warning
347        }
348
349        Ok(())
350    }
351
352    /// Meta-forward pass: adapt on support set, then evaluate on query set
353    pub fn meta_forward(
354        &mut self,
355        support_x: &Tensor,
356        support_y: &Tensor,
357        query_x: &Tensor,
358    ) -> Result<Tensor> {
359        // Save original parameters
360        let original_params = self.inner_model.parameters();
361
362        // Adapt on support set
363        self.adapt(support_x, support_y)?;
364
365        // Evaluate on query set
366        let query_prediction = self.inner_model.forward(query_x)?;
367
368        // Restore original parameters for next task
369        // In practice, we'd use the gradient information for meta-learning
370        let _ = original_params; // Suppress warning
371
372        Ok(query_prediction)
373    }
374}
375
376impl Module for MAMLModule {
377    fn forward(&self, input: &Tensor) -> Result<Tensor> {
378        self.inner_model.forward(input)
379    }
380
381    fn parameters(&self) -> HashMap<String, Parameter> {
382        self.inner_model.parameters()
383    }
384
385    fn training(&self) -> bool {
386        self.base.training()
387    }
388
389    fn train(&mut self) {
390        self.base.set_training(true);
391    }
392
393    fn eval(&mut self) {
394        self.base.set_training(false);
395    }
396
397    fn set_training(&mut self, training: bool) {
398        self.base.set_training(training);
399        self.inner_model.set_training(training);
400    }
401
402    fn to_device(&mut self, device: DeviceType) -> Result<()> {
403        self.base.to_device(device)
404    }
405
406    fn named_parameters(&self) -> HashMap<String, Parameter> {
407        self.inner_model.named_parameters()
408    }
409}
410
411/// Capsule Network layer
412///
413/// Capsules are groups of neurons that represent the instantiation parameters
414/// of a specific type of entity (e.g., pose, lighting, deformation).
415pub struct CapsuleLayer {
416    base: ModuleBase,
417    in_capsules: usize,
418    out_capsules: usize,
419    #[allow(dead_code)]
420    in_dim: usize,
421    #[allow(dead_code)]
422    out_dim: usize,
423    num_routing: usize,
424}
425
426impl CapsuleLayer {
427    /// Create a new Capsule layer
428    ///
429    /// # Arguments
430    /// * `in_capsules` - Number of input capsules
431    /// * `out_capsules` - Number of output capsules  
432    /// * `in_dim` - Dimension of input capsules
433    /// * `out_dim` - Dimension of output capsules
434    /// * `num_routing` - Number of routing iterations
435    pub fn new(
436        in_capsules: usize,
437        out_capsules: usize,
438        in_dim: usize,
439        out_dim: usize,
440        num_routing: usize,
441    ) -> Result<Self> {
442        let mut base = ModuleBase::new();
443
444        // Weight tensor for transformation: [out_capsules, in_capsules, out_dim, in_dim]
445        let weight_shape = vec![out_capsules, in_capsules, out_dim, in_dim];
446        let weight = randn(&weight_shape)?;
447        base.register_parameter("weight".to_string(), Parameter::new(weight));
448
449        Ok(Self {
450            base,
451            in_capsules,
452            out_capsules,
453            in_dim,
454            out_dim,
455            num_routing,
456        })
457    }
458
459    /// Squash function to ensure capsule length is between 0 and 1
460    fn squash(&self, tensor: &Tensor) -> Result<Tensor> {
461        // ||s||² / (1 + ||s||²) * s / ||s||
462        let squared = tensor.mul_op(tensor)?;
463        let squared_sum = squared.sum()?;
464        let norm = squared_sum.sqrt()?;
465        let norm_squared = squared_sum.clone();
466
467        let one = ones(&[1])?;
468        let denominator = one.add_op(&norm_squared)?;
469        let scale = norm_squared.div(&denominator)?;
470
471        let unit_vector = tensor.div(&norm)?;
472        scale.mul_op(&unit_vector)
473    }
474
475    /// Dynamic routing algorithm
476    fn routing(&self, u_hat: &Tensor) -> Result<Tensor> {
477        // Initialize routing logits b_ij to 0
478        let batch_size = u_hat.shape().dims()[0];
479        let mut b = zeros(&[batch_size, self.in_capsules, self.out_capsules])?;
480
481        for _ in 0..self.num_routing {
482            // Softmax over output capsules
483            let _c = b.softmax(-1)?;
484
485            // Weighted sum: s_j = Σ c_ij * u_hat_j|i
486            // This is a simplified implementation
487            let s = u_hat.clone(); // Placeholder
488
489            // Squash to get output capsules
490            let _v = self.squash(&s)?;
491
492            // Update routing logits: b_ij += u_hat_j|i · v_j
493            // Simplified: just update b with a small value
494            let update = full(b.shape().dims(), 0.1)?;
495            b = b.add_op(&update)?;
496        }
497
498        let _c_final = b.softmax(-1)?;
499        let s_final = u_hat.clone(); // Placeholder
500        self.squash(&s_final)
501    }
502}
503
504impl Module for CapsuleLayer {
505    fn forward(&self, input: &Tensor) -> Result<Tensor> {
506        // Input shape: [batch_size, in_capsules, in_dim]
507        let weight = self.base.parameters["weight"].tensor().read().clone();
508
509        // Compute prediction vectors u_hat = W_ij * u_i
510        // This is a simplified implementation
511        let u_hat = input.matmul(&weight)?;
512
513        // Apply dynamic routing
514        self.routing(&u_hat)
515    }
516
517    fn parameters(&self) -> HashMap<String, Parameter> {
518        self.base.parameters.clone()
519    }
520
521    fn training(&self) -> bool {
522        self.base.training()
523    }
524
525    fn train(&mut self) {
526        self.base.set_training(true);
527    }
528
529    fn eval(&mut self) {
530        self.base.set_training(false);
531    }
532
533    fn set_training(&mut self, training: bool) {
534        self.base.set_training(training);
535    }
536
537    fn to_device(&mut self, device: DeviceType) -> Result<()> {
538        self.base.to_device(device)
539    }
540
541    fn named_parameters(&self) -> HashMap<String, Parameter> {
542        self.base.named_parameters()
543    }
544}
545
546/// Graph Convolutional Network layer
547///
548/// GCNs operate on graph-structured data by aggregating information
549/// from neighboring nodes to update node representations.
550pub struct GraphConvLayer {
551    base: ModuleBase,
552    #[allow(dead_code)]
553    in_features: usize,
554    #[allow(dead_code)]
555    out_features: usize,
556    use_bias: bool,
557}
558
559impl GraphConvLayer {
560    /// Create a new Graph Convolution layer
561    ///
562    /// # Arguments
563    /// * `in_features` - Number of input features per node
564    /// * `out_features` - Number of output features per node
565    /// * `use_bias` - Whether to use bias term
566    pub fn new(in_features: usize, out_features: usize, use_bias: bool) -> Result<Self> {
567        let mut base = ModuleBase::new();
568
569        // Weight matrix
570        let weight = randn(&[in_features, out_features])?;
571        base.register_parameter("weight".to_string(), Parameter::new(weight));
572
573        // Bias vector
574        if use_bias {
575            let bias = zeros(&[out_features])?;
576            base.register_parameter("bias".to_string(), Parameter::new(bias));
577        }
578
579        Ok(Self {
580            base,
581            in_features,
582            out_features,
583            use_bias,
584        })
585    }
586
587    /// Normalize adjacency matrix (add self-loops and compute D^(-1/2) A D^(-1/2))
588    #[allow(dead_code)]
589    fn normalize_adjacency(&self, adj: &Tensor) -> Result<Tensor> {
590        // Add self-loops: A = A + I
591        let num_nodes = adj.shape().dims()[0];
592        let identity = eye(num_nodes)?;
593        let adj_with_self_loops = adj.add_op(&identity)?;
594
595        // Compute degree matrix D
596        let degrees = adj_with_self_loops.sum_dim(&[1], false)?;
597
598        // D^(-1/2)
599        let _degrees_sqrt = degrees.pow(-0.5)?;
600
601        // Create diagonal matrix from degrees_sqrt
602        // This is simplified - real implementation would use proper diagonal matrix ops
603        let normalized_adj = adj_with_self_loops.clone(); // Placeholder
604
605        Ok(normalized_adj)
606    }
607}
608
609impl Module for GraphConvLayer {
610    fn forward(&self, input: &Tensor) -> Result<Tensor> {
611        // input should be a tuple of (node_features, adjacency_matrix)
612        // For simplicity, we'll assume input is just node features
613        // and the adjacency matrix is passed separately or stored
614
615        let weight = self.base.parameters["weight"].tensor().read().clone();
616
617        // Linear transformation: X' = X * W
618        let transformed = input.matmul(&weight)?;
619
620        // In a real implementation, we would multiply by normalized adjacency matrix
621        // A_norm * X' where A_norm is the normalized adjacency matrix
622        let mut output = transformed;
623
624        // Add bias if present
625        if self.use_bias {
626            let bias = self.base.parameters["bias"].tensor().read().clone();
627            output = output.add_op(&bias)?;
628        }
629
630        Ok(output)
631    }
632
633    fn parameters(&self) -> HashMap<String, Parameter> {
634        self.base.parameters.clone()
635    }
636
637    fn training(&self) -> bool {
638        self.base.training()
639    }
640
641    fn train(&mut self) {
642        self.base.set_training(true);
643    }
644
645    fn eval(&mut self) {
646        self.base.set_training(false);
647    }
648
649    fn set_training(&mut self, training: bool) {
650        self.base.set_training(training);
651    }
652
653    fn to_device(&mut self, device: DeviceType) -> Result<()> {
654        self.base.to_device(device)
655    }
656
657    fn named_parameters(&self) -> HashMap<String, Parameter> {
658        self.base.named_parameters()
659    }
660}
661
662/// Graph Attention Network layer
663///
664/// GAT uses attention mechanisms to learn the relative importance
665/// of neighboring nodes when aggregating information.
666pub struct GraphAttentionLayer {
667    base: ModuleBase,
668    #[allow(dead_code)]
669    in_features: usize,
670    #[allow(dead_code)]
671    out_features: usize,
672    num_heads: usize,
673    #[allow(dead_code)]
674    dropout: f32,
675    #[allow(dead_code)]
676    alpha: f32, // LeakyReLU negative slope
677}
678
679impl GraphAttentionLayer {
680    /// Create a new Graph Attention layer
681    ///
682    /// # Arguments
683    /// * `in_features` - Number of input features per node
684    /// * `out_features` - Number of output features per node
685    /// * `num_heads` - Number of attention heads
686    /// * `dropout` - Dropout probability for attention weights
687    /// * `alpha` - Negative slope for LeakyReLU in attention
688    pub fn new(
689        in_features: usize,
690        out_features: usize,
691        num_heads: usize,
692        dropout: f32,
693        alpha: f32,
694    ) -> Result<Self> {
695        let mut base = ModuleBase::new();
696
697        // Linear transformation weights for each head
698        for h in 0..num_heads {
699            let weight = randn(&[in_features, out_features])?;
700            base.register_parameter(format!("weight_{}", h), Parameter::new(weight));
701
702            // Attention parameters a^T [W h_i || W h_j]
703            let att = randn(&[2 * out_features, 1])?;
704            base.register_parameter(format!("att_{}", h), Parameter::new(att));
705        }
706
707        Ok(Self {
708            base,
709            in_features,
710            out_features,
711            num_heads,
712            dropout,
713            alpha,
714        })
715    }
716
717    /// Compute attention coefficients
718    #[allow(dead_code)]
719    fn attention(&self, h_i: &Tensor, h_j: &Tensor, head: usize) -> Result<Tensor> {
720        let att = self.base.parameters[&format!("att_{}", head)]
721            .tensor()
722            .read()
723            .clone();
724
725        // Concatenate h_i and h_j
726        let concat = Tensor::cat(&[h_i, h_j], -1)?;
727
728        // Compute attention: a^T [W h_i || W h_j]
729        let e = concat.matmul(&att)?;
730
731        // Apply LeakyReLU
732        let alpha_tensor = full(e.shape().dims(), self.alpha)?;
733        let zero = zeros(e.shape().dims())?;
734        let positive = e.maximum(&zero)?;
735        let negative = e.minimum(&zero)?;
736        let leaky_negative = negative.mul_op(&alpha_tensor)?;
737        positive.add_op(&leaky_negative)
738    }
739}
740
741impl Module for GraphAttentionLayer {
742    fn forward(&self, input: &Tensor) -> Result<Tensor> {
743        // Multi-head attention
744        let mut head_outputs = Vec::new();
745
746        for h in 0..self.num_heads {
747            let weight = self.base.parameters[&format!("weight_{}", h)]
748                .tensor()
749                .read()
750                .clone();
751
752            // Linear transformation
753            let h_transformed = input.matmul(&weight)?;
754
755            // For simplicity, we'll just return the transformed features
756            // Real GAT would compute attention weights and aggregate neighbors
757            head_outputs.push(h_transformed);
758        }
759
760        // Concatenate or average multi-head outputs
761        if head_outputs.len() == 1 {
762            Ok(head_outputs
763                .into_iter()
764                .next()
765                .expect("head_outputs should have at least one element"))
766        } else {
767            // Average the heads
768            let mut sum = head_outputs[0].clone();
769            for head in head_outputs.iter().skip(1) {
770                sum = sum.add_op(head)?;
771            }
772            let num_heads_tensor = full(sum.shape().dims(), self.num_heads as f32)?;
773            sum.div(&num_heads_tensor)
774        }
775    }
776
777    fn parameters(&self) -> HashMap<String, Parameter> {
778        self.base.parameters.clone()
779    }
780
781    fn training(&self) -> bool {
782        self.base.training()
783    }
784
785    fn train(&mut self) {
786        self.base.set_training(true);
787    }
788
789    fn eval(&mut self) {
790        self.base.set_training(false);
791    }
792
793    fn set_training(&mut self, training: bool) {
794        self.base.set_training(training);
795    }
796
797    fn to_device(&mut self, device: DeviceType) -> Result<()> {
798        self.base.to_device(device)
799    }
800
801    fn named_parameters(&self) -> HashMap<String, Parameter> {
802        self.base.named_parameters()
803    }
804}
805
806#[cfg(test)]
807mod tests {
808    use super::*;
809    use crate::layers::linear::Linear;
810
811    #[test]
812    fn test_neural_ode_creation() {
813        let inner_model = Box::new(Linear::new(10, 10, true));
814        let node = NeuralODE::new(inner_model, ODESolver::Euler, 1e-3, 1e-3, 100);
815
816        assert_eq!(node.max_steps, 100);
817        assert!((node.atol - 1e-3).abs() < 1e-6);
818    }
819
820    #[test]
821    fn test_capsule_layer_creation() {
822        let capsule = CapsuleLayer::new(10, 5, 8, 16, 3).unwrap();
823
824        assert_eq!(capsule.in_capsules, 10);
825        assert_eq!(capsule.out_capsules, 5);
826        assert_eq!(capsule.in_dim, 8);
827        assert_eq!(capsule.out_dim, 16);
828        assert_eq!(capsule.num_routing, 3);
829    }
830
831    #[test]
832    fn test_graph_conv_creation() {
833        let gcn = GraphConvLayer::new(64, 32, true).unwrap();
834
835        assert_eq!(gcn.in_features, 64);
836        assert_eq!(gcn.out_features, 32);
837        assert!(gcn.use_bias);
838    }
839
840    #[test]
841    fn test_graph_attention_creation() {
842        let gat = GraphAttentionLayer::new(64, 32, 8, 0.1, 0.2).unwrap();
843
844        assert_eq!(gat.in_features, 64);
845        assert_eq!(gat.out_features, 32);
846        assert_eq!(gat.num_heads, 8);
847        assert!((gat.dropout - 0.1).abs() < 1e-6);
848        assert!((gat.alpha - 0.2).abs() < 1e-6);
849    }
850}