gnsskit 0.4.1__tar.gz → 0.4.3__tar.gz
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.
- {gnsskit-0.4.1 → gnsskit-0.4.3}/PKG-INFO +1 -1
- {gnsskit-0.4.1 → gnsskit-0.4.3}/pyproject.toml +1 -1
- {gnsskit-0.4.1 → gnsskit-0.4.3}/pyproject.toml.orig +1 -1
- {gnsskit-0.4.1 → gnsskit-0.4.3}/src/gnsskit/rinex.py +101 -13
- {gnsskit-0.4.1 → gnsskit-0.4.3}/LICENSE +0 -0
- {gnsskit-0.4.1 → gnsskit-0.4.3}/README.md +0 -0
- {gnsskit-0.4.1 → gnsskit-0.4.3}/src/gnsskit/__init__.py +0 -0
- {gnsskit-0.4.1 → gnsskit-0.4.3}/src/gnsskit/kinematic/__init__.py +0 -0
- {gnsskit-0.4.1 → gnsskit-0.4.3}/src/gnsskit/kinematic/trajectory.py +0 -0
- {gnsskit-0.4.1 → gnsskit-0.4.3}/src/gnsskit/ppp/__init__.py +0 -0
- {gnsskit-0.4.1 → gnsskit-0.4.3}/src/gnsskit/py.typed +0 -0
- {gnsskit-0.4.1 → gnsskit-0.4.3}/src/gnsskit/refsys.py +0 -0
- {gnsskit-0.4.1 → gnsskit-0.4.3}/src/gnsskit/rtklib/__init__.py +0 -0
- {gnsskit-0.4.1 → gnsskit-0.4.3}/src/gnsskit/rtklib/config.py +0 -0
- {gnsskit-0.4.1 → gnsskit-0.4.3}/src/gnsskit/rtklib/pos.py +0 -0
- {gnsskit-0.4.1 → gnsskit-0.4.3}/src/gnsskit/rtklib/runner.py +0 -0
- {gnsskit-0.4.1 → gnsskit-0.4.3}/src/gnsskit/static/__init__.py +0 -0
- {gnsskit-0.4.1 → gnsskit-0.4.3}/src/gnsskit/static/baseline.py +0 -0
- {gnsskit-0.4.1 → gnsskit-0.4.3}/src/gnsskit/static/closure.py +0 -0
- {gnsskit-0.4.1 → gnsskit-0.4.3}/src/gnsskit/static/network.py +0 -0
- {gnsskit-0.4.1 → gnsskit-0.4.3}/src/gnsskit/static/solve.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gnsskit
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: RTKLIB-based GNSS post-processing toolkit: RINEX tools, RTKLIB wrappers, static baselines and network adjustment, kinematic trajectories
|
|
5
5
|
Keywords: gnss,gps,rtklib,ppk,surveying,least-squares
|
|
6
6
|
Author: Francesco Ioli
|
|
@@ -236,18 +236,26 @@ def read_visibility(obs: Path | str) -> pd.DataFrame:
|
|
|
236
236
|
"""Satellites tracked at each epoch of a RINEX 3 observation file.
|
|
237
237
|
|
|
238
238
|
A satellite counts as tracked if its record has at least one non-blank observation.
|
|
239
|
-
Event records (epoch flag
|
|
239
|
+
Event records (epoch flag 2-6) are skipped and counted in `attrs["n_events"]`. The marker
|
|
240
|
+
names are collected from the header and from the events inside the data: receivers such
|
|
241
|
+
as Leica write the point name typed on site as a new site occupation (flag 3) followed by
|
|
242
|
+
a `MARKER NAME` line, while the header holds an automatic name.
|
|
240
243
|
|
|
241
244
|
Args:
|
|
242
245
|
obs: RINEX 3 observation file.
|
|
243
246
|
|
|
244
247
|
Returns:
|
|
245
248
|
DataFrame with columns `t` (epoch, as written in the file) and `sat` (e.g. "G05"),
|
|
246
|
-
one row per tracked satellite and epoch. `attrs` holds `marker`
|
|
249
|
+
one row per tracked satellite and epoch. `attrs` holds `marker` (header MARKER NAME,
|
|
250
|
+
or the file name if missing), `markers` (list of (start, name): every marker name in
|
|
251
|
+
file order, start None for the header one, else the first epoch after the event) and
|
|
252
|
+
`n_events`.
|
|
247
253
|
"""
|
|
248
254
|
times: list[pd.Timestamp] = []
|
|
249
255
|
sats: list[str] = []
|
|
250
256
|
marker, n_events = Path(obs).stem, 0
|
|
257
|
+
markers: list[tuple[pd.Timestamp | None, str]] = []
|
|
258
|
+
pending: str | None = None # marker name of an event, until the next epoch
|
|
251
259
|
t = None
|
|
252
260
|
with Path(obs).open() as f:
|
|
253
261
|
for line in f:
|
|
@@ -255,18 +263,24 @@ def read_visibility(obs: Path | str) -> pd.DataFrame:
|
|
|
255
263
|
break
|
|
256
264
|
if "MARKER NAME" in line[60:] and line[:60].strip():
|
|
257
265
|
marker = line[:60].strip()
|
|
266
|
+
markers.append((None, marker))
|
|
258
267
|
for line in f:
|
|
259
268
|
if line.startswith(">"):
|
|
260
|
-
if line[31:32]
|
|
269
|
+
if line[31:32] not in ("0", "1"): # 0 ok, 1 power failure before: both data
|
|
261
270
|
t = None
|
|
262
271
|
n_events += 1
|
|
263
272
|
continue
|
|
264
273
|
t = _epoch_time(line[1:])
|
|
274
|
+
if pending is not None:
|
|
275
|
+
markers.append((t, pending))
|
|
276
|
+
pending = None
|
|
277
|
+
elif t is None and "MARKER NAME" in line[60:] and line[:60].strip():
|
|
278
|
+
pending = line[:60].strip()
|
|
265
279
|
elif t is not None and line[:1] in SYSTEMS and line[3:].strip():
|
|
266
280
|
times.append(t)
|
|
267
281
|
sats.append(line[:3])
|
|
268
282
|
df = pd.DataFrame({"t": times, "sat": sats})
|
|
269
|
-
df.attrs.update(marker=marker, n_events=n_events)
|
|
283
|
+
df.attrs.update(marker=marker, markers=markers, n_events=n_events)
|
|
270
284
|
return df
|
|
271
285
|
|
|
272
286
|
|
|
@@ -279,7 +293,9 @@ def obs_summary(obs: Path | str, gap_factor: float = 1.5) -> dict[str, object]:
|
|
|
279
293
|
is a gap.
|
|
280
294
|
|
|
281
295
|
Returns:
|
|
282
|
-
Dict with `marker
|
|
296
|
+
Dict with `marker` (header MARKER NAME), `markers` (all the marker names, header and
|
|
297
|
+
site-occupation events, in file order), `first`, `last`, `duration_s`, `interval_s`,
|
|
298
|
+
`n_epochs`,
|
|
283
299
|
`n_gaps`, `max_gap_s`, `n_events`, `mean_sats` (per epoch) and `n_sats` (unique
|
|
284
300
|
satellites per system).
|
|
285
301
|
|
|
@@ -295,6 +311,7 @@ def obs_summary(obs: Path | str, gap_factor: float = 1.5) -> dict[str, object]:
|
|
|
295
311
|
gaps = step[step > gap_factor * interval] if len(step) else step
|
|
296
312
|
return {
|
|
297
313
|
"marker": vis.attrs["marker"],
|
|
314
|
+
"markers": [name for _, name in vis.attrs["markers"]],
|
|
298
315
|
"first": epochs.iloc[0],
|
|
299
316
|
"last": epochs.iloc[-1],
|
|
300
317
|
"duration_s": float((epochs.iloc[-1] - epochs.iloc[0]).total_seconds()),
|
|
@@ -423,7 +440,12 @@ def signal_stats(obs: Path | str) -> pd.DataFrame:
|
|
|
423
440
|
|
|
424
441
|
|
|
425
442
|
def plot_visibility(
|
|
426
|
-
obs_files: Sequence[Path | str],
|
|
443
|
+
obs_files: Sequence[Path | str],
|
|
444
|
+
out: Path | str | None = None,
|
|
445
|
+
dpi: int = 150,
|
|
446
|
+
start: pd.Timestamp | None = None,
|
|
447
|
+
end: pd.Timestamp | None = None,
|
|
448
|
+
common_xlim: bool = True,
|
|
427
449
|
) -> Figure:
|
|
428
450
|
"""Satellite visibility plot (as in RTKPLOT), one panel per observation file.
|
|
429
451
|
|
|
@@ -433,6 +455,12 @@ def plot_visibility(
|
|
|
433
455
|
obs_files: RINEX 3 observation files.
|
|
434
456
|
out: If given, the figure is also saved to this path.
|
|
435
457
|
dpi: Resolution of the saved figure.
|
|
458
|
+
start: Plot only from this time on, in the time system of the files (GPST).
|
|
459
|
+
None for the start of the data.
|
|
460
|
+
end: Plot only up to this time, same time system. None for the end of the data.
|
|
461
|
+
common_xlim: Give all the panels the same time axis, from the first to the last epoch
|
|
462
|
+
of all the files (within `start`/`end`), so that the common windows line up.
|
|
463
|
+
If False each panel spans its own data.
|
|
436
464
|
|
|
437
465
|
Returns:
|
|
438
466
|
The matplotlib figure.
|
|
@@ -440,28 +468,88 @@ def plot_visibility(
|
|
|
440
468
|
try:
|
|
441
469
|
import matplotlib.dates as mdates
|
|
442
470
|
from matplotlib.figure import Figure
|
|
471
|
+
from matplotlib.lines import Line2D
|
|
443
472
|
except ImportError as e: # pragma: no cover
|
|
444
473
|
raise ImportError("plot_visibility needs matplotlib: pip install 'gnsskit[plot]'") from e
|
|
445
474
|
|
|
475
|
+
visibility = []
|
|
476
|
+
for path in obs_files:
|
|
477
|
+
vis = read_visibility(path)
|
|
478
|
+
keep = pd.Series(True, index=vis.index)
|
|
479
|
+
if start is not None:
|
|
480
|
+
keep &= vis.t >= start
|
|
481
|
+
if end is not None:
|
|
482
|
+
keep &= vis.t <= end
|
|
483
|
+
visibility.append(vis[keep])
|
|
484
|
+
|
|
485
|
+
def xlim(times: pd.Series) -> tuple[pd.Timestamp, pd.Timestamp] | None:
|
|
486
|
+
"""Axis range: `start`/`end` if given, else the first and last epoch."""
|
|
487
|
+
if times.empty and (start is None or end is None):
|
|
488
|
+
return None
|
|
489
|
+
lo = start if start is not None else times.min()
|
|
490
|
+
hi = end if end is not None else times.max()
|
|
491
|
+
return lo, hi
|
|
492
|
+
|
|
493
|
+
common = xlim(pd.concat([v.t for v in visibility])) if common_xlim else None
|
|
494
|
+
|
|
446
495
|
colors = {"G": "tab:blue", "R": "tab:red", "E": "tab:green", "C": "tab:orange",
|
|
447
496
|
"J": "tab:purple", "S": "tab:gray", "I": "tab:brown"} # fmt: skip
|
|
497
|
+
names = {"G": "GPS", "R": "GLONASS", "E": "Galileo", "C": "BeiDou", "J": "QZSS",
|
|
498
|
+
"S": "SBAS", "I": "NavIC"} # fmt: skip
|
|
448
499
|
fig = Figure(figsize=(12, 3.5 * len(obs_files))) # no pyplot: no GUI backend needed
|
|
449
500
|
axes = fig.subplots(len(obs_files), 1, squeeze=False)
|
|
450
|
-
for ax, path in zip(axes[:, 0], obs_files):
|
|
451
|
-
vis = read_visibility(path)
|
|
501
|
+
for ax, path, vis in zip(axes[:, 0], obs_files, visibility):
|
|
452
502
|
sats = sorted(vis.sat.unique())
|
|
453
503
|
for i, sat in enumerate(sats):
|
|
454
504
|
t = vis.t[vis.sat == sat]
|
|
455
505
|
ax.plot(t, [i] * len(t), "|", ms=3, color=colors[sat[0]])
|
|
456
506
|
ax.set_yticks(range(len(sats)), sats, fontsize=5)
|
|
507
|
+
limits = common if common_xlim else xlim(vis.t)
|
|
508
|
+
if limits is not None and limits[0] < limits[1]:
|
|
509
|
+
ax.set_xlim(*limits)
|
|
457
510
|
ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M"))
|
|
511
|
+
ax.set_xlabel("GPST", fontsize=7, loc="right")
|
|
458
512
|
span = f"{vis.t.min():%H:%M:%S} - {vis.t.max():%H:%M:%S}" if len(vis) else "no data"
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
513
|
+
site = ", ".join(name for _, name in vis.attrs["markers"]) or vis.attrs["marker"]
|
|
514
|
+
ax.set_title(f"{Path(path).name} [{site}] {span} ({len(sats)} sats)", fontsize=9)
|
|
515
|
+
for t0, name in vis.attrs["markers"]: # new site occupations inside the data
|
|
516
|
+
if t0 is not None and (limits is None or limits[0] <= t0 <= limits[1]):
|
|
517
|
+
ax.axvline(t0, color="k", ls="--", lw=1)
|
|
518
|
+
ax.annotate(name, (t0, 1), xycoords=("data", "axes fraction"), xytext=(3, -3),
|
|
519
|
+
textcoords="offset points", va="top", fontsize=7, weight="bold") # fmt: skip
|
|
463
520
|
ax.grid(alpha=0.3)
|
|
464
|
-
|
|
521
|
+
if vis.empty:
|
|
522
|
+
continue
|
|
523
|
+
ax.axvspan(vis.t.min(), vis.t.max(), color="0.5", alpha=0.08, lw=0) # file span
|
|
524
|
+
|
|
525
|
+
# satellites tracked per epoch, on a right-hand axis
|
|
526
|
+
n_sats = vis.groupby("t").size()
|
|
527
|
+
ax_n = ax.twinx()
|
|
528
|
+
ax_n.plot(n_sats.index, n_sats.to_numpy(), color="k", lw=0.8, alpha=0.6)
|
|
529
|
+
ax_n.set_ylim(0, 1.15 * n_sats.max())
|
|
530
|
+
ax_n.set_ylabel("sats per epoch", fontsize=7)
|
|
531
|
+
ax_n.tick_params(labelsize=6)
|
|
532
|
+
|
|
533
|
+
# sampling and tracking of the plotted window
|
|
534
|
+
epochs = n_sats.index.to_series()
|
|
535
|
+
step = epochs.diff().dt.total_seconds()
|
|
536
|
+
interval = step.median() if len(epochs) > 1 else float("nan")
|
|
537
|
+
gaps = int((step > 1.5 * interval).sum()) if len(epochs) > 1 else 0
|
|
538
|
+
per_sys = " ".join(
|
|
539
|
+
f"{s}{sum(x[0] == s for x in sats)}" for s in sorted({x[0] for x in sats})
|
|
540
|
+
)
|
|
541
|
+
info = (
|
|
542
|
+
f"interval {interval:g} s, {len(epochs)} epochs, {gaps} gaps\n"
|
|
543
|
+
f"sats {per_sys}, per epoch {n_sats.min()}-{n_sats.max()} (mean {n_sats.mean():.1f})"
|
|
544
|
+
)
|
|
545
|
+
ax.text(0.005, 0.98, info, transform=ax.transAxes, va="top", ha="left", fontsize=6,
|
|
546
|
+
bbox={"boxstyle": "round", "fc": "white", "ec": "0.7", "alpha": 0.85}) # fmt: skip
|
|
547
|
+
|
|
548
|
+
used = sorted({s[0] for v in visibility for s in v.sat.unique()}, key=list(colors).index)
|
|
549
|
+
handles = [Line2D([], [], color=colors[s], lw=3, label=names[s]) for s in used]
|
|
550
|
+
handles.append(Line2D([], [], color="k", lw=0.8, alpha=0.6, label="sats per epoch"))
|
|
551
|
+
fig.legend(handles=handles, loc="upper center", ncol=len(handles), fontsize=8, frameon=False)
|
|
552
|
+
fig.tight_layout(rect=(0, 0, 1, 1 - 0.25 / fig.get_figheight()))
|
|
465
553
|
if out is not None:
|
|
466
554
|
fig.savefig(out, dpi=dpi)
|
|
467
555
|
return fig
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|