πŸ“‘ 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

πŸ“‘ Contents: Quick Start Configuration Analyzer Analysis Result Quality Score Constellation Data Anomalies Timeseries Skyplot 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
)
ParameterTypeDefaultDescription
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)

PresetSensitivityThresholdUse CaseReference
Full Analysis0.38 dBComplete analysis with all plotsGeneral purpose
Quick Summary0.510 dBFast overview, skip heavy plotsRapid assessment
Interference Focus0.154 dBDetect subtle interferenceITU I/N=-6dB criterion
Jamming Detection0.26 dBRapid CN0 drops in <3sStanford GPS Lab
Spoofing Check0.15 dBCN0 uniformity anomaliesGPS 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)
MethodParametersReturnsDescription
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

TypeExtensionsDescription
Observation.obs, .rnx, .crx, .YYo (e.g., .24o, .25o)RINEX 2.x/3.x/4.x observation files
Navigation.nav, .rnx, .YYn, .YYgBRDC navigation files
Precise Orbit.sp3, .SP3SP3 precise ephemeris (IGS/MGEX)
Compressed.gz, .ZGzip or Unix compress (auto-decompressed)

πŸ“Š AnalysisResult

Container for all analysis results. Returned by analyzer methods.

πŸ“‹ File Information Properties

PropertyTypeDescription
filenamestrInput filename
rinex_versionstrRINEX format version (e.g., "3.04")
station_namestr | NoneStation/marker name from header
duration_hoursfloatData duration in hours
epoch_countintNumber of observation epochs
constellationsList[str]Constellation names present (GPS, Galileo, etc.)

πŸ“Ά Signal Quality Properties

PropertyTypeUnitDescription
avg_cn0 / mean_cn0floatdB-HzAverage carrier-to-noise ratio
cn0_std_devfloatdB-HzCN0 standard deviation
min_cn0floatdB-HzMinimum CN0 observed
max_cn0floatdB-HzMaximum CN0 observed
skyplot_coveragefloat%Sky coverage percentage (requires nav)

πŸ›‘οΈ Threat Detection Properties

PropertyTypeDescription
jamming_detectedboolTrue if jamming patterns detected (rapid >6dB drops)
spoofing_detectedboolTrue if spoofing indicators detected β€” unexpected satellite detection via BRDC ephemeris comparison (requires nav file)
interference_detectedboolTrue if any interference detected
anomaly_countintNumber of anomaly events detected
summarystrHuman-readable analysis summary

πŸ›°οΈ Visibility Prediction Properties (requires nav file)

PropertyTypeDescription
has_visibility_predictionboolTrue if ephemeris-based visibility assessment was performed
visibility_prediction_sourcestr | NoneEphemeris source used (e.g., "BRDC")
visibility_mean_observedfloatMean number of observed satellites per epoch
visibility_mean_predictedfloatMean number of predicted visible satellites per epoch
visibility_mean_unexpectedfloatMean number of observed-but-not-predicted satellites (spoofing indicator)
visibility_mean_missingfloatMean number of predicted-but-not-observed satellites
visibility_confirmation_ratefloatFraction of observed satellites confirmed by ephemeris (0.0–1.0)
visibility_anomaly_countintNumber of visibility anomaly events detected

πŸ”§ Methods

MethodReturnsDescription
get_systems()List[str]List of constellation codes present
get_constellation_summary(name)DictStatistics for specific constellation
get_anomalies()List[Dict]List of detected anomaly events
get_timeseries_data()DictTime-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()strSerialize 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()strPretty-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}")
PropertyTypeRangeWeightDescription
overallfloat0-100β€”Weighted composite score
ratingstrβ€”β€”"Excellent", "Good", "Degraded", "Poor"
cn0_qualityfloat0-10035%Signal strength quality (based on mean CN0)
availabilityfloat0-10020%Satellite availability ratio
continuityfloat0-10020%Signal continuity (few gaps/slips)
stabilityfloat0-10015%CN0 stability (low variance)
diversityfloat0-10010%Multi-constellation diversity
post_processing_suitableboolβ€”β€”True if overall β‰₯70

πŸ“Š Rating Thresholds

