Skip to main content

scirs2_core/array_protocol/
neural.rs

1// Copyright (c) 2025, `SciRS2` Team
2//
3// Licensed under the Apache License, Version 2.0
4// (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
5//
6
7//! Neural network layers and models using the array protocol.
8//!
9//! This module provides neural network layers and models that work with
10//! any array type implementing the ArrayProtocol trait.
11
12use ::ndarray::{Array, Array1, Array4, ArrayD, Axis, Ix1, Ix2, Ix4, IxDyn, Zip};
13
14use rand::{Rng, RngExt, SeedableRng};
15
16use crate::array_protocol::ml_ops::ActivationFunc;
17use crate::array_protocol::operations::OperationError;
18use crate::array_protocol::{ArrayProtocol, NdarrayWrapper};
19
20/// Gradients produced by a single layer's [`Layer::backward`] pass.
21pub struct LayerGrad {
22    /// Gradient of the loss with respect to this layer's input, to be
23    /// passed as `grad_output` to the previous layer's `backward`.
24    pub grad_input: Box<dyn ArrayProtocol>,
25
26    /// Gradients of the loss with respect to each of this layer's
27    /// parameters, in the same order as [`Layer::parameters`] /
28    /// [`Layer::parameter_names`].
29    pub grad_params: Vec<Box<dyn ArrayProtocol>>,
30}
31
32/// Trait for neural network layers.
33pub trait Layer: Send + Sync {
34    /// Forward pass through the layer.
35    /// Get the layer type name for serialization.
36    fn layer_type(&self) -> &str;
37
38    fn forward(&self, inputs: &dyn ArrayProtocol)
39        -> Result<Box<dyn ArrayProtocol>, OperationError>;
40
41    /// Backward pass through the layer: given the layer's original `input`
42    /// (as seen by the preceding `forward` call) and `grad_output` (the
43    /// gradient of the loss with respect to this layer's output), compute
44    /// the gradient with respect to the input and every parameter.
45    ///
46    /// `forward` does not cache any intermediate state, so implementations
47    /// that need it (e.g. a pre-activation value) recompute it from `input`
48    /// — cheap relative to the training pipeline's own forward pass, and it
49    /// keeps `Layer` object-safe and side-effect-free.
50    ///
51    /// The default returns [`OperationError::NotImplemented`] so that
52    /// existing third-party `Layer` implementations continue to compile;
53    /// every layer defined in this module overrides it with a real
54    /// implementation (or, where forward itself is already a documented
55    /// simplification, an explicit and equally honest error).
56    fn backward(
57        &self,
58        _input: &dyn ArrayProtocol,
59        _grad_output: &dyn ArrayProtocol,
60    ) -> Result<LayerGrad, OperationError> {
61        Err(OperationError::NotImplemented(format!(
62            "backward() is not implemented for layer type '{layertype}'",
63            layertype = self.layer_type()
64        )))
65    }
66
67    /// Get the layer's parameters.
68    fn parameters(&self) -> Vec<Box<dyn ArrayProtocol>>;
69
70    /// Get mutable references to the layer's parameters.
71    fn parameters_mut(&mut self) -> Vec<&mut Box<dyn ArrayProtocol>>;
72
73    /// Update a specific parameter by name
74    fn update_parameter(
75        &mut self,
76        name: &str,
77        value: Box<dyn ArrayProtocol>,
78    ) -> Result<(), OperationError>;
79
80    /// Get parameter names
81    fn parameter_names(&self) -> Vec<String>;
82
83    /// Set the layer to training mode.
84    fn train(&mut self);
85
86    /// Set the layer to evaluation mode.
87    fn eval(&mut self);
88
89    /// Check if the layer is in training mode.
90    fn is_training(&self) -> bool;
91
92    /// Get the layer's name.
93    fn name(&self) -> &str;
94}
95
96/// Downcasts an `ArrayProtocol` object to an owned `f64` array, regardless
97/// of its concrete (static or dynamic) dimensionality. Used internally by
98/// `Layer::backward` implementations, which only need to do plain
99/// elementwise/linear-algebra math on the underlying data.
100fn as_f64_array(a: &dyn ArrayProtocol) -> Result<ArrayD<f64>, OperationError> {
101    if let Some(w) = a.as_any().downcast_ref::<NdarrayWrapper<f64, IxDyn>>() {
102        return Ok(w.as_array().clone());
103    }
104    if let Some(w) = a.as_any().downcast_ref::<NdarrayWrapper<f64, Ix1>>() {
105        return Ok(w.as_array().clone().into_dyn());
106    }
107    if let Some(w) = a.as_any().downcast_ref::<NdarrayWrapper<f64, Ix2>>() {
108        return Ok(w.as_array().clone().into_dyn());
109    }
110    if let Some(w) = a.as_any().downcast_ref::<NdarrayWrapper<f64, Ix4>>() {
111        return Ok(w.as_array().clone().into_dyn());
112    }
113    Err(OperationError::TypeMismatch(
114        "Layer::backward currently only supports f64 NdarrayWrapper arrays (Ix1/Ix2/Ix4/IxDyn)"
115            .to_string(),
116    ))
117}
118
119/// Wraps `data` to match the concrete dimensionality that `reference`'s own
120/// `NdarrayWrapper` uses, so the result stays dispatch-compatible with
121/// operations (like `subtract`/`multiply_by_scalar_f64` in a gradient-descent
122/// update) that require both operands to share the same concrete `D`.
123/// Falls back to `IxDyn` when `reference` isn't one of the recognized
124/// concrete dimensionalities.
125fn wrap_like(
126    reference: &dyn ArrayProtocol,
127    data: ArrayD<f64>,
128) -> Result<Box<dyn ArrayProtocol>, OperationError> {
129    if reference
130        .as_any()
131        .downcast_ref::<NdarrayWrapper<f64, Ix1>>()
132        .is_some()
133    {
134        let arr = data
135            .into_dimensionality::<Ix1>()
136            .map_err(|e| OperationError::ShapeMismatch(format!("wrap_like (Ix1): {e}")))?;
137        return Ok(Box::new(NdarrayWrapper::new(arr)));
138    }
139    if reference
140        .as_any()
141        .downcast_ref::<NdarrayWrapper<f64, Ix2>>()
142        .is_some()
143    {
144        let arr = data
145            .into_dimensionality::<Ix2>()
146            .map_err(|e| OperationError::ShapeMismatch(format!("wrap_like (Ix2): {e}")))?;
147        return Ok(Box::new(NdarrayWrapper::new(arr)));
148    }
149    if reference
150        .as_any()
151        .downcast_ref::<NdarrayWrapper<f64, Ix4>>()
152        .is_some()
153    {
154        let arr = data
155            .into_dimensionality::<Ix4>()
156            .map_err(|e| OperationError::ShapeMismatch(format!("wrap_like (Ix4): {e}")))?;
157        return Ok(Box::new(NdarrayWrapper::new(arr)));
158    }
159    Ok(Box::new(NdarrayWrapper::new(data)))
160}
161
162/// Elementwise gradient of `ActivationFunc` (see `ml_ops::apply_activation`
163/// for the matching forward computation): given the pre-activation `z` and
164/// `grad_y` (`dL/dy` where `y = activation(z)`), returns `dL/dz`.
165fn activation_grad(
166    z: &ArrayD<f64>,
167    act: ActivationFunc,
168    grad_y: &ArrayD<f64>,
169) -> Result<ArrayD<f64>, OperationError> {
170    match act {
171        ActivationFunc::ReLU => {
172            Ok(Zip::from(z)
173                .and(grad_y)
174                .map_collect(|&zv, &gy| if zv > 0.0 { gy } else { 0.0 }))
175        }
176        ActivationFunc::Sigmoid => Ok(Zip::from(z).and(grad_y).map_collect(|&zv, &gy| {
177            let s = 1.0 / (1.0 + (-zv).exp());
178            gy * s * (1.0 - s)
179        })),
180        ActivationFunc::Tanh => Ok(Zip::from(z).and(grad_y).map_collect(|&zv, &gy| {
181            let t = zv.tanh();
182            gy * (1.0 - t * t)
183        })),
184        ActivationFunc::LeakyReLU(alpha) => Ok(Zip::from(z)
185            .and(grad_y)
186            .map_collect(|&zv, &gy| if zv > 0.0 { gy } else { gy * alpha })),
187        ActivationFunc::Softmax => Err(OperationError::NotImplemented(
188            "Softmax backward is not implemented: ml_ops::apply_activation's multi-dimensional \
189             Softmax normalizes along the array's last axis regardless of what that axis \
190             semantically represents for a given layer, so a generic gradient here would not \
191             reliably correspond to forward's behavior. Use an element-wise activation (ReLU, \
192             Sigmoid, Tanh, or LeakyReLU) on layers that need to be part of a differentiated model."
193                .to_string(),
194        )),
195    }
196}
197
198/// Linear (dense/fully-connected) layer.
199pub struct Linear {
200    /// The layer's name.
201    name: String,
202
203    /// Weight matrix.
204    weights: Box<dyn ArrayProtocol>,
205
206    /// Bias vector.
207    bias: Option<Box<dyn ArrayProtocol>>,
208
209    /// Activation function.
210    activation: Option<ActivationFunc>,
211
212    /// Training mode flag.
213    training: bool,
214}
215
216impl Linear {
217    /// Create a new linear layer.
218    pub fn new(
219        name: &str,
220        weights: Box<dyn ArrayProtocol>,
221        bias: Option<Box<dyn ArrayProtocol>>,
222        activation: Option<ActivationFunc>,
223    ) -> Self {
224        Self {
225            name: name.to_string(),
226            weights,
227            bias,
228            activation,
229            training: true,
230        }
231    }
232
233    /// Create a new linear layer with randomly initialized weights.
234    pub fn new_random(
235        name: &str,
236        in_features: usize,
237        out_features: usize,
238        withbias: bool,
239        activation: Option<ActivationFunc>,
240    ) -> Self {
241        // Create random weights using Xavier/Glorot initialization
242        let scale = (6.0 / (in_features + out_features) as f64).sqrt();
243        let mut rng = rand::rng();
244        let weights = Array::from_shape_fn((out_features, in_features), |_| {
245            (rng.random::<f64>() * 2.0_f64 - 1.0) * scale
246        });
247
248        // Create bias if needed
249        let bias = if withbias {
250            let bias_array: Array<f64, Ix1> = Array::zeros(out_features);
251            Some(Box::new(NdarrayWrapper::new(bias_array)) as Box<dyn ArrayProtocol>)
252        } else {
253            None
254        };
255
256        Self {
257            name: name.to_string(),
258            weights: Box::new(NdarrayWrapper::new(weights)),
259            bias,
260            activation,
261            training: true,
262        }
263    }
264}
265
266impl Layer for Linear {
267    fn layer_type(&self) -> &str {
268        "Linear"
269    }
270
271    fn forward(
272        &self,
273        inputs: &dyn ArrayProtocol,
274    ) -> Result<Box<dyn ArrayProtocol>, OperationError> {
275        // Perform matrix multiplication: y = Wx
276        let mut result = crate::array_protocol::matmul(self.weights.as_ref(), inputs)?;
277
278        // Add bias if present: y = Wx + b
279        if let Some(bias) = &self.bias {
280            // Create a temporary for the intermediate result
281            let intermediate = crate::array_protocol::add(result.as_ref(), bias.as_ref())?;
282            result = intermediate;
283        }
284
285        // Apply activation if present
286        if let Some(act_fn) = self.activation {
287            // Create a temporary for the intermediate result
288            let intermediate = crate::array_protocol::ml_ops::activation(result.as_ref(), act_fn)?;
289            result = intermediate;
290        }
291
292        Ok(result)
293    }
294
295    fn backward(
296        &self,
297        input: &dyn ArrayProtocol,
298        grad_output: &dyn ArrayProtocol,
299    ) -> Result<LayerGrad, OperationError> {
300        let x = as_f64_array(input)?
301            .into_dimensionality::<Ix2>()
302            .map_err(|e| {
303                OperationError::ShapeMismatch(format!(
304                    "Linear::backward expects a 2D input (in_features, batch): {e}"
305                ))
306            })?;
307        let w = as_f64_array(self.weights.as_ref())?
308            .into_dimensionality::<Ix2>()
309            .map_err(|e| {
310                OperationError::ShapeMismatch(format!("Linear::backward expects 2D weights: {e}"))
311            })?;
312
313        // Recompute the pre-activation z = Wx [+ b], since `forward` doesn't
314        // cache it and it's needed to evaluate the activation's derivative.
315        let mut z = w.dot(&x);
316        if let Some(bias) = &self.bias {
317            let b = as_f64_array(bias.as_ref())?
318                .into_dimensionality::<Ix1>()
319                .map_err(|e| {
320                    OperationError::ShapeMismatch(format!(
321                        "Linear::backward expects a 1D bias: {e}"
322                    ))
323                })?;
324            for mut col in z.axis_iter_mut(Axis(1)) {
325                col += &b;
326            }
327        }
328
329        let grad_y = as_f64_array(grad_output)?
330            .into_dimensionality::<Ix2>()
331            .map_err(|e| {
332                OperationError::ShapeMismatch(format!(
333                    "Linear::backward expects a 2D grad_output: {e}"
334                ))
335            })?;
336
337        let grad_z = match self.activation {
338            Some(act) => activation_grad(&z.into_dyn(), act, &grad_y.into_dyn())?
339                .into_dimensionality::<Ix2>()
340                .map_err(|e| {
341                    OperationError::Other(format!(
342                        "internal shape error recovering activation_grad's result: {e}"
343                    ))
344                })?,
345            None => grad_y,
346        };
347
348        // dL/dW = dL/dz @ x^T ; dL/dx = W^T @ dL/dz
349        let grad_w = grad_z.dot(&x.t());
350        let grad_input = w.t().dot(&grad_z);
351
352        let mut grad_params = vec![wrap_like(self.weights.as_ref(), grad_w.into_dyn())?];
353        if let Some(bias) = &self.bias {
354            // dL/db = sum over the batch axis of dL/dz
355            let grad_b = grad_z.sum_axis(Axis(1));
356            grad_params.push(wrap_like(bias.as_ref(), grad_b.into_dyn())?);
357        }
358
359        Ok(LayerGrad {
360            grad_input: wrap_like(input, grad_input.into_dyn())?,
361            grad_params,
362        })
363    }
364
365    fn parameters(&self) -> Vec<Box<dyn ArrayProtocol>> {
366        let mut params = vec![self.weights.clone()];
367        if let Some(bias) = &self.bias {
368            params.push(bias.clone());
369        }
370        params
371    }
372
373    fn parameters_mut(&mut self) -> Vec<&mut Box<dyn ArrayProtocol>> {
374        let mut params = vec![&mut self.weights];
375        if let Some(bias) = &mut self.bias {
376            params.push(bias);
377        }
378        params
379    }
380
381    fn update_parameter(
382        &mut self,
383        name: &str,
384        value: Box<dyn ArrayProtocol>,
385    ) -> Result<(), OperationError> {
386        match name {
387            "weights" => {
388                self.weights = value;
389                Ok(())
390            }
391            "bias" => {
392                self.bias = Some(value);
393                Ok(())
394            }
395            _ => Err(OperationError::Other(format!("Unknown parameter: {name}"))),
396        }
397    }
398
399    fn parameter_names(&self) -> Vec<String> {
400        let mut names = vec!["weights".to_string()];
401        if self.bias.is_some() {
402            names.push("bias".to_string());
403        }
404        names
405    }
406
407    fn train(&mut self) {
408        self.training = true;
409    }
410
411    fn eval(&mut self) {
412        self.training = false;
413    }
414
415    fn is_training(&self) -> bool {
416        self.training
417    }
418
419    fn name(&self) -> &str {
420        &self.name
421    }
422}
423
424/// Convolutional layer.
425pub struct Conv2D {
426    /// The layer's name.
427    name: String,
428
429    /// Filters tensor.
430    filters: Box<dyn ArrayProtocol>,
431
432    /// Bias vector.
433    bias: Option<Box<dyn ArrayProtocol>>,
434
435    /// Stride for the convolution.
436    stride: (usize, usize),
437
438    /// Padding for the convolution.
439    padding: (usize, usize),
440
441    /// Activation function.
442    activation: Option<ActivationFunc>,
443
444    /// Training mode flag.
445    training: bool,
446}
447
448impl Conv2D {
449    /// Create a new convolutional layer.
450    pub fn new(
451        name: &str,
452        filters: Box<dyn ArrayProtocol>,
453        bias: Option<Box<dyn ArrayProtocol>>,
454        stride: (usize, usize),
455        padding: (usize, usize),
456        activation: Option<ActivationFunc>,
457    ) -> Self {
458        Self {
459            name: name.to_string(),
460            filters,
461            bias,
462            stride,
463            padding,
464            activation,
465            training: true,
466        }
467    }
468
469    /// Create a new convolutional layer with randomly initialized weights.
470    #[allow(clippy::too_many_arguments)]
471    pub fn withshape(
472        name: &str,
473        filter_height: usize,
474        filter_width: usize,
475        in_channels: usize,
476        out_channels: usize,
477        stride: (usize, usize),
478        padding: (usize, usize),
479        withbias: bool,
480        activation: Option<ActivationFunc>,
481    ) -> Self {
482        // Create random filters using Kaiming initialization
483        let fan_in = filter_height * filter_width * in_channels;
484        let scale = (2.0 / fan_in as f64).sqrt();
485        let mut rng = rand::rng();
486        let filters = Array::from_shape_fn(
487            (filter_height, filter_width, in_channels, out_channels),
488            |_| (rng.random::<f64>() * 2.0_f64 - 1.0) * scale,
489        );
490
491        // Create bias if needed
492        let bias = if withbias {
493            let bias_array: Array<f64, Ix1> = Array::zeros(out_channels);
494            Some(Box::new(NdarrayWrapper::new(bias_array)) as Box<dyn ArrayProtocol>)
495        } else {
496            None
497        };
498
499        Self {
500            name: name.to_string(),
501            filters: Box::new(NdarrayWrapper::new(filters)),
502            bias,
503            stride,
504            padding,
505            activation,
506            training: true,
507        }
508    }
509}
510
511impl Layer for Conv2D {
512    fn layer_type(&self) -> &str {
513        "Conv2D"
514    }
515
516    fn forward(
517        &self,
518        inputs: &dyn ArrayProtocol,
519    ) -> Result<Box<dyn ArrayProtocol>, OperationError> {
520        // Perform convolution
521        let mut result = crate::array_protocol::ml_ops::conv2d(
522            inputs,
523            self.filters.as_ref(),
524            self.stride,
525            self.padding,
526        )?;
527
528        // Add bias if present
529        if let Some(bias) = &self.bias {
530            result = crate::array_protocol::add(result.as_ref(), bias.as_ref())?;
531        }
532
533        // Apply activation if present
534        if let Some(act_fn) = self.activation {
535            result = crate::array_protocol::ml_ops::activation(result.as_ref(), act_fn)?;
536        }
537
538        Ok(result)
539    }
540
541    #[allow(clippy::needless_range_loop)]
542    fn backward(
543        &self,
544        input: &dyn ArrayProtocol,
545        grad_output: &dyn ArrayProtocol,
546    ) -> Result<LayerGrad, OperationError> {
547        let inputarr = as_f64_array(input)?
548            .into_dimensionality::<Ix4>()
549            .map_err(|e| {
550                OperationError::ShapeMismatch(format!("Conv2D::backward expects a 4D input: {e}"))
551            })?;
552        let filters = as_f64_array(self.filters.as_ref())?
553            .into_dimensionality::<Ix4>()
554            .map_err(|e| {
555                OperationError::ShapeMismatch(format!("Conv2D::backward expects 4D filters: {e}"))
556            })?;
557
558        let (stride_h, stride_w) = self.stride;
559        let (pad_h, pad_w) = self.padding;
560        let batch_size = inputarr.shape()[0];
561        let input_height = inputarr.shape()[1];
562        let input_width = inputarr.shape()[2];
563        let input_channels = inputarr.shape()[3];
564        let filter_height = filters.shape()[0];
565        let filter_width = filters.shape()[1];
566        let filter_out_channels = filters.shape()[3];
567        let out_height = (input_height - filter_height + 2 * pad_h) / stride_h + 1;
568        let out_width = (input_width - filter_width + 2 * pad_w) / stride_w + 1;
569
570        // Recompute the pre-activation convolution output (forward doesn't
571        // cache it), needed to evaluate the activation's derivative.
572        let mut conv_out =
573            Array4::<f64>::zeros((batch_size, out_height, out_width, filter_out_channels));
574        for b in 0..batch_size {
575            for oc in 0..filter_out_channels {
576                for oh in 0..out_height {
577                    for ow in 0..out_width {
578                        let mut sum = 0.0;
579                        for fh in 0..filter_height {
580                            for fw in 0..filter_width {
581                                let in_h = (oh * stride_h) as i32 + fh as i32 - pad_h as i32;
582                                let in_w = (ow * stride_w) as i32 + fw as i32 - pad_w as i32;
583                                if in_h >= 0
584                                    && in_h < input_height as i32
585                                    && in_w >= 0
586                                    && in_w < input_width as i32
587                                {
588                                    for ic in 0..input_channels {
589                                        sum += inputarr[[b, in_h as usize, in_w as usize, ic]]
590                                            * filters[[fh, fw, ic, oc]];
591                                    }
592                                }
593                            }
594                        }
595                        conv_out[[b, oh, ow, oc]] = sum;
596                    }
597                }
598            }
599        }
600        if let Some(bias) = &self.bias {
601            let bias_arr = as_f64_array(bias.as_ref())?;
602            for b in 0..batch_size {
603                for oh in 0..out_height {
604                    for ow in 0..out_width {
605                        for oc in 0..filter_out_channels {
606                            conv_out[[b, oh, ow, oc]] += bias_arr[[oc]];
607                        }
608                    }
609                }
610            }
611        }
612
613        let grad_y = as_f64_array(grad_output)?
614            .into_dimensionality::<Ix4>()
615            .map_err(|e| {
616                OperationError::ShapeMismatch(format!(
617                    "Conv2D::backward expects a 4D grad_output: {e}"
618                ))
619            })?;
620
621        let grad_preact = match self.activation {
622            Some(act) => activation_grad(&conv_out.into_dyn(), act, &grad_y.into_dyn())?
623                .into_dimensionality::<Ix4>()
624                .map_err(|e| {
625                    OperationError::Other(format!(
626                        "internal shape error recovering activation_grad's result: {e}"
627                    ))
628                })?,
629            None => grad_y,
630        };
631
632        // Backward via loop transposition of the exact forward computation:
633        // every (b, oc, oh, ow, fh, fw, ic) contribution to `conv_out` above
634        // contributes symmetrically to `grad_filters` and `grad_input`.
635        let mut grad_input =
636            Array4::<f64>::zeros((batch_size, input_height, input_width, input_channels));
637        let mut grad_filters = Array4::<f64>::zeros((
638            filter_height,
639            filter_width,
640            input_channels,
641            filter_out_channels,
642        ));
643
644        for b in 0..batch_size {
645            for oc in 0..filter_out_channels {
646                for oh in 0..out_height {
647                    for ow in 0..out_width {
648                        let g = grad_preact[[b, oh, ow, oc]];
649                        for fh in 0..filter_height {
650                            for fw in 0..filter_width {
651                                let in_h = (oh * stride_h) as i32 + fh as i32 - pad_h as i32;
652                                let in_w = (ow * stride_w) as i32 + fw as i32 - pad_w as i32;
653                                if in_h >= 0
654                                    && in_h < input_height as i32
655                                    && in_w >= 0
656                                    && in_w < input_width as i32
657                                {
658                                    let (in_h, in_w) = (in_h as usize, in_w as usize);
659                                    for ic in 0..input_channels {
660                                        grad_filters[[fh, fw, ic, oc]] +=
661                                            g * inputarr[[b, in_h, in_w, ic]];
662                                        grad_input[[b, in_h, in_w, ic]] +=
663                                            g * filters[[fh, fw, ic, oc]];
664                                    }
665                                }
666                            }
667                        }
668                    }
669                }
670            }
671        }
672
673        let mut grad_params = vec![wrap_like(self.filters.as_ref(), grad_filters.into_dyn())?];
674        if let Some(bias) = &self.bias {
675            let mut grad_bias = Array1::<f64>::zeros(filter_out_channels);
676            for b in 0..batch_size {
677                for oh in 0..out_height {
678                    for ow in 0..out_width {
679                        for oc in 0..filter_out_channels {
680                            grad_bias[oc] += grad_preact[[b, oh, ow, oc]];
681                        }
682                    }
683                }
684            }
685            grad_params.push(wrap_like(bias.as_ref(), grad_bias.into_dyn())?);
686        }
687
688        Ok(LayerGrad {
689            grad_input: wrap_like(input, grad_input.into_dyn())?,
690            grad_params,
691        })
692    }
693
694    fn parameters(&self) -> Vec<Box<dyn ArrayProtocol>> {
695        let mut params = vec![self.filters.clone()];
696        if let Some(bias) = &self.bias {
697            params.push(bias.clone());
698        }
699        params
700    }
701
702    fn parameters_mut(&mut self) -> Vec<&mut Box<dyn ArrayProtocol>> {
703        let mut params = vec![&mut self.filters];
704        if let Some(bias) = &mut self.bias {
705            params.push(bias);
706        }
707        params
708    }
709
710    fn update_parameter(
711        &mut self,
712        name: &str,
713        value: Box<dyn ArrayProtocol>,
714    ) -> Result<(), OperationError> {
715        match name {
716            "filters" => {
717                self.filters = value;
718                Ok(())
719            }
720            "bias" => {
721                self.bias = Some(value);
722                Ok(())
723            }
724            _ => Err(OperationError::Other(format!("Unknown parameter: {name}"))),
725        }
726    }
727
728    fn parameter_names(&self) -> Vec<String> {
729        let mut names = vec!["filters".to_string()];
730        if self.bias.is_some() {
731            names.push("bias".to_string());
732        }
733        names
734    }
735
736    fn train(&mut self) {
737        self.training = true;
738    }
739
740    fn eval(&mut self) {
741        self.training = false;
742    }
743
744    fn is_training(&self) -> bool {
745        self.training
746    }
747
748    fn name(&self) -> &str {
749        &self.name
750    }
751}
752
753/// Builder for creating Conv2D layers
754pub struct Conv2DBuilder {
755    name: String,
756    filter_height: usize,
757    filter_width: usize,
758    in_channels: usize,
759    out_channels: usize,
760    stride: (usize, usize),
761    padding: (usize, usize),
762    withbias: bool,
763    activation: Option<ActivationFunc>,
764}
765
766impl Conv2DBuilder {
767    /// Create a new Conv2D builder
768    pub fn new(name: &str) -> Self {
769        Self {
770            name: name.to_string(),
771            filter_height: 3,
772            filter_width: 3,
773            in_channels: 1,
774            out_channels: 1,
775            stride: (1, 1),
776            padding: (0, 0),
777            withbias: true,
778            activation: None,
779        }
780    }
781
782    /// Set filter dimensions
783    pub const fn filter_size(mut self, height: usize, width: usize) -> Self {
784        self.filter_height = height;
785        self.filter_width = width;
786        self
787    }
788
789    /// Set input and output channels
790    pub const fn channels(mut self, input: usize, output: usize) -> Self {
791        self.in_channels = input;
792        self.out_channels = output;
793        self
794    }
795
796    /// Set stride
797    pub fn stride(mut self, stride: (usize, usize)) -> Self {
798        self.stride = stride;
799        self
800    }
801
802    /// Set padding
803    pub fn padding(mut self, padding: (usize, usize)) -> Self {
804        self.padding = padding;
805        self
806    }
807
808    /// Set whether to include bias
809    pub fn withbias(mut self, withbias: bool) -> Self {
810        self.withbias = withbias;
811        self
812    }
813
814    /// Set activation function
815    pub fn activation(mut self, activation: ActivationFunc) -> Self {
816        self.activation = Some(activation);
817        self
818    }
819
820    /// Build the Conv2D layer
821    pub fn build(self) -> Conv2D {
822        Conv2D::withshape(
823            &self.name,
824            self.filter_height,
825            self.filter_width,
826            self.in_channels,
827            self.out_channels,
828            self.stride,
829            self.padding,
830            self.withbias,
831            self.activation,
832        )
833    }
834}
835
836/// Max pooling layer.
837#[allow(dead_code)]
838pub struct MaxPool2D {
839    /// The layer's name.
840    name: String,
841
842    /// Kernel size.
843    kernel_size: (usize, usize),
844
845    /// Stride.
846    stride: (usize, usize),
847
848    /// Padding.
849    padding: (usize, usize),
850
851    /// Training mode flag.
852    training: bool,
853}
854
855impl MaxPool2D {
856    /// Create a new max pooling layer.
857    pub fn new(
858        name: &str,
859        kernel_size: (usize, usize),
860        stride: Option<(usize, usize)>,
861        padding: (usize, usize),
862    ) -> Self {
863        let stride = stride.unwrap_or(kernel_size);
864
865        Self {
866            name: name.to_string(),
867            kernel_size,
868            stride,
869            padding,
870            training: true,
871        }
872    }
873}
874
875impl Layer for MaxPool2D {
876    fn layer_type(&self) -> &str {
877        "MaxPool2D"
878    }
879
880    fn forward(
881        &self,
882        inputs: &dyn ArrayProtocol,
883    ) -> Result<Box<dyn ArrayProtocol>, OperationError> {
884        // Use the max_pool2d implementation from ml_ops module
885        crate::array_protocol::ml_ops::max_pool2d(
886            inputs,
887            self.kernel_size,
888            self.stride,
889            self.padding,
890        )
891    }
892
893    fn backward(
894        &self,
895        input: &dyn ArrayProtocol,
896        grad_output: &dyn ArrayProtocol,
897    ) -> Result<LayerGrad, OperationError> {
898        let inputarr = as_f64_array(input)?
899            .into_dimensionality::<Ix4>()
900            .map_err(|e| {
901                OperationError::ShapeMismatch(format!(
902                    "MaxPool2D::backward expects a 4D input: {e}"
903                ))
904            })?;
905        let grad_y = as_f64_array(grad_output)?
906            .into_dimensionality::<Ix4>()
907            .map_err(|e| {
908                OperationError::ShapeMismatch(format!(
909                    "MaxPool2D::backward expects a 4D grad_output: {e}"
910                ))
911            })?;
912
913        let (kernel_h, kernel_w) = self.kernel_size;
914        let (stride_h, stride_w) = self.stride;
915        let (pad_h, pad_w) = self.padding;
916
917        let batch_size = inputarr.shape()[0];
918        let input_height = inputarr.shape()[1];
919        let input_width = inputarr.shape()[2];
920        let channels = inputarr.shape()[3];
921        let out_height = grad_y.shape()[1];
922        let out_width = grad_y.shape()[2];
923
924        let mut grad_input =
925            Array4::<f64>::zeros((batch_size, input_height, input_width, channels));
926
927        // Route each output cell's gradient to whichever input cell was the
928        // max in its pooling window (accumulating, since overlapping
929        // windows can route to the same input cell more than once).
930        for b in 0..batch_size {
931            for c in 0..channels {
932                for out_h in 0..out_height {
933                    for out_w in 0..out_width {
934                        let mut max_val = f64::NEG_INFINITY;
935                        let mut argmax: Option<(usize, usize)> = None;
936                        for k_h in 0..kernel_h {
937                            for k_w in 0..kernel_w {
938                                let in_h = (out_h * stride_h) as i32 + k_h as i32 - pad_h as i32;
939                                let in_w = (out_w * stride_w) as i32 + k_w as i32 - pad_w as i32;
940                                if in_h >= 0
941                                    && in_h < input_height as i32
942                                    && in_w >= 0
943                                    && in_w < input_width as i32
944                                {
945                                    let val = inputarr[[b, in_h as usize, in_w as usize, c]];
946                                    if val > max_val {
947                                        max_val = val;
948                                        argmax = Some((in_h as usize, in_w as usize));
949                                    }
950                                }
951                            }
952                        }
953                        if let Some((in_h, in_w)) = argmax {
954                            grad_input[[b, in_h, in_w, c]] += grad_y[[b, out_h, out_w, c]];
955                        }
956                    }
957                }
958            }
959        }
960
961        Ok(LayerGrad {
962            grad_input: wrap_like(input, grad_input.into_dyn())?,
963            grad_params: Vec::new(),
964        })
965    }
966
967    fn parameters(&self) -> Vec<Box<dyn ArrayProtocol>> {
968        // Pooling layers have no parameters
969        Vec::new()
970    }
971
972    fn parameters_mut(&mut self) -> Vec<&mut Box<dyn ArrayProtocol>> {
973        // Pooling layers have no parameters
974        Vec::new()
975    }
976
977    fn update_parameter(
978        &mut self,
979        name: &str,
980        _value: Box<dyn ArrayProtocol>,
981    ) -> Result<(), OperationError> {
982        Err(OperationError::Other(format!(
983            "MaxPool2D has no parameter: {name}"
984        )))
985    }
986
987    fn parameter_names(&self) -> Vec<String> {
988        // Pooling layers have no parameters
989        Vec::new()
990    }
991
992    fn train(&mut self) {
993        self.training = true;
994    }
995
996    fn eval(&mut self) {
997        self.training = false;
998    }
999
1000    fn is_training(&self) -> bool {
1001        self.training
1002    }
1003
1004    fn name(&self) -> &str {
1005        &self.name
1006    }
1007}
1008
1009/// Batch normalization layer.
1010pub struct BatchNorm {
1011    /// The layer's name.
1012    name: String,
1013
1014    /// Scale parameter.
1015    scale: Box<dyn ArrayProtocol>,
1016
1017    /// Offset parameter.
1018    offset: Box<dyn ArrayProtocol>,
1019
1020    /// Running mean (for inference).
1021    running_mean: Box<dyn ArrayProtocol>,
1022
1023    /// Running variance (for inference).
1024    running_var: Box<dyn ArrayProtocol>,
1025
1026    /// Epsilon for numerical stability.
1027    epsilon: f64,
1028
1029    /// Training mode flag.
1030    training: bool,
1031}
1032
1033impl BatchNorm {
1034    /// Create a new batch normalization layer.
1035    pub fn new(
1036        name: &str,
1037        scale: Box<dyn ArrayProtocol>,
1038        offset: Box<dyn ArrayProtocol>,
1039        running_mean: Box<dyn ArrayProtocol>,
1040        running_var: Box<dyn ArrayProtocol>,
1041        epsilon: f64,
1042    ) -> Self {
1043        Self {
1044            name: name.to_string(),
1045            scale,
1046            offset,
1047            running_mean,
1048            running_var,
1049            epsilon,
1050            training: true,
1051        }
1052    }
1053
1054    /// Create a new batch normalization layer with initialized parameters.
1055    pub fn withshape(
1056        name: &str,
1057        num_features: usize,
1058        epsilon: Option<f64>,
1059        _momentum: Option<f64>,
1060    ) -> Self {
1061        // Initialize parameters with explicit types
1062        let scale: Array<f64, Ix1> = Array::ones(num_features);
1063        let offset: Array<f64, Ix1> = Array::zeros(num_features);
1064        let running_mean: Array<f64, Ix1> = Array::zeros(num_features);
1065        let running_var: Array<f64, Ix1> = Array::ones(num_features);
1066
1067        Self {
1068            name: name.to_string(),
1069            scale: Box::new(NdarrayWrapper::new(scale)),
1070            offset: Box::new(NdarrayWrapper::new(offset)),
1071            running_mean: Box::new(NdarrayWrapper::new(running_mean)),
1072            running_var: Box::new(NdarrayWrapper::new(running_var)),
1073            epsilon: epsilon.unwrap_or(1e-5),
1074            training: true,
1075        }
1076    }
1077}
1078
1079impl Layer for BatchNorm {
1080    fn layer_type(&self) -> &str {
1081        "BatchNorm"
1082    }
1083
1084    fn forward(
1085        &self,
1086        inputs: &dyn ArrayProtocol,
1087    ) -> Result<Box<dyn ArrayProtocol>, OperationError> {
1088        crate::array_protocol::ml_ops::batch_norm(
1089            inputs,
1090            self.scale.as_ref(),
1091            self.offset.as_ref(),
1092            self.running_mean.as_ref(),
1093            self.running_var.as_ref(),
1094            self.epsilon,
1095        )
1096    }
1097
1098    fn backward(
1099        &self,
1100        input: &dyn ArrayProtocol,
1101        grad_output: &dyn ArrayProtocol,
1102    ) -> Result<LayerGrad, OperationError> {
1103        // `ml_ops::batch_norm`'s forward always normalizes with the given
1104        // `mean`/`variance` directly (it does not compute batch statistics
1105        // even in training mode), so — matching that — they're treated here
1106        // as constants rather than differentiated through: only `scale` and
1107        // `offset` are learnable parameters (see `parameters()` below).
1108        let x = as_f64_array(input)?
1109            .into_dimensionality::<Ix4>()
1110            .map_err(|e| {
1111                OperationError::ShapeMismatch(format!(
1112                    "BatchNorm::backward expects a 4D input: {e}"
1113                ))
1114            })?;
1115        let grad_y = as_f64_array(grad_output)?
1116            .into_dimensionality::<Ix4>()
1117            .map_err(|e| {
1118                OperationError::ShapeMismatch(format!(
1119                    "BatchNorm::backward expects a 4D grad_output: {e}"
1120                ))
1121            })?;
1122        let scale = as_f64_array(self.scale.as_ref())?;
1123        let mean = as_f64_array(self.running_mean.as_ref())?;
1124        let variance = as_f64_array(self.running_var.as_ref())?;
1125
1126        let (batch_size, height, width, channels) = x.dim();
1127        let mut grad_input = Array4::<f64>::zeros((batch_size, height, width, channels));
1128        let mut grad_scale = Array1::<f64>::zeros(channels);
1129        let mut grad_offset = Array1::<f64>::zeros(channels);
1130
1131        for c in 0..channels {
1132            let inv_std = 1.0 / (variance[[c]] + self.epsilon).sqrt();
1133            let m = mean[[c]];
1134            let s = scale[[c]];
1135            let mut grad_scale_c = 0.0;
1136            let mut grad_offset_c = 0.0;
1137            for b in 0..batch_size {
1138                for h in 0..height {
1139                    for w in 0..width {
1140                        let gy = grad_y[[b, h, w, c]];
1141                        let normalized = (x[[b, h, w, c]] - m) * inv_std;
1142                        grad_scale_c += gy * normalized;
1143                        grad_offset_c += gy;
1144                        grad_input[[b, h, w, c]] = gy * s * inv_std;
1145                    }
1146                }
1147            }
1148            grad_scale[c] = grad_scale_c;
1149            grad_offset[c] = grad_offset_c;
1150        }
1151
1152        Ok(LayerGrad {
1153            grad_input: wrap_like(input, grad_input.into_dyn())?,
1154            grad_params: vec![
1155                wrap_like(self.scale.as_ref(), grad_scale.into_dyn())?,
1156                wrap_like(self.offset.as_ref(), grad_offset.into_dyn())?,
1157            ],
1158        })
1159    }
1160
1161    fn parameters(&self) -> Vec<Box<dyn ArrayProtocol>> {
1162        vec![self.scale.clone(), self.offset.clone()]
1163    }
1164
1165    fn parameters_mut(&mut self) -> Vec<&mut Box<dyn ArrayProtocol>> {
1166        vec![&mut self.scale, &mut self.offset]
1167    }
1168
1169    fn update_parameter(
1170        &mut self,
1171        name: &str,
1172        value: Box<dyn ArrayProtocol>,
1173    ) -> Result<(), OperationError> {
1174        match name {
1175            "scale" => {
1176                self.scale = value;
1177                Ok(())
1178            }
1179            "offset" => {
1180                self.offset = value;
1181                Ok(())
1182            }
1183            _ => Err(OperationError::Other(format!("Unknown parameter: {name}"))),
1184        }
1185    }
1186
1187    fn parameter_names(&self) -> Vec<String> {
1188        vec!["scale".to_string(), "offset".to_string()]
1189    }
1190
1191    fn train(&mut self) {
1192        self.training = true;
1193    }
1194
1195    fn eval(&mut self) {
1196        self.training = false;
1197    }
1198
1199    fn is_training(&self) -> bool {
1200        self.training
1201    }
1202
1203    fn name(&self) -> &str {
1204        &self.name
1205    }
1206}
1207
1208/// Dropout layer.
1209pub struct Dropout {
1210    /// The layer's name.
1211    name: String,
1212
1213    /// Dropout rate.
1214    rate: f64,
1215
1216    /// Optional seed for reproducibility.
1217    seed: Option<u64>,
1218
1219    /// Training mode flag.
1220    training: bool,
1221}
1222
1223impl Dropout {
1224    /// Create a new dropout layer.
1225    pub fn new(name: &str, rate: f64, seed: Option<u64>) -> Self {
1226        Self {
1227            name: name.to_string(),
1228            rate,
1229            seed,
1230            training: true,
1231        }
1232    }
1233}
1234
1235impl Layer for Dropout {
1236    fn layer_type(&self) -> &str {
1237        "Dropout"
1238    }
1239
1240    fn forward(
1241        &self,
1242        inputs: &dyn ArrayProtocol,
1243    ) -> Result<Box<dyn ArrayProtocol>, OperationError> {
1244        crate::array_protocol::ml_ops::dropout(inputs, self.rate, self.training, self.seed)
1245    }
1246
1247    fn backward(
1248        &self,
1249        input: &dyn ArrayProtocol,
1250        grad_output: &dyn ArrayProtocol,
1251    ) -> Result<LayerGrad, OperationError> {
1252        if !self.training {
1253            // Inference-mode dropout is the identity function.
1254            return Ok(LayerGrad {
1255                grad_input: grad_output.box_clone(),
1256                grad_params: Vec::new(),
1257            });
1258        }
1259
1260        // `ml_ops::dropout`'s forward doesn't cache the mask it drew, so it
1261        // can only be reproduced deterministically (and thus differentiated
1262        // through) when the layer was constructed with a fixed seed.
1263        let seed = self.seed.ok_or_else(|| {
1264            OperationError::NotImplemented(
1265                "Dropout::backward requires a fixed `seed` to deterministically reproduce the \
1266                 forward mask (forward() does not cache it); construct the layer via \
1267                 `Dropout::new(name, rate, Some(seed))` to backpropagate through training-mode \
1268                 dropout"
1269                    .to_string(),
1270            )
1271        })?;
1272
1273        let grad_y = as_f64_array(grad_output)?;
1274        let inputarr = as_f64_array(input)?;
1275        if inputarr.shape() != grad_y.shape() {
1276            return Err(OperationError::ShapeMismatch(format!(
1277                "Dropout::backward: input shape {inputshape:?} != grad_output shape {gradshape:?}",
1278                inputshape = inputarr.shape(),
1279                gradshape = grad_y.shape()
1280            )));
1281        }
1282
1283        // Reproduce the exact mask `forward()` would have drawn for this
1284        // input shape, using the same seeded RNG sequence.
1285        let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
1286        let rate = self.rate;
1287        let mask = Array::from_shape_fn(inputarr.raw_dim(), |_| {
1288            if rng.random::<f64>() >= rate {
1289                1.0
1290            } else {
1291                0.0
1292            }
1293        });
1294        let scale = 1.0 / (1.0 - self.rate);
1295        let grad_input = grad_y * &mask * scale;
1296
1297        Ok(LayerGrad {
1298            grad_input: wrap_like(input, grad_input)?,
1299            grad_params: Vec::new(),
1300        })
1301    }
1302
1303    fn parameters(&self) -> Vec<Box<dyn ArrayProtocol>> {
1304        // Dropout layers have no parameters
1305        Vec::new()
1306    }
1307
1308    fn parameters_mut(&mut self) -> Vec<&mut Box<dyn ArrayProtocol>> {
1309        // Dropout layers have no parameters
1310        Vec::new()
1311    }
1312
1313    fn update_parameter(
1314        &mut self,
1315        name: &str,
1316        _value: Box<dyn ArrayProtocol>,
1317    ) -> Result<(), OperationError> {
1318        Err(OperationError::Other(format!(
1319            "Dropout has no parameter: {name}"
1320        )))
1321    }
1322
1323    fn parameter_names(&self) -> Vec<String> {
1324        // Dropout layers have no parameters
1325        Vec::new()
1326    }
1327
1328    fn train(&mut self) {
1329        self.training = true;
1330    }
1331
1332    fn eval(&mut self) {
1333        self.training = false;
1334    }
1335
1336    fn is_training(&self) -> bool {
1337        self.training
1338    }
1339
1340    fn name(&self) -> &str {
1341        &self.name
1342    }
1343}
1344
1345/// Multi-head attention layer.
1346pub struct MultiHeadAttention {
1347    /// The layer's name.
1348    name: String,
1349
1350    /// Query projection.
1351    wq: Box<dyn ArrayProtocol>,
1352
1353    /// Key projection.
1354    wk: Box<dyn ArrayProtocol>,
1355
1356    /// Value projection.
1357    wv: Box<dyn ArrayProtocol>,
1358
1359    /// Output projection.
1360    wo: Box<dyn ArrayProtocol>,
1361
1362    /// Number of attention heads.
1363    num_heads: usize,
1364
1365    /// Model dimension.
1366    dmodel: usize,
1367
1368    /// Training mode flag.
1369    training: bool,
1370}
1371
1372impl MultiHeadAttention {
1373    /// Create a new multi-head attention layer.
1374    pub fn new(
1375        name: &str,
1376        wq: Box<dyn ArrayProtocol>,
1377        wk: Box<dyn ArrayProtocol>,
1378        wv: Box<dyn ArrayProtocol>,
1379        wo: Box<dyn ArrayProtocol>,
1380        num_heads: usize,
1381        dmodel: usize,
1382    ) -> Self {
1383        Self {
1384            name: name.to_string(),
1385            wq,
1386            wk,
1387            wv,
1388            wo,
1389            num_heads,
1390            dmodel,
1391            training: true,
1392        }
1393    }
1394
1395    /// Create a new multi-head attention layer with randomly initialized weights.
1396    pub fn with_params(name: &str, num_heads: usize, dmodel: usize) -> Self {
1397        // Check if dmodel is divisible by num_heads
1398        assert!(
1399            dmodel % num_heads == 0,
1400            "dmodel must be divisible by num_heads"
1401        );
1402
1403        // Initialize parameters
1404        let scale = (1.0_f64 / dmodel as f64).sqrt();
1405        let mut rng = rand::rng();
1406
1407        let wq = Array::from_shape_fn((dmodel, dmodel), |_| {
1408            (rng.random::<f64>() * 2.0_f64 - 1.0) * scale
1409        });
1410
1411        let wk = Array::from_shape_fn((dmodel, dmodel), |_| {
1412            (rng.random::<f64>() * 2.0_f64 - 1.0) * scale
1413        });
1414
1415        let wv = Array::from_shape_fn((dmodel, dmodel), |_| {
1416            (rng.random::<f64>() * 2.0_f64 - 1.0) * scale
1417        });
1418
1419        let wo = Array::from_shape_fn((dmodel, dmodel), |_| {
1420            (rng.random::<f64>() * 2.0_f64 - 1.0) * scale
1421        });
1422
1423        Self {
1424            name: name.to_string(),
1425            wq: Box::new(NdarrayWrapper::new(wq)),
1426            wk: Box::new(NdarrayWrapper::new(wk)),
1427            wv: Box::new(NdarrayWrapper::new(wv)),
1428            wo: Box::new(NdarrayWrapper::new(wo)),
1429            num_heads,
1430            dmodel,
1431            training: true,
1432        }
1433    }
1434}
1435
1436impl Layer for MultiHeadAttention {
1437    fn layer_type(&self) -> &str {
1438        "MultiHeadAttention"
1439    }
1440
1441    fn forward(
1442        &self,
1443        inputs: &dyn ArrayProtocol,
1444    ) -> Result<Box<dyn ArrayProtocol>, OperationError> {
1445        // For a real implementation, this would:
1446        // 1. Project inputs to queries, keys, and values
1447        // 2. Reshape for multi-head attention
1448        // 3. Compute self-attention
1449        // 4. Reshape and project back to output space
1450
1451        // This is a simplified placeholder implementation
1452        let queries = crate::array_protocol::matmul(self.wq.as_ref(), inputs)?;
1453        let keys = crate::array_protocol::matmul(self.wk.as_ref(), inputs)?;
1454        let values = crate::array_protocol::matmul(self.wv.as_ref(), inputs)?;
1455
1456        // Compute self-attention
1457        let attention = crate::array_protocol::ml_ops::self_attention(
1458            queries.as_ref(),
1459            keys.as_ref(),
1460            values.as_ref(),
1461            None,
1462            Some((self.dmodel / self.num_heads) as f64),
1463        )?;
1464
1465        // Project back to output space
1466        let output = crate::array_protocol::matmul(self.wo.as_ref(), attention.as_ref())?;
1467
1468        Ok(output)
1469    }
1470
1471    fn backward(
1472        &self,
1473        _input: &dyn ArrayProtocol,
1474        _grad_output: &dyn ArrayProtocol,
1475    ) -> Result<LayerGrad, OperationError> {
1476        // `forward()` above is already documented as "a simplified
1477        // placeholder implementation" that doesn't perform real per-head
1478        // reshape/splitting — differentiating it exactly wouldn't give a
1479        // gradient for the multi-head attention operation this layer is
1480        // meant to represent. Fixing `forward()` for real is a prerequisite
1481        // for a meaningful `backward()` here.
1482        Err(OperationError::NotImplemented(
1483            "MultiHeadAttention::backward is not implemented (forward() is itself a documented \
1484             simplified placeholder, not real multi-head attention)"
1485                .to_string(),
1486        ))
1487    }
1488
1489    fn parameters(&self) -> Vec<Box<dyn ArrayProtocol>> {
1490        vec![
1491            self.wq.clone(),
1492            self.wk.clone(),
1493            self.wv.clone(),
1494            self.wo.clone(),
1495        ]
1496    }
1497
1498    fn parameters_mut(&mut self) -> Vec<&mut Box<dyn ArrayProtocol>> {
1499        vec![&mut self.wq, &mut self.wk, &mut self.wv, &mut self.wo]
1500    }
1501
1502    fn update_parameter(
1503        &mut self,
1504        name: &str,
1505        value: Box<dyn ArrayProtocol>,
1506    ) -> Result<(), OperationError> {
1507        match name {
1508            "wq" => {
1509                self.wq = value;
1510                Ok(())
1511            }
1512            "wk" => {
1513                self.wk = value;
1514                Ok(())
1515            }
1516            "wv" => {
1517                self.wv = value;
1518                Ok(())
1519            }
1520            "wo" => {
1521                self.wo = value;
1522                Ok(())
1523            }
1524            _ => Err(OperationError::Other(format!("Unknown parameter: {name}"))),
1525        }
1526    }
1527
1528    fn parameter_names(&self) -> Vec<String> {
1529        vec![
1530            "wq".to_string(),
1531            "wk".to_string(),
1532            "wv".to_string(),
1533            "wo".to_string(),
1534        ]
1535    }
1536
1537    fn train(&mut self) {
1538        self.training = true;
1539    }
1540
1541    fn eval(&mut self) {
1542        self.training = false;
1543    }
1544
1545    fn is_training(&self) -> bool {
1546        self.training
1547    }
1548
1549    fn name(&self) -> &str {
1550        &self.name
1551    }
1552}
1553
1554/// Sequential model that chains layers together.
1555pub struct Sequential {
1556    /// The model's name.
1557    name: String,
1558
1559    /// The layers in the model.
1560    layers: Vec<Box<dyn Layer>>,
1561
1562    /// Training mode flag.
1563    training: bool,
1564}
1565
1566impl Sequential {
1567    /// Create a new sequential model.
1568    pub fn new(name: &str, layers: Vec<Box<dyn Layer>>) -> Self {
1569        Self {
1570            name: name.to_string(),
1571            layers,
1572            training: true,
1573        }
1574    }
1575
1576    /// Add a layer to the model.
1577    pub fn add_layer(&mut self, layer: Box<dyn Layer>) {
1578        self.layers.push(layer);
1579    }
1580
1581    /// Forward pass through the model.
1582    pub fn forward(
1583        &self,
1584        inputs: &dyn ArrayProtocol,
1585    ) -> Result<Box<dyn ArrayProtocol>, OperationError> {
1586        // Clone the input to a Box
1587        let mut x: Box<dyn ArrayProtocol> = inputs.box_clone();
1588
1589        for layer in &self.layers {
1590            // Get a reference from the box for the layer
1591            let x_ref: &dyn ArrayProtocol = x.as_ref();
1592            // Update x with the layer output
1593            x = layer.forward(x_ref)?;
1594        }
1595
1596        Ok(x)
1597    }
1598
1599    /// Get all parameters in the model.
1600    pub fn parameters(&self) -> Vec<Box<dyn ArrayProtocol>> {
1601        let mut params = Vec::new();
1602
1603        for layer in &self.layers {
1604            params.extend(layer.parameters());
1605        }
1606
1607        params
1608    }
1609
1610    /// Set the model to training mode.
1611    pub fn train(&mut self) {
1612        self.training = true;
1613
1614        for layer in &mut self.layers {
1615            layer.train();
1616        }
1617    }
1618
1619    /// Set the model to evaluation mode.
1620    pub fn eval(&mut self) {
1621        self.training = false;
1622
1623        for layer in &mut self.layers {
1624            layer.eval();
1625        }
1626    }
1627
1628    /// Get the model's name.
1629    pub fn name(&self) -> &str {
1630        &self.name
1631    }
1632
1633    /// Get the layers in the model.
1634    pub fn layers(&self) -> &[Box<dyn Layer>] {
1635        &self.layers
1636    }
1637
1638    /// Get mutable access to the layers in the model — e.g. so a
1639    /// deserializer can restore saved parameter values in place.
1640    pub fn layers_mut(&mut self) -> &mut [Box<dyn Layer>] {
1641        &mut self.layers
1642    }
1643
1644    /// Backward pass through the model: given the original `input` fed to
1645    /// `forward` and `grad_output` (the gradient of the loss with respect
1646    /// to the model's final output — e.g. `Loss::backward`'s result),
1647    /// computes the gradient for every parameter in every layer via
1648    /// backpropagation. The returned dictionary is keyed
1649    /// `"{layer_index}.{param_name}"`, matching [`Self::all_parameter_names`]
1650    /// and [`Self::update_parameter`]'s expected format.
1651    ///
1652    /// `Layer::forward` doesn't cache intermediate activations, so this
1653    /// first recomputes the forward pass (retaining each layer's input) and
1654    /// then walks the layers in reverse, threading each layer's
1655    /// `grad_input` to the previous layer as its `grad_output`.
1656    pub fn backward(
1657        &self,
1658        input: &dyn ArrayProtocol,
1659        grad_output: &dyn ArrayProtocol,
1660    ) -> Result<crate::array_protocol::grad::GradientDict, crate::error::CoreError> {
1661        let mut gradients = crate::array_protocol::grad::GradientDict::new();
1662        if self.layers.is_empty() {
1663            return Ok(gradients);
1664        }
1665
1666        // Recompute the forward pass, caching each layer's input so that
1667        // `activations[i]` is the input seen by `self.layers[i]`.
1668        let mut activations: Vec<Box<dyn ArrayProtocol>> =
1669            Vec::with_capacity(self.layers.len() + 1);
1670        activations.push(input.box_clone());
1671        for layer in &self.layers {
1672            let layer_input: &dyn ArrayProtocol = activations
1673                .last()
1674                .ok_or_else(|| {
1675                    crate::error::CoreError::ComputationError(crate::error::ErrorContext::new(
1676                        "internal error: activation cache unexpectedly empty".to_string(),
1677                    ))
1678                })?
1679                .as_ref();
1680            let out = layer.forward(layer_input).map_err(|e| {
1681                crate::error::CoreError::ComputationError(crate::error::ErrorContext::new(format!(
1682                    "backward(): forward recompute failed in layer '{name}': {e}",
1683                    name = layer.name()
1684                )))
1685            })?;
1686            activations.push(out);
1687        }
1688
1689        // Walk the layers in reverse, propagating the gradient and
1690        // collecting each layer's parameter gradients.
1691        let mut grad_current: Box<dyn ArrayProtocol> = grad_output.box_clone();
1692        for (layer_idx, layer) in self.layers.iter().enumerate().rev() {
1693            let layer_input: &dyn ArrayProtocol = activations[layer_idx].as_ref();
1694            let layer_grad = layer
1695                .backward(layer_input, grad_current.as_ref())
1696                .map_err(|e| {
1697                    crate::error::CoreError::ComputationError(crate::error::ErrorContext::new(
1698                        format!(
1699                            "backward() failed in layer '{name}' (index {layer_idx}): {e}",
1700                            name = layer.name()
1701                        ),
1702                    ))
1703                })?;
1704
1705            let param_names = layer.parameter_names();
1706            for (param_idx, grad_param) in layer_grad.grad_params.into_iter().enumerate() {
1707                let param_name = param_names.get(param_idx).ok_or_else(|| {
1708                    crate::error::CoreError::ComputationError(crate::error::ErrorContext::new(
1709                        format!(
1710                            "layer '{name}' (index {layer_idx}) returned {ngrads} parameter \
1711                             gradient(s) but parameter_names() only has {nnames}",
1712                            name = layer.name(),
1713                            ngrads = param_idx + 1,
1714                            nnames = param_names.len()
1715                        ),
1716                    ))
1717                })?;
1718                gradients.insert(format!("{layer_idx}.{param_name}"), grad_param);
1719            }
1720
1721            grad_current = layer_grad.grad_input;
1722        }
1723
1724        Ok(gradients)
1725    }
1726
1727    /// Update a parameter in the model
1728    pub fn update_parameter(
1729        &mut self,
1730        param_name: &str,
1731        gradient: &dyn ArrayProtocol,
1732        learningrate: f64,
1733    ) -> Result<(), crate::error::CoreError> {
1734        // Parse parameter name: layer_index.parameter_name (e.g., "0.weights", "1.bias")
1735        let parts: Vec<&str> = param_name.split('.').collect();
1736        if parts.len() != 2 {
1737            return Err(crate::error::CoreError::ValueError(
1738                crate::error::ErrorContext::new(format!(
1739                    "Invalid parameter name format. Expected 'layer_index.param_name', got: {param_name}"
1740                )),
1741            ));
1742        }
1743
1744        let layer_index: usize = parts[0].parse().map_err(|_| {
1745            crate::error::CoreError::ValueError(crate::error::ErrorContext::new(format!(
1746                "Invalid layer index: {layer_idx}",
1747                layer_idx = parts[0]
1748            )))
1749        })?;
1750
1751        let param_name = parts[1];
1752
1753        if layer_index >= self.layers.len() {
1754            return Err(crate::error::CoreError::ValueError(
1755                crate::error::ErrorContext::new(format!(
1756                    "Layer index {layer_index} out of bounds (model has {num_layers} layers)",
1757                    num_layers = self.layers.len()
1758                )),
1759            ));
1760        }
1761
1762        // Get the current parameter value
1763        let layer = &mut self.layers[layer_index];
1764        let current_params = layer.parameters();
1765        let param_names = layer.parameter_names();
1766
1767        // Find the parameter by name
1768        let param_idx = param_names
1769            .iter()
1770            .position(|name| name == param_name)
1771            .ok_or_else(|| {
1772                crate::error::CoreError::ValueError(crate::error::ErrorContext::new(format!(
1773                    "Parameter '{param_name}' not found in layer {layer_index}"
1774                )))
1775            })?;
1776
1777        // Perform gradient descent update: param = param - learningrate * gradient
1778        let current_param = &current_params[param_idx];
1779
1780        // Multiply gradient by learning _rate
1781        let scaled_gradient =
1782            crate::array_protocol::operations::multiply_by_scalar_f64(gradient, learningrate)
1783                .map_err(|e| {
1784                    crate::error::CoreError::ComputationError(crate::error::ErrorContext::new(
1785                        format!("Failed to scale gradient: {e}"),
1786                    ))
1787                })?;
1788
1789        // Subtract scaled gradient from current parameter
1790        let updated_param = crate::array_protocol::operations::subtract(
1791            current_param.as_ref(),
1792            scaled_gradient.as_ref(),
1793        )
1794        .map_err(|e| {
1795            crate::error::CoreError::ComputationError(crate::error::ErrorContext::new(format!(
1796                "Failed to update parameter: {e}"
1797            )))
1798        })?;
1799
1800        // Update the parameter in the layer
1801        layer
1802            .update_parameter(param_name, updated_param)
1803            .map_err(|e| {
1804                crate::error::CoreError::ComputationError(crate::error::ErrorContext::new(format!(
1805                    "Failed to set parameter in layer: {e}"
1806                )))
1807            })?;
1808
1809        Ok(())
1810    }
1811
1812    /// Get all parameter names in the model with layer prefixes
1813    pub fn all_parameter_names(&self) -> Vec<String> {
1814        let mut all_names = Vec::new();
1815        for (layer_idx, layer) in self.layers.iter().enumerate() {
1816            let layer_param_names = layer.parameter_names();
1817            for param_name in layer_param_names {
1818                all_names.push(format!("{layer_idx}.{param_name}"));
1819            }
1820        }
1821        all_names
1822    }
1823
1824    /// Get all parameters in the model
1825    pub fn all_parameters(&self) -> Vec<Box<dyn ArrayProtocol>> {
1826        let mut all_params = Vec::new();
1827        for layer in &self.layers {
1828            all_params.extend(layer.parameters());
1829        }
1830        all_params
1831    }
1832}
1833
1834/// Example function to create a simple CNN model.
1835#[allow(dead_code)]
1836pub fn create_simple_cnn(inputshape: (usize, usize, usize), num_classes: usize) -> Sequential {
1837    let (height, width, channels) = inputshape;
1838
1839    let mut model = Sequential::new("SimpleCNN", Vec::new());
1840
1841    // First convolutional block
1842    model.add_layer(Box::new(Conv2D::withshape(
1843        "conv1",
1844        3,
1845        3, // Filter size
1846        channels,
1847        32,     // In/out channels
1848        (1, 1), // Stride
1849        (1, 1), // Padding
1850        true,   // With bias
1851        Some(ActivationFunc::ReLU),
1852    )));
1853
1854    model.add_layer(Box::new(MaxPool2D::new(
1855        "pool1",
1856        (2, 2), // Kernel size
1857        None,   // Stride (default to kernel size)
1858        (0, 0), // Padding
1859    )));
1860
1861    // Second convolutional block
1862    model.add_layer(Box::new(Conv2D::withshape(
1863        "conv2",
1864        3,
1865        3, // Filter size
1866        32,
1867        64,     // In/out channels
1868        (1, 1), // Stride
1869        (1, 1), // Padding
1870        true,   // With bias
1871        Some(ActivationFunc::ReLU),
1872    )));
1873
1874    model.add_layer(Box::new(MaxPool2D::new(
1875        "pool2",
1876        (2, 2), // Kernel size
1877        None,   // Stride (default to kernel size)
1878        (0, 0), // Padding
1879    )));
1880
1881    // Flatten layer (implemented as a Linear layer with reshape)
1882
1883    // Fully connected layers
1884    model.add_layer(Box::new(Linear::new_random(
1885        "fc1",
1886        64 * (height / 4) * (width / 4), // Input features
1887        128,                             // Output features
1888        true,                            // With bias
1889        Some(ActivationFunc::ReLU),
1890    )));
1891
1892    model.add_layer(Box::new(Dropout::new(
1893        "dropout", 0.5,  // Dropout rate
1894        None, // No fixed seed
1895    )));
1896
1897    model.add_layer(Box::new(Linear::new_random(
1898        "fc2",
1899        128,         // Input features
1900        num_classes, // Output features
1901        true,        // With bias
1902        None,        // No activation (will be applied in loss function)
1903    )));
1904
1905    model
1906}
1907
1908#[cfg(test)]
1909mod tests {
1910    use super::*;
1911    use crate::array_protocol::{self, NdarrayWrapper};
1912    use ndarray::{Array1, Array2};
1913
1914    #[test]
1915    fn test_linear_layer() {
1916        // Initialize the array protocol system
1917        array_protocol::init();
1918
1919        // Create a linear layer
1920        let weights = Array2::<f64>::eye(3);
1921        let bias = Array1::<f64>::ones(3);
1922
1923        let layer = Linear::new(
1924            "linear",
1925            Box::new(NdarrayWrapper::new(weights)),
1926            Some(Box::new(NdarrayWrapper::new(bias))),
1927            Some(ActivationFunc::ReLU),
1928        );
1929
1930        // Create input - ensure we use a dynamic array
1931        // (commented out since we're not using it in the test now)
1932        // let x = array![[-1.0, 2.0, -3.0]].into_dyn();
1933        // let input = NdarrayWrapper::new(x);
1934
1935        // We can't actually run the operation without proper implementation
1936        // Skip the actual forward pass for now
1937        // let output = layer.forward(&input).expect("Operation failed");
1938
1939        // For now, just make sure the layer is created correctly
1940        assert_eq!(layer.name(), "linear");
1941        assert!(layer.is_training());
1942    }
1943
1944    #[test]
1945    fn test_sequential_model() {
1946        // Initialize the array protocol system
1947        array_protocol::init();
1948
1949        // Create a simple sequential model
1950        let mut model = Sequential::new("test_model", Vec::new());
1951
1952        // Add linear layers
1953        model.add_layer(Box::new(Linear::new_random(
1954            "fc1",
1955            3,    // Input features
1956            2,    // Output features
1957            true, // With bias
1958            Some(ActivationFunc::ReLU),
1959        )));
1960
1961        model.add_layer(Box::new(Linear::new_random(
1962            "fc2",
1963            2,    // Input features
1964            1,    // Output features
1965            true, // With bias
1966            Some(ActivationFunc::Sigmoid),
1967        )));
1968
1969        // Just test that the model is constructed correctly
1970        assert_eq!(model.name(), "test_model");
1971        assert_eq!(model.layers().len(), 2);
1972        assert!(model.training);
1973    }
1974
1975    #[test]
1976    fn test_simple_cnn_creation() {
1977        // Initialize the array protocol system
1978        array_protocol::init();
1979
1980        // Create a simple CNN
1981        let model = create_simple_cnn((28, 28, 1), 10);
1982
1983        // Check the model structure
1984        assert_eq!(model.layers().len(), 7);
1985        assert_eq!(model.name(), "SimpleCNN");
1986
1987        // Check parameters
1988        let params = model.parameters();
1989        assert!(!params.is_empty());
1990    }
1991}
1992
1993// Split out to keep neural.rs under the workspace's 2000-line-per-file limit
1994// (see grad.rs/grad_tests.rs for the same pattern).
1995#[cfg(test)]
1996#[path = "neural_backward_tests.rs"]
1997mod backward_tests;