Skip to content

Union{Nothing, T} data fields make consumer arithmetic box — consider publishing concrete ephemeris/clock substructs on validation #75

Description

@giove-a

Problem

Every navigation-data field on the published data (e.g. GPSL1CAData.sqrt_A, .e, .a_f0, …) is Union{Nothing, T}, because fields fill in incrementally as the message decodes. That layout is right for the staging phase, but consumers pay for it forever: any code that reads many of these fields in arithmetic chains (Kepler propagation, clock correction) exceeds the compiler's union-splitting budget, so every intermediate is boxed.

Measured in PositionVelocityTime.jl, this cost over half of calc_pvt's runtime and ~70% of its allocations. We worked around it downstream by extracting the fields once per satellite into concrete Float64 structs behind a function barrier — see JuliaGNSS/PositionVelocityTime.jl#52 (branch sc/performance) for the working layout and benchmark numbers (calc_pvt warm solve: 18.6 µs / 622 allocs → 11.3 µs / 183 allocs for 9 GPS satellites).

Suggestion

GNSSDecoder already has the right skeleton: decoders accumulate into raw_data and validate_data promotes to data once is_decoding_completed_for_positioning(raw_data) holds. The promotion point is exactly where a concrete snapshot could be published:

  • Keep the Union{Nothing, T} per-field layout for the staging area (raw_data) — it must exist during decoding anyway.
  • At promotion, additionally build concrete immutable substructs, e.g. data.ephemeris::Union{Nothing, GPSLNAVEphemeris} and data.clock::Union{Nothing, ClockPolynomial}, with all-Float64 fields inside. A single small union at the access point splits fine; everything downstream is concrete.

One design point worth stating explicitly: substructs as modeled by the existing almanac records would not fix this — GalileoAlmanac is itself all Union{Nothing, Float64} fields. The pattern that works is nullable at the top, concrete inside.

Side benefits beyond performance

  • Atomic, IOD-consistent publication: the ephemeris becomes one immutable value assembled at a consistency checkpoint, structurally preventing mixed-issue-of-data reads that the per-field model only avoids by care.
  • is_decoding_completed_for_positioning collapses to a couple of !isnothing checks.
  • Downstream packages (PVT) can drop their extraction layers and consume the decoder's types directly.

Costs / open questions

  • Breaking change to the public data layout → major version bump; all consumers reading data.sqrt_A etc. need migration (including PositionVelocityTime's iono/GGTO/week handling and both test suites).
  • T_GD and the CNAV/CNAV-2 ISCs can legitimately still be missing on a decoder that is complete for positioning (they ride in less-frequent message types), so they'd stay nullable or zero-defaulted inside the clock substruct — needs an explicit decision.
  • Whether to publish broadcast-faithful per-message-type structs (LNAV Keplerian vs. CNAV quasi-Keplerian) or a normalised orbit: PositionVelocityTime.jl#52 normalises at extraction (orbital_terms), which a decoder package may or may not want to own. Both options preserve the performance win.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions