Skip to main content

datasynth_core/distributions/
behavioral_drift.rs

1//! Behavioral drift models for realistic entity behavior evolution.
2//!
3//! Provides comprehensive behavioral drift modeling including:
4//! - Vendor behavioral drift (payment terms, quality, pricing)
5//! - Customer behavioral drift (payment patterns, order patterns)
6//! - Employee behavioral drift (approval patterns, error patterns)
7//! - Collective behavioral drift (year-end intensity, automation adoption)
8
9use serde::{Deserialize, Serialize};
10
11/// Context for behavioral drift calculations.
12#[derive(Debug, Clone, Default)]
13pub struct DriftContext {
14    /// Economic cycle factor (1.0 = neutral, <1.0 = downturn, >1.0 = growth).
15    pub economic_cycle_factor: f64,
16    /// Whether currently in a recession.
17    pub is_recession: bool,
18    /// Current inflation rate.
19    pub inflation_rate: f64,
20    /// Market sentiment.
21    pub market_sentiment: MarketSentiment,
22    /// Period number (0-indexed).
23    pub period: u32,
24    /// Total periods in simulation.
25    pub total_periods: u32,
26}
27
28impl DriftContext {
29    /// Create a neutral context.
30    pub fn neutral() -> Self {
31        Self {
32            economic_cycle_factor: 1.0,
33            is_recession: false,
34            inflation_rate: 0.02,
35            market_sentiment: MarketSentiment::Neutral,
36            period: 0,
37            total_periods: 12,
38        }
39    }
40
41    /// Get years elapsed (fractional).
42    pub fn years_elapsed(&self) -> f64 {
43        self.period as f64 / 12.0
44    }
45}
46
47/// Market sentiment indicator.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
49#[serde(rename_all = "snake_case")]
50pub enum MarketSentiment {
51    /// Very pessimistic market.
52    VeryPessimistic,
53    /// Pessimistic market.
54    Pessimistic,
55    /// Neutral market.
56    #[default]
57    Neutral,
58    /// Optimistic market.
59    Optimistic,
60    /// Very optimistic market.
61    VeryOptimistic,
62}
63
64impl MarketSentiment {
65    /// Get the sentiment factor (0.6 to 1.4).
66    pub fn factor(&self) -> f64 {
67        match self {
68            Self::VeryPessimistic => 0.6,
69            Self::Pessimistic => 0.8,
70            Self::Neutral => 1.0,
71            Self::Optimistic => 1.2,
72            Self::VeryOptimistic => 1.4,
73        }
74    }
75}
76
77/// Behavioral state snapshot.
78#[derive(Debug, Clone, Default)]
79pub struct BehavioralState {
80    /// Payment behavior adjustment (days delta).
81    pub payment_days_delta: f64,
82    /// Order pattern adjustment factor.
83    pub order_factor: f64,
84    /// Error rate adjustment factor.
85    pub error_factor: f64,
86    /// Processing time adjustment factor.
87    pub processing_time_factor: f64,
88    /// Quality adjustment factor.
89    pub quality_factor: f64,
90    /// Price sensitivity factor.
91    pub price_sensitivity: f64,
92}
93
94impl BehavioralState {
95    /// Create a neutral state.
96    pub fn neutral() -> Self {
97        Self {
98            payment_days_delta: 0.0,
99            order_factor: 1.0,
100            error_factor: 1.0,
101            processing_time_factor: 1.0,
102            quality_factor: 1.0,
103            price_sensitivity: 1.0,
104        }
105    }
106}
107
108// =============================================================================
109// Vendor Behavioral Drift
110// =============================================================================
111
112/// Vendor behavioral drift configuration.
113#[derive(Debug, Clone, Default, Serialize, Deserialize)]
114pub struct VendorBehavioralDrift {
115    /// Payment terms drift configuration.
116    #[serde(default)]
117    pub payment_terms_drift: PaymentTermsDrift,
118    /// Vendor quality drift configuration.
119    #[serde(default)]
120    pub quality_drift: VendorQualityDrift,
121    /// Pricing behavior drift configuration.
122    #[serde(default)]
123    pub pricing_drift: PricingBehaviorDrift,
124}
125
126impl VendorBehavioralDrift {
127    /// Calculate combined behavioral state.
128    pub fn state_at(&self, context: &DriftContext) -> BehavioralState {
129        let years = context.years_elapsed();
130
131        // Payment terms extension
132        let payment_days = self.payment_terms_drift.extension_rate_per_year
133            * years
134            * (1.0
135                + self.payment_terms_drift.economic_sensitivity
136                    * (context.economic_cycle_factor - 1.0));
137
138        // Quality drift
139        let quality_factor = if years < 1.0 {
140            // New vendor improvement
141            1.0 + self.quality_drift.new_vendor_improvement_rate * years
142        } else {
143            // Complacency after first year
144            1.0 + self.quality_drift.new_vendor_improvement_rate
145                - self.quality_drift.complacency_decline_rate * (years - 1.0)
146        };
147
148        // Price sensitivity to inflation
149        let price_sensitivity =
150            1.0 + self.pricing_drift.inflation_pass_through * context.inflation_rate * years;
151
152        BehavioralState {
153            payment_days_delta: payment_days,
154            quality_factor: quality_factor.clamp(0.7, 1.3),
155            price_sensitivity,
156            ..BehavioralState::neutral()
157        }
158    }
159}
160
161/// Payment terms drift configuration.
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct PaymentTermsDrift {
164    /// Average days extension per year.
165    #[serde(default = "default_extension_rate")]
166    pub extension_rate_per_year: f64,
167    /// Economic sensitivity (how much economic conditions affect terms).
168    #[serde(default = "default_economic_sensitivity")]
169    pub economic_sensitivity: f64,
170}
171
172fn default_extension_rate() -> f64 {
173    2.5
174}
175
176fn default_economic_sensitivity() -> f64 {
177    1.0
178}
179
180impl Default for PaymentTermsDrift {
181    fn default() -> Self {
182        Self {
183            extension_rate_per_year: 2.5,
184            economic_sensitivity: 1.0,
185        }
186    }
187}
188
189/// Vendor quality drift configuration.
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct VendorQualityDrift {
192    /// Quality improvement rate for new vendors (per year).
193    #[serde(default = "default_improvement_rate")]
194    pub new_vendor_improvement_rate: f64,
195    /// Quality decline rate due to complacency (per year after first year).
196    #[serde(default = "default_decline_rate")]
197    pub complacency_decline_rate: f64,
198}
199
200fn default_improvement_rate() -> f64 {
201    0.02
202}
203
204fn default_decline_rate() -> f64 {
205    0.01
206}
207
208impl Default for VendorQualityDrift {
209    fn default() -> Self {
210        Self {
211            new_vendor_improvement_rate: 0.02,
212            complacency_decline_rate: 0.01,
213        }
214    }
215}
216
217/// Pricing behavior drift configuration.
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct PricingBehaviorDrift {
220    /// How much inflation is passed through (0.0 to 1.0).
221    #[serde(default = "default_pass_through")]
222    pub inflation_pass_through: f64,
223    /// Price volatility factor.
224    #[serde(default = "default_volatility")]
225    pub price_volatility: f64,
226}
227
228fn default_pass_through() -> f64 {
229    0.80
230}
231
232fn default_volatility() -> f64 {
233    0.10
234}
235
236impl Default for PricingBehaviorDrift {
237    fn default() -> Self {
238        Self {
239            inflation_pass_through: 0.80,
240            price_volatility: 0.10,
241        }
242    }
243}
244
245// =============================================================================
246// Customer Behavioral Drift
247// =============================================================================
248
249/// Customer behavioral drift configuration.
250#[derive(Debug, Clone, Default, Serialize, Deserialize)]
251pub struct CustomerBehavioralDrift {
252    /// Customer payment drift configuration.
253    #[serde(default)]
254    pub payment_drift: CustomerPaymentDrift,
255    /// Order pattern drift configuration.
256    #[serde(default)]
257    pub order_drift: OrderPatternDrift,
258}
259
260impl CustomerBehavioralDrift {
261    /// Calculate combined behavioral state.
262    pub fn state_at(&self, context: &DriftContext) -> BehavioralState {
263        // Payment delays during downturns
264        let payment_days = if context.is_recession || context.economic_cycle_factor < 0.9 {
265            let severity = 1.0 - context.economic_cycle_factor;
266            self.payment_drift.downturn_days_extension.0 as f64
267                + (self.payment_drift.downturn_days_extension.1 as f64
268                    - self.payment_drift.downturn_days_extension.0 as f64)
269                    * severity
270        } else {
271            0.0
272        };
273
274        // Order pattern shift (digital adoption)
275        let years = context.years_elapsed();
276        let order_factor = 1.0 + self.order_drift.digital_shift_rate * years;
277
278        BehavioralState {
279            payment_days_delta: payment_days,
280            order_factor,
281            ..BehavioralState::neutral()
282        }
283    }
284}
285
286/// Customer payment drift configuration.
287#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct CustomerPaymentDrift {
289    /// Days extension range during economic downturn (min, max).
290    #[serde(default = "default_downturn_extension")]
291    pub downturn_days_extension: (u32, u32),
292    /// Bad debt rate increase during downturn.
293    #[serde(default = "default_bad_debt_increase")]
294    pub downturn_bad_debt_increase: f64,
295}
296
297fn default_downturn_extension() -> (u32, u32) {
298    (5, 15)
299}
300
301fn default_bad_debt_increase() -> f64 {
302    0.02
303}
304
305impl Default for CustomerPaymentDrift {
306    fn default() -> Self {
307        Self {
308            downturn_days_extension: (5, 15),
309            downturn_bad_debt_increase: 0.02,
310        }
311    }
312}
313
314/// Order pattern drift configuration.
315#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct OrderPatternDrift {
317    /// Rate of shift to digital channels (per year).
318    #[serde(default = "default_digital_shift")]
319    pub digital_shift_rate: f64,
320    /// Order consolidation rate (fewer, larger orders).
321    #[serde(default = "default_consolidation")]
322    pub order_consolidation_rate: f64,
323}
324
325fn default_digital_shift() -> f64 {
326    0.05
327}
328
329fn default_consolidation() -> f64 {
330    0.02
331}
332
333impl Default for OrderPatternDrift {
334    fn default() -> Self {
335        Self {
336            digital_shift_rate: 0.05,
337            order_consolidation_rate: 0.02,
338        }
339    }
340}
341
342// =============================================================================
343// Employee Behavioral Drift
344// =============================================================================
345
346/// Employee behavioral drift configuration.
347#[derive(Debug, Clone, Default, Serialize, Deserialize)]
348pub struct EmployeeBehavioralDrift {
349    /// Approval pattern drift configuration.
350    #[serde(default)]
351    pub approval_drift: ApprovalPatternDrift,
352    /// Error pattern drift configuration.
353    #[serde(default)]
354    pub error_drift: ErrorPatternDrift,
355}
356
357impl EmployeeBehavioralDrift {
358    /// Calculate combined behavioral state.
359    pub fn state_at(&self, context: &DriftContext, is_period_end: bool) -> BehavioralState {
360        let years = context.years_elapsed();
361
362        // EOM intensity increase
363        let eom_factor = if is_period_end {
364            1.0 + self.approval_drift.eom_intensity_increase_per_year * years
365        } else {
366            1.0
367        };
368
369        // Learning curve effect on errors
370        // Error factor > 1.0 means more errors; 1.0 means baseline
371        let months = context.period as f64;
372        let error_factor = if months < self.error_drift.learning_curve_months as f64 {
373            // New employee learning curve: starts at (1 + new_employee_error_rate) and decreases to 1.0
374            let progress = months / self.error_drift.learning_curve_months as f64;
375            1.0 + self.error_drift.new_employee_error_rate * (1.0 - progress)
376        } else {
377            // Fatigue factor after learning: gradually increases from 1.0
378            let fatigue_years = (months - self.error_drift.learning_curve_months as f64) / 12.0;
379            1.0 + self.error_drift.fatigue_error_increase * fatigue_years
380        };
381
382        BehavioralState {
383            processing_time_factor: eom_factor,
384            error_factor: error_factor.clamp(0.5, 2.0),
385            ..BehavioralState::neutral()
386        }
387    }
388}
389
390/// Approval pattern drift configuration.
391#[derive(Debug, Clone, Serialize, Deserialize)]
392pub struct ApprovalPatternDrift {
393    /// EOM intensity increase per year.
394    #[serde(default = "default_eom_intensity")]
395    pub eom_intensity_increase_per_year: f64,
396    /// Volume threshold for rubber-stamp behavior.
397    #[serde(default = "default_rubber_stamp")]
398    pub rubber_stamp_volume_threshold: u32,
399}
400
401fn default_eom_intensity() -> f64 {
402    0.05
403}
404
405fn default_rubber_stamp() -> u32 {
406    50
407}
408
409impl Default for ApprovalPatternDrift {
410    fn default() -> Self {
411        Self {
412            eom_intensity_increase_per_year: 0.05,
413            rubber_stamp_volume_threshold: 50,
414        }
415    }
416}
417
418/// Error pattern drift configuration.
419#[derive(Debug, Clone, Serialize, Deserialize)]
420pub struct ErrorPatternDrift {
421    /// Initial error rate for new employees.
422    #[serde(default = "default_new_error_rate")]
423    pub new_employee_error_rate: f64,
424    /// Learning curve duration in months.
425    #[serde(default = "default_learning_months")]
426    pub learning_curve_months: u32,
427    /// Error rate increase due to fatigue (per year after learning).
428    #[serde(default = "default_fatigue_increase")]
429    pub fatigue_error_increase: f64,
430}
431
432fn default_new_error_rate() -> f64 {
433    0.08
434}
435
436fn default_learning_months() -> u32 {
437    6
438}
439
440fn default_fatigue_increase() -> f64 {
441    0.01
442}
443
444impl Default for ErrorPatternDrift {
445    fn default() -> Self {
446        Self {
447            new_employee_error_rate: 0.08,
448            learning_curve_months: 6,
449            fatigue_error_increase: 0.01,
450        }
451    }
452}
453
454// =============================================================================
455// Collective Behavioral Drift
456// =============================================================================
457
458/// Collective behavioral drift across the organization.
459#[derive(Debug, Clone, Default, Serialize, Deserialize)]
460pub struct CollectiveBehavioralDrift {
461    /// Year-end intensity drift.
462    #[serde(default)]
463    pub year_end_intensity: YearEndIntensityDrift,
464    /// Automation adoption drift.
465    #[serde(default)]
466    pub automation_adoption: AutomationAdoptionDrift,
467    /// Remote work impact drift.
468    #[serde(default)]
469    pub remote_work_impact: RemoteWorkDrift,
470}
471
472impl CollectiveBehavioralDrift {
473    /// Calculate collective state.
474    pub fn state_at(&self, context: &DriftContext, month: u32) -> CollectiveState {
475        let years = context.years_elapsed();
476
477        // Year-end intensity
478        let is_year_end = month == 11 || month == 0; // December or January
479        let year_end_factor = if is_year_end {
480            1.0 + self.year_end_intensity.intensity_increase_per_year * years
481        } else {
482            1.0
483        };
484
485        // Automation adoption (S-curve)
486        let automation_rate = if self.automation_adoption.s_curve_enabled {
487            let midpoint_years = self.automation_adoption.adoption_midpoint_months as f64 / 12.0;
488            let steepness = self.automation_adoption.steepness;
489            1.0 / (1.0 + (-steepness * (years - midpoint_years)).exp())
490        } else {
491            0.0
492        };
493
494        // Remote work posting time flattening
495        let posting_time_variance = if self.remote_work_impact.enabled {
496            1.0 - self.remote_work_impact.posting_time_flattening * years.min(2.0)
497        } else {
498            1.0
499        };
500
501        CollectiveState {
502            year_end_intensity_factor: year_end_factor,
503            automation_rate,
504            posting_time_variance_factor: posting_time_variance.max(0.5),
505        }
506    }
507}
508
509/// Collective behavioral state.
510#[derive(Debug, Clone, Default)]
511pub struct CollectiveState {
512    /// Year-end intensity factor.
513    pub year_end_intensity_factor: f64,
514    /// Automation adoption rate (0.0 to 1.0).
515    pub automation_rate: f64,
516    /// Posting time variance factor.
517    pub posting_time_variance_factor: f64,
518}
519
520/// Year-end intensity drift configuration.
521#[derive(Debug, Clone, Serialize, Deserialize)]
522pub struct YearEndIntensityDrift {
523    /// Intensity increase per year.
524    #[serde(default = "default_intensity_increase")]
525    pub intensity_increase_per_year: f64,
526}
527
528fn default_intensity_increase() -> f64 {
529    0.05
530}
531
532impl Default for YearEndIntensityDrift {
533    fn default() -> Self {
534        Self {
535            intensity_increase_per_year: 0.05,
536        }
537    }
538}
539
540/// Automation adoption drift configuration.
541#[derive(Debug, Clone, Serialize, Deserialize)]
542pub struct AutomationAdoptionDrift {
543    /// Enable S-curve adoption model.
544    #[serde(default)]
545    pub s_curve_enabled: bool,
546    /// Adoption midpoint in months.
547    #[serde(default = "default_midpoint")]
548    pub adoption_midpoint_months: u32,
549    /// Steepness of adoption curve.
550    #[serde(default = "default_steepness")]
551    pub steepness: f64,
552}
553
554fn default_midpoint() -> u32 {
555    24
556}
557
558fn default_steepness() -> f64 {
559    0.15
560}
561
562impl Default for AutomationAdoptionDrift {
563    fn default() -> Self {
564        Self {
565            s_curve_enabled: false,
566            adoption_midpoint_months: 24,
567            steepness: 0.15,
568        }
569    }
570}
571
572/// Remote work impact drift configuration.
573#[derive(Debug, Clone, Serialize, Deserialize)]
574pub struct RemoteWorkDrift {
575    /// Enable remote work impact.
576    #[serde(default)]
577    pub enabled: bool,
578    /// Posting time flattening factor (reduction in time-of-day variance).
579    #[serde(default = "default_flattening")]
580    pub posting_time_flattening: f64,
581}
582
583fn default_flattening() -> f64 {
584    0.3
585}
586
587impl Default for RemoteWorkDrift {
588    fn default() -> Self {
589        Self {
590            enabled: false,
591            posting_time_flattening: 0.3,
592        }
593    }
594}
595
596// =============================================================================
597// Behavioral Drift Controller
598// =============================================================================
599
600/// Main controller for all behavioral drift.
601#[derive(Debug, Clone, Serialize, Deserialize, Default)]
602pub struct BehavioralDriftConfig {
603    /// Enable behavioral drift.
604    #[serde(default)]
605    pub enabled: bool,
606    /// Vendor behavioral drift.
607    #[serde(default)]
608    pub vendor_behavior: VendorBehavioralDrift,
609    /// Customer behavioral drift.
610    #[serde(default)]
611    pub customer_behavior: CustomerBehavioralDrift,
612    /// Employee behavioral drift.
613    #[serde(default)]
614    pub employee_behavior: EmployeeBehavioralDrift,
615    /// Collective behavioral drift.
616    #[serde(default)]
617    pub collective: CollectiveBehavioralDrift,
618}
619
620impl BehavioralDriftConfig {
621    /// Compute all behavioral effects for a given context.
622    pub fn compute_effects(
623        &self,
624        context: &DriftContext,
625        month: u32,
626        is_period_end: bool,
627    ) -> BehavioralEffects {
628        if !self.enabled {
629            return BehavioralEffects::neutral();
630        }
631
632        BehavioralEffects {
633            vendor: self.vendor_behavior.state_at(context),
634            customer: self.customer_behavior.state_at(context),
635            employee: self.employee_behavior.state_at(context, is_period_end),
636            collective: self.collective.state_at(context, month),
637        }
638    }
639}
640
641/// Combined behavioral effects.
642#[derive(Debug, Clone, Default)]
643pub struct BehavioralEffects {
644    /// Vendor behavioral state.
645    pub vendor: BehavioralState,
646    /// Customer behavioral state.
647    pub customer: BehavioralState,
648    /// Employee behavioral state.
649    pub employee: BehavioralState,
650    /// Collective behavioral state.
651    pub collective: CollectiveState,
652}
653
654impl BehavioralEffects {
655    /// Create neutral effects.
656    pub fn neutral() -> Self {
657        Self {
658            vendor: BehavioralState::neutral(),
659            customer: BehavioralState::neutral(),
660            employee: BehavioralState::neutral(),
661            collective: CollectiveState::default(),
662        }
663    }
664}
665
666#[cfg(test)]
667#[allow(clippy::unwrap_used)]
668mod tests {
669    use super::*;
670
671    #[test]
672    fn test_vendor_behavioral_drift() {
673        let drift = VendorBehavioralDrift::default();
674        let context = DriftContext {
675            period: 24, // 2 years
676            total_periods: 36,
677            ..DriftContext::neutral()
678        };
679
680        let state = drift.state_at(&context);
681        // Payment terms should have increased
682        assert!(state.payment_days_delta > 0.0);
683        // Quality factor should have decreased due to complacency
684        assert!(state.quality_factor < 1.02);
685    }
686
687    #[test]
688    fn test_customer_downturn_drift() {
689        let drift = CustomerBehavioralDrift::default();
690        let context = DriftContext {
691            is_recession: true,
692            economic_cycle_factor: 0.8,
693            ..DriftContext::neutral()
694        };
695
696        let state = drift.state_at(&context);
697        // Payment delays during downturn
698        assert!(state.payment_days_delta > 0.0);
699    }
700
701    #[test]
702    fn test_employee_learning_curve() {
703        let drift = EmployeeBehavioralDrift::default();
704
705        // New employee (month 1)
706        let context_new = DriftContext {
707            period: 1,
708            ..DriftContext::neutral()
709        };
710        let state_new = drift.state_at(&context_new, false);
711        assert!(state_new.error_factor > 1.0); // Higher errors initially
712
713        // Experienced employee (month 12)
714        let context_exp = DriftContext {
715            period: 12,
716            ..DriftContext::neutral()
717        };
718        let state_exp = drift.state_at(&context_exp, false);
719        assert!(state_exp.error_factor < state_new.error_factor); // Lower errors
720    }
721
722    #[test]
723    fn test_automation_s_curve() {
724        let drift = CollectiveBehavioralDrift {
725            automation_adoption: AutomationAdoptionDrift {
726                s_curve_enabled: true,
727                adoption_midpoint_months: 24,
728                steepness: 0.15,
729            },
730            ..Default::default()
731        };
732
733        // Early (month 6)
734        let context_early = DriftContext {
735            period: 6,
736            ..DriftContext::neutral()
737        };
738        let state_early = drift.state_at(&context_early, 6);
739
740        // Midpoint (month 24)
741        let context_mid = DriftContext {
742            period: 24,
743            ..DriftContext::neutral()
744        };
745        let state_mid = drift.state_at(&context_mid, 0);
746
747        // Late (month 48)
748        let context_late = DriftContext {
749            period: 48,
750            ..DriftContext::neutral()
751        };
752        let state_late = drift.state_at(&context_late, 0);
753
754        // S-curve: slow start, fast middle, slow end
755        assert!(state_early.automation_rate < 0.5);
756        assert!((state_mid.automation_rate - 0.5).abs() < 0.2);
757        assert!(state_late.automation_rate > 0.5);
758    }
759}