Sudden, shared CN0 drops
A jammer lowers CN0 on many satellites at once. The flag needs critical CN0 drops in more than 5 % of epochs, together with a low mean CN0 or drops shared across constellations.
geoveil-cn0 reads RINEX 2, 3 and 4 observation files and turns carrier-to-noise density into a 0–100 quality score, per-constellation statistics, CN0 skyplots and interference, jamming and spoofing checks. Give it a broadcast navigation file and it also predicts which satellites should be in view.
pip install geoveil-cn0
Tracks from broadcast ephemeris, sampled every 5 minutes. Timeline: mean CN0 of all satellites above 10° every 30 s, and the number tracked.
Mean CN0 in 5° elevation bins for each constellation, from the same BOR1 day. A healthy antenna gives a smooth climb. Dips at mid elevations point to obstructions or multipath, and a flat, low curve points to interference.
| Constellation | Mean CN0 | Std dev | Min – max | Satellites | Observations |
|---|
The quality score weighs signal strength, availability, continuity, stability and constellation diversity. These are the real components for one hour of 1 Hz data from BUCU, Bucharest.
Rating: A ≥ 90 · B ≥ 80 · C ≥ 70 · D ≥ 60 · F below. CN0 quality is 0 at a mean of 30 dB-Hz and 100 at 45 dB-Hz. Stability is 100 at a CN0 standard deviation of 2 dB-Hz and 0 at 10 dB-Hz.
Each detector watches a different signature. Thresholds are part of AnalysisConfig, so a station on a noisy rooftop and one in an open field can be tuned differently.
A jammer lowers CN0 on many satellites at once. The flag needs critical CN0 drops in more than 5 % of epochs, together with a low mean CN0 or drops shared across constellations.
Interference is quieter and more frequent. The flag needs CN0 anomalies, drops beyond the configured thresholds, in more than 1 % of epochs.
With a navigation file, the library predicts which satellites are above the horizon. A spoofer replays signals that do not match, so a sustained high share of unexpected satellites raises the flag.
| Check | Signature | Default |
|---|---|---|
| Jamming | Critical drops in > 5 % of epochs, plus mean CN0 < 35 dB-Hz or drops across constellations | critical at interference_threshold_db = 6.0 |
| Interference | CN0 anomalies in > 1 % of epochs, at least 3 | anomaly_sensitivity = 0.5 |
| Spoofing | Unexpected satellites against broadcast ephemeris, sustained over 5 minutes or above 60 % | > 40 % and > 8 satellites |
Wall-clock time on public IGS files from 31 December 2025, the same set used for geoveil-mp, on a 2014 server CPU. The analysis runs on one core, so a batch of files scales with the cores you give it. Visibility prediction from broadcast ephemeris is the extra cost of analyze_with_nav.
24 hours at 1 Hz: 86,400 epochs of 127 satellites take 129.1 s without navigation and 263.8 s with it, peaking at 14.2 GB.
| File | Epochs × sats | analyze_file | with nav | Peak memory | Quality |
|---|---|---|---|---|---|
| RINEX 2.11 · 24 h · 30 s ZIMM · GPS · 5.4 MB | 2,880 × 32 | 0.14 s | 4.31 s | 104 MB | 66.5 |
| RINEX 3.02 · 24 h · 30 s BOR1 · 6 GNSS · 19.4 MB | 2,880 × 106 | 2.57 s | 7.97 s | 446 MB | 75.7 |
| RINEX 3.05 · 15 min · 1 Hz BUCU · 4 GNSS · 9.6 MB | 900 × 44 | 0.99 s | 2.62 s | 201 MB | 96.9 |
| RINEX 3.05 · 1 h · 1 Hz BUCU · 4 GNSS · 39.9 MB | 3,600 × 52 | 3.78 s | 9.98 s | 739 MB | 96.3 |
| RINEX 3.05 · 24 h · 1 Hz BUCU · 4 GNSS · 948.1 MB | 86,400 × 127 | 129.1 s | 263.8 s | 14.2 GB | 76.4 |
The GeoVeil batch platform runs geoveil-cn0 on every station day it receives. These screenshots show one day of the CRNM station, a Trimble Alloy receiver. Select a chart to see it full size.
Explore the platformNo sign-in needed to browse.
Install from PyPI, point it at a RINEX file, and read plain Python values back. Wheels for Linux, Windows and macOS.
import hatanaka from geoveil_cn0 import AnalysisConfig, CN0Analyzer obs = hatanaka.decompress_on_disk("BOR100POL_R_20253650000_01D_30S_MO.crx.gz") # geoveil-cn0 reads plain RINEX nav = hatanaka.decompress_on_disk("BRDC00IGS_R_20253650000_01D_MN.rnx.gz") analyzer = CN0Analyzer(AnalysisConfig(min_elevation=10.0, time_bin_seconds=60)) result = analyzer.analyze_with_nav(str(obs), str(nav)) q = result.quality_score print(q.overall, q.rating) # composite 0–100 and its letter print(result.mean_cn0, result.total_satellites) # 45.1 dB-Hz, 106 satellites for BOR1 print(result.get_constellation_summary("Galileo")["mean_cn0"]) tracks = result.get_skyplot_data() # azimuth, elevation and CN0 per satellite
| Member | Returns |
|---|---|
| analyze_file(obs) | CN0 statistics, quality score, anomaly checks |
| analyze_with_nav(obs, nav) | All of the above plus predicted visibility, skyplot and the spoofing check |
| quality_score | overall, rating and the five components |
| get_constellation_summary(name) | Mean, spread and range of CN0, satellites observed and expected |
| get_timeseries_data() | Mean CN0 and satellites tracked per epoch |
| get_skyplot_data() | Azimuth, elevation and CN0 tracks per satellite |
| get_anomalies() | Detected events with time, severity and affected satellites |
| to_json() | The full result as JSON |
From 0.4.0, geoveil-cn0 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.3.9 and earlier remain available under the MIT license. The full terms are in the LICENSE file.
@software{geoveil_cn0_2026,
title = {geoveil-cn0: High-performance GNSS signal quality analysis},
author = {Dulea-Flueras, Miluta},
year = {2026},
version = {0.4.2},
url = {https://github.com/miluta7/geoveil-cn0},
license = {PolyForm-Noncommercial-1.0.0}
}