Skip to main content

caustic/tooling/core/time/
rkn6.rs

1//! 6th-order symplectic integrator via Suzuki triple-jump composition.
2//!
3//! Achieves 6th-order accuracy by composing three 4th-order Yoshida (Strang)
4//! blocks with Suzuki (1990) / Kahan & Li (1997) triple-jump coefficients:
5//!
6//!   S₆(Δt) = S₄(s₁·Δt) . S₄(s₂·Δt) . S₄(s₁·Δt)
7//!
8//! where s₁ = 1/(2 - 2^{1/5}), s₂ = 1 - 2*s₁. The negative middle
9//! coefficient s₂ cancels the 5th-order error term from the outer blocks.
10//! Each inner S₄ step is a standard Yoshida 4th-order splitting with 7
11//! sub-steps (4 drifts + 3 kicks), giving 3 x 7 = 21 sub-steps total,
12//! reduced to 19 when adjacent boundary drifts are merged at the two
13//! composition seams. Provides much tighter energy conservation than 4th-order
14//! methods at the cost of roughly 3x more Poisson solves per time step.
15
16use std::sync::Arc;
17
18use super::super::{
19    advecator::Advector,
20    integrator::{StepProducts, StepTimings, TimeIntegrator},
21    phasespace::PhaseSpaceRepr,
22    progress::{StepPhase, StepProgress},
23    solver::PoissonSolver,
24    types::*,
25};
26use super::helpers;
27use crate::CausticError;
28
29/// Suzuki triple-jump coefficient s₁ = 1 / (2 − 2^{1/5}).
30const S1: f64 = 1.1746881100325735;
31/// Suzuki triple-jump coefficient s₂ = 1 − 2·s₁.
32const S2: f64 = -1.349_376_220_065_147;
33
34/// Yoshida coefficient w₁ = 1 / (2 − 2^{1/3}).
35const YOSHIDA_W1: f64 = 1.3512071919596578;
36/// Yoshida coefficient w₀ = 1 − 2·w₁.
37const YOSHIDA_W0: f64 = -1.7024143839193153;
38
39/// 6th-order symplectic integrator (triple-jump composition of Yoshida).
40///
41/// Higher accuracy than Yoshida alone (4th-order), at the cost of 3x
42/// the sub-steps per time step. Useful when very tight energy conservation
43/// is required or when large time steps are desirable.
44pub struct Rkn6Splitting {
45    /// Gravitational constant G used in the Poisson solve.
46    pub g: f64,
47    /// Timing breakdown from the most recent `advance` call.
48    last_timings: StepTimings,
49    /// Optional lock-free progress reporter for TUI sub-step tracking.
50    progress: Option<Arc<StepProgress>>,
51}
52
53impl Rkn6Splitting {
54    /// Creates a 6th-order triple-jump integrator with the given gravitational constant.
55    pub fn new(g: f64) -> Self {
56        Self {
57            g,
58            last_timings: StepTimings::default(),
59            progress: None,
60        }
61    }
62
63    /// Single drift sub-step with progress reporting and timing.
64    #[inline]
65    fn rkn6_drift(
66        repr: &mut dyn PhaseSpaceRepr,
67        advector: &dyn Advector,
68        coeff: f64,
69        timings: &mut StepTimings,
70        progress: &Option<Arc<StepProgress>>,
71        phase: StepPhase,
72        sub: u8,
73    ) {
74        helpers::report_phase!(progress, phase, sub, 19);
75        let _s = tracing::info_span!("rkn6_drift").entered();
76        helpers::time_ms!(timings, drift_ms, advector.drift(repr, coeff));
77    }
78
79    /// Single kick sub-step (density → potential → acceleration → kick) with timing.
80    #[inline]
81    #[allow(clippy::too_many_arguments)]
82    fn rkn6_kick(
83        g: f64,
84        repr: &mut dyn PhaseSpaceRepr,
85        solver: &dyn PoissonSolver,
86        advector: &dyn Advector,
87        coeff: f64,
88        timings: &mut StepTimings,
89        progress: &Option<Arc<StepProgress>>,
90        sub: u8,
91    ) {
92        helpers::report_phase!(progress, StepPhase::Kick, sub, 19);
93        let _s = tracing::info_span!("rkn6_kick").entered();
94        let (_density, _potential, accel) =
95            helpers::time_ms!(timings, poisson_ms, helpers::solve_poisson(repr, solver, g));
96        helpers::time_ms!(timings, kick_ms, advector.kick(repr, &accel, coeff));
97    }
98
99    /// Execute one Yoshida 4th-order step with the given (scaled) time step.
100    ///
101    /// Pattern: D(w₁/2) K(w₁) D((w₁+w₀)/2) K(w₀) D((w₀+w₁)/2) K(w₁) D(w₁/2)
102    #[allow(clippy::too_many_arguments)]
103    fn yoshida_step(
104        &self,
105        repr: &mut dyn PhaseSpaceRepr,
106        solver: &dyn PoissonSolver,
107        advector: &dyn Advector,
108        dt: f64,
109        timings: &mut StepTimings,
110        progress: &Option<Arc<StepProgress>>,
111        base_sub: u8,
112        total_sub: u8,
113    ) {
114        // Sub-step 1: drift w1·dt/2
115        helpers::report_phase!(progress, StepPhase::DriftHalf1, base_sub, total_sub);
116        {
117            let _s = tracing::info_span!("rkn6_drift").entered();
118            helpers::time_ms!(
119                timings,
120                drift_ms,
121                advector.drift(repr, YOSHIDA_W1 * dt / 2.0)
122            );
123        }
124
125        // Sub-step 2: kick w1·dt
126        helpers::report_phase!(progress, StepPhase::Kick, base_sub + 1, total_sub);
127        {
128            let _s = tracing::info_span!("rkn6_kick").entered();
129            let (_density, _potential, accel) = helpers::time_ms!(
130                timings,
131                poisson_ms,
132                helpers::solve_poisson(repr, solver, self.g)
133            );
134            helpers::time_ms!(
135                timings,
136                kick_ms,
137                advector.kick(repr, &accel, YOSHIDA_W1 * dt)
138            );
139        }
140
141        // Sub-step 3: drift (w1+w0)·dt/2
142        helpers::report_phase!(progress, StepPhase::DriftHalf2, base_sub + 2, total_sub);
143        {
144            let _s = tracing::info_span!("rkn6_drift").entered();
145            helpers::time_ms!(
146                timings,
147                drift_ms,
148                advector.drift(repr, (YOSHIDA_W1 + YOSHIDA_W0) * dt / 2.0)
149            );
150        }
151
152        // Sub-step 4: kick w0·dt
153        helpers::report_phase!(progress, StepPhase::Kick, base_sub + 3, total_sub);
154        {
155            let _s = tracing::info_span!("rkn6_kick").entered();
156            let (_density, _potential, accel) = helpers::time_ms!(
157                timings,
158                poisson_ms,
159                helpers::solve_poisson(repr, solver, self.g)
160            );
161            helpers::time_ms!(
162                timings,
163                kick_ms,
164                advector.kick(repr, &accel, YOSHIDA_W0 * dt)
165            );
166        }
167
168        // Sub-step 5: drift (w0+w1)·dt/2
169        helpers::report_phase!(progress, StepPhase::DriftHalf1, base_sub + 4, total_sub);
170        {
171            let _s = tracing::info_span!("rkn6_drift").entered();
172            helpers::time_ms!(
173                timings,
174                drift_ms,
175                advector.drift(repr, (YOSHIDA_W0 + YOSHIDA_W1) * dt / 2.0)
176            );
177        }
178
179        // Sub-step 6: kick w1·dt
180        helpers::report_phase!(progress, StepPhase::Kick, base_sub + 5, total_sub);
181        {
182            let _s = tracing::info_span!("rkn6_kick").entered();
183            let (_density, _potential, accel) = helpers::time_ms!(
184                timings,
185                poisson_ms,
186                helpers::solve_poisson(repr, solver, self.g)
187            );
188            helpers::time_ms!(
189                timings,
190                kick_ms,
191                advector.kick(repr, &accel, YOSHIDA_W1 * dt)
192            );
193        }
194
195        // Sub-step 7: drift w1·dt/2
196        helpers::report_phase!(progress, StepPhase::DriftHalf2, base_sub + 6, total_sub);
197        {
198            let _s = tracing::info_span!("rkn6_drift").entered();
199            helpers::time_ms!(
200                timings,
201                drift_ms,
202                advector.drift(repr, YOSHIDA_W1 * dt / 2.0)
203            );
204        }
205    }
206}
207
208impl TimeIntegrator for Rkn6Splitting {
209    fn advance(
210        &mut self,
211        repr: &mut dyn PhaseSpaceRepr,
212        solver: &dyn PoissonSolver,
213        advector: &dyn Advector,
214        dt: f64,
215    ) -> Result<StepProducts, CausticError> {
216        let _span = tracing::info_span!("rkn6_advance").entered();
217        let mut timings = StepTimings::default();
218        let g = self.g;
219        let progress = self.progress.clone();
220
221        if let Some(ref p) = progress {
222            p.start_step();
223        }
224
225        // Inlined 19-sub-step sequence: three Yoshida S₄ steps composed via
226        // Suzuki triple-jump, with adjacent boundary drifts merged at the two
227        // composition seams (saves 2 drift operations vs the naive 21-step form).
228        //
229        // S₆(Δt) = S₄(s₁·Δt) ∘ S₄(s₂·Δt) ∘ S₄(s₁·Δt)
230
231        let s1dt = S1 * dt;
232        let s2dt = S2 * dt;
233
234        // Drift coefficients
235        let d_half_w1_s1 = YOSHIDA_W1 * s1dt / 2.0;
236        let d_half_w01_s1 = (YOSHIDA_W1 + YOSHIDA_W0) * s1dt / 2.0;
237        let d_half_w01_s2 = (YOSHIDA_W1 + YOSHIDA_W0) * s2dt / 2.0;
238        // Merged boundary drift at composition seams: w₁·(s₁+s₂)·dt/2
239        let d_seam = YOSHIDA_W1 * (S1 + S2) * dt / 2.0;
240
241        // Kick coefficients
242        let k_w1_s1 = YOSHIDA_W1 * s1dt;
243        let k_w0_s1 = YOSHIDA_W0 * s1dt;
244        let k_w1_s2 = YOSHIDA_W1 * s2dt;
245        let k_w0_s2 = YOSHIDA_W0 * s2dt;
246
247        // ── S₄(s₁·Δt) ─────────────────────────────────────────────────────
248        Self::rkn6_drift(
249            repr,
250            advector,
251            d_half_w1_s1,
252            &mut timings,
253            &progress,
254            StepPhase::DriftHalf1,
255            0,
256        );
257        Self::rkn6_kick(
258            g,
259            repr,
260            solver,
261            advector,
262            k_w1_s1,
263            &mut timings,
264            &progress,
265            1,
266        );
267        Self::rkn6_drift(
268            repr,
269            advector,
270            d_half_w01_s1,
271            &mut timings,
272            &progress,
273            StepPhase::DriftHalf2,
274            2,
275        );
276        Self::rkn6_kick(
277            g,
278            repr,
279            solver,
280            advector,
281            k_w0_s1,
282            &mut timings,
283            &progress,
284            3,
285        );
286        Self::rkn6_drift(
287            repr,
288            advector,
289            d_half_w01_s1,
290            &mut timings,
291            &progress,
292            StepPhase::DriftHalf1,
293            4,
294        );
295        Self::rkn6_kick(
296            g,
297            repr,
298            solver,
299            advector,
300            k_w1_s1,
301            &mut timings,
302            &progress,
303            5,
304        );
305
306        // Merged: last drift of S₄(s₁) + first drift of S₄(s₂)
307        Self::rkn6_drift(
308            repr,
309            advector,
310            d_seam,
311            &mut timings,
312            &progress,
313            StepPhase::DriftHalf2,
314            6,
315        );
316
317        // ── S₄(s₂·Δt) interior ─────────────────────────────────────────────
318        Self::rkn6_kick(
319            g,
320            repr,
321            solver,
322            advector,
323            k_w1_s2,
324            &mut timings,
325            &progress,
326            7,
327        );
328        Self::rkn6_drift(
329            repr,
330            advector,
331            d_half_w01_s2,
332            &mut timings,
333            &progress,
334            StepPhase::DriftHalf1,
335            8,
336        );
337        Self::rkn6_kick(
338            g,
339            repr,
340            solver,
341            advector,
342            k_w0_s2,
343            &mut timings,
344            &progress,
345            9,
346        );
347        Self::rkn6_drift(
348            repr,
349            advector,
350            d_half_w01_s2,
351            &mut timings,
352            &progress,
353            StepPhase::DriftHalf2,
354            10,
355        );
356        Self::rkn6_kick(
357            g,
358            repr,
359            solver,
360            advector,
361            k_w1_s2,
362            &mut timings,
363            &progress,
364            11,
365        );
366
367        // Merged: last drift of S₄(s₂) + first drift of S₄(s₁)
368        Self::rkn6_drift(
369            repr,
370            advector,
371            d_seam,
372            &mut timings,
373            &progress,
374            StepPhase::DriftHalf1,
375            12,
376        );
377
378        // ── S₄(s₁·Δt) ─────────────────────────────────────────────────────
379        Self::rkn6_kick(
380            g,
381            repr,
382            solver,
383            advector,
384            k_w1_s1,
385            &mut timings,
386            &progress,
387            13,
388        );
389        Self::rkn6_drift(
390            repr,
391            advector,
392            d_half_w01_s1,
393            &mut timings,
394            &progress,
395            StepPhase::DriftHalf2,
396            14,
397        );
398        Self::rkn6_kick(
399            g,
400            repr,
401            solver,
402            advector,
403            k_w0_s1,
404            &mut timings,
405            &progress,
406            15,
407        );
408        Self::rkn6_drift(
409            repr,
410            advector,
411            d_half_w01_s1,
412            &mut timings,
413            &progress,
414            StepPhase::DriftHalf1,
415            16,
416        );
417        Self::rkn6_kick(
418            g,
419            repr,
420            solver,
421            advector,
422            k_w1_s1,
423            &mut timings,
424            &progress,
425            17,
426        );
427        Self::rkn6_drift(
428            repr,
429            advector,
430            d_half_w1_s1,
431            &mut timings,
432            &progress,
433            StepPhase::DriftHalf2,
434            18,
435        );
436
437        helpers::report_phase!(progress, StepPhase::StepComplete, 19, 19);
438
439        // Compute end-of-step products for caller reuse
440        let (density, potential, acceleration) = helpers::time_ms!(
441            timings,
442            density_ms,
443            helpers::solve_poisson(repr, solver, self.g)
444        );
445
446        self.last_timings = timings;
447
448        Ok(StepProducts {
449            density,
450            potential,
451            acceleration,
452        })
453    }
454
455    fn max_dt(&self, repr: &dyn PhaseSpaceRepr, cfl_factor: f64) -> f64 {
456        helpers::dynamical_timestep(repr, self.g, cfl_factor)
457    }
458
459    fn last_step_timings(&self) -> Option<&StepTimings> {
460        Some(&self.last_timings)
461    }
462
463    fn set_progress(&mut self, progress: Arc<StepProgress>) {
464        self.progress = Some(progress);
465    }
466}