rounding: record F-034, arc segment counts tip on the last bit
No behaviour change. Comments and a FAILURES entry. The Y profile builds a section area of 135.572973 against a recorded 135.574, out by 0.001027, while every other value for that case matches exactly. Three-Fin matches on everything including area. Isolated by probing the reference inside the pinned toolchain image. The hull cap is identical to nine figures, the bare union is identical, and a single filleted pair is identical at 30 vertices and 125.699057 mm2. The difference appears only when the three filleted pairs are combined, and the three pairs, which are related by 120 degree symmetry and must be identical, come back as 125.699057, 125.698029, 125.699057. Cause proven. The arc segment count is a ceiling on a quantity that is frequently an exact integer: a 60 degree half-angle at $fn=48 gives exactly 8. Floating point delivers that as 8.000000000000004 on one corner and 7.999999999999998 on the others, so one corner gets a whole extra segment. The half-angles come from the merged polygon, whose vertices come from the boolean kernel, and BOSL2 clipper and GEOS disagree in the last bit. Reproduced unguarded because the reference is unguarded. Rounding the count before the ceiling was implemented and reverted: it makes the three pairs identical and fixes Y exactly, and breaks Three-Fin, which had been matching to the digit. Three-Fin has the same asymmetry and the oracle records it. BOSL2 tipped the same way GEOS does there and the opposite way on Y. Two consequences for the project rather than the code. Some recorded values encode float noise rather than geometry, so a port that is geometrically more correct than the reference will fail those cases. And the tolerance model may need revisiting: VOLUME_MM3 is compared at the lengths tolerance of 1e-4 despite being area times 100 mm, so a 1e-3 area difference becomes a 1e-1 volume difference. MASS_G is derived the same way. No decision yet. The number of affected cases is unknown and is the only thing that should drive it, and that is not knowable until build() exists and all 123 cases can run.
This commit is contained in:
@@ -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
|
## Open, not closed
|
||||||
|
|
||||||
| # | Status |
|
| # | 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-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-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-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.
|
Everything else is closed with a proven cause and a proven correction.
|
||||||
|
|||||||
@@ -229,7 +229,19 @@ def _circlecorner(points: Tuple[Point, Point, Point],
|
|||||||
scale = r / math.sin(math.radians(angle))
|
scale = r / math.sin(math.radians(angle))
|
||||||
center = (scale * bx / bn + here[0], scale * by / bn + here[1])
|
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))
|
return arc(n, center, (start, end))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user