use super::experiment::FalsificationCriterion;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct FalsificationResult {
pub falsified: bool,
pub falsifying_params: Option<HashMap<String, f64>>,
pub violated_criterion: Option<String>,
pub robustness: f64,
pub tests_performed: usize,
pub summary: String,
}
impl FalsificationResult {
#[must_use]
pub fn not_falsified(tests_performed: usize, robustness: f64) -> Self {
Self {
falsified: false,
falsifying_params: None,
violated_criterion: None,
robustness,
tests_performed,
summary: format!(
"Model not falsified after {tests_performed} tests (robustness: {robustness:.4})"
),
}
}
#[must_use]
pub fn falsified(
params: HashMap<String, f64>,
criterion: &str,
robustness: f64,
tests_performed: usize,
) -> Self {
Self {
falsified: true,
falsifying_params: Some(params),
violated_criterion: Some(criterion.to_string()),
robustness,
tests_performed,
summary: format!(
"Model FALSIFIED at test {tests_performed}: criterion '{criterion}' violated (robustness: {robustness:.4})"
),
}
}
}
#[derive(Debug, Clone)]
pub struct Trajectory {
pub times: Vec<f64>,
pub states: Vec<Vec<f64>>,
pub state_names: Vec<String>,
}
impl Trajectory {
#[must_use]
pub fn new(state_names: Vec<String>) -> Self {
Self {
times: Vec::new(),
states: vec![Vec::new(); state_names.len()],
state_names,
}
}
pub fn push(&mut self, time: f64, state: &[f64]) {
self.times.push(time);
for (i, &val) in state.iter().enumerate() {
if i < self.states.len() {
self.states[i].push(val);
}
}
}
#[must_use]
pub fn len(&self) -> usize {
self.times.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.times.is_empty()
}
#[must_use]
pub fn get_state(&self, name: &str, time_idx: usize) -> Option<f64> {
let state_idx = self.state_names.iter().position(|n| n == name)?;
self.states.get(state_idx)?.get(time_idx).copied()
}
}
#[derive(Debug, Clone)]
pub struct ParamSpace {
pub bounds: HashMap<String, (f64, f64)>,
pub samples_per_dim: usize,
pub use_lhs: bool,
}
impl ParamSpace {
#[must_use]
pub fn new() -> Self {
Self {
bounds: HashMap::new(),
samples_per_dim: 10,
use_lhs: true,
}
}
#[must_use]
pub fn with_param(mut self, name: &str, min: f64, max: f64) -> Self {
self.bounds.insert(name.to_string(), (min, max));
self
}
#[must_use]
pub fn with_samples(mut self, n: usize) -> Self {
self.samples_per_dim = n;
self
}
#[must_use]
pub fn grid_points(&self) -> Vec<HashMap<String, f64>> {
let params: Vec<(&String, &(f64, f64))> = self.bounds.iter().collect();
if params.is_empty() {
return vec![HashMap::new()];
}
let mut points = Vec::new();
let n = self.samples_per_dim;
let total_points = n.pow(params.len() as u32);
for i in 0..total_points {
let mut point = HashMap::new();
let mut idx = i;
for (name, (min, max)) in ¶ms {
let dim_idx = idx % n;
idx /= n;
let t = if n > 1 {
dim_idx as f64 / (n - 1) as f64
} else {
0.5
};
let value = min + t * (max - min);
point.insert((*name).clone(), value);
}
points.push(point);
}
points
}
}
impl Default for ParamSpace {
fn default() -> Self {
Self::new()
}
}
pub trait FalsifiableSimulation {
fn falsification_criteria(&self) -> Vec<FalsificationCriterion>;
fn evaluate(&self, params: &HashMap<String, f64>) -> Trajectory;
fn check_criterion(&self, criterion: &str, trajectory: &Trajectory) -> f64;
fn seek_falsification(&self, params: &ParamSpace) -> FalsificationResult {
let criteria = self.falsification_criteria();
let points = params.grid_points();
let mut min_robustness = f64::INFINITY;
for (test_idx, point) in points.iter().enumerate() {
let trajectory = self.evaluate(point);
for criterion in &criteria {
let robustness = self.check_criterion(&criterion.name, &trajectory);
min_robustness = min_robustness.min(robustness);
if robustness < 0.0 {
return FalsificationResult::falsified(
point.clone(),
&criterion.name,
robustness,
test_idx + 1,
);
}
}
}
FalsificationResult::not_falsified(points.len(), min_robustness)
}
fn robustness(&self, trajectory: &Trajectory) -> f64 {
let criteria = self.falsification_criteria();
criteria
.iter()
.map(|c| self.check_criterion(&c.name, trajectory))
.fold(f64::INFINITY, f64::min)
}
}
#[derive(Debug, Clone)]
pub struct ExperimentSeed {
pub master_seed: u64,
pub component_seeds: HashMap<String, u64>,
pub ieee_strict: bool,
}
impl ExperimentSeed {
#[must_use]
pub fn new(master_seed: u64) -> Self {
Self {
master_seed,
component_seeds: HashMap::new(),
ieee_strict: true,
}
}
#[must_use]
pub fn relaxed(mut self) -> Self {
self.ieee_strict = false;
self
}
#[must_use]
pub fn with_component(mut self, component: &str, seed: u64) -> Self {
self.component_seeds.insert(component.to_string(), seed);
self
}
#[must_use]
pub fn derive_seed(&self, component: &str) -> u64 {
if let Some(&seed) = self.component_seeds.get(component) {
return seed;
}
let mut hash = self.master_seed;
for byte in component.as_bytes() {
hash = hash.wrapping_mul(0x517c_c1b7_2722_0a95);
hash = hash.wrapping_add(u64::from(*byte));
hash ^= hash >> 33;
}
hash
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::edd::experiment::FalsificationAction;
#[test]
fn test_falsification_result_not_falsified() {
let result = FalsificationResult::not_falsified(100, 0.5);
assert!(!result.falsified);
assert_eq!(result.tests_performed, 100);
assert!((result.robustness - 0.5).abs() < f64::EPSILON);
}
#[test]
fn test_falsification_result_falsified() {
let mut params = HashMap::new();
params.insert("x".to_string(), 1.0);
let result = FalsificationResult::falsified(params, "energy_drift", -0.1, 42);
assert!(result.falsified);
assert!(result.falsifying_params.is_some());
assert_eq!(result.violated_criterion, Some("energy_drift".to_string()));
}
#[test]
fn test_trajectory() {
let mut traj = Trajectory::new(vec!["x".to_string(), "v".to_string()]);
traj.push(0.0, &[1.0, 0.0]);
traj.push(1.0, &[0.0, -1.0]);
assert_eq!(traj.len(), 2);
assert!(!traj.is_empty());
assert!((traj.get_state("x", 0).unwrap() - 1.0).abs() < f64::EPSILON);
assert!((traj.get_state("v", 1).unwrap() - (-1.0)).abs() < f64::EPSILON);
}
#[test]
fn test_param_space_grid() {
let space = ParamSpace::new().with_param("x", 0.0, 1.0).with_samples(3);
let points = space.grid_points();
assert_eq!(points.len(), 3);
assert!((points[0]["x"] - 0.0).abs() < f64::EPSILON);
assert!((points[1]["x"] - 0.5).abs() < f64::EPSILON);
assert!((points[2]["x"] - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_param_space_2d_grid() {
let space = ParamSpace::new()
.with_param("x", 0.0, 1.0)
.with_param("y", 0.0, 1.0)
.with_samples(2);
let points = space.grid_points();
assert_eq!(points.len(), 4); }
#[test]
fn test_experiment_seed_derivation() {
let seed = ExperimentSeed::new(42);
let seed1 = seed.derive_seed("arrivals");
let seed2 = seed.derive_seed("service");
let seed3 = seed.derive_seed("arrivals");
assert_eq!(seed1, seed3);
assert_ne!(seed1, seed2);
}
#[test]
fn test_experiment_seed_preregistered() {
let seed = ExperimentSeed::new(42).with_component("arrivals", 12345);
assert_eq!(seed.derive_seed("arrivals"), 12345);
}
struct MockSimulation {
fail_on: Option<String>,
}
impl FalsifiableSimulation for MockSimulation {
fn falsification_criteria(&self) -> Vec<FalsificationCriterion> {
vec![FalsificationCriterion::new(
"bounds_check",
"x < 10",
FalsificationAction::RejectModel,
)]
}
fn evaluate(&self, params: &HashMap<String, f64>) -> Trajectory {
let mut traj = Trajectory::new(vec!["x".to_string()]);
let x = params.get("x").copied().unwrap_or(0.0);
traj.push(0.0, &[x]);
traj
}
fn check_criterion(&self, criterion: &str, trajectory: &Trajectory) -> f64 {
if Some(criterion.to_string()) == self.fail_on {
return -1.0;
}
let x = trajectory.get_state("x", 0).unwrap_or(0.0);
10.0 - x }
}
#[test]
fn test_falsifiable_simulation_not_falsified() {
let sim = MockSimulation { fail_on: None };
let params = ParamSpace::new().with_param("x", 0.0, 5.0).with_samples(3);
let result = sim.seek_falsification(¶ms);
assert!(!result.falsified);
}
#[test]
fn test_falsifiable_simulation_robustness() {
let sim = MockSimulation { fail_on: None };
let mut traj = Trajectory::new(vec!["x".to_string()]);
traj.push(0.0, &[3.0]);
let robustness = sim.robustness(&traj);
assert!((robustness - 7.0).abs() < f64::EPSILON); }
}