use henad_compute::entry::ModelEntry;
use henad_compute::gpu::GpuContext;
use henad_core::explore::value::parse_value;
use henad_core::params::{ParamKind, ParamValue};
use super::determinism::COARSE_CADENCE;
use super::{ModelCheck, declared_defaults, error_text};
const DEFAULT_TICKS: u64 = 20;
pub const MIN_TICKS: u64 = COARSE_CADENCE + 1;
const SMALL_SIZES: [(&str, u32); 5] = [
("grid_width", 128),
("grid_height", 128),
("num_agents", 256),
("world_width", 128),
("world_height", 128),
];
#[derive(Debug, Clone)]
pub struct CheckSettings {
gpu: Option<GpuContext>,
ticks: u64,
thread_counts: (usize, usize),
texts: Vec<(String, String, String)>,
exemptions: Vec<(String, ModelCheck, String)>,
}
impl Default for CheckSettings {
fn default() -> Self {
Self {
gpu: None,
ticks: DEFAULT_TICKS,
thread_counts: (1, 7),
texts: Vec::new(),
exemptions: Vec::new(),
}
}
}
impl CheckSettings {
pub fn gpu(mut self, device: GpuContext) -> Self {
self.gpu = Some(device);
self
}
pub fn ticks(mut self, ticks: u64) -> Self {
assert!(
ticks >= MIN_TICKS,
"a check runs at least {MIN_TICKS} ticks, not {ticks}"
);
self.ticks = ticks;
self
}
pub fn thread_counts(mut self, low: usize, high: usize) -> Self {
assert!(
0 < low && low < high,
"thread counts {low} and {high} are not two counts in order"
);
self.thread_counts = (low, high);
self
}
pub fn set_text(mut self, model_id: &str, param_id: &str, text: &str) -> Self {
self.texts
.push((model_id.to_owned(), param_id.to_owned(), text.to_owned()));
self
}
pub fn exempt(mut self, model_id: &str, check: ModelCheck, reason: &str) -> Self {
self.exemptions.push((model_id.to_owned(), check, reason.to_owned()));
self
}
pub(super) fn device(&self) -> Option<&GpuContext> {
self.gpu.as_ref()
}
pub(super) fn run_ticks(&self) -> u64 {
self.ticks
}
pub(super) fn low_and_high_threads(&self) -> (usize, usize) {
self.thread_counts
}
pub(super) fn exemption(&self, model_id: &str, check: ModelCheck) -> Option<&str> {
self.exemptions
.iter()
.find(|(model, exempt, _)| model == model_id && *exempt == check)
.map(|(_, _, reason)| reason.as_str())
}
pub(super) fn named_models(&self) -> impl Iterator<Item = &str> {
let mut ids: Vec<&str> = self
.texts
.iter()
.map(|(model, _, _)| model.as_str())
.chain(self.exemptions.iter().map(|(model, _, _)| model.as_str()))
.collect();
ids.sort_unstable();
ids.dedup();
ids.into_iter()
}
pub(super) fn overrides(&self, entry: &ModelEntry, param_id: &str) -> bool {
self.texts
.iter()
.any(|(model, param, _)| model == entry.id() && param == param_id)
}
pub(super) fn check_values(&self, entry: &ModelEntry) -> Result<Vec<ParamValue>, String> {
let mut values = declared_defaults(entry);
for (param_id, small) in SMALL_SIZES {
if let Some(index) = entry.param_index(param_id) {
values[index] = shrunk(&entry.param_descriptors()[index].kind, small);
}
}
self.apply_overrides(entry, values)
}
pub(super) fn default_values(&self, entry: &ModelEntry) -> Result<Vec<ParamValue>, String> {
self.apply_overrides(entry, declared_defaults(entry))
}
fn apply_overrides(&self, entry: &ModelEntry, mut values: Vec<ParamValue>) -> Result<Vec<ParamValue>, String> {
for (_, param_id, text) in self.texts.iter().filter(|(model, _, _)| model == entry.id()) {
let index = entry
.param_index(param_id)
.ok_or_else(|| format!("The settings set parameter '{param_id}', which the model does not declare."))?;
values[index] = parse_value(&entry.param_descriptors()[index].kind, text).map_err(|error| {
format!(
"The settings set parameter '{param_id}' to '{text}': {}.",
error_text(&error)
)
})?;
}
Ok(values)
}
}
fn shrunk(kind: &ParamKind, small: u32) -> ParamValue {
match *kind {
ParamKind::U32 { min, max, default } => ParamValue::U32(small.min(default).clamp(min, max)),
#[expect(clippy::cast_precision_loss, reason = "the small sizes are below 2^24")]
ParamKind::F32 { min, max, default, .. } => ParamValue::F32((small as f32).min(default).clamp(min, max)),
_ => kind.default_value(),
}
}