datasynth_core/distributions/
weibull.rs1use rand::prelude::*;
9use rand_chacha::ChaCha8Rng;
10use rand_distr::{Distribution, Weibull};
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct WeibullConfig {
16 pub shape: f64,
21 pub scale: f64,
24 #[serde(default)]
26 pub min_value: f64,
27 #[serde(default)]
29 pub max_value: Option<f64>,
30 #[serde(default)]
32 pub round_to_integer: bool,
33}
34
35impl Default for WeibullConfig {
36 fn default() -> Self {
37 Self {
38 shape: 1.5, scale: 30.0, min_value: 0.0,
41 max_value: None,
42 round_to_integer: false,
43 }
44 }
45}
46
47impl WeibullConfig {
48 pub fn new(shape: f64, scale: f64) -> Self {
50 Self {
51 shape,
52 scale,
53 ..Default::default()
54 }
55 }
56
57 pub fn days_to_payment() -> Self {
59 Self {
60 shape: 1.8, scale: 35.0, min_value: 1.0, max_value: Some(120.0), round_to_integer: true,
65 }
66 }
67
68 pub fn early_payment() -> Self {
70 Self {
71 shape: 2.5, scale: 15.0, min_value: 1.0,
74 max_value: Some(30.0),
75 round_to_integer: true,
76 }
77 }
78
79 pub fn late_payment() -> Self {
81 Self {
82 shape: 0.8, scale: 60.0, min_value: 30.0, max_value: Some(180.0),
86 round_to_integer: true,
87 }
88 }
89
90 pub fn processing_time() -> Self {
92 Self {
93 shape: 2.0, scale: 3.0, min_value: 0.5, max_value: Some(24.0), round_to_integer: false,
98 }
99 }
100
101 pub fn asset_useful_life() -> Self {
103 Self {
104 shape: 3.5, scale: 7.0, min_value: 1.0,
107 max_value: Some(20.0),
108 round_to_integer: true,
109 }
110 }
111
112 pub fn validate(&self) -> Result<(), String> {
114 if self.shape <= 0.0 {
115 return Err("shape must be positive".to_string());
116 }
117 if self.scale <= 0.0 {
118 return Err("scale must be positive".to_string());
119 }
120 if let Some(max) = self.max_value {
121 if max <= self.min_value {
122 return Err("max_value must be greater than min_value".to_string());
123 }
124 }
125 Ok(())
126 }
127
128 pub fn expected_value(&self) -> f64 {
130 use std::f64::consts::PI;
131
132 let arg = 1.0 + 1.0 / self.shape;
135 let gamma_approx = (2.0 * PI / arg).sqrt() * (arg / std::f64::consts::E).powf(arg);
136 self.min_value + self.scale * gamma_approx
137 }
138
139 pub fn median(&self) -> f64 {
141 self.min_value + self.scale * (2.0_f64.ln()).powf(1.0 / self.shape)
142 }
143
144 pub fn mode(&self) -> Option<f64> {
147 if self.shape > 1.0 {
148 let mode = self.scale * ((self.shape - 1.0) / self.shape).powf(1.0 / self.shape);
149 Some(self.min_value + mode)
150 } else {
151 None
152 }
153 }
154}
155
156pub struct WeibullSampler {
158 rng: ChaCha8Rng,
159 config: WeibullConfig,
160 distribution: Weibull<f64>,
161}
162
163impl WeibullSampler {
164 pub fn new(seed: u64, config: WeibullConfig) -> Result<Self, String> {
166 config.validate()?;
167
168 let distribution = Weibull::new(config.scale, config.shape)
169 .map_err(|e| format!("Invalid Weibull distribution: {}", e))?;
170
171 Ok(Self {
172 rng: ChaCha8Rng::seed_from_u64(seed),
173 config,
174 distribution,
175 })
176 }
177
178 pub fn sample(&mut self) -> f64 {
180 let mut value = self.distribution.sample(&mut self.rng) + self.config.min_value;
181
182 if let Some(max) = self.config.max_value {
184 value = value.min(max);
185 }
186
187 if self.config.round_to_integer {
189 value = value.round();
190 }
191
192 value
193 }
194
195 pub fn sample_days(&mut self) -> u32 {
197 self.sample().max(0.0) as u32
198 }
199
200 pub fn sample_n(&mut self, n: usize) -> Vec<f64> {
202 (0..n).map(|_| self.sample()).collect()
203 }
204
205 pub fn sample_n_days(&mut self, n: usize) -> Vec<u32> {
207 (0..n).map(|_| self.sample_days()).collect()
208 }
209
210 pub fn reset(&mut self, seed: u64) {
212 self.rng = ChaCha8Rng::seed_from_u64(seed);
213 }
214
215 pub fn config(&self) -> &WeibullConfig {
217 &self.config
218 }
219}
220
221#[derive(Debug, Clone)]
223pub struct WeibullSurvivalResult {
224 pub time: f64,
226 pub survival_probability: f64,
228 pub hazard_rate: f64,
230}
231
232impl WeibullConfig {
233 pub fn survival_probability(&self, t: f64) -> f64 {
235 if t <= self.min_value {
236 return 1.0;
237 }
238 let adjusted_t = t - self.min_value;
239 (-((adjusted_t / self.scale).powf(self.shape))).exp()
240 }
241
242 pub fn hazard_rate(&self, t: f64) -> f64 {
244 if t <= self.min_value {
245 if self.shape < 1.0 {
246 return f64::INFINITY; }
248 return 0.0;
249 }
250 let adjusted_t = t - self.min_value;
251 (self.shape / self.scale) * (adjusted_t / self.scale).powf(self.shape - 1.0)
252 }
253
254 pub fn survival_analysis(&self, time_points: &[f64]) -> Vec<WeibullSurvivalResult> {
256 time_points
257 .iter()
258 .map(|&t| WeibullSurvivalResult {
259 time: t,
260 survival_probability: self.survival_probability(t),
261 hazard_rate: self.hazard_rate(t),
262 })
263 .collect()
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 #[test]
272 fn test_weibull_validation() {
273 let config = WeibullConfig::new(1.5, 30.0);
274 assert!(config.validate().is_ok());
275
276 let invalid_shape = WeibullConfig::new(-1.0, 30.0);
277 assert!(invalid_shape.validate().is_err());
278
279 let invalid_scale = WeibullConfig::new(1.5, 0.0);
280 assert!(invalid_scale.validate().is_err());
281 }
282
283 #[test]
284 fn test_weibull_sampling() {
285 let config = WeibullConfig::new(1.5, 30.0);
286 let mut sampler = WeibullSampler::new(42, config).unwrap();
287
288 let samples = sampler.sample_n(1000);
289 assert_eq!(samples.len(), 1000);
290
291 assert!(samples.iter().all(|&x| x >= 0.0));
293 }
294
295 #[test]
296 fn test_weibull_determinism() {
297 let config = WeibullConfig::new(1.5, 30.0);
298
299 let mut sampler1 = WeibullSampler::new(42, config.clone()).unwrap();
300 let mut sampler2 = WeibullSampler::new(42, config).unwrap();
301
302 for _ in 0..100 {
303 assert_eq!(sampler1.sample(), sampler2.sample());
304 }
305 }
306
307 #[test]
308 fn test_weibull_days_to_payment() {
309 let config = WeibullConfig::days_to_payment();
310 let mut sampler = WeibullSampler::new(42, config.clone()).unwrap();
311
312 let samples = sampler.sample_n_days(1000);
313
314 assert!(samples.iter().all(|&x| (1..=120).contains(&x)));
316
317 let median_approx = samples.iter().copied().sum::<u32>() as f64 / 1000.0;
319 assert!(median_approx > 20.0 && median_approx < 60.0);
320 }
321
322 #[test]
323 fn test_weibull_median() {
324 let config = WeibullConfig::new(2.0, 30.0);
325 let median = config.median();
326
327 assert!((median - 24.99).abs() < 0.1);
329 }
330
331 #[test]
332 fn test_weibull_mode() {
333 let config = WeibullConfig::new(2.0, 30.0);
334 let mode = config.mode();
335
336 assert!(mode.is_some());
338 assert!((mode.unwrap() - 21.21).abs() < 0.1);
339
340 let no_mode_config = WeibullConfig::new(0.8, 30.0);
342 assert!(no_mode_config.mode().is_none());
343 }
344
345 #[test]
346 fn test_weibull_survival() {
347 let config = WeibullConfig::new(2.0, 30.0);
348
349 assert!((config.survival_probability(0.0) - 1.0).abs() < 0.001);
351
352 let median = config.median();
354 assert!((config.survival_probability(median) - 0.5).abs() < 0.01);
355
356 assert!(config.survival_probability(1000.0) < 0.001);
358 }
359
360 #[test]
361 fn test_weibull_hazard_shapes() {
362 let config_dec = WeibullConfig::new(0.5, 30.0);
364 assert!(config_dec.hazard_rate(10.0) > config_dec.hazard_rate(50.0));
365
366 let config_inc = WeibullConfig::new(2.0, 30.0);
368 assert!(config_inc.hazard_rate(10.0) < config_inc.hazard_rate(50.0));
369 }
370
371 #[test]
372 fn test_weibull_presets() {
373 let early = WeibullConfig::early_payment();
374 assert!(early.validate().is_ok());
375
376 let late = WeibullConfig::late_payment();
377 assert!(late.validate().is_ok());
378
379 let processing = WeibullConfig::processing_time();
380 assert!(processing.validate().is_ok());
381
382 let asset = WeibullConfig::asset_useful_life();
383 assert!(asset.validate().is_ok());
384 }
385}