fastsim_core/
vehicle_utils.rs

1//! Module for utility functions that support the vehicle struct.
2
3#[cfg(feature = "default")]
4use argmin::core::{CostFunction, Executor, State};
5#[cfg(feature = "default")]
6use argmin::solver::neldermead::NelderMead;
7use std::{result::Result, thread, time::Duration};
8use ureq::{Error as OtherError, Error::Status, Response};
9
10#[cfg(feature = "default")]
11use crate::air::*;
12#[cfg(feature = "default")]
13use crate::cycle::RustCycle;
14use crate::imports::*;
15#[cfg(feature = "default")]
16use crate::params::*;
17#[cfg(all(feature = "pyo3", feature = "default"))]
18use crate::pyo3imports::*;
19#[cfg(feature = "default")]
20use crate::simdrive::RustSimDrive;
21#[cfg(feature = "default")]
22use crate::vehicle::RustVehicle;
23
24pub const NETWORK_TEST_DISABLE_ENV_VAR_NAME: &str = "FASTSIM_DISABLE_NETWORK_TESTS";
25
26#[allow(non_snake_case)]
27#[cfg_attr(feature = "pyo3", pyfunction)]
28#[allow(clippy::too_many_arguments)]
29#[cfg(feature = "default")]
30#[cfg_attr(feature = "pyo3", pyo3(signature = (
31    veh,
32    a_lbf,
33    b_lbf__mph,
34    c_lbf__mph2,
35    custom_rho=None,
36    custom_rho_temp_degC=None,
37    custom_rho_elevation_m=None,
38    simdrive_optimize=None,
39    _show_plots=None,
40)))]
41pub fn abc_to_drag_coeffs(
42    veh: &mut RustVehicle,
43    a_lbf: f64,
44    b_lbf__mph: f64,
45    c_lbf__mph2: f64,
46    custom_rho: Option<bool>,
47    custom_rho_temp_degC: Option<f64>,
48    custom_rho_elevation_m: Option<f64>,
49    simdrive_optimize: Option<bool>,
50    _show_plots: Option<bool>,
51) -> (f64, f64) {
52    // For a given vehicle and target A, B, and C coefficients;
53    // calculate and return drag and rolling resistance coefficients.
54    //
55    // Arguments:
56    // ----------
57    // veh: vehicle.RustVehicle with all parameters correct except for drag and rolling resistance coefficients
58    // a_lbf, b_lbf__mph, c_lbf__mph2: coastdown coefficients for road load [lbf] vs speed [mph]
59    // custom_rho: if True, use `air::get_rho()` to calculate the current ambient density
60    // custom_rho_temp_degC: ambient temperature [degree C] for `get_rho()`;
61    //     will only be used when `custom_rho` is True
62    // custom_rho_elevation_m: location elevation [degree C] for `get_rho()`;
63    //     will only be used when `custom_rho` is True; default value is elevation of Chicago, IL
64    // simdrive_optimize: if True, use `SimDrive` to optimize the drag and rolling resistance;
65    //     otherwise, directly use target A, B, C to calculate the results
66    // show_plots: if True, plots are shown
67
68    let air_props = AirProperties::default();
69    let props = RustPhysicalProperties::default();
70    let cur_ambient_air_density_kg__m3 = if custom_rho.unwrap_or(false) {
71        air_props.get_rho(custom_rho_temp_degC.unwrap_or(20.0), custom_rho_elevation_m)
72    } else {
73        props.air_density_kg_per_m3
74    };
75
76    let vmax_mph = 70.0;
77    let a_newton = a_lbf * super::params::N_PER_LBF;
78    let _b_newton__mps = b_lbf__mph * super::params::N_PER_LBF * super::params::MPH_PER_MPS;
79    let c_newton__mps2 = c_lbf__mph2
80        * super::params::N_PER_LBF
81        * super::params::MPH_PER_MPS
82        * super::params::MPH_PER_MPS;
83
84    let cd_len = 300;
85
86    let cyc = RustCycle {
87        time_s: (0..cd_len as i32).map(f64::from).collect(),
88        mps: Array::linspace(vmax_mph / super::params::MPH_PER_MPS, 0.0, cd_len),
89        grade: Array::zeros(cd_len),
90        road_type: Array::zeros(cd_len),
91        name: String::from("cycle"),
92        orphaned: false,
93    };
94
95    // polynomial function for pounds vs speed
96    let dyno_func_lb = |x: &f64| a_lbf + b_lbf__mph * x + c_lbf__mph2 * x.powi(2);
97
98    let drag_coef: f64;
99    let wheel_rr_coef: f64;
100
101    if simdrive_optimize.unwrap_or(true) {
102        let cost = GetError {
103            cycle: &cyc,
104            vehicle: veh,
105            dyno_func_lb: &dyno_func_lb,
106        };
107        let solver = NelderMead::new(vec![vec![0.0, 0.0], vec![0.5, 0.0], vec![0.5, 0.1]]);
108        let res = Executor::new(cost, solver)
109            .configure(|state| state.max_iters(100))
110            .run()
111            .unwrap();
112        let best_param = res.state().get_best_param().unwrap();
113        drag_coef = best_param[0];
114        wheel_rr_coef = best_param[1];
115    } else {
116        drag_coef = c_newton__mps2 / (0.5 * veh.frontal_area_m2 * cur_ambient_air_density_kg__m3);
117        wheel_rr_coef = a_newton / veh.veh_kg / props.a_grav_mps2;
118    }
119
120    veh.drag_coef = drag_coef;
121    veh.wheel_rr_coef = wheel_rr_coef;
122
123    (drag_coef, wheel_rr_coef)
124}
125
126pub fn get_error_val(model: Array1<f64>, test: Array1<f64>, time_steps: Array1<f64>) -> f64 {
127    // Returns time-averaged error for model and test signal.
128    // Arguments:
129    // ----------
130    // model: array of values for signal from model
131    // test: array of values for signal from test data
132    // time_steps: array (or scalar for constant) of values for model time steps [s]
133    // test: array of values for signal from test
134
135    // Output:
136    // -------
137    // err: integral of absolute value of difference between model and
138    // test per time
139
140    assert!(
141        model.len() == test.len() && test.len() == time_steps.len(),
142        "{}, {}, {}",
143        model.len(),
144        test.len(),
145        time_steps.len()
146    );
147
148    let mut err = 0.0;
149    let y = (model - test).mapv(f64::abs);
150
151    for index in 0..time_steps.len() - 1 {
152        err += 0.5 * (time_steps[index + 1] - time_steps[index]) * (y[index] + y[index + 1]);
153    }
154
155    return err / (time_steps.last().unwrap() - time_steps[0]);
156}
157
158#[cfg(feature = "default")]
159struct GetError<'a, F>
160where
161    F: Fn(&f64) -> f64,
162{
163    cycle: &'a RustCycle,
164    vehicle: &'a RustVehicle,
165    dyno_func_lb: &'a F,
166}
167
168#[cfg(feature = "default")]
169impl<F> CostFunction for GetError<'_, F>
170where
171    F: Fn(&f64) -> f64,
172{
173    type Param = Vec<f64>;
174    type Output = f64;
175
176    fn cost(&self, x: &Self::Param) -> anyhow::Result<Self::Output> {
177        let mut veh = self.vehicle.clone();
178        let cyc = self.cycle.clone();
179
180        veh.drag_coef = x[0];
181        veh.wheel_rr_coef = x[1];
182
183        let mut sd_coast = RustSimDrive::new(self.cycle.clone(), veh);
184        sd_coast.impose_coast = Array::from_vec(vec![true; sd_coast.impose_coast.len()]);
185        let _sim_drive_result = sd_coast.sim_drive(None, None);
186
187        let cutoff_vec: Vec<usize> = sd_coast
188            .mps_ach
189            .indexed_iter()
190            .filter_map(|(index, &item)| (item < 0.1).then_some(index))
191            .collect();
192        let cutoff = if cutoff_vec.is_empty() {
193            sd_coast.mps_ach.len()
194        } else {
195            cutoff_vec[0]
196        };
197
198        Ok(get_error_val(
199            (Array::from_vec(vec![1000.0; sd_coast.mps_ach.len()])
200                * (sd_coast.drag_kw + sd_coast.rr_kw)
201                / sd_coast.mps_ach)
202                .slice_move(s![0..cutoff]),
203            (sd_coast.mph_ach.map(self.dyno_func_lb)
204                * Array::from_vec(vec![super::params::N_PER_LBF; sd_coast.mph_ach.len()]))
205            .slice_move(s![0..cutoff]),
206            cyc.time_s.slice_move(s![0..cutoff]),
207        ))
208    }
209}
210
211/// Given the path to a zip archive, print out the names of the files within that archive
212pub fn list_zip_contents(filepath: &Path) -> anyhow::Result<()> {
213    let f = File::open(filepath)?;
214    let mut zip = zip::ZipArchive::new(f)?;
215    for i in 0..zip.len() {
216        let file = zip.by_index(i)?;
217        println!("Filename: {}", file.name());
218    }
219    Ok(())
220}
221
222/// Extract zip archive at filepath to destination directory at dest_dir
223pub fn extract_zip(filepath: &Path, dest_dir: &Path) -> anyhow::Result<()> {
224    let f = File::open(filepath)?;
225    let mut zip = zip::ZipArchive::new(f)?;
226    zip.extract(dest_dir)?;
227    Ok(())
228}
229
230#[derive(Deserialize)]
231pub struct ObjectLinks {
232    #[serde(rename = "self")]
233    pub self_url: Option<String>,
234    pub git: Option<String>,
235    pub html: Option<String>,
236}
237
238#[derive(Deserialize)]
239pub struct GitObjectInfo {
240    pub name: String,
241    pub path: String,
242    pub sha: Option<String>,
243    pub size: Option<i64>,
244    pub url: String,
245    pub html_url: Option<String>,
246    pub git_url: Option<String>,
247    pub download_url: Option<String>,
248    #[serde(rename = "type")]
249    pub url_type: String,
250    #[serde(rename = "_links")]
251    pub links: Option<ObjectLinks>,
252}
253
254const VEHICLE_REPO_LIST_URL: &str =
255    "https://api.github.com/repos/NREL/fastsim-vehicles/contents/public";
256
257/// Function that takes a url and calls the url. If a 503 or 429 error is
258/// thrown, it tries again after a pause, up to four times. It returns either a
259/// result or an error.  
260/// # Arguments  
261/// - url: url to be called
262///
263/// Source: https://docs.rs/ureq/latest/ureq/enum.Error.html
264fn get_response<S: AsRef<str>>(url: S) -> Result<Response, OtherError> {
265    for _ in 1..4 {
266        match ureq::get(url.as_ref()).call() {
267            Err(Status(503, r)) | Err(Status(429, r)) | Err(Status(403, r)) => {
268                let retry: Option<u64> = r.header("retry-after").and_then(|h| h.parse().ok());
269                let retry = retry.unwrap_or(5);
270                eprintln!("{} for {}, retry in {}", r.status(), r.get_url(), retry);
271                thread::sleep(Duration::from_secs(retry));
272            }
273            result => return result,
274        };
275    }
276    // Ran out of retries; try one last time and return whatever result we get.
277    ureq::get(url.as_ref()).call()
278}
279
280/// Returns a list of vehicle file names in the Fastsim Vehicle Repo, or,
281/// optionally, a different GitHub repo, in which case the url provided needs to
282/// be the url for the file tree within GitHub for the root folder the Rust
283/// objects, for example
284/// "https://api.github.com/repos/NREL/fastsim-vehicles/contents/public"  
285/// Note: for each file, the output will list the vehicle file name, including
286/// the path from the root of the repository  
287/// # Arguments  
288/// - repo_url: url to the GitHub repository, Option, if None, defaults to the
289///   FASTSim Vehicle Repo
290pub fn fetch_github_list(repo_url: Option<String>) -> anyhow::Result<Vec<String>> {
291    let repo_url = repo_url.unwrap_or(VEHICLE_REPO_LIST_URL.to_string());
292    let response = get_response(repo_url)?.into_reader();
293    let github_list: Vec<GitObjectInfo> =
294        serde_json::from_reader(response).with_context(|| "Cannot parse github vehicle list.")?;
295    let mut vehicle_name_list = Vec::new();
296    for object in github_list.iter() {
297        if object.url_type == "dir" {
298            let url = &object.url;
299            let vehicle_name_sublist = fetch_github_list(Some(url.to_owned()))?;
300            for name in vehicle_name_sublist.iter() {
301                vehicle_name_list.push(name.to_owned());
302            }
303        } else if object.url_type == "file" {
304            let url = url::Url::parse(&object.url)?;
305            let path = &object.path;
306            let format = url
307                .path_segments()
308                .and_then(|segments| segments.last())
309                .and_then(|filename| Path::new(filename).extension())
310                .and_then(OsStr::to_str)
311                .with_context(|| "Could not parse file format from URL: {url:?}")?;
312            match format.trim_start_matches('.').to_lowercase().as_str() {
313                "yaml" | "yml" => vehicle_name_list.push(path.to_owned()),
314                "json" => vehicle_name_list.push(path.to_owned()),
315                _ => continue,
316            }
317        } else {
318            continue;
319        }
320    }
321    Ok(vehicle_name_list)
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327    use std::env;
328
329    #[test]
330    fn test_get_error_val() {
331        let time_steps = array![0.0, 1.0, 2.0, 3.0, 4.0];
332        let model = array![1.1, 4.6, 2.5, 3.7, 5.0];
333        let test = array![2.1, 4.5, 3.4, 4.8, 6.3];
334
335        let error_val = get_error_val(model, test, time_steps);
336        println!("Error Value: {}", error_val);
337
338        assert!(error_val.approx_eq(&0.8124999999999998, 1e-10));
339    }
340
341    #[cfg(feature = "default")]
342    #[test]
343    fn test_abc_to_drag_coeffs() {
344        let mut veh = RustVehicle::mock_vehicle();
345        let a = 25.91;
346        let b = 0.1943;
347        let c = 0.01796;
348
349        let (drag_coef, wheel_rr_coef) = abc_to_drag_coeffs(
350            &mut veh,
351            a,
352            b,
353            c,
354            Some(false),
355            None,
356            None,
357            Some(true),
358            Some(false),
359        );
360        println!("Drag Coef: {}", drag_coef);
361        println!("Wheel RR Coef: {}", wheel_rr_coef);
362
363        assert!(drag_coef.approx_eq(&0.24676817210529464, 1e-5));
364        assert!(wheel_rr_coef.approx_eq(&0.0068603812443132645, 1e-6));
365        assert_eq!(drag_coef, veh.drag_coef);
366        assert_eq!(wheel_rr_coef, veh.wheel_rr_coef);
367    }
368
369    // NOTE: this test does not seem to reliably pass. Sometimes the function
370    // will give a 403 error and sometimes it will succeed -- I don't think
371    // there's any way to ensure the function succeeds 100% of the time.
372    #[test]
373    fn test_fetch_github_list() {
374        if env::var(NETWORK_TEST_DISABLE_ENV_VAR_NAME).is_ok() {
375            println!("SKIPPING: test_fetch_github_list");
376            return;
377        }
378        let list = fetch_github_list(Some(
379            "https://api.github.com/repos/NREL/fastsim-vehicles/contents".to_owned(),
380        ))
381        .unwrap();
382        let other_list = fetch_github_list(None).unwrap();
383        println!("{:?}", list);
384        println!("{:?}", other_list);
385    }
386}