1use serde::{Deserialize, Serialize};
18use std::time::{Duration, Instant};
19
20use crate::error::SimResult;
21use crate::orbit::physics::{NBodyState, YoshidaIntegrator};
22use crate::orbit::units::OrbitTime;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
26pub enum QualityLevel {
27 Minimum,
29 Low,
31 Medium,
33 #[default]
35 High,
36 Maximum,
38}
39
40impl QualityLevel {
41 #[must_use]
43 pub fn degrade(self) -> Self {
44 match self {
45 Self::Maximum => Self::High,
46 Self::High => Self::Medium,
47 Self::Medium => Self::Low,
48 Self::Low | Self::Minimum => Self::Minimum,
49 }
50 }
51
52 #[must_use]
54 pub fn upgrade(self) -> Self {
55 match self {
56 Self::Minimum => Self::Low,
57 Self::Low => Self::Medium,
58 Self::Medium => Self::High,
59 Self::High | Self::Maximum => Self::Maximum,
60 }
61 }
62
63 #[must_use]
65 pub fn substep_multiplier(self) -> usize {
66 contract_pre_iterator!();
67 match self {
68 Self::Minimum => 1,
69 Self::Low => 2,
70 Self::Medium => 4,
71 Self::High => 8,
72 Self::Maximum => 16,
73 }
74 }
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct HeijunkaConfig {
80 pub frame_budget_ms: f64,
82 pub physics_budget_fraction: f64,
84 pub base_dt: f64,
86 pub max_substeps: usize,
88 pub min_substeps: usize,
90 pub auto_adjust_quality: bool,
92 pub upgrade_threshold: usize,
94}
95
96impl Default for HeijunkaConfig {
97 fn default() -> Self {
98 Self {
99 frame_budget_ms: 16.0, physics_budget_fraction: 0.5, base_dt: 3600.0, max_substeps: 100,
103 min_substeps: 1,
104 auto_adjust_quality: true,
105 upgrade_threshold: 10,
106 }
107 }
108}
109
110impl HeijunkaConfig {
111 #[must_use]
113 pub fn physics_budget_ms(&self) -> f64 {
114 self.frame_budget_ms * self.physics_budget_fraction
115 }
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct FrameResult {
121 pub substeps: usize,
123 pub physics_time_ms: f64,
125 pub quality: QualityLevel,
127 pub budget_exceeded: bool,
129 pub sim_time_advanced: f64,
131}
132
133#[derive(Debug, Clone, Default, Serialize, Deserialize)]
135pub struct HeijunkaStatus {
136 pub budget_ms: f64,
138 pub used_ms: f64,
140 pub substeps: usize,
142 pub quality: QualityLevel,
144 pub avg_physics_ms: f64,
146 pub utilization: f64,
148}
149
150#[derive(Debug, Clone)]
152pub struct HeijunkaScheduler {
153 config: HeijunkaConfig,
154 quality: QualityLevel,
155 integrator: YoshidaIntegrator,
156 consecutive_under_budget: usize,
157 physics_times: Vec<f64>,
158 status: HeijunkaStatus,
159}
160
161impl HeijunkaScheduler {
162 #[must_use]
164 pub fn new(config: HeijunkaConfig) -> Self {
165 Self {
166 config,
167 quality: QualityLevel::default(),
168 integrator: YoshidaIntegrator::new(),
169 consecutive_under_budget: 0,
170 physics_times: Vec::with_capacity(100),
171 status: HeijunkaStatus::default(),
172 }
173 }
174
175 #[must_use]
177 pub fn quality(&self) -> QualityLevel {
178 self.quality
179 }
180
181 pub fn set_quality(&mut self, quality: QualityLevel) {
183 self.quality = quality;
184 }
185
186 #[must_use]
188 pub fn status(&self) -> &HeijunkaStatus {
189 &self.status
190 }
191
192 pub fn execute_frame(&mut self, state: &mut NBodyState) -> SimResult<FrameResult> {
198 let budget_ms = self.config.physics_budget_ms();
199 let budget_duration = Duration::from_secs_f64(budget_ms / 1000.0);
200
201 let target_substeps = self
202 .quality
203 .substep_multiplier()
204 .min(self.config.max_substeps)
205 .max(self.config.min_substeps);
206
207 let start = Instant::now();
208 let mut substeps = 0;
209 let mut sim_time_advanced = 0.0;
210
211 while substeps < target_substeps {
213 let elapsed = start.elapsed();
215 if elapsed >= budget_duration && substeps > 0 {
216 break;
217 }
218
219 let dt = OrbitTime::from_seconds(self.config.base_dt);
221 self.integrator.step(state, dt)?;
222
223 substeps += 1;
224 sim_time_advanced += self.config.base_dt;
225 }
226
227 let physics_time_ms = start.elapsed().as_secs_f64() * 1000.0;
228 let budget_exceeded = physics_time_ms > budget_ms;
229
230 self.physics_times.push(physics_time_ms);
232 if self.physics_times.len() > 100 {
233 self.physics_times.remove(0);
234 }
235
236 if self.config.auto_adjust_quality {
238 self.adjust_quality(budget_exceeded, physics_time_ms, budget_ms);
239 }
240
241 let avg_physics_ms = if self.physics_times.is_empty() {
243 0.0
244 } else {
245 self.physics_times.iter().sum::<f64>() / self.physics_times.len() as f64
246 };
247
248 self.status = HeijunkaStatus {
249 budget_ms,
250 used_ms: physics_time_ms,
251 substeps,
252 quality: self.quality,
253 avg_physics_ms,
254 utilization: physics_time_ms / budget_ms,
255 };
256
257 Ok(FrameResult {
258 substeps,
259 physics_time_ms,
260 quality: self.quality,
261 budget_exceeded,
262 sim_time_advanced,
263 })
264 }
265
266 fn adjust_quality(&mut self, budget_exceeded: bool, physics_time_ms: f64, budget_ms: f64) {
267 if budget_exceeded {
268 self.quality = self.quality.degrade();
270 self.consecutive_under_budget = 0;
271 } else if physics_time_ms < budget_ms * 0.5 {
272 self.consecutive_under_budget += 1;
274 if self.consecutive_under_budget >= self.config.upgrade_threshold {
275 self.quality = self.quality.upgrade();
276 self.consecutive_under_budget = 0;
277 }
278 } else {
279 self.consecutive_under_budget = 0;
281 }
282 }
283
284 #[must_use]
286 pub fn estimate_substeps(&self, _state: &NBodyState) -> usize {
287 contract_pre_iterator!();
288 if self.physics_times.is_empty() {
290 return self.quality.substep_multiplier();
291 }
292
293 let avg_per_step = self.physics_times.iter().sum::<f64>()
294 / self.physics_times.len() as f64
295 / self.config.max_substeps.max(1) as f64;
296
297 if avg_per_step > 0.0 {
298 let budget_ms = self.config.physics_budget_ms();
299 (budget_ms / avg_per_step) as usize
300 } else {
301 self.quality.substep_multiplier()
302 }
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use crate::orbit::physics::OrbitBody;
310 use crate::orbit::units::{OrbitMass, Position3D, Velocity3D, AU, EARTH_MASS, G, SOLAR_MASS};
311
312 fn create_test_state() -> NBodyState {
313 let v_circular = (G * SOLAR_MASS / AU).sqrt();
314 let bodies = vec![
315 OrbitBody::new(
316 OrbitMass::from_kg(SOLAR_MASS),
317 Position3D::zero(),
318 Velocity3D::zero(),
319 ),
320 OrbitBody::new(
321 OrbitMass::from_kg(EARTH_MASS),
322 Position3D::from_au(1.0, 0.0, 0.0),
323 Velocity3D::from_mps(0.0, v_circular, 0.0),
324 ),
325 ];
326 NBodyState::new(bodies, 1e6)
327 }
328
329 #[test]
330 fn test_quality_level_degrade() {
331 assert_eq!(QualityLevel::Maximum.degrade(), QualityLevel::High);
332 assert_eq!(QualityLevel::High.degrade(), QualityLevel::Medium);
333 assert_eq!(QualityLevel::Medium.degrade(), QualityLevel::Low);
334 assert_eq!(QualityLevel::Low.degrade(), QualityLevel::Minimum);
335 assert_eq!(QualityLevel::Minimum.degrade(), QualityLevel::Minimum);
336 }
337
338 #[test]
339 fn test_quality_level_upgrade() {
340 assert_eq!(QualityLevel::Minimum.upgrade(), QualityLevel::Low);
341 assert_eq!(QualityLevel::Low.upgrade(), QualityLevel::Medium);
342 assert_eq!(QualityLevel::Medium.upgrade(), QualityLevel::High);
343 assert_eq!(QualityLevel::High.upgrade(), QualityLevel::Maximum);
344 assert_eq!(QualityLevel::Maximum.upgrade(), QualityLevel::Maximum);
345 }
346
347 #[test]
348 fn test_quality_level_substep_multiplier() {
349 assert_eq!(QualityLevel::Minimum.substep_multiplier(), 1);
350 assert_eq!(QualityLevel::Low.substep_multiplier(), 2);
351 assert_eq!(QualityLevel::Medium.substep_multiplier(), 4);
352 assert_eq!(QualityLevel::High.substep_multiplier(), 8);
353 assert_eq!(QualityLevel::Maximum.substep_multiplier(), 16);
354 }
355
356 #[test]
357 fn test_heijunka_config_default() {
358 let config = HeijunkaConfig::default();
359 assert!((config.frame_budget_ms - 16.0).abs() < 1e-10);
360 assert!((config.physics_budget_fraction - 0.5).abs() < 1e-10);
361 assert!(config.auto_adjust_quality);
362 }
363
364 #[test]
365 fn test_heijunka_config_physics_budget() {
366 let config = HeijunkaConfig::default();
367 let physics_budget = config.physics_budget_ms();
368 assert!((physics_budget - 8.0).abs() < 1e-10);
369 }
370
371 #[test]
372 fn test_heijunka_scheduler_creation() {
373 let config = HeijunkaConfig::default();
374 let scheduler = HeijunkaScheduler::new(config);
375 assert_eq!(scheduler.quality(), QualityLevel::High);
376 }
377
378 #[test]
379 fn test_heijunka_scheduler_set_quality() {
380 let config = HeijunkaConfig::default();
381 let mut scheduler = HeijunkaScheduler::new(config);
382
383 scheduler.set_quality(QualityLevel::Low);
384 assert_eq!(scheduler.quality(), QualityLevel::Low);
385 }
386
387 #[test]
388 fn test_heijunka_execute_frame() {
389 let mut config = HeijunkaConfig::default();
390 config.frame_budget_ms = 1000.0; config.max_substeps = 4;
392
393 let mut scheduler = HeijunkaScheduler::new(config);
394 let mut state = create_test_state();
395
396 let result = scheduler.execute_frame(&mut state).expect("frame failed");
397
398 assert!(result.substeps > 0);
399 assert!(result.sim_time_advanced > 0.0);
400 assert!(result.physics_time_ms >= 0.0);
401 }
402
403 #[test]
404 fn test_heijunka_status_update() {
405 let mut config = HeijunkaConfig::default();
406 config.frame_budget_ms = 1000.0;
407 config.max_substeps = 2;
408
409 let mut scheduler = HeijunkaScheduler::new(config);
410 let mut state = create_test_state();
411
412 scheduler.execute_frame(&mut state).expect("frame failed");
413
414 let status = scheduler.status();
415 assert!((status.budget_ms - 500.0).abs() < 1e-10); assert!(status.used_ms >= 0.0);
417 assert!(status.substeps > 0);
418 }
419
420 #[test]
421 fn test_heijunka_frame_result() {
422 let result = FrameResult {
423 substeps: 4,
424 physics_time_ms: 5.0,
425 quality: QualityLevel::High,
426 budget_exceeded: false,
427 sim_time_advanced: 14400.0,
428 };
429
430 assert_eq!(result.substeps, 4);
431 assert!(!result.budget_exceeded);
432 }
433
434 #[test]
435 fn test_heijunka_estimate_substeps() {
436 let config = HeijunkaConfig::default();
437 let scheduler = HeijunkaScheduler::new(config);
438 let state = create_test_state();
439
440 let estimate = scheduler.estimate_substeps(&state);
441 assert!(estimate > 0);
442 }
443
444 #[test]
445 fn test_heijunka_auto_quality_disabled() {
446 let mut config = HeijunkaConfig::default();
447 config.auto_adjust_quality = false;
448 config.frame_budget_ms = 1000.0;
449
450 let mut scheduler = HeijunkaScheduler::new(config);
451 let mut state = create_test_state();
452
453 let initial_quality = scheduler.quality();
454
455 for _ in 0..20 {
456 scheduler.execute_frame(&mut state).expect("frame failed");
457 }
458
459 assert_eq!(scheduler.quality(), initial_quality);
461 }
462}