fastsim_core/utils/
mod.rs

1use crate::imports::*;
2use paste::paste;
3use regex::Regex;
4
5pub mod interp;
6pub use interp::*;
7pub mod tracked_state;
8pub use tracked_state::*;
9
10/// Error message for when user attempts to set value in a nested struct.
11pub const DIRECT_SET_ERR: &str = "Setting field value directly not allowed";
12
13/// returns true for use with serde default
14pub fn return_true() -> bool {
15    true
16}
17
18/// Function for sorting a slice that implements `std::cmp::PartialOrd`.
19/// Remove this once is_sorted is stabilized in std
20pub fn is_sorted<T: std::cmp::PartialOrd>(data: &[T]) -> bool {
21    data.windows(2).all(|w| w[0] <= w[1])
22}
23
24/// Download a file to a specified filepath, assuming all necessary parent directories exist.
25///
26/// If supplied filepath has no file extension,
27/// this function will attempt to parse a filename from the last segment of the URL.
28#[cfg(feature = "web")]
29#[allow(dead_code)]
30pub(crate) fn download_file<S: AsRef<str>, P: AsRef<Path>>(
31    url: S,
32    filepath: P,
33) -> anyhow::Result<()> {
34    let url = url::Url::parse(url.as_ref())?;
35    let filepath = filepath.as_ref();
36    let filepath = if filepath.extension().is_none() {
37        // No extension in filepath, parse from URL
38        let filename = url
39            .path_segments()
40            .and_then(|segments| segments.last())
41            .with_context(|| "Could not parse filename from last URL segment: {url:?}")?;
42        filepath.join(filename)
43    } else {
44        filepath.to_path_buf()
45    };
46    let mut rdr = ureq::get(url.as_ref()).call()?.into_reader();
47    let mut wtr = File::create(filepath)?;
48    std::io::copy(&mut rdr, &mut wtr)?;
49    Ok(())
50}
51
52#[allow(unused)]
53/// Helper function to find where a query falls on an axis of discrete values;
54/// NOTE: this assumes the axis array is sorted with values ascending and that there are no repeating values!
55fn find_interp_indices(query: &f64, axis: &[f64]) -> anyhow::Result<(usize, usize)> {
56    let axis_size = axis.len();
57    match axis
58        .windows(2)
59        .position(|w| query >= &w[0] && query < &w[1])
60    {
61        Some(p) => {
62            if query == &axis[p] {
63                Ok((p, p))
64            } else if query == &axis[p + 1] {
65                Ok((p + 1, p + 1))
66            } else {
67                Ok((p, p + 1))
68            }
69        }
70        None => {
71            if query <= &axis[0] {
72                Ok((0, 0))
73            } else if query >= &axis[axis_size - 1] {
74                Ok((axis_size - 1, axis_size - 1))
75            } else {
76                bail!("Unable to find where the query fits in the values, check grid.")
77            }
78        }
79    }
80}
81
82#[allow(unused)]
83/// Helper function to compute the difference between a value and a set of bounds
84fn compute_interp_diff(value: &f64, lower: &f64, upper: &f64) -> f64 {
85    if lower == upper {
86        0.0
87    } else {
88        (value - lower) / (upper - lower)
89    }
90}
91
92impl<T> SerdeAPI for Extrapolate<T> where T: Serialize + for<'de> Deserialize<'de> {}
93impl<T> Init for Extrapolate<T> {}
94
95/// Returns absolute value of `x_val`
96pub fn abs_checked_x_val(x_val: f64, x_data: &[f64]) -> anyhow::Result<f64> {
97    if *x_data
98        .first()
99        .with_context(|| anyhow!("{}\nExpected `first` to return `Some`.", format_dbg!()))?
100        == 0.
101    {
102        Ok(x_val.abs())
103    } else {
104        Ok(x_val)
105    }
106}
107
108// public to enable exposure in docs
109pub const COMP_EPSILON: f64 = 1e-8;
110
111/// Returns true if `val1` and `val2` are within a relative/absolute `epsilon` of each other,
112/// depending on magnitude.
113pub fn almost_eq(val1: f64, val2: f64, epsilon: Option<f64>) -> bool {
114    let epsilon = epsilon.unwrap_or(COMP_EPSILON);
115    ((val2 - val1) / (val1 + val2)).abs() < epsilon || (val2 - val1).abs() < epsilon
116}
117
118pub fn almost_gt(val1: f64, val2: f64, epsilon: Option<f64>) -> bool {
119    let epsilon = epsilon.unwrap_or(COMP_EPSILON);
120    val1 > val2 * (1.0 + epsilon)
121}
122
123pub fn almost_lt(val1: f64, val2: f64, epsilon: Option<f64>) -> bool {
124    let epsilon = epsilon.unwrap_or(COMP_EPSILON);
125    val1 < val2 * (1.0 - epsilon)
126}
127
128/// Returns true if `val1` is greater than or equal to `val2` with some error margin, `epsilon`
129pub fn almost_ge(val1: f64, val2: f64, epsilon: Option<f64>) -> bool {
130    let epsilon = epsilon.unwrap_or(COMP_EPSILON);
131    val1 > val2 * (1.0 - epsilon) || val1 > val2 - epsilon
132}
133
134/// Returns true if `val1` is less than or equal to `val2` with some error margin, `epsilon`
135pub fn almost_le(val1: f64, val2: f64, epsilon: Option<f64>) -> bool {
136    let epsilon = epsilon.unwrap_or(COMP_EPSILON);
137    val1 < val2 * (1.0 + epsilon) || val1 < val2 + epsilon
138}
139
140lazy_static! {
141    static ref TIRE_CODE_REGEX: Regex = Regex::new(
142        r"(?i)[P|LT|ST|T]?((?:[0-9]{2,3}\.)?[0-9]+)/((?:[0-9]{1,2}\.)?[0-9]+) ?[B|D|R]?[x|\-| ]?((?:[0-9]{1,2}\.)?[0-9]+)[A|B|C|D|E|F|G|H|J|L|M|N]?"
143    ).expect("Failed compile tire code regex");
144}
145
146/// Calculate tire radius (in meters) from an [ISO metric tire code](https://en.wikipedia.org/wiki/Tire_code#ISO_metric_tire_codes)
147///
148/// # Arguments
149/// * `tire_code` - A string containing a parsable ISO metric tire code
150///
151/// # Examples
152/// ## Example 1:
153///
154/// ```rust
155/// // Note the floating point imprecision in the result
156/// use fastsim_core::utils::tire_code_to_radius;
157/// let tire_code = "225/70Rx19.5G";
158/// assert_eq!(tire_code_to_radius(&tire_code).unwrap(), 0.40514999999999995);
159/// ```
160///
161/// ## Example 2:
162///
163/// ```rust
164/// // Either `&str`, `&String`, or `String` can be passed
165/// use fastsim_core::utils::tire_code_to_radius;
166/// let tire_code = String::from("P205/60R16");
167/// assert_eq!(tire_code_to_radius(tire_code).unwrap(), 0.3262);
168/// ```
169///
170pub fn tire_code_to_radius<S: AsRef<str>>(tire_code: S) -> anyhow::Result<f64> {
171    let tire_code = tire_code.as_ref();
172    let captures = TIRE_CODE_REGEX.captures(tire_code).with_context(|| {
173        format!(
174            "Regex pattern does not match for {:?}: {:?}",
175            tire_code,
176            TIRE_CODE_REGEX.as_str(),
177        )
178    })?;
179    let width_mm: f64 = captures[1].parse()?;
180    let aspect_ratio: f64 = captures[2].parse()?;
181    let rim_diameter_in: f64 = captures[3].parse()?;
182
183    let sidewall_height_mm = width_mm * aspect_ratio / 100.0;
184    let radius_mm = (rim_diameter_in * 25.4) / 2.0 + sidewall_height_mm;
185
186    Ok(radius_mm / 1000.0)
187}
188
189make_uom_cmp_fn!(almost_eq);
190make_uom_cmp_fn!(almost_gt);
191make_uom_cmp_fn!(almost_lt);
192make_uom_cmp_fn!(almost_ge);
193make_uom_cmp_fn!(almost_le);
194
195#[derive(IsVariant, derive_more::From, TryInto)]
196pub(crate) enum InterpRange {
197    ZeroThroughOne,
198    NegativeOneThroughOne,
199    Either,
200}
201
202/// Ensures that passed data is between 0 and 1 and monotonically increasing.  
203/// # Arguments:
204/// - `data`: data used for interpolating efficiency from fraction of peak power
205/// - `interp_range`: allowed range
206pub(crate) fn check_interp_frac_data(
207    data: &[f64],
208    interp_range: InterpRange,
209) -> anyhow::Result<InterpRange> {
210    check_monotonicity(data).with_context(|| anyhow!(format_dbg!()))?;
211    let min = data.first().with_context(|| {
212        anyhow!(
213            "{}\nProblem extracting first element of `data`",
214            format_dbg!()
215        )
216    })?;
217    let max = data.last().with_context(|| {
218        anyhow!(
219            "{}\nProblem extracting first element of `data`",
220            format_dbg!()
221        )
222    })?;
223    match interp_range {
224        InterpRange::ZeroThroughOne => {
225            ensure!(
226                *min == 0. && *max == 1.,
227                "data min ({}) and max ({}) must be zero and one, respectively.",
228                min,
229                max
230            );
231        }
232        InterpRange::NegativeOneThroughOne => {
233            ensure!(
234                *min == -1. && *max == 1.,
235                "data min ({}) and max ({}) must be zero and one, respectively.",
236                min,
237                max
238            );
239        }
240        InterpRange::Either => {
241            ensure!(
242                (*min == -1. || *min == 0.) && *max == 1.,
243                "data min ({}) and max ({}) must be zero or negative one and one, respectively.",
244                min,
245                max
246            );
247        }
248    }
249    if *min == 0. && *max == 1. {
250        Ok(InterpRange::ZeroThroughOne)
251    } else {
252        Ok(InterpRange::NegativeOneThroughOne)
253    }
254}
255
256/// Verifies that passed `data` is monotonically increasing.
257pub fn check_monotonicity(data: &[f64]) -> anyhow::Result<()> {
258    ensure!(
259        data.windows(2).all(|w| w[0] < w[1]),
260        format_dbg!("{}\n`data` must be monotonically increasing")
261    );
262    Ok(())
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268    #[test]
269    fn test_linspace() {
270        assert_eq!(Vec::linspace(0.0, 1.0, 3), vec![0.0, 0.5, 1.0]);
271    }
272
273    #[test]
274    fn test_almost_gt_zero() {
275        assert!(almost_gt(1e-9, 0.0, None));
276        assert!(!almost_gt(0.0, 1e-9, None));
277        assert!(almost_gt(1e-7, 0.0, None));
278        assert!(!almost_gt(0.0, 1e-7, None));
279    }
280
281    #[test]
282    fn test_almost_ge_zero() {
283        assert!(almost_ge(1e-9, 0.0, None));
284        assert!(almost_ge(0.0, 1e-9, None));
285        assert!(almost_ge(1e-7, 0.0, None));
286        assert!(!almost_ge(0.0, 1e-7, None));
287    }
288
289    #[test]
290    fn test_almost_eq_zero() {
291        assert!(almost_eq(0.0, 1e-9, None));
292        assert!(almost_eq(1e-9, 0.0, None));
293        assert!(!almost_eq(0.0, 1e-7, None));
294        assert!(!almost_eq(1e-7, 0.0, None));
295    }
296
297    #[test]
298    fn test_almost_le_zero() {
299        assert!(almost_le(1e-9, 0.0, None));
300        assert!(almost_le(0.0, 1e-9, None));
301        assert!(!almost_le(1e-7, 0.0, None));
302        assert!(almost_le(0.0, 1e-7, None));
303    }
304
305    #[test]
306    fn test_almost_lt_zero() {
307        assert!(!almost_lt(1e-9, 0.0, None));
308        assert!(almost_lt(0.0, 1e-9, None));
309        assert!(!almost_lt(1e-7, 0.0, None));
310        assert!(almost_lt(0.0, 1e-7, None));
311    }
312
313    #[test]
314    fn test_almost_gt_large() {
315        assert!(!almost_gt(1e9 * (1.0 + 1e-9), 1e9, None));
316        assert!(!almost_gt(1e9, 1e9 * (1.0 + 1e-9), None));
317        assert!(almost_gt(1e9 * (1.0 + 1e-7), 1e9, None));
318        assert!(!almost_gt(1e9, 1e9 * (1.0 + 1e-7), None));
319    }
320
321    #[test]
322    fn test_almost_ge_large() {
323        assert!(almost_ge(1e9 * (1.0 + 1e-9), 1e9, None));
324        assert!(almost_ge(1e9, 1e9 * (1.0 + 1e-9), None));
325        assert!(almost_ge(1e9 * (1.0 + 1e-7), 1e9, None));
326        assert!(!almost_ge(1e9, 1e9 * (1.0 + 1e-7), None));
327    }
328
329    #[test]
330    fn test_almost_eq_large() {
331        assert!(almost_eq(1e9 * (1.0 + 1e-9), 1e9, None));
332        assert!(almost_eq(1e9, 1e9 * (1.0 + 1e-9), None));
333        assert!(!almost_eq(1e9 * (1.0 + 1e-7), 1e9, None));
334        assert!(!almost_eq(1e9, 1e9 * (1.0 + 1e-7), None));
335    }
336
337    #[test]
338    fn test_almost_le_large() {
339        assert!(almost_le(1e9 * (1.0 + 1e-9), 1e9, None));
340        assert!(almost_le(1e9, 1e9 * (1.0 + 1e-9), None));
341        assert!(!almost_le(1e9 * (1.0 + 1e-7), 1e9, None));
342        assert!(almost_le(1e9, 1e9 * (1.0 + 1e-7), None));
343    }
344
345    #[test]
346    fn test_almost_lt_large() {
347        assert!(!almost_lt(1e9 * (1.0 + 1e-9), 1e9, None));
348        assert!(!almost_lt(1e9, 1e9 * (1.0 + 1e-9), None));
349        assert!(!almost_lt(1e9 * (1.0 + 1e-7), 1e9, None));
350        assert!(almost_lt(1e9, 1e9 * (1.0 + 1e-7), None));
351    }
352}