//! # Activation Functions
//!
//! This module provides a set of activation functions for use in neural
//! networks.
//!
//! ## Usage
//!
//! You can use the `activate` and `activate_derivative` methods to apply the
//! activation functions:
//!
//! ```rust
//! use smyl::maths::activation::{Function, ReLU, Sigmoid};
//!
//! let input: f64 = 5.0;
//! let relu_output = ReLU::activate(input);
//! let sigmoid_output = Sigmoid::activate(input);
//! let relu_derivative = ReLU::activate_derivative(input);
//! let sigmoid_derivative = Sigmoid::activate_derivative(input);
//! ```
use std::fmt::Debug;

/// A trait that defines the behavior of an activation function.
pub trait Function<T>: Default + Debug + Clone + Copy {
    /// Applies the activation function to the input `x`.
    fn activate(x: T) -> T;

    /// Calculates the derivative of the activation function at the input `x`.
    fn activate_derivative(x: T) -> T;
}

/// The Rectified Linear Unit (`ReLU`) activation function.
#[derive(Default, Debug, Copy, Clone)]
pub struct ReLU;

impl<T> Function<T> for ReLU
where
    T: Default + PartialOrd + From<i32>,
{
    /// Applies the `ReLU` activation function to the input `x`.
    ///
    /// # Examples
    ///
    /// ```
    /// use smyl::maths::activation::{Function, ReLU};
    ///
    /// assert_eq!(ReLU::activate(5.0), 5.0);
    /// assert_eq!(ReLU::activate(-5.0), 0.0);
    /// ```
    fn activate(x: T) -> T {
        if x >= T::default() {
            x
        } else {
            T::default()
        }
    }

    /// Calculates the derivative of the `ReLU` activation function at the input
    /// `x`.
    ///
    /// # Examples
    ///
    /// ```
    /// use smyl::maths::activation::{Function, ReLU};
    ///
    /// assert_eq!(ReLU::activate_derivative(5.0), 1.0);
    /// assert_eq!(ReLU::activate_derivative(-5.0), 0.0);
    /// ```
    fn activate_derivative(x: T) -> T {
        if x > T::default() {
            T::from(1)
        } else {
            T::default()
        }
    }
}

/// The Sigmoid activation function.
#[derive(Default, Debug, Copy, Clone)]
pub struct Sigmoid;

impl Function<f64> for Sigmoid {
    /// Applies the Sigmoid activation function to the input `x`.
    ///
    /// # Examples
    ///
    /// ```
    /// use smyl::maths::activation::{Function, Sigmoid};
    ///
    /// assert_eq!(Sigmoid::activate(0.0), 0.5);
    /// assert!(Sigmoid::activate(5.0) > 0.9);
    /// assert!(Sigmoid::activate(-5.0) < 0.1);
    /// ```
    #[inline]
    fn activate(x: f64) -> f64 { 1.0 / (1.0 + f64::exp(-x)) }

    /// Calculates the derivative of the Sigmoid activation function at the
    /// input `x`.
    ///
    /// # Examples
    ///
    /// ```
    /// use smyl::maths::activation::{Function, Sigmoid};
    ///
    /// let x = 0.0;
    /// let derivative = Sigmoid::activate_derivative(x);
    /// assert!((derivative - 0.25).abs() < 1e-10);
    /// ```
    #[inline]
    fn activate_derivative(x: f64) -> f64 {
        let sigmoid_x = Self::activate(x);
        sigmoid_x * (1.0 - sigmoid_x)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    pub fn assert_eq_f64(value: f64, expected: f64) {
        assert!(
            (expected - value).abs() < f64::EPSILON,
            "Expected {expected}, got {value}"
        );
    }

    mod relu {
        use super::*;

        #[test]
        fn test_activate() {
            assert_eq_f64(ReLU::activate(-10.0), 0.0);
            assert_eq_f64(ReLU::activate(-5.0), 0.0);
            assert_eq_f64(ReLU::activate(0.0), 0.0);
            assert_eq_f64(ReLU::activate(1.0), 1.0);
            assert_eq_f64(ReLU::activate(5.0), 5.0);
            assert_eq_f64(ReLU::activate(10.0), 10.0);
            assert_eq_f64(ReLU::activate(3.6), 3.6);
        }

        #[test]
        fn test_activate_derivative() {
            assert_eq_f64(ReLU::activate(-10.0), 0.0);
            assert_eq_f64(ReLU::activate(-5.0), 0.0);
            assert_eq_f64(ReLU::activate(0.0), 0.0);
            assert_eq_f64(ReLU::activate(1.0), 1.0);
            assert_eq_f64(ReLU::activate(5.0), 5.0);
            assert_eq_f64(ReLU::activate(10.0), 10.0);
            assert_eq_f64(ReLU::activate(3.6), 3.6);
        }
    }

    mod sigmoid {
        use super::*;

        #[test]
        fn test_activate() {
            assert_eq_f64(Sigmoid::activate(-10.0), 0.000_045_397_868_702_434_395);
            assert_eq_f64(Sigmoid::activate(-5.0), 0.006_692_850_924_284_855_4);
            assert_eq_f64(Sigmoid::activate(0.0), 0.5);
            assert_eq_f64(Sigmoid::activate(1.0), 0.731_058_578_630_004_9);
            assert_eq_f64(Sigmoid::activate(5.0), 0.993_307_149_075_715_3);
            assert_eq_f64(Sigmoid::activate(0.6), 0.645_656_306_225_795_4);
        }

        #[test]
        fn test_activate_derivative() {
            assert_eq_f64(Sigmoid::activate_derivative(-10.0), 0.000_045_395_807_736);
            assert_eq_f64(Sigmoid::activate_derivative(-5.0), 0.006_648_056_670_79);
            assert_eq_f64(Sigmoid::activate_derivative(0.0), 0.25);
            assert_eq_f64(Sigmoid::activate_derivative(1.0), 0.196_611_933_241_481_85);
            assert_eq_f64(Sigmoid::activate_derivative(5.0), 0.006_648_056_670_79);
            assert_eq_f64(Sigmoid::activate_derivative(0.6), 0.228_784_240_456_657_32);
        }
    }
}
