//! Neural network architecture for source localization PINN. //! //! Implements a physics-informed network with Fourier feature encoding //! for learning neural source distributions from MEG/EEG data. use crate::error::{PinnError, PinnResult}; use ndarray::{Array1, Array2}; use serde::{Deserialize, Serialize}; /// Fourier feature encoding for positional inputs /// /// Maps 3D coordinates to higher-dimensional space using: /// γ(x) = [sin(2πBx), cos(2πBx)] /// /// where B is a matrix of learnable or random frequencies. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FourierFeatures { /// Frequency matrix [n_features, 3] frequencies: Array2, /// Scale factor for frequencies scale: f64, } impl FourierFeatures { /// Create new Fourier features with random frequencies pub fn new(n_features: usize, scale: f64) -> Self { // Initialize with random frequencies (normally distributed) let mut frequencies = Array2::zeros((n_features, 3)); for i in 0..n_features { for j in 0..3 { // Simple pseudo-random initialization let seed = (i * 3 + j) as f64; frequencies[[i, j]] = (seed * 0.618033988749895).fract() * 2.0 - 1.0; } } Self { frequencies: frequencies * scale, scale, } } /// Create with specific frequencies pub fn with_frequencies(frequencies: Array2, scale: f64) -> Self { Self { frequencies, scale } } /// Number of output features (2x input for sin and cos) pub fn output_dim(&self) -> usize { self.frequencies.nrows() * 2 } /// Encode 3D coordinates pub fn encode(&self, coords: &Array2) -> Array2 { let n_points = coords.nrows(); let n_freqs = self.frequencies.nrows(); let mut output = Array2::zeros((n_points, n_freqs * 2)); for p in 0..n_points { for f in 0..n_freqs { // Compute dot product: B[f] · x[p] let mut dot = 0.0; for d in 0..3 { dot += self.frequencies[[f, d]] * coords[[p, d]]; } let angle = 2.0 * std::f64::consts::PI * dot; output[[p, f]] = angle.sin(); output[[p, f + n_freqs]] = angle.cos(); } } output } /// Encode a single point pub fn encode_point(&self, point: &[f64; 3]) -> Array1 { let n_freqs = self.frequencies.nrows(); let mut output = Array1::zeros(n_freqs * 2); for f in 0..n_freqs { let mut dot = 0.0; for d in 0..3 { dot += self.frequencies[[f, d]] * point[d]; } let angle = 2.0 * std::f64::consts::PI * dot; output[f] = angle.sin(); output[f + n_freqs] = angle.cos(); } output } } /// Activation function types #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] pub enum Activation { /// Hyperbolic tangent Tanh, /// Rectified Linear Unit ReLU, /// Sigmoid Sigmoid, /// Swish (x * sigmoid(x)) Swish, /// GELU (Gaussian Error Linear Unit) GELU, } impl Activation { /// Apply activation function pub fn apply(&self, x: f64) -> f64 { match self { Activation::Tanh => x.tanh(), Activation::ReLU => x.max(0.0), Activation::Sigmoid => 1.0 / (1.0 + (-x).exp()), Activation::Swish => x / (1.0 + (-x).exp()), Activation::GELU => { let sqrt2 = std::f64::consts::SQRT_2; 0.5 * x * (1.0 + libm::erf(x / sqrt2)) } } } /// Apply derivative for backpropagation pub fn derivative(&self, x: f64) -> f64 { match self { Activation::Tanh => { let t = x.tanh(); 1.0 - t * t } Activation::ReLU => { if x > 0.0 { 1.0 } else { 0.0 } } Activation::Sigmoid => { let s = 1.0 / (1.0 + (-x).exp()); s * (1.0 - s) } Activation::Swish => { let s = 1.0 / (1.0 + (-x).exp()); s + x * s * (1.0 - s) } Activation::GELU => { let sqrt2 = std::f64::consts::SQRT_2; let sqrt2pi = (2.0 * std::f64::consts::PI).sqrt(); let erf_term = libm::erf(x / sqrt2); let exp_term = (-0.5 * x * x).exp(); 0.5 * (1.0 + erf_term) + x * exp_term / sqrt2pi } } } } /// Dense (fully connected) layer #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DenseLayer { /// Weight matrix [in_features, out_features] weights: Array2, /// Bias vector [out_features] biases: Array1, /// Activation function activation: Option, } impl DenseLayer { /// Create a new dense layer with Xavier initialization pub fn new(in_features: usize, out_features: usize, activation: Option) -> Self { // Xavier/Glorot initialization let scale = (2.0 / (in_features + out_features) as f64).sqrt(); let mut weights = Array2::zeros((in_features, out_features)); // Simple deterministic initialization for reproducibility for i in 0..in_features { for j in 0..out_features { let seed = (i * out_features + j) as f64; weights[[i, j]] = ((seed * 0.618033988749895).fract() * 2.0 - 1.0) * scale; } } let biases = Array1::zeros(out_features); Self { weights, biases, activation, } } /// Forward pass pub fn forward(&self, input: &Array2) -> Array2 { let linear = input.dot(&self.weights) + &self.biases; if let Some(act) = self.activation { linear.mapv(|x| act.apply(x)) } else { linear } } /// Forward pass for single input pub fn forward_single(&self, input: &Array1) -> Array1 { let linear = input.dot(&self.weights) + &self.biases; if let Some(act) = self.activation { linear.mapv(|x| act.apply(x)) } else { linear } } /// Get weights (for updates) pub fn weights(&self) -> &Array2 { &self.weights } /// Get mutable weights pub fn weights_mut(&mut self) -> &mut Array2 { &mut self.weights } /// Get biases pub fn biases(&self) -> &Array1 { &self.biases } /// Get mutable biases pub fn biases_mut(&mut self) -> &mut Array1 { &mut self.biases } } /// Source network configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SourceNetworkConfig { /// Hidden layer sizes pub hidden_layers: Vec, /// Number of Fourier features pub n_fourier_features: usize, /// Fourier feature scale pub fourier_scale: f64, /// Activation function pub activation: Activation, /// Output current density components (typically 3 for Jx, Jy, Jz) pub output_dim: usize, /// Whether to also output conductivity pub output_conductivity: bool, } impl Default for SourceNetworkConfig { fn default() -> Self { Self { hidden_layers: vec![64, 128, 128, 64], n_fourier_features: 32, fourier_scale: 10.0, activation: Activation::Tanh, output_dim: 3, output_conductivity: false, } } } /// Neural network for source localization /// /// Takes 3D coordinates as input and outputs: /// - Current density components (Jx, Jy, Jz) /// - Optionally, tissue conductivity σ(x) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SourceNetwork { /// Configuration config: SourceNetworkConfig, /// Fourier feature encoder fourier: FourierFeatures, /// Hidden layers layers: Vec, /// Current density output layer current_output: DenseLayer, /// Conductivity output layer (optional) conductivity_output: Option, } impl SourceNetwork { /// Create a new source network pub fn new(config: SourceNetworkConfig) -> Self { let fourier = FourierFeatures::new(config.n_fourier_features, config.fourier_scale); // Build hidden layers let input_dim = fourier.output_dim(); let mut layers = Vec::new(); let mut prev_dim = input_dim; for &hidden_dim in &config.hidden_layers { layers.push(DenseLayer::new( prev_dim, hidden_dim, Some(config.activation), )); prev_dim = hidden_dim; } // Output layer for current density (no activation - unbounded output) let current_output = DenseLayer::new(prev_dim, config.output_dim, None); // Optional conductivity output (sigmoid for bounded positive output) let conductivity_output = if config.output_conductivity { Some(DenseLayer::new(prev_dim, 1, Some(Activation::Sigmoid))) } else { None }; Self { config, fourier, layers, current_output, conductivity_output, } } /// Forward pass for multiple points /// /// # Arguments /// * `coords` - Input coordinates [n_points, 3] /// /// # Returns /// * Current density [n_points, 3] and optionally conductivity [n_points] pub fn forward(&self, coords: &Array2) -> (Array2, Option>) { // Fourier encoding let mut x = self.fourier.encode(coords); // Hidden layers for layer in &self.layers { x = layer.forward(&x); } // Current density output let current = self.current_output.forward(&x); // Conductivity output (if enabled) let conductivity = self.conductivity_output.as_ref().map(|layer| { let sigma = layer.forward(&x); // Convert to 1D array and scale to reasonable conductivity range (0.01 - 2.0 S/m) sigma.column(0).mapv(|s| 0.01 + s * 1.99).to_owned() }); (current, conductivity) } /// Forward pass for single point pub fn forward_single(&self, point: &[f64; 3]) -> (Array1, Option) { // Fourier encoding let mut x = self.fourier.encode_point(point); // Hidden layers for layer in &self.layers { x = layer.forward_single(&x); } // Current density output let current = self.current_output.forward_single(&x); // Conductivity output let conductivity = self.conductivity_output.as_ref().map(|layer| { let sigma = layer.forward_single(&x); 0.01 + sigma[0] * 1.99 }); (current, conductivity) } /// Get network configuration pub fn config(&self) -> &SourceNetworkConfig { &self.config } /// Total number of parameters pub fn n_parameters(&self) -> usize { let mut count = 0; for layer in &self.layers { count += layer.weights().len(); count += layer.biases().len(); } count += self.current_output.weights().len(); count += self.current_output.biases().len(); if let Some(ref layer) = self.conductivity_output { count += layer.weights().len(); count += layer.biases().len(); } count } /// Get all layer weights as flat vector (for optimization) pub fn get_parameters(&self) -> Vec { let mut params = Vec::with_capacity(self.n_parameters()); for layer in &self.layers { params.extend(layer.weights().iter()); params.extend(layer.biases().iter()); } params.extend(self.current_output.weights().iter()); params.extend(self.current_output.biases().iter()); if let Some(ref layer) = self.conductivity_output { params.extend(layer.weights().iter()); params.extend(layer.biases().iter()); } params } /// Set all layer weights from flat vector pub fn set_parameters(&mut self, params: &[f64]) -> PinnResult<()> { if params.len() != self.n_parameters() { return Err(PinnError::DimensionMismatch(format!( "Expected {} parameters, got {}", self.n_parameters(), params.len() ))); } let mut idx = 0; for layer in &mut self.layers { let w_len = layer.weights().len(); let b_len = layer.biases().len(); for (i, w) in layer.weights_mut().iter_mut().enumerate() { *w = params[idx + i]; } idx += w_len; for (i, b) in layer.biases_mut().iter_mut().enumerate() { *b = params[idx + i]; } idx += b_len; } // Current output layer let w_len = self.current_output.weights().len(); let b_len = self.current_output.biases().len(); for (i, w) in self.current_output.weights_mut().iter_mut().enumerate() { *w = params[idx + i]; } idx += w_len; for (i, b) in self.current_output.biases_mut().iter_mut().enumerate() { *b = params[idx + i]; } idx += b_len; // Conductivity layer (if present) if let Some(ref mut layer) = self.conductivity_output { let w_len = layer.weights().len(); for (i, w) in layer.weights_mut().iter_mut().enumerate() { *w = params[idx + i]; } idx += w_len; for b in layer.biases_mut().iter_mut() { *b = params[idx]; idx += 1; } } Ok(()) } } /// Sensor network for mapping sensor data to latent representation #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SensorEncoder { /// Encoder layers layers: Vec, /// Output dimension latent_dim: usize, } impl SensorEncoder { /// Create a new sensor encoder pub fn new(n_sensors: usize, hidden_dims: &[usize], latent_dim: usize) -> Self { let mut layers = Vec::new(); let mut prev_dim = n_sensors; for &dim in hidden_dims { layers.push(DenseLayer::new(prev_dim, dim, Some(Activation::Tanh))); prev_dim = dim; } // Final layer to latent space layers.push(DenseLayer::new(prev_dim, latent_dim, None)); Self { layers, latent_dim } } /// Encode sensor measurements pub fn encode(&self, measurements: &Array1) -> Array1 { let mut x = measurements.clone(); for layer in &self.layers { x = layer.forward_single(&x); } x } /// Encode batch of measurements pub fn encode_batch(&self, measurements: &Array2) -> Array2 { let mut x = measurements.clone(); for layer in &self.layers { x = layer.forward(&x); } x } /// Get latent dimension pub fn latent_dim(&self) -> usize { self.latent_dim } } #[cfg(test)] mod tests { use super::*; use ndarray::array; #[test] fn test_fourier_features() { let ff = FourierFeatures::new(16, 10.0); assert_eq!(ff.output_dim(), 32); let coords = Array2::from_shape_vec((2, 3), vec![0.0, 0.0, 0.05, 0.01, 0.0, 0.05]).unwrap(); let encoded = ff.encode(&coords); assert_eq!(encoded.shape(), &[2, 32]); } #[test] fn test_activation_functions() { assert!((Activation::Tanh.apply(0.0)).abs() < 1e-10); assert!((Activation::ReLU.apply(-1.0)).abs() < 1e-10); assert!((Activation::Sigmoid.apply(0.0) - 0.5).abs() < 1e-10); } #[test] fn test_dense_layer() { let layer = DenseLayer::new(4, 8, Some(Activation::Tanh)); let input = Array2::zeros((3, 4)); let output = layer.forward(&input); assert_eq!(output.shape(), &[3, 8]); } #[test] fn test_source_network() { let config = SourceNetworkConfig { hidden_layers: vec![32, 64, 32], n_fourier_features: 16, fourier_scale: 10.0, activation: Activation::Tanh, output_dim: 3, output_conductivity: true, }; let network = SourceNetwork::new(config); let coords = Array2::from_shape_vec( (5, 3), vec![ 0.0, 0.0, 0.05, 0.01, 0.0, 0.05, -0.01, 0.0, 0.05, 0.0, 0.01, 0.05, 0.0, -0.01, 0.05, ], ) .unwrap(); let (current, conductivity) = network.forward(&coords); assert_eq!(current.shape(), &[5, 3]); assert!(conductivity.is_some()); assert_eq!(conductivity.unwrap().len(), 5); } #[test] fn test_parameter_io() { let config = SourceNetworkConfig::default(); let mut network = SourceNetwork::new(config); let params = network.get_parameters(); let n_params = network.n_parameters(); assert_eq!(params.len(), n_params); // Modify and restore let mut new_params = params.clone(); new_params[0] = 999.0; network.set_parameters(&new_params).unwrap(); let retrieved = network.get_parameters(); assert!((retrieved[0] - 999.0).abs() < 1e-10); } #[test] fn test_sensor_encoder() { let encoder = SensorEncoder::new(64, &[32, 16], 8); let measurements = Array1::zeros(64); let latent = encoder.encode(&measurements); assert_eq!(latent.len(), 8); } }