From 83ee2661ce219f0721b27a44ad3325acbf7a98ca Mon Sep 17 00:00:00 2001 From: siebc <226531417+siebc@users.noreply.github.com> Date: Sat, 4 Jul 2026 15:37:41 +0000 Subject: [PATCH] feat: introduce per-constellation data supertypes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add AbstractGPSData and AbstractGalileoData as intermediate abstract supertypes between AbstractGNSSData and the concrete per-signal data types. Constellation membership is now encoded at each struct definition site (the `<: Abstract*Data` line written anyway), so a new signal inherits the shared behaviour with nothing to remember. is_ephemeris_decoded and is_clock_correction_decoded were byte-identical for Galileo I/NAV (E1B) and F/NAV (E5a) — the same orbital and clock fields must be present — so they are a per-constellation fact. They now dispatch once on AbstractGalileoData (in galileo/galileo.jl) instead of once per signal (4 methods -> 2). Genuinely per-signal checks (is_health_status_decoded, is_decoding_completed_for_positioning, is_sat_healthy) stay on the concrete types. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/src/api.md | 10 +++++++ src/galileo/e1b.jl | 32 ++++----------------- src/galileo/e5a.jl | 32 ++++----------------- src/galileo/galileo.jl | 32 +++++++++++++++++++++ src/gnss.jl | 32 +++++++++++++++++++++ src/gps/cnav.jl | 2 +- src/gps/l1c_d.jl | 2 +- src/gps/l1ca.jl | 2 +- test/gnss_supertype.jl | 64 ++++++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 1 + 10 files changed, 152 insertions(+), 57 deletions(-) create mode 100644 test/gnss_supertype.jl diff --git a/docs/src/api.md b/docs/src/api.md index 7ff370c..0b8d6cd 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -52,6 +52,16 @@ interleave! ## Data Types +Every concrete per-signal data type subtypes the abstract supertype of its +constellation, which in turn subtypes `AbstractGNSSData`. The supertypes carry +the facts every signal of a constellation shares (e.g. the Galileo +ephemeris/clock completeness checks), stated once via subtype dispatch. + +```@docs +GNSSDecoder.AbstractGPSData +GNSSDecoder.AbstractGalileoData +``` + ### GPS L1 C/A ```@docs diff --git a/src/galileo/e1b.jl b/src/galileo/e1b.jl index 60563ad..63d9588 100644 --- a/src/galileo/e1b.jl +++ b/src/galileo/e1b.jl @@ -286,7 +286,7 @@ OS SIS ICD, Issue 2.2. Galileo OS SIS ICD, Issue 2.2, Tables 42-55, 67-87 """ -Base.@kwdef struct GalileoE1BData <: AbstractGNSSData +Base.@kwdef struct GalileoE1BData <: AbstractGalileoData WN::Union{Nothing,Int64} = nothing TOW::Union{Nothing,Int64} = nothing @@ -492,32 +492,10 @@ function Base.:(==)(a::GalileoE1BData, b::GalileoE1BData) return true end -function is_ephemeris_decoded(data::GalileoE1BData) - !isnothing(data.t_0e) && - !isnothing(data.M_0) && - !isnothing(data.e) && - !isnothing(data.sqrt_A) && - !isnothing(data.Ω_0) && - !isnothing(data.i_0) && - !isnothing(data.ω) && - !isnothing(data.i_dot) && - !isnothing(data.Ω_dot) && - !isnothing(data.Δn) && - !isnothing(data.C_uc) && - !isnothing(data.C_us) && - !isnothing(data.C_rc) && - !isnothing(data.C_rs) && - !isnothing(data.C_ic) && - !isnothing(data.C_is) -end - -function is_clock_correction_decoded(data::GalileoE1BData) - !isnothing(data.t_0c) && - !isnothing(data.a_f0) && - !isnothing(data.a_f1) && - !isnothing(data.a_f2) -end - +# `is_ephemeris_decoded` and `is_clock_correction_decoded` are per-constellation +# facts (identical fields for I/NAV and F/NAV), defined once on +# `AbstractGalileoData` in `galileo/galileo.jl`. Only the health-status check +# below is genuinely per-signal (E1B carries E1-B/E5b health). function is_health_status_decoded(data::GalileoE1BData) !isnothing(data.signal_health_e1b) && !isnothing(data.signal_health_e5b) && diff --git a/src/galileo/e5a.jl b/src/galileo/e5a.jl index e62d0ce..78d5ddd 100644 --- a/src/galileo/e5a.jl +++ b/src/galileo/e5a.jl @@ -177,7 +177,7 @@ the decoder multiplies by π), matching the convention used by [`GalileoE1BData` Galileo OS SIS ICD, Issue 2.2, §5.1, Tables 75-80 """ -Base.@kwdef struct GalileoE5aData <: AbstractGNSSData +Base.@kwdef struct GalileoE5aData <: AbstractGalileoData WN::Union{Nothing,Int64} = nothing TOW::Union{Nothing,Int64} = nothing @@ -369,32 +369,10 @@ function Base.:(==)(a::GalileoE5aData, b::GalileoE5aData) return true end -function is_ephemeris_decoded(data::GalileoE5aData) - !isnothing(data.t_0e) && - !isnothing(data.M_0) && - !isnothing(data.e) && - !isnothing(data.sqrt_A) && - !isnothing(data.Ω_0) && - !isnothing(data.i_0) && - !isnothing(data.ω) && - !isnothing(data.i_dot) && - !isnothing(data.Ω_dot) && - !isnothing(data.Δn) && - !isnothing(data.C_uc) && - !isnothing(data.C_us) && - !isnothing(data.C_rc) && - !isnothing(data.C_rs) && - !isnothing(data.C_ic) && - !isnothing(data.C_is) -end - -function is_clock_correction_decoded(data::GalileoE5aData) - !isnothing(data.t_0c) && - !isnothing(data.a_f0) && - !isnothing(data.a_f1) && - !isnothing(data.a_f2) -end - +# `is_ephemeris_decoded` and `is_clock_correction_decoded` are per-constellation +# facts (identical fields for I/NAV and F/NAV), defined once on +# `AbstractGalileoData` in `galileo/galileo.jl`. Only the health-status check +# below is genuinely per-signal (E5a carries only E5a health). function is_health_status_decoded(data::GalileoE5aData) !isnothing(data.signal_health_e5a) && !isnothing(data.data_validity_status_e5a) end diff --git a/src/galileo/galileo.jl b/src/galileo/galileo.jl index bd366f7..9ecdabe 100644 --- a/src/galileo/galileo.jl +++ b/src/galileo/galileo.jl @@ -158,6 +158,38 @@ function GalileoAlmanac( ) end +# Ephemeris/clock completeness are per-constellation facts: I/NAV (E1B) and +# F/NAV (E5a) broadcast the same orbital and clock parameters, so the "all +# present?" checks are identical and dispatch on the constellation supertype +# `AbstractGalileoData` (both `GalileoE1BData` and `GalileoE5aData` subtype it). +# The health-status and positioning-readiness checks genuinely differ per signal +# and stay in `e1b.jl` / `e5a.jl`. +function is_ephemeris_decoded(data::AbstractGalileoData) + !isnothing(data.t_0e) && + !isnothing(data.M_0) && + !isnothing(data.e) && + !isnothing(data.sqrt_A) && + !isnothing(data.Ω_0) && + !isnothing(data.i_0) && + !isnothing(data.ω) && + !isnothing(data.i_dot) && + !isnothing(data.Ω_dot) && + !isnothing(data.Δn) && + !isnothing(data.C_uc) && + !isnothing(data.C_us) && + !isnothing(data.C_rc) && + !isnothing(data.C_rs) && + !isnothing(data.C_ic) && + !isnothing(data.C_is) +end + +function is_clock_correction_decoded(data::AbstractGalileoData) + !isnothing(data.t_0c) && + !isnothing(data.a_f0) && + !isnothing(data.a_f1) && + !isnothing(data.a_f2) +end + """ galileo_viterbi(decoder, soft_page, interleaver_rows, interleaver_cols, ::Type{T}) -> T diff --git a/src/gnss.jl b/src/gnss.jl index 8e0f89f..1caed60 100644 --- a/src/gnss.jl +++ b/src/gnss.jl @@ -2,6 +2,38 @@ abstract type AbstractGNSSConstants end abstract type AbstractGNSSData end abstract type AbstractGNSSCache end +""" + AbstractGPSData <: AbstractGNSSData + +Abstract supertype for the decoded navigation data of a signal transmitted by +the GPS constellation, e.g. `GPSL1CAData`, `GPSCNAVData`. + +Its purpose is to carry the constellation-level facts every GPS signal's data +shares, so they can be stated once (on the supertype, via subtype dispatch) +instead of once per signal. Constellation membership is encoded at the struct +definition site — the `<: AbstractGPSData` line written anyway — so a new GPS +signal inherits the shared behaviour with nothing to remember. Genuinely +per-signal facts (the subframe/message-type completeness checks, the health-bit +selection in `is_sat_healthy`) stay defined on the concrete data types. +""" +abstract type AbstractGPSData <: AbstractGNSSData end + +""" + AbstractGalileoData <: AbstractGNSSData + +Abstract supertype for the decoded navigation data of a signal transmitted by +the Galileo constellation, e.g. `GalileoE1BData`, `GalileoE5aData`. + +The Galileo counterpart to [`AbstractGPSData`](@ref). It carries the facts every +Galileo signal's data shares: `is_ephemeris_decoded` and +`is_clock_correction_decoded` check the same orbital and clock fields for I/NAV +(E1B) and F/NAV (E5a), so they are defined once on this supertype (see +`src/galileo/galileo.jl`) instead of once per signal. The health-status and +positioning-readiness checks genuinely differ per signal and stay on the +concrete data types. +""" +abstract type AbstractGalileoData <: AbstractGNSSData end + # Physical constants common to every GNSS handled here. Each per-signal # `*Constants` struct exposes these as fields (so the orbit/clock math reads # `state.constants.PI` etc.); the defaults are sourced from here to keep a single diff --git a/src/gps/cnav.jl b/src/gps/cnav.jl index 5f20720..b561bc3 100644 --- a/src/gps/cnav.jl +++ b/src/gps/cnav.jl @@ -409,7 +409,7 @@ code that applies these corrections must handle `nothing` (treat as 0). IS-GPS-705J, Figures 20-1 through 20-17 and Tables 20-I through 20-XIa. """ -Base.@kwdef struct GPSCNAVData <: AbstractGNSSData +Base.@kwdef struct GPSCNAVData <: AbstractGPSData last_message_id::Int = 0 TOW::Union{Nothing,Int64} = nothing alert_flag::Union{Nothing,Bool} = nothing diff --git a/src/gps/l1c_d.jl b/src/gps/l1c_d.jl index 74bb6b0..b8de33d 100644 --- a/src/gps/l1c_d.jl +++ b/src/gps/l1c_d.jl @@ -368,7 +368,7 @@ CRC-valid SF3 page regardless of whether its page format is parsed. IS-GPS-800J, Figures 3.5-1 through 3.5-9 and Tables 3.5-1, 3.5-3 … 3.5-8. """ -Base.@kwdef struct GPSL1C_DData <: AbstractGNSSData +Base.@kwdef struct GPSL1C_DData <: AbstractGPSData toi::Union{Nothing,Int} = nothing ITOW::Union{Nothing,Int64} = nothing WN::Union{Nothing,Int64} = nothing diff --git a/src/gps/l1ca.jl b/src/gps/l1ca.jl index 56753d3..eec7141 100755 --- a/src/gps/l1ca.jl +++ b/src/gps/l1ca.jl @@ -137,7 +137,7 @@ subframes 1, 2, and 3 of the GPS LNAV message. All parameters conform to IS-GPS- IS-GPS-200N, Tables 20-I, 20-II, 20-III, Sections 20.3.3.3-20.3.3.4 """ -Base.@kwdef struct GPSL1CAData <: AbstractGNSSData +Base.@kwdef struct GPSL1CAData <: AbstractGPSData last_subframe_id::Int = 0 integrity_status_flag::Union{Nothing,Bool} = nothing TOW::Union{Nothing,Int64} = nothing diff --git a/test/gnss_supertype.jl b/test/gnss_supertype.jl new file mode 100644 index 0000000..bae47df --- /dev/null +++ b/test/gnss_supertype.jl @@ -0,0 +1,64 @@ +@testset "GNSS data supertypes" begin + # The per-constellation abstract supertypes sit between `AbstractGNSSData` + # and the concrete per-signal data types. + @test GNSSDecoder.AbstractGPSData <: GNSSDecoder.AbstractGNSSData + @test GNSSDecoder.AbstractGalileoData <: GNSSDecoder.AbstractGNSSData + + gps_data = [GNSSDecoder.GPSL1CAData, GNSSDecoder.GPSL1C_DData, GNSSDecoder.GPSCNAVData] + galileo_data = [GNSSDecoder.GalileoE1BData, GNSSDecoder.GalileoE5aData] + + # Constellation membership is encoded at each struct's definition site. + for D in gps_data + @test D <: GNSSDecoder.AbstractGPSData + @test !(D <: GNSSDecoder.AbstractGalileoData) + end + for D in galileo_data + @test D <: GNSSDecoder.AbstractGalileoData + @test !(D <: GNSSDecoder.AbstractGPSData) + end + + # `is_ephemeris_decoded` / `is_clock_correction_decoded` are stated once per + # constellation: every Galileo data type dispatches to the single method on + # `AbstractGalileoData` rather than a per-signal copy. + for D in galileo_data + @test which(GNSSDecoder.is_ephemeris_decoded, (D,)).sig == Tuple{ + typeof(GNSSDecoder.is_ephemeris_decoded), + GNSSDecoder.AbstractGalileoData, + } + @test which(GNSSDecoder.is_clock_correction_decoded, (D,)).sig == Tuple{ + typeof(GNSSDecoder.is_clock_correction_decoded), + GNSSDecoder.AbstractGalileoData, + } + end + + # The collapsed methods still behave: all fields present ⇒ decoded, a + # missing field ⇒ not decoded. Exercised for both Galileo signals. + for D in galileo_data + empty = D() + @test !GNSSDecoder.is_ephemeris_decoded(empty) + @test !GNSSDecoder.is_clock_correction_decoded(empty) + + full_eph = D(; + t_0e = 0, + M_0 = 0.0, + e = 0.0, + sqrt_A = 0.0, + Ω_0 = 0.0, + i_0 = 0.0, + ω = 0.0, + i_dot = 0.0, + Ω_dot = 0.0, + Δn = 0.0, + C_uc = 0.0, + C_us = 0.0, + C_rc = 0.0, + C_rs = 0.0, + C_ic = 0.0, + C_is = 0.0, + ) + @test GNSSDecoder.is_ephemeris_decoded(full_eph) + + full_clock = D(; t_0c = 0, a_f0 = 0.0, a_f1 = 0.0, a_f2 = 0.0) + @test GNSSDecoder.is_clock_correction_decoded(full_clock) + end +end diff --git a/test/runtests.jl b/test/runtests.jl index 42237c6..51cfc81 100755 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -32,6 +32,7 @@ end Aqua.test_all(GNSSDecoder) end + include("gnss_supertype.jl") include("bit_fiddling.jl") include("gpsl1.jl") include("gps_l1c_d.jl")