1use super::experiment::FalsificationCriterion;
18use std::collections::HashMap;
19
20#[derive(Debug, Clone)]
22pub struct FalsificationResult {
23 pub falsified: bool,
25 pub falsifying_params: Option<HashMap<String, f64>>,
27 pub violated_criterion: Option<String>,
29 pub robustness: f64,
31 pub tests_performed: usize,
33 pub summary: String,
35}
36
37impl FalsificationResult {
38 #[must_use]
40 pub fn not_falsified(tests_performed: usize, robustness: f64) -> Self {
41 Self {
42 falsified: false,
43 falsifying_params: None,
44 violated_criterion: None,
45 robustness,
46 tests_performed,
47 summary: format!(
48 "Model not falsified after {tests_performed} tests (robustness: {robustness:.4})"
49 ),
50 }
51 }
52
53 #[must_use]
55 pub fn falsified(
56 params: HashMap<String, f64>,
57 criterion: &str,
58 robustness: f64,
59 tests_performed: usize,
60 ) -> Self {
61 Self {
62 falsified: true,
63 falsifying_params: Some(params),
64 violated_criterion: Some(criterion.to_string()),
65 robustness,
66 tests_performed,
67 summary: format!(
68 "Model FALSIFIED at test {tests_performed}: criterion '{criterion}' violated (robustness: {robustness:.4})"
69 ),
70 }
71 }
72}
73
74#[derive(Debug, Clone)]
76pub struct Trajectory {
77 pub times: Vec<f64>,
79 pub states: Vec<Vec<f64>>,
81 pub state_names: Vec<String>,
83}
84
85impl Trajectory {
86 #[must_use]
88 pub fn new(state_names: Vec<String>) -> Self {
89 Self {
90 times: Vec::new(),
91 states: vec![Vec::new(); state_names.len()],
92 state_names,
93 }
94 }
95
96 pub fn push(&mut self, time: f64, state: &[f64]) {
98 self.times.push(time);
99 for (i, &val) in state.iter().enumerate() {
100 if i < self.states.len() {
101 self.states[i].push(val);
102 }
103 }
104 }
105
106 #[must_use]
108 pub fn len(&self) -> usize {
109 self.times.len()
110 }
111
112 #[must_use]
114 pub fn is_empty(&self) -> bool {
115 self.times.is_empty()
116 }
117
118 #[must_use]
120 pub fn get_state(&self, name: &str, time_idx: usize) -> Option<f64> {
121 let state_idx = self.state_names.iter().position(|n| n == name)?;
122 self.states.get(state_idx)?.get(time_idx).copied()
123 }
124}
125
126#[derive(Debug, Clone)]
128pub struct ParamSpace {
129 pub bounds: HashMap<String, (f64, f64)>,
131 pub samples_per_dim: usize,
133 pub use_lhs: bool,
135}
136
137impl ParamSpace {
138 #[must_use]
140 pub fn new() -> Self {
141 Self {
142 bounds: HashMap::new(),
143 samples_per_dim: 10,
144 use_lhs: true,
145 }
146 }
147
148 #[must_use]
150 pub fn with_param(mut self, name: &str, min: f64, max: f64) -> Self {
151 self.bounds.insert(name.to_string(), (min, max));
152 self
153 }
154
155 #[must_use]
157 pub fn with_samples(mut self, n: usize) -> Self {
158 self.samples_per_dim = n;
159 self
160 }
161
162 #[must_use]
164 pub fn grid_points(&self) -> Vec<HashMap<String, f64>> {
165 let params: Vec<(&String, &(f64, f64))> = self.bounds.iter().collect();
166 if params.is_empty() {
167 return vec![HashMap::new()];
168 }
169
170 let mut points = Vec::new();
171 let n = self.samples_per_dim;
172
173 let total_points = n.pow(params.len() as u32);
175 for i in 0..total_points {
176 let mut point = HashMap::new();
177 let mut idx = i;
178 for (name, (min, max)) in ¶ms {
179 let dim_idx = idx % n;
180 idx /= n;
181 let t = if n > 1 {
182 dim_idx as f64 / (n - 1) as f64
183 } else {
184 0.5
185 };
186 let value = min + t * (max - min);
187 point.insert((*name).clone(), value);
188 }
189 points.push(point);
190 }
191
192 points
193 }
194}
195
196impl Default for ParamSpace {
197 fn default() -> Self {
198 Self::new()
199 }
200}
201
202pub trait FalsifiableSimulation {
232 fn falsification_criteria(&self) -> Vec<FalsificationCriterion>;
234
235 fn evaluate(&self, params: &HashMap<String, f64>) -> Trajectory;
237
238 fn check_criterion(&self, criterion: &str, trajectory: &Trajectory) -> f64;
244
245 fn seek_falsification(&self, params: &ParamSpace) -> FalsificationResult {
250 let criteria = self.falsification_criteria();
251 let points = params.grid_points();
252 let mut min_robustness = f64::INFINITY;
253
254 for (test_idx, point) in points.iter().enumerate() {
255 let trajectory = self.evaluate(point);
256
257 for criterion in &criteria {
258 let robustness = self.check_criterion(&criterion.name, &trajectory);
259 min_robustness = min_robustness.min(robustness);
260
261 if robustness < 0.0 {
262 return FalsificationResult::falsified(
264 point.clone(),
265 &criterion.name,
266 robustness,
267 test_idx + 1,
268 );
269 }
270 }
271 }
272
273 FalsificationResult::not_falsified(points.len(), min_robustness)
274 }
275
276 fn robustness(&self, trajectory: &Trajectory) -> f64 {
280 let criteria = self.falsification_criteria();
281 criteria
282 .iter()
283 .map(|c| self.check_criterion(&c.name, trajectory))
284 .fold(f64::INFINITY, f64::min)
285 }
286}
287
288#[derive(Debug, Clone)]
298pub struct ExperimentSeed {
299 pub master_seed: u64,
301 pub component_seeds: HashMap<String, u64>,
303 pub ieee_strict: bool,
305}
306
307impl ExperimentSeed {
308 #[must_use]
310 pub fn new(master_seed: u64) -> Self {
311 Self {
312 master_seed,
313 component_seeds: HashMap::new(),
314 ieee_strict: true,
315 }
316 }
317
318 #[must_use]
320 pub fn relaxed(mut self) -> Self {
321 self.ieee_strict = false;
322 self
323 }
324
325 #[must_use]
327 pub fn with_component(mut self, component: &str, seed: u64) -> Self {
328 self.component_seeds.insert(component.to_string(), seed);
329 self
330 }
331
332 #[must_use]
336 pub fn derive_seed(&self, component: &str) -> u64 {
337 if let Some(&seed) = self.component_seeds.get(component) {
338 return seed;
339 }
340
341 let mut hash = self.master_seed;
344 for byte in component.as_bytes() {
345 hash = hash.wrapping_mul(0x517c_c1b7_2722_0a95);
346 hash = hash.wrapping_add(u64::from(*byte));
347 hash ^= hash >> 33;
348 }
349 hash
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356 use crate::edd::experiment::FalsificationAction;
357
358 #[test]
359 fn test_falsification_result_not_falsified() {
360 let result = FalsificationResult::not_falsified(100, 0.5);
361 assert!(!result.falsified);
362 assert_eq!(result.tests_performed, 100);
363 assert!((result.robustness - 0.5).abs() < f64::EPSILON);
364 }
365
366 #[test]
367 fn test_falsification_result_falsified() {
368 let mut params = HashMap::new();
369 params.insert("x".to_string(), 1.0);
370 let result = FalsificationResult::falsified(params, "energy_drift", -0.1, 42);
371 assert!(result.falsified);
372 assert!(result.falsifying_params.is_some());
373 assert_eq!(result.violated_criterion, Some("energy_drift".to_string()));
374 }
375
376 #[test]
377 fn test_trajectory() {
378 let mut traj = Trajectory::new(vec!["x".to_string(), "v".to_string()]);
379 traj.push(0.0, &[1.0, 0.0]);
380 traj.push(1.0, &[0.0, -1.0]);
381
382 assert_eq!(traj.len(), 2);
383 assert!(!traj.is_empty());
384 assert!((traj.get_state("x", 0).unwrap() - 1.0).abs() < f64::EPSILON);
385 assert!((traj.get_state("v", 1).unwrap() - (-1.0)).abs() < f64::EPSILON);
386 }
387
388 #[test]
389 fn test_param_space_grid() {
390 let space = ParamSpace::new().with_param("x", 0.0, 1.0).with_samples(3);
391
392 let points = space.grid_points();
393 assert_eq!(points.len(), 3);
394 assert!((points[0]["x"] - 0.0).abs() < f64::EPSILON);
395 assert!((points[1]["x"] - 0.5).abs() < f64::EPSILON);
396 assert!((points[2]["x"] - 1.0).abs() < f64::EPSILON);
397 }
398
399 #[test]
400 fn test_param_space_2d_grid() {
401 let space = ParamSpace::new()
402 .with_param("x", 0.0, 1.0)
403 .with_param("y", 0.0, 1.0)
404 .with_samples(2);
405
406 let points = space.grid_points();
407 assert_eq!(points.len(), 4); }
409
410 #[test]
411 fn test_experiment_seed_derivation() {
412 let seed = ExperimentSeed::new(42);
413
414 let seed1 = seed.derive_seed("arrivals");
415 let seed2 = seed.derive_seed("service");
416 let seed3 = seed.derive_seed("arrivals");
417
418 assert_eq!(seed1, seed3);
420 assert_ne!(seed1, seed2);
422 }
423
424 #[test]
425 fn test_experiment_seed_preregistered() {
426 let seed = ExperimentSeed::new(42).with_component("arrivals", 12345);
427
428 assert_eq!(seed.derive_seed("arrivals"), 12345);
429 }
430
431 struct MockSimulation {
433 fail_on: Option<String>,
434 }
435
436 impl FalsifiableSimulation for MockSimulation {
437 fn falsification_criteria(&self) -> Vec<FalsificationCriterion> {
438 vec![FalsificationCriterion::new(
439 "bounds_check",
440 "x < 10",
441 FalsificationAction::RejectModel,
442 )]
443 }
444
445 fn evaluate(&self, params: &HashMap<String, f64>) -> Trajectory {
446 let mut traj = Trajectory::new(vec!["x".to_string()]);
447 let x = params.get("x").copied().unwrap_or(0.0);
448 traj.push(0.0, &[x]);
449 traj
450 }
451
452 fn check_criterion(&self, criterion: &str, trajectory: &Trajectory) -> f64 {
453 if Some(criterion.to_string()) == self.fail_on {
454 return -1.0;
455 }
456 let x = trajectory.get_state("x", 0).unwrap_or(0.0);
457 10.0 - x }
459 }
460
461 #[test]
462 fn test_falsifiable_simulation_not_falsified() {
463 let sim = MockSimulation { fail_on: None };
464 let params = ParamSpace::new().with_param("x", 0.0, 5.0).with_samples(3);
465
466 let result = sim.seek_falsification(¶ms);
467 assert!(!result.falsified);
468 }
469
470 #[test]
471 fn test_falsifiable_simulation_robustness() {
472 let sim = MockSimulation { fail_on: None };
473 let mut traj = Trajectory::new(vec!["x".to_string()]);
474 traj.push(0.0, &[3.0]);
475
476 let robustness = sim.robustness(&traj);
477 assert!((robustness - 7.0).abs() < f64::EPSILON); }
479}