• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wildjames / predictive_coding_rs / 22864321797

09 Mar 2026 04:45PM UTC coverage: 91.047% (-0.6%) from 91.662%
22864321797

push

github

web-flow
Move eval and infer code out to its own module (#13)

* Move eval code out to library. Also, rename the training handler files.

* Move the data handler access to the mod file, to make import paths a bit shorter

* Restructure model access

* Make training imports cleaner

* better import path for ingerence module

* Go through and check for super imports I missed

* Run rust fmt

1582 of 1742 new or added lines in 20 files covered. (90.82%)

2 existing lines in 2 files now uncovered.

1678 of 1843 relevant lines covered (91.05%)

16.14 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.04
/src/model/model_structure.rs
1
//! Predictive coding model implementation.
2
//!
3
//! Defines a layered model with local prediction errors and weight updates.
4

5
use super::maths::{ActivationFunction, outer_product};
6

7
use ndarray::{Array1, Array2};
8
use rand::RngExt;
9
use serde::{Deserialize, Serialize};
10

11
/// A single predictive coding layer with values, predictions, errors, and weights.
12
#[derive(Clone, Debug, Serialize, Deserialize)]
13
pub struct Layer {
14
    pub values: Array1<f32>,
15
    /// node activation values for this layer, x^l
16
    pub predictions: Array1<f32>, //Predictions for the value of nodes in this layer, according to the layer above. u^l = f(x^{l+1}, w^{l+1})
17
    pub errors: Array1<f32>,  // Errors for this layer, e^l
18
    pub weights: Array2<f32>, // weights to predict the layer below, w^l
19
    pub pinned: bool, // If a layer is pinned, its values are not updated during time evolution (e.g. input layers in unsupervised learning, or input and output layers in supervised learning)
20
    pub activation_function: ActivationFunction,
21
    pub size: usize, // The number of nodes in this layer, for easy reference. Should be the same as values.len(), predictions.len(), and /errors.len()
22
    xavier_limit: f32,
23
}
24

25
impl Layer {
26
    /// Initialises a layer of the given size.
27
    /// Populates the values if given, and pins the layer against changing the values during compute iterations if specified.
28
    /// If values are not given, they're set to random vlaues between 0 and 1
29
    /// Weights are randomly initialised, and predictions and errors are initialised to 0.0
30
    /// Takes ownership of the given values, if they are given, so that we can updated them in place later.
31
    fn new(
26✔
32
        size: usize,
26✔
33
        lower_size: Option<usize>,
26✔
34
        activation_function: ActivationFunction,
26✔
35
        values: Option<Array1<f32>>,
26✔
36
        pinned: Option<bool>,
26✔
37
    ) -> Self {
26✔
38
        let mut rng = rand::rng();
26✔
39

40
        // Use provided values if we have them, otherwise random data 0..1
41
        let values: Array1<f32> = match values {
26✔
42
            Some(v) => v,
2✔
43
            None => Array1::from_shape_fn(size, |_| rng.random_range(0.0..1.0)),
146✔
44
        };
45

46
        // Generate random weights for a blank model layer.
47
        // Shape is (lower_size, size) to map from this layer to the one below.
48
        let weights_shape = match lower_size {
26✔
49
            Some(lower) => (lower, size),
13✔
50
            None => (0, size),
13✔
51
        };
52
        // Xavier initialization: U(-limit, limit) where limit = sqrt(6 / (fan_in + fan_out))
53
        let xavier_limit: f32 = if weights_shape.0 + weights_shape.1 > 0 {
26✔
54
            (6.0_f32 / (weights_shape.0 + weights_shape.1) as f32).sqrt()
25✔
55
        } else {
56
            1.0
1✔
57
        };
58
        let weights = Array2::from_shape_fn(weights_shape, |_| {
408✔
59
            rng.random_range(-xavier_limit..xavier_limit)
408✔
60
        });
408✔
61

62
        Layer {
26✔
63
            values,
26✔
64
            predictions: Array1::zeros(size),
26✔
65
            errors: Array1::zeros(size),
26✔
66
            weights,
26✔
67
            pinned: pinned.unwrap_or(false),
26✔
68
            activation_function,
26✔
69
            size,
26✔
70
            xavier_limit,
26✔
71
        }
26✔
72
    }
26✔
73

74
    /// Randomise weights between -xavier_limit and xavier_limit for all nodes in this layer.
75
    pub fn randomise_weights(&mut self) {
1✔
76
        let mut rng = rand::rng();
1✔
77
        self.weights = Array2::from_shape_fn(self.weights.dim(), |_| {
6✔
78
            rng.random_range(-self.xavier_limit..self.xavier_limit)
6✔
79
        });
6✔
80
    }
1✔
81

82
    /// Randomise values between 0..1 for all nodes in this layer.
83
    pub fn randomise_values(&mut self, rng: &mut rand::prelude::ThreadRng) {
5✔
84
        self.values = Array1::from_shape_fn(self.values.len(), |_| rng.random_range(0.0..1.0));
38✔
85
    }
5✔
86

87
    /// Replace the layer values and pin them to avoid updates during inference.
88
    fn pin_values(&mut self, values: Array1<f32>) {
64✔
89
        self.values = values;
64✔
90
        self.pinned = true;
64✔
91
    }
64✔
92

93
    /// Unpin the layer values to allow updates during inference.
94
    fn unpin_values(&mut self) {
2✔
95
        self.pinned = false;
2✔
96
    }
2✔
97

98
    /// Update the predictions for this layer based on the values of the layer above it.
99
    fn compute_predictions(&mut self, upper_layer: &Layer) {
32✔
100
        // Note that the prediction computation should *never* be run for an output layer, but making sure of this is the responsibility of the model, not the layer.
101
        // Besides, since an output layer has no upper layer to pass in, this function would not be callable
102

103
        // u^l = phi(W^{l+1} * x^{l+1})
104
        // Preactivation first, then apply nonlinearity
105
        let preactivation: Array1<f32> = upper_layer.weights.dot(&upper_layer.values);
32✔
106
        self.predictions = preactivation.mapv(|a| upper_layer.activation_function.apply(a));
128✔
107
    }
32✔
108

109
    /// Update the errors for this layer based on the predictions and values of this layer.
110
    fn compute_errors(&mut self) {
64✔
111
        self.errors = &self.values - &self.predictions;
64✔
112
    }
64✔
113

114
    /// Sum the signed error values for all nodes in this layer.
NEW
115
    fn read_total_error(&self) -> f32 {
×
NEW
116
        self.errors.iter().sum()
×
NEW
117
    }
×
118

119
    /// Sum the squared error values for all nodes in this layer.
120
    fn read_total_energy(&self) -> f32 {
10✔
121
        // E = 1/2 * sum(err^2)
122
        self.errors.mapv(|x| x.powi(2)).iter().sum()
70✔
123
    }
10✔
124

125
    /// Compute the change in node values under a single timestep of PC.
126
    /// Returns the summed absolute change in node values across this layer.
127
    /// For the input layer, there is no lower layer and None should be passed in instead.
128
    fn values_timestep(
65✔
129
        &mut self,
65✔
130
        is_top_level: bool,
65✔
131
        gamma: f32,
65✔
132
        lower_layer: Option<&Layer>,
65✔
133
    ) -> f32 {
65✔
134
        if self.pinned {
65✔
135
            return 0.0;
63✔
136
        }
2✔
137

138
        let rhs: Array1<f32> = if let Some(lower_layer) = lower_layer {
2✔
139
            // RHS: W^{l,T} * (phi'(a^{l-1}) (hammard) e^{l-1})
140
            // where a^{l-1} = W^l * x^l is the preactivation for the layer below (see 2506.06332)
141

142
            // a^{l-1} = W^l * x^l
143
            let preactivation: Array1<f32> = self.weights.dot(&self.values);
2✔
144

145
            // phi'(a^{l-1})
146
            let activation_function_eval_derivitive: Array1<f32> =
2✔
147
                preactivation.mapv(|a| self.activation_function.derivative(a));
5✔
148

149
            // phi'(a^{l-1}) (hammard) e^{l-1}
150
            let gain_modulated_errors: Array1<f32> =
2✔
151
                activation_function_eval_derivitive * &lower_layer.errors;
2✔
152

153
            // W^{l,T} * (phi'(a^{l-1}) (hammard) e^{l-1})
154
            self.weights.t().dot(&gain_modulated_errors)
2✔
155
        } else {
NEW
156
            Array1::zeros(self.values.len())
×
157
        };
158

159
        // Note that in the output layer, errors are always 0 so the first term of the parentheses is ignored.
160
        let value_changes: Array1<f32> = if is_top_level {
2✔
161
            gamma * rhs
1✔
162
        } else {
163
            gamma * (-&self.errors + rhs)
1✔
164
        };
165

166
        // Update my values and sum the changes to return
167
        self.values += &value_changes;
2✔
168
        value_changes.mapv(|x| x.abs()).sum()
11✔
169
    }
65✔
170

171
    fn compute_weight_updates(&mut self, alpha: f32, lower_layer: &Layer) -> Array2<f32> {
47✔
172
        // W^{l+1} += alpha * (phi'(a^l) (hammard) e^l) * x^{l+1,T}
173
        // where a^l = W^{l+1} * x^{l+1} is the preactivation for the layer below
174
        let preactivation: Array1<f32> = self.weights.dot(&self.values);
47✔
175
        let activation_function_result: Array1<f32> =
47✔
176
            preactivation.mapv(|a| self.activation_function.derivative(a));
188✔
177
        let gain_modulated_errors: Array1<f32> = &activation_function_result * &lower_layer.errors;
47✔
178

179
        // outer product yields (lower_size, upper_size)
180
        alpha * outer_product(&gain_modulated_errors, &self.values)
47✔
181
    }
47✔
182

183
    /// Update prediction weights after convergence based on lower-layer errors.
184
    fn update_weights(&mut self, alpha: f32, lower_layer: &Layer) {
27✔
185
        let weight_changes: Array2<f32> = self.compute_weight_updates(alpha, lower_layer);
27✔
186
        self.weights += &weight_changes;
27✔
187
    }
27✔
188
}
189

190
/// A multi-layer predictive coding model with value and weight updates.
191
#[derive(Clone, Debug, Serialize, Deserialize)]
192
pub struct PredictiveCodingModel {
193
    layers: Vec<Layer>,
194
    alpha: f32, // synaptic learning rate
195
    gamma: f32, // neural learning rate
196
    convergence_threshold: f32,
197
    convergence_steps: u32,
198
}
199

200
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
201
pub struct PredictiveCodingModelConfig {
202
    pub layer_sizes: Vec<usize>,
203
    pub alpha: f32,
204
    pub gamma: f32,
205
    pub convergence_threshold: f32,
206
    pub convergence_steps: u32,
207
    pub activation_function: ActivationFunction,
208
}
209

210
impl PredictiveCodingModel {
211
    /// Construct a model with the given layer sizes and learning rates.
212
    ///
213
    /// alpha is the synaptic learning rate, which controls how much the weights are updated after each inference step.
214
    /// gamma is the neural learning rate, which controls how much the node values are updated during inference.
215
    /// activation_function is applied to the node values when computing predictions for the layer below.
216
    pub fn new(config: &PredictiveCodingModelConfig) -> Self {
11✔
217
        let mut layers = Vec::new();
11✔
218
        for (index, layer_size) in config.layer_sizes.iter().enumerate() {
23✔
219
            let lower_size = if index == 0 {
23✔
220
                None
11✔
221
            } else {
222
                Some(config.layer_sizes[index - 1])
12✔
223
            };
224

225
            layers.push(Layer::new(
23✔
226
                *layer_size,
23✔
227
                lower_size,
23✔
228
                config.activation_function,
23✔
229
                None,
23✔
230
                None,
23✔
231
            ));
232
        }
233

234
        PredictiveCodingModel {
11✔
235
            layers,
11✔
236
            alpha: config.alpha,
11✔
237
            gamma: config.gamma,
11✔
238
            convergence_threshold: config.convergence_threshold,
11✔
239
            convergence_steps: config.convergence_steps,
11✔
240
        }
11✔
241
    }
11✔
242

243
    // Getters for model properties, so I don't have to expose the model fields directly
244
    pub fn get_config(&self) -> PredictiveCodingModelConfig {
10✔
245
        PredictiveCodingModelConfig {
246
            layer_sizes: self.layers.iter().map(|l| l.size).collect(),
10✔
247
            alpha: self.alpha,
10✔
248
            gamma: self.gamma,
10✔
249
            convergence_steps: self.convergence_steps,
10✔
250
            convergence_threshold: self.convergence_threshold,
10✔
251
            // I only allow that all layers have the same activation function
252
            activation_function: self.layers.first().unwrap().activation_function,
10✔
253
        }
254
    }
10✔
NEW
255
    pub fn get_layers(&self) -> &Vec<Layer> {
×
NEW
256
        &self.layers
×
NEW
257
    }
×
258
    pub fn get_layer(&self, index: usize) -> &Layer {
2✔
259
        &self.layers[index]
2✔
260
    }
2✔
261
    pub fn get_layer_sizes(&self) -> Vec<usize> {
12✔
262
        self.layers.iter().map(|l| l.size).collect()
12✔
263
    }
12✔
NEW
264
    pub fn get_alpha(&self) -> f32 {
×
NEW
265
        self.alpha
×
NEW
266
    }
×
NEW
267
    pub fn get_gamma(&self) -> f32 {
×
NEW
268
        self.gamma
×
NEW
269
    }
×
NEW
270
    pub fn get_activation_function(&self) -> ActivationFunction {
×
NEW
271
        self.layers.first().unwrap().activation_function
×
NEW
272
    }
×
273

274
    /// Set the values of the input layer to the given input values, and pin the input layer.
275
    pub fn get_input(&self) -> &Array1<f32> {
1✔
276
        &self.layers[0].values
1✔
277
    }
1✔
278

279
    /// Sets the values of the input layer to the given input values, and pin the input layer.
280
    pub fn set_input(&mut self, input_values: Array1<f32>) {
33✔
281
        self.layers[0].pin_values(input_values);
33✔
282
    }
33✔
283

284
    /// Prevent the input layer values from being updated during inference, by pinning the input layer.
285
    pub fn pin_input(&mut self) {
4✔
286
        self.layers[0].pinned = true;
4✔
287
    }
4✔
288

289
    /// Allow the input layer values to be updated during inference, by unpinning the input layer.
NEW
290
    pub fn unpin_input(&mut self) {
×
NEW
291
        self.layers[0].unpin_values();
×
NEW
292
    }
×
293

294
    /// Randomise the input layer values between 0..1 and unpin the input layer to allow updates during inference.
NEW
295
    pub fn randomise_input(&mut self) {
×
NEW
296
        let input_layer = &mut self.layers[0];
×
NEW
297
        let mut rng = rand::rng();
×
NEW
298
        input_layer.randomise_values(&mut rng);
×
NEW
299
        input_layer.unpin_values();
×
NEW
300
    }
×
301

302
    /// Set the values of the output layer to the given output values, and pins the output layer.
303
    pub fn get_output(&self) -> &Array1<f32> {
2✔
304
        &self.layers.last().unwrap().values
2✔
305
    }
2✔
306

307
    /// Sets the values of the output layer to the given output values, and pins the output layer.
308
    pub fn set_output(&mut self, output_values: Array1<f32>) {
31✔
309
        self.layers.last_mut().unwrap().pin_values(output_values);
31✔
310
    }
31✔
311

312
    /// Prevent the output layer values from being updated during inference, by pinning the output layer.
313
    pub fn pin_output(&mut self) {
4✔
314
        self.layers.last_mut().unwrap().pinned = true;
4✔
315
    }
4✔
316

317
    /// Allow the output layer values to be updated during inference, by unpinning the output layer.
318
    pub fn unpin_output(&mut self) {
2✔
319
        self.layers.last_mut().unwrap().unpin_values();
2✔
320
    }
2✔
321

322
    /// Randomise the output layer values between 0..1 and unpin the output layer to allow updates during inference.
NEW
323
    pub fn randomise_output(&mut self) {
×
NEW
324
        let output_layer = self.layers.last_mut().unwrap();
×
NEW
325
        let mut rng = rand::rng();
×
NEW
326
        output_layer.randomise_values(&mut rng);
×
NEW
327
        output_layer.unpin_values();
×
NEW
328
    }
×
329

330
    /// Reinitialise all unpinned (latent) layer values to small random values.
331
    /// Should be called before each new training sample to avoid carrying over
332
    /// converged state from a previous sample.
333
    pub fn reinitialise_latents(&mut self) {
32✔
334
        let mut rng = rand::rng();
32✔
335
        for layer in &mut self.layers {
64✔
336
            if !layer.pinned {
64✔
337
                layer.randomise_values(&mut rng);
5✔
338
            }
59✔
339
        }
340
    }
32✔
341

342
    /// Evolves node values until convergence, recomputing predictions and errors each step.
343
    /// Returns the number of steps taken to converge.
344
    pub fn converge_values(&mut self) -> u32 {
31✔
345
        let mut converged: bool = false;
31✔
346
        let mut convergence_count: u32 = 0;
31✔
347

348
        while !converged && (convergence_count < self.convergence_steps) {
62✔
349
            self.compute_predictions_and_errors();
31✔
350

351
            if self.timestep().abs() < self.convergence_threshold {
31✔
NEW
352
                converged = true;
×
353
            }
31✔
354
            convergence_count += 1;
31✔
355
        }
356

357
        convergence_count
31✔
358
    }
31✔
359

360
    /// Compute predictions for each layer and then update errors.
361
    pub fn compute_predictions_and_errors(&mut self) {
32✔
362
        self.compute_predictions();
32✔
363
        self.compute_errors();
32✔
364
    }
32✔
365

366
    /// Compute predictions for all layers from top to bottom.
367
    pub fn compute_predictions(&mut self) {
32✔
368
        let num_layers = self.layers.len();
32✔
369
        for i in (0..num_layers - 1).rev() {
32✔
370
            // iterate backwards through the layers
32✔
371
            // Since the target layer needs to be mutable to update the predictions, I need to split the vector
32✔
372
            // Luckily, this is not a transformative operation, so split_at_mut is still fast
32✔
373
            let (lower, upper) = self.layers.split_at_mut(i + 1);
32✔
374
            let lower_layer = &mut lower[i];
32✔
375
            let upper_layer = &upper[0];
32✔
376

32✔
377
            lower_layer.compute_predictions(upper_layer);
32✔
378
        }
32✔
379
    }
32✔
380

381
    /// Compute prediction errors for all layers.
382
    pub fn compute_errors(&mut self) {
32✔
383
        for i in 0..self.layers.len() {
64✔
384
            self.layers[i].compute_errors();
64✔
385
        }
64✔
386
    }
32✔
387

388
    /// Sum signed errors across all layers.
NEW
389
    pub fn read_total_error(&self) -> f32 {
×
390
        // Sum the errors of all nodes in all layers
NEW
391
        let mut total_error = 0.0;
×
NEW
392
        for layer in &self.layers {
×
NEW
393
            total_error += layer.read_total_error();
×
NEW
394
        }
×
NEW
395
        total_error
×
NEW
396
    }
×
397

398
    /// Sum squared errors across all layers.
399
    pub fn read_total_energy(&self) -> f32 {
5✔
400
        // Sum the energy of all nodes in all layers
401
        let mut total_energy = 0.0;
5✔
402
        for layer in &self.layers {
10✔
403
            total_energy += layer.read_total_energy();
10✔
404
        }
10✔
405

406
        0.5 * total_energy
5✔
407
    }
5✔
408

409
    /// Compute the change in node values under a single timestep of PC.
410
    /// Returns the mean change in node values across all layers.
411
    pub fn timestep(&mut self) -> f32 {
32✔
412
        let mut total_value_changes = 0.0;
32✔
413

414
        // update the input layer, which has no lower layer
415
        total_value_changes += self.layers[0].values_timestep(false, self.gamma, None);
32✔
416

417
        // the update of a node value depends on the errors of the layer below it.
418
        let num_layers: usize = self.layers.len();
32✔
419
        for i in 1..num_layers {
33✔
420
            // Skip the first layer. The range is exclusive of the upper bound
421
            let (lower, upper) = self.layers.split_at_mut(i);
33✔
422
            let lower_layer: &Layer = &lower[i - 1];
33✔
423
            let upper_layer: &mut Layer = &mut upper[0];
33✔
424

425
            // The last layer is handled differently.
426
            if i == num_layers - 1 {
33✔
427
                // the last i to be processed will be the second to last layer
32✔
428
                total_value_changes +=
32✔
429
                    upper_layer.values_timestep(true, self.gamma, Some(lower_layer));
32✔
430
            } else {
32✔
431
                total_value_changes +=
1✔
432
                    upper_layer.values_timestep(false, self.gamma, Some(lower_layer));
1✔
433
            }
1✔
434
        }
435

436
        // Mean
437
        let total_num_nodes = self
32✔
438
            .layers
32✔
439
            .iter()
32✔
440
            .map(|layer| layer.values.len())
65✔
441
            .sum::<usize>() as f32;
32✔
442
        total_value_changes / total_num_nodes
32✔
443
    }
32✔
444

445
    /// Compute and apply prediction weights for all layers after inference.
446
    pub fn update_weights(&mut self) {
27✔
447
        let num_layers = self.layers.len();
27✔
448
        for i in 0..num_layers - 1 {
27✔
449
            let (lower, upper) = self.layers.split_at_mut(i + 1);
27✔
450
            let lower_layer: &Layer = &lower[i];
27✔
451
            let upper_layer: &mut Layer = &mut upper[0];
27✔
452

27✔
453
            upper_layer.update_weights(self.alpha, lower_layer);
27✔
454
        }
27✔
455
    }
27✔
456

457
    /// Compute the change in weights based on the current errors and values, without applying the changes to the model.
458
    /// Returns a Vec where index i contains the weight updates for layers[i+1].weights.
459
    pub fn compute_weight_updates(&mut self) -> Vec<Array2<f32>> {
20✔
460
        let mut weight_updates: Vec<Array2<f32>> = Vec::new();
20✔
461

462
        let num_layers = self.layers.len();
20✔
463
        for i in 0..num_layers - 1 {
20✔
464
            let (lower, upper) = self.layers.split_at_mut(i + 1);
20✔
465
            let lower_layer: &Layer = &lower[i];
20✔
466
            let upper_layer: &mut Layer = &mut upper[0];
20✔
467

20✔
468
            weight_updates.push(upper_layer.compute_weight_updates(self.alpha, lower_layer));
20✔
469
        }
20✔
470

471
        weight_updates
20✔
472
    }
20✔
473

474
    pub fn apply_weight_updates(&mut self, weight_updates: Vec<Array2<f32>>) {
5✔
475
        // weight_updates[i] corresponds to layers[i+1].weights
476
        for (i, weights) in weight_updates.iter().enumerate() {
5✔
477
            self.layers[i + 1].weights += weights;
5✔
478
        }
5✔
479
    }
5✔
480
}
481

482
#[cfg(test)]
483
mod tests {
484
    use super::*;
485
    use ndarray::array;
486

487
    #[test]
488
    fn layer_construction_uses_provided_values_and_default_xavier_limit() {
1✔
489
        let provided_values = array![0.25, 0.75];
1✔
490
        let layer = Layer::new(
1✔
491
            2,
492
            None,
1✔
493
            ActivationFunction::Relu,
1✔
494
            Some(provided_values.clone()),
1✔
495
            Some(true),
1✔
496
        );
497

498
        assert_eq!(layer.values, provided_values);
1✔
499
        assert_eq!(layer.weights.dim(), (0, 2));
1✔
500
        assert!(layer.pinned);
1✔
501

502
        let zero_sized_layer = Layer::new(
1✔
503
            0,
504
            None,
1✔
505
            ActivationFunction::Relu,
1✔
506
            Some(Array1::zeros(0)),
1✔
507
            None,
1✔
508
        );
509
        assert_eq!(zero_sized_layer.xavier_limit, 1.0);
1✔
510
    }
1✔
511

512
    #[test]
513
    fn randomise_weights_preserves_shape_and_xavier_bound() {
1✔
514
        let mut layer = Layer::new(3, Some(2), ActivationFunction::Relu, None, None);
1✔
515
        let xavier_limit = layer.xavier_limit;
1✔
516

517
        layer.randomise_weights();
1✔
518

519
        assert_eq!(layer.weights.dim(), (2, 3));
1✔
520
        assert!(
1✔
521
            layer
1✔
522
                .weights
1✔
523
                .iter()
1✔
524
                .all(|weight| weight.abs() <= xavier_limit),
6✔
525
            "weights should stay within the Xavier initialisation bounds"
526
        );
527
    }
1✔
528

529
    #[test]
530
    fn timestep_uses_hidden_layer_error_term_for_non_top_layers() {
1✔
531
        let mut model = PredictiveCodingModel::new(&PredictiveCodingModelConfig {
1✔
532
            layer_sizes: vec![1, 1, 1],
1✔
533
            alpha: 0.05,
1✔
534
            gamma: 0.5,
1✔
535
            convergence_threshold: 0.0,
1✔
536
            convergence_steps: 1,
1✔
537
            activation_function: ActivationFunction::Relu,
1✔
538
        });
1✔
539

540
        model.layers[0].pinned = true;
1✔
541
        model.layers[0].errors = array![0.0];
1✔
542
        model.layers[1].values = array![1.0];
1✔
543
        model.layers[1].errors = array![0.25];
1✔
544
        model.layers[1].weights = array![[1.0]];
1✔
545
        model.layers[2].pinned = true;
1✔
546

547
        model.timestep();
1✔
548

549
        assert_eq!(model.layers[1].values, array![0.875]);
1✔
550
    }
1✔
551
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc