use super::{CriterionStatus, EddDemo, FalsificationStatus};
use crate::engine::rng::SimRng;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KingmanHockeyDemo {
pub time: f64,
pub utilization: f64,
pub mean_service_time: f64,
pub cv_arrivals: f64,
pub cv_service: f64,
pub queue_length: usize,
pub total_wait_time: f64,
pub customers_served: u64,
#[serde(skip)]
arrival_times: VecDeque<f64>,
next_arrival: f64,
next_departure: Option<f64>,
pub history: Vec<(f64, f64, f64)>,
#[serde(skip)]
rng: Option<SimRng>,
pub seed: u64,
}
impl Default for KingmanHockeyDemo {
fn default() -> Self {
Self::new(42)
}
}
impl KingmanHockeyDemo {
#[must_use]
pub fn new(seed: u64) -> Self {
let mut demo = Self {
time: 0.0,
utilization: 0.5,
mean_service_time: 1.0,
cv_arrivals: 1.0, cv_service: 1.0, queue_length: 0,
total_wait_time: 0.0,
customers_served: 0,
arrival_times: VecDeque::new(),
next_arrival: 0.0,
next_departure: None,
history: Vec::new(),
rng: Some(SimRng::new(seed)),
seed,
};
demo.schedule_arrival();
demo
}
pub fn set_utilization(&mut self, rho: f64) {
self.utilization = rho.clamp(0.01, 0.99);
}
pub fn set_cv(&mut self, cv_arrivals: f64, cv_service: f64) {
self.cv_arrivals = cv_arrivals.max(0.1);
self.cv_service = cv_service.max(0.1);
}
#[must_use]
pub fn arrival_rate(&self) -> f64 {
self.utilization / self.mean_service_time
}
#[must_use]
pub fn service_rate(&self) -> f64 {
1.0 / self.mean_service_time
}
#[must_use]
pub fn kingman_prediction(&self) -> f64 {
let rho = self.utilization;
let ca2 = self.cv_arrivals * self.cv_arrivals;
let cs2 = self.cv_service * self.cv_service;
let ts = self.mean_service_time;
(rho / (1.0 - rho)) * ((ca2 + cs2) / 2.0) * ts
}
#[must_use]
pub fn linear_prediction(&self, base_util: f64, base_wait: f64) -> f64 {
let slope = base_wait / base_util;
slope * self.utilization
}
#[must_use]
pub fn average_wait_time(&self) -> f64 {
if self.customers_served > 0 {
self.total_wait_time / self.customers_served as f64
} else {
0.0
}
}
#[allow(clippy::option_if_let_else)]
fn generate_variate(&mut self, mean: f64, cv: f64) -> f64 {
if let Some(ref mut rng) = self.rng {
if (cv - 1.0).abs() < 0.01 {
let u: f64 = rng.gen_range_f64(0.0001, 1.0);
-mean * u.ln()
} else {
let variance = (cv * mean).powi(2);
let shape = mean * mean / variance;
let k = shape.round().max(1.0) as usize;
let lambda = k as f64 / mean;
let mut sum = 0.0;
for _ in 0..k {
let u: f64 = rng.gen_range_f64(0.0001, 1.0);
sum += -u.ln() / lambda;
}
sum
}
} else {
mean
}
}
fn schedule_arrival(&mut self) {
let mean_interarrival = 1.0 / self.arrival_rate();
let interarrival = self.generate_variate(mean_interarrival, self.cv_arrivals);
self.next_arrival = self.time + interarrival;
}
fn schedule_departure(&mut self) {
let service_time = self.generate_variate(self.mean_service_time, self.cv_service);
self.next_departure = Some(self.time + service_time);
}
fn process_arrival(&mut self) {
let server_idle = self.next_departure.is_none();
if server_idle {
self.customers_served += 1;
self.total_wait_time += 0.0;
self.schedule_departure();
} else {
self.arrival_times.push_back(self.time);
self.queue_length += 1;
}
self.schedule_arrival();
}
fn process_departure(&mut self) {
if self.queue_length > 0 {
self.queue_length -= 1;
self.customers_served += 1;
if let Some(arrival_time) = self.arrival_times.pop_front() {
let queue_wait_time = self.time - arrival_time;
self.total_wait_time += queue_wait_time;
}
self.schedule_departure();
} else {
self.next_departure = None;
}
}
#[allow(clippy::while_float)]
pub fn run_until(&mut self, end_time: f64) {
while self.time < end_time {
self.step(0.0);
}
}
pub fn build_hockey_stick(&mut self, utilization_levels: &[f64], sim_time: f64) {
let mut results = Vec::with_capacity(utilization_levels.len());
for &rho in utilization_levels {
self.reset();
self.set_utilization(rho);
self.run_until(sim_time);
results.push((rho, self.average_wait_time(), self.kingman_prediction()));
}
self.history = results;
}
#[must_use]
pub fn calculate_r_squared(&self) -> f64 {
if self.history.len() < 3 {
return 0.0;
}
let n = self.history.len() as f64;
let mean_sim: f64 = self.history.iter().map(|(_, sim, _)| sim).sum::<f64>() / n;
let ss_tot: f64 = self
.history
.iter()
.map(|(_, sim, _)| (sim - mean_sim).powi(2))
.sum();
let ss_res: f64 = self
.history
.iter()
.map(|(_, sim, pred)| (sim - pred).powi(2))
.sum();
if ss_tot > f64::EPSILON {
1.0 - (ss_res / ss_tot)
} else {
0.0
}
}
#[must_use]
pub fn hockey_stick_ratio(&self) -> f64 {
let wait_50 = self
.history
.iter()
.find(|(rho, _, _)| (*rho - 0.5).abs() < 0.05)
.map(|(_, w, _)| *w);
let wait_95 = self
.history
.iter()
.find(|(rho, _, _)| (*rho - 0.95).abs() < 0.05)
.map(|(_, w, _)| *w);
match (wait_50, wait_95) {
(Some(w50), Some(w95)) if w50 > f64::EPSILON => w95 / w50,
_ => 0.0,
}
}
}
impl EddDemo for KingmanHockeyDemo {
fn name(&self) -> &'static str {
"Kingman's Hockey Stick"
}
fn emc_ref(&self) -> &'static str {
"operations/kingmans_formula"
}
fn step(&mut self, _dt: f64) {
let next_arrival = self.next_arrival;
let next_departure = self.next_departure;
let is_arrival = next_departure.is_none_or(|dep| next_arrival <= dep);
if is_arrival {
self.time = next_arrival;
self.process_arrival();
} else if let Some(dep) = next_departure {
self.time = dep;
self.process_departure();
}
}
fn verify_equation(&self) -> bool {
let ratio = self.hockey_stick_ratio();
let r_squared = self.calculate_r_squared();
ratio > 10.0 && r_squared > 0.90
}
fn get_falsification_status(&self) -> FalsificationStatus {
let ratio = self.hockey_stick_ratio();
let r_squared = self.calculate_r_squared();
let hockey_passed = ratio > 10.0;
let fit_passed = r_squared > 0.90;
FalsificationStatus {
verified: hockey_passed && fit_passed,
criteria: vec![
CriterionStatus {
id: "KF-HOCKEY".to_string(),
name: "Hockey stick shape".to_string(),
passed: hockey_passed,
value: ratio,
threshold: 10.0,
},
CriterionStatus {
id: "KF-FIT".to_string(),
name: "Kingman fit R²".to_string(),
passed: fit_passed,
value: r_squared,
threshold: 0.90,
},
],
message: if hockey_passed && fit_passed {
format!("Kingman verified: wait_95/wait_50={ratio:.1}×, R²={r_squared:.4}")
} else if !hockey_passed {
format!("Hockey stick not pronounced: ratio={ratio:.1}× (expected >10×)")
} else {
format!("Kingman fit poor: R²={r_squared:.4} (expected >0.90)")
},
}
}
fn reset(&mut self) {
let seed = self.seed;
let cv_a = self.cv_arrivals;
let cv_s = self.cv_service;
*self = Self::new(seed);
self.cv_arrivals = cv_a;
self.cv_service = cv_s;
}
}
#[cfg(feature = "wasm")]
mod wasm {
use super::{EddDemo, KingmanHockeyDemo};
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct WasmKingmanHockey {
inner: KingmanHockeyDemo,
}
#[wasm_bindgen]
impl WasmKingmanHockey {
#[wasm_bindgen(constructor)]
pub fn new(seed: u64) -> Self {
Self {
inner: KingmanHockeyDemo::new(seed),
}
}
pub fn step(&mut self) {
self.inner.step(0.0);
}
pub fn get_utilization(&self) -> f64 {
self.inner.utilization
}
pub fn get_queue_length(&self) -> usize {
self.inner.queue_length
}
pub fn get_average_wait(&self) -> f64 {
self.inner.average_wait_time()
}
pub fn get_kingman_prediction(&self) -> f64 {
self.inner.kingman_prediction()
}
pub fn set_utilization(&mut self, rho: f64) {
self.inner.set_utilization(rho);
}
pub fn set_cv(&mut self, cv_arrivals: f64, cv_service: f64) {
self.inner.set_cv(cv_arrivals, cv_service);
}
pub fn verify_equation(&self) -> bool {
self.inner.verify_equation()
}
pub fn reset(&mut self) {
self.inner.reset();
}
pub fn get_status_json(&self) -> String {
serde_json::to_string(&self.inner.get_falsification_status()).unwrap_or_default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_equation_kingman_formula() {
let demo = KingmanHockeyDemo::new(42);
let rho = demo.utilization;
let ca2 = demo.cv_arrivals * demo.cv_arrivals;
let cs2 = demo.cv_service * demo.cv_service;
let ts = demo.mean_service_time;
let expected = (rho / (1.0 - rho)) * ((ca2 + cs2) / 2.0) * ts;
let prediction = demo.kingman_prediction();
assert!((expected - prediction).abs() < 1e-10);
}
#[test]
fn test_equation_hyperbolic_growth() {
let values = [0.5, 0.7, 0.9, 0.95, 0.99];
let factors: Vec<f64> = values.iter().map(|&rho| rho / (1.0 - rho)).collect();
assert!((factors[0] - 1.0).abs() < 0.01);
assert!((factors[2] - 9.0).abs() < 0.01);
assert!((factors[4] - 99.0).abs() < 1.0);
}
#[test]
fn test_failing_linear_underestimates() {
let mut demo = KingmanHockeyDemo::new(42);
demo.set_utilization(0.5);
demo.run_until(1000.0);
let wait_50 = demo.average_wait_time();
let linear_95 = demo.linear_prediction(0.5, wait_50);
demo.set_utilization(0.95);
let kingman_95 = demo.kingman_prediction();
assert!(
kingman_95 > linear_95 * 5.0,
"Kingman {} should be >>5x linear {}",
kingman_95,
linear_95
);
}
#[test]
fn test_verification_kingman_accuracy() {
let mut demo = KingmanHockeyDemo::new(42);
let utils = [0.5, 0.7, 0.85];
for &rho in &utils {
demo.reset();
demo.set_utilization(rho);
demo.run_until(5000.0);
let simulated = demo.average_wait_time();
let predicted = demo.kingman_prediction();
let error = (simulated - predicted).abs() / predicted;
assert!(
error < 0.5,
"At ρ={rho}: simulated={simulated:.2}, predicted={predicted:.2}, error={:.0}%",
error * 100.0
);
}
}
#[test]
fn test_verification_hockey_stick() {
let mut demo = KingmanHockeyDemo::new(42);
let utilizations = vec![0.3, 0.5, 0.7, 0.8, 0.85, 0.9, 0.95];
demo.build_hockey_stick(&utilizations, 2000.0);
let ratio = demo.hockey_stick_ratio();
assert!(
ratio > 5.0,
"Hockey stick ratio should be >5, got {ratio:.1}"
);
}
#[test]
fn test_verification_exponential_growth() {
let mut demo = KingmanHockeyDemo::new(42);
let utils = [0.5, 0.7, 0.85, 0.9, 0.95];
let mut waits = Vec::new();
for &rho in &utils {
demo.reset();
demo.set_utilization(rho);
demo.run_until(3000.0);
waits.push(demo.average_wait_time());
}
for i in 1..waits.len() - 1 {
let delta_prev = waits[i] - waits[i - 1];
let delta_curr = waits[i + 1] - waits[i];
println!(
"Util {:.0}%→{:.0}%: Δ={:.2}, {:.0}%→{:.0}%: Δ={:.2}",
utils[i - 1] * 100.0,
utils[i] * 100.0,
delta_prev,
utils[i] * 100.0,
utils[i + 1] * 100.0,
delta_curr
);
}
}
#[test]
fn test_falsification_high_cv() {
let mut low_cv = KingmanHockeyDemo::new(42);
low_cv.set_cv(0.5, 0.5);
low_cv.set_utilization(0.8);
low_cv.run_until(3000.0);
let wait_low = low_cv.average_wait_time();
let mut high_cv = KingmanHockeyDemo::new(42);
high_cv.set_cv(1.5, 1.5);
high_cv.set_utilization(0.8);
high_cv.run_until(3000.0);
let wait_high = high_cv.average_wait_time();
assert!(
wait_high > wait_low,
"High CV wait {wait_high:.2} should > low CV wait {wait_low:.2}"
);
}
#[test]
fn test_falsification_status_structure() {
let demo = KingmanHockeyDemo::new(42);
let status = demo.get_falsification_status();
assert_eq!(status.criteria.len(), 2);
assert_eq!(status.criteria[0].id, "KF-HOCKEY");
assert_eq!(status.criteria[1].id, "KF-FIT");
}
#[test]
fn test_demo_trait_implementation() {
let mut demo = KingmanHockeyDemo::new(42);
assert_eq!(demo.name(), "Kingman's Hockey Stick");
assert_eq!(demo.emc_ref(), "operations/kingmans_formula");
demo.step(0.0);
assert!(demo.time > 0.0);
demo.reset();
assert_eq!(demo.time, 0.0);
}
#[test]
fn test_reproducibility() {
let mut demo1 = KingmanHockeyDemo::new(42);
let mut demo2 = KingmanHockeyDemo::new(42);
demo1.run_until(100.0);
demo2.run_until(100.0);
assert_eq!(demo1.customers_served, demo2.customers_served);
}
#[test]
fn test_default() {
let demo = KingmanHockeyDemo::default();
assert_eq!(demo.seed, 42);
assert!((demo.utilization - 0.5).abs() < 1e-10);
}
#[test]
fn test_clone() {
let demo = KingmanHockeyDemo::new(42);
let cloned = demo.clone();
assert_eq!(demo.seed, cloned.seed);
assert!((demo.utilization - cloned.utilization).abs() < 1e-10);
}
#[test]
fn test_debug() {
let demo = KingmanHockeyDemo::new(42);
let debug_str = format!("{demo:?}");
assert!(debug_str.contains("KingmanHockeyDemo"));
}
#[test]
fn test_serialization() {
let demo = KingmanHockeyDemo::new(42);
let json = serde_json::to_string(&demo).expect("serialize");
assert!(json.contains("utilization"));
let restored: KingmanHockeyDemo = serde_json::from_str(&json).expect("deserialize");
assert!((restored.utilization - demo.utilization).abs() < 1e-10);
}
#[test]
fn test_set_utilization_clamping() {
let mut demo = KingmanHockeyDemo::new(42);
demo.set_utilization(0.001);
assert!((demo.utilization - 0.01).abs() < 1e-10);
demo.set_utilization(1.5);
assert!((demo.utilization - 0.99).abs() < 1e-10);
demo.set_utilization(0.75);
assert!((demo.utilization - 0.75).abs() < 1e-10);
}
#[test]
fn test_set_cv_clamping() {
let mut demo = KingmanHockeyDemo::new(42);
demo.set_cv(0.01, 0.01);
assert!((demo.cv_arrivals - 0.1).abs() < 1e-10);
assert!((demo.cv_service - 0.1).abs() < 1e-10);
demo.set_cv(2.0, 1.5);
assert!((demo.cv_arrivals - 2.0).abs() < 1e-10);
assert!((demo.cv_service - 1.5).abs() < 1e-10);
}
#[test]
fn test_arrival_rate() {
let mut demo = KingmanHockeyDemo::new(42);
demo.set_utilization(0.8);
demo.mean_service_time = 2.0;
let rate = demo.arrival_rate();
assert!((rate - 0.4).abs() < 1e-10);
}
#[test]
fn test_service_rate() {
let mut demo = KingmanHockeyDemo::new(42);
demo.mean_service_time = 2.0;
let rate = demo.service_rate();
assert!((rate - 0.5).abs() < 1e-10);
}
#[test]
fn test_average_wait_time_zero_customers() {
let demo = KingmanHockeyDemo::new(42);
assert!((demo.average_wait_time() - 0.0).abs() < 1e-10);
}
#[test]
fn test_calculate_r_squared_insufficient_data() {
let mut demo = KingmanHockeyDemo::new(42);
demo.history = vec![(0.5, 1.0, 1.1)];
assert!((demo.calculate_r_squared() - 0.0).abs() < 1e-10);
demo.history = vec![(0.5, 1.0, 1.1), (0.7, 2.0, 2.1)];
assert!((demo.calculate_r_squared() - 0.0).abs() < 1e-10);
}
#[test]
fn test_calculate_r_squared_perfect_fit() {
let mut demo = KingmanHockeyDemo::new(42);
demo.history = vec![(0.5, 1.0, 1.0), (0.7, 2.0, 2.0), (0.9, 9.0, 9.0)];
let r2 = demo.calculate_r_squared();
assert!((r2 - 1.0).abs() < 1e-10, "R² should be 1.0, got {r2}");
}
#[test]
fn test_calculate_r_squared_zero_variance() {
let mut demo = KingmanHockeyDemo::new(42);
demo.history = vec![(0.5, 1.0, 1.1), (0.7, 1.0, 1.2), (0.9, 1.0, 1.3)];
let r2 = demo.calculate_r_squared();
assert!((r2 - 0.0).abs() < 1e-10);
}
#[test]
fn test_hockey_stick_ratio_no_data() {
let mut demo = KingmanHockeyDemo::new(42);
demo.history = vec![];
assert!((demo.hockey_stick_ratio() - 0.0).abs() < 1e-10);
}
#[test]
fn test_hockey_stick_ratio_missing_50() {
let mut demo = KingmanHockeyDemo::new(42);
demo.history = vec![(0.9, 5.0, 5.0), (0.95, 10.0, 10.0)];
assert!((demo.hockey_stick_ratio() - 0.0).abs() < 1e-10);
}
#[test]
fn test_hockey_stick_ratio_missing_95() {
let mut demo = KingmanHockeyDemo::new(42);
demo.history = vec![(0.5, 1.0, 1.0), (0.7, 2.0, 2.0)];
assert!((demo.hockey_stick_ratio() - 0.0).abs() < 1e-10);
}
#[test]
fn test_hockey_stick_ratio_zero_wait_50() {
let mut demo = KingmanHockeyDemo::new(42);
demo.history = vec![(0.5, 0.0, 0.1), (0.95, 10.0, 10.0)];
assert!((demo.hockey_stick_ratio() - 0.0).abs() < 1e-10);
}
#[test]
fn test_linear_prediction() {
let mut demo = KingmanHockeyDemo::new(42);
demo.set_utilization(0.9);
let linear = demo.linear_prediction(0.5, 1.0);
assert!((linear - 1.8).abs() < 1e-10);
}
#[test]
fn test_process_departure_empty_queue() {
let mut demo = KingmanHockeyDemo::new(42);
demo.run_until(10.0);
}
#[test]
fn test_high_cv_gamma_distribution() {
let mut demo = KingmanHockeyDemo::new(42);
demo.set_cv(0.3, 0.3); demo.run_until(100.0);
assert!(demo.customers_served > 0);
}
#[test]
fn test_reset_preserves_cv() {
let mut demo = KingmanHockeyDemo::new(42);
demo.set_cv(2.0, 1.5);
demo.run_until(100.0);
demo.reset();
assert!((demo.cv_arrivals - 2.0).abs() < 1e-10);
assert!((demo.cv_service - 1.5).abs() < 1e-10);
assert_eq!(demo.time, 0.0);
}
#[test]
fn test_run_until_boundary() {
let mut demo = KingmanHockeyDemo::new(42);
demo.run_until(0.0);
assert!(demo.time >= 0.0);
}
#[test]
fn test_step_multiple() {
let mut demo = KingmanHockeyDemo::new(42);
for _ in 0..100 {
demo.step(0.0);
}
assert!(demo.time > 0.0);
assert!(demo.customers_served > 0);
}
#[test]
fn test_falsification_status_failed_hockey() {
let mut demo = KingmanHockeyDemo::new(42);
demo.history = vec![
(0.5, 1.0, 1.0),
(0.95, 5.0, 19.0), ];
let status = demo.get_falsification_status();
assert!(!status.verified);
assert!(status.message.contains("Hockey stick not pronounced"));
}
#[test]
fn test_falsification_status_failed_fit() {
let mut demo = KingmanHockeyDemo::new(42);
demo.history = vec![
(0.5, 1.0, 5.0), (0.7, 2.0, 10.0), (0.95, 19.0, 50.0), ];
let status = demo.get_falsification_status();
assert!(status.message.contains("R²") || !status.verified);
}
#[test]
fn test_queue_dynamics() {
let mut demo = KingmanHockeyDemo::new(42);
demo.set_utilization(0.95); demo.run_until(500.0);
assert!(demo.customers_served > 10);
}
}