hydra-common 10.1.0

Hydra foundation contracts — engine identity and reportable-output interfaces shared by all engines and applications
Documentation
//! Quantity contract: engine-declared physical quantities and their two
//! display-system renderings (spec §5).
//!
//! Values crossing an engine boundary for a quantity-bearing field are in
//! that quantity's **SI display unit**; applications convert for display
//! and convert back on input using only the descriptor. Engines never
//! format, and applications never hardcode a conversion — so a quantity
//! this layer has never heard of (a rainfall intensity, an infiltration
//! rate) costs an application nothing to support.

use serde::{Deserialize, Serialize};

/// One of the two display families applications offer (spec §5).
///
/// Not a unit *system* — that remains a non-goal (spec §1). This names
/// which of a descriptor's two renderings a consumer wants: the label,
/// conversion direction, and advisory decimals all follow from it.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DisplayFamily {
    /// SI/metric display units — also the identity family: every
    /// quantity-tagged value crossing an engine boundary is already in
    /// its quantity's SI display unit.
    Si,
    /// US-customary display units.
    Us,
}

impl QuantityDescriptor {
    /// The unit label for one display family.
    pub fn label(&self, family: DisplayFamily) -> &'static str {
        match family {
            DisplayFamily::Si => self.si_label,
            DisplayFamily::Us => self.us_label,
        }
    }

    /// Re-express an SI display value in `family`.
    pub fn from_si(&self, si: f64, family: DisplayFamily) -> f64 {
        match family {
            DisplayFamily::Si => si,
            DisplayFamily::Us => self.si_to_us(si),
        }
    }

    /// The advisory display precision for one family.
    pub fn decimals(&self, family: DisplayFamily) -> u8 {
        match family {
            DisplayFamily::Si => self.si_decimals,
            DisplayFamily::Us => self.us_decimals,
        }
    }
}

/// Descriptor of one physical quantity in an engine's catalog (spec §5).
///
/// Quantity keys are engine-scoped: two engines may both declare a `flow`
/// quantity without their descriptors agreeing, because no value ever
/// crosses between engines.
#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct QuantityDescriptor {
    /// Stable quantity identifier, opaque to this layer; referenced by
    /// attribute schemas (spec §4.4) and result variables (spec §6).
    pub key: &'static str,
    /// Unit text in the SI display system (e.g. "m", "L/s", "mm/hr").
    pub si_label: &'static str,
    /// Unit text in the US-customary display system (e.g. "ft", "gpm").
    pub us_label: &'static str,
    /// Scale of the affine SI→US display conversion:
    /// `us = si * si_to_us_scale + si_to_us_offset`.
    pub si_to_us_scale: f64,
    /// Offset of the affine SI→US display conversion. Zero for all but
    /// temperature-like quantities.
    pub si_to_us_offset: f64,
    /// Suggested display precision in the SI system. Advisory.
    pub si_decimals: u8,
    /// Suggested display precision in the US system. Advisory.
    pub us_decimals: u8,
}

impl QuantityDescriptor {
    /// Convert a value from the SI display unit to the US display unit.
    pub fn si_to_us(&self, si: f64) -> f64 {
        si * self.si_to_us_scale + self.si_to_us_offset
    }

    /// Convert a value from the US display unit back to the SI display
    /// unit — the exact inverse of [`Self::si_to_us`].
    pub fn us_to_si(&self, us: f64) -> f64 {
        (us - self.si_to_us_offset) / self.si_to_us_scale
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const TEMP: QuantityDescriptor = QuantityDescriptor {
        key: "temperature",
        si_label: "°C",
        us_label: "°F",
        si_to_us_scale: 1.8,
        si_to_us_offset: 32.0,
        si_decimals: 1,
        us_decimals: 1,
    };

    #[test]
    fn affine_conversion_round_trips() {
        assert_eq!(TEMP.si_to_us(100.0), 212.0);
        assert_eq!(TEMP.us_to_si(212.0), 100.0);
        assert_eq!(TEMP.us_to_si(TEMP.si_to_us(37.5)), 37.5);
    }
}