1#[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 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 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 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
211pub 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
222pub 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
257fn 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 ureq::get(url.as_ref()).call()
278}
279
280pub 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 #[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}