phyz_coupling/
subcycling.rs1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum TimeScale {
9 UltraFast,
11 Fast,
13 Medium,
15 Slow,
17 VerySlow,
19}
20
21impl TimeScale {
22 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#[derive(Clone, Debug)]
36pub struct SubcyclingSchedule {
37 pub dt_base: f64,
39 pub ratios: Vec<usize>,
45}
46
47impl SubcyclingSchedule {
48 pub fn new(dt_base: f64, ratios: Vec<usize>) -> Self {
50 Self { dt_base, ratios }
51 }
52
53 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 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 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 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
86pub struct RespaPartition {
91 pub r_short: f64,
93 pub r_long: f64,
95}
96
97impl RespaPartition {
98 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 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
118pub enum ForceType {
119 Fast,
121 Slow,
123 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 assert!(schedule.should_step(0, 0));
149 assert!(schedule.should_step(0, 1));
150 assert!(schedule.should_step(0, 10));
151
152 assert!(schedule.should_step(1, 0));
154 assert!(!schedule.should_step(1, 1));
155 assert!(schedule.should_step(1, 10));
156
157 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}