1use crate::calibration::skewness_shift;
4use 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
18pub 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
29pub 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 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 pub fn to_rust(&self) -> Self {
104 self.clone()
105 }
106
107 #[pyo3(name = "list_resources")]
108 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)]
137pub struct RustVehicle {
147 #[serde(skip)]
148 #[api(has_orphaned)]
149 #[doc_field(skip_doc)]
151 pub props: RustPhysicalProperties,
152 #[serde(alias = "name")]
154 #[doc_field(skip_doc)]
155 pub scenario_name: String,
156 #[serde(skip)]
158 #[doc_field(skip_doc)]
159 pub selection: u32,
160 #[serde(alias = "vehModelYear")]
162 #[doc_field(skip_doc)]
163 pub veh_year: u32,
164 #[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 #[serde(alias = "dragCoef")]
177 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
178 pub drag_coef: f64,
179 #[serde(alias = "frontalAreaM2")]
181 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
182 pub frontal_area_m2: f64,
183 #[serde(alias = "gliderKg")]
185 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
186 pub glider_kg: f64,
187 #[serde(alias = "vehCgM")]
190 pub veh_cg_m: f64,
191 #[serde(alias = "driveAxleWeightFrac")]
193 #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))]
194 pub drive_axle_weight_frac: f64,
195 #[serde(alias = "wheelBaseM")]
197 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
198 pub wheel_base_m: f64,
199 #[serde(alias = "cargoKg")]
201 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
202 pub cargo_kg: f64,
203 #[serde(alias = "vehOverrideKg")]
205 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
206 pub veh_override_kg: Option<f64>,
207 #[serde(alias = "compMassMultiplier")]
209 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
210 pub comp_mass_multiplier: f64,
211 #[serde(alias = "maxFuelStorKw")]
213 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
214 pub fs_max_kw: f64,
215 #[serde(alias = "fuelStorSecsToPeakPwr")]
217 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
218 pub fs_secs_to_peak_pwr: f64,
219 #[serde(alias = "fuelStorKwh")]
221 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
222 pub fs_kwh: f64,
223 #[serde(alias = "fuelStorKwhPerKg")]
225 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
226 pub fs_kwh_per_kg: f64,
227 #[serde(alias = "maxFuelConvKw")]
229 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
230 pub fc_max_kw: f64,
231 #[serde(alias = "fcPwrOutPerc")]
233 pub fc_pwr_out_perc: Array1<f64>,
234 #[serde(default)]
236 pub fc_eff_map: Array1<f64>,
237 #[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 #[serde(alias = "fuelConvSecsToPeakPwr")]
250 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
251 pub fc_sec_to_peak_pwr: f64,
252 #[serde(alias = "fuelConvBaseKg")]
254 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
255 pub fc_base_kg: f64,
256 #[serde(alias = "fuelConvKwPerKg")]
258 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
259 pub fc_kw_per_kg: f64,
260 #[serde(alias = "minFcTimeOn")]
262 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
263 pub min_fc_time_on: f64,
264 #[serde(alias = "idleFcKw")]
266 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
267 pub idle_fc_kw: f64,
268 #[serde(alias = "mcMaxElecInKw")]
270 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
271 pub mc_max_kw: f64,
272 #[serde(alias = "mcPwrOutPerc")]
274 pub mc_pwr_out_perc: Array1<f64>,
275 #[serde(alias = "mcEffArray")]
277 pub mc_eff_map: Array1<f64>,
278 #[serde(alias = "motorSecsToPeakPwr")]
280 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
281 pub mc_sec_to_peak_pwr: f64,
282 #[serde(alias = "mcPeKgPerKw")]
284 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
285 pub mc_pe_kg_per_kw: f64,
286 #[serde(alias = "mcPeBaseKg")]
288 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
289 pub mc_pe_base_kg: f64,
290 #[serde(alias = "maxEssKw")]
292 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
293 pub ess_max_kw: f64,
294 #[serde(alias = "maxEssKwh")]
296 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
297 pub ess_max_kwh: f64,
298 #[serde(alias = "essKgPerKwh")]
300 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
301 pub ess_kg_per_kwh: f64,
302 #[serde(alias = "essBaseKg")]
304 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
305 pub ess_base_kg: f64,
306 #[serde(alias = "essRoundTripEff")]
308 #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))]
309 pub ess_round_trip_eff: f64,
310 #[serde(alias = "essLifeCoefA")]
312 pub ess_life_coef_a: f64,
313 #[serde(alias = "essLifeCoefB")]
315 pub ess_life_coef_b: f64,
316 #[serde(alias = "minSoc")]
318 #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))]
319 pub min_soc: f64,
320 #[serde(alias = "maxSoc")]
322 #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))]
323 pub max_soc: f64,
324 #[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 #[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 #[serde(alias = "wheelInertiaKgM2")]
334 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
335 pub wheel_inertia_kg_m2: f64,
336 #[serde(alias = "numWheels")]
338 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
339 pub num_wheels: f64, #[serde(alias = "wheelRrCoef")]
342 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
343 pub wheel_rr_coef: f64,
344 #[serde(alias = "wheelRadiusM")]
346 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
347 pub wheel_radius_m: f64,
348 #[serde(alias = "wheelCoefOfFric")]
350 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
351 pub wheel_coef_of_fric: f64,
352 #[serde(alias = "maxAccelBufferMph")]
354 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
355 pub max_accel_buffer_mph: f64,
356 #[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 #[serde(alias = "percHighAccBuf")]
362 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
363 pub perc_high_acc_buf: f64,
364 #[serde(alias = "mphFcOn")]
366 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
367 pub mph_fc_on: f64,
368 #[serde(alias = "kwDemandFcOn")]
370 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
371 pub kw_demand_fc_on: f64,
372 #[serde(alias = "maxRegen")]
374 #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))]
375 pub max_regen: f64,
376 pub stop_start: bool,
378 #[serde(alias = "forceAuxOnFC")]
380 pub force_aux_on_fc: bool,
381 #[serde(alias = "altEff")]
383 #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))]
384 pub alt_eff: f64,
385 #[serde(alias = "chgEff")]
387 #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))]
388 pub chg_eff: f64,
389 #[serde(alias = "auxKw")]
391 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
392 pub aux_kw: f64,
393 #[serde(alias = "transKg")]
395 #[cfg_attr(feature = "validation", validate(range(min = 0)))]
396 pub trans_kg: f64,
397 #[serde(alias = "transEff")]
399 #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))]
400 pub trans_eff: f64,
401 #[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 #[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 #[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 #[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 #[serde(skip)]
585 #[cfg_attr(feature = "validation", validate(range(min = 0, max = 1)))]
586 pub fc_peak_eff_override: Option<f64>,
587 #[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
601impl RustVehicle {
603 const VEHICLE_DIRECTORY_URL: &'static str =
604 "https://raw.githubusercontent.com/NREL/fastsim-vehicles/main/";
605 #[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 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 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 pub fn get_mc_eff_min(&self) -> anyhow::Result<&f64> {
695 self.mc_eff_array.min()
696 }
697
698 pub fn get_mc_eff_max(&self) -> anyhow::Result<&f64> {
700 self.mc_eff_array.max()
701 }
702
703 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 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 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 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 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 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 pub fn set_derived(&mut self) -> anyhow::Result<()> {
831 #[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 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 self.no_elec_aux =
880 self.no_elec_sys || (self.mc_max_kw <= self.aux_kw) || self.force_aux_on_fc;
881
882 self.fc_perc_out_array = FC_PERC_OUT_ARRAY.clone().to_vec();
884
885 self.input_kw_out_array = &self.fc_pwr_out_perc * self.fc_max_kw;
887 self.fc_kw_out_array = self
889 .fc_perc_out_array
890 .iter()
891 .map(|n| n * self.fc_max_kw)
892 .collect();
893 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 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 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 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 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 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 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 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 }
1341
1342 #[cfg(feature = "validation")]
1343 #[test]
1344 fn test_input_validation() {
1345 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"); let drag_coef = 0.355;
1351 let frontal_area_m2 = 3.066;
1352 let glider_kg = -50.0; 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; 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; let max_soc = 1.5; 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); 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 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 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, drag_coef,
1517 frontal_area_m2,
1518 glider_kg, 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, 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, max_soc, 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, ..Default::default()
1602 };
1603
1604 let validation_result = veh.set_derived();
1605
1606 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 let validation_errs = validation_result
1619 .unwrap_err()
1620 .downcast::<ValidationErrors>()
1621 .unwrap();
1622 let validation_errs_hashmap = validation_errs.errors();
1623 assert!(validation_errs_hashmap
1625 .keys()
1626 .all(|key| bad_fields.contains(key)));
1627 assert!(validation_errs_hashmap.len() == bad_fields.len());
1628 }
1629}