Skip to main content

geop_core_math/solvers/system/
enclose.rs

1//! [`System::enclose`]: a rigorous enclosure of a true solution near where
2//! a solve ended, by Krawczyk's test, in each block's step coordinates.
3//!
4//! The constraints determine some variables and leave the rest free. The
5//! free ones are the designer's choice, and stay exactly as they are. The
6//! determined ones are first polished by Newton's method on the
7//! independent rows, then enclosed: for a box `Δ` of steps around the
8//! polished point `x̃` — a value `x = retract(x̃, δ)` for every `δ` in it —
9//! with `Y` any approximate inverse of the Jacobian at `x̃`,
10//!
11//! ```text
12//! K(Δ) = -Y f(x̃) + (I - Y J(Δ)) Δ
13//! ```
14//!
15//! and `K(Δ) ⊆ Δ` together with `‖I - Y J(Δ)‖∞ < 1` prove that `Δ` holds
16//! exactly one solution of the independent rows — in interval arithmetic,
17//! so the proof is rigorous. `K(Δ) ⊆ Δ` gives a zero of `Y f` in `Δ`
18//! (Brouwer); the norm bound makes `Y` and every matrix of `J(Δ)` regular,
19//! so it is a zero of `f`, the independent rows stay independent over the
20//! whole box, and no second zero exists. `J(Δ)` is the Jacobian by the
21//! steps at every `x` the box reaches, times the differential of the
22//! retraction there ([`Variable::differential`]). The box is widened until
23//! the first test passes; its width is then how precisely the constraints
24//! pin the solution down, and `retract(x̃, K(Δ) ∩ Δ)` is returned.
25//!
26//! Nothing is sharpened but the Newton iterates, each only a seed for the
27//! next: `Y`, `f(x̃)` and `J(Δ)` are enclosures, and so is the box.
28//!
29//! **Which rows are independent** is decided over a box, not at a point. A
30//! redundant constraint — a rectangle's fourth right angle, an arc
31//! concentric with a center it already has, a mate a joint implies — says
32//! what the others do, often only on the solution. Where the blocks are
33//! now, a solution only to the solver's tolerance, it looks independent by
34//! a pivot about as small as that tolerance; taken as independent, it makes
35//! the solution a curve rather than a point, which nothing proves. So the
36//! independent rows are chosen by eliminating the Jacobian over a box of
37//! steps around where the blocks are — first the point itself, then a box
38//! as wide as the solver's tolerance, then twice as wide, and so on: a
39//! pivot that could be zero somewhere in the box is no pivot, and once the
40//! box reaches the solution a redundant row's pivot straddles zero. Each
41//! new choice is put to the Krawczyk test.
42//!
43//! Then, over the proven box, the rows left out must vanish and add nothing
44//! to the rank. A constraint the others imply leaves the rank as it is
45//! wherever they all hold, so over a box holding such a point too. One left
46//! out only because the configuration is singular — a point on a circle and
47//! on its tangent, the solve a hair off the touching point — is independent
48//! there: its value too small to tell from zero, its derivative not. Without
49//! this the box held the free variable exactly where the solve left it,
50//! where no solution is. How free the blocks are is decided over the same
51//! box.
52//!
53//! A row left out that does not vanish is a conflict only where one is
54//! proven ([`EncloseError::Conflict`], naming it and the constraints it was
55//! reduced by): the solution the box holds was found with the free
56//! variables held, so it says nothing of other values of them. A
57//! combination of the rows vanishes wherever they all do; definitely not
58//! zero over a box in every variable around where the solve ended, it
59//! proves that no point of that box is a solution. Interval arithmetic can
60//! prove a conflict, never its absence: a row enclosing zero agrees with
61//! the others to every digit the box resolves. A limit is enclosed as a
62//! constraint that vanishes where it could be at its bound, and must be
63//! proven to hold over the box elsewhere.
64//!
65//! The solution proven is the one Newton reaches from where the solve
66//! ended: for a solve that converged, the one it ended near.
67
68use crate::{
69    geop_error::{GeopError, GeopResult},
70    scalars::Scalar,
71    solvers::linear::{augmented, eliminate, inverse, null_space, rank},
72};
73
74use super::{Batch, Bound, Equations, Group, Solving, System, Term, Variable, tolerance};
75
76/// Newton steps polishing a solution before it is enclosed: each squares the
77/// error, so a handful take [`super::RELATIVE_TOLERANCE`]
78/// to rounding.
79const POLISH_STEPS: usize = 6;
80
81/// How many candidate boxes are tried, each twice as wide as the last one
82/// needed: how hard it tries, never what a verified box means.
83const ENCLOSE_ATTEMPTS: usize = 24;
84
85/// An enclosure of a true solution, group by group, and how free the
86/// blocks are (see [`System::enclose`]).
87#[derive(Debug)]
88pub struct Enclosure<V: Variable> {
89    /// Per block, every value it could have at the true solution its group
90    /// was proven to hold — `None` for a block whose group was not
91    /// enclosed, or not proven.
92    pub boxes: Vec<Option<V>>,
93    /// Per block, how many independent ways it can still move: decided
94    /// over the proven box, or — not proven — at the point.
95    pub freedom: Vec<usize>,
96    /// How many independent ways the blocks can move together.
97    pub total: usize,
98    /// Why the groups not proven were not, each with its blocks.
99    pub failures: Vec<(Vec<usize>, EncloseError)>,
100    /// How many times the equations were made to enclose it — every
101    /// group's at once: the work it took, which a test can bound.
102    pub evaluations: usize,
103    /// Per block, how it moves along the ways the blocks can move together
104    /// that reach it: each way by its index, and the block's step along
105    /// it. What a pendant moving with what it hangs off reads (see
106    /// [`Enclosure::moved`]).
107    motions: Vec<Vec<(usize, Vec<Scalar>)>>,
108}
109
110impl<V: Variable> Enclosure<V> {
111    /// Nothing enclosed yet, of `blocks` blocks.
112    pub(super) fn empty(blocks: usize) -> Self {
113        Self {
114            boxes: vec![None; blocks],
115            freedom: vec![0; blocks],
116            total: 0,
117            failures: Vec::new(),
118            evaluations: 0,
119            motions: vec![Vec::new(); blocks],
120        }
121    }
122
123    /// How free the blocks of `group` are — each of `dimensions` numbers —
124    /// as `motions` says it moves: the ways it moves of its own, and for a
125    /// pendant, the ways what it hangs off moves, each of which its rows
126    /// make it follow by the step `δ` with `R δ = -R_on v` — `R` its rows
127    /// by its own steps, `R_on` by those of what it hangs off, `v` their
128    /// step along that way. Of a block-triangular system, the null space
129    /// is that of the cores, carried along, and each pendant's own.
130    fn moved(&mut self, group: &Group, dimensions: &[usize], on: &[usize], motions: Motions) {
131        let n: usize = dimensions.iter().sum();
132        let mut ways: Vec<usize> = (group.on.iter())
133            .flat_map(|&s| self.motions[s].iter().map(|(way, _)| *way))
134            .collect();
135        ways.sort_unstable();
136        ways.dedup();
137        // Per way it follows, its step: along every column of the group.
138        let mut steps: Vec<(usize, Vec<Scalar>)> = Vec::new();
139        if !ways.is_empty() && !motions.rows.is_empty() {
140            let along: Vec<Vec<Scalar>> = (ways.iter())
141                .map(|way| {
142                    let mut v = Vec::new();
143                    for (&s, &width) in group.on.iter().zip(on) {
144                        match self.motions[s].iter().find(|(w, _)| w == way) {
145                            Some((_, step)) => v.extend(step.iter().copied()),
146                            None => v.extend(std::iter::repeat_n(Scalar::ZERO, width)),
147                        }
148                    }
149                    v
150                })
151                .collect();
152            let rows: Vec<Vec<Scalar>> = (motions.rows.iter())
153                .map(|(slope, by)| {
154                    let mut row = slope.clone();
155                    row.extend(along.iter().map(|v| {
156                        (by.iter().zip(v)).fold(Scalar::ZERO, |sum, (a, b)| sum.add(a.mul(*b)))
157                    }));
158                    row
159                })
160                .collect();
161            let reduced = eliminate(rows, n);
162            for (j, &way) in ways.iter().enumerate() {
163                let mut step = vec![Scalar::ZERO; n];
164                for (k, &(column, _)) in reduced.pivots.iter().enumerate() {
165                    step[column] = reduced.rows[k][n + j].neg();
166                }
167                steps.push((way, step));
168            }
169        }
170        // Its own ways, numbered after every way before.
171        let (first, own) = (self.total, motions.basis.len());
172        steps.extend((motions.basis.into_iter().enumerate()).map(|(k, v)| (first + k, v)));
173        self.total += own;
174        let mut column = 0;
175        for (&block, &dimension) in group.blocks.iter().zip(dimensions) {
176            let own: Vec<(usize, Vec<Scalar>)> = (steps.iter())
177                .map(|(way, step)| (*way, step[column..column + dimension].to_vec()))
178                .collect();
179            self.freedom[block] = rank(own.iter().map(|(_, v)| v.clone()).collect(), dimension);
180            self.motions[block] = own;
181            column += dimension;
182        }
183    }
184}
185
186/// How a group moves where its rows are decided, to first order (see
187/// [`Enclosure::moved`]): the null space of its rows by its own steps, and
188/// the rows that vanish — each its slope by its own steps and by those of
189/// what it hangs off.
190pub(super) struct Motions {
191    basis: Vec<Vec<Scalar>>,
192    rows: Vec<(Vec<Scalar>, Vec<Scalar>)>,
193}
194
195impl Motions {
196    /// Of a group of `n` variables no row is known of: every way free.
197    fn free(n: usize) -> Self {
198        Motions {
199            basis: (0..n)
200                .map(|k| {
201                    (0..n)
202                        .map(|j| if j == k { Scalar::ONE } else { Scalar::ZERO })
203                        .collect()
204                })
205                .collect(),
206            rows: Vec::new(),
207        }
208    }
209}
210
211/// Why no enclosure of a solution is proven.
212#[derive(Debug)]
213pub enum EncloseError {
214    /// Constraints that cannot all hold anywhere within `within` — in
215    /// each block's steps — of where the solve ended: a combination of the
216    /// rows, which vanishes wherever they all do, is definitely not zero
217    /// over that box. By the constraint's index, each with what the
218    /// combination reducing it is there; `with` are the constraints it was
219    /// reduced by, by index: what it conflicts with.
220    Conflict {
221        conflicting: Vec<(usize, Scalar)>,
222        with: Vec<usize>,
223        within: Scalar,
224    },
225    /// No solution was proven (see [`System::enclose`]).
226    Unproven(GeopError),
227}
228
229impl From<GeopError> for EncloseError {
230    fn from(e: GeopError) -> Self {
231        EncloseError::Unproven(e)
232    }
233}
234
235impl EncloseError {
236    /// As an error, each constraint named by `name`.
237    pub fn named(self, name: impl Fn(usize) -> String) -> GeopError {
238        match self {
239            EncloseError::Conflict {
240                conflicting,
241                with,
242                within,
243            } => {
244                let mut constraints: Vec<usize> = conflicting.iter().map(|&(c, _)| c).collect();
245                constraints.dedup();
246                let names =
247                    |cs: &[usize]| cs.iter().map(|&c| name(c)).collect::<Vec<_>>().join(", ");
248                let others = if with.is_empty() {
249                    "the others".to_string()
250                } else {
251                    names(&with)
252                };
253                let off: Vec<Scalar> = conflicting.iter().map(|&(_, v)| v).collect();
254                GeopError::new(format!(
255                    "the constraints conflict: {} cannot hold where {others} do within {within:?} of here (off by {off:?} there)",
256                    names(&constraints),
257                ))
258            }
259            EncloseError::Unproven(e) => e,
260        }
261    }
262}
263
264/// Why a proof of the rows chosen is not taken: rows that do not vanish
265/// where the chosen ones do, by index among the rows — which may be a
266/// conflict ([`Solving::conflict`]) — or why it failed.
267enum Rejected {
268    Off(Vec<usize>),
269    Failed(EncloseError),
270}
271
272use Rejected::{Failed, Off};
273
274impl From<GeopError> for Rejected {
275    fn from(e: GeopError) -> Self {
276        Failed(EncloseError::Unproven(e))
277    }
278}
279
280/// One row of a constraint over a box (see [`Solving::rows_over`]).
281#[derive(Clone)]
282struct Row {
283    /// The constraint it is one of, by index.
284    constraint: usize,
285    value: Scalar,
286    /// Its derivative by every variable of the group.
287    slope: Vec<Scalar>,
288    /// Its derivative by the steps of the blocks the group hangs off, one
289    /// after another.
290    on: Vec<Scalar>,
291}
292
293/// A group enclosed: the box of steps around `center` its true solution
294/// is in — or why there is none — and how it moves.
295pub(super) struct Enclosed<V: Variable> {
296    pub boxes: Result<Vec<V>, EncloseError>,
297    pub motions: Motions,
298}
299
300impl<E: Equations> System<'_, E> {
301    /// [`System::enclose`] of the groups `groups`, layer by layer (see
302    /// [`super::Structure`]): a pendant over the boxes of what it hangs off
303    /// — a box of its steps holding, for every value of theirs in theirs,
304    /// exactly one solution of its own constraints — and none where what it
305    /// hangs off is not proven.
306    pub(super) fn enclose_groups(
307        &self,
308        at: &[E::Value],
309        groups: &[Group],
310    ) -> GeopResult<super::Enclosure<E::Value>> {
311        self.enclose_into(at, groups, Enclosure::empty(self.mobility.len()))
312    }
313
314    /// `enclosure` with the groups `groups` enclosed too (see
315    /// [`System::enclose_groups`]), over the boxes it has.
316    pub(super) fn enclose_into(
317        &self,
318        at: &[E::Value],
319        groups: &[Group],
320        mut enclosure: Enclosure<E::Value>,
321    ) -> GeopResult<super::Enclosure<E::Value>> {
322        let layers = groups.iter().map(|g| g.layer + 1).max().unwrap_or(0);
323        for layer in 0..layers {
324            let mut base = at.to_vec();
325            for (block, boxed) in enclosure.boxes.iter().enumerate() {
326                if let Some(boxed) = boxed {
327                    base[block] = boxed.clone();
328                }
329            }
330            let unproven = |b: &usize| (enclosure.failures.iter()).any(|(bs, _)| bs.contains(b));
331            let (now, orphans): (Vec<&Group>, Vec<&Group>) = (groups.iter())
332                .filter(|g| g.layer == layer)
333                .partition(|g| !g.on.iter().any(unproven));
334            let batch = Batch::new(self, &base);
335            let enclosed = batch.run(now.iter().map(|&group| {
336                let (batch, base) = (&batch, &base);
337                async move { self.enclose_group(base, group, batch).await }
338            }))?;
339            enclosure.evaluations += batch.evaluations();
340            let dimensions = |group: &Group| -> (Vec<usize>, Vec<usize>) {
341                (
342                    group.blocks.iter().map(|&b| at[b].dimension()).collect(),
343                    group.on.iter().map(|&b| at[b].dimension()).collect(),
344                )
345            };
346            for group in orphans {
347                let (own, on) = dimensions(group);
348                let free = Motions::free(own.iter().sum());
349                enclosure.moved(group, &own, &on, free);
350                let error = GeopError::new(format!(
351                    "it hangs off blocks {:?}, which no true solution is proven for",
352                    group.on
353                ));
354                (enclosure.failures).push((group.blocks.clone(), EncloseError::Unproven(error)));
355            }
356            for (group, enclosed) in now.into_iter().zip(enclosed) {
357                let (own, on) = dimensions(group);
358                enclosure.moved(group, &own, &on, enclosed.motions);
359                match enclosed.boxes {
360                    Ok(boxes) => {
361                        for (&block, value) in group.blocks.iter().zip(boxes) {
362                            enclosure.boxes[block] = Some(value);
363                        }
364                    }
365                    Err(e) => enclosure.failures.push((group.blocks.clone(), e)),
366                }
367            }
368        }
369        Ok(enclosure)
370    }
371
372    /// The enclosure of the group `group` of the blocks at `at`, its terms
373    /// asked of `batch`.
374    pub(super) async fn enclose_group(
375        &self,
376        at: &[E::Value],
377        group: &Group,
378        batch: &Batch<'_, '_, E>,
379    ) -> GeopResult<Enclosed<E::Value>> {
380        let solving = Solving::new(self, at, group, batch);
381        let x = solving.start();
382        let n = solving.n;
383        let tolerance = tolerance(self.scale)?;
384        // The limits that could be at their bound are enclosed as
385        // constraints that vanish; the others must hold over the box.
386        let limits: Vec<usize> = (group.constraints.iter())
387            .filter(|&&c| self.equations.bound(c) == Bound::AtMost)
388            .copied()
389            .collect();
390        let made = match limits.is_empty() {
391            true => Some(Vec::new()),
392            false => solving.constraints_at(&x, &limits).await,
393        };
394        let (mut equalities, mut inactive) = (Vec::new(), Vec::new());
395        for &c in &group.constraints {
396            let k = limits.iter().position(|&l| l == c);
397            let short = match (k, &made) {
398                (None, _) => false,
399                (Some(k), Some(made)) => {
400                    (made[k].values.iter()).all(|r| r.definitely_less(tolerance.neg()))
401                }
402                (Some(_), None) => false,
403            };
404            match short {
405                true => inactive.push(c),
406                false => equalities.push(c),
407            }
408        }
409        let zero = vec![Scalar::ZERO; n];
410        let mut radius = Scalar::ZERO;
411        let mut tried: Vec<Vec<(usize, usize)>> = Vec::new();
412        let mut failure: Option<EncloseError> = None;
413        // How free the blocks are: at the point, until a proof decides it
414        // over its box.
415        let mut freedom = None;
416        for attempt in 0..ENCLOSE_ATTEMPTS {
417            if attempt == 1 {
418                radius = tolerance;
419            } else if attempt > 1 {
420                radius = Scalar::TWO.mul(radius);
421            }
422            let around: Vec<Scalar> = match attempt {
423                0 => zero.clone(),
424                _ => vec![radius.neg().union(radius); n],
425            };
426            let rows = match solving.rows_over(&x, &around, &equalities).await {
427                Ok(rows) => rows,
428                // Undecidable somewhere in this box: no wider one helps.
429                Err(e) => {
430                    failure.get_or_insert(e.into());
431                    break;
432                }
433            };
434            let slopes: Vec<Vec<Scalar>> = rows.iter().map(|r| r.slope.clone()).collect();
435            if freedom.is_none() {
436                freedom = Some(solving.freedom(&rows));
437            }
438            let mut pivots = eliminate(slopes, n).pivots;
439            pivots.sort_unstable();
440            if tried.contains(&pivots) {
441                continue;
442            }
443            tried.push(pivots.clone());
444            // At the point, the rows chosen over it are those Newton starts
445            // from.
446            let at = (attempt == 0).then(|| rows.clone());
447            let proven = match solving.prove(x.clone(), at, &equalities, &pivots).await {
448                Ok((center, steps)) => {
449                    match (solving.verify(&center, &steps, &equalities, &inactive, &pivots)).await {
450                        Ok(over) => Ok((center, steps, over)),
451                        Err(e) => Err(e),
452                    }
453                }
454                Err(e) => Err(e),
455            };
456            let proven = match proven {
457                Ok(proven) => Ok(proven),
458                Err(Off(off)) => {
459                    Err((solving.conflict(&x, radius, &equalities, &rows, &off)).await)
460                }
461                Err(Failed(e)) => Err(e),
462            };
463            match proven {
464                Ok((center, steps, over)) => {
465                    let boxes = (group.blocks.iter().enumerate())
466                        .map(|(k, _)| {
467                            let column = solving.columns[k];
468                            let dimension = center[k].dimension();
469                            center[k].retract_over(&steps[column..column + dimension])
470                        })
471                        .collect::<GeopResult<Vec<_>>>()?;
472                    // How free the blocks are, over the box proven.
473                    return Ok(Enclosed {
474                        boxes: Ok(boxes),
475                        motions: solving.freedom(&over),
476                    });
477                }
478                Err(e) => {
479                    // A conflict is proven: no wider box makes it hold.
480                    let conflict = matches!(e, EncloseError::Conflict { .. });
481                    failure = Some(e);
482                    if conflict {
483                        break;
484                    }
485                }
486            }
487        }
488        let independent: Vec<usize> = tried.iter().map(Vec::len).collect();
489        let failure = match failure {
490            Some(EncloseError::Unproven(e)) => EncloseError::Unproven(e.with_context(format!(
491                "enclose: no choice of independent rows was proven — tried {independent:?} rows, over {n} variables"
492            ))),
493            Some(conflict) => conflict,
494            None => EncloseError::Unproven(GeopError::new(format!(
495                "enclose: no choice of independent rows was proven — tried {independent:?} rows, over {n} variables"
496            ))),
497        };
498        Ok(Enclosed {
499            boxes: Err(failure),
500            motions: freedom.unwrap_or_else(|| Motions::free(n)),
501        })
502    }
503}
504
505impl<E: Equations> Solving<'_, '_, E> {
506    /// Every row of the constraints `which` over the box of steps `steps`
507    /// from `x` — the group's values — and its derivative by every step:
508    /// an enclosure of both, for every point of the box. Fails where a
509    /// constraint cannot be made somewhere in it.
510    async fn rows_over(
511        &self,
512        x: &[E::Value],
513        steps: &[Scalar],
514        which: &[usize],
515    ) -> GeopResult<Vec<Row>> {
516        let mut values = Vec::with_capacity(x.len());
517        let mut differentials = Vec::with_capacity(x.len());
518        for (k, value) in x.iter().enumerate() {
519            let column = self.columns[k];
520            let own = &steps[column..column + value.dimension()];
521            values.push(value.retract_over(own)?);
522            differentials.push(value.differential(own)?);
523        }
524        let terms: Vec<Term> = which.iter().map(|&c| Term::Constraint(c)).collect();
525        let made = self.made(&values, &terms).await;
526        let differential = |block: usize| {
527            let k = self.group.blocks.binary_search(&block).ok()?;
528            differentials.get(k)?.as_ref()
529        };
530        let mut offsets = Vec::with_capacity(self.group.on.len());
531        let mut width = 0;
532        for &s in &self.group.on {
533            offsets.push(width);
534            width += self.base[s].dimension();
535        }
536        let mut rows = Vec::new();
537        for (&c, made) in which.iter().zip(made) {
538            let made = made.ok_or_else(|| {
539                GeopError::new(format!(
540                    "constraint {c} cannot be made over the box of steps {steps:?}"
541                ))
542            })?;
543            let linear = self.rows(&made, &differential);
544            for (r, (value, slope)) in linear.values.into_iter().zip(linear.jacobian).enumerate() {
545                let mut on = vec![Scalar::ZERO; width];
546                for (block, derivatives) in &made.jacobian {
547                    let (Some(k), Some(local)) = (
548                        self.group.on.iter().position(|s| s == block),
549                        derivatives.get(r),
550                    ) else {
551                        continue;
552                    };
553                    for (slot, &d) in on.iter_mut().skip(offsets[k]).zip(local) {
554                        *slot = slot.add(d);
555                    }
556                }
557                rows.push(Row {
558                    constraint: c,
559                    value,
560                    slope,
561                    on,
562                });
563            }
564        }
565        Ok(rows)
566    }
567
568    /// How the group moves by the rows `rows` (see [`Motions`]): by those
569    /// that vanish — a limit leaves free the motion away from it.
570    fn freedom(&self, rows: &[Row]) -> Motions {
571        let rows: Vec<&Row> = (rows.iter())
572            .filter(|r| self.system.equations.bound(r.constraint) == Bound::Zero)
573            .collect();
574        let slopes = rows.iter().map(|r| r.slope.clone()).collect();
575        Motions {
576            basis: null_space(slopes, self.n),
577            rows: (rows.into_iter())
578                .map(|r| (r.slope.clone(), r.on.clone()))
579                .collect(),
580        }
581    }
582
583    /// The Krawczyk proof for the independent rows `pivots` — `(variable,
584    /// row)` pairs — from `x`, where the rows of `which` are `at` if given:
585    /// the
586    /// polished center, and the box of steps
587    /// from it holding their solution, every variable no pivot determines
588    /// at an exact zero step.
589    #[allow(clippy::type_complexity)]
590    async fn prove(
591        &self,
592        x: Vec<E::Value>,
593        at: Option<Vec<Row>>,
594        which: &[usize],
595        pivots: &[(usize, usize)],
596    ) -> Result<(Vec<E::Value>, Vec<Scalar>), Rejected> {
597        let n = self.n;
598        let zero = vec![Scalar::ZERO; n];
599        if pivots.is_empty() {
600            return Ok((x, zero));
601        }
602        let (cols, picked): (Vec<usize>, Vec<usize>) = pivots.iter().copied().unzip();
603        let m = cols.len();
604        let square = |rows: &[Row]| -> Vec<Vec<Scalar>> {
605            picked
606                .iter()
607                .map(|&r| cols.iter().map(|&c| rows[r].slope[c]).collect())
608                .collect()
609        };
610        let singular = |rows: &[Row]| {
611            let rows: Vec<(Scalar, &Vec<Scalar>)> =
612                rows.iter().map(|r| (r.value, &r.slope)).collect();
613            GeopError::new(format!(
614                "the constraints are singular at their solution: rows {picked:?} over variables {cols:?} of {rows:?}"
615            ))
616        };
617        let product = |y: &[Vec<Scalar>], k: usize, column: &dyn Fn(usize) -> Scalar| {
618            (0..m).fold(Scalar::ZERO, |sum, j| sum.add(y[k][j].mul(column(j))))
619        };
620        // Which blocks a pivot moves: only those are stepped.
621        let moved: Vec<bool> = (0..x.len())
622            .map(|k| {
623                let column = self.columns[k];
624                let dimension = x[k].dimension();
625                cols.iter()
626                    .any(|&c| (column..column + dimension).contains(&c))
627            })
628            .collect();
629
630        // Newton, on the determined variables. Every iterate is only a seed
631        // for the next, so it is sharp (a free choice, and without it the
632        // width of each step compounds); the last pass takes no step: it is
633        // the one the box is proven around. A step that could be zero
634        // throughout is rounding: no step after it does better, and the
635        // polish is done.
636        let mut polished = 0;
637        let mut xt = x;
638        let mut made = at;
639        let (rows, y) = loop {
640            let rows = match made.take() {
641                Some(rows) => rows,
642                None => self.rows_over(&xt, &zero, which).await?,
643            };
644            let y = inverse(&square(&rows)).ok_or_else(|| singular(&rows))?;
645            if polished == POLISH_STEPS {
646                break (rows, y);
647            }
648            polished += 1;
649            let mut step = zero.clone();
650            for (k, &c) in cols.iter().enumerate() {
651                step[c] = product(&y, k, &|j| rows[picked[j]].value).neg();
652            }
653            if step.iter().all(|d| d.could_be_equal(Scalar::ZERO)) {
654                break (rows, y);
655            }
656            for (k, value) in xt.iter_mut().enumerate() {
657                if moved[k] {
658                    let column = self.columns[k];
659                    *value = value.retract(&step[column..column + value.dimension()])?;
660                }
661            }
662        };
663        let yf: Vec<Scalar> = (0..m)
664            .map(|k| product(&y, k, &|j| rows[picked[j]].value))
665            .collect();
666        // Half-widths of the candidate box: at least what the Newton step
667        // from `x̃` still reaches.
668        let mut radius: Vec<Scalar> = yf.iter().map(|v| v.abs().upper()).collect();
669        for _ in 0..ENCLOSE_ATTEMPTS {
670            let candidate: Vec<Scalar> = (0..m)
671                .map(|k| {
672                    let r = Scalar::TWO.mul(radius[k]);
673                    r.neg().union(r)
674                })
675                .collect();
676            let mut boxed = zero.clone();
677            for (k, &c) in cols.iter().enumerate() {
678                boxed[c] = candidate[k];
679            }
680            let over = self.rows_over(&xt, &boxed, which).await?;
681            // `I - Y J(Δ)`.
682            let slack: Vec<Vec<Scalar>> = (0..m)
683                .map(|k| {
684                    (0..m)
685                        .map(|l| {
686                            let yj = product(&y, k, &|j| over[picked[j]].slope[cols[l]]);
687                            let identity = match k == l {
688                                true => Scalar::ONE,
689                                false => Scalar::ZERO,
690                            };
691                            identity.sub(yj)
692                        })
693                        .collect()
694                })
695                .collect();
696            let krawczyk: Vec<Scalar> = (0..m)
697                .map(|k| {
698                    let spread = (0..m).fold(Scalar::ZERO, |sum, l| {
699                        sum.add(slack[k][l].mul(candidate[l]))
700                    });
701                    yf[k].neg().add(spread)
702                })
703                .collect();
704            if (0..m).all(|k| krawczyk[k].is_subset_of(candidate[k])) {
705                // `K(Δ) ⊆ Δ` alone gives a zero of `Y f` in `Δ`, which is
706                // one of `f` only if `Y` is regular. `‖I - Y J(Δ)‖∞ < 1`
707                // proves that for `Y` and every matrix of `J(Δ)`, and with
708                // it that the zero is the only one in `Δ`: two of them
709                // differ by `(I - Y J)` times that difference.
710                let norm = slack
711                    .iter()
712                    .map(|row| row.iter().fold(Scalar::ZERO, |sum, e| sum.add(e.abs())))
713                    .fold(Scalar::ZERO, |most, row| most.max(row));
714                if !norm.definitely_less(Scalar::ONE) {
715                    return Err(GeopError::new(format!(
716                        "the constraints are not proven independent over the box around the solution: ‖I - Y J(Δ)‖∞ is {norm:?}, not below one, for rows {picked:?} over variables {cols:?}"
717                    ))
718                    .into());
719                }
720                for (k, &c) in cols.iter().enumerate() {
721                    boxed[c] = krawczyk[k].intersect(candidate[k]);
722                }
723                return Ok((xt, boxed));
724            }
725            for k in 0..m {
726                let reach = krawczyk[k].abs().upper();
727                if reach.could_be_greater(radius[k]) {
728                    radius[k] = reach;
729                }
730                if !radius[k].is_finite() {
731                    return Err(singular(&rows).into());
732                }
733            }
734        }
735        Err(GeopError::new(format!(
736            "could not enclose the solution: no box around it passed the Krawczyk test in {ENCLOSE_ATTEMPTS} attempts"
737        ))
738        .into())
739    }
740
741    /// The rows of the constraints `which` over the box of steps `steps`
742    /// from `center` — the proven solution of the independent rows `pivots`
743    /// — if what the proof is of is a solution of them all, as far as the
744    /// box resolves: every limit `inactive` holds over it; every other row
745    /// encloses zero there — else [`Rejected::Off`], those that do not —
746    /// and adds nothing to the independent ones' rank over it.
747    ///
748    /// The rank is decided over this box, around the solution, and not only
749    /// over the box the rows were chosen over, around where the solve
750    /// ended: a constraint the others imply leaves the rank as it is
751    /// wherever they all hold, and so over a box holding such a point; one
752    /// they do not — a line tangent to a circle a point is on — is
753    /// independent a hair off its singular point, where the free variables
754    /// were left. Its value there is too small to tell from zero, its
755    /// derivative is not.
756    async fn verify(
757        &self,
758        center: &[E::Value],
759        steps: &[Scalar],
760        which: &[usize],
761        inactive: &[usize],
762        pivots: &[(usize, usize)],
763    ) -> Result<Vec<Row>, Rejected> {
764        let limits = match inactive.is_empty() {
765            true => Vec::new(),
766            false => self.rows_over(center, steps, inactive).await?,
767        };
768        for row in limits {
769            if row.value.could_be_greater(Scalar::ZERO) {
770                return Err(GeopError::new(format!(
771                    "the limit {} is not proven to hold over the box around the solution: it is {:?} there",
772                    row.constraint, row.value
773                ))
774                .into());
775            }
776        }
777        let picked: Vec<usize> = pivots.iter().map(|&(_, r)| r).collect();
778        let rows = self.rows_over(center, steps, which).await?;
779        let off: Vec<usize> = (0..rows.len())
780            .filter(|i| !picked.contains(i) && !rows[*i].value.could_be_equal(Scalar::ZERO))
781            .collect();
782        if !off.is_empty() {
783            return Err(Rejected::Off(off));
784        }
785        let rank = eliminate(rows.iter().map(|r| r.slope.clone()).collect(), self.n);
786        let independent: Vec<usize> = (rank.pivots.iter())
787            .map(|&(_, r)| rows[r].constraint)
788            .filter(|c| !picked.iter().any(|&p| rows[p].constraint == *c))
789            .collect();
790        if rank.pivots.len() > picked.len() {
791            return Err(GeopError::new(format!(
792                "the constraints are singular at their solution: {independent:?} vanish over the box around the solution of the others, but are independent of them there — {} rows of rank {}",
793                picked.len(),
794                rank.pivots.len()
795            ))
796            .into());
797        }
798        Ok(rows)
799    }
800
801    /// Why the rows `off` of the constraints `which` — the rows chosen
802    /// over the box of steps of half-width `radius` around `x`, where the
803    /// solve ended, `selection` — do not vanish where the independent ones
804    /// do: a conflict where one is proven within the box of half-width
805    /// `radius`, or the tolerance if wider, around `x`, and otherwise not
806    /// proven.
807    ///
808    /// The solution of the independent rows that left them off was found
809    /// with the free variables held as they are, so it proves nothing about
810    /// other values of those. What does: a combination `g` of the rows
811    /// vanishes wherever they all do, and over the box `B`, by the mean
812    /// value theorem, `g` is within `g(x) + ∇g(B) B`. Where that is
813    /// definitely not zero, no point of `B` is a solution. The combination
814    /// is the one the elimination that chose the rows reduced each of
815    /// `off` by, sharpened: any combination serves, and that one makes
816    /// `∇g` as small as the rows allow.
817    async fn conflict(
818        &self,
819        x: &[E::Value],
820        radius: Scalar,
821        which: &[usize],
822        selection: &[Row],
823        off: &[usize],
824    ) -> EncloseError {
825        let n = self.n;
826        let proven = async || -> GeopResult<Option<EncloseError>> {
827            let within = radius.max(tolerance(self.system.scale)?);
828            let at = (self.rows_over(x, &vec![Scalar::ZERO; n], which)).await?;
829            let around = vec![within.neg().union(within); n];
830            let over = self.rows_over(x, &around, which).await?;
831            let reduced = eliminate(augmented(selection.iter().map(|r| r.slope.clone())), n);
832            let (mut conflicting, mut with) = (Vec::new(), Vec::new());
833            for (k, &o) in reduced.origin.iter().enumerate() {
834                if !off.contains(&o) {
835                    continue;
836                }
837                // Zero where it could be: a free choice, which keeps the
838                // rows the elimination never combined out of `g`.
839                let combination: Vec<Scalar> = (0..selection.len())
840                    .map(|j| reduced.rows[k][n + j])
841                    .map(|a| match a.could_be_equal(Scalar::ZERO) {
842                        true => Scalar::ZERO,
843                        false => a.midpoint(),
844                    })
845                    .collect();
846                let mut g = Scalar::ZERO;
847                let mut gradient = vec![Scalar::ZERO; n];
848                for (j, a) in combination.iter().enumerate().filter(|(_, a)| !a.is_zero()) {
849                    g = g.add(a.mul(at[j].value));
850                    for (slot, s) in gradient.iter_mut().zip(&over[j].slope) {
851                        *slot = slot.add(a.mul(*s));
852                    }
853                }
854                let reach = (gradient.iter()).fold(g, |sum, d| sum.add(d.mul(around[0])));
855                if reach.could_be_equal(Scalar::ZERO) {
856                    continue;
857                }
858                conflicting.push((selection[o].constraint, reach));
859                for (j, a) in combination.iter().enumerate() {
860                    let c = selection[j].constraint;
861                    if j != o && !a.is_zero() && !off.contains(&j) && !with.contains(&c) {
862                        with.push(c);
863                    }
864                }
865            }
866            with.sort_unstable();
867            Ok((!conflicting.is_empty()).then_some(EncloseError::Conflict {
868                conflicting,
869                with,
870                within,
871            }))
872        };
873        let named: Vec<usize> = off.iter().map(|&o| selection[o].constraint).collect();
874        match proven().await {
875            Ok(Some(conflict)) => conflict,
876            Ok(None) => EncloseError::Unproven(GeopError::new(format!(
877                "the constraints {named:?} do not vanish over the box around the solution of the others, but no conflict is proven near where the solve ended: a singular configuration"
878            ))),
879            Err(e) => EncloseError::Unproven(e.with_context(format!(
880                "looking for a conflict of the constraints {named:?}"
881            ))),
882        }
883    }
884}