axiolid_evaluate/bound/separation.rs
1//! The separation of a surface's parameters over a box (#414).
2//!
3//! A parameter box maps injectively, and by a certified rate, when the
4//! surface's Jacobian `J = [S_u S_v]` is bounded below over it. Take the
5//! box's centre `m` and the scaling `D = diag(|S_u(m)|, |S_v(m)|)`, so that
6//! `J(m) D^-1` has unit columns, the smallest singular value of which is
7//! `sqrt(1 - |cos theta|)` (`theta` the angle between the columns; the
8//! entries of the 2x2 Gram matrix, bounded by Gershgorin). Over the box the
9//! columns move by at most the second partials' suprema times the box's
10//! half widths, and Weyl's inequality carries the singular value across: a
11//! matrix within `delta` (in the spectral norm) of one with smallest
12//! singular value `s` has smallest singular value at least `s - delta`,
13//! and `delta` is at most the Frobenius norm of the scaled column spread.
14//! Every matrix of the convex hull of `J` over the box is within `delta`
15//! too, which is what the injectivity needs: for `x`, `y` in the box,
16//! `S(x) - S(y)` is the mean of `J` along the segment applied to `x - y`.
17//!
18//! The computed jet at `m` is padded by [`JET_PAD`] per scaled column. A
19//! box where the separation cannot be shown positive (a pole or apex, a
20//! fold, a box too wide, a crease of a B-spline) has none.
21
22use axiolid_core::{Point3, Scalar, Vec3};
23use axiolid_curve::Carrier;
24use axiolid_surface::Surface;
25
26use super::interval::Iv;
27use super::{ordered, SurfaceBoundOracle, SurfaceBounds};
28
29/// Relative rounding allowance of the computed jet at a box's centre, per
30/// scaled column.
31const JET_PAD: Scalar = 256.0 * Scalar::EPSILON;
32
33/// A certified lower bound of a surface's Jacobian over a parameter box.
34///
35/// With `D = diag(scale)`, `|S(x) - S(y)| >= sigma |D (x - y)|` for `x`,
36/// `y` in the box, and `|J(x) w| >= sigma |D w|` at every point of it.
37/// `scale` is `|S_u|, |S_v|` at the box's centre as computed; it depends
38/// only on the centre.
39#[derive(Debug, Clone, Copy, PartialEq)]
40pub struct ParameterSeparation {
41 /// The scaling `|S_u|, |S_v|` at the box's centre.
42 pub scale: [Scalar; 2],
43 /// The separation, positive.
44 pub sigma: Scalar,
45}
46
47/// A box's jet at its centre, and how far the columns move over it.
48#[derive(Debug, Clone, Copy)]
49pub(crate) struct BoxJet {
50 /// `S_u` and `S_v` at the centre.
51 pub columns: [Vec3; 2],
52 /// Their norms, the scaling `D = diag(scale)`.
53 pub scale: [Scalar; 2],
54 /// The Frobenius norm of the scaled columns' movement over the box,
55 /// the padded jet included.
56 pub spread: Scalar,
57 /// The surface's bounds over the box.
58 pub bounds: SurfaceBounds,
59}
60
61impl SurfaceBoundOracle<'_> {
62 /// The point and the first partials at `(u, v)`, or `None` where the
63 /// surface does not evaluate to finite numbers there.
64 pub(crate) fn jet(&self, u: Scalar, v: Scalar) -> Option<(Point3, Vec3, Vec3)> {
65 use super::Source;
66 let (point, du, dv) = match self.source {
67 Source::Carrier(c) => {
68 let j = c.jet(u, v);
69 (j.point, j.u, j.v)
70 }
71 Source::Surface(s) => {
72 let j = crate::surface::jet(s, u, v).ok()?;
73 (j.point, j.du, j.dv)
74 }
75 };
76 let finite = point.is_finite() && du.is_finite() && dv.is_finite();
77 finite.then_some((point, du, dv))
78 }
79
80 /// The parameter domain of a B-spline surface, `None` for the
81 /// unbounded analytic ones.
82 pub(crate) fn domain(&self) -> Option<((Scalar, Scalar), (Scalar, Scalar))> {
83 use super::Source;
84 match self.source {
85 Source::Carrier(Carrier::Spline(b)) => b.domain(),
86 Source::Surface(Surface::BSpline(b)) => b.domain(),
87 _ => None,
88 }
89 }
90
91 /// The surface's columns at the centre of the box `[u.0, u.1] x [v.0,
92 /// v.1]`, their norms, and the Frobenius norm of how far the columns
93 /// can move over the box in parameters scaled by those norms (the
94 /// padded jet included); `None` for a box that is not smooth or has no
95 /// finite jet or a vanishing column at its centre.
96 pub(crate) fn box_jet(&self, u: (Scalar, Scalar), v: (Scalar, Scalar)) -> Option<BoxJet> {
97 let (u0, u1) = ordered(u.0, u.1);
98 let (v0, v1) = ordered(v.0, v.1);
99 let bounds = self.bounds((u0, u1), (v0, v1))?;
100 if !bounds.smooth {
101 return None;
102 }
103 let (um, vm) = (0.5 * (u0 + u1), 0.5 * (v0 + v1));
104 let (_, su, sv) = self.jet(um, vm)?;
105 let scale = [su.length(), sv.length()];
106 if !(scale.iter().all(|x| x.is_finite() && *x > 0.0)) {
107 return None;
108 }
109 // The columns' spread over the box, in scaled parameters.
110 let half = |lo: Scalar, hi: Scalar, m: Scalar| {
111 Iv::point(hi)
112 .add(Iv::point(-m))
113 .hi
114 .max(Iv::point(m).add(Iv::point(-lo)).hi)
115 };
116 let (ru, rv) = (Iv::point(half(u0, u1, um)), Iv::point(half(v0, v1, vm)));
117 let column = |x: Scalar, y: Scalar, scale: Scalar| {
118 Iv::point(x)
119 .mul(ru)
120 .add(Iv::point(y).mul(rv))
121 .div(Iv::point(scale))
122 };
123 let (cu, cv) = (
124 column(bounds.duu, bounds.duv, scale[0]),
125 column(bounds.duv, bounds.dvv, scale[1]),
126 );
127 let moved = cu.sq().add(cv.sq()).sqrt();
128 let spread = Iv::point(moved.hi).add(Iv::point(2.0 * JET_PAD)).hi;
129 spread.is_finite().then_some(BoxJet {
130 columns: [su, sv],
131 scale,
132 spread,
133 bounds,
134 })
135 }
136
137 /// The certified separation over the box `[u.0, u.1] x [v.0, v.1]`, or
138 /// `None` where it cannot be shown positive: a pole, an apex, a fold, a
139 /// box too wide, or a B-spline crease or edge inside it (`!smooth`).
140 #[must_use]
141 pub fn separation(
142 &self,
143 u: (Scalar, Scalar),
144 v: (Scalar, Scalar),
145 ) -> Option<ParameterSeparation> {
146 let jet = self.box_jet(u, v)?;
147 let [su, sv] = jet.columns;
148 let comp = |a: Vec3, b: Vec3| {
149 Iv::point(a.x)
150 .mul(Iv::point(b.x))
151 .add(Iv::point(a.y).mul(Iv::point(b.y)))
152 .add(Iv::point(a.z).mul(Iv::point(b.z)))
153 };
154 // The Gram matrix of the scaled columns.
155 let (sa, sb) = (Iv::point(jet.scale[0]), Iv::point(jet.scale[1]));
156 let (guu, gvv) = (comp(su, su).div(sa.mul(sa)), comp(sv, sv).div(sb.mul(sb)));
157 let guv = comp(su, sv).div(sa.mul(sb));
158 if !(guu.finite() && gvv.finite() && guv.finite()) {
159 return None;
160 }
161 let lambda = Iv::point(guu.lo.min(gvv.lo)).add(Iv::point(-guv.mag()));
162 let sigma = Iv::point(lambda.sqrt().lo).add(Iv::point(-jet.spread)).lo;
163 (sigma.is_finite() && sigma > 0.0).then_some(ParameterSeparation {
164 scale: jet.scale,
165 sigma,
166 })
167 }
168}