gnss-js 2.13.1 → 2.14.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.
@@ -363,4 +363,138 @@ interface QualityResult {
363
363
  }
364
364
  declare function analyzeQuality(file: File, header: RinexHeader, onProgress?: (percent: number) => void, signal?: AbortSignal): Promise<QualityResult>;
365
365
 
366
- export { type CompleteSatSignal, type CompleteSignalStats, CompletenessAccumulator, type CompletenessResult, CycleSlipAccumulator, type CycleSlipEvent, type CycleSlipResult, type CycleSlipSignalStats, IonoAccumulator, type IonoDcbResult, type IonoPoint, type IonoRatePoint, type IonoRateSeries, type IonoResult, type IonoSeries, MultipathAccumulator, type MultipathPoint, type MultipathResult, type MultipathSeries, type MultipathSignalStat, type QualityResult, type SatDcbMap, analyzeQuality, applyIonoDcb, computeIonoRate, detrendIonoArcs, parseSinexBiasDcb };
366
+ /** One position solution (e.g. a daily PPP result). */
367
+ interface CoordinateSample {
368
+ /** Epoch of the solution (ms), e.g. the session midpoint. */
369
+ tMs: number;
370
+ /** Position, ECEF (m), all in one reference frame. */
371
+ ecef: readonly [number, number, number];
372
+ /** Formal 1σ in East/North/Up (m), if known. */
373
+ sigmaEnu?: readonly [number, number, number];
374
+ }
375
+ interface SeriesFitOptions {
376
+ /** Known step epochs (ms): antenna/receiver changes, earthquakes… */
377
+ jumps?: number[];
378
+ /** Search for further jumps (BIC). Default true. */
379
+ detectJumps?: boolean;
380
+ /** Most jumps the search adds. Default 3. */
381
+ maxJumps?: number;
382
+ /** Annual + semi-annual terms: true, false or 'auto' (span ≥ 2 years). */
383
+ seasonal?: boolean | 'auto';
384
+ /** Outlier threshold in robust standard deviations. Default 4. */
385
+ outlierK?: number;
386
+ /** Reference epoch for the offset (ms). Default: middle of the span. */
387
+ refEpochMs?: number;
388
+ /** Samples a detected jump needs on each side. Default 5. */
389
+ minSamplesPerSide?: number;
390
+ /** Fit white + flicker noise for realistic velocity sigmas. Default true. */
391
+ noiseModel?: boolean;
392
+ }
393
+ interface SeriesComponent {
394
+ /** Value at the reference epoch (m), relative to the reference position. */
395
+ offset: number;
396
+ /** Velocity (m/yr). */
397
+ velocity: number;
398
+ /** White-noise formal 1σ of the velocity, scaled by the variance factor. */
399
+ velocitySigma: number;
400
+ /** Velocity 1σ under a white + flicker noise model fitted by maximum
401
+ * likelihood — the realistic uncertainty; null if the series is too
402
+ * short or `noiseModel` is off. */
403
+ velocitySigmaRealistic: number | null;
404
+ /** Share of the residual variance the noise fit attributes to flicker. */
405
+ flickerShare: number | null;
406
+ /** Step size at each jump (m), in the order of `SeriesFit.jumps`. */
407
+ steps: number[];
408
+ /** Annual / semi-annual amplitudes (m), when fitted. */
409
+ annualAmplitude: number | null;
410
+ semiannualAmplitude: number | null;
411
+ /** Weighted RMS of the residuals of the used samples (m). */
412
+ wrms: number;
413
+ }
414
+ interface SeriesPoint {
415
+ tMs: number;
416
+ /** Position relative to the reference position, local ENU (m). */
417
+ enu: [number, number, number];
418
+ /** Model value at tMs (m). */
419
+ model: [number, number, number];
420
+ /** Rejected as an outlier. */
421
+ outlier: boolean;
422
+ }
423
+ interface SeriesFit {
424
+ /** Median position the ENU values are relative to (ECEF, m). */
425
+ refEcef: [number, number, number];
426
+ refEpochMs: number;
427
+ e: SeriesComponent;
428
+ n: SeriesComponent;
429
+ u: SeriesComponent;
430
+ /** Jumps in the model, chronological; `detected` = found by the search. */
431
+ jumps: {
432
+ tMs: number;
433
+ detected: boolean;
434
+ }[];
435
+ points: SeriesPoint[];
436
+ spanYears: number;
437
+ used: number;
438
+ outliers: number;
439
+ seasonal: boolean;
440
+ }
441
+ /**
442
+ * Fit the trajectory model to a station's position series. Needs at least
443
+ * five samples spanning some time; the velocity is meaningful once the span
444
+ * covers a good part of a year (seasonal signals otherwise alias into it).
445
+ */
446
+ declare function fitCoordinateSeries(samples: readonly CoordinateSample[], opts?: SeriesFitOptions): SeriesFit | null;
447
+
448
+ /**
449
+ * Network alignment: the transformation that maps one day's solutions of
450
+ * reference stations onto their known coordinates, applied to every station
451
+ * of that day.
452
+ *
453
+ * Daily PPP positions share errors over a region — orbit and clock errors
454
+ * of the day, the frame realisation of the product — so a station's series
455
+ * scatters more than the station moves. Estimating those common errors from
456
+ * reference stations with trusted coordinates (IGS/EUREF, propagated to the
457
+ * day by their velocities) and removing them is the regional counterpart of
458
+ * Bernese's datum definition by minimum constraints, and of "common-mode
459
+ * filtering" in GNSS time-series work.
460
+ *
461
+ * Model (position-vector convention, small angles, as `applyHelmert`):
462
+ * x_ref = x + t + d·x + r × x
463
+ * 'translation' estimates t only — the robust choice for a regional network,
464
+ * where rotations are nearly indistinguishable from translations; 'similarity'
465
+ * adds rotation and scale (7 parameters) for a continental or wider spread.
466
+ */
467
+ type HelmertModel = 'translation' | 'similarity';
468
+ interface HelmertFit {
469
+ model: HelmertModel;
470
+ /** Translation (m). */
471
+ t: [number, number, number];
472
+ /** Rotation (rad), zero for 'translation'. */
473
+ r: [number, number, number];
474
+ /** Scale (unitless, e.g. 1e-9 = 1 ppb), zero for 'translation'. */
475
+ d: number;
476
+ /** Residual (reference − transformed) per input pair, ECEF (m). */
477
+ residuals: [number, number, number][];
478
+ /** Pairs kept in the final fit (false = rejected reference). */
479
+ used: boolean[];
480
+ /** RMS of the used residuals, per coordinate (m). */
481
+ rms: number;
482
+ }
483
+ interface HelmertOptions {
484
+ model?: HelmertModel;
485
+ /** A reference whose 3-D residual exceeds this is dropped (m). Default 0.015. */
486
+ maxResidualM?: number;
487
+ /** Keep at least this many references. Default: 3 (translation) or 4. */
488
+ minRefs?: number;
489
+ }
490
+ type V3 = readonly [number, number, number];
491
+ /** Apply an estimated transformation to one position. */
492
+ declare function applyHelmertFit(x: V3, f: HelmertFit): [number, number, number];
493
+ /**
494
+ * Estimate the transformation from `from` (e.g. the day's PPP positions of
495
+ * the reference stations) to `to` (their known coordinates at that epoch).
496
+ * Rejects the worst-fitting reference while any exceeds `maxResidualM`.
497
+ */
498
+ declare function estimateHelmert(from: readonly V3[], to: readonly V3[], opts?: HelmertOptions): HelmertFit | null;
499
+
500
+ export { type CompleteSatSignal, type CompleteSignalStats, CompletenessAccumulator, type CompletenessResult, type CoordinateSample, CycleSlipAccumulator, type CycleSlipEvent, type CycleSlipResult, type CycleSlipSignalStats, type HelmertFit, type HelmertModel, type HelmertOptions, IonoAccumulator, type IonoDcbResult, type IonoPoint, type IonoRatePoint, type IonoRateSeries, type IonoResult, type IonoSeries, MultipathAccumulator, type MultipathPoint, type MultipathResult, type MultipathSeries, type MultipathSignalStat, type QualityResult, type SatDcbMap, type SeriesComponent, type SeriesFit, type SeriesFitOptions, type SeriesPoint, analyzeQuality, applyHelmertFit, applyIonoDcb, computeIonoRate, detrendIonoArcs, estimateHelmert, fitCoordinateSeries, parseSinexBiasDcb };
package/dist/analysis.js CHANGED
@@ -4,11 +4,16 @@ import {
4
4
  IonoAccumulator,
5
5
  MultipathAccumulator,
6
6
  analyzeQuality,
7
+ applyHelmertFit,
7
8
  applyIonoDcb,
8
9
  computeIonoRate,
9
10
  detrendIonoArcs,
11
+ estimateHelmert,
12
+ fitCoordinateSeries,
10
13
  parseSinexBiasDcb
11
- } from "./chunk-MFCPYUEI.js";
14
+ } from "./chunk-GGWK7XLZ.js";
15
+ import "./chunk-37QNKGTC.js";
16
+ import "./chunk-6FAL6P4G.js";
12
17
  import "./chunk-FEABDV5P.js";
