1use crate::march::{Marching, Stopped};
31use ogeom_core::{OgeomResult, Tolerances};
32use ogeom_math::{Point, Vector, solve};
33use smallvec::SmallVec;
34
35pub trait Condition {
41 fn unknowns(&self) -> usize;
43
44 fn position(&self, x: &[f64], tol: Tolerances) -> Option<Point>;
46
47 fn position_gradient(&self, x: &[f64], tol: Tolerances) -> Option<Vec<Vector>>;
53
54 fn system(&self, x: &[f64], tol: Tolerances) -> Option<(Vec<f64>, Vec<Vec<f64>>)>;
59
60 #[allow(clippy::type_complexity, reason = "the three answers, together")]
66 fn system_at(
67 &self,
68 x: &[f64],
69 tol: Tolerances,
70 ) -> Option<((Vec<f64>, Vec<Vec<f64>>), Point, Vec<Vector>)> {
71 Some((
72 self.system(x, tol)?,
73 self.position(x, tol)?,
74 self.position_gradient(x, tol)?,
75 ))
76 }
77
78 fn clamp(&self, x: &mut [f64]);
81
82 fn outside(&self, x: &[f64], tol: Tolerances) -> bool;
84
85 fn near_edge(&self, x: &[f64]) -> bool;
88
89 fn extent(&self) -> f64;
91
92 fn tangent_is_oriented(&self) -> bool {
104 false
105 }
106
107 fn tangent(&self, x: &[f64], tol: Tolerances) -> Option<Vector> {
116 let (_, jacobian) = self.system(x, tol)?;
117 let gradient = self.position_gradient(x, tol)?;
118 null_tangent(&jacobian, &gradient, tol)
119 }
120
121 fn tangent_from(
130 &self,
131 x: &[f64],
132 jacobian: &[Vec<f64>],
133 gradient: &[Vector],
134 tol: Tolerances,
135 ) -> Option<Vector> {
136 let _ = (jacobian, gradient);
137 self.tangent(x, tol)
138 }
139}
140
141#[must_use]
146pub fn null_tangent(jacobian: &[Vec<f64>], gradient: &[Vector], tol: Tolerances) -> Option<Vector> {
147 let null = null_vector(jacobian, gradient.len())?;
148 let mut out = Vector::ZERO;
149 for (g, n) in gradient.iter().zip(&null) {
150 out += *g * *n;
151 }
152 let length = out.magnitude();
153 if length <= tol.confusion() {
154 return None;
155 }
156 Some(out / length)
157}
158
159#[derive(Debug, Clone, PartialEq)]
161pub struct Walked {
162 pub states: Vec<Vec<f64>>,
164 pub points: Vec<Point>,
166 pub stopped: Stopped,
168}
169
170pub fn follow<C: Condition + ?Sized>(
182 condition: &C,
183 start: &[f64],
184 options: Marching,
185 tol: Tolerances,
186) -> OgeomResult<Walked> {
187 options.validate()?;
188 if start.len() != condition.unknowns() {
189 ogeom_core::ogeom_bail!(
190 Construction,
191 "the condition is posed in {} unknowns and the start has {}",
192 condition.unknowns(),
193 start.len()
194 );
195 }
196 let ahead = walk_one_way(condition, start, 1.0, options, tol)?;
197 if ahead.stopped == Stopped::Closed {
198 return Ok(ahead);
199 }
200 let behind = walk_one_way(condition, start, -1.0, options, tol)?;
201 if behind.stopped == Stopped::Closed {
207 let mut whole = behind;
208 whole.states.reverse();
209 whole.points.reverse();
210 return Ok(whole);
211 }
212
213 let mut states = behind.states;
214 let mut points = behind.points;
215 states.reverse();
216 points.reverse();
217 states.pop();
218 points.pop();
219 states.extend(ahead.states);
220 points.extend(ahead.points);
221
222 let stopped = if ahead.stopped == Stopped::RanOut || behind.stopped == Stopped::RanOut {
225 Stopped::RanOut
226 } else if ahead.stopped == Stopped::Stalled || behind.stopped == Stopped::Stalled {
227 Stopped::Stalled
228 } else {
229 Stopped::LeftTheDomain
230 };
231 Ok(Walked {
232 states,
233 points,
234 stopped,
235 })
236}
237
238pub fn walk_one_way<C: Condition + ?Sized>(
245 condition: &C,
246 start: &[f64],
247 sense: f64,
248 options: Marching,
249 tol: Tolerances,
250) -> OgeomResult<Walked> {
251 Ok(walk_one_way_past(condition, start, sense, options, tol)?.0)
252}
253
254pub(crate) type Past = (Vec<f64>, Point);
257
258pub(crate) fn walk_one_way_past<C: Condition + ?Sized>(
262 condition: &C,
263 start: &[f64],
264 sense: f64,
265 options: Marching,
266 tol: Tolerances,
267) -> OgeomResult<(Walked, Option<Past>)> {
268 let mut at: Vec<f64> = start.to_vec();
269 condition.clamp(&mut at);
270 let Some(from) = condition.position(&at, tol) else {
271 let walked = Walked {
272 states: vec![at],
273 points: Vec::new(),
274 stopped: Stopped::Stalled,
275 };
276 return Ok((walked, None));
277 };
278 let mut past = None;
279 let mut states = vec![at.clone()];
280 let mut points = vec![from];
281 let mut stopped = Stopped::RanOut;
282
283 let reach = condition.extent();
288 let ceiling = reach / 8.0;
289 let mut step = (options.chord * reach)
290 .sqrt()
291 .clamp(tol.confusion(), ceiling);
292 let mut heading: Option<Vector> = None;
295 let mut ahead: Option<Option<Vector>> = None;
299 let mut start_heading: Option<Vector> = None;
301
302 while points.len() < options.max_points {
303 ogeom_core::progress::checkpoint()?;
304 let direction = match ahead.take() {
305 Some(known) => known,
306 None => oriented(condition, &at, heading, sense, tol),
307 };
308 let Some(direction) = direction else {
309 stopped = Stopped::Stalled;
310 break;
311 };
312 let here = points[points.len() - 1];
313
314 let mut taken = None;
315 for _ in 0..40 {
316 let Some((state, point, evaluated)) =
317 correct(condition, &at, (here, direction, step), options.chord, tol)
318 else {
319 step *= 0.5;
320 if step <= tol.confusion() {
321 break;
322 }
323 continue;
324 };
325 let next = (state, point);
326 let there = match evaluated {
330 Some((jacobian, gradient)) => orient(
331 condition,
332 condition.tangent_from(&next.0, &jacobian, &gradient, tol),
333 Some(direction),
334 sense,
335 ),
336 None => oriented(condition, &next.0, Some(direction), sense, tol),
337 };
338 let turn = there.map_or(0.0, |t| direction.dot(t).clamp(-1.0, 1.0).acos());
339 let sag = step * turn / 8.0;
340 if sag <= options.chord || step <= tol.confusion() * 8.0 {
341 let scale = if sag > 0.0 {
346 (options.chord / sag).sqrt().clamp(0.5, 2.0)
347 } else {
348 2.0
349 };
350 taken = Some((next, (step * scale).clamp(tol.confusion(), ceiling), there));
351 break;
352 }
353 step *= (options.chord / sag).sqrt().clamp(0.25, 0.9);
354 }
355 let Some(((next_state, next_point), following, there)) = taken else {
356 stopped = if condition.near_edge(&at) {
361 Stopped::LeftTheDomain
362 } else {
363 Stopped::Stalled
364 };
365 break;
366 };
367
368 if points.len() > 3
372 && here.distance(from) <= step * 2.0
373 && closes(
374 condition,
375 &at,
376 (here, direction, step),
377 from,
378 options.chord,
379 tol,
380 )
381 {
382 states.push(states[0].clone());
383 points.push(from);
384 stopped = Stopped::Closed;
385 break;
386 }
387 if points.len() > 3
394 && start_heading.is_some_and(|h| h.dot(direction) > 0.0)
395 && passes_over(from, here, next_point, options.chord.max(tol.confusion()))
396 {
397 states.push(states[0].clone());
398 points.push(from);
399 stopped = Stopped::Closed;
400 break;
401 }
402 if condition.outside(&next_state, tol) {
403 stopped = Stopped::LeftTheDomain;
404 past = Some((next_state, next_point));
405 break;
406 }
407
408 heading = Some(direction);
409 start_heading.get_or_insert(direction);
410 states.push(next_state);
411 points.push(next_point);
412 at.clone_from(&states[states.len() - 1]);
413 step = following;
414 ahead = Some(there);
415 }
416
417 let walked = Walked {
418 states,
419 points,
420 stopped,
421 };
422 Ok((walked, past))
423}
424
425fn closes<C: Condition + ?Sized>(
439 condition: &C,
440 at: &[f64],
441 (here, direction, step): (Point, Vector, f64),
442 from: Point,
443 chord: f64,
444 tol: Tolerances,
445) -> bool {
446 let reach = (from - here).dot(direction);
447 if reach <= 0.0 || reach > step {
448 return false;
449 }
450 let near = chord.max(tol.confusion());
451 let Some((_, landed, _)) = correct(condition, at, (here, direction, reach), chord, tol) else {
452 return false;
453 };
454 if landed.distance(from) > near {
455 return false;
456 }
457 let Some((_, middle, _)) = correct(condition, at, (here, direction, reach * 0.5), chord, tol)
458 else {
459 return false;
460 };
461 distance_to_segment(middle, here, from) <= near
462}
463
464fn passes_over(p: Point, a: Point, b: Point, near: f64) -> bool {
467 let along = b - a;
468 let length = along.square_magnitude();
469 if length <= f64::MIN_POSITIVE {
470 return false;
471 }
472 let t = (p - a).dot(along) / length;
473 t > 0.0 && t < 1.0 && p.distance(a + along * t) <= near
474}
475
476fn distance_to_segment(p: Point, a: Point, b: Point) -> f64 {
478 let along = b - a;
479 let length = along.square_magnitude();
480 if length <= f64::MIN_POSITIVE {
481 return p.distance(a);
482 }
483 let t = ((p - a).dot(along) / length).clamp(0.0, 1.0);
484 p.distance(a + along * t)
485}
486
487fn oriented<C: Condition + ?Sized>(
489 condition: &C,
490 at: &[f64],
491 heading: Option<Vector>,
492 sense: f64,
493 tol: Tolerances,
494) -> Option<Vector> {
495 orient(condition, condition.tangent(at, tol), heading, sense)
496}
497
498fn orient<C: Condition + ?Sized>(
500 condition: &C,
501 direction: Option<Vector>,
502 heading: Option<Vector>,
503 sense: f64,
504) -> Option<Vector> {
505 let direction = direction?;
506 if condition.tangent_is_oriented() {
507 return Some(direction * sense);
509 }
510 let along = match heading {
511 Some(previous) if direction.dot(previous) < 0.0 => -direction,
514 _ => direction,
515 };
516 Some(if heading.is_none() {
517 along * sense
518 } else {
519 along
520 })
521}
522
523type Landed = (Vec<f64>, Point, Option<(Vec<Vec<f64>>, Vec<Vector>)>);
527
528fn correct<C: Condition + ?Sized>(
539 condition: &C,
540 from: &[f64],
541 travel: (Point, Vector, f64),
542 within: f64,
543 tol: Tolerances,
544) -> Option<Landed> {
545 match condition.unknowns() {
546 2 => correct_fixed::<C, 2>(condition, from, travel, within, tol),
547 3 => correct_fixed::<C, 3>(condition, from, travel, within, tol),
548 4 => correct_fixed::<C, 4>(condition, from, travel, within, tol),
549 5 => correct_fixed::<C, 5>(condition, from, travel, within, tol),
550 _ => correct_any(condition, from, travel, tol),
551 }
552}
553
554fn correction_criteria(tol: Tolerances) -> solve::Criteria {
557 solve::Criteria {
558 residual: tol.confusion() * 0.01,
559 step: tol.parametric(),
560 max_iterations: 40,
561 }
562}
563
564pub(crate) fn settling_residual<const N: usize>(
581 jacobian: &[[f64; N]; N],
582 gradient: &[Vector],
583 within: f64,
584 stop: f64,
585) -> f64 {
586 let mut spread = 0.0;
587 for row in 0..N - 1 {
588 let mut unit = [0.0; N];
589 unit[row] = 1.0;
590 let Some(column) = solve::linear_fixed(*jacobian, unit) else {
591 return stop;
592 };
593 let moved = gradient
594 .iter()
595 .zip(&column)
596 .fold(Vector::ZERO, |sum, (g, c)| sum + *g * *c);
597 spread += moved.square_magnitude();
598 }
599 let spread = spread.sqrt();
600 if spread.is_finite() && spread * stop > within {
601 within / spread
602 } else {
603 stop
604 }
605}
606
607pub(crate) fn settle<const N: usize>(
623 mut system: impl FnMut(&[f64; N]) -> ([f64; N], [[f64; N]; N]),
624 (found, residual): ([f64; N], &[f64; N]),
625 (jacobian, gradient): (&[[f64; N]; N], &[Vector]),
626 within: f64,
627 criteria: solve::Criteria,
628) -> Option<[f64; N]> {
629 let step = solve::linear_fixed(*jacobian, *residual)?;
630 let moved = gradient
631 .iter()
632 .zip(&step)
633 .fold(Vector::ZERO, |sum, (g, c)| sum + *g * *c);
634 if moved.magnitude() <= within {
635 return None;
636 }
637 let norm = residual.iter().map(|v| v * v).sum::<f64>().sqrt();
638 let residual = settling_residual(jacobian, gradient, within, criteria.residual);
639 if norm <= residual {
640 return None;
641 }
642 let tighter = solve::Criteria {
643 residual,
644 ..criteria
645 };
646 let (settled, left, _, _) = solve::newton_system_fixed(&mut system, found, tighter).ok()?;
647 if left > norm {
648 return None;
649 }
650 let (_, after) = system(&settled);
651 let mut drift = 0.0;
652 for column in 0..N {
653 let change: [f64; N] =
654 core::array::from_fn(|row| after[row][column] - jacobian[row][column]);
655 let moved = solve::linear_fixed(*jacobian, change)?;
656 drift += moved.iter().map(|v| v * v).sum::<f64>();
657 }
658 (drift.sqrt() <= 0.5).then_some(settled)
659}
660
661type Evaluated<const N: usize> = (
665 [f64; N],
666 Point,
667 Vec<Vec<f64>>,
668 Vec<Vector>,
669 [[f64; N]; N],
670 [f64; N],
671);
672
673fn correct_fixed<C: Condition + ?Sized, const N: usize>(
676 condition: &C,
677 from: &[f64],
678 (anchor, along, reach): (Point, Vector, f64),
679 within: f64,
680 tol: Tolerances,
681) -> Option<Landed> {
682 let start: [f64; N] = from.try_into().ok()?;
683 let last: core::cell::Cell<Option<Evaluated<N>>> = core::cell::Cell::new(None);
684 let system = |x: &[f64; N]| {
685 let mut at = *x;
686 condition.clamp(&mut at);
687 last.set(None);
688 let mut residual = [f64::INFINITY; N];
691 let mut jacobian = [[0.0; N]; N];
692 let Some(((rows, matrix), point, gradient)) = condition.system_at(&at, tol) else {
693 return (residual, jacobian);
694 };
695 if rows.len() + 1 != N
696 || matrix.len() + 1 != N
697 || gradient.len() != N
698 || matrix.iter().any(|row| row.len() != N)
699 {
700 return (residual, jacobian);
701 }
702 residual[..N - 1].copy_from_slice(&rows);
703 for (to, row) in jacobian.iter_mut().zip(&matrix) {
704 to.copy_from_slice(row);
705 }
706 residual[N - 1] = (point - anchor).dot(along) - reach;
707 for (entry, g) in jacobian[N - 1].iter_mut().zip(&gradient) {
708 *entry = g.dot(along);
709 }
710 last.set(Some((at, point, matrix, gradient, jacobian, residual)));
711 (residual, jacobian)
712 };
713 let criteria = correction_criteria(tol);
714 let (mut found, norm, _, _) = solve::newton_system_fixed(system, start, criteria).ok()?;
715 if norm > tol.confusion() {
716 return None;
717 }
718 let landed_at = |found: &[f64; N]| {
719 let mut at = *found;
720 condition.clamp(&mut at);
721 at
722 };
723 let mut evaluated = last.take();
724 if evaluated.as_ref().is_none_or(|e| e.0 != landed_at(&found)) {
725 system(&found);
726 evaluated = last.take();
727 }
728 if let Some((_, _, _, gradient, jacobian, residual)) = &evaluated
729 && let Some(settled) = settle(
730 system,
731 (found, residual),
732 (jacobian, gradient),
733 within,
734 criteria,
735 )
736 {
737 found = settled;
738 evaluated = last.take();
739 }
740 let at = landed_at(&found);
741 if let Some((evaluated, point, matrix, gradient, _, _)) = evaluated
742 && evaluated == at
743 {
744 return Some((at.to_vec(), point, Some((matrix, gradient))));
745 }
746 let point = condition.position(&at, tol)?;
747 Some((at.to_vec(), point, None))
748}
749
750fn correct_any<C: Condition + ?Sized>(
754 condition: &C,
755 from: &[f64],
756 (anchor, along, reach): (Point, Vector, f64),
757 tol: Tolerances,
758) -> Option<Landed> {
759 let n = condition.unknowns();
760 let system = |x: &[f64]| {
761 let mut at = x.to_vec();
762 condition.clamp(&mut at);
763 let Some(((mut residual, mut jacobian), point, gradient)) = condition.system_at(&at, tol)
764 else {
765 return (vec![f64::INFINITY; n], vec![vec![0.0; n]; n]);
766 };
767 residual.push((point - anchor).dot(along) - reach);
768 jacobian.push(gradient.iter().map(|g| g.dot(along)).collect());
769 (residual, jacobian)
770 };
771 let found = solve::newton_system(system, from, correction_criteria(tol)).ok()?;
772 if found.residual > tol.confusion() {
773 return None;
774 }
775 let mut at = found.value;
776 condition.clamp(&mut at);
777 let point = condition.position(&at, tol)?;
778 Some((at, point, None))
779}
780
781fn null_vector(jacobian: &[Vec<f64>], n: usize) -> Option<Vec<f64>> {
789 if n == 0 || jacobian.len() + 1 != n || jacobian.iter().any(|row| row.len() < n) {
790 return None;
791 }
792 let all: Columns = (0..n).collect();
793 let mut out = Vec::with_capacity(n);
794 for column in 0..n {
795 let kept = without(&all, column);
796 let sign = if column % 2 == 0 { 1.0 } else { -1.0 };
797 out.push(sign * determinant(jacobian, &kept));
798 }
799 let length = out.iter().map(|v| v * v).sum::<f64>().sqrt();
800 if length <= f64::MIN_POSITIVE {
801 return None;
802 }
803 for v in &mut out {
804 *v /= length;
805 }
806 Some(out)
807}
808
809type Columns = SmallVec<[usize; 8]>;
811
812fn without(columns: &[usize], position: usize) -> Columns {
814 columns
815 .iter()
816 .enumerate()
817 .filter(|(k, _)| *k != position)
818 .map(|(_, c)| *c)
819 .collect()
820}
821
822fn determinant(rows: &[Vec<f64>], columns: &[usize]) -> f64 {
826 let rows = &rows[rows.len() - columns.len()..];
827 match columns.len() {
828 0 => 1.0,
829 1 => rows[0][columns[0]],
830 2 => {
831 let (a, b) = (&rows[0], &rows[1]);
832 a[columns[0]].mul_add(b[columns[1]], -(a[columns[1]] * b[columns[0]]))
833 }
834 n => {
835 let mut total = 0.0;
836 for position in 0..n {
837 let sign = if position % 2 == 0 { 1.0 } else { -1.0 };
838 total += sign
839 * rows[0][columns[position]]
840 * determinant(&rows[1..], &without(columns, position));
841 }
842 total
843 }
844 }
845}
846
847#[cfg(test)]
848#[allow(clippy::unwrap_used, clippy::expect_used)]
849mod tests {
850 use super::*;
851
852 const T: Tolerances = Tolerances::millimetres();
853
854 fn cofactor_null(jacobian: &[Vec<f64>], n: usize) -> Vec<f64> {
857 fn det(m: &[Vec<f64>]) -> f64 {
858 match m.len() {
859 0 => 1.0,
860 1 => m[0][0],
861 2 => m[0][0].mul_add(m[1][1], -(m[0][1] * m[1][0])),
862 n => (0..n)
863 .map(|c| {
864 let minor: Vec<Vec<f64>> = m[1..]
865 .iter()
866 .map(|r| (0..n).filter(|&k| k != c).map(|k| r[k]).collect())
867 .collect();
868 let sign = if c % 2 == 0 { 1.0 } else { -1.0 };
869 sign * m[0][c] * det(&minor)
870 })
871 .fold(0.0, |a, b| a + b),
872 }
873 }
874 let mut out: Vec<f64> = (0..n)
875 .map(|c| {
876 let minor: Vec<Vec<f64>> = jacobian
877 .iter()
878 .map(|r| (0..n).filter(|&k| k != c).map(|k| r[k]).collect())
879 .collect();
880 let sign = if c % 2 == 0 { 1.0 } else { -1.0 };
881 sign * det(&minor)
882 })
883 .collect();
884 let length = out.iter().map(|v| v * v).sum::<f64>().sqrt();
885 for v in &mut out {
886 *v /= length;
887 }
888 out
889 }
890
891 #[test]
892 fn the_null_vector_matches_plain_cofactors_exactly() {
893 let mut seed = 0x2545_f491_4f6c_dd1d_u64;
894 let mut next = || {
895 seed ^= seed << 13;
896 seed ^= seed >> 7;
897 seed ^= seed << 17;
898 f64::from((seed >> 32) as u32) / f64::from(u32::MAX) - 0.5
900 };
901 for n in 2..=5 {
902 for _ in 0..50 {
903 let jacobian: Vec<Vec<f64>> = (0..n - 1)
904 .map(|_| (0..n).map(|_| next()).collect())
905 .collect();
906 let got = null_vector(&jacobian, n).unwrap();
907 let want = cofactor_null(&jacobian, n);
908 assert_eq!(
909 got.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
910 want.iter().map(|v| v.to_bits()).collect::<Vec<_>>()
911 );
912 }
913 }
914 }
915
916 struct CircleAt {
921 radius: f64,
922 height: f64,
923 }
924
925 impl Condition for CircleAt {
926 fn unknowns(&self) -> usize {
927 3
928 }
929 fn position(&self, x: &[f64], _tol: Tolerances) -> Option<Point> {
930 Some(Point::new(x[0], x[1], x[2]))
931 }
932 fn position_gradient(&self, _x: &[f64], _tol: Tolerances) -> Option<Vec<Vector>> {
933 Some(vec![Vector::X, Vector::Y, Vector::Z])
934 }
935 fn system(&self, x: &[f64], _tol: Tolerances) -> Option<(Vec<f64>, Vec<Vec<f64>>)> {
936 Some((
937 vec![
938 x[0].mul_add(x[0], x[1] * x[1]) - self.radius * self.radius,
939 x[2] - self.height,
940 ],
941 vec![vec![2.0 * x[0], 2.0 * x[1], 0.0], vec![0.0, 0.0, 1.0]],
942 ))
943 }
944 fn clamp(&self, _x: &mut [f64]) {}
945 fn outside(&self, _x: &[f64], _tol: Tolerances) -> bool {
946 false
947 }
948 fn near_edge(&self, _x: &[f64]) -> bool {
949 false
950 }
951 fn extent(&self) -> f64 {
952 self.radius * 4.0
953 }
954 }
955
956 #[test]
960 fn a_condition_the_walker_knows_nothing_about_is_followed_to_its_chord() {
961 let circle = CircleAt {
962 radius: 3.0,
963 height: 1.5,
964 };
965 let options = Marching {
966 chord: 1e-5,
967 ..Marching::default()
968 };
969 let walked = follow(&circle, &[3.0, 0.0, 1.5], options, T).unwrap();
970 assert_eq!(walked.stopped, Stopped::Closed, "a circle closes");
971 assert!(walked.points.len() > 20, "{} points", walked.points.len());
972
973 for p in &walked.points {
974 assert!((p.x.hypot(p.y) - 3.0).abs() < 1e-9, "on the circle: {p:?}");
975 assert!((p.z - 1.5).abs() < 1e-9, "in its plane: {p:?}");
976 }
977 let length: f64 = walked.points.windows(2).map(|w| w[0].distance(w[1])).sum();
979 let circumference = 2.0 * core::f64::consts::PI * 3.0;
980 assert!(
981 length <= circumference && length > circumference * (1.0 - 1e-4),
982 "the inscribed polygon: {length} against {circumference}"
983 );
984 }
985
986 struct Ellipse {
990 a: f64,
991 b: f64,
992 }
993
994 impl Condition for Ellipse {
995 fn unknowns(&self) -> usize {
996 3
997 }
998 fn position(&self, x: &[f64], _tol: Tolerances) -> Option<Point> {
999 Some(Point::new(x[0], x[1], x[2]))
1000 }
1001 fn position_gradient(&self, _x: &[f64], _tol: Tolerances) -> Option<Vec<Vector>> {
1002 Some(vec![Vector::X, Vector::Y, Vector::Z])
1003 }
1004 fn system(&self, x: &[f64], _tol: Tolerances) -> Option<(Vec<f64>, Vec<Vec<f64>>)> {
1005 let (aa, bb) = (self.a * self.a, self.b * self.b);
1006 Some((
1007 vec![
1008 (x[0] * x[0]).mul_add(1.0 / aa, x[1] * x[1] / bb) - 1.0,
1009 x[2],
1010 ],
1011 vec![
1012 vec![2.0 * x[0] / aa, 2.0 * x[1] / bb, 0.0],
1013 vec![0.0, 0.0, 1.0],
1014 ],
1015 ))
1016 }
1017 fn clamp(&self, _x: &mut [f64]) {}
1018 fn outside(&self, _x: &[f64], _tol: Tolerances) -> bool {
1019 false
1020 }
1021 fn near_edge(&self, _x: &[f64]) -> bool {
1022 false
1023 }
1024 fn extent(&self) -> f64 {
1025 self.a * 4.0
1026 }
1027 }
1028
1029 #[test]
1036 fn a_loop_closed_going_backwards_is_the_loop() {
1037 let ellipse = Ellipse { a: 3.0, b: 1.0 };
1038 let options = Marching {
1039 chord: 1e-5,
1040 ..Marching::default()
1041 };
1042 let (start, ahead, behind) = (0..64)
1043 .map(|k| {
1044 let t = f64::from(k) * 0.1;
1045 let start = [3.0 * t.cos(), t.sin(), 0.0];
1046 let ahead = walk_one_way(&ellipse, &start, 1.0, options, T).unwrap();
1047 let behind = walk_one_way(&ellipse, &start, -1.0, options, T).unwrap();
1048 (start, ahead, behind)
1049 })
1050 .find(|(_, ahead, behind)| {
1051 ahead.stopped == Stopped::Closed
1052 && behind.stopped == Stopped::Closed
1053 && behind.points.len() < ahead.points.len()
1054 })
1055 .expect("a start the backward walk closes from in fewer points");
1056
1057 let options = Marching {
1058 max_points: behind.points.len(),
1059 ..options
1060 };
1061 let walked = follow(&ellipse, &start, options, T).unwrap();
1062 assert_eq!(walked.stopped, Stopped::Closed, "the backward half closed");
1063 assert_eq!(walked.points.len(), behind.points.len(), "once round");
1064 let (first, last) = (walked.points[0], walked.points[walked.points.len() - 1]);
1065 assert_eq!(first, last, "closed on its start");
1066 assert!(
1067 (walked.points[1] - first).dot(ahead.points[1] - first) > 0.0,
1068 "it runs the way the forward half set off"
1069 );
1070 for p in &walked.points {
1071 let on = (p.x / 3.0).hypot(p.y) - 1.0;
1072 assert!(
1073 on.abs() < 1e-9 && p.z.abs() < 1e-12,
1074 "on the ellipse: {p:?}"
1075 );
1076 }
1077 }
1078
1079 struct Climbing {
1085 rise: f64,
1086 }
1087
1088 impl Climbing {
1089 fn miss(&self, p: Point) -> f64 {
1092 let u = p.y.atan2(p.x).rem_euclid(core::f64::consts::TAU) / core::f64::consts::TAU;
1093 [u, u + 1.0]
1094 .into_iter()
1095 .filter(|u| *u <= 1.0)
1096 .map(|u| {
1097 let a = u * core::f64::consts::TAU;
1098 p.distance(Point::new(5.0 * a.cos(), 5.0 * a.sin(), self.rise * u))
1099 })
1100 .fold(f64::INFINITY, f64::min)
1101 }
1102 }
1103
1104 impl Condition for Climbing {
1105 fn unknowns(&self) -> usize {
1106 2
1107 }
1108 fn position(&self, x: &[f64], _tol: Tolerances) -> Option<Point> {
1109 let a = x[0] * core::f64::consts::TAU;
1110 Some(Point::new(5.0 * a.cos(), 5.0 * a.sin(), x[1]))
1111 }
1112 fn position_gradient(&self, x: &[f64], _tol: Tolerances) -> Option<Vec<Vector>> {
1113 let a = x[0] * core::f64::consts::TAU;
1114 Some(vec![
1115 Vector::new(-a.sin(), a.cos(), 0.0) * (5.0 * core::f64::consts::TAU),
1116 Vector::Z,
1117 ])
1118 }
1119 fn system(&self, x: &[f64], _tol: Tolerances) -> Option<(Vec<f64>, Vec<Vec<f64>>)> {
1120 Some((
1121 vec![self.rise.mul_add(-x[0], x[1])],
1122 vec![vec![-self.rise, 1.0]],
1123 ))
1124 }
1125 fn clamp(&self, x: &mut [f64]) {
1126 x[0] = x[0].clamp(0.0, 1.0);
1127 x[1] = x[1].clamp(-1.0, 1.0);
1128 }
1129 fn outside(&self, x: &[f64], tol: Tolerances) -> bool {
1130 let band = tol.parametric();
1131 x[0] < -band || x[0] > 1.0 + band || x[1] < -1.0 - band || x[1] > 1.0 + band
1132 }
1133 fn near_edge(&self, x: &[f64]) -> bool {
1134 x[0] <= 1e-6 || x[0] >= 1.0 - 1e-6
1135 }
1136 fn extent(&self) -> f64 {
1137 14.0
1138 }
1139 }
1140
1141 #[test]
1150 fn a_walk_closes_only_through_a_step_it_solved() {
1151 let chord = 1e-5;
1152 let options = Marching {
1153 chord,
1154 ..Marching::default()
1155 };
1156 let broken = Climbing { rise: 0.004 };
1157 for u in [0.0, 1e-4, 0.3, 0.9999, 1.0] {
1158 let walked = follow(&broken, &[u, broken.rise * u], options, T).unwrap();
1159 assert_ne!(
1160 walked.stopped,
1161 Stopped::Closed,
1162 "from {u}: a curve broken at the seam is not a loop"
1163 );
1164 for pair in walked.points.windows(2) {
1165 let middle = pair[0] + (pair[1] - pair[0]) * 0.5;
1166 let miss = broken.miss(middle);
1167 assert!(
1168 miss <= chord,
1169 "from {u}: the chord {:?} to {:?} misses the curve by {miss:e}",
1170 pair[0],
1171 pair[1]
1172 );
1173 }
1174 let (first, last) = (&walked.states[0], &walked.states[walked.states.len() - 1]);
1175 assert!(
1176 first[0].min(last[0]) <= 1e-6 && first[0].max(last[0]) >= 1.0 - 1e-6,
1177 "from {u}: the whole way round, seam to seam: {first:?} to {last:?}"
1178 );
1179 }
1180
1181 let whole = Climbing { rise: 0.0 };
1182 let walked = follow(&whole, &[0.0, 0.0], options, T).unwrap();
1183 assert_eq!(
1184 walked.stopped,
1185 Stopped::Closed,
1186 "a circle met across the seam"
1187 );
1188 }
1189
1190 #[test]
1193 fn the_null_vector_is_the_generalized_cross_product() {
1194 let null = null_vector(&[vec![3.0, 4.0]], 2).unwrap();
1196 assert!((null[0] - 0.8).abs() < 1e-12 && (null[1] + 0.6).abs() < 1e-12);
1197 let null = null_vector(&[vec![1.0, 0.0, 0.0], vec![0.0, 1.0, 0.0]], 3).unwrap();
1199 assert!(null[0].abs() < 1e-12 && null[1].abs() < 1e-12 && null[2].abs() - 1.0 < 1e-12);
1200 assert!(null_vector(&[vec![1.0, 2.0, 3.0], vec![2.0, 4.0, 6.0]], 3).is_none());
1202 }
1203}