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: Vec<si::Temperature> = [-6.7, -6.7, 38.0]
915 .iter()
916 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
917 .collect();
918 let te_batt_and_cab_init: Vec<si::Temperature> = [-6.7, 22.0, 45.0]
919 .iter()
920 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
921 .collect();
922 let te_fc_init: Vec<si::Temperature> = [-6.7, 70.0, 90.0]
923 .iter()
924 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
925 .collect();
926 for ((te_amb, te_init), te_fc_init) in
927 te_amb.iter().zip(te_batt_and_cab_init).zip(te_fc_init)
928 {
929 let mut veh = _veh.clone();
930
931 veh.res_mut()
932 .unwrap()
933 .res_thrml_state_mut()
934 .unwrap()
935 .temperature
936 .mark_stale();
937 veh.res_mut()
938 .unwrap()
939 .res_thrml_state_mut()
940 .unwrap()
941 .temperature
942 .update(te_init, || format_dbg!())
943 .unwrap();
944
945 veh.res_mut()
946 .unwrap()
947 .res_thrml_state_mut()
948 .unwrap()
949 .temp_prev
950 .mark_stale();
951 veh.res_mut()
952 .unwrap()
953 .res_thrml_state_mut()
954 .unwrap()
955 .temp_prev
956 .update(te_init, || format_dbg!())
957 .unwrap();
958 if let CabinOption::LumpedCabin(lc) = &mut veh.cabin {
959 lc.state.temperature.mark_stale();
960 lc.state
961 .temperature
962 .update(te_init, || format_dbg!())
963 .unwrap();
964 lc.state.temp_prev.mark_stale();
965 lc.state
966 .temp_prev
967 .update(te_init, || format_dbg!())
968 .unwrap();
969 }
970
971 veh.fc_mut()
972 .unwrap()
973 .fc_thrml_state_mut()
974 .unwrap()
975 .temperature
976 .mark_stale();
977 veh.fc_mut()
978 .unwrap()
979 .fc_thrml_state_mut()
980 .unwrap()
981 .temperature
982 .update(te_fc_init, || format_dbg!())
983 .unwrap();
984 let mut cyc = _cyc.clone();
985 cyc.temp_amb_air = vec![*te_amb; cyc.len_checked().unwrap()];
986 let mut sd = SimDrive::new(veh, cyc, Default::default());
987 sd.walk()
988 .with_context(|| {
989 format!(
990 "ambient temperature: {}*C\ninit temperature: {}",
991 te_amb.get::<si::degree_celsius>(),
992 te_init.get::<si::degree_celsius>()
993 )
994 })
995 .unwrap();
996 assert!(
997 *sd.veh.state.i.get_fresh(String::new).unwrap()
998 == sd.cyc.len_checked().unwrap() - 1
999 );
1000 assert!(
1001 *sd.veh
1002 .fc()
1003 .unwrap()
1004 .state
1005 .energy_fuel
1006 .get_fresh(String::new)
1007 .unwrap()
1008 > si::Energy::ZERO
1009 );
1010 assert!(
1011 *sd.veh
1012 .res()
1013 .unwrap()
1014 .state
1015 .energy_out_chemical
1016 .get_fresh(String::new)
1017 .unwrap()
1018 != si::Energy::ZERO
1019 );
1020 }
1021 }
1022
1023 #[test]
1024 #[cfg(feature = "resources")]
1025 fn test_sim_drive_hev_thrml_soak() {
1026 let _veh = Vehicle::from_resource("2021_Hyundai_Sonata_Hybrid_Blue.yaml", false).unwrap();
1027 let mut cyc = Cycle::from_resource("udds.csv", false).unwrap();
1028 cyc.speed.iter_mut().for_each(|v| *v = si::Velocity::ZERO);
1030
1031 let te_amb: Vec<si::Temperature> = [-6.7, -6.7, 38.0]
1032 .iter()
1033 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1034 .collect();
1035 let te_batt_and_cab_init: Vec<si::Temperature> = [-6.7, 22.0, 45.0]
1036 .iter()
1037 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1038 .collect();
1039 let te_fc_init: Vec<si::Temperature> = [-6.7, 70.0, 90.0]
1040 .iter()
1041 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1042 .collect();
1043 for ((te_amb, te_init), te_fc_init) in
1044 te_amb.iter().zip(te_batt_and_cab_init).zip(te_fc_init)
1045 {
1046 let mut veh = _veh.clone();
1047
1048 veh.res_mut()
1049 .unwrap()
1050 .res_thrml_state_mut()
1051 .unwrap()
1052 .temperature
1053 .mark_stale();
1054 veh.res_mut()
1055 .unwrap()
1056 .res_thrml_state_mut()
1057 .unwrap()
1058 .temperature
1059 .update(te_init, || format_dbg!())
1060 .unwrap();
1061
1062 veh.res_mut()
1063 .unwrap()
1064 .res_thrml_state_mut()
1065 .unwrap()
1066 .temp_prev
1067 .mark_stale();
1068 veh.res_mut()
1069 .unwrap()
1070 .res_thrml_state_mut()
1071 .unwrap()
1072 .temp_prev
1073 .update(te_init, || format_dbg!())
1074 .unwrap();
1075 if let CabinOption::LumpedCabin(lc) = &mut veh.cabin {
1076 lc.state.temperature.mark_stale();
1077 lc.state
1078 .temperature
1079 .update(te_init, || format_dbg!())
1080 .unwrap();
1081 lc.state.temp_prev.mark_stale();
1082 lc.state
1083 .temp_prev
1084 .update(te_init, || format_dbg!())
1085 .unwrap();
1086 }
1087
1088 veh.fc_mut()
1089 .unwrap()
1090 .fc_thrml_state_mut()
1091 .unwrap()
1092 .temperature
1093 .mark_stale();
1094 veh.fc_mut()
1095 .unwrap()
1096 .fc_thrml_state_mut()
1097 .unwrap()
1098 .temperature
1099 .update(te_fc_init, || format_dbg!())
1100 .unwrap();
1101 let mut cyc = cyc.clone();
1102 cyc.temp_amb_air = vec![*te_amb; cyc.len_checked().unwrap()];
1103 let mut sd = SimDrive::new(
1104 veh,
1105 cyc,
1106 Some(SimParams {
1107 ambient_thermal_soak: true,
1108 ..Default::default()
1109 }),
1110 );
1111 sd.walk()
1112 .with_context(|| {
1113 format!(
1114 "ambient temperature: {}*C\ninit temperature: {}",
1115 te_amb.get::<si::degree_celsius>(),
1116 te_init.get::<si::degree_celsius>()
1117 )
1118 })
1119 .unwrap();
1120 assert!(
1121 *sd.veh.state.i.get_fresh(String::new).unwrap()
1122 == sd.cyc.len_checked().unwrap() - 1
1123 );
1124 }
1125 }
1126
1127 #[test]
1128 #[cfg(feature = "resources")]
1129 fn test_sim_drive_bev() {
1130 let _veh = mock_bev();
1131 let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
1132 let mut sd = SimDrive {
1133 veh: _veh,
1134 cyc: _cyc,
1135 sim_params: Default::default(),
1136 };
1137 sd.walk().unwrap();
1138 assert!(
1139 *sd.veh.state.i.get_fresh(String::new).unwrap() == sd.cyc.len_checked().unwrap() - 1
1140 );
1141 assert!(sd.veh.fc().is_none());
1142 assert!(
1143 *sd.veh
1144 .res()
1145 .unwrap()
1146 .state
1147 .energy_out_chemical
1148 .get_fresh(String::new)
1149 .unwrap()
1150 != si::Energy::ZERO
1151 );
1152 }
1153
1154 #[test]
1155 #[cfg(feature = "resources")]
1156 fn test_sim_drive_bev_thrml() {
1157 let _veh = Vehicle::from_resource("2020 Chevrolet Bolt EV.yaml", false).unwrap();
1158 let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
1159
1160 let te_amb: Vec<si::Temperature> = [-6.7, -6.7, 38.0]
1161 .iter()
1162 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1163 .collect();
1164 let te_batt_and_cab_init: Vec<si::Temperature> = [-6.7, 22.0, 45.0]
1165 .iter()
1166 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1167 .collect();
1168 for (te_amb, te_init) in te_amb.iter().zip(te_batt_and_cab_init) {
1169 let mut veh = _veh.clone();
1170 veh.res_mut()
1171 .unwrap()
1172 .res_thrml_state_mut()
1173 .unwrap()
1174 .temperature
1175 .mark_stale();
1176 veh.res_mut()
1177 .unwrap()
1178 .res_thrml_state_mut()
1179 .unwrap()
1180 .temperature
1181 .update(te_init, || format_dbg!())
1182 .unwrap();
1183
1184 veh.res_mut()
1185 .unwrap()
1186 .res_thrml_state_mut()
1187 .unwrap()
1188 .temp_prev
1189 .mark_stale();
1190 veh.res_mut()
1191 .unwrap()
1192 .res_thrml_state_mut()
1193 .unwrap()
1194 .temp_prev
1195 .update(te_init, || format_dbg!())
1196 .unwrap();
1197
1198 if let CabinOption::LumpedCabin(lc) = &mut veh.cabin {
1199 lc.state.temperature.mark_stale();
1200 lc.state
1201 .temperature
1202 .update(te_init, || format_dbg!())
1203 .unwrap();
1204
1205 lc.state.temp_prev.mark_stale();
1206 lc.state
1207 .temp_prev
1208 .update(te_init, || format_dbg!())
1209 .unwrap();
1210 } else {
1211 panic!("cabin should have been configured");
1212 }
1213 let mut cyc = _cyc.clone();
1214 cyc.temp_amb_air = vec![*te_amb; cyc.len_checked().unwrap()];
1215 let mut sd = SimDrive::new(veh, cyc, Default::default());
1216 if let CabinOption::LumpedCabin(lc) = sd.veh.cabin.clone() {
1217 assert_eq!(
1218 *lc.state.temperature.get_fresh(|| format_dbg!()).unwrap(),
1219 te_init
1220 );
1221 } else {
1222 panic!();
1223 };
1224 sd.walk()
1225 .with_context(|| {
1226 format!(
1227 "ambient temperature: {}*C\ninit temperature: {}",
1228 te_amb.get::<si::degree_celsius>(),
1229 te_init.get::<si::degree_celsius>()
1230 )
1231 })
1232 .unwrap();
1233 assert!(
1234 *sd.veh.state.i.get_fresh(String::new).unwrap()
1235 == sd.cyc.len_checked().unwrap() - 1
1236 );
1237 assert!(sd.veh.fc().is_none());
1238 assert!(
1239 *sd.veh
1240 .res()
1241 .unwrap()
1242 .state
1243 .energy_out_chemical
1244 .get_fresh(String::new)
1245 .unwrap()
1246 != si::Energy::ZERO
1247 );
1248 sd.veh.reset_step(|| format_dbg!()).unwrap();
1249 sd.veh.state.time.mark_stale();
1250 sd.veh
1251 .state
1252 .time
1253 .update(si::Time::ZERO, || format_dbg!())
1254 .unwrap();
1255 assert!(*sd.veh.state.i.get_fresh(|| format_dbg!()).unwrap() == 0);
1256 sd.walk()
1257 .with_context(|| {
1258 format!(
1259 "ambient temperature: {}*C\ninit temperature: {}",
1260 te_amb.get::<si::degree_celsius>(),
1261 te_init.get::<si::degree_celsius>()
1262 )
1263 })
1264 .unwrap();
1265 assert_eq!(*sd.veh.state.i.get_fresh(|| format_dbg!()).unwrap(), 1369);
1266 }
1267 }
1268}