///|
/// Errors raised when a simulation checkpoint cannot be trusted.
pub(all) suberror CheckpointError {
InvalidHeader
InvalidRecord(String)
InvalidSize
InvalidPayload
}
///|
/// Encode a floating-point array as a comma-separated record.
fn doubles_to_record(values : Array[Double]) -> String {
let builder = StringBuilder(size_hint=values.length() * 12)
for i, value in values {
if i > 0 {
builder.write_char(',')
}
builder.write_string("\{value}")
}
builder.to_string()
}
///|
/// Encode a solid mask as a compact binary record.
fn solids_to_record(values : Array[Bool]) -> String {
let builder = StringBuilder(size_hint=values.length())
for value in values {
builder.write_char(if value { '1' } else { '0' })
}
builder.to_string()
}
///|
/// Parse an integer token and convert parser failures to checkpoint errors.
fn checkpoint_int(token : StringView) -> Int raise CheckpointError {
@string.parse_int(token) catch {
_ => raise InvalidRecord(token.to_owned())
}
}
///|
/// Parse a floating-point token and convert parser failures to checkpoint errors.
fn checkpoint_double(token : StringView) -> Double raise CheckpointError {
@string.parse_double(token) catch {
_ => raise InvalidRecord(token.to_owned())
}
}
///|
/// Require a record with a specific keyword and minimum field count.
fn checkpoint_record(
line : StringView,
keyword~ : String,
minimum~ : Int,
) -> Array[StringView] raise CheckpointError {
let fields = line.split(" ").to_array()
if fields.length() < minimum || fields[0].to_owned() != keyword {
raise InvalidRecord(line.to_owned())
}
fields
}
///|
/// Serialize a simulation into a versioned, dependency-free text checkpoint.
pub fn Simulation::to_checkpoint(self : Simulation) -> String {
let model_name = match self.options.model {
Bgk(_) => "bgk"
Regularized(_) => "regularized"
Mrt(_) => "mrt"
}
let boundary_name = match self.options.boundary {
BounceBack => "bounce"
Periodic => "periodic"
Open => "open"
}
let builder = StringBuilder(
size_hint=self.f.length() * 10 + self.solid.length() + 256,
)
builder.write_string("MLBM/1\n")
builder.write_string("size \{self.size.width} \{self.size.height}\n")
builder.write_string("steps \{self.step_count}\n")
builder.write_string("model \{model_name} \{self.options.model.omega()}\n")
builder.write_string("boundary \{boundary_name}\n")
builder.write_string(
"force \{self.options.force_x} \{self.options.force_y}\n",
)
builder.write_string("solid \{solids_to_record(self.solid)}\n")
builder.write_string("dist \{doubles_to_record(self.f)}\n")
builder.to_string()
}
///|
/// Restore a simulation from `Simulation::to_checkpoint` output.
pub fn Simulation::from_checkpoint(
text : String,
) -> Simulation raise CheckpointError {
let lines = text.split("\n").to_array()
if lines.length() == 0 || lines[0].to_owned() != "MLBM/1" {
raise InvalidHeader
}
if lines.length() < 8 {
raise InvalidRecord("incomplete checkpoint")
}
let size_fields = checkpoint_record(lines[1], keyword="size", minimum=3)
let width = checkpoint_int(size_fields[1])
let height = checkpoint_int(size_fields[2])
if width <= 0 || height <= 0 {
raise InvalidSize
}
let steps_fields = checkpoint_record(lines[2], keyword="steps", minimum=2)
let steps = checkpoint_int(steps_fields[1])
let model_fields = checkpoint_record(lines[3], keyword="model", minimum=3)
let omega = checkpoint_double(model_fields[2])
let model = match model_fields[1].to_owned() {
"bgk" => Bgk(omega)
"regularized" => Regularized(omega)
"mrt" => Mrt(omega)
_ => raise InvalidRecord(lines[3].to_owned())
}
let boundary_fields = checkpoint_record(
lines[4],
keyword="boundary",
minimum=2,
)
let boundary = match boundary_fields[1].to_owned() {
"bounce" => BounceBack
"periodic" => Periodic
"open" => Open
_ => raise InvalidRecord(lines[4].to_owned())
}
let force_fields = checkpoint_record(lines[5], keyword="force", minimum=3)
let force_x = checkpoint_double(force_fields[1])
let force_y = checkpoint_double(force_fields[2])
let solid_fields = checkpoint_record(lines[6], keyword="solid", minimum=2)
let dist_fields = checkpoint_record(lines[7], keyword="dist", minimum=2)
let cells = width * height
let solid_record = solid_fields[1].to_owned()
if solid_record.length() != cells {
raise InvalidPayload
}
let distributions = dist_fields[1].split(",").to_array()
if distributions.length() != cells * q {
raise InvalidPayload
}
let simulation = Simulation::new(
size=Size::new(width~, height~),
options=SimulationOptions::new(model~, boundary~, force_x~, force_y~),
)
simulation.step_count = steps
for i, character in solid_record {
simulation.solid[i] = character == '1'
}
for i, token in distributions {
simulation.f[i] = checkpoint_double(token)
}
simulation
}
///|
/// Validate a checkpoint without retaining the restored simulation.
pub fn validate_checkpoint(text : String) -> Result[Unit, CheckpointError] {
try {
ignore(Simulation::from_checkpoint(text))
Ok(())
} catch {
error => Err(error)
}
}