1use crate::prelude::*;
2#[cfg(feature = "resources")]
3use crate::resources;
4
5use super::{hev::HEVPowertrainControls, *};
6pub mod fastsim2_interface;
7
8#[derive(
10 Clone, Debug, Serialize, Deserialize, PartialEq, IsVariant, derive_more::From, TryInto,
11)]
12pub enum AuxSource {
13 ReversibleEnergyStorage,
16 FuelConverter,
19}
20
21impl SerdeAPI for AuxSource {}
22impl Init for AuxSource {}
23
24#[serde_api]
25#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
26#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, StateMethods)]
27#[non_exhaustive]
28#[serde(deny_unknown_fields)]
29pub struct Vehicle {
31 name: String,
33 year: u32,
35 #[has_state]
36 pub pt_type: PowertrainType,
38
39 pub chassis: Chassis,
41
42 #[serde(default, skip_serializing_if = "CabinOption::is_none")]
44 #[has_state]
45 pub cabin: CabinOption,
46
47 #[serde(default, skip_serializing_if = "HVACOption::is_none")]
49 #[has_state]
50 pub hvac: HVACOption,
51
52 pub(crate) mass: Option<si::Mass>,
54
55 pub pwr_aux_base: si::Power,
57
58 pub trans_eff: si::Ratio,
63
64 save_interval: Option<usize>,
66 #[serde(default)]
68 pub state: VehicleState,
69 #[serde(default, skip_serializing_if = "VehicleStateHistoryVec::is_empty")]
71 pub history: VehicleStateHistoryVec,
72}
73
74#[named_struct_pyo3_api]
75impl Vehicle {
76 #[staticmethod]
77 fn try_from_fastsim2(veh: fastsim_2::vehicle::RustVehicle) -> PyResult<Vehicle> {
78 Ok(Self::try_from(veh.clone())?)
79 }
80
81 #[pyo3(name = "set_save_interval")]
82 #[pyo3(signature = (save_interval=None))]
83 fn set_save_interval_py(&mut self, save_interval: Option<usize>) -> PyResult<()> {
85 self.set_save_interval(save_interval)
86 .map_err(|e| PyAttributeError::new_err(e.to_string()))
87 }
88
89 #[getter("save_interval")]
91 fn get_save_interval_py(&self) -> anyhow::Result<Option<usize>> {
93 self.save_interval()
94 }
95
96 #[getter]
97 fn get_fc(&self) -> Option<FuelConverter> {
98 self.fc().cloned()
99 }
100
101 #[getter]
102 fn get_res(&self) -> Option<ReversibleEnergyStorage> {
103 self.res().cloned()
104 }
105
106 #[getter]
107 fn get_em(&self) -> Option<ElectricMachine> {
108 self.em().cloned()
109 }
110
111 fn veh_type(&self) -> String {
112 self.pt_type.to_string()
113 }
114
115 #[cfg(feature = "resources")]
126 #[pyo3(name = "list_resources")]
127 #[staticmethod]
128 fn list_resources_py() -> Vec<String> {
130 resources::list_resources(Self::RESOURCE_PREFIX)
131 }
132
133 #[pyo3(name = "from_f2_file")]
135 #[staticmethod]
136 fn from_f2_file_py(file: PathBuf) -> anyhow::Result<Self> {
137 Self::from_f2_file(file)
138 }
139
140 #[pyo3(name = "clear")]
141 fn clear_py(&mut self) {
142 self.clear()
143 }
144}
145
146impl Mass for Vehicle {
147 fn mass(&self) -> anyhow::Result<Option<si::Mass>> {
148 let derived_mass = self
149 .derived_mass()
150 .with_context(|| anyhow!(format_dbg!()))?;
151 match (derived_mass, self.mass) {
152 (Some(derived_mass), Some(set_mass)) => {
153 ensure!(
154 utils::almost_eq_uom(&set_mass, &derived_mass, None),
155 format!(
156 "{}",
157 format_dbg!(utils::almost_eq_uom(&set_mass, &derived_mass, None)),
158 )
159 );
160 Ok(Some(set_mass))
161 }
162 (None, None) => bail!(
163 "Not all mass fields in `{}` are set and no mass was previously set.",
164 stringify!(Vehicle)
165 ),
166 _ => Ok(self.mass.or(derived_mass)),
167 }
168 }
169
170 fn set_mass(
171 &mut self,
172 new_mass: Option<si::Mass>,
173 side_effect: MassSideEffect,
174 ) -> anyhow::Result<()> {
175 ensure!(
176 side_effect == MassSideEffect::None,
177 "At the vehicle level, only `MassSideEffect::None` is allowed"
178 );
179
180 let derived_mass = self
181 .derived_mass()
182 .with_context(|| anyhow!(format_dbg!()))?;
183 self.mass = match new_mass {
184 Some(new_mass) => {
186 if let Some(dm) = derived_mass {
187 if dm != new_mass {
188 self.expunge_mass_fields();
189 }
190 }
191 Some(new_mass)
192 }
193 None => Some(derived_mass.with_context(|| {
195 format!(
196 "Not all mass fields in `{}` are set and no mass was provided.",
197 stringify!(Vehicle)
198 )
199 })?),
200 };
201 Ok(())
202 }
203
204 fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
205 let chassis_mass = self
206 .chassis
207 .mass()
208 .with_context(|| anyhow!(format_dbg!()))?;
209 let pt_mass = match &self.pt_type {
210 PowertrainType::ConventionalVehicle(conv) => conv.mass()?,
211 PowertrainType::HybridElectricVehicle(hev) => hev.mass()?,
212 PowertrainType::BatteryElectricVehicle(bev) => bev.mass()?,
213 };
214 if let (Some(pt_mass), Some(chassis_mass)) = (pt_mass, chassis_mass) {
215 Ok(Some(pt_mass + chassis_mass))
216 } else {
217 Ok(None)
218 }
219 }
220
221 fn expunge_mass_fields(&mut self) {
222 self.chassis.expunge_mass_fields();
223 match &mut self.pt_type {
224 PowertrainType::ConventionalVehicle(conv) => conv.expunge_mass_fields(),
225 PowertrainType::HybridElectricVehicle(hev) => hev.expunge_mass_fields(),
226 PowertrainType::BatteryElectricVehicle(bev) => bev.expunge_mass_fields(),
227 };
228 }
229}
230
231impl SerdeAPI for Vehicle {
232 #[cfg(feature = "resources")]
233 const RESOURCE_PREFIX: &'static str = "vehicles";
234}
235impl Init for Vehicle {
236 fn init(&mut self) -> Result<(), Error> {
237 let _mass = self
238 .mass()
239 .map_err(|err| Error::InitError(format_dbg!(err)))?;
240 self.calculate_wheel_radius()
241 .map_err(|err| Error::InitError(format_dbg!(err)))?;
242 self.pt_type
243 .init()
244 .map_err(|err| Error::InitError(format_dbg!(err)))?;
245 Ok(())
246 }
247}
248
249impl HistoryMethods for Vehicle {
250 fn save_interval(&self) -> anyhow::Result<Option<usize>> {
251 Ok(self.save_interval)
252 }
253 fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
254 self.save_interval = save_interval;
255 self.pt_type.set_save_interval(save_interval)?;
256 self.cabin.set_save_interval(save_interval)?;
257 self.hvac.set_save_interval(save_interval)?;
258 Ok(())
259 }
260 fn clear(&mut self) {
261 self.history.clear();
262 self.pt_type.clear();
263 self.cabin.clear();
264 self.hvac.clear();
265 }
266}
267
268const FUEL_LHV_MJ_PER_KG: f64 = 43.2;
270const CONV: &str = "Conv";
271const HEV: &str = "HEV";
272const PHEV: &str = "PHEV";
273const BEV: &str = "BEV";
274
275impl SetCumulative for Vehicle {
276 fn set_cumulative(&mut self, dt: si::Time) -> anyhow::Result<()> {
277 self.state.set_cumulative(dt)?;
278 self.pt_type.set_cumulative(dt)?;
279 self.cabin.set_cumulative(dt)?;
280 self.hvac.set_cumulative(dt)?;
281 self.state.dist.increment(
283 *self.state.speed_ach.get_fresh(|| format_dbg!())? * dt,
284 || format_dbg!(),
285 )?;
286 Ok(())
287 }
288}
289
290impl Vehicle {
291 pub fn get_pwr_rated(&self) -> si::Power {
294 match (self.fc(), self.res()) {
295 (Some(fc), Some(res)) => fc.pwr_out_max + res.pwr_out_max,
296 (Some(fc), None) => fc.pwr_out_max,
297 (None, Some(res)) => res.pwr_out_max,
298 (None, None) => unreachable!(),
299 }
300 }
301
302 pub fn conv(&self) -> Option<&ConventionalVehicle> {
303 self.pt_type.conv()
304 }
305
306 pub fn hev(&self) -> Option<&HybridElectricVehicle> {
307 self.pt_type.hev()
308 }
309
310 pub fn bev(&self) -> Option<&BatteryElectricVehicle> {
315 self.pt_type.bev()
316 }
317
318 pub fn conv_mut(&mut self) -> Option<&mut ConventionalVehicle> {
319 self.pt_type.conv_mut()
320 }
321
322 pub fn hev_mut(&mut self) -> Option<&mut HybridElectricVehicle> {
323 self.pt_type.hev_mut()
324 }
325
326 pub fn bev_mut(&mut self) -> Option<&mut BatteryElectricVehicle> {
331 self.pt_type.bev_mut()
332 }
333
334 pub fn fc(&self) -> Option<&FuelConverter> {
335 self.pt_type.fc()
336 }
337
338 pub fn fc_mut(&mut self) -> Option<&mut FuelConverter> {
339 self.pt_type.fc_mut()
340 }
341
342 pub fn set_fc(&mut self, fc: FuelConverter) -> anyhow::Result<()> {
343 self.pt_type.set_fc(fc)
344 }
345
346 pub fn fs(&self) -> Option<&FuelStorage> {
347 self.pt_type.fs()
348 }
349
350 pub fn fs_mut(&mut self) -> Option<&mut FuelStorage> {
351 self.pt_type.fs_mut()
352 }
353
354 pub fn set_fs(&mut self, fs: FuelStorage) -> anyhow::Result<()> {
355 self.pt_type.set_fs(fs)
356 }
357
358 pub fn res(&self) -> Option<&ReversibleEnergyStorage> {
359 self.pt_type.res()
360 }
361
362 pub fn res_mut(&mut self) -> Option<&mut ReversibleEnergyStorage> {
363 self.pt_type.res_mut()
364 }
365
366 pub fn set_res(&mut self, res: ReversibleEnergyStorage) -> anyhow::Result<()> {
367 self.pt_type.set_res(res)
368 }
369
370 pub fn em(&self) -> Option<&ElectricMachine> {
371 self.pt_type.em()
372 }
373
374 pub fn em_mut(&mut self) -> Option<&mut ElectricMachine> {
375 self.pt_type.em_mut()
376 }
377
378 pub fn set_em(&mut self, em: ElectricMachine) -> anyhow::Result<()> {
379 self.pt_type.set_em(em)
380 }
381
382 fn calculate_wheel_radius(&mut self) -> anyhow::Result<()> {
384 ensure!(
385 self.chassis.wheel_radius.is_some() || self.chassis.tire_code.is_some(),
386 "Either `wheel_radius` or `tire_code` must be supplied"
387 );
388 if self.chassis.wheel_radius.is_none() {
389 self.chassis.wheel_radius =
390 Some(utils::tire_code_to_radius(self.chassis.tire_code.as_ref().unwrap())? * uc::M)
391 }
392 Ok(())
393 }
394
395 pub fn solve_powertrain(&mut self, dt: si::Time) -> anyhow::Result<()> {
397 self.pt_type
398 .solve(
399 *self.state.pwr_tractive.get_fresh(|| format_dbg!())?,
400 true, dt,
402 )
403 .with_context(|| anyhow!(format_dbg!()))?;
404 self.state.pwr_brake.update(
405 -self
406 .state
407 .pwr_tractive
408 .get_fresh(|| format_dbg!())?
409 .max(si::Power::ZERO)
410 - self.pt_type.pwr_regen().with_context(|| format_dbg!())?,
411 || format_dbg!(),
412 )?;
413 Ok(())
414 }
415
416 pub fn set_curr_pwr_out_max(&mut self, dt: si::Time) -> anyhow::Result<()> {
417 self.pt_type
419 .set_curr_pwr_prop_out_max(
420 *self.state.pwr_aux.get_fresh(|| format_dbg!())?,
421 dt,
422 &self.state,
423 )
424 .with_context(|| anyhow!(format_dbg!()))?;
425 let pwr_prop_maxes = self
426 .pt_type
427 .get_curr_pwr_prop_out_max()
428 .with_context(|| anyhow!(format_dbg!()))?;
429 self.state
430 .pwr_prop_fwd_max
431 .update(pwr_prop_maxes.0, || format_dbg!())?;
432 self.state
433 .pwr_prop_bwd_max
434 .update(pwr_prop_maxes.1, || format_dbg!())?;
435
436 Ok(())
437 }
438
439 pub fn solve_thermal(
440 &mut self,
441 te_amb_air: si::Temperature,
442 dt: si::Time,
443 ) -> anyhow::Result<()> {
444 let te_fc: Option<si::Temperature> = self
445 .fc()
446 .and_then(|fc| fc.temperature().map(|fct| fct.get_stale(|| format_dbg!())))
447 .transpose()
448 .with_context(|| {
449 format!(
450 "{}\nfuel converter temperature has not been properly set",
451 format_dbg!()
452 )
453 })?
454 .copied();
455 let pwr_thrml_cab_to_res: si::Power = match self.res() {
456 Some(res) => match &res.thrml {
457 RESThermalOption::RESLumpedThermal(rlt) => {
458 *rlt.state.pwr_thrml_from_cabin.get_stale(|| format_dbg!())?
459 }
460 RESThermalOption::None => si::Power::ZERO,
461 },
462 None => si::Power::ZERO,
463 };
464
465 let res_thrml_state: Option<RESLumpedThermalState> =
466 self.res().and_then(|res| res.res_thrml_state().cloned());
467
468 let (pwr_thrml_fc_to_cabin, pwr_thrml_hvac_to_res, te_cab) =
469 self.solve_hvac_cab_res(te_amb_air, dt, te_fc, res_thrml_state, pwr_thrml_cab_to_res)?;
470
471 self.pt_type
472 .solve_thermal(
473 te_amb_air,
474 pwr_thrml_fc_to_cabin,
475 &mut self.state,
476 pwr_thrml_hvac_to_res,
477 te_cab,
478 dt,
479 )
480 .with_context(|| format_dbg!())?;
481 Ok(())
482 }
483
484 fn solve_hvac_cab_res(
485 &mut self,
486 te_amb_air: si::Temperature,
487 dt: si::Time,
488 te_fc: Option<si::Temperature>,
489 res_thrml_state: Option<RESLumpedThermalState>,
490 pwr_thrml_cab_to_res: si::Power,
491 ) -> anyhow::Result<(
492 Option<si::Power>,
493 Option<si::Power>,
494 Option<si::Temperature>,
495 )> {
496 let (pwr_thrml_fc_to_cabin, pwr_thrml_hvac_to_res, te_cab): (
497 Option<si::Power>,
498 Option<si::Power>,
499 Option<si::Temperature>,
500 ) = match (&mut self.cabin, &mut self.hvac) {
501 (CabinOption::None, HVACOption::None) => {
502 self.state
503 .pwr_aux
504 .update(self.pwr_aux_base, || format_dbg!())?;
505 (None, None, None)
506 }
507 (CabinOption::LumpedCabin(cab), HVACOption::LumpedCabin(hvac)) => {
508 let (pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cab) = hvac
509 .solve(te_amb_air, te_fc, &cab.state, cab.heat_capacitance, dt)
510 .with_context(|| format_dbg!())?;
511 let te_cab = cab
512 .solve(
513 te_amb_air,
514 &self.state,
515 pwr_thrml_hvac_to_cabin,
516 Default::default(),
517 dt,
518 )
519 .with_context(|| format_dbg!())?;
520 self.state.pwr_aux.update(
521 self.pwr_aux_base
522 + *hvac
523 .state
524 .pwr_aux_for_hvac
525 .get_fresh(|| format_dbg!("hvac.state.pwr_aux_for_hvac"))?,
526 || format_dbg!(),
527 )?;
528 (Some(pwr_thrml_fc_to_cab), None, Some(te_cab))
529 }
530 (CabinOption::LumpedCabin(cab), HVACOption::LumpedCabinAndRES(hvac)) => {
531 let (pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cab, pwr_thrml_hvac_to_res) = hvac
532 .solve(
533 te_amb_air,
534 te_fc,
535 &cab.state,
536 cab.heat_capacitance,
537 res_thrml_state
538 .with_context(
539 || "{}\n[HVACOption::LumpedCabinAndRES] requires [ReversibleEnergyStorage::thrml] to be `Some`"
540 )?,
541 dt,
542 )
543 .with_context(|| format_dbg!())?;
544 let te_cab = cab
545 .solve(
546 te_amb_air,
547 &self.state,
548 pwr_thrml_hvac_to_cabin,
549 pwr_thrml_cab_to_res,
550 dt,
551 )
552 .with_context(|| format_dbg!())?;
553 self.state.pwr_aux.update(
554 self.pwr_aux_base
555 + *hvac
556 .state
557 .pwr_aux_for_cab_hvac
558 .get_fresh(|| format_dbg!())?
559 + *hvac
560 .state
561 .pwr_aux_for_res_hvac
562 .get_fresh(|| format_dbg!())?,
563 || format_dbg!(),
564 )?;
565 ensure!(
566 *self.state.pwr_aux.get_fresh(|| format_dbg!())? > si::Power::ZERO,
567 format!(
568 "{}\n{}\n{}",
569 format_dbg!(self.state.pwr_aux),
570 format_dbg!(hvac.state.pwr_aux_for_res_hvac),
571 format_dbg!(hvac.state.pwr_aux_for_cab_hvac)
572 )
573 );
574 (
575 Some(pwr_thrml_fc_to_cab),
576 Some(pwr_thrml_hvac_to_res),
577 Some(te_cab),
578 )
579 }
580 (CabinOption::LumpedCabinWithShell, HVACOption::LumpedCabinWithShell) => {
581 bail!("{}\nNot yet implemented.", format_dbg!())
582 }
583 (CabinOption::None, HVACOption::ReversibleEnergyStorageOnly) => {
584 bail!("{}\nNot yet implemented.", format_dbg!())
585 }
586 (CabinOption::None, _) => {
587 bail!(
588 "{}\n`CabinOption::is_none` must be true if `HVACOption::is_none` is true.",
589 format_dbg!()
590 )
591 }
592 (_, HVACOption::None) => {
593 bail!(
594 "{}\n`CabinOption::is_none` must be true if `HVACOption::is_none` is true.",
595 format_dbg!()
596 )
597 }
598 _ => todo!(
599 "This match needs more match arms to be fully correct in validating model config."
600 ),
601 };
602 Ok((pwr_thrml_fc_to_cabin, pwr_thrml_hvac_to_res, te_cab))
603 }
604
605 fn from_f2_file(file: PathBuf) -> anyhow::Result<Self> {
606 use fastsim_2::traits::SerdeAPI;
607 let f2veh = fastsim_2::vehicle::RustVehicle::from_file(file, false)
608 .with_context(|| format_dbg!())?;
609 Self::try_from(f2veh)
610 }
611}
612
613#[serde_api]
615#[derive(
616 Clone, Debug, Deserialize, Serialize, PartialEq, HistoryVec, StateMethods, SetCumulative,
617)]
618#[non_exhaustive]
619#[serde(default)]
620#[serde(deny_unknown_fields)]
621pub struct VehicleState {
622 pub i: TrackedState<usize>,
624
625 pub time: TrackedState<si::Time>,
627
628 pub pwr_prop_fwd_max: TrackedState<si::Power>,
631 pub pwr_prop_bwd_max: TrackedState<si::Power>,
634 pub pwr_tractive: TrackedState<si::Power>,
636 pub pwr_tractive_for_cyc: TrackedState<si::Power>,
638 pub energy_tractive: TrackedState<si::Energy>,
640 pub pwr_aux: TrackedState<si::Power>,
642 pub energy_aux: TrackedState<si::Energy>,
644 pub pwr_drag: TrackedState<si::Power>,
646 pub energy_drag: TrackedState<si::Energy>,
648 pub pwr_accel: TrackedState<si::Power>,
650 pub energy_accel: TrackedState<si::Energy>,
652 pub pwr_ascent: TrackedState<si::Power>,
654 pub energy_ascent: TrackedState<si::Energy>,
656 pub pwr_rr: TrackedState<si::Power>,
658 pub energy_rr: TrackedState<si::Energy>,
660 pub pwr_whl_inertia: TrackedState<si::Power>,
662 pub energy_whl_inertia: TrackedState<si::Energy>,
664 pub pwr_brake: TrackedState<si::Power>,
666 pub energy_brake: TrackedState<si::Energy>,
668 pub cyc_met: TrackedState<bool>,
672 pub cyc_met_overall: TrackedState<bool>,
675 pub speed_ach: TrackedState<si::Velocity>,
677 pub dist: TrackedState<si::Length>,
679 pub grade_curr: TrackedState<si::Ratio>,
681 pub elev_curr: TrackedState<si::Length>,
684 pub air_density: TrackedState<si::MassDensity>,
686 pub mass: TrackedState<si::Mass>,
689}
690
691impl SerdeAPI for VehicleState {}
692impl Init for VehicleState {}
693impl Default for VehicleState {
694 fn default() -> Self {
695 Self {
696 i: TrackedState::new(Default::default()),
697 time: Default::default(),
698 pwr_prop_fwd_max: Default::default(),
699 pwr_prop_bwd_max: Default::default(),
700 pwr_tractive: Default::default(),
701 pwr_tractive_for_cyc: Default::default(),
702 energy_tractive: Default::default(),
703 pwr_aux: Default::default(),
704 energy_aux: Default::default(),
705 pwr_drag: Default::default(),
706 energy_drag: Default::default(),
707 pwr_accel: Default::default(),
708 energy_accel: Default::default(),
709 pwr_ascent: Default::default(),
710 energy_ascent: Default::default(),
711 pwr_rr: Default::default(),
712 energy_rr: Default::default(),
713 pwr_whl_inertia: Default::default(),
714 energy_whl_inertia: Default::default(),
715 pwr_brake: Default::default(),
716 energy_brake: Default::default(),
717 cyc_met: TrackedState::new(true),
718 cyc_met_overall: TrackedState::new(true),
719 speed_ach: Default::default(),
720 dist: Default::default(),
721 grade_curr: Default::default(),
723 elev_curr: Default::default(),
725 air_density: Default::default(),
726 mass: TrackedState::new(uc::KG * f64::NAN),
727 }
728 }
729}
730
731#[cfg(test)]
732pub(crate) mod tests {
733 use super::*;
734
735 #[allow(dead_code)]
736 fn vehicles_dir() -> PathBuf {
737 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/vehicles")
738 }
739
740 #[cfg(feature = "yaml")]
741 pub(crate) fn mock_conv_veh() -> Vehicle {
742 let file_contents = include_str!("fastsim-2_2012_Ford_Fusion.yaml");
743 use fastsim_2::traits::SerdeAPI;
744 let veh = {
745 let f2veh = fastsim_2::vehicle::RustVehicle::from_yaml(file_contents, false).unwrap();
746 let veh = Vehicle::try_from(f2veh);
747 veh.unwrap()
748 };
749
750 veh.to_file(vehicles_dir().join("2012_Ford_Fusion.yaml"))
751 .unwrap();
752 assert!(veh.pt_type.is_conventional_vehicle());
753 veh
754 }
755
756 #[cfg(feature = "yaml")]
757 pub(crate) fn mock_hev() -> Vehicle {
758 let file_contents = include_str!("fastsim-2_2016_TOYOTA_Prius_Two.yaml");
759 use fastsim_2::traits::SerdeAPI;
760 let veh = {
761 let f2veh = fastsim_2::vehicle::RustVehicle::from_yaml(file_contents, false).unwrap();
762 let veh = Vehicle::try_from(f2veh);
763 veh.unwrap()
764 };
765
766 veh.to_file(vehicles_dir().join("2016_TOYOTA_Prius_Two.yaml"))
767 .unwrap();
768 assert!(veh.pt_type.is_hybrid_electric_vehicle());
769 veh
770 }
771
772 #[cfg(feature = "yaml")]
773 pub(crate) fn mock_bev() -> Vehicle {
774 let file_contents = include_str!("fastsim-2_2022_Renault_Zoe_ZE50_R135.yaml");
775 use fastsim_2::traits::SerdeAPI;
776 let veh = {
777 let f2veh = fastsim_2::vehicle::RustVehicle::from_yaml(file_contents, false).unwrap();
778 let veh = Vehicle::try_from(f2veh);
779 veh.unwrap()
780 };
781
782 veh.to_file(vehicles_dir().join("2022_Renault_Zoe_ZE50_R135.yaml"))
783 .unwrap();
784 assert!(veh.pt_type.is_battery_electric_vehicle());
785 veh
786 }
787
788 #[test]
789 #[cfg(feature = "yaml")]
790 pub(crate) fn test_conv_veh_init() {
791 use pretty_assertions::assert_eq;
792 let veh = mock_conv_veh();
793 let mut veh1 = veh.clone();
794 assert_eq!(veh.to_yaml().unwrap(), veh1.to_yaml().unwrap());
797 veh1.init().unwrap();
798 assert_eq!(veh.to_yaml().unwrap(), veh1.to_yaml().unwrap());
799 }
800
801 #[test]
802 #[cfg(all(feature = "csv", feature = "resources"))]
803 fn test_to_fastsim2_conv() {
804 let veh = mock_conv_veh();
805 let cyc = crate::drive_cycle::Cycle::from_resource("udds.csv", false).unwrap();
806 let sd = crate::simdrive::SimDrive::new(veh, cyc, Default::default());
807 let mut sd2 = sd.to_fastsim2().unwrap();
808 sd2.sim_drive(None, None).unwrap();
809 }
810
811 #[test]
812 #[cfg(all(feature = "csv", feature = "resources"))]
813 fn test_to_fastsim2_hev() {
814 let veh = mock_hev();
815 let cyc = crate::drive_cycle::Cycle::from_resource("udds.csv", false).unwrap();
816 let sd = crate::simdrive::SimDrive::new(veh, cyc, Default::default());
817 let mut sd2 = sd.to_fastsim2().unwrap();
818 sd2.sim_drive(None, None).unwrap();
819 }
820
821 #[test]
822 #[cfg(all(feature = "csv", feature = "resources"))]
823 fn test_to_fastsim2_bev() {
824 let veh = mock_bev();
825 let cyc = crate::drive_cycle::Cycle::from_resource("udds.csv", false).unwrap();
826 let sd = crate::simdrive::SimDrive::new(veh, cyc, Default::default());
827 let mut sd2 = sd.to_fastsim2().unwrap();
828 sd2.sim_drive(None, None).unwrap();
829 }
830}