Skip to main content

phyz_coupling/
subcycling.rs

1//! Subcycling and multi-timescale integration.
2//!
3//! Implements r-RESPA (Reference System Propagator Algorithm) for
4//! partitioning forces into fast and slow components.
5
6/// Timescale classification for different physics.
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum TimeScale {
9    /// Ultra-fast (e.g., electronic motion, high-frequency EM).
10    UltraFast,
11    /// Fast (e.g., bond vibrations, EM field updates).
12    Fast,
13    /// Medium (e.g., angle bending, soft collisions).
14    Medium,
15    /// Slow (e.g., rigid body motion, torsional rotation).
16    Slow,
17    /// Very slow (e.g., long-range forces, thermal diffusion).
18    VerySlow,
19}
20
21impl TimeScale {
22    /// Get typical timestep size for this scale (relative units).
23    pub fn typical_dt_ratio(&self) -> usize {
24        match self {
25            TimeScale::UltraFast => 1,
26            TimeScale::Fast => 10,
27            TimeScale::Medium => 100,
28            TimeScale::Slow => 1000,
29            TimeScale::VerySlow => 10000,
30        }
31    }
32}
33
34/// Subcycling schedule for multi-timescale integration.
35#[derive(Clone, Debug)]
36pub struct SubcyclingSchedule {
37    /// Base timestep (smallest unit).
38    pub dt_base: f64,
39    /// Subcycling ratios for each level.
40    /// e.g., [1, 10, 100] means:
41    /// - Level 0: dt_base (every step)
42    /// - Level 1: 10 * dt_base (every 10 steps)
43    /// - Level 2: 100 * dt_base (every 100 steps)
44    pub ratios: Vec<usize>,
45}
46
47impl SubcyclingSchedule {
48    /// Create a new subcycling schedule.
49    pub fn new(dt_base: f64, ratios: Vec<usize>) -> Self {
50        Self { dt_base, ratios }
51    }
52
53    /// Create a schedule from timescales.
54    pub fn from_timescales(dt_base: f64, scales: &[TimeScale]) -> Self {
55        let ratios = scales.iter().map(|s| s.typical_dt_ratio()).collect();
56        Self { dt_base, ratios }
57    }
58
59    /// Get timestep for a given level.
60    pub fn dt_for_level(&self, level: usize) -> f64 {
61        if level < self.ratios.len() {
62            self.dt_base * self.ratios[level] as f64
63        } else {
64            self.dt_base
65        }
66    }
67
68    /// Check if a level should step at given global step count.
69    pub fn should_step(&self, level: usize, global_step: usize) -> bool {
70        if level >= self.ratios.len() {
71            return true;
72        }
73        global_step.is_multiple_of(self.ratios[level])
74    }
75
76    /// Get number of substeps for a level relative to base.
77    pub fn num_substeps(&self, level: usize) -> usize {
78        if level < self.ratios.len() {
79            self.ratios[level]
80        } else {
81            1
82        }
83    }
84}
85
86/// r-RESPA force partitioning.
87///
88/// Partitions forces into fast (short-range) and slow (long-range) components
89/// for efficient multi-timescale integration.
90pub struct RespaPartition {
91    /// Cutoff distance for fast forces.
92    pub r_short: f64,
93    /// Cutoff distance for slow forces.
94    pub r_long: f64,
95}
96
97impl RespaPartition {
98    /// Create a new RESPA partition.
99    pub fn new(r_short: f64, r_long: f64) -> Self {
100        assert!(r_short < r_long, "r_short must be less than r_long");
101        Self { r_short, r_long }
102    }
103
104    /// Classify a distance as fast, slow, or both.
105    pub fn classify(&self, r: f64) -> ForceType {
106        if r <= self.r_short {
107            ForceType::Fast
108        } else if r <= self.r_long {
109            ForceType::Slow
110        } else {
111            ForceType::None
112        }
113    }
114}
115
116/// Force type classification for RESPA.
117#[derive(Clone, Copy, Debug, PartialEq, Eq)]
118pub enum ForceType {
119    /// Fast-varying force (updated every substep).
120    Fast,
121    /// Slow-varying force (updated every outer step).
122    Slow,
123    /// No force (beyond cutoff).
124    None,
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn test_timescale_ratios() {
133        assert_eq!(TimeScale::UltraFast.typical_dt_ratio(), 1);
134        assert_eq!(TimeScale::Fast.typical_dt_ratio(), 10);
135        assert_eq!(TimeScale::Medium.typical_dt_ratio(), 100);
136        assert_eq!(TimeScale::Slow.typical_dt_ratio(), 1000);
137    }
138
139    #[test]
140    fn test_subcycling_schedule() {
141        let schedule = SubcyclingSchedule::new(0.001, vec![1, 10, 100]);
142
143        assert_eq!(schedule.dt_for_level(0), 0.001);
144        assert_eq!(schedule.dt_for_level(1), 0.01);
145        assert_eq!(schedule.dt_for_level(2), 0.1);
146
147        // Level 0 steps every time
148        assert!(schedule.should_step(0, 0));
149        assert!(schedule.should_step(0, 1));
150        assert!(schedule.should_step(0, 10));
151
152        // Level 1 steps every 10
153        assert!(schedule.should_step(1, 0));
154        assert!(!schedule.should_step(1, 1));
155        assert!(schedule.should_step(1, 10));
156
157        // Level 2 steps every 100
158        assert!(schedule.should_step(2, 0));
159        assert!(!schedule.should_step(2, 10));
160        assert!(schedule.should_step(2, 100));
161    }
162
163    #[test]
164    fn test_from_timescales() {
165        let scales = vec![TimeScale::Fast, TimeScale::Medium, TimeScale::Slow];
166        let schedule = SubcyclingSchedule::from_timescales(1e-6, &scales);
167
168        assert_eq!(schedule.ratios, vec![10, 100, 1000]);
169    }
170
171    #[test]
172    fn test_respa_partition() {
173        let partition = RespaPartition::new(2.5, 10.0);
174
175        assert_eq!(partition.classify(2.0), ForceType::Fast);
176        assert_eq!(partition.classify(5.0), ForceType::Slow);
177        assert_eq!(partition.classify(15.0), ForceType::None);
178    }
179
180    #[test]
181    #[should_panic(expected = "r_short must be less than r_long")]
182    fn test_respa_partition_invalid() {
183        RespaPartition::new(10.0, 5.0);
184    }
185
186    #[test]
187    fn test_num_substeps() {
188        let schedule = SubcyclingSchedule::new(0.001, vec![1, 10, 100]);
189
190        assert_eq!(schedule.num_substeps(0), 1);
191        assert_eq!(schedule.num_substeps(1), 10);
192        assert_eq!(schedule.num_substeps(2), 100);
193    }
194}