///|
pub(all) struct EnsembleMember {
  id : Int
  seed : Int
  diagnostics : Array[PicDiagnostics]
} derive(Debug, ToJson)

///|
pub(all) struct EnsembleSummary {
  members : Int
  mean_final_energy : Double
  spread_final_energy : Double
  mean_charge_error : Double
  maximum_particle_loss : Int
} derive(Debug, ToJson)

///|
pub fn run_ensemble(
  scenario : SimulationScenario,
  members : Int,
) -> Array[EnsembleMember] {
  let output : Array[EnsembleMember] = []
  for id in 0.. Array[Double] {
  members.map(fn(item) {
    if item.diagnostics.length() == 0 {
      0.0
    } else {
      item.diagnostics[item.diagnostics.length() - 1].total_energy()
    }
  })
}

///|
pub fn ensemble_charge_errors(
  members : ArrayView[EnsembleMember],
) -> Array[Double] {
  members.map(fn(item) {
    if item.diagnostics.length() < 2 {
      0.0
    } else {
      (item.diagnostics[0].total_charge -
      item.diagnostics[item.diagnostics.length() - 1].total_charge).abs()
    }
  })
}

///|
pub fn ensemble_particle_losses(
  members : ArrayView[EnsembleMember],
) -> Array[Int] {
  members.map(fn(item) {
    if item.diagnostics.length() < 2 {
      0
    } else {
      item.diagnostics[0].particles -
      item.diagnostics[item.diagnostics.length() - 1].particles
    }
  })
}

///|
pub fn summarize_ensemble(
  members : ArrayView[EnsembleMember],
) -> EnsembleSummary {
  let energies = ensemble_final_energies(members)
  let charges = ensemble_charge_errors(members)
  let losses = ensemble_particle_losses(members)
  {
    members: members.length(),
    mean_final_energy: mean(energies),
    spread_final_energy: sample_span(energies),
    mean_charge_error: mean(charges),
    maximum_particle_loss: losses.fold(init=0, fn(acc, value) { acc.max(value) }),
  }
}

///|
pub fn ensemble_summary_to_csv(summary : EnsembleSummary) -> String {
  "members,mean_final_energy,spread_final_energy,mean_charge_error,maximum_particle_loss\n\{summary.members},\{summary.mean_final_energy},\{summary.spread_final_energy},\{summary.mean_charge_error},\{summary.maximum_particle_loss}\n"
}

///|
pub fn ensemble_diagnostics_to_csv(
  members : ArrayView[EnsembleMember],
) -> String {
  let output = StringBuilder()
  output.write_string("member,step,time,particles,total_charge,total_energy\n")
  for ensemble_item in members {
    for i in 0.. Bool {
  summary.mean_charge_error <= charge_tolerance &&
  summary.spread_final_energy <= energy_spread_tolerance
}