gnsskit 0.4.0__py3-none-any.whl

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.
@@ -0,0 +1,448 @@
1
+ """Least-squares adjustment of a GNSS baseline network with fixed points.
2
+
3
+ Notation (as in the course notebooks):
4
+ y = A x + b baselines X_rover - X_base = dxyz, fixed points in b
5
+ y0 = y + v, v ~ N(0, Q) observations y0 (n values), parameters x (m values)
6
+ N = A^T Q^-1 A
7
+ X_hat = N^-1 A^T Q^-1 (y0 - b), Y_hat = A X_hat + b, v_hat = y0 - Y_hat
8
+ sigma_v_hat = v_hat^T Q^-1 v_hat / (n - m) a-posteriori variance factor
9
+ Cxx = sigma_v_hat N^-1, Cyy = sigma_v_hat A N^-1 A^T, Cvv = sigma_v_hat (Q - A N^-1 A^T)
10
+
11
+ Stochastic model of a baseline (`stochastic` argument of `adjust_network`):
12
+ "rtklib_scaled": formal RTKLIB covariance times a variance factor k_s (default, see K_S).
13
+ "rtklib": formal covariance of the RTKLIB solution; optimistic (sigma_v_hat >> 1),
14
+ because RTKLIB treats the epochs as independent.
15
+ "apriori": east/north/up errors, uncorrelated, with sd = a + b[ppm] * length.
16
+
17
+ Scaling Q by k_s leaves X_hat, Cxx, Cvv and tau unchanged and divides sigma_v_hat by k_s:
18
+ only the global test depends on it.
19
+
20
+ Layout: the stochastic model (Q) and the functional model (A, b) are built from the
21
+ baselines, then `weighted_least_squares` solves the generic problem, which does not depend
22
+ on GNSS and can be reused for other adjustments.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from collections.abc import Callable, Mapping, Sequence
28
+ from dataclasses import dataclass
29
+ from typing import Literal
30
+
31
+ import numpy as np
32
+ import pandas as pd
33
+
34
+ from ..refsys import ENUFrame, ecef_to_llh
35
+ from .baseline import BaselineResult
36
+
37
+ try:
38
+ from pyproj import Transformer
39
+ from scipy.stats import chi2
40
+ except ImportError as e: # pragma: no cover
41
+ raise ImportError(
42
+ "gnsskit.static.network needs the 'network' extra: pip install 'gnsskit[network]'"
43
+ ) from e
44
+
45
+ # =============================================================================
46
+ # Defaults and thresholds
47
+ # =============================================================================
48
+
49
+ # Default a-priori baseline precision as (constant [m], ppm of the length)
50
+ SIGMA_H = (5e-3, 1.0) # horizontal: 5 mm + 1 ppm
51
+ SIGMA_V = (10e-3, 2.0) # vertical: 10 mm + 2 ppm
52
+
53
+ # Variance factor of the RTKLIB covariance for 5 s epochs. RTKLIB treats the epochs as independent; with errors correlated as exp(-dt / T), the variance of the mean is too small by (1 + rho) / (1 - rho) ~ 2 T / dt. T ~ 60 s (multipath at a static site), dt = 5 s -> 24.
54
+ # For another interval use k_s ~ 2 * 60 / dt, e.g. 120 for 1 s, 8 for 15 s.
55
+ K_S = 25.0
56
+
57
+ # RTKLIB prints standard deviations rounded to 0.1 mm, which can make a 3x3 covariance matrix non-positive-definite; its eigenvalues are floored at this variance before inversion.
58
+ MIN_VARIANCE = 1e-6**2 # m^2, (1 um)^2: far below any GNSS precision, touches only broken matrices
59
+
60
+ TAU_CRITICAL = 3.29 # two-sided 0.1 % level of a standard normal (Baarda data snooping)
61
+
62
+
63
+ # =============================================================================
64
+ # Generic weighted least squares
65
+ # =============================================================================
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class LsqSolution:
70
+ """Solution of y0 = A x + b + v, v ~ N(0, sigma^2 Q) (see the module docstring)."""
71
+
72
+ X_hat: np.ndarray # (m,) estimated parameters
73
+ Y_hat: np.ndarray # (n,) adjusted observations
74
+ v_hat: np.ndarray # (n,) residuals
75
+ sigma_v_hat: float # a-posteriori variance factor, expected ~1
76
+ Cxx: np.ndarray # (m, m) covariance of the parameters
77
+ Cyy: np.ndarray # (n, n) covariance of the adjusted observations
78
+ Cvv: np.ndarray # (n, n) covariance of the residuals
79
+ R: np.ndarray # (n, n) redundancy matrix, trace = n - m
80
+ tau: np.ndarray # (n,) normalised residuals |v_i| / sd(v_i)
81
+
82
+
83
+ def weighted_least_squares(
84
+ A: np.ndarray, Q: np.ndarray, y0: np.ndarray, b: np.ndarray | None = None
85
+ ) -> LsqSolution:
86
+ """Weighted least-squares solution of y0 = A x + b + v with observation covariance Q.
87
+
88
+ Args:
89
+ A: (n, m) design matrix, full column rank.
90
+ Q: (n, n) a-priori covariance of the observations (cofactor matrix).
91
+ y0: (n,) observations.
92
+ b: (n,) known terms. None for zeros.
93
+
94
+ Returns:
95
+ Estimates, their covariances scaled by the a-posteriori variance factor, the
96
+ redundancy matrix and the normalised residuals.
97
+ """
98
+ b = np.zeros_like(y0) if b is None else b
99
+ n, m = A.shape
100
+
101
+ Q_inv = np.linalg.inv(Q) # weight matrix
102
+ N = A.T @ Q_inv @ A # normal matrix
103
+ X_hat = np.linalg.solve(N, A.T @ Q_inv @ (y0 - b))
104
+ Y_hat = A @ X_hat + b
105
+ v_hat = y0 - Y_hat
106
+ sigma_v_hat = float(v_hat @ Q_inv @ v_hat / (n - m))
107
+
108
+ N_inv = np.linalg.inv(N)
109
+ Q_vv = Q - A @ N_inv @ A.T # cofactor matrix of the residuals
110
+ Cvv = sigma_v_hat * Q_vv
111
+ return LsqSolution(
112
+ X_hat=X_hat,
113
+ Y_hat=Y_hat,
114
+ v_hat=v_hat,
115
+ sigma_v_hat=sigma_v_hat,
116
+ Cxx=sigma_v_hat * N_inv,
117
+ Cyy=sigma_v_hat * (A @ N_inv @ A.T),
118
+ Cvv=Cvv,
119
+ R=Q_vv @ Q_inv,
120
+ tau=np.abs(v_hat) / np.sqrt(np.maximum(np.diag(Cvv), 1e-30)),
121
+ )
122
+
123
+
124
+ # =============================================================================
125
+ # Network adjustment result
126
+ # =============================================================================
127
+
128
+
129
+ @dataclass(frozen=True)
130
+ class Adjustment:
131
+ """Result of `adjust_network`. Observations are ordered baseline by baseline (dx, dy, dz),
132
+ parameters point by point (X, Y, Z), the points following `unknowns`."""
133
+
134
+ unknowns: list[str] # adjusted (free) points
135
+ fixed: dict[str, np.ndarray] # fixed points, ECEF m
136
+ baselines: list[BaselineResult]
137
+ y0: np.ndarray # (n,) observations
138
+ b: np.ndarray # (n,) known terms (fixed points)
139
+ A: np.ndarray # (n, m) design matrix
140
+ Q: np.ndarray # (n, n) covariance of the observations
141
+ X_hat: np.ndarray # (m,) estimated coordinates
142
+ Y_hat: np.ndarray # (n,) adjusted observations
143
+ v_hat: np.ndarray # (n,) residuals
144
+ sigma_v_hat: float # a-posteriori variance factor, expected ~1
145
+ Cxx: np.ndarray # (m, m) covariance of the parameters
146
+ Cyy: np.ndarray # (n, n) covariance of the adjusted observations
147
+ Cvv: np.ndarray # (n, n) covariance of the residuals
148
+ R: np.ndarray # (n, n) redundancy matrix, trace = n - m
149
+ tau: np.ndarray # (n,) normalised residuals
150
+
151
+ @property
152
+ def n(self) -> int:
153
+ """Number of observations."""
154
+ return self.y0.size
155
+
156
+ @property
157
+ def m(self) -> int:
158
+ """Number of estimated parameters."""
159
+ return self.X_hat.size
160
+
161
+ @property
162
+ def dof(self) -> int:
163
+ """Degrees of freedom, n - m."""
164
+ return self.n - self.m
165
+
166
+ @property
167
+ def xyz(self) -> np.ndarray:
168
+ """(len(unknowns), 3) adjusted ECEF coordinates."""
169
+ return self.X_hat.reshape(-1, 3)
170
+
171
+ @property
172
+ def sigma_v_hat_bounds(self) -> tuple[float, float]:
173
+ """95 % acceptance interval of sigma_v_hat under the chi-square test."""
174
+ lo, hi = chi2.ppf([0.025, 0.975], self.dof) / self.dof
175
+ return float(lo), float(hi)
176
+
177
+
178
+ # =============================================================================
179
+ # Stochastic model: covariance Q of the baselines
180
+ # =============================================================================
181
+
182
+
183
+ def _make_pd(cov: np.ndarray) -> np.ndarray:
184
+ """Covariance with eigenvalues floored at MIN_VARIANCE; unchanged if already above it."""
185
+ w, vec = np.linalg.eigh(cov)
186
+ if w.min() >= MIN_VARIANCE:
187
+ return cov
188
+ return (vec * np.maximum(w, MIN_VARIANCE)) @ vec.T
189
+
190
+
191
+ def _apriori_block(
192
+ length: float,
193
+ R_enu: np.ndarray,
194
+ sigma_h: tuple[float, float],
195
+ sigma_v: tuple[float, float],
196
+ ) -> np.ndarray:
197
+ """ECEF 3x3 covariance of a baseline: east/north/up errors a + b[ppm] * length."""
198
+ sd_h = sigma_h[0] + sigma_h[1] * 1e-6 * length
199
+ sd_v = sigma_v[0] + sigma_v[1] * 1e-6 * length
200
+ Q_enu = np.diag([sd_h**2, sd_h**2, sd_v**2])
201
+ return R_enu.T @ Q_enu @ R_enu
202
+
203
+
204
+ def _observation_covariance(
205
+ baselines: Sequence[BaselineResult],
206
+ fixed: Mapping[str, np.ndarray],
207
+ stochastic: str,
208
+ sigma_h: tuple[float, float],
209
+ sigma_v: tuple[float, float],
210
+ k_s: float,
211
+ ) -> np.ndarray:
212
+ """Block-diagonal ECEF covariance Q of the observations, one 3x3 block per baseline.
213
+
214
+ "apriori" blocks are built in east, north, up and rotated to ECEF; the RTKLIB blocks are
215
+ already ECEF and only made positive definite (and scaled by `k_s` for "rtklib_scaled").
216
+ """
217
+ # One east-north-up rotation, taken at the first fixed point, serves a small network
218
+ R_enu = ENUFrame.from_ecef(next(iter(fixed.values()))).rotation
219
+
220
+ Q = np.zeros((3 * len(baselines),) * 2)
221
+ for k, bl in enumerate(baselines):
222
+ if stochastic == "apriori":
223
+ block = _apriori_block(bl.length, R_enu, sigma_h, sigma_v)
224
+ else:
225
+ block = _make_pd(bl.cov) * (k_s if stochastic == "rtklib_scaled" else 1.0)
226
+ Q[3 * k : 3 * k + 3, 3 * k : 3 * k + 3] = block
227
+ return Q
228
+
229
+
230
+ # =============================================================================
231
+ # Functional model: design matrix A and known terms b
232
+ # =============================================================================
233
+
234
+
235
+ def _design_matrix(
236
+ baselines: Sequence[BaselineResult],
237
+ unknowns: Sequence[str],
238
+ fixed: Mapping[str, np.ndarray],
239
+ ) -> tuple[np.ndarray, np.ndarray]:
240
+ """Design matrix A and known terms b of y = A x + b.
241
+
242
+ A has +I (3x3) in the columns of the rover and -I in those of the base. When the rover
243
+ or the base is a fixed point, its coordinates are moved to the known terms b.
244
+ """
245
+ col = {p: 3 * j for j, p in enumerate(unknowns)}
246
+ A = np.zeros((3 * len(baselines), 3 * len(unknowns)))
247
+ b = np.zeros(3 * len(baselines))
248
+ for k, bl in enumerate(baselines):
249
+ rows = slice(3 * k, 3 * k + 3)
250
+ for point, sign in ((bl.rover, +1), (bl.base, -1)):
251
+ if point in col:
252
+ A[rows, col[point] : col[point] + 3] = sign * np.eye(3)
253
+ else:
254
+ b[rows] += sign * fixed[point]
255
+ return A, b
256
+
257
+
258
+ # =============================================================================
259
+ # Network adjustment
260
+ # =============================================================================
261
+
262
+
263
+ def adjust_network(
264
+ baselines: Sequence[BaselineResult],
265
+ fixed: Mapping[str, Sequence[float] | np.ndarray],
266
+ stochastic: Literal["rtklib_scaled", "rtklib", "apriori"] = "rtklib_scaled",
267
+ sigma_h: tuple[float, float] = SIGMA_H,
268
+ sigma_v: tuple[float, float] = SIGMA_V,
269
+ k_s: float = K_S,
270
+ ) -> Adjustment:
271
+ """Weighted least squares of the baseline vectors with the `fixed` points held.
272
+
273
+ The default model is the RTKLIB covariance scaled by `k_s` = 25 (sd x 5), a value for
274
+ baselines processed at 5 s epochs: RTKLIB treats the epochs as independent, while real
275
+ errors are correlated over about a minute (see K_S). For another processing interval dt
276
+ use k_s ~ 120 / dt [s]. Choose k_s before looking at the network: setting it to the
277
+ sigma_v_hat of the "rtklib" model makes the global test meaningless.
278
+
279
+ Args:
280
+ baselines: Baselines (base -> rover vectors) forming the network.
281
+ fixed: Point name -> ECEF coordinates [m] of the held points.
282
+ stochastic: Baseline error model: "rtklib_scaled" (RTKLIB covariance x `k_s`), "rtklib" (formal RTKLIB covariance) or "apriori" (ENU errors from `sigma_h`/`sigma_v`); see the module docstring.
283
+ sigma_h: A-priori horizontal precision as (constant [m], ppm of the length). Used by the "apriori" model only.
284
+ sigma_v: A-priori vertical precision, same form. "apriori" only.
285
+ k_s: Variance factor of the RTKLIB covariance. "rtklib_scaled" only.
286
+
287
+ Returns:
288
+ The adjustment, with parameters for the points not in `fixed`.
289
+
290
+ Raises:
291
+ ValueError: If `stochastic` is not one of the three models.
292
+ """
293
+ if stochastic not in ("rtklib_scaled", "rtklib", "apriori"):
294
+ raise ValueError(
295
+ f"stochastic must be 'rtklib_scaled', 'rtklib' or 'apriori', not {stochastic!r}"
296
+ )
297
+ if stochastic == "rtklib_scaled":
298
+ print(
299
+ f"adjust_network: RTKLIB covariance x k_s = {k_s:g} (sd x {np.sqrt(k_s):.1f}); "
300
+ "k_s = 25 suits 5 s epochs, use k_s ~ 120 / dt[s] for another interval"
301
+ )
302
+ fixed_xyz = {p: np.asarray(x, dtype=float) for p, x in fixed.items()}
303
+ points = dict.fromkeys(p for bl in baselines for p in (bl.base, bl.rover))
304
+ unknowns = [p for p in points if p not in fixed_xyz]
305
+
306
+ y0 = np.concatenate([bl.dxyz for bl in baselines])
307
+ Q = _observation_covariance(baselines, fixed_xyz, stochastic, sigma_h, sigma_v, k_s)
308
+ A, b = _design_matrix(baselines, unknowns, fixed_xyz)
309
+ sol = weighted_least_squares(A, Q, y0, b)
310
+
311
+ return Adjustment(
312
+ unknowns=unknowns,
313
+ fixed=fixed_xyz,
314
+ baselines=list(baselines),
315
+ y0=y0,
316
+ b=b,
317
+ A=A,
318
+ Q=Q,
319
+ X_hat=sol.X_hat,
320
+ Y_hat=sol.Y_hat,
321
+ v_hat=sol.v_hat,
322
+ sigma_v_hat=sol.sigma_v_hat,
323
+ Cxx=sol.Cxx,
324
+ Cyy=sol.Cyy,
325
+ Cvv=sol.Cvv,
326
+ R=sol.R,
327
+ tau=sol.tau,
328
+ )
329
+
330
+
331
+ # =============================================================================
332
+ # Result tables: residuals and coordinates
333
+ # =============================================================================
334
+
335
+
336
+ def residual_table(adj: Adjustment) -> pd.DataFrame:
337
+ """Residuals, redundancy number and normalised residual of every baseline.
338
+
339
+ Args:
340
+ adj: Result of `adjust_network`.
341
+
342
+ Returns:
343
+ One row per baseline: session, base, rover, vx_mm, vy_mm, vz_mm [mm], redundancy,
344
+ tau (largest of the three normalised residuals) and outlier (tau > TAU_CRITICAL).
345
+ """
346
+ tau = adj.tau.reshape(-1, 3).max(axis=1)
347
+ return pd.DataFrame({
348
+ "session": [bl.session for bl in adj.baselines],
349
+ "base": [bl.base for bl in adj.baselines],
350
+ "rover": [bl.rover for bl in adj.baselines],
351
+ "vx_mm": adj.v_hat[0::3] * 1e3,
352
+ "vy_mm": adj.v_hat[1::3] * 1e3,
353
+ "vz_mm": adj.v_hat[2::3] * 1e3,
354
+ "redundancy": np.diag(adj.R).reshape(-1, 3).sum(axis=1),
355
+ "tau": tau,
356
+ "outlier": tau > TAU_CRITICAL,
357
+ }) # fmt: skip
358
+
359
+
360
+ def ecef_table(adj: Adjustment) -> pd.DataFrame:
361
+ """Adjusted ECEF coordinates and standard deviations of the unknown points.
362
+
363
+ Args:
364
+ adj: Result of `adjust_network`.
365
+
366
+ Returns:
367
+ DataFrame indexed by point with X, Y, Z, sd_X, sd_Y, sd_Z [m].
368
+ """
369
+ sd = np.sqrt(np.diag(adj.Cxx))
370
+ return pd.DataFrame(
371
+ {
372
+ "point": adj.unknowns,
373
+ "X": adj.X_hat[0::3], "Y": adj.X_hat[1::3], "Z": adj.X_hat[2::3],
374
+ "sd_X": sd[0::3], "sd_Y": sd[1::3], "sd_Z": sd[2::3],
375
+ }
376
+ ).set_index("point") # fmt: skip
377
+
378
+
379
+ def _ecef_to_utm(epsg: int) -> Callable[[np.ndarray], np.ndarray]:
380
+ """Function ECEF -> (E, N, ellipsoidal height) of the UTM zone `epsg`."""
381
+ to_utm = Transformer.from_crs("EPSG:4978", f"EPSG:{epsg}", always_xy=True)
382
+ return lambda xyz: np.array(to_utm.transform(*xyz))
383
+
384
+
385
+ def _propagate(
386
+ f: Callable[[np.ndarray], np.ndarray],
387
+ X: np.ndarray,
388
+ C: np.ndarray,
389
+ step: float = 1.0,
390
+ ) -> np.ndarray:
391
+ """Covariance of f(X) as J C J^T, with the Jacobian J by central differences [step m]."""
392
+ J = np.column_stack([(f(X + step * e) - f(X - step * e)) / (2 * step) for e in np.eye(3)])
393
+ return J @ C @ J.T
394
+
395
+
396
+ def utm_table(adj: Adjustment, utm_epsg: int = 32632) -> pd.DataFrame:
397
+ """UTM coordinates (ellipsoidal height) with propagated standard deviations.
398
+
399
+ The covariance is propagated as C_utm = J C J^T, with J = d(E, N, h) / d(X, Y, Z)
400
+ computed by central differences. Fixed points are listed with zero standard deviation.
401
+ The input ECEF coordinates are treated as WGS84, which equals ETRF2000 / RDN2008 within
402
+ the accuracy of GNSS; for those frames pass their UTM code (e.g. 7791 for RDN2008 UTM 32N).
403
+
404
+ Args:
405
+ adj: Result of `adjust_network`.
406
+ utm_epsg: EPSG code of the UTM zone (32632 = WGS84 UTM 32N, 7791 = RDN2008 UTM 32N).
407
+
408
+ Returns:
409
+ DataFrame with point, E, N, h_ell, sd_E, sd_N, sd_h [m]; unknown points first.
410
+ """
411
+ ecef_to_utm = _ecef_to_utm(utm_epsg)
412
+ rows = []
413
+ for j, point in enumerate(adj.unknowns):
414
+ X = adj.X_hat[3 * j : 3 * j + 3] # adjusted ECEF coordinates
415
+ C = adj.Cxx[3 * j : 3 * j + 3, 3 * j : 3 * j + 3] # their 3x3 covariance
416
+ sd = np.sqrt(np.diag(_propagate(ecef_to_utm, X, C)))
417
+ rows.append([point, *ecef_to_utm(X), *sd])
418
+ for point, X in adj.fixed.items():
419
+ rows.append([point, *ecef_to_utm(X), 0.0, 0.0, 0.0])
420
+ return pd.DataFrame(rows, columns=["point", "E", "N", "h_ell", "sd_E", "sd_N", "sd_h"])
421
+
422
+
423
+ def coordinate_table(adj: Adjustment, utm_epsg: int = 32632) -> pd.DataFrame:
424
+ """All adjusted points (fixed ones last): ECEF, geographic and UTM, with standard deviations.
425
+
426
+ Geographic coordinates are on GRS80 (ellipsoidal height), UTM coordinates in `utm_epsg`.
427
+
428
+ Args:
429
+ adj: Result of `adjust_network`.
430
+ utm_epsg: EPSG code of the UTM zone (32632 = WGS84 UTM 32N, 7791 = RDN2008 UTM 32N).
431
+
432
+ Returns:
433
+ DataFrame indexed by point, with the ECEF, geographic and UTM columns.
434
+ """
435
+ utm = utm_table(adj, utm_epsg).set_index("point")
436
+ ecef = ecef_table(adj)
437
+ fixed = pd.DataFrame(
438
+ [[*X, 0.0, 0.0, 0.0] for X in adj.fixed.values()],
439
+ index=pd.Index(list(adj.fixed), name="point"),
440
+ columns=ecef.columns,
441
+ )
442
+ table = pd.concat([ecef, fixed]).join(utm.drop(columns="h_ell"))
443
+ lat, lon, h = ecef_to_llh(*table[["X", "Y", "Z"]].to_numpy().T)
444
+ table.insert(6, "lat", lat)
445
+ table.insert(7, "lon", lon)
446
+ table.insert(8, "h_ell", h)
447
+ table.insert(0, "fixed", table.index.isin(list(adj.fixed)))
448
+ return table.reset_index()
@@ -0,0 +1,54 @@
1
+ """Static solution of a rover over a time window (one rnx2rtkp run, last filter epoch)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import tempfile
6
+ from collections.abc import Sequence
7
+ from pathlib import Path
8
+
9
+ import pandas as pd
10
+
11
+ from ..rtklib.config import RtkConfig
12
+ from ..rtklib.pos import read_pos
13
+ from ..rtklib.runner import run_rnx2rtkp
14
+
15
+
16
+ def solve_static(
17
+ rover: Path | str,
18
+ base: Path | str,
19
+ nav: Sequence[Path | str],
20
+ config: RtkConfig,
21
+ start: pd.Timestamp,
22
+ end: pd.Timestamp,
23
+ exe: str = "rnx2rtkp",
24
+ tint: float | None = None,
25
+ ) -> pd.Series | None:
26
+ """Static solution over [start, end], run in a temporary directory.
27
+
28
+ Args:
29
+ rover: Rover RINEX observation file.
30
+ base: Base RINEX observation file.
31
+ nav: Navigation, precise orbit and clock files (see `run_rnx2rtkp`).
32
+ config: RTKLIB processing options.
33
+ start: Processing start time, GPST.
34
+ end: Processing end time, GPST.
35
+ exe: rnx2rtkp executable name or path.
36
+ tint: Processing interval [s]. None for every epoch.
37
+
38
+ Returns:
39
+ The last (cumulative) epoch of the forward filter as a row of the `read_pos` table,
40
+ or None if there is no solution.
41
+
42
+ Raises:
43
+ FileNotFoundError: If the executable is not found.
44
+ RuntimeError: If rnx2rtkp exits with a non-zero code.
45
+ """
46
+ with tempfile.TemporaryDirectory() as tmp:
47
+ out = run_rnx2rtkp(
48
+ rover, base, nav, config, Path(tmp) / "out.pos",
49
+ start=start, end=end, tint=tint, exe=exe,
50
+ ) # fmt: skip
51
+ if not out.exists() or out.stat().st_size == 0:
52
+ return None
53
+ pos = read_pos(out)
54
+ return pos.iloc[-1] if len(pos) else None
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: gnsskit
3
+ Version: 0.4.0
4
+ Summary: RTKLIB-based GNSS post-processing toolkit: RINEX tools, RTKLIB wrappers, static baselines and network adjustment, kinematic trajectories
5
+ Keywords: gnss,gps,rtklib,ppk,surveying,least-squares
6
+ Author: Francesco Ioli
7
+ Author-email: Francesco Ioli <francesco.ioli@polimi.it>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Topic :: Scientific/Engineering :: GIS
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Typing :: Typed
19
+ Requires-Dist: numpy
20
+ Requires-Dist: pandas>=2
21
+ Requires-Dist: scipy ; extra == 'network'
22
+ Requires-Dist: pyproj ; extra == 'network'
23
+ Requires-Dist: matplotlib ; extra == 'plot'
24
+ Requires-Python: >=3.10
25
+ Project-URL: Repository, https://github.com/franioli/gnsskit
26
+ Project-URL: Issues, https://github.com/franioli/gnsskit/issues
27
+ Provides-Extra: network
28
+ Provides-Extra: plot
29
+ Description-Content-Type: text/markdown
30
+
31
+ # gnsskit
32
+
33
+ Shared tools for post-processing GNSS data with [RTKLIB](https://www.rtklib.com/) `rnx2rtkp`, used by
34
+ [djippk](https://github.com/franioli/djippk) (drone PPK) and [stopgo](https://github.com/franioli/stopgo)
35
+ (stop-and-go surveys).
36
+
37
+ - **RTKLIB wrappers** (`gnsskit.rtklib`): `RtkConfig`, a typed, immutable set of `rnx2rtkp` options
38
+ (`from_file`, `updated`, `with_options`, `with_base_llh`, `write`); `run_rnx2rtkp` and `find_rnx2rtkp`;
39
+ `read_pos` (llh, xyz and enu `.pos` files).
40
+ - **RINEX** (`gnsskit.rinex`): `rinex_span`, `header_llh`, `trim_rinex`; inspection with `obs_summary`
41
+ (span, sampling, gaps, satellites), `read_visibility` and `plot_visibility` (RTKPLOT-style satellite
42
+ visibility, needs the `plot` extra).
43
+ - **Static** (`gnsskit.static`): `solve_static` (one solution over a time window), `process_baseline`
44
+ (static baselines with covariance), baseline IO and `loop_closures` (misclosure of every triangle of baselines); least-squares adjustment of a redundant baseline
45
+ network in `gnsskit.static.network` (needs the `network` extra).
46
+ - **Kinematic** (`gnsskit.kinematic`): trajectory interpolation and averaging.
47
+ - **Reference systems** (`gnsskit.refsys`): GPST/UTC, ellipsoids, ECEF/geodetic conversions and
48
+ `ENUFrame`, a local east-north-up frame at a point (points, vectors and covariances, both ways).
49
+
50
+ ## Install
51
+
52
+ ```sh
53
+ pip install gnsskit # library
54
+ pip install "gnsskit[network]" # + network adjustment (scipy, pyproj)
55
+ pip install "gnsskit[network,plot]" # + satellite visibility plots (matplotlib)
56
+ pip install git+https://github.com/franioli/gnsskit # development version
57
+ ```
58
+
59
+ `rnx2rtkp` is not installed by pip. Build the [demo5 fork](https://github.com/rtklibexplorer/RTKLIB) and put it on
60
+ your `PATH`; if it is missing, `run_rnx2rtkp` raises an error with instructions. Developed with `rnx2rtkp ver.EX 2.5.1`.
61
+
62
+ ## Example
63
+
64
+ ```python
65
+ from gnsskit import RtkConfig, read_pos, run_rnx2rtkp
66
+
67
+ config = RtkConfig(posmode="static", navsys="GEC", solformat="llh").with_base_llh(46.2, 9.8, 2300.0)
68
+ pos_file = run_rnx2rtkp(rover_obs, base_obs, [nav], config, "out.pos")
69
+ pos = read_pos(pos_file)
70
+ ```
71
+
72
+ See [example/](https://github.com/franioli/gnsskit/tree/main/example) for complete static, kinematic and network adjustment examples.
@@ -0,0 +1,20 @@
1
+ gnsskit/__init__.py,sha256=Vc0w5O7xgj98-Qy20FGIfKqso_5g4-v42eUZk2b3A_E,1360
2
+ gnsskit/kinematic/__init__.py,sha256=acQv70N3oaqU_yc2W1KI10eO1lAcrgNW5zDcQBHZMr4,177
3
+ gnsskit/kinematic/trajectory.py,sha256=UdVASx4N7jBRrDxM0c_WeeoRgvKvDcqHnGLxyDISwd8,2949
4
+ gnsskit/ppp/__init__.py,sha256=dwsBzFMH3ZFIt5H7FmtgjnUto69LwK4Rak5WgWdyA9A,758
5
+ gnsskit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ gnsskit/refsys.py,sha256=AxtzYyf800vDS9IihBMjY2Hm8RFhK0T9jVZlsG0a1V4,10791
7
+ gnsskit/rinex.py,sha256=UPAU4JLaAeVBF73jMKE_r4ia5thoT5J3VirP8oexafU,17184
8
+ gnsskit/rtklib/__init__.py,sha256=36tnultivDVoy8lYGQNDGN3wjJ3TXMZr5FkTucmypsE,460
9
+ gnsskit/rtklib/config.py,sha256=NyCLeLjaMw2hqGLXzsORQr-6gyiy9-eSObu7GthtyAc,13699
10
+ gnsskit/rtklib/pos.py,sha256=EfGQF7-3DOPTNPAhib5tivPJV2_bBylaywjV2mjDDPA,5888
11
+ gnsskit/rtklib/runner.py,sha256=EgWaUFY9GkaNfGldWDKDULSg1qwcv-2INzn5PV8p6Mc,5083
12
+ gnsskit/static/__init__.py,sha256=ZR0KDTSTb5PuEjKSaxM1Xr_GLbb_kM5uAS1SI7QVzo4,758
13
+ gnsskit/static/baseline.py,sha256=QfKljWZThrIhj23yUr-4ricUo2yAPdAksXxW6xd8cGg,11632
14
+ gnsskit/static/closure.py,sha256=xFFSLAfaYw63Js8HfoeesuSbfHaTyqKGLgJ_z_qhLq8,3551
15
+ gnsskit/static/network.py,sha256=XeAi7y1X6yK-8nDsl1ZlXcRaq2brxyYCn2sqPs6M2XQ,17997
16
+ gnsskit/static/solve.py,sha256=11vsP3LRz028rsYT5obonHcRkJ1ZHDrf7pJh6joI_C8,1728
17
+ gnsskit-0.4.0.dist-info/licenses/LICENSE,sha256=dLFf8Hru3SXGRjRJtNJ6yGCpMYutI00NKvyX8Q5oyTo,1076
18
+ gnsskit-0.4.0.dist-info/WHEEL,sha256=si2p8vW03RnhyBnhWl4tj-ObJa78SpE-c9HsEtblGR0,80
19
+ gnsskit-0.4.0.dist-info/METADATA,sha256=opONMGjSqWXYsTYMOr-uWoK6DH73ryj-g4Za2WaVlJE,3512
20
+ gnsskit-0.4.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.13.5
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023-2026 Francesco Ioli
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.