1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
//! Shared atomic progress state for intra-step/intra-build visibility.
//!
//! `StepProgress` uses atomics for lock-free, allocation-free updates.
//! The simulation thread writes phases; the TUI thread reads snapshots
//! on each ~33ms render frame.
use std::sync::Arc;
use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
/// Enumeration of all possible progress phases.
///
/// Build phases (1–8) are set by phasma before caustic starts stepping.
/// Step phases (10+) are set by integrators and `Simulation::step()`.
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StepPhase {
/// No computation in progress; the simulation is idle.
Idle = 0,
// Build phases (set by phasma, not caustic)
/// Constructing the computational domain (spatial/velocity extents, resolution, boundaries).
BuildDomain = 1,
/// Generating initial conditions (sampling the distribution function).
BuildIC = 2,
/// Sampling phase-space points for the initial distribution function.
BuildICSampling = 7,
/// Compressing the initial conditions into a low-rank representation (HT/TT).
BuildICCompression = 8,
/// Initializing the Poisson solver (FFT plans, Green's function, tree structure).
BuildPoisson = 3,
/// Constructing the time integrator (splitting scheme, RK tableaux).
BuildIntegrator = 4,
/// Registering exit/termination conditions (energy drift, wall clock, etc.).
BuildExitConditions = 5,
/// Final assembly of the simulation object from all constructed components.
BuildAssembly = 6,
// Strang sub-phases
/// First half-step spatial drift in Strang splitting (advance x by v*dt/2).
DriftHalf1 = 10,
/// Computing the density field rho(x) by integrating f over velocity space.
ComputeDensity = 11,
/// Solving the Poisson equation nabla^2 Phi = 4*pi*G*rho for the gravitational potential.
PoissonSolve = 12,
/// Computing the acceleration field g = -nabla(Phi) from the potential.
ComputeAcceleration = 13,
/// Full velocity kick in Strang splitting (advance v by g*dt).
Kick = 14,
/// Second half-step spatial drift in Strang splitting (advance x by v*dt/2).
DriftHalf2 = 15,
// Post-advance (Simulation::step)
/// Applying the LoMaC conservative projection to restore mass/momentum/energy conservation.
LoMaC = 16,
/// Recomputing the density field after the time step for diagnostics.
PostDensity = 17,
/// Computing global diagnostics (energy, momentum, Casimir invariants, etc.).
Diagnostics = 18,
/// The time step has completed; all sub-phases finished.
StepComplete = 19,
// Yoshida (7 sub-steps)
/// Yoshida 4th-order splitting: first drift sub-step (c1*dt).
YoshidaDrift1 = 20,
/// Yoshida 4th-order splitting: first kick sub-step (d1*dt).
YoshidaKick1 = 21,
/// Yoshida 4th-order splitting: second drift sub-step (c2*dt).
YoshidaDrift2 = 22,
/// Yoshida 4th-order splitting: second kick sub-step (d2*dt).
YoshidaKick2 = 23,
/// Yoshida 4th-order splitting: third drift sub-step (c3*dt).
YoshidaDrift3 = 24,
/// Yoshida 4th-order splitting: third kick sub-step (d3*dt).
YoshidaKick3 = 25,
/// Yoshida 4th-order splitting: fourth (final) drift sub-step (c4*dt).
YoshidaDrift4 = 26,
// RKEI (3 stages)
/// RKEI (SSP-RK3 exponential integrator): first stage evaluation.
RkeiStage1 = 30,
/// RKEI (SSP-RK3 exponential integrator): second stage evaluation.
RkeiStage2 = 31,
/// RKEI (SSP-RK3 exponential integrator): third stage evaluation and combination.
RkeiStage3 = 32,
// Unsplit RK stages
/// Unsplit Runge-Kutta integrator: first stage (used by RK2, RK3, and RK4).
UnsplitStage1 = 40,
/// Unsplit Runge-Kutta integrator: second stage (used by RK2, RK3, and RK4).
UnsplitStage2 = 41,
/// Unsplit Runge-Kutta integrator: third stage (used by RK3 and RK4).
UnsplitStage3 = 42,
/// Unsplit Runge-Kutta integrator: fourth stage (used by RK4 only).
UnsplitStage4 = 43,
// Adaptive error estimation
/// Adaptive time-stepping: computing the Lie (first-order) estimate for error control.
AdaptiveLie = 44,
/// Adaptive time-stepping: applying the velocity kick in the Lie estimate.
AdaptiveLieKick = 45,
/// Adaptive time-stepping: computing the error norm between splitting orders.
AdaptiveError = 46,
// BM4 (6-stage, 11 sub-steps)
/// Blanes-Moan 4th-order splitting: first drift sub-step (a1*dt).
Bm4Sub0 = 50,
/// Blanes-Moan 4th-order splitting: first kick sub-step (b1*dt).
Bm4Sub1 = 51,
/// Blanes-Moan 4th-order splitting: second drift sub-step (a2*dt).
Bm4Sub2 = 52,
/// Blanes-Moan 4th-order splitting: second kick sub-step (b2*dt).
Bm4Sub3 = 53,
/// Blanes-Moan 4th-order splitting: third drift sub-step (a3*dt).
Bm4Sub4 = 54,
/// Blanes-Moan 4th-order splitting: third kick sub-step (b3*dt).
Bm4Sub5 = 55,
/// Blanes-Moan 4th-order splitting: fourth drift sub-step (a4*dt).
Bm4Sub6 = 56,
/// Blanes-Moan 4th-order splitting: fourth kick sub-step (b4*dt).
Bm4Sub7 = 57,
/// Blanes-Moan 4th-order splitting: fifth drift sub-step (a5*dt).
Bm4Sub8 = 58,
/// Blanes-Moan 4th-order splitting: fifth kick sub-step (b5*dt).
Bm4Sub9 = 59,
// RKN6 (triple-jump composition)
/// 6th-order Runge-Kutta-Nystrom triple-jump composition: first Strang block.
Rkn6Phase1 = 60,
/// 6th-order Runge-Kutta-Nystrom triple-jump composition: second (scaled) Strang block.
Rkn6Phase2 = 61,
/// 6th-order Runge-Kutta-Nystrom triple-jump composition: third Strang block.
Rkn6Phase3 = 62,
// BUG integrator
/// BUG integrator: K-step (kinetic/drift sub-step).
BugKStep = 80,
/// BUG integrator: L-step (Lie/interaction sub-step).
BugLStep = 81,
/// BUG integrator: S-step (Strang-like combined sub-step).
BugSStep = 82,
}
impl StepPhase {
/// Convert from raw u8, returning `Idle` for unknown values.
pub fn from_u8(v: u8) -> Self {
match v {
0 => Self::Idle,
1 => Self::BuildDomain,
2 => Self::BuildIC,
3 => Self::BuildPoisson,
4 => Self::BuildIntegrator,
5 => Self::BuildExitConditions,
6 => Self::BuildAssembly,
7 => Self::BuildICSampling,
8 => Self::BuildICCompression,
10 => Self::DriftHalf1,
11 => Self::ComputeDensity,
12 => Self::PoissonSolve,
13 => Self::ComputeAcceleration,
14 => Self::Kick,
15 => Self::DriftHalf2,
16 => Self::LoMaC,
17 => Self::PostDensity,
18 => Self::Diagnostics,
19 => Self::StepComplete,
20 => Self::YoshidaDrift1,
21 => Self::YoshidaKick1,
22 => Self::YoshidaDrift2,
23 => Self::YoshidaKick2,
24 => Self::YoshidaDrift3,
25 => Self::YoshidaKick3,
26 => Self::YoshidaDrift4,
30 => Self::RkeiStage1,
31 => Self::RkeiStage2,
32 => Self::RkeiStage3,
40 => Self::UnsplitStage1,
41 => Self::UnsplitStage2,
42 => Self::UnsplitStage3,
43 => Self::UnsplitStage4,
44 => Self::AdaptiveLie,
45 => Self::AdaptiveLieKick,
46 => Self::AdaptiveError,
50 => Self::Bm4Sub0,
51 => Self::Bm4Sub1,
52 => Self::Bm4Sub2,
53 => Self::Bm4Sub3,
54 => Self::Bm4Sub4,
55 => Self::Bm4Sub5,
56 => Self::Bm4Sub6,
57 => Self::Bm4Sub7,
58 => Self::Bm4Sub8,
59 => Self::Bm4Sub9,
60 => Self::Rkn6Phase1,
61 => Self::Rkn6Phase2,
62 => Self::Rkn6Phase3,
80 => Self::BugKStep,
81 => Self::BugLStep,
82 => Self::BugSStep,
_ => Self::Idle,
}
}
/// Human-readable label for display.
pub fn label(self) -> &'static str {
match self {
Self::Idle => "Idle",
Self::BuildDomain => "Domain",
Self::BuildIC => "IC generation",
Self::BuildICSampling => "IC sampling",
Self::BuildICCompression => "IC compression",
Self::BuildPoisson => "Poisson solver",
Self::BuildIntegrator => "Integrator",
Self::BuildExitConditions => "Exit conditions",
Self::BuildAssembly => "Assembly",
Self::DriftHalf1 => "Drift ½",
Self::ComputeDensity => "Density",
Self::PoissonSolve => "Poisson solve",
Self::ComputeAcceleration => "Acceleration",
Self::Kick => "Kick",
Self::DriftHalf2 => "Drift ½",
Self::LoMaC => "LoMaC",
Self::PostDensity => "Post density",
Self::Diagnostics => "Diagnostics",
Self::StepComplete => "Complete",
Self::YoshidaDrift1 => "Drift 1",
Self::YoshidaKick1 => "Kick 1",
Self::YoshidaDrift2 => "Drift 2",
Self::YoshidaKick2 => "Kick 2",
Self::YoshidaDrift3 => "Drift 3",
Self::YoshidaKick3 => "Kick 3",
Self::YoshidaDrift4 => "Drift 4",
Self::RkeiStage1 => "Stage 1",
Self::RkeiStage2 => "Stage 2",
Self::RkeiStage3 => "Stage 3",
Self::UnsplitStage1 => "Stage 1",
Self::UnsplitStage2 => "Stage 2",
Self::UnsplitStage3 => "Stage 3",
Self::UnsplitStage4 => "Stage 4",
Self::AdaptiveLie => "Lie estimate",
Self::AdaptiveLieKick => "Lie kick",
Self::AdaptiveError => "Error estimate",
Self::Bm4Sub0 => "BM4 drift 1",
Self::Bm4Sub1 => "BM4 kick 1",
Self::Bm4Sub2 => "BM4 drift 2",
Self::Bm4Sub3 => "BM4 kick 2",
Self::Bm4Sub4 => "BM4 drift 3",
Self::Bm4Sub5 => "BM4 kick 3",
Self::Bm4Sub6 => "BM4 drift 4",
Self::Bm4Sub7 => "BM4 kick 4",
Self::Bm4Sub8 => "BM4 drift 5",
Self::Bm4Sub9 => "BM4 kick 5",
Self::Rkn6Phase1 => "RKN6 phase 1",
Self::Rkn6Phase2 => "RKN6 phase 2",
Self::Rkn6Phase3 => "RKN6 phase 3",
Self::BugKStep => "BUG K-step",
Self::BugLStep => "BUG L-step",
Self::BugSStep => "BUG S-step",
}
}
/// Whether this is a build phase (set by phasma before caustic starts stepping).
pub fn is_build(self) -> bool {
matches!(
self,
Self::BuildDomain
| Self::BuildIC
| Self::BuildICSampling
| Self::BuildICCompression
| Self::BuildPoisson
| Self::BuildIntegrator
| Self::BuildExitConditions
| Self::BuildAssembly
)
}
}
/// Shared atomic progress state. Written by the sim thread, read by the TUI.
pub struct StepProgress {
phase: AtomicU8,
sub_step: AtomicU8,
sub_step_total: AtomicU8,
sub_done: AtomicU64,
sub_total: AtomicU64,
step_start_ns: AtomicU64,
}
/// A consistent snapshot of progress at one instant.
pub struct ProgressSnapshot {
pub phase: StepPhase,
pub sub_step: u8,
pub sub_step_total: u8,
pub sub_done: u64,
pub sub_total: u64,
pub elapsed_ms: f64,
}
impl StepProgress {
/// Create a new `StepProgress` wrapped in `Arc` for sharing.
pub fn new() -> Arc<Self> {
Arc::new(Self {
phase: AtomicU8::new(0),
sub_step: AtomicU8::new(0),
sub_step_total: AtomicU8::new(0),
sub_done: AtomicU64::new(0),
sub_total: AtomicU64::new(0),
step_start_ns: AtomicU64::new(now_ns()),
})
}
/// Set the current phase.
pub fn set_phase(&self, phase: StepPhase) {
self.phase.store(phase as u8, Ordering::Relaxed);
// Reset intra-phase counters
self.sub_done.store(0, Ordering::Relaxed);
self.sub_total.store(0, Ordering::Relaxed);
}
/// Set sub-step index and total (e.g. 2 of 5 for Strang).
pub fn set_sub_step(&self, current: u8, total: u8) {
self.sub_step.store(current, Ordering::Relaxed);
self.sub_step_total.store(total, Ordering::Relaxed);
}
/// Set intra-phase work units (e.g. grid cells processed).
pub fn set_intra_progress(&self, done: u64, total: u64) {
self.sub_done.store(done, Ordering::Relaxed);
self.sub_total.store(total, Ordering::Relaxed);
}
/// Mark the start of a new step (resets timer).
pub fn start_step(&self) {
self.step_start_ns.store(now_ns(), Ordering::Relaxed);
self.sub_step.store(0, Ordering::Relaxed);
self.sub_step_total.store(0, Ordering::Relaxed);
self.sub_done.store(0, Ordering::Relaxed);
self.sub_total.store(0, Ordering::Relaxed);
}
/// Read a consistent-enough snapshot (all Relaxed loads).
pub fn read(&self) -> ProgressSnapshot {
let start = self.step_start_ns.load(Ordering::Relaxed);
let now = now_ns();
let elapsed_ns = now.saturating_sub(start);
ProgressSnapshot {
phase: StepPhase::from_u8(self.phase.load(Ordering::Relaxed)),
sub_step: self.sub_step.load(Ordering::Relaxed),
sub_step_total: self.sub_step_total.load(Ordering::Relaxed),
sub_done: self.sub_done.load(Ordering::Relaxed),
sub_total: self.sub_total.load(Ordering::Relaxed),
elapsed_ms: elapsed_ns as f64 / 1_000_000.0,
}
}
}
fn now_ns() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64
}