use scirs2_core::ndarray::Array1;
use scirs2_core::numeric::Float;
use std::fmt::Debug;
use super::{NtmController, NtmOptimizer};
use crate::error::Result;
use crate::es_meta_training::{
expect_fully_consumed, pull_matrix, pull_scalar, pull_vector, push_matrix, push_scalar,
push_vector, MetaTrainable,
};
pub use crate::es_meta_training::EsMetaTrainer as NtmMetaTrainer;
pub use crate::es_meta_training::{MetaTrainingConfig, MetaTrainingReport, SelectionMetric};
impl<T: Float + Debug + Send + Sync + 'static> NtmController<T> {
fn to_flat(&self) -> Vec<f64> {
let mut out = Vec::new();
push_matrix(&self.w_in, &mut out);
push_vector(&self.b_in, &mut out);
push_matrix(&self.w_key, &mut out);
push_vector(&self.b_key, &mut out);
push_vector(&self.w_beta, &mut out);
push_scalar(self.b_beta, &mut out);
push_vector(&self.w_gate, &mut out);
push_scalar(self.b_gate, &mut out);
push_matrix(&self.w_shift, &mut out);
push_vector(&self.b_shift, &mut out);
push_vector(&self.w_gamma, &mut out);
push_scalar(self.b_gamma, &mut out);
push_matrix(&self.w_erase, &mut out);
push_vector(&self.b_erase, &mut out);
push_matrix(&self.w_add, &mut out);
push_vector(&self.b_add, &mut out);
push_vector(&self.w_out, &mut out);
push_vector(&self.v_out, &mut out);
push_scalar(self.b_out, &mut out);
out
}
fn load_flat(&mut self, flat: &[f64]) -> Result<()> {
let mut cursor = 0usize;
pull_matrix(&mut self.w_in, flat, &mut cursor)?;
pull_vector(&mut self.b_in, flat, &mut cursor)?;
pull_matrix(&mut self.w_key, flat, &mut cursor)?;
pull_vector(&mut self.b_key, flat, &mut cursor)?;
pull_vector(&mut self.w_beta, flat, &mut cursor)?;
pull_scalar(&mut self.b_beta, flat, &mut cursor)?;
pull_vector(&mut self.w_gate, flat, &mut cursor)?;
pull_scalar(&mut self.b_gate, flat, &mut cursor)?;
pull_matrix(&mut self.w_shift, flat, &mut cursor)?;
pull_vector(&mut self.b_shift, flat, &mut cursor)?;
pull_vector(&mut self.w_gamma, flat, &mut cursor)?;
pull_scalar(&mut self.b_gamma, flat, &mut cursor)?;
pull_matrix(&mut self.w_erase, flat, &mut cursor)?;
pull_vector(&mut self.b_erase, flat, &mut cursor)?;
pull_matrix(&mut self.w_add, flat, &mut cursor)?;
pull_vector(&mut self.b_add, flat, &mut cursor)?;
pull_vector(&mut self.w_out, flat, &mut cursor)?;
pull_vector(&mut self.v_out, flat, &mut cursor)?;
pull_scalar(&mut self.b_out, flat, &mut cursor)?;
expect_fully_consumed(flat, cursor)
}
}
impl<T: Float + Debug + Send + Sync + 'static> NtmOptimizer<T> {
pub fn weight_vector(&self) -> Vec<f64> {
self.controller.to_flat()
}
pub fn weight_count(&self) -> usize {
self.controller.to_flat().len()
}
pub fn set_weight_vector(&mut self, flat: &[f64]) -> Result<()> {
self.controller.load_flat(flat)
}
pub fn reset_state(&mut self) {
self.memory = self.initial_memory.clone();
let uniform: T = scirs2_core::numeric::NumCast::from(1.0 / self.num_locations as f64)
.unwrap_or_else(|| T::zero());
self.prev_weights = Array1::from_elem(self.num_locations, uniform);
self.last_weights = self.prev_weights.clone();
self.prev_read = Array1::zeros(self.mem_width);
self.param_len = 0;
self.num_slots = 0;
self.chunk_bounds.clear();
self.grad_ema = Array1::zeros(0);
self.grad_sq_ema = Array1::zeros(0);
self.step_count = 0;
self.grad_norm_ema = T::zero();
self.current_lr = self.base_lr;
}
}
impl<T: Float + Debug + Send + Sync + 'static> MetaTrainable<T> for NtmOptimizer<T> {
fn weight_vector(&self) -> Vec<f64> {
NtmOptimizer::weight_vector(self)
}
fn set_weight_vector(&mut self, flat: &[f64]) -> Result<()> {
NtmOptimizer::set_weight_vector(self, flat)
}
fn reset_state(&mut self) {
NtmOptimizer::reset_state(self)
}
fn weight_count(&self) -> usize {
NtmOptimizer::weight_count(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain_objectives::{MetaObjective, QuadraticObjective};
use crate::domain_optimizers::AdvancedOptimizer;
use crate::ntm_optimizer::NtmOptimizerConfig;
fn optimizer(seed: u64) -> NtmOptimizer<f64> {
NtmOptimizer::new(NtmOptimizerConfig {
num_slots: 4,
num_locations: 6,
mem_width: 4,
hidden_dim: 6,
shift_range: 1,
base_lr: 0.2,
seed,
..Default::default()
})
.expect("optimizer")
}
fn training_tasks() -> Vec<QuadraticObjective<f64>> {
vec![
QuadraticObjective::isotropic(8, 1.0, 0.7).expect("task"),
QuadraticObjective::from_curvature(vec![1.0, 4.0, 1.0, 4.0, 1.0, 4.0, 1.0, 4.0], 0.5)
.expect("task"),
]
}
#[test]
fn weight_vector_round_trips() {
let mut opt = optimizer(13);
let flat = opt.weight_vector();
assert!(!flat.is_empty());
assert_eq!(flat.len(), opt.weight_count());
assert!(
flat.iter().any(|v| v.abs() > 1e-9),
"the weight vector is all zeros, so it is not reading real weights"
);
opt.set_weight_vector(&flat).expect("round trip");
assert_eq!(opt.weight_vector(), flat);
assert!(opt.set_weight_vector(&flat[..flat.len() - 1]).is_err());
let mut too_long = flat.clone();
too_long.push(0.0);
assert!(opt.set_weight_vector(&too_long).is_err());
}
#[test]
fn the_memory_matrix_is_not_part_of_the_weight_vector() {
let opt = optimizer(13);
let weights = opt.weight_count();
let memory_cells = opt.memory().len();
assert!(memory_cells > 0);
let mut mutated = opt.clone();
let params = Array1::from_vec(vec![1.0; 8]);
let gradient = Array1::from_vec(vec![0.3; 8]);
for _ in 0..4 {
mutated.step(¶ms, &gradient).expect("step");
}
assert!(
(mutated.memory() - opt.memory()).mapv(f64::abs).sum() > 1e-12,
"stepping did not write to memory, so there is nothing to distinguish"
);
assert_eq!(
mutated.weight_count(),
weights,
"the weight count changed when only the memory did"
);
assert_eq!(
mutated.weight_vector(),
opt.weight_vector(),
"writing to memory changed the weight vector, so memory is being \
treated as a learned parameter"
);
}
#[test]
fn reset_state_restores_the_seeded_memory_and_clears_momentum() {
let params = Array1::from_vec(vec![1.0; 8]);
let gradient = Array1::from_vec(vec![0.5; 8]);
let mut fresh = optimizer(29);
let pristine_memory = fresh.memory().clone();
let baseline = fresh.step(¶ms, &gradient).expect("step");
let mut used = optimizer(29);
for _ in 0..5 {
used.step(¶ms, &gradient).expect("warm-up step");
}
assert!(
(used.memory() - &pristine_memory).mapv(f64::abs).sum() > 1e-12,
"the warm-up left the memory untouched"
);
used.reset_state();
assert!(
(used.memory() - &pristine_memory).mapv(f64::abs).sum() < 1e-12,
"reset_state did not restore the seeded memory"
);
let after_reset = used.step(¶ms, &gradient).expect("step");
assert!(
(&after_reset - &baseline).mapv(f64::abs).sum() < 1e-12,
"reset_state did not restore the fresh-optimizer behaviour"
);
assert_eq!(used.weight_vector(), fresh.weight_vector());
}
#[test]
fn every_accessor_stays_safe_after_a_bare_reset() {
let mut opt = optimizer(37);
let params = Array1::from_vec(vec![1.0; 8]);
let gradient = Array1::from_vec(vec![0.25; 8]);
for _ in 0..3 {
opt.step(¶ms, &gradient).expect("warm-up");
}
opt.reset_state();
let state = opt.get_state();
assert_eq!(state.step_count, 0);
assert!(state.current_lr.is_finite());
assert!(state.grad_norm_ema.is_finite());
assert_eq!(opt.get_learning_rate(), state.current_lr);
assert_eq!(opt.num_slots(), 0);
assert_eq!(opt.num_locations(), 6);
assert_eq!(opt.mem_width(), 4);
assert!(opt.hidden_dim() > 0);
assert_eq!(opt.shift_range(), 1);
assert_eq!(opt.seed(), 37);
assert_eq!(opt.name(), "NtmOptimizer");
let weights = opt.last_address_weights();
assert_eq!(weights.len(), opt.num_locations());
let total: f64 = weights.iter().sum();
assert!(
(total - 1.0).abs() < 1e-12,
"addressing weights should sum to 1 after a reset, got {total}"
);
assert_eq!(opt.memory().dim(), (6, 4));
assert!(!opt.weight_vector().is_empty());
opt.reset_state();
assert_eq!(opt.num_slots(), 0);
assert!(opt.step(¶ms, &gradient).is_ok());
assert!(opt.num_slots() > 0);
}
#[test]
fn meta_loss_is_a_real_measurement() {
let tasks = training_tasks();
let refs: Vec<&dyn MetaObjective<f64>> =
tasks.iter().map(|t| t as &dyn MetaObjective<f64>).collect();
let trainer = NtmMetaTrainer::new(MetaTrainingConfig::default()).expect("trainer");
let a = trainer.meta_loss(&optimizer(1), &refs).expect("loss a");
let b = trainer.meta_loss(&optimizer(2), &refs).expect("loss b");
assert!(a.is_finite() && b.is_finite() && a > 0.0 && b > 0.0);
assert!(
(a - b).abs() > 1e-9,
"two different controller draws scored identically ({a} vs {b})"
);
let opt = optimizer(1);
let snapshot = opt.weight_vector();
let repeat = trainer.meta_loss(&opt, &refs).expect("repeat");
assert_eq!(opt.weight_vector(), snapshot);
assert!((repeat - a).abs() < 1e-12, "meta_loss is not deterministic");
}
}