π‘ GeoVeil CN0 Library β v0.3.8
Comprehensive GNSS Signal Quality Analysis Library (Rust/PyO3)
β Updated for v0.3.8 β GPS ToW fix, visibility-based spoofing detection, configurable thresholds
π Quick Start
import geoveil_cn0 as gcn0
# Check version
print(f"Version: {gcn0.VERSION}")
# Create configuration
config = gcn0.AnalysisConfig(
min_elevation=5.0,
time_bin_seconds=60,
detect_anomalies=True,
)
# Create analyzer
analyzer = gcn0.CN0Analyzer(config)
# Analyze observation file (without navigation)
result = analyzer.analyze_file("/path/to/observation.rnx")
# Analyze with navigation (enables skyplots & accurate elevations)
result = analyzer.analyze_with_nav("/path/to/obs.rnx", "/path/to/nav.rnx")
# Access results
print(f"Quality Score: {result.quality_score.overall}/100")
print(f"Mean CN0: {result.avg_cn0:.1f} dB-Hz")
print(f"Anomalies: {result.anomaly_count}")
βοΈ AnalysisConfig
Configuration class for CN0 analysis parameters.
config = gcn0.AnalysisConfig(
min_elevation=5.0, # Elevation mask (degrees)
time_bin_seconds=60, # Time binning (seconds)
systems=['G', 'E', 'R', 'C'],# GNSS systems to analyze
detect_anomalies=True, # Enable anomaly detection
anomaly_sensitivity=0.3, # Anomaly sensitivity (0.1-1.0)
interference_threshold_db=8.0,# Interference threshold (dB)
verbose=False, # Verbose logging
nav_file=None, # Optional nav file path
)
| Parameter | Type | Default | Description |
|---|---|---|---|
| min_elevation | float | 5.0 | Minimum satellite elevation angle in degrees. Satellites below this are excluded. |
| time_bin_seconds | int | 60 | Time binning interval in seconds for timeseries aggregation. |
| systems | List[str] | ['G','R','E','C'] | GNSS systems to analyze: G=GPS, R=GLONASS, E=Galileo, C=BeiDou, J=QZSS, I=NavIC |
| detect_anomalies | bool | True | Enable anomaly/interference detection algorithms. |
| anomaly_sensitivity | float | 0.5 | Anomaly detection sensitivity (0.1=strict, 1.0=loose). Lower = fewer false positives. |
| interference_threshold_db | float | 6.0 | CN0 drop threshold (dB) for interference detection. Based on ITU-R M.1902-1. |
| spoofing_unexpected_threshold | float | 0.4 | Ratio of unexpected satellites triggering spoofing detection (0.0β1.0). Requires corroboration. |
| spoofing_min_unexpected_count | float | 8.0 | Minimum count of unexpected satellites for spoofing check. |
| anomaly_threshold_critical | float | 6.0 | CN0 drop (dB) threshold for critical anomaly severity. |
| anomaly_threshold_high | float | 4.5 | CN0 drop (dB) threshold for high anomaly severity. |
| anomaly_threshold_low | float | 2.25 | CN0 drop (dB) threshold for low anomaly severity. |
| max_cn0 | float | 60.0 | Maximum plausible CN0 (dB-Hz); values above are clipped. |
| min_cn0 | float | 0.0 | Minimum plausible CN0 (dB-Hz); values below are ignored. |
| verbose | bool | False | Enable verbose logging output. |
| nav_file | str | None | None | Optional path to navigation file for ephemeris data. |
π Preset Configurations (Research-Based)
| Preset | Sensitivity | Threshold | Use Case | Reference |
|---|---|---|---|---|
| Full Analysis | 0.3 | 8 dB | Complete analysis with all plots | General purpose |
| Quick Summary | 0.5 | 10 dB | Fast overview, skip heavy plots | Rapid assessment |
| Interference Focus | 0.15 | 4 dB | Detect subtle interference | ITU I/N=-6dB criterion |
| Jamming Detection | 0.2 | 6 dB | Rapid CN0 drops in <3s | Stanford GPS Lab |
| Spoofing Check | 0.1 | 5 dB | CN0 uniformity anomalies | GPS Solutions journal |
π¬ CN0Analyzer
Main analyzer class for processing RINEX observation files.
analyzer = gcn0.CN0Analyzer(config) # Method 1: Analyze without navigation (elevations estimated) result = analyzer.analyze_file(obs_path) # Method 2: Analyze with navigation (accurate elevations, skyplots) result = analyzer.analyze_with_nav(obs_path, nav_path)
| Method | Parameters | Returns | Description |
|---|---|---|---|
| analyze_file() | obs_path: str |
AnalysisResult |
Analyze RINEX OBS file without navigation. Elevations are estimated. |
| analyze_with_nav() | obs_path: str, nav_path: str |
AnalysisResult |
Analyze with BRDC/SP3 navigation. Enables accurate elevations and skyplots. |
π Supported File Formats
| Type | Extensions | Description |
|---|---|---|
| Observation | .obs, .rnx, .crx, .YYo (e.g., .24o, .25o) | RINEX 2.x/3.x/4.x observation files |
| Navigation | .nav, .rnx, .YYn, .YYg | BRDC navigation files |
| Precise Orbit | .sp3, .SP3 | SP3 precise ephemeris (IGS/MGEX) |
| Compressed | .gz, .Z | Gzip or Unix compress (auto-decompressed) |
π AnalysisResult
Container for all analysis results. Returned by analyzer methods.
π File Information Properties
| Property | Type | Description |
|---|---|---|
| filename | str | Input filename |
| rinex_version | str | RINEX format version (e.g., "3.04") |
| station_name | str | None | Station/marker name from header |
| duration_hours | float | Data duration in hours |
| epoch_count | int | Number of observation epochs |
| constellations | List[str] | Constellation names present (GPS, Galileo, etc.) |
πΆ Signal Quality Properties
| Property | Type | Unit | Description |
|---|---|---|---|
| avg_cn0 / mean_cn0 | float | dB-Hz | Average carrier-to-noise ratio |
| cn0_std_dev | float | dB-Hz | CN0 standard deviation |
| min_cn0 | float | dB-Hz | Minimum CN0 observed |
| max_cn0 | float | dB-Hz | Maximum CN0 observed |
| skyplot_coverage | float | % | Sky coverage percentage (requires nav) |
π‘οΈ Threat Detection Properties
| Property | Type | Description |
|---|---|---|
| jamming_detected | bool | True if jamming patterns detected (rapid >6dB drops) |
| spoofing_detected | bool | True if spoofing indicators detected β unexpected satellite detection via BRDC ephemeris comparison (requires nav file) |
| interference_detected | bool | True if any interference detected |
| anomaly_count | int | Number of anomaly events detected |
| summary | str | Human-readable analysis summary |
π°οΈ Visibility Prediction Properties (requires nav file)
| Property | Type | Description |
|---|---|---|
| has_visibility_prediction | bool | True if ephemeris-based visibility assessment was performed |
| visibility_prediction_source | str | None | Ephemeris source used (e.g., "BRDC") |
| visibility_mean_observed | float | Mean number of observed satellites per epoch |
| visibility_mean_predicted | float | Mean number of predicted visible satellites per epoch |
| visibility_mean_unexpected | float | Mean number of observed-but-not-predicted satellites (spoofing indicator) |
| visibility_mean_missing | float | Mean number of predicted-but-not-observed satellites |
| visibility_confirmation_rate | float | Fraction of observed satellites confirmed by ephemeris (0.0β1.0) |
| visibility_anomaly_count | int | Number of visibility anomaly events detected |
π§ Methods
| Method | Returns | Description |
|---|---|---|
| get_systems() | List[str] | List of constellation codes present |
| get_constellation_summary(name) | Dict | Statistics for specific constellation |
| get_anomalies() | List[Dict] | List of detected anomaly events |
| get_timeseries_data() | Dict | Time-binned CN0 and satellite data |
| get_timestamps() | List[str] | ISO timestamp strings |
| get_mean_cn0_series() | List[float] | Mean CN0 per time bin |
| get_satellite_count_series() | List[int] | Satellite count per time bin |
| get_skyplot_data() | List[Dict] | Satellite traces for skyplot (requires nav) |
| to_json() | str | Serialize entire result to JSON |
| get_visibility_anomaly_types() | List[str] | Visibility anomaly type strings |
| get_visibility_anomaly_descriptions() | List[str] | Human-readable descriptions of visibility anomalies |
| get_frequently_unexpected_satellites() | List[Dict] | Satellites most often observed but not predicted |
| get_frequently_missing_satellites() | List[Dict] | Satellites most often predicted but not observed |
| to_json_pretty() | str | Pretty-printed JSON (indented) |
| get_anomaly_severities() | List[str] | Severity string per anomaly |
| get_anomaly_timestamps() | List[str] | ISO timestamps per anomaly |
| get_anomaly_cn0_drops() | List[float] | CN0 drop (dB) per anomaly |
# Example usage
result = analyzer.analyze_with_nav(obs_path, nav_path)
# Access properties
print(f"File: {result.filename}")
print(f"Duration: {result.duration_hours:.2f} hours")
print(f"Mean CN0: {result.avg_cn0:.1f} dB-Hz")
print(f"Jamming: {result.jamming_detected}")
# Get constellation summary
gps_stats = result.get_constellation_summary('GPS')
print(f"GPS satellites: {gps_stats['satellites_observed']}")
# Export to JSON
json_str = result.to_json()
π QualityScore
Composite quality assessment based on ITU recommendations and industry standards.
qs = result.quality_score
print(f"Overall: {qs.overall}/100 ({qs.rating})")
print(f"CN0 Quality: {qs.cn0_quality}")
print(f"Availability: {qs.availability}")
print(f"Continuity: {qs.continuity}")
print(f"Stability: {qs.stability}")
print(f"Diversity: {qs.diversity}")
print(f"Post-processing OK: {qs.post_processing_suitable}")
| Property | Type | Range | Weight | Description |
|---|---|---|---|---|
| overall | float | 0-100 | β | Weighted composite score |
| rating | str | β | β | "Excellent", "Good", "Degraded", "Poor" |
| cn0_quality | float | 0-100 | 35% | Signal strength quality (based on mean CN0) |
| availability | float | 0-100 | 20% | Satellite availability ratio |
| continuity | float | 0-100 | 20% | Signal continuity (few gaps/slips) |
| stability | float | 0-100 | 15% | CN0 stability (low variance) |
| diversity | float | 0-100 | 10% | Multi-constellation diversity |
| post_processing_suitable | bool | β | β | True if overall β₯70 |
π Rating Thresholds
| Score Range | Rating | Interpretation |
|---|---|---|
| β₯80 | Excellent | High quality data for precise positioning |
| 60-79 | Good | Suitable for standard GNSS applications |
| 40-59 | Degraded | Some issues detected, review anomalies |
| <40 | Poor | Significant interference or equipment issues |
π Lock Integrity (Computed Separately)
Lock Integrity measures signal continuity based on cycle slips and data gaps.
# Calculate Lock Integrity Score (0-100, higher = better)
total_cycle_slips = 0
total_data_gaps = 0
total_satellites = 0
for const_name in result.constellations:
cs = result.get_constellation_summary(const_name)
if cs:
total_cycle_slips += int(cs.get('cycle_slips', 0))
total_data_gaps += int(cs.get('data_gaps', 0))
total_satellites += int(cs.get('satellites_observed', 0))
duration_hours = max(result.duration_hours, 0.01)
slips_per_sat_hour = (total_cycle_slips / duration_hours) / max(total_satellites, 1)
# Score: Target <0.1 slips/sat/hour = 100, >2 slips/sat/hour = 0
lock_integrity_score = max(0, min(100, 100 - (slips_per_sat_hour * 50)))
π°οΈ Constellation Summary
Per-constellation statistics returned by get_constellation_summary(name).
# Get stats for each constellation
for const_name in ['GPS', 'GLONASS', 'Galileo', 'BeiDou']:
stats = result.get_constellation_summary(const_name)
if stats:
print(f"{const_name}:")
print(f" Satellites: {stats['satellites_observed']}/{stats['satellites_expected']}")
print(f" CN0: {stats['cn0_mean']} Β± {stats['cn0_std']} dB-Hz")
print(f" Cycle Slips: {stats['cycle_slips']}")
print(f" Data Gaps: {stats['data_gaps']}")
| Key | Type | Description |
|---|---|---|
| constellation | str | Constellation code (G, R, E, C, J, I) |
| satellites_observed | int | Number of unique satellites tracked |
| satellites_expected | int | Expected satellites (from ephemeris or nominal) |
| availability_ratio | float | Ratio observed/expected (0.0-1.0) |
| cn0_mean | float | Mean CN0 for this constellation (dB-Hz) |
| cn0_std | float | CN0 standard deviation (dB-Hz) |
| cycle_slips | int | Number of detected cycle slips |
| data_gaps | int | Number of data gaps/outages |
π°οΈ GNSS System Codes
| Code | System | Nominal Satellites | Frequencies |
|---|---|---|---|
| G | GPS (USA) | 31 | L1, L2, L5 |
| R | GLONASS (Russia) | 24 | G1, G2, G3 |
| E | Galileo (EU) | 30 | E1, E5a, E5b, E6 |
| C | BeiDou (China) | 35+ | B1, B2, B3 |
| J | QZSS (Japan) | 4 | L1, L2, L5, L6 |
| I | NavIC/IRNSS (India) | 7 | L5, S |
β οΈ Anomalies
Detected interference events returned by get_anomalies().
anomalies = result.get_anomalies()
for a in anomalies:
print(f"Type: {a.get('anomaly_type', a.get('type'))}")
print(f"Severity: {a.get('severity')}")
print(f"Time: {a.get('start_time', a.get('timestamp'))}")
print(f"Duration: {a.get('duration_seconds', a.get('duration'))}s")
print(f"CN0 Drop: {a.get('cn0_drop', a.get('cn0_drop_db'))} dB")
print(f"Affected: {a.get('affected_satellite_count')} satellites")
print(f"Confidence: {a.get('confidence') * 100:.0f}%")
print(f"Recommendation: {a.get('recommendation')}")
print("---")
| Key | Type | Description |
|---|---|---|
| anomaly_type / type | str | Anomaly classification (see below) |
| severity | str | "Critical", "High", "Medium", "Low" |
| start_time / timestamp | str | ISO timestamp of event start |
| duration_seconds / duration | float | Event duration in seconds |
| cn0_drop / cn0_drop_db | float | CN0 degradation magnitude (dB) |
| confidence | float | Detection confidence (0.0-1.0) |
| affected_satellite_count | int | Number of satellites affected |
| recommendation / description | str | Suggested action or description |
π Anomaly Types
| Type | Description | Typical Cause |
|---|---|---|
| Jamming | Rapid CN0 drop >6dB in <3s across multiple satellites | Intentional interference, nearby transmitter |
| Spoofing | Abnormally uniform CN0, elevation anomalies | GNSS signal falsification attempt |
| Multipath | Periodic CN0 variations correlated with satellite motion | Signal reflections from buildings/terrain |
| Interference | Gradual CN0 degradation or elevated noise floor | Unintentional RF interference |
| Signal Loss | Complete loss of satellite tracking | Obstruction, receiver fault |
| Cycle Slip | Discontinuity in carrier phase | Signal interruption, low CN0 |
π¦ Severity Levels
| Severity | CN0 Drop | Affected Sats | Action |
|---|---|---|---|
| Critical | >15 dB | >50% | Immediate investigation required |
| High | 10-15 dB | 25-50% | Review data quality carefully |
| Medium | 6-10 dB | 10-25% | Note for post-processing |
| Low | <6 dB | <10% | Informational only |
π Timeseries Data
Time-binned data returned by get_timeseries_data() and related methods.
# Method 1: Individual series
timestamps = result.get_timestamps() # List[str]
cn0_series = result.get_mean_cn0_series() # List[float]
sat_counts = result.get_satellite_count_series() # List[int]
# Method 2: Complete timeseries dict
ts_data = result.get_timeseries_data()
timestamps = ts_data['timestamps']
cn0_mean = ts_data['cn0_mean']
sat_counts = ts_data.get('satellite_counts', [])
by_const = ts_data.get('by_constellation', {}) # Per-constellation data
# Method 3: Full JSON with satellite_timeseries
result_json = json.loads(result.to_json())
sat_timeseries = result_json['timeseries']['satellite_timeseries']
# Per-satellite data structure
for sat_id, sat_data in sat_timeseries.items():
cn0_series = sat_data.get('cn0_series', [])
for point in cn0_series:
ts = point.get('timestamp') # ISO timestamp
cn0 = point.get('value') # CN0 in dB-Hz
π Timeseries Dict Structure
| Key | Type | Description |
|---|---|---|
| timestamps | List[str] | ISO-8601 timestamps for each bin |
| cn0_mean | List[float] | Mean CN0 per time bin (dB-Hz) |
| satellite_counts | List[int] | Number of tracked satellites per bin |
| by_constellation | Dict | Per-constellation timeseries |
| satellite_timeseries | Dict | Per-satellite CN0 series (in JSON) |
π°οΈ Satellite Timeseries Structure (from JSON)
{
"satellite_timeseries": {
"G01": {
"cn0_series": [
{"timestamp": "2024-06-01T00:00:00Z", "value": 45.2},
{"timestamp": "2024-06-01T00:01:00Z", "value": 44.8},
...
]
},
"G02": { ... },
"E01": { ... },
...
}
}
π°οΈ Skyplot Data
Satellite position traces returned by get_skyplot_data(). Requires navigation file.
skyplot_data = result.get_skyplot_data() # List[Dict]
for trace in skyplot_data:
sat_id = trace.get('satellite', trace.get('name'))
system = trace.get('system', trace.get('constellation'))
# Data may be CSV strings or lists
azimuths = trace.get('azimuths') # "45.2,46.1,47.0" or [45.2, 46.1, 47.0]
elevations = trace.get('elevations') # "30.5,31.2,32.0" or [30.5, 31.2, 32.0]
cn0_values = trace.get('cn0_values') # "42.1,42.5,43.0" or [42.1, 42.5, 43.0]
timestamps = trace.get('timestamps') # Optional
# Parse CSV strings if needed
if isinstance(azimuths, str):
azimuths = [float(x) for x in azimuths.split(',') if x]
| Key | Type | Unit | Description |
|---|---|---|---|
| satellite / name | str | β | Satellite ID (e.g., "G01", "E05") |
| system / constellation | str | β | System code (G, R, E, C) |
| azimuths | str | List[float] | degrees | Azimuth angles (0=N, 90=E, 180=S, 270=W) |
| elevations | str | List[float] | degrees | Elevation angles (0=horizon, 90=zenith) |
| cn0_values | str | List[float] | dB-Hz | CN0 at each position |
| timestamps | str | List[str] | β | Optional: ISO timestamps |
πΊοΈ Polar Plot Conversion
# Convert to polar coordinates for plotting
# r = 90 - elevation (so zenith is at center, horizon at edge)
# theta = azimuth
for trace in skyplot_data:
elevations = [float(x) for x in trace['elevations'].split(',') if x]
azimuths = [float(x) for x in trace['azimuths'].split(',') if x]
r_vals = [90 - el for el in elevations] # Radial distance
theta_vals = azimuths # Angular position
# Use with plotly.Scatterpolar or matplotlib polar plot
π CN0 Thresholds & Interpretation
Reference values for interpreting CN0 measurements.
πΆ Signal Quality Thresholds
| CN0 Range (dB-Hz) | Quality | Interpretation |
|---|---|---|
| β₯45 | Excellent | Strong signal, clear sky, no interference |
| 38-45 | Good | Normal operation, minor obstructions OK |
| 30-38 | Moderate | Usable but degraded, possible multipath |
| 25-30 | Poor | Marginal tracking, high noise |
| <25 | Critical | Tracking threshold, likely loss of lock |
π‘οΈ Interference Detection Thresholds
| Threshold | Value | Reference | Description |
|---|---|---|---|
| ITU Interference | 1 dB noise rise | ITU-R M.1902-1 | I/N = -6 dB criterion |
| Subtle Interference | 4 dB drop | Industry practice | Detectable impact on position accuracy |
| Significant Interference | 6 dB drop | Stanford GPS Lab | Noticeable degradation |
| Severe Interference | 10 dB drop | Industry practice | Major impact on tracking |
| Jamming | >15 dB drop in <3s | Research literature | Intentional interference |
π― Spoofing Indicators
| Indicator | Threshold | Description |
|---|---|---|
| Unexpected satellites | >40% of observed satellites not predicted by BRDC ephemeris | Primary spoofing indicator β signals from unexpected directions |
| Unexpected count | >8 unexpected satellites | Minimum count filter to suppress sparse-data false positives |
| Corroboration (A) | Sustained anomaly >300 s | Must have either A or B; prevents noise-burst false positives |
| Corroboration (B) | Unexpected ratio >60% | Overwhelming ratio bypasses duration requirement |
| Low CN0 uniformity | Std Dev <2 dB-Hz | Legacy indicator β still useful for single-transmitter spoofing |
| Elevated Mean CN0 | >50 dB-Hz | Unrealistically strong signals |
π References
- ITU-R M.1902-1: Characteristics and protection criteria for RNSS
- Stanford GPS Lab: GNSS interference monitoring research
- GPS Solutions Journal: Multi-GNSS orbit quality and monitoring
- ICAO SARPs: Standards and Recommended Practices for navigation
π‘ GeoVeil CN0 Library
GNSS Signal Quality Analysis β’ Interference Detection β’ Multi-Constellation Support
Based on: GNSS_Multipath_Analysis_Software, ITU recommendations, Stanford GPS Lab research
================================================================================
π QUICK CODE REFERENCE
================================================================================
# ============================================================================
# geoveil_cn0 Quick Reference
# ============================================================================
import geoveil_cn0 as gcn0
# === CONFIGURATION ===
config = gcn0.AnalysisConfig(
min_elevation=5.0, # degrees (default: 5.0)
time_bin_seconds=60, # seconds (default: 60)
systems=['G', 'R', 'E', 'C'],# G=GPS, R=GLONASS, E=Galileo, C=BeiDou
detect_anomalies=True, # Enable anomaly detection
anomaly_sensitivity=0.5, # 0.1 (strict) to 1.0 (loose)
interference_threshold_db=6.0, # dB threshold
)
# New in 0.3.8:
config.spoofing_unexpected_threshold # float: 0.4 (ratio threshold)
config.spoofing_min_unexpected_count # float: 8.0 (min count)
# === ANALYSIS ===
analyzer = gcn0.CN0Analyzer(config)
result = analyzer.analyze_with_nav(obs_path, nav_path) # With navigation
# result = analyzer.analyze_file(obs_path) # Without navigation
# === RESULT PROPERTIES ===
result.filename # str: Input filename
result.rinex_version # str: "3.04", etc.
result.duration_hours # float: Data duration
result.epoch_count # int: Number of epochs
result.constellations # List[str]: ['GPS', 'Galileo', ...]
result.avg_cn0 # float: Mean CN0 (dB-Hz)
result.cn0_std_dev # float: CN0 std dev
result.min_cn0, result.max_cn0 # float: CN0 range
result.jamming_detected # bool: Jamming detected?
result.spoofing_detected # bool: Spoofing detected?
result.interference_detected # bool: Any interference?
result.anomaly_count # int: Number of anomalies
result.summary # str: Human-readable summary
result.has_visibility_prediction # bool: nav-based visibility used?
result.visibility_confirmation_rate # float: fraction confirmed by ephemeris
result.visibility_mean_unexpected # float: mean unexpected satellites
result.visibility_mean_missing # float: mean missing satellites
result.visibility_anomaly_count # int: visibility anomaly events
# === QUALITY SCORE ===
qs = result.quality_score
qs.overall # float: 0-100 composite score
qs.rating # str: "Excellent"/"Good"/"Degraded"/"Poor"
qs.cn0_quality # float: 0-100 (35% weight)
qs.availability # float: 0-100 (20% weight)
qs.continuity # float: 0-100 (20% weight)
qs.stability # float: 0-100 (15% weight)
qs.diversity # float: 0-100 (10% weight)
qs.post_processing_suitable # bool: overall >= 70
# === METHODS ===
result.get_systems() # List[str]: Constellation codes
result.get_constellation_summary('GPS') # Dict: Per-constellation stats
result.get_anomalies() # List[Dict]: Anomaly events
result.get_timeseries_data() # Dict: Time-binned data
result.get_timestamps() # List[str]: ISO timestamps
result.get_mean_cn0_series() # List[float]: CN0 timeseries
result.get_satellite_count_series() # List[int]: Satellite counts
result.get_skyplot_data() # List[Dict]: Satellite traces
result.to_json() # str: Full JSON export
result.get_visibility_anomaly_types() # List[str]
result.get_visibility_anomaly_descriptions() # List[str]
result.get_frequently_unexpected_satellites() # List[Dict]
result.to_json_pretty() # str: pretty JSON
# === CONSTELLATION SUMMARY KEYS ===
# cs = result.get_constellation_summary('GPS')
# cs['constellation'] # str: 'G'
# cs['satellites_observed'] # int: Tracked satellites
# cs['satellites_expected'] # int: Expected from ephemeris
# cs['availability_ratio'] # float: 0.0-1.0
# cs['cn0_mean'] # float: Mean CN0
# cs['cn0_std'] # float: CN0 std dev
# cs['cycle_slips'] # int: Cycle slip count
# cs['data_gaps'] # int: Data gap count
# === ANOMALY DICT KEYS ===
# a = result.get_anomalies()[0]
# a['anomaly_type'] # str: 'Jamming', 'Spoofing', 'Interference', etc.
# a['severity'] # str: 'Critical', 'High', 'Medium', 'Low'
# a['start_time'] # str: ISO timestamp
# a['duration_seconds'] # float: Duration
# a['cn0_drop'] # float: CN0 drop in dB
# a['confidence'] # float: 0.0-1.0
# a['affected_satellite_count'] # int: Affected satellites
# a['recommendation'] # str: Suggested action
# === SKYPLOT TRACE KEYS ===
# trace = result.get_skyplot_data()[0]
# trace['satellite'] # str: 'G01', 'E05', etc.
# trace['system'] # str: 'G', 'E', etc.
# trace['azimuths'] # str/List: Azimuth angles (deg)
# trace['elevations'] # str/List: Elevation angles (deg)
# trace['cn0_values'] # str/List: CN0 values (dB-Hz)