ADR-0002: Boolean mapper flags (sparse_data / sampled / continuous_*domain) as the transform interface contract

ADR-0002: Boolean mapper flags (sparse_data / sampled / continuous_*domain) as the transform interface contract#

Field

Value

Kind

ADR

Status

Accepted

Decided

historical

Deciders

Antoine Berchet

Supersedes

Superseded by

Context#

Transforms in CIF’s pipeline exchange data through a shared mapper dict, one entry per (component, parameter). Data at each end of a transform is held in one of two container types: a gridded xarray.Dataset (dims time, lev, lat, lon) for full-field data, or a pandas.DataFrame (one row per point) for sparse data such as observations. A precursor and its successor transform don’t necessarily agree on which container is expected on each side, and some transforms (e.g. Lagrangian footprint models) already interpolate to observation locations internally, while others (Eulerian models) need pyCIF to insert that interpolation for them. As the number of transforms and plugins grew, this had to be resolved automatically for the whole pipeline rather than negotiated ad hoc inside each plugin.

See also: the full mapper-field reference in Transforms transform (docstring of pycif.plugins.transforms), and the auto-wiring implementation in pycif/plugins/obsoperators/standard/transforms/utils/init_sparse.py.

Decision#

We represent this state as a handful of independent boolean flags on each mapper entry — sparse_data, sampled, continuous_hdomain, continuous_vdomain (plus domain itself) — not as a single geometry_kind enumeration. During pipeline construction, init_sparse.py compares a precursor’s flags against its successor’s and automatically inserts the matching conversion transform (sparse2sample, array2sampled, regrid, vertical_interpolation, time_interpolation) whenever they disagree.

Consequences#

  • Plugin/transform authors declare a handful of independent flags per mapper entry; the pipeline — not the plugin — is responsible for inserting the right conversion between a precursor and its successor.

  • Because the flags are independent booleans rather than a closed enum, invalid combinations aren’t ruled out by construction — only by the explicit checks each transform’s init step happens to encode (e.g. init_sparse.py raises CifError("Sparse to sample should not happen") if sampled is set on a target that expects sparse input).

  • Adding a genuinely new data representation means threading a new boolean through every place sparse_data / sampled / continuous_*domain are checked (init_sparse.py, aggreg_deaggreg_inout.py, fetch_inoutputs.py, and others), rather than adding one enum member — this is real, ongoing maintenance cost, not a hypothetical one.