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        for (idx, x) in self.mc_perc_out_array.iter().enumerate() {
937            self.mc_full_eff_array.push(if idx == 0 {
938                0.0
939            } else {
940                interpolate(&x, &self.mc_pwr_out_perc, &self.mc_eff_array, false)
941                    .with_context(|| format_dbg!())?
942            })
943        }
944
945        self.mc_kw_in_array = [0.0; 101]
946            .iter()
947            .enumerate()
948            .map(|(idx, _)| {
949                if idx == 0 {
950                    0.0
951                } else {
952                    self.mc_kw_out_array[idx] / self.mc_full_eff_array[idx]
953                }
954            })
955            .collect();
956
957        self.mc_max_elec_in_kw = arrmax(&self.mc_kw_in_array);
958
959        #[cfg(feature = "pyo3")]
960        if let Some(new_fc_peak) = self.fc_peak_eff_override {
961            self.set_fc_peak_eff(new_fc_peak);
962            self.fc_peak_eff_override = None;
963        }
964        #[cfg(feature = "pyo3")]
965        if let Some(new_mc_peak) = self.mc_peak_eff_override {
966            self.set_mc_peak_eff(new_mc_peak);
967            self.mc_peak_eff_override = None;
968        }
969
970        // check that efficiencies are not violating the first law of thermo
971        // TODO: this could perhaps be done in the input validators
972        ensure!(
973            arrmin(&self.fc_eff_array) >= 0.0,
974            "minimum FC efficiency < 0 is not allowed"
975        );
976        ensure!(self.fc_peak_eff() < 1.0, "fc_peak_eff >= 1 is not allowed");
977        if !self.no_elec_sys {
978            ensure!(
979                arrmin(&self.mc_full_eff_array) >= 0.0,
980                "minimum MC efficiency < 0 is not allowed"
981            );
982            ensure!(self.mc_peak_eff() < 1.0, "mc_peak_eff >= 1 is not allowed");
983        }
984
985        self.set_veh_mass();
986
987        self.max_regen_kwh = 0.5 * self.veh_kg * (27.0 * 27.0) / (3_600.0 * 1_000.0);
988
989        Ok(())
990    }
991
992    pub fn mock_vehicle() -> Self {
993        // NOTE: Default::default() uses mock_vehicle so this method can't "splat" defaults in
994        // (i.e., = Self { ..Default::default() } will cause a stack overflow)
995        let mut v = Self {
996            scenario_name: String::from("2016 FORD Escape 4cyl 2WD"),
997            selection: 5,
998            veh_year: 2016,
999            veh_pt_type: String::from("Conv"),
1000            drag_coef: 0.355,
1001            frontal_area_m2: 3.066,
1002            glider_kg: 1359.166,
1003            veh_cg_m: 0.53,
1004            drive_axle_weight_frac: 0.59,
1005            wheel_base_m: 2.6,
1006            cargo_kg: 136.0,
1007            veh_override_kg: None,
1008            comp_mass_multiplier: 1.4,
1009            fs_max_kw: 2000.0,
1010            fs_secs_to_peak_pwr: 1.0,
1011            fs_kwh: 504.0,
1012            fs_kwh_per_kg: 9.89,
1013            fc_max_kw: 125.0,
1014            fc_pwr_out_perc: array![
1015                0.0, 0.005, 0.015, 0.04, 0.06, 0.1, 0.14, 0.2, 0.4, 0.6, 0.8, 1.0,
1016            ],
1017            fc_eff_map: array![
1018                0.1, 0.12, 0.16, 0.22, 0.28, 0.33, 0.35, 0.36, 0.35, 0.34, 0.32, 0.3,
1019            ],
1020            fc_peak_eff_override: Default::default(),
1021            fc_eff_type: String::from("SI"),
1022            fc_sec_to_peak_pwr: 6.0,
1023            fc_base_kg: 61.0,
1024            fc_kw_per_kg: 2.13,
1025            min_fc_time_on: 30.0,
1026            idle_fc_kw: 2.5,
1027            mc_max_kw: 0.0,
1028            mc_peak_eff_override: Default::default(),
1029            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],
1030            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,],
1031            mc_sec_to_peak_pwr: 4.0,
1032            mc_pe_kg_per_kw: 0.833,
1033            mc_pe_base_kg: 21.6,
1034            small_motor_power_kw: 7.5,
1035            large_motor_power_kw: 75.0,
1036            modern_max: MODERN_MAX,
1037            charging_on: false,
1038            max_roadway_chg_kw: Array1::<f64>::from_vec(vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
1039            ess_max_kw: 0.0,
1040            ess_max_kwh: 0.0,
1041            ess_kg_per_kwh: 8.0,
1042            ess_base_kg: 75.0,
1043            ess_round_trip_eff: 0.97,
1044            ess_life_coef_a: 110.0,
1045            ess_life_coef_b: -0.6811,
1046            min_soc: 0.4,
1047            max_soc: 0.8,
1048            ess_dischg_to_fc_max_eff_perc: 0.0,
1049            ess_chg_to_fc_max_eff_perc: 0.0,
1050            wheel_inertia_kg_m2: 0.815,
1051            num_wheels: 4.0,
1052            wheel_rr_coef: 0.006,
1053            wheel_radius_m: 0.336,
1054            wheel_coef_of_fric: 0.7,
1055            max_accel_buffer_mph: 60.0,
1056            max_accel_buffer_perc_of_useable_soc: 0.2,
1057            perc_high_acc_buf: 0.0,
1058            mph_fc_on: 30.0,
1059            kw_demand_fc_on: 100.0,
1060            max_regen: 0.98,
1061            stop_start: false,
1062            force_aux_on_fc: true,
1063            alt_eff: 1.0,
1064            chg_eff: 0.86,
1065            aux_kw: 0.7,
1066            trans_kg: 114.0,
1067            trans_eff: 0.92,
1068            ess_to_fuel_ok_error: 0.005,
1069            val_udds_mpgge: 23.0,
1070            val_hwy_mpgge: 32.0,
1071            val_comb_mpgge: 26.0,
1072            val_udds_kwh_per_mile: f64::NAN,
1073            val_hwy_kwh_per_mile: f64::NAN,
1074            val_comb_kwh_per_mile: f64::NAN,
1075            val_cd_range_mi: f64::NAN,
1076            val_const65_mph_kwh_per_mile: f64::NAN,
1077            val_const60_mph_kwh_per_mile: f64::NAN,
1078            val_const55_mph_kwh_per_mile: f64::NAN,
1079            val_const45_mph_kwh_per_mile: f64::NAN,
1080            val_unadj_udds_kwh_per_mile: f64::NAN,
1081            val_unadj_hwy_kwh_per_mile: f64::NAN,
1082            val0_to60_mph: 9.9,
1083            val_ess_life_miles: f64::NAN,
1084            val_range_miles: f64::NAN,
1085            val_veh_base_cost: f64::NAN,
1086            val_msrp: f64::NAN,
1087            props: RustPhysicalProperties::default(),
1088            regen_a: 500.0,
1089            regen_b: 0.99,
1090            orphaned: Default::default(),
1091            // fields that get overriden by `set_derived`
1092            no_elec_sys: Default::default(),
1093            no_elec_aux: Default::default(),
1094            fc_perc_out_array: Default::default(),
1095            input_kw_out_array: Default::default(),
1096            fc_kw_out_array: Default::default(),
1097            fc_eff_array: Default::default(),
1098            max_regen_kwh: Default::default(),
1099            mc_eff_array: Default::default(),
1100            mc_perc_out_array: Default::default(),
1101            mc_kw_out_array: Default::default(),
1102            mc_full_eff_array: Default::default(),
1103            mc_kw_in_array: Default::default(),
1104            mc_max_elec_in_kw: Default::default(),
1105            ess_mass_kg: Default::default(),
1106            mc_mass_kg: Default::default(),
1107            fc_mass_kg: Default::default(),
1108            fs_mass_kg: Default::default(),
1109            veh_kg: Default::default(),
1110            max_trac_mps2: Default::default(),
1111            doc: Default::default(),
1112            drag_coef_doc: Default::default(),
1113            frontal_area_m2_doc: Default::default(),
1114            glider_kg_doc: Default::default(),
1115            veh_cg_m_doc: Default::default(),
1116            drive_axle_weight_frac_doc: Default::default(),
1117            wheel_base_m_doc: Default::default(),
1118            cargo_kg_doc: Default::default(),
1119            veh_override_kg_doc: Default::default(),
1120            comp_mass_multiplier_doc: Default::default(),
1121            fs_max_kw_doc: Default::default(),
1122            fs_secs_to_peak_pwr_doc: Default::default(),
1123            fs_kwh_doc: Default::default(),
1124            fs_kwh_per_kg_doc: Default::default(),
1125            fc_max_kw_doc: Default::default(),
1126            fc_pwr_out_perc_doc: Default::default(),
1127            fc_eff_map_doc: Default::default(),
1128            fc_eff_type_doc: Default::default(),
1129            fc_sec_to_peak_pwr_doc: Default::default(),
1130            fc_base_kg_doc: Default::default(),
1131            fc_kw_per_kg_doc: Default::default(),
1132            min_fc_time_on_doc: Default::default(),
1133            idle_fc_kw_doc: Default::default(),
1134            mc_max_kw_doc: Default::default(),
1135            mc_pwr_out_perc_doc: Default::default(),
1136            mc_eff_map_doc: Default::default(),
1137            mc_sec_to_peak_pwr_doc: Default::default(),
1138            mc_pe_kg_per_kw_doc: Default::default(),
1139            mc_pe_base_kg_doc: Default::default(),
1140            ess_max_kw_doc: Default::default(),
1141            ess_max_kwh_doc: Default::default(),
1142            ess_kg_per_kwh_doc: Default::default(),
1143            ess_base_kg_doc: Default::default(),
1144            ess_round_trip_eff_doc: Default::default(),
1145            ess_life_coef_a_doc: Default::default(),
1146            ess_life_coef_b_doc: Default::default(),
1147            min_soc_doc: Default::default(),
1148            max_soc_doc: Default::default(),
1149            ess_dischg_to_fc_max_eff_perc_doc: Default::default(),
1150            ess_chg_to_fc_max_eff_perc_doc: Default::default(),
1151            wheel_inertia_kg_m2_doc: Default::default(),
1152            num_wheels_doc: Default::default(),
1153            wheel_rr_coef_doc: Default::default(),
1154            wheel_radius_m_doc: Default::default(),
1155            wheel_coef_of_fric_doc: Default::default(),
1156            max_accel_buffer_mph_doc: Default::default(),
1157            max_accel_buffer_perc_of_useable_soc_doc: Default::default(),
1158            perc_high_acc_buf_doc: Default::default(),
1159            mph_fc_on_doc: Default::default(),
1160            kw_demand_fc_on_doc: Default::default(),
1161            max_regen_doc: Default::default(),
1162            stop_start_doc: Default::default(),
1163            force_aux_on_fc_doc: Default::default(),
1164            alt_eff_doc: Default::default(),
1165            chg_eff_doc: Default::default(),
1166            aux_kw_doc: Default::default(),
1167            trans_kg_doc: Default::default(),
1168            trans_eff_doc: Default::default(),
1169            ess_to_fuel_ok_error_doc: Default::default(),
1170            fc_peak_eff_override_doc: Default::default(),
1171            mc_peak_eff_override_doc: Default::default(),
1172        };
1173        v.set_derived().unwrap();
1174        v
1175    }
1176
1177    /// Downloads specified vehicle from FASTSim vehicle repo or url and
1178    /// instantiates it into a RustVehicle. Notes in vehicle.doc the origin of
1179    /// the vehicle. Returns vehicle.  
1180    /// # Arguments  
1181    /// - vehicle_file_name: file name for vehicle to be downloaded, including
1182    ///   path from url directory or FASTSim repository (if applicable)  
1183    /// - url: url for vehicle repository where vehicle will be downloaded from,
1184    ///   if None, assumed to be downloaded from vehicle FASTSim repo  
1185    ///
1186    /// Note: The URL needs to be a URL pointing directly to a file, for example
1187    /// a raw github URL, split up so that the "url" argument is the path to the
1188    /// directory, and the "vehicle_file_name" is the path within the directory
1189    /// to the file.  
1190    /// Note: If downloading from the FASTSim Vehicle Repo, the
1191    /// vehicle_file_name should include the path to the file from the root of
1192    /// the Repo, as listed in the output of the
1193    /// vehicle_utils::fetch_github_list() function.  
1194    /// Note: the url should not include the file name, only the path to the
1195    /// file or a root directory of the file.
1196    pub fn from_github_or_url<S: AsRef<str>>(
1197        vehicle_file_name: S,
1198        url: Option<S>,
1199    ) -> anyhow::Result<Self> {
1200        let url_internal = match url {
1201            Some(s) => {
1202                s.as_ref().trim_end_matches('/').to_owned()
1203                    + "/"
1204                    + vehicle_file_name.as_ref().trim_start_matches('/')
1205            }
1206            None => Self::VEHICLE_DIRECTORY_URL.to_string() + vehicle_file_name.as_ref(),
1207        };
1208        let mut vehicle = Self::from_url(&url_internal, false)
1209            .with_context(|| "Could not parse vehicle from url")?;
1210        let vehicle_origin = "Vehicle from ".to_owned() + url_internal.as_str();
1211        vehicle.doc = Some(vehicle_origin);
1212        Ok(vehicle)
1213    }
1214
1215    /// Skews the peak of motor efficiency curve to new x-value, redistributing other
1216    /// x-values linearly, preserving relative distances between peak and endpoints.  
1217    /// # Arguments  
1218    /// - `new_peak_x`: new x-value at which to relocate peak  
1219    pub fn set_mc_eff_peak_pwr(&mut self, new_peak_x: f64) -> anyhow::Result<()> {
1220        let short_arrays = skewness_shift(&self.mc_pwr_out_perc, &self.mc_eff_map, new_peak_x)?;
1221        self.mc_pwr_out_perc = short_arrays.0;
1222        self.mc_eff_map = short_arrays.1.clone();
1223        self.mc_eff_array = short_arrays.1;
1224        for (idx, x) in self.mc_perc_out_array.iter().enumerate() {
1225            self.mc_full_eff_array.push(if idx == 0 {
1226                0.0
1227            } else {
1228                interpolate(x, &self.mc_pwr_out_perc, &self.mc_eff_array, false)
1229                    .with_context(|| format_dbg!())?
1230            });
1231        }
1232        Ok(())
1233    }
1234
1235    /// Skews the peak of fc efficiency curve to new x-value, redistributing other
1236    /// x-values linearly, preserving relative distances between peak and endpoints.  
1237    /// # Arguments
1238    /// - `new_peak_x`: new x-value at which to relocate peak  
1239    pub fn set_fc_eff_peak_pwr(&mut self, new_peak_x: f64) -> anyhow::Result<()> {
1240        let short_arrays = skewness_shift(&self.fc_pwr_out_perc, &self.fc_eff_map, new_peak_x)?;
1241        self.fc_pwr_out_perc = short_arrays.0;
1242        self.fc_eff_array = short_arrays.1.to_vec();
1243        self.fc_eff_map = short_arrays.1;
1244        Ok(())
1245    }
1246}
1247
1248impl Default for RustVehicle {
1249    fn default() -> Self {
1250        let mut veh = RustVehicle::mock_vehicle();
1251        veh.scenario_name = Default::default();
1252        veh.selection = Default::default();
1253        veh.veh_year = Default::default();
1254        veh.val_udds_kwh_per_mile = Default::default();
1255        veh.val_hwy_kwh_per_mile = Default::default();
1256        veh.val_comb_kwh_per_mile = Default::default();
1257        veh.val_cd_range_mi = Default::default();
1258        veh.val_const65_mph_kwh_per_mile = Default::default();
1259        veh.val_const60_mph_kwh_per_mile = Default::default();
1260        veh.val_const55_mph_kwh_per_mile = Default::default();
1261        veh.val_const45_mph_kwh_per_mile = Default::default();
1262        veh.val_unadj_udds_kwh_per_mile = Default::default();
1263        veh.val_unadj_hwy_kwh_per_mile = Default::default();
1264        veh.val0_to60_mph = Default::default();
1265        veh.val_ess_life_miles = Default::default();
1266        veh.val_range_miles = Default::default();
1267        veh.val_veh_base_cost = Default::default();
1268        veh.val_msrp = Default::default();
1269        veh
1270    }
1271}
1272
1273impl SerdeAPI for RustVehicle {
1274    const RESOURCE_PREFIX: &'static str = "vehicles";
1275    const CACHE_FOLDER: &'static str = "vehicles";
1276
1277    fn init(&mut self) -> anyhow::Result<()> {
1278        self.set_derived()
1279    }
1280
1281    /// instantiates a vehicle from a url, and notes in vehicle.doc the origin
1282    /// of the vehicle.  
1283    /// accepts yaml and json file types  
1284    /// # Arguments  
1285    /// - url: URL (either as a string or url type) to object  
1286    ///
1287    /// Note: The URL needs to be a URL pointing directly to a file, for example
1288    /// a raw github URL.
1289    fn from_url<S: AsRef<str>>(url: S, skip_init: bool) -> anyhow::Result<Self> {
1290        let url = url::Url::parse(url.as_ref())?;
1291        let format = url
1292            .path_segments()
1293            .and_then(|segments| segments.last())
1294            .and_then(|filename| Path::new(filename).extension())
1295            .and_then(OsStr::to_str)
1296            .with_context(|| "Could not parse file format from URL: {url:?}")?;
1297        let response = ureq::get(url.as_ref()).call()?.into_reader();
1298        let mut vehicle = Self::from_reader(response, format, skip_init)?;
1299        let vehicle_origin = "Vehicle from ".to_owned() + url.as_ref();
1300        vehicle.doc = Some(vehicle_origin);
1301        Ok(vehicle)
1302    }
1303}
1304
1305#[cfg(test)]
1306mod tests {
1307    use super::*;
1308    #[cfg(feature = "validation")]
1309    use validator::ValidationErrors;
1310
1311    #[test]
1312    fn test_set_derived_via_new() {
1313        let veh = RustVehicle::mock_vehicle();
1314        assert!(veh.veh_kg > 0.0);
1315    }
1316
1317    #[test]
1318    fn test_set_mc_eff_range() {
1319        let mut veh = RustVehicle::mock_vehicle();
1320        veh.set_mc_eff_range(0.7).unwrap();
1321        assert!(
1322            0.699 < veh.get_mc_eff_range().unwrap() && veh.get_mc_eff_range().unwrap() <= 0.701
1323        );
1324        veh.set_mc_eff_range(0.5).unwrap();
1325        assert!(
1326            0.499 < veh.get_mc_eff_range().unwrap() && veh.get_mc_eff_range().unwrap() <= 0.501
1327        );
1328        veh.set_mc_eff_range(0.).unwrap();
1329        assert!(veh.get_mc_eff_range().unwrap() == 0.);
1330    }
1331
1332    #[test]
1333    fn test_veh_kg_override() {
1334        let veh_file = resources_path().join("vehdb/test_overrides.yaml");
1335        let veh = RustVehicle::from_file(veh_file, false).unwrap();
1336        assert!(veh.veh_kg == veh.veh_override_kg.unwrap());
1337        // test input validation by providing bad inputs, then checking
1338        // the produced error for the offending field names
1339    }
1340
1341    #[cfg(feature = "validation")]
1342    #[test]
1343    fn test_input_validation() {
1344        // set up vehicle input parameters
1345        let scenario_name = String::from("2016 FORD Escape 4cyl 2WD");
1346        let selection = 5;
1347        let veh_year = 2016;
1348        let veh_pt_type = String::from("whoops"); // bad input
1349        let drag_coef = 0.355;
1350        let frontal_area_m2 = 3.066;
1351        let glider_kg = -50.0; // bad input
1352        let veh_cg_m = 0.53;
1353        let drive_axle_weight_frac = 0.59;
1354        let wheel_base_m = 2.6;
1355        let cargo_kg = 136.0;
1356        let veh_override_kg = None;
1357        let comp_mass_multiplier = 1.4;
1358        let fs_max_kw = 2000.0;
1359        let fs_secs_to_peak_pwr = 1.0;
1360        let fs_kwh = 504.0;
1361        let fs_kwh_per_kg = 9.89;
1362        let fc_max_kw = -60.0; // bad input
1363        let fc_pwr_out_perc = vec![
1364            0.0, 0.005, 0.015, 0.04, 0.06, 0.1, 0.14, 0.2, 0.4, 0.6, 0.8, 1.0,
1365        ];
1366        let fc_eff_type = String::from("SI");
1367        let fc_sec_to_peak_pwr = 6.0;
1368        let fc_base_kg = 61.0;
1369        let fc_kw_per_kg = 2.13;
1370        let min_fc_time_on = 30.0;
1371        let idle_fc_kw = 2.5;
1372        let mc_max_kw = 0.0;
1373        let mc_sec_to_peak_pwr = 4.0;
1374        let mc_pe_kg_per_kw = 0.833;
1375        let mc_pe_base_kg = 21.6;
1376        let ess_max_kw = 0.0;
1377        let ess_max_kwh = 0.0;
1378        let ess_kg_per_kwh = 8.0;
1379        let ess_base_kg = 75.0;
1380        let ess_round_trip_eff = 0.97;
1381        let ess_life_coef_a = 110.0;
1382        let ess_life_coef_b = -0.6811;
1383        let min_soc = -0.5; // bad input
1384        let max_soc = 1.5; // bad input
1385        let ess_dischg_to_fc_max_eff_perc = 0.0;
1386        let ess_chg_to_fc_max_eff_perc = 0.0;
1387        let wheel_inertia_kg_m2 = 0.815;
1388        let num_wheels = 4.0;
1389        let wheel_rr_coef = 0.006;
1390        let wheel_radius_m = 0.336;
1391        let wheel_coef_of_fric = 0.7;
1392        let max_accel_buffer_mph = 60.0;
1393        let max_accel_buffer_perc_of_useable_soc = 0.2;
1394        let perc_high_acc_buf = 0.0;
1395        let mph_fc_on = 30.0;
1396        let kw_demand_fc_on = 100.0;
1397        let max_regen = 0.98;
1398        let stop_start = false;
1399        let force_aux_on_fc = true;
1400        let alt_eff = 1.0;
1401        let chg_eff = 0.86;
1402        let aux_kw = 0.7;
1403        let trans_kg = 114.0;
1404        let trans_eff = 0.92;
1405        let ess_to_fuel_ok_error = 0.005;
1406        let val_udds_mpgge = 23.0;
1407        let val_hwy_mpgge = 32.0;
1408        let val_comb_mpgge = 26.0;
1409        let val_udds_kwh_per_mile = f64::NAN;
1410        let val_hwy_kwh_per_mile = f64::NAN;
1411        let val_comb_kwh_per_mile = f64::NAN;
1412        let val_cd_range_mi = f64::NAN;
1413        let val_const65_mph_kwh_per_mile = f64::NAN;
1414        let val_const60_mph_kwh_per_mile = f64::NAN;
1415        let val_const55_mph_kwh_per_mile = f64::NAN;
1416        let val_const45_mph_kwh_per_mile = f64::NAN;
1417        let val_unadj_udds_kwh_per_mile = f64::NAN;
1418        let val_unadj_hwy_kwh_per_mile = f64::NAN;
1419        let val0_to60_mph = 9.9;
1420        let val_ess_life_miles = f64::NAN;
1421        let val_range_miles = f64::NAN;
1422        let val_veh_base_cost = f64::NAN;
1423        let val_msrp = f64::NAN;
1424        let props = RustPhysicalProperties::default();
1425        let regen_a = 500.0;
1426        let regen_b = 0.99;
1427        let fc_peak_eff_override = None;
1428        let mc_peak_eff_override = Some(-0.50); // bad input
1429        let small_motor_power_kw = 7.5;
1430        let large_motor_power_kw = 75.0;
1431        let fc_perc_out_array = FC_PERC_OUT_ARRAY.clone().to_vec();
1432        let mc_eff_map = Array1::<f64>::zeros(LARGE_BASELINE_EFF.len());
1433        let mc_kw_out_array =
1434            (Array::linspace(0.0, 1.0, MC_PERC_OUT_ARRAY.len()) * mc_max_kw).to_vec();
1435        let mc_perc_out_array = MC_PERC_OUT_ARRAY.clone().to_vec();
1436        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];
1437        let mc_full_eff_array: Vec<f64> = mc_perc_out_array
1438            .iter()
1439            .enumerate()
1440            .map(|(idx, &x): (usize, &f64)| -> f64 {
1441                if idx == 0 {
1442                    0.0
1443                } else {
1444                    interpolate(&x, &mc_pwr_out_perc, &mc_eff_map, false).unwrap()
1445                }
1446            })
1447            .collect();
1448        let mc_kw_in_array: Vec<f64> = [0.0; 101]
1449            .iter()
1450            .enumerate()
1451            .map(|(idx, _)| {
1452                if idx == 0 {
1453                    0.0
1454                } else {
1455                    mc_kw_out_array[idx] / mc_full_eff_array[idx]
1456                }
1457            })
1458            .collect();
1459        let mc_max_elec_in_kw = arrmax(&mc_kw_in_array);
1460
1461        // instantiate vehicle result
1462        let mut veh = RustVehicle {
1463            small_motor_power_kw,
1464            large_motor_power_kw,
1465            fc_perc_out_array: FC_PERC_OUT_ARRAY.clone().to_vec(),
1466            charging_on: Default::default(),
1467            no_elec_sys: Default::default(),
1468            no_elec_aux: Default::default(),
1469            max_roadway_chg_kw: Default::default(),
1470            input_kw_out_array: Array1::from_vec(fc_pwr_out_perc.clone()) * fc_max_kw,
1471            fc_kw_out_array: fc_perc_out_array.iter().map(|n| n * fc_max_kw).collect(),
1472            fc_eff_array: fc_perc_out_array
1473                .iter()
1474                .map(|x: &f64| -> f64 {
1475                    interpolate(
1476                        x,
1477                        &Array1::from(fc_pwr_out_perc.to_vec()),
1478                        &array![
1479                            0.10, 0.12, 0.16, 0.22, 0.28, 0.33, 0.35, 0.36, 0.35, 0.34, 0.32, 0.30
1480                        ],
1481                        false,
1482                    )
1483                    .unwrap()
1484                })
1485                .collect(),
1486            modern_max: MODERN_MAX,
1487            mc_eff_array: mc_eff_map,
1488            mc_kw_in_array: [0.0; 101]
1489                .iter()
1490                .enumerate()
1491                .map(|(idx, _)| {
1492                    if idx == 0 {
1493                        0.0
1494                    } else {
1495                        mc_kw_out_array[idx] / mc_full_eff_array[idx]
1496                    }
1497                })
1498                .collect(),
1499            mc_kw_out_array,
1500            mc_max_elec_in_kw,
1501            mc_full_eff_array,
1502            // these get calculated in `se
1503            veh_kg: Default::default(),
1504            max_trac_mps2: Default::default(),
1505            ess_mass_kg: Default::default(),
1506            mc_mass_kg: Default::default(),
1507            fc_mass_kg: Default::default(),
1508            fs_mass_kg: Default::default(),
1509            mc_perc_out_array,
1510            orphaned: Default::default(),
1511            scenario_name,
1512            selection,
1513            veh_year,
1514            veh_pt_type, // bad input
1515            drag_coef,
1516            frontal_area_m2,
1517            glider_kg, // bad input
1518            veh_cg_m,
1519            drive_axle_weight_frac,
1520            wheel_base_m,
1521            cargo_kg,
1522            veh_override_kg,
1523            comp_mass_multiplier,
1524            fs_max_kw,
1525            fs_secs_to_peak_pwr,
1526            fs_kwh,
1527            fs_kwh_per_kg,
1528            fc_max_kw, // bad input
1529            fc_pwr_out_perc: array![
1530                0.0, 0.005, 0.015, 0.04, 0.06, 0.1, 0.14, 0.2, 0.4, 0.6, 0.8, 1.0,
1531            ],
1532            fc_eff_map: array![
1533                0.1, 0.12, 0.16, 0.22, 0.28, 0.33, 0.35, 0.36, 0.35, 0.34, 0.32, 0.3,
1534            ],
1535            fc_eff_type,
1536            fc_sec_to_peak_pwr,
1537            fc_base_kg,
1538            fc_kw_per_kg,
1539            min_fc_time_on,
1540            idle_fc_kw,
1541            mc_max_kw,
1542            mc_pwr_out_perc,
1543            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],
1544            mc_sec_to_peak_pwr,
1545            mc_pe_kg_per_kw,
1546            mc_pe_base_kg,
1547            ess_max_kw,
1548            ess_max_kwh,
1549            ess_kg_per_kwh,
1550            ess_base_kg,
1551            ess_round_trip_eff,
1552            ess_life_coef_a,
1553            ess_life_coef_b,
1554            min_soc, // bad input
1555            max_soc, // bad input
1556            ess_dischg_to_fc_max_eff_perc,
1557            ess_chg_to_fc_max_eff_perc,
1558            wheel_inertia_kg_m2,
1559            num_wheels,
1560            wheel_rr_coef,
1561            wheel_radius_m,
1562            wheel_coef_of_fric,
1563            max_accel_buffer_mph,
1564            max_accel_buffer_perc_of_useable_soc,
1565            perc_high_acc_buf,
1566            mph_fc_on,
1567            kw_demand_fc_on,
1568            max_regen,
1569            stop_start,
1570            force_aux_on_fc,
1571            alt_eff,
1572            chg_eff,
1573            aux_kw,
1574            trans_kg,
1575            trans_eff,
1576            ess_to_fuel_ok_error,
1577            val_udds_mpgge,
1578            val_hwy_mpgge,
1579            val_comb_mpgge,
1580            val_udds_kwh_per_mile,
1581            val_hwy_kwh_per_mile,
1582            val_comb_kwh_per_mile,
1583            val_cd_range_mi,
1584            val_const65_mph_kwh_per_mile,
1585            val_const60_mph_kwh_per_mile,
1586            val_const55_mph_kwh_per_mile,
1587            val_const45_mph_kwh_per_mile,
1588            val_unadj_udds_kwh_per_mile,
1589            val_unadj_hwy_kwh_per_mile,
1590            val0_to60_mph,
1591            val_ess_life_miles,
1592            val_range_miles,
1593            val_veh_base_cost,
1594            val_msrp,
1595            props,
1596            regen_a,
1597            regen_b,
1598            fc_peak_eff_override,
1599            mc_peak_eff_override, // bad input
1600            ..Default::default()
1601        };
1602
1603        let validation_result = veh.set_derived();
1604
1605        // hard-coded fields where bad inputs were provided above
1606        let bad_fields = [
1607            "veh_pt_type",
1608            "glider_kg",
1609            "fc_max_kw",
1610            "min_soc",
1611            "max_soc",
1612            "mc_peak_eff_override",
1613        ];
1614        // downcast anyhow::error back into validator::ValidationErrors
1615        // this test will fail on the unwrap() if the error is not downcastable to ValidationErrors
1616        // e.g. if the error was not from input validation
1617        let validation_errs = validation_result
1618            .unwrap_err()
1619            .downcast::<ValidationErrors>()
1620            .unwrap();
1621        let validation_errs_hashmap = validation_errs.errors();
1622        // assert that specified bad fields were caught
1623        assert!(validation_errs_hashmap
1624            .keys()
1625            .all(|key| bad_fields.contains(key)));
1626        assert!(validation_errs_hashmap.len() == bad_fields.len());
1627    }
1628}