1use 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
76const POLISH_STEPS: usize = 6;
80
81const ENCLOSE_ATTEMPTS: usize = 24;
84
85#[derive(Debug)]
88pub struct Enclosure<V: Variable> {
89 pub boxes: Vec<Option<V>>,
93 pub freedom: Vec<usize>,
96 pub total: usize,
98 pub failures: Vec<(Vec<usize>, EncloseError)>,
100 pub evaluations: usize,
103 motions: Vec<Vec<(usize, Vec<Scalar>)>>,
108}
109
110impl<V: Variable> Enclosure<V> {
111 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 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 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 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
186pub(super) struct Motions {
191 basis: Vec<Vec<Scalar>>,
192 rows: Vec<(Vec<Scalar>, Vec<Scalar>)>,
193}
194
195impl Motions {
196 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#[derive(Debug)]
213pub enum EncloseError {
214 Conflict {
221 conflicting: Vec<(usize, Scalar)>,
222 with: Vec<usize>,
223 within: Scalar,
224 },
225 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 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
264enum 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#[derive(Clone)]
282struct Row {
283 constraint: usize,
285 value: Scalar,
286 slope: Vec<Scalar>,
288 on: Vec<Scalar>,
291}
292
293pub(super) struct Enclosed<V: Variable> {
296 pub boxes: Result<Vec<V>, EncloseError>,
297 pub motions: Motions,
298}
299
300impl<E: Equations> System<'_, E> {
301 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 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 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 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 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 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 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(¢er, &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 return Ok(Enclosed {
474 boxes: Ok(boxes),
475 motions: solving.freedom(&over),
476 });
477 }
478 Err(e) => {
479 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 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 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 #[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 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 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 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 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 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 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 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 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}