use std::marker::PhantomData;
use burn::tensor::{Tensor, TensorData, backend::Backend};
use rand::Rng;
use rand::RngExt;
use rlevo_core::bounds::Bounds;
use rlevo_core::config::{self, ConfigError, ConstraintKind, Validate};
use crate::ops::linalg::{cholesky, matvec, symmetrize};
use crate::rng::{SeedPurpose, seed_stream};
use crate::strategy::{Strategy, StrategyMetrics};
fn cholesky_with_jitter(cov: &[f32], d: usize) -> Vec<f32> {
if let Some(l) = cholesky(cov, d) {
return l;
}
let trace: f32 = (0..d).map(|i| cov[i * d + i]).sum();
#[allow(clippy::cast_precision_loss)]
let mean_diag: f32 = (trace / d as f32).max(f32::MIN_POSITIVE);
let mut jitter: f32 = mean_diag * 1e-8;
for _ in 0..6 {
let mut jittered: Vec<f32> = cov.to_vec();
for i in 0..d {
jittered[i * d + i] += jitter;
}
if let Some(l) = cholesky(&jittered, d) {
return l;
}
jitter *= 10.0;
}
let mut id: Vec<f32> = vec![0.0; d * d];
for i in 0..d {
id[i * d + i] = 1.0;
}
id
}
#[derive(Debug, Clone)]
pub struct CmsaEsConfig {
pub pop_size: usize,
pub genome_dim: usize,
pub bounds: Bounds,
pub initial_sigma: f32,
pub mu: usize,
pub tau: f32,
pub tau_c: f32,
}
impl CmsaEsConfig {
#[must_use]
pub fn default_for(genome_dim: usize) -> Self {
#[allow(clippy::cast_precision_loss)]
let d = genome_dim as f32;
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let lambda = 4 + (3.0 * d.ln()).floor() as usize;
Self::with_pop_size(lambda, genome_dim)
}
#[must_use]
pub fn with_pop_size(pop_size: usize, genome_dim: usize) -> Self {
#[allow(clippy::cast_precision_loss)]
let d = genome_dim as f32;
let mu: usize = pop_size / 2;
#[allow(clippy::cast_precision_loss)]
let mu_f = mu as f32;
let tau: f32 = 1.0 / (2.0 * d).sqrt();
let tau_c: f32 = 1.0 + d * (d + 1.0) / (2.0 * mu_f);
Self {
pop_size,
genome_dim,
bounds: Bounds::new(-5.12, 5.12),
initial_sigma: 1.0,
mu,
tau,
tau_c,
}
}
}
impl Validate for CmsaEsConfig {
fn validate(&self) -> Result<(), ConfigError> {
const C: &str = "CmsaEsConfig";
config::at_least(C, "pop_size", self.pop_size, 2)?;
config::nonzero(C, "genome_dim", self.genome_dim)?;
config::positive(C, "initial_sigma", f64::from(self.initial_sigma))?;
config::at_least(C, "mu", self.mu, 1)?;
if self.mu > self.pop_size {
return Err(ConfigError {
config: C,
field: "mu",
kind: ConstraintKind::Custom("mu must not exceed pop_size"),
});
}
config::positive(C, "tau", f64::from(self.tau))?;
config::in_range(C, "tau_c", 1.0, f64::INFINITY, f64::from(self.tau_c))?;
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct CmsaEsState<B: Backend> {
mean: Vec<f32>,
cov: Vec<f32>,
sigma: f32,
offspring_sigmas: Vec<f32>,
generation: usize,
best_genome: Option<Tensor<B, 2>>,
best_fitness: f32,
}
impl<B: Backend> CmsaEsState<B> {
#[allow(clippy::too_many_arguments)]
pub fn try_new(
mean: Vec<f32>,
mut cov: Vec<f32>,
sigma: f32,
offspring_sigmas: Vec<f32>,
generation: usize,
best_genome: Option<Tensor<B, 2>>,
best_fitness: f32,
) -> Result<Self, ConfigError> {
let d = mean.len();
config::nonzero("CmsaEsState", "mean", d)?;
if cov.len() != d * d {
return Err(ConfigError {
config: "CmsaEsState",
field: "cov",
kind: ConstraintKind::Custom("covariance must be a row-major D × D matrix"),
});
}
config::positive("CmsaEsState", "sigma", f64::from(sigma))?;
symmetrize(&mut cov, d);
Ok(Self {
mean,
cov,
sigma,
offspring_sigmas,
generation,
best_genome,
best_fitness,
})
}
#[must_use]
pub fn mean(&self) -> &[f32] {
&self.mean
}
#[must_use]
pub fn cov(&self) -> &[f32] {
&self.cov
}
#[must_use]
pub fn sigma(&self) -> f32 {
self.sigma
}
#[must_use]
pub fn offspring_sigmas(&self) -> &[f32] {
&self.offspring_sigmas
}
#[must_use]
pub fn generation(&self) -> usize {
self.generation
}
#[must_use]
pub fn best_genome(&self) -> Option<&Tensor<B, 2>> {
self.best_genome.as_ref()
}
#[must_use]
pub fn best_fitness(&self) -> f32 {
self.best_fitness
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CmsaEs<B: Backend> {
_backend: PhantomData<fn() -> B>,
}
impl<B: Backend> CmsaEs<B> {
#[must_use]
pub fn new() -> Self {
Self {
_backend: PhantomData,
}
}
}
impl<B: Backend> Strategy<B> for CmsaEs<B>
where
B::Device: Clone,
{
type Params = CmsaEsConfig;
type State = CmsaEsState<B>;
type Genome = Tensor<B, 2>;
fn init(
&self,
params: &CmsaEsConfig,
rng: &mut dyn Rng,
_device: &<B as burn::tensor::backend::BackendTypes>::Device,
) -> CmsaEsState<B> {
debug_assert!(
params.validate().is_ok(),
"invalid CmsaEsConfig reached init: {params:?}"
);
let d = params.genome_dim;
let (lo, hi): (f32, f32) = params.bounds.into();
let mut stream = seed_stream(rng.next_u64(), 0, SeedPurpose::Init);
let mean: Vec<f32> = (0..d)
.map(|_| lo + (hi - lo) * stream.random::<f32>())
.collect();
let mut cov: Vec<f32> = vec![0.0; d * d];
for i in 0..d {
cov[i * d + i] = 1.0;
}
CmsaEsState {
mean,
cov,
sigma: params.initial_sigma,
offspring_sigmas: Vec::new(),
generation: 0,
best_genome: None,
best_fitness: f32::NEG_INFINITY,
}
}
fn ask(
&self,
params: &CmsaEsConfig,
state: &CmsaEsState<B>,
rng: &mut dyn Rng,
device: &<B as burn::tensor::backend::BackendTypes>::Device,
) -> (Tensor<B, 2>, CmsaEsState<B>) {
let d = params.genome_dim;
let lambda = params.pop_size;
let factor: Vec<f32> = cholesky_with_jitter(&state.cov, d);
let mut stream = seed_stream(
rng.next_u64(),
state.generation as u64,
SeedPurpose::CmaSampling,
);
let mut rows: Vec<f32> = Vec::with_capacity(lambda * d);
let mut sigmas: Vec<f32> = Vec::with_capacity(lambda);
for _ in 0..lambda {
let sigma_i: f32 = (state.sigma
* (params.tau * crate::sampling::standard_normal(&mut stream)).exp())
.max(f32::MIN_POSITIVE);
let z: Vec<f32> = (0..d)
.map(|_| crate::sampling::standard_normal(&mut stream))
.collect();
let s: Vec<f32> = matvec(&factor, &z, d);
for (mean_i, s_i) in state.mean.iter().zip(s.iter()) {
rows.push(mean_i + sigma_i * s_i);
}
sigmas.push(sigma_i);
}
let population = Tensor::<B, 2>::from_data(TensorData::new(rows, [lambda, d]), device);
let mut next: CmsaEsState<B> = state.clone();
next.offspring_sigmas = sigmas;
(population, next)
}
fn tell(
&self,
params: &CmsaEsConfig,
population: Tensor<B, 2>,
fitness: Tensor<B, 1>,
mut state: CmsaEsState<B>,
_rng: &mut dyn Rng,
) -> (CmsaEsState<B>, StrategyMetrics) {
let d = params.genome_dim;
let lambda = params.pop_size;
let mu = params.mu;
let fitness_host: Vec<f32> = fitness
.into_data()
.into_vec::<f32>()
.expect("fitness tensor must be readable as f32");
let pop_host: Vec<f32> = population
.clone()
.into_data()
.into_vec::<f32>()
.expect("population tensor must be readable as f32");
let mut ranked: Vec<usize> = (0..lambda).collect();
let sane: Vec<f32> = fitness_host
.iter()
.map(|&f| crate::fitness::sanitize_fitness(f))
.collect();
ranked.sort_by(|&a, &b| sane[b].total_cmp(&sane[a]));
let m_old: Vec<f32> = state.mean.clone();
#[allow(clippy::cast_precision_loss)]
let inv_mu: f32 = 1.0 / mu as f32;
let mut mean_new: Vec<f32> = vec![0.0; d];
let mut sigma_sum: f32 = 0.0;
let mut s_sel: Vec<Vec<f32>> = Vec::with_capacity(mu);
for &idx in ranked.iter().take(mu) {
let sigma_i = state
.offspring_sigmas
.get(idx)
.copied()
.unwrap_or(state.sigma);
sigma_sum += sigma_i;
let mut si: Vec<f32> = vec![0.0; d];
for i in 0..d {
let xi = pop_host[idx * d + i];
mean_new[i] += inv_mu * xi;
si[i] = (xi - m_old[i]) / sigma_i;
}
s_sel.push(si);
}
let sigma_new: f32 = sigma_sum * inv_mu;
let blend: f32 = 1.0 / params.tau_c;
let c_old: Vec<f32> = state.cov.clone();
let mut cov_new: Vec<f32> = vec![0.0; d * d];
for i in 0..d {
for j in 0..d {
let mut rankmu: f32 = 0.0;
for si in &s_sel {
rankmu += si[i] * si[j];
}
rankmu *= inv_mu;
cov_new[i * d + j] = (1.0 - blend) * c_old[i * d + j] + blend * rankmu;
}
}
symmetrize(&mut cov_new, d);
update_best(&mut state, &population, &fitness_host);
state.generation += 1;
let metrics =
StrategyMetrics::from_host_fitness(state.generation, &fitness_host, state.best_fitness);
state.best_fitness = metrics.best_fitness_ever();
state.mean = mean_new;
state.cov = cov_new;
state.sigma = sigma_new;
state.offspring_sigmas = Vec::new();
(state, metrics)
}
fn best(&self, state: &CmsaEsState<B>) -> Option<(Tensor<B, 2>, f32)> {
state
.best_genome
.as_ref()
.map(|g| (g.clone(), state.best_fitness))
}
}
fn update_best<B: Backend>(state: &mut CmsaEsState<B>, pop: &Tensor<B, 2>, fitness: &[f32]) {
if fitness.is_empty() {
return;
}
let mut best_idx: usize = 0;
let mut best: f32 = f32::NEG_INFINITY;
for (i, &f) in fitness.iter().enumerate() {
if f > best {
best = f;
best_idx = i;
}
}
if best > state.best_fitness {
let device = pop.device();
#[allow(clippy::cast_possible_wrap)]
let idx = Tensor::<B, 1, burn::tensor::Int>::from_data(
TensorData::new(vec![best_idx as i64], [1]),
&device,
);
state.best_genome = Some(pop.clone().select(0, idx));
state.best_fitness = best;
}
}
#[cfg(test)]
mod tests {
use super::*;
use burn::backend::Flex;
use proptest::prelude::*;
use rand::SeedableRng;
use rand::rngs::StdRng;
fn recon_llt(l: &[f32], n: usize) -> Vec<f32> {
let mut out: Vec<f32> = vec![0.0; n * n];
for i in 0..n {
for j in 0..n {
let mut acc: f32 = 0.0;
for k in 0..n {
acc += l[i * n + k] * l[j * n + k];
}
out[i * n + j] = acc;
}
}
out
}
#[test]
fn cholesky_with_jitter_recovers_from_non_pd_covariance() {
let cov: Vec<f32> = vec![-1e-5, 0.0, 0.0, 1.0];
let factor: Vec<f32> = cholesky_with_jitter(&cov, 2);
assert!(
factor.iter().all(|x| x.is_finite()),
"factor has non-finite entries: {factor:?}"
);
approx::assert_relative_eq!(factor[1], 0.0, epsilon = 1e-9);
assert!(
factor[0] > 0.0 && factor[3] > 0.0,
"non-positive pivots: {factor:?}"
);
let recon: Vec<f32> = recon_llt(&factor, 2);
assert!(
recon[0] < 0.5,
"identity fallback fired instead of jitter recovery: recon = {recon:?}"
);
approx::assert_relative_eq!(recon[3], 1.0, epsilon = 1e-3);
}
#[test]
fn cholesky_with_jitter_falls_back_to_identity_when_degenerate() {
let cov: Vec<f32> = vec![1.0, 2.0, 2.0, 1.0];
let factor: Vec<f32> = cholesky_with_jitter(&cov, 2);
assert!(
factor.iter().all(|x| x.is_finite()),
"factor has non-finite entries: {factor:?}"
);
approx::assert_relative_eq!(factor[0], 1.0, epsilon = 1e-9);
approx::assert_relative_eq!(factor[1], 0.0, epsilon = 1e-9);
approx::assert_relative_eq!(factor[2], 0.0, epsilon = 1e-9);
approx::assert_relative_eq!(factor[3], 1.0, epsilon = 1e-9);
}
#[test]
fn ask_tell_round_trip_survives_non_pd_covariance() {
let strategy = CmsaEs::<Flex>::new();
let params = CmsaEsConfig::with_pop_size(8, 2);
let device = Default::default();
let mut rng = StdRng::seed_from_u64(1);
let state: CmsaEsState<Flex> = CmsaEsState::try_new(
vec![0.0, 0.0],
vec![-1e-5, 0.0, 0.0, 1.0],
1.0,
Vec::new(),
0,
None,
f32::NEG_INFINITY,
)
.expect("valid state");
let (population, asked) = strategy.ask(¶ms, &state, &mut rng, &device);
let fitness = Tensor::<Flex, 1>::from_data(
TensorData::new(vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], [8]),
&device,
);
let (told, _metrics) = strategy.tell(¶ms, population, fitness, asked, &mut rng);
assert!(
told.cov().iter().all(|c| c.is_finite()),
"covariance has non-finite entries: {:?}",
told.cov()
);
assert!(
told.mean().iter().all(|m| m.is_finite()),
"mean has non-finite entries: {:?}",
told.mean()
);
assert!(
told.sigma().is_finite() && told.sigma() > 0.0,
"sigma is not finite and positive: {}",
told.sigma()
);
}
#[test]
fn sigma_i_underflow_does_not_poison_covariance() {
let strategy = CmsaEs::<Flex>::new();
let params = CmsaEsConfig::with_pop_size(8, 2);
let device = Default::default();
let mut rng = StdRng::seed_from_u64(1);
let state: CmsaEsState<Flex> = CmsaEsState::try_new(
vec![0.0, 0.0],
vec![1.0, 0.0, 0.0, 1.0],
f32::from_bits(1),
Vec::new(),
0,
None,
f32::NEG_INFINITY,
)
.expect("valid state");
let (population, asked) = strategy.ask(¶ms, &state, &mut rng, &device);
assert!(
asked.offspring_sigmas().contains(&f32::MIN_POSITIVE),
"test precondition: the σᵢ floor must engage on at least one offspring"
);
let fitness = Tensor::<Flex, 1>::from_data(
TensorData::new(vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], [8]),
&device,
);
let (told, _metrics) = strategy.tell(¶ms, population, fitness, asked, &mut rng);
assert!(
told.cov().iter().all(|c| c.is_finite()),
"covariance has non-finite entries: {:?}",
told.cov()
);
assert!(
told.mean().iter().all(|m| m.is_finite()),
"mean has non-finite entries: {:?}",
told.mean()
);
assert!(
told.sigma().is_finite() && told.sigma() > 0.0,
"sigma is not finite and positive: {}",
told.sigma()
);
}
#[test]
fn try_new_rejects_empty_mean() {
let err = CmsaEsState::<Flex>::try_new(
Vec::new(),
Vec::new(),
0.5,
Vec::new(),
0,
None,
f32::MIN,
)
.unwrap_err();
assert_eq!(err.field, "mean");
}
#[test]
fn try_new_rejects_wrong_cov_length() {
let err = CmsaEsState::<Flex>::try_new(
vec![0.0, 0.0],
vec![1.0, 0.0, 0.0],
0.5,
Vec::new(),
0,
None,
f32::MIN,
)
.unwrap_err();
assert_eq!(err.field, "cov");
}
#[test]
fn try_new_rejects_non_positive_sigma() {
let err = CmsaEsState::<Flex>::try_new(
vec![0.0, 0.0],
vec![1.0, 0.0, 0.0, 1.0],
0.0,
Vec::new(),
0,
None,
f32::MIN,
)
.unwrap_err();
assert_eq!(err.field, "sigma");
}
#[test]
fn try_new_symmetrizes_covariance() {
let state = CmsaEsState::<Flex>::try_new(
vec![0.0, 0.0],
vec![1.0, 0.4, 0.2, 1.0],
0.5,
Vec::new(),
0,
None,
f32::MIN,
)
.expect("valid state");
approx::assert_relative_eq!(state.cov()[1], 0.3, epsilon = 1e-6);
approx::assert_relative_eq!(state.cov()[2], 0.3, epsilon = 1e-6);
}
#[test]
fn accessors_round_trip_constructor_values() {
let genome = Tensor::<Flex, 2>::from_data(
TensorData::new(vec![1.0f32, 2.0], [1, 2]),
&Default::default(),
);
let state = CmsaEsState::<Flex>::try_new(
vec![1.0, -2.0],
vec![2.0, 0.0, 0.0, 3.0],
0.75,
vec![0.1, 0.2, 0.3],
7,
Some(genome),
42.0,
)
.expect("valid state");
assert_eq!(state.mean(), &[1.0, -2.0]);
assert_eq!(state.cov(), &[2.0, 0.0, 0.0, 3.0]);
approx::assert_relative_eq!(state.sigma(), 0.75, epsilon = 1e-6);
assert_eq!(state.offspring_sigmas(), &[0.1, 0.2, 0.3]);
assert_eq!(state.generation(), 7);
assert!(state.best_genome().is_some());
approx::assert_relative_eq!(state.best_fitness(), 42.0, epsilon = 1e-6);
}
#[test]
fn default_config_validates() {
assert!(CmsaEsConfig::default_for(10).validate().is_ok());
}
#[test]
fn rejects_pop_size_below_two() {
let mut cfg = CmsaEsConfig::default_for(10);
cfg.pop_size = 1;
assert_eq!(cfg.validate().unwrap_err().field, "pop_size");
}
#[test]
fn default_for_d10_constants() {
let cfg = CmsaEsConfig::default_for(10);
assert_eq!(cfg.pop_size, 10);
assert_eq!(cfg.mu, 5);
approx::assert_relative_eq!(cfg.tau, 1.0 / 20.0_f32.sqrt(), epsilon = 1e-6);
approx::assert_relative_eq!(cfg.tau_c, 12.0, epsilon = 1e-5);
}
#[test]
fn tau_differs_from_es_classical() {
let cfg = CmsaEsConfig::default_for(10);
#[allow(clippy::cast_precision_loss)]
let d = 10.0_f32;
let es_classical_tau = 1.0 / (2.0 * d.sqrt()).sqrt();
assert!(
(cfg.tau - es_classical_tau).abs() > 0.1,
"canonical CMSA τ must differ from es_classical τ"
);
}
#[test]
fn same_seed_yields_identical_trajectories() {
fn run() -> (Vec<f32>, Vec<f32>, f32) {
let strategy = CmsaEs::<Flex>::new();
let params = CmsaEsConfig::with_pop_size(8, 3);
let device = Default::default();
let mut rng = StdRng::seed_from_u64(0xD37E_2711);
let mut state = strategy.init(¶ms, &mut rng, &device);
for _ in 0..4 {
let (population, asked) = strategy.ask(¶ms, &state, &mut rng, &device);
let fitness = Tensor::<Flex, 1>::from_data(
TensorData::new(vec![8.0f32, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0], [8]),
&device,
);
let (told, _metrics) = strategy.tell(¶ms, population, fitness, asked, &mut rng);
state = told;
}
(state.mean().to_vec(), state.cov().to_vec(), state.sigma())
}
let (mean_a, cov_a, sigma_a): (Vec<f32>, Vec<f32>, f32) = run();
let (mean_b, cov_b, sigma_b): (Vec<f32>, Vec<f32>, f32) = run();
assert_eq!(
mean_a, mean_b,
"mean trajectory diverged under a fixed seed"
);
assert_eq!(
cov_a, cov_b,
"covariance trajectory diverged under a fixed seed"
);
assert_eq!(
sigma_a.to_bits(),
sigma_b.to_bits(),
"σ̄ trajectory diverged under a fixed seed"
);
}
#[test]
fn covariance_stays_symmetric_across_generations() {
let strategy = CmsaEs::<Flex>::new();
let params = CmsaEsConfig::with_pop_size(8, 3);
let d: usize = params.genome_dim;
let device = Default::default();
let mut rng = StdRng::seed_from_u64(0x5A11_9E77);
let mut state = strategy.init(¶ms, &mut rng, &device);
for generation in 0..5 {
let (population, asked) = strategy.ask(¶ms, &state, &mut rng, &device);
let fitness = Tensor::<Flex, 1>::from_data(
TensorData::new(vec![8.0f32, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0], [8]),
&device,
);
let (told, _metrics) = strategy.tell(¶ms, population, fitness, asked, &mut rng);
let cov: &[f32] = told.cov();
for i in 0..d {
for j in 0..d {
assert_eq!(
cov[i * d + j].to_bits(),
cov[j * d + i].to_bits(),
"asymmetry at ({i}, {j}) in generation {generation}"
);
}
}
state = told;
}
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 16,
max_shrink_iters: 256,
..ProptestConfig::default()
})]
#[test]
fn ask_tell_preserves_stochastic_invariants(
lambda in 4usize..=32,
d in 2usize..=30,
seed in any::<u64>(),
) {
let strategy = CmsaEs::<Flex>::new();
let params = CmsaEsConfig::with_pop_size(lambda, d);
let device = Default::default();
let mut rng = StdRng::seed_from_u64(seed);
let mut fitness_vals: Vec<f32> = Vec::with_capacity(lambda);
let mut v: f32 = 1.0;
for _ in 0..lambda {
fitness_vals.push(v);
v -= 1.0;
}
let mut state = strategy.init(¶ms, &mut rng, &device);
for generation in 0..5 {
let (population, asked) = strategy.ask(¶ms, &state, &mut rng, &device);
for (k, &s) in asked.offspring_sigmas().iter().enumerate() {
prop_assert!(
s.is_finite() && s > 0.0,
"offspring σ[{k}] not finite-positive in generation \
{generation}: {s} (lambda={lambda}, d={d}, seed={seed})"
);
}
let fitness = Tensor::<Flex, 1>::from_data(
TensorData::new(fitness_vals.clone(), [lambda]),
&device,
);
let (told, _metrics) =
strategy.tell(¶ms, population, fitness, asked, &mut rng);
prop_assert!(
told.sigma().is_finite() && told.sigma() > 0.0,
"σ̄ not finite-positive in generation {generation}: {} \
(lambda={lambda}, d={d}, seed={seed})",
told.sigma()
);
let cov: &[f32] = told.cov();
for i in 0..d {
for j in 0..d {
prop_assert_eq!(
cov[i * d + j].to_bits(),
cov[j * d + i].to_bits(),
"cov asymmetry at ({}, {}) in generation {} \
(lambda={}, d={}, seed={})",
i, j, generation, lambda, d, seed
);
}
}
for (k, &c) in cov.iter().enumerate() {
prop_assert!(
c.is_finite(),
"cov[{k}] non-finite in generation {generation}: {c} \
(lambda={lambda}, d={d}, seed={seed})"
);
}
for (k, &m) in told.mean().iter().enumerate() {
prop_assert!(
m.is_finite(),
"mean[{k}] non-finite in generation {generation}: {m} \
(lambda={lambda}, d={d}, seed={seed})"
);
}
state = told;
}
}
}
}