Skip to main content

simular/edd/
operations.rs

1//! Operations Science equations for manufacturing and service systems.
2//!
3//! This module implements the fundamental equations from operations science
4//! that govern queueing, inventory, and production systems. These equations
5//! are derived from decades of peer-reviewed research and form the mathematical
6//! foundation for simulating production and service systems.
7//!
8//! # References
9//!
10//! - [30] Little, J.D.C. (1961). "A Proof for the Queuing Formula: L = λW"
11//! - [31] Kingman, J.F.C. (1961). "The single server queue in heavy traffic"
12//! - [32] Lee, H.L., et al. (1997). "The Bullwhip Effect in Supply Chains"
13//! - [33] Hopp, W.J. & Spearman, M.L. (2004). "To Pull or Not to Pull"
14
15use super::equation::{
16    Citation, EquationClass, EquationVariable, GoverningEquation, VariableConstraints,
17};
18
19// =============================================================================
20// Little's Law: L = λW
21// =============================================================================
22
23/// Little's Law: The fundamental equation relating WIP, throughput, and cycle time.
24///
25/// `L = λW` where:
26/// - L: Average number of items in the system (WIP)
27/// - λ: Average arrival rate (throughput at steady state)
28/// - W: Average time an item spends in the system (cycle time)
29///
30/// This law holds for ANY stable queueing system regardless of arrival distribution,
31/// service distribution, or queue discipline. It is one of the most fundamental
32/// results in queueing theory.
33#[derive(Debug, Clone)]
34pub struct LittlesLaw {
35    latex: String,
36    description: String,
37}
38
39impl LittlesLaw {
40    /// Create a new Little's Law equation.
41    #[must_use]
42    pub fn new() -> Self {
43        Self {
44            latex: r"L = \lambda W".to_string(),
45            description: "Little's Law relates the average number of items in a stable system (L) \
46                         to the arrival rate (λ) and the average time spent in the system (W)."
47                .to_string(),
48        }
49    }
50
51    /// Evaluate L = λW given arrival rate and wait time.
52    #[must_use]
53    pub fn evaluate(&self, lambda: f64, w: f64) -> f64 {
54        lambda * w
55    }
56
57    /// Validate that observed values satisfy Little's Law within tolerance.
58    ///
59    /// # Arguments
60    /// - `l`: Observed average WIP
61    /// - `lambda`: Observed throughput/arrival rate
62    /// - `w`: Observed average cycle time
63    /// - `tolerance`: Relative tolerance for validation
64    ///
65    /// # Errors
66    /// Returns error message if `|L - λW| / L >= tolerance`.
67    pub fn validate(&self, l: f64, lambda: f64, w: f64, tolerance: f64) -> Result<(), String> {
68        let expected = lambda * w;
69        let relative_error = if l.abs() > f64::EPSILON {
70            (l - expected).abs() / l
71        } else if expected.abs() > f64::EPSILON {
72            (l - expected).abs() / expected
73        } else {
74            0.0
75        };
76
77        if relative_error <= tolerance {
78            Ok(())
79        } else {
80            Err(format!(
81                "Little's Law violation: L={l:.4}, λW={expected:.4}, relative_error={relative_error:.4} > tolerance={tolerance:.4}"
82            ))
83        }
84    }
85
86    /// Solve for L given λ and W.
87    #[must_use]
88    pub fn solve_l(&self, lambda: f64, w: f64) -> f64 {
89        lambda * w
90    }
91
92    /// Solve for λ given L and W.
93    #[must_use]
94    pub fn solve_lambda(&self, l: f64, w: f64) -> f64 {
95        if w.abs() < f64::EPSILON {
96            f64::INFINITY
97        } else {
98            l / w
99        }
100    }
101
102    /// Solve for W given L and λ.
103    #[must_use]
104    pub fn solve_w(&self, l: f64, lambda: f64) -> f64 {
105        if lambda.abs() < f64::EPSILON {
106            f64::INFINITY
107        } else {
108            l / lambda
109        }
110    }
111}
112
113impl Default for LittlesLaw {
114    fn default() -> Self {
115        Self::new()
116    }
117}
118
119impl GoverningEquation for LittlesLaw {
120    fn latex(&self) -> &str {
121        &self.latex
122    }
123
124    fn class(&self) -> EquationClass {
125        EquationClass::Queueing
126    }
127
128    fn citation(&self) -> Citation {
129        Citation::new(&["Little, J.D.C."], "Operations Research", 1961)
130            .with_title("A Proof for the Queuing Formula: L = λW")
131            .with_doi("10.1287/opre.9.3.383")
132    }
133
134    fn variables(&self) -> Vec<EquationVariable> {
135        vec![
136            EquationVariable::new("L", "wip", "items")
137                .with_description("Average number of items in the system (work in progress)")
138                .with_constraints(VariableConstraints {
139                    min: Some(0.0),
140                    max: None,
141                    positive: false,
142                    integer: false,
143                }),
144            EquationVariable::new("λ", "arrival_rate", "items/time")
145                .with_description("Average arrival rate (throughput at steady state)")
146                .with_constraints(VariableConstraints {
147                    min: Some(0.0),
148                    max: None,
149                    positive: true,
150                    integer: false,
151                }),
152            EquationVariable::new("W", "cycle_time", "time")
153                .with_description("Average time an item spends in the system")
154                .with_constraints(VariableConstraints {
155                    min: Some(0.0),
156                    max: None,
157                    positive: true,
158                    integer: false,
159                }),
160        ]
161    }
162
163    fn description(&self) -> &str {
164        &self.description
165    }
166
167    fn name(&self) -> &'static str {
168        "Little's Law"
169    }
170
171    fn validate_consistency(&self, values: &[(&str, f64)], tolerance: f64) -> Result<(), String> {
172        let mut l = None;
173        let mut lambda = None;
174        let mut w = None;
175
176        for (name, value) in values {
177            match *name {
178                "L" | "l" | "wip" => l = Some(*value),
179                "λ" | "lambda" | "arrival_rate" | "throughput" => lambda = Some(*value),
180                "W" | "w" | "cycle_time" => w = Some(*value),
181                _ => {}
182            }
183        }
184
185        match (l, lambda, w) {
186            (Some(l_val), Some(lambda_val), Some(w_val)) => {
187                self.validate(l_val, lambda_val, w_val, tolerance)
188            }
189            _ => Err("Missing required variables for Little's Law validation".to_string()),
190        }
191    }
192}
193
194// =============================================================================
195// Kingman's Formula: VUT Equation
196// =============================================================================
197
198/// Kingman's VUT Formula for queue wait times.
199///
200/// The expected wait time in queue is approximated by:
201/// `W_q ≈ (ρ / (1-ρ)) × ((c_a² + c_s²) / 2) × t_s`
202///
203/// where:
204/// - ρ: Utilization (arrival rate / service rate)
205/// - `c_a`: Coefficient of variation of inter-arrival times
206/// - `c_s`: Coefficient of variation of service times
207/// - `t_s`: Mean service time
208///
209/// This formula reveals the "hockey stick" effect: wait times grow exponentially
210/// as utilization approaches 100%.
211#[derive(Debug, Clone)]
212pub struct KingmanFormula {
213    latex: String,
214    description: String,
215}
216
217impl KingmanFormula {
218    /// Create a new Kingman's Formula equation.
219    #[must_use]
220    pub fn new() -> Self {
221        Self {
222            latex: r"W_q \approx \frac{\rho}{1-\rho} \cdot \frac{c_a^2 + c_s^2}{2} \cdot t_s"
223                .to_string(),
224            description: "Kingman's VUT equation approximates expected queue wait time as a \
225                         function of Variability, Utilization, and Time."
226                .to_string(),
227        }
228    }
229
230    /// Calculate expected wait time in queue.
231    ///
232    /// # Arguments
233    /// - `rho`: Utilization (0 < ρ < 1)
234    /// - `c_a`: Coefficient of variation of arrivals
235    /// - `c_s`: Coefficient of variation of service
236    /// - `t_s`: Mean service time
237    ///
238    /// # Returns
239    /// Expected wait time in queue.
240    #[must_use]
241    pub fn expected_wait_time(&self, rho: f64, c_a: f64, c_s: f64, t_s: f64) -> f64 {
242        if rho >= 1.0 {
243            return f64::INFINITY;
244        }
245        if rho <= 0.0 {
246            return 0.0;
247        }
248
249        let utilization_factor = rho / (1.0 - rho);
250        let variability_factor = (c_a * c_a + c_s * c_s) / 2.0;
251
252        utilization_factor * variability_factor * t_s
253    }
254
255    /// Get the utilization factor ρ/(1-ρ).
256    #[must_use]
257    pub fn utilization_factor(&self, rho: f64) -> f64 {
258        if rho >= 1.0 {
259            f64::INFINITY
260        } else if rho <= 0.0 {
261            0.0
262        } else {
263            rho / (1.0 - rho)
264        }
265    }
266
267    /// Get the variability factor `(c_a² + c_s²)/2`.
268    #[must_use]
269    pub fn variability_factor(&self, c_a: f64, c_s: f64) -> f64 {
270        (c_a * c_a + c_s * c_s) / 2.0
271    }
272}
273
274impl Default for KingmanFormula {
275    fn default() -> Self {
276        Self::new()
277    }
278}
279
280impl GoverningEquation for KingmanFormula {
281    fn latex(&self) -> &str {
282        &self.latex
283    }
284
285    fn class(&self) -> EquationClass {
286        EquationClass::Queueing
287    }
288
289    fn citation(&self) -> Citation {
290        Citation::new(
291            &["Kingman, J.F.C."],
292            "Annals of Mathematical Statistics",
293            1961,
294        )
295        .with_title("The single server queue in heavy traffic")
296        .with_doi("10.1214/aoms/1177704567")
297    }
298
299    fn variables(&self) -> Vec<EquationVariable> {
300        vec![
301            EquationVariable::new("ρ", "utilization", "dimensionless")
302                .with_description("Server utilization ratio (arrival rate / service rate)")
303                .with_constraints(VariableConstraints {
304                    min: Some(0.0),
305                    max: Some(1.0),
306                    positive: false,
307                    integer: false,
308                }),
309            EquationVariable::new("c_a", "arrival_cv", "dimensionless")
310                .with_description("Coefficient of variation of inter-arrival times"),
311            EquationVariable::new("c_s", "service_cv", "dimensionless")
312                .with_description("Coefficient of variation of service times"),
313            EquationVariable::new("t_s", "service_time", "time")
314                .with_description("Mean service time"),
315            EquationVariable::new("W_q", "queue_wait", "time")
316                .with_description("Expected wait time in queue"),
317        ]
318    }
319
320    fn description(&self) -> &str {
321        &self.description
322    }
323
324    fn name(&self) -> &'static str {
325        "Kingman's VUT Formula"
326    }
327
328    fn validate_consistency(&self, values: &[(&str, f64)], tolerance: f64) -> Result<(), String> {
329        let mut rho = None;
330        let mut c_a = None;
331        let mut c_s = None;
332        let mut t_s = None;
333        let mut w_q = None;
334
335        for (name, value) in values {
336            match *name {
337                "ρ" | "rho" | "utilization" => rho = Some(*value),
338                "c_a" | "arrival_cv" => c_a = Some(*value),
339                "c_s" | "service_cv" => c_s = Some(*value),
340                "t_s" | "service_time" => t_s = Some(*value),
341                "W_q" | "queue_wait" => w_q = Some(*value),
342                _ => {}
343            }
344        }
345
346        match (rho, c_a, c_s, t_s, w_q) {
347            (Some(r), Some(ca), Some(cs), Some(ts), Some(wq)) => {
348                let expected = self.expected_wait_time(r, ca, cs, ts);
349                let relative_error = if expected.abs() > f64::EPSILON {
350                    (wq - expected).abs() / expected
351                } else if wq.abs() > f64::EPSILON {
352                    (wq - expected).abs() / wq
353                } else {
354                    0.0
355                };
356
357                if relative_error <= tolerance {
358                    Ok(())
359                } else {
360                    Err(format!(
361                        "Kingman violation: W_q={wq:.4}, expected={expected:.4}, error={relative_error:.4}"
362                    ))
363                }
364            }
365            _ => Err("Missing required variables for Kingman validation".to_string()),
366        }
367    }
368}
369
370// =============================================================================
371// Square Root Law for Safety Stock
372// =============================================================================
373
374/// The Square Root Law for safety stock scaling.
375///
376/// Safety stock scales with the square root of demand, not linearly:
377/// `I_safety = z × σ_D × √L`
378///
379/// where:
380/// - z: Service level z-score (e.g., 1.96 for 95% service)
381/// - `σ_D`: Standard deviation of demand per period
382/// - L: Lead time in periods
383///
384/// This means pooling inventory reduces total safety stock requirements.
385#[derive(Debug, Clone)]
386pub struct SquareRootLaw {
387    latex: String,
388    description: String,
389}
390
391impl SquareRootLaw {
392    /// Create a new Square Root Law equation.
393    #[must_use]
394    pub fn new() -> Self {
395        Self {
396            latex: r"I_{safety} = z \cdot \sigma_D \cdot \sqrt{L}".to_string(),
397            description: "The Square Root Law shows that safety stock requirements scale with \
398                         the square root of demand or lead time, not linearly."
399                .to_string(),
400        }
401    }
402
403    /// Calculate required safety stock.
404    ///
405    /// # Arguments
406    /// - `demand_std`: Standard deviation of demand per period (`σ_D`)
407    /// - `lead_time`: Lead time in periods (L)
408    /// - `z_score`: Service level z-score (z)
409    ///
410    /// # Returns
411    /// Required safety stock units.
412    #[must_use]
413    pub fn safety_stock(&self, demand_std: f64, lead_time: f64, z_score: f64) -> f64 {
414        z_score * demand_std * lead_time.sqrt()
415    }
416
417    /// Calculate safety stock reduction from pooling.
418    ///
419    /// When combining n locations with equal demand variance, the total
420    /// safety stock is reduced by a factor of √n.
421    #[must_use]
422    pub fn pooling_reduction(&self, num_locations: usize) -> f64 {
423        if num_locations == 0 {
424            return 0.0;
425        }
426        1.0 / (num_locations as f64).sqrt()
427    }
428
429    /// Common z-scores for service levels.
430    #[must_use]
431    pub fn z_score_for_service_level(service_level: f64) -> f64 {
432        match service_level {
433            x if (x - 0.90).abs() < 0.001 => 1.28,
434            x if (x - 0.95).abs() < 0.001 => 1.65,
435            x if (x - 0.99).abs() < 0.001 => 2.33,
436            x if (x - 0.999).abs() < 0.001 => 3.09,
437            _ => {
438                // Approximation using inverse normal
439                // For 95% confidence interval (2-sided), use 1.96
440                1.96
441            }
442        }
443    }
444}
445
446impl Default for SquareRootLaw {
447    fn default() -> Self {
448        Self::new()
449    }
450}
451
452impl GoverningEquation for SquareRootLaw {
453    fn latex(&self) -> &str {
454        &self.latex
455    }
456
457    fn class(&self) -> EquationClass {
458        EquationClass::Inventory
459    }
460
461    fn citation(&self) -> Citation {
462        Citation::new(&["Eppen, G.D."], "Management Science", 1979)
463            .with_title(
464                "Effects of Centralization on Expected Costs in a Multi-Location Newsboy Problem",
465            )
466            .with_doi("10.1287/mnsc.25.5.498")
467    }
468
469    fn variables(&self) -> Vec<EquationVariable> {
470        vec![
471            EquationVariable::new("I_safety", "safety_stock", "units")
472                .with_description("Required safety stock quantity"),
473            EquationVariable::new("z", "z_score", "dimensionless")
474                .with_description("Service level z-score from normal distribution"),
475            EquationVariable::new("σ_D", "demand_std", "units/period")
476                .with_description("Standard deviation of demand per period"),
477            EquationVariable::new("L", "lead_time", "periods")
478                .with_description("Lead time in periods"),
479        ]
480    }
481
482    fn description(&self) -> &str {
483        &self.description
484    }
485
486    fn name(&self) -> &'static str {
487        "Square Root Law"
488    }
489
490    fn validate_consistency(&self, values: &[(&str, f64)], tolerance: f64) -> Result<(), String> {
491        let mut z = None;
492        let mut sigma_d = None;
493        let mut lead_time = None;
494        let mut i_safety = None;
495
496        for (name, value) in values {
497            match *name {
498                "z" | "z_score" => z = Some(*value),
499                "σ_D" | "demand_std" => sigma_d = Some(*value),
500                "L" | "lead_time" => lead_time = Some(*value),
501                "I_safety" | "safety_stock" => i_safety = Some(*value),
502                _ => {}
503            }
504        }
505
506        match (z, sigma_d, lead_time, i_safety) {
507            (Some(z_val), Some(sd), Some(lt), Some(is)) => {
508                let expected = self.safety_stock(sd, lt, z_val);
509                let relative_error = if expected.abs() > f64::EPSILON {
510                    (is - expected).abs() / expected
511                } else {
512                    0.0
513                };
514
515                if relative_error <= tolerance {
516                    Ok(())
517                } else {
518                    Err(format!(
519                        "Square Root Law violation: I_safety={is:.4}, expected={expected:.4}"
520                    ))
521                }
522            }
523            _ => Err("Missing required variables for Square Root Law validation".to_string()),
524        }
525    }
526}
527
528// =============================================================================
529// Bullwhip Effect
530// =============================================================================
531
532/// The Bullwhip Effect: Variance amplification in supply chains.
533///
534/// Demand variance amplifies as you move upstream in a supply chain:
535/// `Var(Orders) / Var(Demand) ≥ 1`
536///
537/// The amplification factor depends on lead time, order batching,
538/// price fluctuations, and demand signal processing.
539#[derive(Debug, Clone)]
540pub struct BullwhipEffect {
541    latex: String,
542    description: String,
543}
544
545impl BullwhipEffect {
546    /// Create a new Bullwhip Effect equation.
547    #[must_use]
548    pub fn new() -> Self {
549        Self {
550            latex: r"\frac{Var(Orders)}{Var(Demand)} \geq 1 + \frac{2L}{p} + \frac{2L^2}{p^2}"
551                .to_string(),
552            description: "The Bullwhip Effect describes how demand variance amplifies upstream \
553                         in supply chains due to demand signal processing."
554                .to_string(),
555        }
556    }
557
558    /// Calculate the minimum amplification factor for order-up-to policy.
559    ///
560    /// # Arguments
561    /// - `lead_time`: Lead time in periods (L)
562    /// - `review_period`: Review period (p)
563    ///
564    /// # Returns
565    /// Minimum variance amplification factor.
566    #[must_use]
567    pub fn amplification_factor(&self, lead_time: f64, review_period: f64) -> f64 {
568        if review_period <= 0.0 {
569            return f64::INFINITY;
570        }
571        let ratio = lead_time / review_period;
572        1.0 + 2.0 * ratio + 2.0 * ratio * ratio
573    }
574
575    /// Calculate total variance amplification across n echelons.
576    #[must_use]
577    pub fn multi_echelon_amplification(&self, lead_times: &[f64], review_periods: &[f64]) -> f64 {
578        if lead_times.len() != review_periods.len() {
579            return f64::NAN;
580        }
581
582        lead_times
583            .iter()
584            .zip(review_periods.iter())
585            .map(|(lt, rp)| self.amplification_factor(*lt, *rp))
586            .product()
587    }
588}
589
590impl Default for BullwhipEffect {
591    fn default() -> Self {
592        Self::new()
593    }
594}
595
596impl GoverningEquation for BullwhipEffect {
597    fn latex(&self) -> &str {
598        &self.latex
599    }
600
601    fn class(&self) -> EquationClass {
602        EquationClass::Inventory
603    }
604
605    fn citation(&self) -> Citation {
606        Citation::new(
607            &["Lee, H.L.", "Padmanabhan, V.", "Whang, S."],
608            "Management Science",
609            1997,
610        )
611        .with_title("The Bullwhip Effect in Supply Chains")
612        .with_doi("10.1287/mnsc.43.4.546")
613    }
614
615    fn variables(&self) -> Vec<EquationVariable> {
616        vec![
617            EquationVariable::new("L", "lead_time", "periods")
618                .with_description("Lead time between ordering and receiving"),
619            EquationVariable::new("p", "review_period", "periods")
620                .with_description("Time between inventory reviews"),
621            EquationVariable::new("Var(Orders)", "order_variance", "units²")
622                .with_description("Variance of orders placed"),
623            EquationVariable::new("Var(Demand)", "demand_variance", "units²")
624                .with_description("Variance of end-customer demand"),
625        ]
626    }
627
628    fn description(&self) -> &str {
629        &self.description
630    }
631
632    fn name(&self) -> &'static str {
633        "Bullwhip Effect"
634    }
635
636    fn validate_consistency(&self, values: &[(&str, f64)], tolerance: f64) -> Result<(), String> {
637        let mut lead_time = None;
638        let mut review_period = None;
639        let mut order_var = None;
640        let mut demand_var = None;
641
642        for (name, value) in values {
643            match *name {
644                "L" | "lead_time" => lead_time = Some(*value),
645                "p" | "review_period" => review_period = Some(*value),
646                "Var(Orders)" | "order_variance" => order_var = Some(*value),
647                "Var(Demand)" | "demand_variance" => demand_var = Some(*value),
648                _ => {}
649            }
650        }
651
652        match (lead_time, review_period, order_var, demand_var) {
653            (Some(lt), Some(rp), Some(ov), Some(dv)) => {
654                let min_factor = self.amplification_factor(lt, rp);
655                let observed_factor = if dv > f64::EPSILON { ov / dv } else { 0.0 };
656
657                // Allow some tolerance below minimum (measurement noise)
658                if observed_factor >= min_factor * (1.0 - tolerance) {
659                    Ok(())
660                } else {
661                    Err(format!(
662                        "Bullwhip violation: observed amplification {observed_factor:.4} < minimum {min_factor:.4}"
663                    ))
664                }
665            }
666            _ => Err("Missing required variables for Bullwhip validation".to_string()),
667        }
668    }
669}
670
671#[cfg(test)]
672mod tests {
673    use super::*;
674
675    // =========================================================================
676    // Little's Law Tests
677    // =========================================================================
678
679    #[test]
680    fn test_littles_law_basic() {
681        let law = LittlesLaw::new();
682
683        // L = λW: If λ=5 and W=2, then L=10
684        let result = law.evaluate(5.0, 2.0);
685        assert!((result - 10.0).abs() < f64::EPSILON);
686    }
687
688    #[test]
689    fn test_littles_law_validation_pass() {
690        let law = LittlesLaw::new();
691        let result = law.validate(10.0, 5.0, 2.0, 0.01);
692        assert!(result.is_ok());
693    }
694
695    #[test]
696    fn test_littles_law_validation_fail() {
697        let law = LittlesLaw::new();
698        let result = law.validate(15.0, 5.0, 2.0, 0.01);
699        assert!(result.is_err());
700    }
701
702    #[test]
703    fn test_littles_law_solvers() {
704        let law = LittlesLaw::new();
705
706        assert!((law.solve_l(5.0, 2.0) - 10.0).abs() < f64::EPSILON);
707        assert!((law.solve_lambda(10.0, 2.0) - 5.0).abs() < f64::EPSILON);
708        assert!((law.solve_w(10.0, 5.0) - 2.0).abs() < f64::EPSILON);
709    }
710
711    #[test]
712    fn test_littles_law_has_citation() {
713        let law = LittlesLaw::new();
714        let citation = law.citation();
715        assert_eq!(citation.year, 1961);
716        assert!(!citation.authors.is_empty());
717    }
718
719    #[test]
720    fn test_littles_law_has_variables() {
721        let law = LittlesLaw::new();
722        let vars = law.variables();
723        assert!(vars.len() >= 3);
724    }
725
726    // =========================================================================
727    // Kingman's Formula Tests
728    // =========================================================================
729
730    #[test]
731    fn test_kingman_basic() {
732        let formula = KingmanFormula::new();
733
734        // At 50% utilization with CV=1, wait = (0.5/0.5) * 1 * t_s = t_s
735        let wait = formula.expected_wait_time(0.5, 1.0, 1.0, 1.0);
736        assert!((wait - 1.0).abs() < f64::EPSILON);
737    }
738
739    #[test]
740    fn test_kingman_hockey_stick() {
741        let formula = KingmanFormula::new();
742
743        let wait_50 = formula.expected_wait_time(0.5, 1.0, 1.0, 1.0);
744        let wait_95 = formula.expected_wait_time(0.95, 1.0, 1.0, 1.0);
745
746        // At 95% utilization, wait should be much higher than at 50%
747        assert!(wait_95 > wait_50 * 10.0);
748    }
749
750    #[test]
751    fn test_kingman_100_percent_utilization() {
752        let formula = KingmanFormula::new();
753        let wait = formula.expected_wait_time(1.0, 1.0, 1.0, 1.0);
754        assert!(wait.is_infinite());
755    }
756
757    #[test]
758    fn test_kingman_has_citation() {
759        let formula = KingmanFormula::new();
760        let citation = formula.citation();
761        assert_eq!(citation.year, 1961);
762    }
763
764    // =========================================================================
765    // Square Root Law Tests
766    // =========================================================================
767
768    #[test]
769    fn test_square_root_law_basic() {
770        let law = SquareRootLaw::new();
771
772        // z=1.96, σ_D=100, L=1 => I = 1.96 * 100 * 1 = 196
773        let stock = law.safety_stock(100.0, 1.0, 1.96);
774        assert!((stock - 196.0).abs() < f64::EPSILON);
775    }
776
777    #[test]
778    fn test_square_root_law_scaling() {
779        let law = SquareRootLaw::new();
780
781        // demand_std scaling is linear (not the square root property)
782        let _stock_100 = law.safety_stock(100.0, 1.0, 1.96);
783        let _stock_400 = law.safety_stock(400.0, 1.0, 1.96);
784
785        // The sqrt applies to lead time - this is the key property:
786        // If lead_time quadruples (1→4), safety stock doubles (√4 = 2)
787        let stock_l1 = law.safety_stock(100.0, 1.0, 1.96);
788        let stock_l4 = law.safety_stock(100.0, 4.0, 1.96);
789
790        let lt_ratio = stock_l4 / stock_l1;
791        assert!((lt_ratio - 2.0).abs() < 0.01);
792    }
793
794    #[test]
795    fn test_square_root_law_pooling() {
796        let law = SquareRootLaw::new();
797
798        // Pooling 4 locations should reduce safety stock by factor of 2
799        let reduction = law.pooling_reduction(4);
800        assert!((reduction - 0.5).abs() < f64::EPSILON);
801    }
802
803    // =========================================================================
804    // Bullwhip Effect Tests
805    // =========================================================================
806
807    #[test]
808    fn test_bullwhip_basic() {
809        let effect = BullwhipEffect::new();
810
811        // With L=1, p=1: factor = 1 + 2*1 + 2*1 = 5
812        let factor = effect.amplification_factor(1.0, 1.0);
813        assert!((factor - 5.0).abs() < f64::EPSILON);
814    }
815
816    #[test]
817    fn test_bullwhip_multi_echelon() {
818        let effect = BullwhipEffect::new();
819
820        // Two echelons, each with L=1, p=1 => 5 * 5 = 25
821        let factor = effect.multi_echelon_amplification(&[1.0, 1.0], &[1.0, 1.0]);
822        assert!((factor - 25.0).abs() < f64::EPSILON);
823    }
824
825    #[test]
826    fn test_bullwhip_has_citation() {
827        let effect = BullwhipEffect::new();
828        let citation = effect.citation();
829        assert_eq!(citation.year, 1997);
830        assert!(citation.authors.len() >= 3);
831    }
832
833    // =========================================================================
834    // GoverningEquation Trait Tests (for coverage)
835    // =========================================================================
836
837    #[test]
838    fn test_littles_law_trait_methods() {
839        let law = LittlesLaw::new();
840
841        // Test latex
842        assert!(law.latex().contains("lambda"));
843
844        // Test class
845        assert_eq!(law.class(), EquationClass::Queueing);
846
847        // Test name
848        assert_eq!(law.name(), "Little's Law");
849
850        // Test description
851        assert!(!law.description().is_empty());
852    }
853
854    #[test]
855    fn test_littles_law_validate_consistency() {
856        let law = LittlesLaw::new();
857
858        // Valid consistency
859        let values = vec![("L", 10.0), ("lambda", 5.0), ("W", 2.0)];
860        assert!(law.validate_consistency(&values, 0.01).is_ok());
861
862        // Alternative names
863        let values2 = vec![("wip", 10.0), ("arrival_rate", 5.0), ("cycle_time", 2.0)];
864        assert!(law.validate_consistency(&values2, 0.01).is_ok());
865
866        // Missing variables
867        let incomplete = vec![("L", 10.0), ("lambda", 5.0)];
868        assert!(law.validate_consistency(&incomplete, 0.01).is_err());
869    }
870
871    #[test]
872    fn test_littles_law_default() {
873        let law = LittlesLaw::default();
874        assert_eq!(law.name(), "Little's Law");
875    }
876
877    #[test]
878    fn test_littles_law_solve_edge_cases() {
879        let law = LittlesLaw::new();
880
881        // Division by zero handling
882        assert!(law.solve_lambda(10.0, 0.0).is_infinite());
883        assert!(law.solve_w(10.0, 0.0).is_infinite());
884    }
885
886    #[test]
887    fn test_kingman_trait_methods() {
888        let formula = KingmanFormula::new();
889
890        assert!(formula.latex().contains("rho"));
891        assert_eq!(formula.class(), EquationClass::Queueing);
892        assert_eq!(formula.name(), "Kingman's VUT Formula");
893        assert!(!formula.description().is_empty());
894    }
895
896    #[test]
897    fn test_kingman_default() {
898        let formula = KingmanFormula::default();
899        assert_eq!(formula.name(), "Kingman's VUT Formula");
900    }
901
902    #[test]
903    fn test_kingman_variables() {
904        let formula = KingmanFormula::new();
905        let vars = formula.variables();
906        assert!(vars.len() >= 4);
907    }
908
909    #[test]
910    fn test_kingman_validate_consistency() {
911        let formula = KingmanFormula::new();
912
913        // At 50% utilization, CV=1, t_s=1: wait = 1.0
914        let values = vec![
915            ("rho", 0.5),
916            ("c_a", 1.0),
917            ("c_s", 1.0),
918            ("t_s", 1.0),
919            ("W_q", 1.0),
920        ];
921        assert!(formula.validate_consistency(&values, 0.1).is_ok());
922
923        // Missing variables
924        let incomplete = vec![("rho", 0.5)];
925        assert!(formula.validate_consistency(&incomplete, 0.1).is_err());
926    }
927
928    #[test]
929    fn test_kingman_utilization_and_variability() {
930        let formula = KingmanFormula::new();
931
932        // Utilization factor
933        assert_eq!(formula.utilization_factor(0.5), 1.0);
934        assert!(formula.utilization_factor(0.9) > 1.0);
935
936        // Variability factor
937        assert_eq!(formula.variability_factor(1.0, 1.0), 1.0);
938        assert!(formula.variability_factor(2.0, 2.0) > 1.0);
939    }
940
941    #[test]
942    fn test_square_root_law_trait_methods() {
943        let law = SquareRootLaw::new();
944
945        assert!(law.latex().contains("sqrt"));
946        assert_eq!(law.class(), EquationClass::Inventory);
947        assert_eq!(law.name(), "Square Root Law");
948        assert!(!law.description().is_empty());
949    }
950
951    #[test]
952    fn test_square_root_law_default() {
953        let law = SquareRootLaw::default();
954        assert_eq!(law.name(), "Square Root Law");
955    }
956
957    #[test]
958    fn test_square_root_law_variables() {
959        let law = SquareRootLaw::new();
960        let vars = law.variables();
961        assert!(!vars.is_empty());
962    }
963
964    #[test]
965    fn test_square_root_law_citation() {
966        let law = SquareRootLaw::new();
967        let citation = law.citation();
968        assert_eq!(citation.year, 1979); // Eppen 1979
969    }
970
971    #[test]
972    fn test_square_root_law_validate_consistency() {
973        let law = SquareRootLaw::new();
974
975        // z=1.96, σ_D=100, L=1 => I_safety = 1.96 * 100 * sqrt(1) = 196
976        let values = vec![
977            ("demand_std", 100.0),
978            ("lead_time", 1.0),
979            ("z_score", 1.96),
980            ("I_safety", 196.0),
981        ];
982        assert!(law.validate_consistency(&values, 0.01).is_ok());
983    }
984
985    #[test]
986    fn test_square_root_law_z_score() {
987        // Test z_score_for_service_level - specific values
988        let z_90 = SquareRootLaw::z_score_for_service_level(0.90);
989        assert!((z_90 - 1.28).abs() < 0.01);
990
991        let z_95 = SquareRootLaw::z_score_for_service_level(0.95);
992        assert!((z_95 - 1.65).abs() < 0.01);
993
994        let z_99 = SquareRootLaw::z_score_for_service_level(0.99);
995        assert!((z_99 - 2.33).abs() < 0.01);
996
997        let z_999 = SquareRootLaw::z_score_for_service_level(0.999);
998        assert!((z_999 - 3.09).abs() < 0.01);
999
1000        // Unknown values default to 1.96
1001        let z_other = SquareRootLaw::z_score_for_service_level(0.50);
1002        assert!((z_other - 1.96).abs() < 0.01);
1003    }
1004
1005    #[test]
1006    fn test_bullwhip_trait_methods() {
1007        let effect = BullwhipEffect::new();
1008
1009        assert!(effect.latex().contains("L"));
1010        assert_eq!(effect.class(), EquationClass::Inventory);
1011        assert_eq!(effect.name(), "Bullwhip Effect");
1012        assert!(!effect.description().is_empty());
1013    }
1014
1015    #[test]
1016    fn test_bullwhip_default() {
1017        let effect = BullwhipEffect::default();
1018        assert_eq!(effect.name(), "Bullwhip Effect");
1019    }
1020
1021    #[test]
1022    fn test_bullwhip_variables() {
1023        let effect = BullwhipEffect::new();
1024        let vars = effect.variables();
1025        assert!(!vars.is_empty());
1026    }
1027
1028    #[test]
1029    fn test_bullwhip_validate_consistency() {
1030        let effect = BullwhipEffect::new();
1031
1032        // L=1, p=1 => factor = 5, so Var(Orders)/Var(Demand) >= 5
1033        let values = vec![
1034            ("lead_time", 1.0),
1035            ("review_period", 1.0),
1036            ("order_variance", 50.0),
1037            ("demand_variance", 10.0),
1038        ];
1039        // 50/10 = 5, which equals the minimum amplification factor
1040        assert!(effect.validate_consistency(&values, 0.1).is_ok());
1041    }
1042
1043    #[test]
1044    fn test_bullwhip_empty_echelons() {
1045        let effect = BullwhipEffect::new();
1046
1047        // Empty arrays should return 1.0
1048        let factor = effect.multi_echelon_amplification(&[], &[]);
1049        assert!((factor - 1.0).abs() < f64::EPSILON);
1050    }
1051}