wb_cache/test/simulation/scriptwriter/model/
customer.rs

1use fieldx::fxstruct;
2
3use crate::test::simulation::scriptwriter::math::bisect;
4
5#[fxstruct(sync, new(off), default(off), builder(post_build), get(copy))]
6pub struct CustomerModel {
7    /// The initial number of customers.
8    initial_customers: f64,
9    /// The maximum number of customers the company can have.
10    market_capacity:   f64,
11    /// Where customer base growth reaches its peak.
12    inflection_point:  f64,
13    /// Company's "success" rate – how fast the customer base grows
14    #[fieldx(lock, set)]
15    growth_rate:       f64,
16    /// Precision of the bisection method
17    #[fieldx(default(0.0001))]
18    tolerance:         f64,
19    #[fieldx(private, set, builder(off))]
20    v:                 f64,
21    #[fieldx(private, set, builder(off))]
22    q:                 f64,
23}
24
25impl CustomerModel {
26    pub fn new(initial_customers: f64) -> Self {
27        Self::builder().initial_customers(initial_customers).build().unwrap()
28    }
29
30    fn post_build(mut self) -> Self {
31        self.set_v(self.calc_v());
32        self.set_q(self.calc_q());
33        self
34    }
35
36    fn calc_v(&self) -> f64 {
37        // Use bisection method to find v
38        let v0 = 0.0;
39        let v1 = 10.0;
40        let expected = self.inflection_point / self.market_capacity;
41
42        #[inline(always)]
43        fn coeff(v: f64) -> f64 {
44            (v / (1.0 + v)).powf(1.0 / v)
45        }
46
47        bisect(v0, v1, expected, self.tolerance, coeff).expect("failed to bisect Richards model asymmetry parameter v")
48    }
49
50    fn calc_q(&self) -> f64 {
51        (self.market_capacity / self.initial_customers).powf(self.v()) - 1.0
52    }
53
54    pub fn adjust_growth_rate(&self, expected_customers: f64, day: i32) {
55        let gr0 = 0.0;
56        let gr1 = 1.0;
57
58        self.set_growth_rate(gr1);
59
60        self.set_growth_rate(
61            bisect(gr0, gr1, expected_customers, self.tolerance, |gr| {
62                self.set_growth_rate(gr);
63                self.expected_customers(day)
64            })
65            .unwrap_or_else(|err| {
66                panic!("failed to bisect growth rate for expected customers {expected_customers} on day {day}: {err}")
67            }),
68        );
69    }
70
71    /// [Richards growth function](https://en.wikipedia.org/wiki/Generalised_logistic_function)
72    pub fn expected_customers(&self, t: i32) -> f64 {
73        let t = t as f64;
74        self.market_capacity / (1.0 + self.q() * (-self.growth_rate() * t).exp()).powf(1.0 / self.v)
75    }
76}
77
78#[cfg(test)]
79mod test {
80    use super::*;
81
82    #[test]
83    fn test_v_param() {
84        let richards = CustomerModel::builder()
85            .initial_customers(1.)
86            .market_capacity(1_000_000.)
87            .inflection_point(200_000.)
88            .tolerance(0.0001)
89            .growth_rate(0.05)
90            .build()
91            .unwrap();
92        let v = richards.v();
93        assert_eq!((v * 10000.0).round(), 6058.);
94        richards.adjust_growth_rate(500_000., 365);
95        assert_eq!((richards.growth_rate() * 10000.0).round(), 0247.);
96    }
97}