///|
/// Advance a scalar solver several times.
pub fn ScalarSolver::step_many(self : ScalarSolver, steps : Int) -> Unit {
  for _ in 0.. ScalarReport {
  let stats = self.statistics()
  {
    name,
    size: self.size,
    steps: self.step_count,
    mass: self.mass(),
    mean: stats.mean,
    minimum: stats.minimum,
    maximum: stats.maximum,
    variance: stats.variance,
    finite: self.is_finite(),
  }
}

///|
/// Export a scalar report as CSV.
pub fn ScalarReport::to_csv(self : ScalarReport) -> String {
  "\{self.name},\{self.size.width},\{self.size.height},\{self.steps},\{self.mass},\{self.mean},\{self.minimum},\{self.maximum},\{self.variance},\{self.finite}\n"
}

///|
/// Export a scalar report as a Markdown row.
pub fn ScalarReport::to_markdown(self : ScalarReport) -> String {
  "| \{self.name} | \{self.size.width}x\{self.size.height} | \{self.steps} | \{self.mass} | \{self.mean} | \{self.minimum} | \{self.maximum} | \{self.variance} | \{self.finite} |"
}

///|
/// Return the fraction of the initial mass remaining.
pub fn ScalarSolver::mass_ratio(
  self : ScalarSolver,
  initial_mass~ : Double,
) -> Double {
  safe_divide(self.mass(), initial_mass, fallback=0.0)
}

///|
/// Return the L2 energy of scalar gradients.
pub fn ScalarSolver::gradient_energy(self : ScalarSolver) -> Double {
  let gradient = self.gradient_magnitude()
  dot_product(gradient.data, gradient.data)
}

///|
/// Return the sum of absolute scalar values.
pub fn ScalarSolver::absolute_mass(self : ScalarSolver) -> Double {
  self.field().absolute_integral()
}

///|
/// Return the mean source contribution.
pub fn ScalarSolver::mean_source(self : ScalarSolver) -> Double {
  mean_value(self.source)
}

///|
/// Return the maximum source magnitude.
pub fn ScalarSolver::maximum_source(self : ScalarSolver) -> Double {
  min_max(self.source).1
}

///|
/// Return the dimensionless diffusion stability number.
pub fn ScalarSolver::stability_number(self : ScalarSolver) -> Double {
  self.diffusion_number() * 4.0
}

///|
/// Return true when an explicit scalar step is within the five-point limit.
pub fn ScalarSolver::explicitly_stable(self : ScalarSolver) -> Bool {
  self.stability_number() <= 0.5
}

///|
/// Return the scalar state as normalized output.
pub fn ScalarSolver::normalized_field(self : ScalarSolver) -> Field2D {
  self.field().normalize()
}