Skip to main content

datasynth_core/distributions/
drift.rs

1//! Temporal drift simulation for realistic data distribution evolution.
2//!
3//! Implements gradual, sudden, and seasonal drift patterns commonly observed
4//! in real-world enterprise data, useful for training drift detection models.
5//!
6//! Enhanced with:
7//! - Regime changes (structural breaks like acquisitions, policy changes)
8//! - Economic cycles (sinusoidal patterns with recessions)
9//! - Parameter drifts (gradual changes in distribution parameters)
10
11use rand::prelude::*;
12use rand_chacha::ChaCha8Rng;
13use serde::{Deserialize, Serialize};
14
15/// Types of temporal drift patterns.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
17#[serde(rename_all = "snake_case")]
18pub enum DriftType {
19    /// Gradual, continuous drift over time (like inflation).
20    #[default]
21    Gradual,
22    /// Sudden, point-in-time shifts (like policy changes).
23    Sudden,
24    /// Recurring patterns that cycle (like seasonal variations).
25    Recurring,
26    /// Combination of gradual background drift with occasional sudden shifts.
27    Mixed,
28    /// Regime-based changes with structural breaks.
29    Regime,
30    /// Economic cycle patterns.
31    EconomicCycle,
32}
33
34/// Type of regime change event.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "snake_case")]
37#[derive(Default)]
38pub enum RegimeChangeType {
39    /// Acquisition - sudden volume and amount increase
40    Acquisition,
41    /// Divestiture - sudden volume and amount decrease
42    Divestiture,
43    /// Price increase - amounts increase, volume may decrease
44    PriceIncrease,
45    /// Price decrease - amounts decrease, volume may increase
46    PriceDecrease,
47    /// New product launch - volume ramp-up
48    ProductLaunch,
49    /// Product discontinuation - volume ramp-down
50    ProductDiscontinuation,
51    /// Policy change - affects patterns without changing volumes
52    #[default]
53    PolicyChange,
54    /// Competitor entry - market disruption
55    CompetitorEntry,
56    /// Custom effect with specified multipliers
57    Custom,
58}
59
60/// Effect of a regime change on specific fields.
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct RegimeEffect {
63    /// Field being affected (e.g., "transaction_volume", "amount_mean")
64    pub field: String,
65    /// Multiplier to apply (1.0 = no change, 1.5 = 50% increase)
66    pub multiplier: f64,
67}
68
69/// A single regime change event.
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct RegimeChange {
72    /// Period when the change occurs (0-indexed)
73    pub period: u32,
74    /// Type of regime change
75    pub change_type: RegimeChangeType,
76    /// Description of the change
77    #[serde(default)]
78    pub description: Option<String>,
79    /// Custom effects (only used if change_type is Custom)
80    #[serde(default)]
81    pub effects: Vec<RegimeEffect>,
82    /// Transition duration in periods (0 = immediate, >0 = gradual)
83    #[serde(default)]
84    pub transition_periods: u32,
85}
86
87impl RegimeChange {
88    /// Create a new regime change.
89    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    /// Get the volume multiplier for this regime change.
100    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    /// Get the amount mean multiplier for this regime change.
120    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, // New products often cheaper
127            RegimeChangeType::ProductDiscontinuation => 1.10, // Remaining products higher-value
128            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/// Configuration for economic cycle patterns.
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct EconomicCycleConfig {
143    /// Enable economic cycle patterns
144    pub enabled: bool,
145    /// Cycle length in periods (e.g., 48 months for a 4-year cycle)
146    #[serde(default = "default_cycle_length")]
147    pub cycle_length: u32,
148    /// Amplitude of the cycle (0.0-1.0, affects the peak-to-trough ratio)
149    #[serde(default = "default_amplitude")]
150    pub amplitude: f64,
151    /// Phase offset in periods (shifts the cycle start)
152    #[serde(default)]
153    pub phase_offset: u32,
154    /// Recession periods (list of periods that are in recession)
155    #[serde(default)]
156    pub recession_periods: Vec<u32>,
157    /// Recession severity multiplier (0.0-1.0, lower = more severe)
158    #[serde(default = "default_recession_severity")]
159    pub recession_severity: f64,
160}
161
162fn default_cycle_length() -> u32 {
163    48 // 4-year business cycle
164}
165
166fn default_amplitude() -> f64 {
167    0.15 // 15% peak-to-trough variation
168}
169
170fn default_recession_severity() -> f64 {
171    0.75 // 25% reduction during recession
172}
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/// Types of parameter drift patterns.
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
189#[serde(rename_all = "snake_case")]
190pub enum ParameterDriftType {
191    /// Linear drift: parameter = initial + rate * period
192    #[default]
193    Linear,
194    /// Exponential drift: parameter = initial * (1 + rate)^period
195    Exponential,
196    /// Logistic drift: S-curve transition between start and end values
197    Logistic,
198    /// Step drift: sudden change at specified periods
199    Step,
200}
201
202/// Configuration for a parameter drift.
203#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct ParameterDrift {
205    /// Name of the parameter being drifted
206    pub parameter: String,
207    /// Type of drift pattern
208    pub drift_type: ParameterDriftType,
209    /// Initial value
210    pub initial_value: f64,
211    /// Final value (for logistic) or rate (for linear/exponential)
212    pub target_or_rate: f64,
213    /// Start period for the drift
214    #[serde(default)]
215    pub start_period: u32,
216    /// End period for the drift (for logistic), or ignored for linear/exponential
217    #[serde(default)]
218    pub end_period: Option<u32>,
219    /// Steepness for logistic curve (higher = sharper transition)
220    #[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    /// Calculate the parameter value at a given period.
244    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/// Configuration for temporal drift simulation.
278#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct DriftConfig {
280    /// Enable temporal drift simulation.
281    pub enabled: bool,
282    /// Amount mean drift per period (e.g., 0.02 = 2% shift per month).
283    pub amount_mean_drift: f64,
284    /// Amount variance drift per period.
285    pub amount_variance_drift: f64,
286    /// Anomaly rate drift per period.
287    pub anomaly_rate_drift: f64,
288    /// Concept drift rate (0.0-1.0).
289    pub concept_drift_rate: f64,
290    /// Probability of sudden drift in any period.
291    pub sudden_drift_probability: f64,
292    /// Magnitude of sudden drift events.
293    pub sudden_drift_magnitude: f64,
294    /// Enable seasonal drift patterns.
295    pub seasonal_drift: bool,
296    /// Period to start drift (0 = from beginning).
297    pub drift_start_period: u32,
298    /// Type of drift pattern.
299    pub drift_type: DriftType,
300    /// Regime changes (structural breaks)
301    #[serde(default)]
302    pub regime_changes: Vec<RegimeChange>,
303    /// Economic cycle configuration
304    #[serde(default)]
305    pub economic_cycle: EconomicCycleConfig,
306    /// Parameter drifts
307    #[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    /// Create a configuration with regime changes.
333    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    /// Create a configuration with economic cycle.
343    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/// Drift adjustments computed for a specific period.
354#[derive(Debug, Clone, Default)]
355pub struct DriftAdjustments {
356    /// Multiplier for amount mean (1.0 = no change).
357    pub amount_mean_multiplier: f64,
358    /// Multiplier for amount variance (1.0 = no change).
359    pub amount_variance_multiplier: f64,
360    /// Additive adjustment to anomaly rate.
361    pub anomaly_rate_adjustment: f64,
362    /// Overall concept drift factor (0.0-1.0).
363    pub concept_drift_factor: f64,
364    /// Whether a sudden drift event occurred.
365    pub sudden_drift_occurred: bool,
366    /// Seasonal factor (1.0 = baseline, varies by month).
367    pub seasonal_factor: f64,
368    /// Volume multiplier from regime changes (1.0 = no change).
369    pub volume_multiplier: f64,
370    /// Economic cycle factor (1.0 = neutral, varies with cycle).
371    pub economic_cycle_factor: f64,
372    /// Whether currently in a recession period.
373    pub in_recession: bool,
374    /// Active regime changes in this period.
375    pub active_regime_changes: Vec<RegimeChangeType>,
376    /// Parameter drift values for this period.
377    pub parameter_values: std::collections::HashMap<String, f64>,
378}
379
380impl DriftAdjustments {
381    /// No drift (identity adjustments).
382    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    /// Get the combined multiplier for transaction amounts.
399    pub fn combined_amount_multiplier(&self) -> f64 {
400        self.amount_mean_multiplier * self.seasonal_factor * self.economic_cycle_factor
401    }
402
403    /// Get the combined multiplier for transaction volume.
404    pub fn combined_volume_multiplier(&self) -> f64 {
405        self.volume_multiplier * self.seasonal_factor * self.economic_cycle_factor
406    }
407}
408
409/// Controller for computing and applying temporal drift.
410pub struct DriftController {
411    config: DriftConfig,
412    rng: ChaCha8Rng,
413    /// Track which periods had sudden drift events for reproducibility.
414    sudden_drift_periods: Vec<u32>,
415    /// Total periods in the simulation.
416    total_periods: u32,
417}
418
419impl DriftController {
420    /// Create a new drift controller with the given configuration.
421    pub fn new(config: DriftConfig, seed: u64, total_periods: u32) -> Self {
422        let mut controller = Self {
423            config,
424            rng: ChaCha8Rng::seed_from_u64(seed),
425            sudden_drift_periods: Vec::new(),
426            total_periods,
427        };
428
429        // Pre-compute sudden drift events for reproducibility
430        if controller.config.enabled
431            && (controller.config.drift_type == DriftType::Sudden
432                || controller.config.drift_type == DriftType::Mixed)
433        {
434            controller.precompute_sudden_drifts();
435        }
436
437        controller
438    }
439
440    /// Pre-compute which periods will have sudden drift events.
441    fn precompute_sudden_drifts(&mut self) {
442        for period in 0..self.total_periods {
443            if period >= self.config.drift_start_period
444                && self.rng.gen::<f64>() < self.config.sudden_drift_probability
445            {
446                self.sudden_drift_periods.push(period);
447            }
448        }
449    }
450
451    /// Check if drift is enabled.
452    pub fn is_enabled(&self) -> bool {
453        self.config.enabled
454    }
455
456    /// Compute drift adjustments for a specific period (0-indexed).
457    pub fn compute_adjustments(&self, period: u32) -> DriftAdjustments {
458        if !self.config.enabled {
459            return DriftAdjustments::none();
460        }
461
462        // No drift before start period
463        if period < self.config.drift_start_period {
464            return DriftAdjustments::none();
465        }
466
467        let effective_period = period - self.config.drift_start_period;
468        let mut adjustments = DriftAdjustments::none();
469
470        match self.config.drift_type {
471            DriftType::Gradual => {
472                self.apply_gradual_drift(&mut adjustments, effective_period);
473            }
474            DriftType::Sudden => {
475                self.apply_sudden_drift(&mut adjustments, period);
476            }
477            DriftType::Recurring => {
478                self.apply_recurring_drift(&mut adjustments, effective_period);
479            }
480            DriftType::Mixed => {
481                // Combine gradual background drift with sudden events
482                self.apply_gradual_drift(&mut adjustments, effective_period);
483                self.apply_sudden_drift(&mut adjustments, period);
484            }
485            DriftType::Regime => {
486                self.apply_regime_drift(&mut adjustments, period);
487            }
488            DriftType::EconomicCycle => {
489                self.apply_economic_cycle(&mut adjustments, period);
490            }
491        }
492
493        // Apply seasonal drift if enabled (additive to other drift)
494        if self.config.seasonal_drift {
495            adjustments.seasonal_factor = self.compute_seasonal_factor(period);
496        }
497
498        // Apply parameter drifts
499        self.apply_parameter_drifts(&mut adjustments, period);
500
501        adjustments
502    }
503
504    /// Apply gradual drift (compound growth model).
505    fn apply_gradual_drift(&self, adjustments: &mut DriftAdjustments, effective_period: u32) {
506        let p = effective_period as f64;
507
508        // Compound growth: (1 + rate)^period
509        adjustments.amount_mean_multiplier = (1.0 + self.config.amount_mean_drift).powf(p);
510
511        adjustments.amount_variance_multiplier = (1.0 + self.config.amount_variance_drift).powf(p);
512
513        // Linear accumulation for anomaly rate
514        adjustments.anomaly_rate_adjustment = self.config.anomaly_rate_drift * p;
515
516        // Concept drift accumulates but is bounded 0-1
517        adjustments.concept_drift_factor = (self.config.concept_drift_rate * p).min(1.0);
518    }
519
520    /// Apply sudden drift based on pre-computed events.
521    fn apply_sudden_drift(&self, adjustments: &mut DriftAdjustments, period: u32) {
522        // Count how many sudden events have occurred up to this period
523        let events_occurred: usize = self
524            .sudden_drift_periods
525            .iter()
526            .filter(|&&p| p <= period)
527            .count();
528
529        if events_occurred > 0 {
530            adjustments.sudden_drift_occurred = self.sudden_drift_periods.contains(&period);
531
532            // Each sudden event multiplies by the magnitude
533            let cumulative_magnitude = self
534                .config
535                .sudden_drift_magnitude
536                .powi(events_occurred as i32);
537
538            adjustments.amount_mean_multiplier *= cumulative_magnitude;
539            adjustments.amount_variance_multiplier *= cumulative_magnitude.sqrt();
540            // Variance grows slower
541        }
542    }
543
544    /// Apply recurring (seasonal) drift patterns.
545    fn apply_recurring_drift(&self, adjustments: &mut DriftAdjustments, effective_period: u32) {
546        // 12-month cycle for seasonality
547        let cycle_position = (effective_period % 12) as f64;
548        let cycle_radians = (cycle_position / 12.0) * 2.0 * std::f64::consts::PI;
549
550        // Sinusoidal pattern with configurable amplitude
551        let seasonal_amplitude = self.config.concept_drift_rate;
552        adjustments.amount_mean_multiplier = 1.0 + seasonal_amplitude * cycle_radians.sin();
553
554        // Phase-shifted variance pattern
555        adjustments.amount_variance_multiplier =
556            1.0 + (seasonal_amplitude * 0.5) * (cycle_radians + std::f64::consts::FRAC_PI_2).sin();
557    }
558
559    /// Compute seasonal factor based on period (month).
560    fn compute_seasonal_factor(&self, period: u32) -> f64 {
561        // Map period to month (0-11)
562        let month = period % 12;
563
564        // Q4 spike (Oct-Dec), Q1 dip (Jan-Feb)
565        match month {
566            0 | 1 => 0.85, // Jan-Feb: post-holiday slowdown
567            2 => 0.90,     // Mar: recovering
568            3 | 4 => 0.95, // Apr-May: Q2 start
569            5 => 1.0,      // Jun: mid-year
570            6 | 7 => 0.95, // Jul-Aug: summer slowdown
571            8 => 1.0,      // Sep: back to business
572            9 => 1.10,     // Oct: Q4 ramp-up
573            10 => 1.20,    // Nov: pre-holiday surge
574            11 => 1.30,    // Dec: year-end close
575            _ => 1.0,
576        }
577    }
578
579    /// Get the list of periods with sudden drift events.
580    pub fn sudden_drift_periods(&self) -> &[u32] {
581        &self.sudden_drift_periods
582    }
583
584    /// Get the configuration.
585    pub fn config(&self) -> &DriftConfig {
586        &self.config
587    }
588
589    /// Apply regime change drift.
590    fn apply_regime_drift(&self, adjustments: &mut DriftAdjustments, period: u32) {
591        let mut volume_mult = 1.0;
592        let mut amount_mult = 1.0;
593
594        for regime_change in &self.config.regime_changes {
595            if period >= regime_change.period {
596                // Calculate transition factor
597                let periods_since = period - regime_change.period;
598                let transition_factor = if regime_change.transition_periods == 0 {
599                    1.0
600                } else {
601                    (periods_since as f64 / regime_change.transition_periods as f64).min(1.0)
602                };
603
604                // Apply multipliers with transition
605                let vol_change = regime_change.volume_multiplier() - 1.0;
606                let amt_change = regime_change.amount_mean_multiplier() - 1.0;
607
608                volume_mult *= 1.0 + vol_change * transition_factor;
609                amount_mult *= 1.0 + amt_change * transition_factor;
610
611                adjustments
612                    .active_regime_changes
613                    .push(regime_change.change_type);
614            }
615        }
616
617        adjustments.volume_multiplier = volume_mult;
618        adjustments.amount_mean_multiplier *= amount_mult;
619    }
620
621    /// Apply economic cycle pattern.
622    fn apply_economic_cycle(&self, adjustments: &mut DriftAdjustments, period: u32) {
623        let cycle = &self.config.economic_cycle;
624        if !cycle.enabled {
625            return;
626        }
627
628        // Calculate position in cycle (0.0 to 1.0)
629        let adjusted_period = period + cycle.phase_offset;
630        let cycle_position =
631            (adjusted_period % cycle.cycle_length) as f64 / cycle.cycle_length as f64;
632
633        // Sinusoidal cycle: 1.0 + amplitude * sin(2*pi*position)
634        let cycle_radians = cycle_position * 2.0 * std::f64::consts::PI;
635        let cycle_factor = 1.0 + cycle.amplitude * cycle_radians.sin();
636
637        // Check for recession
638        let in_recession = cycle.recession_periods.contains(&period);
639        adjustments.in_recession = in_recession;
640
641        // Apply recession severity if in recession
642        let final_factor = if in_recession {
643            cycle_factor * cycle.recession_severity
644        } else {
645            cycle_factor
646        };
647
648        adjustments.economic_cycle_factor = final_factor;
649        adjustments.amount_mean_multiplier *= final_factor;
650        adjustments.volume_multiplier = final_factor;
651    }
652
653    /// Apply parameter drifts.
654    fn apply_parameter_drifts(&self, adjustments: &mut DriftAdjustments, period: u32) {
655        for param_drift in &self.config.parameter_drifts {
656            let value = param_drift.value_at(period);
657            adjustments
658                .parameter_values
659                .insert(param_drift.parameter.clone(), value);
660        }
661    }
662
663    /// Get regime changes that occurred up to a given period.
664    pub fn regime_changes_until(&self, period: u32) -> Vec<&RegimeChange> {
665        self.config
666            .regime_changes
667            .iter()
668            .filter(|rc| rc.period <= period)
669            .collect()
670    }
671}
672
673#[cfg(test)]
674mod tests {
675    use super::*;
676
677    #[test]
678    fn test_no_drift_when_disabled() {
679        let config = DriftConfig::default();
680        let controller = DriftController::new(config, 42, 12);
681
682        let adjustments = controller.compute_adjustments(6);
683        assert!(!controller.is_enabled());
684        assert!((adjustments.amount_mean_multiplier - 1.0).abs() < 0.001);
685        assert!((adjustments.anomaly_rate_adjustment).abs() < 0.001);
686    }
687
688    #[test]
689    fn test_gradual_drift() {
690        let config = DriftConfig {
691            enabled: true,
692            amount_mean_drift: 0.02,
693            anomaly_rate_drift: 0.001,
694            drift_type: DriftType::Gradual,
695            ..Default::default()
696        };
697        let controller = DriftController::new(config, 42, 12);
698
699        // Period 0: no drift yet
700        let adj0 = controller.compute_adjustments(0);
701        assert!((adj0.amount_mean_multiplier - 1.0).abs() < 0.001);
702
703        // Period 6: ~12.6% drift (1.02^6 ≈ 1.126)
704        let adj6 = controller.compute_adjustments(6);
705        assert!(adj6.amount_mean_multiplier > 1.10);
706        assert!(adj6.amount_mean_multiplier < 1.15);
707
708        // Period 12: ~26.8% drift (1.02^12 ≈ 1.268)
709        let adj12 = controller.compute_adjustments(12);
710        assert!(adj12.amount_mean_multiplier > 1.20);
711        assert!(adj12.amount_mean_multiplier < 1.30);
712    }
713
714    #[test]
715    fn test_drift_start_period() {
716        let config = DriftConfig {
717            enabled: true,
718            amount_mean_drift: 0.02,
719            drift_start_period: 3,
720            drift_type: DriftType::Gradual,
721            ..Default::default()
722        };
723        let controller = DriftController::new(config, 42, 12);
724
725        // Before drift start: no drift
726        let adj2 = controller.compute_adjustments(2);
727        assert!((adj2.amount_mean_multiplier - 1.0).abs() < 0.001);
728
729        // At drift start: no drift yet (effective_period = 0)
730        let adj3 = controller.compute_adjustments(3);
731        assert!((adj3.amount_mean_multiplier - 1.0).abs() < 0.001);
732
733        // After drift start: drift begins
734        let adj6 = controller.compute_adjustments(6);
735        assert!(adj6.amount_mean_multiplier > 1.0);
736    }
737
738    #[test]
739    fn test_seasonal_factor() {
740        let config = DriftConfig {
741            enabled: true,
742            seasonal_drift: true,
743            drift_type: DriftType::Gradual,
744            ..Default::default()
745        };
746        let controller = DriftController::new(config, 42, 12);
747
748        // December (month 11) should have highest seasonal factor
749        let adj_dec = controller.compute_adjustments(11);
750        assert!(adj_dec.seasonal_factor > 1.2);
751
752        // January (month 0) should have lower seasonal factor
753        let adj_jan = controller.compute_adjustments(0);
754        assert!(adj_jan.seasonal_factor < 0.9);
755    }
756
757    #[test]
758    fn test_sudden_drift_reproducibility() {
759        let config = DriftConfig {
760            enabled: true,
761            sudden_drift_probability: 0.5,
762            sudden_drift_magnitude: 1.5,
763            drift_type: DriftType::Sudden,
764            ..Default::default()
765        };
766
767        // Same seed should produce same sudden drift periods
768        let controller1 = DriftController::new(config.clone(), 42, 12);
769        let controller2 = DriftController::new(config, 42, 12);
770
771        assert_eq!(
772            controller1.sudden_drift_periods(),
773            controller2.sudden_drift_periods()
774        );
775    }
776
777    #[test]
778    fn test_regime_change() {
779        let config = DriftConfig {
780            enabled: true,
781            drift_type: DriftType::Regime,
782            regime_changes: vec![RegimeChange::new(6, RegimeChangeType::Acquisition)],
783            ..Default::default()
784        };
785        let controller = DriftController::new(config, 42, 12);
786
787        // Before regime change
788        let adj_before = controller.compute_adjustments(5);
789        assert!((adj_before.volume_multiplier - 1.0).abs() < 0.001);
790
791        // After regime change
792        let adj_after = controller.compute_adjustments(6);
793        assert!(adj_after.volume_multiplier > 1.3); // Acquisition increases volume
794        assert!(adj_after.amount_mean_multiplier > 1.1); // And amounts
795        assert!(adj_after
796            .active_regime_changes
797            .contains(&RegimeChangeType::Acquisition));
798    }
799
800    #[test]
801    fn test_regime_change_gradual_transition() {
802        let config = DriftConfig {
803            enabled: true,
804            drift_type: DriftType::Regime,
805            regime_changes: vec![RegimeChange {
806                period: 6,
807                change_type: RegimeChangeType::PriceIncrease,
808                description: None,
809                effects: vec![],
810                transition_periods: 4, // 4 period transition
811            }],
812            ..Default::default()
813        };
814        let controller = DriftController::new(config, 42, 12);
815
816        // At regime change start
817        let adj_start = controller.compute_adjustments(6);
818        // Midway through transition
819        let adj_mid = controller.compute_adjustments(8);
820        // After transition complete
821        let adj_end = controller.compute_adjustments(10);
822
823        // Should gradually increase
824        assert!(adj_start.amount_mean_multiplier < adj_mid.amount_mean_multiplier);
825        assert!(adj_mid.amount_mean_multiplier < adj_end.amount_mean_multiplier);
826    }
827
828    #[test]
829    fn test_economic_cycle() {
830        let config = DriftConfig {
831            enabled: true,
832            drift_type: DriftType::EconomicCycle,
833            economic_cycle: EconomicCycleConfig {
834                enabled: true,
835                cycle_length: 12, // 1-year cycle for testing
836                amplitude: 0.20,
837                phase_offset: 0,
838                recession_periods: vec![],
839                recession_severity: 0.75,
840            },
841            ..Default::default()
842        };
843        let controller = DriftController::new(config, 42, 24);
844
845        // At cycle start (period 0)
846        let adj_0 = controller.compute_adjustments(0);
847        // At cycle peak (period 3, which is 25% through cycle = 90 degrees)
848        let adj_3 = controller.compute_adjustments(3);
849        // At cycle trough (period 9, which is 75% through cycle = 270 degrees)
850        let adj_9 = controller.compute_adjustments(9);
851
852        // Peak should be higher than start
853        assert!(adj_3.economic_cycle_factor > adj_0.economic_cycle_factor);
854        // Trough should be lower than start
855        assert!(adj_9.economic_cycle_factor < adj_0.economic_cycle_factor);
856    }
857
858    #[test]
859    fn test_economic_cycle_recession() {
860        let config = DriftConfig {
861            enabled: true,
862            drift_type: DriftType::EconomicCycle,
863            economic_cycle: EconomicCycleConfig {
864                enabled: true,
865                cycle_length: 12,
866                amplitude: 0.10,
867                phase_offset: 0,
868                recession_periods: vec![6, 7, 8],
869                recession_severity: 0.70,
870            },
871            ..Default::default()
872        };
873        let controller = DriftController::new(config, 42, 12);
874
875        // Not in recession
876        let adj_5 = controller.compute_adjustments(5);
877        assert!(!adj_5.in_recession);
878
879        // In recession
880        let adj_7 = controller.compute_adjustments(7);
881        assert!(adj_7.in_recession);
882        assert!(adj_7.economic_cycle_factor < adj_5.economic_cycle_factor);
883    }
884
885    #[test]
886    fn test_parameter_drift_linear() {
887        let config = DriftConfig {
888            enabled: true,
889            drift_type: DriftType::Gradual,
890            parameter_drifts: vec![ParameterDrift {
891                parameter: "discount_rate".to_string(),
892                drift_type: ParameterDriftType::Linear,
893                initial_value: 0.02,
894                target_or_rate: 0.001, // Increases by 0.1% per period
895                start_period: 0,
896                end_period: None,
897                steepness: 0.1,
898            }],
899            ..Default::default()
900        };
901        let controller = DriftController::new(config, 42, 12);
902
903        let adj_0 = controller.compute_adjustments(0);
904        let adj_6 = controller.compute_adjustments(6);
905
906        let rate_0 = adj_0.parameter_values.get("discount_rate").unwrap();
907        let rate_6 = adj_6.parameter_values.get("discount_rate").unwrap();
908
909        // Should increase linearly
910        assert!((rate_0 - 0.02).abs() < 0.0001);
911        assert!((rate_6 - 0.026).abs() < 0.0001);
912    }
913
914    #[test]
915    fn test_parameter_drift_logistic() {
916        let config = DriftConfig {
917            enabled: true,
918            drift_type: DriftType::Gradual,
919            parameter_drifts: vec![ParameterDrift {
920                parameter: "market_share".to_string(),
921                drift_type: ParameterDriftType::Logistic,
922                initial_value: 0.10,  // 10% starting market share
923                target_or_rate: 0.40, // 40% target market share
924                start_period: 0,
925                end_period: Some(24), // 24 period transition
926                steepness: 0.3,
927            }],
928            ..Default::default()
929        };
930        let controller = DriftController::new(config, 42, 36);
931
932        let adj_0 = controller.compute_adjustments(0);
933        let adj_12 = controller.compute_adjustments(12);
934        let adj_24 = controller.compute_adjustments(24);
935
936        let share_0 = *adj_0.parameter_values.get("market_share").unwrap();
937        let share_12 = *adj_12.parameter_values.get("market_share").unwrap();
938        let share_24 = *adj_24.parameter_values.get("market_share").unwrap();
939
940        // S-curve: starts slow, accelerates in middle, slows at end
941        assert!(share_0 < 0.15); // Near initial
942        assert!(share_12 > 0.20 && share_12 < 0.30); // Around midpoint
943        assert!(share_24 > 0.35); // Near target
944    }
945
946    #[test]
947    fn test_combined_drift_adjustments() {
948        let adj = DriftAdjustments {
949            amount_mean_multiplier: 1.2,
950            seasonal_factor: 1.1,
951            economic_cycle_factor: 0.9,
952            volume_multiplier: 1.3,
953            ..DriftAdjustments::none()
954        };
955
956        // Combined amount = 1.2 * 1.1 * 0.9 = 1.188
957        assert!((adj.combined_amount_multiplier() - 1.188).abs() < 0.001);
958
959        // Combined volume = 1.3 * 1.1 * 0.9 = 1.287
960        assert!((adj.combined_volume_multiplier() - 1.287).abs() < 0.001);
961    }
962
963    #[test]
964    fn test_regime_change_volume_multipliers() {
965        assert!(RegimeChange::new(0, RegimeChangeType::Acquisition).volume_multiplier() > 1.0);
966        assert!(RegimeChange::new(0, RegimeChangeType::Divestiture).volume_multiplier() < 1.0);
967        assert!(
968            (RegimeChange::new(0, RegimeChangeType::PolicyChange).volume_multiplier() - 1.0).abs()
969                < 0.001
970        );
971    }
972}