1use rand::prelude::*;
12use rand_chacha::ChaCha8Rng;
13use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
17#[serde(rename_all = "snake_case")]
18pub enum DriftType {
19 #[default]
21 Gradual,
22 Sudden,
24 Recurring,
26 Mixed,
28 Regime,
30 EconomicCycle,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "snake_case")]
37#[derive(Default)]
38pub enum RegimeChangeType {
39 Acquisition,
41 Divestiture,
43 PriceIncrease,
45 PriceDecrease,
47 ProductLaunch,
49 ProductDiscontinuation,
51 #[default]
53 PolicyChange,
54 CompetitorEntry,
56 Custom,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct RegimeEffect {
63 pub field: String,
65 pub multiplier: f64,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct RegimeChange {
72 pub period: u32,
74 pub change_type: RegimeChangeType,
76 #[serde(default)]
78 pub description: Option<String>,
79 #[serde(default)]
81 pub effects: Vec<RegimeEffect>,
82 #[serde(default)]
84 pub transition_periods: u32,
85}
86
87impl RegimeChange {
88 pub fn new(period: u32, change_type: RegimeChangeType) -> Self {
90 Self {
91 period,
92 change_type,
93 description: None,
94 effects: Vec::new(),
95 transition_periods: 0,
96 }
97 }
98
99 pub fn volume_multiplier(&self) -> f64 {
101 match self.change_type {
102 RegimeChangeType::Acquisition => 1.35,
103 RegimeChangeType::Divestiture => 0.70,
104 RegimeChangeType::PriceIncrease => 0.95,
105 RegimeChangeType::PriceDecrease => 1.10,
106 RegimeChangeType::ProductLaunch => 1.20,
107 RegimeChangeType::ProductDiscontinuation => 0.85,
108 RegimeChangeType::PolicyChange => 1.0,
109 RegimeChangeType::CompetitorEntry => 0.90,
110 RegimeChangeType::Custom => self
111 .effects
112 .iter()
113 .find(|e| e.field == "transaction_volume")
114 .map(|e| e.multiplier)
115 .unwrap_or(1.0),
116 }
117 }
118
119 pub fn amount_mean_multiplier(&self) -> f64 {
121 match self.change_type {
122 RegimeChangeType::Acquisition => 1.15,
123 RegimeChangeType::Divestiture => 0.90,
124 RegimeChangeType::PriceIncrease => 1.25,
125 RegimeChangeType::PriceDecrease => 0.80,
126 RegimeChangeType::ProductLaunch => 0.90, RegimeChangeType::ProductDiscontinuation => 1.10, RegimeChangeType::PolicyChange => 1.0,
129 RegimeChangeType::CompetitorEntry => 0.95,
130 RegimeChangeType::Custom => self
131 .effects
132 .iter()
133 .find(|e| e.field == "amount_mean")
134 .map(|e| e.multiplier)
135 .unwrap_or(1.0),
136 }
137 }
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct EconomicCycleConfig {
143 pub enabled: bool,
145 #[serde(default = "default_cycle_length")]
147 pub cycle_length: u32,
148 #[serde(default = "default_amplitude")]
150 pub amplitude: f64,
151 #[serde(default)]
153 pub phase_offset: u32,
154 #[serde(default)]
156 pub recession_periods: Vec<u32>,
157 #[serde(default = "default_recession_severity")]
159 pub recession_severity: f64,
160}
161
162fn default_cycle_length() -> u32 {
163 48 }
165
166fn default_amplitude() -> f64 {
167 0.15 }
169
170fn default_recession_severity() -> f64 {
171 0.75 }
173
174impl Default for EconomicCycleConfig {
175 fn default() -> Self {
176 Self {
177 enabled: false,
178 cycle_length: 48,
179 amplitude: 0.15,
180 phase_offset: 0,
181 recession_periods: Vec::new(),
182 recession_severity: 0.75,
183 }
184 }
185}
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
189#[serde(rename_all = "snake_case")]
190pub enum ParameterDriftType {
191 #[default]
193 Linear,
194 Exponential,
196 Logistic,
198 Step,
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct ParameterDrift {
205 pub parameter: String,
207 pub drift_type: ParameterDriftType,
209 pub initial_value: f64,
211 pub target_or_rate: f64,
213 #[serde(default)]
215 pub start_period: u32,
216 #[serde(default)]
218 pub end_period: Option<u32>,
219 #[serde(default = "default_steepness")]
221 pub steepness: f64,
222}
223
224fn default_steepness() -> f64 {
225 0.1
226}
227
228impl Default for ParameterDrift {
229 fn default() -> Self {
230 Self {
231 parameter: String::new(),
232 drift_type: ParameterDriftType::Linear,
233 initial_value: 1.0,
234 target_or_rate: 0.01,
235 start_period: 0,
236 end_period: None,
237 steepness: 0.1,
238 }
239 }
240}
241
242impl ParameterDrift {
243 pub fn value_at(&self, period: u32) -> f64 {
245 if period < self.start_period {
246 return self.initial_value;
247 }
248
249 let effective_period = period - self.start_period;
250
251 match self.drift_type {
252 ParameterDriftType::Linear => {
253 self.initial_value + self.target_or_rate * (effective_period as f64)
254 }
255 ParameterDriftType::Exponential => {
256 self.initial_value * (1.0 + self.target_or_rate).powi(effective_period as i32)
257 }
258 ParameterDriftType::Logistic => {
259 let end_period = self.end_period.unwrap_or(self.start_period + 24);
260 let midpoint = (self.start_period + end_period) as f64 / 2.0;
261 let t = period as f64;
262 let range = self.target_or_rate - self.initial_value;
263 self.initial_value + range / (1.0 + (-self.steepness * (t - midpoint)).exp())
264 }
265 ParameterDriftType::Step => {
266 if let Some(end) = self.end_period {
267 if period >= end {
268 return self.target_or_rate;
269 }
270 }
271 self.initial_value
272 }
273 }
274 }
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct DriftConfig {
280 pub enabled: bool,
282 pub amount_mean_drift: f64,
284 pub amount_variance_drift: f64,
286 pub anomaly_rate_drift: f64,
288 pub concept_drift_rate: f64,
290 pub sudden_drift_probability: f64,
292 pub sudden_drift_magnitude: f64,
294 pub seasonal_drift: bool,
296 pub drift_start_period: u32,
298 pub drift_type: DriftType,
300 #[serde(default)]
302 pub regime_changes: Vec<RegimeChange>,
303 #[serde(default)]
305 pub economic_cycle: EconomicCycleConfig,
306 #[serde(default)]
308 pub parameter_drifts: Vec<ParameterDrift>,
309}
310
311impl Default for DriftConfig {
312 fn default() -> Self {
313 Self {
314 enabled: false,
315 amount_mean_drift: 0.02,
316 amount_variance_drift: 0.0,
317 anomaly_rate_drift: 0.0,
318 concept_drift_rate: 0.01,
319 sudden_drift_probability: 0.0,
320 sudden_drift_magnitude: 2.0,
321 seasonal_drift: false,
322 drift_start_period: 0,
323 drift_type: DriftType::Gradual,
324 regime_changes: Vec::new(),
325 economic_cycle: EconomicCycleConfig::default(),
326 parameter_drifts: Vec::new(),
327 }
328 }
329}
330
331impl DriftConfig {
332 pub fn with_regime_changes(regime_changes: Vec<RegimeChange>) -> Self {
334 Self {
335 enabled: true,
336 drift_type: DriftType::Regime,
337 regime_changes,
338 ..Default::default()
339 }
340 }
341
342 pub fn with_economic_cycle(cycle_config: EconomicCycleConfig) -> Self {
344 Self {
345 enabled: true,
346 drift_type: DriftType::EconomicCycle,
347 economic_cycle: cycle_config,
348 ..Default::default()
349 }
350 }
351}
352
353#[derive(Debug, Clone, Default)]
355pub struct DriftAdjustments {
356 pub amount_mean_multiplier: f64,
358 pub amount_variance_multiplier: f64,
360 pub anomaly_rate_adjustment: f64,
362 pub concept_drift_factor: f64,
364 pub sudden_drift_occurred: bool,
366 pub seasonal_factor: f64,
368 pub volume_multiplier: f64,
370 pub economic_cycle_factor: f64,
372 pub in_recession: bool,
374 pub active_regime_changes: Vec<RegimeChangeType>,
376 pub parameter_values: std::collections::HashMap<String, f64>,
378}
379
380impl DriftAdjustments {
381 pub fn none() -> Self {
383 Self {
384 amount_mean_multiplier: 1.0,
385 amount_variance_multiplier: 1.0,
386 anomaly_rate_adjustment: 0.0,
387 concept_drift_factor: 0.0,
388 sudden_drift_occurred: false,
389 seasonal_factor: 1.0,
390 volume_multiplier: 1.0,
391 economic_cycle_factor: 1.0,
392 in_recession: false,
393 active_regime_changes: Vec::new(),
394 parameter_values: std::collections::HashMap::new(),
395 }
396 }
397
398 pub fn combined_amount_multiplier(&self) -> f64 {
400 self.amount_mean_multiplier * self.seasonal_factor * self.economic_cycle_factor
401 }
402
403 pub fn combined_volume_multiplier(&self) -> f64 {
405 self.volume_multiplier * self.seasonal_factor * self.economic_cycle_factor
406 }
407}
408
409#[derive(Clone)]
411pub struct DriftController {
412 config: DriftConfig,
413 rng: ChaCha8Rng,
414 sudden_drift_periods: Vec<u32>,
416 total_periods: u32,
418}
419
420impl DriftController {
421 pub fn new(config: DriftConfig, seed: u64, total_periods: u32) -> Self {
423 let mut controller = Self {
424 config,
425 rng: ChaCha8Rng::seed_from_u64(seed),
426 sudden_drift_periods: Vec::new(),
427 total_periods,
428 };
429
430 if controller.config.enabled
432 && (controller.config.drift_type == DriftType::Sudden
433 || controller.config.drift_type == DriftType::Mixed)
434 {
435 controller.precompute_sudden_drifts();
436 }
437
438 controller
439 }
440
441 fn precompute_sudden_drifts(&mut self) {
443 for period in 0..self.total_periods {
444 if period >= self.config.drift_start_period
445 && self.rng.random::<f64>() < self.config.sudden_drift_probability
446 {
447 self.sudden_drift_periods.push(period);
448 }
449 }
450 }
451
452 pub fn is_enabled(&self) -> bool {
454 self.config.enabled
455 }
456
457 pub fn compute_adjustments(&self, period: u32) -> DriftAdjustments {
459 if !self.config.enabled {
460 return DriftAdjustments::none();
461 }
462
463 if period < self.config.drift_start_period {
465 return DriftAdjustments::none();
466 }
467
468 let effective_period = period - self.config.drift_start_period;
469 let mut adjustments = DriftAdjustments::none();
470
471 match self.config.drift_type {
472 DriftType::Gradual => {
473 self.apply_gradual_drift(&mut adjustments, effective_period);
474 }
475 DriftType::Sudden => {
476 self.apply_sudden_drift(&mut adjustments, period);
477 }
478 DriftType::Recurring => {
479 self.apply_recurring_drift(&mut adjustments, effective_period);
480 }
481 DriftType::Mixed => {
482 self.apply_gradual_drift(&mut adjustments, effective_period);
484 self.apply_sudden_drift(&mut adjustments, period);
485 }
486 DriftType::Regime => {
487 self.apply_regime_drift(&mut adjustments, period);
488 }
489 DriftType::EconomicCycle => {
490 self.apply_economic_cycle(&mut adjustments, period);
491 }
492 }
493
494 if self.config.seasonal_drift {
496 adjustments.seasonal_factor = self.compute_seasonal_factor(period);
497 }
498
499 self.apply_parameter_drifts(&mut adjustments, period);
501
502 adjustments
503 }
504
505 fn apply_gradual_drift(&self, adjustments: &mut DriftAdjustments, effective_period: u32) {
507 let p = effective_period as f64;
508
509 adjustments.amount_mean_multiplier = (1.0 + self.config.amount_mean_drift).powf(p);
511
512 adjustments.amount_variance_multiplier = (1.0 + self.config.amount_variance_drift).powf(p);
513
514 adjustments.anomaly_rate_adjustment = self.config.anomaly_rate_drift * p;
516
517 adjustments.concept_drift_factor = (self.config.concept_drift_rate * p).min(1.0);
519 }
520
521 fn apply_sudden_drift(&self, adjustments: &mut DriftAdjustments, period: u32) {
523 let events_occurred: usize = self
525 .sudden_drift_periods
526 .iter()
527 .filter(|&&p| p <= period)
528 .count();
529
530 if events_occurred > 0 {
531 adjustments.sudden_drift_occurred = self.sudden_drift_periods.contains(&period);
532
533 let cumulative_magnitude = self
535 .config
536 .sudden_drift_magnitude
537 .powi(events_occurred as i32);
538
539 adjustments.amount_mean_multiplier *= cumulative_magnitude;
540 adjustments.amount_variance_multiplier *= cumulative_magnitude.sqrt();
541 }
543 }
544
545 fn apply_recurring_drift(&self, adjustments: &mut DriftAdjustments, effective_period: u32) {
547 let cycle_position = (effective_period % 12) as f64;
549 let cycle_radians = (cycle_position / 12.0) * 2.0 * std::f64::consts::PI;
550
551 let seasonal_amplitude = self.config.concept_drift_rate;
553 adjustments.amount_mean_multiplier = 1.0 + seasonal_amplitude * cycle_radians.sin();
554
555 adjustments.amount_variance_multiplier =
557 1.0 + (seasonal_amplitude * 0.5) * (cycle_radians + std::f64::consts::FRAC_PI_2).sin();
558 }
559
560 fn compute_seasonal_factor(&self, period: u32) -> f64 {
562 let month = period % 12;
564
565 match month {
567 0 | 1 => 0.85, 2 => 0.90, 3 | 4 => 0.95, 5 => 1.0, 6 | 7 => 0.95, 8 => 1.0, 9 => 1.10, 10 => 1.20, 11 => 1.30, _ => 1.0,
577 }
578 }
579
580 pub fn sudden_drift_periods(&self) -> &[u32] {
582 &self.sudden_drift_periods
583 }
584
585 pub fn config(&self) -> &DriftConfig {
587 &self.config
588 }
589
590 fn apply_regime_drift(&self, adjustments: &mut DriftAdjustments, period: u32) {
592 let mut volume_mult = 1.0;
593 let mut amount_mult = 1.0;
594
595 for regime_change in &self.config.regime_changes {
596 if period >= regime_change.period {
597 let periods_since = period - regime_change.period;
599 let transition_factor = if regime_change.transition_periods == 0 {
600 1.0
601 } else {
602 (periods_since as f64 / regime_change.transition_periods as f64).min(1.0)
603 };
604
605 let vol_change = regime_change.volume_multiplier() - 1.0;
607 let amt_change = regime_change.amount_mean_multiplier() - 1.0;
608
609 volume_mult *= 1.0 + vol_change * transition_factor;
610 amount_mult *= 1.0 + amt_change * transition_factor;
611
612 adjustments
613 .active_regime_changes
614 .push(regime_change.change_type);
615 }
616 }
617
618 adjustments.volume_multiplier = volume_mult;
619 adjustments.amount_mean_multiplier *= amount_mult;
620 }
621
622 fn apply_economic_cycle(&self, adjustments: &mut DriftAdjustments, period: u32) {
624 let cycle = &self.config.economic_cycle;
625 if !cycle.enabled {
626 return;
627 }
628
629 let adjusted_period = period + cycle.phase_offset;
631 let cycle_position =
632 (adjusted_period % cycle.cycle_length) as f64 / cycle.cycle_length as f64;
633
634 let cycle_radians = cycle_position * 2.0 * std::f64::consts::PI;
636 let cycle_factor = 1.0 + cycle.amplitude * cycle_radians.sin();
637
638 let in_recession = cycle.recession_periods.contains(&period);
640 adjustments.in_recession = in_recession;
641
642 let final_factor = if in_recession {
644 cycle_factor * cycle.recession_severity
645 } else {
646 cycle_factor
647 };
648
649 adjustments.economic_cycle_factor = final_factor;
650 adjustments.amount_mean_multiplier *= final_factor;
651 adjustments.volume_multiplier = final_factor;
652 }
653
654 fn apply_parameter_drifts(&self, adjustments: &mut DriftAdjustments, period: u32) {
656 for param_drift in &self.config.parameter_drifts {
657 let value = param_drift.value_at(period);
658 adjustments
659 .parameter_values
660 .insert(param_drift.parameter.clone(), value);
661 }
662 }
663
664 pub fn regime_changes_until(&self, period: u32) -> Vec<&RegimeChange> {
666 self.config
667 .regime_changes
668 .iter()
669 .filter(|rc| rc.period <= period)
670 .collect()
671 }
672}
673
674#[cfg(test)]
675#[allow(clippy::unwrap_used)]
676mod tests {
677 use super::*;
678
679 #[test]
680 fn test_no_drift_when_disabled() {
681 let config = DriftConfig::default();
682 let controller = DriftController::new(config, 42, 12);
683
684 let adjustments = controller.compute_adjustments(6);
685 assert!(!controller.is_enabled());
686 assert!((adjustments.amount_mean_multiplier - 1.0).abs() < 0.001);
687 assert!((adjustments.anomaly_rate_adjustment).abs() < 0.001);
688 }
689
690 #[test]
691 fn test_gradual_drift() {
692 let config = DriftConfig {
693 enabled: true,
694 amount_mean_drift: 0.02,
695 anomaly_rate_drift: 0.001,
696 drift_type: DriftType::Gradual,
697 ..Default::default()
698 };
699 let controller = DriftController::new(config, 42, 12);
700
701 let adj0 = controller.compute_adjustments(0);
703 assert!((adj0.amount_mean_multiplier - 1.0).abs() < 0.001);
704
705 let adj6 = controller.compute_adjustments(6);
707 assert!(adj6.amount_mean_multiplier > 1.10);
708 assert!(adj6.amount_mean_multiplier < 1.15);
709
710 let adj12 = controller.compute_adjustments(12);
712 assert!(adj12.amount_mean_multiplier > 1.20);
713 assert!(adj12.amount_mean_multiplier < 1.30);
714 }
715
716 #[test]
717 fn test_drift_start_period() {
718 let config = DriftConfig {
719 enabled: true,
720 amount_mean_drift: 0.02,
721 drift_start_period: 3,
722 drift_type: DriftType::Gradual,
723 ..Default::default()
724 };
725 let controller = DriftController::new(config, 42, 12);
726
727 let adj2 = controller.compute_adjustments(2);
729 assert!((adj2.amount_mean_multiplier - 1.0).abs() < 0.001);
730
731 let adj3 = controller.compute_adjustments(3);
733 assert!((adj3.amount_mean_multiplier - 1.0).abs() < 0.001);
734
735 let adj6 = controller.compute_adjustments(6);
737 assert!(adj6.amount_mean_multiplier > 1.0);
738 }
739
740 #[test]
741 fn test_seasonal_factor() {
742 let config = DriftConfig {
743 enabled: true,
744 seasonal_drift: true,
745 drift_type: DriftType::Gradual,
746 ..Default::default()
747 };
748 let controller = DriftController::new(config, 42, 12);
749
750 let adj_dec = controller.compute_adjustments(11);
752 assert!(adj_dec.seasonal_factor > 1.2);
753
754 let adj_jan = controller.compute_adjustments(0);
756 assert!(adj_jan.seasonal_factor < 0.9);
757 }
758
759 #[test]
760 fn test_sudden_drift_reproducibility() {
761 let config = DriftConfig {
762 enabled: true,
763 sudden_drift_probability: 0.5,
764 sudden_drift_magnitude: 1.5,
765 drift_type: DriftType::Sudden,
766 ..Default::default()
767 };
768
769 let controller1 = DriftController::new(config.clone(), 42, 12);
771 let controller2 = DriftController::new(config, 42, 12);
772
773 assert_eq!(
774 controller1.sudden_drift_periods(),
775 controller2.sudden_drift_periods()
776 );
777 }
778
779 #[test]
780 fn test_regime_change() {
781 let config = DriftConfig {
782 enabled: true,
783 drift_type: DriftType::Regime,
784 regime_changes: vec![RegimeChange::new(6, RegimeChangeType::Acquisition)],
785 ..Default::default()
786 };
787 let controller = DriftController::new(config, 42, 12);
788
789 let adj_before = controller.compute_adjustments(5);
791 assert!((adj_before.volume_multiplier - 1.0).abs() < 0.001);
792
793 let adj_after = controller.compute_adjustments(6);
795 assert!(adj_after.volume_multiplier > 1.3); assert!(adj_after.amount_mean_multiplier > 1.1); assert!(adj_after
798 .active_regime_changes
799 .contains(&RegimeChangeType::Acquisition));
800 }
801
802 #[test]
803 fn test_regime_change_gradual_transition() {
804 let config = DriftConfig {
805 enabled: true,
806 drift_type: DriftType::Regime,
807 regime_changes: vec![RegimeChange {
808 period: 6,
809 change_type: RegimeChangeType::PriceIncrease,
810 description: None,
811 effects: vec![],
812 transition_periods: 4, }],
814 ..Default::default()
815 };
816 let controller = DriftController::new(config, 42, 12);
817
818 let adj_start = controller.compute_adjustments(6);
820 let adj_mid = controller.compute_adjustments(8);
822 let adj_end = controller.compute_adjustments(10);
824
825 assert!(adj_start.amount_mean_multiplier < adj_mid.amount_mean_multiplier);
827 assert!(adj_mid.amount_mean_multiplier < adj_end.amount_mean_multiplier);
828 }
829
830 #[test]
831 fn test_economic_cycle() {
832 let config = DriftConfig {
833 enabled: true,
834 drift_type: DriftType::EconomicCycle,
835 economic_cycle: EconomicCycleConfig {
836 enabled: true,
837 cycle_length: 12, amplitude: 0.20,
839 phase_offset: 0,
840 recession_periods: vec![],
841 recession_severity: 0.75,
842 },
843 ..Default::default()
844 };
845 let controller = DriftController::new(config, 42, 24);
846
847 let adj_0 = controller.compute_adjustments(0);
849 let adj_3 = controller.compute_adjustments(3);
851 let adj_9 = controller.compute_adjustments(9);
853
854 assert!(adj_3.economic_cycle_factor > adj_0.economic_cycle_factor);
856 assert!(adj_9.economic_cycle_factor < adj_0.economic_cycle_factor);
858 }
859
860 #[test]
861 fn test_economic_cycle_recession() {
862 let config = DriftConfig {
863 enabled: true,
864 drift_type: DriftType::EconomicCycle,
865 economic_cycle: EconomicCycleConfig {
866 enabled: true,
867 cycle_length: 12,
868 amplitude: 0.10,
869 phase_offset: 0,
870 recession_periods: vec![6, 7, 8],
871 recession_severity: 0.70,
872 },
873 ..Default::default()
874 };
875 let controller = DriftController::new(config, 42, 12);
876
877 let adj_5 = controller.compute_adjustments(5);
879 assert!(!adj_5.in_recession);
880
881 let adj_7 = controller.compute_adjustments(7);
883 assert!(adj_7.in_recession);
884 assert!(adj_7.economic_cycle_factor < adj_5.economic_cycle_factor);
885 }
886
887 #[test]
888 fn test_parameter_drift_linear() {
889 let config = DriftConfig {
890 enabled: true,
891 drift_type: DriftType::Gradual,
892 parameter_drifts: vec![ParameterDrift {
893 parameter: "discount_rate".to_string(),
894 drift_type: ParameterDriftType::Linear,
895 initial_value: 0.02,
896 target_or_rate: 0.001, start_period: 0,
898 end_period: None,
899 steepness: 0.1,
900 }],
901 ..Default::default()
902 };
903 let controller = DriftController::new(config, 42, 12);
904
905 let adj_0 = controller.compute_adjustments(0);
906 let adj_6 = controller.compute_adjustments(6);
907
908 let rate_0 = adj_0.parameter_values.get("discount_rate").unwrap();
909 let rate_6 = adj_6.parameter_values.get("discount_rate").unwrap();
910
911 assert!((rate_0 - 0.02).abs() < 0.0001);
913 assert!((rate_6 - 0.026).abs() < 0.0001);
914 }
915
916 #[test]
917 fn test_parameter_drift_logistic() {
918 let config = DriftConfig {
919 enabled: true,
920 drift_type: DriftType::Gradual,
921 parameter_drifts: vec![ParameterDrift {
922 parameter: "market_share".to_string(),
923 drift_type: ParameterDriftType::Logistic,
924 initial_value: 0.10, target_or_rate: 0.40, start_period: 0,
927 end_period: Some(24), steepness: 0.3,
929 }],
930 ..Default::default()
931 };
932 let controller = DriftController::new(config, 42, 36);
933
934 let adj_0 = controller.compute_adjustments(0);
935 let adj_12 = controller.compute_adjustments(12);
936 let adj_24 = controller.compute_adjustments(24);
937
938 let share_0 = *adj_0.parameter_values.get("market_share").unwrap();
939 let share_12 = *adj_12.parameter_values.get("market_share").unwrap();
940 let share_24 = *adj_24.parameter_values.get("market_share").unwrap();
941
942 assert!(share_0 < 0.15); assert!(share_12 > 0.20 && share_12 < 0.30); assert!(share_24 > 0.35); }
947
948 #[test]
949 fn test_combined_drift_adjustments() {
950 let adj = DriftAdjustments {
951 amount_mean_multiplier: 1.2,
952 seasonal_factor: 1.1,
953 economic_cycle_factor: 0.9,
954 volume_multiplier: 1.3,
955 ..DriftAdjustments::none()
956 };
957
958 assert!((adj.combined_amount_multiplier() - 1.188).abs() < 0.001);
960
961 assert!((adj.combined_volume_multiplier() - 1.287).abs() < 0.001);
963 }
964
965 #[test]
966 fn test_regime_change_volume_multipliers() {
967 assert!(RegimeChange::new(0, RegimeChangeType::Acquisition).volume_multiplier() > 1.0);
968 assert!(RegimeChange::new(0, RegimeChangeType::Divestiture).volume_multiplier() < 1.0);
969 assert!(
970 (RegimeChange::new(0, RegimeChangeType::PolicyChange).volume_multiplier() - 1.0).abs()
971 < 0.001
972 );
973 }
974}