gnss-js 1.23.0 → 1.25.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.
@@ -1,5 +1,6 @@
1
1
  import { E as Ephemeris } from './nav-DImXUdbp.cjs';
2
2
  import { DopValues } from './orbit.cjs';
3
+ import { I as IonexGrid } from './ionex-DaW5x9nq.cjs';
3
4
  import './ephemeris-Ohl6NAfw.cjs';
4
5
  import './ecef-CF0uAysr.cjs';
5
6
  import './nav-BYmYoh9A.cjs';
@@ -34,6 +35,52 @@ interface KlobucharCoeffs {
34
35
  */
35
36
  declare function klobucharDelay(coeffs: KlobucharCoeffs, latRad: number, lonRad: number, azRad: number, elRad: number, gpsTowSec: number): number;
36
37
 
38
+ /**
39
+ * Slant ionospheric delay from a Global Ionosphere Map (IONEX/GIM).
40
+ *
41
+ * A GIM gives vertical TEC on an hourly lat/lon grid (see
42
+ * {@link parseIonex}). For single-frequency positioning this is a much
43
+ * stronger correction than the broadcast Klobuchar model — GIMs capture
44
+ * ~80–90% of the true ionosphere versus Klobuchar's ~50% — so feeding
45
+ * `solveSpp` a GIM (the `gim` option) collapses most of the residual
46
+ * height bias a single-frequency solution otherwise carries.
47
+ *
48
+ * The evaluation is the standard thin-shell recipe: pierce the shell at
49
+ * {@link H_ION}, interpolate vertical TEC there (bilinear in space,
50
+ * linear in time), map to slant with the obliquity factor, and convert
51
+ * TEC → L1 group delay. The result is the delay on GPS L1; `solveSpp`
52
+ * scales it to each system's primary frequency by (f_L1/f)².
53
+ */
54
+
55
+ /**
56
+ * L1 group delay per unit slant TEC: 40.3 · 10¹⁶ / f_L1² (m per TECU).
57
+ * 40.3 m·Hz²/(el/m²), one TECU = 10¹⁶ el/m². ≈ 0.1624 m/TECU.
58
+ */
59
+ declare const IONO_L1_M_PER_TECU: number;
60
+ /**
61
+ * Space- and time-interpolated vertical TEC (TECU) at `timeMs`, or null
62
+ * outside the map's time span or where the grid marks no value.
63
+ *
64
+ * IGS GIMs are global, so spatial coverage is total; a pierce point is
65
+ * bilinearly interpolated and clamped to the nearest edge cell rather
66
+ * than rejected. Null therefore signals a time gap or a no-value cell,
67
+ * not an out-of-area position.
68
+ */
69
+ declare function gimVerticalTec(grid: IonexGrid, timeMs: number, latDeg: number, lonDeg: number): number | null;
70
+ /**
71
+ * Slant ionospheric group delay on GPS L1 (m) from a GIM, or null when
72
+ * the pierce point falls outside the map's coverage in space or time.
73
+ *
74
+ * @param grid Parsed IONEX grid ({@link parseIonex}).
75
+ * @param latRad Receiver geodetic latitude (radians).
76
+ * @param lonRad Receiver geodetic longitude (radians).
77
+ * @param azRad Satellite azimuth (radians).
78
+ * @param elRad Satellite elevation (radians).
79
+ * @param timeMs Epoch (ms). GIM epochs are UTC; a GPS-scale time differs
80
+ * by leap seconds (~18 s), negligible against hourly maps.
81
+ */
82
+ declare function gimSlantIonoDelayL1(grid: IonexGrid, latRad: number, lonRad: number, azRad: number, elRad: number, timeMs: number): number | null;
83
+
37
84
  /**
38
85
  * RTK positioning, stage 1: epoch-wise double-differenced (DD)
39
86
  * processing of a base/rover receiver pair.
@@ -54,7 +101,29 @@ declare function klobucharDelay(coeffs: KlobucharCoeffs, latRad: number, lonRad:
54
101
  * (LAMBDA) needs the joint float ambiguity covariance — which the
55
102
  * EKF maintains natively. This mirrors RTKLIB's float-filter
56
103
  * structure (rtkpos.c), which the implementation was validated
57
- * against; no integer fixing is attempted (`ratio` is reserved).
104
+ * against.
105
+ *
106
+ * Stage 2 — integer ambiguity resolution (opt-in via
107
+ * `ambiguityResolution: 'instant'`): after each float update the
108
+ * engine extracts the DD ambiguity subvector and its joint covariance
109
+ * from the filter, runs MLAMBDA (`lambdaSearch`, a port of RTKLIB's
110
+ * lambda.c) and validates the best candidate with the ratio test
111
+ * (s₂/s₁ ≥ `ratioThreshold`, default 3.0 — RTKLIB's default). On
112
+ * acceptance the rover position is conditioned on the integer
113
+ * ambiguities (xa = x − P_xb·Q_b⁻¹·(b − ǎ), RTKLIB rtkpos.c
114
+ * `resamb_LAMBDA`) and reported as a FIXED solution; the filter state
115
+ * itself is *not* modified (instant, per-epoch fixing — RTKLIB's
116
+ * `armode=instantaneous`; continuous AR/fix-and-hold, which feed the
117
+ * fix back into the filter, are stage-3 continuity options). When the
118
+ * full set fails the ratio test, partial fixing retries after
119
+ * dropping the lowest-elevation ambiguity, one at a time, down to
120
+ * half the candidate set (and never below `minFixAmbiguities`) — an
121
+ * elevation-ordered subset walk in the spirit of RTKLIB demo5's
122
+ * satellite exclusion, floored so an unconverged float cannot buy a
123
+ * lucky ratio on a tiny subset. GLONASS ambiguities are excluded from
124
+ * fixing by
125
+ * default (DD IFB is only integer-safe across same-model receivers;
126
+ * enable with `glonassAr`).
58
127
  *
59
128
  * Differencing model: for satellites s and reference r observed by
60
129
  * rover R and base B, the DD pseudorange/phase cancels satellite
@@ -208,18 +277,65 @@ interface RtkFloatOptions {
208
277
  slipGateM?: number;
209
278
  /** Measurement-update relinearisations (IEKF). Default 2. */
210
279
  updateIterations?: number;
280
+ /**
281
+ * Integer ambiguity resolution: 'off' keeps the stage-1 float-only
282
+ * behaviour; 'instant' attempts a per-epoch LAMBDA fix after each
283
+ * float update (no state feedback — RTKLIB `armode=instantaneous`).
284
+ * Default 'off'.
285
+ */
286
+ ambiguityResolution?: 'off' | 'instant';
287
+ /** Ratio-test acceptance threshold (s₂/s₁). Default 3.0 (RTKLIB). */
288
+ ratioThreshold?: number;
289
+ /**
290
+ * Additional elevation mask (deg) for ambiguities entering the fix
291
+ * (RTKLIB `elmaskar`); 0 = use every measured ambiguity. Default 0.
292
+ */
293
+ arElevationMaskDeg?: number;
294
+ /**
295
+ * Include GLONASS FDMA ambiguities in the integer fix. Only sound
296
+ * across same-model receivers (differential IFB). Default false.
297
+ */
298
+ glonassAr?: boolean;
299
+ /**
300
+ * Partial fixing: when the full ambiguity set fails the ratio test,
301
+ * drop the lowest-elevation ambiguity and retry (elevation-ordered
302
+ * subset walk), never below half the candidates. Default true.
303
+ */
304
+ partialFixing?: boolean;
305
+ /** Minimum DD ambiguities to attempt/accept a fix. Default 4. */
306
+ minFixAmbiguities?: number;
211
307
  }
