use crate::error::{KernelError, Result};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Activation {
Identity,
ReLU,
Tanh,
}
impl Activation {
pub fn apply_inplace(&self, values: &mut [f64]) {
match self {
Self::Identity => {}
Self::ReLU => {
for v in values.iter_mut() {
if *v < 0.0 {
*v = 0.0;
}
}
}
Self::Tanh => {
for v in values.iter_mut() {
*v = v.tanh();
}
}
}
}
pub fn apply_scalar(&self, v: f64) -> f64 {
match self {
Self::Identity => v,
Self::ReLU => v.max(0.0),
Self::Tanh => v.tanh(),
}
}
pub fn derivative(&self, pre_activation: f64) -> f64 {
match self {
Self::Identity => 1.0,
Self::ReLU => {
if pre_activation > 0.0 {
1.0
} else {
0.0
}
}
Self::Tanh => {
let t = pre_activation.tanh();
1.0 - t * t
}
}
}
pub fn name(&self) -> &'static str {
match self {
Self::Identity => "Identity",
Self::ReLU => "ReLU",
Self::Tanh => "Tanh",
}
}
}
#[derive(Clone, Debug)]
pub struct DenseLayer {
pub weights: Vec<Vec<f64>>,
pub biases: Vec<f64>,
pub activation: Activation,
}
impl DenseLayer {
pub fn new(weights: Vec<Vec<f64>>, biases: Vec<f64>, activation: Activation) -> Result<Self> {
if weights.is_empty() {
return Err(KernelError::InvalidParameter {
parameter: "weights".to_string(),
value: "[]".to_string(),
reason: "dense layer must have at least one output".to_string(),
});
}
let input_dim = weights[0].len();
if input_dim == 0 {
return Err(KernelError::InvalidParameter {
parameter: "weights[0]".to_string(),
value: "[]".to_string(),
reason: "dense layer must have at least one input".to_string(),
});
}
for (i, row) in weights.iter().enumerate() {
if row.len() != input_dim {
return Err(KernelError::DimensionMismatch {
expected: vec![input_dim],
got: vec![row.len()],
context: format!("DenseLayer::new weights[{}]", i),
});
}
for (j, &w) in row.iter().enumerate() {
if !w.is_finite() {
return Err(KernelError::InvalidParameter {
parameter: format!("weights[{}][{}]", i, j),
value: w.to_string(),
reason: "weights must be finite".to_string(),
});
}
}
}
if biases.len() != weights.len() {
return Err(KernelError::DimensionMismatch {
expected: vec![weights.len()],
got: vec![biases.len()],
context: "DenseLayer::new biases length".to_string(),
});
}
for (i, &b) in biases.iter().enumerate() {
if !b.is_finite() {
return Err(KernelError::InvalidParameter {
parameter: format!("biases[{}]", i),
value: b.to_string(),
reason: "biases must be finite".to_string(),
});
}
}
Ok(Self {
weights,
biases,
activation,
})
}
pub fn input_dim(&self) -> usize {
self.weights[0].len()
}
pub fn output_dim(&self) -> usize {
self.weights.len()
}
pub fn activation(&self) -> Activation {
self.activation
}
pub fn forward(&self, input: &[f64]) -> Result<Vec<f64>> {
let (_, post) = self.forward_with_preactivation(input)?;
Ok(post)
}
pub fn forward_with_preactivation(&self, input: &[f64]) -> Result<(Vec<f64>, Vec<f64>)> {
if input.len() != self.input_dim() {
return Err(KernelError::DimensionMismatch {
expected: vec![self.input_dim()],
got: vec![input.len()],
context: "DenseLayer::forward input length".to_string(),
});
}
let mut pre = Vec::with_capacity(self.output_dim());
for (row, &bias) in self.weights.iter().zip(self.biases.iter()) {
let mut acc = bias;
for (w, x) in row.iter().zip(input.iter()) {
acc += w * x;
}
pre.push(acc);
}
let mut post = pre.clone();
self.activation.apply_inplace(&mut post);
Ok((pre, post))
}
pub fn parameter_count(&self) -> usize {
self.output_dim() * self.input_dim() + self.output_dim()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn activation_relu_clamps_negative_to_zero() {
let mut v = vec![-2.0, -0.5, 0.0, 0.5, 3.5];
Activation::ReLU.apply_inplace(&mut v);
assert_eq!(v, vec![0.0, 0.0, 0.0, 0.5, 3.5]);
}
#[test]
fn activation_tanh_matches_std() {
let v = Activation::Tanh.apply_scalar(0.7);
assert!((v - 0.7_f64.tanh()).abs() < 1e-12);
}
#[test]
fn activation_derivative_identity_is_one() {
assert_eq!(Activation::Identity.derivative(-5.0), 1.0);
assert_eq!(Activation::Identity.derivative(7.0), 1.0);
}
#[test]
fn dense_layer_forward_identity() {
let layer = DenseLayer::new(
vec![vec![1.0, 0.0], vec![0.0, 1.0]],
vec![0.0, 0.0],
Activation::Identity,
)
.expect("valid layer");
let out = layer.forward(&[3.0, 4.0]).expect("forward");
assert_eq!(out, vec![3.0, 4.0]);
}
#[test]
fn dense_layer_rejects_dim_mismatch_input() {
let layer =
DenseLayer::new(vec![vec![1.0, 2.0]], vec![0.5], Activation::Identity).expect("valid");
let err = layer
.forward(&[1.0, 2.0, 3.0])
.expect_err("must fail on 3-dim input");
assert!(matches!(err, KernelError::DimensionMismatch { .. }));
}
#[test]
fn dense_layer_rejects_jagged_weights() {
let err = DenseLayer::new(
vec![vec![1.0, 2.0], vec![3.0]],
vec![0.0, 0.0],
Activation::Identity,
)
.expect_err("must fail");
assert!(matches!(err, KernelError::DimensionMismatch { .. }));
}
#[test]
fn dense_layer_rejects_bias_length_mismatch() {
let err = DenseLayer::new(
vec![vec![1.0, 2.0], vec![3.0, 4.0]],
vec![0.0],
Activation::Identity,
)
.expect_err("must fail");
assert!(matches!(err, KernelError::DimensionMismatch { .. }));
}
#[test]
fn dense_layer_parameter_count() {
let layer = DenseLayer::new(
vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]],
vec![0.1, 0.2],
Activation::ReLU,
)
.expect("valid");
assert_eq!(layer.parameter_count(), 8);
}
}