Wavelet spectra per satellite
SNR is converted from dB-Hz to linear amplitude, detrended arc by arc, and passed through a Morlet transform. You get band power in periods matched to your sampling interval and a 95 % red-noise significance level.
geoveil-mp reads RINEX 2, 3 and 4 and estimates code multipath for every pseudorange signal on GPS, GLONASS, Galileo, BeiDou, QZSS and NavIC, with cycle slips detected per carrier. Version 1.0 adds SNR wavelet analysis and first Fresnel zone mapping, so the numbers point back to the ground that caused them.
pip install geoveil-mp
b = √(λh / sin θ + (λ / 2 sin θ)²), a = b / sin θ, d = h / tan θ. Satellite tracks are illustrative; the geometry is the library's.
The wavelet and Fresnel analysis that the GeoVeil batch platform ran in numpy now lives in the library, in Rust, with the Python API you already use. Everything from 0.2 works as before.
SNR is converted from dB-Hz to linear amplitude, detrended arc by arc, and passed through a Morlet transform. You get band power in periods matched to your sampling interval and a 95 % red-noise significance level.
For every 10° × 10° cell a satellite crosses, the library returns the zone's semi-axes and its distance from the antenna, for your antenna height and carrier. Large multipath now has an address on the ground.
Each function was checked against the numpy reference on synthetic series and on a real station hour. Satellites run in parallel and the GIL is released, so thread pools keep working.
| Measured on | numpy reference | geoveil-mp 1.0 |
|---|---|---|
| SNR wavelets, 51 satellites, 3,600 epochs at 1 Hz | 7.05 s | 0.35 s |
| Largest relative difference in wavelet power | reference | 1.8 × 10⁻¹⁵ |
| Largest relative difference in detrended residuals | reference | 4.3 × 10⁻¹² |
Wall-clock time for the full pipeline on public IGS files from 31 December 2025, measured with the geoveil-mp 1.0.1 wheel from PyPI on a 2014 server CPU. Parsing and multipath analysis run on one core. SNR wavelets use every core.
24 hours at 1 Hz: a 948 MB file with 86,400 epochs and 127 satellites takes 66 s and peaks at 10.7 GB of memory. Split high-rate days into hours to keep memory near 562 MB.
| File | Epochs × sats | Hatanaka | Parse | Multipath | SNR wavelets | Total | 1 thread | Peak memory | vs numpy |
|---|---|---|---|---|---|---|---|---|---|
| RINEX 2.11 · 24 h · 30 s ZIMM · Hatanaka + gzip · 5.4 MB | 2,880 × 32 | 0.22 s | 0.15 s | 0.42 s | 0.07 s | 0.64 s | 0.90 s | 129 MB | 18× |
| RINEX 3.02 · 24 h · 30 s BOR1 · Hatanaka + gzip · 19.4 MB | 2,880 × 106 | 0.69 s | 0.34 s | 1.47 s | 0.20 s | 2.01 s | 3.43 s | 398 MB | 22× |
| RINEX 3.02 · 24 h · 30 s BOR1 · gzip, read directly · 5.8 MB | 2,880 × 106 | – | 0.45 s | 1.75 s | 0.21 s | 2.42 s | 3.59 s | 401 MB | 23× |
| RINEX 3.05 · 15 min · 1 Hz BUCU · Hatanaka · 9.6 MB | 900 × 44 | 0.30 s | 0.14 s | 0.44 s | 0.08 s | 0.66 s | 0.98 s | 135 MB | 28× |
| RINEX 3.05 · 1 h · 1 Hz BUCU · Hatanaka · 39.9 MB | 3,600 × 52 | 1.18 s | 0.61 s | 1.94 s | 0.35 s | 2.90 s | 4.57 s | 562 MB | 19× |
| RINEX 3.05 · 24 h · 1 Hz BUCU · Hatanaka · 948.1 MB | 86,400 × 127 | 28.4 s | 14.6 s | 46.4 s | 5.13 s | 66.1 s | 96.3 s | 10.7 GB | 14× |
The GeoVeil batch platform runs this analysis on every station day it receives. These screenshots show one day of the HAR1 station, a daily 30 s RINEX file. Select a chart to see it full size.
Explore the platformNo sign-in needed to browse.
Two processing lanes share one parsed file. The code lane measures how much multipath each signal carries. The SNR lane finds its periods and the Fresnel step places it around the antenna.
| Constellation | Best signal | RMS | Worst signal | RMS |
|---|---|---|---|---|
| Galileo | E5 AltBOC (M8X) | 0.165 m | E5b (M7X) | 0.395 m |
| GPS | L2C (M2X) | 0.312 m | L1 C/A (M1C) | 0.342 m |
| BeiDou | B2I (M7I) | 0.295 m | B3I (M6I) | 0.354 m |
| GLONASS | L2 P (M2P) | 0.318 m | L1 C/A (M1C) | 0.566 m |
Install from PyPI, point it at a RINEX file, and read plain Python lists and dicts back. The Rust crate exposes the same functions.
import geoveil_mp as gm obs = gm.read_rinex_obs("HAR100ROU_R_20253650000_01D_30S_MO.rnx") # Code multipath and cycle slips per signal res = gm.MultipathAnalyzer(obs, 10.0, ["G", "R", "E"]).analyze() for s in res.statistics: print(s.signal, round(s.rms, 3), s.cycle_slips) # GLOM3Q 0.395 ... # New in 1.0: SNR wavelets for every satellite, in parallel w = gm.analyze_snr_wavelets(obs) w["bands"] # [[60, 120], [120, 300], [300, 900], [900, 1800]] w["band_power"]["R02"]["power"] # binned power, one series per band r = w["residuals"]["R02"] scal = gm.scalogram(r["t"], r["v"], w["interval"]) # power, significance, coi # First Fresnel zone for a 2 m antenna at 15° elevation, GPS L1 a, b, dist = gm.fresnel_zone(15.0, 2.0) # (4.896, 1.267, 7.464) m
use geoveil_mp::analysis::advanced::{fresnel_zone, morlet_cwt, snr_wavelets, SnrWaveletConfig, DJ, DEFAULT_WAVELENGTH}; use geoveil_mp::RinexObsReader; let obs = RinexObsReader::new().read("HAR100ROU_R_20253650000_01D_30S_MO.rnx")?; // SNR wavelets for all satellites (rayon-parallel) let sats = snr_wavelets(&obs, &[], None, &SnrWaveletConfig::default()); // Band power of one residual series let cwt = morlet_cwt(&sats[0].residual, 30.0, DJ, None); let power = cwt.band_power((120.0, 300.0)); // First Fresnel zone: a, b and distance in metres let z = fresnel_zone(15.0, 2.0, DEFAULT_WAVELENGTH);
| New in 1.0 | Returns |
|---|---|
| analyze_snr_wavelets(obs) | Residuals and binned band power per satellite, in the layout the platform stores |
| scalogram(t, values, interval) | Power matrix, periods, 95 % significance and cone of influence for the longest arc |
| morlet_cwt(y, dt) | Wavelet power, scales, Fourier periods and cone of influence |
| band_power(y, dt, bands) | Scale-averaged power for each (low, high) period band |
| red_noise_significance(y, dt, periods) | Lag-1 red-noise significance level per period |
| period_bands(dt) | The four default bands for a sampling interval |
| detrend_arc(t, values) | Residual and the chosen polynomial order |
| split_arcs(times, dt) | (start, stop) index pairs of continuous arcs |
| fresnel_zone(elevation, height) | Semi-major axis, semi-minor axis and specular distance in metres |
| fresnel_map(tracks, height) | Zone footprints along satellite tracks, one per 10° × 10° cell |
From 1.0, geoveil-mp is source-available under PolyForm Noncommercial 1.0.0 with an attribution term.
Products, paid services and work done for a company need a separate commercial license. Write to:
Versions 0.2.1 and earlier remain available under the MIT license. The full terms are in the LICENSE file.
@software{geoveil_mp,
author = {Dulea-Flueras, Miluta},
title = {geoveil-mp: GNSS Code Multipath Analysis Library},
year = {2026},
version = {1.0.1},
url = {https://github.com/miluta7/geoveil-mp},
license = {PolyForm-Noncommercial-1.0.0}
}
Method: Hunegnaw, A.; Teferle, F.N. Evaluation of the Multipath Environment Using Electromagnetic-Absorbing Materials at Continuous GNSS Stations. Sensors 2022, 22, 3384. Wavelets: Torrence, C.; Compo, G.P. A Practical Guide to Wavelet Analysis. Bull. Amer. Meteor. Soc. 1998, 79, 61–78.