use glam::Vec3;
use super::{
FurnaceResult, bruted_girdle_finishes,
furnace::{
EDGE_ROUNDING_FURNACE_CONVERGENCE_TOLERANCE, FROSTED_FURNACE_CONVERGENCE_TOLERANCE,
FURNACE_CONVERGENCE_TOLERANCE, FURNACE_DEFAULT_MAX_BOUNCES,
SCATTERING_FURNACE_CONVERGENCE_TOLERANCE, SCATTERING_FURNACE_MAX_BOUNCES,
run_furnace_for_run,
},
furnace_material, round_brilliant_planes,
};
use crate::{
optics::{materials::GemMaterial, raytracer::FacetFinish},
renderer::gpu::GpuContext,
};
const RUNS: u32 = 4;
const HIGH_CAP: u32 = 256;
const SIGMA: f32 = 4.0;
struct Anchor {
name: &'static str,
budget: f32,
max_bounces: u32,
material: GemMaterial,
finishes: Vec<FacetFinish>,
use_hdr_nee: bool,
}
fn anchors() -> Vec<Anchor> {
let frosted = bruted_girdle_finishes(round_brilliant_planes().len());
let scattering = || furnace_material().with_scattering(1.2, 0.4);
vec![
Anchor {
name: "polished",
budget: FURNACE_CONVERGENCE_TOLERANCE,
max_bounces: FURNACE_DEFAULT_MAX_BOUNCES,
material: furnace_material(),
finishes: Vec::new(),
use_hdr_nee: false,
},
Anchor {
name: "frosted girdle",
budget: FROSTED_FURNACE_CONVERGENCE_TOLERANCE,
max_bounces: FURNACE_DEFAULT_MAX_BOUNCES,
material: furnace_material(),
finishes: frosted.clone(),
use_hdr_nee: false,
},
Anchor {
name: "lossless scattering",
budget: SCATTERING_FURNACE_CONVERGENCE_TOLERANCE,
max_bounces: SCATTERING_FURNACE_MAX_BOUNCES,
material: scattering(),
finishes: Vec::new(),
use_hdr_nee: false,
},
Anchor {
name: "edge rounding",
budget: EDGE_ROUNDING_FURNACE_CONVERGENCE_TOLERANCE,
max_bounces: FURNACE_DEFAULT_MAX_BOUNCES,
material: furnace_material().with_edge_rounding(0.03),
finishes: Vec::new(),
use_hdr_nee: false,
},
Anchor {
name: "lossless scattering, HDR NEE",
budget: SCATTERING_FURNACE_CONVERGENCE_TOLERANCE,
max_bounces: SCATTERING_FURNACE_MAX_BOUNCES,
material: scattering(),
finishes: Vec::new(),
use_hdr_nee: true,
},
Anchor {
name: "frosted girdle, HDR NEE",
budget: FROSTED_FURNACE_CONVERGENCE_TOLERANCE,
max_bounces: FURNACE_DEFAULT_MAX_BOUNCES,
material: furnace_material(),
finishes: frosted,
use_hdr_nee: true,
},
]
}
#[derive(Clone, Copy)]
struct Side {
bias: [f32; 3],
noise: [f32; 3],
standard_error: [f32; 3],
}
struct Level {
max_bounces: u32,
cpu: Side,
gpu: Side,
}
fn relative(value: Vec3, target: Vec3) -> [f32; 3] {
let rel = |v: f32, t: f32| v / t.abs().max(1e-6);
[
rel(value.x, target.x),
rel(value.y, target.y),
rel(value.z, target.z),
]
}
fn side(errors: &[[f32; 3]], standard_errors: &[[f32; 3]]) -> Side {
let n = errors.len() as f32;
let mut out = Side {
bias: [0.0; 3],
noise: [0.0; 3],
standard_error: [0.0; 3],
};
for c in 0..3 {
let mean = errors.iter().map(|e| e[c]).sum::<f32>() / n;
let variance = errors
.iter()
.map(|e| e[c] - mean)
.fold(0.0f32, |acc, d| d.mul_add(d, acc))
/ (n - 1.0);
out.bias[c] = mean;
out.noise[c] = variance.sqrt();
out.standard_error[c] = standard_errors.iter().map(|e| e[c]).sum::<f32>() / n;
}
out
}
fn measure(ctx: &GpuContext, anchor: &Anchor, max_bounces: u32) -> Level {
let results: Vec<FurnaceResult> = (0..RUNS)
.map(|run| {
run_furnace_for_run(
ctx,
&anchor.material,
&anchor.finishes,
anchor.use_hdr_nee,
max_bounces,
run,
)
})
.collect();
let signed = |pick: fn(&FurnaceResult) -> Vec3| -> Vec<[f32; 3]> {
results
.iter()
.map(|r| relative(pick(r) - r.analytic_target, r.analytic_target))
.collect()
};
let spread = |pick: fn(&FurnaceResult) -> Vec3| -> Vec<[f32; 3]> {
results
.iter()
.map(|r| relative(pick(r), r.analytic_target))
.collect()
};
Level {
max_bounces,
cpu: side(&signed(|r| r.cpu_mean), &spread(|r| r.cpu_standard_error)),
gpu: side(&signed(|r| r.gpu_mean), &spread(|r| r.gpu_standard_error)),
}
}
fn describe(label: &str, max_bounces: u32, side: &Side) -> String {
let [bx, by, bz] = side.bias;
let [nx, ny, nz] = side.noise;
let [sx, sy, sz] = side.standard_error;
format!(
"{label} cap {max_bounces:>3}: bias ({bx:+.4}, {by:+.4}, {bz:+.4}) noise ({nx:.4}, \
{ny:.4}, {nz:.4}) standard error ({sx:.4}, {sy:.4}, {sz:.4})"
)
}
fn justified_budget(own: &Level) -> f32 {
let needed = [own.cpu, own.gpu]
.iter()
.flat_map(|side| {
(0..3).map(|c| {
SIGMA.mul_add(
side.noise[c].max(side.standard_error[c]),
side.bias[c].abs(),
)
})
})
.fold(0.0, f32::max);
(needed / 0.005).ceil() * 0.005
}
fn report(anchor: &Anchor, own: &Level, high: &Level) {
for (label, own_side, high_side) in [("cpu", own.cpu, high.cpu), ("gpu", own.gpu, high.gpu)] {
let [tx, ty, tz] = [0, 1, 2].map(|c| own_side.bias[c] - high_side.bias[c]);
println!(
" truncation loss at cap {} on the {label}: ({tx:+.4}, {ty:+.4}, {tz:+.4})",
anchor.max_bounces
);
}
println!(
" budget {:.3} today; the measurements justify {:.3}",
anchor.budget,
justified_budget(own)
);
}
fn check(anchor: &Anchor, own: &Level, high: &Level, findings: &mut Vec<String>) {
for (label, side) in [("cpu", high.cpu), ("gpu", high.gpu)] {
for c in 0..3 {
if side.bias[c].abs() > SIGMA * side.standard_error[c] {
findings.push(format!(
"{}: {label} component {c} bias {:+.4} at cap {HIGH_CAP} exceeds {SIGMA} \
standard errors ({:.4}): an energy error beyond the truncation loss",
anchor.name, side.bias[c], side.standard_error[c]
));
}
}
}
let justified = justified_budget(own);
if justified > anchor.budget {
findings.push(format!(
"{}: the measurements need {justified:.3}, above the budget {:.3} in force",
anchor.name, anchor.budget
));
}
}
#[test]
#[ignore = "a measurement on the GPU adapter over millions of samples per anchor: run \
with --features gpu --ignored --nocapture, see the module doc"]
fn furnace_budgets_from_measured_runs() {
let ctx = match GpuContext::acquire() {
Ok(ctx) => ctx,
Err(error) => {
println!("skipping furnace_budgets_from_measured_runs: no GPU adapter ({error})");
return;
}
};
let mut findings = Vec::new();
for anchor in anchors() {
println!(
"[{}] budget {:.3} at cap {}",
anchor.name, anchor.budget, anchor.max_bounces
);
let own = measure(&ctx, &anchor, anchor.max_bounces);
let high = measure(&ctx, &anchor, HIGH_CAP);
for level in [&own, &high] {
println!(" {}", describe("cpu", level.max_bounces, &level.cpu));
println!(" {}", describe("gpu", level.max_bounces, &level.gpu));
}
report(&anchor, &own, &high);
check(&anchor, &own, &high, &mut findings);
}
assert!(findings.is_empty(), "{}", findings.join("\n"));
}