Source code for empyrean.od.radar_observations
"""ADES radar observation types."""
from collections.abc import Callable
import pyarrow as pa
import pyarrow.compute as pc
import quivr as qv
# pyarrow's compute functions are generated at runtime into the
# ``pyarrow.compute`` module namespace (see ``_make_global_functions``),
# so the bundled type stubs do not declare them and mypy cannot resolve
# ``pc.is_in`` / ``pc.equal`` as attributes. Bind the ones we use to
# precisely-typed module-level aliases via the module ``__dict__`` (the
# alias is the exact same function object — runtime behavior is unchanged)
# so every call site gets a real signature.
_is_in: Callable[..., pa.BooleanArray] = pc.__dict__["is_in"]
_equal: Callable[[pa.ChunkedArray | pa.Array, pa.Scalar], pa.BooleanArray] = pc.__dict__["equal"]
[docs]
class ADESRadarObservations(qv.Table):
"""ADES radar observations — full schema.
ADES models radar astrometry as its own top-level table, parallel to
the optical :class:`~empyrean.od.ades_observations.ADESObservations`
(not as an optical ``mode``). Each record carries a round-trip time
**delay** *or* a **Doppler** shift — the ADES ``RadarValue`` is an XSD
``<choice>`` — referred to a transmitting (``trx``) and receiving
(``rcv``) station (equal for a monostatic observation, distinct for a
bistatic one).
Units are ADES-native end to end — no conversion is applied in this
layer (the single SI normalization happens once downstream):
* ``delay`` — round-trip time delay in **seconds**
* ``rms_delay`` — its 1σ uncertainty in **microseconds** (the
asymmetry vs ``delay`` is intentional in the ADES schema)
* ``doppler`` / ``rms_doppler`` — Doppler shift and 1σ in **Hz**
(signed value)
* ``frq`` — transmit carrier reference frequency in **MHz**
The ``observable`` discriminator column (``"delay"`` / ``"doppler"``)
— *not* a NaN probe — records which measurement each row carries, so a
genuine 0.0-Hz Doppler is never confused with an absent one. The
inactive value column is null for that row.
"""
# ── Identification ────────────────────────────────
perm_id = qv.LargeStringColumn(nullable=True)
prov_id = qv.LargeStringColumn(nullable=True)
trk_sub = qv.LargeStringColumn(nullable=True)
# ── Bistatic geometry ─────────────────────────────
trx = qv.LargeStringColumn() # MPC station code of the transmitter
rcv = qv.LargeStringColumn() # MPC station code of the receiver
# ── Core measurement ──────────────────────────────
obs_time = qv.LargeStringColumn() # ISO 8601 UTC (receive epoch)
# Discriminator: which RadarValue choice this row carries.
observable = qv.LargeStringColumn() # "delay" | "doppler"
delay = qv.Float64Column(nullable=True) # seconds
rms_delay = qv.Float64Column(nullable=True) # MICROSECONDS (ADES rmsDelay)
doppler = qv.Float64Column(nullable=True) # Hz (signed)
rms_doppler = qv.Float64Column(nullable=True) # Hz
# ── Reduction metadata ────────────────────────────
frq = qv.Float64Column() # MHz (transmit carrier reference)
com = qv.BooleanColumn(nullable=True) # center-of-mass flag (ADES com)
log_snr = qv.Float64Column(nullable=True)
remarks = qv.LargeStringColumn(nullable=True)
# ── Selection helpers ────────────────────────────────
[docs]
def select_station(self, codes: str | list[str]) -> "ADESRadarObservations":
"""Rows whose receiving station (``rcv``) is one of ``codes``."""
wanted = [codes] if isinstance(codes, str) else list(codes)
mask = _is_in(self.column("rcv"), value_set=pa.array(wanted))
return self.apply_mask(mask)
[docs]
def delays(self) -> "ADESRadarObservations":
"""Rows carrying a delay measurement (``observable == "delay"``)."""
mask = _equal(self.column("observable"), pa.scalar("delay"))
return self.apply_mask(mask)
[docs]
def dopplers(self) -> "ADESRadarObservations":
"""Rows carrying a Doppler measurement (``observable == "doppler"``)."""
mask = _equal(self.column("observable"), pa.scalar("doppler"))
return self.apply_mask(mask)