1use super::{hev::HEVPowertrainControls, hev::HEVStartStopControl, *};
2use crate::{
3 prelude::*,
4 vehicle::conv::{ConvPowertrainControls, ConvStartStopControl},
5};
6
7use semver::Version;
8
9#[derive(
11 Clone, Debug, Serialize, Deserialize, PartialEq, IsVariant, derive_more::From, TryInto,
12)]
13pub enum AuxSource {
14 ReversibleEnergyStorage,
17 FuelConverter,
20}
21
22impl SerdeAPI for AuxSource {}
23impl Init for AuxSource {}
24
25#[serde_api]
26#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
27#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, StateMethods)]
28#[non_exhaustive]
29#[serde(deny_unknown_fields)]
30pub struct Vehicle {
32 pub name: String,
34 #[serde(default = "crate::current_fastsim_version")]
36 pub min_fastsim_version: Version,
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub db_path: Option<database::VehicleSchema>,
40 pub doc: Option<String>,
42 pub year: Option<String>,
44 pub make: Option<String>,
46 pub model: Option<String>,
48 pub trim: Option<String>,
50
51 #[has_state]
52 pub pt_type: PowertrainType,
54
55 pub chassis: Chassis,
57
58 #[has_state]
60 #[serde(default)]
61 pub cabin: CabinOption,
62
63 #[has_state]
65 #[serde(default)]
66 pub hvac: HVACOption,
67
68 pub(crate) mass: Option<si::Mass>,
70
71 pub pwr_aux_base: si::Power,
73
74 pub(crate) save_interval: Option<usize>,
76 #[serde(default)]
78 pub state: VehicleState,
79 #[serde(default, skip_serializing_if = "VehicleStateHistoryVec::is_empty")]
81 pub history: VehicleStateHistoryVec,
82}
83
84#[pyo3_api]
85impl Vehicle {
86 #[pyo3(name = "set_save_interval")]
87 #[pyo3(signature = (save_interval=None))]
88 fn set_save_interval_py(&mut self, save_interval: Option<usize>) -> PyResult<()> {
90 self.set_save_interval(save_interval)
91 .map_err(|e| PyAttributeError::new_err(e.to_string()))
92 }
93
94 #[getter("save_interval")]
96 fn get_save_interval_py(&self) -> anyhow::Result<Option<usize>> {
98 self.save_interval()
99 }
100
101 #[getter]
102 fn get_fc(&self) -> Option<FuelConverter> {
103 self.fc().cloned()
104 }
105
106 #[getter]
107 fn get_res(&self) -> Option<ReversibleEnergyStorage> {
108 self.res().cloned()
109 }
110
111 #[getter]
112 fn get_em(&self) -> Option<ElectricMachine> {
113 self.em().cloned()
114 }
115
116 fn veh_type(&self) -> String {
117 self.pt_type.to_string()
118 }
119
120 #[cfg(feature = "compat")]
122 #[pyo3(name = "from_f2_file")]
123 #[staticmethod]
124 #[allow(deprecated)]
125 fn from_f2_file_py(py: pyo3::Python<'_>, file: PathBuf) -> anyhow::Result<Self> {
126 if let Ok(warnings) = py.import("warnings") {
127 let msg = "Vehicle.from_f2_file is deprecated; use Vehicle.from_file / from_reader / from_yaml / from_json / from_toml instead.";
128 let kwargs = pyo3::types::PyDict::new(py);
129 let _ = kwargs.set_item("stacklevel", 2);
130 if let Ok(dep_warn) = warnings.getattr("DeprecationWarning") {
131 let _ = kwargs.set_item("category", dep_warn);
132 }
133 let _ = warnings.call_method("warn", (msg,), Some(&kwargs));
134 }
135 Self::from_f2_file(file)
136 }
137
138 #[pyo3(name = "from_db_path_v1")]
141 #[staticmethod]
142 #[pyo3(signature = (
143 db_path_or_url,
144 path,
145 extension,
146 skip_init=false,
147 ))]
148 fn from_db_path_v1_py(
149 db_path_or_url: Option<&str>,
150 path: &str,
151 extension: &str,
152 skip_init: bool,
153 ) -> anyhow::Result<Self> {
154 Self::from_db_path_v1(db_path_or_url, path, extension, skip_init)
155 }
156
157 #[pyo3(name = "from_db_fields_v1")]
160 #[staticmethod]
161 #[pyo3(signature = (
162 db_path_or_url,
163 fastsim_version,
164 powertrain,
165 make,
166 model,
167 year,
168 variant,
169 revision,
170 extension,
171 skip_init=false,
172 ))]
173 fn from_db_fields_v1_py(
174 db_path_or_url: Option<&str>,
175 fastsim_version: u32,
176 powertrain: &str,
177 make: &str,
178 model: &str,
179 year: &str,
180 variant: &str,
181 revision: u32,
182 extension: &str,
183 skip_init: bool,
184 ) -> anyhow::Result<Self> {
185 Self::from_db_fields_v1(
186 db_path_or_url,
187 fastsim_version,
188 powertrain,
189 make,
190 model,
191 year,
192 variant,
193 revision,
194 extension,
195 skip_init,
196 )
197 }
198
199 #[pyo3(name = "reset_py")]
200 fn reset_py(&mut self) -> anyhow::Result<()> {
202 self.reset_cumulative(|| format_dbg!())?;
203 self.reset_step(|| format_dbg!())?;
204 self.clear();
205 Ok(())
206 }
207
208 #[pyo3(name = "clear")]
209 fn clear_py(&mut self) {
210 self.clear()
211 }
212
213 #[pyo3(name = "reset_step")]
214 fn reset_step_py(&mut self) -> anyhow::Result<()> {
215 self.reset_step(|| format_dbg!())
216 }
217
218 #[pyo3(name = "reset_cumulative")]
219 fn reset_cumulative_py(&mut self) -> anyhow::Result<()> {
220 self.reset_cumulative(|| format_dbg!())
221 }
222
223 #[pyo3(name = "use_start_stop_controller")]
224 fn use_start_stop_controller_py(&mut self) -> anyhow::Result<()> {
225 self.use_start_stop_controller()
226 }
227
228 #[pyo3(name = "use_normal_controller")]
229 fn use_normal_controller_py(&mut self) -> anyhow::Result<()> {
230 self.use_normal_controller()
231 }
232
233 #[pyo3(name = "set_dfco_params")]
234 fn set_dfco_params_py(
235 &mut self,
236 enabled: bool,
237 min_dfco_speed_m_per_s: f64,
238 max_accel_for_dfco_m_per_s2: f64,
239 ) -> anyhow::Result<()> {
240 self.set_dfco_params(enabled, min_dfco_speed_m_per_s, max_accel_for_dfco_m_per_s2)
241 }
242}
243
244impl Vehicle {
246 pub fn new(
248 name: String,
249 min_fastsim_version: Option<Version>,
250 db_path: Option<database::VehicleSchema>,
251 doc: Option<String>,
252 year: Option<String>,
253 make: Option<String>,
254 model: Option<String>,
255 trim: Option<String>,
256 pt_type: PowertrainType,
257 chassis: Chassis,
258 cabin: CabinOption,
259 hvac: HVACOption,
260 mass: Option<si::Mass>,
261 pwr_aux_base: si::Power,
262 save_interval: Option<usize>,
263 ) -> anyhow::Result<Self> {
264 let mut veh = Self {
265 name,
266 min_fastsim_version: min_fastsim_version
267 .unwrap_or_else(|| crate::FASTSIM_VERSION.clone()),
268 db_path,
269 doc,
270 year,
271 make,
272 model,
273 trim,
274 pt_type,
275 chassis,
276 cabin,
277 hvac,
278 mass,
279 pwr_aux_base,
280 state: VehicleState::default(),
281 history: VehicleStateHistoryVec::default(),
282 save_interval,
283 };
284 veh.init()?;
285 Ok(veh)
286 }
287
288 pub fn use_start_stop_controller(&mut self) -> anyhow::Result<()> {
289 match &mut self.pt_type {
290 PowertrainType::ConventionalVehicle(veh) => match veh.pt_cntrl {
291 ConvPowertrainControls::Normal => {
292 let save_interval = veh.save_interval().unwrap_or(Option::None);
293 veh.pt_cntrl =
294 ConvPowertrainControls::StartStop(Box::new(ConvStartStopControl::new(
295 None, None, None, None, save_interval,
300 )?));
301 }
302 ConvPowertrainControls::StartStop(_) => (),
303 },
304 PowertrainType::HybridElectricVehicle(veh) => match veh.pt_cntrl {
305 HEVPowertrainControls::RGWDB(_) => {
306 let save_interval = veh.save_interval().unwrap_or(Option::None);
307 veh.pt_cntrl =
308 HEVPowertrainControls::StartStop(Box::new(HEVStartStopControl::new(
309 None, None, None, None, None, None, None, save_interval,
317 )?));
318 }
319 HEVPowertrainControls::StartStop(_) => (),
320 },
321 _ => (),
322 }
323 Ok(())
324 }
325
326 pub fn use_normal_controller(&mut self) -> anyhow::Result<()> {
327 let save_interval = self.save_interval().unwrap_or(Option::None);
328 match &mut self.pt_type {
329 PowertrainType::ConventionalVehicle(conv) => match &conv.pt_cntrl {
330 ConvPowertrainControls::StartStop(_) => {
331 conv.pt_cntrl = ConvPowertrainControls::Normal;
332 }
333 ConvPowertrainControls::Normal => (),
334 },
335 PowertrainType::HybridElectricVehicle(hev) => match &hev.pt_cntrl {
336 HEVPowertrainControls::StartStop(_) => {
337 hev.pt_cntrl =
338 HEVPowertrainControls::RGWDB(Box::new(RESGreedyWithDynamicBuffers::new(
339 None, None, None, None, None, None, None, None, None, None, None, None, save_interval,
352 )?))
353 }
354 HEVPowertrainControls::RGWDB(_) => (),
355 },
356 _ => (),
357 }
358 Ok(())
359 }
360
361 pub fn set_dfco_params(
362 &mut self,
363 enabled: bool,
364 min_dfco_speed_m_per_s: f64,
365 max_accel_for_dfco_m_per_s2: f64,
366 ) -> anyhow::Result<()> {
367 let min_dfco_speed_m_per_s = min_dfco_speed_m_per_s.max(0.0);
368 let max_accel_for_dfco_m_per_s2 = max_accel_for_dfco_m_per_s2.min(0.0);
369 match &mut self.pt_type {
370 PowertrainType::ConventionalVehicle(conv) => {
371 conv.dfco_cntrl.dfco_enabled = enabled;
372 conv.dfco_cntrl.minimum_dfco_speed = min_dfco_speed_m_per_s * uc::MPS;
373 conv.dfco_cntrl.minimum_dfco_deceleration = max_accel_for_dfco_m_per_s2 * uc::MPS2;
374 conv.dfco_cntrl.save_interval = self.save_interval;
375 }
376 _ => (),
377 }
378 Ok(())
379 }
380}
381
382impl Mass for Vehicle {
383 fn mass(&self) -> anyhow::Result<Option<si::Mass>> {
384 let derived_mass = self
385 .derived_mass()
386 .with_context(|| anyhow!(format_dbg!()))?;
387 match (derived_mass, self.mass) {
388 (Some(derived_mass), Some(set_mass)) => {
389 ensure!(
390 utils::almost_eq_uom(&set_mass, &derived_mass, None),
391 format!(
392 "{}",
393 format_dbg!(utils::almost_eq_uom(&set_mass, &derived_mass, None)),
394 )
395 );
396 Ok(Some(set_mass))
397 }
398 (None, None) => bail!(
399 "Not all mass fields in `{}` are set and no mass was previously set.",
400 stringify!(Vehicle)
401 ),
402 _ => Ok(self.mass.or(derived_mass)),
403 }
404 }
405
406 fn set_mass(
407 &mut self,
408 new_mass: Option<si::Mass>,
409 side_effect: MassSideEffect,
410 ) -> anyhow::Result<()> {
411 ensure!(
412 side_effect == MassSideEffect::None,
413 "At the vehicle level, only `MassSideEffect::None` is allowed"
414 );
415 let derived_mass = self
416 .derived_mass()
417 .with_context(|| anyhow!(format_dbg!()))?;
418 self.mass = match (new_mass, derived_mass) {
419 (Some(new_mass), Some(dm)) => {
421 if dm != new_mass {
422 self.expunge_mass_fields();
423 }
424 Some(new_mass)
425 }
426 (Some(new_mass), None) => Some(new_mass),
427 (None, Some(dm)) => Some(dm),
428 (None, None) => bail!(
429 "Not all mass fields in `{}` are set and no mass was provided.",
430 stringify!(Vehicle)
431 ),
432 };
433 ensure!(
434 self.mass > Some(0.0 * uc::KG),
435 "{} mass must be positive",
436 stringify!(Vehicle)
437 );
438 Ok(())
439 }
440
441 fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
442 let chassis_mass = self
443 .chassis
444 .mass()
445 .with_context(|| anyhow!(format_dbg!()))?;
446 let pt_mass = match &self.pt_type {
447 PowertrainType::ConventionalVehicle(conv) => conv.mass()?,
448 PowertrainType::HybridElectricVehicle(hev) => hev.mass()?,
449 PowertrainType::PlugInHybridElectricVehicle(phev) => phev.mass()?,
450 PowertrainType::BatteryElectricVehicle(bev) => bev.mass()?,
451 };
452 if let (Some(pt_mass), Some(chassis_mass)) = (pt_mass, chassis_mass) {
453 Ok(Some(pt_mass + chassis_mass))
454 } else {
455 Ok(None)
456 }
457 }
458
459 fn expunge_mass_fields(&mut self) {
460 self.chassis.expunge_mass_fields();
461 match &mut self.pt_type {
462 PowertrainType::ConventionalVehicle(conv) => conv.expunge_mass_fields(),
463 PowertrainType::HybridElectricVehicle(hev) => hev.expunge_mass_fields(),
464 PowertrainType::PlugInHybridElectricVehicle(phev) => phev.expunge_mass_fields(),
465 PowertrainType::BatteryElectricVehicle(bev) => bev.expunge_mass_fields(),
466 };
467 }
468}
469
470impl Vehicle {
471 fn warn_if_version_mismatch(min_ver: Option<Version>) {
472 if let Some(min_ver) = min_ver {
473 if min_ver > *crate::FASTSIM_VERSION {
474 eprintln!(
475 "WARNING: vehicle file requires FASTSim >= {min_ver} but the installed \
476 version is {}. Loading will be attempted but may fail or produce \
477 unexpected results. Please update FASTSim.",
478 *crate::FASTSIM_VERSION
479 );
480 } else if min_ver.major < crate::FASTSIM_VERSION.major {
481 eprintln!(
482 "WARNING: vehicle file has min_fastsim_version {min_ver}, which is from \
483 an older major version than the installed FASTSim {}. Major-version \
484 upgrades may introduce breaking changes; loading will be attempted.",
485 *crate::FASTSIM_VERSION
486 );
487 }
488 }
489 }
490}
491
492impl SerdeAPI for Vehicle {
493 #[cfg(feature = "resources")]
494 const RESOURCES_SUBDIR: &'static str = "vehicles";
495
496 fn from_reader<R: std::io::Read>(
504 rdr: &mut R,
505 format: &str,
506 skip_init: bool,
507 ) -> Result<Self, Error> {
508 #[derive(Deserialize)]
509 struct VersionCheck {
510 #[serde(default = "crate::current_fastsim_version")]
511 min_fastsim_version: Version,
512 }
513
514 let mut buf = Vec::new();
515 rdr.read_to_end(&mut buf)
516 .map_err(|err| Error::SerdeError(format!("{err}")))?;
517
518 let fmt = format.trim_start_matches('.').to_lowercase();
519
520 let min_ver: Option<Version> = match fmt.as_str() {
523 #[cfg(feature = "yaml")]
524 "yaml" | "yml" => serde_yaml::from_slice::<VersionCheck>(&buf)
525 .ok()
526 .map(|v| v.min_fastsim_version),
527 #[cfg(feature = "json")]
528 "json" => serde_json::from_slice::<VersionCheck>(&buf)
529 .ok()
530 .map(|v| v.min_fastsim_version),
531 #[cfg(feature = "msgpack")]
532 "msgpack" => rmp_serde::decode::from_slice::<VersionCheck>(&buf)
533 .ok()
534 .map(|v| v.min_fastsim_version),
535 #[cfg(feature = "toml")]
536 "toml" => std::str::from_utf8(&buf)
537 .ok()
538 .and_then(|s| toml::from_str::<VersionCheck>(s).ok())
539 .map(|v| v.min_fastsim_version),
540 _ => None,
541 };
542 Self::warn_if_version_mismatch(min_ver);
543
544 let parse_result: Result<Self, Error> = match fmt.as_str() {
546 #[cfg(feature = "yaml")]
547 "yaml" | "yml" => {
548 serde_yaml::from_slice(&buf).map_err(|err| Error::SerdeError(format!("{err}")))
549 }
550 #[cfg(feature = "json")]
551 "json" => {
552 serde_json::from_slice(&buf).map_err(|err| Error::SerdeError(format!("{err}")))
553 }
554 #[cfg(feature = "msgpack")]
555 "msgpack" => rmp_serde::decode::from_slice(&buf)
556 .map_err(|err| Error::SerdeError(format!("{err}"))),
557 #[cfg(feature = "toml")]
558 "toml" => {
559 let toml_str =
560 std::str::from_utf8(&buf).map_err(|err| Error::SerdeError(format!("{err}")))?;
561 toml::from_str(toml_str).map_err(|err| Error::SerdeError(format!("{err}")))
562 }
563 _ => Err(Error::SerdeError(format!(
564 "Unsupported format {format:?}, must be one of {:?}",
565 Self::ACCEPTED_BYTE_FORMATS,
566 ))),
567 };
568 match parse_result {
569 Ok(mut deserialized) => {
573 if !skip_init {
574 deserialized.init()?;
575 }
576 Ok(deserialized)
577 }
578 Err(format_parse_err) => {
584 #[cfg(feature = "compat")]
585 {
586 let mut buf_rdr = std::io::Cursor::new(buf.as_slice());
587 use crate::compat::fastsim_2::fastsim_core::traits::SerdeAPI;
588 if let Ok(f2_veh) =
589 crate::compat::fastsim_2::fastsim_core::vehicle::RustVehicle::from_reader(
590 &mut buf_rdr,
591 format,
592 skip_init,
593 )
594 {
595 return Vehicle::try_from(f2_veh)
596 .map_err(|err| Error::SerdeError(format!("{err}")));
597 }
598 }
599 Err(format_parse_err)
600 }
601 }
602 }
603
604 #[cfg(feature = "yaml")]
606 fn from_yaml<S: AsRef<str>>(yaml_str: S, skip_init: bool) -> anyhow::Result<Self> {
607 #[derive(Deserialize)]
608 struct VersionCheck {
609 #[serde(default = "crate::current_fastsim_version")]
610 min_fastsim_version: Version,
611 }
612 let min_ver = serde_yaml::from_str::<VersionCheck>(yaml_str.as_ref())
613 .ok()
614 .map(|v| v.min_fastsim_version);
615 Self::warn_if_version_mismatch(min_ver);
616
617 match serde_yaml::from_str::<Self>(yaml_str.as_ref()) {
618 Ok(mut yaml_de) => {
622 if !skip_init {
623 yaml_de.init()?;
624 }
625 Ok(yaml_de)
626 }
627 Err(format_parse_err) => {
633 #[cfg(feature = "compat")]
634 {
635 use crate::compat::fastsim_2::fastsim_core::traits::SerdeAPI;
636 if let Ok(f2_veh) =
637 crate::compat::fastsim_2::fastsim_core::vehicle::RustVehicle::from_yaml(
638 &yaml_str, skip_init,
639 )
640 {
641 return Vehicle::try_from(f2_veh);
642 }
643 }
644 Err(format_parse_err.into())
645 }
646 }
647 }
648
649 #[cfg(feature = "json")]
651 fn from_json<S: AsRef<str>>(json_str: S, skip_init: bool) -> anyhow::Result<Self> {
652 #[derive(Deserialize)]
653 struct VersionCheck {
654 #[serde(default = "crate::current_fastsim_version")]
655 min_fastsim_version: Version,
656 }
657 let min_ver = serde_json::from_str::<VersionCheck>(json_str.as_ref())
658 .ok()
659 .map(|v| v.min_fastsim_version);
660 Self::warn_if_version_mismatch(min_ver);
661
662 match serde_json::from_str::<Self>(json_str.as_ref()) {
663 Ok(mut json_de) => {
667 if !skip_init {
668 json_de.init()?;
669 }
670 Ok(json_de)
671 }
672 Err(format_parse_err) => {
678 #[cfg(feature = "compat")]
679 {
680 use crate::compat::fastsim_2::fastsim_core::traits::SerdeAPI;
681 if let Ok(f2_veh) =
682 crate::compat::fastsim_2::fastsim_core::vehicle::RustVehicle::from_json(
683 &json_str, skip_init,
684 )
685 {
686 return Vehicle::try_from(f2_veh);
687 }
688 }
689 Err(format_parse_err.into())
690 }
691 }
692 }
693
694 #[cfg(feature = "toml")]
696 fn from_toml<S: AsRef<str>>(toml_str: S, skip_init: bool) -> anyhow::Result<Self> {
697 #[derive(Deserialize)]
698 struct VersionCheck {
699 #[serde(default = "crate::current_fastsim_version")]
700 min_fastsim_version: Version,
701 }
702 let min_ver = toml::from_str::<VersionCheck>(toml_str.as_ref())
703 .ok()
704 .map(|v| v.min_fastsim_version);
705 Self::warn_if_version_mismatch(min_ver);
706
707 match toml::from_str::<Self>(toml_str.as_ref()) {
708 Ok(mut toml_de) => {
712 if !skip_init {
713 toml_de.init()?;
714 }
715 Ok(toml_de)
716 }
717 Err(format_parse_err) => {
723 #[cfg(feature = "compat")]
724 {
725 use crate::compat::fastsim_2::fastsim_core::traits::SerdeAPI;
726 if let Ok(f2_veh) =
727 crate::compat::fastsim_2::fastsim_core::vehicle::RustVehicle::from_toml(
728 &toml_str, skip_init,
729 )
730 {
731 return Vehicle::try_from(f2_veh);
732 }
733 }
734 Err(format_parse_err.into())
735 }
736 }
737 }
738}
739
740impl Init for Vehicle {
741 fn init(&mut self) -> Result<(), Error> {
742 let _mass = self
743 .mass()
744 .map_err(|err| Error::InitError(format_dbg!(err)))?;
745 self.calculate_wheel_radius()
746 .map_err(|err| Error::InitError(format_dbg!(err)))?;
747 self.pt_type
748 .init()
749 .map_err(|err| Error::InitError(format_dbg!(err)))?;
750 let mass = self
751 .mass()
752 .unwrap_or(Some(0.0 * uc::KG))
753 .unwrap_or(0.0 * uc::KG);
754 let _ = match &self.pt_type {
755 PowertrainType::HybridElectricVehicle(hev) => hev.check_buffers(mass),
756 PowertrainType::PlugInHybridElectricVehicle(hev) => hev.check_buffers(mass),
757 _ => Ok(()),
758 };
759 Ok(())
760 }
761}
762
763impl HistoryMethods for Vehicle {
764 fn save_interval(&self) -> anyhow::Result<Option<usize>> {
765 Ok(self.save_interval)
766 }
767 fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
768 self.save_interval = save_interval;
769 self.pt_type.set_save_interval(save_interval)?;
770 self.cabin.set_save_interval(save_interval)?;
771 self.hvac.set_save_interval(save_interval)?;
772 Ok(())
773 }
774 fn clear(&mut self) {
775 self.history.clear();
776 self.pt_type.clear();
777 self.cabin.clear();
778 self.hvac.clear();
779 }
780}
781
782impl SetCumulative for Vehicle {
783 fn set_cumulative<F: Fn() -> String>(&mut self, dt: si::Time, loc: F) -> anyhow::Result<()> {
784 self.state
785 .set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?;
786 self.pt_type
787 .set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?;
788 self.cabin
789 .set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?;
790 self.hvac
791 .set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?;
792 self.state.dist.increment(
794 *self.state.speed_ach.get_fresh(|| format_dbg!())? * dt,
795 || format_dbg!(),
796 )?;
797 Ok(())
798 }
799
800 fn reset_cumulative<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
801 self.state
802 .reset_cumulative(|| format!("{}\n{}", loc(), format_dbg!()))?;
803 self.pt_type
804 .reset_cumulative(|| format!("{}\n{}", loc(), format_dbg!()))?;
805 self.cabin
806 .reset_cumulative(|| format!("{}\n{}", loc(), format_dbg!()))?;
807 self.hvac
808 .reset_cumulative(|| format!("{}\n{}", loc(), format_dbg!()))?;
809 self.state.dist.mark_stale();
811 self.state.dist.update(si::Length::ZERO, || format_dbg!())?;
812 self.state.time.mark_stale();
813 self.state.time.update(si::Time::ZERO, || format_dbg!())?;
814 self.state.speed_ach.mark_stale();
815 self.state
816 .speed_ach
817 .update(si::Velocity::ZERO, || format_dbg!())?;
818 Ok(())
819 }
820}
821
822impl Vehicle {
823 pub fn get_pwr_rated(&self) -> si::Power {
826 match (self.fc(), self.res()) {
827 (Some(fc), Some(res)) => fc.pwr_out_max + res.pwr_out_max,
828 (Some(fc), None) => fc.pwr_out_max,
829 (None, Some(res)) => res.pwr_out_max,
830 (None, None) => unreachable!(),
831 }
832 }
833
834 pub fn conv(&self) -> Option<&ConventionalVehicle> {
835 self.pt_type.conv()
836 }
837
838 pub fn hev(&self) -> Option<&HybridElectricVehicle> {
839 self.pt_type.hev()
840 }
841
842 pub fn bev(&self) -> Option<&BatteryElectricVehicle> {
847 self.pt_type.bev()
848 }
849
850 pub fn conv_mut(&mut self) -> Option<&mut ConventionalVehicle> {
851 self.pt_type.conv_mut()
852 }
853
854 pub fn hev_mut(&mut self) -> Option<&mut HybridElectricVehicle> {
855 self.pt_type.hev_mut()
856 }
857
858 pub fn bev_mut(&mut self) -> Option<&mut BatteryElectricVehicle> {
863 self.pt_type.bev_mut()
864 }
865
866 pub fn fc(&self) -> Option<&FuelConverter> {
867 self.pt_type.fc()
868 }
869
870 pub fn fc_mut(&mut self) -> Option<&mut FuelConverter> {
871 self.pt_type.fc_mut()
872 }
873
874 pub fn set_fc(&mut self, fc: FuelConverter) -> anyhow::Result<()> {
875 self.pt_type.set_fc(fc)
876 }
877
878 pub fn fs(&self) -> Option<&FuelStorage> {
879 self.pt_type.fs()
880 }
881
882 pub fn fs_mut(&mut self) -> Option<&mut FuelStorage> {
883 self.pt_type.fs_mut()
884 }
885
886 pub fn set_fs(&mut self, fs: FuelStorage) -> anyhow::Result<()> {
887 self.pt_type.set_fs(fs)
888 }
889
890 pub fn res(&self) -> Option<&ReversibleEnergyStorage> {
891 self.pt_type.res()
892 }
893
894 pub fn res_mut(&mut self) -> Option<&mut ReversibleEnergyStorage> {
895 self.pt_type.res_mut()
896 }
897
898 pub fn set_res(&mut self, res: ReversibleEnergyStorage) -> anyhow::Result<()> {
899 self.pt_type.set_res(res)
900 }
901
902 pub fn em(&self) -> Option<&ElectricMachine> {
903 self.pt_type.em()
904 }
905
906 pub fn em_mut(&mut self) -> Option<&mut ElectricMachine> {
907 self.pt_type.em_mut()
908 }
909
910 pub fn set_em(&mut self, em: ElectricMachine) -> anyhow::Result<()> {
911 self.pt_type.set_em(em)
912 }
913
914 pub fn trans(&self) -> Option<&Transmission> {
915 self.pt_type.trans()
916 }
917
918 pub fn trans_mut(&mut self) -> Option<&mut Transmission> {
919 self.pt_type.trans_mut()
920 }
921
922 pub fn set_trans(&mut self, trans: Transmission) -> anyhow::Result<()> {
923 self.pt_type.set_trans(trans)
924 }
925
926 fn calculate_wheel_radius(&mut self) -> anyhow::Result<()> {
928 ensure!(
929 self.chassis.wheel_radius.is_some() || self.chassis.tire_code.is_some(),
930 "Either `wheel_radius` or `tire_code` must be supplied"
931 );
932 if self.chassis.wheel_radius.is_none() {
933 self.chassis.wheel_radius =
934 Some(utils::tire_code_to_radius(self.chassis.tire_code.as_ref().unwrap())? * uc::M)
935 }
936 Ok(())
937 }
938
939 pub fn solve_powertrain(&mut self, dt: si::Time) -> anyhow::Result<()> {
941 self.pt_type
942 .solve(
943 *self.state.pwr_tractive.get_fresh(|| format_dbg!())?,
944 true, dt,
946 )
947 .map_err(|err| {
948 anyhow::anyhow!(
949 "solve() failed at line {} with originating error [{}]",
950 format_dbg!(),
951 err
952 )
953 })?;
954 self.state.pwr_brake.update(
955 -self
956 .state
957 .pwr_tractive
958 .get_fresh(|| format_dbg!())?
959 .max(si::Power::ZERO)
960 - self.pt_type.pwr_regen().with_context(|| format_dbg!())?,
961 || format_dbg!(),
962 )?;
963 Ok(())
964 }
965
966 pub fn set_curr_pwr_out_max(&mut self, dt: si::Time) -> anyhow::Result<()> {
967 let cg_height_abs = self.chassis.cg_height.abs();
976 let weight_transfer_sign: f64 = match self.chassis.drive_type {
977 chassis::DriveTypes::FWD => 1.0, chassis::DriveTypes::RWD => -1.0, chassis::DriveTypes::AWD | chassis::DriveTypes::FourWD => 0.0, };
981 let max_trac_accel =
982 self.chassis.wheel_fric_coef * self.chassis.drive_axle_weight_frac * uc::ACC_GRAV
983 / (1.0 * uc::R
984 + weight_transfer_sign * cg_height_abs * self.chassis.wheel_fric_coef
985 / self.chassis.wheel_base);
986 let prev_speed = *self.state.speed_ach.get_stale(|| format_dbg!())?;
987 let max_trac_speed = prev_speed + (max_trac_accel * dt);
988 self.state
989 .speed_trac_fwd_max
990 .update(max_trac_speed, || format_dbg!())?;
991
992 self.pt_type
994 .set_curr_pwr_prop_out_max(
995 (si::Power::ZERO, si::Power::ZERO),
996 *self.state.pwr_aux.get_fresh(|| format_dbg!())?,
997 dt,
998 &self.state,
999 )
1000 .with_context(|| anyhow!(format_dbg!()))?;
1001 let pwr_prop_maxes = self
1002 .pt_type
1003 .get_curr_pwr_prop_out_max()
1004 .with_context(|| anyhow!(format_dbg!()))?;
1005 self.state
1006 .pwr_prop_fwd_max
1007 .update(pwr_prop_maxes.0, || format_dbg!())?;
1008 self.state
1009 .pwr_prop_bwd_max
1010 .update(pwr_prop_maxes.1, || format_dbg!())?;
1011
1012 Ok(())
1013 }
1014
1015 pub fn solve_thermal(
1016 &mut self,
1017 te_amb_air: si::Temperature,
1018 dt: si::Time,
1019 ) -> anyhow::Result<()> {
1020 let te_fc: Option<si::Temperature> = self
1021 .fc()
1022 .and_then(|fc| fc.temperature().map(|fct| fct.get_stale(|| format_dbg!())))
1023 .transpose()
1024 .with_context(|| {
1025 format!(
1026 "{}\nfuel converter temperature has not been properly set",
1027 format_dbg!()
1028 )
1029 })?
1030 .copied();
1031 let pwr_thrml_cab_to_res: si::Power = match self.res() {
1032 Some(res) => match &res.thrml {
1033 RESThermalOption::RESLumpedThermal(rlt) => {
1034 *rlt.state.pwr_thrml_from_cabin.get_stale(|| format_dbg!())?
1035 }
1036 RESThermalOption::None => si::Power::ZERO,
1037 },
1038 None => si::Power::ZERO,
1039 };
1040
1041 let (pwr_thrml_fc_to_cabin, pwr_thrml_hvac_to_res, te_cab) = self
1042 .solve_hvac_cab_res(te_amb_air, dt, te_fc, pwr_thrml_cab_to_res)
1043 .with_context(|| format_dbg!())?;
1044
1045 self.pt_type
1046 .solve_thermal(
1047 te_amb_air,
1048 pwr_thrml_fc_to_cabin,
1049 &mut self.state,
1050 pwr_thrml_hvac_to_res,
1051 te_cab,
1052 dt,
1053 )
1054 .with_context(|| format_dbg!())?;
1055 Ok(())
1056 }
1057
1058 fn solve_hvac_cab_res(
1059 &mut self,
1060 te_amb_air: si::Temperature,
1061 dt: si::Time,
1062 te_fc: Option<si::Temperature>,
1063 pwr_thrml_cab_to_res: si::Power,
1064 ) -> anyhow::Result<(
1065 Option<si::Power>,
1066 Option<si::Power>,
1067 Option<si::Temperature>,
1068 )> {
1069 let res_thrml_state = self.pt_type.res_mut().and_then(|rm| rm.res_thrml_state());
1070 let (pwr_thrml_fc_to_cabin, pwr_thrml_hvac_to_res, te_cab): (
1071 Option<si::Power>,
1072 Option<si::Power>,
1073 Option<si::Temperature>,
1074 ) = match (&mut self.cabin, &mut self.hvac, res_thrml_state) {
1075 (CabinOption::None, HVACOption::None, None) => {
1076 self.state
1077 .pwr_aux
1078 .update(self.pwr_aux_base, || format_dbg!())?;
1079 (None, None, None)
1080 }
1081 (CabinOption::LumpedCabin(cab), HVACOption::LumpedCabin(hvac), None) => {
1082 let (pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cab) = hvac
1083 .solve(te_amb_air, te_fc, &cab.state, cab.heat_capacitance, dt)
1084 .with_context(|| format_dbg!())?;
1085 let te_cab = cab
1086 .solve(
1087 te_amb_air,
1088 &self.state,
1089 pwr_thrml_hvac_to_cabin,
1090 Default::default(),
1091 dt,
1092 )
1093 .with_context(|| format_dbg!())?;
1094 self.state.pwr_aux.update(
1095 self.pwr_aux_base
1096 + *hvac
1097 .state
1098 .pwr_aux_for_hvac
1099 .get_fresh(|| format_dbg!("hvac.state.pwr_aux_for_hvac"))?,
1100 || format_dbg!(),
1101 )?;
1102 (Some(pwr_thrml_fc_to_cab), None, Some(te_cab))
1103 }
1104 (
1105 CabinOption::LumpedCabin(cab),
1106 HVACOption::LumpedCabinAndRES(hvac),
1107 Some(res_thrml_state),
1108 ) => {
1109 let (pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cab, pwr_thrml_hvac_to_res) = hvac
1110 .solve(
1111 te_amb_air,
1112 te_fc,
1113 &cab.state,
1114 cab.heat_capacitance,
1115 res_thrml_state,
1116 dt,
1117 )
1118 .with_context(|| format_dbg!())?;
1119 let te_cab = cab
1120 .solve(
1121 te_amb_air,
1122 &self.state,
1123 pwr_thrml_hvac_to_cabin,
1124 pwr_thrml_cab_to_res,
1125 dt,
1126 )
1127 .with_context(|| format_dbg!())?;
1128 self.state.pwr_aux.update(
1129 self.pwr_aux_base
1130 + *hvac
1131 .state
1132 .pwr_aux_for_cab_hvac
1133 .get_fresh(|| format_dbg!())?
1134 + *hvac
1135 .state
1136 .pwr_aux_for_res_hvac
1137 .get_fresh(|| format_dbg!())?,
1138 || format_dbg!(),
1139 )?;
1140 ensure!(
1141 *self.state.pwr_aux.get_fresh(|| format_dbg!())? > si::Power::ZERO,
1142 format!(
1143 "{}\n{}\n{}",
1144 format_dbg!(self.state.pwr_aux),
1145 format_dbg!(hvac.state.pwr_aux_for_res_hvac),
1146 format_dbg!(hvac.state.pwr_aux_for_cab_hvac)
1147 )
1148 );
1149 (
1150 Some(pwr_thrml_fc_to_cab),
1151 Some(pwr_thrml_hvac_to_res),
1152 Some(te_cab),
1153 )
1154 }
1155 (CabinOption::LumpedCabin(cab), HVACOption::LumpedCabin(hvac), Some(_)) => {
1156 let (pwr_thrml_hvac_to_cabin, pwr_thrml_fc_to_cab) = hvac
1157 .solve(te_amb_air, te_fc, &cab.state, cab.heat_capacitance, dt)
1158 .with_context(|| format_dbg!())?;
1159 let te_cab = cab
1160 .solve(
1161 te_amb_air,
1162 &self.state,
1163 pwr_thrml_hvac_to_cabin,
1164 Default::default(),
1165 dt,
1166 )
1167 .with_context(|| format_dbg!())?;
1168 self.state.pwr_aux.update(
1169 self.pwr_aux_base
1170 + *hvac
1171 .state
1172 .pwr_aux_for_hvac
1173 .get_fresh(|| format_dbg!("hvac.state.pwr_aux_for_hvac"))?,
1174 || format_dbg!(),
1175 )?;
1176 (Some(pwr_thrml_fc_to_cab), None, Some(te_cab))
1177 }
1178 (CabinOption::LumpedCabin(cab), HVACOption::None, Some(_)) => {
1179 let te_cab = cab
1180 .solve(
1181 te_amb_air,
1182 &self.state,
1183 si::Power::ZERO,
1184 si::Power::ZERO,
1185 dt,
1186 )
1187 .with_context(|| format_dbg!())?;
1188 self.state
1189 .pwr_aux
1190 .update(self.pwr_aux_base, || format_dbg!())?;
1191 (None, None, Some(te_cab))
1192 }
1193 (CabinOption::LumpedCabin(cab), HVACOption::None, None) => {
1194 let te_cab = cab
1195 .solve(
1196 te_amb_air,
1197 &self.state,
1198 si::Power::ZERO,
1199 si::Power::ZERO,
1200 dt,
1201 )
1202 .with_context(|| format_dbg!())?;
1203 self.state
1204 .pwr_aux
1205 .update(self.pwr_aux_base, || format_dbg!())?;
1206 (None, None, Some(te_cab))
1207 }
1208 (_, _, _) => {
1209 bail!(
1210 "{}\nCabin, HVAC, and RESThermal configuration is either invalid or not yet implemented.\n{} - {} - {}",
1211 format_dbg!(),
1212 format!("{}", self.hvac),
1213 format!("{}", self.cabin),
1214 format!(
1215 "`res.res_thrml_state().is_some()`: {}",
1216 self.pt_type.res().and_then(|res| res.res_thrml_state()).is_some()
1217 ),
1218 );
1219 }
1220 };
1221 Ok((pwr_thrml_fc_to_cabin, pwr_thrml_hvac_to_res, te_cab))
1222 }
1223
1224 pub(crate) fn mark_non_thermal_fresh(&mut self) -> Result<(), anyhow::Error> {
1225 self.state.i.mark_stale();
1226 self.state.time.mark_stale();
1227 self.state.pwr_aux.mark_stale();
1228 self.state.mass.mark_stale();
1229 self.state.mark_fresh(|| format_dbg!())?;
1230 self.state.energy_tractive.mark_stale();
1231 self.state.energy_aux.mark_stale();
1232 self.state.energy_drag.mark_stale();
1233 self.state.energy_accel.mark_stale();
1234 self.state.energy_ascent.mark_stale();
1235 self.state.energy_rr.mark_stale();
1236 self.state.energy_whl_inertia.mark_stale();
1237 self.state.energy_brake.mark_stale();
1238 self.state.dist.mark_stale();
1239 if let Some(fc) = self.fc_mut() {
1240 fc.state.i.mark_stale();
1241 fc.state.mark_fresh(|| format_dbg!())?;
1242 fc.state.energy_prop.mark_stale();
1243 fc.state.energy_aux.mark_stale();
1244 fc.state.energy_fuel.mark_stale();
1245 fc.state.energy_loss.mark_stale();
1246 }
1247 if let Some(res) = self.res_mut() {
1248 res.state.i.mark_stale();
1249 res.state.soh.mark_stale();
1250 res.state.mark_fresh(|| format_dbg!())?;
1251 res.state.energy_out_electrical.mark_stale();
1252 res.state.energy_out_prop.mark_stale();
1253 res.state.energy_aux.mark_stale();
1254 res.state.energy_loss.mark_stale();
1255 res.state.energy_out_chemical.mark_stale();
1256 }
1257
1258 if let Some(em) = self.em_mut() {
1259 em.state.i.mark_stale();
1260 em.state.mark_fresh(|| format_dbg!())?;
1261 em.state.energy_out_req.mark_stale();
1262 em.state.energy_elec_prop_in.mark_stale();
1263 em.state.energy_mech_prop_out.mark_stale();
1264 em.state.energy_mech_dyn_brake.mark_stale();
1265 em.state.energy_elec_dyn_brake.mark_stale();
1266 em.state.energy_loss.mark_stale();
1267 }
1268 if let Some(trans) = self.trans_mut() {
1269 trans.state.i.mark_stale();
1270 trans.state.mark_fresh(|| format_dbg!())?;
1271 trans.state.energy_out.mark_stale();
1272 trans.state.energy_in.mark_stale();
1273 trans.state.energy_loss.mark_stale();
1274 }
1275 if let PowertrainType::ConventionalVehicle(conv) = &mut self.pt_type {
1276 match &mut conv.pt_cntrl {
1277 ConvPowertrainControls::StartStop(cntrl) => cntrl.state.i.mark_stale(),
1278 ConvPowertrainControls::Normal => {}
1279 }
1280 conv.pt_cntrl.mark_fresh(|| format_dbg!())?;
1281 conv.dfco_cntrl.state.i.mark_stale();
1282 conv.dfco_cntrl.mark_fresh(|| format_dbg!())?;
1283 }
1284 if let PowertrainType::HybridElectricVehicle(hev) = &mut self.pt_type {
1285 match &mut hev.pt_cntrl {
1286 HEVPowertrainControls::RGWDB(rgwdb) => rgwdb.state.i.mark_stale(),
1287 HEVPowertrainControls::StartStop(cntrl) => cntrl.state.i.mark_stale(),
1288 }
1289 hev.pt_cntrl.mark_fresh(|| format_dbg!())?
1290 }
1291 Ok(())
1292 }
1293}
1294
1295#[serde_api]
1297#[derive(
1298 Clone, Debug, Deserialize, Serialize, PartialEq, HistoryVec, StateMethods, SetCumulative,
1299)]
1300#[non_exhaustive]
1301#[serde(default)]
1302#[serde(deny_unknown_fields)]
1303pub struct VehicleState {
1304 pub i: TrackedState<usize>,
1306
1307 pub time: TrackedState<si::Time>,
1309
1310 pub pwr_prop_fwd_max: TrackedState<si::Power>,
1313 pub speed_trac_fwd_max: TrackedState<si::Velocity>,
1315 pub pwr_prop_bwd_max: TrackedState<si::Power>,
1318 pub pwr_tractive: TrackedState<si::Power>,
1320 pub pwr_tractive_for_cyc: TrackedState<si::Power>,
1322 pub energy_tractive: TrackedState<si::Energy>,
1324 pub pwr_aux: TrackedState<si::Power>,
1326 pub energy_aux: TrackedState<si::Energy>,
1328 pub pwr_drag: TrackedState<si::Power>,
1330 pub energy_drag: TrackedState<si::Energy>,
1332 pub pwr_accel: TrackedState<si::Power>,
1334 pub energy_accel: TrackedState<si::Energy>,
1336 pub pwr_ascent: TrackedState<si::Power>,
1338 pub energy_ascent: TrackedState<si::Energy>,
1340 pub pwr_rr: TrackedState<si::Power>,
1342 pub energy_rr: TrackedState<si::Energy>,
1344 pub pwr_whl_inertia: TrackedState<si::Power>,
1346 pub energy_whl_inertia: TrackedState<si::Energy>,
1348 pub pwr_brake: TrackedState<si::Power>,
1350 pub energy_brake: TrackedState<si::Energy>,
1352 pub cyc_met: TrackedState<bool>,
1356 pub cyc_met_overall: TrackedState<bool>,
1359 pub speed_ach: TrackedState<si::Velocity>,
1361 pub dist: TrackedState<si::Length>,
1363 pub grade_curr: TrackedState<si::Ratio>,
1365 pub elev_curr: TrackedState<si::Length>,
1368 pub air_density: TrackedState<si::MassDensity>,
1370 pub mass: TrackedState<si::Mass>,
1373}
1374
1375impl SerdeAPI for VehicleState {}
1376impl Init for VehicleState {}
1377impl Default for VehicleState {
1378 fn default() -> Self {
1379 Self {
1380 i: TrackedState::new(Default::default()),
1381 time: Default::default(),
1382 pwr_prop_fwd_max: Default::default(),
1383 speed_trac_fwd_max: Default::default(),
1384 pwr_prop_bwd_max: Default::default(),
1385 pwr_tractive: Default::default(),
1386 pwr_tractive_for_cyc: Default::default(),
1387 energy_tractive: Default::default(),
1388 pwr_aux: Default::default(),
1389 energy_aux: Default::default(),
1390 pwr_drag: Default::default(),
1391 energy_drag: Default::default(),
1392 pwr_accel: Default::default(),
1393 energy_accel: Default::default(),
1394 pwr_ascent: Default::default(),
1395 energy_ascent: Default::default(),
1396 pwr_rr: Default::default(),
1397 energy_rr: Default::default(),
1398 pwr_whl_inertia: Default::default(),
1399 energy_whl_inertia: Default::default(),
1400 pwr_brake: Default::default(),
1401 energy_brake: Default::default(),
1402 cyc_met: TrackedState::new(true),
1403 cyc_met_overall: TrackedState::new(true),
1404 speed_ach: Default::default(),
1405 dist: Default::default(),
1406 grade_curr: Default::default(),
1408 elev_curr: Default::default(),
1410 air_density: Default::default(),
1411 mass: TrackedState::new(uc::KG * f64::NAN),
1412 }
1413 }
1414}
1415
1416#[cfg(test)]
1417pub(crate) mod tests {
1418 use crate::vehicle::conv::{ConvPowertrainControls, ConvStartStopControl};
1419 use crate::vehicle::hev::{HEVAuxControls, HEVSimulationParams, HEVStartStopControl};
1420 use crate::vehicle::powertrain::reversible_energy_storage::RESEfficiency;
1421
1422 use super::*;
1423
1424 #[allow(dead_code)]
1425 fn vehicles_dir() -> PathBuf {
1426 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/vehicles")
1427 }
1428
1429 #[test]
1430 #[cfg(feature = "yaml")]
1431 pub(crate) fn test_conv_veh_init() {
1432 assert!(Vehicle::from_resource("2012_Ford_Fusion.yaml", false).is_ok());
1433 }
1434
1435 type StructWithResources = Vehicle;
1436
1437 #[test]
1438 #[cfg(all(feature = "compat", feature = "yaml"))]
1439 fn test_f2_vehicle_assets_load_via_from_reader() {
1440 let vehicle_assets = crate::compat::fastsim_2::ASSETS_DIR
1441 .get_dir("vehicles")
1442 .unwrap();
1443
1444 for file in vehicle_assets.files() {
1445 let mut contents = file.contents();
1446 let result = Vehicle::from_reader(&mut contents, "yaml", false);
1447 assert!(
1448 result.is_ok(),
1449 "from_reader failed for {:?}: {:?}",
1450 file.path(),
1451 result.err()
1452 );
1453 }
1454 }
1455
1456 #[test]
1457 #[cfg(all(feature = "compat", feature = "yaml"))]
1458 fn test_f2_vehicle_assets_load_via_from_yaml() {
1459 let vehicle_assets = crate::compat::fastsim_2::ASSETS_DIR
1460 .get_dir("vehicles")
1461 .unwrap();
1462
1463 for file in vehicle_assets.files() {
1464 let yaml_str = std::str::from_utf8(file.contents()).unwrap();
1465 let result = Vehicle::from_yaml(yaml_str, false);
1466 assert!(
1467 result.is_ok(),
1468 "from_yaml failed for {:?}: {:?}",
1469 file.path(),
1470 result.err()
1471 );
1472 }
1473 }
1474
1475 #[test]
1476 fn test_resources() {
1477 let mut time_to_panic = false;
1478
1479 let resource_list = StructWithResources::list_resources().unwrap();
1480 assert!(!resource_list.is_empty());
1481
1482 for resource in resource_list {
1484 if let Err(e) = StructWithResources::from_resource(resource.clone(), false) {
1485 time_to_panic = true;
1486 eprintln!("Error loading {resource:?}: {e}\n");
1487 }
1488 }
1489 if time_to_panic {
1490 panic!()
1491 }
1492 }
1493
1494 #[test]
1495 fn test_calibrated_vehicles() {
1496 let mut time_to_panic = false;
1497
1498 let paths: Vec<_> = std::fs::read_dir("../cal_and_val/f3-vehicles")
1500 .unwrap()
1501 .collect();
1502 assert!(!paths.is_empty());
1503 for path in paths {
1504 let p = path.unwrap().path();
1505 if let Err(e) = StructWithResources::from_file(p.clone(), false) {
1506 time_to_panic = true;
1507 eprintln!("Error loading {p:?}: {e}\n");
1508 }
1509 }
1510
1511 let paths: Vec<_> = std::fs::read_dir("../cal_and_val/thermal/f3-vehicles")
1513 .unwrap()
1514 .collect();
1515 assert!(!paths.is_empty());
1516 for path in paths {
1517 let p = path.unwrap().path();
1518 if let Err(e) = StructWithResources::from_file(p.clone(), false) {
1519 time_to_panic = true;
1520 eprintln!("Error loading {p:?}: {e}\n");
1521 }
1522 }
1523
1524 if time_to_panic {
1525 panic!()
1526 }
1527 }
1528
1529 fn make_conv_pacifica(with_conv_start_stop: bool, with_dfco: bool) -> anyhow::Result<Vehicle> {
1530 let fs = FuelStorage::new(
1531 2000000.0 * uc::W,
1532 1.1 * uc::S,
1533 Some(FuelType::Gasoline),
1534 2305080000.0 * uc::J,
1535 None,
1536 None,
1537 )?;
1538 let fc = FuelConverter::new(
1539 FuelConverterThermalOption::None, None, None, 211088.0 * uc::W, 34604.59016393443 * uc::W, 6.1 * uc::S, InterpolatorEnum::new_1d(
1546 vec![
1547 0.0, 0.005, 0.015, 0.04, 0.06, 0.1, 0.14, 0.2, 0.4, 0.6, 0.8, 1.0,
1548 ]
1549 .into(),
1550 vec![
1551 0.0,
1552 0.0875106035,
1553 0.143482108,
1554 0.216273855,
1555 0.252599848,
1556 0.301508117,
1557 0.33,
1558 0.34,
1559 0.35,
1560 0.34,
1561 0.32,
1562 0.3,
1563 ]
1564 .into(),
1565 strategy::Linear,
1566 Extrapolate::Error,
1567 )?, 0.4 * 211088.0 * uc::W, 0.0 * uc::W, None,
1571 )?;
1572 let tx = Transmission::new(
1573 None, InterpolatorEnum::new_0d(0.95), None, )?;
1577 let pt_controls = {
1578 if with_conv_start_stop {
1579 let cntrl = ConvStartStopControl::new(
1580 None, None, None, None, None, )
1586 .map_err(|err| {
1587 assert!(
1588 false,
1589 "Unable to create start-stop control for conv: {}",
1590 err
1591 );
1592 });
1593 let cntrl = cntrl.ok().unwrap();
1594 ConvPowertrainControls::StartStop(Box::new(cntrl))
1595 } else {
1596 ConvPowertrainControls::Normal
1597 }
1598 };
1599 let dfco_controls = conv::DfcoControls::new(
1600 with_dfco, 25.0 * uc::MPH, -0.2 * uc::MPS2, None, )?;
1605 let conv = ConventionalVehicle::new(
1606 fs, fc, tx, None, pt_controls, dfco_controls, 1.0 * uc::R, )?;
1614 let chassis = Chassis {
1615 drag_coef: 0.3303036837542712 * uc::R,
1616 frontal_area: 3.05124164 * uc::M2,
1617 wheel_rr_coef: 0.0064798953284486704 * uc::R,
1618 wheel_inertia: 0.815 * uc::KGM2,
1619 num_wheels: 4,
1620 wheel_radius: Some(0.36865 * uc::M),
1621 tire_code: None,
1622 cg_height: 0.53 * uc::M,
1623 wheel_fric_coef: 0.8 * uc::R,
1624 drive_type: chassis::DriveTypes::FWD,
1625 drive_axle_weight_frac: 0.61 * uc::R,
1626 wheel_base: 3.08864 * uc::M,
1627 mass: None,
1628 glider_mass: None,
1629 cargo_mass: None,
1630 };
1631 let boxed_conv = Box::new(conv);
1632 let mut veh = Vehicle::new(
1633 String::from("2026 Chrysler Pacifica Select"), None,
1635 None,
1636 None,
1637 Some(String::from("2026")),
1638 Some(String::from("Chrysler")),
1639 Some(String::from("Pacifica")),
1640 Some(String::from("Select")),
1641 PowertrainType::ConventionalVehicle(boxed_conv),
1642 chassis,
1643 CabinOption::None,
1644 HVACOption::None,
1645 Some(2154.564 * uc::KG),
1646 700.0 * uc::W,
1647 None,
1648 )?;
1649 veh.set_save_interval(Option::Some(1))?;
1650 Ok(veh)
1651 }
1652
1653 fn make_microhybrid_pacifica() -> anyhow::Result<Vehicle> {
1654 let res = ReversibleEnergyStorage::new(
1655 RESThermalOption::None, None, None, 5.0 * uc::KW, 1.0 * uc::KWH, RESEfficiency::Constant(Interp0D(0.9)), 0.0 * uc::R, 1.0 * uc::R, None,
1664 )?;
1665 let fs = FuelStorage::new(
1666 2000000.0 * uc::W,
1667 1.1 * uc::S,
1668 Some(FuelType::Gasoline),
1669 2305080000.0 * uc::J,
1670 None,
1671 None,
1672 )?;
1673 let fc = FuelConverter::new(
1674 FuelConverterThermalOption::None, None, None, 211088.0 * uc::W, 34604.59016393443 * uc::W, 6.1 * uc::S, InterpolatorEnum::new_1d(
1681 vec![
1682 0.0, 0.005, 0.015, 0.04, 0.06, 0.1, 0.14, 0.2, 0.4, 0.6, 0.8, 1.0,
1683 ]
1684 .into(),
1685 vec![
1686 0.0,
1687 0.0875106035,
1688 0.143482108,
1689 0.216273855,
1690 0.252599848,
1691 0.301508117,
1692 0.33,
1693 0.34,
1694 0.35,
1695 0.34,
1696 0.32,
1697 0.3,
1698 ]
1699 .into(),
1700 strategy::Linear,
1701 Extrapolate::Error,
1702 )?, 0.4 * 211088.0 * uc::W, 0.0 * uc::W, None,
1706 )?;
1707 let em = ElectricMachine::new(
1708 InterpolatorEnum::new_1d(
1709 vec![0.0, 1.0].into(),
1710 vec![0.95, 0.95].into(),
1711 strategy::Linear,
1712 Extrapolate::Error,
1713 )?, None, 5.0 * uc::KW, None, None, None, )?;
1720 let tx = Transmission::new(
1721 None, InterpolatorEnum::new_0d(0.95), None, )?;
1725 let cntrl = HEVStartStopControl::new(
1726 None, None, None, None, None, None, None, None, )?;
1735 let pt_cntrl = HEVPowertrainControls::StartStop(Box::new(cntrl));
1736 let aux_cntrl = HEVAuxControls::AuxOnResPriority;
1737 let sim_params = HEVSimulationParams::new(
1738 0.05 * uc::R, 5, false, false, )?;
1743 let hev = HybridElectricVehicle::new(
1744 res, fs, fc, em, tx, pt_cntrl, aux_cntrl, None, sim_params, )?;
1754 let chassis = Chassis {
1755 drag_coef: 0.3303036837542712 * uc::R,
1756 frontal_area: 3.05124164 * uc::M2,
1757 wheel_rr_coef: 0.0064798953284486704 * uc::R,
1758 wheel_inertia: 0.815 * uc::KGM2,
1759 num_wheels: 4,
1760 wheel_radius: Some(0.36865 * uc::M),
1761 tire_code: None,
1762 cg_height: 0.53 * uc::M,
1763 wheel_fric_coef: 0.8 * uc::R,
1764 drive_type: chassis::DriveTypes::FWD,
1765 drive_axle_weight_frac: 0.61 * uc::R,
1766 wheel_base: 3.08864 * uc::M,
1767 mass: None,
1768 glider_mass: None,
1769 cargo_mass: None,
1770 };
1771 let boxed_hev = Box::new(hev);
1772 let mut veh = Vehicle::new(
1773 String::from("2026 Chrysler Pacifica Select (uHEV Test)"),
1774 None,
1775 None,
1776 None,
1777 Some(String::from("2026")),
1778 Some(String::from("Chrysler")),
1779 Some(String::from("Pacifica")),
1780 Some(String::from("Select")),
1781 PowertrainType::HybridElectricVehicle(boxed_hev),
1782 chassis,
1783 CabinOption::None,
1784 HVACOption::None,
1785 Some(2154.564 * uc::KG),
1786 700.0 * uc::W,
1787 None,
1788 )?;
1789 veh.set_save_interval(Option::Some(1))?;
1790 Ok(veh)
1791 }
1792
1793 #[test]
1794 fn we_can_create_and_simulate_a_micro_hybrid_vehicle() {
1795 let veh_result = make_microhybrid_pacifica();
1796 assert!(veh_result.is_ok());
1797 let veh = veh_result.unwrap();
1798 let cyc = crate::drive_cycle::Cycle::from_resource("udds.csv", false).unwrap();
1799 let mut sd = crate::simdrive::SimDrive::new(veh, cyc, Default::default());
1800 let run_result = sd.run();
1801 if let Err(err) = run_result {
1802 panic!("Error: {}", err);
1803 }
1804 assert!(run_result.is_ok());
1805 }
1806
1807 fn accumulate_for_zero_speed(speeds_mps: &[f64], fuels_mj: &[f64]) -> f64 {
1808 let shortest_idx = speeds_mps.len().min(fuels_mj.len());
1809 let mut result_mj = 0.0;
1810 for idx in 0..shortest_idx {
1811 if speeds_mps[idx] == 0.0 {
1812 result_mj += fuels_mj[idx];
1813 }
1814 }
1815 result_mj
1816 }
1817
1818 #[test]
1819 fn micro_hybrid_saves_more_fuel_than_conventional() {
1820 let veh_uhev_result = make_microhybrid_pacifica();
1821 assert!(veh_uhev_result.is_ok());
1822 let veh_uhev = veh_uhev_result.unwrap();
1823 let veh_conv_result = make_conv_pacifica(false, false);
1824 assert!(veh_conv_result.is_ok());
1825 let veh_conv = veh_conv_result.unwrap();
1826 let cyc = crate::drive_cycle::Cycle::from_resource("udds.csv", false).unwrap();
1827 let mut sd_uhev = crate::simdrive::SimDrive::new(veh_uhev, cyc.clone(), Default::default());
1828 let result_uhev = sd_uhev.run();
1829 if let Err(err) = result_uhev {
1830 panic!("Error: {}", err);
1831 }
1832 assert!(result_uhev.is_ok());
1833 let mut sd_conv = crate::simdrive::SimDrive::new(veh_conv, cyc.clone(), Default::default());
1834 let result_conv = sd_conv.run();
1835 assert!(result_conv.is_ok());
1836 let speeds_mps: Vec<f64> = cyc
1837 .speed
1838 .iter()
1839 .map(|spd| spd.get::<si::meter_per_second>())
1840 .collect();
1841 let fc_uhev = sd_uhev.veh.pt_type.fc().unwrap();
1842 let fc_conv = sd_conv.veh.pt_type.fc().unwrap();
1843 let fuels_uhev_mj: Vec<f64> = fc_uhev
1844 .history
1845 .energy_fuel
1846 .iter()
1847 .map(|ef| ef.get_fresh(|| format_dbg!()).unwrap().get::<si::joule>() / 1e6)
1848 .collect();
1849 let fuel_uhev_mj: f64 = fuels_uhev_mj.iter().sum();
1850 let fuels_conv_mj: Vec<f64> = fc_conv
1851 .history
1852 .energy_fuel
1853 .iter()
1854 .map(|ef| ef.get_fresh(|| format_dbg!()).unwrap().get::<si::joule>() / 1e6)
1855 .collect();
1856 let fuel_conv_mj: f64 = fuels_conv_mj.iter().sum();
1857 assert_eq!(speeds_mps.len(), fuels_uhev_mj.len());
1858 assert_eq!(speeds_mps.len(), fuels_conv_mj.len());
1859 eprintln!("fuel_uhev: {} MJ", fuel_uhev_mj);
1860 eprintln!("fuel_conv: {} MJ", fuel_conv_mj);
1861 assert!(
1862 fuel_uhev_mj < fuel_conv_mj,
1863 "Expected uHEV fuel ({fuel_uhev_mj}) to be less than conventional ({fuel_conv_mj})"
1864 );
1865 let fuel_stopped_uhev_mj = accumulate_for_zero_speed(&speeds_mps, &fuels_uhev_mj);
1866 let fuel_stopped_conv_mj = accumulate_for_zero_speed(&speeds_mps, &fuels_conv_mj);
1867 eprintln!("fuel_stopped_uhev: {} MJ", fuel_stopped_uhev_mj);
1868 eprintln!("fuel_stopped_conv: {} MJ", fuel_stopped_conv_mj);
1869 assert!(
1870 fuel_stopped_uhev_mj < fuel_stopped_conv_mj,
1871 "Expected stopped uHEV fuel ({fuel_stopped_uhev_mj}) to be less than stopped conventional ({fuel_stopped_conv_mj})"
1872 );
1873 }
1874
1875 #[test]
1876 fn start_stop_conv_saves_more_fuel_than_normal_conventional() {
1877 let veh_ss_result = make_conv_pacifica(true, false);
1878 assert!(veh_ss_result.is_ok());
1879 let veh_ss = veh_ss_result.unwrap();
1880 let veh_conv_result = make_conv_pacifica(false, false);
1881 assert!(veh_conv_result.is_ok());
1882 let veh_conv = veh_conv_result.unwrap();
1883 let cyc = crate::drive_cycle::Cycle::from_resource("udds.csv", false).unwrap();
1884 let mut sd_ss = crate::simdrive::SimDrive::new(veh_ss, cyc.clone(), Default::default());
1885 let result_ss = sd_ss.run();
1886 if let Err(err) = result_ss {
1887 panic!("Error: {}", err);
1888 }
1889 assert!(result_ss.is_ok());
1890 let mut sd_conv = crate::simdrive::SimDrive::new(veh_conv, cyc.clone(), Default::default());
1891 let result_conv = sd_conv.run();
1892 assert!(result_conv.is_ok());
1893 let speeds_mps: Vec<f64> = cyc
1894 .speed
1895 .iter()
1896 .map(|spd| spd.get::<si::meter_per_second>())
1897 .collect();
1898 let fc_ss = sd_ss.veh.pt_type.fc().unwrap();
1899 let fc_conv = sd_conv.veh.pt_type.fc().unwrap();
1900 let fuels_ss_mj: Vec<f64> = fc_ss
1901 .history
1902 .energy_fuel
1903 .iter()
1904 .map(|ef| ef.get_fresh(|| format_dbg!()).unwrap().get::<si::joule>() / 1e6)
1905 .collect();
1906 let fuel_ss_mj: f64 = fuels_ss_mj.iter().sum();
1907 let fuels_conv_mj: Vec<f64> = fc_conv
1908 .history
1909 .energy_fuel
1910 .iter()
1911 .map(|ef| ef.get_fresh(|| format_dbg!()).unwrap().get::<si::joule>() / 1e6)
1912 .collect();
1913 let fuel_conv_mj: f64 = fuels_conv_mj.iter().sum();
1914 assert_eq!(speeds_mps.len(), fuels_ss_mj.len());
1915 assert_eq!(speeds_mps.len(), fuels_conv_mj.len());
1916 eprintln!("fuel_ss: {} MJ", fuel_ss_mj);
1917 eprintln!("fuel_conv: {} MJ", fuel_conv_mj);
1918 assert!(
1919 fuel_ss_mj < fuel_conv_mj,
1920 "Expected ss fuel ({fuel_ss_mj}) to be less than conventional ({fuel_conv_mj})"
1921 );
1922 let fuel_stopped_ss_mj = accumulate_for_zero_speed(&speeds_mps, &fuels_ss_mj);
1923 let fuel_stopped_conv_mj = accumulate_for_zero_speed(&speeds_mps, &fuels_conv_mj);
1924 eprintln!("fuel_stopped_ss: {} MJ", fuel_stopped_ss_mj);
1925 eprintln!("fuel_stopped_conv: {} MJ", fuel_stopped_conv_mj);
1926 assert!(
1927 fuel_stopped_ss_mj < fuel_stopped_conv_mj,
1928 "Expected stopped ss fuel ({fuel_stopped_ss_mj}) to be less than stopped conventional ({fuel_stopped_conv_mj})"
1929 );
1930 }
1931
1932 #[test]
1933 fn that_use_start_stop_switches_the_conv_controller() {
1934 let veh_result = make_conv_pacifica(false, false);
1935 assert!(veh_result.is_ok());
1936 let mut veh = veh_result.unwrap();
1937 let use_result = veh.use_start_stop_controller();
1938 assert!(use_result.is_ok());
1939 match &veh.pt_type {
1940 PowertrainType::ConventionalVehicle(conv) => match conv.pt_cntrl {
1941 ConvPowertrainControls::Normal => {
1942 assert!(false, "Powertrain controls didn't change");
1943 }
1944 _ => (),
1945 },
1946 _ => {
1947 assert!(false, "Unexpected powertrain type");
1948 }
1949 }
1950 let normal_result = veh.use_normal_controller();
1951 assert!(normal_result.is_ok());
1952 match &veh.pt_type {
1953 PowertrainType::ConventionalVehicle(conv) => match conv.pt_cntrl {
1954 ConvPowertrainControls::StartStop(_) => {
1955 assert!(false, "Powertrain controls didn't change");
1956 }
1957 _ => (),
1958 },
1959 _ => {
1960 assert!(false, "Unexpected powertrain type");
1961 }
1962 }
1963 }
1964
1965 #[test]
1966 fn that_use_start_stop_switches_the_hev_controller() {
1967 let veh_result = make_microhybrid_pacifica();
1968 assert!(veh_result.is_ok());
1969 let mut veh = veh_result.unwrap();
1970 let use_result = veh.use_normal_controller();
1971 assert!(use_result.is_ok());
1972 match &veh.pt_type {
1973 PowertrainType::HybridElectricVehicle(hev) => match &hev.pt_cntrl {
1974 HEVPowertrainControls::StartStop(_) => {
1975 assert!(false, "Powertrain controls didn't change");
1976 }
1977 HEVPowertrainControls::RGWDB(_) => (),
1978 },
1979 _ => {
1980 assert!(false, "Unexpected powertrain type");
1981 }
1982 }
1983 let use_ss_result = veh.use_start_stop_controller();
1984 assert!(use_ss_result.is_ok());
1985 match &veh.pt_type {
1986 PowertrainType::HybridElectricVehicle(hev) => match &hev.pt_cntrl {
1987 HEVPowertrainControls::RGWDB(_) => {
1988 assert!(
1989 false,
1990 "Powertrain controls didn't change: RGWDB => StartStop"
1991 );
1992 }
1993 HEVPowertrainControls::StartStop(_) => (),
1994 },
1995 _ => {
1996 assert!(false, "Unexpected powertrain type");
1997 }
1998 }
1999 }
2000
2001 fn sum_fuel_in_mj(fc: &FuelConverter) -> f64 {
2002 let fuels_mj: Vec<f64> = fc
2003 .history
2004 .energy_fuel
2005 .iter()
2006 .map(|ef| ef.get_fresh(|| format_dbg!()).unwrap().get::<si::joule>() / 1e6)
2007 .collect();
2008 fuels_mj.iter().sum()
2009 }
2010
2011 #[test]
2012 fn that_a_vehicle_with_dfco_enabled_uses_less_fuel() {
2013 let veh_result = make_conv_pacifica(false, false);
2014 assert!(veh_result.is_ok());
2015 let veh = veh_result.unwrap();
2016 let veh_dfco_result = make_conv_pacifica(false, true);
2017 assert!(veh_dfco_result.is_ok());
2018 let veh_dfco = veh_dfco_result.unwrap();
2019 let cyc = crate::drive_cycle::Cycle::from_resource("udds.csv", false).unwrap();
2020 let mut sd = crate::simdrive::SimDrive::new(veh, cyc.clone(), Default::default());
2021 let sd_result = sd.run();
2022 if let Err(err) = sd_result {
2023 panic!("Error: {}", err);
2024 }
2025 assert!(sd_result.is_ok());
2026 let mut sd_dfco = crate::simdrive::SimDrive::new(veh_dfco, cyc.clone(), Default::default());
2027 let sd_dfco_result = sd_dfco.run();
2028 assert!(sd_dfco_result.is_ok());
2029 let fc = sd.veh.pt_type.fc().unwrap();
2030 let fc_dfco = sd_dfco.veh.pt_type.fc().unwrap();
2031 let fuel_mj: f64 = sum_fuel_in_mj(&fc);
2032 let fuel_dfco_mj: f64 = sum_fuel_in_mj(&fc_dfco);
2033 eprintln!("fuel : {} MJ", fuel_mj);
2034 eprintln!("fuel_dfco: {} MJ", fuel_dfco_mj);
2035 let percent_reduction = ((fuel_mj - fuel_dfco_mj) * 100.0) / fuel_mj;
2036 eprintln!("percent reduction: {}", percent_reduction);
2037 assert!(
2038 fuel_dfco_mj < fuel_mj,
2039 "Expected DFCO fuel ({fuel_dfco_mj}) to be less than conventional ({fuel_mj})"
2040 );
2041 }
2042}