use super::{Interior, SweptFaces, chernyaev_numerator_test};
use crate::Error;
use crate::marching_cubes::ambiguity::face_is_joined;
fn opposed() -> SweptFaces<f64> {
SweptFaces::new([0.1, -2.0, 10.0, -2.0], [-10.0, 2.0, -0.1, 2.0]).expect("ambiguous faces")
}
#[test]
fn the_derived_coefficients_reproduce_the_numerator() {
let faces = opposed();
let d = |k: usize| faces.hi[k] - faces.lo[k];
let a = d(0) * d(2) - d(1) * d(3);
let b = faces.lo[2] * d(0) + faces.lo[0] * d(2) - faces.lo[3] * d(1) - faces.lo[1] * d(3);
let c = faces.lo[0] * faces.lo[2] - faces.lo[1] * faces.lo[3];
for step in 0..=20 {
let t = f64::from(step) / 20.0;
let expanded = a * t * t + b * t + c;
let direct = faces.numerator(t);
assert!(
(expanded - direct).abs() <= 1e-12 * direct.abs().max(1.0),
"t = {t}: expanded {expanded}, direct {direct}"
);
}
for root in faces.numerator_roots() {
assert!(
faces.numerator(root).abs() <= 1e-12 * a.abs().max(1.0),
"root {root} is not a root: F = {}",
faces.numerator(root)
);
}
}
#[test]
fn the_numerator_alone_reads_a_joined_cell_as_separated() {
let faces = opposed();
assert!(faces.denominator(0.0) > 0.0);
assert!(faces.denominator(1.0) < 0.0);
assert_eq!(faces.pole(), Some(0.5));
for step in 0..=100 {
let t = f64::from(step) / 100.0;
assert!(faces.numerator(t) < 0.0, "F({t}) = {}", faces.numerator(t));
}
assert!(
faces.saddle(0.75) > 0.0,
"saddle(0.75) = {}",
faces.saddle(0.75)
);
assert_eq!(chernyaev_numerator_test(&faces), Interior::Separated);
assert_eq!(faces.test(), Interior::Joined);
}
#[test]
fn the_saddle_position_is_where_the_bilinear_gradient_vanishes() {
let faces = opposed();
for step in 0..=20 {
let t = f64::from(step) / 20.0;
if faces.pole() == Some(t) {
continue;
}
let s = 1.0 - t;
let c = |k: usize| faces.lo[k] * s + faces.hi[k] * t;
let (a, b, cc, d) = (c(0), c(1), c(2), c(3));
let [u, v] = faces.saddle_position(t);
let df_du = -a * (1.0 - v) + b * (1.0 - v) + cc * v - d * v;
let df_dv = -a * (1.0 - u) - b * u + cc * u + d * (1.0 - u);
let scale = (a.abs() + b.abs() + cc.abs() + d.abs()).max(1.0);
assert!(
df_du.abs() <= 1e-9 * scale && df_dv.abs() <= 1e-9 * scale,
"t = {t}: gradient at the saddle is ({df_du}, {df_dv}), not zero"
);
let f = a * (1.0 - u) * (1.0 - v) + b * u * (1.0 - v) + cc * u * v + d * (1.0 - u) * v;
assert!(
(f - faces.saddle(t)).abs() <= 1e-9 * scale,
"t = {t}: bilinear at the saddle is {f}, saddle() says {}",
faces.saddle(t)
);
}
}
#[test]
fn without_a_pole_the_correction_changes_nothing() {
let aligned = [
([1.0, -1.0, 1.0, -1.0], [2.0, -3.0, 0.5, -1.0]),
([0.25, -4.0, 4.0, -0.25], [3.0, -0.5, 0.5, -3.0]),
([5.0, -1.0, 0.2, -2.0], [0.1, -0.1, 9.0, -9.0]),
];
for (lo, hi) in aligned {
let faces = SweptFaces::new(lo, hi).expect("ambiguous faces");
assert_eq!(faces.pole(), None, "{lo:?} -> {hi:?}");
assert!(faces.denominator(0.0) > 0.0 && faces.denominator(1.0) > 0.0);
assert_eq!(
faces.test(),
chernyaev_numerator_test(&faces),
"{lo:?} -> {hi:?}: the two tests should agree with no pole"
);
}
}
#[test]
fn a_face_with_no_saddle_is_rejected_rather_than_defaulted() {
assert_eq!(
SweptFaces::new([1.0, 1.0, 1.0, 1.0], [1.0, -1.0, 1.0, -1.0]),
Err(Error::DegenerateSweep)
);
assert_eq!(
SweptFaces::new([1.0, -1.0, 1.0, -1.0], [2.0, 2.0, 2.0, 2.0]),
Err(Error::DegenerateSweep)
);
assert!(SweptFaces::new([0.1, -2.0, 10.0, -2.0], [-10.0, 2.0, -0.1, 2.0]).is_ok());
}
#[test]
fn how_often_the_correction_changes_the_answer() {
let magnitudes = [0.1, 0.25, 1.0, 4.0, 10.0];
let mut total = 0u32;
let mut disagree = 0u32;
let mut with_pole = 0u32;
for a0 in magnitudes {
for c0 in magnitudes {
for b0 in magnitudes {
for a1 in magnitudes {
for c1 in magnitudes {
for b1 in magnitudes {
let lo = [a0, -b0, c0, -b0];
let hi = [-a1, b1, -c1, b1];
let Ok(faces) = SweptFaces::new(lo, hi) else {
continue;
};
total += 1;
if faces.pole().is_some() {
with_pole += 1;
}
if faces.test() != chernyaev_numerator_test(&faces) {
disagree += 1;
}
}
}
}
}
}
}
std::println!(
"opposed face pairs: {total} configurations, {with_pole} with a pole in the sweep, \
{disagree} where the numerator-only test gives the wrong answer"
);
assert_eq!((total, with_pole, disagree), (15625, 15625, 1966));
}
#[test]
fn the_test_is_deterministic_and_independent_of_diagonal_order() {
let faces = opposed();
let rotate2 = |v: [f64; 4]| [v[2], v[3], v[0], v[1]];
let rotated =
SweptFaces::new(rotate2(faces.lo), rotate2(faces.hi)).expect("still ambiguous faces");
assert_eq!(faces.test(), rotated.test());
assert_eq!(faces.pole(), rotated.pole());
let pole = faces.pole();
for step in 0..=20 {
let t = f64::from(step) / 20.0;
if pole == Some(t) {
assert!(
!faces.saddle(t).is_finite(),
"the pole should not be finite"
);
continue;
}
let (a, b) = (faces.saddle(t), rotated.saddle(t));
assert_eq!(
a.to_bits(),
b.to_bits(),
"t = {t}: {a} vs {b} -- two cells reading one face must agree \
bit-for-bit, or they can disagree about a tunnel"
);
}
assert_eq!(faces.test(), faces.test());
}
#[test]
fn catastrophic_cancellation_does_not_hide_a_positive_saddle() {
let faces = SweptFaces::new(
[0.3, -1.0, 0.3, -0.9926951163344483],
[-1.7, 1.0, -1.7, 1.0073048836605518],
)
.expect("ambiguous faces");
let pole = faces.pole().expect("Δ changes sign inside this sweep");
assert!(
faces.saddle(pole - 1e-7) > 0.0,
"saddle(pole − 1e-7) = {}",
faces.saddle(pole - 1e-7)
);
assert!(faces.saddle(0.0) < 0.0 && faces.saddle(1.0) < 0.0);
assert_eq!(faces.test(), Interior::Joined);
}
#[test]
#[allow(clippy::float_cmp)] fn joined_names_the_positive_regions_and_the_face_rule_names_the_negative() {
let faces = opposed();
for (v, t) in [(faces.lo, 0.0), (faces.hi, 1.0)] {
let s = faces.saddle(t);
assert_ne!(s, 0.0, "a tied saddle at {v:?} would pin nothing");
assert_eq!(
face_is_joined(v),
s < 0.0,
"face {v:?}: saddle {s} disagrees with the face rule's polarity"
);
}
assert!(faces.saddle(1.0) > 0.0);
assert!(!face_is_joined(faces.hi));
assert_eq!(faces.test(), Interior::Joined);
}
#[test]
fn two_cells_reading_one_face_agree_bit_for_bit() {
let old = |v: [f64; 4]| ((v[0] + v[2]) - v[1]) - v[3];
let rotate2 = |v: [f64; 4]| [v[2], v[3], v[0], v[1]];
let corners = [1.0, 1.0, 1.0, 1e-8];
assert_ne!(
old(corners).to_bits(),
old(rotate2(corners)).to_bits(),
"the fixture no longer distinguishes the two subtraction orders, so it \
cannot show that grouping the diagonals is what fixes them"
);
let faces = SweptFaces::new(corners, corners).expect("a non-degenerate sweep");
let rotated =
SweptFaces::new(rotate2(corners), rotate2(corners)).expect("a non-degenerate sweep");
for step in 0..=20 {
let t = f64::from(step) / 20.0;
assert_eq!(
faces.denominator(t).to_bits(),
rotated.denominator(t).to_bits(),
"t = {t}: the denominator moved under a rotation that preserves \
both diagonals"
);
if faces.pole() == Some(t) {
continue;
}
assert_eq!(
faces.saddle(t).to_bits(),
rotated.saddle(t).to_bits(),
"t = {t}: the saddle moved, so two cells could disagree about a tunnel"
);
}
assert_eq!(faces.test(), rotated.test());
assert_eq!(faces.pole(), rotated.pole());
}