Score RangeRatingInterpretation
β‰₯80ExcellentHigh quality data for precise positioning
60-79GoodSuitable for standard GNSS applications
40-59DegradedSome issues detected, review anomalies
<40PoorSignificant 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']}")
KeyTypeDescription
constellationstrConstellation code (G, R, E, C, J, I)
satellites_observedintNumber of unique satellites tracked
satellites_expectedintExpected satellites (from ephemeris or nominal)
availability_ratiofloatRatio observed/expected (0.0-1.0)
cn0_meanfloatMean CN0 for this constellation (dB-Hz)
cn0_stdfloatCN0 standard deviation (dB-Hz)
cycle_slipsintNumber of detected cycle slips
data_gapsintNumber of data gaps/outages

πŸ›°οΈ GNSS System Codes

CodeSystemNominal SatellitesFrequencies
GGPS (USA)31L1, L2, L5
RGLONASS (Russia)24G1, G2, G3
EGalileo (EU)30E1, E5a, E5b, E6
CBeiDou (China)35+B1, B2, B3
JQZSS (Japan)4L1, L2, L5, L6
INavIC/IRNSS (India)7L5, 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("---")
KeyTypeDescription
anomaly_type / typestrAnomaly classification (see below)
severitystr"Critical", "High", "Medium", "Low"
start_time / timestampstrISO timestamp of event start
duration_seconds / durationfloatEvent duration in seconds
cn0_drop / cn0_drop_dbfloatCN0 degradation magnitude (dB)
confidencefloatDetection confidence (0.0-1.0)
affected_satellite_countintNumber of satellites affected
recommendation / descriptionstrSuggested action or description

πŸ“‹ Anomaly Types

TypeDescriptionTypical Cause
JammingRapid CN0 drop >6dB in <3s across multiple satellitesIntentional interference, nearby transmitter
SpoofingAbnormally uniform CN0, elevation anomaliesGNSS signal falsification attempt
MultipathPeriodic CN0 variations correlated with satellite motionSignal reflections from buildings/terrain
InterferenceGradual CN0 degradation or elevated noise floorUnintentional RF interference
Signal LossComplete loss of satellite trackingObstruction, receiver fault
Cycle SlipDiscontinuity in carrier phaseSignal interruption, low CN0

🚦 Severity Levels

SeverityCN0 DropAffected SatsAction
Critical>15 dB>50%Immediate investigation required
High10-15 dB25-50%Review data quality carefully
Medium6-10 dB10-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

KeyTypeDescription
timestampsList[str]ISO-8601 timestamps for each bin
cn0_meanList[float]Mean CN0 per time bin (dB-Hz)
satellite_countsList[int]Number of tracked satellites per bin
by_constellationDictPer-constellation timeseries
satellite_timeseriesDictPer-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]
KeyTypeUnitDescription
satellite / namestrβ€”Satellite ID (e.g., "G01", "E05")
system / constellationstrβ€”System code (G, R, E, C)
azimuthsstr | List[float]degreesAzimuth angles (0=N, 90=E, 180=S, 270=W)
elevationsstr | List[float]degreesElevation angles (0=horizon, 90=zenith)
cn0_valuesstr | List[float]dB-HzCN0 at each position
timestampsstr | 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)QualityInterpretation
β‰₯45ExcellentStrong signal, clear sky, no interference
38-45GoodNormal operation, minor obstructions OK
30-38ModerateUsable but degraded, possible multipath
25-30PoorMarginal tracking, high noise
<25CriticalTracking threshold, likely loss of lock

πŸ›‘οΈ Interference Detection Thresholds

ThresholdValueReferenceDescription
ITU Interference1 dB noise riseITU-R M.1902-1I/N = -6 dB criterion
Subtle Interference4 dB dropIndustry practiceDetectable impact on position accuracy
Significant Interference6 dB dropStanford GPS LabNoticeable degradation
Severe Interference10 dB dropIndustry practiceMajor impact on tracking
Jamming>15 dB drop in <3sResearch literatureIntentional interference

🎯 Spoofing Indicators

IndicatorThresholdDescription
Unexpected satellites>40% of observed satellites not predicted by BRDC ephemerisPrimary spoofing indicator β€” signals from unexpected directions
Unexpected count>8 unexpected satellitesMinimum count filter to suppress sparse-data false positives
Corroboration (A)Sustained anomaly >300 sMust have either A or B; prevents noise-burst false positives
Corroboration (B)Unexpected ratio >60%Overwhelming ratio bypasses duration requirement
Low CN0 uniformityStd Dev <2 dB-HzLegacy indicator β€” still useful for single-transmitter spoofing
Elevated Mean CN0>50 dB-HzUnrealistically 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)