1pub fn cfl_timestep(max_velocity: f64, grid_spacing: f64, cfl_number: f64) -> f64 {
39 let v = max_velocity.abs();
40 if v < 1e-30 {
41 return f64::MAX;
42 }
43 cfl_number * grid_spacing / v
44}
45
46pub fn courant_dt(dx: f64, c: f64, cfl: f64) -> f64 {
52 let c_abs = c.abs();
53 if c_abs < 1e-30 {
54 return f64::MAX;
55 }
56 cfl * dx / c_abs
57}
58
59pub fn diffusive_dt(dx: f64, nu: f64, safety: f64) -> f64 {
66 let nu_abs = nu.abs();
67 if nu_abs < 1e-30 {
68 return f64::MAX;
69 }
70 safety * dx * dx / (2.0 * nu_abs)
71}
72
73pub fn richardson_dt(dt_old: f64, error: f64, tolerance: f64, order: u32) -> f64 {
88 const MIN_FACTOR: f64 = 0.1;
89 const MAX_FACTOR: f64 = 5.0;
90
91 if error < 1e-30 {
92 return dt_old * MAX_FACTOR;
93 }
94
95 let exponent = 1.0 / (order as f64 + 1.0);
96 let ratio = (tolerance / error).powf(exponent);
97 let factor = ratio.clamp(MIN_FACTOR, MAX_FACTOR);
98 dt_old * factor
99}
100
101#[derive(Debug, Clone)]
107pub struct TimeDomainConfig {
108 pub min_dt: f64,
110 pub max_dt: f64,
112 pub cfl_factor: f64,
114 pub safety_factor: f64,
116}
117
118impl TimeDomainConfig {
119 pub fn new(min_dt: f64, max_dt: f64, cfl_factor: f64, safety_factor: f64) -> Self {
121 Self {
122 min_dt,
123 max_dt,
124 cfl_factor,
125 safety_factor,
126 }
127 }
128}
129
130#[derive(Debug, Clone)]
136pub struct TimeDomainState {
137 pub current_dt: f64,
139 pub error_estimate: f64,
141 pub step_count: u64,
143 pub config: TimeDomainConfig,
146}
147
148impl TimeDomainState {
149 pub fn from_config(config: TimeDomainConfig) -> Self {
153 let initial_dt = config.max_dt * config.safety_factor;
154 Self {
155 current_dt: initial_dt,
156 error_estimate: 0.0,
157 step_count: 0,
158 config,
159 }
160 }
161}
162
163#[derive(Debug, Clone, PartialEq)]
169pub struct StepSchedule {
170 pub domain_idx: usize,
172 pub n_substeps: usize,
174 pub substep_dt: f64,
176}
177
178#[derive(Debug, Clone)]
190pub struct UnifiedTimeStepper {
191 pub domains: Vec<TimeDomainState>,
193 pub global_dt: f64,
195 pub global_time: f64,
197 pub subcycle_ratios: Vec<usize>,
199}
200
201impl UnifiedTimeStepper {
202 pub fn new(configs: Vec<TimeDomainConfig>) -> Self {
204 let domains: Vec<TimeDomainState> = configs
205 .into_iter()
206 .map(TimeDomainState::from_config)
207 .collect();
208 let n = domains.len();
209 let mut stepper = Self {
210 domains,
211 global_dt: 0.0,
212 global_time: 0.0,
213 subcycle_ratios: vec![1; n],
214 };
215 stepper.global_dt = stepper.raw_global_dt();
216 stepper.recompute_subcycle_ratios();
217 stepper
218 }
219
220 fn raw_global_dt(&self) -> f64 {
224 self.domains
225 .iter()
226 .map(|d| d.current_dt * d.config.safety_factor)
227 .fold(f64::MAX, f64::min)
228 }
229
230 fn recompute_subcycle_ratios(&mut self) {
232 if self.global_dt <= 0.0 {
233 for r in &mut self.subcycle_ratios {
235 *r = 1;
236 }
237 return;
238 }
239 for (i, domain) in self.domains.iter().enumerate() {
240 let effective_dt = domain.current_dt * domain.config.safety_factor;
241 if effective_dt >= self.global_dt {
245 self.subcycle_ratios[i] = 1;
246 } else {
247 let ratio = (self.global_dt / effective_dt).ceil() as usize;
249 self.subcycle_ratios[i] = ratio.max(1);
250 }
251 }
252 }
253
254 pub fn compute_global_dt(&mut self) -> f64 {
260 self.global_dt = self.raw_global_dt();
261 let global_min = self
263 .domains
264 .iter()
265 .map(|d| d.config.min_dt)
266 .fold(0.0_f64, f64::max);
267 let global_max = self
268 .domains
269 .iter()
270 .map(|d| d.config.max_dt)
271 .fold(f64::MAX, f64::min);
272 self.global_dt = self.global_dt.clamp(global_min, global_max);
273 self.recompute_subcycle_ratios();
274 self.global_dt
275 }
276
277 pub fn compute_subcycle_ratios(&mut self) {
280 self.recompute_subcycle_ratios();
281 }
282
283 pub fn advance_global_time(&mut self) {
286 self.global_time += self.global_dt;
287 for (i, domain) in self.domains.iter_mut().enumerate() {
288 domain.step_count += self.subcycle_ratios[i] as u64;
289 }
290 }
291
292 pub fn update_domain_dt(&mut self, domain_idx: usize, new_dt: f64) -> bool {
298 if let Some(domain) = self.domains.get_mut(domain_idx) {
299 domain.current_dt = new_dt.clamp(domain.config.min_dt, domain.config.max_dt);
300 true
301 } else {
302 false
303 }
304 }
305
306 pub fn update_domain_error(&mut self, domain_idx: usize, error: f64) -> bool {
311 if let Some(domain) = self.domains.get_mut(domain_idx) {
312 domain.error_estimate = error;
313 true
314 } else {
315 false
316 }
317 }
318
319 pub fn step_schedule(&self) -> Vec<StepSchedule> {
322 self.domains
323 .iter()
324 .enumerate()
325 .map(|(i, _domain)| {
326 let n = self.subcycle_ratios[i];
327 let sub_dt = if n > 0 {
328 self.global_dt / n as f64
329 } else {
330 self.global_dt
331 };
332 StepSchedule {
333 domain_idx: i,
334 n_substeps: n,
335 substep_dt: sub_dt,
336 }
337 })
338 .collect()
339 }
340}
341
342#[cfg(test)]
347mod tests {
348 use super::*;
349
350 #[test]
353 fn test_cfl_timestep_basic() {
354 let dt = cfl_timestep(10.0, 1.0, 0.5);
355 assert!((dt - 0.05).abs() < 1e-12, "dt = {dt}");
356 }
357
358 #[test]
359 fn test_cfl_timestep_zero_velocity_returns_max() {
360 let dt = cfl_timestep(0.0, 1.0, 0.5);
361 assert_eq!(dt, f64::MAX);
362 }
363
364 #[test]
365 fn test_courant_dt_basic() {
366 let dt = courant_dt(0.1, 340.0, 0.8);
367 let expected = 0.8 * 0.1 / 340.0;
368 assert!((dt - expected).abs() < 1e-14, "dt = {dt}");
369 }
370
371 #[test]
372 fn test_courant_dt_zero_speed() {
373 assert_eq!(courant_dt(0.1, 0.0, 0.8), f64::MAX);
374 }
375
376 #[test]
377 fn test_diffusive_dt_basic() {
378 let dt = diffusive_dt(0.01, 1e-3, 0.9);
379 let expected = 0.9 * 0.01 * 0.01 / (2.0 * 1e-3);
380 assert!((dt - expected).abs() < 1e-14, "dt = {dt}");
381 }
382
383 #[test]
384 fn test_diffusive_dt_zero_nu() {
385 assert_eq!(diffusive_dt(0.01, 0.0, 0.9), f64::MAX);
386 }
387
388 #[test]
391 fn test_richardson_increases_dt_when_error_below_tolerance() {
392 let dt_old = 0.01;
393 let error = 1e-6;
394 let tolerance = 1e-4;
395 let dt_new = richardson_dt(dt_old, error, tolerance, 2);
396 assert!(
397 dt_new > dt_old,
398 "dt_new={dt_new} should be > dt_old={dt_old}"
399 );
400 }
401
402 #[test]
403 fn test_richardson_decreases_dt_when_error_above_tolerance() {
404 let dt_old = 0.01;
405 let error = 1e-2;
406 let tolerance = 1e-4;
407 let dt_new = richardson_dt(dt_old, error, tolerance, 2);
408 assert!(
409 dt_new < dt_old,
410 "dt_new={dt_new} should be < dt_old={dt_old}"
411 );
412 }
413
414 #[test]
415 fn test_richardson_zero_error_gives_max_growth() {
416 let dt_old = 0.01;
417 let dt_new = richardson_dt(dt_old, 0.0, 1e-4, 2);
418 assert!((dt_new - dt_old * 5.0).abs() < 1e-14);
419 }
420
421 #[test]
422 fn test_richardson_clamped_growth() {
423 let dt_new = richardson_dt(0.01, 1e-30, 1.0, 1);
425 assert!((dt_new - 0.05).abs() < 1e-10);
426 }
427
428 #[test]
429 fn test_richardson_clamped_shrink() {
430 let dt_new = richardson_dt(0.01, 1e10, 1e-4, 2);
432 assert!((dt_new - 0.001).abs() < 1e-10);
433 }
434
435 #[test]
438 fn test_single_domain_dt_converges() {
439 let cfg = TimeDomainConfig::new(1e-6, 1e-2, 0.5, 0.9);
440 let mut stepper = UnifiedTimeStepper::new(vec![cfg]);
441
442 for _ in 0..20 {
444 let target_dt = 5e-4;
445 stepper.update_domain_dt(0, target_dt);
446 stepper.compute_global_dt();
447 }
448
449 let effective = stepper.global_dt;
450 let expected = 5e-4 * 0.9;
451 assert!(
452 (effective - expected).abs() < 1e-10,
453 "effective={effective}, expected={expected}"
454 );
455 }
456
457 #[test]
460 fn test_two_domains_subcycling() {
461 let fast = TimeDomainConfig::new(1e-6, 1e-2, 0.25, 0.9);
462 let slow = TimeDomainConfig::new(1e-5, 1e-1, 0.5, 0.9);
463 let mut stepper = UnifiedTimeStepper::new(vec![fast, slow]);
464
465 stepper.update_domain_dt(0, 1e-4);
467 stepper.update_domain_dt(1, 1e-3);
468 stepper.compute_global_dt();
469
470 let g = stepper.global_dt;
472 let fast_eff = 1e-4 * 0.9;
473 assert!(
474 (g - fast_eff).abs() < 1e-12,
475 "global_dt={g}, expected={fast_eff}"
476 );
477
478 assert_eq!(stepper.subcycle_ratios[0], 1);
480 assert_eq!(stepper.subcycle_ratios[1], 1);
481 }
482
483 #[test]
484 fn test_global_dt_is_minimum_across_domains() {
485 let d1 = TimeDomainConfig::new(1e-6, 1.0, 0.5, 1.0);
486 let d2 = TimeDomainConfig::new(1e-6, 1.0, 0.5, 1.0);
487 let d3 = TimeDomainConfig::new(1e-6, 1.0, 0.5, 1.0);
488 let mut stepper = UnifiedTimeStepper::new(vec![d1, d2, d3]);
489
490 stepper.update_domain_dt(0, 0.1);
491 stepper.update_domain_dt(1, 0.05);
492 stepper.update_domain_dt(2, 0.2);
493 let g = stepper.compute_global_dt();
494
495 assert!((g - 0.05).abs() < 1e-14, "global_dt={g}");
497 }
498
499 #[test]
502 fn test_schedule_single_domain() {
503 let cfg = TimeDomainConfig::new(1e-6, 1.0, 0.5, 1.0);
504 let mut stepper = UnifiedTimeStepper::new(vec![cfg]);
505 stepper.update_domain_dt(0, 0.01);
506 stepper.compute_global_dt();
507
508 let sched = stepper.step_schedule();
509 assert_eq!(sched.len(), 1);
510 assert_eq!(sched[0].domain_idx, 0);
511 assert_eq!(sched[0].n_substeps, 1);
512 assert!((sched[0].substep_dt - 0.01).abs() < 1e-14);
513 }
514
515 #[test]
516 fn test_schedule_multi_domain() {
517 let d1 = TimeDomainConfig::new(1e-6, 1.0, 0.5, 1.0);
518 let d2 = TimeDomainConfig::new(1e-6, 1.0, 0.5, 1.0);
519 let mut stepper = UnifiedTimeStepper::new(vec![d1, d2]);
520
521 stepper.update_domain_dt(0, 0.01);
522 stepper.update_domain_dt(1, 0.1);
523 stepper.compute_global_dt();
524
525 let sched = stepper.step_schedule();
526 assert_eq!(sched.len(), 2);
527
528 assert_eq!(sched[0].n_substeps, 1);
530 assert!((sched[0].substep_dt - 0.01).abs() < 1e-14);
531
532 assert_eq!(sched[1].n_substeps, 1);
534 assert!((sched[1].substep_dt - 0.01).abs() < 1e-14);
535 }
536
537 #[test]
540 fn test_advance_global_time() {
541 let cfg = TimeDomainConfig::new(1e-6, 1.0, 0.5, 1.0);
542 let mut stepper = UnifiedTimeStepper::new(vec![cfg]);
543 stepper.update_domain_dt(0, 0.01);
544 stepper.compute_global_dt();
545
546 let dt = stepper.global_dt;
547 stepper.advance_global_time();
548 assert!((stepper.global_time - dt).abs() < 1e-14);
549 assert_eq!(stepper.domains[0].step_count, 1); }
551
552 #[test]
553 fn test_advance_accumulates() {
554 let cfg = TimeDomainConfig::new(1e-6, 1.0, 0.5, 1.0);
555 let mut stepper = UnifiedTimeStepper::new(vec![cfg]);
556 stepper.update_domain_dt(0, 0.01);
557 stepper.compute_global_dt();
558
559 for _ in 0..100 {
560 stepper.advance_global_time();
561 }
562 let expected_time = 100.0 * stepper.global_dt;
563 assert!(
564 (stepper.global_time - expected_time).abs() < 1e-10,
565 "time={}, expected={}",
566 stepper.global_time,
567 expected_time
568 );
569 }
570
571 #[test]
574 fn test_update_domain_error() {
575 let cfg = TimeDomainConfig::new(1e-6, 1.0, 0.5, 1.0);
576 let mut stepper = UnifiedTimeStepper::new(vec![cfg]);
577 assert!(stepper.update_domain_error(0, 1e-5));
578 assert!((stepper.domains[0].error_estimate - 1e-5).abs() < 1e-20);
579 }
580
581 #[test]
582 fn test_update_domain_error_invalid_index() {
583 let cfg = TimeDomainConfig::new(1e-6, 1.0, 0.5, 1.0);
584 let mut stepper = UnifiedTimeStepper::new(vec![cfg]);
585 assert!(!stepper.update_domain_error(5, 1e-5));
586 }
587
588 #[test]
589 fn test_update_domain_dt_clamps() {
590 let cfg = TimeDomainConfig::new(1e-6, 1e-2, 0.5, 1.0);
591 let mut stepper = UnifiedTimeStepper::new(vec![cfg]);
592
593 stepper.update_domain_dt(0, 1.0);
595 assert!((stepper.domains[0].current_dt - 1e-2).abs() < 1e-14);
596
597 stepper.update_domain_dt(0, 1e-10);
599 assert!((stepper.domains[0].current_dt - 1e-6).abs() < 1e-14);
600 }
601
602 #[test]
603 fn test_update_domain_dt_invalid_index() {
604 let cfg = TimeDomainConfig::new(1e-6, 1.0, 0.5, 1.0);
605 let mut stepper = UnifiedTimeStepper::new(vec![cfg]);
606 assert!(!stepper.update_domain_dt(99, 0.01));
607 }
608
609 #[test]
612 fn test_richardson_driven_adaptation() {
613 let cfg = TimeDomainConfig::new(1e-8, 1.0, 0.5, 0.9);
614 let mut stepper = UnifiedTimeStepper::new(vec![cfg]);
615 stepper.update_domain_dt(0, 0.01);
616
617 let tolerance = 1e-4;
618 let order = 2_u32;
619
620 let error = 1e-2;
622 let new_dt = richardson_dt(stepper.domains[0].current_dt, error, tolerance, order);
623 stepper.update_domain_dt(0, new_dt);
624 stepper.compute_global_dt();
625 assert!(stepper.global_dt < 0.01 * 0.9, "should have shrunk");
626
627 let error2 = 1e-10;
629 let new_dt2 = richardson_dt(stepper.domains[0].current_dt, error2, tolerance, order);
630 stepper.update_domain_dt(0, new_dt2);
631 stepper.compute_global_dt();
632 assert!(
633 stepper.domains[0].current_dt > stepper.domains[0].config.min_dt,
634 "should have grown"
635 );
636 }
637
638 #[test]
641 fn test_subcycling_fast_domain() {
642 let d0 = TimeDomainConfig::new(1e-6, 1.0, 0.5, 1.0);
652 let d1 = TimeDomainConfig::new(1e-6, 1.0, 0.5, 1.0);
653 let mut stepper = UnifiedTimeStepper::new(vec![d0, d1]);
654
655 stepper.update_domain_dt(0, 0.1);
657 stepper.update_domain_dt(1, 0.025);
658 stepper.compute_global_dt();
659
660 assert_eq!(stepper.subcycle_ratios[0], 1);
662 assert_eq!(stepper.subcycle_ratios[1], 1);
663 assert!((stepper.global_dt - 0.025).abs() < 1e-14);
664 }
665
666 #[test]
669 fn test_empty_domains() {
670 let stepper = UnifiedTimeStepper::new(vec![]);
671 assert_eq!(stepper.domains.len(), 0);
672 assert_eq!(stepper.subcycle_ratios.len(), 0);
673 assert!(stepper.step_schedule().is_empty());
674 }
675
676 #[test]
677 fn test_identical_domains() {
678 let c1 = TimeDomainConfig::new(1e-6, 1.0, 0.5, 0.9);
679 let c2 = TimeDomainConfig::new(1e-6, 1.0, 0.5, 0.9);
680 let mut stepper = UnifiedTimeStepper::new(vec![c1, c2]);
681
682 stepper.update_domain_dt(0, 0.01);
683 stepper.update_domain_dt(1, 0.01);
684 stepper.compute_global_dt();
685
686 assert_eq!(stepper.subcycle_ratios[0], 1);
687 assert_eq!(stepper.subcycle_ratios[1], 1);
688 assert!((stepper.global_dt - 0.01 * 0.9).abs() < 1e-14);
689 }
690
691 #[test]
692 fn test_global_dt_respects_min_dt_bound() {
693 let c1 = TimeDomainConfig::new(0.001, 1.0, 0.5, 0.01);
696 let mut stepper = UnifiedTimeStepper::new(vec![c1]);
697 stepper.update_domain_dt(0, 0.001); let g = stepper.compute_global_dt();
699 assert!(g >= 0.001, "global_dt={g} should be >= min_dt=0.001");
700 }
701
702 #[test]
703 fn test_negative_velocity_cfl() {
704 let dt = cfl_timestep(-100.0, 1.0, 0.5);
706 let expected = 0.5 * 1.0 / 100.0;
707 assert!((dt - expected).abs() < 1e-14);
708 }
709
710 #[test]
711 fn test_diffusive_dt_negative_nu() {
712 let dt = diffusive_dt(0.1, -0.01, 0.9);
714 let expected = 0.9 * 0.01 / 0.02;
715 assert!((dt - expected).abs() < 1e-14);
716 }
717}