Skip to main content

native/sidereon_nif/src/carrier_phase.rs

//! Rustler boundary for carrier-phase combinations and arc processing.
//!
//! Pure glue over `sidereon_core::carrier_phase`: decode already-normalized
//! arc tuples, forward thresholds/window caps to the crate, and encode the
//! unchanged Sidereon public result shapes.

use rustler::{Encoder, Env, NifResult, Term};
use sidereon_core::carrier_phase::{
    self, ArcEpoch, CarrierPhaseError, CycleSlipOptions, SlipReason,
};

#[derive(Debug, Clone, rustler::NifMap)]
struct ArcEpochTerm {
    phi1: Option<f64>,
    phi2: Option<f64>,
    p1: Option<f64>,
    p2: Option<f64>,
    lli1: Option<i64>,
    lli2: Option<i64>,
    f1: Option<f64>,
    f2: Option<f64>,
    gap_time_s: Option<f64>,
}

mod atoms {
    rustler::atoms! {
        ok,
        error,
        equal_frequencies,
        invalid_frequency,
        invalid_observation,
        invalid_threshold,
        lli,
        geometry_free,
        melbourne_wubbena,
        data_gap
    }
}

#[rustler::nif]
fn carrier_phase_phase_meters<'a>(env: Env<'a>, phi_cycles: f64, f_hz: f64) -> Term<'a> {
    encode_float_result(env, carrier_phase::phase_meters(phi_cycles, f_hz))
}

#[rustler::nif]
fn carrier_phase_geometry_free(l1_m: f64, l2_m: f64) -> NifResult<f64> {
    carrier_phase::geometry_free(l1_m, l2_m).map_err(crate::errors::invalid_input)
}

#[rustler::nif]
fn carrier_phase_wide_lane_wavelength<'a>(env: Env<'a>, f1_hz: f64, f2_hz: f64) -> Term<'a> {
    encode_float_result(env, carrier_phase::wide_lane_wavelength(f1_hz, f2_hz))
}

#[rustler::nif]
fn carrier_phase_narrow_lane_code<'a>(
    env: Env<'a>,
    p1_m: f64,
    p2_m: f64,
    f1_hz: f64,
    f2_hz: f64,
) -> Term<'a> {
    encode_float_result(
        env,
        carrier_phase::narrow_lane_code(p1_m, p2_m, f1_hz, f2_hz),
    )
}

#[rustler::nif]
#[allow(clippy::too_many_arguments)]
fn carrier_phase_melbourne_wubbena<'a>(
    env: Env<'a>,
    phi1_cycles: f64,
    phi2_cycles: f64,
    p1_m: f64,
    p2_m: f64,
    f1_hz: f64,
    f2_hz: f64,
) -> Term<'a> {
    encode_float_result(
        env,
        carrier_phase::melbourne_wubbena(phi1_cycles, phi2_cycles, p1_m, p2_m, f1_hz, f2_hz),
    )
}

#[rustler::nif]
#[allow(clippy::too_many_arguments)]
fn carrier_phase_wide_lane_cycles<'a>(
    env: Env<'a>,
    phi1_cycles: f64,
    phi2_cycles: f64,
    p1_m: f64,
    p2_m: f64,
    f1_hz: f64,
    f2_hz: f64,
) -> Term<'a> {
    encode_float_result(
        env,
        carrier_phase::wide_lane_cycles(phi1_cycles, phi2_cycles, p1_m, p2_m, f1_hz, f2_hz),
    )
}

#[rustler::nif]
fn carrier_phase_code_minus_carrier<'a>(
    env: Env<'a>,
    p_m: f64,
    phi_cycles: f64,
    f_hz: f64,
) -> Term<'a> {
    encode_float_result(
        env,
        carrier_phase::code_minus_carrier(p_m, phi_cycles, f_hz),
    )
}

