diff --git a/docs/FAILURES.md b/docs/FAILURES.md index c175770..7bf9907 100644 --- a/docs/FAILURES.md +++ b/docs/FAILURES.md @@ -771,6 +771,71 @@ it with `git status` whenever the question is *which* oracle is present. --- +### F-034 — arc segment counts land on exact integers and tip on the last bit +CT 100. Shapely port, `round_corners`. + +**Observed:** the Y profile builds a section area of 135.572973 against a +recorded 135.574 — out by 0.001027, just past the 1e-3 area tolerance — while +every other recorded value for that case matches exactly: envelope, solved spoke +radius, minimum wall, channel count. Three-Fin matches on every value including +area. + +Isolated by probing the reference directly inside the pinned toolchain image. +The hull cap is identical to nine figures. The bare union before filleting is +identical. A single filleted pair is identical, 30 vertices and 125.699057 mm^2. +The whole difference appears when the three filleted pairs are combined. + +The three pairs are related by 120 degree symmetry and must be identical. They +come back as `[125.699057, 125.698029, 125.699057]`. + +**Cause:** **Proven.** `_circlecorner` sets the arc segment count to +`max(3, ceil((90 - half_angle)/180 * $fn))`. At a 60 degree half-angle with +`$fn = 48` that expression is mathematically exactly 8. Floating point delivers +it as `8.000000000000004` on one corner and `7.999999999999998` on the other +two, so the ceiling gives 9 on one and 8 on the others — one extra segment, +slightly less enclosed area. + +The half-angle is computed from the merged polygon's vertices, and those come +from the boolean kernel. BOSL2's clipper and GEOS agree to well within any +meaningful tolerance but not to the last bit, so they tip these ceilings +differently. + +**Correction: none. Reproduced unguarded, because the reference is unguarded.** + +Rounding the count before the ceiling was implemented and reverted. It makes all +three pairs identical and fixes Y exactly — and breaks Three-Fin, which had been +matching to the digit. Three-Fin has the same near-integer corners and the same +one-pair-tips-to-9 asymmetry, and the oracle **records that asymmetry**. BOSL2 +happened to tip the same way GEOS does there, and the opposite way on Y. + +BOSL2 guards this identical hazard inside `segs()` with a `2e-15` subtraction +but not at this call site. Adding the guard is better engineering and produces a +different answer from the reference, which is what matters here. + +**Consequence:** Three things. + +**Some recorded values encode float noise, not geometry.** Three-Fin's area +reflects a spurious extra segment on one of three symmetric corners. A port that +is geometrically more correct than the reference will fail that case. + +**Bit-exact agreement across a different boolean kernel is not achievable in +general.** Not for want of care in the port. Wherever a corner angle lands on an +exact segment boundary, the result is decided by the kernel's last bit. + +**The tolerance model may need revisiting, and that is CIVICVS's call.** +`VOLUME_MM3` ends in `_MM3`, so `test_oracle.py` compares it at the lengths +tolerance of 1e-4 rather than the areas tolerance of 1e-3. It is section area +times a 100 mm length, so an area difference of 1e-3 becomes a volume difference +of 1e-1 — a thousand times the tolerance it is checked against. `MASS_G` is +derived the same way. Any case affected by this failure mode fails on volume and +mass long before it fails on area. + +Do not act on this until `build()` exists and all 123 cases can be run. The +number of affected cases is unknown and is the only thing that should drive the +decision. + +--- + ## Open, not closed | # | Status | @@ -791,5 +856,6 @@ it with `git status` whenever the question is *which* oracle is present. | F-031 | **Corrected** 2026-08-18. Root cause of `ct-baseline.sh`. | | F-032 | **Closed** 2026-08-18. No correction required; encoded in `ct-baseline.sh`. | | F-033 | **Corrected** 2026-08-19. Restore path unreachable under `set -e`. | +| F-034 | **Open** 2026-08-19. Reproduced unguarded; affects an unknown number of oracle cases. | Everything else is closed with a proven cause and a proven correction. diff --git a/src/mechcomp/geom/rounding.py b/src/mechcomp/geom/rounding.py index 3452ddc..f6e9ed4 100644 --- a/src/mechcomp/geom/rounding.py +++ b/src/mechcomp/geom/rounding.py @@ -229,7 +229,19 @@ def _circlecorner(points: Tuple[Point, Point, Point], scale = r / math.sin(math.radians(angle)) center = (scale * bx / bn + here[0], scale * by / bn + here[1]) - n = max(3, math.ceil((90.0 - angle) / 180.0 * segs(r, None, fn))) + # The segment count is a ceiling on a value that is frequently an exact + # integer -- a 60 degree half-angle at $fn=48 gives exactly 8. Floating + # point delivers that as 8.000000000000004 or 7.999999999999998 depending + # on how the corner was reached, and the ceiling then differs by a whole + # segment, changing the enclosed area by about a thousandth of a square + # millimetre. + # + # This is reproduced unguarded because the reference is unguarded. Rounding + # first was tried and is wrong: it fixes the Y profile and breaks Three-Fin, + # because BOSL2's own output is asymmetric in exactly this way and the + # oracle records that asymmetry. See FAILURES.md F-034. + raw = (90.0 - angle) / 180.0 * segs(r, None, fn) + n = max(3, math.ceil(raw)) return arc(n, center, (start, end))