Skip to main content

hydra_common/
quantity.rs

1//! Quantity contract: engine-declared physical quantities and their two
2//! display-system renderings (spec §5).
3//!
4//! Values crossing an engine boundary for a quantity-bearing field are in
5//! that quantity's **SI display unit**; applications convert for display
6//! and convert back on input using only the descriptor. Engines never
7//! format, and applications never hardcode a conversion — so a quantity
8//! this layer has never heard of (a rainfall intensity, an infiltration
9//! rate) costs an application nothing to support.
10
11use serde::{Deserialize, Serialize};
12
13/// One of the two display families applications offer (spec §5).
14///
15/// Not a unit *system* — that remains a non-goal (spec §1). This names
16/// which of a descriptor's two renderings a consumer wants: the label,
17/// conversion direction, and advisory decimals all follow from it.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "lowercase")]
20pub enum DisplayFamily {
21    /// SI/metric display units — also the identity family: every
22    /// quantity-tagged value crossing an engine boundary is already in
23    /// its quantity's SI display unit.
24    Si,
25    /// US-customary display units.
26    Us,
27}
28
29impl QuantityDescriptor {
30    /// The unit label for one display family.
31    pub fn label(&self, family: DisplayFamily) -> &'static str {
32        match family {
33            DisplayFamily::Si => self.si_label,
34            DisplayFamily::Us => self.us_label,
35        }
36    }
37
38    /// Re-express an SI display value in `family`.
39    pub fn from_si(&self, si: f64, family: DisplayFamily) -> f64 {
40        match family {
41            DisplayFamily::Si => si,
42            DisplayFamily::Us => self.si_to_us(si),
43        }
44    }
45
46    /// The advisory display precision for one family.
47    pub fn decimals(&self, family: DisplayFamily) -> u8 {
48        match family {
49            DisplayFamily::Si => self.si_decimals,
50            DisplayFamily::Us => self.us_decimals,
51        }
52    }
53}
54
55/// Descriptor of one physical quantity in an engine's catalog (spec §5).
56///
57/// Quantity keys are engine-scoped: two engines may both declare a `flow`
58/// quantity without their descriptors agreeing, because no value ever
59/// crosses between engines.
60#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
61#[serde(rename_all = "camelCase")]
62pub struct QuantityDescriptor {
63    /// Stable quantity identifier, opaque to this layer; referenced by
64    /// attribute schemas (spec §4.4) and result variables (spec §6).
65    pub key: &'static str,
66    /// Unit text in the SI display system (e.g. "m", "L/s", "mm/hr").
67    pub si_label: &'static str,
68    /// Unit text in the US-customary display system (e.g. "ft", "gpm").
69    pub us_label: &'static str,
70    /// Scale of the affine SI→US display conversion:
71    /// `us = si * si_to_us_scale + si_to_us_offset`.
72    pub si_to_us_scale: f64,
73    /// Offset of the affine SI→US display conversion. Zero for all but
74    /// temperature-like quantities.
75    pub si_to_us_offset: f64,
76    /// Suggested display precision in the SI system. Advisory.
77    pub si_decimals: u8,
78    /// Suggested display precision in the US system. Advisory.
79    pub us_decimals: u8,
80}
81
82impl QuantityDescriptor {
83    /// Convert a value from the SI display unit to the US display unit.
84    pub fn si_to_us(&self, si: f64) -> f64 {
85        si * self.si_to_us_scale + self.si_to_us_offset
86    }
87
88    /// Convert a value from the US display unit back to the SI display
89    /// unit — the exact inverse of [`Self::si_to_us`].
90    pub fn us_to_si(&self, us: f64) -> f64 {
91        (us - self.si_to_us_offset) / self.si_to_us_scale
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    const TEMP: QuantityDescriptor = QuantityDescriptor {
100        key: "temperature",
101        si_label: "°C",
102        us_label: "°F",
103        si_to_us_scale: 1.8,
104        si_to_us_offset: 32.0,
105        si_decimals: 1,
106        us_decimals: 1,
107    };
108
109    #[test]
110    fn affine_conversion_round_trips() {
111        assert_eq!(TEMP.si_to_us(100.0), 212.0);
112        assert_eq!(TEMP.us_to_si(212.0), 100.0);
113        assert_eq!(TEMP.us_to_si(TEMP.si_to_us(37.5)), 37.5);
114    }
115}