Skip to main content

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}