///|
/// Export a scalar field as an ASCII PPM image.
pub fn Field2D::to_ppm(self : Field2D) -> String {
  let stats = self.statistics()
  let denominator = if stats.maximum == stats.minimum {
    1.0
  } else {
    stats.maximum - stats.minimum
  }
  let builder = StringBuilder(
    size_hint=self.size.width * self.size.height * 12 + 32,
  )
  builder.write_string("P3\n\{self.size.width} \{self.size.height}\n255\n")
  for y in 0.. String {
  let builder = StringBuilder(
    size_hint=self.size.width * self.size.height * 20 + 256,
  )
  builder.write_string("# vtk DataFile Version 3.0\n")
  builder.write_string("moonbit-lbm field\nASCII\nDATASET STRUCTURED_POINTS\n")
  builder.write_string("DIMENSIONS \{self.size.width} \{self.size.height} 1\n")
  builder.write_string("ORIGIN 0 0 0\nSPACING 1 1 1\n")
  builder.write_string("POINT_DATA \{self.size.width * self.size.height}\n")
  builder.write_string("SCALARS \{name} double 1\nLOOKUP_TABLE default\n")
  for value in self.data {
    builder.write_string("\{value}\n")
  }
  builder.to_string()
}

///|
/// Export a vector field as CSV.
pub fn VectorField2D::to_csv(self : VectorField2D) -> String {
  let builder = StringBuilder(
    size_hint=self.size.width * self.size.height * 24 + 24,
  )
  builder.write_string("x,y,ux,uy,speed\n")
  for y in 0.. String {
  let builder = StringBuilder(size_hint=self.samples.length() * 40 + 2)
  builder.write_char('[')
  for i, sample in self.samples {
    if i > 0 {
      builder.write_char(',')
    }
    builder.write_string(
      "{\"step\":\{sample.step},\"residual\":\{sample.residual},\"mass\":\{sample.mass}}",
    )
  }
  builder.write_char(']')
  builder.to_string()
}