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