# 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: ```rust 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`: ```rust fn prepare_frequency_switch( scan: &ScanData, ) -> Result { // 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: ```rust 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()`: ```rust "freqswitch" | "fs" => Ok(ObsMode::FrequencySwitch), ``` In `cal-py/src/lib.rs` -> `parse_config()`: ```rust "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 {doc}`/source/physics/observation-modes` - Update the `ObsMode` table in {doc}`/source/reference/configuration` ## Checklist ```text [ ] 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 ```