Skip to main content

simular/demos/
littles_law_factory.rs

1//! Demo 2: Little's Law Factory Simulation
2//!
3//! Interactive factory floor demonstrating WIP, throughput, and cycle time relationship.
4//!
5//! # Governing Equation
6//!
7//! ```text
8//! Little's Law: L = λW
9//!
10//! Where:
11//!   L = Average number in system (WIP)
12//!   λ = Average arrival rate (Throughput)
13//!   W = Average time in system (Cycle Time)
14//! ```
15//!
16//! # EDD Cycle
17//!
18//! 1. **Equation**: WIP = Throughput × Cycle Time (holds for ANY stable system)
19//! 2. **Failing Test**: |L - λW| / L > 0.05 (5% tolerance violation)
20//! 3. **Implementation**: M/M/1 discrete event simulation queue
21//! 4. **Verification**: Linear regression R² > 0.98 for WIP vs TH×CT
22//! 5. **Falsification**: During transients (startup), law temporarily violated
23
24use super::{CriterionStatus, EddDemo, FalsificationStatus};
25use crate::engine::rng::SimRng;
26use serde::{Deserialize, Serialize};
27use std::collections::VecDeque;
28
29/// Little's Law Factory demo state.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct LittlesLawFactoryDemo {
32    /// Current simulation time.
33    pub time: f64,
34    /// Arrival rate λ (items/hour).
35    pub arrival_rate: f64,
36    /// Service rate μ (items/hour).
37    pub service_rate: f64,
38    /// Current WIP (items in system).
39    pub wip: usize,
40    /// Total items that have entered the system.
41    pub total_arrivals: u64,
42    /// Total items that have exited the system.
43    pub total_departures: u64,
44    /// Sum of cycle times for departed items.
45    pub total_cycle_time: f64,
46    /// Time-weighted WIP integral (for average WIP calculation).
47    pub wip_integral: f64,
48    /// Last time WIP changed (for integral calculation).
49    pub last_wip_change_time: f64,
50    /// Queue of arrival times (for cycle time calculation).
51    #[serde(skip)]
52    arrival_times: VecDeque<f64>,
53    /// Next arrival event time.
54    pub next_arrival: f64,
55    /// Next departure event time (None if queue empty).
56    pub next_departure: Option<f64>,
57    /// WIP cap for CONWIP mode (None = infinite).
58    pub wip_cap: Option<usize>,
59    /// Tolerance for Little's Law verification.
60    pub tolerance: f64,
61    /// Minimum simulation time before verification.
62    pub warmup_time: f64,
63    /// RNG for stochastic arrivals/service.
64    #[serde(skip)]
65    rng: Option<SimRng>,
66    /// Seed for reproducibility.
67    pub seed: u64,
68    /// History of (time, wip, throughput, `cycle_time`) for analysis.
69    #[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    /// Create a new Little's Law factory demo.
81    #[must_use]
82    pub fn new(seed: u64) -> Self {
83        let mut demo = Self {
84            time: 0.0,
85            arrival_rate: 4.0, // 4 items/hour
86            service_rate: 5.0, // 5 items/hour (utilization = 80%)
87            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        // Schedule first arrival
105        demo.schedule_arrival();
106        demo
107    }
108
109    /// Set arrival and service rates.
110    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    /// Enable CONWIP mode with WIP cap.
116    pub fn set_wip_cap(&mut self, cap: Option<usize>) {
117        self.wip_cap = cap;
118    }
119
120    /// Get current utilization ρ = λ/μ.
121    #[must_use]
122    pub fn utilization(&self) -> f64 {
123        self.arrival_rate / self.service_rate
124    }
125
126    /// Get average WIP (time-weighted).
127    #[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    /// Get throughput (departures per unit time).
138    #[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    /// Get average cycle time.
148    #[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    /// Get Little's Law prediction: L = λW.
158    #[must_use]
159    pub fn littles_law_prediction(&self) -> f64 {
160        self.throughput() * self.average_cycle_time()
161    }
162
163    /// Get Little's Law error: |L - λW| / L.
164    #[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    /// Check if system is in steady state.
177    #[must_use]
178    pub fn is_steady_state(&self) -> bool {
179        self.time >= self.warmup_time && self.total_departures >= 100
180    }
181
182    /// Schedule next arrival using exponential interarrival time.
183    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    /// Schedule next departure using exponential service time.
192    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    /// Update WIP integral before changing WIP.
201    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    /// Process an arrival event.
207    fn process_arrival(&mut self) {
208        // Check CONWIP cap
209        if let Some(cap) = self.wip_cap {
210            if self.wip >= cap {
211                // Blocked arrival - schedule next one anyway
212                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 server was idle, start service
223        if self.next_departure.is_none() {
224            self.schedule_departure();
225        }
226
227        // Schedule next arrival
228        self.schedule_arrival();
229    }
230
231    /// Process a departure event.
232    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        // Calculate cycle time for this item
243        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 more items waiting, schedule next departure
249        if self.wip > 0 {
250            self.schedule_departure();
251        } else {
252            self.next_departure = None;
253        }
254    }
255
256    /// Record history point for analysis.
257    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    /// Calculate R² for Little's Law validation.
269    #[must_use]
270    pub fn calculate_r_squared(&self) -> f64 {
271        if self.history.len() < 10 {
272            return 0.0;
273        }
274
275        // Calculate R² for WIP vs TH×CT
276        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; // TH × CT
285            let y = wip; // Actual WIP
286
287            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    /// Run simulation for a given duration.
306    #[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); // Step size ignored for DES
310        }
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        // Discrete event simulation - find next event
325        let next_event_time = match self.next_departure {
326            Some(dep) => self.next_arrival.min(dep),
327            None => self.next_arrival,
328        };
329
330        // Advance time to next event
331        self.time = next_event_time;
332
333        // Process event(s) at this time
334        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        // Periodically record history
345        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// =============================================================================
418// WASM Bindings
419// =============================================================================
420
421#[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// =============================================================================
487// Tests - Following EDD Methodology
488// =============================================================================
489
490#[cfg(test)]
491mod tests {
492    use super::*;
493
494    // =========================================================================
495    // Phase 1: Equation - Define what we're testing
496    // =========================================================================
497
498    #[test]
499    fn test_equation_littles_law_formula() {
500        // L = λW (average WIP = throughput × average cycle time)
501        let mut demo = LittlesLawFactoryDemo::new(42);
502
503        // Run to steady state
504        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        // L should approximately equal λW
511        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        // ρ = λ/μ
526        let expected_util = demo.arrival_rate / demo.service_rate;
527        assert!((demo.utilization() - expected_util).abs() < 1e-10);
528    }
529
530    // =========================================================================
531    // Phase 2: Failing Test - During transients, law violated
532    // =========================================================================
533
534    #[test]
535    fn test_failing_transient_period() {
536        let mut demo = LittlesLawFactoryDemo::new(42);
537
538        // During warmup, system is NOT in steady state
539        demo.run_until(1.0); // Very short run
540
541        assert!(
542            !demo.is_steady_state(),
543            "Should not be in steady state after 1 time unit"
544        );
545
546        // Verification should fail during transient
547        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        // Document: High variability makes convergence slower
556        let mut demo = LittlesLawFactoryDemo::new(42);
557        demo.tolerance = 0.01; // Very strict tolerance
558
559        // Short run may not converge
560        demo.run_until(50.0);
561
562        let error = demo.littles_law_error();
563        // Just document the error, may or may not pass
564        println!("Short run error: {:.2}%", error * 100.0);
565    }
566
567    // =========================================================================
568    // Phase 3: Implementation - Long run verifies law
569    // =========================================================================
570
571    #[test]
572    fn test_verification_long_run() {
573        let mut demo = LittlesLawFactoryDemo::new(42);
574        demo.tolerance = 0.05;
575
576        // Long run to steady state
577        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        // Long run with history
596        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    // =========================================================================
606    // Phase 4: Verification - Different utilization levels
607    // =========================================================================
608
609    #[test]
610    fn test_verification_low_utilization() {
611        let mut demo = LittlesLawFactoryDemo::new(42);
612        demo.set_rates(2.0, 5.0); // ρ = 0.4
613        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); // ρ = 0.9
626        demo.run_until(1000.0); // Need longer for high util
627
628        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    // =========================================================================
636    // Phase 5: Falsification - CONWIP changes behavior
637    // =========================================================================
638
639    #[test]
640    fn test_falsification_conwip_mode() {
641        let mut demo = LittlesLawFactoryDemo::new(42);
642        demo.set_wip_cap(Some(5)); // Cap WIP at 5
643
644        demo.run_until(500.0);
645
646        // Little's Law still holds for CONWIP!
647        assert!(
648            demo.verify_equation(),
649            "Little's Law should hold even with CONWIP"
650        );
651
652        // But WIP is bounded
653        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); // ρ > 1 (unstable!)
660        demo.tolerance = 0.05;
661
662        // Run - queue will grow unbounded
663        demo.run_until(100.0);
664
665        // System is unstable - WIP grows without bound
666        // Little's Law "holds" but averages are meaningless
667        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    // =========================================================================
685    // Integration tests
686    // =========================================================================
687
688    #[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    // =========================================================================
715    // Additional coverage tests
716    // =========================================================================
717
718    #[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        // At time 0, average WIP should be 0
754        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        // When L=0, error should be 0
773        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        // May not have 100 departures yet
790        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); // Very high arrival rate, slow service
820
821        demo.run_until(100.0);
822
823        // WIP should never exceed cap
824        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        // No history
831        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        // Less than 3 points
839        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        // All same WIP values
846        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        // With zero variance, R² should be 0
853        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        // History should have been recorded
862        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; // Very long warmup
879        demo.run_until(10.0);
880
881        let status = demo.get_falsification_status();
882        // Should not be verified (not in steady state)
883        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        // Ensure WIP is 0 and next_departure is Some
890        demo.wip = 0;
891        demo.next_departure = Some(1.0);
892
893        // Process a departure with empty system
894        demo.time = 1.0;
895        demo.process_departure();
896
897        // next_departure should be None now
898        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        // WIP integral should be > 0 after running
907        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        // Should not advance
935        assert!(demo.time <= demo.next_arrival);
936    }
937}