fastsim_core/
vehicle_import.rs

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)]
19/// Struct containing list of makes for a year from fueleconomy.gov
20struct VehicleMakesFE {
21    #[serde(rename = "menuItem")]
22    /// List of vehicle makes
23    makes: Vec<MakeFE>,
24}
25
26#[derive(Debug, Serialize, Deserialize, PartialEq)]
27/// Struct containing make information for a year fueleconomy.gov
28struct MakeFE {
29    #[serde(rename = "text")]
30    /// Transmission of vehicle
31    make_name: String,
32}
33
34#[derive(Debug, Serialize, Deserialize, PartialEq)]
35/// Struct containing list of models for a year and make from fueleconomy.gov
36struct VehicleModelsFE {
37    #[serde(rename = "menuItem")]
38    /// List of vehicle models
39    models: Vec<ModelFE>,
40}
41
42#[derive(Debug, Serialize, Deserialize, PartialEq)]
43/// Struct containing model information for a year and make from fueleconomy.gov
44struct ModelFE {
45    #[serde(rename = "text")]
46    /// Transmission of vehicle
47    model_name: String,
48}
49
50#[derive(Debug, Serialize, Deserialize, PartialEq)]
51/// Struct containing list of transmission options for vehicle from fueleconomy.gov
52struct VehicleOptionsFE {
53    #[serde(rename = "menuItem")]
54    /// List of vehicle options (transmission and id)
55    options: Vec<OptionFE>,
56}
57
58#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
59#[add_pyo3_api]
60/// Struct containing transmission and id of a vehicle option from fueleconomy.gov
61pub struct OptionFE {
62    #[serde(rename = "text")]
63    /// Transmission of vehicle
64    pub transmission: String,
65    #[serde(rename = "value")]
66    /// ID of vehicle on fueleconomy.gov
67    pub id: String,
68}
69
70impl SerdeAPI for OptionFE {}
71
72#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)]
73#[add_pyo3_api]
74/// Struct containing vehicle data from fueleconomy.gov
75pub struct VehicleDataFE {
76    /// Vehicle ID
77    pub id: i32,
78
79    /// Model year
80    pub year: u32,
81    /// Vehicle make
82    pub make: String,
83    /// Vehicle model
84    pub model: String,
85
86    /// EPA vehicle size class
87    #[serde(rename = "VClass")]
88    pub veh_class: String,
89
90    /// Drive axle type (FWD, RWD, AWD, 4WD)
91    pub drive: String,
92    /// Type of alternative fuel vehicle (Hybrid, Plug-in Hybrid, EV)
93    #[serde(default, rename = "atvType")]
94    pub alt_veh_type: String,
95
96    /// Combined vehicle fuel type (fuel 1 and fuel 2)
97    #[serde(rename = "fuelType")]
98    pub fuel_type: String,
99    /// Fuel type 1
100    #[serde(rename = "fuelType1")]
101    pub fuel1: String,
102    /// Fuel type 2
103    #[serde(default, rename = "fuelType2")]
104    pub fuel2: String,
105
106    /// Description of engine
107    #[serde(default)]
108    pub eng_dscr: String,
109    /// Number of engine cylinders
110    #[serde(default)]
111    pub cylinders: String,
112    /// Engine displacement in liters
113    #[serde(default)]
114    pub displ: String,
115    /// transmission
116    #[serde(rename = "trany")]
117    pub transmission: String,
118
119    /// "S" if vehicle has supercharger
120    #[serde(default, rename = "sCharger")]
121    pub super_charger: String,
122    /// "T" if vehicle has turbocharger
123    #[serde(default, rename = "tCharger")]
124    pub turbo_charger: String,
125
126    /// Stop-start technology
127    #[serde(rename = "startStop")]
128    pub start_stop: String,
129
130    /// Vehicle operates on blend of gasoline and electricity
131    #[serde(rename = "phevBlended")]
132    pub phev_blended: bool,
133    /// EPA composite gasoline-electricity city MPGe
134    #[serde(rename = "phevCity")]
135    pub phev_city_mpge: i32,
136    /// EPA composite gasoline-electricity combined MPGe
137    #[serde(rename = "phevComb")]
138    pub phev_comb_mpge: i32,
139    /// EPA composite gasoline-electricity highway MPGe
140    #[serde(rename = "phevHwy")]
141    pub phev_hwy_mpge: i32,
142
143    /// Electric motor power (kW), not very consistent as an input
144    #[serde(default, rename = "evMotor")]
145    pub ev_motor_kw: String,
146    /// EV range
147    #[serde(rename = "range")]
148    pub range_ev: i32,
149
150    /// City MPG for fuel 1
151    #[serde(rename = "city08U")]
152    pub city_mpg_fuel1: f64,
153    /// City MPG for fuel 2
154    #[serde(rename = "cityA08U")]
155    pub city_mpg_fuel2: f64,
156    /// Unadjusted unroaded city MPG for fuel 1
157    #[serde(rename = "UCity")]
158    pub unadj_city_mpg_fuel1: f64,
159    /// Unadjusted unroaded city MPG for fuel 2
160    #[serde(rename = "UCityA")]
161    pub unadj_city_mpg_fuel2: f64,
162    /// City electricity consumption in kWh/100 mi
163    #[serde(rename = "cityE")]
164    pub city_kwh_per_100mi: f64,
165
166    /// Adjusted unrounded highway MPG for fuel 1
167    #[serde(rename = "highway08U")]
168    pub highway_mpg_fuel1: f64,
169    /// Adjusted unrounded highway MPG for fuel 2
170    #[serde(rename = "highwayA08U")]
171    pub highway_mpg_fuel2: f64,
172    /// Unadjusted unrounded highway MPG for fuel 1
173    #[serde(default, rename = "UHighway")]
174    pub unadj_highway_mpg_fuel1: f64,
175    /// Unadjusted unrounded highway MPG for fuel 2
176    #[serde(default, rename = "UHighwayA")]
177    pub unadj_highway_mpg_fuel2: f64,
178    /// Highway electricity consumption in kWh/100 mi
179    #[serde(default, rename = "highwayE")]
180    pub highway_kwh_per_100mi: f64,
181
182    /// Combined MPG for fuel 1
183    #[serde(rename = "comb08U")]
184    pub comb_mpg_fuel1: f64,
185    /// Combined MPG for fuel 2
186    #[serde(rename = "combA08U")]
187    pub comb_mpg_fuel2: f64,
188    /// Combined electricity consumption in kWh/100 mi
189    #[serde(default, rename = "combE")]
190    pub comb_kwh_per_100mi: f64,
191
192    /// List of emissions tests
193    #[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]
202/// Struct containing list of emissions tests from fueleconomy.gov
203pub struct EmissionsListFE {
204    ///
205    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]
213/// Struct containing emissions test results from fueleconomy.gov
214pub struct EmissionsInfoFE {
215    /// Engine family id / EPA test group
216    pub efid: String,
217    /// EPA smog rating
218    pub score: f64,
219    /// SmartWay score
220    pub smartway_score: i32,
221    /// Vehicle emission standard code
222    pub standard: String,
223    /// Vehicle emission standard
224    pub std_text: String,
225}
226
227impl SerdeAPI for EmissionsInfoFE {}
228
229#[derive(Default, PartialEq, Clone, Debug, Deserialize, Serialize)]
230#[add_pyo3_api]
231/// Struct containing vehicle data from EPA database
232pub struct VehicleDataEPA {
233    /// Index
234    pub index: u32,
235    /// Model year
236    #[serde(rename = "Model Year")]
237    pub year: u32,
238    /// Vehicle make
239    #[serde(rename = "Represented Test Veh Make")]
240    pub make: String,
241    /// Vehicle model
242    #[serde(rename = "Represented Test Veh Model")]
243    pub model: String,
244    /// Vehicle test group
245    #[serde(rename = "Actual Tested Testgroup")]
246    pub test_id: String,
247    /// Engine displacement
248    #[serde(rename = "Test Veh Displacement (L)")]
249    pub displ: f64,
250    /// Engine power in hp
251    #[serde(rename = "Rated Horsepower")]
252    pub eng_pwr_hp: u32,
253    /// Number of cylinders
254    #[serde(rename = "# of Cylinders and Rotors")]
255    pub cylinders: String,
256    /// Transmission type code
257    #[serde(rename = "Tested Transmission Type Code")]
258    pub transmission_code: String,
259    /// Transmission type
260    #[serde(rename = "Tested Transmission Type")]
261    pub transmission_type: String,
262    /// Number of gears
263    #[serde(rename = "# of Gears")]
264    pub gears: u32,
265    /// Drive system code
266    #[serde(rename = "Drive System Code")]
267    pub drive_code: String,
268    /// Drive system type
269    #[serde(rename = "Drive System Description")]
270    pub drive: String,
271    /// Test weight in lbs
272    #[serde(rename = "Equivalent Test Weight (lbs.)")]
273    pub test_weight_lbs: f64,
274    /// Fuel type used for EPA test
275    #[serde(rename = "Test Fuel Type Description")]
276    pub test_fuel_type: String,
277    /// Dyno coefficient a in lbf
278    #[serde(rename = "Target Coef A (lbf)")]
279    pub a_lbf: f64,
280    /// Dyno coefficient b in lbf/mph
281    #[serde(rename = "Target Coef B (lbf/mph)")]
282    pub b_lbf_per_mph: f64,
283    /// Dyno coefficient c in lbf/mph^2
284    #[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)))]
298/// Gets options from fueleconomy.gov for the given vehicle year, make, and model
299///
300/// Arguments:
301/// ----------
302/// year: Vehicle year
303/// make: Vehicle make
304/// model: Vehicle model (must match model on fueleconomy.gov)
305///
306/// Returns:
307/// --------
308/// Vec<OptionFE>: Data for the available options for that vehicle year/make/model from fueleconomy.gov
309pub 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    // prep the cache for year
317    let y = year.trim().parse()?;
318    let ys = {
319        let mut h = HashSet::new();
320        h.insert(y);
321        h
322    };
323    // TODO: replace with unwrap_or_else
324    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    // prep the cache for year
360    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    // Based on reference: https://www.fueleconomy.gov/feg/findacarhelp.shtml#engine
390    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
433/// Match EPA Test Data with FuelEconomy.gov data and return best match
434/// The matching algorithm tries to find the best match in the EPA Test data for the given FuelEconomy.gov data
435/// The algorithm works as follows:
436/// - only EPA Test Data matching the year and make of the FuelEconomy.gov data will be considered
437/// - we try to match on both the efid/test id and also the model name
438/// - next, for each match, we calculate a score based on matching various powertrain aspects based on:
439///     - transmission type
440///     - number of gears in the transmission
441///     - drive type (all-wheel drive / 4-wheel drive, etc.)
442///     - (for non-EVs)
443///         - engine displacement
444///         - number of cylinders
445/// RETURNS: the EPA Test data with the best match on make and/or efid/test id. When multiple vehicles match
446///          the same make name/ efid/test-id, we return the one with the highest score
447fn 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                // Things we Don't Want to Match
478                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                // ID match
487                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                // By PT Type
506                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                // Drive Code
517                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                // Transmission Type and Num Gears
529                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/// Match EPA Test Data with FuelEconomy.gov data and return best match
566#[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    // Keep track of best match to fueleconomy.gov model name for all vehicles and vehicles with matching efid/test id
575    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        // Find matches between EPA vehicle model name and fe.gov vehicle model name
589        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        // Calculate composite match percentage
597        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        // Update overall hashmap with new entry
601        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        // Update efid hashmap if fe.gov efid matches EPA test id
615        // (for some reason first character in id is almost always different)
616        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    // Get EPA vehicle model that is best match to fe.gov vehicle
632    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    // Get number of gears and convert fe.gov transmission description to EPA transmission description
644    let num_gears_fe_gov: u32;
645    let transmission_fe_gov: String;
646    // Based on reference: https://www.fueleconomy.gov/feg/findacarhelp.shtml#engine
647    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    // Find EPA vehicle entry that matches fe.gov vehicle data
689    // If same vehicle model has multiple configurations, get most common configuration
690    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)))]
787/// Creates RustVehicle for the given vehicle using data from fueleconomy.gov and EPA databases
788/// The created RustVehicle is also written as a yaml file
789///
790/// Arguments:
791/// ----------
792/// vehicle_id: i32, Identifier at fueleconomy.gov for the desired vehicle
793/// year: u32, the year of the vehicle
794/// other_inputs: Other vehicle inputs required to create the vehicle
795///
796/// Returns:
797/// --------
798/// veh: RustVehicle for specificed vehicle
799pub 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    // TODO: replace with unwrap_or_else
808    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
868/// Try to make a single vehicle using the provided data sets.
869fn 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    // TODO: fix glider_kg calculation
989    // https://github.com/NREL/fastsim/pull/30#issuecomment-1841413126
990    //
991    // let glider_kg = (epa_data.test_weight_lbs / LBS_PER_KG)
992    //     - ref_veh.cargo_kg
993    //     - ref_veh.trans_kg
994    //     - ref_veh.comp_mass_multiplier
995    //         * ((fs_max_kw / ref_veh.fs_kwh_per_kg)
996    //             + (ref_veh.fc_base_kg + fc_max_kw / ref_veh.fc_kw_per_kg)
997    //             + (ref_veh.mc_pe_base_kg + mc_max_kw * ref_veh.mc_pe_kg_per_kw)
998    //             + (ref_veh.ess_base_kg + ess_max_kwh * ref_veh.ess_kg_per_kwh));
999    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        // glider_kg,
1007        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, // overridden
1016        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, // overridden
1022        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    // TODO: Aaron wanted custom scenario name option
1069    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
1119/// Transltate a VehicleInputRecord to OtherVehicleInputs
1120fn 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            // download from URL and cache
1286            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)))]
1330/// Import All Vehicles for the given Year, Make, and Model and supplied other inputs
1331pub 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
1381/// Import and Save All Vehicles Specified via Input File
1382pub 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
1462/// Checks the cache directory to see if data files have been downloaded
1463/// If so, moves on without any further action.
1464/// If not, downloads data by year from remote site if it exists
1465fn 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        // Ensure 2019 data is cached
1680        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        // Load all year/make/models for 2019
1690        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        // NOTE: below, we can use fewer records in the interest of time as this is a long test with all records
1718        // We skip because the vehicles at the beginning of the file tend to be more exotic and to not have
1719        // EPA test entries. Thus, they are a bad representation of the whole.
1720        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}