Observation ID (ObsID) Registry#
Libera’s ICIE flight software tags certain telemetry packets with a small integer ObsID
identifying which calibration event or scan mode is running (gain/noise cal, a specific LED or
blackbody temperature, a solar diffuser face, a lunar scan, cross-track imaging, and so on).
libera_utils.obsids is the sole source of truth mapping those ObsIDs to the
DataProductIdentifiers they produce. This page covers how the registry is structured and how it
is used, both inside this package and by downstream algorithm repos.
ObsID sources#
ObsIDs are reported in two separate NOM-HK telemetry fields, one per instrument:
Source |
NOM-HK variable |
Typical events |
|---|---|---|
|
|
Radiometer cal (gain, noise, SWC, LWC, solar, lunar, VIIRS) |
|
|
Camera cal (CT/RAPS video, darks, VIIRS lunar) |
The numeric values are not globally unique — ObsID 256 means “SWC-365NM” on the radiometer
but “Darks of Darks” on the camera. Because of this, everything in libera_utils.obsids is keyed
by (NomHkObsidSource, obsid), never by obsid alone. NomHkObsidSource is the enum that
disambiguates which telemetry field a given ObsID was read from.
ObsIdSpec and OBSID_REGISTRY#
Each known (source, obsid) pair maps to one ObsIdSpec:
@dataclass(frozen=True)
class ObsIdSpec:
obsid: int
source: NomHkObsidSource
kind: ObsIdKind
description: str
trimmed_product: DataProductIdentifier | None
cal_product: DataProductIdentifier | None
kindis one ofObsIdKind.RAD_CAL,ObsIdKind.CAM_CAL, orObsIdKind.SCIENCE. Science/scan modes (e.g. Cross Track, RAP Scan, the Geo scans) are cataloged for documentation but never produce TRIMMED or CAL products — both product fields areNone.trimmed_productis the L1ANOM-HK-*-FAMILY-TRIMMEDDataProductIdentifierproduced by the trim step (below) for this ObsID, orNonefor non-trim-eligible entries. It names a calibration dependency family, not a single ObsID — see below.cal_productis theCAL-levelDataProductIdentifiera downstream cal-combine algorithm is expected to produce for this ObsID, orNone.
OBSID_REGISTRY is the dict[tuple[NomHkObsidSource, int], ObsIdSpec] built from all of these.
Look entries up with get_obsid_spec, or iterate the trim-eligible subset with
iter_trim_eligible (optionally filtered by source):
from libera_utils.obsids import NomHkObsidSource, get_obsid_spec, iter_trim_eligible
spec = get_obsid_spec(NomHkObsidSource.RAD, 256)
spec.cal_product # DataProductIdentifier.cal_swc_365nm
spec.trimmed_product # DataProductIdentifier.l1a_icie_nom_hk_swc_family_trimmed
# All RAD entries that produce a TRIMMED product (excludes science/scan modes):
rad_trim_eligible = list(iter_trim_eligible(NomHkObsidSource.RAD))
get_obsid_spec raises KeyError for a pair that isn’t registered — there is no silent fallback,
so an unrecognized ObsID in telemetry surfaces immediately rather than being misfiled.
Calibration dependency families#
An ObsID does not get its own TRIMMED product. Several ObsIDs share one: the TRIMMED column
names a calibration dependency family, one product per group of ObsIDs that a downstream
algorithm processes identically and that therefore share a set of input products. libera_rad runs
the same shortwave-LED cal on ObsIDs 256 through 261 regardless of wavelength, so all six share
NOM-HK-SWC-FAMILY-TRIMMED. There is one ProcessingStepIdentifier per family, not per ObsID.
Family ProductID |
Source |
ObsIDs |
|---|---|---|
|
RAD |
512, 515 |
|
RAD |
256–261 |
|
RAD |
320–324 |
|
RAD |
384–395 |
|
RAD |
448, 449 |
|
RAD |
513, 514 |
|
WFOV |
129, 130, 131 |
|
WFOV |
133, 134, 135 |
|
WFOV |
256, 257, 258 |
|
WFOV |
513, 514 |
TRIM_FAMILIES is the inverse view of that column, and get_family_specs is how a downstream step
enumerates the ObsIDs — and CAL products — it is responsible for:
from libera_utils.constants import DataProductIdentifier
from libera_utils.obsids import get_family_specs
for spec in get_family_specs(DataProductIdentifier.l1a_icie_nom_hk_swc_family_trimmed):
spec.obsid, spec.cal_product # (256, cal_swc_365nm), (257, cal_swc_405nm), ...
Two invariants hold, and both are enforced at import time:
Each ObsID keeps its own CAL product. A family groups inputs, not outputs: a family step maps every ObsID it sees to exactly one distinct CAL product. A duplicate
cal_productcell raisesValueError.A family never spans both NOM-HK ObsID fields. Trimming scans one field (
spec.source.value) at a time, so every trimmed file is attributable to one source. ObsIDs 513/514 (VIIRS lunar) run on both instruments and are registered as two families,NOM-HK-RAD-VIIRS-LUNAR-FAMILY-TRIMMEDandNOM-HK-WFOV-VIIRS-LUNAR-FAMILY-TRIMMED, each with its own CAL products.
The ObsID itself is never lost: a trimmed file still carries the ICIE__SW_OBSID_RAD /
ICIE__SW_OBSID_WFOV variable it was cut on, so a consumer recovers the exact ObsID from the data
rather than from the ProductID.
Consumer 1: the L1A trim step#
libera_utils.l1a.nom_hk_trim is the first real consumer. After a daily NOM-HK-DECODED product
is produced, calibration pipelines need a per-ObsID subset of it. find_obsid_runs scans the
Dataset for contiguous runs of each trim-eligible ObsID (using spec.source.value to pick the
right NOM-HK field), and write_trimmed_nom_hk_products writes one NetCDF per run, stamped with
that run’s family ProductID:
from libera_utils.l1a.nom_hk_trim import write_trimmed_nom_hk_products
# After parse_packets_to_l1a_dataset(...) for NOM-HK:
trimmed_paths = write_trimmed_nom_hk_products(
nom_hk_ds,
output_dir,
time_variable="PACKET_ICIE_TIME",
add_archive_path_prefix=True, # L1A preprocessor / ingest dropbox
)
TRIMMED products reuse the NOM-HK-DECODED variable schema; only ProductID (and thus the
filename product token) changes. Because the ProductID names a family, one day normally yields
several files sharing a ProductID — six NOM-HK-SWC-FAMILY-TRIMMED granules for six LED ObsIDs,
say — told apart by their filename time ranges. Each file still covers exactly one ObsID run.
Normal operations expect each calibration ObsID at most once per day — if the same
(source, obsid) appears in multiple disjoint runs, each run is written as a separate file and a
warning is logged, since that’s unexpected outside of ground testing. Different ObsIDs of one
family are not that case and do not warn.
Consumer 2: downstream cal-combine dispatch#
The registry’s second job is letting downstream algorithm repos (e.g. libera_rad) build their
own ObsID dispatch without hand-maintaining a duplicate ObsID → product mapping. A downstream
repo derives its dispatch table directly from the registry:
from libera_utils.constants import DataProductIdentifier
from libera_utils.obsids import get_family_specs
# Build once at import time: the ObsIDs this step is responsible for, and their CAL outputs
cal_event_by_obsid = {
spec.obsid: spec
for spec in get_family_specs(DataProductIdentifier.l1a_icie_nom_hk_swc_family_trimmed)
}
The radiometer cal-combine ProcessingStepIdentifier members (cal-gain-family,
cal-swc-family, cal-lwc-family, cal-solar-family) all set
shared_ecr_name=CAL_RAD_SHARED_ECR_NAME, so ProcessingStepIdentifier.ecr_name resolves every
one of them to the same cal-rad-docker-repo image, and each step’s products are the CAL
products of its family’s ObsIDs. A Batch job reads the ObsID out of the trimmed input it was
handed and runs the matching algorithm: one container image, and one deployed step per family.
The other L1A inputs a cal step needs#
NOM-HK is the only product the L1A preprocessor trims. A calibration algorithm needs more than NOM-HK — the shortwave LED cal, for instance, also needs PEV-SW, PEC-SW, RAD-SAMPLE, CAL-SAMPLE and AXIS-SAMPLE — and those arrive as the full daily L1A granules. The cal container reads the run’s time range off the TRIMMED NOM-HK filename it was handed and subsets them itself.
Which products those are is stored in libera_utils/data/trim_family_inputs.csv. This file maps
each family to its L1A inputs, and the two catalog files are cross-checked at import, so a family
cannot exist in one without the other:
from libera_utils.constants import DataProductIdentifier
from libera_utils.obsids import get_family_inputs
get_family_inputs(DataProductIdentifier.l1a_icie_nom_hk_swc_family_trimmed)
# (PEV-SW-STAT-DECODED, PEC-SW-STAT-DECODED,
# RAD-SAMPLE-DECODED, CAL-SAMPLE-DECODED, AXIS-SAMPLE-DECODED)
That tuple, plus the family’s own TRIMMED product, is what a cal-*-family node’s
input-products should list in libera_cdk’s processing_system_dag.json. A cal step is expected
to take its family’s NOM-HK already trimmed as the TRIMMED product, so required_inputs normally
omits the full-day NOM-HK-DECODED granule — listing it would stage a second, redundant NOM-HK
input. Families whose dependency set is still undecided return an empty tuple; declaring it is
part of closing TODO[LIBSDC-811].
Coverage#
Every radiometer (
RAD_CAL) and camera (CAM_CAL) entry has bothtrimmed_productandcal_productset, so all of them are trim-eligible.Science/scan mode entries (Boot-Up, Safe Mode, Stowed, Cross Track, RAP Scan, Along Track, Earth Target, the Geo scans) are catalog-only and never trim-eligible.
Radiometer cal-combine steps exist for the gain, SWC, LWC and solar families. The lunar (448/449), VIIRS-lunar (513/514) and camera families have no
ProcessingStepIdentifierstep (TODO[LIBSDC-811]); only the TRIMMED side is wired up for them.Camera cal ObsIDs are registered on
WFOVonly, and the loader enforces that. Science modes are dual-registered, since both instruments assert them, but CT video (129-131), RAPS video (133-135) and the darks (256-258) are asserted only onICIE__SW_OBSID_WFOV, soget_obsid_spec(NomHkObsidSource.RAD, 129)raisesKeyError.
Extending the registry#
When a new calibration ObsID needs to be added:
Add its
CALDataProductIdentifiermember inlibera_utils/constants.py. If it belongs to an existing calibration dependency family, reuse that family’sNOM-HK-*-FAMILY-TRIMMEDproduct; only add a new TRIMMED member when the ObsID genuinely introduces a new input dependency, in which case add aProcessingStepIdentifierfor it too.Add a row to
libera_utils/data/obsid_registry.csvnaming those two members, with a plain-languagedescription(it shows up in error messages and this documentation, so keep it accurate rather than terse). Edit the file with a text editor or thecsvmodule — never a spreadsheet app that may rewrite quoting, since descriptions contain commas. No Python change is needed for an ObsID joining an existing family.If the ObsID joins the family of an existing cal step, add its CAL product to that step’s
productslist so the step still declares everything it can emit. If instead you added a new TRIMMED family in step 1, add a matching row tolibera_utils/data/trim_family_inputs.csvnaming the L1A products that family’s algorithm consumes — import fails if the two files disagree. Leaverequired_inputsempty if the dependency set is not settled yet.Do this here, in
libera_utils, first — not as a local dict in a downstream repo. Downstream dispatch tables should be derived fromget_family_specs/iter_trim_eligible/get_obsid_spec, matching the pattern in Consumer 2 above.