Skip to main content

ipfrs_tensorlogic/
activation_function.rs

1//! Neural network activation functions with forward pass, derivative, and vectorized operations.
2//!
3//! Provides a comprehensive set of activation functions for neural network layers including
4//! ReLU variants, sigmoid, tanh, softmax, GELU, Swish, Mish, HardSwish, and more.
5//! Each activation supports forward evaluation, derivative computation for backpropagation,
6//! in-place mutation, and call statistics tracking.
7//!
8//! # Naming Convention
9//!
10//! Types in this module are prefixed with `Af` (Activation Function) in re-exports to
11//! avoid collision with the pre-existing `activation` module in the same crate.
12
13use std::f64::consts::PI;
14
15// ---------------------------------------------------------------------------
16// ActivationType
17// ---------------------------------------------------------------------------
18
19/// Specifies which activation function to apply.
20#[derive(Debug, Clone, PartialEq)]
21pub enum ActivationType {
22    /// Rectified Linear Unit: max(0, x)
23    ReLU,
24    /// Leaky ReLU: x if x >= 0, slope * x otherwise.
25    LeakyReLU(f64),
26    /// Exponential Linear Unit: x if x >= 0, alpha * (exp(x) - 1) otherwise.
27    ELU(f64),
28    /// Logistic sigmoid: 1 / (1 + exp(-x))
29    Sigmoid,
30    /// Hyperbolic tangent.
31    Tanh,
32    /// Normalised exponentials across the full input slice.
33    Softmax,
34    /// Gaussian Error Linear Unit (tanh approximation).
35    GELU,
36    /// Swish / SiLU: x * sigmoid(x)
37    Swish,
38    /// Mish: x * tanh(softplus(x)) where softplus(x) = ln(1 + exp(x))
39    Mish,
40    /// Hard Swish: x * relu6(x + 3) / 6
41    HardSwish,
42    /// Identity: f(x) = x
43    Linear,
44    /// Threshold: value if x > threshold, else 0.0
45    Threshold(f64, f64),
46}
47
48// ---------------------------------------------------------------------------
49// ActivationConfig
50// ---------------------------------------------------------------------------
51
52/// Configuration for an [`ActivationFunction`] instance.
53#[derive(Debug, Clone)]
54pub struct ActivationConfig {
55    /// Which activation function to apply.
56    pub activation_type: ActivationType,
57    /// When `true`, `apply_inplace` mutates the slice in place rather than
58    /// allocating a new buffer.  `forward` always returns a new `Vec`.
59    pub inplace: bool,
60}
61
62impl ActivationConfig {
63    /// Convenience constructor.
64    pub fn new(activation_type: ActivationType) -> Self {
65        Self {
66            activation_type,
67            inplace: false,
68        }
69    }
70
71    /// Enable in-place mode.
72    pub fn with_inplace(mut self) -> Self {
73        self.inplace = true;
74        self
75    }
76}
77
78// ---------------------------------------------------------------------------
79// ActivationStats
80// ---------------------------------------------------------------------------
81
82/// Runtime statistics collected by an [`ActivationFunction`].
83#[derive(Debug, Clone, Default)]
84pub struct ActivationStats {
85    /// Total number of `forward` / `apply_inplace` calls.
86    pub total_calls: u64,
87    /// Cumulative number of elements processed across all calls.
88    pub total_elements: u64,
89    /// Number of ReLU output elements that were clamped to zero (dead neurons).
90    pub dead_relu_count: u64,
91}
92
93// ---------------------------------------------------------------------------
94// ActivationFunction
95// ---------------------------------------------------------------------------
96
97/// Neural network activation layer with forward pass, derivative, in-place
98/// application, and call statistics.
99pub struct ActivationFunction {
100    config: ActivationConfig,
101    stats: ActivationStats,
102}
103
104impl ActivationFunction {
105    /// Create a new activation layer with the given configuration.
106    pub fn new(config: ActivationConfig) -> Self {
107        Self {
108            config,
109            stats: ActivationStats::default(),
110        }
111    }
112
113    // ------------------------------------------------------------------
114    // Public forward API
115    // ------------------------------------------------------------------
116
117    /// Apply the configured activation to every element of `input` and return
118    /// a newly allocated `Vec<f64>`.
119    ///
120    /// For `Softmax` the operation is applied across the entire slice.
121    pub fn forward(&mut self, input: &[f64]) -> Vec<f64> {
122        self.stats.total_calls += 1;
123        self.stats.total_elements += input.len() as u64;
124
125        let result = match &self.config.activation_type {
126            ActivationType::ReLU => {
127                let out: Vec<f64> = input.iter().map(|&x| Self::relu(x)).collect();
128                // Count dead ReLU outputs before returning.
129                let dead = out.iter().filter(|&&v| v == 0.0).count() as u64;
130                self.stats.dead_relu_count += dead;
131                out
132            }
133            ActivationType::LeakyReLU(slope) => {
134                let s = *slope;
135                input.iter().map(|&x| Self::leaky_relu(x, s)).collect()
136            }
137            ActivationType::ELU(alpha) => {
138                let a = *alpha;
139                input.iter().map(|&x| Self::elu(x, a)).collect()
140            }
141            ActivationType::Sigmoid => input.iter().map(|&x| Self::sigmoid(x)).collect(),
142            ActivationType::Tanh => input.iter().map(|&x| Self::tanh_activation(x)).collect(),
143            ActivationType::Softmax => Self::softmax(input),
144            ActivationType::GELU => input.iter().map(|&x| Self::gelu(x)).collect(),
145            ActivationType::Swish => input.iter().map(|&x| Self::swish(x)).collect(),
146            ActivationType::Mish => input.iter().map(|&x| Self::mish(x)).collect(),
147            ActivationType::HardSwish => input.iter().map(|&x| Self::hard_swish(x)).collect(),
148            ActivationType::Linear => input.to_vec(),
149            ActivationType::Threshold(threshold, value) => {
150                let (t, v) = (*threshold, *value);
151                input.iter().map(|&x| if x > t { v } else { 0.0 }).collect()
152            }
153        };
154
155        result
156    }
157
158    /// Compute the element-wise derivative of the activation with respect to
159    /// the pre-activation input.
160    ///
161    /// # Argument semantics
162    ///
163    /// The `output` slice is interpreted differently per activation:
164    ///
165    /// - **Sigmoid** — `output` should be the *post*-activation value `σ(x)`.
166    ///   The derivative is `σ(x) * (1 − σ(x))` which avoids re-computing the
167    ///   expensive exponential.
168    /// - **Tanh** — `output` should be the *post*-activation value `tanh(x)`.
169    ///   The derivative is `1 − tanh(x)²`.
170    /// - **All others** — `output` is treated as the *pre*-activation value `x`
171    ///   and the derivative is computed from first principles.
172    pub fn derivative(&self, output: &[f64]) -> Vec<f64> {
173        match &self.config.activation_type {
174            ActivationType::ReLU => output
175                .iter()
176                .map(|&x| if x > 0.0 { 1.0 } else { 0.0 })
177                .collect(),
178
179            ActivationType::LeakyReLU(slope) => {
180                let s = *slope;
181                output
182                    .iter()
183                    .map(|&x| if x >= 0.0 { 1.0 } else { s })
184                    .collect()
185            }
186
187            ActivationType::ELU(alpha) => {
188                let a = *alpha;
189                output
190                    .iter()
191                    .map(|&x| {
192                        if x >= 0.0 {
193                            1.0
194                        } else {
195                            // d/dx ELU = alpha * exp(x)
196                            a * x.exp()
197                        }
198                    })
199                    .collect()
200            }
201
202            // Sigmoid derivative from post-activation value: σ * (1 - σ)
203            ActivationType::Sigmoid => output.iter().map(|&s| s * (1.0 - s)).collect(),
204
205            // Tanh derivative from post-activation value: 1 - tanh²
206            ActivationType::Tanh => output.iter().map(|&t| 1.0 - t * t).collect(),
207
208            // Softmax Jacobian diagonal (useful for element-wise backprop):
209            // ∂softmax_i/∂x_i = softmax_i * (1 - softmax_i)
210            // Here `output` is treated as the softmax output vector.
211            ActivationType::Softmax => output.iter().map(|&s| s * (1.0 - s)).collect(),
212
213            ActivationType::GELU => output.iter().map(|&x| Self::gelu_derivative(x)).collect(),
214
215            ActivationType::Swish => output.iter().map(|&x| Self::swish_derivative(x)).collect(),
216
217            ActivationType::Mish => output.iter().map(|&x| Self::mish_derivative(x)).collect(),
218
219            ActivationType::HardSwish => output
220                .iter()
221                .map(|&x| Self::hard_swish_derivative(x))
222                .collect(),
223
224            ActivationType::Linear => vec![1.0; output.len()],
225
226            ActivationType::Threshold(threshold, _value) => {
227                let t = *threshold;
228                output
229                    .iter()
230                    .map(|&x| {
231                        // The function is a step, so the derivative is 0 everywhere
232                        // except at the threshold itself (where it is undefined / ∞).
233                        // Convention: return 0 everywhere.
234                        if (x - t).abs() < f64::EPSILON {
235                            f64::INFINITY
236                        } else {
237                            0.0
238                        }
239                    })
240                    .collect()
241            }
242        }
243    }
244
245    /// Apply the configured activation **in place**, mutating `data`.
246    ///
247    /// Statistics are updated identically to [`forward`](Self::forward).
248    pub fn apply_inplace(&mut self, data: &mut [f64]) {
249        self.stats.total_calls += 1;
250        self.stats.total_elements += data.len() as u64;
251
252        match &self.config.activation_type.clone() {
253            ActivationType::ReLU => {
254                let mut dead: u64 = 0;
255                for x in data.iter_mut() {
256                    if *x <= 0.0 {
257                        *x = 0.0;
258                        dead += 1;
259                    }
260                }
261                self.stats.dead_relu_count += dead;
262            }
263            ActivationType::LeakyReLU(slope) => {
264                let s = *slope;
265                for x in data.iter_mut() {
266                    if *x < 0.0 {
267                        *x *= s;
268                    }
269                }
270            }
271            ActivationType::ELU(alpha) => {
272                let a = *alpha;
273                for x in data.iter_mut() {
274                    if *x < 0.0 {
275                        *x = a * (x.exp() - 1.0);
276                    }
277                }
278            }
279            ActivationType::Sigmoid => {
280                for x in data.iter_mut() {
281                    *x = Self::sigmoid(*x);
282                }
283            }
284            ActivationType::Tanh => {
285                for x in data.iter_mut() {
286                    *x = Self::tanh_activation(*x);
287                }
288            }
289            ActivationType::Softmax => {
290                let result = Self::softmax(data);
291                data.copy_from_slice(&result);
292            }
293            ActivationType::GELU => {
294                for x in data.iter_mut() {
295                    *x = Self::gelu(*x);
296                }
297            }
298            ActivationType::Swish => {
299                for x in data.iter_mut() {
300                    *x = Self::swish(*x);
301                }
302            }
303            ActivationType::Mish => {
304                for x in data.iter_mut() {
305                    *x = Self::mish(*x);
306                }
307            }
308            ActivationType::HardSwish => {
309                for x in data.iter_mut() {
310                    *x = Self::hard_swish(*x);
311                }
312            }
313            ActivationType::Linear => { /* identity — nothing to do */ }
314            ActivationType::Threshold(threshold, value) => {
315                let (t, v) = (*threshold, *value);
316                for x in data.iter_mut() {
317                    *x = if *x > t { v } else { 0.0 };
318                }
319            }
320        }
321    }
322
323    // ------------------------------------------------------------------
324    // Read-only accessors
325    // ------------------------------------------------------------------
326
327    /// Return a reference to the accumulated statistics for this instance.
328    pub fn stats(&self) -> &ActivationStats {
329        &self.stats
330    }
331
332    /// Return a reference to the current configuration.
333    pub fn config(&self) -> &ActivationConfig {
334        &self.config
335    }
336
337    // ------------------------------------------------------------------
338    // Static scalar activations
339    // ------------------------------------------------------------------
340
341    /// Rectified Linear Unit: `max(0, x)`.
342    #[inline]
343    pub fn relu(x: f64) -> f64 {
344        x.max(0.0)
345    }
346
347    /// Leaky ReLU: `x` if `x >= 0`, else `slope * x`.
348    #[inline]
349    pub fn leaky_relu(x: f64, slope: f64) -> f64 {
350        if x >= 0.0 {
351            x
352        } else {
353            slope * x
354        }
355    }
356
357    /// Exponential Linear Unit: `x` if `x >= 0`, else `alpha * (exp(x) - 1)`.
358    #[inline]
359    pub fn elu(x: f64, alpha: f64) -> f64 {
360        if x >= 0.0 {
361            x
362        } else {
363            alpha * (x.exp() - 1.0)
364        }
365    }
366
367    /// Logistic sigmoid: `1 / (1 + exp(-x))`.
368    #[inline]
369    pub fn sigmoid(x: f64) -> f64 {
370        1.0 / (1.0 + (-x).exp())
371    }
372
373    /// Hyperbolic tangent activation.
374    #[inline]
375    pub fn tanh_activation(x: f64) -> f64 {
376        x.tanh()
377    }
378
379    /// Numerically stable softmax over the entire `input` slice.
380    ///
381    /// Subtracts `max(input)` before exponentiating to prevent overflow.
382    pub fn softmax(input: &[f64]) -> Vec<f64> {
383        if input.is_empty() {
384            return Vec::new();
385        }
386
387        let max_val = input.iter().copied().fold(f64::NEG_INFINITY, f64::max);
388
389        let exps: Vec<f64> = input.iter().map(|&x| (x - max_val).exp()).collect();
390        let sum: f64 = exps.iter().sum();
391
392        if sum == 0.0 {
393            // Degenerate — return uniform distribution.
394            let n = input.len();
395            return vec![1.0 / n as f64; n];
396        }
397
398        exps.iter().map(|&e| e / sum).collect()
399    }
400
401    /// GELU activation (tanh approximation).
402    ///
403    /// `0.5 * x * (1 + tanh(sqrt(2/π) * (x + 0.044715 * x³)))`
404    #[inline]
405    pub fn gelu(x: f64) -> f64 {
406        // sqrt(2 / π)
407        let c = (2.0_f64 / PI).sqrt();
408        let inner = c * (x + 0.044715 * x * x * x);
409        0.5 * x * (1.0 + inner.tanh())
410    }
411
412    /// Swish / SiLU: `x * sigmoid(x)`.
413    #[inline]
414    pub fn swish(x: f64) -> f64 {
415        x * Self::sigmoid(x)
416    }
417
418    /// Mish: `x * tanh(softplus(x))` where `softplus(x) = ln(1 + exp(x))`.
419    ///
420    /// Uses the numerically stable form `ln(1 + exp(x))`:
421    /// for large positive `x`, `softplus(x) ≈ x`; for large negative `x`,
422    /// `softplus(x) ≈ exp(x)`.
423    #[inline]
424    pub fn mish(x: f64) -> f64 {
425        // Numerically stable softplus
426        let sp = if x > 20.0 { x } else { (1.0 + x.exp()).ln() };
427        x * sp.tanh()
428    }
429
430    /// Hard Swish: `x * relu6(x + 3) / 6` where `relu6(t) = min(max(t, 0), 6)`.
431    #[inline]
432    pub fn hard_swish(x: f64) -> f64 {
433        let relu6 = (x + 3.0).clamp(0.0, 6.0);
434        x * relu6 / 6.0
435    }
436
437    // ------------------------------------------------------------------
438    // Private derivative helpers
439    // ------------------------------------------------------------------
440
441    /// GELU derivative (analytical, using tanh approximation).
442    #[inline]
443    fn gelu_derivative(x: f64) -> f64 {
444        let c = (2.0_f64 / PI).sqrt();
445        let inner = c * (x + 0.044715 * x * x * x);
446        let t = inner.tanh();
447        // d/dx [0.5 * x * (1 + tanh(inner))]
448        // = 0.5 * (1 + tanh(inner)) + 0.5 * x * sech²(inner) * d_inner/dx
449        let sech2 = 1.0 - t * t;
450        let d_inner = c * (1.0 + 3.0 * 0.044715 * x * x);
451        0.5 * (1.0 + t) + 0.5 * x * sech2 * d_inner
452    }
453
454    /// Swish derivative: `sigmoid(x) + x * sigmoid(x) * (1 - sigmoid(x))`.
455    #[inline]
456    fn swish_derivative(x: f64) -> f64 {
457        let s = Self::sigmoid(x);
458        s + x * s * (1.0 - s)
459    }
460
461    /// Mish derivative.
462    ///
463    /// Let `sp = softplus(x) = ln(1 + exp(x))` and `omega = tanh(sp)`.
464    /// Then `d mish / dx = omega + x * (1 - omega²) * sigmoid(x)`.
465    #[inline]
466    fn mish_derivative(x: f64) -> f64 {
467        let sp = if x > 20.0 { x } else { (1.0 + x.exp()).ln() };
468        let omega = sp.tanh();
469        let sech2_sp = 1.0 - omega * omega;
470        let sig = Self::sigmoid(x);
471        omega + x * sech2_sp * sig
472    }
473
474    /// Hard Swish derivative.
475    ///
476    /// The piecewise function is:
477    /// - `x <= -3`  →  0
478    /// - `-3 < x < 3`  →  `(2x + 3) / 6`
479    /// - `x >= 3`   →  1
480    #[inline]
481    fn hard_swish_derivative(x: f64) -> f64 {
482        if x <= -3.0 {
483            0.0
484        } else if x >= 3.0 {
485            1.0
486        } else {
487            (2.0 * x + 3.0) / 6.0
488        }
489    }
490}
491
492// ---------------------------------------------------------------------------
493// Tests
494// ---------------------------------------------------------------------------
495
496#[cfg(test)]
497mod tests {
498    use super::*;
499
500    const EPS: f64 = 1e-9;
501
502    // -----------------------------------------------------------------------
503    // Helper
504    // -----------------------------------------------------------------------
505
506    fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
507        (a - b).abs() < tol
508    }
509
510    fn make_af(at: ActivationType) -> ActivationFunction {
511        ActivationFunction::new(ActivationConfig::new(at))
512    }
513
514    // -----------------------------------------------------------------------
515    // ReLU
516    // -----------------------------------------------------------------------
517
518    #[test]
519    fn test_relu_positive() {
520        assert!(approx_eq(ActivationFunction::relu(3.5), 3.5, EPS));
521    }
522
523    #[test]
524    fn test_relu_negative() {
525        assert!(approx_eq(ActivationFunction::relu(-2.0), 0.0, EPS));
526    }
527
528    #[test]
529    fn test_relu_zero() {
530        assert!(approx_eq(ActivationFunction::relu(0.0), 0.0, EPS));
531    }
532
533    #[test]
534    fn test_relu_derivative_positive() {
535        let af = make_af(ActivationType::ReLU);
536        let d = af.derivative(&[1.0, 2.0]);
537        assert!(approx_eq(d[0], 1.0, EPS));
538        assert!(approx_eq(d[1], 1.0, EPS));
539    }
540
541    #[test]
542    fn test_relu_derivative_negative() {
543        let af = make_af(ActivationType::ReLU);
544        let d = af.derivative(&[-1.0, -5.0]);
545        assert!(approx_eq(d[0], 0.0, EPS));
546        assert!(approx_eq(d[1], 0.0, EPS));
547    }
548
549    // -----------------------------------------------------------------------
550    // Dead ReLU tracking
551    // -----------------------------------------------------------------------
552
553    #[test]
554    fn test_dead_relu_count() {
555        let mut af = make_af(ActivationType::ReLU);
556        let _out = af.forward(&[-1.0, 2.0, -3.0, 4.0]);
557        // Two negative inputs → two dead neurons.
558        assert_eq!(af.stats().dead_relu_count, 2);
559    }
560
561    #[test]
562    fn test_dead_relu_accumulates_across_calls() {
563        let mut af = make_af(ActivationType::ReLU);
564        let _ = af.forward(&[-1.0, 1.0]);
565        let _ = af.forward(&[-2.0, -3.0]);
566        assert_eq!(af.stats().dead_relu_count, 3);
567    }
568
569    // -----------------------------------------------------------------------
570    // Leaky ReLU
571    // -----------------------------------------------------------------------
572
573    #[test]
574    fn test_leaky_relu_negative_slope() {
575        let slope = 0.1;
576        let val = ActivationFunction::leaky_relu(-5.0, slope);
577        assert!(approx_eq(val, -0.5, EPS));
578    }
579
580    #[test]
581    fn test_leaky_relu_positive() {
582        assert!(approx_eq(
583            ActivationFunction::leaky_relu(3.0, 0.1),
584            3.0,
585            EPS
586        ));
587    }
588
589    #[test]
590    fn test_leaky_relu_derivative_negative() {
591        let slope = 0.01;
592        let af = make_af(ActivationType::LeakyReLU(slope));
593        let d = af.derivative(&[-2.0]);
594        assert!(approx_eq(d[0], slope, EPS));
595    }
596
597    // -----------------------------------------------------------------------
598    // ELU — continuity at zero
599    // -----------------------------------------------------------------------
600
601    #[test]
602    fn test_elu_zero() {
603        // ELU(0) = 0 regardless of alpha.
604        assert!(approx_eq(ActivationFunction::elu(0.0, 1.0), 0.0, EPS));
605    }
606
607    #[test]
608    fn test_elu_positive() {
609        assert!(approx_eq(ActivationFunction::elu(2.0, 1.0), 2.0, EPS));
610    }
611
612    #[test]
613    fn test_elu_negative() {
614        let alpha = 1.0;
615        let x = -1.0_f64;
616        let expected = alpha * (x.exp() - 1.0);
617        assert!(approx_eq(
618            ActivationFunction::elu(x, alpha),
619            expected,
620            1e-12
621        ));
622    }
623
624    #[test]
625    fn test_elu_continuity() {
626        // ELU should be continuous at 0: left-limit == right-limit == 0.
627        let left = ActivationFunction::elu(-1e-10, 1.0);
628        let right = ActivationFunction::elu(1e-10, 1.0);
629        assert!(approx_eq(left, right, 1e-8));
630    }
631
632    // -----------------------------------------------------------------------
633    // Sigmoid
634    // -----------------------------------------------------------------------
635
636    #[test]
637    fn test_sigmoid_zero() {
638        assert!(approx_eq(ActivationFunction::sigmoid(0.0), 0.5, EPS));
639    }
640
641    #[test]
642    fn test_sigmoid_large_positive() {
643        // σ(large) ≈ 1
644        assert!(ActivationFunction::sigmoid(100.0) > 0.9999);
645    }
646
647    #[test]
648    fn test_sigmoid_large_negative() {
649        // σ(-large) ≈ 0
650        assert!(ActivationFunction::sigmoid(-100.0) < 1e-10);
651    }
652
653    #[test]
654    fn test_sigmoid_derivative_from_output() {
655        // Derivative of sigmoid from its output: σ * (1 - σ)
656        // At x=0, σ=0.5 → derivative = 0.25
657        let af = make_af(ActivationType::Sigmoid);
658        let d = af.derivative(&[0.5]);
659        assert!(approx_eq(d[0], 0.25, EPS));
660    }
661
662    // -----------------------------------------------------------------------
663    // Tanh
664    // -----------------------------------------------------------------------
665
666    #[test]
667    fn test_tanh_zero() {
668        assert!(approx_eq(
669            ActivationFunction::tanh_activation(0.0),
670            0.0,
671            EPS
672        ));
673    }
674
675    #[test]
676    fn test_tanh_derivative_from_output() {
677        // At tanh(x)=0 (i.e. x=0), derivative = 1 - 0^2 = 1
678        let af = make_af(ActivationType::Tanh);
679        let d = af.derivative(&[0.0]);
680        assert!(approx_eq(d[0], 1.0, EPS));
681    }
682
683    #[test]
684    fn test_tanh_derivative_saturated() {
685        // At tanh(x)≈1, derivative ≈ 0
686        let af = make_af(ActivationType::Tanh);
687        let d = af.derivative(&[0.9999]);
688        assert!(d[0] < 0.001);
689    }
690
691    // -----------------------------------------------------------------------
692    // Softmax
693    // -----------------------------------------------------------------------
694
695    #[test]
696    fn test_softmax_sums_to_one() {
697        let input = vec![1.0, 2.0, 3.0, 4.0];
698        let out = ActivationFunction::softmax(&input);
699        let sum: f64 = out.iter().sum();
700        assert!(approx_eq(sum, 1.0, 1e-12));
701    }
702
703    #[test]
704    fn test_softmax_numerical_stability_large_values() {
705        // All very large — should not overflow / produce NaN.
706        let input = vec![1000.0, 1001.0, 1002.0];
707        let out = ActivationFunction::softmax(&input);
708        let sum: f64 = out.iter().sum();
709        assert!(!sum.is_nan());
710        assert!(approx_eq(sum, 1.0, 1e-12));
711    }
712
713    #[test]
714    fn test_softmax_uniform_input() {
715        // Equal logits → uniform distribution.
716        let input = vec![2.0; 4];
717        let out = ActivationFunction::softmax(&input);
718        for &v in &out {
719            assert!(approx_eq(v, 0.25, 1e-12));
720        }
721    }
722
723    #[test]
724    fn test_softmax_empty_input() {
725        let out = ActivationFunction::softmax(&[]);
726        assert!(out.is_empty());
727    }
728
729    #[test]
730    fn test_softmax_all_outputs_positive() {
731        let input = vec![-5.0, 0.0, 5.0];
732        let out = ActivationFunction::softmax(&input);
733        for &v in &out {
734            assert!(v > 0.0);
735        }
736    }
737
738    // -----------------------------------------------------------------------
739    // GELU
740    // -----------------------------------------------------------------------
741
742    #[test]
743    fn test_gelu_zero() {
744        // GELU(0) = 0 (since tanh(0) = 0, so 0.5*0*(1+0) = 0)
745        assert!(approx_eq(ActivationFunction::gelu(0.0), 0.0, EPS));
746    }
747
748    #[test]
749    fn test_gelu_positive_domain_close_to_x() {
750        // For large positive x, GELU(x) ≈ x
751        let x = 10.0_f64;
752        let g = ActivationFunction::gelu(x);
753        assert!(approx_eq(g, x, 1e-4));
754    }
755
756    #[test]
757    fn test_gelu_negative_domain_small() {
758        // For large negative x, GELU(x) ≈ 0
759        let g = ActivationFunction::gelu(-10.0);
760        assert!(g.abs() < 1e-3);
761    }
762
763    #[test]
764    fn test_gelu_approximation_bounds() {
765        // The tanh-approximation GELU should be within 0.001 of the exact
766        // erf-based GELU for x in [-3, 3].
767        // Exact GELU: x * 0.5 * (1 + erf(x / sqrt(2)))
768        // We use the polynomial approximation for erf.
769        for i in -30..=30 {
770            let x = i as f64 / 10.0;
771            let approx_gelu = ActivationFunction::gelu(x);
772            // Rough bound: GELU output should be within (-0.2, x+0.1)
773            assert!(
774                approx_gelu > -0.2,
775                "GELU({x}) = {approx_gelu} unexpectedly low"
776            );
777            assert!(
778                approx_gelu <= x + 0.1 || x < 0.0,
779                "GELU({x}) = {approx_gelu} unexpectedly high"
780            );
781        }
782    }
783
784    // -----------------------------------------------------------------------
785    // Swish = x * sigmoid(x)
786    // -----------------------------------------------------------------------
787
788    #[test]
789    fn test_swish_equals_x_times_sigmoid() {
790        for i in -5..=5 {
791            let x = i as f64;
792            let swish_val = ActivationFunction::swish(x);
793            let expected = x * ActivationFunction::sigmoid(x);
794            assert!(approx_eq(swish_val, expected, EPS));
795        }
796    }
797
798    #[test]
799    fn test_swish_zero() {
800        assert!(approx_eq(ActivationFunction::swish(0.0), 0.0, EPS));
801    }
802
803    // -----------------------------------------------------------------------
804    // Mish — positive domain and zero
805    // -----------------------------------------------------------------------
806
807    #[test]
808    fn test_mish_zero() {
809        assert!(approx_eq(ActivationFunction::mish(0.0), 0.0, EPS));
810    }
811
812    #[test]
813    fn test_mish_positive_domain_greater_than_zero() {
814        // Mish of a positive value should itself be positive.
815        for i in 1..=10 {
816            let x = i as f64;
817            assert!(
818                ActivationFunction::mish(x) > 0.0,
819                "mish({x}) should be positive"
820            );
821        }
822    }
823
824    #[test]
825    fn test_mish_monotone_positive() {
826        // Mish should be (roughly) monotone increasing for positive x.
827        let mut prev = ActivationFunction::mish(0.0);
828        for i in 1..=20 {
829            let cur = ActivationFunction::mish(i as f64 * 0.5);
830            assert!(cur >= prev, "Mish not monotone at x={}", i as f64 * 0.5);
831            prev = cur;
832        }
833    }
834
835    // -----------------------------------------------------------------------
836    // HardSwish
837    // -----------------------------------------------------------------------
838
839    #[test]
840    fn test_hard_swish_clamp_negative() {
841        // x <= -3 → 0
842        assert!(approx_eq(ActivationFunction::hard_swish(-3.0), 0.0, EPS));
843        assert!(approx_eq(ActivationFunction::hard_swish(-5.0), 0.0, EPS));
844    }
845
846    #[test]
847    fn test_hard_swish_clamp_positive() {
848        // x >= 3 → x
849        assert!(approx_eq(ActivationFunction::hard_swish(3.0), 3.0, EPS));
850        assert!(approx_eq(ActivationFunction::hard_swish(6.0), 6.0, EPS));
851    }
852
853    #[test]
854    fn test_hard_swish_zero() {
855        // x=0 → 0 * relu6(3)/6 = 0 * 3/6 = 0
856        assert!(approx_eq(ActivationFunction::hard_swish(0.0), 0.0, EPS));
857    }
858
859    // -----------------------------------------------------------------------
860    // Linear / Identity
861    // -----------------------------------------------------------------------
862
863    #[test]
864    fn test_linear_forward() {
865        let mut af = make_af(ActivationType::Linear);
866        let input = vec![1.0, -2.0, std::f64::consts::PI];
867        let out = af.forward(&input);
868        assert_eq!(out, input);
869    }
870
871    #[test]
872    fn test_linear_derivative() {
873        let af = make_af(ActivationType::Linear);
874        let d = af.derivative(&[1.0, 2.0, 3.0]);
875        for v in d {
876            assert!(approx_eq(v, 1.0, EPS));
877        }
878    }
879
880    // -----------------------------------------------------------------------
881    // Threshold
882    // -----------------------------------------------------------------------
883
884    #[test]
885    fn test_threshold_above() {
886        let mut af = make_af(ActivationType::Threshold(0.5, 1.0));
887        let out = af.forward(&[0.6, 1.0, 2.0]);
888        for v in out {
889            assert!(approx_eq(v, 1.0, EPS));
890        }
891    }
892
893    #[test]
894    fn test_threshold_below() {
895        let mut af = make_af(ActivationType::Threshold(0.5, 1.0));
896        let out = af.forward(&[0.0, 0.5, 0.4]);
897        for v in out {
898            assert!(approx_eq(v, 0.0, EPS));
899        }
900    }
901
902    #[test]
903    fn test_threshold_exact() {
904        // x == threshold should NOT trigger → 0.0
905        let mut af = make_af(ActivationType::Threshold(1.0, 42.0));
906        let out = af.forward(&[1.0]);
907        assert!(approx_eq(out[0], 0.0, EPS));
908    }
909
910    // -----------------------------------------------------------------------
911    // Vectorised apply / in-place
912    // -----------------------------------------------------------------------
913
914    #[test]
915    fn test_apply_inplace_relu() {
916        let mut af = make_af(ActivationType::ReLU);
917        let mut data = vec![-1.0, 2.0, -3.0, 4.0];
918        af.apply_inplace(&mut data);
919        assert_eq!(data, vec![0.0, 2.0, 0.0, 4.0]);
920    }
921
922    #[test]
923    fn test_apply_inplace_sigmoid() {
924        let mut af = make_af(ActivationType::Sigmoid);
925        let mut data = vec![0.0];
926        af.apply_inplace(&mut data);
927        assert!(approx_eq(data[0], 0.5, EPS));
928    }
929
930    #[test]
931    fn test_apply_inplace_softmax_sums_one() {
932        let mut af = make_af(ActivationType::Softmax);
933        let mut data = vec![1.0, 2.0, 3.0];
934        af.apply_inplace(&mut data);
935        let sum: f64 = data.iter().sum();
936        assert!(approx_eq(sum, 1.0, 1e-12));
937    }
938
939    // -----------------------------------------------------------------------
940    // Stats tracking
941    // -----------------------------------------------------------------------
942
943    #[test]
944    fn test_stats_total_calls() {
945        let mut af = make_af(ActivationType::Sigmoid);
946        let _ = af.forward(&[1.0]);
947        let _ = af.forward(&[2.0, 3.0]);
948        assert_eq!(af.stats().total_calls, 2);
949    }
950
951    #[test]
952    fn test_stats_total_elements() {
953        let mut af = make_af(ActivationType::Tanh);
954        let _ = af.forward(&[1.0, 2.0, 3.0]);
955        assert_eq!(af.stats().total_elements, 3);
956    }
957
958    #[test]
959    fn test_stats_inplace_increments() {
960        let mut af = make_af(ActivationType::Linear);
961        let mut data = vec![1.0; 5];
962        af.apply_inplace(&mut data);
963        assert_eq!(af.stats().total_calls, 1);
964        assert_eq!(af.stats().total_elements, 5);
965    }
966
967    // -----------------------------------------------------------------------
968    // PI usage sanity
969    // -----------------------------------------------------------------------
970
971    #[test]
972    fn test_pi_used_in_gelu() {
973        // GELU uses PI internally; verify the constant is consistent.
974        let c = (2.0_f64 / PI).sqrt();
975        assert!(approx_eq(c, 0.7978845608028654, 1e-12));
976    }
977}