@knowvah/dot-engine 2.2.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,8 @@
3
3
 
4
4
  import type { Point, Poly, Edge } from './types.js';
5
5
  import { solve3 } from './solvers.js';
6
+ import { fma, fms } from '../common/fma.js';
7
+ import { libmHypot } from '../common/libm-hypot.js';
6
8
 
7
9
  const EPSILON1 = 1e-3;
8
10
  const EPSILON2 = 1e-6;
@@ -18,28 +20,53 @@ class RouteHelper {
18
20
  static B1(t: number): number { const u = 1-t; return 3*t*u*u; }
19
21
  static B2(t: number): number { const u = 1-t; return 3*t*t*u; }
20
22
  static B3(t: number): number { return t*t*t; }
21
- static B01(t: number): number { const u = 1-t; return u*u*(u+3*t); }
22
- static B23(t: number): number { const u = 1-t; return t*t*(3*u+t); }
23
+ /** @see lib/pathplan/route.c:B01 */
24
+ static B01(t: number): number {
25
+ const u = 1-t;
26
+ // clang -ffp-contract=on: fmadd at route.c:488 (`tmp + 3*t`)
27
+ return (u*u) * fma(3, t, u);
28
+ }
29
+ /** @see lib/pathplan/route.c:B23 */
30
+ static B23(t: number): number {
31
+ const u = 1-t;
32
+ // clang -ffp-contract=on: fmadd at route.c:494 (`3*tmp + t`)
33
+ return (t*t) * fma(3, u, t);
34
+ }
23
35
 
36
+ /** @see lib/pathplan/route.c:normv */
24
37
  static normv(v: Point): Point {
25
- const d = v.x*v.x + v.y*v.y;
38
+ // clang -ffp-contract=on: fmadd at route.c:413 (v.x*v.x fused, v.y*v.y rounded)
39
+ const d = fma(v.x, v.x, v.y*v.y);
26
40
  if (d > 1e-6) { const s = Math.sqrt(d); return { x: v.x/s, y: v.y/s }; }
27
41
  return v;
28
42
  }
29
43
  static ptAdd(a: Point, b: Point): Point { return { x: a.x+b.x, y: a.y+b.y }; }
30
44
  static ptSub(a: Point, b: Point): Point { return { x: a.x-b.x, y: a.y-b.y }; }
31
45
  static ptScale(p: Point, c: number): Point { return { x: p.x*c, y: p.y*c }; }
32
- static ptDot(a: Point, b: Point): number { return a.x*b.x + a.y*b.y; }
33
- static ptDist(a: Point, b: Point): number { return Math.hypot(a.x-b.x, a.y-b.y); }
46
+ /** @see lib/pathplan/route.c:dot */
47
+ static ptDot(a: Point, b: Point): number {
48
+ // clang -ffp-contract=on: fmadd at route.c:459 (p1.x*p2.x fused, p1.y*p2.y rounded)
49
+ return fma(a.x, b.x, a.y*b.y);
50
+ }
51
+ /** @see lib/pathplan/route.c:dist (libm hypot, no contraction) */
52
+ static ptDist(a: Point, b: Point): number { return libmHypot(a.x-b.x, a.y-b.y); }
34
53
  static distN(pts: Point[]): number {
35
54
  let rv = 0;
36
55
  for (let i = 1; i < pts.length; i++) rv += RouteHelper.ptDist(pts[i], pts[i-1]);
37
56
  return rv;
38
57
  }
39
58
 
59
+ /**
60
+ * The cubic's coefficients come out of catastrophic cancellation when the
61
+ * spline is tangent to a barrier (tests/2646.dot), so clang's contraction
62
+ * here decides solve3's root count; mirror it exactly.
63
+ * @see lib/pathplan/route.c:points2coeff
64
+ */
40
65
  static points2coeff(v0: number, v1: number, v2: number, v3: number, c: number[]): void {
41
- c[3] = v3 + 3*v1 - (v0 + 3*v2);
42
- c[2] = 3*v0 + 3*v2 - 6*v1;
66
+ // clang -ffp-contract=on: fmadd x2 at route.c:397 (`v3 + 3*v1`, `v0 + 3*v2`; outer sub rounded)
67
+ c[3] = fma(3, v1, v3) - fma(3, v2, v0);
68
+ // clang -ffp-contract=on: fmadd at route.c:398 (`3*v0` fused onto rounded `3*v2`, then `- 6*v1` fused)
69
+ c[2] = fma(-6, v1, fma(3, v0, 3*v2));
43
70
  c[1] = 3*(v1 - v0);
44
71
  c[0] = v0;
45
72
  }
@@ -79,7 +106,8 @@ class RouteHelper {
79
106
  const tv = xr[i];
80
107
  if (tv >= 0 && tv <= 1) {
81
108
  RouteHelper.points2coeff(sps[0].y, sps[1].y, sps[2].y, sps[3].y, sc);
82
- const sv = (sc[0] + tv*(sc[1] + tv*(sc[2] + tv*sc[3])) - lc.yc0) / lc.yc1;
109
+ // clang -ffp-contract=on: fmadd x3 at route.c:360-361 (Horner `sv`; `(sv - ycoeff[0]) / ycoeff[1]` plain)
110
+ const sv = (fma(tv, fma(tv, fma(tv, sc[3], sc[2]), sc[1]), sc[0]) - lc.yc0) / lc.yc1;
83
111
  if (sv >= 0 && sv <= 1) RouteHelper.addRoot(tv, acc);
84
112
  }
85
113
  }
@@ -89,23 +117,27 @@ class RouteHelper {
89
117
  static silGeneral(sps: Point[], lc: LineCoeff, acc: RootAccum): number {
90
118
  const sc = [0,0,0,0], xr = [0,0,0];
91
119
  const rat = lc.yc1 / lc.xc1;
120
+ // clang -ffp-contract=on: fmsub x4 at route.c:371-372 (`sps[k].y - rat*sps[k].x`)
92
121
  RouteHelper.points2coeff(
93
- sps[0].y - rat*sps[0].x, sps[1].y - rat*sps[1].x,
94
- sps[2].y - rat*sps[2].x, sps[3].y - rat*sps[3].x, sc);
95
- sc[0] += rat*lc.xc0 - lc.yc0;
122
+ fms(rat, sps[0].x, sps[0].y), fms(rat, sps[1].x, sps[1].y),
123
+ fms(rat, sps[2].x, sps[2].y), fms(rat, sps[3].x, sps[3].y), sc);
124
+ // clang -ffp-contract=on: fnmsub at route.c:374 (`rat*xcoeff[0] - ycoeff[0]`; `+=` rounded)
125
+ sc[0] += fma(rat, lc.xc0, -lc.yc0);
96
126
  const xn = solve3(sc, xr);
97
127
  if (xn === 4) return 4;
98
128
  for (let i = 0; i < xn; i++) {
99
129
  const tv = xr[i];
100
130
  if (tv >= 0 && tv <= 1) {
101
131
  RouteHelper.points2coeff(sps[0].x, sps[1].x, sps[2].x, sps[3].x, sc);
102
- const sv = (sc[0] + tv*(sc[1] + tv*(sc[2] + tv*sc[3])) - lc.xc0) / lc.xc1;
132
+ // clang -ffp-contract=on: fmadd x3 at route.c:383-384 (Horner `sv`; `(sv - xcoeff[0]) / xcoeff[1]` plain)
133
+ const sv = (fma(tv, fma(tv, fma(tv, sc[3], sc[2]), sc[1]), sc[0]) - lc.xc0) / lc.xc1;
103
134
  if (sv >= 0 && sv <= 1) RouteHelper.addRoot(tv, acc);
104
135
  }
105
136
  }
106
137
  return acc.n;
107
138
  }
108
139
 
140
+ /** @see lib/pathplan/route.c:splineintersectsline */
109
141
  static splineIntersectsLine(sps: Point[], lps: Point[], roots: number[]): number {
110
142
  const lc: LineCoeff = {
111
143
  xc0: lps[0].x, xc1: lps[1].x - lps[0].x,
@@ -156,8 +188,13 @@ class RouteHelper {
156
188
  x0 += RouteHelper.ptDot(tnas[i].a[0], tmp);
157
189
  x1 += RouteHelper.ptDot(tnas[i].a[1], tmp);
158
190
  }
159
- const det01 = c00*c11 - c01*c01;
160
- const detX1 = x0*c11 - x1*c01, det0X = c00*x1 - c01*x0;
191
+ // C's c[1][0] is a copy of c[0][1] (route.c:174), so c10 == c01 here.
192
+ // clang -ffp-contract=on: fnmul+fmadd at route.c:181 (c00*c11 fused, c10*c01 rounded)
193
+ const det01 = fma(c00, c11, -(c01*c01));
194
+ // clang -ffp-contract=on: fnmul+fmadd at route.c:182 (c00*x1 fused, c01*x0 rounded)
195
+ const det0X = fma(c00, x1, -(c01*x0));
196
+ // clang -ffp-contract=on: fnmul+fmadd at route.c:183 (x0*c11 fused, x1*c01 rounded)
197
+ const detX1 = fma(x0, c11, -(x1*c01));
161
198
  if (Math.abs(det01) >= 1e-6) { scale0 = detX1/det01; scale3 = det0X/det01; }
162
199
  if (Math.abs(det01) < 1e-6 || scale0 <= 0 || scale3 <= 0) {
163
200
  const d = RouteHelper.ptDist(inps[0], inps[n-1]) / 3;
@@ -179,6 +216,27 @@ class RouteHelper {
179
216
  return tnas;
180
217
  }
181
218
 
219
+ /**
220
+ * The max-deviation split of `reallyroutespline` (route.c:130-143): the
221
+ * interior polyline point farthest from the fitted bezier.
222
+ *
223
+ * Faithful contract. On a geometrically symmetric channel two interior
224
+ * deviations tie to within an ulp, so the split point (and the whole
225
+ * spline) is decided by C's exact arithmetic, which this mirrors bit for
226
+ * bit against the native arm64 oracle:
227
+ * - `dist` is libm `hypot` (route.c:448) -> `libmHypot` via `ptDist`;
228
+ * - the test is C's strict `(d = dist(p, inps[i])) > maxd` (route.c:139),
229
+ * so an exact tie keeps the first index;
230
+ * - clang's default `-ffp-contract=on` fuses the bezier eval at
231
+ * route.c:137-138 as `fma(B3,p2, fma(B2,cp2, fma(B0,p1, B1*cp1)))`; the
232
+ * B0..B3 weights and cp1/cp2 (route.c:130-131) are not fused. Fusing the
233
+ * other product first is NOT equivalent (flips pt-issue-23-w9 and 32).
234
+ * C is therefore not translation-equivariant on such ties (the winner
235
+ * depends on absolute-coordinate rounding), and neither is this port.
236
+ * This supersedes the former tolerant tie-break (accepted divergence A3).
237
+ * Contraction map: .agent-notes/pathplan-fma-contraction-map.md.
238
+ * @see lib/pathplan/route.c:reallyroutespline
239
+ */
182
240
  static findMaxDev(inps: Point[], tnas: Tna[], ends: SplineEndpoints): number {
183
241
  const { p0, v0, p1, v1 } = ends;
184
242
  const cp1 = RouteHelper.ptAdd(p0, RouteHelper.ptScale(v0, 1/3));
@@ -186,27 +244,14 @@ class RouteHelper {
186
244
  let maxi = -1, maxd = -1;
187
245
  for (let i = 1; i < inps.length - 1; i++) {
188
246
  const t = tnas[i].t;
189
- const px = RouteHelper.B0(t)*p0.x + RouteHelper.B1(t)*cp1.x + RouteHelper.B2(t)*cp2.x + RouteHelper.B3(t)*p1.x;
190
- const py = RouteHelper.B0(t)*p0.y + RouteHelper.B1(t)*cp1.y + RouteHelper.B2(t)*cp2.y + RouteHelper.B3(t)*p1.y;
247
+ const b0 = RouteHelper.B0(t), b1 = RouteHelper.B1(t);
248
+ const b2 = RouteHelper.B2(t), b3 = RouteHelper.B3(t);
249
+ // clang -ffp-contract=on: fmadd at route.c:137 (x3; B1*cp1.x rounded first)
250
+ const px = fma(b3, p1.x, fma(b2, cp2.x, fma(b0, p0.x, b1*cp1.x)));
251
+ // clang -ffp-contract=on: fmadd at route.c:138 (x3; B1*cp1.y rounded first)
252
+ const py = fma(b3, p1.y, fma(b2, cp2.y, fma(b0, p0.y, b1*cp1.y)));
191
253
  const d = RouteHelper.ptDist({ x: px, y: py }, inps[i]);
192
- // C `reallyroutespline` uses strict `if (d > maxd)`. On a geometrically
193
- // SYMMETRIC channel the two interior deviations are an exact tie, and the
194
- // winner is then decided by ~1e-14 cancellation noise in the
195
- // absolute-coordinate bezier eval above — whose sign depends on absolute
196
- // position. So C is NOT consistent: it splits two translation-congruent
197
- // arcs toward opposite corners (e.g. 2368 `376->76` mirrors `256->436`),
198
- // because Apple's macOS libm `hypot` (which generated the oracle) is a
199
- // proprietary ~1-ULP-divergent implementation that no portable hypot
200
- // reproduces. We deliberately keep the tolerant tie-break: a true tie
201
- // always resolves to the first index, so the port is TRANSLATION-
202
- // EQUIVARIANT (same channel anywhere -> same spline). This matches C where
203
- // C's noise also keeps-first (#241_0 5:ne->8:nw, and the cnt=3 oracle
204
- // case) and consciously diverges where C's noise keeps-second (2368
205
- // `376->76`, the lone surviving residual: structural-match, maxΔ ~10pt on
206
- // one flat-edge arc). See .agent-notes/2368-residual-flat-label-ranksep.md
207
- // and the pow precedent (src/common/arm-pow.ts) for the same Apple-libm
208
- // boundary. @see lib/pathplan/route.c:reallyroutespline (max-deviation split)
209
- if (d > maxd * (1 + 1e-10) + 1e-10) { maxd = d; maxi = i; }
254
+ if (d > maxd) { maxd = d; maxi = i; }
210
255
  }
211
256
  return maxi;
212
257
  }
@@ -1,65 +1,93 @@
1
1
  // SPDX-License-Identifier: EPL-2.0
2
- /** @see lib/pathplan/solvers.c */
2
+ /**
3
+ * @see lib/pathplan/solvers.c
4
+ *
5
+ * The native oracle's libpathplan is compiled by clang with the default
6
+ * `-ffp-contract=on`, which fuses five multiply-adds in this file into single
7
+ * fmadd/fmsub/fnmsub instructions (solve2 is inlined into solve3). Those sites
8
+ * are mirrored with the software `fma`; every other operation is plain IEEE
9
+ * double, in C's evaluation order. Contraction map:
10
+ * .agent-notes/pathplan-fma-contraction-map.md.
11
+ */
3
12
 
13
+ import { fma } from '../common/fma.js';
14
+
15
+ /** @see lib/pathplan/solvers.c:EPS */
4
16
  const EPS = 1e-7;
5
17
 
6
- class Solver {
7
- static aeq0(x: number): boolean {
8
- return x < EPS && x > -EPS;
9
- }
18
+ /** @see lib/pathplan/solvers.c:AEQ0 */
19
+ function aeq0(x: number): boolean {
20
+ return x < EPS && x > -EPS;
21
+ }
10
22
 
11
- static solve1(c: number[], r: number[]): number {
12
- if (Solver.aeq0(c[1])) return Solver.aeq0(c[0]) ? 4 : 0;
13
- r[0] = -c[0] / c[1];
14
- return 1;
15
- }
23
+ /**
24
+ * Solve cubic coeff[3]*x^3 + coeff[2]*x^2 + coeff[1]*x + coeff[0] = 0.
25
+ * Returns the root count; 4 means every x satisfies.
26
+ * @see lib/pathplan/solvers.c:solve3
27
+ */
28
+ export function solve3(coeff: number[], roots: number[]): number {
29
+ const a = coeff[3], b = coeff[2], c = coeff[1], d = coeff[0];
30
+ if (aeq0(a)) return solve2(coeff, roots);
31
+ const bOver3a = b / (3 * a), cOverA = c / a, dOverA = d / a;
16
32
 
17
- static solve2(c: number[], r: number[]): number {
18
- if (Solver.aeq0(c[2])) return Solver.solve1(c, r);
19
- const b2a = c[1] / (2 * c[2]);
20
- const disc = b2a * b2a - c[0] / c[2];
21
- if (disc < 0) return 0;
22
- if (disc > 0) {
23
- r[0] = -b2a + Math.sqrt(disc);
24
- r[1] = -2 * b2a - r[0];
25
- return 2;
26
- }
27
- r[0] = -b2a;
28
- return 1;
29
- }
33
+ let p = bOver3a * bOver3a;
34
+ // clang -ffp-contract=on: fnmul+fmadd at solvers.c:41 (`2*b_over_3a*p` fused,
35
+ // `b_over_3a*c_over_a` rounded; `+ d_over_a` is a separate rounded add)
36
+ const q = dOverA + fma(2 * bOver3a, p, -(bOver3a * cOverA));
37
+ p = cOverA / 3 - p;
38
+ // clang -ffp-contract=on: fmadd at solvers.c:43 (q*q fused, 4*p*p*p rounded)
39
+ const disc = fma(q, q, 4 * p * p * p);
30
40
 
31
- static solve3neg(disc: number, q: number, b3a: number, r: number[]): number {
32
- const rr = 0.5 * Math.sqrt(-disc + q * q);
41
+ let rootn: number;
42
+ if (disc < 0) {
43
+ // clang -ffp-contract=on: fnmsub at solvers.c:46 (`-disc + q*q`)
44
+ const r = 0.5 * Math.sqrt(fma(q, q, -disc));
33
45
  const theta = Math.atan2(Math.sqrt(-disc), -q);
34
- const temp = 2 * Math.cbrt(rr);
35
- r[0] = temp * Math.cos(theta / 3) - b3a;
36
- r[1] = temp * Math.cos((theta + 2 * Math.PI) / 3) - b3a;
37
- r[2] = temp * Math.cos((theta - 2 * Math.PI) / 3) - b3a;
38
- return 3;
46
+ const temp = 2 * Math.cbrt(r);
47
+ roots[0] = temp * Math.cos(theta / 3);
48
+ roots[1] = temp * Math.cos((theta + Math.PI + Math.PI) / 3);
49
+ roots[2] = temp * Math.cos((theta - Math.PI - Math.PI) / 3);
50
+ rootn = 3;
51
+ } else {
52
+ const alpha = 0.5 * (Math.sqrt(disc) - q);
53
+ const beta = -q - alpha;
54
+ roots[0] = Math.cbrt(alpha) + Math.cbrt(beta);
55
+ if (disc > 0) rootn = 1;
56
+ else { roots[1] = roots[2] = -0.5 * roots[0]; rootn = 3; }
39
57
  }
40
58
 
41
- static solve3pos(disc: number, q: number, b3a: number, r: number[]): number {
42
- const al = 0.5 * (Math.sqrt(disc) - q);
43
- r[0] = Math.cbrt(al) + Math.cbrt(-q - al) - b3a;
44
- if (disc > 0) return 1;
45
- r[1] = r[2] = -0.5 * (r[0] + b3a) - b3a;
46
- return 3;
47
- }
59
+ for (let i = 0; i < rootn; i++) roots[i] -= bOver3a;
60
+ return rootn;
61
+ }
62
+
63
+ /** @see lib/pathplan/solvers.c:solve2 */
64
+ function solve2(coeff: number[], roots: number[]): number {
65
+ const a = coeff[2], b = coeff[1], c = coeff[0];
66
+ if (aeq0(a)) return solve1(coeff, roots);
67
+ const bOver2a = b / (2 * a);
68
+ const cOverA = c / a;
48
69
 
49
- static solve3(c: number[], r: number[]): number {
50
- const a = c[3], b = c[2], cv = c[1], d = c[0];
51
- if (Solver.aeq0(a)) return Solver.solve2(c, r);
52
- const b3a = b / (3 * a), p0 = b3a * b3a;
53
- const q = 2 * b3a * p0 - b3a * cv / a + d / a;
54
- const p = cv / (3 * a) - p0;
55
- const disc = q * q + 4 * p * p * p;
56
- return disc < 0 ? Solver.solve3neg(disc, q, b3a, r)
57
- : Solver.solve3pos(disc, q, b3a, r);
70
+ // clang -ffp-contract=on: fmadd at solvers.c:80 (`b_over_2a*b_over_2a - c_over_a`)
71
+ const disc = fma(bOver2a, bOver2a, -cOverA);
72
+ if (disc < 0) {
73
+ return 0;
74
+ } else if (disc > 0) {
75
+ roots[0] = -bOver2a + Math.sqrt(disc);
76
+ // clang -ffp-contract=on: fnmsub at solvers.c:85 (`-2*b_over_2a - roots[0]`)
77
+ roots[1] = fma(-2, bOver2a, -roots[0]);
78
+ return 2;
58
79
  }
80
+ roots[0] = -bOver2a;
81
+ return 1;
59
82
  }
60
83
 
61
- /** Solve cubic c[3]*x^3 + c[2]*x^2 + c[1]*x + c[0] = 0.
62
- * Returns root count; 4 means all x satisfy. @see lib/pathplan/solvers.c:solve3 */
63
- export function solve3(coeff: number[], roots: number[]): number {
64
- return Solver.solve3(coeff, roots);
84
+ /** @see lib/pathplan/solvers.c:solve1 (no contraction) */
85
+ function solve1(coeff: number[], roots: number[]): number {
86
+ const a = coeff[1], b = coeff[0];
87
+ if (aeq0(a)) {
88
+ if (aeq0(b)) return 4;
89
+ return 0;
90
+ }
91
+ roots[0] = -b / a;
92
+ return 1;
65
93
  }