///|
/// A deterministic simulation run plan.
pub(all) struct RunPlan {
  steps : Int
  sample_every : Int
  record_initial : Bool
  stop_on_unstable : Bool
} derive(Debug)

///|
/// Build a conservative run plan.
pub fn RunPlan::new(
  steps~ : Int,
  sample_every? : Int = 1,
  record_initial? : Bool = true,
  stop_on_unstable? : Bool = false,
) -> RunPlan {
  {
    steps: steps.max(0),
    sample_every: sample_every.max(1),
    record_initial,
    stop_on_unstable,
  }
}

///|
/// Summary returned by a planned run.
pub(all) struct RunSummary {
  steps : Int
  samples : Int
  final_mass : Double
  final_max_speed : Double
  converged : Bool
  stopped_early : Bool
} derive(Debug)

///|
/// Execute a run plan and record deterministic observation count.
pub fn Simulation::run_plan(self : Simulation, plan : RunPlan) -> RunSummary {
  let initial_mass = self.mass()
  let mut samples = if plan.record_initial { 1 } else { 0 }
  let mut stopped_early = false
  for step in 1..<=plan.steps {
    self.step()
    if step % plan.sample_every == 0 {
      samples += 1
    }
    if plan.stop_on_unstable && !self.health().pass {
      stopped_early = true
      break
    }
  }
  let actual_steps = if stopped_early { self.step_count } else { plan.steps }
  let final_mass = self.mass()
  {
    steps: actual_steps,
    samples,
    final_mass,
    final_max_speed: self.stability().max_speed,
    converged: abs_double(final_mass - initial_mass) < 0.000001,
    stopped_early,
  }
}

///|
/// Return the absolute mass change represented by a run summary.
pub fn RunSummary::mass_change(
  self : RunSummary,
  initial_mass~ : Double,
) -> Double {
  abs_double(self.final_mass - initial_mass)
}

///|
/// Serialize a run summary as CSV.
pub fn RunSummary::to_csv(self : RunSummary) -> String {
  "\{self.steps},\{self.samples},\{self.final_mass},\{self.final_max_speed},\{self.converged},\{self.stopped_early}\n"
}

///|
/// Plan a geometrically increasing sequence of sample intervals.
pub fn schedule_intervals(total_steps~ : Int, levels~ : Int) -> Array[Int] {
  let result = Array::new()
  let count = levels.max(0)
  for level in 0.. Bool {
  summary.steps == plan.steps && !summary.stopped_early
}

///|
/// Compare two run summaries by final mass and speed.
pub fn run_summary_distance(left : RunSummary, right : RunSummary) -> Double {
  abs_double(left.final_mass - right.final_mass) +
  abs_double(left.final_max_speed - right.final_max_speed)
}