use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::diagnostics::GenerationStats;
use crate::genome::traits::EvolutionaryGenome;
use crate::population::individual::Individual;
pub const CHECKPOINT_VERSION: u32 = 1;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(bound = "")]
pub struct Checkpoint<G>
where
G: Clone + Serialize + EvolutionaryGenome,
{
pub version: u32,
pub generation: usize,
pub evaluations: usize,
pub population: Vec<Individual<G>>,
pub rng_state: Option<Vec<u8>>,
pub best: Option<Individual<G>>,
pub algorithm_state: AlgorithmState,
pub hyperparameter_state: Option<HyperparameterState>,
pub statistics: Vec<GenerationStats>,
pub metadata: HashMap<String, String>,
}
impl<G> Checkpoint<G>
where
G: Clone + Serialize + EvolutionaryGenome,
{
pub fn new(generation: usize, population: Vec<Individual<G>>) -> Self {
Self {
version: CHECKPOINT_VERSION,
generation,
evaluations: 0,
population,
rng_state: None,
best: None,
algorithm_state: AlgorithmState::SimpleGA,
hyperparameter_state: None,
statistics: Vec::new(),
metadata: HashMap::new(),
}
}
pub fn with_evaluations(mut self, evaluations: usize) -> Self {
self.evaluations = evaluations;
self
}
pub fn with_best(mut self, best: Individual<G>) -> Self {
self.best = Some(best);
self
}
pub fn with_algorithm_state(mut self, state: AlgorithmState) -> Self {
self.algorithm_state = state;
self
}
pub fn with_hyperparameter_state(mut self, state: HyperparameterState) -> Self {
self.hyperparameter_state = Some(state);
self
}
pub fn with_statistics(mut self, stats: Vec<GenerationStats>) -> Self {
self.statistics = stats;
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn with_rng_state(mut self, state: Vec<u8>) -> Self {
self.rng_state = Some(state);
self
}
pub fn with_rng<R: crate::checkpoint::rng::SnapshotRng>(
mut self,
rng: &R,
) -> Result<Self, crate::error::CheckpointError> {
self.rng_state = Some(rng.capture()?);
Ok(self)
}
pub fn restore_rng<R: crate::checkpoint::rng::SnapshotRng>(
&self,
) -> Result<Option<R>, crate::error::CheckpointError> {
match &self.rng_state {
Some(bytes) => Ok(Some(R::restore(bytes)?)),
None => Ok(None),
}
}
pub fn is_compatible(&self) -> bool {
self.version <= CHECKPOINT_VERSION
}
pub fn version(&self) -> u32 {
self.version
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum AlgorithmState {
SimpleGA,
SteadyState { replacement_count: usize },
CmaEs(CmaEsCheckpointState),
Nsga2 { pareto_front_indices: Vec<usize> },
Hbga {
population_params: Vec<f64>,
temperature: f64,
},
Island {
island_populations: Vec<Vec<usize>>,
migration_count: usize,
},
Interactive {
aggregator_state: String,
pending_evaluations: usize,
evaluation_mode: String,
},
Custom(String),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CmaEsCheckpointState {
pub mean: Vec<f64>,
pub sigma: f64,
pub covariance: Vec<f64>,
pub path_sigma: Vec<f64>,
pub path_c: Vec<f64>,
pub dimension: usize,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct HyperparameterState {
pub mutation_rate_posterior: Option<(f64, f64)>,
pub crossover_prob_posterior: Option<(f64, f64)>,
pub temperature_posterior: Option<(f64, f64)>,
pub step_size_posteriors: Vec<(f64, f64)>,
pub operator_weights: Vec<f64>,
pub history_size: usize,
}
impl Default for HyperparameterState {
fn default() -> Self {
Self {
mutation_rate_posterior: None,
crossover_prob_posterior: None,
temperature_posterior: None,
step_size_posteriors: Vec::new(),
operator_weights: Vec::new(),
history_size: 100,
}
}
}
pub struct CheckpointBuilder<G>
where
G: Clone + Serialize + EvolutionaryGenome,
{
checkpoint: Checkpoint<G>,
}
impl<G> CheckpointBuilder<G>
where
G: Clone + Serialize + EvolutionaryGenome,
{
pub fn new(generation: usize, population: Vec<Individual<G>>) -> Self {
Self {
checkpoint: Checkpoint::new(generation, population),
}
}
pub fn evaluations(mut self, count: usize) -> Self {
self.checkpoint.evaluations = count;
self
}
pub fn best(mut self, individual: Individual<G>) -> Self {
self.checkpoint.best = Some(individual);
self
}
pub fn algorithm_state(mut self, state: AlgorithmState) -> Self {
self.checkpoint.algorithm_state = state;
self
}
pub fn hyperparameters(mut self, state: HyperparameterState) -> Self {
self.checkpoint.hyperparameter_state = Some(state);
self
}
pub fn statistics(mut self, stats: Vec<GenerationStats>) -> Self {
self.checkpoint.statistics = stats;
self
}
pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.checkpoint.metadata.insert(key.into(), value.into());
self
}
pub fn rng_state(mut self, state: Vec<u8>) -> Self {
self.checkpoint.rng_state = Some(state);
self
}
pub fn build(self) -> Checkpoint<G> {
self.checkpoint
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::genome::real_vector::RealVector;
#[test]
fn test_checkpoint_creation() {
let population: Vec<Individual<RealVector>> = vec![
Individual::new(RealVector::new(vec![1.0, 2.0])),
Individual::new(RealVector::new(vec![3.0, 4.0])),
];
let checkpoint = Checkpoint::new(10, population.clone());
assert_eq!(checkpoint.version, CHECKPOINT_VERSION);
assert_eq!(checkpoint.generation, 10);
assert_eq!(checkpoint.population.len(), 2);
}
#[test]
fn test_checkpoint_builder() {
let population: Vec<Individual<RealVector>> =
vec![Individual::new(RealVector::new(vec![1.0]))];
let checkpoint = CheckpointBuilder::new(5, population)
.evaluations(1000)
.algorithm_state(AlgorithmState::SimpleGA)
.metadata("experiment", "test_run")
.build();
assert_eq!(checkpoint.generation, 5);
assert_eq!(checkpoint.evaluations, 1000);
assert_eq!(
checkpoint.metadata.get("experiment"),
Some(&"test_run".to_string())
);
}
#[test]
fn test_checkpoint_compatibility() {
let population: Vec<Individual<RealVector>> = vec![];
let checkpoint = Checkpoint::new(0, population);
assert!(checkpoint.is_compatible());
}
#[test]
fn test_cmaes_checkpoint_state() {
let state = CmaEsCheckpointState {
mean: vec![0.0, 0.0],
sigma: 1.0,
covariance: vec![1.0, 0.0, 0.0, 1.0],
path_sigma: vec![0.0, 0.0],
path_c: vec![0.0, 0.0],
dimension: 2,
};
let alg_state = AlgorithmState::CmaEs(state);
if let AlgorithmState::CmaEs(s) = alg_state {
assert_eq!(s.dimension, 2);
assert_eq!(s.sigma, 1.0);
} else {
panic!("Expected CmaEs state");
}
}
#[test]
fn test_checkpoint_with_methods() {
let population: Vec<Individual<RealVector>> =
vec![Individual::new(RealVector::new(vec![1.0, 2.0]))];
let best = Individual::with_fitness(RealVector::new(vec![0.5, 0.5]), 10.0);
let checkpoint = Checkpoint::new(5, population)
.with_evaluations(500)
.with_best(best.clone())
.with_algorithm_state(AlgorithmState::SteadyState {
replacement_count: 10,
})
.with_metadata("run_id", "test123")
.with_rng_state(vec![1, 2, 3, 4]);
assert_eq!(checkpoint.evaluations, 500);
assert!(checkpoint.best.is_some());
assert_eq!(
checkpoint.metadata.get("run_id"),
Some(&"test123".to_string())
);
assert!(checkpoint.rng_state.is_some());
}
#[test]
fn test_checkpoint_with_hyperparameters() {
let population: Vec<Individual<RealVector>> = vec![];
let hp_state = HyperparameterState {
mutation_rate_posterior: Some((2.0, 8.0)),
crossover_prob_posterior: Some((5.0, 5.0)),
..Default::default()
};
let checkpoint = Checkpoint::new(0, population).with_hyperparameter_state(hp_state);
assert!(checkpoint.hyperparameter_state.is_some());
let hp = checkpoint.hyperparameter_state.unwrap();
assert_eq!(hp.mutation_rate_posterior, Some((2.0, 8.0)));
}
#[test]
fn test_checkpoint_with_statistics() {
use crate::diagnostics::{GenerationStats, TimingStats};
let population: Vec<Individual<RealVector>> = vec![];
let stats = vec![
GenerationStats {
generation: 0,
evaluations: 100,
best_fitness: 10.0,
worst_fitness: 1.0,
mean_fitness: 5.0,
median_fitness: 5.0,
fitness_std: 2.0,
diversity: 0.5,
timing: TimingStats::default(),
},
GenerationStats {
generation: 1,
evaluations: 200,
best_fitness: 15.0,
worst_fitness: 2.0,
mean_fitness: 7.0,
median_fitness: 7.0,
fitness_std: 1.5,
diversity: 0.4,
timing: TimingStats::default(),
},
];
let checkpoint = Checkpoint::new(2, population).with_statistics(stats.clone());
assert_eq!(checkpoint.statistics.len(), 2);
}
#[test]
fn test_checkpoint_version() {
let population: Vec<Individual<RealVector>> = vec![];
let checkpoint = Checkpoint::new(0, population);
assert_eq!(checkpoint.version(), CHECKPOINT_VERSION);
}
#[test]
fn test_checkpoint_builder_full() {
use crate::diagnostics::{GenerationStats, TimingStats};
let population: Vec<Individual<RealVector>> =
vec![Individual::new(RealVector::new(vec![1.0]))];
let best = Individual::with_fitness(RealVector::new(vec![0.0]), 100.0);
let hp_state = HyperparameterState::default();
let stats = vec![GenerationStats {
generation: 0,
evaluations: 100,
best_fitness: 100.0,
worst_fitness: 10.0,
mean_fitness: 50.0,
median_fitness: 50.0,
fitness_std: 10.0,
diversity: 0.5,
timing: TimingStats::default(),
}];
let checkpoint = CheckpointBuilder::new(10, population)
.evaluations(5000)
.best(best)
.algorithm_state(AlgorithmState::Nsga2 {
pareto_front_indices: vec![0, 1, 2],
})
.hyperparameters(hp_state)
.statistics(stats)
.metadata("version", "1.0")
.rng_state(vec![0, 1, 2, 3])
.build();
assert_eq!(checkpoint.generation, 10);
assert_eq!(checkpoint.evaluations, 5000);
assert!(checkpoint.best.is_some());
assert!(checkpoint.hyperparameter_state.is_some());
assert_eq!(checkpoint.statistics.len(), 1);
assert!(checkpoint.rng_state.is_some());
}
#[test]
fn test_algorithm_state_variants() {
let simple_ga = AlgorithmState::SimpleGA;
let steady_state = AlgorithmState::SteadyState {
replacement_count: 5,
};
let nsga2 = AlgorithmState::Nsga2 {
pareto_front_indices: vec![0, 1],
};
let hbga = AlgorithmState::Hbga {
population_params: vec![1.0, 2.0],
temperature: 1.5,
};
let island = AlgorithmState::Island {
island_populations: vec![vec![0, 1], vec![2, 3]],
migration_count: 3,
};
let interactive = AlgorithmState::Interactive {
aggregator_state: "{}".to_string(),
pending_evaluations: 10,
evaluation_mode: "pairwise".to_string(),
};
let custom = AlgorithmState::Custom("custom_state".to_string());
assert!(matches!(simple_ga, AlgorithmState::SimpleGA));
assert!(matches!(steady_state, AlgorithmState::SteadyState { .. }));
assert!(matches!(nsga2, AlgorithmState::Nsga2 { .. }));
assert!(matches!(hbga, AlgorithmState::Hbga { .. }));
assert!(matches!(island, AlgorithmState::Island { .. }));
assert!(matches!(interactive, AlgorithmState::Interactive { .. }));
assert!(matches!(custom, AlgorithmState::Custom(_)));
}
#[test]
fn test_checkpoint_rng_capture_restore_round_trip() {
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
let mut rng = ChaCha8Rng::seed_from_u64(1234);
for _ in 0..11 {
let _: u64 = rng.gen();
}
let population: Vec<Individual<RealVector>> = vec![];
let checkpoint = Checkpoint::new(0, population).with_rng(&rng).unwrap();
let mut restored: ChaCha8Rng = checkpoint
.restore_rng()
.unwrap()
.expect("rng state must be present");
for _ in 0..500 {
assert_eq!(rng.gen::<u64>(), restored.gen::<u64>());
}
}
#[test]
fn test_checkpoint_restore_rng_absent() {
use rand_chacha::ChaCha8Rng;
let population: Vec<Individual<RealVector>> = vec![];
let checkpoint = Checkpoint::new(0, population);
let restored: Option<ChaCha8Rng> = checkpoint.restore_rng().unwrap();
assert!(restored.is_none());
}
#[test]
fn test_hyperparameter_state_default() {
let hp = HyperparameterState::default();
assert!(hp.mutation_rate_posterior.is_none());
assert!(hp.crossover_prob_posterior.is_none());
assert!(hp.temperature_posterior.is_none());
assert!(hp.step_size_posteriors.is_empty());
assert!(hp.operator_weights.is_empty());
assert_eq!(hp.history_size, 100);
}
}