use std::fmt::Debug;
use std::marker::PhantomData;
use burn::tensor::{Tensor, TensorData, backend::Backend};
use parking_lot::Mutex;
use rand::{Rng, RngExt};
use crate::fitness::{BatchFitnessFn, FitnessFn};
use crate::local_search::LocalSearch;
use crate::rng::{SeedPurpose, seed_stream};
use crate::strategy::{Strategy, StrategyMetrics};
use rlevo_core::config::{ConfigError, Validate};
use rlevo_core::objective::ObjectiveSense;
use rlevo_core::probability::Probability;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum WritebackPolicy {
Lamarckian,
Baldwinian,
Partial(Probability),
}
impl Default for WritebackPolicy {
fn default() -> Self {
Self::Partial(Probability::new(0.5))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CoveragePolicy {
Full,
TopK {
k: usize,
},
}
impl Default for CoveragePolicy {
fn default() -> Self {
Self::TopK { k: 1 }
}
}
#[derive(Clone, Debug)]
pub struct MemeticParams<SP, LP> {
pub inner: SP,
pub local: LP,
pub writeback: WritebackPolicy,
pub coverage: CoveragePolicy,
}
impl<SP: Validate, LP> Validate for MemeticParams<SP, LP> {
fn validate(&self) -> Result<(), ConfigError> {
self.inner.validate()
}
}
#[derive(Clone, Debug)]
pub struct MemeticState<St> {
inner: St,
generation: u64,
}
impl<St> MemeticState<St> {
#[must_use]
pub fn new(inner: St, generation: u64) -> Self {
Self { inner, generation }
}
#[must_use]
pub fn inner(&self) -> &St {
&self.inner
}
pub fn inner_mut(&mut self) -> &mut St {
&mut self.inner
}
#[must_use]
pub fn generation(&self) -> u64 {
self.generation
}
}
pub struct MemeticWrapper<B, S, L, F>
where
B: Backend,
S: Strategy<B, Genome = Tensor<B, 2>>,
L: LocalSearch<B>,
F: BatchFitnessFn<B, Tensor<B, 2>>,
{
inner: S,
local: L,
fitness: Mutex<F>,
_backend: PhantomData<fn() -> B>,
}
impl<B, S, L, F> MemeticWrapper<B, S, L, F>
where
B: Backend,
S: Strategy<B, Genome = Tensor<B, 2>>,
L: LocalSearch<B>,
F: BatchFitnessFn<B, Tensor<B, 2>>,
{
pub fn new(inner: S, local: L, fitness: F) -> Self {
Self {
inner,
local,
fitness: Mutex::new(fitness),
_backend: PhantomData,
}
}
}
impl<B, S, L, F> Debug for MemeticWrapper<B, S, L, F>
where
B: Backend,
S: Strategy<B, Genome = Tensor<B, 2>>,
L: LocalSearch<B>,
F: BatchFitnessFn<B, Tensor<B, 2>>,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MemeticWrapper").finish_non_exhaustive()
}
}
struct RowFitness<'a, B: Backend, F> {
inner: &'a mut F,
device: &'a B::Device,
sense: ObjectiveSense,
}
impl<B, F> FitnessFn<Vec<f32>> for RowFitness<'_, B, F>
where
B: Backend,
F: BatchFitnessFn<B, Tensor<B, 2>>,
{
fn evaluate_one(&mut self, member: &Vec<f32>) -> f32 {
let dim: usize = member.len();
let data: TensorData = TensorData::new(member.clone(), [1, dim]);
let row: Tensor<B, 2> = Tensor::<B, 2>::from_data(data, self.device);
let fitness: Tensor<B, 1> = self.inner.evaluate_batch(&row, self.device);
let values: Vec<f32> = fitness
.into_data()
.into_vec::<f32>()
.expect("fitness tensor must be readable as f32");
let natural = values.first().copied().unwrap_or(f32::NEG_INFINITY);
self.sense.to_canonical(natural)
}
}
impl<B, S, L, F> Strategy<B> for MemeticWrapper<B, S, L, F>
where
B: Backend,
S: Strategy<B, Genome = Tensor<B, 2>>,
L: LocalSearch<B>,
F: BatchFitnessFn<B, Tensor<B, 2>>,
{
type Params = MemeticParams<S::Params, L::Params>;
type State = MemeticState<S::State>;
type Genome = Tensor<B, 2>;
fn init(
&self,
params: &Self::Params,
rng: &mut dyn Rng,
device: &<B as burn::tensor::backend::BackendTypes>::Device,
) -> Self::State {
let inner: S::State = self.inner.init(¶ms.inner, rng, device);
MemeticState {
inner,
generation: 0,
}
}
fn ask(
&self,
params: &Self::Params,
state: &Self::State,
rng: &mut dyn Rng,
device: &<B as burn::tensor::backend::BackendTypes>::Device,
) -> (Self::Genome, Self::State) {
let (population, inner): (Tensor<B, 2>, S::State) =
self.inner.ask(¶ms.inner, &state.inner, rng, device);
(
population,
MemeticState {
inner,
generation: state.generation,
},
)
}
fn tell(
&self,
params: &Self::Params,
population: Self::Genome,
fitness: Tensor<B, 1>,
state: Self::State,
rng: &mut dyn Rng,
) -> (Self::State, StrategyMetrics) {
let generation: u64 = state.generation;
let mut refined_fit: Vec<f32> = fitness
.into_data()
.into_vec::<f32>()
.expect("fitness tensor must be readable as f32");
let dims: [usize; 2] = population.dims();
let pop_size: usize = dims[0];
let dim: usize = dims[1];
let device: B::Device = population.device();
let flat: Vec<f32> = population
.to_data()
.into_vec::<f32>()
.expect("population tensor must be readable as f32");
let mut indices: Vec<usize> = coverage_indices(¶ms.coverage, &refined_fit, pop_size);
indices.sort_unstable();
let base: u64 = rng.next_u64();
let mut ls_rng = seed_stream(base, generation, SeedPurpose::LocalSearch);
let mut mask_rng = seed_stream(base, generation, SeedPurpose::Replacement);
let mut writeback_rows: Vec<(usize, Vec<f32>)> = Vec::with_capacity(indices.len());
{
let mut guard = self.fitness.lock();
let sense = guard.sense();
let mut row_fitness: RowFitness<'_, B, F> = RowFitness {
inner: &mut *guard,
device: &device,
sense,
};
for &i in &indices {
let start: usize = i * dim;
let row: Vec<f32> = flat[start..start + dim].to_vec();
let known_fit: f32 = refined_fit[i];
let (refined, f_refined): (Vec<f32>, f32) = self.local.refine_with_known_fitness(
¶ms.local,
row,
known_fit,
&mut row_fitness,
&mut ls_rng,
);
debug_assert_eq!(
refined.len(),
dim,
"local search must preserve genome length"
);
refined_fit[i] = f_refined;
let writeback: bool = match params.writeback {
WritebackPolicy::Lamarckian => true,
WritebackPolicy::Baldwinian => false,
WritebackPolicy::Partial(p) => mask_rng.random::<f32>() < p.get(),
};
if writeback {
writeback_rows.push((i, refined));
}
}
}
let mut new_pop: Tensor<B, 2> = population;
let mut run_start: Option<usize> = None;
let mut run_len: usize = 0;
let mut run_buf: Vec<f32> = Vec::new();
for (i, row) in writeback_rows {
match run_start {
Some(s) if i == s + run_len => {
run_buf.extend(row);
run_len += 1;
}
Some(s) => {
let flushed: Tensor<B, 2> = Tensor::<B, 2>::from_data(
TensorData::new(core::mem::take(&mut run_buf), [run_len, dim]),
&device,
);
new_pop = new_pop.slice_assign([s..s + run_len, 0..dim], flushed);
run_start = Some(i);
run_len = 1;
run_buf = row;
}
None => {
run_start = Some(i);
run_len = 1;
run_buf = row;
}
}
}
if let (Some(s), false) = (run_start, run_buf.is_empty()) {
let flushed: Tensor<B, 2> =
Tensor::<B, 2>::from_data(TensorData::new(run_buf, [run_len, dim]), &device);
new_pop = new_pop.slice_assign([s..s + run_len, 0..dim], flushed);
}
let new_fit: Tensor<B, 1> =
Tensor::<B, 1>::from_data(TensorData::new(refined_fit, [pop_size]), &device);
let (inner, metrics): (S::State, StrategyMetrics) =
self.inner
.tell(¶ms.inner, new_pop, new_fit, state.inner, rng);
(
MemeticState {
inner,
generation: generation + 1,
},
metrics,
)
}
fn best(&self, state: &Self::State) -> Option<(Self::Genome, f32)> {
self.inner.best(&state.inner)
}
}
fn coverage_indices(policy: &CoveragePolicy, fitness: &[f32], pop_size: usize) -> Vec<usize> {
match *policy {
CoveragePolicy::Full => (0..pop_size).collect(),
CoveragePolicy::TopK { k } => {
let k: usize = k.min(pop_size);
let mut ranked: Vec<usize> = (0..pop_size).collect();
let sane: Vec<f32> = fitness
.iter()
.map(|&f| crate::fitness::sanitize_fitness(f))
.collect();
ranked.sort_by(|&a, &b| sane[b].total_cmp(&sane[a]));
ranked.truncate(k);
ranked
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::algorithms::de::{DeConfig, DifferentialEvolution};
use crate::algorithms::ga::{GaConfig, GeneticAlgorithm};
use crate::local_search::{
HillClimbing, HillClimbingParams, SimulatedAnnealing, SimulatedAnnealingParams,
};
use crate::strategy::EvolutionaryHarness;
use burn::backend::Flex;
use burn::tensor::backend::BackendTypes;
use rand::SeedableRng;
use rand::rngs::StdRng;
use rlevo_core::bounds::Bounds;
type TestBackend = Flex;
#[test]
fn memetic_state_new_round_trips() {
let mut state = MemeticState::new(7_u32, 3);
assert_eq!(*state.inner(), 7);
assert_eq!(state.generation(), 3);
*state.inner_mut() = 11;
assert_eq!(*state.inner(), 11);
}
const BOUNDS: Bounds = Bounds::new(-5.12, 5.12);
#[derive(Debug, Clone, Copy)]
struct RecordingStrategy;
#[derive(Debug, Clone)]
struct RecParams {
rows: Vec<f32>,
pop: usize,
dim: usize,
}
#[derive(Debug, Clone)]
struct RecState {
received_pop: Option<Vec<f32>>,
received_fit: Option<Vec<f32>>,
best: f32,
generation: usize,
}
impl Strategy<TestBackend> for RecordingStrategy {
type Params = RecParams;
type State = RecState;
type Genome = Tensor<TestBackend, 2>;
fn init(
&self,
_params: &RecParams,
_rng: &mut dyn Rng,
_device: &<TestBackend as BackendTypes>::Device,
) -> RecState {
RecState {
received_pop: None,
received_fit: None,
best: f32::NEG_INFINITY,
generation: 0,
}
}
fn ask(
&self,
params: &RecParams,
state: &RecState,
_rng: &mut dyn Rng,
device: &<TestBackend as BackendTypes>::Device,
) -> (Tensor<TestBackend, 2>, RecState) {
let data = TensorData::new(params.rows.clone(), [params.pop, params.dim]);
let pop = Tensor::<TestBackend, 2>::from_data(data, device);
(pop, state.clone())
}
fn tell(
&self,
_params: &RecParams,
population: Tensor<TestBackend, 2>,
fitness: Tensor<TestBackend, 1>,
mut state: RecState,
_rng: &mut dyn Rng,
) -> (RecState, StrategyMetrics) {
let pop_host = population
.into_data()
.into_vec::<f32>()
.expect("population host-read of a tensor this test just built");
let fit_host = fitness
.into_data()
.into_vec::<f32>()
.expect("fitness host-read of a tensor this test just built");
state.received_pop = Some(pop_host);
state.received_fit = Some(fit_host.clone());
state.generation += 1;
let metrics =
StrategyMetrics::from_host_fitness(state.generation, &fit_host, state.best);
state.best = metrics.best_fitness_ever();
(state, metrics)
}
fn best(&self, _state: &RecState) -> Option<(Tensor<TestBackend, 2>, f32)> {
None
}
}
#[derive(Debug, Default)]
struct CountingBatchFitness {
rows: usize,
}
impl<B: Backend> BatchFitnessFn<B, Tensor<B, 2>> for CountingBatchFitness {
fn evaluate_batch(
&mut self,
population: &Tensor<B, 2>,
device: &<B as BackendTypes>::Device,
) -> Tensor<B, 1> {
let dims = population.dims();
self.rows += dims[0];
let flat = population
.clone()
.into_data()
.into_vec::<f32>()
.expect("population host-read of a tensor this test just built");
let (pop, dim) = (dims[0], dims[1]);
let mut out = Vec::with_capacity(pop);
for r in 0..pop {
let start = r * dim;
let f: f32 = -flat[start..start + dim].iter().map(|v| v * v).sum::<f32>();
out.push(f);
}
Tensor::<B, 1>::from_data(TensorData::new(out, [pop]), device)
}
fn sense(&self) -> ObjectiveSense {
ObjectiveSense::Maximize
}
}
fn neg_sphere(row: &[f32]) -> f32 {
-row.iter().map(|v| v * v).sum::<f32>()
}
fn rec_params(rows: Vec<f32>, pop: usize, dim: usize) -> RecParams {
RecParams { rows, pop, dim }
}
fn fixed_population(pop: usize, dim: usize) -> Vec<f32> {
let mut rows = Vec::with_capacity(pop * dim);
for r in 0..pop {
for c in 0..dim {
#[allow(clippy::cast_precision_loss)]
let v = 0.5 + (r as f32) * 0.37 + (c as f32) * 0.11;
rows.push(v);
}
}
rows
}
#[test]
fn writeback_policy_default_is_partial_half() {
assert_eq!(
WritebackPolicy::default(),
WritebackPolicy::Partial(Probability::new(0.5))
);
}
#[test]
fn coverage_policy_default_is_top_k_one() {
assert_eq!(CoveragePolicy::default(), CoveragePolicy::TopK { k: 1 });
}
#[test]
fn coverage_indices_never_covers_nan_fitness() {
let fitness = [3.0f32, f32::NAN, 5.0, 1.0];
let top3 = coverage_indices(&CoveragePolicy::TopK { k: 3 }, &fitness, 4);
assert_eq!(top3, vec![2, 0, 3]);
assert!(!top3.contains(&1));
let all = coverage_indices(&CoveragePolicy::TopK { k: 4 }, &fitness, 4);
assert_eq!(all, vec![2, 0, 3, 1]);
}
#[test]
fn coverage_indices_topk_zero_is_empty() {
let cover = coverage_indices(&CoveragePolicy::TopK { k: 0 }, &[3.0, 1.0, 2.0], 3);
assert!(cover.is_empty(), "TopK{{0}} must cover no rows");
}
#[test]
fn coverage_indices_empty_population_is_empty() {
assert!(coverage_indices(&CoveragePolicy::Full, &[], 0).is_empty());
assert!(coverage_indices(&CoveragePolicy::TopK { k: 3 }, &[], 0).is_empty());
}
#[test]
fn coverage_indices_all_equal_breaks_ties_by_lowest_index() {
let fitness = [5.0f32; 4];
let top2 = coverage_indices(&CoveragePolicy::TopK { k: 2 }, &fitness, 4);
assert_eq!(top2, vec![0, 1], "ties must break toward the lowest index");
}
#[derive(Debug, Default)]
struct MinSphereBatch;
impl<B: Backend> BatchFitnessFn<B, Tensor<B, 2>> for MinSphereBatch {
fn evaluate_batch(
&mut self,
population: &Tensor<B, 2>,
device: &<B as BackendTypes>::Device,
) -> Tensor<B, 1> {
let dims = population.dims();
let flat = population
.clone()
.into_data()
.into_vec::<f32>()
.expect("population host-read of a tensor this test just built");
let (pop, dim) = (dims[0], dims[1]);
let mut out: Vec<f32> = Vec::with_capacity(pop);
for r in 0..pop {
let start = r * dim;
out.push(flat[start..start + dim].iter().map(|v| v * v).sum::<f32>());
}
Tensor::<B, 1>::from_data(TensorData::new(out, [pop]), device)
}
fn sense(&self) -> ObjectiveSense {
ObjectiveSense::Minimize
}
}
fn sphere_cost(row: &[f32]) -> f32 {
row.iter().map(|v| v * v).sum::<f32>()
}
#[test]
#[allow(clippy::float_cmp)]
fn minimize_sense_refinement_reduces_cost() {
let device = <TestBackend as BackendTypes>::Device::default();
let (pop, dim) = (5usize, 3usize);
let rows = fixed_population(pop, dim);
let strategy = MemeticWrapper::<TestBackend, _, _, _>::new(
RecordingStrategy,
HillClimbing,
MinSphereBatch,
);
let params = MemeticParams {
inner: rec_params(rows, pop, dim),
local: HillClimbingParams::default_for(BOUNDS),
writeback: WritebackPolicy::Lamarckian,
coverage: CoveragePolicy::Full,
};
let mut rng = StdRng::seed_from_u64(9);
let state = strategy.init(¶ms, &mut rng, &device);
let (ask_pop, asked) = strategy.ask(¶ms, &state, &mut rng, &device);
let ask_bytes = ask_pop
.clone()
.into_data()
.into_vec::<f32>()
.expect("population host-read of a tensor this test just built");
let canonical: Vec<f32> = (0..pop)
.map(|i| {
let s = i * dim;
-sphere_cost(&ask_bytes[s..s + dim])
})
.collect();
let fit =
Tensor::<TestBackend, 1>::from_data(TensorData::new(canonical.clone(), [pop]), &device);
let (next, _m) = strategy.tell(¶ms, ask_pop, fit, asked, &mut rng);
let recv_pop = next.inner.received_pop.clone().unwrap();
let recv_fit = next.inner.received_fit.clone().unwrap();
let mut any_improved = false;
#[allow(clippy::needless_range_loop)]
for i in 0..pop {
let s = i * dim;
let recv_row = &recv_pop[s..s + dim];
let ask_row = &ask_bytes[s..s + dim];
let recv_cost = sphere_cost(recv_row);
let ask_cost = sphere_cost(ask_row);
assert!(
recv_cost <= ask_cost + 1e-6,
"row {i}: refined cost {recv_cost} must not exceed original {ask_cost}"
);
assert!(
recv_fit[i] >= canonical[i] - 1e-6,
"row {i}: canonical fitness must not drop"
);
approx::assert_relative_eq!(recv_fit[i], -recv_cost, epsilon = 1e-5);
if recv_cost < ask_cost - 1e-6 {
any_improved = true;
}
}
assert!(
any_improved,
"the Minimize path must strictly reduce cost on at least one row"
);
}
#[test]
#[allow(clippy::float_cmp)]
fn baldwinian_population_bit_identical_to_ask() {
let device = <TestBackend as BackendTypes>::Device::default();
let (pop, dim) = (5usize, 3usize);
let rows = fixed_population(pop, dim);
let strategy = MemeticWrapper::<TestBackend, _, _, _>::new(
RecordingStrategy,
HillClimbing,
CountingBatchFitness::default(),
);
let params = MemeticParams {
inner: rec_params(rows.clone(), pop, dim),
local: HillClimbingParams::default_for(BOUNDS),
writeback: WritebackPolicy::Baldwinian,
coverage: CoveragePolicy::TopK { k: 2 },
};
let mut rng = StdRng::seed_from_u64(7);
let state = strategy.init(¶ms, &mut rng, &device);
let (ask_pop, asked) = strategy.ask(¶ms, &state, &mut rng, &device);
let ask_bytes = ask_pop
.clone()
.into_data()
.into_vec::<f32>()
.expect("population host-read of a tensor this test just built");
let mut orig_fit = CountingBatchFitness::default();
let orig = <CountingBatchFitness as BatchFitnessFn<TestBackend, _>>::evaluate_batch(
&mut orig_fit,
&ask_pop,
&device,
)
.into_data()
.into_vec::<f32>()
.expect("fitness host-read of a tensor this test just built");
let fit =
Tensor::<TestBackend, 1>::from_data(TensorData::new(orig.clone(), [pop]), &device);
let (next, _m) = strategy.tell(¶ms, ask_pop, fit, asked, &mut rng);
let recv_pop = next.inner.received_pop.clone().unwrap();
assert_eq!(recv_pop, ask_bytes, "Baldwinian must not alter the genome");
let recv_fit = next.inner.received_fit.clone().unwrap();
for i in 0..pop {
if i < 2 {
assert!(
recv_fit[i] >= orig[i],
"covered row {i}: refined {} must be >= original {}",
recv_fit[i],
orig[i]
);
assert!(recv_fit[i] <= 1e-6);
} else {
assert_eq!(recv_fit[i], orig[i], "uncovered row {i} must be unchanged");
}
}
}
#[test]
#[allow(clippy::float_cmp)]
fn lamarckian_covered_rows_change_uncovered_identical() {
let device = <TestBackend as BackendTypes>::Device::default();
let (pop, dim) = (5usize, 3usize);
let rows = fixed_population(pop, dim);
let strategy = MemeticWrapper::<TestBackend, _, _, _>::new(
RecordingStrategy,
HillClimbing,
CountingBatchFitness::default(),
);
let params = MemeticParams {
inner: rec_params(rows.clone(), pop, dim),
local: HillClimbingParams::default_for(BOUNDS),
writeback: WritebackPolicy::Lamarckian,
coverage: CoveragePolicy::TopK { k: 2 },
};
let mut rng = StdRng::seed_from_u64(11);
let state = strategy.init(¶ms, &mut rng, &device);
let (ask_pop, asked) = strategy.ask(¶ms, &state, &mut rng, &device);
let ask_bytes = ask_pop
.clone()
.into_data()
.into_vec::<f32>()
.expect("population host-read of a tensor this test just built");
let mut fitfn = CountingBatchFitness::default();
let orig = <CountingBatchFitness as BatchFitnessFn<TestBackend, _>>::evaluate_batch(
&mut fitfn, &ask_pop, &device,
)
.into_data()
.into_vec::<f32>()
.expect("fitness host-read of a tensor this test just built");
let fit = Tensor::<TestBackend, 1>::from_data(TensorData::new(orig, [pop]), &device);
let (next, _m) = strategy.tell(¶ms, ask_pop, fit, asked, &mut rng);
let recv_pop = next.inner.received_pop.clone().unwrap();
let recv_fit = next.inner.received_fit.clone().unwrap();
#[allow(clippy::needless_range_loop)]
for i in 0..pop {
let start = i * dim;
let recv_row = &recv_pop[start..start + dim];
let ask_row = &ask_bytes[start..start + dim];
if i < 2 {
assert_ne!(recv_row, ask_row, "covered row {i} should have changed");
approx::assert_relative_eq!(recv_fit[i], neg_sphere(recv_row), epsilon = 1e-5);
} else {
assert_eq!(recv_row, ask_row, "uncovered row {i} must be bit-identical");
}
}
}
#[test]
#[allow(clippy::float_cmp)]
fn lamarckian_noncontiguous_covered_rows_coalesce_correctly() {
let device = <TestBackend as BackendTypes>::Device::default();
let (pop, dim) = (5usize, 3usize);
let rows: Vec<f32> = vec![
0.3, 0.3, 0.3, 3.0, 3.0, 3.0, 0.4, 0.4, 0.4, 3.5, 3.5, 3.5, 0.5, 0.5, 0.5, ];
let strategy = MemeticWrapper::<TestBackend, _, _, _>::new(
RecordingStrategy,
HillClimbing,
CountingBatchFitness::default(),
);
let params = MemeticParams {
inner: rec_params(rows.clone(), pop, dim),
local: HillClimbingParams::default_for(BOUNDS),
writeback: WritebackPolicy::Lamarckian,
coverage: CoveragePolicy::TopK { k: 3 },
};
let mut rng = StdRng::seed_from_u64(13);
let state = strategy.init(¶ms, &mut rng, &device);
let (ask_pop, asked) = strategy.ask(¶ms, &state, &mut rng, &device);
let ask_bytes = ask_pop
.clone()
.into_data()
.into_vec::<f32>()
.expect("population host-read of a tensor this test just built");
let mut fitfn = CountingBatchFitness::default();
let orig = <CountingBatchFitness as BatchFitnessFn<TestBackend, _>>::evaluate_batch(
&mut fitfn, &ask_pop, &device,
)
.into_data()
.into_vec::<f32>()
.expect("fitness host-read of a tensor this test just built");
let fit = Tensor::<TestBackend, 1>::from_data(TensorData::new(orig, [pop]), &device);
let (next, _m) = strategy.tell(¶ms, ask_pop, fit, asked, &mut rng);
let recv_pop = next.inner.received_pop.clone().unwrap();
let recv_fit = next.inner.received_fit.clone().unwrap();
let covered = [true, false, true, false, true];
#[allow(clippy::needless_range_loop)]
for i in 0..pop {
let start = i * dim;
let recv_row = &recv_pop[start..start + dim];
let ask_row = &ask_bytes[start..start + dim];
if covered[i] {
assert_ne!(recv_row, ask_row, "covered row {i} should have changed");
approx::assert_relative_eq!(recv_fit[i], neg_sphere(recv_row), epsilon = 1e-5);
} else {
assert_eq!(recv_row, ask_row, "gap row {i} must be bit-identical");
}
}
}
#[test]
#[allow(clippy::float_cmp)]
fn lamarckian_multirow_runs_coalesce_correctly() {
let device = <TestBackend as BackendTypes>::Device::default();
let (pop, dim) = (8usize, 3usize);
let near: [f32; 3] = [0.3, 0.3, 0.3];
let far: [f32; 3] = [4.0, 4.0, 4.0];
let gaps = [2usize, 6usize];
let mut rows: Vec<f32> = Vec::with_capacity(pop * dim);
for r in 0..pop {
if gaps.contains(&r) {
rows.extend_from_slice(&far);
} else {
rows.extend_from_slice(&near);
}
}
let strategy = MemeticWrapper::<TestBackend, _, _, _>::new(
RecordingStrategy,
HillClimbing,
CountingBatchFitness::default(),
);
let params = MemeticParams {
inner: rec_params(rows.clone(), pop, dim),
local: HillClimbingParams::default_for(BOUNDS),
writeback: WritebackPolicy::Lamarckian,
coverage: CoveragePolicy::TopK { k: 6 },
};
let mut rng = StdRng::seed_from_u64(17);
let state = strategy.init(¶ms, &mut rng, &device);
let (ask_pop, asked) = strategy.ask(¶ms, &state, &mut rng, &device);
let ask_bytes = ask_pop
.clone()
.into_data()
.into_vec::<f32>()
.expect("population host-read of a tensor this test just built");
let mut fitfn = CountingBatchFitness::default();
let orig = <CountingBatchFitness as BatchFitnessFn<TestBackend, _>>::evaluate_batch(
&mut fitfn, &ask_pop, &device,
)
.into_data()
.into_vec::<f32>()
.expect("fitness host-read of a tensor this test just built");
let fit = Tensor::<TestBackend, 1>::from_data(TensorData::new(orig, [pop]), &device);
let (next, _m) = strategy.tell(¶ms, ask_pop, fit, asked, &mut rng);
let recv_pop = next.inner.received_pop.clone().unwrap();
let recv_fit = next.inner.received_fit.clone().unwrap();
let covered = [true, true, false, true, true, true, false, true];
#[allow(clippy::needless_range_loop)]
for i in 0..pop {
let start = i * dim;
let recv_row = &recv_pop[start..start + dim];
let ask_row = &ask_bytes[start..start + dim];
if covered[i] {
assert_ne!(recv_row, ask_row, "covered row {i} should have changed");
approx::assert_relative_eq!(recv_fit[i], neg_sphere(recv_row), epsilon = 1e-5);
} else {
assert_eq!(recv_row, ask_row, "gap row {i} must be bit-identical");
}
}
}
fn sa_trajectory(
writeback: WritebackPolicy,
seed: u64,
gens: usize,
) -> Vec<(Vec<f32>, Vec<f32>)> {
let device = <TestBackend as BackendTypes>::Device::default();
let (pop, dim) = (4usize, 3usize);
let rows = fixed_population(pop, dim);
let strategy = MemeticWrapper::<TestBackend, _, _, _>::new(
RecordingStrategy,
SimulatedAnnealing,
CountingBatchFitness::default(),
);
let params = MemeticParams {
inner: rec_params(rows, pop, dim),
local: SimulatedAnnealingParams::default_for(BOUNDS),
writeback,
coverage: CoveragePolicy::Full,
};
let mut rng = StdRng::seed_from_u64(seed);
let mut state = strategy.init(¶ms, &mut rng, &device);
let mut trajectory = Vec::with_capacity(gens);
for _ in 0..gens {
let (ask_pop, asked) = strategy.ask(¶ms, &state, &mut rng, &device);
let mut fitfn = CountingBatchFitness::default();
let orig = <CountingBatchFitness as BatchFitnessFn<TestBackend, _>>::evaluate_batch(
&mut fitfn, &ask_pop, &device,
);
let (next, _m) = strategy.tell(¶ms, ask_pop, orig, asked, &mut rng);
trajectory.push((
next.inner.received_pop.clone().unwrap(),
next.inner.received_fit.clone().unwrap(),
));
state = next;
}
trajectory
}
#[test]
fn partial_one_equals_lamarckian_partial_zero_equals_baldwinian() {
let lam = sa_trajectory(WritebackPolicy::Lamarckian, 33, 3);
let p1 = sa_trajectory(WritebackPolicy::Partial(Probability::new(1.0)), 33, 3);
assert_eq!(lam, p1, "Partial(1.0) must be bit-identical to Lamarckian");
let bald = sa_trajectory(WritebackPolicy::Baldwinian, 33, 3);
let p0 = sa_trajectory(WritebackPolicy::Partial(Probability::new(0.0)), 33, 3);
assert_eq!(bald, p0, "Partial(0.0) must be bit-identical to Baldwinian");
}
#[test]
fn partial_is_seed_reproducible_and_seed_sensitive() {
let a = sa_trajectory(WritebackPolicy::Partial(Probability::new(0.5)), 55, 3);
let b = sa_trajectory(WritebackPolicy::Partial(Probability::new(0.5)), 55, 3);
assert_eq!(a, b, "same seed must replay identically");
let c = sa_trajectory(WritebackPolicy::Partial(Probability::new(0.5)), 56, 3);
assert_ne!(a, c, "different seed should diverge");
}
#[test]
#[allow(clippy::float_cmp)]
fn topk_refines_exactly_k_rows_and_k_ge_pop_equals_full() {
let device = <TestBackend as BackendTypes>::Device::default();
let (pop, dim) = (6usize, 2usize);
let rows = fixed_population(pop, dim);
let run = |coverage: CoveragePolicy| -> Vec<bool> {
let strategy = MemeticWrapper::<TestBackend, _, _, _>::new(
RecordingStrategy,
HillClimbing,
CountingBatchFitness::default(),
);
let params = MemeticParams {
inner: rec_params(rows.clone(), pop, dim),
local: HillClimbingParams::default_for(BOUNDS),
writeback: WritebackPolicy::Lamarckian,
coverage,
};
let mut rng = StdRng::seed_from_u64(3);
let state = strategy.init(¶ms, &mut rng, &device);
let (ask_pop, asked) = strategy.ask(¶ms, &state, &mut rng, &device);
let ask_bytes = ask_pop
.clone()
.into_data()
.into_vec::<f32>()
.expect("population host-read of a tensor this test just built");
let mut fitfn = CountingBatchFitness::default();
let orig = <CountingBatchFitness as BatchFitnessFn<TestBackend, _>>::evaluate_batch(
&mut fitfn, &ask_pop, &device,
);
let (next, _m) = strategy.tell(¶ms, ask_pop, orig, asked, &mut rng);
let recv = next.inner.received_pop.clone().unwrap();
(0..pop)
.map(|i| {
let s = i * dim;
recv[s..s + dim] != ask_bytes[s..s + dim]
})
.collect()
};
let changed_k3 = run(CoveragePolicy::TopK { k: 3 });
assert_eq!(
changed_k3.iter().filter(|&&c| c).count(),
3,
"TopK{{3}} must refine exactly 3 rows"
);
let changed_full = run(CoveragePolicy::Full);
let changed_big_k = run(CoveragePolicy::TopK { k: pop + 4 });
assert_eq!(
changed_full, changed_big_k,
"TopK{{k>=pop}} must equal Full"
);
}
#[derive(Debug, Default)]
struct SphereBatch;
impl<B: Backend> BatchFitnessFn<B, Tensor<B, 2>> for SphereBatch {
fn evaluate_batch(
&mut self,
population: &Tensor<B, 2>,
device: &<B as BackendTypes>::Device,
) -> Tensor<B, 1> {
let dims = population.dims();
let flat = population
.clone()
.into_data()
.into_vec::<f32>()
.expect("population host-read of a tensor this test just built");
let (pop, dim) = (dims[0], dims[1]);
let mut out: Vec<f32> = Vec::with_capacity(pop);
for r in 0..pop {
let start = r * dim;
out.push(-flat[start..start + dim].iter().map(|v| v * v).sum::<f32>());
}
Tensor::<B, 1>::from_data(TensorData::new(out, [pop]), device)
}
fn sense(&self) -> ObjectiveSense {
ObjectiveSense::Maximize
}
}
#[test]
fn de_roundtrip_improves_over_generations() {
let device = <TestBackend as BackendTypes>::Device::default();
let dim = 4usize;
let strategy = MemeticWrapper::<TestBackend, _, _, _>::new(
DifferentialEvolution::<TestBackend>::new(),
HillClimbing,
SphereBatch,
);
let params = MemeticParams {
inner: DeConfig::default_for(20, dim),
local: HillClimbingParams::default_for(BOUNDS),
writeback: WritebackPolicy::Lamarckian,
coverage: CoveragePolicy::TopK { k: 3 },
};
let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
strategy,
params,
SphereBatch,
17,
device,
20,
)
.expect("valid params");
harness.reset();
let _ = harness.step(());
let first: f32 = harness.latest_metrics().unwrap().best_fitness_ever();
loop {
if harness.step(()).done {
break;
}
}
let last: f32 = harness.latest_metrics().unwrap().best_fitness_ever();
assert!(last.is_finite(), "best must stay finite");
assert!(
last >= first,
"best_fitness_ever must improve: {last} >= {first}"
);
}
#[test]
fn ga_roundtrip_smoke() {
let device = <TestBackend as BackendTypes>::Device::default();
let dim = 4usize;
let strategy = MemeticWrapper::<TestBackend, _, _, _>::new(
GeneticAlgorithm::<TestBackend>::new(),
HillClimbing,
SphereBatch,
);
let params = MemeticParams {
inner: GaConfig::default_for(16, dim),
local: HillClimbingParams::default_for(BOUNDS),
writeback: WritebackPolicy::default(),
coverage: CoveragePolicy::default(),
};
let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
strategy,
params,
SphereBatch,
5,
device,
5,
)
.expect("valid params");
harness.reset();
for _ in 0..5 {
let _ = harness.step(());
}
assert!(
harness
.latest_metrics()
.unwrap()
.best_fitness_ever()
.is_finite()
);
}
#[test]
fn one_draw_invariant_across_policies() {
let device = <TestBackend as BackendTypes>::Device::default();
let (pop, dim) = (5usize, 3usize);
let rows = fixed_population(pop, dim);
let next_after = |writeback: WritebackPolicy, coverage: CoveragePolicy| -> u64 {
let strategy = MemeticWrapper::<TestBackend, _, _, _>::new(
RecordingStrategy,
HillClimbing,
CountingBatchFitness::default(),
);
let params = MemeticParams {
inner: rec_params(rows.clone(), pop, dim),
local: HillClimbingParams::default_for(BOUNDS),
writeback,
coverage,
};
let mut rng = StdRng::seed_from_u64(101);
let state = strategy.init(¶ms, &mut rng, &device);
let (ask_pop, asked) = strategy.ask(¶ms, &state, &mut rng, &device);
let mut fitfn = CountingBatchFitness::default();
let orig = <CountingBatchFitness as BatchFitnessFn<TestBackend, _>>::evaluate_batch(
&mut fitfn, &ask_pop, &device,
);
let (_next, _m) = strategy.tell(¶ms, ask_pop, orig, asked, &mut rng);
rng.next_u64()
};
let baseline = next_after(WritebackPolicy::Lamarckian, CoveragePolicy::TopK { k: 1 });
assert_eq!(
baseline,
next_after(WritebackPolicy::Baldwinian, CoveragePolicy::TopK { k: 1 }),
);
assert_eq!(
baseline,
next_after(
WritebackPolicy::Partial(Probability::new(0.5)),
CoveragePolicy::Full
),
);
assert_eq!(
baseline,
next_after(WritebackPolicy::Lamarckian, CoveragePolicy::Full),
);
}
}