Telescope Profiles#
Telescope profiles define how FITS keywords are mapped to canonical field names, what unit conversions to apply, and observatory metadata.
Profile Format#
Profiles are YAML files in the profiles/ directory:
name: ccat
version: 2
match_rules:
- TELESCOP: "CCAT"
- TELESCOP: "FYST"
keyword_map:
mjd: MJD-OBS
elevation: ELEVATIO
azimuth: AZIMUTH
signal_freq: OBSFREQ
sobsmode: SOBSMODE
otf_lon_base: LAMOFF
otf_lon_pixel: PIXOFFX
otf_lat_base: BETOFF
otf_lat_pixel: PIXOFFY
unit_conversions:
signal_freq: MHz_to_Hz
elevation: deg_to_rad
otf_lon: arcsec_to_deg
inject:
observatory: CCAT
site_latitude_deg: -22.9586
site_longitude_deg: -67.7875
site_altitude_m: 5612.0
defaults:
freq_off: 0.0
tcold: 77.0
atm_table:
freq_min_ghz: 200.0
freq_max_ghz: 1200.0
freq_step_mhz: 1.0
pwv_low_mm: 0.05
pwv_high_mm: 0.50
Scan Metadata Passthrough#
The optional scan_metadata section maps snake_case attribute names to FITS
keywords that are carried verbatim into L0 scan-group attributes and then
propagated unchanged into L1 scan-group attributes by the calibration engine:
scan_metadata:
mission_id: MISSN-ID
obs_id: OBS_ID
plan_id: PLANID
aor_id: AOR_ID
aot_id: AOT_ID
Unlike keyword_map (which feeds the calibration math via canonical fields),
scan_metadata is pure identity / QA passthrough — the values never affect
calibration. Values are read as strings (numeric headers are stringified) and
keywords absent from a header are simply omitted. This lets downstream tools
(e.g. SOFIA flight / ground-day grouping) read identifiers directly off L0/L1
instead of reparsing raw FITS, and lets new keywords be added with a YAML edit
only — no Rust change. The sofia_upgreat profile folds in the kalibrate
header set (user_sofia / housekeeping tags) plus operationally useful
QA/atmosphere keywords (WVZ_STA, ZA_START, TAMBSOFI, …).
In addition, the L0 reader records a representative scan mjd (the first finite
subscan MJD) and propagates it to L1 alongside telescope and date_obs.
Per-Pixel Focal-Plane Offsets#
Multi-pixel arrays (e.g. SOFIA upGREAT) place each receiver at a different
focal-plane position. The converter stores these as
pixel_offset_lon[R, A, S] / pixel_offset_lat[R, A, S] (degrees) so each
pixel gets its correct sky footprint. How they are read from a header is
selected by the optional pixel_offset field:
pixel_offset: kosma_versioned # default: direct
Strategy |
Behaviour |
|---|---|
|
Read |
|
KOSMA/SOFIA version-gated reconstruction. Needed for pre-FITSVERS-2.12 data, which has no PIXOFFX/PIXOFFY. |
The kosma_versioned strategy mirrors the legacy reader (kalibrate
buffers.cpp) and is gated on FITSVERS (parsed as integer major.minor, so
2.10 is version 2.10, not decimal 2.1):
FITSVERS > 2.11→ use PIXOFFX/PIXOFFY directly (same asdirect).2.8 < FITSVERS ≤ 2.11→ rotate the active pixel’s offset relative to the reference pixel,RXDX − REFRXDX/RXDY − REFRXDY, into the map frame byPOSANGLE + ANGLEDIF, applyingSIGNRXDX/SIGNRXDYafter rotation.FITSVERS ≤ 2.8→ as above, but the sign is applied before rotation and theCOSYDELframe conventions flip the longitude/latitude sign.
This requires the legacy keywords to be present in keyword_map:
keyword_map:
fits_version: FITSVERS
rxdx: RXDX
rxdy: RXDY
refrxdx: REFRXDX
refrxdy: REFRXDY
signrxdx: SIGNRXDX
signrxdy: SIGNRXDY
posangle: POSANGLE
anglediff: ANGLEDIF
cosydel: COSYDEL
pixel_offset: kosma_versioned
A new metadata standard that encodes per-pixel offsets differently adds a new
PixelOffsetStrategy variant in coords.rs — the keyword names stay in the
profile, so no other wiring changes. The writer logs a warning if a multi-pixel
scan ends up with every per-pixel offset exactly zero (a footprint collapse).
Note
Per-subscan rotation (issue #48). The KOSMA offset rotates with
POSANGLE + ANGLEDIF, which changes per subscan as the array turns on sky, so
the offset is stored per-subscan as pixel_offset[R, A, S] (L0 schema ≥ 2,
L1 schema ≥ 1.3). The direct (PIXOFFX) path is the same: PIXOFFX is itself the
pre-rotated offset, written once per subscan. The earlier [R, A] layout kept
only the last subscan and applied its rotation to every dump — on real M51 OTF
scan 013690 (POSANGLE sweeps 30°) that mis-placed ring pixels by up to ~17″,
larger than the 14″ beam. Readers (cal-io, yoda) stay backward-compatible
with old [R, A] stores by broadcasting them as constant across subscans.
Profile Auto-Detection#
When no --profile override is given, profiles are tested against
the first FITS file’s header in alphabetical order. The first profile
whose match_rules all match is selected. If none match,
generic.yaml is used as fallback.
Embedded Profiles#
Profiles are embedded at compile time via include_str! in
zarr-fits-core/src/embedded_profiles.rs. This means the binary
works without an external profiles/ directory.
Current profiles:
ccat.yaml— CCAT/FYST telescopesofia_upgreat.yaml— SOFIA/upGREAT airborne observatorygeneric.yaml— fallback for unknown telescopes
Adding a New Telescope#
Create
profiles/<telescope_name>.yamlDefine
match_rulesfor auto-detection from FITS headersMap all required keywords in
keyword_mapSet
injectfields for observatory metadataAdd the file to
embedded_profiles.rs:pub const NEW_TELESCOPE_YAML: &str = include_str!("../../../profiles/new_telescope.yaml"); pub const ALL: &[(&str, &str)] = &[ ("sofia_upgreat.yaml", SOFIA_UPGREAT_YAML), ("ccat.yaml", CCAT_YAML), ("new_telescope.yaml", NEW_TELESCOPE_YAML), // ← add ("generic.yaml", GENERIC_YAML), ];
Rebuild and test with sample FITS data
Implementation: zarr-fits-core/src/profile.rs