Skip to main content

optirs_learned/transformer_based_optimizer/
layers.rs

1// Core transformer layer implementations
2
3use super::config::ActivationFunction;
4use crate::error::{OptimError, Result};
5use scirs2_core::ndarray::{Array1, Array2};
6use scirs2_core::numeric::Float;
7use std::fmt::Debug;
8
9/// Draw a Xavier/Glorot uniform matrix with limit `sqrt(6 / (fan_in + fan_out))`.
10fn xavier_uniform<
11    T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
12>(
13    fan_in: usize,
14    fan_out: usize,
15) -> Array2<T> {
16    let mut rng = scirs2_core::random::thread_rng();
17    let bound = (6.0 / (fan_in + fan_out).max(1) as f64).sqrt();
18    let mut weights = Array2::zeros((fan_in, fan_out));
19    for elem in weights.iter_mut() {
20        *elem = scirs2_core::numeric::NumCast::from((rng.random::<f64>() - 0.5) * 2.0 * bound)
21            .unwrap_or_else(|| T::zero());
22    }
23    weights
24}
25
26/// Input projection layer.
27///
28/// Optimization traces are continuous vectors, not token ids, so this is a
29/// dense linear projection `input @ W` rather than a vocabulary lookup. The
30/// previous implementation cast each float to a `usize` index, which mapped
31/// every value in `(-1, 1)` onto row 0 and discarded the input entirely.
32pub struct EmbeddingLayer<
33    T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
34> {
35    /// Projection matrix, shape `(input_dimension, output_dimension)`
36    embedding_matrix: Array2<T>,
37
38    /// Input dimension
39    input_dimension: usize,
40
41    /// Output dimension (model dimension)
42    output_dimension: usize,
43}
44
45impl<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>
46    EmbeddingLayer<T>
47{
48    /// Create a new input projection with Xavier-initialized weights.
49    pub fn new(input_dimension: usize, output_dimension: usize) -> Result<Self> {
50        if input_dimension == 0 || output_dimension == 0 {
51            return Err(OptimError::InvalidConfig(
52                "Embedding dimensions must be positive".to_string(),
53            ));
54        }
55
56        Ok(Self {
57            embedding_matrix: xavier_uniform(input_dimension, output_dimension),
58            input_dimension,
59            output_dimension,
60        })
61    }
62
63    /// Project a `(sequence_length, input_dimension)` matrix to the model dimension.
64    pub fn forward(&self, input: &Array2<T>) -> Result<Array2<T>> {
65        let (_sequence_length, width) = input.dim();
66        if width != self.input_dimension {
67            return Err(OptimError::InvalidConfig(format!(
68                "Embedding input width {width} does not match the expected {}",
69                self.input_dimension
70            )));
71        }
72
73        Ok(input.dot(&self.embedding_matrix))
74    }
75
76    /// Get parameter count
77    pub fn parameter_count(&self) -> usize {
78        self.input_dimension * self.output_dimension
79    }
80
81    /// Projection weights
82    pub fn weights(&self) -> &Array2<T> {
83        &self.embedding_matrix
84    }
85
86    /// Replace the projection weights
87    pub fn set_weights(&mut self, weights: Array2<T>) -> Result<()> {
88        if weights.dim() != (self.input_dimension, self.output_dimension) {
89            return Err(OptimError::InvalidConfig(format!(
90                "Weight shape {:?} does not match ({}, {})",
91                weights.dim(),
92                self.input_dimension,
93                self.output_dimension
94            )));
95        }
96        self.embedding_matrix = weights;
97        Ok(())
98    }
99
100    /// Gradient of the projection with respect to its weights, and an SGD step.
101    ///
102    /// For `y = x W`, `dL/dW = x^T (dL/dy)`.
103    pub fn backward(
104        &mut self,
105        input: &Array2<T>,
106        grad_output: &Array2<T>,
107        learning_rate: T,
108    ) -> Result<Array2<T>> {
109        if input.ncols() != self.input_dimension || grad_output.ncols() != self.output_dimension {
110            return Err(OptimError::InvalidConfig(
111                "Embedding backward received mismatched shapes".to_string(),
112            ));
113        }
114        if input.nrows() != grad_output.nrows() {
115            return Err(OptimError::InvalidConfig(
116                "Embedding backward received mismatched row counts".to_string(),
117            ));
118        }
119
120        let grad_weights = input.t().dot(grad_output);
121        self.embedding_matrix = &self.embedding_matrix - &(grad_weights.clone() * learning_rate);
122        Ok(grad_weights)
123    }
124
125    /// Re-randomize the projection weights.
126    pub fn reset(&mut self) -> Result<()> {
127        self.embedding_matrix = xavier_uniform(self.input_dimension, self.output_dimension);
128        Ok(())
129    }
130}
131
132/// Layer normalization implementation
133pub struct LayerNormalization<
134    T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
135> {
136    /// Layer dimension
137    dimension: usize,
138
139    /// Learnable scale parameters
140    gamma: Array1<T>,
141
142    /// Learnable shift parameters
143    beta: Array1<T>,
144
145    /// Small constant for numerical stability
146    epsilon: T,
147}
148
149impl<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>
150    LayerNormalization<T>
151{
152    /// Create new layer normalization
153    pub fn new(dimension: usize) -> Result<Self> {
154        let gamma = Array1::ones(dimension);
155        let beta = Array1::zeros(dimension);
156        let epsilon = scirs2_core::numeric::NumCast::from(1e-5).unwrap_or_else(|| T::zero());
157
158        Ok(Self {
159            dimension,
160            gamma,
161            beta,
162            epsilon,
163        })
164    }
165
166    /// Forward pass through layer normalization.
167    ///
168    /// Rejects inputs whose width differs from the configured dimension instead
169    /// of indexing out of bounds.
170    pub fn forward(&self, input: &Array2<T>) -> Result<Array2<T>> {
171        let (rows, width) = input.dim();
172        if width != self.dimension {
173            return Err(OptimError::InvalidConfig(format!(
174                "Layer norm input width {width} does not match its dimension {}",
175                self.dimension
176            )));
177        }
178        if width == 0 {
179            return Ok(input.clone());
180        }
181
182        let mut output = input.clone();
183        let width_t: T =
184            scirs2_core::numeric::NumCast::from(width as f64).unwrap_or_else(|| T::one());
185
186        for i in 0..rows {
187            let row = input.row(i);
188
189            let mean = row.sum() / width_t;
190            let variance = row
191                .iter()
192                .map(|&x| {
193                    let diff = x - mean;
194                    diff * diff
195                })
196                .fold(T::zero(), |acc, x| acc + x)
197                / width_t;
198
199            let std_dev = (variance + self.epsilon).sqrt();
200
201            for j in 0..self.dimension {
202                let normalized = (input[[i, j]] - mean) / std_dev;
203                output[[i, j]] = self.gamma[j] * normalized + self.beta[j];
204            }
205        }
206
207        Ok(output)
208    }
209
210    /// Normalized dimension
211    pub fn dimension(&self) -> usize {
212        self.dimension
213    }
214
215    /// Backward pass through layer normalization.
216    ///
217    /// With `xhat = (x - mu) / sqrt(var + eps)` and `y = gamma * xhat + beta`,
218    /// the exact input gradient for one row is
219    /// `dL/dx = (1/sigma) * (g' - mean(g') - xhat * mean(g' * xhat))`
220    /// where `g' = dL/dy * gamma`. `gamma` and `beta` are updated in place with
221    /// a plain SGD step and the input gradient is returned.
222    pub fn backward(
223        &mut self,
224        input: &Array2<T>,
225        grad_output: &Array2<T>,
226        learning_rate: T,
227    ) -> Result<Array2<T>> {
228        if input.dim() != grad_output.dim() {
229            return Err(OptimError::InvalidConfig(format!(
230                "Layer norm backward shape mismatch: {:?} vs {:?}",
231                input.dim(),
232                grad_output.dim()
233            )));
234        }
235        let (rows, width) = input.dim();
236        if width != self.dimension {
237            return Err(OptimError::InvalidConfig(format!(
238                "Layer norm backward width {width} does not match dimension {}",
239                self.dimension
240            )));
241        }
242        if width == 0 {
243            return Ok(Array2::zeros(input.dim()));
244        }
245
246        let width_t: T =
247            scirs2_core::numeric::NumCast::from(width as f64).unwrap_or_else(|| T::one());
248        let mut grad_input = Array2::zeros(input.dim());
249        let mut grad_gamma = Array1::zeros(width);
250        let mut grad_beta = Array1::zeros(width);
251
252        for i in 0..rows {
253            let row = input.row(i);
254            let mean = row.sum() / width_t;
255            let variance = row
256                .iter()
257                .map(|&x| (x - mean) * (x - mean))
258                .fold(T::zero(), |acc, x| acc + x)
259                / width_t;
260            let sigma = (variance + self.epsilon).sqrt();
261
262            let mut xhat = Array1::zeros(width);
263            for j in 0..width {
264                xhat[j] = (input[[i, j]] - mean) / sigma;
265            }
266
267            let mut g_scaled = Array1::zeros(width);
268            for j in 0..width {
269                let g = grad_output[[i, j]];
270                grad_gamma[j] = grad_gamma[j] + g * xhat[j];
271                grad_beta[j] = grad_beta[j] + g;
272                g_scaled[j] = g * self.gamma[j];
273            }
274
275            let mean_g = g_scaled.iter().fold(T::zero(), |a, &b| a + b) / width_t;
276            let mean_gx = g_scaled
277                .iter()
278                .zip(xhat.iter())
279                .map(|(&g, &x)| g * x)
280                .fold(T::zero(), |a, b| a + b)
281                / width_t;
282
283            for j in 0..width {
284                grad_input[[i, j]] = (g_scaled[j] - mean_g - xhat[j] * mean_gx) / sigma;
285            }
286        }
287
288        self.gamma = &self.gamma - &(grad_gamma * learning_rate);
289        self.beta = &self.beta - &(grad_beta * learning_rate);
290
291        Ok(grad_input)
292    }
293
294    /// Get parameter count
295    pub fn parameter_count(&self) -> usize {
296        2 * self.dimension // gamma + beta
297    }
298
299    /// Reset normalization parameters
300    pub fn reset(&mut self) -> Result<()> {
301        self.gamma.fill(T::one());
302        self.beta.fill(T::zero());
303        Ok(())
304    }
305}
306
307/// Dropout layer for regularization, backed by a seeded generator so training
308/// runs are reproducible.
309#[derive(Debug, Clone)]
310pub struct DropoutLayer {
311    /// Dropout probability
312    dropout_rate: f64,
313
314    /// Training mode flag. Defaults to `false`, so inference is the safe default.
315    training: bool,
316
317    /// xorshift64* state (never zero)
318    rng_state: u64,
319}
320
321impl DropoutLayer {
322    /// Create a new dropout layer with a default seed. Starts in inference mode.
323    pub fn new(dropout_rate: f64) -> Self {
324        Self::with_seed(dropout_rate, 0x9E37_79B9_7F4A_7C15)
325    }
326
327    /// Create a new dropout layer with an explicit seed. Starts in inference mode.
328    pub fn with_seed(dropout_rate: f64, seed: u64) -> Self {
329        Self {
330            dropout_rate: dropout_rate.clamp(0.0, 1.0),
331            training: false,
332            rng_state: seed | 1,
333        }
334    }
335
336    fn next_f64(&mut self) -> f64 {
337        let mut x = self.rng_state;
338        x ^= x >> 12;
339        x ^= x << 25;
340        x ^= x >> 27;
341        self.rng_state = x;
342        let value = x.wrapping_mul(0x2545_F491_4F6C_DD1D);
343        (value >> 11) as f64 / (1u64 << 53) as f64
344    }
345
346    /// Inverted dropout: zero entries with probability `dropout_rate` and scale
347    /// the survivors by `1 / (1 - rate)` so the expectation is preserved.
348    pub fn forward<
349        T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
350    >(
351        &mut self,
352        input: &Array2<T>,
353    ) -> Array2<T> {
354        if !self.training || self.dropout_rate <= 0.0 {
355            return input.clone();
356        }
357        if self.dropout_rate >= 1.0 {
358            return Array2::zeros(input.dim());
359        }
360
361        let mut output = input.clone();
362        let keep_prob = 1.0 - self.dropout_rate;
363        let scale: T =
364            scirs2_core::numeric::NumCast::from(1.0 / keep_prob).unwrap_or_else(|| T::one());
365
366        for elem in output.iter_mut() {
367            if self.next_f64() < self.dropout_rate {
368                *elem = T::zero();
369            } else {
370                *elem = *elem * scale;
371            }
372        }
373
374        output
375    }
376
377    /// Set training mode
378    pub fn set_training(&mut self, training: bool) {
379        self.training = training;
380    }
381
382    /// Get training mode
383    pub fn is_training(&self) -> bool {
384        self.training
385    }
386
387    /// Dropout probability
388    pub fn dropout_rate(&self) -> f64 {
389        self.dropout_rate
390    }
391}
392
393/// Output projection layer
394pub struct OutputProjection<
395    T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
396> {
397    /// Weight matrix
398    weight: Array2<T>,
399
400    /// Bias vector
401    bias: Array1<T>,
402
403    /// Input dimension
404    input_dim: usize,
405
406    /// Output dimension
407    output_dim: usize,
408}
409
410impl<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>
411    OutputProjection<T>
412{
413    /// Create a new output projection with Xavier-initialized weights.
414    pub fn new(input_dim: usize, output_dim: usize) -> Result<Self> {
415        if input_dim == 0 || output_dim == 0 {
416            return Err(OptimError::InvalidConfig(
417                "Output projection dimensions must be positive".to_string(),
418            ));
419        }
420
421        Ok(Self {
422            weight: xavier_uniform(input_dim, output_dim),
423            bias: Array1::zeros(output_dim),
424            input_dim,
425            output_dim,
426        })
427    }
428
429    /// Forward pass: `input @ weight + bias`.
430    pub fn forward(&self, input: &Array2<T>) -> Result<Array2<T>> {
431        let (_rows, width) = input.dim();
432        if width != self.input_dim {
433            return Err(OptimError::InvalidConfig(format!(
434                "Projection input width {width} does not match the expected {}",
435                self.input_dim
436            )));
437        }
438
439        Ok(input.dot(&self.weight) + &self.bias)
440    }
441
442    /// Get parameter count
443    pub fn parameter_count(&self) -> usize {
444        self.input_dim * self.output_dim + self.output_dim
445    }
446
447    /// Projection weights
448    pub fn weights(&self) -> &Array2<T> {
449        &self.weight
450    }
451
452    /// Projection bias
453    pub fn bias(&self) -> &Array1<T> {
454        &self.bias
455    }
456
457    /// Replace the projection parameters
458    pub fn set_parameters(&mut self, weight: Array2<T>, bias: Array1<T>) -> Result<()> {
459        if weight.dim() != (self.input_dim, self.output_dim) || bias.len() != self.output_dim {
460            return Err(OptimError::InvalidConfig(format!(
461                "Parameter shapes {:?}/{} do not match ({}, {})",
462                weight.dim(),
463                bias.len(),
464                self.input_dim,
465                self.output_dim
466            )));
467        }
468        self.weight = weight;
469        self.bias = bias;
470        Ok(())
471    }
472
473    /// Backward pass for `y = x W + b`.
474    ///
475    /// Returns `dL/dx` and applies an SGD step to `W` and `b`.
476    pub fn backward(
477        &mut self,
478        input: &Array2<T>,
479        grad_output: &Array2<T>,
480        learning_rate: T,
481    ) -> Result<Array2<T>> {
482        if input.ncols() != self.input_dim || grad_output.ncols() != self.output_dim {
483            return Err(OptimError::InvalidConfig(
484                "Output projection backward received mismatched shapes".to_string(),
485            ));
486        }
487        if input.nrows() != grad_output.nrows() {
488            return Err(OptimError::InvalidConfig(
489                "Output projection backward received mismatched row counts".to_string(),
490            ));
491        }
492
493        let grad_weight = input.t().dot(grad_output);
494        let mut grad_bias = Array1::zeros(self.output_dim);
495        for j in 0..self.output_dim {
496            grad_bias[j] = grad_output.column(j).iter().fold(T::zero(), |a, &b| a + b);
497        }
498
499        let grad_input = grad_output.dot(&self.weight.t());
500
501        self.weight = &self.weight - &(grad_weight * learning_rate);
502        self.bias = &self.bias - &(grad_bias * learning_rate);
503
504        Ok(grad_input)
505    }
506
507    /// Re-randomize the projection weights and zero the bias.
508    pub fn reset(&mut self) -> Result<()> {
509        self.weight = xavier_uniform(self.input_dim, self.output_dim);
510        self.bias.fill(T::zero());
511        Ok(())
512    }
513}
514
515/// Residual connections manager
516pub struct ResidualConnections<
517    T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
518> {
519    /// Model dimension
520    dimension: usize,
521
522    /// Optional learnable scaling factor
523    scale_factor: Option<T>,
524}
525
526impl<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>
527    ResidualConnections<T>
528{
529    /// Create new residual connections
530    pub fn new(dimension: usize) -> Self {
531        Self {
532            dimension,
533            scale_factor: None,
534        }
535    }
536
537    /// Create with learnable scaling
538    pub fn new_with_scaling(dimension: usize, initial_scale: T) -> Self {
539        Self {
540            dimension,
541            scale_factor: Some(initial_scale),
542        }
543    }
544
545    /// The model dimension this residual path was built for.
546    pub fn dimension(&self) -> usize {
547        self.dimension
548    }
549
550    /// Add residual connection.
551    ///
552    /// Both operands must be `(rows, dimension)`. Checking only that the two
553    /// shapes matched each other let a pair that agreed with each other but not
554    /// with the model dimension pass silently — the residual would then be added
555    /// to activations from a differently-sized layer without complaint.
556    pub fn add(&self, input: &Array2<T>, residual: &Array2<T>) -> Result<Array2<T>> {
557        if input.shape() != residual.shape() {
558            return Err(crate::error::OptimError::Other(
559                "Shape mismatch in residual connection".to_string(),
560            ));
561        }
562        if input.ncols() != self.dimension {
563            return Err(crate::error::OptimError::InvalidConfig(format!(
564                "Residual connection was built for model dimension {} but received \
565                 {} features",
566                self.dimension,
567                input.ncols()
568            )));
569        }
570
571        let mut output = input + residual;
572
573        if let Some(scale) = self.scale_factor {
574            output.mapv_inplace(|x| x * scale);
575        }
576
577        Ok(output)
578    }
579
580    /// Set scaling factor
581    pub fn set_scale_factor(&mut self, scale: T) {
582        self.scale_factor = Some(scale);
583    }
584
585    /// Get scaling factor
586    pub fn get_scale_factor(&self) -> Option<T> {
587        self.scale_factor
588    }
589}
590
591/// Activation layer with various activation functions
592pub struct ActivationLayer;
593
594impl ActivationLayer {
595    /// Apply activation function
596    pub fn apply<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>(
597        input: &Array2<T>,
598        activation: ActivationFunction,
599    ) -> Array2<T> {
600        match activation {
601            ActivationFunction::ReLU => Self::relu(input),
602            ActivationFunction::GELU => Self::gelu(input),
603            ActivationFunction::Swish => Self::swish(input),
604            ActivationFunction::Tanh => Self::tanh(input),
605            ActivationFunction::Sigmoid => Self::sigmoid(input),
606            ActivationFunction::LeakyReLU => Self::leaky_relu(
607                input,
608                scirs2_core::numeric::NumCast::from(0.01).unwrap_or_else(|| T::zero()),
609            ),
610        }
611    }
612
613    /// ReLU activation
614    fn relu<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>(
615        input: &Array2<T>,
616    ) -> Array2<T> {
617        input.map(|&x| if x > T::zero() { x } else { T::zero() })
618    }
619
620    /// GELU activation (approximation)
621    fn gelu<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>(
622        input: &Array2<T>,
623    ) -> Array2<T> {
624        input.map(|&x| {
625            let half = scirs2_core::numeric::NumCast::from(0.5).unwrap_or_else(|| T::zero());
626            let one = T::one();
627            let sqrt_2_pi =
628                scirs2_core::numeric::NumCast::from(0.797884560802865).unwrap_or_else(|| T::zero()); // sqrt(2/π)
629            let coeff = scirs2_core::numeric::NumCast::from(0.044715).unwrap_or_else(|| T::zero());
630
631            let tanh_arg = sqrt_2_pi * (x + coeff * x * x * x);
632            let tanh_val = tanh_arg.tanh();
633
634            half * x * (one + tanh_val)
635        })
636    }
637
638    /// Swish activation
639    fn swish<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>(
640        input: &Array2<T>,
641    ) -> Array2<T> {
642        input.map(|&x| x * Self::sigmoid_scalar(x))
643    }
644
645    /// Tanh activation
646    fn tanh<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>(
647        input: &Array2<T>,
648    ) -> Array2<T> {
649        input.map(|&x| x.tanh())
650    }
651
652    /// Sigmoid activation
653    fn sigmoid<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>(
654        input: &Array2<T>,
655    ) -> Array2<T> {
656        input.map(|&x| Self::sigmoid_scalar(x))
657    }
658
659    /// Leaky ReLU activation
660    fn leaky_relu<
661        T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
662    >(
663        input: &Array2<T>,
664        alpha: T,
665    ) -> Array2<T> {
666        input.map(|&x| if x > T::zero() { x } else { alpha * x })
667    }
668
669    /// Numerically stable sigmoid.
670    fn sigmoid_scalar<
671        T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
672    >(
673        x: T,
674    ) -> T {
675        let one = T::one();
676        if x >= T::zero() {
677            one / (one + (-x).exp())
678        } else {
679            let e = x.exp();
680            e / (one + e)
681        }
682    }
683
684    /// Elementwise derivative of the activation, evaluated at its *input*.
685    pub fn derivative<
686        T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
687    >(
688        pre_activation: &Array2<T>,
689        activation: ActivationFunction,
690    ) -> Array2<T> {
691        match activation {
692            ActivationFunction::ReLU => {
693                pre_activation.map(|&x| if x > T::zero() { T::one() } else { T::zero() })
694            }
695            ActivationFunction::LeakyReLU => {
696                let alpha: T =
697                    scirs2_core::numeric::NumCast::from(0.01).unwrap_or_else(|| T::zero());
698                pre_activation.map(|&x| if x > T::zero() { T::one() } else { alpha })
699            }
700            ActivationFunction::Tanh => pre_activation.map(|&x| {
701                let t = x.tanh();
702                T::one() - t * t
703            }),
704            ActivationFunction::Sigmoid => pre_activation.map(|&x| {
705                let s = Self::sigmoid_scalar(x);
706                s * (T::one() - s)
707            }),
708            ActivationFunction::Swish => pre_activation.map(|&x| {
709                let s = Self::sigmoid_scalar(x);
710                s + x * s * (T::one() - s)
711            }),
712            ActivationFunction::GELU => pre_activation.map(|&x| {
713                // Derivative of 0.5 x (1 + tanh(a (x + c x^3)))
714                let a: T = scirs2_core::numeric::NumCast::from(0.797_884_560_802_865)
715                    .unwrap_or_else(|| T::one());
716                let c: T =
717                    scirs2_core::numeric::NumCast::from(0.044715).unwrap_or_else(|| T::zero());
718                let half: T = scirs2_core::numeric::NumCast::from(0.5).unwrap_or_else(|| T::one());
719                let three: T = scirs2_core::numeric::NumCast::from(3.0).unwrap_or_else(|| T::one());
720                let inner = a * (x + c * x * x * x);
721                let t = inner.tanh();
722                let sech2 = T::one() - t * t;
723                half * (T::one() + t) + half * x * sech2 * a * (T::one() + three * c * x * x)
724            }),
725        }
726    }
727}
728
729#[cfg(test)]
730mod tests {
731    use super::*;
732
733    #[test]
734    fn embedding_layer_projects_continuous_inputs() {
735        let embedding = EmbeddingLayer::<f64>::new(6, 4).expect("embedding creation");
736        assert_eq!(embedding.parameter_count(), 24);
737
738        // Values inside (-1, 1) used to collapse onto embedding row 0.
739        let input = Array2::<f64>::from_shape_fn((3, 6), |(i, j)| 0.1 * (i + j) as f64 - 0.3);
740        let output = embedding.forward(&input).expect("forward");
741        assert_eq!(output.dim(), (3, 4));
742        assert!(
743            output.iter().any(|&v| v != 0.0),
744            "projection produced all zeros"
745        );
746
747        // Different rows must yield different projections.
748        assert!(output.row(0) != output.row(2));
749    }
750
751    #[test]
752    fn embedding_layer_matches_a_manual_matmul() {
753        let mut embedding = EmbeddingLayer::<f64>::new(2, 2).expect("embedding creation");
754        embedding
755            .set_weights(
756                Array2::from_shape_vec((2, 2), vec![1.0, 2.0, 3.0, 4.0]).expect("valid shape"),
757            )
758            .expect("weights");
759        let input = Array2::from_shape_vec((1, 2), vec![1.0, 1.0]).expect("valid shape");
760        let output = embedding.forward(&input).expect("forward");
761        assert!((output[[0, 0]] - 4.0).abs() < 1e-12);
762        assert!((output[[0, 1]] - 6.0).abs() < 1e-12);
763    }
764
765    #[test]
766    fn embedding_reset_re_randomizes() {
767        let mut embedding = EmbeddingLayer::<f64>::new(8, 8).expect("embedding creation");
768        let before = embedding.weights().clone();
769        embedding.reset().expect("reset");
770        assert!(embedding.weights().iter().any(|&v| v != 0.0));
771        assert_ne!(*embedding.weights(), before);
772    }
773
774    #[test]
775    fn embedding_rejects_wrong_width() {
776        let embedding = EmbeddingLayer::<f64>::new(6, 4).expect("embedding creation");
777        assert!(embedding.forward(&Array2::<f64>::zeros((2, 5))).is_err());
778        assert!(EmbeddingLayer::<f64>::new(0, 4).is_err());
779    }
780
781    #[test]
782    fn output_projection_produces_nonzero_output() {
783        let projection = OutputProjection::<f64>::new(8, 4).expect("projection creation");
784        let input = Array2::<f64>::ones((2, 8));
785        let output = projection.forward(&input).expect("forward");
786        assert_eq!(output.dim(), (2, 4));
787        assert!(
788            output.iter().any(|&v| v != 0.0),
789            "zero-initialized projection produced all zeros"
790        );
791        assert_eq!(projection.parameter_count(), 8 * 4 + 4);
792    }
793
794    #[test]
795    fn output_projection_reset_re_randomizes() {
796        let mut projection = OutputProjection::<f64>::new(8, 4).expect("projection creation");
797        let before = projection.weights().clone();
798        projection.reset().expect("reset");
799        assert!(projection.weights().iter().any(|&v| v != 0.0));
800        assert_ne!(*projection.weights(), before);
801    }
802
803    #[test]
804    fn layer_normalization_standardizes_and_validates() {
805        let norm = LayerNormalization::<f64>::new(4).expect("layer norm creation");
806        let input =
807            Array2::<f64>::from_shape_vec((1, 4), vec![1.0, 2.0, 3.0, 4.0]).expect("valid shape");
808        let output = norm.forward(&input).expect("forward");
809        let mean: f64 = output.row(0).iter().sum::<f64>() / 4.0;
810        assert!(mean.abs() < 1e-9, "mean {mean}");
811        assert_eq!(norm.parameter_count(), 8);
812
813        // Mismatched widths are an error, not an out-of-bounds index.
814        assert!(norm.forward(&Array2::<f64>::zeros((2, 8))).is_err());
815    }
816
817    #[test]
818    fn dropout_defaults_to_inference_and_is_reproducible() {
819        let mut dropout = DropoutLayer::with_seed(0.5, 11);
820        let input = Array2::<f64>::ones((8, 8));
821
822        assert!(!dropout.is_training());
823        assert_eq!(dropout.forward(&input), input);
824
825        dropout.set_training(true);
826        let first = dropout.forward(&input);
827        let zeros = first.iter().filter(|&&v| v == 0.0).count();
828        assert!(zeros > 0 && zeros < input.len());
829        assert!(first.iter().all(|&v| v == 0.0 || (v - 2.0).abs() < 1e-12));
830
831        let mut replay = DropoutLayer::with_seed(0.5, 11);
832        replay.set_training(true);
833        assert_eq!(replay.forward(&input), first);
834    }
835
836    #[test]
837    fn residual_connections_validate_shapes() {
838        let residual = ResidualConnections::<f64>::new(4);
839        let input = Array2::<f64>::ones((2, 4));
840        let other = Array2::<f64>::from_elem((2, 4), 0.5);
841        let output = residual.add(&input, &other).expect("residual add");
842        assert!((output[[0, 0]] - 1.5).abs() < 1e-12);
843        assert!(residual.add(&input, &Array2::<f64>::ones((3, 4))).is_err());
844    }
845
846    #[test]
847    fn activation_functions_behave() {
848        let input =
849            Array2::<f64>::from_shape_vec((2, 2), vec![-1.0, 0.0, 0.5, 1.0]).expect("valid shape");
850
851        let relu = ActivationLayer::apply(&input, ActivationFunction::ReLU);
852        assert_eq!(relu[[0, 0]], 0.0);
853        assert_eq!(relu[[1, 1]], 1.0);
854
855        let gelu = ActivationLayer::apply(&input, ActivationFunction::GELU);
856        assert_eq!(gelu.shape(), input.shape());
857
858        let sigmoid = ActivationLayer::apply(&input, ActivationFunction::Sigmoid);
859        assert!(sigmoid.iter().all(|&x| (0.0..=1.0).contains(&x)));
860    }
861
862    #[test]
863    fn activations_stay_finite_for_extreme_inputs() {
864        let input =
865            Array2::<f64>::from_shape_vec((1, 3), vec![-1e6, 0.0, 1e6]).expect("valid shape");
866        for activation in [
867            ActivationFunction::ReLU,
868            ActivationFunction::GELU,
869            ActivationFunction::Swish,
870            ActivationFunction::Tanh,
871            ActivationFunction::Sigmoid,
872            ActivationFunction::LeakyReLU,
873        ] {
874            let output = ActivationLayer::apply(&input, activation);
875            assert!(
876                output.iter().all(|v| v.is_finite()),
877                "{activation:?} produced {output:?}"
878            );
879        }
880    }
881}