fastsim_core/
vehicle.rs

1//! Module containing vehicle struct and related functions.
2
3use crate::calibration::skewness_shift;
4// local
5use crate::imports::*;
6use crate::params::*;
7use crate::proc_macros::{add_pyo3_api, doc_field, ApproxEq};
8#[cfg(feature = "pyo3")]
9use crate::pyo3imports::*;
10
11#[cfg(feature = "validation")]
12use lazy_static::lazy_static;
13#[cfg(feature = "validation")]
14use regex::Regex;
15#[cfg(feature = "validation")]
16use validator::Validate;
17
18// veh_pt_type options
19pub const CONV: &str = "Conv";
20pub const HEV: &str = "HEV";
21pub const PHEV: &str = "PHEV";
22pub const BEV: &str = "BEV";
23pub const VEH_PT_TYPES: [&str; 4] = [CONV, HEV, PHEV, BEV];
24#[cfg(feature = "validation")]
25lazy_static! {
26    static ref VEH_PT_TYPE_OPTIONS_REGEX: Regex = Regex::new("Conv|HEV|PHEV|BEV").unwrap();
27}
28
29// fc_eff_type options
30pub const SI: &str = "SI";
31pub const ATKINSON: &str = "Atkinson";
32pub const DIESEL: &str = "Diesel";
33pub const H2FC: &str = "H2FC";
34pub const HD_DIESEL: &str = "HD_Diesel";
35pub const FC_EFF_TYPES: [&str; 5] = [SI, ATKINSON, DIESEL, H2FC, HD_DIESEL];
36#[cfg(feature = "validation")]
37lazy_static! {
38    static ref FC_EFF_TYPE_OPTIONS_REGEX: Regex =
39        Regex::new("SI|Atkinson|Diesel|H2FC|HD_Diesel").unwrap();
40}
41
42#[doc_field]
43#[add_pyo3_api(
44    #[pyo3(name = "set_veh_mass")]
45    pub fn set_veh_mass_py(&mut self) {
46        // TODO: not urgent, but I think it'd better for all instances
47        // of `set_veh_mass` to be `update_veh_mass`
48        self.set_veh_mass()
49    }
50
51    #[getter]
52    pub fn get_mc_peak_eff(&self) -> f64 {
53        self.mc_peak_eff()
54    }
55
56    #[setter("mc_peak_eff")]
57    pub fn set_mc_peak_eff_py(&mut self, new_peak: f64) {
58        self.set_mc_peak_eff(new_peak);
59    }
60
61    #[getter]
62    pub fn get_mc_eff_range_py(&self) -> anyhow::Result<f64> {
63        self.get_mc_eff_range()
64    }
65
66    #[setter("mc_eff_range")]
67    pub fn set_mc_eff_range_py(&mut self, new_range: f64) -> anyhow::Result<()> {
68        self.set_mc_eff_range(new_range)
69    }
70
71    #[getter]
72    pub fn get_fc_eff_range_py(&self) -> anyhow::Result<f64> {
73        self.get_fc_eff_range()
74    }
75
76    #[setter("fc_eff_range")]
77    pub fn set_fc_eff_range_py(&mut self, new_range: f64) -> anyhow::Result<()> {
78        self.set_fc_eff_range(new_range)
79    }
80
81    #[getter]
82    pub fn get_max_fc_eff_kw(&self) -> f64 {
83        self.max_fc_eff_kw()
84    }
85
86    #[setter("fc_peak_eff")]
87    pub fn set_fc_peak_eff_py(&mut self, new_peak: f64) {
88        self.set_fc_peak_eff(new_peak);
89    }
90
91    #[getter]
92    pub fn get_fc_peak_eff(&self) -> f64 {
93        self.fc_peak_eff()
94    }
95
96    #[pyo3(name = "set_derived")]
97    pub fn set_derived_py(&mut self) {
98        self.set_derived().unwrap()
99    }
100
101    /// An identify function to allow RustVehicle to be used as a python vehicle and respond to this method
102    /// Returns a clone of the current object
103    pub fn to_rust(&self) -> Self {
104        self.clone()
105    }
106
107    #[pyo3(name = "list_resources")]
108    /// list available vehicle resources
109    pub fn list_resources_py(&self) -> Vec<String> {
110        RustVehicle::list_resources()
111    }
112
113    #[staticmethod]
114    #[pyo3(name = "mock_vehicle")]
115    fn mock_vehicle_py() -> Self {
116        Self::mock_vehicle()
117    }
118
119    #[setter("mc_eff_peak_pwr")]
120    pub fn set_mc_eff_peak_pwr_py<'py>(
121        &mut self,
122        new_peak_x: f64,
123    ) -> anyhow::Result<()> {
124        self.set_mc_eff_peak_pwr(new_peak_x)
125    }
126
127    #[setter("fc_eff_peak_pwr")]
128    pub fn set_fc_eff_peak_pwr_py<'py>(
129        &mut self,
130        new_peak_x: f64,
131    ) -> anyhow::Result<()> {
132        self.set_fc_eff_peak_pwr(new_peak_x)
133    }
134)]
135#[cfg_attr(feature = "validation", derive(Validate))]
136#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, ApproxEq)]
137/// Struct containing vehicle attributes
138/// # Python Examples
139/// ```python
140/// import fastsim
141///
142/// ## Load drive cycle by name
143/// cyc_py = fastsim.cycle.Cycle.from_file("udds")
144/// cyc_rust = cyc_py.to_rust()
145/// ```
146pub struct RustVehicle {
147    #[serde(skip)]
148    #[api(has_orphaned)]
149    /// Physical properties, see [RustPhysicalProperties](RustPhysicalProperties)
150    #[doc_field(skip_doc)]
151    pub props: RustPhysicalProperties,
152    /// Vehicle name
153    #[serde(alias = "name")]
154    #[doc_field(skip_doc)]
155    pub scenario_name: String,
156    /// Vehicle database ID
157    #[serde(skip)]
158    #[doc_field(skip_doc)]
159    pub selection: u32,
160    /// Vehicle year
161    #[serde(alias = "vehModelYear")]
162    #[doc_field(skip_doc)]
163    pub veh_year: u32,
164    /// Vehicle powertrain type, one of \[[CONV](CONV), [HEV](HEV), [PHEV](PHEV), [BEV](BEV)\]
165    #[serde(alias = "vehPtType")]
166    #[cfg_attr(
167        feature = "validation",
168        validate(regex(
169            path = "VEH_PT_TYPE_OPTIONS_REGEX",
170            message = "must be one of [\"Conv\", \"HEV\", \"PHEV\", \"BEV\"]"
171        ))
172    )]
173    #[doc_field(skip_doc)]
174    pub veh_pt_type: String,
175    /// Aerodynamic drag coefficient
176    #[serde(alias = "dragCoef")]
177    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
178    pub drag_coef: f64,
179    /// Frontal area, $m^2$
180    #[serde(alias = "frontalAreaM2")]
181    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
182    pub frontal_area_m2: f64,
183    /// Vehicle mass excluding cargo, passengers, and powertrain components, $kg$
184    #[serde(alias = "gliderKg")]
185    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
186    pub glider_kg: f64,
187    /// Vehicle center of mass height, $m$
188    /// **NOTE:** positive for FWD, negative for RWD, AWD, 4WD
189    #[serde(alias = "vehCgM")]
190    pub veh_cg_m: f64,
191    /// Fraction of weight on the drive axle while stopped
192    #[serde(alias = "driveAxleWeightFrac")]
193    #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))]
194    pub drive_axle_weight_frac: f64,
195    /// Wheelbase, $m$
196    #[serde(alias = "wheelBaseM")]
197    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
198    pub wheel_base_m: f64,
199    /// Cargo mass including passengers, $kg$
200    #[serde(alias = "cargoKg")]
201    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
202    pub cargo_kg: f64,
203    /// Total vehicle mass, overrides mass calculation, $kg$
204    #[serde(alias = "vehOverrideKg")]
205    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
206    pub veh_override_kg: Option<f64>,
207    /// Component mass multiplier for vehicle mass calculation
208    #[serde(alias = "compMassMultiplier")]
209    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
210    pub comp_mass_multiplier: f64,
211    /// Fuel storage max power output, $kW$
212    #[serde(alias = "maxFuelStorKw")]
213    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
214    pub fs_max_kw: f64,
215    /// Fuel storage time to peak power, $s$
216    #[serde(alias = "fuelStorSecsToPeakPwr")]
217    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
218    pub fs_secs_to_peak_pwr: f64,
219    /// Fuel storage energy capacity, $kWh$
220    #[serde(alias = "fuelStorKwh")]
221    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
222    pub fs_kwh: f64,
223    /// Fuel specific energy, $\frac{kWh}{kg}$
224    #[serde(alias = "fuelStorKwhPerKg")]
225    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
226    pub fs_kwh_per_kg: f64,
227    /// Fuel converter peak continuous power, $kW$
228    #[serde(alias = "maxFuelConvKw")]
229    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
230    pub fc_max_kw: f64,
231    /// Fuel converter output power percentage map, x values of [fc_eff_map](RustVehicle::fc_eff_map)
232    #[serde(alias = "fcPwrOutPerc")]
233    pub fc_pwr_out_perc: Array1<f64>,
234    /// Fuel converter efficiency map
235    #[serde(default)]
236    pub fc_eff_map: Array1<f64>,
237    /// Fuel converter efficiency type, one of \[[SI](SI), [ATKINSON](ATKINSON), [DIESEL](DIESEL), [H2FC](H2FC), [HD_DIESEL](HD_DIESEL)\]
238    /// Used for calculating [fc_eff_map](RustVehicle::fc_eff_map), and other calculations if H2FC
239    #[serde(alias = "fcEffType")]
240    #[cfg_attr(
241        feature = "validation",
242        validate(regex(
243            path = "FC_EFF_TYPE_OPTIONS_REGEX",
244            message = "must be one of [\"SI\", \"Atkinson\", \"Diesel\", \"H2FC\", \"HD_Diesel\"]"
245        ))
246    )]
247    pub fc_eff_type: String,
248    /// Fuel converter time to peak power, $s$
249    #[serde(alias = "fuelConvSecsToPeakPwr")]
250    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
251    pub fc_sec_to_peak_pwr: f64,
252    /// Fuel converter base mass, $kg$
253    #[serde(alias = "fuelConvBaseKg")]
254    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
255    pub fc_base_kg: f64,
256    /// Fuel converter specific power (power-to-weight ratio), $\frac{kW}{kg}$
257    #[serde(alias = "fuelConvKwPerKg")]
258    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
259    pub fc_kw_per_kg: f64,
260    /// Minimum time fuel converter must be on before shutoff (for HEV, PHEV)
261    #[serde(alias = "minFcTimeOn")]
262    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
263    pub min_fc_time_on: f64,
264    /// Fuel converter idle power, $kW$
265    #[serde(alias = "idleFcKw")]
266    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
267    pub idle_fc_kw: f64,
268    /// Peak continuous electric motor power, $kW$
269    #[serde(alias = "mcMaxElecInKw")]
270    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
271    pub mc_max_kw: f64,
272    /// Electric motor output power percentage map, x values of [mc_eff_map](RustVehicle::mc_eff_map)
273    #[serde(alias = "mcPwrOutPerc")]
274    pub mc_pwr_out_perc: Array1<f64>,
275    /// Electric motor efficiency map
276    #[serde(alias = "mcEffArray")]
277    pub mc_eff_map: Array1<f64>,
278    /// Electric motor time to peak power, $s$
279    #[serde(alias = "motorSecsToPeakPwr")]
280    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
281    pub mc_sec_to_peak_pwr: f64,
282    /// Motor power electronics mass per power output, $\frac{kg}{kW}$
283    #[serde(alias = "mcPeKgPerKw")]
284    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
285    pub mc_pe_kg_per_kw: f64,
286    /// Motor power electronics base mass, $kg$
287    #[serde(alias = "mcPeBaseKg")]
288    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
289    pub mc_pe_base_kg: f64,
290    /// Traction battery maximum power output, $kW$
291    #[serde(alias = "maxEssKw")]
292    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
293    pub ess_max_kw: f64,
294    /// Traction battery energy capacity, $kWh$
295    #[serde(alias = "maxEssKwh")]
296    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
297    pub ess_max_kwh: f64,
298    /// Traction battery mass per energy, $\frac{kg}{kWh}$
299    #[serde(alias = "essKgPerKwh")]
300    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
301    pub ess_kg_per_kwh: f64,
302    /// Traction battery base mass, $kg$
303    #[serde(alias = "essBaseKg")]
304    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
305    pub ess_base_kg: f64,
306    /// Traction battery round-trip efficiency
307    #[serde(alias = "essRoundTripEff")]
308    #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))]
309    pub ess_round_trip_eff: f64,
310    /// Traction battery cycle life coefficient A, see [reference](https://web.archive.org/web/20090529194442/http://www.ocean.udel.edu/cms/wkempton/Kempton-V2G-pdfFiles/PDF%20format/Duvall-V2G-batteries-June05.pdf)
311    #[serde(alias = "essLifeCoefA")]
312    pub ess_life_coef_a: f64,
313    /// Traction battery cycle life coefficient B, see [reference](https://web.archive.org/web/20090529194442/http://www.ocean.udel.edu/cms/wkempton/Kempton-V2G-pdfFiles/PDF%20format/Duvall-V2G-batteries-June05.pdf)
314    #[serde(alias = "essLifeCoefB")]
315    pub ess_life_coef_b: f64,
316    /// Traction battery minimum state of charge
317    #[serde(alias = "minSoc")]
318    #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))]
319    pub min_soc: f64,
320    /// Traction battery maximum state of charge
321    #[serde(alias = "maxSoc")]
322    #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))]
323    pub max_soc: f64,
324    /// ESS discharge effort toward max FC efficiency
325    #[serde(alias = "essDischgToFcMaxEffPerc")]
326    #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))]
327    pub ess_dischg_to_fc_max_eff_perc: f64,
328    /// ESS charge effort toward max FC efficiency
329    #[serde(alias = "essChgToFcMaxEffPerc")]
330    #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))]
331    pub ess_chg_to_fc_max_eff_perc: f64,
332    /// Mass moment of inertia per wheel, $kg \cdot m^2$
333    #[serde(alias = "wheelInertiaKgM2")]
334    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
335    pub wheel_inertia_kg_m2: f64,
336    /// Number of wheels
337    #[serde(alias = "numWheels")]
338    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
339    pub num_wheels: f64, // TODO: Shouldn't this just be a unsigned integer? u8 would work fine.
340    /// Rolling resistance coefficient
341    #[serde(alias = "wheelRrCoef")]
342    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
343    pub wheel_rr_coef: f64,
344    /// Wheel radius, $m$
345    #[serde(alias = "wheelRadiusM")]
346    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
347    pub wheel_radius_m: f64,
348    /// Wheel coefficient of friction
349    #[serde(alias = "wheelCoefOfFric")]
350    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
351    pub wheel_coef_of_fric: f64,
352    /// Speed where the battery reserved for accelerating is zero
353    #[serde(alias = "maxAccelBufferMph")]
354    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
355    pub max_accel_buffer_mph: f64,
356    /// Percent of usable battery energy reserved to help accelerate
357    #[serde(alias = "maxAccelBufferPercOfUseableSoc")]
358    #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))]
359    pub max_accel_buffer_perc_of_useable_soc: f64,
360    /// Percent SOC buffer for high accessory loads during cycles with long idle time
361    #[serde(alias = "percHighAccBuf")]
362    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
363    pub perc_high_acc_buf: f64,
364    /// Speed at which the fuel converter must turn on, $mph$
365    #[serde(alias = "mphFcOn")]
366    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
367    pub mph_fc_on: f64,
368    /// Power demand above which to require fuel converter on, $kW$
369    #[serde(alias = "kwDemandFcOn")]
370    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
371    pub kw_demand_fc_on: f64,
372    /// Maximum brake regeneration efficiency
373    #[serde(alias = "maxRegen")]
374    #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))]
375    pub max_regen: f64,
376    /// Stop/start micro-HEV flag
377    pub stop_start: bool,
378    /// Force auxiliary power load to come from fuel converter
379    #[serde(alias = "forceAuxOnFC")]
380    pub force_aux_on_fc: bool,
381    /// Alternator efficiency
382    #[serde(alias = "altEff")]
383    #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))]
384    pub alt_eff: f64,
385    /// Charger efficiency
386    #[serde(alias = "chgEff")]
387    #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))]
388    pub chg_eff: f64,
389    /// Auxiliary load power, $kW$
390    #[serde(alias = "auxKw")]
391    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
392    pub aux_kw: f64,
393    /// Transmission mass, $kg$
394    #[serde(alias = "transKg")]
395    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
396    pub trans_kg: f64,
397    /// Transmission efficiency
398    #[serde(alias = "transEff")]
399    #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))]
400    pub trans_eff: f64,
401    /// Maximum acceptable ratio of change in ESS energy to expended fuel energy (used in hybrid SOC balancing), $\frac{\Delta E_{ESS}}{\Delta E_{fuel}}$
402    #[serde(alias = "essToFuelOkError")]
403    #[cfg_attr(feature = "validation", validate(range(min = 0)))]
404    pub ess_to_fuel_ok_error: f64,
405    #[doc(hidden)]
406    #[doc_field(skip_doc)]
407    #[serde(skip)]
408    pub small_motor_power_kw: f64,
409    #[doc(hidden)]
410    #[doc_field(skip_doc)]
411    #[serde(skip)]
412    pub large_motor_power_kw: f64,
413    // this and other fixed-size arrays can probably be vectors
414    // without any performance penalty with the current implementation
415    // of the functions in utils.rs
416    #[doc(hidden)]
417    #[doc_field(skip_doc)]
418    #[serde(skip)]
419    pub fc_perc_out_array: Vec<f64>,
420    #[doc(hidden)]
421    #[doc_field(skip_doc)]
422    #[serde(default = "RustVehicle::default_regen_a")]
423    pub regen_a: f64,
424    #[doc(hidden)]
425    #[doc_field(skip_doc)]
426    #[serde(default = "RustVehicle::default_regen_b")]
427    pub regen_b: f64,
428    #[doc(hidden)]
429    #[doc_field(skip_doc)]
430    #[serde(skip)]
431    pub charging_on: bool,
432    #[doc(hidden)]
433    #[doc_field(skip_doc)]
434    #[serde(skip)]
435    pub no_elec_sys: bool,
436    #[doc(hidden)]
437    #[doc_field(skip_doc)]
438    // all of the parameters that are set in `set_derived` should be skipped by serde
439    #[serde(skip)]
440    pub no_elec_aux: bool,
441    #[doc(hidden)]
442    #[doc_field(skip_doc)]
443    #[serde(skip)]
444    pub max_roadway_chg_kw: Array1<f64>,
445    #[doc(hidden)]
446    #[doc_field(skip_doc)]
447    #[serde(skip)]
448    pub input_kw_out_array: Array1<f64>,
449    #[doc(hidden)]
450    #[doc_field(skip_doc)]
451    #[serde(skip)]
452    pub fc_kw_out_array: Vec<f64>,
453    #[doc(hidden)]
454    #[doc_field(skip_doc)]
455    #[serde(default)]
456    #[serde(alias = "fcEffArray", skip_serializing)]
457    pub fc_eff_array: Vec<f64>,
458    #[doc(hidden)]
459    #[doc_field(skip_doc)]
460    #[serde(skip)]
461    pub modern_max: f64,
462    #[doc(hidden)]
463    #[doc_field(skip_doc)]
464    #[serde(skip)]
465    pub mc_eff_array: Array1<f64>,
466    #[doc(hidden)]
467    #[doc_field(skip_doc)]
468    #[serde(skip)]
469    pub mc_kw_in_array: Vec<f64>,
470    #[doc(hidden)]
471    #[doc_field(skip_doc)]
472    #[serde(skip)]
473    pub mc_kw_out_array: Vec<f64>,
474    #[doc(hidden)]
475    #[doc_field(skip_doc)]
476    #[serde(skip)]
477    pub mc_max_elec_in_kw: f64,
478    #[doc(hidden)]
479    #[doc_field(skip_doc)]
480    #[serde(skip)]
481    pub mc_full_eff_array: Vec<f64>,
482    #[doc(hidden)]
483    #[doc_field(skip_doc)]
484    #[serde(skip)]
485    pub veh_kg: f64,
486    #[doc(hidden)]
487    #[doc_field(skip_doc)]
488    #[serde(skip)]
489    pub max_trac_mps2: f64,
490    #[doc(hidden)]
491    #[doc_field(skip_doc)]
492    #[serde(skip)]
493    pub ess_mass_kg: f64,
494    #[doc(hidden)]
495    #[doc_field(skip_doc)]
496    #[serde(skip)]
497    pub mc_mass_kg: f64,
498    #[doc(hidden)]
499    #[doc_field(skip_doc)]
500    #[serde(skip)]
501    pub fc_mass_kg: f64,
502    #[doc(hidden)]
503    #[doc_field(skip_doc)]
504    #[serde(skip)]
505    pub fs_mass_kg: f64,
506    #[doc(hidden)]
507    #[doc_field(skip_doc)]
508    #[serde(skip)]
509    pub mc_perc_out_array: Vec<f64>,
510    // these probably don't need to be in rust
511    #[doc(hidden)]
512    #[doc_field(skip_doc)]
513    #[serde(skip)]
514    pub val_udds_mpgge: f64,
515    #[doc(hidden)]
516    #[doc_field(skip_doc)]
517    #[serde(skip)]
518    pub val_hwy_mpgge: f64,
519    #[doc(hidden)]
520    #[doc_field(skip_doc)]
521    #[serde(skip)]
522    pub val_comb_mpgge: f64,
523    #[doc(hidden)]
524    #[doc_field(skip_doc)]
525    #[serde(skip)]
526    pub val_udds_kwh_per_mile: f64,
527    #[doc(hidden)]
528    #[doc_field(skip_doc)]
529    #[serde(skip)]
530    pub val_hwy_kwh_per_mile: f64,
531    #[doc(hidden)]
532    #[doc_field(skip_doc)]
533    #[serde(skip)]
534    pub val_comb_kwh_per_mile: f64,
535    #[doc(hidden)]
536    #[doc_field(skip_doc)]
537    #[serde(skip)]
538    pub val_cd_range_mi: f64,
539    #[doc(hidden)]
540    #[doc_field(skip_doc)]
541    #[serde(skip)]
542    pub val_const65_mph_kwh_per_mile: f64,
543    #[doc(hidden)]
544    #[doc_field(skip_doc)]
545    #[serde(skip)]
546    pub val_const60_mph_kwh_per_mile: f64,
547    #[doc(hidden)]
548    #[doc_field(skip_doc)]
549    #[serde(skip)]
550    pub val_const55_mph_kwh_per_mile: f64,
551    #[doc(hidden)]
552    #[doc_field(skip_doc)]
553    #[serde(skip)]
554    pub val_const45_mph_kwh_per_mile: f64,
555    #[doc(hidden)]
556    #[doc_field(skip_doc)]
557    #[serde(skip)]
558    pub val_unadj_udds_kwh_per_mile: f64,
559    #[doc(hidden)]
560    #[doc_field(skip_doc)]
561    #[serde(skip)]
562    pub val_unadj_hwy_kwh_per_mile: f64,
563    #[doc(hidden)]
564    #[doc_field(skip_doc)]
565    #[serde(skip)]
566    pub val0_to60_mph: f64,
567    #[doc(hidden)]
568    #[doc_field(skip_doc)]
569    #[serde(skip)]
570    pub val_ess_life_miles: f64,
571    #[doc(hidden)]
572    #[doc_field(skip_doc)]
573    #[serde(skip)]
574    pub val_range_miles: f64,
575    #[doc(hidden)]
576    #[doc_field(skip_doc)]
577    #[serde(skip)]
578    pub val_veh_base_cost: f64,
579    #[doc(hidden)]
580    #[doc_field(skip_doc)]
581    #[serde(skip)]
582    pub val_msrp: f64,
583    /// Fuel converter efficiency peak override, scales entire curve
584    #[serde(skip)]
585    #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))]
586    pub fc_peak_eff_override: Option<f64>,
587    /// Motor efficiency peak override, scales entire curve
588    #[serde(skip)]
589    #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))]
590    pub mc_peak_eff_override: Option<f64>,
591    #[serde(skip)]
592    #[doc(hidden)]
593    #[doc_field(skip_doc)]
594    pub orphaned: bool,
595    #[serde(skip)]
596    #[doc(hidden)]
597    #[doc_field(skip_doc)]
598    pub max_regen_kwh: f64,
599}
600
601/// RustVehicle rust methods
602impl RustVehicle {
603    const VEHICLE_DIRECTORY_URL: &'static str =
604        "https://raw.githubusercontent.com/NREL/fastsim-vehicles/main/";
605    /// Sets the following parameters:
606    /// - `ess_mass_kg`
607    /// - `mc_mass_kg`
608    /// - `fc_mass_kg`
609    /// - `fs_mass_kg`
610    /// - `veh_kg`
611    /// - `max_trac_mps2`
612    #[allow(clippy::neg_cmp_op_on_partial_ord)]
613    pub fn set_veh_mass(&mut self) {
614        if self.veh_override_kg.is_none() {
615            self.ess_mass_kg = if self.ess_max_kwh == 0.0 || self.ess_max_kw == 0.0 {
616                0.0
617            } else {
618                ((self.ess_max_kwh * self.ess_kg_per_kwh) + self.ess_base_kg)
619                    * self.comp_mass_multiplier
620            };
621            self.mc_mass_kg = if self.mc_max_kw == 0.0 {
622                0.0
623            } else {
624                (self.mc_pe_base_kg + (self.mc_pe_kg_per_kw * self.mc_max_kw))
625                    * self.comp_mass_multiplier
626            };
627            self.fc_mass_kg = if self.fc_max_kw == 0.0 {
628                0.0
629            } else {
630                (1.0 / self.fc_kw_per_kg * self.fc_max_kw + self.fc_base_kg)
631                    * self.comp_mass_multiplier
632            };
633            self.fs_mass_kg = if self.fs_max_kw == 0.0 {
634                0.0
635            } else {
636                ((1.0 / self.fs_kwh_per_kg) * self.fs_kwh) * self.comp_mass_multiplier
637            };
638            self.veh_kg = self.cargo_kg
639                + self.glider_kg
640                + self.trans_kg * self.comp_mass_multiplier
641                + self.ess_mass_kg
642                + self.mc_mass_kg
643                + self.fc_mass_kg
644                + self.fs_mass_kg;
645        } else {
646            // if positive real number is specified for veh_override_kg, use that
647            self.veh_kg = self.veh_override_kg.unwrap();
648        }
649
650        self.max_trac_mps2 = (self.wheel_coef_of_fric
651            * self.drive_axle_weight_frac
652            * self.veh_kg
653            * self.props.a_grav_mps2
654            / (1.0 + self.veh_cg_m * self.wheel_coef_of_fric / self.wheel_base_m))
655            / (self.veh_kg * self.props.a_grav_mps2)
656            * self.props.a_grav_mps2;
657    }
658
659    const fn default_regen_a() -> f64 {
660        500.0
661    }
662    const fn default_regen_b() -> f64 {
663        0.99
664    }
665
666    pub fn mc_peak_eff(&self) -> f64 {
667        arrmax(&self.mc_full_eff_array)
668    }
669
670    /// Returns _first_ FC output power at which peak efficiency occurs
671    pub fn max_fc_eff_kw(&self) -> f64 {
672        let fc_eff_arr_max_i =
673            first_eq(&self.fc_eff_array, arrmax(&self.fc_eff_array)).unwrap_or(0);
674        self.fc_kw_out_array[fc_eff_arr_max_i]
675    }
676
677    pub fn fc_peak_eff(&self) -> f64 {
678        arrmax(&self.fc_eff_array)
679    }
680
681    pub fn set_mc_peak_eff(&mut self, new_peak: f64) {
682        let mc_max_eff = self.mc_eff_array.max().unwrap().clone();
683        self.mc_eff_array *= new_peak / mc_max_eff;
684        self.mc_eff_map *= new_peak / mc_max_eff;
685        let mc_max_full_eff = arrmax(&self.mc_full_eff_array);
686        self.mc_full_eff_array = self
687            .mc_full_eff_array
688            .iter()
689            .map(|e: &f64| -> f64 { e * (new_peak / mc_max_full_eff) })
690            .collect();
691    }
692
693    /// Gets the minimum value of mc_eff_array
694    pub fn get_mc_eff_min(&self) -> anyhow::Result<&f64> {
695        self.mc_eff_array.min()
696    }
697
698    /// Gets the max value of mc_eff_array
699    pub fn get_mc_eff_max(&self) -> anyhow::Result<&f64> {
700        self.mc_eff_array.max()
701    }
702
703    /// Gets the range of mc_eff_array
704    pub fn get_mc_eff_range(&self) -> anyhow::Result<f64> {
705        Ok(self.get_mc_eff_max()? - self.get_mc_eff_min()?)
706    }
707
708    /// Changes the range (max value - min value) of mc_eff_map and mc_eff_array
709    /// # Arguments  
710    /// - new_range: new range for the mc_eff_map and mc_eff_array
711    pub fn set_mc_eff_range(&mut self, new_range: f64) -> anyhow::Result<()> {
712        let mc_eff_max = *self.get_mc_eff_max()?;
713        if new_range == 0.0 {
714            self.mc_eff_map = Array::zeros(self.mc_eff_map.len()) + mc_eff_max;
715            self.mc_eff_array = Array::zeros(self.mc_eff_array.len()) + mc_eff_max;
716            Ok(())
717        } else if (0.0..=1.0).contains(&new_range) {
718            let old_range = self.get_mc_eff_range()?;
719            self.mc_eff_map = mc_eff_max + (&self.mc_eff_map - mc_eff_max) * new_range / old_range;
720            if self.get_mc_eff_min()? < &0.0 {
721                bail!(
722                    "`mc_eff_min` ({:.3}) must not be negative",
723                    self.get_mc_eff_min()?
724                )
725            }
726            ensure!(
727                self.get_mc_eff_max()? <= &1.0,
728                format!(
729                    "{}\n`mc_eff_max` ({:.3}) must be no greater than 1.0",
730                    format_dbg!(self.get_mc_eff_max()? <= &1.0),
731                    self.get_mc_eff_max()?
732                )
733            );
734            self.mc_eff_array = self.mc_eff_map.clone();
735            Ok(())
736        } else {
737            bail!("`new_range` ({:.3}) must be between 0.0 and 1.0", new_range)
738        }
739    }
740
741    /// Gets the minimum value of fc_eff_array
742    pub fn get_fc_eff_min(&self) -> anyhow::Result<f64> {
743        Ok(self.fc_eff_array.iter().copied().fold(f64::NAN, f64::min))
744    }
745
746    /// Gets the max value of fc_eff_array
747    pub fn get_fc_eff_max(&self) -> anyhow::Result<f64> {
748        Ok(self.fc_eff_array.iter().copied().fold(f64::NAN, f64::max))
749    }
750
751    /// Gets the range of fc_eff_array
752    pub fn get_fc_eff_range(&self) -> anyhow::Result<f64> {
753        Ok(self.get_fc_eff_max()? - self.get_fc_eff_min()?)
754    }
755
756    /// Changes the range (max value - min value) of fc_eff_map and fc_eff_array
757    /// # Arguments  
758    /// - new_range: new range for the fc_eff_map and fc_eff_array
759    pub fn set_fc_eff_range(&mut self, new_range: f64) -> anyhow::Result<()> {
760        let fc_eff_max = self.get_fc_eff_max()?;
761        if new_range == 0.0 {
762            self.fc_eff_map = Array::zeros(self.fc_eff_map.len()) + fc_eff_max;
763            self.fc_eff_array = (Array::zeros(self.fc_eff_array.len()) + fc_eff_max).to_vec();
764            Ok(())
765        } else if (0.0..=1.0).contains(&new_range) {
766            let old_range = self.get_fc_eff_range()?;
767            self.fc_eff_map = fc_eff_max + (&self.fc_eff_map - fc_eff_max) * new_range / old_range;
768            if self.get_fc_eff_min()? < 0.0 {
769                bail!(
770                    "`fc_eff_min` ({:.3}) must not be negative",
771                    self.get_fc_eff_min()?
772                )
773            }
774            ensure!(
775                self.get_fc_eff_max()? <= 1.0,
776                format!(
777                    "{}\n`fc_eff_max` ({:.3}) must be no greater than 1.0",
778                    format_dbg!(self.get_fc_eff_max()? <= 1.0),
779                    self.get_fc_eff_max()?
780                )
781            );
782            self.fc_eff_array = self.fc_eff_map.to_vec();
783            Ok(())
784        } else {
785            bail!("`new_range` ({:.3}) must be between 0.0 and 1.0", new_range)
786        }
787    }
788
789    pub fn set_fc_peak_eff(&mut self, new_peak: f64) {
790        let old_fc_peak_eff = self.fc_peak_eff();
791        let multiplier = new_peak / old_fc_peak_eff;
792        self.fc_eff_array = self
793            .fc_eff_array
794            .iter()
795            .map(|eff: &f64| -> f64 { eff * multiplier })
796            .collect();
797        let new_fc_peak_eff = self.fc_peak_eff();
798        let eff_map_multiplier = new_peak / new_fc_peak_eff;
799        self.fc_eff_map = self
800            .fc_eff_map
801            .map(|eff| -> f64 { eff * eff_map_multiplier });
802    }
803
804    /// Sets derived parameters:
805    /// - `no_elec_sys`
806    /// - `no_elec_aux`
807    /// - `fc_perc_out_array`
808    /// - `input_kw_out_array`
809    /// - `fc_kw_out_array`
810    /// - `fc_eff_array`
811    /// - `modern_diff`
812    /// - `large_baseline_eff_adj`
813    /// - `mc_kw_adj_perc`
814    /// - `mc_eff_map`
815    /// - `mc_eff_array`
816    /// - `mc_perc_out_array`
817    /// - `mc_kw_out_array`
818    /// - `mc_full_eff_array`
819    /// - `mc_kw_in_array`
820    /// - `mc_max_elec_in_kw`
821    /// - `set_fc_peak_eff()`
822    /// - `set_mc_peak_eff()`
823    /// - `set_veh_mass()`
824    ///     - `ess_mass_kg`
825    ///     - `mc_mass_kg`
826    ///     - `fc_mass_kg`
827    ///     - `fs_mass_kg`
828    ///     - `veh_kg`
829    ///     - `max_trac_mps2`
830    pub fn set_derived(&mut self) -> anyhow::Result<()> {
831        // Vehicle input validation
832        #[cfg(feature = "validation")]
833        self.validate()?;
834
835        if self.scenario_name != "Template Vehicle for setting up data types" {
836            if self.veh_pt_type == BEV {
837                assert!(
838                    self.fs_max_kw == 0.0,
839                    "max_fuel_stor_kw must be zero for provided BEV powertrain type in {}",
840                    self.scenario_name
841                );
842                assert!(
843                    self.fs_kwh == 0.0,
844                    "fuel_stor_kwh must be zero for provided BEV powertrain type in {}",
845                    self.scenario_name
846                );
847                assert!(
848                    self.fc_max_kw == 0.0,
849                    "max_fuel_conv_kw must be zero for provided BEV powertrain type in {}",
850                    self.scenario_name
851                );
852            } else if (self.veh_pt_type == CONV) && !self.stop_start {
853                assert!(
854                    self.mc_max_kw == 0.0,
855                    "max_mc_kw must be zero for provided Conv powertrain type in {}",
856                    self.scenario_name
857                );
858                assert!(
859                    self.ess_max_kw == 0.0,
860                    "max_ess_kw must be zero for provided Conv powertrain type in {}",
861                    self.scenario_name
862                );
863                assert!(
864                    self.ess_max_kwh == 0.0,
865                    "max_ess_kwh must be zero for provided Conv powertrain type in {}",
866                    self.scenario_name
867                );
868            }
869        }
870        // ### Build roadway power lookup table
871        // self.max_roadway_chg_kw = Array1::from_vec(vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
872        // self.charging_on = false;
873
874        // Checking if a vehicle has any hybrid components
875        self.no_elec_sys =
876            (self.ess_max_kwh == 0.0) || (self.ess_max_kw == 0.0) || (self.mc_max_kw == 0.0);
877
878        // Checking if aux loads go through an alternator
879        self.no_elec_aux =
880            self.no_elec_sys || (self.mc_max_kw <= self.aux_kw) || self.force_aux_on_fc;
881
882        // TODO: this probably shouldnt be set if already provided
883        self.fc_perc_out_array = FC_PERC_OUT_ARRAY.clone().to_vec();
884
885        // discrete array of possible engine power outputs
886        self.input_kw_out_array = &self.fc_pwr_out_perc * self.fc_max_kw;
887        // Relatively continuous array of possible engine power outputs
888        self.fc_kw_out_array = self
889            .fc_perc_out_array
890            .iter()
891            .map(|n| n * self.fc_max_kw)
892            .collect();
893        // Creates relatively continuous array for fc_eff
894        if self.fc_eff_array.is_empty() {
895            for x in &self.fc_perc_out_array {
896                self.fc_eff_array.push(
897                    interpolate(
898                        x,
899                        &Array1::from(self.fc_pwr_out_perc.to_vec()),
900                        &self.fc_eff_map,
901                        false,
902                    )
903                    .with_context(|| format_dbg!())?,
904                )
905            }
906        }
907
908        if self.mc_eff_map == Array1::<f64>::zeros(LARGE_BASELINE_EFF.len()) {
909            if self.modern_max == 0.0 {
910                self.modern_max = MODERN_MAX;
911            }
912            let modern_diff = self.modern_max - arrmax(&LARGE_BASELINE_EFF);
913            let large_baseline_eff_adj: Vec<f64> =
914                LARGE_BASELINE_EFF.iter().map(|x| x + modern_diff).collect();
915            let mc_kw_adj_perc = max(
916                0.0,
917                min(
918                    (self.mc_max_kw - self.small_motor_power_kw)
919                        / (self.large_motor_power_kw - self.small_motor_power_kw),
920                    1.0,
921                ),
922            );
923            self.mc_eff_map = large_baseline_eff_adj
924                .iter()
925                .zip(SMALL_BASELINE_EFF)
926                .map(|(&x, y)| mc_kw_adj_perc * x + (1.0 - mc_kw_adj_perc) * y)
927                .collect();
928        }
929        self.mc_eff_array = self.mc_eff_map.clone();
930
931        self.mc_perc_out_array = MC_PERC_OUT_ARRAY.clone().to_vec();
932
933        self.mc_kw_out_array =
934            (Array::linspace(0.0, 1.0, self.mc_perc_out_array.len()) * self.mc_max_kw).to_vec();
935
936        self.mc_full_eff_array = vec![];
937        for (idx, x) in self.mc_perc_out_array.iter().enumerate() {
938            self.mc_full_eff_array.push(if idx == 0 {
939                0.0
940            } else {
941                interpolate(&x, &self.mc_pwr_out_perc, &self.mc_eff_array, false)
942                    .with_context(|| format_dbg!())?
943            })
944        }
945
946        self.mc_kw_in_array = [0.0; 101]
947            .iter()
948            .enumerate()
949            .map(|(idx, _)| {
950                if idx == 0 {
951                    0.0
952                } else {
953                    self.mc_kw_out_array[idx] / self.mc_full_eff_array[idx]
954                }
955            })
956            .collect();
957
958        self.mc_max_elec_in_kw = arrmax(&self.mc_kw_in_array);
959
960        #[cfg(feature = "pyo3")]
961        if let Some(new_fc_peak) = self.fc_peak_eff_override {
962            self.set_fc_peak_eff(new_fc_peak);
963            self.fc_peak_eff_override = None;
964        }
965        #[cfg(feature = "pyo3")]
966        if let Some(new_mc_peak) = self.mc_peak_eff_override {
967            self.set_mc_peak_eff(new_mc_peak);
968            self.mc_peak_eff_override = None;
969        }
970
971        // check that efficiencies are not violating the first law of thermo
972        // TODO: this could perhaps be done in the input validators
973        ensure!(
974            arrmin(&self.fc_eff_array) >= 0.0,
975            "minimum FC efficiency < 0 is not allowed"
976        );
977        ensure!(self.fc_peak_eff() < 1.0, "fc_peak_eff >= 1 is not allowed");
978        if !self.no_elec_sys {
979            ensure!(
980                arrmin(&self.mc_full_eff_array) >= 0.0,
981                "minimum MC efficiency < 0 is not allowed"
982            );
983            ensure!(self.mc_peak_eff() < 1.0, "mc_peak_eff >= 1 is not allowed");
984        }
985
986        self.set_veh_mass();
987
988        self.max_regen_kwh = 0.5 * self.veh_kg * (27.0 * 27.0) / (3_600.0 * 1_000.0);
989
990        Ok(())
991    }
992
993    pub fn mock_vehicle() -> Self {
994        // NOTE: Default::default() uses mock_vehicle so this method can't "splat" defaults in
995        // (i.e., = Self { ..Default::default() } will cause a stack overflow)
996        let mut v = Self {
997            scenario_name: String::from("2016 FORD Escape 4cyl 2WD"),
998            selection: 5,
999            veh_year: 2016,
1000            veh_pt_type: String::from("Conv"),
1001            drag_coef: 0.355,
1002            frontal_area_m2: 3.066,
1003            glider_kg: 1359.166,
1004            veh_cg_m: 0.53,
1005            drive_axle_weight_frac: 0.59,
1006            wheel_base_m: 2.6,
1007            cargo_kg: 136.0,
1008            veh_override_kg: None,
1009            comp_mass_multiplier: 1.4,
1010            fs_max_kw: 2000.0,
1011            fs_secs_to_peak_pwr: 1.0,
1012            fs_kwh: 504.0,
1013            fs_kwh_per_kg: 9.89,
1014            fc_max_kw: 125.0,
1015            fc_pwr_out_perc: array![
1016                0.0, 0.005, 0.015, 0.04, 0.06, 0.1, 0.14, 0.2, 0.4, 0.6, 0.8, 1.0,
1017            ],
1018            fc_eff_map: array![
1019                0.1, 0.12, 0.16, 0.22, 0.28, 0.33, 0.35, 0.36, 0.35, 0.34, 0.32, 0.3,
1020            ],
1021            fc_peak_eff_override: Default::default(),
1022            fc_eff_type: String::from("SI"),
1023            fc_sec_to_peak_pwr: 6.0,
1024            fc_base_kg: 61.0,
1025            fc_kw_per_kg: 2.13,
1026            min_fc_time_on: 30.0,
1027            idle_fc_kw: 2.5,
1028            mc_max_kw: 0.0,
1029            mc_peak_eff_override: Default::default(),
1030            mc_pwr_out_perc: array![0.0, 0.02, 0.04, 0.06, 0.08, 0.1, 0.2, 0.4, 0.6, 0.8, 1.0],
1031            mc_eff_map: array![0.12, 0.16, 0.21, 0.29, 0.35, 0.42, 0.75, 0.92, 0.93, 0.93, 0.92,],
1032            mc_sec_to_peak_pwr: 4.0,
1033            mc_pe_kg_per_kw: 0.833,
1034            mc_pe_base_kg: 21.6,
1035            small_motor_power_kw: 7.5,
1036            large_motor_power_kw: 75.0,
1037            modern_max: MODERN_MAX,
1038            charging_on: false,
1039            max_roadway_chg_kw: Array1::<f64>::from_vec(vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
1040            ess_max_kw: 0.0,
1041            ess_max_kwh: 0.0,
1042            ess_kg_per_kwh: 8.0,
1043            ess_base_kg: 75.0,
1044            ess_round_trip_eff: 0.97,
1045            ess_life_coef_a: 110.0,
1046            ess_life_coef_b: -0.6811,
1047            min_soc: 0.4,
1048            max_soc: 0.8,
1049            ess_dischg_to_fc_max_eff_perc: 0.0,
1050            ess_chg_to_fc_max_eff_perc: 0.0,
1051            wheel_inertia_kg_m2: 0.815,
1052            num_wheels: 4.0,
1053            wheel_rr_coef: 0.006,
1054            wheel_radius_m: 0.336,
1055            wheel_coef_of_fric: 0.7,
1056            max_accel_buffer_mph: 60.0,
1057            max_accel_buffer_perc_of_useable_soc: 0.2,
1058            perc_high_acc_buf: 0.0,
1059            mph_fc_on: 30.0,
1060            kw_demand_fc_on: 100.0,
1061            max_regen: 0.98,
1062            stop_start: false,
1063            force_aux_on_fc: true,
1064            alt_eff: 1.0,
1065            chg_eff: 0.86,
1066            aux_kw: 0.7,
1067            trans_kg: 114.0,
1068            trans_eff: 0.92,
1069            ess_to_fuel_ok_error: 0.005,
1070            val_udds_mpgge: 23.0,
1071            val_hwy_mpgge: 32.0,
1072            val_comb_mpgge: 26.0,
1073            val_udds_kwh_per_mile: f64::NAN,
1074            val_hwy_kwh_per_mile: f64::NAN,
1075            val_comb_kwh_per_mile: f64::NAN,
1076            val_cd_range_mi: f64::NAN,
1077            val_const65_mph_kwh_per_mile: f64::NAN,
1078            val_const60_mph_kwh_per_mile: f64::NAN,
1079            val_const55_mph_kwh_per_mile: f64::NAN,
1080            val_const45_mph_kwh_per_mile: f64::NAN,
1081            val_unadj_udds_kwh_per_mile: f64::NAN,
1082            val_unadj_hwy_kwh_per_mile: f64::NAN,
1083            val0_to60_mph: 9.9,
1084            val_ess_life_miles: f64::NAN,
1085            val_range_miles: f64::NAN,
1086            val_veh_base_cost: f64::NAN,
1087            val_msrp: f64::NAN,
1088            props: RustPhysicalProperties::default(),
1089            regen_a: 500.0,
1090            regen_b: 0.99,
1091            orphaned: Default::default(),
1092            // fields that get overriden by `set_derived`
1093            no_elec_sys: Default::default(),
1094            no_elec_aux: Default::default(),
1095            fc_perc_out_array: Default::default(),
1096            input_kw_out_array: Default::default(),
1097            fc_kw_out_array: Default::default(),
1098            fc_eff_array: Default::default(),
1099            max_regen_kwh: Default::default(),
1100            mc_eff_array: Default::default(),
1101            mc_perc_out_array: Default::default(),
1102            mc_kw_out_array: Default::default(),
1103            mc_full_eff_array: Default::default(),
1104            mc_kw_in_array: Default::default(),
1105            mc_max_elec_in_kw: Default::default(),
1106            ess_mass_kg: Default::default(),
1107            mc_mass_kg: Default::default(),
1108            fc_mass_kg: Default::default(),
1109            fs_mass_kg: Default::default(),
1110            veh_kg: Default::default(),
1111            max_trac_mps2: Default::default(),
1112            doc: Default::default(),
1113            drag_coef_doc: Default::default(),
1114            frontal_area_m2_doc: Default::default(),
1115            glider_kg_doc: Default::default(),
1116            veh_cg_m_doc: Default::default(),
1117            drive_axle_weight_frac_doc: Default::default(),
1118            wheel_base_m_doc: Default::default(),
1119            cargo_kg_doc: Default::default(),
1120            veh_override_kg_doc: Default::default(),
1121            comp_mass_multiplier_doc: Default::default(),
1122            fs_max_kw_doc: Default::default(),
1123            fs_secs_to_peak_pwr_doc: Default::default(),
1124            fs_kwh_doc: Default::default(),
1125            fs_kwh_per_kg_doc: Default::default(),
1126            fc_max_kw_doc: Default::default(),
1127            fc_pwr_out_perc_doc: Default::default(),
1128            fc_eff_map_doc: Default::default(),
1129            fc_eff_type_doc: Default::default(),
1130            fc_sec_to_peak_pwr_doc: Default::default(),
1131            fc_base_kg_doc: Default::default(),
1132            fc_kw_per_kg_doc: Default::default(),
1133            min_fc_time_on_doc: Default::default(),
1134            idle_fc_kw_doc: Default::default(),
1135            mc_max_kw_doc: Default::default(),
1136            mc_pwr_out_perc_doc: Default::default(),
1137            mc_eff_map_doc: Default::default(),
1138            mc_sec_to_peak_pwr_doc: Default::default(),
1139            mc_pe_kg_per_kw_doc: Default::default(),
1140            mc_pe_base_kg_doc: Default::default(),
1141            ess_max_kw_doc: Default::default(),
1142            ess_max_kwh_doc: Default::default(),
1143            ess_kg_per_kwh_doc: Default::default(),
1144            ess_base_kg_doc: Default::default(),
1145            ess_round_trip_eff_doc: Default::default(),
1146            ess_life_coef_a_doc: Default::default(),
1147            ess_life_coef_b_doc: Default::default(),
1148            min_soc_doc: Default::default(),
1149            max_soc_doc: Default::default(),
1150            ess_dischg_to_fc_max_eff_perc_doc: Default::default(),
1151            ess_chg_to_fc_max_eff_perc_doc: Default::default(),
1152            wheel_inertia_kg_m2_doc: Default::default(),
1153            num_wheels_doc: Default::default(),
1154            wheel_rr_coef_doc: Default::default(),
1155            wheel_radius_m_doc: Default::default(),
1156            wheel_coef_of_fric_doc: Default::default(),
1157            max_accel_buffer_mph_doc: Default::default(),
1158            max_accel_buffer_perc_of_useable_soc_doc: Default::default(),
1159            perc_high_acc_buf_doc: Default::default(),
1160            mph_fc_on_doc: Default::default(),
1161            kw_demand_fc_on_doc: Default::default(),
1162            max_regen_doc: Default::default(),
1163            stop_start_doc: Default::default(),
1164            force_aux_on_fc_doc: Default::default(),
1165            alt_eff_doc: Default::default(),
1166            chg_eff_doc: Default::default(),
1167            aux_kw_doc: Default::default(),
1168            trans_kg_doc: Default::default(),
1169            trans_eff_doc: Default::default(),
1170            ess_to_fuel_ok_error_doc: Default::default(),
1171            fc_peak_eff_override_doc: Default::default(),
1172            mc_peak_eff_override_doc: Default::default(),
1173        };
1174        v.set_derived().unwrap();
1175        v
1176    }
1177
1178    /// Downloads specified vehicle from FASTSim vehicle repo or url and
1179    /// instantiates it into a RustVehicle. Notes in vehicle.doc the origin of
1180    /// the vehicle. Returns vehicle.  
1181    /// # Arguments  
1182    /// - vehicle_file_name: file name for vehicle to be downloaded, including
1183    ///   path from url directory or FASTSim repository (if applicable)  
1184    /// - url: url for vehicle repository where vehicle will be downloaded from,
1185    ///   if None, assumed to be downloaded from vehicle FASTSim repo  
1186    ///
1187    /// Note: The URL needs to be a URL pointing directly to a file, for example
1188    /// a raw github URL, split up so that the "url" argument is the path to the
1189    /// directory, and the "vehicle_file_name" is the path within the directory
1190    /// to the file.  
1191    /// Note: If downloading from the FASTSim Vehicle Repo, the
1192    /// vehicle_file_name should include the path to the file from the root of
1193    /// the Repo, as listed in the output of the
1194    /// vehicle_utils::fetch_github_list() function.  
1195    /// Note: the url should not include the file name, only the path to the
1196    /// file or a root directory of the file.
1197    pub fn from_github_or_url<S: AsRef<str>>(
1198        vehicle_file_name: S,
1199        url: Option<S>,
1200    ) -> anyhow::Result<Self> {
1201        let url_internal = match url {
1202            Some(s) => {
1203                s.as_ref().trim_end_matches('/').to_owned()
1204                    + "/"
1205                    + vehicle_file_name.as_ref().trim_start_matches('/')
1206            }
1207            None => Self::VEHICLE_DIRECTORY_URL.to_string() + vehicle_file_name.as_ref(),
1208        };
1209        let mut vehicle = Self::from_url(&url_internal, false)
1210            .with_context(|| "Could not parse vehicle from url")?;
1211        let vehicle_origin = "Vehicle from ".to_owned() + url_internal.as_str();
1212        vehicle.doc = Some(vehicle_origin);
1213        Ok(vehicle)
1214    }
1215
1216    /// Skews the peak of motor efficiency curve to new x-value, redistributing other
1217    /// x-values linearly, preserving relative distances between peak and endpoints.  
1218    /// # Arguments  
1219    /// - `new_peak_x`: new x-value at which to relocate peak  
1220    pub fn set_mc_eff_peak_pwr(&mut self, new_peak_x: f64) -> anyhow::Result<()> {
1221        let short_arrays = skewness_shift(&self.mc_pwr_out_perc, &self.mc_eff_map, new_peak_x)?;
1222        self.mc_pwr_out_perc = short_arrays.0;
1223        self.mc_eff_map = short_arrays.1.clone();
1224        self.mc_eff_array = short_arrays.1;
1225        for (idx, x) in self.mc_perc_out_array.iter().enumerate() {
1226            self.mc_full_eff_array.push(if idx == 0 {
1227                0.0
1228            } else {
1229                interpolate(x, &self.mc_pwr_out_perc, &self.mc_eff_array, false)
1230                    .with_context(|| format_dbg!())?
1231            });
1232        }
1233        Ok(())
1234    }
1235
1236    /// Skews the peak of fc efficiency curve to new x-value, redistributing other
1237    /// x-values linearly, preserving relative distances between peak and endpoints.  
1238    /// # Arguments
1239    /// - `new_peak_x`: new x-value at which to relocate peak  
1240    pub fn set_fc_eff_peak_pwr(&mut self, new_peak_x: f64) -> anyhow::Result<()> {
1241        let short_arrays = skewness_shift(&self.fc_pwr_out_perc, &self.fc_eff_map, new_peak_x)?;
1242        self.fc_pwr_out_perc = short_arrays.0;
1243        self.fc_eff_array = short_arrays.1.to_vec();
1244        self.fc_eff_map = short_arrays.1;
1245        Ok(())
1246    }
1247}
1248
1249impl Default for RustVehicle {
1250    fn default() -> Self {
1251        let mut veh = RustVehicle::mock_vehicle();
1252        veh.scenario_name = Default::default();
1253        veh.selection = Default::default();
1254        veh.veh_year = Default::default();
1255        veh.val_udds_kwh_per_mile = Default::default();
1256        veh.val_hwy_kwh_per_mile = Default::default();
1257        veh.val_comb_kwh_per_mile = Default::default();
1258        veh.val_cd_range_mi = Default::default();
1259        veh.val_const65_mph_kwh_per_mile = Default::default();
1260        veh.val_const60_mph_kwh_per_mile = Default::default();
1261        veh.val_const55_mph_kwh_per_mile = Default::default();
1262        veh.val_const45_mph_kwh_per_mile = Default::default();
1263        veh.val_unadj_udds_kwh_per_mile = Default::default();
1264        veh.val_unadj_hwy_kwh_per_mile = Default::default();
1265        veh.val0_to60_mph = Default::default();
1266        veh.val_ess_life_miles = Default::default();
1267        veh.val_range_miles = Default::default();
1268        veh.val_veh_base_cost = Default::default();
1269        veh.val_msrp = Default::default();
1270        veh
1271    }
1272}
1273
1274impl SerdeAPI for RustVehicle {
1275    const RESOURCE_PREFIX: &'static str = "vehicles";
1276    const CACHE_FOLDER: &'static str = "vehicles";
1277
1278    fn init(&mut self) -> anyhow::Result<()> {
1279        self.set_derived()
1280    }
1281
1282    /// instantiates a vehicle from a url, and notes in vehicle.doc the origin
1283    /// of the vehicle.  
1284    /// accepts yaml and json file types  
1285    /// # Arguments  
1286    /// - url: URL (either as a string or url type) to object  
1287    ///
1288    /// Note: The URL needs to be a URL pointing directly to a file, for example
1289    /// a raw github URL.
1290    fn from_url<S: AsRef<str>>(url: S, skip_init: bool) -> anyhow::Result<Self> {
1291        let url = url::Url::parse(url.as_ref())?;
1292        let format = url
1293            .path_segments()
1294            .and_then(|segments| segments.last())
1295            .and_then(|filename| Path::new(filename).extension())
1296            .and_then(OsStr::to_str)
1297            .with_context(|| "Could not parse file format from URL: {url:?}")?;
1298        let response = ureq::get(url.as_ref()).call()?.into_reader();
1299        let mut vehicle = Self::from_reader(response, format, skip_init)?;
1300        let vehicle_origin = "Vehicle from ".to_owned() + url.as_ref();
1301        vehicle.doc = Some(vehicle_origin);
1302        Ok(vehicle)
1303    }
1304}
1305
1306#[cfg(test)]
1307mod tests {
1308    use super::*;
1309    #[cfg(feature = "validation")]
1310    use validator::ValidationErrors;
1311
1312    #[test]
1313    fn test_set_derived_via_new() {
1314        let veh = RustVehicle::mock_vehicle();
1315        assert!(veh.veh_kg > 0.0);
1316    }
1317
1318    #[test]
1319    fn test_set_mc_eff_range() {
1320        let mut veh = RustVehicle::mock_vehicle();
1321        veh.set_mc_eff_range(0.7).unwrap();
1322        assert!(
1323            0.699 < veh.get_mc_eff_range().unwrap() && veh.get_mc_eff_range().unwrap() <= 0.701
1324        );
1325        veh.set_mc_eff_range(0.5).unwrap();
1326        assert!(
1327            0.499 < veh.get_mc_eff_range().unwrap() && veh.get_mc_eff_range().unwrap() <= 0.501
1328        );
1329        veh.set_mc_eff_range(0.).unwrap();
1330        assert!(veh.get_mc_eff_range().unwrap() == 0.);
1331    }
1332
1333    #[test]
1334    fn test_veh_kg_override() {
1335        let veh_file = resources_path().join("vehdb/test_overrides.yaml");
1336        let veh = RustVehicle::from_file(veh_file, false).unwrap();
1337        assert!(veh.veh_kg == veh.veh_override_kg.unwrap());
1338        // test input validation by providing bad inputs, then checking
1339        // the produced error for the offending field names
1340    }
1341
1342    #[cfg(feature = "validation")]
1343    #[test]
1344    fn test_input_validation() {
1345        // set up vehicle input parameters
1346        let scenario_name = String::from("2016 FORD Escape 4cyl 2WD");
1347        let selection = 5;
1348        let veh_year = 2016;
1349        let veh_pt_type = String::from("whoops"); // bad input
1350        let drag_coef = 0.355;
1351        let frontal_area_m2 = 3.066;
1352        let glider_kg = -50.0; // bad input
1353        let veh_cg_m = 0.53;
1354        let drive_axle_weight_frac = 0.59;
1355        let wheel_base_m = 2.6;
1356        let cargo_kg = 136.0;
1357        let veh_override_kg = None;
1358        let comp_mass_multiplier = 1.4;
1359        let fs_max_kw = 2000.0;
1360        let fs_secs_to_peak_pwr = 1.0;
1361        let fs_kwh = 504.0;
1362        let fs_kwh_per_kg = 9.89;
1363        let fc_max_kw = -60.0; // bad input
1364        let fc_pwr_out_perc = vec![
1365            0.0, 0.005, 0.015, 0.04, 0.06, 0.1, 0.14, 0.2, 0.4, 0.6, 0.8, 1.0,
1366        ];
1367        let fc_eff_type = String::from("SI");
1368        let fc_sec_to_peak_pwr = 6.0;
1369        let fc_base_kg = 61.0;
1370        let fc_kw_per_kg = 2.13;
1371        let min_fc_time_on = 30.0;
1372        let idle_fc_kw = 2.5;
1373        let mc_max_kw = 0.0;
1374        let mc_sec_to_peak_pwr = 4.0;
1375        let mc_pe_kg_per_kw = 0.833;
1376        let mc_pe_base_kg = 21.6;
1377        let ess_max_kw = 0.0;
1378        let ess_max_kwh = 0.0;
1379        let ess_kg_per_kwh = 8.0;
1380        let ess_base_kg = 75.0;
1381        let ess_round_trip_eff = 0.97;
1382        let ess_life_coef_a = 110.0;
1383        let ess_life_coef_b = -0.6811;
1384        let min_soc = -0.5; // bad input
1385        let max_soc = 1.5; // bad input
1386        let ess_dischg_to_fc_max_eff_perc = 0.0;
1387        let ess_chg_to_fc_max_eff_perc = 0.0;
1388        let wheel_inertia_kg_m2 = 0.815;
1389        let num_wheels = 4.0;
1390        let wheel_rr_coef = 0.006;
1391        let wheel_radius_m = 0.336;
1392        let wheel_coef_of_fric = 0.7;
1393        let max_accel_buffer_mph = 60.0;
1394        let max_accel_buffer_perc_of_useable_soc = 0.2;
1395        let perc_high_acc_buf = 0.0;
1396        let mph_fc_on = 30.0;
1397        let kw_demand_fc_on = 100.0;
1398        let max_regen = 0.98;
1399        let stop_start = false;
1400        let force_aux_on_fc = true;
1401        let alt_eff = 1.0;
1402        let chg_eff = 0.86;
1403        let aux_kw = 0.7;
1404        let trans_kg = 114.0;
1405        let trans_eff = 0.92;
1406        let ess_to_fuel_ok_error = 0.005;
1407        let val_udds_mpgge = 23.0;
1408        let val_hwy_mpgge = 32.0;
1409        let val_comb_mpgge = 26.0;
1410        let val_udds_kwh_per_mile = f64::NAN;
1411        let val_hwy_kwh_per_mile = f64::NAN;
1412        let val_comb_kwh_per_mile = f64::NAN;
1413        let val_cd_range_mi = f64::NAN;
1414        let val_const65_mph_kwh_per_mile = f64::NAN;
1415        let val_const60_mph_kwh_per_mile = f64::NAN;
1416        let val_const55_mph_kwh_per_mile = f64::NAN;
1417        let val_const45_mph_kwh_per_mile = f64::NAN;
1418        let val_unadj_udds_kwh_per_mile = f64::NAN;
1419        let val_unadj_hwy_kwh_per_mile = f64::NAN;
1420        let val0_to60_mph = 9.9;
1421        let val_ess_life_miles = f64::NAN;
1422        let val_range_miles = f64::NAN;
1423        let val_veh_base_cost = f64::NAN;
1424        let val_msrp = f64::NAN;
1425        let props = RustPhysicalProperties::default();
1426        let regen_a = 500.0;
1427        let regen_b = 0.99;
1428        let fc_peak_eff_override = None;
1429        let mc_peak_eff_override = Some(-0.50); // bad input
1430        let small_motor_power_kw = 7.5;
1431        let large_motor_power_kw = 75.0;
1432        let fc_perc_out_array = FC_PERC_OUT_ARRAY.clone().to_vec();
1433        let mc_eff_map = Array1::<f64>::zeros(LARGE_BASELINE_EFF.len());
1434        let mc_kw_out_array =
1435            (Array::linspace(0.0, 1.0, MC_PERC_OUT_ARRAY.len()) * mc_max_kw).to_vec();
1436        let mc_perc_out_array = MC_PERC_OUT_ARRAY.clone().to_vec();
1437        let mc_pwr_out_perc = array![0.0, 0.02, 0.04, 0.06, 0.08, 0.1, 0.2, 0.4, 0.6, 0.8, 1.0];
1438        let mc_full_eff_array: Vec<f64> = mc_perc_out_array
1439            .iter()
1440            .enumerate()
1441            .map(|(idx, &x): (usize, &f64)| -> f64 {
1442                if idx == 0 {
1443                    0.0
1444                } else {
1445                    interpolate(&x, &mc_pwr_out_perc, &mc_eff_map, false).unwrap()
1446                }
1447            })
1448            .collect();
1449        let mc_kw_in_array: Vec<f64> = [0.0; 101]
1450            .iter()
1451            .enumerate()
1452            .map(|(idx, _)| {
1453                if idx == 0 {
1454                    0.0
1455                } else {
1456                    mc_kw_out_array[idx] / mc_full_eff_array[idx]
1457                }
1458            })
1459            .collect();
1460        let mc_max_elec_in_kw = arrmax(&mc_kw_in_array);
1461
1462        // instantiate vehicle result
1463        let mut veh = RustVehicle {
1464            small_motor_power_kw,
1465            large_motor_power_kw,
1466            fc_perc_out_array: FC_PERC_OUT_ARRAY.clone().to_vec(),
1467            charging_on: Default::default(),
1468            no_elec_sys: Default::default(),
1469            no_elec_aux: Default::default(),
1470            max_roadway_chg_kw: Default::default(),
1471            input_kw_out_array: Array1::from_vec(fc_pwr_out_perc.clone()) * fc_max_kw,
1472            fc_kw_out_array: fc_perc_out_array.iter().map(|n| n * fc_max_kw).collect(),
1473            fc_eff_array: fc_perc_out_array
1474                .iter()
1475                .map(|x: &f64| -> f64 {
1476                    interpolate(
1477                        x,
1478                        &Array1::from(fc_pwr_out_perc.to_vec()),
1479                        &array![
1480                            0.10, 0.12, 0.16, 0.22, 0.28, 0.33, 0.35, 0.36, 0.35, 0.34, 0.32, 0.30
1481                        ],
1482                        false,
1483                    )
1484                    .unwrap()
1485                })
1486                .collect(),
1487            modern_max: MODERN_MAX,
1488            mc_eff_array: mc_eff_map,
1489            mc_kw_in_array: [0.0; 101]
1490                .iter()
1491                .enumerate()
1492                .map(|(idx, _)| {
1493                    if idx == 0 {
1494                        0.0
1495                    } else {
1496                        mc_kw_out_array[idx] / mc_full_eff_array[idx]
1497                    }
1498                })
1499                .collect(),
1500            mc_kw_out_array,
1501            mc_max_elec_in_kw,
1502            mc_full_eff_array,
1503            // these get calculated in `se
1504            veh_kg: Default::default(),
1505            max_trac_mps2: Default::default(),
1506            ess_mass_kg: Default::default(),
1507            mc_mass_kg: Default::default(),
1508            fc_mass_kg: Default::default(),
1509            fs_mass_kg: Default::default(),
1510            mc_perc_out_array,
1511            orphaned: Default::default(),
1512            scenario_name,
1513            selection,
1514            veh_year,
1515            veh_pt_type, // bad input
1516            drag_coef,
1517            frontal_area_m2,
1518            glider_kg, // bad input
1519            veh_cg_m,
1520            drive_axle_weight_frac,
1521            wheel_base_m,
1522            cargo_kg,
1523            veh_override_kg,
1524            comp_mass_multiplier,
1525            fs_max_kw,
1526            fs_secs_to_peak_pwr,
1527            fs_kwh,
1528            fs_kwh_per_kg,
1529            fc_max_kw, // bad input
1530            fc_pwr_out_perc: array![
1531                0.0, 0.005, 0.015, 0.04, 0.06, 0.1, 0.14, 0.2, 0.4, 0.6, 0.8, 1.0,
1532            ],
1533            fc_eff_map: array![
1534                0.1, 0.12, 0.16, 0.22, 0.28, 0.33, 0.35, 0.36, 0.35, 0.34, 0.32, 0.3,
1535            ],
1536            fc_eff_type,
1537            fc_sec_to_peak_pwr,
1538            fc_base_kg,
1539            fc_kw_per_kg,
1540            min_fc_time_on,
1541            idle_fc_kw,
1542            mc_max_kw,
1543            mc_pwr_out_perc,
1544            mc_eff_map: array![0.12, 0.16, 0.21, 0.29, 0.35, 0.42, 0.75, 0.92, 0.93, 0.93, 0.92],
1545            mc_sec_to_peak_pwr,
1546            mc_pe_kg_per_kw,
1547            mc_pe_base_kg,
1548            ess_max_kw,
1549            ess_max_kwh,
1550            ess_kg_per_kwh,
1551            ess_base_kg,
1552            ess_round_trip_eff,
1553            ess_life_coef_a,
1554            ess_life_coef_b,
1555            min_soc, // bad input
1556            max_soc, // bad input
1557            ess_dischg_to_fc_max_eff_perc,
1558            ess_chg_to_fc_max_eff_perc,
1559            wheel_inertia_kg_m2,
1560            num_wheels,
1561            wheel_rr_coef,
1562            wheel_radius_m,
1563            wheel_coef_of_fric,
1564            max_accel_buffer_mph,
1565            max_accel_buffer_perc_of_useable_soc,
1566            perc_high_acc_buf,
1567            mph_fc_on,
1568            kw_demand_fc_on,
1569            max_regen,
1570            stop_start,
1571            force_aux_on_fc,
1572            alt_eff,
1573            chg_eff,
1574            aux_kw,
1575            trans_kg,
1576            trans_eff,
1577            ess_to_fuel_ok_error,
1578            val_udds_mpgge,
1579            val_hwy_mpgge,
1580            val_comb_mpgge,
1581            val_udds_kwh_per_mile,
1582            val_hwy_kwh_per_mile,
1583            val_comb_kwh_per_mile,
1584            val_cd_range_mi,
1585            val_const65_mph_kwh_per_mile,
1586            val_const60_mph_kwh_per_mile,
1587            val_const55_mph_kwh_per_mile,
1588            val_const45_mph_kwh_per_mile,
1589            val_unadj_udds_kwh_per_mile,
1590            val_unadj_hwy_kwh_per_mile,
1591            val0_to60_mph,
1592            val_ess_life_miles,
1593            val_range_miles,
1594            val_veh_base_cost,
1595            val_msrp,
1596            props,
1597            regen_a,
1598            regen_b,
1599            fc_peak_eff_override,
1600            mc_peak_eff_override, // bad input
1601            ..Default::default()
1602        };
1603
1604        let validation_result = veh.set_derived();
1605
1606        // hard-coded fields where bad inputs were provided above
1607        let bad_fields = [
1608            "veh_pt_type",
1609            "glider_kg",
1610            "fc_max_kw",
1611            "min_soc",
1612            "max_soc",
1613            "mc_peak_eff_override",
1614        ];
1615        // downcast anyhow::error back into validator::ValidationErrors
1616        // this test will fail on the unwrap() if the error is not downcastable to ValidationErrors
1617        // e.g. if the error was not from input validation
1618        let validation_errs = validation_result
1619            .unwrap_err()
1620            .downcast::<ValidationErrors>()
1621            .unwrap();
1622        let validation_errs_hashmap = validation_errs.errors();
1623        // assert that specified bad fields were caught
1624        assert!(validation_errs_hashmap
1625            .keys()
1626            .all(|key| bad_fields.contains(key)));
1627        assert!(validation_errs_hashmap.len() == bad_fields.len());
1628    }
1629}