Skip to main content

sim_lib_interference_core/
quantity.rs

1//! Finite scalar wrappers with explicit physical admission rules.
2
3use std::f64::consts::{PI, TAU};
4
5use crate::InterferenceError;
6
7fn invalid(name: &'static str, value: f64) -> InterferenceError {
8    InterferenceError::InvalidQuantity { name, value }
9}
10
11fn finite(name: &'static str, value: f64) -> Result<f64, InterferenceError> {
12    value
13        .is_finite()
14        .then_some(canonical_zero(value))
15        .ok_or_else(|| invalid(name, value))
16}
17
18fn positive(name: &'static str, value: f64) -> Result<f64, InterferenceError> {
19    (value.is_finite() && value > 0.0)
20        .then_some(value)
21        .ok_or_else(|| invalid(name, value))
22}
23
24fn non_negative(name: &'static str, value: f64) -> Result<f64, InterferenceError> {
25    (value.is_finite() && value >= 0.0)
26        .then_some(canonical_zero(value))
27        .ok_or_else(|| invalid(name, value))
28}
29
30fn canonical_zero(value: f64) -> f64 {
31    if value == 0.0 { 0.0 } else { value }
32}
33
34/// A finite signed distance or coordinate in metres.
35#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
36pub struct Metres(f64);
37
38impl Metres {
39    /// Stable diagnostic name.
40    pub const NAME: &'static str = "distance-m";
41
42    /// Admits any finite signed coordinate, including zero.
43    pub fn new(value: f64) -> Result<Self, InterferenceError> {
44        finite(Self::NAME, value).map(Self)
45    }
46
47    /// Returns the validated value in metres.
48    pub fn get(self) -> f64 {
49        self.0
50    }
51}
52
53/// A finite distance in metres that is strictly greater than zero.
54#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
55pub struct PositiveMetres(f64);
56
57impl PositiveMetres {
58    /// Stable diagnostic name.
59    pub const NAME: &'static str = "positive-distance-m";
60
61    /// Admits a finite, strictly positive distance.
62    pub fn new(value: f64) -> Result<Self, InterferenceError> {
63        positive(Self::NAME, value).map(Self)
64    }
65
66    /// Returns the validated value in metres.
67    pub fn get(self) -> f64 {
68        self.0
69    }
70}
71
72/// A finite frequency in hertz that is strictly greater than zero.
73#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
74pub struct Hertz(f64);
75
76impl Hertz {
77    /// Stable diagnostic name.
78    pub const NAME: &'static str = "frequency-hz";
79
80    /// Admits a finite, strictly positive frequency.
81    pub fn new(value: f64) -> Result<Self, InterferenceError> {
82        positive(Self::NAME, value).map(Self)
83    }
84
85    /// Returns the validated value in hertz.
86    pub fn get(self) -> f64 {
87        self.0
88    }
89}
90
91/// A finite propagation speed in metres per second.
92#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
93pub struct MetresPerSecond(f64);
94
95impl MetresPerSecond {
96    /// Stable diagnostic name.
97    pub const NAME: &'static str = "speed-m-s";
98
99    /// Admits a finite, strictly positive propagation speed.
100    pub fn new(value: f64) -> Result<Self, InterferenceError> {
101        positive(Self::NAME, value).map(Self)
102    }
103
104    /// Returns the validated value in metres per second.
105    pub fn get(self) -> f64 {
106        self.0
107    }
108}
109
110/// A finite non-negative attenuation coefficient in nepers per metre.
111#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
112pub struct NepersPerMetre(f64);
113
114impl NepersPerMetre {
115    /// Stable diagnostic name.
116    pub const NAME: &'static str = "attenuation-np-m";
117
118    /// Admits finite attenuation greater than or equal to zero.
119    pub fn new(value: f64) -> Result<Self, InterferenceError> {
120        non_negative(Self::NAME, value).map(Self)
121    }
122
123    /// Returns the validated value in nepers per metre.
124    pub fn get(self) -> f64 {
125        self.0
126    }
127}
128
129/// A finite phase angle stored in the half-open interval `[-pi, pi)`.
130#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
131pub struct Radians(f64);
132
133impl Radians {
134    /// Stable diagnostic name.
135    pub const NAME: &'static str = "phase-rad";
136
137    /// Admits a finite angle and normalizes whole turns without rounding.
138    pub fn new(value: f64) -> Result<Self, InterferenceError> {
139        let value = finite(Self::NAME, value)?;
140        let positive_turn = value.rem_euclid(TAU);
141        let normalized = if positive_turn >= PI {
142            positive_turn - TAU
143        } else {
144            positive_turn
145        };
146        Ok(Self(canonical_zero(normalized)))
147    }
148
149    /// Returns the normalized value in radians.
150    pub fn get(self) -> f64 {
151        self.0
152    }
153}
154
155/// A finite non-negative scalar field amplitude.
156///
157/// A negative real contribution is represented by phase, not by a signed
158/// amplitude, so zero is admitted while negative values are rejected.
159#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
160pub struct FieldAmplitude(f64);
161
162impl FieldAmplitude {
163    /// Stable diagnostic name.
164    pub const NAME: &'static str = "field-amplitude";
165
166    /// Admits a finite amplitude greater than or equal to zero.
167    pub fn new(value: f64) -> Result<Self, InterferenceError> {
168        non_negative(Self::NAME, value).map(Self)
169    }
170
171    /// Returns the validated field amplitude.
172    pub fn get(self) -> f64 {
173        self.0
174    }
175}