1use super::{CriterionStatus, EddDemo, FalsificationStatus};
25use crate::engine::rng::SimRng;
26use serde::{Deserialize, Serialize};
27use std::collections::VecDeque;
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct LittlesLawFactoryDemo {
32 pub time: f64,
34 pub arrival_rate: f64,
36 pub service_rate: f64,
38 pub wip: usize,
40 pub total_arrivals: u64,
42 pub total_departures: u64,
44 pub total_cycle_time: f64,
46 pub wip_integral: f64,
48 pub last_wip_change_time: f64,
50 #[serde(skip)]
52 arrival_times: VecDeque<f64>,
53 pub next_arrival: f64,
55 pub next_departure: Option<f64>,
57 pub wip_cap: Option<usize>,
59 pub tolerance: f64,
61 pub warmup_time: f64,
63 #[serde(skip)]
65 rng: Option<SimRng>,
66 pub seed: u64,
68 #[serde(skip)]
70 pub history: Vec<(f64, f64, f64, f64)>,
71}
72
73impl Default for LittlesLawFactoryDemo {
74 fn default() -> Self {
75 Self::new(42)
76 }
77}
78
79impl LittlesLawFactoryDemo {
80 #[must_use]
82 pub fn new(seed: u64) -> Self {
83 let mut demo = Self {
84 time: 0.0,
85 arrival_rate: 4.0, service_rate: 5.0, wip: 0,
88 total_arrivals: 0,
89 total_departures: 0,
90 total_cycle_time: 0.0,
91 wip_integral: 0.0,
92 last_wip_change_time: 0.0,
93 arrival_times: VecDeque::new(),
94 next_arrival: 0.0,
95 next_departure: None,
96 wip_cap: None,
97 tolerance: 0.05,
98 warmup_time: 10.0,
99 rng: Some(SimRng::new(seed)),
100 seed,
101 history: Vec::new(),
102 };
103
104 demo.schedule_arrival();
106 demo
107 }
108
109 pub fn set_rates(&mut self, arrival_rate: f64, service_rate: f64) {
111 self.arrival_rate = arrival_rate;
112 self.service_rate = service_rate;
113 }
114
115 pub fn set_wip_cap(&mut self, cap: Option<usize>) {
117 self.wip_cap = cap;
118 }
119
120 #[must_use]
122 pub fn utilization(&self) -> f64 {
123 self.arrival_rate / self.service_rate
124 }
125
126 #[must_use]
128 pub fn average_wip(&self) -> f64 {
129 if self.time > 0.0 {
130 (self.wip_integral + self.wip as f64 * (self.time - self.last_wip_change_time))
131 / self.time
132 } else {
133 0.0
134 }
135 }
136
137 #[must_use]
139 pub fn throughput(&self) -> f64 {
140 if self.time > 0.0 {
141 self.total_departures as f64 / self.time
142 } else {
143 0.0
144 }
145 }
146
147 #[must_use]
149 pub fn average_cycle_time(&self) -> f64 {
150 if self.total_departures > 0 {
151 self.total_cycle_time / self.total_departures as f64
152 } else {
153 0.0
154 }
155 }
156
157 #[must_use]
159 pub fn littles_law_prediction(&self) -> f64 {
160 self.throughput() * self.average_cycle_time()
161 }
162
163 #[must_use]
165 pub fn littles_law_error(&self) -> f64 {
166 let l = self.average_wip();
167 let prediction = self.littles_law_prediction();
168
169 if l > 0.0 {
170 (l - prediction).abs() / l
171 } else {
172 0.0
173 }
174 }
175
176 #[must_use]
178 pub fn is_steady_state(&self) -> bool {
179 self.time >= self.warmup_time && self.total_departures >= 100
180 }
181
182 fn schedule_arrival(&mut self) {
184 if let Some(ref mut rng) = self.rng {
185 let u: f64 = rng.gen_range_f64(0.0001, 1.0);
186 let interarrival = -u.ln() / self.arrival_rate;
187 self.next_arrival = self.time + interarrival;
188 }
189 }
190
191 fn schedule_departure(&mut self) {
193 if let Some(ref mut rng) = self.rng {
194 let u: f64 = rng.gen_range_f64(0.0001, 1.0);
195 let service_time = -u.ln() / self.service_rate;
196 self.next_departure = Some(self.time + service_time);
197 }
198 }
199
200 fn update_wip_integral(&mut self) {
202 self.wip_integral += self.wip as f64 * (self.time - self.last_wip_change_time);
203 self.last_wip_change_time = self.time;
204 }
205
206 fn process_arrival(&mut self) {
208 if let Some(cap) = self.wip_cap {
210 if self.wip >= cap {
211 self.schedule_arrival();
213 return;
214 }
215 }
216
217 self.update_wip_integral();
218 self.wip += 1;
219 self.total_arrivals += 1;
220 self.arrival_times.push_back(self.time);
221
222 if self.next_departure.is_none() {
224 self.schedule_departure();
225 }
226
227 self.schedule_arrival();
229 }
230
231 fn process_departure(&mut self) {
233 if self.wip == 0 {
234 self.next_departure = None;
235 return;
236 }
237
238 self.update_wip_integral();
239 self.wip -= 1;
240 self.total_departures += 1;
241
242 if let Some(arrival_time) = self.arrival_times.pop_front() {
244 let cycle_time = self.time - arrival_time;
245 self.total_cycle_time += cycle_time;
246 }
247
248 if self.wip > 0 {
250 self.schedule_departure();
251 } else {
252 self.next_departure = None;
253 }
254 }
255
256 fn record_history(&mut self) {
258 if self.total_departures > 0 {
259 self.history.push((
260 self.time,
261 self.average_wip(),
262 self.throughput(),
263 self.average_cycle_time(),
264 ));
265 }
266 }
267
268 #[must_use]
270 pub fn calculate_r_squared(&self) -> f64 {
271 if self.history.len() < 10 {
272 return 0.0;
273 }
274
275 let n = self.history.len() as f64;
277 let mut sum_x = 0.0;
278 let mut sum_y = 0.0;
279 let mut sum_xy = 0.0;
280 let mut sum_x2 = 0.0;
281 let mut sum_y2 = 0.0;
282
283 for &(_, wip, th, ct) in &self.history {
284 let x = th * ct; let y = wip; sum_x += x;
288 sum_y += y;
289 sum_xy += x * y;
290 sum_x2 += x * x;
291 sum_y2 += y * y;
292 }
293
294 let numerator = n * sum_xy - sum_x * sum_y;
295 let denominator = ((n * sum_x2 - sum_x * sum_x) * (n * sum_y2 - sum_y * sum_y)).sqrt();
296
297 if denominator > f64::EPSILON {
298 let r = numerator / denominator;
299 r * r
300 } else {
301 0.0
302 }
303 }
304
305 #[allow(clippy::while_float)]
307 pub fn run_until(&mut self, end_time: f64) {
308 while self.time < end_time {
309 self.step(0.0); }
311 }
312}
313
314impl EddDemo for LittlesLawFactoryDemo {
315 fn name(&self) -> &'static str {
316 "Little's Law Factory Simulation"
317 }
318
319 fn emc_ref(&self) -> &'static str {
320 "operations/littles_law"
321 }
322
323 fn step(&mut self, _dt: f64) {
324 let next_event_time = match self.next_departure {
326 Some(dep) => self.next_arrival.min(dep),
327 None => self.next_arrival,
328 };
329
330 self.time = next_event_time;
332
333 if self.next_arrival <= next_event_time {
335 self.process_arrival();
336 }
337
338 if let Some(dep) = self.next_departure {
339 if dep <= next_event_time {
340 self.process_departure();
341 }
342 }
343
344 if self.total_departures % 10 == 0 {
346 self.record_history();
347 }
348 }
349
350 fn verify_equation(&self) -> bool {
351 if !self.is_steady_state() {
352 return false;
353 }
354
355 self.littles_law_error() < self.tolerance
356 }
357
358 fn get_falsification_status(&self) -> FalsificationStatus {
359 let error = self.littles_law_error();
360 let r_squared = self.calculate_r_squared();
361 let steady_state = self.is_steady_state();
362
363 let linear_passed = r_squared > 0.98;
364 let error_passed = error < self.tolerance;
365 let steady_passed = steady_state;
366
367 FalsificationStatus {
368 verified: linear_passed && error_passed && steady_passed,
369 criteria: vec![
370 CriterionStatus {
371 id: "LL-LINEAR".to_string(),
372 name: "Linear relationship".to_string(),
373 passed: linear_passed,
374 value: r_squared,
375 threshold: 0.98,
376 },
377 CriterionStatus {
378 id: "LL-ERROR".to_string(),
379 name: "Little's Law error".to_string(),
380 passed: error_passed,
381 value: error,
382 threshold: self.tolerance,
383 },
384 CriterionStatus {
385 id: "LL-STEADY".to_string(),
386 name: "Steady state".to_string(),
387 passed: steady_passed,
388 value: self.time,
389 threshold: self.warmup_time,
390 },
391 ],
392 message: if linear_passed && error_passed && steady_passed {
393 format!(
394 "Little's Law verified: L={:.2}, λW={:.2}, R²={:.4}",
395 self.average_wip(),
396 self.littles_law_prediction(),
397 r_squared
398 )
399 } else if !steady_passed {
400 "System not yet in steady state (transient)".to_string()
401 } else {
402 format!(
403 "FALSIFIED: error={:.2}% > {:.2}%, R²={:.4}",
404 error * 100.0,
405 self.tolerance * 100.0,
406 r_squared
407 )
408 },
409 }
410 }
411
412 fn reset(&mut self) {
413 *self = Self::new(self.seed);
414 }
415}
416
417#[cfg(feature = "wasm")]
422mod wasm {
423 use super::{EddDemo, LittlesLawFactoryDemo};
424 use wasm_bindgen::prelude::*;
425
426 #[wasm_bindgen]
427 pub struct WasmLittlesLawFactory {
428 inner: LittlesLawFactoryDemo,
429 }
430
431 #[wasm_bindgen]
432 impl WasmLittlesLawFactory {
433 #[wasm_bindgen(constructor)]
434 pub fn new(seed: u64) -> Self {
435 Self {
436 inner: LittlesLawFactoryDemo::new(seed),
437 }
438 }
439
440 pub fn step(&mut self) {
441 self.inner.step(0.0);
442 }
443
444 pub fn get_wip(&self) -> usize {
445 self.inner.wip
446 }
447
448 pub fn get_throughput(&self) -> f64 {
449 self.inner.throughput()
450 }
451
452 pub fn get_cycle_time(&self) -> f64 {
453 self.inner.average_cycle_time()
454 }
455
456 pub fn get_utilization(&self) -> f64 {
457 self.inner.utilization()
458 }
459
460 pub fn get_time(&self) -> f64 {
461 self.inner.time
462 }
463
464 pub fn verify_equation(&self) -> bool {
465 self.inner.verify_equation()
466 }
467
468 pub fn set_rates(&mut self, arrival_rate: f64, service_rate: f64) {
469 self.inner.set_rates(arrival_rate, service_rate);
470 }
471
472 pub fn set_wip_cap(&mut self, cap: usize) {
473 self.inner.set_wip_cap(Some(cap));
474 }
475
476 pub fn reset(&mut self) {
477 self.inner.reset();
478 }
479
480 pub fn get_status_json(&self) -> String {
481 serde_json::to_string(&self.inner.get_falsification_status()).unwrap_or_default()
482 }
483 }
484}
485
486#[cfg(test)]
491mod tests {
492 use super::*;
493
494 #[test]
499 fn test_equation_littles_law_formula() {
500 let mut demo = LittlesLawFactoryDemo::new(42);
502
503 demo.run_until(100.0);
505
506 let l = demo.average_wip();
507 let lambda = demo.throughput();
508 let w = demo.average_cycle_time();
509
510 let prediction = lambda * w;
512 let error = (l - prediction).abs() / l.max(0.001);
513
514 assert!(
515 error < 0.10,
516 "Little's Law: L={l:.2}, λW={prediction:.2}, error={:.1}%",
517 error * 100.0
518 );
519 }
520
521 #[test]
522 fn test_equation_steady_state_utilization() {
523 let demo = LittlesLawFactoryDemo::new(42);
524
525 let expected_util = demo.arrival_rate / demo.service_rate;
527 assert!((demo.utilization() - expected_util).abs() < 1e-10);
528 }
529
530 #[test]
535 fn test_failing_transient_period() {
536 let mut demo = LittlesLawFactoryDemo::new(42);
537
538 demo.run_until(1.0); assert!(
542 !demo.is_steady_state(),
543 "Should not be in steady state after 1 time unit"
544 );
545
546 assert!(
548 !demo.verify_equation(),
549 "Little's Law verification should fail during transient"
550 );
551 }
552
553 #[test]
554 fn test_failing_high_variability() {
555 let mut demo = LittlesLawFactoryDemo::new(42);
557 demo.tolerance = 0.01; demo.run_until(50.0);
561
562 let error = demo.littles_law_error();
563 println!("Short run error: {:.2}%", error * 100.0);
565 }
566
567 #[test]
572 fn test_verification_long_run() {
573 let mut demo = LittlesLawFactoryDemo::new(42);
574 demo.tolerance = 0.05;
575
576 demo.run_until(500.0);
578
579 assert!(
580 demo.is_steady_state(),
581 "Should be in steady state after 500 time units"
582 );
583
584 assert!(
585 demo.verify_equation(),
586 "Little's Law should be verified. Error: {:.2}%",
587 demo.littles_law_error() * 100.0
588 );
589 }
590
591 #[test]
592 fn test_verification_r_squared() {
593 let mut demo = LittlesLawFactoryDemo::new(42);
594
595 demo.run_until(500.0);
597
598 let r_squared = demo.calculate_r_squared();
599 assert!(
600 r_squared > 0.90,
601 "R² should be high for Little's Law: {r_squared}"
602 );
603 }
604
605 #[test]
610 fn test_verification_low_utilization() {
611 let mut demo = LittlesLawFactoryDemo::new(42);
612 demo.set_rates(2.0, 5.0); demo.run_until(500.0);
614
615 assert!(
616 demo.verify_equation(),
617 "Little's Law at low util: error={:.2}%",
618 demo.littles_law_error() * 100.0
619 );
620 }
621
622 #[test]
623 fn test_verification_high_utilization() {
624 let mut demo = LittlesLawFactoryDemo::new(42);
625 demo.set_rates(4.5, 5.0); demo.run_until(1000.0); assert!(
629 demo.verify_equation(),
630 "Little's Law at high util: error={:.2}%",
631 demo.littles_law_error() * 100.0
632 );
633 }
634
635 #[test]
640 fn test_falsification_conwip_mode() {
641 let mut demo = LittlesLawFactoryDemo::new(42);
642 demo.set_wip_cap(Some(5)); demo.run_until(500.0);
645
646 assert!(
648 demo.verify_equation(),
649 "Little's Law should hold even with CONWIP"
650 );
651
652 assert!(demo.wip <= 5, "WIP should be capped at 5");
654 }
655
656 #[test]
657 fn test_falsification_unstable_system() {
658 let mut demo = LittlesLawFactoryDemo::new(42);
659 demo.set_rates(6.0, 5.0); demo.tolerance = 0.05;
661
662 demo.run_until(100.0);
664
665 println!(
668 "Unstable system: WIP={}, departures={}",
669 demo.wip, demo.total_departures
670 );
671 }
672
673 #[test]
674 fn test_falsification_status_structure() {
675 let demo = LittlesLawFactoryDemo::new(42);
676 let status = demo.get_falsification_status();
677
678 assert_eq!(status.criteria.len(), 3);
679 assert_eq!(status.criteria[0].id, "LL-LINEAR");
680 assert_eq!(status.criteria[1].id, "LL-ERROR");
681 assert_eq!(status.criteria[2].id, "LL-STEADY");
682 }
683
684 #[test]
689 fn test_demo_trait_implementation() {
690 let mut demo = LittlesLawFactoryDemo::new(42);
691
692 assert_eq!(demo.name(), "Little's Law Factory Simulation");
693 assert_eq!(demo.emc_ref(), "operations/littles_law");
694
695 demo.step(0.0);
696 assert!(demo.time > 0.0);
697
698 demo.reset();
699 assert_eq!(demo.time, 0.0);
700 }
701
702 #[test]
703 fn test_reproducibility() {
704 let mut demo1 = LittlesLawFactoryDemo::new(42);
705 let mut demo2 = LittlesLawFactoryDemo::new(42);
706
707 demo1.run_until(100.0);
708 demo2.run_until(100.0);
709
710 assert_eq!(demo1.total_arrivals, demo2.total_arrivals);
711 assert_eq!(demo1.total_departures, demo2.total_departures);
712 }
713
714 #[test]
719 fn test_default() {
720 let demo = LittlesLawFactoryDemo::default();
721 assert_eq!(demo.seed, 42);
722 assert!((demo.arrival_rate - 4.0).abs() < 1e-10);
723 }
724
725 #[test]
726 fn test_clone() {
727 let demo = LittlesLawFactoryDemo::new(42);
728 let cloned = demo.clone();
729 assert_eq!(demo.seed, cloned.seed);
730 assert!((demo.arrival_rate - cloned.arrival_rate).abs() < 1e-10);
731 }
732
733 #[test]
734 fn test_debug() {
735 let demo = LittlesLawFactoryDemo::new(42);
736 let debug_str = format!("{demo:?}");
737 assert!(debug_str.contains("LittlesLawFactoryDemo"));
738 }
739
740 #[test]
741 fn test_serialization() {
742 let demo = LittlesLawFactoryDemo::new(42);
743 let json = serde_json::to_string(&demo).expect("serialize");
744 assert!(json.contains("arrival_rate"));
745
746 let restored: LittlesLawFactoryDemo = serde_json::from_str(&json).expect("deserialize");
747 assert!((restored.arrival_rate - demo.arrival_rate).abs() < 1e-10);
748 }
749
750 #[test]
751 fn test_average_wip_zero_time() {
752 let demo = LittlesLawFactoryDemo::new(42);
753 assert!((demo.average_wip() - 0.0).abs() < 1e-10);
755 }
756
757 #[test]
758 fn test_throughput_zero_time() {
759 let demo = LittlesLawFactoryDemo::new(42);
760 assert!((demo.throughput() - 0.0).abs() < 1e-10);
761 }
762
763 #[test]
764 fn test_average_cycle_time_zero_departures() {
765 let demo = LittlesLawFactoryDemo::new(42);
766 assert!((demo.average_cycle_time() - 0.0).abs() < 1e-10);
767 }
768
769 #[test]
770 fn test_littles_law_error_zero_wip() {
771 let demo = LittlesLawFactoryDemo::new(42);
772 assert!((demo.littles_law_error() - 0.0).abs() < 1e-10);
774 }
775
776 #[test]
777 fn test_is_steady_state_false_short_time() {
778 let mut demo = LittlesLawFactoryDemo::new(42);
779 demo.warmup_time = 100.0;
780 demo.run_until(50.0);
781 assert!(!demo.is_steady_state());
782 }
783
784 #[test]
785 fn test_is_steady_state_false_few_departures() {
786 let mut demo = LittlesLawFactoryDemo::new(42);
787 demo.warmup_time = 1.0;
788 demo.run_until(5.0);
789 if demo.total_departures < 100 {
791 assert!(!demo.is_steady_state());
792 }
793 }
794
795 #[test]
796 fn test_set_rates() {
797 let mut demo = LittlesLawFactoryDemo::new(42);
798 demo.set_rates(10.0, 15.0);
799 assert!((demo.arrival_rate - 10.0).abs() < 1e-10);
800 assert!((demo.service_rate - 15.0).abs() < 1e-10);
801 }
802
803 #[test]
804 fn test_set_wip_cap() {
805 let mut demo = LittlesLawFactoryDemo::new(42);
806 assert!(demo.wip_cap.is_none());
807
808 demo.set_wip_cap(Some(10));
809 assert_eq!(demo.wip_cap, Some(10));
810
811 demo.set_wip_cap(None);
812 assert!(demo.wip_cap.is_none());
813 }
814
815 #[test]
816 fn test_conwip_blocks_arrivals() {
817 let mut demo = LittlesLawFactoryDemo::new(42);
818 demo.set_wip_cap(Some(3));
819 demo.set_rates(10.0, 1.0); demo.run_until(100.0);
822
823 assert!(demo.wip <= 3, "WIP {} should be <= 3", demo.wip);
825 }
826
827 #[test]
828 fn test_calculate_r_squared_empty_history() {
829 let demo = LittlesLawFactoryDemo::new(42);
830 assert!((demo.calculate_r_squared() - 0.0).abs() < 1e-10);
832 }
833
834 #[test]
835 fn test_calculate_r_squared_insufficient_data() {
836 let mut demo = LittlesLawFactoryDemo::new(42);
837 demo.history = vec![(1.0, 1.0, 1.0, 1.0), (2.0, 2.0, 2.0, 2.0)];
838 assert!((demo.calculate_r_squared() - 0.0).abs() < 1e-10);
840 }
841
842 #[test]
843 fn test_calculate_r_squared_zero_variance() {
844 let mut demo = LittlesLawFactoryDemo::new(42);
845 demo.history = vec![
847 (1.0, 5.0, 1.0, 1.0),
848 (2.0, 5.0, 1.0, 1.0),
849 (3.0, 5.0, 1.0, 1.0),
850 ];
851 let r2 = demo.calculate_r_squared();
852 assert!((r2 - 0.0).abs() < 1e-10);
854 }
855
856 #[test]
857 fn test_record_history_interval() {
858 let mut demo = LittlesLawFactoryDemo::new(42);
859 demo.run_until(100.0);
860
861 assert!(!demo.history.is_empty(), "History should be populated");
863 }
864
865 #[test]
866 fn test_step_multiple_events() {
867 let mut demo = LittlesLawFactoryDemo::new(42);
868 for _ in 0..200 {
869 demo.step(0.0);
870 }
871 assert!(demo.time > 0.0);
872 assert!(demo.total_arrivals > 0);
873 }
874
875 #[test]
876 fn test_falsification_status_not_steady() {
877 let mut demo = LittlesLawFactoryDemo::new(42);
878 demo.warmup_time = 1000.0; demo.run_until(10.0);
880
881 let status = demo.get_falsification_status();
882 assert!(!status.verified || status.message.contains("not in steady state"));
884 }
885
886 #[test]
887 fn test_process_departure_empty_system() {
888 let mut demo = LittlesLawFactoryDemo::new(42);
889 demo.wip = 0;
891 demo.next_departure = Some(1.0);
892
893 demo.time = 1.0;
895 demo.process_departure();
896
897 assert!(demo.next_departure.is_none());
899 }
900
901 #[test]
902 fn test_wip_integral_tracking() {
903 let mut demo = LittlesLawFactoryDemo::new(42);
904 demo.run_until(50.0);
905
906 let avg_wip = demo.average_wip();
908 assert!(avg_wip >= 0.0);
909 }
910
911 #[test]
912 fn test_utilization_calculation() {
913 let mut demo = LittlesLawFactoryDemo::new(42);
914 demo.set_rates(3.0, 6.0);
915 assert!((demo.utilization() - 0.5).abs() < 1e-10);
916 }
917
918 #[test]
919 fn test_littles_law_prediction() {
920 let mut demo = LittlesLawFactoryDemo::new(42);
921 demo.run_until(200.0);
922
923 let prediction = demo.littles_law_prediction();
924 let throughput = demo.throughput();
925 let cycle_time = demo.average_cycle_time();
926
927 assert!((prediction - throughput * cycle_time).abs() < 1e-10);
928 }
929
930 #[test]
931 fn test_run_until_zero() {
932 let mut demo = LittlesLawFactoryDemo::new(42);
933 demo.run_until(0.0);
934 assert!(demo.time <= demo.next_arrival);
936 }
937}