///|
/// Return the number of completed time steps.
pub fn Simulation::steps(self : Simulation) -> Int {
  self.step_count
}

///|
/// Return the number of stored distribution populations.
pub fn Simulation::distribution_count(self : Simulation) -> Int {
  self.f.length()
}

///|
/// Copy the distribution payload for diagnostics or persistence adapters.
pub fn Simulation::distribution_values(self : Simulation) -> Array[Double] {
  self.f.copy()
}

///|
/// Copy the solid-cell mask in row-major order.
pub fn Simulation::solid_values(self : Simulation) -> Array[Bool] {
  self.solid.copy()
}

///|
/// Compute a deterministic checksum over geometry and populations.
pub fn Simulation::state_checksum(self : Simulation) -> Double {
  let mut checksum = self.size.width.to_double() * 0.000001 +
    self.size.height.to_double() * 0.000000001
  for i, value in self.f {
    checksum += (i + 1).to_double() * value
  }
  for i, solid in self.solid {
    if solid {
      checksum += (i + 1).to_double() * 0.0000001
    }
  }
  checksum + self.step_count.to_double() * 0.0000000001
}

///|
/// Produce a compact, deterministic identity for a simulation state.
pub fn Simulation::state_fingerprint(self : Simulation) -> String {
  let builder = StringBuilder(size_hint=96)
  builder.write_string("MLBM:")
  builder.write_string("\{self.size.width}x\{self.size.height}:")
  builder.write_string("\{self.step_count}:")
  builder.write_string("\{self.mass()}:")
  builder.write_string("\{self.state_checksum()}")
  builder.to_string()
}

///|
/// Return the serialized checkpoint size in bytes.
pub fn Simulation::checkpoint_size(self : Simulation) -> Int {
  self.to_checkpoint().length()
}

///|
/// Estimate the payload bytes without allocating a checkpoint string.
pub fn Simulation::payload_bytes(self : Simulation) -> Int {
  self.f.length() * 8 + self.solid.length()
}

///|
/// Return whether the distribution is within tolerance of local equilibrium.
pub fn Simulation::is_equilibrium(
  self : Simulation,
  tolerance? : Double = 0.000000001,
) -> Bool {
  let limit = tolerance.max(0.0)
  let mut result = true
  for y in 0.. Point {
  let mut ux = 0.0
  let mut uy = 0.0
  let mut count = 0
  for y in 0.. Double {
  self.density_field().statistics_masked(mask=self.current_mask()).maximum
}

///|
/// Find the minimum fluid density.
pub fn Simulation::minimum_density(self : Simulation) -> Double {
  let stats = self.density_field().statistics_masked(mask=self.current_mask())
  stats.minimum
}

///|
/// Construct a field of local kinetic energy densities.
pub fn Simulation::kinetic_energy_field(self : Simulation) -> Field2D {
  let result = Field2D::new(size=self.size)
  for y in 0.. Field2D {
  self.density_field().map((x, y, rho) => rho * self.cell(x~, y~).ux)
}

///|
/// Construct the y component of local momentum density.
pub fn Simulation::momentum_y_field(self : Simulation) -> Field2D {
  let result = Field2D::new(size=self.size)
  for y in 0.. String {
  self.density_field().to_csv(name="density")
}

///|
/// Export velocity magnitude as a CSV grid.
pub fn Simulation::speed_csv(self : Simulation) -> String {
  self.speed_field().to_csv(name="speed")
}

///|
/// Export velocity components as a CSV point table.
pub fn Simulation::velocity_csv(self : Simulation) -> String {
  self.velocity_field().to_csv()
}

///|
/// Compare two states using a normalized distribution L2 error.
pub fn Simulation::state_delta(self : Simulation, other : Simulation) -> Double {
  if self.size != other.size {
    1.0
  } else {
    relative_l2_error(self.f[:], other.f[:])
  }
}

///|
/// Return whether two simulations share dimensions and solid geometry.
pub fn Simulation::same_geometry(self : Simulation, other : Simulation) -> Bool {
  self.size == other.size && self.solid[:] == other.solid[:]
}

///|
/// Return a human-readable state summary for logs and notebooks.
pub fn Simulation::state_summary(self : Simulation) -> String {
  let stability = self.stability()
  let mean = self.mean_velocity()
  let builder = StringBuilder(size_hint=256)
  builder.write_string("steps=\{self.step_count}")
  builder.write_string(" mass=\{stability.mass}")
  builder.write_string(" rho=[\{stability.min_rho},\{stability.max_rho}]")
  builder.write_string(" mean_velocity=(\{mean.x},\{mean.y})")
  builder.write_string(" max_speed=\{stability.max_speed}")
  builder.write_string(" checksum=\{self.state_checksum()}")
  builder.to_string()
}