Adding an Observation Mode#

This guide walks through adding a new observation mode (e.g., FrequencySwitch) to the calibration engine.

Architecture Note#

There is no CalibrationMode trait or per-mode calibration function. All modes share a single calibration path (calibrate_full). Mode-specific logic lives exclusively in the preparation phase (cal-core/src/modes/prepare.rs), which produces a PreparedData struct consumed uniformly by the mode-agnostic calibration math.

Step 1: Add ObsMode Variant#

In cal-core/src/config.rs, add the variant to the ObsMode enum:

pub enum ObsMode {
    Auto,
    TotalPower,
    OtfTotalPower,
    OtfChopped,
    FrequencySwitch,   // <- new
    BeamSwitch,
}

Step 2: Implement Preparation#

Add a preparation function in cal-core/src/modes/prepare.rs:

fn prepare_frequency_switch(
    scan: &ScanData,
) -> Result<PreparedData, CalibrationError> {
    // 1. Identify ON subscan indices from sobsmode
    // 2. Build reference from frequency-shifted spectra
    // 3. Return PreparedData { ref_data, on_subscan_indices, ... }
    todo!()
}

The key contract: PreparedData must provide:

  • ref_data[C, R, A, S_on] – reference spectra for each ON

  • on_subscan_indices – which entries in ScanData.data are ON

  • off_index_per_on – OFF subscan index for per-subscan atmosphere

  • t_int[S_on] – integration time per ON subscan

Step 3: Wire into the Dispatcher#

In cal-core/src/modes/prepare.rs -> prepare(), add a match arm for the new mode:

pub fn prepare(scan: &ScanData) -> Result<(PreparedData, ObsMode), CalibrationError> {
    let mode = scan.detect_obs_mode();
    let prepared = match mode {
        ObsMode::OtfTotalPower => prepare_otf(scan)?,
        ObsMode::OtfChopped => prepare_otf_chopped(scan)?,
        ObsMode::TotalPower => prepare_tp(scan)?,
        ObsMode::FrequencySwitch => prepare_frequency_switch(scan)?,  // <- new
        _ => return Err(CalibrationError::InvalidMode { ... }),
    };
    Ok((prepared, mode))
}

The calibration math (calibrate_full) is mode-agnostic – it operates on the PreparedData output uniformly. No changes needed in calibrate.rs.

Step 4: Add Detection Logic#

In ScanData::detect_obs_mode() (cal-core/src/scan/scan_data.rs), add pattern matching for the new mode’s sobsmode strings or instmode attribute.

Step 5: Add CLI/Python Parsing#

In cal-cli/src/main.rs -> parse_obs_mode():

"freqswitch" | "fs" => Ok(ObsMode::FrequencySwitch),

In cal-py/src/lib.rs -> parse_config():

"frequency_switch" | "freq_switch" | "fs" => ObsMode::FrequencySwitch,

Step 6: Add Parity Test#

  1. Obtain reference output from the legacy kalibrate for a test dataset

  2. Place the reference CSV in kalibrate_test_lib/reference_data/

  3. Add a test case to the parity suite

Step 7: Update Documentation#

  • Add the mode to /source/physics/observation-modes

  • Update the ObsMode table in /source/reference/configuration

Checklist#

[ ] ObsMode variant added to config.rs
[ ] Preparation function added to modes/prepare.rs
[ ] Dispatcher match arm in prepare()
[ ] Mode detection logic in scan_data.rs
[ ] CLI parsing added (main.rs)
[ ] Python parsing added (cal-py)
[ ] Parity test with reference data
[ ] Documentation updated