use std::ops::ControlFlow;
use henad_compute::entry::{ModelEntry, ModelState};
use henad_compute::gpu::GpuContext;
use henad_compute::simulation::{ExportError, RunSetup, Simulation};
use henad_core::action::Schedule;
use henad_core::metadata::Structure;
use henad_core::params::{ParamKind, ParamValue};
use henad_core::view::StatEntry;
use super::settings::CheckSettings;
use super::{Ran, SEED, SkipReason, error_text, fault_text, first_stat_difference};
pub(super) const COARSE_CADENCE: u64 = 7;
struct RunEnd {
stats: Vec<StatEntry>,
state: Option<Vec<u8>>,
}
impl RunEnd {
fn difference(&self, other: &Self) -> Option<String> {
first_stat_difference(&self.stats, &other.stats)
.or_else(|| (self.state != other.state).then(|| "the exported state".to_owned()))
}
}
fn simulation(
entry: &ModelEntry,
values: &[ParamValue],
seed: u64,
gpu: Option<&GpuContext>,
) -> Result<Simulation, String> {
RunSetup::from_parts(entry, values, Some(seed), Schedule::default())
.map_err(|error| format!("The check values were refused: {}.", error_text(&error)))?
.build(gpu)
.map_err(|fault| format!("The model did not build. {}", fault_text(&fault)))
}
fn exported(simulation: &mut Simulation) -> Result<Option<Vec<u8>>, String> {
let mut bytes = Vec::new();
match simulation.write_state(&mut bytes) {
Ok(()) => Ok(Some(bytes)),
Err(ExportError::GpuState) => Ok(None),
Err(error) => Err(format!("The state was not exported: {}.", error_text(&error))),
}
}
fn run(
entry: &ModelEntry,
values: &[ParamValue],
seed: u64,
ticks: u64,
gpu: Option<&GpuContext>,
) -> Result<RunEnd, String> {
let mut simulation = simulation(entry, values, seed, gpu)?;
simulation.run_for(ticks).map_err(|fault| fault_text(&fault))?;
let stats = simulation
.stats()
.map_err(|fault| fault_text(&fault))?
.entries()
.to_vec();
let state = exported(&mut simulation)?;
Ok(RunEnd { stats, state })
}
pub(super) fn thread_count(entry: &ModelEntry, settings: &CheckSettings) -> Result<Ran, String> {
let (low, high) = settings.low_and_high_threads();
let ticks = settings.run_ticks();
let mut values = settings.check_values(entry)?;
let pinned = size_for_jobs(entry, settings, &mut values, high.saturating_mul(2))?;
let jobs = jobs_at(entry, &values)?;
if jobs <= 1 {
return Ok(Ran::Skipped(match pinned {
Some(param_id) => SkipReason::OneJobAtOverride(param_id.to_owned()),
None => SkipReason::OneJob,
}));
}
let at_low = in_pool(low, || run(entry, &values, SEED, ticks, None))?;
let at_high = in_pool(high, || run(entry, &values, SEED, ticks, None))?;
match at_low.difference(&at_high) {
None => Ok(Ran::PassedAtJobs(jobs)),
Some(difference) => Err(format!(
"Runs at {low} and {high} threads differ in {difference} after {ticks} ticks, with a step split into \
{jobs} jobs."
)),
}
}
pub(super) fn same_seed(entry: &ModelEntry, settings: &CheckSettings, gpu: Option<&GpuContext>) -> Result<(), String> {
let values = settings.check_values(entry)?;
let ticks = settings.run_ticks();
let first = run(entry, &values, SEED, ticks, gpu)?;
let second = run(entry, &values, SEED, ticks, gpu)?;
match first.difference(&second) {
None => Ok(()),
Some(difference) => Err(format!(
"Two builds on seed {SEED} differ in {difference} after {ticks} ticks."
)),
}
}
pub(super) fn seed_sensitivity(
entry: &ModelEntry,
settings: &CheckSettings,
gpu: Option<&GpuContext>,
) -> Result<(), String> {
let values = settings.check_values(entry)?;
let ticks = settings.run_ticks();
let other = SEED + 1;
let first = run(entry, &values, SEED, ticks, gpu)?;
let second = run(entry, &values, other, ticks, gpu)?;
match first.difference(&second) {
Some(_) => Ok(()),
None => Err(format!(
"Seeds {SEED} and {other} give the same stats and state after {ticks} ticks. A model that draws no \
random number takes an exemption."
)),
}
}
pub(super) fn sampling_cadence(
entry: &ModelEntry,
settings: &CheckSettings,
gpu: Option<&GpuContext>,
) -> Result<(), String> {
let values = settings.check_values(entry)?;
let ticks = settings.run_ticks();
let (every_tick, every_tick_end) = sampled(entry, &values, ticks, 1, gpu)?;
let (coarse, coarse_end) = sampled(entry, &values, ticks, COARSE_CADENCE, gpu)?;
for (tick, stats) in &coarse {
let Some((_, fine)) = every_tick.iter().find(|(fine_tick, _)| fine_tick == tick) else {
return Err(format!("The run sampled every tick took no sample at tick {tick}."));
};
if let Some(difference) = first_stat_difference(fine, stats) {
return Err(format!(
"Runs sampled every tick and every {COARSE_CADENCE} ticks differ in {difference} at tick {tick}."
));
}
}
if every_tick_end != coarse_end {
return Err(format!(
"Runs sampled every tick and every {COARSE_CADENCE} ticks export different states at tick {ticks}."
));
}
Ok(())
}
type Samples = Vec<(u64, Vec<StatEntry>)>;
fn sampled(
entry: &ModelEntry,
values: &[ParamValue],
ticks: u64,
interval: u64,
gpu: Option<&GpuContext>,
) -> Result<(Samples, Option<Vec<u8>>), String> {
let mut simulation = simulation(entry, values, SEED, gpu)?;
let mut samples = Vec::new();
let end = simulation
.run_sampled::<()>(ticks, interval, |sample| {
samples.push((sample.tick(), sample.entries().to_vec()));
ControlFlow::Continue(())
})
.map_err(|fault| fault_text(&fault))?;
if end.is_break() {
return Err(format!(
"A run sampled every {interval} ticks stopped before tick {ticks}."
));
}
let state = exported(&mut simulation)?;
Ok((samples, state))
}
fn in_pool<T: Send>(threads: usize, task: impl FnOnce() -> Result<T, String> + Send) -> Result<T, String> {
rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build()
.map_err(|error| format!("A pool of {threads} threads was not built: {error}."))?
.install(task)
}
fn jobs_at(entry: &ModelEntry, values: &[ParamValue]) -> Result<usize, String> {
let mut built = entry
.build(values, Some(SEED), None)
.map_err(|fault| format!("The model did not build. {}", fault_text(&fault)))?;
let jobs = match &mut built {
ModelState::Cpu(state) => state.parallel_jobs(),
ModelState::Gpu(_) => None,
};
Ok(jobs.unwrap_or(1))
}
fn size_for_jobs(
entry: &ModelEntry,
settings: &CheckSettings,
values: &mut [ParamValue],
target: usize,
) -> Result<Option<&'static str>, String> {
let (param_id, chunk) = match &entry.metadata().structure {
Structure::Agents { chunk, .. } | Structure::Network { chunk, .. } => ("num_agents", Some(*chunk)),
Structure::Grid { .. } => ("grid_height", None),
Structure::GpuGrid { .. } | Structure::GpuAgents { .. } => return Ok(None),
};
if settings.overrides(entry, param_id) {
return Ok(Some(param_id));
}
let Some(index) = entry.param_index(param_id) else {
return Ok(None);
};
let ParamKind::U32 { min, max, .. } = entry.param_descriptors()[index].kind else {
return Ok(None);
};
let within = |size: usize| u32::try_from(size).unwrap_or(u32::MAX).clamp(min, max);
if let Some(chunk) = chunk {
values[index] = ParamValue::U32(within(target.saturating_mul(chunk)));
return Ok(None);
}
let ParamValue::U32(start) = values[index] else {
return Ok(None);
};
let mut jobs_with = |size: u32| {
values[index] = ParamValue::U32(size);
jobs_at(entry, values)
};
let (mut fits, mut too_many) = if jobs_with(start)? > target {
(min, Some(start))
} else {
(start, None)
};
if too_many.is_none() {
while fits < max {
let next = fits.saturating_mul(2).min(max);
if jobs_with(next)? > target {
too_many = Some(next);
break;
}
fits = next;
}
}
if let Some(mut too_many) = too_many {
while too_many - fits > 1 {
let middle = fits + (too_many - fits) / 2;
if jobs_with(middle)? <= target {
fits = middle;
} else {
too_many = middle;
}
}
}
values[index] = ParamValue::U32(fits);
Ok(None)
}