1pub mod roadload;
2
3use roadload::StepInfo;
4
5use super::drive_cycle::Cycle;
6use super::vehicle::Vehicle;
7use crate::drive_cycle::manipulation_utils::calc_best_rendezvous;
8use crate::imports::*;
9use crate::prelude::*;
10
11#[serde_api]
12#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
13#[non_exhaustive]
14#[serde(deny_unknown_fields)]
15#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
16pub struct SimParams {
18 #[serde(default = "SimParams::def_ach_speed_max_iter")]
19 pub ach_speed_max_iter: u32,
22 #[serde(default = "SimParams::def_ach_speed_tol")]
23 pub ach_speed_tol: si::Ratio,
26 #[serde(default = "SimParams::def_ach_speed_solver_gain")]
27 pub ach_speed_solver_gain: f64,
29 #[serde(default = "SimParams::def_trace_miss_tol")]
33 pub trace_miss_tol: TraceMissTolerance,
34 #[serde(default = "SimParams::def_trace_miss_opts")]
35 pub trace_miss_opts: TraceMissOptions,
36 #[serde(default = "SimParams::def_trace_miss_correct_max_steps")]
37 pub trace_miss_correct_max_steps: u32,
42 #[serde(default = "SimParams::def_f2_const_air_density")]
44 pub f2_const_air_density: bool,
45 pub ambient_thermal_soak: bool,
47}
48
49#[pyo3_api]
50impl SimParams {
51 #[staticmethod]
52 #[pyo3(name = "default")]
53 fn default_py() -> Self {
54 Self::default()
55 }
56}
57
58impl SimParams {
59 fn def_ach_speed_max_iter() -> u32 {
60 Self::default().ach_speed_max_iter
61 }
62 fn def_ach_speed_tol() -> si::Ratio {
63 Self::default().ach_speed_tol
64 }
65 fn def_ach_speed_solver_gain() -> f64 {
66 Self::default().ach_speed_solver_gain
67 }
68 fn def_trace_miss_tol() -> TraceMissTolerance {
69 Self::default().trace_miss_tol
70 }
71 fn def_trace_miss_opts() -> TraceMissOptions {
72 Self::default().trace_miss_opts
73 }
74 fn def_trace_miss_correct_max_steps() -> u32 {
75 Self::default().trace_miss_correct_max_steps
76 }
77 fn def_f2_const_air_density() -> bool {
78 Self::default().f2_const_air_density
79 }
80}
81
82impl SerdeAPI for SimParams {}
83impl Init for SimParams {}
84
85impl Default for SimParams {
86 fn default() -> Self {
87 Self {
88 ach_speed_max_iter: 3,
89 ach_speed_tol: 1.0e-3 * uc::R,
90 ach_speed_solver_gain: 0.9,
91 trace_miss_tol: Default::default(),
92 trace_miss_opts: Default::default(),
93 trace_miss_correct_max_steps: 6,
94 f2_const_air_density: true,
95 ambient_thermal_soak: false,
96 }
97 }
98}
99
100#[serde_api]
101#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, StateMethods)]
102#[non_exhaustive]
103#[serde(deny_unknown_fields)]
104#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
105pub struct SimDrive {
106 #[has_state]
107 pub veh: Vehicle,
108 pub cyc: Cycle,
109 pub sim_params: SimParams,
110}
111
112#[pyo3_api]
113impl SimDrive {
114 #[new]
115 #[pyo3(signature = (veh, cyc, sim_params=None))]
116 fn __new__(veh: Vehicle, cyc: Cycle, sim_params: Option<SimParams>) -> anyhow::Result<Self> {
117 Ok(SimDrive::new(veh, cyc, sim_params))
118 }
119
120 #[pyo3(name = "walk_once")]
122 fn walk_once_py(&mut self) -> anyhow::Result<()> {
123 self.walk_once()
124 }
125
126 #[pyo3(name = "walk")]
130 fn walk_py(&mut self) -> anyhow::Result<()> {
131 self.walk()
132 }
133
134 #[pyo3(name = "to_fastsim2")]
135 fn to_fastsim2_py(&self) -> anyhow::Result<fastsim_2::simdrive::RustSimDrive> {
136 self.to_fastsim2()
137 }
138}
139
140impl SerdeAPI for SimDrive {}
141impl Init for SimDrive {
142 fn init(&mut self) -> Result<(), Error> {
143 self.veh
144 .init()
145 .map_err(|err| Error::InitError(format_dbg!(err)))?;
146 self.cyc
147 .init()
148 .map_err(|err| Error::InitError(format_dbg!(err)))?;
149 self.sim_params
150 .init()
151 .map_err(|err| Error::InitError(format_dbg!(err)))?;
152 Ok(())
153 }
154}
155
156impl SimDrive {
157 pub fn new(veh: Vehicle, cyc: Cycle, sim_params: Option<SimParams>) -> Self {
158 Self {
159 veh,
160 cyc,
161 sim_params: sim_params.unwrap_or_default(),
162 }
163 }
164
165 pub fn walk(&mut self) -> anyhow::Result<()> {
178 match self.veh.pt_type {
179 PowertrainType::HybridElectricVehicle(_) => {
180 let veh_init = self.veh.clone();
183 loop {
184 self.veh
185 .hev_mut()
186 .with_context(|| format_dbg!())?
187 .soc_bal_iters
188 .mark_stale();
189 self.veh
190 .hev_mut()
191 .with_context(|| format_dbg!())?
192 .soc_bal_iters
193 .increment(1, || format_dbg!())?;
194 self.walk_once().with_context(|| format_dbg!())?;
195 let soc_final = self
196 .veh
197 .res()
198 .with_context(|| format_dbg!())?
199 .state
200 .soc
201 .clone();
202 let res_per_fuel = *self
203 .veh
204 .res()
205 .with_context(|| format_dbg!())?
206 .state
207 .energy_out_chemical
208 .get_fresh(|| format_dbg!())?
209 / *self
210 .veh
211 .fc()
212 .with_context(|| format_dbg!())?
213 .state
214 .energy_fuel
215 .get_fresh(|| format_dbg!())?;
216 if self
217 .veh
218 .hev()
219 .with_context(|| format_dbg!())?
220 .soc_bal_iters
221 .get_fresh(|| format_dbg!())?
222 > &self
223 .veh
224 .hev()
225 .with_context(|| format_dbg!())?
226 .sim_params
227 .soc_balance_iter_err
228 {
229 bail!(
230 "{}",
231 format_dbg!((
232 self.veh
233 .hev()
234 .with_context(|| format_dbg!())?
235 .soc_bal_iters
236 .clone(),
237 self.veh
238 .hev()
239 .with_context(|| format_dbg!())?
240 .sim_params
241 .soc_balance_iter_err
242 ))
243 );
244 }
245 if res_per_fuel.abs()
246 < self
247 .veh
248 .hev()
249 .with_context(|| format_dbg!())?
250 .sim_params
251 .res_per_fuel_lim
252 || !self
253 .veh
254 .hev()
255 .with_context(|| format_dbg!())?
256 .sim_params
257 .balance_soc
258 || self.sim_params.ambient_thermal_soak
259 {
260 break;
261 } else {
262 if let Some(&mut ref mut hev) = self.veh.hev_mut() {
264 if hev.sim_params.save_soc_bal_iters {
265 hev.soc_bal_iter_history.push(hev.clone());
266 hev.soc_bal_iters.mark_stale();
267 }
268 }
269 self.veh = veh_init.clone();
271 self.veh.res_mut().with_context(|| format_dbg!())?.state.soc = soc_final;
273 }
274 }
275 }
276 _ => self.walk_once()?,
277 }
278 Ok(())
279 }
280
281 pub fn walk_once(&mut self) -> anyhow::Result<()> {
283 let len = &self.cyc.len_checked().with_context(|| format_dbg!())?;
284 ensure!(len >= &2, format_dbg!(len < &2));
285 self.save_state(|| format_dbg!())?;
286
287 self.veh.state.mass.mark_stale();
288 self.veh.state.mass.update(
289 self.veh
290 .mass()
291 .with_context(|| format_dbg!())?
292 .with_context(|| format_dbg!("Expected mass to have been set."))?,
293 || format_dbg!(),
294 )?;
295
296 loop {
297 self.check_and_reset(|| format_dbg!())?;
298 self.veh.state.mass.mark_fresh(|| format_dbg!())?;
299 if let Some(res) = self.veh.res_mut() {
300 res.state.soh.mark_fresh(|| format_dbg!())?;
301 }
302 self.step(|| format_dbg!())?;
303 self.solve_step()
304 .with_context(|| format!("{}\ntime step: {:?}", format_dbg!(), self.veh.state.i))?;
305 self.save_state(|| format_dbg!())?;
306 if *self.veh.state.i.get_fresh(|| format_dbg!())? == len - 1 {
307 break;
308 }
309 }
310 Ok(())
311 }
312
313 pub fn calc_dvdd(&self, speed_m_per_s: f64, grade: f64) -> anyhow::Result<f64> {
319 let v = speed_m_per_s;
320 if v <= 0.0 {
321 Ok(0.0)
322 } else {
323 let (atan_grade_sin, atan_grade_cos) = if grade == 0.0 {
324 (0.0, 1.0)
325 } else {
326 let atan_grade = grade.atan();
327 (atan_grade.sin(), atan_grade.cos())
328 };
329 let g = uc::ACC_GRAV.get::<si::meter_per_second_squared>();
330 let m = self
331 .veh
332 .mass
333 .with_context(|| {
334 format!(
335 "{}\nVehicle mass should have been set already.",
336 format_dbg!()
337 )
338 })?
339 .get::<si::kilogram>();
340 let rho_cdfa = self
341 .veh
342 .state
343 .air_density
344 .get_stale(|| format_dbg!())?
345 .get::<si::kilogram_per_cubic_meter>()
346 * self.veh.chassis.drag_coef.get::<si::ratio>()
347 * self.veh.chassis.frontal_area.get::<si::square_meter>();
348 let rrc = self.veh.chassis.wheel_rr_coef.get::<si::ratio>();
349 Ok(-1.0
350 * ((g / v) * (atan_grade_sin + rrc * atan_grade_cos)
351 + (0.5 * rho_cdfa * (1.0 / m) * v)))
352 }
353 }
354
355 pub fn solve_step(&mut self) -> anyhow::Result<()> {
357 let i = *self.veh.state.i.get_fresh(|| format_dbg!())?;
358 let time_prev = *self.veh.state.time.get_stale(|| format_dbg!())?;
359 self.veh
360 .state
361 .time
362 .update(self.cyc.time[i], || format_dbg!())?;
363 let dt = *self.veh.state.time.get_fresh(|| format_dbg!())? - time_prev;
364 self.veh
376 .solve_thermal(self.cyc.temp_amb_air[i], dt)
377 .with_context(|| format_dbg!())?;
378 match self.sim_params.ambient_thermal_soak {
379 false => {
380 self.veh
381 .set_curr_pwr_out_max(dt)
382 .with_context(|| anyhow!(format_dbg!()))?;
383 self.set_pwr_prop_for_speed(
384 self.cyc.speed[i],
385 *self.veh.state.speed_ach.get_stale(|| format_dbg!())?,
386 dt,
387 )
388 .with_context(|| anyhow!(format_dbg!()))?;
389 self.veh.state.pwr_tractive_for_cyc.update(
390 *self.veh.state.pwr_tractive.get_fresh(|| format_dbg!())?,
391 || format_dbg!(),
392 )?;
393 self.set_ach_speed(self.cyc.speed[i], dt)
394 .with_context(|| anyhow!(format_dbg!()))?;
395 if self.sim_params.trace_miss_opts.is_allow_checked() {
396 self.sim_params.trace_miss_tol.check_trace_miss(
397 self.cyc.speed[i],
398 *self.veh.state.speed_ach.get_fresh(|| format_dbg!())?,
399 self.cyc.dist[i],
400 *self.veh.state.dist.get_fresh(|| format_dbg!())?,
401 )?;
402 }
403 self.veh
404 .solve_powertrain(dt)
405 .with_context(|| anyhow!(format_dbg!()))?;
406 }
407 true => {
408 self.veh.mark_non_thermal_fresh()?;
409 }
410 }
411 self.set_cumulative(dt, || format_dbg!())?;
412 Ok(())
413 }
414
415 pub fn set_pwr_prop_for_speed(
420 &mut self,
421 speed: si::Velocity,
422 speed_prev: si::Velocity,
423 dt: si::Time,
424 ) -> anyhow::Result<()> {
425 let i = *self.veh.state.i.get_fresh(|| format_dbg!())?;
426 let vs = &mut self.veh.state;
427 let interp_pt_dist: &[f64] = match self.cyc.grade_interp {
432 Some(InterpolatorEnum::Interp0D(_)) => &[],
433 Some(InterpolatorEnum::Interp1D(_)) => {
434 &[vs.dist.get_fresh(|| format_dbg!())?.get::<si::meter>()]
435 }
436 _ => unreachable!(),
437 };
438 vs.grade_curr.update(
439 if *vs.cyc_met_overall.get_stale(|| format_dbg!())? {
440 *self
441 .cyc
442 .grade
443 .get(i)
444 .with_context(|| format_dbg!(self.cyc.grade.len()))?
445 } else {
446 uc::R
447 * self
448 .cyc
449 .grade_interp
450 .as_ref()
451 .with_context(|| format_dbg!("You might have somehow bypassed `init()`"))?
452 .interpolate(interp_pt_dist)
453 .with_context(|| format_dbg!())?
454 },
455 || format_dbg!(),
456 )?;
457 vs.elev_curr.update(
458 if *vs.cyc_met_overall.get_stale(|| format_dbg!())? {
459 *self.cyc.elev.get(i).with_context(|| format_dbg!())?
460 } else {
461 uc::M
462 * self
463 .cyc
464 .elev_interp
465 .as_ref()
466 .with_context(|| format_dbg!("You might have somehow bypassed `init()`"))?
467 .interpolate(interp_pt_dist)
468 .with_context(|| format_dbg!())?
469 },
470 || format_dbg!(),
471 )?;
472
473 vs.air_density.update(
474 if self.sim_params.f2_const_air_density {
475 1.2 * uc::KGPM3
476 } else {
477 let te_amb_air = {
478 let te_amb_air = self
479 .cyc
480 .temp_amb_air
481 .get(i)
482 .with_context(|| format_dbg!())?;
483 if *te_amb_air == *TE_STD_AIR {
484 None
485 } else {
486 Some(te_amb_air)
487 }
488 };
489 Air::get_density(
490 te_amb_air.copied(),
491 Some(*vs.elev_curr.get_fresh(|| format_dbg!())?),
492 )
493 },
494 || format_dbg!(),
495 )?;
496
497 let mass = self.veh.mass.with_context(|| {
498 format!(
499 "{}\nVehicle mass should have been set already.",
500 format_dbg!()
501 )
502 })?;
503 vs.pwr_accel.update(
504 mass / (2.0 * dt)
505 * (speed.powi(typenum::P2::new()) - speed_prev.powi(typenum::P2::new())),
506 || format_dbg!(),
507 )?;
508 vs.pwr_ascent.update(
509 uc::ACC_GRAV
510 * *vs.grade_curr.get_fresh(|| format_dbg!())?
511 * mass
512 * (speed_prev + speed)
513 / 2.0,
514 || format_dbg!(),
515 )?;
516 vs.pwr_drag.update(
517 0.5
518 * Air::get_density(None, None)
520 * self.veh.chassis.drag_coef
521 * self.veh.chassis.frontal_area
522 * ((speed + speed_prev) / 2.0).powi(typenum::P3::new()),
523 || format_dbg!(),
524 )?;
525 vs.pwr_rr.update(
526 mass * uc::ACC_GRAV
527 * self.veh.chassis.wheel_rr_coef
528 * vs.grade_curr.get_fresh(|| format_dbg!())?.atan().cos()
529 * (speed_prev + speed)
530 / 2.,
531 || format_dbg!(),
532 )?;
533 vs.pwr_whl_inertia.update(
534 0.5 * self.veh.chassis.wheel_inertia
535 * self.veh.chassis.num_wheels as f64
536 * ((speed
537 / self
538 .veh
539 .chassis
540 .wheel_radius
541 .with_context(|| format_dbg!())?)
542 .powi(typenum::P2::new())
543 - (speed_prev
544 / self
545 .veh
546 .chassis
547 .wheel_radius
548 .with_context(|| format_dbg!())?)
549 .powi(typenum::P2::new()))
550 / self.cyc.dt_at_i(i).with_context(|| format_dbg!())?,
551 || format_dbg!(),
552 )?;
553
554 vs.pwr_tractive.update(
555 *vs.pwr_rr.get_fresh(|| format_dbg!())?
556 + *vs.pwr_whl_inertia.get_fresh(|| format_dbg!())?
557 + *vs.pwr_accel.get_fresh(|| format_dbg!())?
558 + *vs.pwr_ascent.get_fresh(|| format_dbg!())?
559 + *vs.pwr_drag.get_fresh(|| format_dbg!())?,
560 || format_dbg!(),
561 )?;
562 Ok(())
563 }
564
565 pub fn set_ach_speed(&mut self, cyc_speed: si::Velocity, dt: si::Time) -> anyhow::Result<()> {
570 let vs = &mut self.veh.state;
571 vs.cyc_met.update(
572 vs.pwr_tractive.get_fresh(|| format_dbg!())?
573 <= vs.pwr_prop_fwd_max.get_fresh(|| format_dbg!())?,
574 || format_dbg!(),
575 )?;
576 vs.cyc_met_overall.update(
577 if !*vs.cyc_met.get_fresh(|| format_dbg!())? {
578 false
581 } else {
582 *vs.cyc_met_overall.get_stale(|| format_dbg!())?
583 },
584 || format_dbg!(),
585 )?;
586 let veh = &mut self.veh;
587 let speed_prev = *veh.state.speed_ach.get_stale(|| format_dbg!())?;
588 if *veh.state.cyc_met.get_fresh(|| format_dbg!())? {
589 veh.state.speed_ach.update(cyc_speed, || format_dbg!())?;
590 return Ok(());
591 } else {
592 match self.sim_params.trace_miss_opts {
593 TraceMissOptions::Allow => {
594 }
596 TraceMissOptions::AllowChecked => {
597 }
599 TraceMissOptions::Error => bail!(
600 "{}\nFailed to meet speed trace.
601prescribed speed: {} mph
602prev speed_ach: {} mph
603pwr_tractive_for_cyc: {} kW
604pwr_tractive: {} kW
605pwr_prop_fwd_max: {} kW,
606pwr deficit: {} kW
607",
608 format_dbg!(),
609 cyc_speed.get::<si::mile_per_hour>(),
610 veh.state
611 .speed_ach
612 .get_stale(|| format_dbg!())?
613 .get::<si::mile_per_hour>(),
614 veh.state
615 .pwr_tractive_for_cyc
616 .get_fresh(|| format_dbg!())?
617 .get::<si::kilowatt>(),
618 veh.state
619 .pwr_tractive
620 .get_fresh(|| format_dbg!())?
621 .get::<si::kilowatt>(),
622 veh.state
623 .pwr_prop_fwd_max
624 .get_fresh(|| format_dbg!())?
625 .get::<si::kilowatt>(),
626 (*veh.state.pwr_tractive.get_fresh(|| format_dbg!())?
627 - *veh.state.pwr_prop_fwd_max.get_fresh(|| format_dbg!())?)
628 .get::<si::kilowatt>()
629 .format_eng(None)
630 ),
631 TraceMissOptions::Correct => {
632 }
637 }
638 }
639 let vs = &mut self.veh.state;
640 let step_info = StepInfo {
641 dt,
642 speed_prev,
643 cyc_speed,
644 grade_curr: *vs.grade_curr.get_fresh(|| format_dbg!())?,
645 air_density: *vs.air_density.get_fresh(|| format_dbg!())?,
646 mass: self.veh.mass.with_context(|| {
647 format!("{}\nMass should have been set before now", format_dbg!())
648 })?,
649 drag_coef: self.veh.chassis.drag_coef,
650 frontal_area: self.veh.chassis.frontal_area,
651 wheel_inertia: self.veh.chassis.wheel_inertia,
652 num_wheels: self.veh.chassis.num_wheels,
653 wheel_radius: self
654 .veh
655 .chassis
656 .wheel_radius
657 .with_context(|| format_dbg!())?,
658 wheel_rr_coef: self.veh.chassis.wheel_rr_coef,
659 pwr_prop_fwd_max: *vs.pwr_prop_fwd_max.get_fresh(|| format_dbg!())?,
660 };
661 let speed_ach = step_info.solve_for_speed(
662 self.sim_params.ach_speed_max_iter * 10,
663 self.sim_params.ach_speed_tol,
664 self.sim_params.ach_speed_solver_gain,
665 );
666 let speed_ach_floored = {
667 let v = ((speed_ach.get::<si::meter_per_second>() * 10.0).floor() / 10.0) * uc::MPS;
671 if v == speed_ach {
674 (v - 0.1 * uc::MPS).max(si::Velocity::ZERO)
675 } else {
676 v
677 }
678 };
679
680 vs.speed_ach.update(speed_ach_floored, || format_dbg!())?;
681 vs.air_density.mark_stale();
687 vs.cyc_met.mark_stale();
688 vs.cyc_met_overall.mark_stale();
689 vs.elev_curr.mark_stale();
690 vs.grade_curr.mark_stale();
691 vs.pwr_accel.mark_stale();
692 vs.pwr_ascent.mark_stale();
693 vs.pwr_drag.mark_stale();
694 vs.pwr_rr.mark_stale();
695 vs.pwr_tractive.mark_stale();
696 vs.pwr_whl_inertia.mark_stale();
697 vs.speed_ach.mark_stale();
698
699 self.set_pwr_prop_for_speed(speed_ach_floored, speed_prev, dt)
701 .with_context(|| format_dbg!())?;
702 self.set_ach_speed(speed_ach, dt)
703 .with_context(|| anyhow!(format_dbg!()))?;
704
705 if self.sim_params.trace_miss_opts == TraceMissOptions::Correct {
706 let i = *self.veh.state.i.get_fresh(|| format_dbg!())?;
707 let max_steps = self.sim_params.trace_miss_correct_max_steps.max(2) as usize;
708 let correction = calc_best_rendezvous(i, max_steps, &self.cyc, speed_ach_floored);
709 if correction.steps >= 2 {
710 self.cyc.speed[i] = speed_ach_floored;
716 self.cyc.modify_by_const_jerk_trajectory(
717 i + 1,
718 correction.steps,
719 correction.jerk_m_per_s3 * uc::MPS3,
720 correction.acceleration_m_per_s2 * uc::MPS2,
721 );
722 self.cyc.dist.clear();
723 self.cyc.elev.clear();
724 self.cyc.init().unwrap();
725 }
726 }
727
728 Ok(())
729 }
730
731 pub fn to_fastsim2(&self) -> anyhow::Result<fastsim_2::simdrive::RustSimDrive> {
732 let veh2 = self
733 .veh
734 .to_fastsim2()
735 .with_context(|| anyhow!(format_dbg!()))?;
736 let cyc2 = self
737 .cyc
738 .to_fastsim2()
739 .with_context(|| anyhow!(format_dbg!()))?;
740 Ok(fastsim_2::simdrive::RustSimDrive::new(cyc2, veh2))
741 }
742}
743
744impl SetCumulative for SimDrive {
745 fn set_cumulative<F: Fn() -> String>(&mut self, dt: si::Time, loc: F) -> anyhow::Result<()> {
746 self.veh
747 .set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?;
748 Ok(())
749 }
750}
751
752#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
753#[serde(deny_unknown_fields)]
754#[non_exhaustive]
755pub struct TraceMissTolerance {
757 tol_dist: si::Length,
760 tol_dist_frac: si::Ratio,
763 tol_speed: si::Velocity,
766 tol_speed_frac: si::Ratio,
769}
770
771impl TraceMissTolerance {
772 fn check_trace_miss(
773 &self,
774 cyc_speed: si::Velocity,
775 ach_speed: si::Velocity,
776 cyc_dist: si::Length,
777 ach_dist: si::Length,
778 ) -> anyhow::Result<()> {
779 ensure!(
780 cyc_speed - ach_speed < self.tol_speed,
781 "{}\n{}\n{}",
782 format_dbg!(cyc_speed),
783 format_dbg!(ach_speed),
784 format_dbg!(self.tol_speed)
785 );
786 if cyc_speed > self.tol_speed {
788 ensure!(
789 (cyc_speed - ach_speed) / cyc_speed < self.tol_speed_frac,
790 "{}\n{}\n{}",
791 format_dbg!(cyc_speed),
792 format_dbg!(ach_speed),
793 format_dbg!(self.tol_speed_frac)
794 )
795 }
796 ensure!(
797 (cyc_dist - ach_dist) < self.tol_dist,
798 "{}\n{}\n{}",
799 format_dbg!(cyc_dist),
800 format_dbg!(ach_dist),
801 format_dbg!(self.tol_dist)
802 );
803 if cyc_dist > self.tol_dist * 5.0 {
805 ensure!(
806 (cyc_dist - ach_dist) / cyc_dist < self.tol_dist_frac,
807 "{}\n{}\n{}",
808 format_dbg!(cyc_dist),
809 format_dbg!(ach_dist),
810 format_dbg!(self.tol_dist_frac)
811 )
812 }
813
814 Ok(())
815 }
816}
817impl SerdeAPI for TraceMissTolerance {}
818impl Init for TraceMissTolerance {}
819impl Default for TraceMissTolerance {
820 fn default() -> Self {
821 Self {
822 tol_dist: 100. * uc::M,
823 tol_dist_frac: 0.05 * uc::R,
824 tol_speed: 10. * uc::MPS,
825 tol_speed_frac: 0.5 * uc::R,
826 }
827 }
828}
829
830#[derive(
831 Clone, Default, Debug, Deserialize, Serialize, PartialEq, IsVariant, derive_more::From, TryInto,
832)]
833pub enum TraceMissOptions {
834 Allow,
836 AllowChecked,
838 #[default]
839 Error,
841 Correct,
843}
844
845impl SerdeAPI for TraceMissOptions {}
846impl Init for TraceMissOptions {}
847
848#[cfg(test)]
849mod tests {
850 use super::*;
851 use crate::vehicle::vehicle_model::tests::*;
852
853 #[test]
854 #[cfg(feature = "resources")]
855 fn test_sim_drive_conv() {
856 let _veh = mock_conv_veh();
857 let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
858 let mut sd = SimDrive::new(_veh, _cyc, Default::default());
859 sd.walk().unwrap();
860 assert!(
861 *sd.veh.state.i.get_fresh(String::new).unwrap() == sd.cyc.len_checked().unwrap() - 1
862 );
863 assert!(
864 *sd.veh
865 .fc()
866 .unwrap()
867 .state
868 .energy_fuel
869 .get_fresh(String::new)
870 .unwrap()
871 > si::Energy::ZERO
872 );
873 assert!(sd.veh.res().is_none());
874 }
875
876 #[test]
877 #[cfg(feature = "resources")]
878 fn test_sim_drive_hev() {
879 let _veh = mock_hev();
880 let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
881 let mut sd = SimDrive::new(_veh, _cyc, Default::default());
882 sd.walk().unwrap();
883 assert!(
884 *sd.veh.state.i.get_fresh(String::new).unwrap() == sd.cyc.len_checked().unwrap() - 1
885 );
886 assert!(
887 *sd.veh
888 .fc()
889 .unwrap()
890 .state
891 .energy_fuel
892 .get_fresh(String::new)
893 .unwrap()
894 > si::Energy::ZERO
895 );
896 assert!(
897 *sd.veh
898 .res()
899 .unwrap()
900 .state
901 .energy_out_chemical
902 .get_fresh(String::new)
903 .unwrap()
904 != si::Energy::ZERO
905 );
906 }
907
908 #[test]
909 #[cfg(feature = "resources")]
910 fn test_sim_drive_hev_thrml() {
911 let _veh = Vehicle::from_resource("2021_Hyundai_Sonata_Hybrid_Blue.yaml", false).unwrap();
912 let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
913
914 let te_amb_and_cab_and_batt_init_deg_c: Vec<(f64, f64)> = vec![
915 (-6.7, -6.7),
916 (5.0, 18.0),
917 (22.0, 22.0),
918 (25.0, 35.0),
919 (45.0, 45.0),
920 ];
921 let te_amb: Vec<si::Temperature> = te_amb_and_cab_and_batt_init_deg_c
922 .iter()
923 .map(|t| (t.0 + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
924 .collect();
925 let te_batt_and_cab_init: Vec<si::Temperature> = te_amb_and_cab_and_batt_init_deg_c
926 .iter()
927 .map(|t| (t.1 + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
928 .collect();
929 let te_fc_init: Vec<si::Temperature> = [-6.7, 70.0, 90.0]
930 .iter()
931 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
932 .collect();
933 for ((te_amb, te_init), te_fc_init) in
934 te_amb.iter().zip(te_batt_and_cab_init).zip(te_fc_init)
935 {
936 let mut veh = _veh.clone();
937
938 veh.res_mut()
939 .unwrap()
940 .res_thrml_state_mut()
941 .unwrap()
942 .temperature
943 .mark_stale();
944 veh.res_mut()
945 .unwrap()
946 .res_thrml_state_mut()
947 .unwrap()
948 .temperature
949 .update(te_init, || format_dbg!())
950 .unwrap();
951
952 veh.res_mut()
953 .unwrap()
954 .res_thrml_state_mut()
955 .unwrap()
956 .temp_prev
957 .mark_stale();
958 veh.res_mut()
959 .unwrap()
960 .res_thrml_state_mut()
961 .unwrap()
962 .temp_prev
963 .update(te_init, || format_dbg!())
964 .unwrap();
965 if let CabinOption::LumpedCabin(lc) = &mut veh.cabin {
966 lc.state.temperature.mark_stale();
967 lc.state
968 .temperature
969 .update(te_init, || format_dbg!())
970 .unwrap();
971 lc.state.temp_prev.mark_stale();
972 lc.state
973 .temp_prev
974 .update(te_init, || format_dbg!())
975 .unwrap();
976 }
977
978 veh.fc_mut()
979 .unwrap()
980 .fc_thrml_state_mut()
981 .unwrap()
982 .temperature
983 .mark_stale();
984 veh.fc_mut()
985 .unwrap()
986 .fc_thrml_state_mut()
987 .unwrap()
988 .temperature
989 .update(te_fc_init, || format_dbg!())
990 .unwrap();
991 let mut cyc = _cyc.clone();
992 cyc.temp_amb_air = vec![*te_amb; cyc.len_checked().unwrap()];
993 let mut sd = SimDrive::new(veh, cyc, Default::default());
994 sd.walk()
995 .with_context(|| {
996 format!(
997 "ambient temperature: {}*C\ninit temperature: {}",
998 te_amb.get::<si::degree_celsius>(),
999 te_init.get::<si::degree_celsius>()
1000 )
1001 })
1002 .unwrap();
1003 assert!(
1004 *sd.veh.state.i.get_fresh(String::new).unwrap()
1005 == sd.cyc.len_checked().unwrap() - 1
1006 );
1007 assert!(
1008 *sd.veh
1009 .fc()
1010 .unwrap()
1011 .state
1012 .energy_fuel
1013 .get_fresh(String::new)
1014 .unwrap()
1015 > si::Energy::ZERO
1016 );
1017 assert!(
1018 *sd.veh
1019 .res()
1020 .unwrap()
1021 .state
1022 .energy_out_chemical
1023 .get_fresh(String::new)
1024 .unwrap()
1025 != si::Energy::ZERO
1026 );
1027 }
1028 }
1029
1030 #[test]
1031 #[cfg(feature = "resources")]
1032 fn test_sim_drive_hev_thrml_soak() {
1033 let _veh = Vehicle::from_resource("2021_Hyundai_Sonata_Hybrid_Blue.yaml", false).unwrap();
1034 let mut cyc = Cycle::from_resource("udds.csv", false).unwrap();
1035 cyc.speed.iter_mut().for_each(|v| *v = si::Velocity::ZERO);
1037
1038 let te_amb: Vec<si::Temperature> = [-6.7, -6.7, 38.0]
1039 .iter()
1040 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1041 .collect();
1042 let te_batt_and_cab_init: Vec<si::Temperature> = [-6.7, 22.0, 45.0]
1043 .iter()
1044 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1045 .collect();
1046 let te_fc_init: Vec<si::Temperature> = [-6.7, 70.0, 90.0]
1047 .iter()
1048 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1049 .collect();
1050 for ((te_amb, te_init), te_fc_init) in
1051 te_amb.iter().zip(te_batt_and_cab_init).zip(te_fc_init)
1052 {
1053 let mut veh = _veh.clone();
1054
1055 veh.res_mut()
1056 .unwrap()
1057 .res_thrml_state_mut()
1058 .unwrap()
1059 .temperature
1060 .mark_stale();
1061 veh.res_mut()
1062 .unwrap()
1063 .res_thrml_state_mut()
1064 .unwrap()
1065 .temperature
1066 .update(te_init, || format_dbg!())
1067 .unwrap();
1068
1069 veh.res_mut()
1070 .unwrap()
1071 .res_thrml_state_mut()
1072 .unwrap()
1073 .temp_prev
1074 .mark_stale();
1075 veh.res_mut()
1076 .unwrap()
1077 .res_thrml_state_mut()
1078 .unwrap()
1079 .temp_prev
1080 .update(te_init, || format_dbg!())
1081 .unwrap();
1082 if let CabinOption::LumpedCabin(lc) = &mut veh.cabin {
1083 lc.state.temperature.mark_stale();
1084 lc.state
1085 .temperature
1086 .update(te_init, || format_dbg!())
1087 .unwrap();
1088 lc.state.temp_prev.mark_stale();
1089 lc.state
1090 .temp_prev
1091 .update(te_init, || format_dbg!())
1092 .unwrap();
1093 }
1094
1095 veh.fc_mut()
1096 .unwrap()
1097 .fc_thrml_state_mut()
1098 .unwrap()
1099 .temperature
1100 .mark_stale();
1101 veh.fc_mut()
1102 .unwrap()
1103 .fc_thrml_state_mut()
1104 .unwrap()
1105 .temperature
1106 .update(te_fc_init, || format_dbg!())
1107 .unwrap();
1108 let mut cyc = cyc.clone();
1109 cyc.temp_amb_air = vec![*te_amb; cyc.len_checked().unwrap()];
1110 let mut sd = SimDrive::new(
1111 veh,
1112 cyc,
1113 Some(SimParams {
1114 ambient_thermal_soak: true,
1115 ..Default::default()
1116 }),
1117 );
1118 sd.walk()
1119 .with_context(|| {
1120 format!(
1121 "ambient temperature: {}*C\ninit temperature: {}",
1122 te_amb.get::<si::degree_celsius>(),
1123 te_init.get::<si::degree_celsius>()
1124 )
1125 })
1126 .unwrap();
1127 assert!(
1128 *sd.veh.state.i.get_fresh(String::new).unwrap()
1129 == sd.cyc.len_checked().unwrap() - 1
1130 );
1131 }
1132 }
1133
1134 #[test]
1135 #[cfg(feature = "resources")]
1136 fn test_sim_drive_bev() {
1137 let _veh = mock_bev();
1138 let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
1139 let mut sd = SimDrive {
1140 veh: _veh,
1141 cyc: _cyc,
1142 sim_params: Default::default(),
1143 };
1144 sd.walk().unwrap();
1145 assert!(
1146 *sd.veh.state.i.get_fresh(String::new).unwrap() == sd.cyc.len_checked().unwrap() - 1
1147 );
1148 assert!(sd.veh.fc().is_none());
1149 assert!(
1150 *sd.veh
1151 .res()
1152 .unwrap()
1153 .state
1154 .energy_out_chemical
1155 .get_fresh(String::new)
1156 .unwrap()
1157 != si::Energy::ZERO
1158 );
1159 }
1160
1161 #[test]
1162 #[cfg(feature = "resources")]
1163 fn test_sim_drive_bev_thrml() {
1164 let _veh = Vehicle::from_resource("2020 Chevrolet Bolt EV.yaml", false).unwrap();
1165 let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
1166
1167 let te_amb_and_cab_and_batt_init_deg_c: Vec<(f64, f64)> = vec![
1168 (-6.7, -6.7),
1169 (5.0, 18.0),
1170 (22.0, 22.0),
1171 (25.0, 35.0),
1172 (45.0, 45.0),
1173 ];
1174 let te_amb: Vec<si::Temperature> = te_amb_and_cab_and_batt_init_deg_c
1175 .iter()
1176 .map(|t| (t.0 + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1177 .collect();
1178 let te_batt_and_cab_init: Vec<si::Temperature> = te_amb_and_cab_and_batt_init_deg_c
1179 .iter()
1180 .map(|t| (t.1 + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1181 .collect();
1182 for (te_amb, te_init) in te_amb.iter().zip(te_batt_and_cab_init) {
1183 let mut veh = _veh.clone();
1184 veh.res_mut()
1185 .unwrap()
1186 .res_thrml_state_mut()
1187 .unwrap()
1188 .temperature
1189 .mark_stale();
1190 veh.res_mut()
1191 .unwrap()
1192 .res_thrml_state_mut()
1193 .unwrap()
1194 .temperature
1195 .update(te_init, || format_dbg!())
1196 .unwrap();
1197
1198 veh.res_mut()
1199 .unwrap()
1200 .res_thrml_state_mut()
1201 .unwrap()
1202 .temp_prev
1203 .mark_stale();
1204 veh.res_mut()
1205 .unwrap()
1206 .res_thrml_state_mut()
1207 .unwrap()
1208 .temp_prev
1209 .update(te_init, || format_dbg!())
1210 .unwrap();
1211
1212 if let CabinOption::LumpedCabin(lc) = &mut veh.cabin {
1213 lc.state.temperature.mark_stale();
1214 lc.state
1215 .temperature
1216 .update(te_init, || format_dbg!())
1217 .unwrap();
1218
1219 lc.state.temp_prev.mark_stale();
1220 lc.state
1221 .temp_prev
1222 .update(te_init, || format_dbg!())
1223 .unwrap();
1224 } else {
1225 panic!("cabin should have been configured");
1226 }
1227 let mut cyc = _cyc.clone();
1228 cyc.temp_amb_air = vec![*te_amb; cyc.len_checked().unwrap()];
1229 let mut sd = SimDrive::new(veh, cyc, Default::default());
1230 if let CabinOption::LumpedCabin(lc) = sd.veh.cabin.clone() {
1231 assert_eq!(
1232 *lc.state.temperature.get_fresh(|| format_dbg!()).unwrap(),
1233 te_init
1234 );
1235 } else {
1236 panic!();
1237 };
1238 sd.walk()
1239 .with_context(|| {
1240 format!(
1241 "ambient temperature: {}*C\ninit temperature: {}",
1242 te_amb.get::<si::degree_celsius>(),
1243 te_init.get::<si::degree_celsius>()
1244 )
1245 })
1246 .unwrap();
1247 assert!(
1248 *sd.veh.state.i.get_fresh(String::new).unwrap()
1249 == sd.cyc.len_checked().unwrap() - 1
1250 );
1251 assert!(sd.veh.fc().is_none());
1252 assert!(
1253 *sd.veh
1254 .res()
1255 .unwrap()
1256 .state
1257 .energy_out_chemical
1258 .get_fresh(String::new)
1259 .unwrap()
1260 != si::Energy::ZERO
1261 );
1262 sd.veh.reset_step(|| format_dbg!()).unwrap();
1263 sd.veh.state.time.mark_stale();
1264 sd.veh
1265 .state
1266 .time
1267 .update(si::Time::ZERO, || format_dbg!())
1268 .unwrap();
1269 assert!(*sd.veh.state.i.get_fresh(|| format_dbg!()).unwrap() == 0);
1270 sd.walk()
1271 .with_context(|| {
1272 format!(
1273 "ambient temperature: {}*C\ninit temperature: {}",
1274 te_amb.get::<si::degree_celsius>(),
1275 te_init.get::<si::degree_celsius>()
1276 )
1277 })
1278 .unwrap();
1279 assert_eq!(*sd.veh.state.i.get_fresh(|| format_dbg!()).unwrap(), 1369);
1280 }
1281 }
1282}