212
308
  interface RtkFloatSolution {
213
309
  /** Epoch, GPS-scale milliseconds. */
214
310
  timeMs: number;
215
- /** Rover position, ECEF metres. */
311
+ /**
312
+ * Rover position, ECEF metres — the ambiguity-fixed position when
313
+ * `status === 'fixed'`, the float filter position otherwise.
314
+ */
216
315
  position: [number, number, number];
217
- /** Float baseline rover − base, ECEF metres. */
316
+ /** Float baseline rover − base, ECEF metres (always the float). */
218
317
  floatBaseline: [number, number, number];
318
+ /**
319
+ * Solution quality: 'fixed' when LAMBDA passed the ratio test,
320
+ * 'float' when carrier ambiguities contributed but no (accepted)
321
+ * fix, 'dgnss' when the epoch was updated from code DDs only.
322
+ */
323
+ status: 'fixed' | 'float' | 'dgnss';
219
324
  /** Satellites contributing DD rows this epoch (incl. references). */
220
325
  nSats: number;
221
- /** Ambiguity-validation ratio — undefined at stage 1 (no LAMBDA). */
326
+ /**
327
+ * Ambiguity-validation ratio (s₂/s₁ of the LAMBDA candidates,
328
+ * capped at 999.9 as RTKLIB): the accepted subset's ratio when
329
+ * fixed, the full-set ratio otherwise. Undefined when no fix was
330
+ * attempted (`ambiguityResolution: 'off'` or no eligible set).
331
+ */
222
332
  ratio?: number;
333
+ /** Number of integer-fixed DD ambiguities (when `status==='fixed'`). */
334
+ nFixed?: number;
335
+ /** Fixed baseline rover − base, ECEF metres (when fixed). */
336
+ fixedBaseline?: [number, number, number];
337
+ /** Integer DD ambiguity (cycles) per fixed PRN (when fixed). */
338
+ fixedAmbiguities?: Record<string, number>;
223
339
  /** Formal position sigmas (filter covariance), ECEF metres. */
224
340
  sigmas: [number, number, number];
225
341
  /** Float DD ambiguity (cycles) per non-reference PRN. */
@@ -262,6 +378,17 @@ declare class RtkFloatEngine {
262
378
  * transform of state and covariance (P' = T P Tᵀ).
263
379
  */
264
380
  private retarget;
381
+ /**
382
+ * Try to fix the ambiguity subset `idxs` (indices into `this.amb`):
383
+ * extract the float subvector b and covariance Q_b from the filter,
384
+ * run MLAMBDA and, without touching the filter state, condition the
385
+ * position on the best integer candidate,
386
+ * xa = x − P_xb·Q_b⁻¹·(b − ǎ)
387
+ * (RTKLIB rtkpos.c `resamb_LAMBDA`). Returns null when Q_b is not
388
+ * positive definite / invertible or the search fails; ratio
389
+ * validation is the caller's job.
390
+ */
391
+ private tryFix;
265
392
  /**
266
393
  * Process one synchronized epoch (same nominal `timeMs` for both
267
394
  * receivers). Returns null until a first position can be
@@ -271,6 +398,88 @@ declare class RtkFloatEngine {
271
398
  process(rover: RtkEpochMeasurements, base: RtkEpochMeasurements, timeMs: number): RtkFloatSolution | null;
272
399
  }
273
400
 
401
+ /**
402
+ * Integer ambiguity resolution: LAMBDA decorrelation + MLAMBDA
403
+ * integer least-squares search.
404
+ *
405
+ * Port of RTKLIB's `lambda.c` (demo5 / rtklibexplorer fork,
406
+ * src/lambda.c, Copyright (c) 2007-2008 T. Takasu, BSD-2-Clause),
407
+ * cross-checked against the reference papers where the C comments are
408
+ * thin:
409
+ *
410
+ * - [1] P.J.G. Teunissen, "The least-squares ambiguity decorrelation
411
+ * adjustment: a method for fast GPS ambiguity estimation",
412
+ * J. Geodesy 70, 65-82, 1995.
413
+ * - [2] X.-W. Chang, X. Yang, T. Zhou, "MLAMBDA: A modified LAMBDA
414
+ * method for integer least-squares estimation", J. Geodesy 79,
415
+ * 552-565, 2005.
416
+ *
417
+ * Pipeline (identical to RTKLIB's `lambda()`):
418
+ * 1. LᵀDL factorization of the float covariance, Q = Lᵀ·diag(D)·L
419
+ * with L unit lower triangular (RTKLIB's `LD`).
420
+ * 2. LAMBDA reduction [1]: integer Gauss transformations interleaved
421
+ * with symmetric permutations until the transformed D is
422
+ * (approximately) descending — producing a unimodular Z with
423
+ * Qz = ZᵀQZ = Lᵀ·diag(D)·L far better conditioned than Q.
424
+ * 3. MLAMBDA search [2] in the decorrelated space: depth-first
425
+ * enumeration over a shrinking ellipsoid, keeping the `m` smallest
426
+ * residual quadratic forms (z−ž)ᵀQz⁻¹(z−ž).
427
+ * 4. Back-transform the candidates to the original space, a = Z⁻ᵀ·ž
428
+ * (RTKLIB solves ZᵀF = E; Z is unimodular so F is exactly integer).
429
+ *
430
+ * Deviations from lambda.c:
431
+ * - matrices are `Float64Array` in the same column-major layout, with
432
+ * allocation-failure paths dropped (JS throws instead),
433
+ * - failures (non-positive-definite Q, search loop overflow) return
434
+ * `null` instead of a status code + stderr message,
435
+ * - back-transformed candidates are rounded to the nearest integer:
436
+ * F = Z⁻ᵀ·ž is exactly integer in ℤ arithmetic and the rounding only
437
+ * removes the ~1e-12 fuzz of the floating-point triangular solve,
438
+ * - the ratio s₂/s₁ used by the RTKLIB caller (`rtkpos.c`) is returned
439
+ * alongside the candidates (∞ when s₁ = 0, i.e. noise-free input).
440
+ */
441
+ /** Result of an integer least-squares search. */
442
+ interface LambdaResult {
443
+ /**
444
+ * The `m` best integer candidate vectors (length n each), sorted by
445
+ * ascending residual quadratic form; `candidates[0]` is the integer
446
+ * least-squares minimizer.
447
+ */
448
+ candidates: Float64Array[];
449
+ /**
450
+ * Residual quadratic forms sᵢ = (a−ǎᵢ)ᵀQ⁻¹(a−ǎᵢ) matching
451
+ * `candidates`, ascending.
452
+ */
453
+ residuals: Float64Array;
454
+ /**
455
+ * Ratio-test statistic s₂/s₁ (second-best over best); `Infinity`
456
+ * when the best residual is exactly 0 (noise-free input). Callers
457
+ * typically cap it (RTKLIB uses 999.9) before thresholding.
458
+ */
459
+ ratio: number;
460
+ }
461
+ /**
462
+ * LAMBDA/MLAMBDA integer least-squares estimation (RTKLIB `lambda`):
463
+ * decorrelate the float ambiguities, search the `m` best integer
464
+ * vectors and back-transform them to the original space.
465
+ *
466
+ * @param a Float ambiguities (length ≥ n).
467
+ * @param Q Their covariance, n×n (symmetric — row/column-major agree).
468
+ * @param n Number of ambiguities.
469
+ * @param m Number of candidates to return (default 2: best +
470
+ * second-best, what the ratio test needs).
471
+ * @returns Candidates + residuals + ratio, or null when Q is not
472
+ * positive definite or the search does not terminate.
473
+ */
474
+ declare function lambdaSearch(a: Float64Array, Q: Float64Array, n: number, m?: number): LambdaResult | null;
475
+ /**
476
+ * LAMBDA reduction only (RTKLIB `lambda_reduction`): the unimodular
477
+ * decorrelation matrix Z (n×n, column-major) such that Qz = ZᵀQZ is
478
+ * (near-)diagonally dominant. Exposed for diagnostics/tests; returns
479
+ * null when Q is not positive definite.
480
+ */
481
+ declare function lambdaReduction(Q: Float64Array, n: number): Float64Array | null;
482
+
274
483
  /**
275
484
  * Single-point positioning (SPP) from pseudoranges and broadcast
276
485
  * ephemerides.
@@ -279,7 +488,7 @@ declare class RtkFloatEngine {
279
488
  * constellation (absorbing inter-system time offsets), satellite clock
280
489
  * polynomial + relativistic correction, broadcast group delay
281
490
  * (TGD/BGD), Sagnac (Earth-rotation) correction, elevation
282
- * masking/weighting, and a simple tropospheric model. Ionospheric
491
+ * masking/weighting, and a Saastamoinen tropospheric model. Ionospheric
283
492
  * delay can be modelled from the broadcast Klobuchar coefficients
284
493
  * (`iono` option) for single-frequency measurements; for metre-level
285
494
  * results prefer the iono-free combination (ionoFree helper) with
@@ -289,7 +498,7 @@ declare class RtkFloatEngine {
289
498
  interface SppOptions {
290
499
  /** Elevation mask in degrees (applied after the first pass). Default 10. */
291
500
  elevationMaskDeg?: number;
292
- /** Apply the simple tropospheric model. Default true. */
501
+ /** Apply the Saastamoinen tropospheric model. Default true. */
293
502
  troposphere?: boolean;
294
503
  /** Maximum Gauss-Newton iterations. Default 15. */
295
504
  maxIterations?: number;
@@ -303,6 +512,15 @@ interface SppOptions {
303
512
  * iono-free combinations.
304
513
  */
305
514
  iono?: KlobucharCoeffs;
515
+ /**
516
+ * Global Ionosphere Map (IONEX/GIM, from `parseIonex`). When given it
517
+ * takes precedence over `iono`: the slant delay is read from the map
518
+ * (~80–90% of the true ionosphere vs Klobuchar's ~50%), falling back
519
+ * to `iono` — if also supplied — only where the map has no value for
520
+ * an epoch/cell (a time gap; global maps always cover space). Omit for
521
+ * iono-free input.
522
+ */
523
+ gim?: IonexGrid;
306
524
  /**
307
525
  * Apply the broadcast group-delay correction (GPS TGD, Galileo
308
526
  * BGD E5a/E1, BeiDou TGD1) to the satellite clock. Correct for
@@ -351,4 +569,4 @@ declare function ionoFree(p1: number, p2: number, f1: number, f2: number): numbe
351
569
  */
352
570
  declare function solveSpp(pseudoranges: Map<string, number>, ephemerides: Map<string, Ephemeris>, timeMs: number, opts?: SppOptions): SppSolution | null;
353
571
 
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 };
572
+ export { type DgnssOptions, type DgnssSolution, type EphemerisSource, IONO_L1_M_PER_TECU, type KlobucharCoeffs, type LambdaResult, type RawObservation, type RtkEpochMeasurements, RtkFloatEngine, type RtkFloatOptions, type RtkFloatSolution, type RtkMeasurement, type SppOptions, type SppSolution, gimSlantIonoDelayL1, gimVerticalTec, ionoFree, klobucharDelay, lambdaReduction, lambdaSearch, satClockCorrection, solveDgnss, solveSpp, toRtkEpoch };
@@ -1,5 +1,6 @@
1
1
  import { E as Ephemeris } from './nav-DImXUdbp.js';
2
2
  import { DopValues } from './orbit.js';
3
+ import { I as IonexGrid } from './ionex-DaW5x9nq.js';
3
4
  import './ephemeris-Ohl6NAfw.js';
4
5
  import './ecef-CF0uAysr.js';
5
6
  import './nav-CN8rxL4h.js';
@@ -34,6 +35,52 @@ interface KlobucharCoeffs {
34
35
  */
35
36
  declare function klobucharDelay(coeffs: KlobucharCoeffs, latRad: number, lonRad: number, azRad: number, elRad: number, gpsTowSec: number): number;
36
37
 
38
+ /**
39
+ * Slant ionospheric delay from a Global Ionosphere Map (IONEX/GIM).
40
+ *
41
+ * A GIM gives vertical TEC on an hourly lat/lon grid (see
42
+ * {@link parseIonex}). For single-frequency positioning this is a much
43
+ * stronger correction than the broadcast Klobuchar model — GIMs capture
44
+ * ~80–90% of the true ionosphere versus Klobuchar's ~50% — so feeding
45
+ * `solveSpp` a GIM (the `gim` option) collapses most of the residual
46
+ * height bias a single-frequency solution otherwise carries.
47
+ *
48
+ * The evaluation is the standard thin-shell recipe: pierce the shell at
49
+ * {@link H_ION}, interpolate vertical TEC there (bilinear in space,
50
+ * linear in time), map to slant with the obliquity factor, and convert
51
+ * TEC → L1 group delay. The result is the delay on GPS L1; `solveSpp`
52
+ * scales it to each system's primary frequency by (f_L1/f)².
53
+ */
54
+
55
+ /**
56
+ * L1 group delay per unit slant TEC: 40.3 · 10¹⁶ / f_L1² (m per TECU).
57
+ * 40.3 m·Hz²/(el/m²), one TECU = 10¹⁶ el/m². ≈ 0.1624 m/TECU.
58
+ */
59
+ declare const IONO_L1_M_PER_TECU: number;
60
+ /**
61
+ * Space- and time-interpolated vertical TEC (TECU) at `timeMs`, or null
62
+ * outside the map's time span or where the grid marks no value.
63
+ *
64
+ * IGS GIMs are global, so spatial coverage is total; a pierce point is
65
+ * bilinearly interpolated and clamped to the nearest edge cell rather
66
+ * than rejected. Null therefore signals a time gap or a no-value cell,
67
+ * not an out-of-area position.
68
+ */
69
+ declare function gimVerticalTec(grid: IonexGrid, timeMs: number, latDeg: number, lonDeg: number): number | null;
70
+ /**
71
+ * Slant ionospheric group delay on GPS L1 (m) from a GIM, or null when
72
+ * the pierce point falls outside the map's coverage in space or time.
73
+ *
74
+ * @param grid Parsed IONEX grid ({@link parseIonex}).
75
+ * @param latRad Receiver geodetic latitude (radians).
76
+ * @param lonRad Receiver geodetic longitude (radians).
77
+ * @param azRad Satellite azimuth (radians).
78
+ * @param elRad Satellite elevation (radians).
79
+ * @param timeMs Epoch (ms). GIM epochs are UTC; a GPS-scale time differs
80
+ * by leap seconds (~18 s), negligible against hourly maps.
81
+ */
82
+ declare function gimSlantIonoDelayL1(grid: IonexGrid, latRad: number, lonRad: number, azRad: number, elRad: number, timeMs: number): number | null;
83
+
37
84
  /**
38
85
  * RTK positioning, stage 1: epoch-wise double-differenced (DD)
39
86
  * processing of a base/rover receiver pair.
@@ -54,7 +101,29 @@ declare function klobucharDelay(coeffs: KlobucharCoeffs, latRad: number, lonRad:
54
101
  * (LAMBDA) needs the joint float ambiguity covariance — which the
55
102
  * EKF maintains natively. This mirrors RTKLIB's float-filter
56
103
  * structure (rtkpos.c), which the implementation was validated
57
- * against; no integer fixing is attempted (`ratio` is reserved).
104
+ * against.
105
+ *
106
+ * Stage 2 — integer ambiguity resolution (opt-in via
107
+ * `ambiguityResolution: 'instant'`): after each float update the
108
+ * engine extracts the DD ambiguity subvector and its joint covariance
109
+ * from the filter, runs MLAMBDA (`lambdaSearch`, a port of RTKLIB's
110
+ * lambda.c) and validates the best candidate with the ratio test
111
+ * (s₂/s₁ ≥ `ratioThreshold`, default 3.0 — RTKLIB's default). On
112
+ * acceptance the rover position is conditioned on the integer
113
+ * ambiguities (xa = x − P_xb·Q_b⁻¹·(b − ǎ), RTKLIB rtkpos.c
114
+ * `resamb_LAMBDA`) and reported as a FIXED solution; the filter state
115
+ * itself is *not* modified (instant, per-epoch fixing — RTKLIB's
116
+ * `armode=instantaneous`; continuous AR/fix-and-hold, which feed the
117
+ * fix back into the filter, are stage-3 continuity options). When the
118
+ * full set fails the ratio test, partial fixing retries after
119
+ * dropping the lowest-elevation ambiguity, one at a time, down to
120
+ * half the candidate set (and never below `minFixAmbiguities`) — an
121
+ * elevation-ordered subset walk in the spirit of RTKLIB demo5's
122
+ * satellite exclusion, floored so an unconverged float cannot buy a
123
+ * lucky ratio on a tiny subset. GLONASS ambiguities are excluded from
124
+ * fixing by
125
+ * default (DD IFB is only integer-safe across same-model receivers;
126
+ * enable with `glonassAr`).
58
127
  *
59
128
  * Differencing model: for satellites s and reference r observed by
60
129
  * rover R and base B, the DD pseudorange/phase cancels satellite
@@ -208,18 +277,65 @@ interface RtkFloatOptions {
208
277
  slipGateM?: number;
209
278
  /** Measurement-update relinearisations (IEKF). Default 2. */
210
279
  updateIterations?: number;
280
+ /**
281
+ * Integer ambiguity resolution: 'off' keeps the stage-1 float-only
282
+ * behaviour; 'instant' attempts a per-epoch LAMBDA fix after each
283
+ * float update (no state feedback — RTKLIB `armode=instantaneous`).
284
+ * Default 'off'.
285
+ */
286
+ ambiguityResolution?: 'off' | 'instant';
287
+ /** Ratio-test acceptance threshold (s₂/s₁). Default 3.0 (RTKLIB). */
288
+ ratioThreshold?: number;
289
+ /**
290
+ * Additional elevation mask (deg) for ambiguities entering the fix
291
+ * (RTKLIB `elmaskar`); 0 = use every measured ambiguity. Default 0.
292
+ */
293
+ arElevationMaskDeg?: number;
294
+ /**
295
+ * Include GLONASS FDMA ambiguities in the integer fix. Only sound
296
+ * across same-model receivers (differential IFB). Default false.
297
+ */
298
+ glonassAr?: boolean;
299
+ /**
300
+ * Partial fixing: when the full ambiguity set fails the ratio test,
301
+ * drop the lowest-elevation ambiguity and retry (elevation-ordered
302
+ * subset walk), never below half the candidates. Default true.
303
+ */
304
+ partialFixing?: boolean;
305
+ /** Minimum DD ambiguities to attempt/accept a fix. Default 4. */
306
+ minFixAmbiguities?: number;
211
307
  }
212
308
  interface RtkFloatSolution {
213
309
  /** Epoch, GPS-scale milliseconds. */
214
310
  timeMs: number;
215
- /** Rover position, ECEF metres. */
311
+ /**
312
+ * Rover position, ECEF metres — the ambiguity-fixed position when
313
+ * `status === 'fixed'`, the float filter position otherwise.
314
+ */
216
315
  position: [number, number, number];
217
- /** Float baseline rover − base, ECEF metres. */
316
+ /** Float baseline rover − base, ECEF metres (always the float). */
218
317
  floatBaseline: [number, number, number];
318
+ /**
319
+ * Solution quality: 'fixed' when LAMBDA passed the ratio test,
320
+ * 'float' when carrier ambiguities contributed but no (accepted)
321
+ * fix, 'dgnss' when the epoch was updated from code DDs only.
322
+ */
323
+ status: 'fixed' | 'float' | 'dgnss';
219
324
  /** Satellites contributing DD rows this epoch (incl. references). */
220
325
  nSats: number;
221
- /** Ambiguity-validation ratio — undefined at stage 1 (no LAMBDA). */
326
+ /**
327
+ * Ambiguity-validation ratio (s₂/s₁ of the LAMBDA candidates,
328
+ * capped at 999.9 as RTKLIB): the accepted subset's ratio when
329
+ * fixed, the full-set ratio otherwise. Undefined when no fix was
330
+ * attempted (`ambiguityResolution: 'off'` or no eligible set).
331
+ */
222
332
  ratio?: number;
333
+ /** Number of integer-fixed DD ambiguities (when `status==='fixed'`). */
334
+ nFixed?: number;
335
+ /** Fixed baseline rover − base, ECEF metres (when fixed). */
336
+ fixedBaseline?: [number, number, number];
337
+ /** Integer DD ambiguity (cycles) per fixed PRN (when fixed). */
338
+ fixedAmbiguities?: Record<string, number>;
223
339
  /** Formal position sigmas (filter covariance), ECEF metres. */
224
340
  sigmas: [number, number, number];
225
341
  /** Float DD ambiguity (cycles) per non-reference PRN. */
@@ -262,6 +378,17 @@ declare class RtkFloatEngine {
262
378
  * transform of state and covariance (P' = T P Tᵀ).
263
379
  */
264
380
  private retarget;
381
+ /**
382
+ * Try to fix the ambiguity subset `idxs` (indices into `this.amb`):
383
+ * extract the float subvector b and covariance Q_b from the filter,
384
+ * run MLAMBDA and, without touching the filter state, condition the
385
+ * position on the best integer candidate,
386
+ * xa = x − P_xb·Q_b⁻¹·(b − ǎ)
387
+ * (RTKLIB rtkpos.c `resamb_LAMBDA`). Returns null when Q_b is not
388
+ * positive definite / invertible or the search fails; ratio
389
+ * validation is the caller's job.
390
+ */
391
+ private tryFix;
265
392
  /**
266
393
  * Process one synchronized epoch (same nominal `timeMs` for both
267
394
  * receivers). Returns null until a first position can be
@@ -271,6 +398,88 @@ declare class RtkFloatEngine {
271
398
  process(rover: RtkEpochMeasurements, base: RtkEpochMeasurements, timeMs: number): RtkFloatSolution | null;
272
399
  }
273
400
 
401
+ /**
402
+ * Integer ambiguity resolution: LAMBDA decorrelation + MLAMBDA
403
+ * integer least-squares search.
404
+ *
405
+ * Port of RTKLIB's `lambda.c` (demo5 / rtklibexplorer fork,
406
+ * src/lambda.c, Copyright (c) 2007-2008 T. Takasu, BSD-2-Clause),
407
+ * cross-checked against the reference papers where the C comments are
408
+ * thin:
409
+ *
410
+ * - [1] P.J.G. Teunissen, "The least-squares ambiguity decorrelation
411
+ * adjustment: a method for fast GPS ambiguity estimation",
412
+ * J. Geodesy 70, 65-82, 1995.
413
+ * - [2] X.-W. Chang, X. Yang, T. Zhou, "MLAMBDA: A modified LAMBDA
414
+ * method for integer least-squares estimation", J. Geodesy 79,
415
+ * 552-565, 2005.
416
+ *
417
+ * Pipeline (identical to RTKLIB's `lambda()`):
418
+ * 1. LᵀDL factorization of the float covariance, Q = Lᵀ·diag(D)·L
419
+ * with L unit lower triangular (RTKLIB's `LD`).
420
+ * 2. LAMBDA reduction [1]: integer Gauss transformations interleaved
421
+ * with symmetric permutations until the transformed D is
422
+ * (approximately) descending — producing a unimodular Z with
423
+ * Qz = ZᵀQZ = Lᵀ·diag(D)·L far better conditioned than Q.
424
+ * 3. MLAMBDA search [2] in the decorrelated space: depth-first
425
+ * enumeration over a shrinking ellipsoid, keeping the `m` smallest
426
+ * residual quadratic forms (z−ž)ᵀQz⁻¹(z−ž).
427
+ * 4. Back-transform the candidates to the original space, a = Z⁻ᵀ·ž
428
+ * (RTKLIB solves ZᵀF = E; Z is unimodular so F is exactly integer).
429
+ *
430
+ * Deviations from lambda.c:
431
+ * - matrices are `Float64Array` in the same column-major layout, with
432
+ * allocation-failure paths dropped (JS throws instead),
433
+ * - failures (non-positive-definite Q, search loop overflow) return
434
+ * `null` instead of a status code + stderr message,
435
+ * - back-transformed candidates are rounded to the nearest integer:
436
+ * F = Z⁻ᵀ·ž is exactly integer in ℤ arithmetic and the rounding only
437
+ * removes the ~1e-12 fuzz of the floating-point triangular solve,
438
+ * - the ratio s₂/s₁ used by the RTKLIB caller (`rtkpos.c`) is returned
439
+ * alongside the candidates (∞ when s₁ = 0, i.e. noise-free input).
440
+ */
441
+ /** Result of an integer least-squares search. */
442
+ interface LambdaResult {
443
+ /**
444
+ * The `m` best integer candidate vectors (length n each), sorted by
445
+ * ascending residual quadratic form; `candidates[0]` is the integer
446
+ * least-squares minimizer.
447
+ */
448
+ candidates: Float64Array[];
449
+ /**
450
+ * Residual quadratic forms sᵢ = (a−ǎᵢ)ᵀQ⁻¹(a−ǎᵢ) matching
451
+ * `candidates`, ascending.
452
+ */
453
+ residuals: Float64Array;
454
+ /**
455
+ * Ratio-test statistic s₂/s₁ (second-best over best); `Infinity`
456
+ * when the best residual is exactly 0 (noise-free input). Callers
457
+ * typically cap it (RTKLIB uses 999.9) before thresholding.
458
+ */
459
+ ratio: number;
460
+ }
461
+ /**
462
+ * LAMBDA/MLAMBDA integer least-squares estimation (RTKLIB `lambda`):
463
+ * decorrelate the float ambiguities, search the `m` best integer
464
+ * vectors and back-transform them to the original space.
465
+ *
466
+ * @param a Float ambiguities (length ≥ n).
467
+ * @param Q Their covariance, n×n (symmetric — row/column-major agree).
468
+ * @param n Number of ambiguities.
469
+ * @param m Number of candidates to return (default 2: best +
470
+ * second-best, what the ratio test needs).
471
+ * @returns Candidates + residuals + ratio, or null when Q is not
472
+ * positive definite or the search does not terminate.
473
+ */
474
+ declare function lambdaSearch(a: Float64Array, Q: Float64Array, n: number, m?: number): LambdaResult | null;
475
+ /**
476
+ * LAMBDA reduction only (RTKLIB `lambda_reduction`): the unimodular
477
+ * decorrelation matrix Z (n×n, column-major) such that Qz = ZᵀQZ is
478
+ * (near-)diagonally dominant. Exposed for diagnostics/tests; returns
479
+ * null when Q is not positive definite.
480
+ */
481
+ declare function lambdaReduction(Q: Float64Array, n: number): Float64Array | null;
482
+
274
483
  /**
275
484
  * Single-point positioning (SPP) from pseudoranges and broadcast
276
485
  * ephemerides.
@@ -279,7 +488,7 @@ declare class RtkFloatEngine {
279
488
  * constellation (absorbing inter-system time offsets), satellite clock
280
489
  * polynomial + relativistic correction, broadcast group delay
281
490
  * (TGD/BGD), Sagnac (Earth-rotation) correction, elevation
282
- * masking/weighting, and a simple tropospheric model. Ionospheric
491
+ * masking/weighting, and a Saastamoinen tropospheric model. Ionospheric
283
492
  * delay can be modelled from the broadcast Klobuchar coefficients
284
493
  * (`iono` option) for single-frequency measurements; for metre-level
285
494
  * results prefer the iono-free combination (ionoFree helper) with
@@ -289,7 +498,7 @@ declare class RtkFloatEngine {
289
498
  interface SppOptions {
290
499
  /** Elevation mask in degrees (applied after the first pass). Default 10. */
291
500
  elevationMaskDeg?: number;
292
- /** Apply the simple tropospheric model. Default true. */
501
+ /** Apply the Saastamoinen tropospheric model. Default true. */
293
502
  troposphere?: boolean;
294
503
  /** Maximum Gauss-Newton iterations. Default 15. */
295
504
  maxIterations?: number;
@@ -303,6 +512,15 @@ interface SppOptions {
303
512
  * iono-free combinations.
304
513
  */
305
514
  iono?: KlobucharCoeffs;
515
+ /**
516
+ * Global Ionosphere Map (IONEX/GIM, from `parseIonex`). When given it
517
+ * takes precedence over `iono`: the slant delay is read from the map
518
+ * (~80–90% of the true ionosphere vs Klobuchar's ~50%), falling back
519
+ * to `iono` — if also supplied — only where the map has no value for
520
+ * an epoch/cell (a time gap; global maps always cover space). Omit for
521
+ * iono-free input.
522
+ */
523
+ gim?: IonexGrid;
306
524
  /**
307
525
  * Apply the broadcast group-delay correction (GPS TGD, Galileo
308
526
  * BGD E5a/E1, BeiDou TGD1) to the satellite clock. Correct for
@@ -351,4 +569,4 @@ declare function ionoFree(p1: number, p2: number, f1: number, f2: number): numbe
351
569
  */
352
570
  declare function solveSpp(pseudoranges: Map<string, number>, ephemerides: Map<string, Ephemeris>, timeMs: number, opts?: SppOptions): SppSolution | null;
353
571
 
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 };
572
+ export { type DgnssOptions, type DgnssSolution, type EphemerisSource, IONO_L1_M_PER_TECU, type KlobucharCoeffs, type LambdaResult, type RawObservation, type RtkEpochMeasurements, RtkFloatEngine, type RtkFloatOptions, type RtkFloatSolution, type RtkMeasurement, type SppOptions, type SppSolution, gimSlantIonoDelayL1, gimVerticalTec, ionoFree, klobucharDelay, lambdaReduction, lambdaSearch, satClockCorrection, solveDgnss, solveSpp, toRtkEpoch };
@@ -1,12 +1,17 @@
1
1
  import {
2
+ IONO_L1_M_PER_TECU,
2
3
  RtkFloatEngine,
4
+ gimSlantIonoDelayL1,
5
+ gimVerticalTec,
3
6
  ionoFree,
4
7
  klobucharDelay,
8
+ lambdaReduction,
9
+ lambdaSearch,
5
10
  satClockCorrection,
6
11
  solveDgnss,
7
12
  solveSpp,
8
13
  toRtkEpoch
9
- } from "./chunk-CUWF56ST.js";
14
+ } from "./chunk-4XGKCVE3.js";
10
15
  import "./chunk-MHGNKSSH.js";
11
16
  import "./chunk-37QNKGTC.js";
12
17
  import "./chunk-6FAL6P4G.js";
@@ -14,9 +19,14 @@ import "./chunk-HVXYFUCB.js";
14
19
  import "./chunk-LEEU5OIO.js";
15
20
  import "./chunk-FIEWO4J4.js";
16
21
  export {
22
+ IONO_L1_M_PER_TECU,
17
23
  RtkFloatEngine,
24
+ gimSlantIonoDelayL1,
25
+ gimVerticalTec,
18
26
  ionoFree,
19
27
  klobucharDelay,
28
+ lambdaReduction,
29
+ lambdaSearch,
20
30
  satClockCorrection,
21
31
  solveDgnss,
22
32
  solveSpp,