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