13
18
  import "./chunk-FIEWO4J4.js";
14
19
  export {
@@ -17,8 +22,11 @@ export {
17
22
  IonoAccumulator,
18
23
  MultipathAccumulator,
19
24
  analyzeQuality,
25
+ applyHelmertFit,
20
26
  applyIonoDcb,
21
27
  computeIonoRate,
22
28
  detrendIonoArcs,
29
+ estimateHelmert,
30
+ fitCoordinateSeries,
23
31
  parseSinexBiasDcb
24
32
  };
@@ -1,3 +1,6 @@
1
+ import {
2
+ ecefToGeodetic
3
+ } from "./chunk-37QNKGTC.js";
1
4
  import {
2
5
  parseRinexStream
3
6
  } from "./chunk-FEABDV5P.js";
@@ -935,6 +938,495 @@ async function analyzeQuality(file, header, onProgress, signal) {
935
938
  };
936
939
  }
937
940
 
941
+ // src/analysis/noise-model.ts
942
+ function cholesky(M, n) {
943
+ for (let j = 0; j < n; j++) {
944
+ let d = M[j * n + j];
945
+ for (let k = 0; k < j; k++) d -= M[j * n + k] ** 2;
946
+ if (d <= 0) return false;
947
+ d = Math.sqrt(d);
948
+ M[j * n + j] = d;
949
+ for (let i = j + 1; i < n; i++) {
950
+ let s = M[i * n + j];
951
+ for (let k = 0; k < j; k++) s -= M[i * n + k] * M[j * n + k];
952
+ M[i * n + j] = s / d;
953
+ }
954
+ }
955
+ return true;
956
+ }
957
+ function forward(L, n, b) {
958
+ for (let i = 0; i < n; i++) {
959
+ let s = b[i];
960
+ for (let k = 0; k < i; k++) s -= L[i * n + k] * b[k];
961
+ b[i] = s / L[i * n + i];
962
+ }
963
+ }
964
+ function flickerCovariance(idx) {
965
+ const N = idx[idx.length - 1] + 1;
966
+ const h = new Float64Array(N);
967
+ h[0] = 1;
968
+ for (let i = 1; i < N; i++) h[i] = h[i - 1] * (i - 1 + 0.5) / i;
969
+ const n = idx.length;
970
+ const F = new Float64Array(n * n);
971
+ for (let a = 0; a < n; a++)
972
+ for (let b = 0; b <= a; b++) {
973
+ const ia = idx[a];
974
+ const ib = idx[b];
975
+ let s = 0;
976
+ for (let k = 0; k <= ib; k++) s += h[ia - k] * h[ib - k];
977
+ F[a * n + b] = s;
978
+ F[b * n + a] = s;
979
+ }
980
+ let tr = 0;
981
+ for (let a = 0; a < n; a++) tr += F[a * n + a];
982
+ const scale = n / tr;
983
+ for (let i = 0; i < F.length; i++) F[i] *= scale;
984
+ return F;
985
+ }
986
+ var MAX_SAMPLES = 360;
987
+ function fitWhiteFlicker(A, y, tYears) {
988
+ let rows = A.map((_, i) => i);
989
+ if (rows.length > MAX_SAMPLES) {
990
+ const step = rows.length / MAX_SAMPLES;
991
+ rows = Array.from({ length: MAX_SAMPLES }, (_, k) => Math.floor(k * step));
992
+ }
993
+ const n = rows.length;
994
+ const p = A[0]?.length ?? 0;
995
+ if (n < p + 5) return null;
996
+ const dts = rows.slice(1).map((r, k) => tYears[r] - tYears[rows[k]]).filter((d) => d > 0).sort((a, b) => a - b);
997
+ const dt = dts[dts.length >> 1] ?? 1 / 365.25;
998
+ const t0 = tYears[rows[0]];
999
+ const idx = rows.map((r) => Math.round((tYears[r] - t0) / dt));
1000
+ for (let k = 1; k < n; k++)
1001
+ if (idx[k] <= idx[k - 1]) idx[k] = idx[k - 1] + 1;
1002
+ const F = flickerCovariance(idx);
1003
+ const evaluate = (phi) => {
1004
+ const C = new Float64Array(n * n);
1005
+ for (let i = 0; i < n * n; i++) C[i] = phi * F[i];
1006
+ for (let i = 0; i < n; i++) C[i * n + i] += 1 - phi;
1007
+ if (!cholesky(C, n)) return null;
1008
+ const Aw = Array.from({ length: p }, (_, j) => {
1009
+ const col = Float64Array.from(rows, (r) => A[r][j]);
1010
+ forward(C, n, col);
1011
+ return col;
1012
+ });
1013
+ const yw = Float64Array.from(rows, (r) => y[r]);
1014
+ forward(C, n, yw);
1015
+ const N = Array.from(
1016
+ { length: p },
1017
+ (_, a) => Array.from({ length: p }, (_2, b) => {
1018
+ let s = 0;
1019
+ for (let i = 0; i < n; i++) s += Aw[a][i] * Aw[b][i];
1020
+ return s;
1021
+ })
1022
+ );
1023
+ const rhs = Array.from({ length: p }, (_, a) => {
1024
+ let s = 0;
1025
+ for (let i = 0; i < n; i++) s += Aw[a][i] * yw[i];
1026
+ return s;
1027
+ });
1028
+ const Ninv = invert(N);
1029
+ if (!Ninv) return null;
1030
+ const beta = Ninv.map((row) => row.reduce((s, v, k) => s + v * rhs[k], 0));
1031
+ let rss = 0;
1032
+ for (let i = 0; i < n; i++) {
1033
+ let m = 0;
1034
+ for (let j = 0; j < p; j++) m += Aw[j][i] * beta[j];
1035
+ rss += (yw[i] - m) ** 2;
1036
+ }
1037
+ const sigma2 = rss / n;
1038
+ let logdet = 0;
1039
+ for (let i = 0; i < n; i++) logdet += 2 * Math.log(C[i * n + i]);
1040
+ const logL = -0.5 * (n * Math.log(sigma2) + logdet + n);
1041
+ return {
1042
+ logL,
1043
+ velocity: beta[1],
1044
+ velocitySigma: Math.sqrt(sigma2 * Ninv[1][1])
1045
+ };
1046
+ };
1047
+ let best = null;
1048
+ for (let k = 0; k <= 20; k++) {
1049
+ const phi = Math.min(k / 20, 0.995);
1050
+ const r = evaluate(phi);
1051
+ if (r && (!best || r.logL > best.r.logL)) best = { phi, r };
1052
+ }
1053
+ if (!best) return null;
1054
+ let lo = Math.max(0, best.phi - 0.05);
1055
+ let hi = Math.min(0.995, best.phi + 0.05);
1056
+ const g = (Math.sqrt(5) - 1) / 2;
1057
+ for (let it = 0; it < 12; it++) {
1058
+ const a = hi - g * (hi - lo);
1059
+ const b = lo + g * (hi - lo);
1060
+ const ra = evaluate(a);
1061
+ const rb = evaluate(b);
1062
+ if (!ra || !rb) break;
1063
+ if (ra.logL > rb.logL) {
1064
+ hi = b;
1065
+ if (ra.logL > best.r.logL) best = { phi: a, r: ra };
1066
+ } else {
1067
+ lo = a;
1068
+ if (rb.logL > best.r.logL) best = { phi: b, r: rb };
1069
+ }
1070
+ }
1071
+ return {
1072
+ flickerShare: best.phi,
1073
+ velocitySigma: best.r.velocitySigma,
1074
+ velocity: best.r.velocity
1075
+ };
1076
+ }
1077
+ function invert(N) {
1078
+ const p = N.length;
1079
+ const M = N.map((row, i) => [
1080
+ ...row,
1081
+ ...Array.from({ length: p }, (_, j) => i === j ? 1 : 0)
1082
+ ]);
1083
+ for (let c = 0; c < p; c++) {
1084
+ let piv = c;
1085
+ for (let r = c + 1; r < p; r++)
1086
+ if (Math.abs(M[r][c]) > Math.abs(M[piv][c])) piv = r;
1087
+ if (Math.abs(M[piv][c]) < 1e-14) return null;
1088
+ [M[c], M[piv]] = [M[piv], M[c]];
1089
+ const d = M[c][c];
1090
+ for (let k = 0; k < 2 * p; k++) M[c][k] /= d;
1091
+ for (let r = 0; r < p; r++) {
1092
+ if (r === c) continue;
1093
+ const f = M[r][c];
1094
+ if (f === 0) continue;
1095
+ for (let k = 0; k < 2 * p; k++) M[r][k] -= f * M[c][k];
1096
+ }
1097
+ }
1098
+ return M.map((row) => row.slice(p));
1099
+ }
1100
+
1101
+ // src/analysis/coordinate-series.ts
1102
+ var MS_PER_YEAR = 365.25 * 864e5;
1103
+ var TWO_PI = 2 * Math.PI;
1104
+ var MIN_SIGMA_M = 5e-4;
1105
+ function lsq(A, y, w) {
1106
+ const p = A[0]?.length ?? 0;
1107
+ const N = Array.from({ length: p }, () => new Array(p).fill(0));
1108
+ const b = new Array(p).fill(0);
1109
+ for (let i = 0; i < A.length; i++) {
1110
+ const a = A[i];
1111
+ for (let j = 0; j < p; j++) {
1112
+ b[j] += w[i] * a[j] * y[i];
1113
+ for (let k = 0; k < p; k++) N[j][k] += w[i] * a[j] * a[k];
1114
+ }
1115
+ }
1116
+ const M = N.map((row, i) => [
1117
+ ...row,
1118
+ ...Array.from({ length: p }, (_, j) => i === j ? 1 : 0)
1119
+ ]);
1120
+ for (let c = 0; c < p; c++) {
1121
+ let piv = c;
1122
+ for (let r = c + 1; r < p; r++)
1123
+ if (Math.abs(M[r][c]) > Math.abs(M[piv][c])) piv = r;
1124
+ if (Math.abs(M[piv][c]) < 1e-12) return null;
1125
+ [M[c], M[piv]] = [M[piv], M[c]];
1126
+ const d = M[c][c];
1127
+ for (let k = 0; k < 2 * p; k++) M[c][k] /= d;
1128
+ for (let r = 0; r < p; r++) {
1129
+ if (r === c) continue;
1130
+ const f = M[r][c];
1131
+ if (f === 0) continue;
1132
+ for (let k = 0; k < 2 * p; k++) M[r][k] -= f * M[c][k];
1133
+ }
1134
+ }
1135
+ const Ninv = M.map((row) => row.slice(p));
1136
+ const x = Ninv.map((row) => row.reduce((s, v, k) => s + v * b[k], 0));
1137
+ return { x, Ninv };
1138
+ }
1139
+ function median2(xs) {
1140
+ const s = [...xs].sort((a, b) => a - b);
1141
+ const m = s.length >> 1;
1142
+ return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
1143
+ }
1144
+ function robustSigma(xs) {
1145
+ if (xs.length < 2) return 0;
1146
+ const m = median2(xs);
1147
+ return 1.4826 * median2(xs.map((x) => Math.abs(x - m)));
1148
+ }
1149
+ function designRow(tMs, d) {
1150
+ const dt = (tMs - d.refEpochMs) / MS_PER_YEAR;
1151
+ const row = [1, dt];
1152
+ for (const j of d.jumps) row.push(tMs >= j ? 1 : 0);
1153
+ if (d.seasonal) {
1154
+ const ph = TWO_PI * (tMs / MS_PER_YEAR);
1155
+ row.push(Math.sin(ph), Math.cos(ph), Math.sin(2 * ph), Math.cos(2 * ph));
1156
+ }
1157
+ return row;
1158
+ }
1159
+ function fitComponent(t, y, w, used, d) {
1160
+ const A = [];
1161
+ const yy = [];
1162
+ const ww = [];
1163
+ for (let i = 0; i < t.length; i++) {
1164
+ if (!used[i]) continue;
1165
+ A.push(designRow(t[i], d));
1166
+ yy.push(y[i]);
1167
+ ww.push(w[i]);
1168
+ }
1169
+ if (A.length <= (A[0]?.length ?? 0)) return null;
1170
+ const s = lsq(A, yy, ww);
1171
+ if (!s) return null;
1172
+ const residuals = t.map(
1173
+ (ti, i) => y[i] - designRow(ti, d).reduce((acc, a, k) => acc + a * s.x[k], 0)
1174
+ );
1175
+ let wrss = 0;
1176
+ let sumW = 0;
1177
+ for (let i = 0; i < t.length; i++) {
1178
+ if (!used[i]) continue;
1179
+ wrss += w[i] * residuals[i] ** 2;
1180
+ sumW += w[i];
1181
+ }
1182
+ return { ...s, wrss, sumW, residuals };
1183
+ }
1184
+ function fitCoordinateSeries(samples, opts = {}) {
1185
+ const pts = [...samples].sort((a, b) => a.tMs - b.tMs);
1186
+ if (pts.length < 5) return null;
1187
+ const t = pts.map((p) => p.tMs);
1188
+ const spanYears = (t[t.length - 1] - t[0]) / MS_PER_YEAR;
1189
+ if (spanYears <= 0) return null;
1190
+ const ref = [0, 1, 2].map(
1191
+ (k2) => median2(pts.map((p) => p.ecef[k2]))
1192
+ );
1193
+ const [lat, lon] = ecefToGeodetic(ref[0], ref[1], ref[2]);
1194
+ const sl = Math.sin(lat);
1195
+ const cl = Math.cos(lat);
1196
+ const so = Math.sin(lon);
1197
+ const co = Math.cos(lon);
1198
+ const enu = pts.map((p) => {
1199
+ const dx = p.ecef[0] - ref[0];
1200
+ const dy = p.ecef[1] - ref[1];
1201
+ const dz = p.ecef[2] - ref[2];
1202
+ return [
1203
+ -so * dx + co * dy,
1204
+ -sl * co * dx - sl * so * dy + cl * dz,
1205
+ cl * co * dx + cl * so * dy + sl * dz
1206
+ ];
1207
+ });
1208
+ const weights = [0, 1, 2].map(
1209
+ (k2) => pts.map((p) => {
1210
+ const s = p.sigmaEnu?.[k2];
1211
+ return s && s > 0 ? 1 / Math.max(s, MIN_SIGMA_M) ** 2 : 1;
1212
+ })
1213
+ );
1214
+ const series = [0, 1, 2].map((k2) => enu.map((v) => v[k2]));
1215
+ const seasonal = opts.seasonal === "auto" || opts.seasonal === void 0 ? spanYears >= 2 : opts.seasonal;
1216
+ const design = {
1217
+ refEpochMs: opts.refEpochMs ?? (t[0] + t[t.length - 1]) / 2,
1218
+ jumps: [...opts.jumps ?? []].filter((j) => j > t[0] && j <= t[t.length - 1]).sort((a, b) => a - b),
1219
+ seasonal
1220
+ };
1221
+ const detected = /* @__PURE__ */ new Set();
1222
+ const k = opts.outlierK ?? 4;
1223
+ const used = pts.map(() => true);
1224
+ const fitAll = () => [0, 1, 2].map(
1225
+ (c) => fitComponent(t, series[c], weights[c], used, design)
1226
+ );
1227
+ const robustPass = () => {
1228
+ for (let iter = 0; iter < 10; iter++) {
1229
+ const sol2 = fitAll();
1230
+ if (sol2.some((s) => !s)) return null;
1231
+ const scales = sol2.map(
1232
+ (s) => Math.max(robustSigma(s.residuals.filter((_, i) => used[i])), 1e-4)
1233
+ );
1234
+ let changed = false;
1235
+ for (let i = 0; i < t.length; i++) {
1236
+ const bad = sol2.some(
1237
+ (s, c) => Math.abs(s.residuals[i]) > k * scales[c]
1238
+ );
1239
+ if (bad === used[i]) {
1240
+ used[i] = !bad;
1241
+ changed = true;
1242
+ }
1243
+ }
1244
+ if (!changed) return sol2;
1245
+ }
1246
+ return fitAll();
1247
+ };
1248
+ let sol = robustPass();
1249
+ if (!sol) return null;
1250
+ if (opts.detectJumps ?? true) {
1251
+ const minSide = opts.minSamplesPerSide ?? 5;
1252
+ for (let added = 0; added < (opts.maxJumps ?? 3); added++) {
1253
+ const idx = t.map((_, i) => i).filter((i) => used[i]);
1254
+ const nUsed = idx.length;
1255
+ const bicOf = (s, p) => s.reduce((acc, c) => acc + nUsed * Math.log(c.wrss / c.sumW), 0) + 3 * p * Math.log(nUsed);
1256
+ const p0 = designRow(t[0], design).length;
1257
+ const bic0 = bicOf(sol, p0);
1258
+ let best = null;
1259
+ for (let j = minSide; j <= nUsed - minSide; j++) {
1260
+ const tj = (t[idx[j - 1]] + t[idx[j]]) / 2;
1261
+ if (design.jumps.some((x) => Math.abs(x - tj) < 7 * 864e5))
1262
+ continue;
1263
+ const trial = {
1264
+ ...design,
1265
+ jumps: [...design.jumps, tj].sort((a, b) => a - b)
1266
+ };
1267
+ const s = [0, 1, 2].map(
1268
+ (c) => fitComponent(t, series[c], weights[c], used, trial)
1269
+ );
1270
+ if (s.some((x) => !x)) continue;
1271
+ const bic = bicOf(s, p0 + 1);
1272
+ if (!best || bic < best.bic)
1273
+ best = { tMs: tj, bic, sol: s };
1274
+ }
1275
+ if (!best || bic0 - best.bic <= 10) break;
1276
+ design.jumps = [...design.jumps, best.tMs].sort((a, b) => a - b);
1277
+ detected.add(best.tMs);
1278
+ sol = robustPass();
1279
+ if (!sol) return null;
1280
+ }
1281
+ }
1282
+ const nJ = design.jumps.length;
1283
+ const component = (c) => {
1284
+ const s = sol[c];
1285
+ const nUsed = used.filter(Boolean).length;
1286
+ const p = s.x.length;
1287
+ const vf = nUsed > p ? s.wrss / (nUsed - p) : 1;
1288
+ const amp = (i) => seasonal ? Math.hypot(s.x[i], s.x[i + 1]) : null;
1289
+ const noise = opts.noiseModel ?? true ? fitWhiteFlicker(
1290
+ t.flatMap((ti, i) => used[i] ? [designRow(ti, design)] : []),
1291
+ series[c].filter((_, i) => used[i]),
1292
+ t.flatMap((ti, i) => used[i] ? [ti / MS_PER_YEAR] : [])
1293
+ ) : null;
1294
+ return {
1295
+ offset: s.x[0],
1296
+ velocity: s.x[1],
1297
+ velocitySigma: Math.sqrt(s.Ninv[1][1] * vf),
1298
+ velocitySigmaRealistic: noise?.velocitySigma ?? null,
1299
+ flickerShare: noise?.flickerShare ?? null,
1300
+ steps: s.x.slice(2, 2 + nJ),
1301
+ annualAmplitude: amp(2 + nJ),
1302
+ semiannualAmplitude: amp(4 + nJ),
1303
+ wrms: Math.sqrt(s.wrss / s.sumW)
1304
+ };
1305
+ };
1306
+ const points = pts.map((p, i) => ({
1307
+ tMs: p.tMs,
1308
+ enu: enu[i],
1309
+ model: [0, 1, 2].map((c) => series[c][i] - sol[c].residuals[i]),
1310
+ outlier: !used[i]
1311
+ }));
1312
+ return {
1313
+ refEcef: ref,
1314
+ refEpochMs: design.refEpochMs,
1315
+ e: component(0),
1316
+ n: component(1),
1317
+ u: component(2),
1318
+ jumps: design.jumps.map((tMs) => ({ tMs, detected: detected.has(tMs) })),
1319
+ points,
1320
+ spanYears,
1321
+ used: used.filter(Boolean).length,
1322
+ outliers: used.filter((u) => !u).length,
1323
+ seasonal
1324
+ };
1325
+ }
1326
+
1327
+ // src/analysis/network-alignment.ts
1328
+ function applyHelmertFit(x, f) {
1329
+ const [r1, r2, r3] = f.r;
1330
+ return [
1331
+ x[0] + f.t[0] + f.d * x[0] - r3 * x[1] + r2 * x[2],
1332
+ x[1] + f.t[1] + r3 * x[0] + f.d * x[1] - r1 * x[2],
1333
+ x[2] + f.t[2] - r2 * x[0] + r1 * x[1] + f.d * x[2]
1334
+ ];
1335
+ }
1336
+ function solve(N, b) {
1337
+ const n = b.length;
1338
+ const M = N.map((row, i) => [...row, b[i]]);
1339
+ for (let c = 0; c < n; c++) {
1340
+ let p = c;
1341
+ for (let r = c + 1; r < n; r++)
1342
+ if (Math.abs(M[r][c]) > Math.abs(M[p][c])) p = r;
1343
+ if (Math.abs(M[p][c]) < 1e-6) return null;
1344
+ [M[c], M[p]] = [M[p], M[c]];
1345
+ for (let r = 0; r < n; r++) {
1346
+ if (r === c) continue;
1347
+ const f = M[r][c] / M[c][c];
1348
+ for (let k = c; k <= n; k++) M[r][k] -= f * M[c][k];
1349
+ }
1350
+ }
1351
+ return M.map((row, i) => row[n] / row[i]);
1352
+ }
1353
+ function estimateHelmert(from, to, opts = {}) {
1354
+ const model = opts.model ?? "translation";
1355
+ const nPar = model === "translation" ? 3 : 7;
1356
+ const minRefs = opts.minRefs ?? (model === "translation" ? 3 : 4);
1357
+ const maxRes = opts.maxResidualM ?? 0.015;
1358
+ if (from.length !== to.length) return null;
1359
+ const used = from.map(() => true);
1360
+ const c = [0, 1, 2].map(
1361
+ (k) => from.reduce((s, p) => s + p[k], 0) / Math.max(from.length, 1)
1362
+ );
1363
+ for (; ; ) {
1364
+ const idx = used.flatMap((u, i) => u ? [i] : []);
1365
+ if (idx.length < minRefs || idx.length * 3 <= nPar) return null;
1366
+ const N = Array.from(
1367
+ { length: nPar },
1368
+ () => new Array(nPar).fill(0)
1369
+ );
1370
+ const b = new Array(nPar).fill(0);
1371
+ for (const i of idx) {
1372
+ const x = [from[i][0] - c[0], from[i][1] - c[1], from[i][2] - c[2]];
1373
+ for (let k = 0; k < 3; k++) {
1374
+ const row = new Array(nPar).fill(0);
1375
+ row[k] = 1;
1376
+ if (nPar === 7) {
1377
+ if (k === 0) {
1378
+ row[4] = x[2];
1379
+ row[5] = -x[1];
1380
+ } else if (k === 1) {
1381
+ row[3] = -x[2];
1382
+ row[5] = x[0];
1383
+ } else {
1384
+ row[3] = x[1];
1385
+ row[4] = -x[0];
1386
+ }
1387
+ row[6] = x[k];
1388
+ }
1389
+ const y = to[i][k] - from[i][k];
1390
+ for (let a = 0; a < nPar; a++) {
1391
+ b[a] += row[a] * y;
1392
+ for (let bb = 0; bb < nPar; bb++) N[a][bb] += row[a] * row[bb];
1393
+ }
1394
+ }
1395
+ }
1396
+ const p = solve(N, b);
1397
+ if (!p) return null;
1398
+ const r = nPar === 7 ? [p[3], p[4], p[5]] : [0, 0, 0];
1399
+ const d = nPar === 7 ? p[6] : 0;
1400
+ const t = [
1401
+ p[0] - (d * c[0] - r[2] * c[1] + r[1] * c[2]),
1402
+ p[1] - (r[2] * c[0] + d * c[1] - r[0] * c[2]),
1403
+ p[2] - (-r[1] * c[0] + r[0] * c[1] + d * c[2])
1404
+ ];
1405
+ const fit = { model, t, r, d, residuals: [], used, rms: 0 };
1406
+ fit.residuals = from.map((x, i) => {
1407
+ const y = applyHelmertFit(x, fit);
1408
+ return [to[i][0] - y[0], to[i][1] - y[1], to[i][2] - y[2]];
1409
+ });
1410
+ let worst = -1;
1411
+ let worstNorm = maxRes;
1412
+ let ss = 0;
1413
+ for (const i of idx) {
1414
+ const n = Math.hypot(...fit.residuals[i]);
1415
+ ss += n * n;
1416
+ if (n > worstNorm) {
1417
+ worstNorm = n;
1418
+ worst = i;
1419
+ }
1420
+ }
1421
+ fit.rms = Math.sqrt(ss / (3 * idx.length));
1422
+ if (worst < 0 || idx.length <= minRefs) {
1423
+ fit.used = [...used];
1424
+ return fit;
1425
+ }
1426
+ used[worst] = false;
1427
+ }
1428
+ }
1429
+
938
1430
  export {
939
1431
  MultipathAccumulator,
940
1432
  CycleSlipAccumulator,
@@ -944,5 +1436,8 @@ export {
944
1436
  detrendIonoArcs,
945
1437
  parseSinexBiasDcb,
946
1438
  applyIonoDcb,
947
- analyzeQuality
1439
+ analyzeQuality,
1440
+ fitCoordinateSeries,
1441
+ applyHelmertFit,
1442
+ estimateHelmert
948
1443
  };