#[rustler::nif(schedule = "DirtyCpu")]
fn carrier_phase_detect_cycle_slips<'a>(
    env: Env<'a>,
    arc: Vec<ArcEpochTerm>,
    gf_threshold_m: f64,
    mw_threshold_cycles: f64,
    min_arc_gap_s: f64,
) -> Term<'a> {
    let results = match carrier_phase::detect_cycle_slips(
        &decode_arc(arc),
        CycleSlipOptions {
            gf_threshold_m,
            mw_threshold_cycles,
            min_arc_gap_s,
        },
    ) {
        Ok(results) => results,
        Err(error) => return (atoms::error(), error_atom(error)).encode(env),
    };

    results
        .into_iter()
        .map(|result| {
            let reasons = result
                .reasons
                .into_iter()
                .map(reason_atom)
                .collect::<Vec<_>>();
            (
                result.slip,
                reasons,
                result.gf_m,
                result.mw_m,
                result.skipped,
            )
                .encode(env)
        })
        .collect::<Vec<Term<'a>>>()
        .encode(env)
}

#[rustler::nif(schedule = "DirtyCpu")]
fn carrier_phase_smooth_code<'a>(
    env: Env<'a>,
    arc: Vec<ArcEpochTerm>,
    gf_threshold_m: f64,
    mw_threshold_cycles: f64,
    min_arc_gap_s: f64,
    hatch_window_cap: u64,
) -> Term<'a> {
    let results = match carrier_phase::smooth_code(
        &decode_arc(arc),
        CycleSlipOptions {
            gf_threshold_m,
            mw_threshold_cycles,
            min_arc_gap_s,
        },
        hatch_window_cap as usize,
    ) {
        Ok(results) => results,
        Err(error) => return (atoms::error(), error_atom(error)).encode(env),
    };

    results
        .into_iter()
        .map(|result| (result.p_smooth_m, result.window as u64, result.reset).encode(env))
        .collect::<Vec<Term<'a>>>()
        .encode(env)
}

#[rustler::nif(schedule = "DirtyCpu")]
fn carrier_phase_smooth_iono_free_code<'a>(
    env: Env<'a>,
    arc: Vec<ArcEpochTerm>,
    gf_threshold_m: f64,
    mw_threshold_cycles: f64,
    min_arc_gap_s: f64,
    hatch_window_cap: u64,
) -> Term<'a> {
    let results = match carrier_phase::smooth_iono_free_code(
        &decode_arc(arc),
        CycleSlipOptions {
            gf_threshold_m,
            mw_threshold_cycles,
            min_arc_gap_s,
        },
        hatch_window_cap as usize,
    ) {
        Ok(results) => results,
        Err(error) => return (atoms::error(), error_atom(error)).encode(env),
    };

    results
        .into_iter()
        .map(|result| {
            (
                result.p_smooth_m,
                result.p_if_m,
                result.l_if_m,
                result.window as u64,
                result.reset,
            )
                .encode(env)
        })
        .collect::<Vec<Term<'a>>>()
        .encode(env)
}

fn decode_arc(arc: Vec<ArcEpochTerm>) -> Vec<ArcEpoch> {
    arc.into_iter()
        .map(|epoch| ArcEpoch {
            phi1_cycles: epoch.phi1,
            phi2_cycles: epoch.phi2,
            p1_m: epoch.p1,
            p2_m: epoch.p2,
            lli1: epoch.lli1,
            lli2: epoch.lli2,
            f1_hz: epoch.f1,
            f2_hz: epoch.f2,
            gap_time_s: epoch.gap_time_s,
        })
        .collect()
}

fn encode_float_result<'a>(env: Env<'a>, result: Result<f64, CarrierPhaseError>) -> Term<'a> {
    match result {
        Ok(value) => (atoms::ok(), value).encode(env),
        Err(error) => (atoms::error(), error_atom(error)).encode(env),
    }
}

fn error_atom(error: CarrierPhaseError) -> rustler::Atom {
    match error {
        CarrierPhaseError::EqualFrequencies => atoms::equal_frequencies(),
        CarrierPhaseError::InvalidFrequency => atoms::invalid_frequency(),
        CarrierPhaseError::InvalidObservation => atoms::invalid_observation(),
        CarrierPhaseError::InvalidThreshold => atoms::invalid_threshold(),
    }
}

fn reason_atom(reason: SlipReason) -> rustler::Atom {
    match reason {
        SlipReason::Lli => atoms::lli(),
        SlipReason::GeometryFree => atoms::geometry_free(),
        SlipReason::MelbourneWubbena => atoms::melbourne_wubbena(),
        SlipReason::DataGap => atoms::data_gap(),
    }
}