Skip to main content

only_brain/
neural_network.rs

1use crate::activation_functions::ActivationFunction;
2use crate::layer::Layer;
3use crate::ModelError;
4use nalgebra::DVector;
5use rand::{rng, Rng};
6use std::fmt;
7use serde::{Deserialize, Serialize};
8
9/// Neural Network
10///
11/// This is the main struct of the library: a chain of fully connected layers, each with
12/// its own weights, biases and [`ActivationFunction`]. You can use this struct and its
13/// methods to create, manipulate and even implement your own ways to train a neural
14/// network.
15///
16/// The number of input neurons (`IN`) and output neurons (`OUT`) are part of the type,
17/// so feeding a wrongly sized input is a compile error rather than a runtime panic. The
18/// hidden layers stay dynamic and are given at construction time.
19///
20/// # Layers
21///
22/// Layer 0 is the input layer, which only passes the inputs on, so it has no weights,
23/// biases or activation. Layers `1..num_layers()` each hold one row of weights per
24/// neuron, with one weight per neuron of the previous layer, and one bias per neuron.
25///
26/// # Example
27///
28/// ```
29/// use only_brain::NeuralNetwork;
30///
31/// // A 2 -> 2 -> 1 network.
32/// let mut nn = NeuralNetwork::<2, 1>::new(&[2]);
33///
34/// // One row per neuron, one weight per neuron of the previous layer.
35/// nn.set_layer_weights(1, &[[0.1, 0.2],
36///                           [0.3, 0.4]]);
37/// nn.set_layer_biases(1, &[0.1, 0.2]);
38///
39/// nn.set_layer_weights(2, &[[0.9, 0.8]]);
40/// nn.set_layer_biases(2, &[0.1]);
41///
42/// let output = nn.feed_forward(&[0.5, 0.2]);
43///
44/// println!("{:?}", output);
45/// ```
46///
47/// # Flat parameter view
48///
49/// Search methods such as genetic algorithms usually work on a flat list of numbers
50/// rather than on layers. [`parameters`](Self::parameters),
51/// [`set_parameters`](Self::set_parameters) and
52/// [`from_parameters`](Self::from_parameters) convert between a network and such a
53/// list, in an order that is documented and stable across versions:
54///
55/// - layer by layer, from layer 1 to the output layer;
56/// - within a layer, every weight first, row by row (all the weights of neuron 0, then
57///   of neuron 1, and so on, each row in the order of the previous layer's neurons);
58/// - then that layer's biases, one per neuron.
59///
60/// Activation functions are part of the network's shape, not of its parameters, so
61/// they are left untouched by [`set_parameters`](Self::set_parameters).
62///
63/// ```
64/// use only_brain::NeuralNetwork;
65///
66/// let mut nn = NeuralNetwork::<2, 1>::new(&[]);
67/// nn.set_layer_weights(1, &[[0.5, -0.25]]);
68/// nn.set_layer_biases(1, &[0.1]);
69///
70/// assert_eq!(nn.parameters(), vec![0.5, -0.25, 0.1]);
71///
72/// let rebuilt = NeuralNetwork::<2, 1>::from_parameters(&[], &nn.parameters());
73/// assert_eq!(rebuilt, nn);
74/// ```
75///
76/// # Serialization
77///
78/// Networks implement serde's `Serialize` and `Deserialize` through a plain form, a
79/// list of layers each with its `activation`, `weights` (one row per neuron) and
80/// `biases`, so they can be embedded in your own types and formats. Deserializing checks
81/// that the layers fit together and match `IN` and `OUT`. To save a network to a file,
82/// see [`crate::dump_model`] and [`crate::load_model`].
83#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
84#[serde(into = "NetworkData", try_from = "NetworkData")]
85pub struct NeuralNetwork<const IN: usize, const OUT: usize> {
86    layers: Vec<Layer>,
87}
88
89/// The plain form of a network, before its shape has been checked.
90///
91/// Serde and the model file readers both go through this, so every entry point, not
92/// only [`crate::load_model`], rejects layers that do not fit together. It is also what
93/// keeps the serialized form independent of how layers are stored in memory.
94#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
95pub(crate) struct NetworkData {
96    pub(crate) layers: Vec<LayerData>,
97}
98
99/// The plain form of one layer.
100#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
101pub(crate) struct LayerData {
102    pub(crate) activation: ActivationFunction,
103    /// One row per neuron, one weight per neuron of the previous layer.
104    pub(crate) weights: Vec<Vec<f64>>,
105    pub(crate) biases: Vec<f64>,
106}
107
108impl<const IN: usize, const OUT: usize> NeuralNetwork<IN, OUT> {
109    /// Creates a new Neural Network with the given hidden layer sizes. The input and
110    /// output widths come from the type parameters, so `hidden` lists only the layers
111    /// between them and may be empty.
112    ///
113    /// Weights are initialised uniformly at random in `[-1, 1)`, biases at zero, and
114    /// every layer uses [`ActivationFunction::Sigmoid`]. Use
115    /// [`NeuralNetwork::new_with_rng`] to control the seed.
116    ///
117    /// # Panics
118    ///
119    /// Panics if any hidden layer size is zero. `IN` or `OUT` being zero is a compile
120    /// error.
121    ///
122    /// # Example
123    ///
124    /// ```
125    /// # use only_brain::NeuralNetwork;
126    /// // 2 -> 2 -> 1
127    /// let nn = NeuralNetwork::<2, 1>::new(&[2]);
128    ///
129    /// // 3 -> 1, no hidden layers
130    /// let direct = NeuralNetwork::<3, 1>::new(&[]);
131    /// ```
132    pub fn new(hidden: &[usize]) -> Self {
133        Self::new_with_rng(hidden, &mut rng())
134    }
135
136    /// Creates a new Neural Network using the given random number generator.
137    ///
138    /// Seeding the generator makes initialisation reproducible, which is what you want
139    /// in tests and when a training run needs to be repeatable.
140    ///
141    /// # Panics
142    ///
143    /// Panics if any hidden layer size is zero.
144    pub fn new_with_rng<R: Rng>(hidden: &[usize], rng: &mut R) -> Self {
145        Self::build(hidden, |neurons, inputs| {
146            Layer::random(neurons, inputs, ActivationFunction::default(), rng)
147        })
148    }
149
150    /// Creates a network with the given hidden layer sizes from a flat list of
151    /// parameters, in the order described in the [flat parameter
152    /// view](Self#flat-parameter-view). Every layer uses
153    /// [`ActivationFunction::Sigmoid`]; change that afterwards with
154    /// [`set_activation_function`](Self::set_activation_function) and friends.
155    ///
156    /// # Panics
157    ///
158    /// Panics if any hidden layer size is zero, or if `parameters` does not hold exactly
159    /// [`parameter_count_for(hidden)`](Self::parameter_count_for) values.
160    ///
161    /// # Example
162    ///
163    /// ```
164    /// # use only_brain::{ActivationFunction, NeuralNetwork};
165    /// // A genome from a genetic algorithm, for a 2 -> 2 -> 1 network.
166    /// let genome = vec![0.5; NeuralNetwork::<2, 1>::parameter_count_for(&[2])];
167    ///
168    /// let mut nn = NeuralNetwork::<2, 1>::from_parameters(&[2], &genome);
169    /// nn.set_output_activation(ActivationFunction::Tanh);
170    ///
171    /// let [steering] = nn.feed_forward(&[0.3, -0.8]);
172    /// assert!((-1.0..=1.0).contains(&steering));
173    /// ```
174    pub fn from_parameters(hidden: &[usize], parameters: &[f64]) -> Self {
175        let mut network = Self::build(hidden, |neurons, inputs| {
176            Layer::zeros(neurons, inputs, ActivationFunction::default())
177        });
178        network.set_parameters(parameters);
179        network
180    }
181
182    /// Chains `IN`, the hidden sizes and `OUT` into layers made by `make(neurons, inputs)`.
183    fn build(hidden: &[usize], mut make: impl FnMut(usize, usize) -> Layer) -> Self {
184        let layers = Self::layer_shapes(hidden)
185            .map(|(neurons, inputs)| make(neurons, inputs))
186            .collect();
187
188        Self { layers }
189    }
190
191    /// The `(neurons, inputs)` of every layer after the input layer.
192    fn layer_shapes(hidden: &[usize]) -> impl Iterator<Item = (usize, usize)> + '_ {
193        const { assert!(IN > 0, "a network needs at least one input neuron") };
194        const { assert!(OUT > 0, "a network needs at least one output neuron") };
195        assert!(
196            hidden.iter().all(|&size| size > 0),
197            "every hidden layer must have at least one neuron, got {hidden:?}"
198        );
199
200        let inputs = std::iter::once(IN).chain(hidden.iter().copied());
201        let neurons = hidden.iter().copied().chain(std::iter::once(OUT));
202        neurons.zip(inputs)
203    }
204
205    /// Feeds the given inputs to the neural network and returns the output.
206    ///
207    /// The input and output widths are checked at compile time.
208    ///
209    /// # Example
210    ///
211    /// ```
212    /// # use only_brain::NeuralNetwork;
213    /// let mut nn = NeuralNetwork::<1, 1>::new(&[]);
214    ///
215    /// nn.set_layer_weights(1, &[[0.5]]);
216    /// nn.set_layer_biases(1, &[0.5]);
217    ///
218    /// let output = nn.feed_forward(&[0.5]);
219    /// assert!((output[0] - 0.679178699175393).abs() < 1e-12);
220    /// ```
221    pub fn feed_forward(&self, inputs: &[f64; IN]) -> [f64; OUT] {
222        let mut outputs = DVector::from_row_slice(inputs);
223
224        for layer in &self.layers {
225            outputs = layer.forward(&outputs);
226        }
227
228        // The layer chain is built from IN..OUT and every setter checks its dimensions,
229        // so the final vector always has exactly OUT elements.
230        <[f64; OUT]>::try_from(outputs.as_slice())
231            .expect("output layer width should match the OUT type parameter")
232    }
233
234    /// Returns the number of weights and biases in the network, which is the length of
235    /// [`parameters`](Self::parameters).
236    pub fn parameter_count(&self) -> usize {
237        self.layers.iter().map(Layer::parameter_count).sum()
238    }
239
240    /// Returns the number of weights and biases a network with these hidden layer sizes
241    /// has, without building one. This is the genome length to give a genetic algorithm.
242    ///
243    /// # Panics
244    ///
245    /// Panics if any hidden layer size is zero.
246    ///
247    /// # Example
248    ///
249    /// ```
250    /// # use only_brain::NeuralNetwork;
251    /// // 10 -> 8 -> 3: (10 + 1) * 8 + (8 + 1) * 3
252    /// assert_eq!(NeuralNetwork::<10, 3>::parameter_count_for(&[8]), 115);
253    /// ```
254    pub fn parameter_count_for(hidden: &[usize]) -> usize {
255        Self::layer_shapes(hidden)
256            .map(|(neurons, inputs)| Layer::parameter_count_for(neurons, inputs))
257            .sum()
258    }
259
260    /// Returns every weight and bias as one flat list, in the order described in the
261    /// [flat parameter view](Self#flat-parameter-view).
262    pub fn parameters(&self) -> Vec<f64> {
263        let mut parameters = Vec::with_capacity(self.parameter_count());
264        for layer in &self.layers {
265            layer.extend_parameters(&mut parameters);
266        }
267        parameters
268    }
269
270    /// Replaces every weight and bias from one flat list, in the order described in the
271    /// [flat parameter view](Self#flat-parameter-view). Activation functions are kept.
272    ///
273    /// # Panics
274    ///
275    /// Panics if `parameters` does not hold exactly
276    /// [`parameter_count()`](Self::parameter_count) values.
277    pub fn set_parameters(&mut self, parameters: &[f64]) {
278        let expected = self.parameter_count();
279        assert_eq!(
280            parameters.len(),
281            expected,
282            "Incompatible parameter count: expected {expected} parameters, got {}",
283            parameters.len()
284        );
285
286        let mut rest = parameters;
287        for layer in &mut self.layers {
288            rest = layer.take_parameters(rest);
289        }
290    }
291
292    /// Sets every weight of the given layer, as one row per neuron of that layer
293    /// holding one weight per neuron of the previous layer.
294    ///
295    /// Layer 0 is the input layer, which has no weights, so `layer` starts at 1. Nested
296    /// arrays and `Vec<Vec<f64>>` are both accepted.
297    ///
298    /// # Panics
299    ///
300    /// Panics if `layer` is 0 or past the output layer, or if `weights` does not have
301    /// exactly `layer_size(layer)` rows of `layer_size(layer - 1)` weights.
302    ///
303    /// # Example
304    ///
305    /// ```
306    /// # use only_brain::NeuralNetwork;
307    /// let mut nn = NeuralNetwork::<3, 1>::new(&[2]);
308    ///
309    /// nn.set_layer_weights(1, &[[0.1, 0.2, 0.3],
310    ///                           [0.4, 0.5, 0.6]]);
311    ///
312    /// // Weights computed at runtime work the same way.
313    /// let output_weights = vec![vec![0.7, 0.8]];
314    /// nn.set_layer_weights(2, &output_weights);
315    ///
316    /// assert_eq!(nn.layer_weights(2), output_weights);
317    /// ```
318    pub fn set_layer_weights<R: AsRef<[f64]>>(&mut self, layer: usize, weights: &[R]) {
319        self.layer_mut(layer).set_weights(weights);
320    }
321
322    /// Returns the weights of the given layer, as one row per neuron of that layer
323    /// holding one weight per neuron of the previous layer.
324    ///
325    /// # Panics
326    ///
327    /// Panics if `layer` is 0 or past the output layer.
328    pub fn layer_weights(&self, layer: usize) -> Vec<Vec<f64>> {
329        self.layer(layer).weight_rows()
330    }
331
332    /// Sets the biases of the given layer, one per neuron.
333    ///
334    /// # Panics
335    ///
336    /// Panics if `layer` is 0 or past the output layer, or if `biases` does not have
337    /// exactly `layer_size(layer)` elements.
338    pub fn set_layer_biases(&mut self, layer: usize, biases: &[f64]) {
339        self.layer_mut(layer).set_biases(biases);
340    }
341
342    /// Returns the biases of the given layer, one per neuron.
343    ///
344    /// # Panics
345    ///
346    /// Panics if `layer` is 0 or past the output layer.
347    pub fn layer_biases(&self, layer: usize) -> &[f64] {
348        self.layer(layer).biases().as_slice()
349    }
350
351    /// Sets the weight of a specific neuron connection. The layer index must be greater
352    /// than 0 since the input layer does not have weights.
353    pub fn set_weight(&mut self, layer: usize, neuron: usize, input: usize, weight: f64) {
354        self.layer_mut(layer).set_weight(neuron, input, weight);
355    }
356
357    /// Gets the weight of a specific neuron connection. The layer index must be greater
358    /// than 0 since the input layer does not have weights.
359    pub fn get_weight(&self, layer: usize, neuron: usize, input: usize) -> f64 {
360        self.layer(layer).weights()[(neuron, input)]
361    }
362
363    /// Sets the bias of a specific neuron. The layer index must be greater than 0 since
364    /// the input layer does not have biases.
365    pub fn set_bias(&mut self, layer: usize, neuron: usize, bias: f64) {
366        self.layer_mut(layer).set_bias(neuron, bias);
367    }
368
369    /// Gets the bias of a specific neuron. The layer index must be greater than 0 since
370    /// the input layer does not have biases.
371    pub fn get_bias(&self, layer: usize, neuron: usize) -> f64 {
372        self.layer(layer).biases()[neuron]
373    }
374
375    /// The layer that receives weights for `layer`, which counts the input layer as 0.
376    fn layer(&self, layer: usize) -> &Layer {
377        if layer == 0 {
378            panic!("Invalid layer index");
379        }
380        &self.layers[layer - 1]
381    }
382
383    fn layer_mut(&mut self, layer: usize) -> &mut Layer {
384        if layer == 0 {
385            panic!("Invalid layer index");
386        }
387        &mut self.layers[layer - 1]
388    }
389
390    /// Returns the number of layers of the neural network, counting the input layer.
391    pub fn num_layers(&self) -> usize {
392        self.layers.len() + 1
393    }
394
395    /// Returns the number of neurons of the given layer. Layer 0 is the input layer.
396    pub fn layer_size(&self, layer: usize) -> usize {
397        if layer == 0 {
398            return IN;
399        }
400        self.layer(layer).size()
401    }
402
403    /// Returns the sizes of the hidden layers, the same list given to
404    /// [`new`](Self::new) or [`from_parameters`](Self::from_parameters).
405    ///
406    /// ```
407    /// # use only_brain::NeuralNetwork;
408    /// let nn = NeuralNetwork::<4, 2>::new(&[8, 6]);
409    ///
410    /// let copy = NeuralNetwork::<4, 2>::from_parameters(&nn.hidden_layer_sizes(), &nn.parameters());
411    /// assert_eq!(copy.hidden_layer_sizes(), vec![8, 6]);
412    /// ```
413    pub fn hidden_layer_sizes(&self) -> Vec<usize> {
414        let hidden = &self.layers[..self.layers.len() - 1];
415        hidden.iter().map(Layer::size).collect()
416    }
417
418    /// Returns the activation function of the given layer.
419    ///
420    /// # Panics
421    ///
422    /// Panics if `layer` is 0, since the input layer has no activation, or past the
423    /// output layer.
424    pub fn layer_activation(&self, layer: usize) -> ActivationFunction {
425        self.layer(layer).activation()
426    }
427
428    /// Sets the activation function of the given layer.
429    ///
430    /// # Panics
431    ///
432    /// Panics if `layer` is 0, since the input layer has no activation, or past the
433    /// output layer.
434    pub fn set_layer_activation(&mut self, layer: usize, activation_function: ActivationFunction) {
435        self.layer_mut(layer).set_activation(activation_function);
436    }
437
438    /// Sets the activation function of every layer of the network.
439    ///
440    /// Combine it with [`set_output_activation`](Self::set_output_activation) to give
441    /// the hidden layers and the output layer different functions.
442    ///
443    /// # Example
444    ///
445    /// ```
446    /// # use only_brain::{ActivationFunction, NeuralNetwork};
447    /// let mut nn = NeuralNetwork::<2, 1>::new(&[3]);
448    /// nn.set_activation_function(ActivationFunction::ReLU);
449    /// nn.set_output_activation(ActivationFunction::Tanh);
450    ///
451    /// assert_eq!(nn.layer_activation(1), ActivationFunction::ReLU);
452    /// assert_eq!(nn.layer_activation(2), ActivationFunction::Tanh);
453    /// ```
454    pub fn set_activation_function(&mut self, activation_function: ActivationFunction) {
455        for layer in &mut self.layers {
456            layer.set_activation(activation_function);
457        }
458    }
459
460    /// Sets the activation function of the output layer only.
461    pub fn set_output_activation(&mut self, activation_function: ActivationFunction) {
462        self.set_layer_activation(self.num_layers() - 1, activation_function);
463    }
464
465    /// Returns the activation function of the output layer.
466    pub fn output_activation(&self) -> ActivationFunction {
467        self.layer_activation(self.num_layers() - 1)
468    }
469
470    pub fn print(&self) {
471        for layer in &self.layers {
472            println!("{:?} {} {}", layer.activation(), layer.weights(), layer.biases());
473        }
474    }
475}
476
477impl<const IN: usize, const OUT: usize> From<NeuralNetwork<IN, OUT>> for NetworkData {
478    fn from(network: NeuralNetwork<IN, OUT>) -> Self {
479        NetworkData::from(&network)
480    }
481}
482
483impl<const IN: usize, const OUT: usize> From<&NeuralNetwork<IN, OUT>> for NetworkData {
484    fn from(network: &NeuralNetwork<IN, OUT>) -> Self {
485        let layers = network
486            .layers
487            .iter()
488            .map(|layer| LayerData {
489                activation: layer.activation(),
490                weights: layer.weight_rows(),
491                biases: layer.biases().iter().copied().collect(),
492            })
493            .collect();
494
495        NetworkData { layers }
496    }
497}
498
499impl<const IN: usize, const OUT: usize> TryFrom<NetworkData> for NeuralNetwork<IN, OUT> {
500    type Error = ModelError;
501
502    /// Checks that the stored layers chain from `IN` inputs to `OUT` outputs and that
503    /// each layer agrees with itself.
504    fn try_from(data: NetworkData) -> Result<Self, Self::Error> {
505        let first = data.layers.first().ok_or(ModelError::EmptyNetwork)?;
506        if let Some(row) = first.weights.first() {
507            if row.len() != IN {
508                return Err(ModelError::DimensionMismatch {
509                    end: "input",
510                    expected: IN,
511                    found: row.len(),
512                });
513            }
514        }
515
516        let mut inputs = IN;
517        let mut layers = Vec::with_capacity(data.layers.len());
518        for (index, layer) in data.layers.iter().enumerate() {
519            let layer = Layer::from_parts(&layer.weights, &layer.biases, layer.activation, inputs)
520                .map_err(|reason| ModelError::InconsistentLayer {
521                    layer: index + 1,
522                    reason,
523                })?;
524            inputs = layer.size();
525            layers.push(layer);
526        }
527
528        if inputs != OUT {
529            return Err(ModelError::DimensionMismatch {
530                end: "output",
531                expected: OUT,
532                found: inputs,
533            });
534        }
535
536        Ok(Self { layers })
537    }
538}
539
540impl<const IN: usize, const OUT: usize> fmt::Display for NeuralNetwork<IN, OUT> {
541    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
542        writeln!(f, "Neural Network")?;
543        writeln!(f)?;
544        writeln!(f, "Input Layer Size: {IN}")?;
545        writeln!(f)?;
546        for (index, layer) in self.layers.iter().enumerate() {
547            writeln!(f, "Layer {}: {}", index + 1, layer)?;
548        }
549        Ok(())
550    }
551}
552
553#[cfg(test)]
554mod tests {
555    use super::*;
556    use crate::activation_functions::{binary_step, identity, relu, sigmoid, tanh};
557
558    const EPSILON: f64 = 1e-12;
559
560    fn assert_all_close(actual: &[f64], expected: &[f64]) {
561        assert_eq!(actual.len(), expected.len(), "length mismatch");
562        for (i, (a, e)) in actual.iter().zip(expected).enumerate() {
563            assert!((a - e).abs() < EPSILON, "at index {i}: expected {e}, got {a}");
564        }
565    }
566
567    /// A 2 -> 1 network with known weights, so outputs can be checked by hand.
568    fn fixed_network() -> NeuralNetwork<2, 1> {
569        let mut nn = NeuralNetwork::<2, 1>::new(&[]);
570        nn.set_layer_weights(1, &[[0.5, -0.25]]);
571        nn.set_layer_biases(1, &[0.1]);
572        nn
573    }
574
575    #[test]
576    fn new_reports_layer_count_and_sizes() {
577        let nn = NeuralNetwork::<3, 2>::new(&[5]);
578
579        assert_eq!(nn.num_layers(), 3);
580        assert_eq!(nn.layer_size(0), 3);
581        assert_eq!(nn.layer_size(1), 5);
582        assert_eq!(nn.layer_size(2), 2);
583    }
584
585    /// A network with no hidden layers is still a valid two-layer network. The old
586    /// "fewer than two layers" runtime check is gone because IN and OUT guarantee it.
587    #[test]
588    fn new_without_hidden_layers_wires_input_straight_to_output() {
589        let nn = NeuralNetwork::<3, 2>::new(&[]);
590
591        assert_eq!(nn.num_layers(), 2);
592        assert_eq!(nn.layer_size(0), 3);
593        assert_eq!(nn.layer_size(1), 2);
594    }
595
596    #[test]
597    #[should_panic(expected = "at least one neuron")]
598    fn new_rejects_a_zero_sized_hidden_layer() {
599        NeuralNetwork::<2, 1>::new(&[0]);
600    }
601
602    #[test]
603    fn new_with_rng_is_reproducible_for_the_same_seed() {
604        use rand::SeedableRng;
605
606        let mut first_rng = rand::rngs::StdRng::seed_from_u64(42);
607        let mut second_rng = rand::rngs::StdRng::seed_from_u64(42);
608
609        let first = NeuralNetwork::<3, 2>::new_with_rng(&[4], &mut first_rng);
610        let second = NeuralNetwork::<3, 2>::new_with_rng(&[4], &mut second_rng);
611
612        for layer in 1..first.num_layers() {
613            for neuron in 0..first.layer_size(layer) {
614                for input in 0..first.layer_size(layer - 1) {
615                    assert_eq!(
616                        first.get_weight(layer, neuron, input),
617                        second.get_weight(layer, neuron, input),
618                        "weight differs at layer {layer}, neuron {neuron}, input {input}"
619                    );
620                }
621            }
622        }
623    }
624
625    #[test]
626    fn feed_forward_applies_weights_bias_and_activation() {
627        let nn = fixed_network();
628
629        // 0.5 * 1.0 + (-0.25) * 2.0 + 0.1 = 0.1
630        let output = nn.feed_forward(&[1.0, 2.0]);
631
632        assert_all_close(&output, &[sigmoid(0.1)]);
633    }
634
635    #[test]
636    fn feed_forward_defaults_to_sigmoid() {
637        let nn = fixed_network();
638
639        assert_eq!(nn.layer_activation(1), ActivationFunction::Sigmoid);
640        assert_all_close(&nn.feed_forward(&[1.0, 2.0]), &[sigmoid(0.1)]);
641    }
642
643    /// The activation function used to be a field with no setter, so every
644    /// network silently ran sigmoid regardless of what was configured.
645    #[test]
646    fn feed_forward_honours_every_activation_function() {
647        let cases = [
648            (ActivationFunction::Sigmoid, sigmoid as fn(f64) -> f64),
649            (ActivationFunction::Tanh, tanh),
650            (ActivationFunction::ReLU, relu),
651            (ActivationFunction::BinaryStep, binary_step),
652            (ActivationFunction::Identity, identity),
653        ];
654
655        for (variant, expected) in cases {
656            let mut nn = fixed_network();
657            nn.set_activation_function(variant);
658
659            assert_eq!(nn.layer_activation(1), variant);
660            assert_all_close(&nn.feed_forward(&[1.0, 2.0]), &[expected(0.1)]);
661        }
662    }
663
664    /// `BinaryStep` was missing from the old lookup table and panicked here.
665    #[test]
666    fn binary_step_does_not_panic() {
667        let mut nn = fixed_network();
668        nn.set_activation_function(ActivationFunction::BinaryStep);
669
670        assert_all_close(&nn.feed_forward(&[1.0, 2.0]), &[1.0]);
671        assert_all_close(&nn.feed_forward(&[-1.0, 2.0]), &[0.0]);
672    }
673
674    #[test]
675    fn set_and_get_weight_round_trip() {
676        let mut nn = NeuralNetwork::<2, 2>::new(&[]);
677        nn.set_weight(1, 1, 0, 0.75);
678
679        assert_eq!(nn.get_weight(1, 1, 0), 0.75);
680    }
681
682    #[test]
683    #[should_panic(expected = "Invalid layer index")]
684    fn set_layer_weights_rejects_layer_zero() {
685        let mut nn = NeuralNetwork::<2, 1>::new(&[]);
686        nn.set_layer_weights(0, &[[0.5, 0.5]]);
687    }
688
689    #[test]
690    #[should_panic(expected = "Incompatible weights matrix size")]
691    fn set_layer_weights_rejects_a_mismatched_matrix() {
692        let mut nn = NeuralNetwork::<2, 1>::new(&[]);
693        nn.set_layer_weights(1, &[[0.5, 0.5, 0.5]]);
694    }
695
696    #[test]
697    #[should_panic(expected = "Incompatible biases vector size")]
698    fn set_layer_biases_rejects_a_mismatched_vector() {
699        let mut nn = NeuralNetwork::<2, 1>::new(&[]);
700        nn.set_layer_biases(1, &[0.1, 0.2]);
701    }
702
703    /// `Display` used to print the address of a function pointer here.
704    #[test]
705    fn display_names_the_activation_function() {
706        let mut nn = fixed_network();
707        nn.set_activation_function(ActivationFunction::ReLU);
708
709        let rendered = nn.to_string();
710
711        assert!(
712            rendered.contains("Activation Function: ReLU"),
713            "unexpected output:\n{rendered}"
714        );
715        assert!(
716            !rendered.contains("0x"),
717            "output leaked a pointer address:\n{rendered}"
718        );
719    }
720
721    #[test]
722    fn display_reports_the_input_layer_size() {
723        let nn = NeuralNetwork::<4, 1>::new(&[2]);
724
725        assert!(nn.to_string().contains("Input Layer Size: 4"));
726    }
727
728    #[test]
729    fn layer_weights_and_biases_round_trip_in_row_major_order() {
730        let mut nn = NeuralNetwork::<2, 3>::new(&[]);
731        let weights = [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]];
732
733        nn.set_layer_weights(1, &weights);
734        nn.set_layer_biases(1, &[0.7, 0.8, 0.9]);
735
736        assert_eq!(nn.layer_weights(1), weights.map(Vec::from).to_vec());
737        assert_eq!(nn.layer_biases(1), &[0.7, 0.8, 0.9]);
738        // Row = neuron, column = input, matching set_weight and get_weight.
739        assert_eq!(nn.get_weight(1, 2, 0), 0.5);
740    }
741
742    #[test]
743    fn set_layer_weights_accepts_weights_built_at_runtime() {
744        let mut nn = NeuralNetwork::<2, 1>::new(&[]);
745        let weights: Vec<Vec<f64>> = vec![vec![0.5, -0.25]];
746
747        nn.set_layer_weights(1, &weights);
748
749        assert_eq!(nn.layer_weights(1), weights);
750    }
751
752    #[test]
753    #[should_panic(expected = "Incompatible weights matrix size")]
754    fn set_layer_weights_rejects_ragged_rows() {
755        let mut nn = NeuralNetwork::<2, 2>::new(&[]);
756        let ragged: Vec<Vec<f64>> = vec![vec![0.1, 0.2], vec![0.3]];
757
758        nn.set_layer_weights(1, &ragged);
759    }
760
761    #[test]
762    #[should_panic(expected = "Incompatible weights matrix size")]
763    fn set_layer_weights_rejects_the_wrong_number_of_rows() {
764        let mut nn = NeuralNetwork::<2, 2>::new(&[]);
765        nn.set_layer_weights(1, &[[0.1, 0.2]]);
766    }
767
768    #[test]
769    fn set_and_get_bias_round_trip() {
770        let mut nn = NeuralNetwork::<2, 2>::new(&[]);
771        nn.set_bias(1, 1, -0.3);
772
773        assert_eq!(nn.get_bias(1, 1), -0.3);
774        assert_eq!(nn.layer_biases(1), &[0.0, -0.3]);
775    }
776
777    #[test]
778    #[should_panic(expected = "Invalid layer index")]
779    fn layer_biases_rejects_layer_zero() {
780        let nn = NeuralNetwork::<2, 1>::new(&[]);
781        nn.layer_biases(0);
782    }
783
784    /// A 2 -> 2 -> 1 network whose every parameter is distinct, so a wrong order shows.
785    fn numbered_network() -> NeuralNetwork<2, 1> {
786        let mut nn = NeuralNetwork::<2, 1>::new(&[2]);
787        nn.set_layer_weights(1, &[[1.0, 2.0],
788                                  [3.0, 4.0]]);
789        nn.set_layer_biases(1, &[5.0, 6.0]);
790        nn.set_layer_weights(2, &[[7.0, 8.0]]);
791        nn.set_layer_biases(2, &[9.0]);
792        nn
793    }
794
795    #[test]
796    fn hidden_and_output_layers_can_use_different_activations() {
797        let mut nn = NeuralNetwork::<1, 1>::new(&[1]);
798        nn.set_layer_weights(1, &[[1.0]]);
799        nn.set_layer_weights(2, &[[1.0]]);
800        nn.set_activation_function(ActivationFunction::ReLU);
801        nn.set_output_activation(ActivationFunction::Tanh);
802
803        // relu(-2) = 0, then tanh(0) = 0; relu(2) = 2, then tanh(2).
804        assert_all_close(&nn.feed_forward(&[-2.0]), &[0.0]);
805        assert_all_close(&nn.feed_forward(&[2.0]), &[tanh(2.0)]);
806        assert_eq!(nn.layer_activation(1), ActivationFunction::ReLU);
807        assert_eq!(nn.output_activation(), ActivationFunction::Tanh);
808    }
809
810    #[test]
811    fn set_layer_activation_changes_only_that_layer() {
812        let mut nn = NeuralNetwork::<2, 1>::new(&[3, 3]);
813        nn.set_layer_activation(2, ActivationFunction::Identity);
814
815        assert_eq!(nn.layer_activation(1), ActivationFunction::Sigmoid);
816        assert_eq!(nn.layer_activation(2), ActivationFunction::Identity);
817        assert_eq!(nn.layer_activation(3), ActivationFunction::Sigmoid);
818    }
819
820    #[test]
821    #[should_panic(expected = "Invalid layer index")]
822    fn the_input_layer_has_no_activation() {
823        NeuralNetwork::<2, 1>::new(&[]).layer_activation(0);
824    }
825
826    #[test]
827    fn parameters_follow_the_documented_order() {
828        let nn = numbered_network();
829
830        assert_eq!(
831            nn.parameters(),
832            vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
833        );
834    }
835
836    #[test]
837    fn parameter_count_matches_the_shape() {
838        let nn = NeuralNetwork::<10, 3>::new(&[8]);
839
840        assert_eq!(nn.parameter_count(), 11 * 8 + 9 * 3);
841        assert_eq!(nn.parameters().len(), nn.parameter_count());
842        assert_eq!(NeuralNetwork::<10, 3>::parameter_count_for(&[8]), nn.parameter_count());
843        assert_eq!(NeuralNetwork::<10, 3>::parameter_count_for(&[]), 11 * 3);
844    }
845
846    #[test]
847    fn set_parameters_writes_the_documented_order() {
848        let mut nn = NeuralNetwork::<2, 1>::new(&[2]);
849        nn.set_parameters(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]);
850
851        assert_eq!(nn, numbered_network());
852        assert_eq!(nn.get_weight(1, 1, 0), 3.0);
853        assert_eq!(nn.get_bias(2, 0), 9.0);
854    }
855
856    #[test]
857    fn set_parameters_keeps_activation_functions() {
858        let mut nn = numbered_network();
859        nn.set_output_activation(ActivationFunction::Tanh);
860
861        nn.set_parameters(&[0.0; 9]);
862
863        assert_eq!(nn.output_activation(), ActivationFunction::Tanh);
864    }
865
866    #[test]
867    fn parameters_round_trip_through_from_parameters() {
868        use rand::SeedableRng;
869        let mut rng = rand::rngs::StdRng::seed_from_u64(7);
870        let original = NeuralNetwork::<3, 2>::new_with_rng(&[4, 5], &mut rng);
871
872        let rebuilt = NeuralNetwork::<3, 2>::from_parameters(
873            &original.hidden_layer_sizes(),
874            &original.parameters(),
875        );
876
877        assert_eq!(rebuilt, original);
878        assert_all_close(
879            &rebuilt.feed_forward(&[0.1, -0.2, 0.3]),
880            &original.feed_forward(&[0.1, -0.2, 0.3]),
881        );
882    }
883
884    #[test]
885    #[should_panic(expected = "expected 9 parameters, got 8")]
886    fn set_parameters_rejects_a_short_list() {
887        numbered_network().set_parameters(&[0.0; 8]);
888    }
889
890    #[test]
891    #[should_panic(expected = "expected 9 parameters, got 10")]
892    fn from_parameters_rejects_a_long_list() {
893        NeuralNetwork::<2, 1>::from_parameters(&[2], &[0.0; 10]);
894    }
895
896    #[test]
897    #[should_panic(expected = "at least one neuron")]
898    fn parameter_count_for_rejects_a_zero_sized_hidden_layer() {
899        NeuralNetwork::<2, 1>::parameter_count_for(&[3, 0]);
900    }
901
902    #[test]
903    fn hidden_layer_sizes_lists_only_hidden_layers() {
904        assert_eq!(NeuralNetwork::<2, 1>::new(&[]).hidden_layer_sizes(), Vec::<usize>::new());
905        assert_eq!(NeuralNetwork::<2, 1>::new(&[5, 3]).hidden_layer_sizes(), vec![5, 3]);
906    }
907
908    /// Fitness functions run in parallel (rayon), which needs Send + Sync.
909    #[test]
910    fn networks_can_be_shared_across_threads() {
911        fn assert_send_sync<T: Send + Sync>() {}
912        assert_send_sync::<NeuralNetwork<10, 3>>();
913    }
914}