use petir::mathf::RealMath;
use std::sync::Arc;
use super::*;
use crate::fields::vol_field::{VolScalarField, VolVectorField};
use crate::mesh::fv_mesh::FvMesh;
use crate::primitives::Vector3;
const BIG: usize = 200_003;
const SIZES: [usize; 6] = [0, 1, 2, 1_000, 131_072, BIG];
const BACKENDS: [ComputeBackend; 3] = [
ComputeBackend::Serial,
ComputeBackend::CpuMulti,
ComputeBackend::Gpu,
];
fn sample(n: usize) -> Field<f64> {
Field::from_fn(n, |i| {
let t = i as f64;
(t * 0.61803398874989).r_sin() * 1.0e6 + (t * 0.123456789012345).r_cos() * 1.0e-7
})
}
fn sample_shift(n: usize) -> Field<f64> {
Field::from_fn(n, |i| {
let t = i as f64 + 0.5;
(t * 0.27182818284590).r_cos() * 1.0e3 + 1.5
})
}
fn sample_vec(n: usize) -> Field<Vector3> {
Field::from_fn(n, |i| {
let t = i as f64;
Vector3::new(t.r_sin(), t.r_cos(), (t * 0.5).r_sin())
})
}
#[test]
fn dispatch_policy_is_the_single_decision_point() {
let big = field_parallel_crossover();
assert!(!should_parallelise(ComputeBackend::Serial, usize::MAX));
assert!(!should_parallelise(ComputeBackend::CpuMulti, 0));
assert!(!should_parallelise(ComputeBackend::CpuMulti, big - 1));
assert_eq!(
should_parallelise(ComputeBackend::CpuMulti, big),
cfg!(feature = "parallel")
);
assert_eq!(
should_parallelise(ComputeBackend::Gpu, big),
cfg!(feature = "parallel"),
"Gpu has no field kernel yet and must take the best CPU path"
);
}
#[test]
fn vv_elementwise_scalar_kernels_are_bit_identical_to_serial() {
for n in SIZES {
let a = sample(n);
let b = sample_shift(n);
let s = ComputeBackend::Serial;
let ref_add = add(s, &a, &b);
let ref_sub = sub(s, &a, &b);
let ref_scale = scale(s, &a, 0.25);
let ref_axpy = axpy(s, &a, -3.5, &b);
let ref_mul = pointwise_mul(s, &a, &b);
let ref_div = pointwise_div(s, &a, &b);
for backend in BACKENDS {
assert_eq!(
add(backend, &a, &b).as_slice(),
ref_add.as_slice(),
"add n={n}"
);
assert_eq!(
sub(backend, &a, &b).as_slice(),
ref_sub.as_slice(),
"sub n={n}"
);
assert_eq!(
scale(backend, &a, 0.25).as_slice(),
ref_scale.as_slice(),
"scale n={n}"
);
assert_eq!(
axpy(backend, &a, -3.5, &b).as_slice(),
ref_axpy.as_slice(),
"axpy n={n}"
);
assert_eq!(
pointwise_mul(backend, &a, &b).as_slice(),
ref_mul.as_slice(),
"pointwise_mul n={n}"
);
assert_eq!(
pointwise_div(backend, &a, &b).as_slice(),
ref_div.as_slice(),
"pointwise_div n={n}"
);
}
}
}
#[test]
fn vv_in_place_kernels_match_out_of_place() {
for n in SIZES {
let a = sample(n);
let b = sample_shift(n);
let s = ComputeBackend::Serial;
for backend in BACKENDS {
let mut y = a.clone();
add_assign(backend, &mut y, &b);
assert_eq!(y.as_slice(), add(s, &a, &b).as_slice(), "add_assign n={n}");
let mut y = a.clone();
sub_assign(backend, &mut y, &b);
assert_eq!(y.as_slice(), sub(s, &a, &b).as_slice(), "sub_assign n={n}");
let mut y = a.clone();
scale_assign(backend, &mut y, 0.25);
assert_eq!(
y.as_slice(),
scale(s, &a, 0.25).as_slice(),
"scale_assign n={n}"
);
let mut y = a.clone();
axpy_assign(backend, &mut y, -3.5, &b);
assert_eq!(
y.as_slice(),
axpy(s, &a, -3.5, &b).as_slice(),
"axpy_assign n={n}"
);
}
}
}
#[test]
fn vv_elementwise_vector_kernels_are_bit_identical_to_serial() {
for n in SIZES {
let u = sample_vec(n);
let v = sample_vec(n);
let rho = sample_shift(n);
let s = ComputeBackend::Serial;
let ref_add = add(s, &u, &v);
let ref_scaled = scale_by_field(s, &u, &rho);
let ref_dot = dot_field(s, &u, &v);
let mut ref_axpy = u.clone();
axpy_assign(s, &mut ref_axpy, 0.125, &v);
for backend in BACKENDS {
assert_eq!(
add(backend, &u, &v).as_slice(),
ref_add.as_slice(),
"vec add n={n}"
);
assert_eq!(
scale_by_field(backend, &u, &rho).as_slice(),
ref_scaled.as_slice(),
"scale_by_field n={n}"
);
assert_eq!(
dot_field(backend, &u, &v).as_slice(),
ref_dot.as_slice(),
"dot_field n={n}"
);
let mut y = u.clone();
axpy_assign(backend, &mut y, 0.125, &v);
assert_eq!(y.as_slice(), ref_axpy.as_slice(), "vec axpy_assign n={n}");
}
}
}
#[test]
fn edge_cases_empty_and_single_element() {
for backend in BACKENDS {
let empty = Field::<f64>::new(vec![]);
assert_eq!(add(backend, &empty, &empty).len(), 0);
assert_eq!(sum(backend, &empty), 0.0);
assert_eq!(mean(backend, &empty), 0.0);
assert_eq!(min(backend, &empty), f64::INFINITY);
assert_eq!(max(backend, &empty), f64::NEG_INFINITY);
assert_eq!(l2_norm(backend, &empty), 0.0);
assert_eq!(dot(backend, &empty, &empty), 0.0);
let one = Field::new(vec![7.5_f64]);
assert_eq!(add(backend, &one, &one).as_slice(), &[15.0]);
assert_eq!(sum(backend, &one), 7.5);
assert_eq!(min(backend, &one), 7.5);
assert_eq!(max(backend, &one), 7.5);
assert_eq!(mean(backend, &one), 7.5);
assert!((l2_norm(backend, &one) - 7.5).abs() < 1e-15);
}
}
#[test]
#[should_panic(expected = "Field length mismatch")]
fn mismatched_lengths_panic() {
let a = Field::new(vec![1.0, 2.0, 3.0]);
let b = Field::new(vec![1.0, 2.0]);
let _ = add(ComputeBackend::CpuMulti, &a, &b);
}
#[test]
fn vv_min_max_are_bit_identical_to_serial() {
for n in SIZES {
let a = sample(n);
for backend in BACKENDS {
assert_eq!(min(backend, &a), a.min(), "min n={n}");
assert_eq!(max(backend, &a), a.max(), "max n={n}");
}
}
}
#[test]
fn vv_parallel_sum_matches_serial_within_tolerance() {
const TOL: f64 = 1e-11;
let a = sample(BIG);
let b = sample_shift(BIG);
let s = ComputeBackend::Serial;
let p = ComputeBackend::CpuMulti;
let checks: [(&str, f64, f64); 3] = [
("sum", sum(s, &a), sum(p, &a)),
("l2_norm", l2_norm(s, &a), l2_norm(p, &a)),
("dot", dot(s, &a, &b), dot(p, &a, &b)),
];
let mut worst = 0.0_f64;
for (name, serial, parallel) in checks {
let rel = ((parallel - serial) / serial).abs();
println!("{name}: serial={serial:.17e} parallel={parallel:.17e} rel_dev={rel:.3e}");
assert!(
rel < TOL,
"{name}: relative deviation {rel:.3e} exceeds tolerance {TOL:.0e}"
);
worst = worst.max(rel);
}
let mesh = Arc::new(FvMesh::periodic_1d(BIG, 1.0, 1.0));
let phi = VolScalarField::new(
"phi",
mesh.clone(),
a.clone(),
mesh.patches
.iter()
.map(|q| crate::fields::boundary::bc::PatchField::zero_gradient(q.size))
.collect(),
);
let (vs, vp) = (vol_integral(s, &phi), vol_integral(p, &phi));
let rel = ((vp - vs) / vs).abs();
println!("vol_integral: serial={vs:.17e} parallel={vp:.17e} rel_dev={rel:.3e}");
assert!(
rel < TOL,
"vol_integral relative deviation {rel:.3e} exceeds {TOL:.0e}"
);
worst = worst.max(rel);
println!("WORST-CASE relative deviation across all reductions: {worst:.3e}");
}
#[test]
fn vv_reduction_is_bit_reproducible_run_to_run() {
let a = sample(BIG);
let b = sample_shift(BIG);
let p = ComputeBackend::CpuMulti;
let (s0, n0, d0) = (sum(p, &a), l2_norm(p, &a), dot(p, &a, &b));
for i in 1..32 {
assert_eq!(sum(p, &a), s0, "sum differed on repeat {i}");
assert_eq!(l2_norm(p, &a), n0, "l2_norm differed on repeat {i}");
assert_eq!(dot(p, &a, &b), d0, "dot differed on repeat {i}");
}
}
#[cfg(feature = "parallel")]
#[test]
fn vv_reduction_is_independent_of_thread_count() {
let a = sample(BIG);
let b = sample_shift(BIG);
let p = ComputeBackend::CpuMulti;
let mut reference: Option<(f64, f64, f64)> = None;
for threads in [1_usize, 2, 3, 4, 7, 8] {
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build()
.expect("test thread pool");
let got = pool.install(|| (sum(p, &a), l2_norm(p, &a), dot(p, &a, &b)));
match reference {
None => reference = Some(got),
Some(want) => assert_eq!(
got, want,
"reduction changed with {threads} threads — determinism guarantee violated"
),
}
}
}
#[test]
fn vv_reduction_against_analytic_reference() {
let ones = Field::uniform(BIG, 1.0_f64);
for backend in BACKENDS {
assert_eq!(sum(backend, &ones), BIG as f64, "sum of ones");
assert_eq!(mean(backend, &ones), 1.0, "mean of ones");
assert_eq!(dot(backend, &ones, &ones), BIG as f64, "dot of ones");
assert!((l2_norm(backend, &ones) - (BIG as f64).sqrt()).abs() < 1e-9);
}
}
fn vol_mesh(n: usize) -> Arc<FvMesh> {
Arc::new(FvMesh::periodic_1d(n, 1.0, 1.0))
}
fn vol_scalar(name: &str, mesh: Arc<FvMesh>, internal: f64, boundary_value: f64) -> VolScalarField {
let boundary = mesh
.patches
.iter()
.map(|p| PatchField::fixed_value(p.size, boundary_value))
.collect();
let n = mesh.n_cells;
VolScalarField::new(name, mesh, Field::uniform(n, internal), boundary)
}
fn surface_scalar(
name: &str,
mesh: Arc<FvMesh>,
internal: f64,
boundary_value: f64,
) -> crate::fields::surface_field::SurfaceScalarField {
let boundary = mesh
.patches
.iter()
.map(|p| PatchField::fixed_value(p.size, boundary_value))
.collect();
let n = mesh.n_internal_faces;
crate::fields::surface_field::SurfaceScalarField::new(
name,
mesh,
Field::uniform(n, internal),
boundary,
)
}
#[test]
fn vv_vol_wrappers_cover_internal_and_boundary() {
let mesh = vol_mesh(4);
let a = vol_scalar("rho", mesh.clone(), 1.0, 4.0);
let b = vol_scalar("drho", mesh.clone(), 0.25, 0.5);
for backend in BACKENDS {
let c = add_vol(backend, &a, &b);
assert_eq!(c.name, "rho");
assert_eq!(c.internal.len(), 4);
for i in 0..4 {
assert!((c.internal[i] - 1.25).abs() < 1e-15);
}
for patch in &c.boundary {
for j in 0..patch.values.len() {
assert!(
(patch.values[j] - 4.5).abs() < 1e-15,
"boundary not updated"
);
}
}
let d = scale_vol(backend, &c, 2.0);
assert_eq!(d.name, "rho");
assert!((d.internal[0] - 2.5).abs() < 1e-15);
assert!((d.boundary[0].values[0] - 9.0).abs() < 1e-15);
let e = sub_vol(backend, &d, &c);
assert!((e.internal[0] - 1.25).abs() < 1e-15);
assert!((e.boundary[0].values[0] - 4.5).abs() < 1e-15);
let mut y = a.clone();
add_vol_assign(backend, &mut y, &b);
assert!((y.internal[0] - 1.25).abs() < 1e-15);
assert!((y.boundary[0].values[0] - 4.5).abs() < 1e-15);
let mut y = a.clone();
sub_vol_assign(backend, &mut y, &b);
assert!((y.internal[0] - 0.75).abs() < 1e-15);
assert!((y.boundary[0].values[0] - 3.5).abs() < 1e-15);
let mut y = a.clone();
scale_vol_assign(backend, &mut y, 3.0);
assert!((y.internal[0] - 3.0).abs() < 1e-15);
assert!((y.boundary[0].values[0] - 12.0).abs() < 1e-15);
let mut y = a.clone();
axpy_vol_assign(backend, &mut y, 4.0, &b);
assert!((y.internal[0] - 2.0).abs() < 1e-15);
assert!((y.boundary[0].values[0] - 6.0).abs() < 1e-15);
}
}
#[test]
fn vv_surface_wrappers_cover_internal_and_boundary() {
let mesh = vol_mesh(4);
let a = surface_scalar("phi", mesh.clone(), 2.0, 7.0);
let b = surface_scalar("dphi", mesh.clone(), 3.0, 1.0);
for backend in BACKENDS {
let c = add_surface(backend, &a, &b);
assert_eq!(c.name, "phi");
assert_eq!(c.internal.len(), mesh.n_internal_faces);
assert!((c.internal[0] - 5.0).abs() < 1e-15);
assert!((c.boundary[0].values[0] - 8.0).abs() < 1e-15);
let d = scale_surface(backend, &c, 0.5);
assert!((d.internal[0] - 2.5).abs() < 1e-15);
assert!((d.boundary[0].values[0] - 4.0).abs() < 1e-15);
let e = sub_surface(backend, &c, &a);
assert!((e.internal[0] - 3.0).abs() < 1e-15);
assert!((e.boundary[0].values[0] - 1.0).abs() < 1e-15);
let mut y = a.clone();
axpy_surface_assign(backend, &mut y, 2.0, &b);
assert!((y.internal[0] - 8.0).abs() < 1e-15);
assert!((y.boundary[0].values[0] - 9.0).abs() < 1e-15);
assert_eq!(y.name, "phi");
}
}
#[test]
fn vv_vol_integral_against_analytic_reference() {
let mesh = Arc::new(FvMesh::periodic_1d(1000, 2.0, 3.0)); let phi = VolScalarField::uniform("rho", mesh, 1.5);
for backend in BACKENDS {
assert!(
(vol_integral(backend, &phi) - 9.0).abs() < 1e-12,
"vol_integral should be 1.5 * 6 = 9.0"
);
assert!((vol_average(backend, &phi) - 1.5).abs() < 1e-12);
assert!((vol_min(backend, &phi) - 1.5).abs() < 1e-15);
assert!((vol_max(backend, &phi) - 1.5).abs() < 1e-15);
assert!((vol_l2_norm(backend, &phi) - 1.5 * (1000.0_f64).sqrt()).abs() < 1e-10);
}
}
#[test]
fn vol_vector_field_axpy() {
let mesh = vol_mesh(4);
let bnd = |v: Vector3| -> Vec<PatchField<Vector3>> {
mesh.patches
.iter()
.map(|p| PatchField::fixed_value_vec(p.size, v))
.collect()
};
let n = mesh.n_cells;
let mut u = VolVectorField::new(
"U",
mesh.clone(),
Field::uniform(n, Vector3::new(1.0, 0.0, 0.0)),
bnd(Vector3::new(2.0, 0.0, 0.0)),
);
let du = VolVectorField::new(
"ddt(U)",
mesh.clone(),
Field::uniform(n, Vector3::new(0.0, 4.0, 0.0)),
bnd(Vector3::new(0.0, 8.0, 0.0)),
);
axpy_vol_assign(ComputeBackend::CpuMulti, &mut u, 0.25, &du);
assert_eq!(u.internal[0], Vector3::new(1.0, 1.0, 0.0));
assert_eq!(u.boundary[0].values[0], Vector3::new(2.0, 2.0, 0.0));
assert_eq!(u.name, "U");
}
#[test]
fn name_does_not_grow_under_self_referential_reassignment_vol() {
for backend in BACKENDS {
let mesh = vol_mesh(16);
let mut rho = VolScalarField::uniform("rho", mesh.clone(), 1.0);
let drho = VolScalarField::uniform("div(phi)", mesh, 0.25);
let name_len_before = rho.name.len();
assert_eq!(name_len_before, 3);
for step in 0..64 {
rho = add_vol(backend, &rho, &drho);
assert_eq!(
rho.name.len(),
name_len_before,
"field name grew at step {step} on {backend:?} — the 2^step bug is back"
);
}
assert_eq!(rho.name, "rho");
assert!(
(rho.internal[0] - 17.0).abs() < 1e-12,
"data must still be right"
);
let mut p = VolScalarField::uniform("p", rho.mesh.clone(), 10.0);
for _ in 0..64 {
p = scale_vol(backend, &p, 1.0);
p = sub_vol(backend, &p, &drho);
}
assert_eq!(p.name, "p", "name grew through scale_vol/sub_vol chain");
}
}
#[test]
fn name_does_not_grow_under_self_referential_reassignment_surface() {
use crate::fields::surface_field::SurfaceScalarField;
for backend in BACKENDS {
let mesh = vol_mesh(16);
let mut phi = SurfaceScalarField::uniform("phi", mesh.clone(), 1.0);
let dphi = SurfaceScalarField::uniform("interpolate(rho)*phi", mesh, 0.5);
let before = phi.name.len();
for step in 0..64 {
phi = add_surface(backend, &phi, &dphi);
phi = scale_surface(backend, &phi, 1.0);
assert_eq!(
phi.name.len(),
before,
"surface field name grew at step {step} on {backend:?}"
);
}
assert_eq!(phi.name, "phi");
}
}
#[test]
fn name_does_not_grow_under_in_place_updates() {
for backend in BACKENDS {
let mesh = vol_mesh(16);
let mut rho = VolScalarField::uniform("rho", mesh.clone(), 1.0);
let ddt = VolScalarField::uniform("ddt(rho)", mesh, 1.0);
for _ in 0..256 {
axpy_vol_assign(backend, &mut rho, 0.5, &ddt);
}
assert_eq!(rho.name, "rho");
assert!((rho.internal[0] - 129.0).abs() < 1e-10);
}
}
#[test]
#[ignore = "measurement, not a correctness check; ~1 s wall clock. Run with --ignored --nocapture"]
fn measure_crossover_add() {
use std::time::Instant;
let cores = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
println!("available_parallelism() = {cores}");
println!("parallel feature enabled = {}", cfg!(feature = "parallel"));
println!(
"field_parallel_crossover() = {}",
field_parallel_crossover()
);
println!(
"{:>10} {:>14} {:>14} {:>10}",
"n", "serial [us]", "cpumulti [us]", "speedup"
);
for n in [
1024_usize, 4096, 16_384, 65_536, 131_072, 262_144, 1_048_576,
] {
let a = sample(n);
let b = sample_shift(n);
let best = |backend: ComputeBackend| -> f64 {
let _warm = add(backend, &a, &b);
let mut best = f64::INFINITY;
for _ in 0..9 {
let t = Instant::now();
let out = add(backend, &a, &b);
let dt = t.elapsed().as_secs_f64() * 1.0e6;
std::hint::black_box(&out);
best = best.min(dt);
}
best
};
let ser = best(ComputeBackend::Serial);
let par = forced_parallel_add_timing(&a, &b);
println!("{n:>10} {ser:>14.2} {par:>14.2} {:>10.2}", ser / par);
}
}
fn forced_parallel_add_timing(a: &Field<f64>, b: &Field<f64>) -> f64 {
use std::time::Instant;
#[cfg(feature = "parallel")]
let run = || -> Field<f64> {
Field::new(
a.as_slice()
.par_iter()
.zip(b.as_slice().par_iter())
.map(|(p, q)| *p + *q)
.collect::<Vec<f64>>(),
)
};
#[cfg(not(feature = "parallel"))]
let run = || -> Field<f64> { add(ComputeBackend::CpuMulti, a, b) };
let _warm = run();
let _warm2 = run();
let mut best = f64::INFINITY;
for _ in 0..9 {
let t = Instant::now();
let out = run();
let dt = t.elapsed().as_secs_f64() * 1.0e6;
std::hint::black_box(&out);
best = best.min(dt);
}
best
}
#[test]
#[ignore = "measurement, not a correctness check; ~2.4 s wall clock. Run with --ignored --nocapture"]
fn measure_kernel_timings() {
use std::time::Instant;
const N: usize = 1_048_576;
let cores = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
println!("available_parallelism() = {cores}, n = {N}");
println!("parallel feature enabled = {}", cfg!(feature = "parallel"));
let a = sample(N);
let b = sample_shift(N);
macro_rules! bench {
($label:expr, $body:expr) => {{
let mut best = f64::INFINITY;
let _warm = $body;
for _ in 0..5 {
let t = Instant::now();
let out = $body;
let dt = t.elapsed().as_secs_f64() * 1.0e6;
std::hint::black_box(&out);
best = best.min(dt);
}
println!("{:<24} {:>12.2} us", $label, best);
best
}};
}
for backend in [ComputeBackend::Serial, ComputeBackend::CpuMulti] {
println!("--- backend {backend:?} ---");
bench!("add", add(backend, &a, &b));
bench!("axpy", axpy(backend, &a, 0.5, &b));
bench!("pointwise_mul", pointwise_mul(backend, &a, &b));
bench!("sum", sum(backend, &a));
bench!("l2_norm", l2_norm(backend, &a));
bench!("dot", dot(backend, &a, &b));
let mut y = a.clone();
bench!("axpy_assign", axpy_assign(backend, &mut y, 1.0e-9, &b));
}
}