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}