1#![cfg(feature = "vehicle-import")]
2
3use crate::params::*;
4use crate::proc_macros::add_pyo3_api;
5use serde::de::DeserializeOwned;
6use std::collections::HashMap;
7use std::collections::HashSet;
8use std::io::Read;
9use std::path::PathBuf;
10use zip::ZipArchive;
11
12use crate::imports::*;
13#[cfg(feature = "pyo3")]
14use crate::pyo3imports::*;
15use crate::vehicle::RustVehicle;
16use crate::vehicle_utils::abc_to_drag_coeffs;
17
18#[derive(Debug, Serialize, Deserialize, PartialEq)]
19struct VehicleMakesFE {
21 #[serde(rename = "menuItem")]
22 makes: Vec<MakeFE>,
24}
25
26#[derive(Debug, Serialize, Deserialize, PartialEq)]
27struct MakeFE {
29 #[serde(rename = "text")]
30 make_name: String,
32}
33
34#[derive(Debug, Serialize, Deserialize, PartialEq)]
35struct VehicleModelsFE {
37 #[serde(rename = "menuItem")]
38 models: Vec<ModelFE>,
40}
41
42#[derive(Debug, Serialize, Deserialize, PartialEq)]
43struct ModelFE {
45 #[serde(rename = "text")]
46 model_name: String,
48}
49
50#[derive(Debug, Serialize, Deserialize, PartialEq)]
51struct VehicleOptionsFE {
53 #[serde(rename = "menuItem")]
54 options: Vec<OptionFE>,
56}
57
58#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
59#[add_pyo3_api]
60pub struct OptionFE {
62 #[serde(rename = "text")]
63 pub transmission: String,
65 #[serde(rename = "value")]
66 pub id: String,
68}
69
70impl SerdeAPI for OptionFE {}
71
72#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)]
73#[add_pyo3_api]
74pub struct VehicleDataFE {
76 pub id: i32,
78
79 pub year: u32,
81 pub make: String,
83 pub model: String,
85
86 #[serde(rename = "VClass")]
88 pub veh_class: String,
89
90 pub drive: String,
92 #[serde(default, rename = "atvType")]
94 pub alt_veh_type: String,
95
96 #[serde(rename = "fuelType")]
98 pub fuel_type: String,
99 #[serde(rename = "fuelType1")]
101 pub fuel1: String,
102 #[serde(default, rename = "fuelType2")]
104 pub fuel2: String,
105
106 #[serde(default)]
108 pub eng_dscr: String,
109 #[serde(default)]
111 pub cylinders: String,
112 #[serde(default)]
114 pub displ: String,
115 #[serde(rename = "trany")]
117 pub transmission: String,
118
119 #[serde(default, rename = "sCharger")]
121 pub super_charger: String,
122 #[serde(default, rename = "tCharger")]
124 pub turbo_charger: String,
125
126 #[serde(rename = "startStop")]
128 pub start_stop: String,
129
130 #[serde(rename = "phevBlended")]
132 pub phev_blended: bool,
133 #[serde(rename = "phevCity")]
135 pub phev_city_mpge: i32,
136 #[serde(rename = "phevComb")]
138 pub phev_comb_mpge: i32,
139 #[serde(rename = "phevHwy")]
141 pub phev_hwy_mpge: i32,
142
143 #[serde(default, rename = "evMotor")]
145 pub ev_motor_kw: String,
146 #[serde(rename = "range")]
148 pub range_ev: i32,
149
150 #[serde(rename = "city08U")]
152 pub city_mpg_fuel1: f64,
153 #[serde(rename = "cityA08U")]
155 pub city_mpg_fuel2: f64,
156 #[serde(rename = "UCity")]
158 pub unadj_city_mpg_fuel1: f64,
159 #[serde(rename = "UCityA")]
161 pub unadj_city_mpg_fuel2: f64,
162 #[serde(rename = "cityE")]
164 pub city_kwh_per_100mi: f64,
165
166 #[serde(rename = "highway08U")]
168 pub highway_mpg_fuel1: f64,
169 #[serde(rename = "highwayA08U")]
171 pub highway_mpg_fuel2: f64,
172 #[serde(default, rename = "UHighway")]
174 pub unadj_highway_mpg_fuel1: f64,
175 #[serde(default, rename = "UHighwayA")]
177 pub unadj_highway_mpg_fuel2: f64,
178 #[serde(default, rename = "highwayE")]
180 pub highway_kwh_per_100mi: f64,
181
182 #[serde(rename = "comb08U")]
184 pub comb_mpg_fuel1: f64,
185 #[serde(rename = "combA08U")]
187 pub comb_mpg_fuel2: f64,
188 #[serde(default, rename = "combE")]
190 pub comb_kwh_per_100mi: f64,
191
192 #[serde(rename = "emissionsList")]
194 pub emissions_list: EmissionsListFE,
195}
196
197impl SerdeAPI for VehicleDataFE {}
198
199#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)]
200#[serde(rename_all = "camelCase")]
201#[add_pyo3_api]
202pub struct EmissionsListFE {
204 pub emissions_info: Vec<EmissionsInfoFE>,
206}
207
208impl SerdeAPI for EmissionsListFE {}
209
210#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)]
211#[serde(rename_all = "camelCase")]
212#[add_pyo3_api]
213pub struct EmissionsInfoFE {
215 pub efid: String,
217 pub score: f64,
219 pub smartway_score: i32,
221 pub standard: String,
223 pub std_text: String,
225}
226
227impl SerdeAPI for EmissionsInfoFE {}
228
229#[derive(Default, PartialEq, Clone, Debug, Deserialize, Serialize)]
230#[add_pyo3_api]
231pub struct VehicleDataEPA {
233 pub index: u32,
235 #[serde(rename = "Model Year")]
237 pub year: u32,
238 #[serde(rename = "Represented Test Veh Make")]
240 pub make: String,
241 #[serde(rename = "Represented Test Veh Model")]
243 pub model: String,
244 #[serde(rename = "Actual Tested Testgroup")]
246 pub test_id: String,
247 #[serde(rename = "Test Veh Displacement (L)")]
249 pub displ: f64,
250 #[serde(rename = "Rated Horsepower")]
252 pub eng_pwr_hp: u32,
253 #[serde(rename = "# of Cylinders and Rotors")]
255 pub cylinders: String,
256 #[serde(rename = "Tested Transmission Type Code")]
258 pub transmission_code: String,
259 #[serde(rename = "Tested Transmission Type")]
261 pub transmission_type: String,
262 #[serde(rename = "# of Gears")]
264 pub gears: u32,
265 #[serde(rename = "Drive System Code")]
267 pub drive_code: String,
268 #[serde(rename = "Drive System Description")]
270 pub drive: String,
271 #[serde(rename = "Equivalent Test Weight (lbs.)")]
273 pub test_weight_lbs: f64,
274 #[serde(rename = "Test Fuel Type Description")]
276 pub test_fuel_type: String,
277 #[serde(rename = "Target Coef A (lbf)")]
279 pub a_lbf: f64,
280 #[serde(rename = "Target Coef B (lbf/mph)")]
282 pub b_lbf_per_mph: f64,
283 #[serde(rename = "Target Coef C (lbf/mph**2)")]
285 pub c_lbf_per_mph2: f64,
286}
287
288impl SerdeAPI for VehicleDataEPA {}
289
290#[cfg_attr(feature = "pyo3", pyfunction)]
291#[cfg_attr(feature = "pyo3", pyo3(signature = (
292 year,
293 make,
294 model,
295 cache_url=None,
296 data_dir=None,
297)))]
298pub fn get_options_for_year_make_model(
310 year: &str,
311 make: &str,
312 model: &str,
313 cache_url: Option<String>,
314 data_dir: Option<String>,
315) -> anyhow::Result<Vec<VehicleDataFE>> {
316 let y = year.trim().parse()?;
318 let ys = {
319 let mut h = HashSet::new();
320 h.insert(y);
321 h
322 };
323 let ddpath = data_dir
325 .and_then(|path| Some(PathBuf::from(path)))
326 .unwrap_or(create_project_subdir("fe_label_data")?);
327 let cache_url = cache_url.unwrap_or_else(get_default_cache_url);
328 populate_cache_for_given_years_if_needed(ddpath.as_path(), &ys, &cache_url)?;
329 let emissions_data = load_emissions_data_for_given_years(ddpath.as_path(), &ys)?;
330 let fegov_data_by_year =
331 load_fegov_data_for_given_years(ddpath.as_path(), &emissions_data, &ys)?;
332 Ok(fegov_data_by_year
333 .get(&y)
334 .and_then(|fegov_db| {
335 let mut hits = Vec::new();
336 for item in fegov_db.iter() {
337 if item.make == make && item.model == model {
338 hits.push(item.clone());
339 }
340 }
341 Some(hits)
342 })
343 .unwrap_or_else(|| vec![]))
344}
345
346#[cfg_attr(feature = "pyo3", pyfunction)]
347#[cfg_attr(feature = "pyo3", pyo3(signature = (
348 id,
349 year,
350 cache_url=None,
351 data_dir=None,
352)))]
353pub fn get_vehicle_data_for_id(
354 id: i32,
355 year: &str,
356 cache_url: Option<String>,
357 data_dir: Option<String>,
358) -> anyhow::Result<VehicleDataFE> {
359 let y: u32 = year.trim().parse()?;
361 let ys: HashSet<u32> = {
362 let mut h = HashSet::new();
363 h.insert(y);
364 h
365 };
366 let ddpath = data_dir
367 .and_then(|dd| Some(PathBuf::from(dd)))
368 .unwrap_or(create_project_subdir("fe_label_data")?);
369 let cache_url = cache_url.unwrap_or_else(get_default_cache_url);
370 populate_cache_for_given_years_if_needed(ddpath.as_path(), &ys, &cache_url)
371 .with_context(|| format!("Unable to load or download cache data from {cache_url}"))?;
372 let emissions_data = load_emissions_data_for_given_years(ddpath.as_path(), &ys)?;
373 let fegov_data_by_year =
374 load_fegov_data_for_given_years(ddpath.as_path(), &emissions_data, &ys)?;
375 let fegov_db = fegov_data_by_year
376 .get(&y)
377 .with_context(|| format!("Could not get fueleconomy.gov data from year {y}"))?;
378 for item in fegov_db.iter() {
379 if item.id == id {
380 return Ok(item.clone());
381 }
382 }
383 bail!("Could not find ID in data {id}");
384}
385
386fn derive_transmission_specs(fegov: &VehicleDataFE) -> (u32, String) {
387 let num_gears_fe_gov: u32;
388 let transmission_fe_gov: String;
389 if fegov.transmission.contains("Manual") {
391 transmission_fe_gov = String::from('M');
392 num_gears_fe_gov = fegov.transmission.as_str()[fegov.transmission.find("-spd").unwrap() - 1
393 ..fegov.transmission.find("-spd").unwrap()]
394 .parse()
395 .unwrap();
396 } else if fegov.transmission.contains("variable gear ratios") {
397 transmission_fe_gov = String::from("CVT");
398 num_gears_fe_gov = 1;
399 } else if fegov.transmission.contains("AV-S") {
400 transmission_fe_gov = String::from("SCV");
401 num_gears_fe_gov = fegov.transmission.as_str()
402 [fegov.transmission.find('S').unwrap() + 1..fegov.transmission.find(')').unwrap()]
403 .parse()
404 .unwrap();
405 } else if fegov.transmission.contains("AM-S") {
406 transmission_fe_gov = String::from("AMS");
407 num_gears_fe_gov = fegov.transmission.as_str()
408 [fegov.transmission.find('S').unwrap() + 1..fegov.transmission.find(')').unwrap()]
409 .parse()
410 .unwrap();
411 } else if fegov.transmission.contains('S') {
412 transmission_fe_gov = String::from("SA");
413 num_gears_fe_gov = fegov.transmission.as_str()
414 [fegov.transmission.find('S').unwrap() + 1..fegov.transmission.find(')').unwrap()]
415 .parse()
416 .unwrap();
417 } else if fegov.transmission.contains("-spd") {
418 transmission_fe_gov = String::from('A');
419 num_gears_fe_gov = fegov.transmission.as_str()[fegov.transmission.find("-spd").unwrap() - 1
420 ..fegov.transmission.find("-spd").unwrap()]
421 .parse()
422 .unwrap();
423 } else {
424 transmission_fe_gov = String::from('A');
425 num_gears_fe_gov = fegov.transmission.as_str()
426 [fegov.transmission.find("(A").unwrap() + 2..fegov.transmission.find(')').unwrap()]
427 .parse()
428 .unwrap_or(1);
429 }
430 (num_gears_fe_gov, transmission_fe_gov)
431}
432
433fn match_epatest_with_fegov_v2(
448 fegov: &VehicleDataFE,
449 epatest_data: &[VehicleDataEPA],
450) -> Option<VehicleDataEPA> {
451 let fe_model_upper = fegov.model.to_uppercase().replace("4WD", "AWD");
452 let fe_model_words: Vec<&str> = fe_model_upper.split_ascii_whitespace().collect();
453 let num_fe_model_words = fe_model_words.len();
454 let fegov_disp = fegov.displ.parse::<f64>().unwrap_or_default();
455 let efid = if !fegov.emissions_list.emissions_info.is_empty() {
456 fegov.emissions_list.emissions_info[0].efid.clone()
457 } else {
458 String::new()
459 };
460 let fegov_drive = {
461 let mut s = String::new();
462 if !fegov.drive.is_empty() {
463 let maybe_char = fegov.drive.chars().next();
464 if let Some(c) = maybe_char {
465 s.push(c);
466 }
467 }
468 s
469 };
470 let (num_gears_fe_gov, transmission_fe_gov) = derive_transmission_specs(fegov);
471 let epa_candidates = {
472 let mut xs = Vec::new();
473 for x in epatest_data {
474 if x.year == fegov.year && x.make.eq_ignore_ascii_case(&fegov.make) {
475 let mut score = 0.0;
476
477 if x.test_fuel_type.contains("Cold CO") {
479 continue;
480 }
481 let matching_test_id = if !x.test_id.is_empty() && !efid.is_empty() {
482 x.test_id.ends_with(&efid[1..efid.len()])
483 } else {
484 false
485 };
486 let name_match = if matching_test_id || x.model.eq_ignore_ascii_case(&fegov.model) {
488 1.0
489 } else {
490 let epa_model_upper = x.model.to_uppercase().replace("4WD", "AWD");
491 let epa_model_words: Vec<&str> =
492 epa_model_upper.split_ascii_whitespace().collect();
493 let num_epa_model_words = epa_model_words.len();
494 let mut match_count = 0;
495 for word in &epa_model_words {
496 match_count += fe_model_words.contains(word) as i64;
497 }
498 let match_frac = (match_count as f64 * match_count as f64)
499 / (num_epa_model_words as f64 * num_fe_model_words as f64);
500 match_frac
501 };
502 if name_match == 0.0 {
503 continue;
504 }
505 if fegov.alt_veh_type == *"EV" {
507 if x.cylinders.is_empty() && x.displ.round() == 0.0 {
508 score += 1.0;
509 }
510 } else {
511 let epa_disp = (x.displ * 10.0).round() / 10.0;
512 if x.cylinders == fegov.cylinders && epa_disp == fegov_disp {
513 score += 1.0;
514 }
515 }
516 let drive_code = if x.model.contains("4WD")
518 || x.model.contains("AWD")
519 || x.drive.contains("4-Wheel Drive")
520 {
521 String::from('A')
522 } else {
523 x.drive.clone()
524 };
525 if drive_code == fegov_drive {
526 score += 1.0;
527 }
528 if x.transmission_code == transmission_fe_gov {
530 score += 0.5;
531 } else if transmission_fe_gov.starts_with(x.transmission_type.as_str()) {
532 score += 0.25;
533 }
534 if x.gears == num_gears_fe_gov {
535 score += 0.5;
536 }
537 xs.push((name_match, score, x.clone()));
538 }
539 }
540 xs
541 };
542 if epa_candidates.is_empty() {
543 None
544 } else {
545 let mut largest_id_match_value = 0.0;
546 let mut largest_score_value = 0.0;
547 let mut best_idx = 0;
548 for (idx, item) in epa_candidates.iter().enumerate() {
549 if item.0 > largest_id_match_value
550 || (item.0 == largest_id_match_value && item.1 > largest_score_value)
551 {
552 largest_id_match_value = item.0;
553 largest_score_value = item.1;
554 best_idx = idx;
555 }
556 }
557 if largest_id_match_value == 0.0 {
558 None
559 } else {
560 Some(epa_candidates[best_idx].2.clone())
561 }
562 }
563}
564
565#[allow(dead_code)]
567fn match_epatest_with_fegov(
568 fegov: &VehicleDataFE,
569 epatest_data: &[VehicleDataEPA],
570) -> Option<VehicleDataEPA> {
571 if fegov.emissions_list.emissions_info.is_empty() {
572 return None;
573 }
574 let mut veh_list_overall: HashMap<String, Vec<VehicleDataEPA>> = HashMap::new();
576 let mut veh_list_efid: HashMap<String, Vec<VehicleDataEPA>> = HashMap::new();
577 let mut best_match_percent_efid = 0.0;
578 let mut best_match_model_efid = String::new();
579 let mut best_match_percent_overall = 0.0;
580 let mut best_match_model_overall = String::new();
581
582 let fe_model_upper = fegov.model.to_uppercase().replace("4WD", "AWD");
583 let fe_model_words: Vec<&str> = fe_model_upper.split(' ').collect();
584 let num_fe_model_words = fe_model_words.len();
585 let efid = &fegov.emissions_list.emissions_info[0].efid;
586
587 for veh_epa in epatest_data {
588 let mut match_count = 0;
590 let epa_model_upper = veh_epa.model.to_uppercase().replace("4WD", "AWD");
591 let epa_model_words: Vec<&str> = epa_model_upper.split(' ').collect();
592 let num_epa_model_words = epa_model_words.len();
593 for word in &epa_model_words {
594 match_count += fe_model_words.contains(word) as i64;
595 }
596 let match_percent = (match_count as f64 * match_count as f64)
598 / (num_epa_model_words as f64 * num_fe_model_words as f64);
599
600 if veh_list_overall.contains_key(&veh_epa.model) {
602 if let Some(x) = veh_list_overall.get_mut(&veh_epa.model) {
603 (*x).push(veh_epa.clone());
604 }
605 } else {
606 veh_list_overall.insert(veh_epa.model.clone(), vec![veh_epa.clone()]);
607
608 if match_percent > best_match_percent_overall {
609 best_match_percent_overall = match_percent;
610 best_match_model_overall = veh_epa.model.clone();
611 }
612 }
613
614 if veh_epa.test_id.ends_with(&efid[1..efid.len()]) {
617 if veh_list_efid.contains_key(&veh_epa.model) {
618 if let Some(x) = veh_list_efid.get_mut(&veh_epa.model) {
619 (*x).push(veh_epa.clone());
620 }
621 } else {
622 veh_list_efid.insert(veh_epa.model.clone(), vec![veh_epa.clone()]);
623 if match_percent > best_match_percent_efid {
624 best_match_percent_efid = match_percent;
625 best_match_model_efid = veh_epa.model.clone();
626 }
627 }
628 }
629 }
630
631 let veh_list = if best_match_model_efid == best_match_model_overall {
633 let x = veh_list_efid.get(&best_match_model_efid);
634 x?;
635 x.unwrap().to_vec()
636 } else {
637 veh_list_overall
638 .get(&best_match_model_overall)
639 .unwrap()
640 .to_vec()
641 };
642
643 let num_gears_fe_gov: u32;
645 let transmission_fe_gov: String;
646 if fegov.transmission.contains("Manual") {
648 transmission_fe_gov = String::from('M');
649 num_gears_fe_gov = fegov.transmission.as_str()[fegov.transmission.find("-spd").unwrap() - 1
650 ..fegov.transmission.find("-spd").unwrap()]
651 .parse()
652 .unwrap();
653 } else if fegov.transmission.contains("variable gear ratios") {
654 transmission_fe_gov = String::from("CVT");
655 num_gears_fe_gov = 1;
656 } else if fegov.transmission.contains("AV-S") {
657 transmission_fe_gov = String::from("SCV");
658 num_gears_fe_gov = fegov.transmission.as_str()
659 [fegov.transmission.find('S').unwrap() + 1..fegov.transmission.find(')').unwrap()]
660 .parse()
661 .unwrap();
662 } else if fegov.transmission.contains("AM-S") {
663 transmission_fe_gov = String::from("AMS");
664 num_gears_fe_gov = fegov.transmission.as_str()
665 [fegov.transmission.find('S').unwrap() + 1..fegov.transmission.find(')').unwrap()]
666 .parse()
667 .unwrap();
668 } else if fegov.transmission.contains('S') {
669 transmission_fe_gov = String::from("SA");
670 num_gears_fe_gov = fegov.transmission.as_str()
671 [fegov.transmission.find('S').unwrap() + 1..fegov.transmission.find(')').unwrap()]
672 .parse()
673 .unwrap();
674 } else if fegov.transmission.contains("-spd") {
675 transmission_fe_gov = String::from('A');
676 num_gears_fe_gov = fegov.transmission.as_str()[fegov.transmission.find("-spd").unwrap() - 1
677 ..fegov.transmission.find("-spd").unwrap()]
678 .parse()
679 .unwrap();
680 } else {
681 transmission_fe_gov = String::from('A');
682 num_gears_fe_gov = fegov.transmission.as_str()
683 [fegov.transmission.find("(A").unwrap() + 2..fegov.transmission.find(')').unwrap()]
684 .parse()
685 .unwrap_or(1)
686 }
687
688 let mut most_common_veh = VehicleDataEPA::default();
691 let mut most_common_count = 0;
692 let mut current_veh = VehicleDataEPA::default();
693 let mut current_count = 0;
694 for mut veh_epa in veh_list {
695 if veh_epa.model.contains("4WD")
696 || veh_epa.model.contains("AWD")
697 || veh_epa.drive.contains("4-Wheel Drive")
698 {
699 veh_epa.drive_code = String::from('A');
700 veh_epa.drive = String::from("All Wheel Drive");
701 }
702 if !veh_epa.test_fuel_type.contains("Cold CO")
703 && (veh_epa.transmission_code == transmission_fe_gov
704 || fegov
705 .transmission
706 .starts_with(veh_epa.transmission_type.as_str()))
707 && veh_epa.gears == num_gears_fe_gov
708 && veh_epa.drive_code == fegov.drive[0..1]
709 && ((fegov.alt_veh_type == *"EV"
710 && veh_epa.displ.round() == 0.0
711 && veh_epa.cylinders == String::new())
712 || ((veh_epa.displ * 10.0).round() / 10.0
713 == (fegov.displ.parse::<f64>().unwrap_or_default())
714 && veh_epa.cylinders == fegov.cylinders))
715 {
716 if veh_epa == current_veh {
717 current_count += 1;
718 } else {
719 if current_count > most_common_count {
720 most_common_veh = current_veh.clone();
721 most_common_count = current_count;
722 }
723 current_veh = veh_epa.clone();
724 current_count = 1;
725 }
726 }
727 }
728 if current_count > most_common_count {
729 Some(current_veh)
730 } else {
731 Some(most_common_veh)
732 }
733}
734
735#[derive(Default, PartialEq, Clone, Debug, Deserialize, Serialize)]
736#[add_pyo3_api(
737 #[new]
738 #[pyo3(signature = (
739 vehicle_width_in,
740 vehicle_height_in,
741 fuel_tank_gal,
742 ess_max_kwh,
743 mc_max_kw,
744 ess_max_kw,
745 fc_max_kw=None
746 ))]
747 pub fn __new__(
748 vehicle_width_in: f64,
749 vehicle_height_in: f64,
750 fuel_tank_gal: f64,
751 ess_max_kwh: f64,
752 mc_max_kw: f64,
753 ess_max_kw: f64,
754 fc_max_kw: Option<f64>
755 ) -> Self {
756 OtherVehicleInputs {
757 vehicle_width_in,
758 vehicle_height_in,
759 fuel_tank_gal,
760 ess_max_kwh,
761 mc_max_kw,
762 ess_max_kw,
763 fc_max_kw
764 }
765 }
766)]
767pub struct OtherVehicleInputs {
768 pub vehicle_width_in: f64,
769 pub vehicle_height_in: f64,
770 pub fuel_tank_gal: f64,
771 pub ess_max_kwh: f64,
772 pub mc_max_kw: f64,
773 pub ess_max_kw: f64,
774 pub fc_max_kw: Option<f64>,
775}
776
777impl SerdeAPI for OtherVehicleInputs {}
778
779#[cfg_attr(feature = "pyo3", pyfunction)]
780#[cfg_attr(feature = "pyo3", pyo3(signature = (
781 vehicle_id,
782 year,
783 other_inputs,
784 cache_url=None,
785 data_dir=None,
786)))]
787pub fn vehicle_import_by_id_and_year(
800 vehicle_id: i32,
801 year: u32,
802 other_inputs: &OtherVehicleInputs,
803 cache_url: Option<String>,
804 data_dir: Option<String>,
805) -> anyhow::Result<RustVehicle> {
806 let mut maybe_veh = None;
807 let data_dir_path = data_dir
809 .and_then(|path| Some(PathBuf::from(path)))
810 .unwrap_or(create_project_subdir("fe_label_data")?);
811 let model_years = {
812 let mut h = HashSet::new();
813 h.insert(year);
814 h
815 };
816 let cache_url = cache_url.unwrap_or(get_default_cache_url());
817 populate_cache_for_given_years_if_needed(&data_dir_path, &model_years, &cache_url)?;
818 let emissions_data = load_emissions_data_for_given_years(&data_dir_path, &model_years)?;
819 let fegov_data_by_year =
820 load_fegov_data_for_given_years(&data_dir_path, &emissions_data, &model_years)?;
821 let epatest_db = read_epa_test_data_for_given_years(&data_dir_path, &model_years)?;
822 if let Some(fe_gov_data) = fegov_data_by_year.get(&year) {
823 if let Some(epa_data) = epatest_db.get(&year) {
824 let fe_gov_data = {
825 let mut maybe_data = None;
826 for item in fe_gov_data {
827 if item.id == vehicle_id {
828 maybe_data = Some(item.clone());
829 break;
830 }
831 }
832 maybe_data
833 };
834 if let Some(fe_gov_data) = fe_gov_data {
835 if let Some(epa_data) = match_epatest_with_fegov_v2(&fe_gov_data, epa_data) {
836 maybe_veh = try_make_single_vehicle(&fe_gov_data, &epa_data, other_inputs);
837 }
838 }
839 }
840 }
841 match maybe_veh {
842 Some(veh) => Ok(veh),
843 None => Err(anyhow!("Unable to find/match vehicle in DB")),
844 }
845}
846
847pub fn get_default_cache_url() -> String {
848 String::from("https://github.com/NREL/vehicle-data/raw/main/")
849}
850
851fn get_fuel_economy_gov_data_for_input_record(
852 vir: &VehicleInputRecord,
853 fegov_data: &[VehicleDataFE],
854) -> Vec<VehicleDataFE> {
855 let mut output = Vec::new();
856 let vir_make = String::from(vir.make.to_lowercase().trim());
857 let vir_model = String::from(vir.model.to_lowercase().trim());
858 for fedat in fegov_data {
859 let fe_make = String::from(fedat.make.to_lowercase().trim());
860 let fe_model = String::from(fedat.model.to_lowercase().trim());
861 if fedat.year == vir.year && fe_make.eq(&vir_make) && fe_model.eq(&vir_model) {
862 output.push(fedat.clone());
863 }
864 }
865 output
866}
867
868fn try_make_single_vehicle(
870 fe_gov_data: &VehicleDataFE,
871 epa_data: &VehicleDataEPA,
872 other_inputs: &OtherVehicleInputs,
873) -> Option<RustVehicle> {
874 if epa_data == &VehicleDataEPA::default() {
875 return None;
876 }
877 let veh_pt_type = match fe_gov_data.alt_veh_type.as_str() {
878 "Hybrid" => crate::vehicle::HEV,
879 "Plug-in Hybrid" => crate::vehicle::PHEV,
880 "EV" => crate::vehicle::BEV,
881 _ => crate::vehicle::CONV,
882 };
883
884 let fs_max_kw: f64;
885 let fc_max_kw: f64;
886 let fc_eff_type: String;
887 let fc_eff_map: Array1<f64>;
888 let mc_max_kw: f64;
889 let min_soc: f64;
890 let max_soc: f64;
891 let ess_dischg_to_fc_max_eff_perc: f64;
892 let mph_fc_on: f64;
893 let kw_demand_fc_on: f64;
894 let aux_kw: f64;
895 let trans_eff: f64;
896 let val_range_miles: f64;
897 let ess_max_kw: f64;
898 let ess_max_kwh: f64;
899 let fs_kwh: f64;
900
901 let ref_veh = RustVehicle::default();
902
903 if veh_pt_type == crate::vehicle::CONV {
904 fs_max_kw = 2000.0;
905 fs_kwh = other_inputs.fuel_tank_gal * ref_veh.props.kwh_per_gge;
906 fc_max_kw = epa_data.eng_pwr_hp as f64 / HP_PER_KW;
907 fc_eff_type = String::from(crate::vehicle::SI);
908 fc_eff_map = Array::from_vec(vec![
909 0.1, 0.12, 0.16, 0.22, 0.28, 0.33, 0.35, 0.36, 0.35, 0.34, 0.32, 0.3,
910 ]);
911 mc_max_kw = 0.0;
912 min_soc = 0.0;
913 max_soc = 1.0;
914 ess_dischg_to_fc_max_eff_perc = 0.0;
915 mph_fc_on = 55.0;
916 kw_demand_fc_on = 100.0;
917 aux_kw = 0.7;
918 trans_eff = 0.92;
919 val_range_miles = 0.0;
920 ess_max_kw = 0.0;
921 ess_max_kwh = 0.0;
922 } else if veh_pt_type == crate::vehicle::HEV {
923 fs_max_kw = 2000.0;
924 fs_kwh = other_inputs.fuel_tank_gal * ref_veh.props.kwh_per_gge;
925 fc_max_kw = other_inputs
926 .fc_max_kw
927 .unwrap_or(epa_data.eng_pwr_hp as f64 / HP_PER_KW);
928 fc_eff_type = String::from(crate::vehicle::ATKINSON);
929 fc_eff_map = Array::from_vec(vec![
930 0.10, 0.12, 0.28, 0.35, 0.375, 0.39, 0.40, 0.40, 0.38, 0.37, 0.36, 0.35,
931 ]);
932 min_soc = 0.0;
933 max_soc = 1.0;
934 ess_dischg_to_fc_max_eff_perc = 0.0;
935 mph_fc_on = 1.0;
936 kw_demand_fc_on = 100.0;
937 aux_kw = 0.5;
938 trans_eff = 0.95;
939 val_range_miles = 0.0;
940 ess_max_kw = other_inputs.ess_max_kw;
941 ess_max_kwh = other_inputs.ess_max_kwh;
942 mc_max_kw = other_inputs.mc_max_kw;
943 } else if veh_pt_type == crate::vehicle::PHEV {
944 fs_max_kw = 2000.0;
945 fs_kwh = other_inputs.fuel_tank_gal * ref_veh.props.kwh_per_gge;
946 fc_max_kw = other_inputs
947 .fc_max_kw
948 .unwrap_or(epa_data.eng_pwr_hp as f64 / HP_PER_KW);
949 fc_eff_type = String::from(crate::vehicle::ATKINSON);
950 fc_eff_map = Array::from_vec(vec![
951 0.10, 0.12, 0.28, 0.35, 0.375, 0.39, 0.40, 0.40, 0.38, 0.37, 0.36, 0.35,
952 ]);
953 min_soc = 0.0;
954 max_soc = 1.0;
955 ess_dischg_to_fc_max_eff_perc = 1.0;
956 mph_fc_on = 85.0;
957 kw_demand_fc_on = 120.0;
958 aux_kw = 0.3;
959 trans_eff = 0.98;
960 val_range_miles = 0.0;
961 ess_max_kw = other_inputs.ess_max_kw;
962 ess_max_kwh = other_inputs.ess_max_kwh;
963 mc_max_kw = other_inputs.mc_max_kw;
964 } else if veh_pt_type == crate::vehicle::BEV {
965 fs_max_kw = 0.0;
966 fs_kwh = 0.0;
967 fc_max_kw = 0.0;
968 fc_eff_type = String::from(crate::vehicle::SI);
969 fc_eff_map = Array::from_vec(vec![
970 0.10, 0.12, 0.16, 0.22, 0.28, 0.33, 0.35, 0.36, 0.35, 0.34, 0.32, 0.30,
971 ]);
972 mc_max_kw = other_inputs.mc_max_kw;
973 min_soc = 0.0;
974 max_soc = 1.0;
975 ess_max_kw = other_inputs.ess_max_kw;
976 ess_max_kwh = other_inputs.ess_max_kwh;
977 mph_fc_on = 1.0;
978 kw_demand_fc_on = 100.0;
979 aux_kw = 0.25;
980 trans_eff = 0.98;
981 val_range_miles = fe_gov_data.range_ev as f64;
982 ess_dischg_to_fc_max_eff_perc = 0.0;
983 } else {
984 println!("Unhandled vehicle powertrain type: {veh_pt_type}");
985 return None;
986 }
987
988 let mut veh = RustVehicle {
1000 doc: Some(format!("EPA ({}) index {}", epa_data.year, epa_data.index)),
1001 veh_override_kg: Some(epa_data.test_weight_lbs / LBS_PER_KG),
1002 veh_cg_m: match fe_gov_data.drive.as_str() {
1003 "Front-Wheel Drive" => 0.53,
1004 _ => -0.53,
1005 },
1006 scenario_name: format!(
1008 "{} {} {}",
1009 fe_gov_data.year, fe_gov_data.make, fe_gov_data.model
1010 ),
1011 max_roadway_chg_kw: Default::default(),
1012 selection: 0,
1013 veh_year: fe_gov_data.year,
1014 veh_pt_type: String::from(veh_pt_type),
1015 drag_coef: 0.0, frontal_area_m2: 0.85 * (other_inputs.vehicle_width_in * other_inputs.vehicle_height_in)
1017 / (IN_PER_M * IN_PER_M),
1018 fs_kwh,
1019 idle_fc_kw: 0.0,
1020 mc_eff_map: Array1::zeros(LARGE_BASELINE_EFF.len()),
1021 wheel_rr_coef: 0.0, stop_start: false,
1023 force_aux_on_fc: false,
1024 val_udds_mpgge: fe_gov_data.city_mpg_fuel1,
1025 val_hwy_mpgge: fe_gov_data.highway_mpg_fuel1,
1026 val_comb_mpgge: fe_gov_data.comb_mpg_fuel1,
1027 fc_peak_eff_override: None,
1028 mc_peak_eff_override: Some(0.95),
1029 fs_max_kw,
1030 fc_max_kw,
1031 fc_eff_type,
1032 fc_eff_map,
1033 mc_max_kw,
1034 min_soc,
1035 max_soc,
1036 ess_dischg_to_fc_max_eff_perc,
1037 mph_fc_on,
1038 kw_demand_fc_on,
1039 aux_kw,
1040 trans_eff,
1041 val_range_miles,
1042 ess_max_kwh,
1043 ess_max_kw,
1044 ..Default::default()
1045 };
1046 veh.set_derived().unwrap();
1047
1048 abc_to_drag_coeffs(
1049 &mut veh,
1050 epa_data.a_lbf,
1051 epa_data.b_lbf_per_mph,
1052 epa_data.c_lbf_per_mph2,
1053 Some(false),
1054 None,
1055 None,
1056 Some(true),
1057 Some(false),
1058 );
1059 Some(veh)
1060}
1061
1062fn try_import_vehicles(
1063 vir: &VehicleInputRecord,
1064 fegov_data: &[VehicleDataFE],
1065 epatest_data: &[VehicleDataEPA],
1066) -> Vec<RustVehicle> {
1067 let other_inputs = vir_to_other_inputs(vir);
1068 let mut outputs = Vec::new();
1070 let fegov_hits = get_fuel_economy_gov_data_for_input_record(vir, fegov_data);
1071 for hit in fegov_hits {
1072 if let Some(epa_data) = match_epatest_with_fegov_v2(&hit, epatest_data) {
1073 if let Some(v) = try_make_single_vehicle(&hit, &epa_data, &other_inputs) {
1074 let mut v = v.clone();
1075 if hit.alt_veh_type == *"EV" {
1076 v.scenario_name = format!("{} (EV)", v.scenario_name);
1077 } else {
1078 let alt_type = if hit.alt_veh_type.is_empty() {
1079 String::from("")
1080 } else {
1081 format!("{}, ", hit.alt_veh_type)
1082 };
1083 v.scenario_name = format!(
1084 "{} ( {} {} cylinders, {} L, {} )",
1085 v.scenario_name, alt_type, hit.cylinders, hit.displ, hit.transmission
1086 );
1087 }
1088 outputs.push(v);
1089 } else {
1090 println!(
1091 "Unable to create vehicle for {}-{}-{}",
1092 vir.year, vir.make, vir.model
1093 );
1094 }
1095 } else {
1096 println!(
1097 "Did not match any EPA data for {}-{}-{}...",
1098 vir.year, vir.make, vir.model
1099 );
1100 }
1101 }
1102 outputs
1103}
1104#[derive(Debug, Serialize, Deserialize, Clone)]
1105pub struct VehicleInputRecord {
1106 pub make: String,
1107 pub model: String,
1108 pub year: u32,
1109 pub output_file_name: String,
1110 pub vehicle_width_in: f64,
1111 pub vehicle_height_in: f64,
1112 pub fuel_tank_gal: f64,
1113 pub ess_max_kwh: f64,
1114 pub mc_max_kw: f64,
1115 pub ess_max_kw: f64,
1116 pub fc_max_kw: Option<f64>,
1117}
1118
1119fn vir_to_other_inputs(vir: &VehicleInputRecord) -> OtherVehicleInputs {
1121 OtherVehicleInputs {
1122 vehicle_width_in: vir.vehicle_width_in,
1123 vehicle_height_in: vir.vehicle_height_in,
1124 fuel_tank_gal: vir.fuel_tank_gal,
1125 ess_max_kwh: vir.ess_max_kwh,
1126 mc_max_kw: vir.mc_max_kw,
1127 ess_max_kw: vir.ess_max_kw,
1128 fc_max_kw: vir.fc_max_kw,
1129 }
1130}
1131
1132fn read_vehicle_input_records_from_file(
1133 filepath: &Path,
1134) -> anyhow::Result<Vec<VehicleInputRecord>> {
1135 let f = File::open(filepath)?;
1136 read_records_from_file(f)
1137}
1138
1139fn read_records_from_file<T: DeserializeOwned>(
1140 rdr: impl std::io::Read + std::io::Seek,
1141) -> anyhow::Result<Vec<T>> {
1142 let mut output = Vec::new();
1143 let mut reader = csv::Reader::from_reader(rdr);
1144 for result in reader.deserialize() {
1145 let record = result?;
1146 output.push(record);
1147 }
1148 Ok(output)
1149}
1150
1151fn read_fuelecon_gov_emissions_to_hashmap(
1152 rdr: impl std::io::Read + std::io::Seek,
1153) -> HashMap<u32, Vec<EmissionsInfoFE>> {
1154 let mut output: HashMap<u32, Vec<EmissionsInfoFE>> = HashMap::new();
1155 let mut reader = csv::Reader::from_reader(rdr);
1156 for result in reader.deserialize() {
1157 if result.is_ok() {
1158 let ok_result: Option<HashMap<String, String>> = result.ok();
1159 if let Some(item) = ok_result {
1160 if let Some(id_str) = item.get("id") {
1161 if let Ok(id) = id_str.parse() {
1162 output.entry(id).or_default();
1163 if let Some(ers) = output.get_mut(&id) {
1164 let emiss = EmissionsInfoFE {
1165 efid: item.get("efid").unwrap().clone(),
1166 score: item.get("score").unwrap().parse().unwrap(),
1167 smartway_score: item.get("smartwayScore").unwrap().parse().unwrap(),
1168 standard: item.get("standard").unwrap().clone(),
1169 std_text: item.get("stdText").unwrap().clone(),
1170 };
1171 ers.push(emiss);
1172 }
1173 }
1174 }
1175 }
1176 }
1177 }
1178 output
1179}
1180
1181fn read_fuelecon_gov_data_from_file(
1182 rdr: impl std::io::Read + std::io::Seek,
1183 emissions: &HashMap<u32, Vec<EmissionsInfoFE>>,
1184) -> anyhow::Result<Vec<VehicleDataFE>> {
1185 let mut output = Vec::new();
1186 let mut reader = csv::Reader::from_reader(rdr);
1187 for result in reader.deserialize() {
1188 let item: HashMap<String, String> = result?;
1189 let id = item.get("id").unwrap().parse().unwrap();
1190 let emissions_list = if emissions.contains_key(&id) {
1191 EmissionsListFE {
1192 emissions_info: emissions.get(&id).unwrap().to_vec(),
1193 }
1194 } else {
1195 EmissionsListFE::default()
1196 };
1197 let vd = VehicleDataFE {
1198 id: item.get("id").unwrap().trim().parse().unwrap(),
1199
1200 year: item.get("year").unwrap().parse().unwrap(),
1201 make: item.get("make").unwrap().clone(),
1202 model: item.get("model").unwrap().clone(),
1203
1204 veh_class: item.get("VClass").unwrap().clone(),
1205
1206 drive: item.get("drive").unwrap().clone(),
1207 alt_veh_type: item.get("atvType").unwrap().clone(),
1208
1209 fuel_type: item.get("fuelType").unwrap().clone(),
1210 fuel1: item.get("fuelType1").unwrap().clone(),
1211 fuel2: item.get("fuelType2").unwrap().clone(),
1212
1213 eng_dscr: item.get("eng_dscr").unwrap().clone(),
1214 cylinders: item.get("cylinders").unwrap().clone(),
1215 displ: item.get("displ").unwrap().clone(),
1216 transmission: item.get("trany").unwrap().clone(),
1217
1218 super_charger: item.get("sCharger").unwrap().clone(),
1219 turbo_charger: item.get("tCharger").unwrap().clone(),
1220
1221 start_stop: item.get("startStop").unwrap().clone(),
1222
1223 phev_blended: item
1224 .get("phevBlended")
1225 .unwrap()
1226 .trim()
1227 .to_lowercase()
1228 .parse()
1229 .unwrap(),
1230 phev_city_mpge: item.get("phevCity").unwrap().parse().unwrap(),
1231 phev_comb_mpge: item.get("phevComb").unwrap().parse().unwrap(),
1232 phev_hwy_mpge: item.get("phevHwy").unwrap().parse().unwrap(),
1233
1234 ev_motor_kw: item.get("evMotor").unwrap().clone(),
1235 range_ev: item.get("range").unwrap().parse().unwrap(),
1236
1237 city_mpg_fuel1: item.get("city08U").unwrap().parse().unwrap(),
1238 city_mpg_fuel2: item.get("cityA08U").unwrap().parse().unwrap(),
1239 unadj_city_mpg_fuel1: item.get("UCity").unwrap().parse().unwrap(),
1240 unadj_city_mpg_fuel2: item.get("UCityA").unwrap().parse().unwrap(),
1241 city_kwh_per_100mi: item.get("cityE").unwrap().parse().unwrap(),
1242
1243 highway_mpg_fuel1: item.get("highway08U").unwrap().parse().unwrap(),
1244 highway_mpg_fuel2: item.get("highwayA08U").unwrap().parse().unwrap(),
1245 unadj_highway_mpg_fuel1: item.get("UHighway").unwrap().parse().unwrap(),
1246 unadj_highway_mpg_fuel2: item.get("UHighwayA").unwrap().parse().unwrap(),
1247 highway_kwh_per_100mi: item.get("highwayE").unwrap().parse().unwrap(),
1248
1249 comb_mpg_fuel1: item.get("comb08U").unwrap().parse().unwrap(),
1250 comb_mpg_fuel2: item.get("combA08U").unwrap().parse().unwrap(),
1251 comb_kwh_per_100mi: item.get("combE").unwrap().parse().unwrap(),
1252
1253 emissions_list,
1254 };
1255 output.push(vd);
1256 }
1257 Ok(output)
1258}
1259fn read_epa_test_data_for_given_years<P: AsRef<Path>>(
1260 data_dir_path: P,
1261 years: &HashSet<u32>,
1262) -> anyhow::Result<HashMap<u32, Vec<VehicleDataEPA>>> {
1263 let mut epatest_db = HashMap::new();
1264 for year in years {
1265 let p = data_dir_path.as_ref().join(format!("{year}-testcar.csv"));
1266 let records = read_records_from_file(File::open(p)?)?;
1267 epatest_db.insert(*year, records);
1268 }
1269 Ok(epatest_db)
1270}
1271
1272fn determine_model_years_of_interest(virs: &[VehicleInputRecord]) -> HashSet<u32> {
1273 HashSet::from_iter(virs.iter().map(|vir| vir.year))
1274}
1275
1276fn load_emissions_data_for_given_years<P: AsRef<Path>>(
1277 data_dir_path: P,
1278 years: &HashSet<u32>,
1279) -> anyhow::Result<HashMap<u32, HashMap<u32, Vec<EmissionsInfoFE>>>> {
1280 let mut data = HashMap::<u32, HashMap<u32, Vec<EmissionsInfoFE>>>::new();
1281 for year in years {
1282 let file_name = format!("{year}-emissions.csv");
1283 let emissions_path = data_dir_path.as_ref().join(file_name);
1284 if !emissions_path.exists() {
1285 println!(
1287 "DATA DOES NOT EXIST AT {}",
1288 emissions_path.to_string_lossy()
1289 );
1290 }
1291 let emissions_db = {
1292 let emissions_file = File::open(emissions_path)?;
1293 read_fuelecon_gov_emissions_to_hashmap(emissions_file)
1294 };
1295 data.insert(*year, emissions_db);
1296 }
1297 Ok(data)
1298}
1299
1300fn load_fegov_data_for_given_years<P: AsRef<Path>>(
1301 data_dir_path: P,
1302 emissions_by_year_and_by_id: &HashMap<u32, HashMap<u32, Vec<EmissionsInfoFE>>>,
1303 years: &HashSet<u32>,
1304) -> anyhow::Result<HashMap<u32, Vec<VehicleDataFE>>> {
1305 let mut data = HashMap::<u32, Vec<VehicleDataFE>>::new();
1306 for year in years {
1307 if let Some(emissions_by_id) = emissions_by_year_and_by_id.get(year) {
1308 let file_name = format!("{year}-vehicles.csv");
1309 let fegov_path = data_dir_path.as_ref().join(file_name);
1310 let fegov_db = {
1311 let fegov_file = File::open(fegov_path.as_path())?;
1312 read_fuelecon_gov_data_from_file(fegov_file, emissions_by_id)?
1313 };
1314 data.insert(*year, fegov_db);
1315 } else {
1316 println!("No fe.gov emissions data available for {year}");
1317 }
1318 }
1319 Ok(data)
1320}
1321#[cfg_attr(feature = "pyo3", pyfunction)]
1322#[cfg_attr(feature = "pyo3", pyo3(signature = (
1323 year,
1324 make,
1325 model,
1326 other_inputs,
1327 cache_url=None,
1328 data_dir=None,
1329)))]
1330pub fn import_all_vehicles(
1332 year: u32,
1333 make: &str,
1334 model: &str,
1335 other_inputs: &OtherVehicleInputs,
1336 cache_url: Option<String>,
1337 data_dir: Option<String>,
1338) -> anyhow::Result<Vec<RustVehicle>> {
1339 let vir = VehicleInputRecord {
1340 year,
1341 make: make.to_string(),
1342 model: model.to_string(),
1343 output_file_name: String::from(""),
1344 vehicle_width_in: other_inputs.vehicle_width_in,
1345 vehicle_height_in: other_inputs.vehicle_height_in,
1346 fuel_tank_gal: other_inputs.fuel_tank_gal,
1347 ess_max_kwh: other_inputs.ess_max_kwh,
1348 mc_max_kw: other_inputs.mc_max_kw,
1349 ess_max_kw: other_inputs.ess_max_kw,
1350 fc_max_kw: other_inputs.fc_max_kw,
1351 };
1352 let inputs = vec![vir];
1353 let model_years = {
1354 let mut h = HashSet::new();
1355 h.insert(year);
1356 h
1357 };
1358 let data_dir_path = if let Some(dd_path) = data_dir {
1359 PathBuf::from(dd_path.clone())
1360 } else {
1361 create_project_subdir("fe_label_data")?
1362 };
1363 let data_dir_path = data_dir_path.as_path();
1364 let cache_url = if let Some(cache_url) = &cache_url {
1365 cache_url.clone()
1366 } else {
1367 get_default_cache_url()
1368 };
1369 populate_cache_for_given_years_if_needed(data_dir_path, &model_years, &cache_url)?;
1370 let emissions_data = load_emissions_data_for_given_years(data_dir_path, &model_years)?;
1371 let fegov_data_by_year =
1372 load_fegov_data_for_given_years(data_dir_path, &emissions_data, &model_years)?;
1373 let epatest_db = read_epa_test_data_for_given_years(data_dir_path, &model_years)?;
1374 let vehs = import_all_vehicles_from_record(&inputs, &fegov_data_by_year, &epatest_db)
1375 .into_iter()
1376 .map(|x| -> RustVehicle { x.1 })
1377 .collect();
1378 Ok(vehs)
1379}
1380
1381pub fn import_and_save_all_vehicles_from_file(
1383 input_path: &Path,
1384 data_dir_path: &Path,
1385 output_dir_path: &Path,
1386 cache_url: Option<String>,
1387) -> anyhow::Result<()> {
1388 let cache_url = cache_url.unwrap_or_else(get_default_cache_url);
1389 let inputs = read_vehicle_input_records_from_file(input_path)?;
1390 println!("Found {} vehicle input records", inputs.len());
1391 let model_years = determine_model_years_of_interest(&inputs);
1392 populate_cache_for_given_years_if_needed(data_dir_path, &model_years, &cache_url)?;
1393 let emissions_data = load_emissions_data_for_given_years(data_dir_path, &model_years)?;
1394 let fegov_data_by_year =
1395 load_fegov_data_for_given_years(data_dir_path, &emissions_data, &model_years)?;
1396 let epatest_db = read_epa_test_data_for_given_years(data_dir_path, &model_years)?;
1397 println!("Read {} files of epa test vehicle data", epatest_db.len());
1398 import_and_save_all_vehicles(&inputs, &fegov_data_by_year, &epatest_db, output_dir_path)
1399}
1400
1401pub fn import_all_vehicles_from_record(
1402 inputs: &[VehicleInputRecord],
1403 fegov_data_by_year: &HashMap<u32, Vec<VehicleDataFE>>,
1404 epatest_data_by_year: &HashMap<u32, Vec<VehicleDataEPA>>,
1405) -> Vec<(VehicleInputRecord, RustVehicle)> {
1406 let mut vehs = Vec::new();
1407 for vir in inputs {
1408 if let Some(fegov_data) = fegov_data_by_year.get(&vir.year) {
1409 if let Some(epatest_data) = epatest_data_by_year.get(&vir.year) {
1410 let vs = try_import_vehicles(vir, fegov_data, epatest_data);
1411 for v in vs.iter() {
1412 vehs.push((vir.clone(), v.clone()));
1413 }
1414 } else {
1415 println!("No EPA test data available for year {}", vir.year);
1416 }
1417 } else {
1418 println!("No FE.gov data available for year {}", vir.year);
1419 }
1420 }
1421 vehs
1422}
1423
1424pub fn import_and_save_all_vehicles(
1425 inputs: &[VehicleInputRecord],
1426 fegov_data_by_year: &HashMap<u32, Vec<VehicleDataFE>>,
1427 epatest_data_by_year: &HashMap<u32, Vec<VehicleDataEPA>>,
1428 output_dir_path: &Path,
1429) -> anyhow::Result<()> {
1430 for (idx, (vir, veh)) in
1431 import_all_vehicles_from_record(inputs, fegov_data_by_year, epatest_data_by_year)
1432 .iter()
1433 .enumerate()
1434 {
1435 let mut outfile = PathBuf::new();
1436 outfile.push(output_dir_path);
1437 if idx > 0 {
1438 let path = Path::new(&vir.output_file_name);
1439 let stem = path.file_stem().unwrap().to_str().unwrap();
1440 let ext = path.extension().unwrap().to_str().unwrap();
1441 let output_file_name = format!("{stem}-{idx}.{ext}");
1442 println!("Multiple configurations found: output_file_name = {output_file_name}");
1443 outfile.push(Path::new(&output_file_name));
1444 } else {
1445 outfile.push(Path::new(&vir.output_file_name));
1446 }
1447 if let Some(full_outfile) = outfile.to_str() {
1448 veh.to_file(full_outfile)?;
1449 } else {
1450 println!("Could not determine output file path");
1451 }
1452 }
1453 Ok(())
1454}
1455
1456fn get_cache_url_for_year(cache_url: &str, year: &u32) -> anyhow::Result<Option<String>> {
1457 let maybe_slash = if cache_url.ends_with('/') { "" } else { "/" };
1458 let target_url = format!("{cache_url}{maybe_slash}{year}.zip");
1459 Ok(Some(target_url))
1460}
1461
1462fn populate_cache_for_given_years_if_needed<P: AsRef<Path>>(
1466 data_dir_path: P,
1467 years: &HashSet<u32>,
1468 cache_url: &str,
1469) -> anyhow::Result<()> {
1470 let data_dir_path = data_dir_path.as_ref();
1471 let mut all_data_available = true;
1472 for year in years {
1473 let veh_file_exists = {
1474 let name = format!("{year}-vehicles.csv");
1475 let path = data_dir_path.join(name);
1476 path.exists()
1477 };
1478 let emissions_file_exists = {
1479 let name = format!("{year}-emissions.csv");
1480 let path = data_dir_path.join(name);
1481 path.exists()
1482 };
1483 let epa_file_exists = {
1484 let name = format!("{year}-testcar.csv");
1485 let path = data_dir_path.join(name);
1486 path.exists()
1487 };
1488 if !veh_file_exists || !emissions_file_exists || !epa_file_exists {
1489 all_data_available = false;
1490 let zip_file_name = format!("{year}.zip");
1491 let zip_file_path = data_dir_path.join(zip_file_name);
1492 if let Some(url) = get_cache_url_for_year(cache_url, year)? {
1493 println!("Downloading data for {year}: {url}");
1494 download_file_from_url(&url, &zip_file_path)?;
1495 println!("... downloading data for {year}");
1496 let emissions_name = format!("{year}-emissions.csv");
1497 extract_file_from_zip(
1498 zip_file_path.as_path(),
1499 &emissions_name,
1500 data_dir_path.join(&emissions_name).as_path(),
1501 )?;
1502 println!("... extracted {}", emissions_name);
1503 let vehicles_name = format!("{year}-vehicles.csv");
1504 extract_file_from_zip(
1505 zip_file_path.as_path(),
1506 &vehicles_name,
1507 data_dir_path.join(&vehicles_name).as_path(),
1508 )?;
1509 println!("... extracted {}", vehicles_name);
1510 let epatests_name = format!("{year}-testcar.csv");
1511 extract_file_from_zip(
1512 zip_file_path.as_path(),
1513 &epatests_name,
1514 data_dir_path.join(&epatests_name).as_path(),
1515 )?;
1516 println!("... extracted {}", epatests_name);
1517 all_data_available = true;
1518 }
1519 }
1520 }
1521 ensure!(
1522 all_data_available,
1523 "Unable to load or download cache data from {cache_url}"
1524 );
1525 Ok(())
1526}
1527
1528fn extract_file_from_zip(
1529 zip_file_path: &Path,
1530 name_of_file_to_extract: &str,
1531 path_to_save_to: &Path,
1532) -> anyhow::Result<()> {
1533 let zipfile = File::open(zip_file_path)?;
1534 let mut archive = ZipArchive::new(zipfile)?;
1535 let mut file = archive.by_name(name_of_file_to_extract)?;
1536 let mut contents = String::new();
1537 file.read_to_string(&mut contents)?;
1538 std::fs::write(path_to_save_to, contents)?;
1539 Ok(())
1540}
1541
1542#[cfg(test)]
1543mod tests {
1544 use super::*;
1545 use crate::vehicle_utils::NETWORK_TEST_DISABLE_ENV_VAR_NAME;
1546 use std::env;
1547
1548 #[test]
1549 fn test_create_new_vehicle_from_input_data() {
1550 let veh_record = VehicleInputRecord {
1551 make: String::from("Toyota"),
1552 model: String::from("Camry"),
1553 year: 2020,
1554 output_file_name: String::from("2020-toyota-camry.yaml"),
1555 vehicle_width_in: 72.4,
1556 vehicle_height_in: 56.9,
1557 fuel_tank_gal: 15.8,
1558 ess_max_kwh: 0.0,
1559 mc_max_kw: 0.0,
1560 ess_max_kw: 0.0,
1561 fc_max_kw: None,
1562 };
1563 let emiss_info = vec![
1564 EmissionsInfoFE {
1565 efid: String::from("LTYXV03.5M5B"),
1566 score: 5.0,
1567 smartway_score: -1,
1568 standard: String::from("L3ULEV70"),
1569 std_text: String::from("California LEV-III ULEV70"),
1570 },
1571 EmissionsInfoFE {
1572 efid: String::from("LTYXV03.5M5B"),
1573 score: 5.0,
1574 smartway_score: -1,
1575 standard: String::from("T3B70"),
1576 std_text: String::from("Federal Tier 3 Bin 70"),
1577 },
1578 ];
1579 let emiss_list = EmissionsListFE {
1580 emissions_info: emiss_info,
1581 };
1582 let fegov_data = VehicleDataFE {
1583 id: 32204,
1584
1585 year: 2020,
1586 make: String::from("Toyota"),
1587 model: String::from("Camry"),
1588
1589 veh_class: String::from("Midsize Cars"),
1590
1591 drive: String::from("Front-Wheel Drive"),
1592 alt_veh_type: String::from(""),
1593
1594 fuel_type: String::from("Regular"),
1595 fuel1: String::from("Regular Gasoline"),
1596 fuel2: String::from(""),
1597
1598 eng_dscr: String::from("SIDI & PFI"),
1599 cylinders: String::from("6"),
1600 displ: String::from("3.5"),
1601 transmission: String::from("Automatic (S8)"),
1602
1603 super_charger: String::from(""),
1604 turbo_charger: String::from(""),
1605
1606 start_stop: String::from("N"),
1607
1608 phev_blended: false,
1609 phev_city_mpge: 0,
1610 phev_comb_mpge: 0,
1611 phev_hwy_mpge: 0,
1612
1613 ev_motor_kw: String::from(""),
1614 range_ev: 0,
1615
1616 city_mpg_fuel1: 16.4596,
1617 city_mpg_fuel2: 0.0,
1618 unadj_city_mpg_fuel1: 20.2988,
1619 unadj_city_mpg_fuel2: 0.0,
1620 city_kwh_per_100mi: 0.0,
1621
1622 highway_mpg_fuel1: 22.5568,
1623 highway_mpg_fuel2: 0.0,
1624 unadj_highway_mpg_fuel1: 30.1798,
1625 unadj_highway_mpg_fuel2: 0.0,
1626 highway_kwh_per_100mi: 0.0,
1627
1628 comb_mpg_fuel1: 18.7389,
1629 comb_mpg_fuel2: 0.0,
1630 comb_kwh_per_100mi: 0.0,
1631
1632 emissions_list: emiss_list,
1633 };
1634 let epatest_data = VehicleDataEPA {
1635 index: 0,
1636 year: 2020,
1637 make: String::from("TOYOTA"),
1638 model: String::from("CAMRY"),
1639 test_id: String::from("JTYXV03.5M5B"),
1640 displ: 3.456,
1641 eng_pwr_hp: 301,
1642 cylinders: String::from("6"),
1643 transmission_code: String::from("A"),
1644 transmission_type: String::from("Automatic"),
1645 gears: 8,
1646 drive_code: String::from("F"),
1647 drive: String::from("2-Wheel Drive, Front"),
1648 test_weight_lbs: 3875.0,
1649 test_fuel_type: String::from("61"),
1650 a_lbf: 24.843,
1651 b_lbf_per_mph: 0.40298,
1652 c_lbf_per_mph2: 0.015068,
1653 };
1654 let other_inputs = vir_to_other_inputs(&veh_record);
1655 let v = try_make_single_vehicle(&fegov_data, &epatest_data, &other_inputs).unwrap();
1656 assert_eq!(v.scenario_name, String::from("2020 Toyota Camry"));
1657 assert_eq!(v.val_comb_mpgge, 18.7389);
1658 }
1659
1660 #[test]
1661 fn test_get_options_for_year_make_model() {
1662 if env::var(NETWORK_TEST_DISABLE_ENV_VAR_NAME).is_ok() {
1663 println!("SKIPPING: test_get_options_for_year_make_model");
1664 return;
1665 }
1666 let year = String::from("2020");
1667 let make = String::from("Toyota");
1668 let model = String::from("Corolla");
1669 let options = get_options_for_year_make_model(&year, &make, &model, None, None).unwrap();
1670 assert!(!options.is_empty());
1671 }
1672
1673 #[test]
1674 fn test_import_robustness() {
1675 if env::var(NETWORK_TEST_DISABLE_ENV_VAR_NAME).is_ok() {
1676 println!("SKIPPING: test_import_robustness");
1677 return;
1678 }
1679 let ddpath = create_project_subdir("fe_label_data").unwrap();
1681 let model_year = 2019;
1682 let years = {
1683 let mut s = HashSet::new();
1684 s.insert(model_year);
1685 s
1686 };
1687 let cache_url = get_default_cache_url();
1688 populate_cache_for_given_years_if_needed(ddpath.as_path(), &years, &cache_url).unwrap();
1689 let vehicles_path = ddpath.join("2019-vehicles.csv");
1691 let veh_records = {
1692 let file = File::open(vehicles_path);
1693 if let Ok(f) = file {
1694 let data_result: anyhow::Result<Vec<HashMap<String, String>>> =
1695 read_records_from_file(f);
1696 if let Ok(data) = data_result {
1697 data
1698 } else {
1699 vec![]
1700 }
1701 } else {
1702 vec![]
1703 }
1704 };
1705 let mut num_success = 0;
1706 let other_inputs = OtherVehicleInputs {
1707 vehicle_height_in: 72.4,
1708 vehicle_width_in: 56.9,
1709 fuel_tank_gal: 15.8,
1710 ess_max_kwh: 0.0,
1711 mc_max_kw: 0.0,
1712 ess_max_kw: 0.0,
1713 fc_max_kw: None,
1714 };
1715 let mut num_records = 0;
1716 let max_iter = veh_records.len();
1717 let skip_idx = 200;
1721 for (num_iter, vr) in veh_records.iter().enumerate() {
1722 if num_iter % skip_idx != 0 {
1723 continue;
1724 }
1725 if num_iter >= max_iter {
1726 break;
1727 }
1728 let make = vr.get("make");
1729 let model = vr.get("model");
1730 if let (Some(make), Some(model)) = (make, model) {
1731 let result =
1732 import_all_vehicles(model_year, make, model, &other_inputs, None, None);
1733 if let Ok(vehs) = &result {
1734 if !vehs.is_empty() {
1735 num_success += 1;
1736 }
1737 }
1738 } else {
1739 panic!("Unable to find make and model in vehicle record");
1740 }
1741 num_records += 1;
1742 }
1743 let success_frac = (num_success as f64) / (num_records as f64);
1744 assert!(success_frac > 0.90, "success_frac = {}", success_frac);
1745 }
1746
1747 #[test]
1748 fn test_get_options_for_year_make_model_for_specified_cacheurl_and_data_dir() {
1749 if env::var(NETWORK_TEST_DISABLE_ENV_VAR_NAME).is_ok() {
1750 println!("SKIPPING: test_get_options_for_year_make_model_for_specified_cacheurl_and_data_dir");
1751 return;
1752 }
1753 let year = String::from("2020");
1754 let make = String::from("Toyota");
1755 let model = String::from("Corolla");
1756 let temp_dir = tempfile::tempdir().unwrap();
1757 let data_dir = temp_dir.path();
1758 let cacheurl = get_default_cache_url();
1759 assert!(!get_options_for_year_make_model(
1760 &year,
1761 &make,
1762 &model,
1763 Some(cacheurl),
1764 Some(data_dir.to_str().unwrap().to_string()),
1765 )
1766 .unwrap()
1767 .is_empty());
1768 }
1769}