Tests: the sample-aliasing trap aliases by its knots, within the degree a record holds

sample_aliasing_does_not_certify_spline_caps built its trap as one polynomial of degree 17 with
a root at each of the 17 old sample locations k/16 (and a circle times such a polynomial, degree
19). A B-rep record holds splines up to degree 16 (MAX_SPLINE_DEGREE, #8618), so no record can
carry either and the evaluator refuses them: the test failed before testing anything. The trap
is now a C0 spline with a knot at every k/16: a quadratic on the line at each knot, bowing 1 mm
off it half way along the first span; and the quarter circle of radius 10 with its homogeneous
coordinates scaled by 1 + bump / 10, radius 10 at every knot and 11 at 1/32. Sampling at the old
locations still sees a line and a circle; the certifier, reading the spans' control points,
must certify neither, and doesn't.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-29 00:31:39 -04:00
committed by Irakli Kourtis
parent 6387be309f
commit 55f48e4e58
+49 -18
View File
@@ -578,14 +578,35 @@ mod tests {
}
out
}
fn trap() -> Vec<f64> {
let mut p = vec![1.];
for k in 0..=16 {
p = multiply(&p, &[-(k as f64) / 16., 1.]);
/// The power basis `p` (in t) over the span t = a + h s, in s.
fn shifted(p: &[f64], a: f64, h: f64) -> Vec<f64> {
let mut out = vec![0.];
for c in p.iter().rev() {
out = multiply(&out, &[a, h]);
out[0] += c;
}
let mid = (0..=16).map(|k| 1. / 32. - k as f64 / 16.).product::<f64>();
p.iter_mut().for_each(|v| *v /= mid);
p
out
}
/// A spline of `degree` over spans [k/16, (k+1)/16] (k = 0..16), joined C0 (each interior
/// knot `degree` times), whose homogeneous `x`, `y`, `w` are the power bases `f(k)` gives
/// in the span's own parameter s ∈ [0, 1]: a curve that bends only between the knots.
fn spans(degree: usize, f: impl Fn(usize) -> [Vec<f64>; 3]) -> Curve {
let mut knots = vec![0.; degree + 1];
let mut homogeneous: Vec<[f64; 4]> = Vec::new();
for k in 0..16 {
let [x, y, w] = f(k);
let (x, y, w) = (bernstein(&x, degree), bernstein(&y, degree), bernstein(&w, degree));
let controls = x.iter().zip(y).zip(w).map(|((x, y), w)| [*x, y, 0., w]);
homogeneous.extend(controls.skip(usize::from(k > 0)));
knots.extend(std::iter::repeat_n((k + 1) as f64 / 16., degree));
}
knots.push(1.);
Curve::RationalSpline { knots, homogeneous }
}
/// The bump s (1 - s) / (1/4) in the span's own parameter: 0 at both its ends (every old
/// sample location k/16 is a knot), 1 half way along the first span (t = 1/32).
fn bump(k: usize) -> Vec<f64> {
if k == 0 { vec![0., 4., -4.] } else { vec![0.] }
}
fn bezier_knots(degree: usize) -> Vec<f64> {
std::iter::repeat_n(0., degree + 1).chain(std::iter::repeat_n(1., degree + 1)).collect()
@@ -615,27 +636,37 @@ mod tests {
Curve::RationalSpline { knots: bezier_knots(2), homogeneous: vec![[10., 0., 0., 1.], [10. * w, 10. * w, 0., w], [0., 10., 0., 1.]] }
}
/// Sampling must not certify a spline cap that merely passes through the sample points.
/// Once that took a single polynomial of degree 17 with a root at each of the 17 old sample
/// locations k/16, more than a B-rep record holds (MAX_SPLINE_DEGREE 16); here the aliasing is
/// in the knots instead: a C0 quadratic spline with a knot at every k/16, on the line (or
/// the circle) at each of them, and bowing off it between (by 1 mm, 1/32 along).
#[test]
fn sample_aliasing_does_not_certify_spline_caps() {
let e = curve(&[0., 10.], &trap(), &[1.], 17);
let e = spans(2, |k| {
let h = 1. / 16.;
[shifted(&[0., 10.], k as f64 * h, h), bump(k), vec![1.]]
});
for k in 0..=16 {
assert!(at(&e, k as f64 / 16.).y.abs() < 1e-4, "old sample locations must see a line");
assert!(at(&e, k as f64 / 16.).y.abs() < 1e-12, "old sample locations must see a line");
}
assert!((at(&e, 1. / 32.).y - 1.).abs() < 1e-6);
assert!((at(&e, 1. / 32.).y - 1.).abs() < 1e-12);
assert!(segment(&run(&e, true), &Plane::XY, 1e-3, &mut 2_000_000).is_none());
let q = trap();
let mut perturb = q.iter().map(|v| v * 0.1).collect::<Vec<_>>();
perturb[0] += 1.;
// The quarter circle of radius 10, its homogeneous coordinates scaled by 1 + bump / 10 but
// its weight not: radius 10 at every k/16, 11 at 1/32.
let w = std::f64::consts::FRAC_1_SQRT_2;
let x = multiply(&[10., 20. * (w - 1.), 10. * (1. - 2. * w)], &perturb);
let y = multiply(&[0., 20. * w, 10. * (1. - 2. * w)], &perturb);
let circle = curve(&x, &y, &[1., 2. * (w - 1.), 2. * (1. - w)], 19);
let (x, y, wt) = ([10., 20. * (w - 1.), 10. * (1. - 2. * w)], [0., 20. * w, 10. * (1. - 2. * w)], [1., 2. * (w - 1.), 2. * (1. - w)]);
let circle = spans(4, |k| {
let (a, h) = (k as f64 / 16., 1. / 16.);
let scale: Vec<f64> = bump(k).iter().enumerate().map(|(i, v)| v / 10. + if i == 0 { 1. } else { 0. }).collect();
[multiply(&shifted(&x, a, h), &scale), multiply(&shifted(&y, a, h), &scale), shifted(&wt, a, h)]
});
for k in 0..=16 {
let p = at(&circle, k as f64 / 16.);
assert!((p.x.hypot(p.y) - 10.).abs() < 1e-3, "old sample locations must see a circle");
assert!((p.x.hypot(p.y) - 10.).abs() < 1e-9, "old sample locations must see a circle");
}
let p = at(&circle, 1. / 32.);
assert!((p.x.hypot(p.y) - 11.).abs() < 1e-5);
assert!((p.x.hypot(p.y) - 11.).abs() < 1e-9);
assert!(segment(&run(&circle, true), &Plane::XY, 1e-3, &mut 2_000_000).is_none());
}