gnss-js 1.23.0 → 1.24.0

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.
@@ -23,6 +23,8 @@ __export(positioning_exports, {
23
23
  RtkFloatEngine: () => RtkFloatEngine,
24
24
  ionoFree: () => ionoFree,
25
25
  klobucharDelay: () => klobucharDelay,
26
+ lambdaReduction: () => lambdaReduction,
27
+ lambdaSearch: () => lambdaSearch,
26
28
  satClockCorrection: () => satClockCorrection,
27
29
  solveDgnss: () => solveDgnss,
28
30
  solveSpp: () => solveSpp,
@@ -462,6 +464,207 @@ function klobucharDelay(coeffs, latRad, lonRad, azRad, elRad, gpsTowSec) {
462
464
  return F * (5e-9 + amp * (1 - x2 / 2 + x2 * x2 / 24));
463
465
  }
464
466
 
467
+ // src/positioning/lambda.ts
468
+ var LOOPMAX = 1e4;
469
+ var ROUND = (x) => Math.floor(x + 0.5);
470
+ var SGN = (x) => x <= 0 ? -1 : 1;
471
+ function ldFactor(n, Q, L, D) {
472
+ const A = Float64Array.from(Q);
473
+ for (let i = n - 1; i >= 0; i--) {
474
+ D[i] = A[i + i * n];
475
+ if (D[i] <= 0) return false;
476
+ const a = Math.sqrt(D[i]);
477
+ for (let j = 0; j <= i; j++) L[i + j * n] = A[i + j * n] / a;
478
+ for (let j = 0; j <= i - 1; j++)
479
+ for (let k = 0; k <= j; k++) A[j + k * n] -= L[i + k * n] * L[i + j * n];
480
+ for (let j = 0; j <= i; j++) L[i + j * n] /= L[i + i * n];
481
+ }
482
+ return true;
483
+ }
484
+ function gauss(n, L, Z, i, j) {
485
+ const mu = ROUND(L[i + j * n]);
486
+ if (mu !== 0) {
487
+ for (let k = i; k < n; k++) L[k + n * j] -= mu * L[k + i * n];
488
+ for (let k = 0; k < n; k++) Z[k + n * j] -= mu * Z[k + i * n];
489
+ }
490
+ }
491
+ function perm(n, L, D, j, del, Z) {
492
+ const eta = D[j] / del;
493
+ const lam = D[j + 1] * L[j + 1 + j * n] / del;
494
+ D[j] = eta * D[j + 1];
495
+ D[j + 1] = del;
496
+ for (let k = 0; k <= j - 1; k++) {
497
+ const a0 = L[j + k * n];
498
+ const a1 = L[j + 1 + k * n];
499
+ L[j + k * n] = -L[j + 1 + j * n] * a0 + a1;
500
+ L[j + 1 + k * n] = eta * a0 + lam * a1;
501
+ }
502
+ L[j + 1 + j * n] = lam;
503
+ for (let k = j + 2; k < n; k++) {
504
+ const t = L[k + j * n];
505
+ L[k + j * n] = L[k + (j + 1) * n];
506
+ L[k + (j + 1) * n] = t;
507
+ }
508
+ for (let k = 0; k < n; k++) {
509
+ const t = Z[k + j * n];
510
+ Z[k + j * n] = Z[k + (j + 1) * n];
511
+ Z[k + (j + 1) * n] = t;
512
+ }
513
+ }
514
+ function reduction(n, L, D, Z) {
515
+ let j = n - 2;
516
+ let k = n - 2;
517
+ while (j >= 0) {
518
+ if (j <= k) for (let i = j + 1; i < n; i++) gauss(n, L, Z, i, j);
519
+ const del = D[j] + L[j + 1 + j * n] * L[j + 1 + j * n] * D[j + 1];
520
+ if (del + 1e-6 < D[j + 1]) {
521
+ perm(n, L, D, j, del, Z);
522
+ k = j;
523
+ j = n - 2;
524
+ } else j--;
525
+ }
526
+ }
527
+ function search(n, m, L, D, zs, zn, s) {
528
+ let nn = 0;
529
+ let imax = 0;
530
+ let maxdist = 1e99;
531
+ const S = new Float64Array(n * n);
532
+ const dist = new Float64Array(n);
533
+ const zb = new Float64Array(n);
534
+ const z = new Float64Array(n);
535
+ const step = new Float64Array(n);
536
+ let k = n - 1;
537
+ dist[k] = 0;
538
+ zb[k] = zs[k];
539
+ z[k] = ROUND(zb[k]);
540
+ let y = zb[k] - z[k];
541
+ step[k] = SGN(y);
542
+ let c = 0;
543
+ for (; c < LOOPMAX; c++) {
544
+ const newdist = dist[k] + y * y / D[k];
545
+ if (newdist < maxdist) {
546
+ if (k !== 0) {
547
+ dist[--k] = newdist;
548
+ for (let i = 0; i <= k; i++)
549
+ S[k + i * n] = S[k + 1 + i * n] + (z[k + 1] - zb[k + 1]) * L[k + 1 + i * n];
550
+ zb[k] = zs[k] + S[k + k * n];
551
+ z[k] = ROUND(zb[k]);
552
+ y = zb[k] - z[k];
553
+ step[k] = SGN(y);
554
+ } else {
555
+ if (nn < m) {
556
+ if (nn === 0 || newdist > s[imax]) imax = nn;
557
+ for (let i = 0; i < n; i++) zn[i + nn * n] = z[i];
558
+ s[nn++] = newdist;
559
+ } else {
560
+ if (newdist < s[imax]) {
561
+ for (let i = 0; i < n; i++) zn[i + imax * n] = z[i];
562
+ s[imax] = newdist;
563
+ imax = 0;
564
+ for (let i = 0; i < m; i++) if (s[imax] < s[i]) imax = i;
565
+ }
566
+ maxdist = s[imax];
567
+ }
568
+ z[0] += step[0];
569
+ y = zb[0] - z[0];
570
+ step[0] = -step[0] - SGN(step[0]);
571
+ }
572
+ } else {
573
+ if (k === n - 1) break;
574
+ k++;
575
+ z[k] += step[k];
576
+ y = zb[k] - z[k];
577
+ step[k] = -step[k] - SGN(step[k]);
578
+ }
579
+ }
580
+ for (let i = 0; i < m - 1; i++) {
581
+ for (let j = i + 1; j < m; j++) {
582
+ if (s[i] < s[j]) continue;
583
+ const t = s[i];
584
+ s[i] = s[j];
585
+ s[j] = t;
586
+ for (let l = 0; l < n; l++) {
587
+ const u = zn[l + i * n];
588
+ zn[l + i * n] = zn[l + j * n];
589
+ zn[l + j * n] = u;
590
+ }
591
+ }
592
+ }
593
+ return c < LOOPMAX;
594
+ }
595
+ function solveZt(n, m, Z, E) {
596
+ const w = n + m;
597
+ const M = new Float64Array(n * w);
598
+ for (let r = 0; r < n; r++) {
599
+ for (let cIdx = 0; cIdx < n; cIdx++) M[r * w + cIdx] = Z[cIdx + r * n];
600
+ for (let cIdx = 0; cIdx < m; cIdx++) M[r * w + n + cIdx] = E[r + cIdx * n];
601
+ }
602
+ for (let col = 0; col < n; col++) {
603
+ let piv = col;
604
+ for (let r = col + 1; r < n; r++)
605
+ if (Math.abs(M[r * w + col]) > Math.abs(M[piv * w + col])) piv = r;
606
+ if (Math.abs(M[piv * w + col]) < 1e-12) return null;
607
+ if (piv !== col) {
608
+ for (let cIdx = col; cIdx < w; cIdx++) {
609
+ const t = M[col * w + cIdx];
610
+ M[col * w + cIdx] = M[piv * w + cIdx];
611
+ M[piv * w + cIdx] = t;
612
+ }
613
+ }
614
+ const d = M[col * w + col];
615
+ for (let cIdx = col; cIdx < w; cIdx++) M[col * w + cIdx] /= d;
616
+ for (let r = 0; r < n; r++) {
617
+ if (r === col) continue;
618
+ const f = M[r * w + col];
619
+ if (f === 0) continue;
620
+ for (let cIdx = col; cIdx < w; cIdx++)
621
+ M[r * w + cIdx] -= f * M[col * w + cIdx];
622
+ }
623
+ }
624
+ const X = new Float64Array(n * m);
625
+ for (let r = 0; r < n; r++)
626
+ for (let cIdx = 0; cIdx < m; cIdx++) X[r + cIdx * n] = M[r * w + n + cIdx];
627
+ return X;
628
+ }
629
+ function lambdaSearch(a, Q, n, m = 2) {
630
+ if (n <= 0 || m <= 0 || a.length < n || Q.length < n * n) return null;
631
+ const L = new Float64Array(n * n);
632
+ const D = new Float64Array(n);
633
+ if (!ldFactor(n, Q, L, D)) return null;
634
+ const Z = new Float64Array(n * n);
635
+ for (let i = 0; i < n; i++) Z[i + i * n] = 1;
636
+ reduction(n, L, D, Z);
637
+ const z = new Float64Array(n);
638
+ for (let j = 0; j < n; j++) {
639
+ let sum = 0;
640
+ for (let k = 0; k < n; k++) sum += Z[k + j * n] * a[k];
641
+ z[j] = sum;
642
+ }
643
+ const E = new Float64Array(n * m);
644
+ const s = new Float64Array(m);
645
+ if (!search(n, m, L, D, z, E, s)) return null;
646
+ const F = solveZt(n, m, Z, E);
647
+ if (!F) return null;
648
+ const candidates = [];
649
+ for (let c = 0; c < m; c++) {
650
+ const v = new Float64Array(n);
651
+ for (let i = 0; i < n; i++) v[i] = ROUND(F[i + c * n]);
652
+ candidates.push(v);
653
+ }
654
+ const ratio = m >= 2 && s[0] > 0 ? s[1] / s[0] : Infinity;
655
+ return { candidates, residuals: s, ratio };
656
+ }
657
+ function lambdaReduction(Q, n) {
658
+ if (n <= 0 || Q.length < n * n) return null;
659
+ const L = new Float64Array(n * n);
660
+ const D = new Float64Array(n);
661
+ if (!ldFactor(n, Q, L, D)) return null;
662
+ const Z = new Float64Array(n * n);
663
+ for (let i = 0; i < n; i++) Z[i + i * n] = 1;
664
+ reduction(n, L, D, Z);
665
+ return Z;
666
+ }
667
+
465
668
  // src/positioning/rtk.ts
466
669
  var L1_CODES = {
467
670
  G: ["1C"],
@@ -822,7 +1025,13 @@ var RtkFloatEngine = class {
822
1025
  maxGapMs: opts.maxGapMs ?? 1e4,
823
1026
  codeGateM: opts.codeGateM ?? 30,
824
1027
  slipGateM: opts.slipGateM ?? 5,
825
- updateIterations: opts.updateIterations ?? 2
1028
+ updateIterations: opts.updateIterations ?? 2,
1029
+ ambiguityResolution: opts.ambiguityResolution ?? "off",
1030
+ ratioThreshold: opts.ratioThreshold ?? 3,
1031
+ arElevationMaskDeg: opts.arElevationMaskDeg ?? 0,
1032
+ glonassAr: opts.glonassAr ?? false,
1033
+ partialFixing: opts.partialFixing ?? true,
1034
+ minFixAmbiguities: opts.minFixAmbiguities ?? 4
826
1035
  };
827
1036
  }
828
1037
  /** Clear all filter state (position, ambiguities, lock history). */
@@ -880,6 +1089,56 @@ var RtkFloatEngine = class {
880
1089
  const entry = this.amb[newRefIdx];
881
1090
  entry.prn = oldRef;
882
1091
  }
1092
+ /**
1093
+ * Try to fix the ambiguity subset `idxs` (indices into `this.amb`):
1094
+ * extract the float subvector b and covariance Q_b from the filter,
1095
+ * run MLAMBDA and, without touching the filter state, condition the
1096
+ * position on the best integer candidate,
1097
+ * xa = x − P_xb·Q_b⁻¹·(b − ǎ)
1098
+ * (RTKLIB rtkpos.c `resamb_LAMBDA`). Returns null when Q_b is not
1099
+ * positive definite / invertible or the search fails; ratio
1100
+ * validation is the caller's job.
1101
+ */
1102
+ tryFix(idxs) {
1103
+ const k = idxs.length;
1104
+ const b = new Float64Array(k);
1105
+ const Qb = new Float64Array(k * k);
1106
+ const QbRows = [];
1107
+ for (let i = 0; i < k; i++) {
1108
+ const si = 3 + idxs[i];
1109
+ b[i] = this.x[si];
1110
+ const row = [];
1111
+ for (let j = 0; j < k; j++) {
1112
+ const sj = 3 + idxs[j];
1113
+ const q = (this.P[si][sj] + this.P[sj][si]) / 2;
1114
+ Qb[i + j * k] = q;
1115
+ row.push(q);
1116
+ }
1117
+ QbRows.push(row);
1118
+ }
1119
+ const res = lambdaSearch(b, Qb, k, 2);
1120
+ if (!res) return null;
1121
+ const best = res.candidates[0];
1122
+ const QbInv = matInv(QbRows);
1123
+ if (!QbInv) return null;
1124
+ const db = [];
1125
+ for (let i = 0; i < k; i++) db.push(b[i] - best[i]);
1126
+ const position = [
1127
+ this.x[0],
1128
+ this.x[1],
1129
+ this.x[2]
1130
+ ];
1131
+ for (let r = 0; r < 3; r++) {
1132
+ let dx = 0;
1133
+ for (let i = 0; i < k; i++) {
1134
+ let yi = 0;
1135
+ for (let j = 0; j < k; j++) yi += QbInv[i][j] * db[j];
1136
+ dx += this.P[r][3 + idxs[i]] * yi;
1137
+ }
1138
+ position[r] -= dx;
1139
+ }
1140
+ return { ratio: res.ratio, position, intAmb: [...best] };
1141
+ }
883
1142
  /**
884
1143
  * Process one synchronized epoch (same nominal `timeMs` for both
885
1144
  * receivers). Returns null until a first position can be
@@ -1130,6 +1389,47 @@ var RtkFloatEngine = class {
1130
1389
  Pnew[j][i] = s;
1131
1390
  }
1132
1391
  this.P = Pnew;
1392
+ let status = rows.some(
1393
+ (r) => r.kind === "phase"
1394
+ ) ? "float" : "dgnss";
1395
+ let ratio;
1396
+ let nFixed;
1397
+ let fixedPos = null;
1398
+ let fixedAmbiguities;
1399
+ if (o.ambiguityResolution === "instant") {
1400
+ const cands = [];
1401
+ for (const row of rows) {
1402
+ if (row.kind !== "phase") continue;
1403
+ if (row.g.prn[0] === "R" && !o.glonassAr) continue;
1404
+ if (row.g.elB < o.arElevationMaskDeg * Math.PI / 180) continue;
1405
+ cands.push({ idx: row.ambIdx, el: row.g.elB });
1406
+ }
1407
+ cands.sort((a, b) => b.el - a.el);
1408
+ const floor = Math.max(
1409
+ o.minFixAmbiguities,
1410
+ Math.ceil(cands.length / 2),
1411
+ 1
1412
+ );
1413
+ while (cands.length >= floor) {
1414
+ const fix = this.tryFix(cands.map((c) => c.idx));
1415
+ if (!fix) break;
1416
+ const capped = Math.min(fix.ratio, 999.9);
1417
+ ratio ??= capped;
1418
+ if (fix.ratio >= o.ratioThreshold) {
1419
+ ratio = capped;
1420
+ status = "fixed";
1421
+ nFixed = cands.length;
1422
+ fixedPos = fix.position;
1423
+ fixedAmbiguities = {};
1424
+ cands.forEach((c, i) => {
1425
+ fixedAmbiguities[this.amb[c.idx].prn] = fix.intAmb[i];
1426
+ });
1427
+ break;
1428
+ }
1429
+ if (!o.partialFixing) break;
1430
+ cands.pop();
1431
+ }
1432
+ }
1133
1433
  for (const g of geom) {
1134
1434
  if (g.cpR === null || g.cpB === null) continue;
1135
1435
  this.track.set(g.prn, { lockR: g.lockR, lockB: g.lockB, lastMs: timeMs });
@@ -1143,14 +1443,22 @@ var RtkFloatEngine = class {
1143
1443
  const nSats = new Set(rows.flatMap((r) => [r.g.prn, r.ref.prn])).size;
1144
1444
  return {
1145
1445
  timeMs,
1146
- position: [this.x[0], this.x[1], this.x[2]],
1446
+ position: fixedPos ?? [this.x[0], this.x[1], this.x[2]],
1147
1447
  floatBaseline: [
1148
1448
  this.x[0] - this.basePos[0],
1149
1449
  this.x[1] - this.basePos[1],
1150
1450
  this.x[2] - this.basePos[2]
1151
1451
  ],
1452
+ status,
1152
1453
  nSats,
1153
- ratio: void 0,
1454
+ ratio,
1455
+ nFixed,
1456
+ fixedBaseline: fixedPos ? [
1457
+ fixedPos[0] - this.basePos[0],
1458
+ fixedPos[1] - this.basePos[1],
1459
+ fixedPos[2] - this.basePos[2]
1460
+ ] : void 0,
1461
+ fixedAmbiguities,
1154
1462
  sigmas: [
1155
1463
  Math.sqrt(Math.max(this.P[0][0], 0)),
1156
1464
  Math.sqrt(Math.max(this.P[1][1], 0)),
@@ -1378,6 +1686,8 @@ function solveSpp(pseudoranges, ephemerides, timeMs, opts = {}) {
1378
1686
  RtkFloatEngine,
1379
1687
  ionoFree,
1380
1688
  klobucharDelay,
1689
+ lambdaReduction,
1690
+ lambdaSearch,
1381
1691
  satClockCorrection,
1382
1692
  solveDgnss,
1383
1693
  solveSpp,
@@ -54,7 +54,29 @@ declare function klobucharDelay(coeffs: KlobucharCoeffs, latRad: number, lonRad:
54
54
  * (LAMBDA) needs the joint float ambiguity covariance — which the
55
55
  * EKF maintains natively. This mirrors RTKLIB's float-filter
56
56
  * structure (rtkpos.c), which the implementation was validated
57
- * against; no integer fixing is attempted (`ratio` is reserved).
57
+ * against.
58
+ *
59
+ * Stage 2 — integer ambiguity resolution (opt-in via
60
+ * `ambiguityResolution: 'instant'`): after each float update the
61
+ * engine extracts the DD ambiguity subvector and its joint covariance
62
+ * from the filter, runs MLAMBDA (`lambdaSearch`, a port of RTKLIB's
63
+ * lambda.c) and validates the best candidate with the ratio test
64
+ * (s₂/s₁ ≥ `ratioThreshold`, default 3.0 — RTKLIB's default). On
65
+ * acceptance the rover position is conditioned on the integer
66
+ * ambiguities (xa = x − P_xb·Q_b⁻¹·(b − ǎ), RTKLIB rtkpos.c
67
+ * `resamb_LAMBDA`) and reported as a FIXED solution; the filter state
68
+ * itself is *not* modified (instant, per-epoch fixing — RTKLIB's
69
+ * `armode=instantaneous`; continuous AR/fix-and-hold, which feed the
70
+ * fix back into the filter, are stage-3 continuity options). When the
71
+ * full set fails the ratio test, partial fixing retries after
72
+ * dropping the lowest-elevation ambiguity, one at a time, down to
73
+ * half the candidate set (and never below `minFixAmbiguities`) — an
74
+ * elevation-ordered subset walk in the spirit of RTKLIB demo5's
75
+ * satellite exclusion, floored so an unconverged float cannot buy a
76
+ * lucky ratio on a tiny subset. GLONASS ambiguities are excluded from
77
+ * fixing by
78
+ * default (DD IFB is only integer-safe across same-model receivers;
79
+ * enable with `glonassAr`).
58
80
  *
59
81
  * Differencing model: for satellites s and reference r observed by
60
82
  * rover R and base B, the DD pseudorange/phase cancels satellite
@@ -208,18 +230,65 @@ interface RtkFloatOptions {
208
230
  slipGateM?: number;
209
231
  /** Measurement-update relinearisations (IEKF). Default 2. */
210
232
  updateIterations?: number;
233
+ /**
234
+ * Integer ambiguity resolution: 'off' keeps the stage-1 float-only
235
+ * behaviour; 'instant' attempts a per-epoch LAMBDA fix after each
236
+ * float update (no state feedback — RTKLIB `armode=instantaneous`).
237
+ * Default 'off'.
238
+ */
239
+ ambiguityResolution?: 'off' | 'instant';
240
+ /** Ratio-test acceptance threshold (s₂/s₁). Default 3.0 (RTKLIB). */
241
+ ratioThreshold?: number;
242
+ /**
243
+ * Additional elevation mask (deg) for ambiguities entering the fix
244
+ * (RTKLIB `elmaskar`); 0 = use every measured ambiguity. Default 0.
245
+ */
246
+ arElevationMaskDeg?: number;
247
+ /**
248
+ * Include GLONASS FDMA ambiguities in the integer fix. Only sound
249
+ * across same-model receivers (differential IFB). Default false.
250
+ */
251
+ glonassAr?: boolean;
252
+ /**
253
+ * Partial fixing: when the full ambiguity set fails the ratio test,
254
+ * drop the lowest-elevation ambiguity and retry (elevation-ordered
255
+ * subset walk), never below half the candidates. Default true.
256
+ */
257
+ partialFixing?: boolean;
258
+ /** Minimum DD ambiguities to attempt/accept a fix. Default 4. */
259
+ minFixAmbiguities?: number;
211
260
  }
212
261
  interface RtkFloatSolution {
213
262
  /** Epoch, GPS-scale milliseconds. */
214
263
  timeMs: number;
215
- /** Rover position, ECEF metres. */
264
+ /**
265
+ * Rover position, ECEF metres — the ambiguity-fixed position when
266
+ * `status === 'fixed'`, the float filter position otherwise.
267
+ */
216
268
  position: [number, number, number];
217
- /** Float baseline rover − base, ECEF metres. */
269
+ /** Float baseline rover − base, ECEF metres (always the float). */
218
270
  floatBaseline: [number, number, number];
271
+ /**
272
+ * Solution quality: 'fixed' when LAMBDA passed the ratio test,
273
+ * 'float' when carrier ambiguities contributed but no (accepted)
274
+ * fix, 'dgnss' when the epoch was updated from code DDs only.
275
+ */
276
+ status: 'fixed' | 'float' | 'dgnss';
219
277
  /** Satellites contributing DD rows this epoch (incl. references). */
220
278
  nSats: number;
221
- /** Ambiguity-validation ratio — undefined at stage 1 (no LAMBDA). */
279
+ /**
280
+ * Ambiguity-validation ratio (s₂/s₁ of the LAMBDA candidates,
281
+ * capped at 999.9 as RTKLIB): the accepted subset's ratio when
282
+ * fixed, the full-set ratio otherwise. Undefined when no fix was
283
+ * attempted (`ambiguityResolution: 'off'` or no eligible set).
284
+ */
222
285
  ratio?: number;
286
+ /** Number of integer-fixed DD ambiguities (when `status==='fixed'`). */
287
+ nFixed?: number;
288
+ /** Fixed baseline rover − base, ECEF metres (when fixed). */
289
+ fixedBaseline?: [number, number, number];
290
+ /** Integer DD ambiguity (cycles) per fixed PRN (when fixed). */
291
+ fixedAmbiguities?: Record<string, number>;
223
292
  /** Formal position sigmas (filter covariance), ECEF metres. */
224
293
  sigmas: [number, number, number];
225
294
  /** Float DD ambiguity (cycles) per non-reference PRN. */
@@ -262,6 +331,17 @@ declare class RtkFloatEngine {
262
331
  * transform of state and covariance (P' = T P Tᵀ).
263
332
  */
264
333
  private retarget;
334
+ /**
335
+ * Try to fix the ambiguity subset `idxs` (indices into `this.amb`):
336
+ * extract the float subvector b and covariance Q_b from the filter,
337
+ * run MLAMBDA and, without touching the filter state, condition the
338
+ * position on the best integer candidate,
339
+ * xa = x − P_xb·Q_b⁻¹·(b − ǎ)
340
+ * (RTKLIB rtkpos.c `resamb_LAMBDA`). Returns null when Q_b is not
341
+ * positive definite / invertible or the search fails; ratio
342
+ * validation is the caller's job.
343
+ */
344
+ private tryFix;
265
345
  /**
266
346
  * Process one synchronized epoch (same nominal `timeMs` for both
267
347
  * receivers). Returns null until a first position can be
@@ -271,6 +351,88 @@ declare class RtkFloatEngine {
271
351
  process(rover: RtkEpochMeasurements, base: RtkEpochMeasurements, timeMs: number): RtkFloatSolution | null;
272
352
  }
273
353
 
354
+ /**
355
+ * Integer ambiguity resolution: LAMBDA decorrelation + MLAMBDA
356
+ * integer least-squares search.
357
+ *
358
+ * Port of RTKLIB's `lambda.c` (demo5 / rtklibexplorer fork,
359
+ * src/lambda.c, Copyright (c) 2007-2008 T. Takasu, BSD-2-Clause),
360
+ * cross-checked against the reference papers where the C comments are
361
+ * thin:
362
+ *
363
+ * - [1] P.J.G. Teunissen, "The least-squares ambiguity decorrelation
364
+ * adjustment: a method for fast GPS ambiguity estimation",
365
+ * J. Geodesy 70, 65-82, 1995.
366
+ * - [2] X.-W. Chang, X. Yang, T. Zhou, "MLAMBDA: A modified LAMBDA
367
+ * method for integer least-squares estimation", J. Geodesy 79,
368
+ * 552-565, 2005.
369
+ *
370
+ * Pipeline (identical to RTKLIB's `lambda()`):
371
+ * 1. LᵀDL factorization of the float covariance, Q = Lᵀ·diag(D)·L
372
+ * with L unit lower triangular (RTKLIB's `LD`).
373
+ * 2. LAMBDA reduction [1]: integer Gauss transformations interleaved
374
+ * with symmetric permutations until the transformed D is
375
+ * (approximately) descending — producing a unimodular Z with
376
+ * Qz = ZᵀQZ = Lᵀ·diag(D)·L far better conditioned than Q.
377
+ * 3. MLAMBDA search [2] in the decorrelated space: depth-first
378
+ * enumeration over a shrinking ellipsoid, keeping the `m` smallest
379
+ * residual quadratic forms (z−ž)ᵀQz⁻¹(z−ž).
380
+ * 4. Back-transform the candidates to the original space, a = Z⁻ᵀ·ž
381
+ * (RTKLIB solves ZᵀF = E; Z is unimodular so F is exactly integer).
382
+ *
383
+ * Deviations from lambda.c:
384
+ * - matrices are `Float64Array` in the same column-major layout, with
385
+ * allocation-failure paths dropped (JS throws instead),
386
+ * - failures (non-positive-definite Q, search loop overflow) return
387
+ * `null` instead of a status code + stderr message,
388
+ * - back-transformed candidates are rounded to the nearest integer:
389
+ * F = Z⁻ᵀ·ž is exactly integer in ℤ arithmetic and the rounding only
390
+ * removes the ~1e-12 fuzz of the floating-point triangular solve,
391
+ * - the ratio s₂/s₁ used by the RTKLIB caller (`rtkpos.c`) is returned
392
+ * alongside the candidates (∞ when s₁ = 0, i.e. noise-free input).
393
+ */
394
+ /** Result of an integer least-squares search. */
395
+ interface LambdaResult {
396
+ /**
397
+ * The `m` best integer candidate vectors (length n each), sorted by
398
+ * ascending residual quadratic form; `candidates[0]` is the integer
399
+ * least-squares minimizer.
400
+ */
401
+ candidates: Float64Array[];
402
+ /**
403
+ * Residual quadratic forms sᵢ = (a−ǎᵢ)ᵀQ⁻¹(a−ǎᵢ) matching
404
+ * `candidates`, ascending.
405
+ */
406
+ residuals: Float64Array;
407
+ /**
408
+ * Ratio-test statistic s₂/s₁ (second-best over best); `Infinity`
409
+ * when the best residual is exactly 0 (noise-free input). Callers
410
+ * typically cap it (RTKLIB uses 999.9) before thresholding.
411
+ */
412
+ ratio: number;
413
+ }
414
+ /**
415
+ * LAMBDA/MLAMBDA integer least-squares estimation (RTKLIB `lambda`):
416
+ * decorrelate the float ambiguities, search the `m` best integer
417
+ * vectors and back-transform them to the original space.
418
+ *
419
+ * @param a Float ambiguities (length ≥ n).
420
+ * @param Q Their covariance, n×n (symmetric — row/column-major agree).
421
+ * @param n Number of ambiguities.
422
+ * @param m Number of candidates to return (default 2: best +
423
+ * second-best, what the ratio test needs).
424
+ * @returns Candidates + residuals + ratio, or null when Q is not
425
+ * positive definite or the search does not terminate.
426
+ */
427
+ declare function lambdaSearch(a: Float64Array, Q: Float64Array, n: number, m?: number): LambdaResult | null;
428
+ /**
429
+ * LAMBDA reduction only (RTKLIB `lambda_reduction`): the unimodular
430
+ * decorrelation matrix Z (n×n, column-major) such that Qz = ZᵀQZ is
431
+ * (near-)diagonally dominant. Exposed for diagnostics/tests; returns
432
+ * null when Q is not positive definite.
433
+ */
434
+ declare function lambdaReduction(Q: Float64Array, n: number): Float64Array | null;
435
+
274
436
  /**
275
437
  * Single-point positioning (SPP) from pseudoranges and broadcast
276
438
  * ephemerides.
@@ -351,4 +513,4 @@ declare function ionoFree(p1: number, p2: number, f1: number, f2: number): numbe
351
513
  */
352
514
  declare function solveSpp(pseudoranges: Map<string, number>, ephemerides: Map<string, Ephemeris>, timeMs: number, opts?: SppOptions): SppSolution | null;
353
515
 
354
- export { type DgnssOptions, type DgnssSolution, type EphemerisSource, type KlobucharCoeffs, type RawObservation, type RtkEpochMeasurements, RtkFloatEngine, type RtkFloatOptions, type RtkFloatSolution, type RtkMeasurement, type SppOptions, type SppSolution, ionoFree, klobucharDelay, satClockCorrection, solveDgnss, solveSpp, toRtkEpoch };
516
+ export { type DgnssOptions, type DgnssSolution, type EphemerisSource, type KlobucharCoeffs, type LambdaResult, type RawObservation, type RtkEpochMeasurements, RtkFloatEngine, type RtkFloatOptions, type RtkFloatSolution, type RtkMeasurement, type SppOptions, type SppSolution, ionoFree, klobucharDelay, lambdaReduction, lambdaSearch, satClockCorrection, solveDgnss, solveSpp, toRtkEpoch };