wb_cache/test/simulation/scriptwriter/model/
product.rs1use anyhow::Result;
2use fieldx::fxstruct;
3use rand_distr::Distribution;
4use rand_distr::LogNormal;
5use rand_distr::SkewNormal;
6use statrs::distribution::ContinuousCDF;
7use statrs::distribution::LogNormal as StatLN;
8
9use crate::test::simulation::scriptwriter::math::bisect;
10
11#[derive(Debug)]
19#[fxstruct(sync, builder(post_build), rc, get(copy))]
20pub struct ProductModel {
21 #[fieldx(default(20.0))]
23 top_price: f64,
24 #[fieldx(default(1.0))]
26 top_low: f64,
27 #[fieldx(default(100.0))]
29 top_high: f64,
30 #[fieldx(default(2.0/3.0))]
32 top_share: f64,
33 #[fieldx(default(0.0001))]
34 tolerance: f64,
35
36 #[fieldx(default(300.))]
39 pivot_price: f64,
40 #[fieldx(default(0.005))]
42 interest_loss_rate: f64,
43 #[fieldx(private, default(162.))]
48 interest_stability: f64,
49
50 #[fieldx(lock, private, set, builder(off))]
51 mu: f64,
52 #[fieldx(lock, private, set, builder(off))]
53 sigma: f64,
54}
55
56impl ProductModel {
57 fn post_build(self) -> Self {
58 self.find_sigma();
59 self
60 }
61
62 fn find_sigma(&self) {
63 bisect(0.01, 5.0, 0.0, self.tolerance(), |sigma| {
64 self.set_sigma(sigma);
65 self.probability_difference()
66 })
67 .expect("failed to bisect sigma parameter for price model");
68 }
69
70 fn probability_difference(&self) -> f64 {
71 let sigma = self.sigma();
72 let mu = self.top_price().ln() + sigma * sigma;
73 self.set_mu(mu);
74 let dist = StatLN::new(mu, sigma).expect("bad parameters for LogNormal distribution");
75 self.top_share() - (dist.cdf(self.top_high()) - dist.cdf(self.top_low()))
76 }
77
78 pub fn next_price(&self) -> f64 {
79 let dist = LogNormal::new(self.mu(), self.sigma()).expect("bad parameters for LogNormal distribution");
80 let mut rng = rand::rng();
81 dist.sample(&mut rng)
82 }
83
84 pub fn customer_interest(&self, price: f64) -> f64 {
85 if price < 0. {
86 panic!("Price cannot be negative");
87 }
88
89 let pivot = self.pivot_price();
98 let price = price - pivot / ((self.interest_stability() * (price - pivot)) / (pivot + price.powi(2))).exp();
100
101 0.5 - (self.interest_loss_rate() * price).atan() / std::f64::consts::PI
102 }
103
104 pub fn supply_distribution(
105 &self,
106 expected_mean: f64,
107 supplier_inaccuracy: f64,
108 supplier_tardiness: f64,
109 ) -> Result<SkewNormal<f64>> {
110 let scale = supplier_inaccuracy * expected_mean / 1.6448536;
111 let shape = (std::f64::consts::PI * (supplier_tardiness - 0.5)).tan();
112
113 SkewNormal::new(expected_mean, scale, shape)
114 .map_err(|_| anyhow::anyhow!("bad parameters for SkewNormal distribution"))
115 }
116}
117
118#[cfg(test)]
119mod test {
120 use super::*;
121
122 #[test]
123 fn test_product_price() {
124 let pp = ProductModel::builder()
125 .top_price(15.0)
126 .top_low(1.0)
127 .top_high(100.0)
128 .top_share(2.0 / 3.0)
129 .tolerance(0.000001)
130 .build()
131 .unwrap();
132 assert_eq!((pp.sigma() * 100000.).round(), 117844.);
133 assert_eq!((pp.mu() * 100000.).round(), 409676.);
134 }
135}