Skip to main content

axiolid_exact_arrangement/
snap.rs

1//! Snap rounding of exact edges to the binary64 grid (ADR 0100).
2//!
3//! A *hot cell* is the set of points that [`ExactPoints::round`] rounds to
4//! the rounded position of an end of an input edge. The rounding is to
5//! nearest with ties to even, so the cells are half-open boxes of the
6//! binary64 tensor grid that partition the plane: the cell of an even centre
7//! owns both of its ties.
8//!
9//! [`snap_round`] routes every edge through the centres of the hot cells that
10//! some point of the closed exact edge rounds to, in order along the edge,
11//! and arranges the routed fragments exactly. Where nothing but the edge's
12//! own ends rounds onto an edge, the fragment is the edge with its ends
13//! rounded once. The result is certified, not assumed: a crossing left among
14//! the fragments is refused.
15//!
16//! The displacement bound is the larger half-extent of the cells a route
17//! visits (zero for a binary64 end), so a point farther from the exact
18//! boundary than the bound has the same winding number before and after.
19//!
20//! [`Snapped::class_boundary`] nets the routes of a class's edges per
21//! fragment: opposite traversals cancel, so a part that became zero-width
22//! vanishes and a zero-width gap merges its two sides.
23
24use std::cmp::Ordering;
25use std::collections::BTreeMap;
26
27use axiolid_contracts::{DisplacementBound, ExecutionOptions, GeomError, GeomResult, Sign};
28use axiolid_core::Point2;
29use axiolid_exact::{Arith, Dyadic};
30
31use crate::arrangement::{
32    arrange, charge, sort_exact, Arrangement, PairBudget, PieceId, Segment, VertexId,
33};
34use crate::points::{inconsistent, ExactPoints, PointId};
35use crate::ring::point_order;
36use crate::trace::HalfEdge;
37
38/// Bytes charged for one hot cell: its exact bounds, the point and its
39/// index entries.
40pub const HOT_POINT_SCRATCH_BYTES: usize = 256;
41
42/// Bytes charged for one routed fragment: the segment and its path entry.
43pub const FRAGMENT_SCRATCH_BYTES: usize = 64;
44
45fn exact(value: f64) -> GeomResult<Dyadic> {
46    Dyadic::try_from_f64(value).ok_or_else(|| GeomError::Degenerate("a cell is not finite".into()))
47}
48
49/// The cell of one centre: per axis its exact bounds, whether it owns them,
50/// and the extents used for the displacement bound and for pruning.
51#[derive(Debug, Clone)]
52struct Cell {
53    center: Point2,
54    /// `[x, y]` lower bounds: the midpoints to the previous binary64 number.
55    lo: [Dyadic; 2],
56    /// `[x, y]` upper bounds.
57    hi: [Dyadic; 2],
58    /// Per axis, whether the cell owns its bounds (an even centre).
59    closed: [bool; 2],
60    /// The largest distance from the centre to a point of the cell, per
61    /// coordinate, over both axes.
62    half_width: f64,
63    /// `[x_lo, y_lo, x_hi, y_hi]` from the previous to the next binary64
64    /// number; contains the cell. Only prunes.
65    reach: [f64; 4],
66}
67
68/// The gaps from a finite `value` down and up to its neighbours; at the
69/// largest magnitude the missing neighbour mirrors the finite gap.
70fn gaps(value: f64) -> (f64, f64) {
71    let (down, up) = (value.next_down(), value.next_up());
72    match (down.is_finite(), up.is_finite()) {
73        (true, true) => (value - down, up - value),
74        (false, _) => (up - value, up - value),
75        (_, false) => (value - down, value - down),
76    }
77}
78
79fn cell(center: Point2) -> GeomResult<Cell> {
80    let center = Point2::new(center.x + 0.0, center.y + 0.0);
81    let half = exact(0.5)?;
82    let mut lo = [Dyadic::zero(), Dyadic::zero()];
83    let mut hi = [Dyadic::zero(), Dyadic::zero()];
84    let mut closed = [false; 2];
85    let mut reach = [0.0; 4];
86    let mut half_width = 0.0f64;
87    for axis in 0..2 {
88        let c = if axis == 0 { center.x } else { center.y };
89        let (down, up) = gaps(c);
90        let c_exact = exact(c)?;
91        lo[axis] = c_exact.sub(&exact(down)?.mul(&half));
92        hi[axis] = c_exact.add(&exact(up)?.mul(&half));
93        closed[axis] = c.abs().to_bits() & 1 == 0;
94        reach[axis] = c - down;
95        reach[axis + 2] = c + up;
96        let gap = up.max(down);
97        let halved = gap * 0.5;
98        half_width = half_width.max(if halved > 0.0 { halved } else { gap });
99    }
100    Ok(Cell {
101        center,
102        lo,
103        hi,
104        closed,
105        half_width,
106        reach,
107    })
108}
109
110/// A parameter `num / den` along an edge (`den > 0`), open when the edge
111/// meets the cell only after it.
112#[derive(Debug, Clone)]
113struct Param {
114    num: Dyadic,
115    den: Dyadic,
116    open: bool,
117}
118
119fn compare_values(a: &Param, b: &Param) -> GeomResult<Ordering> {
120    let sign = a
121        .num
122        .mul(&b.den)
123        .sub(&b.num.mul(&a.den))
124        .sign()
125        .ok_or_else(inconsistent)?;
126    Ok(match sign {
127        Sign::Positive => Ordering::Greater,
128        Sign::Negative => Ordering::Less,
129        _ => Ordering::Equal,
130    })
131}
132
133/// Order by value, a closed parameter before an open one of equal value.
134fn compare_params(a: &Param, b: &Param) -> GeomResult<Ordering> {
135    Ok(compare_values(a, b)?.then(a.open.cmp(&b.open)))
136}
137
138/// Where the closed exact segment `from`-`to` (homogeneous, `w > 0`) first
139/// meets `cell`: the lower end of the parameter interval of the meeting, or
140/// `None` when it does not meet it.
141fn meets(from: &[Dyadic; 3], to: &[Dyadic; 3], cell: &Cell) -> GeomResult<Option<Param>> {
142    let mut lower = Param {
143        num: Dyadic::zero(),
144        den: exact(1.0)?,
145        open: false,
146    };
147    let mut upper = Param {
148        num: exact(1.0)?,
149        den: exact(1.0)?,
150        open: false,
151    };
152    for axis in 0..2 {
153        let open = !cell.closed[axis];
154        // `sigma * (P_axis - bound * P_w)` is at least zero on the cell's
155        // side of the bound; it is linear along the segment.
156        for (bound, negate) in [(&cell.lo[axis], false), (&cell.hi[axis], true)] {
157            let at = |p: &[Dyadic; 3]| {
158                let value = p[axis].sub(&bound.mul(&p[2]));
159                if negate {
160                    value.neg()
161                } else {
162                    value
163                }
164            };
165            let (h0, h1) = (at(from), at(to));
166            let satisfied = |h: &Dyadic| -> GeomResult<bool> {
167                Ok(match h.sign().ok_or_else(inconsistent)? {
168                    Sign::Positive => true,
169                    Sign::Zero => !open,
170                    _ => false,
171                })
172            };
173            match (satisfied(&h0)?, satisfied(&h1)?) {
174                (true, true) => {}
175                (false, false) => return Ok(None),
176                (true, false) => {
177                    let candidate = Param {
178                        num: h0.clone(),
179                        den: h0.sub(&h1),
180                        open,
181                    };
182                    // An open end excludes its value: it wins a tie, also
183                    // against the initial closed end at the edge's far end.
184                    match compare_values(&candidate, &upper)? {
185                        Ordering::Less => upper = candidate,
186                        Ordering::Equal if candidate.open => upper = candidate,
187                        _ => {}
188                    }
189                }
190                (false, true) => {
191                    let candidate = Param {
192                        num: h0.neg(),
193                        den: h1.sub(&h0),
194                        open,
195                    };
196                    match compare_values(&candidate, &lower)? {
197                        Ordering::Greater => lower = candidate,
198                        Ordering::Equal if candidate.open => lower = candidate,
199                        _ => {}
200                    }
201                }
202            }
203        }
204    }
205    // `lower <= s <= upper`, each end excluded when open.
206    Ok(match compare_values(&lower, &upper)? {
207        Ordering::Less => Some(lower),
208        Ordering::Equal if !lower.open && !upper.open => Some(lower),
209        _ => None,
210    })
211}
212
213/// The edges of a snap-rounded set, as fragments of an exact arrangement.
214#[derive(Debug, Clone)]
215pub struct Snapped {
216    /// The hot centres, all flat, with the frame sign of the input points
217    /// (the arrangement may add points only if it was refused).
218    pub points: ExactPoints,
219    /// The arrangement of the fragments; no crossings.
220    pub arrangement: Arrangement,
221    /// Per input edge, its pieces from `ends[0]` to `ends[1]`, each with
222    /// whether that direction runs from `ends[0]` to `ends[1]` of the piece.
223    /// Empty when both ends round to the same cell.
224    pub routes: Vec<Vec<(PieceId, bool)>>,
225    /// How far a boundary point moved at most, in the maximum norm.
226    pub displacement: DisplacementBound,
227    /// The edges routed through a cell other than their ends' cells.
228    pub rerouted: usize,
229}
230
231/// The net boundary of a class after snapping.
232#[derive(Debug, Clone)]
233pub struct ClassBoundary {
234    /// The half-edges with the class on their left, ascending by piece.
235    pub half_edges: Vec<HalfEdge>,
236    /// Per element of the chain, whether some piece of its route carries a
237    /// surviving half-edge.
238    pub survives: Vec<bool>,
239}
240
241fn refused(message: &str) -> GeomError {
242    GeomError::Degenerate(message.into())
243}
244
245/// Snap-round `edges`, straight segments between distinct points of
246/// `points`.
247///
248/// The edges must be pairwise interior-disjoint with no end inside another
249/// (a boundary subgraph of an arrangement). Every examined edge and cell
250/// pair counts against `budget`; cells and fragments are charged against
251/// the memory budget on top of `declared_bytes`, as in [`arrange`].
252pub fn snap_round(
253    points: &ExactPoints,
254    edges: &[Segment],
255    budget: &mut PairBudget,
256    declared_bytes: usize,
257    options: &ExecutionOptions,
258) -> GeomResult<Snapped> {
259    // The hot cells: the distinct rounded ends, by centre.
260    let rounded = |id: PointId| -> GeomResult<Point2> {
261        let at = points.round(id)?;
262        Ok(Point2::new(at.x + 0.0, at.y + 0.0))
263    };
264    let mut centers: Vec<Point2> = Vec::new();
265    for edge in edges {
266        for end in edge.ends {
267            centers.push(rounded(end)?);
268        }
269    }
270    centers.sort_by(point_order);
271    centers.dedup_by(|a, b| point_order(a, b).is_eq());
272    let mut charged = 0usize;
273    let mut store = ExactPoints::with_frame_sign(points.frame_sign());
274    let mut cells = Vec::with_capacity(centers.len());
275    let mut ids = Vec::with_capacity(centers.len());
276    for &center in &centers {
277        charge(
278            options,
279            declared_bytes,
280            &mut charged,
281            HOT_POINT_SCRATCH_BYTES,
282        )?;
283        let hot = cell(center)?;
284        ids.push(store.push_flat(hot.center)?);
285        cells.push(hot);
286    }
287    let find = |at: Point2| {
288        centers
289            .binary_search_by(|probe| point_order(probe, &at))
290            .map_err(|_| inconsistent())
291    };
292
293    // Per edge the route: the start cell, the cells met in order, the end.
294    let mut routes: Vec<Vec<usize>> = Vec::with_capacity(edges.len());
295    let mut displacement = 0.0f64;
296    let mut rerouted = 0usize;
297    for edge in edges {
298        options.check_cancelled()?;
299        let [from, to] = edge.ends;
300        let start = find(rounded(from)?)?;
301        let end = find(rounded(to)?)?;
302        if start == end {
303            // Both ends in one convex cell: the whole edge is inside it.
304            for (id, at) in [(from, start), (to, end)] {
305                if points.flat(id).is_none() {
306                    displacement = displacement.max(cells[at].half_width);
307                }
308            }
309            routes.push(Vec::new());
310            continue;
311        }
312        let (a, b) = (points.bounds(from), points.bounds(to));
313        let reach = [
314            a[0].min(b[0]),
315            a[1].min(b[1]),
316            a[2].max(b[2]),
317            a[3].max(b[3]),
318        ];
319        let first = cells.partition_point(|c| c.reach[2] < reach[0]);
320        let mut found: Vec<(usize, Param)> = Vec::new();
321        for (index, candidate) in cells.iter().enumerate().skip(first) {
322            if candidate.reach[0] > reach[2] {
323                break;
324            }
325            if index == start
326                || index == end
327                || candidate.reach[3] < reach[1]
328                || candidate.reach[1] > reach[3]
329            {
330                continue;
331            }
332            budget.count()?;
333            if let Some(param) = meets(points.exact(from), points.exact(to), candidate)? {
334                found.push((index, param));
335            }
336        }
337        let mut order: Vec<usize> = (0..found.len()).collect();
338        sort_exact(&mut order, |l, r| compare_params(&found[l].1, &found[r].1))?;
339        for pair in order.windows(2) {
340            if compare_params(&found[pair[0]].1, &found[pair[1]].1)?.is_eq() {
341                return Err(inconsistent());
342            }
343        }
344        for (id, at) in [(from, start), (to, end)] {
345            if points.flat(id).is_none() {
346                displacement = displacement.max(cells[at].half_width);
347            }
348        }
349        for &(index, _) in &found {
350            displacement = displacement.max(cells[index].half_width);
351        }
352        rerouted += usize::from(!found.is_empty());
353        let mut route = Vec::with_capacity(found.len() + 2);
354        route.push(start);
355        route.extend(order.iter().map(|&i| found[i].0));
356        route.push(end);
357        routes.push(route);
358    }
359
360    // The fragments between consecutive centres, arranged exactly.
361    let mut segments = Vec::new();
362    let mut spans = Vec::with_capacity(routes.len());
363    for route in &routes {
364        let first = segments.len();
365        for pair in route.windows(2) {
366            charge(
367                options,
368                declared_bytes,
369                &mut charged,
370                FRAGMENT_SCRATCH_BYTES,
371            )?;
372            segments.push(Segment {
373                ends: [ids[pair[0]], ids[pair[1]]],
374            });
375        }
376        spans.push(first..segments.len());
377    }
378    let arrangement = arrange(
379        &mut store,
380        &segments,
381        budget,
382        declared_bytes.saturating_add(charged),
383        options,
384    )?;
385    if arrangement.crossings != 0 {
386        return Err(refused("snap rounding left two fragments crossing"));
387    }
388    let routes = spans
389        .into_iter()
390        .map(|span| {
391            span.flat_map(|segment| arrangement.paths[segment].iter().copied())
392                .collect()
393        })
394        .collect();
395    Ok(Snapped {
396        points: store,
397        arrangement,
398        routes,
399        displacement: DisplacementBound::new(displacement).ok_or_else(inconsistent)?,
400        rerouted,
401    })
402}
403
404impl Snapped {
405    /// The binary64 position of a vertex of the arrangement.
406    pub fn point(&self, vertex: VertexId) -> GeomResult<Point2> {
407        self.points
408            .flat(self.arrangement.vertices[vertex.0].point)
409            .ok_or_else(inconsistent)
410    }
411
412    /// The net boundary of a class whose boundary is `chain`: elements
413    /// `(edge, forward)` of the input edges, `forward` when the class runs
414    /// along the edge from `ends[0]` to `ends[1]` with the class on its
415    /// left. Opposite traversals of a piece cancel. A piece traversed twice
416    /// the same way is refused.
417    pub fn class_boundary(&self, chain: &[(usize, bool)]) -> GeomResult<ClassBoundary> {
418        let route_of = |&(edge, forward): &(usize, bool)| -> Vec<(PieceId, bool)> {
419            let route = &self.routes[edge];
420            if forward {
421                route.clone()
422            } else {
423                route.iter().rev().map(|&(p, f)| (p, !f)).collect()
424            }
425        };
426        let mut net: BTreeMap<PieceId, i32> = BTreeMap::new();
427        for element in chain {
428            for (piece, forward) in route_of(element) {
429                *net.entry(piece).or_default() += if forward { 1 } else { -1 };
430            }
431        }
432        let mut half_edges = Vec::new();
433        for (&piece, &count) in &net {
434            match count {
435                0 => {}
436                1 => half_edges.push(HalfEdge {
437                    piece,
438                    forward: true,
439                }),
440                -1 => half_edges.push(HalfEdge {
441                    piece,
442                    forward: false,
443                }),
444                _ => return Err(refused("snap rounding folded a boundary onto itself")),
445            }
446        }
447        let survives = chain
448            .iter()
449            .map(|element| {
450                route_of(element)
451                    .iter()
452                    .any(|(piece, _)| net.get(piece).is_some_and(|&count| count != 0))
453            })
454            .collect();
455        Ok(ClassBoundary {
456            half_edges,
457            survives,
458        })
459    }
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465
466    fn flat(x: f64, y: f64) -> Point2 {
467        Point2::new(x, y)
468    }
469
470    #[test]
471    fn the_cell_at_one_is_asymmetric_by_a_binade() {
472        let c = cell(flat(1.0, 1.0)).unwrap();
473        let one = exact(1.0).unwrap();
474        // Below 1.0 the spacing is 2^-53, above it 2^-52.
475        assert_eq!(
476            one.sub(&c.lo[0]),
477            exact(2f64.powi(-54)).unwrap(),
478            "lower half-width"
479        );
480        assert_eq!(
481            c.hi[0].sub(&one),
482            exact(2f64.powi(-53)).unwrap(),
483            "upper half-width"
484        );
485        assert_eq!(c.half_width, 2f64.powi(-53));
486        assert!(c.closed[0], "1.0 has an even significand");
487    }
488
489    #[test]
490    fn cells_at_zero_the_smallest_subnormal_and_the_largest_number() {
491        let tiny = f64::from_bits(1);
492        let zero = cell(flat(0.0, -0.0)).unwrap();
493        assert_eq!(zero.center.y.to_bits(), 0, "-0.0 is +0.0");
494        assert_eq!(zero.hi[0], exact(tiny).unwrap().mul(&exact(0.5).unwrap()));
495        assert_eq!(zero.lo[0], zero.hi[0].neg());
496        assert!(zero.closed[0]);
497        assert_eq!(zero.half_width, tiny, "halving underflows");
498
499        let small = cell(flat(tiny, tiny)).unwrap();
500        assert!(!small.closed[0], "the smallest subnormal is odd");
501        assert_eq!(small.lo[0], zero.hi[0]);
502
503        let big = cell(flat(f64::MAX, -f64::MAX)).unwrap();
504        let gap = f64::MAX - f64::MAX.next_down();
505        assert_eq!(
506            big.hi[0].sub(&exact(f64::MAX).unwrap()),
507            exact(gap).unwrap().mul(&exact(0.5).unwrap()),
508            "the missing neighbour mirrors the gap"
509        );
510        assert_eq!(big.reach[2], f64::MAX + gap);
511    }
512
513    fn segment_through(points: &ExactPoints, a: (f64, f64), b: (f64, f64), cell: &Cell) -> bool {
514        let mut store = points.clone();
515        let from = store.push_flat(flat(a.0, a.1)).unwrap();
516        let to = store.push_flat(flat(b.0, b.1)).unwrap();
517        meets(store.exact(from), store.exact(to), cell)
518            .unwrap()
519            .is_some()
520    }
521
522    #[test]
523    fn a_segment_on_a_midpoint_meets_only_the_even_neighbour() {
524        let odd = 1.0 + f64::EPSILON; // 0x3ff0...01: odd significand
525        assert_eq!(1.0f64.to_bits() & 1, 0);
526        assert_eq!(odd.to_bits() & 1, 1);
527        // The vertical line x = tie runs between the cells of 1.0 and odd.
528        let tie = exact(1.0)
529            .unwrap()
530            .add(&exact(odd).unwrap())
531            .mul(&exact(0.5).unwrap());
532        let (even, odd_cell) = (cell(flat(1.0, 1.0)).unwrap(), cell(flat(odd, 1.0)).unwrap());
533        // Homogeneous points on x = tie: (tie * 2, y * 2, 2) and a second.
534        let two = exact(2.0).unwrap();
535        let line = |y: f64| [tie.mul(&two), exact(y).unwrap().mul(&two), two.clone()];
536        let (p, q) = (line(0.9), line(1.1));
537        assert!(meets(&p, &q, &even).unwrap().is_some());
538        assert!(meets(&p, &q, &odd_cell).unwrap().is_none());
539    }
540
541    #[test]
542    fn a_point_meets_its_own_cell_and_no_neighbour() {
543        let mut points = ExactPoints::new();
544        let at = flat(0.3, -2.5);
545        let id = points.push_flat(at).unwrap();
546        let own = cell(at).unwrap();
547        assert!(meets(points.exact(id), points.exact(id), &own)
548            .unwrap()
549            .is_some());
550        for neighbour in [
551            flat(at.x.next_up(), at.y),
552            flat(at.x.next_down(), at.y),
553            flat(at.x, at.y.next_up()),
554            flat(at.x, at.y.next_down()),
555        ] {
556            let other = cell(neighbour).unwrap();
557            assert!(meets(points.exact(id), points.exact(id), &other)
558                .unwrap()
559                .is_none());
560        }
561    }
562
563    #[test]
564    fn a_segment_through_a_cell_is_found_and_one_beside_it_is_not() {
565        let points = ExactPoints::new();
566        let c = cell(flat(1.0, 1.0)).unwrap();
567        assert!(segment_through(&points, (0.0, 0.0), (2.0, 2.0), &c));
568        assert!(!segment_through(&points, (0.0, 0.5), (2.0, 2.5), &c));
569    }
570
571    /// A small seeded generator; the tests need spread, not quality.
572    struct Lcg(u64);
573
574    impl Lcg {
575        fn next(&mut self) -> u64 {
576            self.0 = self
577                .0
578                .wrapping_mul(6364136223846793005)
579                .wrapping_add(1442695040888963407);
580            self.0 >> 11
581        }
582
583        fn below(&mut self, n: u64) -> u64 {
584            self.next() % n
585        }
586    }
587
588    /// The point at parameter `num / den` of the segment, rounded once.
589    fn rounded_at(from: &[Dyadic; 3], to: &[Dyadic; 3], num: &Dyadic, den: &Dyadic) -> Point2 {
590        let rest = den.sub(num);
591        let mix = |i: usize| rest.mul(&from[i]).add(&num.mul(&to[i]));
592        let mut store = ExactPoints::new();
593        let id = store.push([mix(0), mix(1), mix(2)]).unwrap();
594        let at = store.round(id).unwrap();
595        Point2::new(at.x + 0.0, at.y + 0.0)
596    }
597
598    /// `meets` against rounding the points of the segment one by one: the
599    /// lower end it returns is where the segment enters the cell, nothing
600    /// sampled before it rounds into the cell, and every sample that does
601    /// lies at or after it.
602    #[test]
603    fn meets_agrees_with_rounding_the_points_of_the_segment() {
604        let mut rng = Lcg(0x5eed);
605        let (mut hits, mut misses, mut ties) = (0, 0, 0);
606        let tiny = exact(2f64.powi(-200)).unwrap();
607        for case in 0..5000 {
608            let scale = [1.0, 0.1, 1e-3, 3.7e5, 2.0][rng.below(5) as usize];
609            let center = Point2::new(
610                (rng.next() as f64 / (1u64 << 53) as f64 * 8.0 - 4.0) * scale,
611                (rng.next() as f64 / (1u64 << 53) as f64 * 8.0 - 4.0) * scale,
612            );
613            let center = Point2::new(center.x + 0.0, center.y + 0.0);
614            let cell = cell(center).unwrap();
615            let ulp = |c: f64| exact(c.next_up() - c).unwrap();
616            let end = |rng: &mut Lcg| {
617                let w = [1u64, 3, 5, 7][rng.below(4) as usize];
618                let weight = exact(w as f64).unwrap();
619                let quarter = exact(0.25).unwrap();
620                let offset = |rng: &mut Lcg, c: f64| {
621                    let m = rng.below(41) as f64 - 20.0;
622                    exact(c)
623                        .unwrap()
624                        .mul(&weight)
625                        .add(&ulp(c).mul(&exact(m).unwrap()).mul(&quarter))
626                };
627                [offset(rng, center.x), offset(rng, center.y), weight.clone()]
628            };
629            // A third of the ends sit exactly on a corner of the cell, where
630            // ties of both axes meet, whichever way they go.
631            let corner = |rng: &mut Lcg| {
632                let pick = |rng: &mut Lcg, axis: usize| {
633                    if rng.below(2) == 0 {
634                        cell.lo[axis].clone()
635                    } else {
636                        cell.hi[axis].clone()
637                    }
638                };
639                [pick(rng, 0), pick(rng, 1), exact(1.0).unwrap()]
640            };
641            let either = |rng: &mut Lcg| {
642                if rng.below(3) == 0 {
643                    corner(rng)
644                } else {
645                    end(rng)
646                }
647            };
648            let (from, to) = (either(&mut rng), either(&mut rng));
649            let found = meets(&from, &to, &cell).unwrap();
650            let inside =
651                |num: &Dyadic, den: &Dyadic| rounded_at(&from, &to, num, den) == cell.center;
652            match &found {
653                Some(lower) => {
654                    hits += 1;
655                    if lower.open {
656                        assert!(!inside(&lower.num, &lower.den), "case {case}: open");
657                        assert!(
658                            inside(&lower.num.add(&lower.den.mul(&tiny)), &lower.den),
659                            "case {case}"
660                        );
661                    } else {
662                        assert!(inside(&lower.num, &lower.den), "case {case}: closed");
663                    }
664                    let before = lower.num.sub(&lower.den.mul(&tiny));
665                    if before.sign() == Some(Sign::Positive) {
666                        assert!(!inside(&before, &lower.den), "case {case}: before");
667                    }
668                    ties += usize::from(lower.open);
669                }
670                None => misses += 1,
671            }
672            for k in 0..=16 {
673                let num = exact(f64::from(k)).unwrap();
674                let den = exact(16.0).unwrap();
675                if inside(&num, &den) {
676                    let lower = found
677                        .as_ref()
678                        .unwrap_or_else(|| panic!("case {case}: missed s={k}/16"));
679                    let at = Param {
680                        num,
681                        den,
682                        open: false,
683                    };
684                    assert!(
685                        compare_values(lower, &at).unwrap().is_le(),
686                        "case {case}: s={k}/16 before the lower end"
687                    );
688                }
689            }
690        }
691        assert!(hits > 500 && misses > 500, "{hits} hits, {misses} misses");
692        assert!(ties > 0, "no open lower end was exercised");
693    }
694}