use std::time::Instant;
use rand::RngExt;
use rand::SeedableRng;
use rand::rngs::StdRng;
use rayon::iter::IndexedParallelIterator;
use rayon::iter::IntoParallelRefIterator;
use rayon::iter::IntoParallelRefMutIterator;
use rayon::iter::ParallelIterator;
use crate::calibration::CalibrationConfig;
use crate::calibration::{calibrate, generate_trajectories};
use crate::hypotheses::{Hypothesis, surviving_hypotheses};
use crate::metrics::{compute_memory, compute_persistence, compute_storage};
use crate::observation::Observation;
use crate::record::{HypothesisRecord, ResearchRecord, UniverseResult};
use crate::rules::Rule;
use crate::types::BooleanTester;
use crate::types::RuleGenerator;
use crate::types::TestEnsembles;
use crate::universe::InformationUniverse;
#[derive(Debug, Clone)]
pub struct CycleConfig {
pub n_train: usize,
pub n_test: usize,
pub n_ensemble: usize,
pub steps: usize,
pub max_delta: usize,
pub n_shuffles: usize,
pub n_null_universes: usize,
pub seed: u64,
}
impl Default for CycleConfig {
fn default() -> Self {
Self {
n_train: 300,
n_test: 100,
n_ensemble: 10,
steps: 60,
max_delta: 15,
n_shuffles: 10,
n_null_universes: 30,
seed: 42,
}
}
}
pub fn run_cycle<U: InformationUniverse>(
universe: &U,
config: &CycleConfig,
hypotheses: &mut [Hypothesis<U::Rule>],
rule_generator: &mut RuleGenerator<U>,
boolean_tester: Option<&BooleanTester<U>>,
) -> ResearchRecord<U>
where
<U::Observation as Observation<U::State>>::Output: Eq + std::hash::Hash + Clone + Send + Sync,
U::Rule: Send + Sync,
U::State: Send + Sync,
{
let t0 = Instant::now();
let mut record = ResearchRecord::new(env!("CARGO_PKG_VERSION"));
record
.config
.insert("n_train".to_string(), config.n_train.to_string());
record
.config
.insert("n_test".to_string(), config.n_test.to_string());
record
.config
.insert("n_ensemble".to_string(), config.n_ensemble.to_string());
record
.config
.insert("steps".to_string(), config.steps.to_string());
record
.config
.insert("max_delta".to_string(), config.max_delta.to_string());
record
.config
.insert("n_shuffles".to_string(), config.n_shuffles.to_string());
record.config.insert(
"n_null_universes".to_string(),
config.n_null_universes.to_string(),
);
record
.config
.insert("seed".to_string(), config.seed.to_string());
let mut rng = StdRng::seed_from_u64(config.seed);
let state_space = universe.state_space();
let schedule = universe.schedule();
let observer = universe.observation();
let mut train_subsets: Vec<(Vec<U::Rule>, f64)> = Vec::with_capacity(config.n_train);
let mut test_subsets: Vec<(Vec<U::Rule>, f64)> = Vec::with_capacity(config.n_test);
for _ in 0..config.n_train {
train_subsets.push(rule_generator(&mut rng));
}
for _ in 0..config.n_test {
test_subsets.push(rule_generator(&mut rng));
}
let ca_config = CalibrationConfig {
percentile: 95.0,
floor_persistence: 0.01,
floor_storage: 0.01,
floor_memory: 0.01,
max_delta: config.max_delta,
n_shuffles: config.n_shuffles,
seed: config.seed,
};
let calibration = calibrate(
universe,
config.n_null_universes,
config.n_ensemble,
config.steps,
&ca_config,
);
record
.thresholds
.insert("persistence".to_string(), calibration.persistence_threshold);
record
.thresholds
.insert("storage".to_string(), calibration.storage_threshold);
record
.thresholds
.insert("memory".to_string(), calibration.memory_threshold);
let mut results: Vec<UniverseResult> = (0..train_subsets.len())
.map(|i| UniverseResult {
universe_id: i,
structured_ratio: 0.0,
n_rules: 0,
rule_names: vec![],
persistence: 0.0,
storage: 0.0,
memory: 0.0,
})
.collect();
results
.par_iter_mut()
.zip(train_subsets.par_iter())
.enumerate()
.for_each(|(i, (result, (rules, ratio)))| {
let mut local_rng = StdRng::seed_from_u64(config.seed + i as u64 * 137);
let n_pool = state_space.len();
let n_ens = config.n_ensemble.min(n_pool);
let mut init_indices: Vec<usize> = (0..n_pool).collect();
for j in 0..n_ens {
let k = local_rng.random_range(j..n_pool);
init_indices.swap(j, k);
}
let initial_states: Vec<U::State> = init_indices
.iter()
.take(n_ens)
.map(|&idx| state_space[idx].clone())
.collect();
let ensemble = generate_trajectories(
&initial_states,
rules,
config.steps,
schedule,
observer,
config.seed + i as u64 * 137,
);
*result = UniverseResult {
universe_id: i,
structured_ratio: *ratio,
n_rules: rules.len(),
rule_names: rules.iter().map(|r| r.name().to_string()).collect(),
persistence: compute_persistence(&ensemble, 1, config.n_shuffles, config.seed),
storage: compute_storage(
&ensemble,
config.max_delta,
config.n_shuffles,
config.seed,
),
memory: compute_memory(&ensemble, config.max_delta, config.n_shuffles, config.seed),
};
});
record.results = results;
let mut test_ensembles: TestEnsembles<U> =
(0..test_subsets.len()).map(|_| Vec::new()).collect();
test_ensembles
.par_iter_mut()
.zip(test_subsets.par_iter())
.enumerate()
.for_each(|(i, (ensemble_out, (rules, _ratio)))| {
let mut local_rng = StdRng::seed_from_u64(config.seed + 10000 + i as u64 * 137);
let n_pool = state_space.len();
let n_ens = config.n_ensemble.min(n_pool);
let mut init_indices: Vec<usize> = (0..n_pool).collect();
for j in 0..n_ens {
let k = local_rng.random_range(j..n_pool);
init_indices.swap(j, k);
}
let initial_states: Vec<U::State> = init_indices
.iter()
.take(n_ens)
.map(|&idx| state_space[idx].clone())
.collect();
*ensemble_out = generate_trajectories(
&initial_states,
rules,
config.steps,
schedule,
observer,
config.seed + 10000 + i as u64 * 137,
);
});
for h in hypotheses.iter_mut() {
let threshold = record
.thresholds
.get(&h.property_name)
.copied()
.unwrap_or(0.0);
let mut positive = 0usize;
let mut correct = 0usize;
for ((rules, _ratio), ensemble) in test_subsets.iter().zip(test_ensembles.iter()) {
if (h.condition_fn)(rules) {
positive += 1;
let metric_value = match h.property_name.as_str() {
"persistence" => {
compute_persistence(ensemble, 1, config.n_shuffles, config.seed)
}
"storage" => {
compute_storage(ensemble, config.max_delta, config.n_shuffles, config.seed)
}
"memory" => {
compute_memory(ensemble, config.max_delta, config.n_shuffles, config.seed)
}
_ => 0.0,
};
if metric_value > threshold {
correct += 1;
}
}
}
h.accuracy = if positive > 0 {
correct as f64 / positive as f64
} else {
0.0
};
h.score = h.accuracy - 0.1 * h.complexity;
}
record.hypotheses = hypotheses.iter().map(HypothesisRecord::from).collect();
if let Some(tester) = boolean_tester {
for (rules, _ratio) in train_subsets.iter() {
let verified = tester(rules);
for (gate, count) in verified {
*record.boolean_verifications.entry(gate).or_insert(0) += count;
}
}
}
let nand_count = record
.boolean_verifications
.get("NAND")
.copied()
.unwrap_or(0);
if record.n_storage() == 0 {
record
.failure_conditions
.push("F-1 (NULL): No storage universes found.".to_string());
}
if nand_count == 0 && boolean_tester.is_some() {
record
.failure_conditions
.push("F-1 (NULL): NAND not verified.".to_string());
}
let surviving = surviving_hypotheses(hypotheses);
if surviving.is_empty() && !hypotheses.is_empty() {
record
.failure_conditions
.push("F-6 (DISCONFIRMATION): No hypotheses survived.".to_string());
}
record.elapsed_seconds = t0.elapsed().as_secs_f64();
record
}