1use crate::prelude::*;
2
3use super::{hev::HEVPowertrainControls, *};
4pub mod fastsim2_interface;
5
6#[derive(
8 Clone, Debug, Serialize, Deserialize, PartialEq, IsVariant, derive_more::From, TryInto,
9)]
10pub enum AuxSource {
11 ReversibleEnergyStorage,
14 FuelConverter,
17}
18
19impl SerdeAPI for AuxSource {}
20impl Init for AuxSource {}
21
22#[serde_api]
23#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
24#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, StateMethods)]
25#[non_exhaustive]
26#[serde(deny_unknown_fields)]
27pub struct Vehicle {
29 pub name: String,
31 pub year: u32,
33 #[has_state]
34 pub pt_type: PowertrainType,
36
37 pub chassis: Chassis,
39
40 #[serde(default, skip_serializing_if = "CabinOption::is_none")]
42 #[has_state]
43 pub cabin: CabinOption,
44
45 #[serde(default, skip_serializing_if = "HVACOption::is_none")]
47 #[has_state]
48 pub hvac: HVACOption,
49
50 pub(crate) mass: Option<si::Mass>,
52
53 pub pwr_aux_base: si::Power,
55
56 pub trans_eff: si::Ratio,
61
62 save_interval: Option<usize>,
64 #[serde(default)]
66 pub state: VehicleState,
67 #[serde(default, skip_serializing_if = "VehicleStateHistoryVec::is_empty")]
69 pub history: VehicleStateHistoryVec,
70}
71
72#[pyo3_api]
73impl Vehicle {
74 #[staticmethod]
75 fn try_from_fastsim2(veh: fastsim_2::vehicle::RustVehicle) -> PyResult<Vehicle> {
76 Ok(Self::try_from(veh.clone())?)
77 }
78
79 #[pyo3(name = "set_save_interval")]
80 #[pyo3(signature = (save_interval=None))]
81 fn set_save_interval_py(&mut self, save_interval: Option<usize>) -> PyResult<()> {
83 self.set_save_interval(save_interval)
84 .map_err(|e| PyAttributeError::new_err(e.to_string()))
85 }
86
87 #[getter("save_interval")]
89 fn get_save_interval_py(&self) -> anyhow::Result<Option<usize>> {
91 self.save_interval()
92 }
93
94 #[getter]
95 fn get_fc(&self) -> Option<FuelConverter> {
96 self.fc().cloned()
97 }
98
99 #[getter]
100 fn get_res(&self) -> Option<ReversibleEnergyStorage> {
101 self.res().cloned()
102 }
103
104 #[getter]
105 fn get_em(&self) -> Option<ElectricMachine> {
106 self.em().cloned()
107 }
108
109 fn veh_type(&self) -> String {
110 self.pt_type.to_string()
111 }
112
113 #[pyo3(name = "from_f2_file")]
125 #[staticmethod]
126 fn from_f2_file_py(file: PathBuf) -> anyhow::Result<Self> {
127 Self::from_f2_file(file)
128 }
129
130 #[pyo3(name = "clear")]
131 fn clear_py(&mut self) {
132 self.clear()
133 }
134
135 #[pyo3(name = "reset_step")]
136 fn reset_step_py(&mut self) -> anyhow::Result<()> {
137 self.reset_step(|| format_dbg!())
138 }
139}
140
141impl Mass for Vehicle {
142 fn mass(&self) -> anyhow::Result<Option<si::Mass>> {
143 let derived_mass = self
144 .derived_mass()
145 .with_context(|| anyhow!(format_dbg!()))?;
146 match (derived_mass, self.mass) {
147 (Some(derived_mass), Some(set_mass)) => {
148 ensure!(
149 utils::almost_eq_uom(&set_mass, &derived_mass, None),
150 format!(
151 "{}",
152 format_dbg!(utils::almost_eq_uom(&set_mass, &derived_mass, None)),
153 )
154 );
155 Ok(Some(set_mass))
156 }
157 (None, None) => bail!(
158 "Not all mass fields in `{}` are set and no mass was previously set.",
159 stringify!(Vehicle)
160 ),
161 _ => Ok(self.mass.or(derived_mass)),
162 }
163 }
164
165 fn set_mass(
166 &mut self,
167 new_mass: Option<si::Mass>,
168 side_effect: MassSideEffect,
169 ) -> anyhow::Result<()> {
170 ensure!(
171 side_effect == MassSideEffect::None,
172 "At the vehicle level, only `MassSideEffect::None` is allowed"
173 );
174
175 let derived_mass = self
176 .derived_mass()
177 .with_context(|| anyhow!(format_dbg!()))?;
178 self.mass = match new_mass {
179 Some(new_mass) => {
181 if let Some(dm) = derived_mass {
182 if dm != new_mass {
183 self.expunge_mass_fields();
184 }
185 }
186 Some(new_mass)
187 }
188 None => Some(derived_mass.with_context(|| {
190 format!(
191 "Not all mass fields in `{}` are set and no mass was provided.",
192 stringify!(Vehicle)
193 )
194 })?),
195 };
196 Ok(())
197 }
198
199 fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
200 let chassis_mass = self
201 .chassis
202 .mass()
203 .with_context(|| anyhow!(format_dbg!()))?;
204 let pt_mass = match &self.pt_type {
205 PowertrainType::ConventionalVehicle(conv) => conv.mass()?,
206 PowertrainType::HybridElectricVehicle(hev) => hev.mass()?,
207 PowertrainType::BatteryElectricVehicle(bev) => bev.mass()?,
208 };
209 if let (Some(pt_mass), Some(chassis_mass)) = (pt_mass, chassis_mass) {
210 Ok(Some(pt_mass + chassis_mass))
211 } else {
212 Ok(None)
213 }
214 }
215
216 fn expunge_mass_fields(&mut self) {
217 self.chassis.expunge_mass_fields();
218 match &mut self.pt_type {
219 PowertrainType::ConventionalVehicle(conv) => conv.expunge_mass_fields(),
220 PowertrainType::HybridElectricVehicle(hev) => hev.expunge_mass_fields(),
221 PowertrainType::BatteryElectricVehicle(bev) => bev.expunge_mass_fields(),
222 };
223 }
224}
225
226impl SerdeAPI for Vehicle {
227 #[cfg(feature = "resources")]
228 const RESOURCES_SUBDIR: &'static str = "vehicles";
229}
230impl Init for Vehicle {
231 fn init(&mut self) -> Result<(), Error> {
232 let _mass = self
233 .mass()
234 .map_err(|err| Error::InitError(format_dbg!(err)))?;
235 self.calculate_wheel_radius()
236 .map_err(|err| Error::InitError(format_dbg!(err)))?;
237 self.pt_type
238 .init()
239 .map_err(|err| Error::InitError(format_dbg!(err)))?;
240 Ok(())
241 }
242}
243
244impl HistoryMethods for Vehicle {
245 fn save_interval(&self) -> anyhow::Result<Option<usize>> {
246 Ok(self.save_interval)
247 }
248 fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
249 self.save_interval = save_interval;
250 self.pt_type.set_save_interval(save_interval)?;
251 self.cabin.set_save_interval(save_interval)?;
252 self.hvac.set_save_interval(save_interval)?;
253 Ok(())
254 }
255 fn clear(&mut self) {
256 self.history.clear();
257 self.pt_type.clear();
258 self.cabin.clear();
259 self.hvac.clear();
260 }
261}
262
263const FUEL_LHV_MJ_PER_KG: f64 = 43.2;
265const CONV: &str = "Conv";
266const HEV: &str = "HEV";
267const PHEV: &str = "PHEV";
268const BEV: &str = "BEV";
269
270impl SetCumulative for Vehicle {
271 fn set_cumulative<F: Fn() -> String>(&mut self, dt: si::Time, loc: F) -> anyhow::Result<()> {
272 self.state
273 .set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?;
274 self.pt_type
275 .set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?;
276 self.cabin
277 .set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?;
278 self.hvac
279 .set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?;
280 self.state.dist.increment(
282 *self.state.speed_ach.get_fresh(|| format_dbg!())? * dt,
283 || format_dbg!(),
284 )?;
285 Ok(())
286 }
287}
288
289impl Vehicle {
290 pub fn get_pwr_rated(&self) -> si::Power {
293 match (self.fc(), self.res()) {
294 (Some(fc), Some(res)) => fc.pwr_out_max + res.pwr_out_max,
295 (Some(fc), None) => fc.pwr_out_max,
296 (None, Some(res)) => res.pwr_out_max,
297 (None, None) => unreachable!(),
298 }
299 }
300
301 pub fn conv(&self) -> Option<&ConventionalVehicle> {
302 self.pt_type.conv()
303 }
304
305 pub fn hev(&self) -> Option<&HybridElectricVehicle> {
306 self.pt_type.hev()
307 }
308
309 pub fn bev(&self) -> Option<&BatteryElectricVehicle> {
314 self.pt_type.bev()
315 }
316
317 pub fn conv_mut(&mut self) -> Option<&mut ConventionalVehicle> {
318 self.pt_type.conv_mut()
319 }
320
321 pub fn hev_mut(&mut self) -> Option<&mut HybridElectricVehicle> {
322 self.pt_type.hev_mut()
323 }
324
325 pub fn bev_mut(&mut self) -> Option<&mut BatteryElectricVehicle> {
330 self.pt_type.bev_mut()
331 }
332
333 pub fn fc(&self) -> Option<&FuelConverter> {
334 self.pt_type.fc()
335 }
336
337 pub fn fc_mut(&mut self) -> Option<&mut FuelConverter> {
338 self.pt_type.fc_mut()
339 }
340
341 pub fn set_fc(&mut self, fc: FuelConverter) -> anyhow::Result<()> {
342 self.pt_type.set_fc(fc)
343 }
344
345 pub fn fs(&self) -> Option<&FuelStorage> {
346 self.pt_type.fs()
347 }
348
349 pub fn fs_mut(&mut self) -> Option<&mut FuelStorage> {
350 self.pt_type.fs_mut()
351 }
352
353 pub fn set_fs(&mut self, fs: FuelStorage) -> anyhow::Result<()> {
354 self.pt_type.set_fs(fs)
355 }
356
357 pub fn res(&self) -> Option<&ReversibleEnergyStorage> {
358 self.pt_type.res()
359 }
360
361 pub fn res_mut(&mut self) -> Option<&mut ReversibleEnergyStorage> {
362 self.pt_type.res_mut()
363 }
364
365 pub fn set_res(&mut self, res: ReversibleEnergyStorage) -> anyhow::Result<()> {
366 self.pt_type.set_res(res)
367 }
368
369 pub fn em(&self) -> Option<&ElectricMachine> {
370 self.pt_type.em()
371 }
372
373 pub fn em_mut(&mut self) -> Option<&mut ElectricMachine> {
374 self.pt_type.em_mut()
375 }
376
377 pub fn set_em(&mut self, em: ElectricMachine) -> anyhow::Result<()> {
378 self.pt_type.set_em(em)
379 }
380
381 pub fn trans(&self) -> Option<&Transmission> {
382 self.pt_type.trans()
383 }
384
385 pub fn trans_mut(&mut self) -> Option<&mut Transmission> {
386 self.pt_type.trans_mut()
387 }
388
389 pub fn set_trans(&mut self, trans: Transmission) -> anyhow::Result<()> {
390 self.pt_type.set_trans(trans)
391 }
392
393 fn calculate_wheel_radius(&mut self) -> anyhow::Result<()> {
395 ensure!(
396 self.chassis.wheel_radius.is_some() || self.chassis.tire_code.is_some(),
397 "Either `wheel_radius` or `tire_code` must be supplied"
398 );
399 if self.chassis.wheel_radius.is_none() {
400 self.chassis.wheel_radius =
401 Some(utils::tire_code_to_radius(self.chassis.tire_code.as_ref().unwrap())? * uc::M)
402 }
403 Ok(())
404 }
405
406 pub fn solve_powertrain(&mut self, dt: si::Time) -> anyhow::Result<()> {
408 self.pt_type
409 .solve(
410 *self.state.pwr_tractive.get_fresh(|| format_dbg!())?,
411 true, dt,
413 )
414 .with_context(|| anyhow!(format_dbg!()))?;
415 self.state.pwr_brake.update(
416 -self
417 .state
418 .pwr_tractive
419 .get_fresh(|| format_dbg!())?
420 .max(si::Power::ZERO)
421 - self.pt_type.pwr_regen().with_context(|| format_dbg!())?,
422 || format_dbg!(),
423 )?;
424 Ok(())
425 }
426
427 pub fn set_curr_pwr_out_max(&mut self, dt: si::Time) -> anyhow::Result<()> {
428 self.pt_type
430 .set_curr_pwr_prop_out_max(
431 *self.state.pwr_aux.get_fresh(|| format_dbg!())?,
432 dt,
433 &self.state,
434 )
435 .with_context(|| anyhow!(format_dbg!()))?;
436 let pwr_prop_maxes = self
437 .pt_type
438 .get_curr_pwr_prop_out_max()
439 .with_context(|| anyhow!(format_dbg!()))?;
440 self.state
441 .pwr_prop_fwd_max
442 .update(pwr_prop_maxes.0, || format_dbg!())?;
443 self.state
444 .pwr_prop_bwd_max
445 .update(pwr_prop_maxes.1, || format_dbg!())?;
446
447 Ok(())
448 }
449
450 pub fn solve_thermal(
451 &mut self,
452 te_amb_air: si::Temperature,
453 dt: si::Time,
454 ) -> anyhow::Result<()> {
455 let te_fc: Option<si::Temperature> = self
456 .fc()
457 .and_then(|fc| fc.temperature().map(|fct| fct.get_stale(|| format_dbg!())))
458 .transpose()
459 .with_context(|| {
460 format!(
461 "{}\nfuel converter temperature has not been properly set",
462 format_dbg!()
463 )
464 })?
465 .copied();
466 let pwr_thrml_cab_to_res: si::Power = match self.res() {
467 Some(res) => match &res.thrml {
468 RESThermalOption::RESLumpedThermal(rlt) => {
469 *rlt.state.pwr_thrml_from_cabin.get_stale(|| format_dbg!())?
470 }
471 RESThermalOption::None => si::Power::ZERO,
472 },
473 None => si::Power::ZERO,
474 };
475
476 let (pwr_thrml_fc_to_cabin, pwr_thrml_hvac_to_res, te_cab) = self
477 .solve_hvac_cab_res(te_amb_air, dt, te_fc, pwr_thrml_cab_to_res)
478 .with_context(|| format_dbg!())?;
479
480 self.pt_type
481 .solve_thermal(
482 te_amb_air,
483 pwr_thrml_fc_to_cabin,
484 &mut self.state,
485 pwr_thrml_hvac_to_res,
486 te_cab,
487 dt,
488 )
489 .with_context(|| format_dbg!())?;
490 Ok(())
491 }
492
493 fn solve_hvac_cab_res(
494 &mut self,
495 te_amb_air: si::Temperature,
496 dt: si::Time,
497 te_fc: Option<si::Temperature>,
498 pwr_thrml_cab_to_res: si::Power,
499 ) -> anyhow::Result<(
500 Option<si::Power>,
501 Option<si::Power>,
502 Option<si::Temperature>,
503 )> {
504 let res_thrml_state = self.pt_type.res_mut().and_then(|rm| rm.res_thrml_state());
505 let (pwr_thrml_fc_to_cabin, pwr_thrml_hvac_to_res, te_cab): (
506 Option<si::Power>,
507 Option<si::Power>,
508 Option<si::Temperature>,
509 ) = match (&mut self.cabin, &mut self.hvac, res_thrml_state) {
510 (CabinOption::None, HVACOption::None, None) => {
511 self.state
512 .pwr_aux
513 .update(self.pwr_aux_base, || format_dbg!())?;
514 (None, None, None)
515 }
516 (CabinOption::LumpedCabin(cab), HVACOption::LumpedCabin(hvac), None) => {
517 let (pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cab) = hvac
518 .solve(te_amb_air, te_fc, &cab.state, cab.heat_capacitance, dt)
519 .with_context(|| format_dbg!())?;
520 let te_cab = cab
521 .solve(
522 te_amb_air,
523 &self.state,
524 pwr_thrml_hvac_to_cabin,
525 Default::default(),
526 dt,
527 )
528 .with_context(|| format_dbg!())?;
529 self.state.pwr_aux.update(
530 self.pwr_aux_base
531 + *hvac
532 .state
533 .pwr_aux_for_hvac
534 .get_fresh(|| format_dbg!("hvac.state.pwr_aux_for_hvac"))?,
535 || format_dbg!(),
536 )?;
537 (Some(pwr_thrml_fc_to_cab), None, Some(te_cab))
538 }
539 (
540 CabinOption::LumpedCabin(cab),
541 HVACOption::LumpedCabinAndRES(hvac),
542 Some(res_thrml_state),
543 ) => {
544 let (pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cab, pwr_thrml_hvac_to_res) = hvac
545 .solve(
546 te_amb_air,
547 te_fc,
548 &cab.state,
549 cab.heat_capacitance,
550 res_thrml_state,
551 dt,
552 )
553 .with_context(|| format_dbg!())?;
554 let te_cab = cab
555 .solve(
556 te_amb_air,
557 &self.state,
558 pwr_thrml_hvac_to_cabin,
559 pwr_thrml_cab_to_res,
560 dt,
561 )
562 .with_context(|| format_dbg!())?;
563 self.state.pwr_aux.update(
564 self.pwr_aux_base
565 + *hvac
566 .state
567 .pwr_aux_for_cab_hvac
568 .get_fresh(|| format_dbg!())?
569 + *hvac
570 .state
571 .pwr_aux_for_res_hvac
572 .get_fresh(|| format_dbg!())?,
573 || format_dbg!(),
574 )?;
575 ensure!(
576 *self.state.pwr_aux.get_fresh(|| format_dbg!())? > si::Power::ZERO,
577 format!(
578 "{}\n{}\n{}",
579 format_dbg!(self.state.pwr_aux),
580 format_dbg!(hvac.state.pwr_aux_for_res_hvac),
581 format_dbg!(hvac.state.pwr_aux_for_cab_hvac)
582 )
583 );
584 (
585 Some(pwr_thrml_fc_to_cab),
586 Some(pwr_thrml_hvac_to_res),
587 Some(te_cab),
588 )
589 }
590 (CabinOption::LumpedCabin(cab), HVACOption::LumpedCabin(hvac), Some(_)) => {
591 let (pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cab) = hvac
592 .solve(te_amb_air, te_fc, &cab.state, cab.heat_capacitance, dt)
593 .with_context(|| format_dbg!())?;
594 let te_cab = cab
595 .solve(
596 te_amb_air,
597 &self.state,
598 pwr_thrml_hvac_to_cabin,
599 Default::default(),
600 dt,
601 )
602 .with_context(|| format_dbg!())?;
603 self.state.pwr_aux.update(
604 self.pwr_aux_base
605 + *hvac
606 .state
607 .pwr_aux_for_hvac
608 .get_fresh(|| format_dbg!("hvac.state.pwr_aux_for_hvac"))?,
609 || format_dbg!(),
610 )?;
611 (Some(pwr_thrml_fc_to_cab), None, Some(te_cab))
612 }
613 (_, _, _) => {
614 bail!(
615 "{}\nCabin, HVAC, and RESThermal configuration is either invalid or not yet implemented.\n{} - {} - {}",
616 format_dbg!(),
617 format!("{}", self.hvac),
618 format!("{}", self.cabin),
619 format!(
620 "`res.res_thrml_state().is_some()`: {}",
621 self.pt_type.res().and_then(|res| res.res_thrml_state()).is_some()
622 ),
623 )
624 }
625 };
626 Ok((pwr_thrml_fc_to_cabin, pwr_thrml_hvac_to_res, te_cab))
627 }
628
629 fn from_f2_file(file: PathBuf) -> anyhow::Result<Self> {
630 use fastsim_2::traits::SerdeAPI;
631 let f2veh = fastsim_2::vehicle::RustVehicle::from_file(file, false)
632 .with_context(|| format_dbg!())?;
633 Self::try_from(f2veh)
634 }
635
636 pub(crate) fn mark_non_thermal_fresh(&mut self) -> Result<(), anyhow::Error> {
637 self.state.i.mark_stale();
638 self.state.time.mark_stale();
639 self.state.pwr_aux.mark_stale();
640 self.state.mass.mark_stale();
641 self.state.mark_fresh(|| format_dbg!())?;
642 self.state.energy_tractive.mark_stale();
643 self.state.energy_aux.mark_stale();
644 self.state.energy_drag.mark_stale();
645 self.state.energy_accel.mark_stale();
646 self.state.energy_ascent.mark_stale();
647 self.state.energy_rr.mark_stale();
648 self.state.energy_whl_inertia.mark_stale();
649 self.state.energy_brake.mark_stale();
650 self.state.dist.mark_stale();
651 match self.fc_mut() {
652 Some(fc) => {
653 fc.state.i.mark_stale();
654 fc.state.mark_fresh(|| format_dbg!())?;
655 fc.state.energy_prop.mark_stale();
656 fc.state.energy_aux.mark_stale();
657 fc.state.energy_fuel.mark_stale();
658 fc.state.energy_loss.mark_stale();
659 }
660 None => {}
661 }
662 match self.res_mut() {
663 Some(res) => {
664 res.state.i.mark_stale();
665 res.state.soh.mark_stale();
666 res.state.mark_fresh(|| format_dbg!())?;
667 res.state.energy_out_electrical.mark_stale();
668 res.state.energy_out_prop.mark_stale();
669 res.state.energy_aux.mark_stale();
670 res.state.energy_loss.mark_stale();
671 res.state.energy_out_chemical.mark_stale();
672 }
673 None => {}
674 }
675 match self.em_mut() {
676 Some(em) => {
677 em.state.i.mark_stale();
678 em.state.mark_fresh(|| format_dbg!())?;
679 em.state.energy_out_req.mark_stale();
680 em.state.energy_elec_prop_in.mark_stale();
681 em.state.energy_mech_prop_out.mark_stale();
682 em.state.energy_mech_dyn_brake.mark_stale();
683 em.state.energy_elec_dyn_brake.mark_stale();
684 em.state.energy_loss.mark_stale();
685 }
686 None => {}
687 }
688 match self.trans_mut() {
689 Some(trans) => {
690 trans.state.i.mark_stale();
691 trans.state.mark_fresh(|| format_dbg!())?;
692 trans.state.energy_out.mark_stale();
693 trans.state.energy_loss.mark_stale();
694 }
695 None => {}
696 }
697 match &mut self.pt_type {
698 PowertrainType::HybridElectricVehicle(hev) => {
699 match &mut hev.pt_cntrl {
700 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.state.i.mark_stale(),
701 }
702 hev.pt_cntrl.mark_fresh(|| format_dbg!())?
703 }
704 _ => {}
705 }
706 Ok(())
707 }
708}
709
710#[serde_api]
712#[derive(
713 Clone, Debug, Deserialize, Serialize, PartialEq, HistoryVec, StateMethods, SetCumulative,
714)]
715#[non_exhaustive]
716#[serde(default)]
717#[serde(deny_unknown_fields)]
718pub struct VehicleState {
719 pub i: TrackedState<usize>,
721
722 pub time: TrackedState<si::Time>,
724
725 pub pwr_prop_fwd_max: TrackedState<si::Power>,
728 pub pwr_prop_bwd_max: TrackedState<si::Power>,
731 pub pwr_tractive: TrackedState<si::Power>,
733 pub pwr_tractive_for_cyc: TrackedState<si::Power>,
735 pub energy_tractive: TrackedState<si::Energy>,
737 pub pwr_aux: TrackedState<si::Power>,
739 pub energy_aux: TrackedState<si::Energy>,
741 pub pwr_drag: TrackedState<si::Power>,
743 pub energy_drag: TrackedState<si::Energy>,
745 pub pwr_accel: TrackedState<si::Power>,
747 pub energy_accel: TrackedState<si::Energy>,
749 pub pwr_ascent: TrackedState<si::Power>,
751 pub energy_ascent: TrackedState<si::Energy>,
753 pub pwr_rr: TrackedState<si::Power>,
755 pub energy_rr: TrackedState<si::Energy>,
757 pub pwr_whl_inertia: TrackedState<si::Power>,
759 pub energy_whl_inertia: TrackedState<si::Energy>,
761 pub pwr_brake: TrackedState<si::Power>,
763 pub energy_brake: TrackedState<si::Energy>,
765 pub cyc_met: TrackedState<bool>,
769 pub cyc_met_overall: TrackedState<bool>,
772 pub speed_ach: TrackedState<si::Velocity>,
774 pub dist: TrackedState<si::Length>,
776 pub grade_curr: TrackedState<si::Ratio>,
778 pub elev_curr: TrackedState<si::Length>,
781 pub air_density: TrackedState<si::MassDensity>,
783 pub mass: TrackedState<si::Mass>,
786}
787
788impl SerdeAPI for VehicleState {}
789impl Init for VehicleState {}
790impl Default for VehicleState {
791 fn default() -> Self {
792 Self {
793 i: TrackedState::new(Default::default()),
794 time: Default::default(),
795 pwr_prop_fwd_max: Default::default(),
796 pwr_prop_bwd_max: Default::default(),
797 pwr_tractive: Default::default(),
798 pwr_tractive_for_cyc: Default::default(),
799 energy_tractive: Default::default(),
800 pwr_aux: Default::default(),
801 energy_aux: Default::default(),
802 pwr_drag: Default::default(),
803 energy_drag: Default::default(),
804 pwr_accel: Default::default(),
805 energy_accel: Default::default(),
806 pwr_ascent: Default::default(),
807 energy_ascent: Default::default(),
808 pwr_rr: Default::default(),
809 energy_rr: Default::default(),
810 pwr_whl_inertia: Default::default(),
811 energy_whl_inertia: Default::default(),
812 pwr_brake: Default::default(),
813 energy_brake: Default::default(),
814 cyc_met: TrackedState::new(true),
815 cyc_met_overall: TrackedState::new(true),
816 speed_ach: Default::default(),
817 dist: Default::default(),
818 grade_curr: Default::default(),
820 elev_curr: Default::default(),
822 air_density: Default::default(),
823 mass: TrackedState::new(uc::KG * f64::NAN),
824 }
825 }
826}
827
828#[cfg(test)]
829pub(crate) mod tests {
830 use super::*;
831
832 #[allow(dead_code)]
833 fn vehicles_dir() -> PathBuf {
834 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/vehicles")
835 }
836
837 #[cfg(feature = "yaml")]
838 pub(crate) fn mock_conv_veh() -> Vehicle {
839 let file_contents = include_str!("fastsim-2_2012_Ford_Fusion.yaml");
840 use fastsim_2::traits::SerdeAPI;
841 let veh = {
842 let f2veh = fastsim_2::vehicle::RustVehicle::from_yaml(file_contents, false).unwrap();
843 let veh = Vehicle::try_from(f2veh);
844 veh.unwrap()
845 };
846
847 veh.to_file(vehicles_dir().join("2012_Ford_Fusion.yaml"))
848 .unwrap();
849 assert!(veh.pt_type.is_conventional_vehicle());
850 veh
851 }
852
853 #[cfg(feature = "yaml")]
854 pub(crate) fn mock_hev() -> Vehicle {
855 let file_contents = include_str!("fastsim-2_2016_TOYOTA_Prius_Two.yaml");
856 use fastsim_2::traits::SerdeAPI;
857 let veh = {
858 let f2veh = fastsim_2::vehicle::RustVehicle::from_yaml(file_contents, false).unwrap();
859 let veh = Vehicle::try_from(f2veh);
860 veh.unwrap()
861 };
862
863 veh.to_file(vehicles_dir().join("2016_TOYOTA_Prius_Two.yaml"))
864 .unwrap();
865 assert!(veh.pt_type.is_hybrid_electric_vehicle());
866 veh
867 }
868
869 #[cfg(feature = "yaml")]
870 pub(crate) fn mock_bev() -> Vehicle {
871 let file_contents = include_str!("fastsim-2_2022_Renault_Zoe_ZE50_R135.yaml");
872 use fastsim_2::traits::SerdeAPI;
873 let veh = {
874 let f2veh = fastsim_2::vehicle::RustVehicle::from_yaml(file_contents, false).unwrap();
875 let veh = Vehicle::try_from(f2veh);
876 veh.unwrap()
877 };
878
879 veh.to_file(vehicles_dir().join("2022_Renault_Zoe_ZE50_R135.yaml"))
880 .unwrap();
881 assert!(veh.pt_type.is_battery_electric_vehicle());
882 veh
883 }
884
885 #[test]
886 #[cfg(feature = "yaml")]
887 pub(crate) fn test_conv_veh_init() {
888 use pretty_assertions::assert_eq;
889 let veh = mock_conv_veh();
890 let mut veh1 = veh.clone();
891 assert_eq!(veh.to_yaml().unwrap(), veh1.to_yaml().unwrap());
894 veh1.init().unwrap();
895 assert_eq!(veh.to_yaml().unwrap(), veh1.to_yaml().unwrap());
896 }
897
898 #[test]
899 #[cfg(all(feature = "csv", feature = "resources"))]
900 fn test_to_fastsim2_conv() {
901 let veh = mock_conv_veh();
902 let cyc = crate::drive_cycle::Cycle::from_resource("udds.csv", false).unwrap();
903 let sd = crate::simdrive::SimDrive::new(veh, cyc, Default::default());
904 let mut sd2 = sd.to_fastsim2().unwrap();
905 sd2.sim_drive(None, None).unwrap();
906 }
907
908 #[test]
909 #[cfg(all(feature = "csv", feature = "resources"))]
910 fn test_to_fastsim2_hev() {
911 let veh = mock_hev();
912 let cyc = crate::drive_cycle::Cycle::from_resource("udds.csv", false).unwrap();
913 let sd = crate::simdrive::SimDrive::new(veh, cyc, Default::default());
914 let mut sd2 = sd.to_fastsim2().unwrap();
915 sd2.sim_drive(None, None).unwrap();
916 }
917
918 #[test]
919 #[cfg(all(feature = "csv", feature = "resources"))]
920 fn test_to_fastsim2_bev() {
921 let veh = mock_bev();
922 let cyc = crate::drive_cycle::Cycle::from_resource("udds.csv", false).unwrap();
923 let sd = crate::simdrive::SimDrive::new(veh, cyc, Default::default());
924 let mut sd2 = sd.to_fastsim2().unwrap();
925 sd2.sim_drive(None, None).unwrap();
926 }
927
928 type StructWithResources = Vehicle;
929
930 #[test]
931 fn test_resources() {
932 let mut time_to_panic = false;
933
934 let resource_list = StructWithResources::list_resources().unwrap();
935 assert!(!resource_list.is_empty());
936
937 for resource in resource_list {
939 if let Err(e) = StructWithResources::from_resource(resource.clone(), false) {
940 time_to_panic = true;
941 eprintln!("Error loading {resource:?}: {e}\n");
942 }
943 }
944
945 let paths: Vec<_> = std::fs::read_dir("../cal_and_val/f3-vehicles")
946 .unwrap()
947 .collect();
948 assert!(!paths.is_empty());
949 for path in paths {
950 let p = path.unwrap().path();
951 if let Err(e) = StructWithResources::from_file(p.clone(), false) {
952 time_to_panic = true;
953 eprintln!("Error loading {p:?}: {e}\n");
954 }
955 }
956
957 let paths: Vec<_> = std::fs::read_dir("../cal_and_val/thermal/f3-vehicles")
958 .unwrap()
959 .collect();
960 assert!(!paths.is_empty());
961 for path in paths {
962 let p = path.unwrap().path();
963 if let Err(e) = StructWithResources::from_file(p.clone(), false) {
964 time_to_panic = true;
965 eprintln!("Error loading {p:?}: {e}\n");
966 }
967 }
968
969 if time_to_panic {
970 panic!()
971 }
972 }
973}