use std::error::Error;
use brodo::error::FieldError;
use brodo::field::{Field, uniform};
use brodo::gather::{GATHER_VOXELS, gather};
use brodo::parallel::{par_reduce, par_update};
use brodo::topology::Topology;
use brodo::tree::Coord;
type BoxError = Box<dyn Error + Send + Sync>;
const DT: f32 = 0.125;
const DIFFUSIVITY: [f32; 2] = [1.0, 0.5];
const COVER: f32 = 6.0;
struct Species {
d: f32,
now: Field<f32>,
next: Field<f32>,
}
struct Solver {
topology: Topology,
species: Vec<Species>,
radius: i32,
}
#[derive(Clone, Copy)]
pub struct Resolution {
sigma0: f32,
steps: u32,
}
pub const COARSE: Resolution = Resolution { sigma0: 2.0, steps: 20 };
pub const FINE: Resolution = Resolution { sigma0: 4.0, steps: 80 };
pub struct Outcome {
pub error: [f32; 2],
pub sums: [(f32, f32); 2],
}
fn gaussian(v: Coord, sigma0: f32, sigma_sq: f32) -> f32 {
let r_sq: f32 = v.iter().map(|c| (*c as f32) * (*c as f32)).sum();
(sigma0 * sigma0 / sigma_sq).powf(1.5) * (-r_sq / (2.0 * sigma_sq)).exp()
}
#[expect(clippy::cast_possible_truncation, reason = "a radius of a few dozen voxels")]
fn radius_for(sigma: f32) -> i32 {
8 * (COVER * sigma / 8.0).ceil() as i32
}
impl Solver {
fn new(r: Resolution) -> Result<Self, BoxError> {
let mut s = Solver { topology: Topology::new(), species: Vec::new(), radius: 0 };
s.grow(radius_for(r.sigma0))?;
for d in DIFFUSIVITY {
let mut now = Field::new(&s.topology, 0.0);
let t = &s.topology;
par_update(t, &mut now, |n, out| -> Result<(), BoxError> {
for (voxel, u) in t.leaf_voxels(n)?.zip(out.iter_mut()) {
*u = gaussian(voxel, r.sigma0, r.sigma0 * r.sigma0);
}
Ok(())
})?;
let next = Field::new(&s.topology, 0.0);
s.species.push(Species { d, now, next });
}
Ok(s)
}
fn grow(&mut self, radius: i32) -> Result<(), BoxError> {
if radius <= self.radius {
return Ok(());
}
let mut e = self.topology.edit();
for z in (-radius..radius).step_by(8) {
for y in (-radius..radius).step_by(8) {
for x in (-radius..radius).step_by(8) {
e.activate([x, y, z])?;
}
}
}
let (topology, remap) = e.commit()?;
for s in &mut self.species {
s.now = s.now.remap(&remap, &uniform)?;
s.next = s.next.remap(&remap, &uniform)?;
}
self.topology = topology;
self.radius = radius;
Ok(())
}
fn step(&mut self) -> Result<(), FieldError> {
let t = &self.topology;
for s in &mut self.species {
let (now, lambda) = (&s.now, s.d * DT);
par_update(t, &mut s.next, |n, out| {
let mut g = [0.0; GATHER_VOXELS];
gather(t, now, n, &mut g)?;
for (i, u) in out.iter_mut().enumerate() {
let c = (i % 8 + 1) + 10 * (i / 8 % 8 + 1) + 100 * (i / 64 + 1);
let sum = g[c - 1] + g[c + 1] + g[c - 10] + g[c + 10] + g[c - 100] + g[c + 100];
*u = g[c] + lambda * (sum - 6.0 * g[c]);
}
Ok(())
})?;
std::mem::swap(&mut s.now, &mut s.next);
}
Ok(())
}
fn sum(&self, k: usize) -> Result<f32, FieldError> {
par_reduce(&self.topology, &self.species[k].now, 0.0, |_, v| v.iter().sum(), |a, b| a + b)
}
fn error(&self, k: usize, r: Resolution, t: f32) -> Result<f32, BoxError> {
let sigma_sq = r.sigma0 * r.sigma0 + 2.0 * self.species[k].d * t;
let topology = &self.topology;
par_reduce(
topology,
&self.species[k].now,
Ok(0.0_f32),
|n, values| -> Result<f32, BoxError> {
let diff = |(v, u): (Coord, &f32)| (u - gaussian(v, r.sigma0, sigma_sq)).abs();
Ok(topology.leaf_voxels(n)?.zip(values).map(diff).fold(0.0, f32::max))
},
|a, b| Ok(a?.max(b?)),
)?
}
}
pub fn run(r: Resolution) -> Result<Outcome, BoxError> {
let mut solver = Solver::new(r)?;
let start = [solver.sum(0)?, solver.sum(1)?];
let steps = r.steps;
for k in 1..=steps {
let sigma = (r.sigma0 * r.sigma0 + 2.0 * k as f32 * DT).sqrt();
solver.grow(radius_for(sigma))?;
solver.step()?;
}
let t = steps as f32 * DT;
Ok(Outcome {
error: [solver.error(0, r, t)?, solver.error(1, r, t)?],
sums: [(start[0], solver.sum(0)?), (start[1], solver.sum(1)?)],
})
}
fn main() -> Result<(), BoxError> {
let coarse = run(COARSE)?;
let fine = run(FINE)?;
check(&coarse, &fine)
}
const ERROR_TOL: f32 = 1.5e-3;
const RATIO: std::ops::RangeInclusive<f32> = 3.5..=4.5;
const SUM_TOL: f32 = 1e-5;
fn check(coarse: &Outcome, fine: &Outcome) -> Result<(), BoxError> {
for k in 0..2 {
if fine.error[k] > ERROR_TOL {
return Err(format!("field {k}: error {} above {ERROR_TOL}", fine.error[k]).into());
}
let ratio = coarse.error[k] / fine.error[k];
if !RATIO.contains(&ratio) {
return Err(format!("field {k}: error ratio {ratio} outside {RATIO:?}").into());
}
for o in [coarse, fine] {
let (a, b) = o.sums[k];
if ((b - a) / a).abs() > SUM_TOL {
return Err(format!("field {k}: sum {a} became {b}").into());
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_gaussian_is_followed_to_second_order_and_its_sum_kept() {
let coarse = run(COARSE).unwrap();
let fine = run(FINE).unwrap();
check(&coarse, &fine).unwrap();
}
}