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 #[pyo3(name = "reset_py")]
140 fn reset_py(&mut self) -> anyhow::Result<()> {
142 self.reset_cumulative(|| format_dbg!())?;
143 self.reset_step(|| format_dbg!())?;
144 self.clear();
145 Ok(())
146 }
147
148 #[pyo3(name = "clear")]
149 fn clear_py(&mut self) {
150 self.clear()
151 }
152
153 #[pyo3(name = "reset_step")]
154 fn reset_step_py(&mut self) -> anyhow::Result<()> {
155 self.reset_step(|| format_dbg!())
156 }
157
158 #[pyo3(name = "reset_cumulative")]
159 fn reset_cumulative_py(&mut self) -> anyhow::Result<()> {
160 self.reset_cumulative(|| format_dbg!())
161 }
162}
163
164impl SerdeAPI for SimDrive {}
165impl Init for SimDrive {
166 fn init(&mut self) -> Result<(), Error> {
167 self.veh
168 .init()
169 .map_err(|err| Error::InitError(format_dbg!(err)))?;
170 self.cyc
171 .init()
172 .map_err(|err| Error::InitError(format_dbg!(err)))?;
173 self.sim_params
174 .init()
175 .map_err(|err| Error::InitError(format_dbg!(err)))?;
176 Ok(())
177 }
178}
179
180impl SimDrive {
181 pub fn new(veh: Vehicle, cyc: Cycle, sim_params: Option<SimParams>) -> Self {
182 Self {
183 veh,
184 cyc,
185 sim_params: sim_params.unwrap_or_default(),
186 }
187 }
188
189 pub fn walk(&mut self) -> anyhow::Result<()> {
212 match self.veh.pt_type {
213 PowertrainType::HybridElectricVehicle(_) => {
214 let veh_init = self.veh.clone();
217 let res_mut = self.veh.res_mut().with_context(|| format_dbg!())?;
218 res_mut.state.soc.mark_stale();
219 res_mut
220 .state
221 .soc
222 .update(0.5 * (res_mut.min_soc + res_mut.max_soc), || format_dbg!())?;
223 loop {
224 self.veh
225 .hev_mut()
226 .with_context(|| format_dbg!())?
227 .soc_bal_iters
228 .mark_stale();
229 self.veh
230 .hev_mut()
231 .with_context(|| format_dbg!())?
232 .soc_bal_iters
233 .increment(1, || format_dbg!())?;
234 self.walk_once().with_context(|| format_dbg!())?;
235 let soc_final = self
236 .veh
237 .res()
238 .with_context(|| format_dbg!())?
239 .state
240 .soc
241 .clone();
242 let res_per_fuel = *self
243 .veh
244 .res()
245 .with_context(|| format_dbg!())?
246 .state
247 .energy_out_chemical
248 .get_fresh(|| format_dbg!())?
249 / *self
250 .veh
251 .fc()
252 .with_context(|| format_dbg!())?
253 .state
254 .energy_fuel
255 .get_fresh(|| format_dbg!())?;
256 if self
257 .veh
258 .hev()
259 .with_context(|| format_dbg!())?
260 .soc_bal_iters
261 .get_fresh(|| format_dbg!())?
262 > &self
263 .veh
264 .hev()
265 .with_context(|| format_dbg!())?
266 .sim_params
267 .soc_balance_iter_err
268 {
269 bail!(
270 "{}",
271 format_dbg!((
272 self.veh
273 .hev()
274 .with_context(|| format_dbg!())?
275 .soc_bal_iters
276 .clone(),
277 self.veh
278 .hev()
279 .with_context(|| format_dbg!())?
280 .sim_params
281 .soc_balance_iter_err
282 ))
283 );
284 }
285 if res_per_fuel.abs()
286 < self
287 .veh
288 .hev()
289 .with_context(|| format_dbg!())?
290 .sim_params
291 .res_per_fuel_lim
292 || !self
293 .veh
294 .hev()
295 .with_context(|| format_dbg!())?
296 .sim_params
297 .balance_soc
298 || self.sim_params.ambient_thermal_soak
299 {
300 break;
301 } else {
302 if let Some(&mut ref mut hev) = self.veh.hev_mut() {
304 if hev.sim_params.save_soc_bal_iters {
305 hev.soc_bal_iter_history.push(hev.clone());
306 hev.soc_bal_iters.mark_stale();
307 }
308 }
309 self.veh = veh_init.clone();
311 self.veh.res_mut().with_context(|| format_dbg!())?.state.soc = soc_final;
313 }
314 }
315 }
316 PowertrainType::PlugInHybridElectricVehicle(_) => {
317 let res_mut = self.veh.res_mut().with_context(|| format_dbg!())?;
318 res_mut.state.soc.mark_stale();
319 res_mut
320 .state
321 .soc
322 .update(res_mut.max_soc, || format_dbg!())?;
323 self.walk_once()?
324 }
325 PowertrainType::BatteryElectricVehicle(_) => {
326 let res_mut = self.veh.res_mut().with_context(|| format_dbg!())?;
327 res_mut.state.soc.mark_stale();
328 res_mut
329 .state
330 .soc
331 .update(res_mut.max_soc, || format_dbg!())?;
332 self.walk_once()?
333 }
334 PowertrainType::ConventionalVehicle(_) => self.walk_once()?,
335 }
336 Ok(())
337 }
338
339 pub fn walk_once(&mut self) -> anyhow::Result<()> {
341 let len = &self.cyc.len_checked().with_context(|| format_dbg!())?;
342 ensure!(len >= &2, format_dbg!(len < &2));
343 self.save_state(|| format_dbg!())?;
344
345 self.veh.state.mass.mark_stale();
346 self.veh.state.mass.update(
347 self.veh
348 .mass()
349 .with_context(|| format_dbg!())?
350 .with_context(|| format_dbg!("Expected mass to have been set."))?,
351 || format_dbg!(),
352 )?;
353
354 let hvac: Option<HVACOption> = if self.sim_params.ambient_thermal_soak {
355 ensure!(
356 self.cyc.speed.iter().all(|s| *s == si::Velocity::ZERO),
357 format!(
358 "{}\nDuring thermal soak, cycle speed should always be zero",
359 format_dbg!()
360 )
361 );
362 if !self.veh.hvac.is_none() {
363 let hvac_some = Some(self.veh.hvac.clone());
365 self.veh.hvac = HVACOption::None;
366 hvac_some
367 } else {
368 None
369 }
370 } else {
371 None
372 };
373
374 loop {
375 self.check_and_reset(|| format_dbg!())?;
376 self.veh.state.mass.mark_fresh(|| format_dbg!())?;
377 if let Some(res) = self.veh.res_mut() {
378 res.state.soh.mark_fresh(|| format_dbg!())?;
379 }
380 self.step(|| format_dbg!())?;
381 self.solve_step()
382 .with_context(|| format!("{}\ntime step: {:?}", format_dbg!(), self.veh.state.i))?;
383 self.save_state(|| format_dbg!())?;
384 if *self.veh.state.i.get_fresh(|| format_dbg!())? == len - 1 {
385 break;
386 }
387 }
388
389 if let Some(hvac) = hvac {
390 self.veh.hvac = hvac;
392 }
393
394 Ok(())
395 }
396
397 pub fn calc_dvdd(&self, speed_m_per_s: f64, grade: f64) -> anyhow::Result<f64> {
403 let v = speed_m_per_s;
404 if v <= 0.0 {
405 Ok(0.0)
406 } else {
407 let (atan_grade_sin, atan_grade_cos) = if grade == 0.0 {
408 (0.0, 1.0)
409 } else {
410 let atan_grade = grade.atan();
411 (atan_grade.sin(), atan_grade.cos())
412 };
413 let g = uc::ACC_GRAV.get::<si::meter_per_second_squared>();
414 let m = self
415 .veh
416 .mass
417 .with_context(|| {
418 format!(
419 "{}\nVehicle mass should have been set already.",
420 format_dbg!()
421 )
422 })?
423 .get::<si::kilogram>();
424 let rho_cdfa = self
425 .veh
426 .state
427 .air_density
428 .get_stale(|| format_dbg!())?
429 .get::<si::kilogram_per_cubic_meter>()
430 * self.veh.chassis.drag_coef.get::<si::ratio>()
431 * self.veh.chassis.frontal_area.get::<si::square_meter>();
432 let rrc = self.veh.chassis.wheel_rr_coef.get::<si::ratio>();
433 Ok(-((g / v) * (atan_grade_sin + rrc * atan_grade_cos)
434 + (0.5 * rho_cdfa * (1.0 / m) * v)))
435 }
436 }
437
438 pub fn solve_step(&mut self) -> anyhow::Result<()> {
440 let i = *self.veh.state.i.get_fresh(|| format_dbg!())?;
441 let time_prev = *self.veh.state.time.get_stale(|| format_dbg!())?;
442 ensure!(self.cyc.time.len() > i);
443 self.veh.state.time.update(
444 *self.cyc.time.get(i).with_context(|| format_dbg!())?,
445 || format_dbg!(),
446 )?;
447 let dt = *self.veh.state.time.get_fresh(|| format_dbg!())? - time_prev;
448 self.veh
460 .solve_thermal(self.cyc.temp_amb_air[i], dt)
461 .with_context(|| format!("{}\n`self.veh.state.i`: {}", format_dbg!(), i))?;
462 match self.sim_params.ambient_thermal_soak {
463 false => {
464 self.veh
465 .set_curr_pwr_out_max(dt)
466 .with_context(|| anyhow!(format_dbg!()))?;
467 self.set_pwr_prop_for_speed(
468 self.cyc.speed[i],
469 *self.veh.state.speed_ach.get_stale(|| format_dbg!())?,
470 dt,
471 )
472 .with_context(|| anyhow!(format_dbg!()))?;
473 self.veh.state.pwr_tractive_for_cyc.update(
474 *self.veh.state.pwr_tractive.get_fresh(|| format_dbg!())?,
475 || format_dbg!(),
476 )?;
477 self.set_ach_speed(self.cyc.speed[i], dt)
478 .with_context(|| anyhow!(format_dbg!()))?;
479 if self.sim_params.trace_miss_opts.is_allow_checked() {
480 self.sim_params.trace_miss_tol.check_trace_miss(
481 self.cyc.speed[i],
482 *self.veh.state.speed_ach.get_fresh(|| format_dbg!())?,
483 self.cyc.dist[i],
484 *self.veh.state.dist.get_fresh(|| format_dbg!())?,
485 )?;
486 }
487 self.veh
488 .solve_powertrain(dt)
489 .with_context(|| anyhow!(format_dbg!()))?;
490 }
491 true => {
492 self.veh.mark_non_thermal_fresh()?;
493 }
494 }
495 self.set_cumulative(dt, || format_dbg!())?;
496 Ok(())
497 }
498
499 pub fn set_pwr_prop_for_speed(
504 &mut self,
505 speed: si::Velocity,
506 speed_prev: si::Velocity,
507 dt: si::Time,
508 ) -> anyhow::Result<()> {
509 let i = *self.veh.state.i.get_fresh(|| format_dbg!())?;
510 let vs = &mut self.veh.state;
511 let interp_pt_dist: &[f64] = match self.cyc.grade_interp {
516 Some(InterpolatorEnum::Interp0D(_)) => &[],
517 Some(InterpolatorEnum::Interp1D(_)) => {
518 &[vs.dist.get_fresh(|| format_dbg!())?.get::<si::meter>()]
519 }
520 _ => unreachable!(),
521 };
522 vs.grade_curr.update(
523 if *vs.cyc_met_overall.get_stale(|| format_dbg!())? {
524 *self
525 .cyc
526 .grade
527 .get(i)
528 .with_context(|| format_dbg!(self.cyc.grade.len()))?
529 } else {
530 uc::R
531 * self
532 .cyc
533 .grade_interp
534 .as_ref()
535 .with_context(|| format_dbg!("You might have somehow bypassed `init()`"))?
536 .interpolate(interp_pt_dist)
537 .with_context(|| format_dbg!())?
538 },
539 || format_dbg!(),
540 )?;
541 vs.elev_curr.update(
542 if *vs.cyc_met_overall.get_stale(|| format_dbg!())? {
543 *self.cyc.elev.get(i).with_context(|| format_dbg!())?
544 } else {
545 uc::M
546 * self
547 .cyc
548 .elev_interp
549 .as_ref()
550 .with_context(|| format_dbg!("You might have somehow bypassed `init()`"))?
551 .interpolate(interp_pt_dist)
552 .with_context(|| format_dbg!())?
553 },
554 || format_dbg!(),
555 )?;
556
557 vs.air_density.update(
558 if self.sim_params.f2_const_air_density {
559 1.2 * uc::KGPM3
560 } else {
561 let te_amb_air = {
562 let te_amb_air = self
563 .cyc
564 .temp_amb_air
565 .get(i)
566 .with_context(|| format_dbg!())?;
567 if *te_amb_air == *TE_STD_AIR {
568 None
569 } else {
570 Some(te_amb_air)
571 }
572 };
573 Air::get_density(
574 te_amb_air.copied(),
575 Some(*vs.elev_curr.get_fresh(|| format_dbg!())?),
576 )
577 },
578 || format_dbg!(),
579 )?;
580
581 let mass = self.veh.mass.with_context(|| {
582 format!(
583 "{}\nVehicle mass should have been set already.",
584 format_dbg!()
585 )
586 })?;
587 vs.pwr_accel.update(
588 mass / (2.0 * dt) * (speed.powi(P2::new()) - speed_prev.powi(P2::new())),
589 || format_dbg!(),
590 )?;
591 vs.pwr_ascent.update(
592 uc::ACC_GRAV
593 * *vs.grade_curr.get_fresh(|| format_dbg!())?
594 * mass
595 * (speed_prev + speed)
596 / 2.0,
597 || format_dbg!(),
598 )?;
599 vs.pwr_drag.update(
600 0.5
601 * Air::get_density(None, None)
603 * self.veh.chassis.drag_coef
604 * self.veh.chassis.frontal_area
605 * ((speed + speed_prev) / 2.0).powi(P3::new()),
606 || format_dbg!(),
607 )?;
608 vs.pwr_rr.update(
609 mass * uc::ACC_GRAV
610 * self.veh.chassis.wheel_rr_coef
611 * vs.grade_curr.get_fresh(|| format_dbg!())?.atan().cos()
612 * (speed_prev + speed)
613 / 2.,
614 || format_dbg!(),
615 )?;
616 vs.pwr_whl_inertia.update(
617 0.5 * self.veh.chassis.wheel_inertia
618 * self.veh.chassis.num_wheels as f64
619 * ((speed
620 / self
621 .veh
622 .chassis
623 .wheel_radius
624 .with_context(|| format_dbg!())?)
625 .powi(P2::new())
626 - (speed_prev
627 / self
628 .veh
629 .chassis
630 .wheel_radius
631 .with_context(|| format_dbg!())?)
632 .powi(P2::new()))
633 / self.cyc.dt_at_i(i).with_context(|| format_dbg!())?,
634 || format_dbg!(),
635 )?;
636
637 vs.pwr_tractive.update(
638 *vs.pwr_rr.get_fresh(|| format_dbg!())?
639 + *vs.pwr_whl_inertia.get_fresh(|| format_dbg!())?
640 + *vs.pwr_accel.get_fresh(|| format_dbg!())?
641 + *vs.pwr_ascent.get_fresh(|| format_dbg!())?
642 + *vs.pwr_drag.get_fresh(|| format_dbg!())?,
643 || format_dbg!(),
644 )?;
645 Ok(())
646 }
647
648 pub fn set_ach_speed(&mut self, cyc_speed: si::Velocity, dt: si::Time) -> anyhow::Result<()> {
653 let vs = &mut self.veh.state;
654 vs.cyc_met.update(
655 vs.pwr_tractive.get_fresh(|| format_dbg!())?
656 <= vs.pwr_prop_fwd_max.get_fresh(|| format_dbg!())?,
657 || format_dbg!(),
658 )?;
659 vs.cyc_met_overall.update(
660 if !*vs.cyc_met.get_fresh(|| format_dbg!())? {
661 false
664 } else {
665 *vs.cyc_met_overall.get_stale(|| format_dbg!())?
666 },
667 || format_dbg!(),
668 )?;
669 let veh = &mut self.veh;
670 let speed_prev = *veh.state.speed_ach.get_stale(|| format_dbg!())?;
671 if *veh.state.cyc_met.get_fresh(|| format_dbg!())? {
672 veh.state.speed_ach.update(cyc_speed, || format_dbg!())?;
673 return Ok(());
674 } else {
675 match self.sim_params.trace_miss_opts {
676 TraceMissOptions::Allow => {
677 }
679 TraceMissOptions::AllowChecked => {
680 }
682 TraceMissOptions::Error => bail!(
683 "{}\nFailed to meet speed trace.
684prescribed speed: {} mph
685prev speed_ach: {} mph
686pwr_tractive_for_cyc: {} kW
687pwr_tractive: {} kW
688pwr_prop_fwd_max: {} kW,
689pwr deficit: {} kW
690",
691 format_dbg!(),
692 cyc_speed.get::<si::mile_per_hour>(),
693 veh.state
694 .speed_ach
695 .get_stale(|| format_dbg!())?
696 .get::<si::mile_per_hour>(),
697 veh.state
698 .pwr_tractive_for_cyc
699 .get_fresh(|| format_dbg!())?
700 .get::<si::kilowatt>(),
701 veh.state
702 .pwr_tractive
703 .get_fresh(|| format_dbg!())?
704 .get::<si::kilowatt>(),
705 veh.state
706 .pwr_prop_fwd_max
707 .get_fresh(|| format_dbg!())?
708 .get::<si::kilowatt>(),
709 (*veh.state.pwr_tractive.get_fresh(|| format_dbg!())?
710 - *veh.state.pwr_prop_fwd_max.get_fresh(|| format_dbg!())?)
711 .get::<si::kilowatt>()
712 .format_eng(None)
713 ),
714 TraceMissOptions::Correct => {
715 }
720 }
721 }
722 let vs = &mut self.veh.state;
723 let step_info = StepInfo {
724 dt,
725 speed_prev,
726 cyc_speed,
727 grade_curr: *vs.grade_curr.get_fresh(|| format_dbg!())?,
728 air_density: *vs.air_density.get_fresh(|| format_dbg!())?,
729 mass: self.veh.mass.with_context(|| {
730 format!("{}\nMass should have been set before now", format_dbg!())
731 })?,
732 drag_coef: self.veh.chassis.drag_coef,
733 frontal_area: self.veh.chassis.frontal_area,
734 wheel_inertia: self.veh.chassis.wheel_inertia,
735 num_wheels: self.veh.chassis.num_wheels,
736 wheel_radius: self
737 .veh
738 .chassis
739 .wheel_radius
740 .with_context(|| format_dbg!())?,
741 wheel_rr_coef: self.veh.chassis.wheel_rr_coef,
742 pwr_prop_fwd_max: *vs.pwr_prop_fwd_max.get_fresh(|| format_dbg!())?,
743 };
744 let speed_ach = step_info.solve_for_speed(
745 self.sim_params.ach_speed_max_iter * 10,
746 self.sim_params.ach_speed_tol,
747 self.sim_params.ach_speed_solver_gain,
748 );
749 let speed_ach_floored = {
750 let v = ((speed_ach.get::<si::meter_per_second>() * 10.0).floor() / 10.0) * uc::MPS;
754 if v == speed_ach {
757 (v - 0.1 * uc::MPS).max(si::Velocity::ZERO)
758 } else {
759 v
760 }
761 };
762
763 vs.speed_ach.update(speed_ach_floored, || format_dbg!())?;
764 vs.air_density.mark_stale();
770 vs.cyc_met.mark_stale();
771 vs.cyc_met_overall.mark_stale();
772 vs.elev_curr.mark_stale();
773 vs.grade_curr.mark_stale();
774 vs.pwr_accel.mark_stale();
775 vs.pwr_ascent.mark_stale();
776 vs.pwr_drag.mark_stale();
777 vs.pwr_rr.mark_stale();
778 vs.pwr_tractive.mark_stale();
779 vs.pwr_whl_inertia.mark_stale();
780 vs.speed_ach.mark_stale();
781
782 self.set_pwr_prop_for_speed(speed_ach_floored, speed_prev, dt)
784 .with_context(|| format_dbg!())?;
785 self.set_ach_speed(speed_ach, dt)
786 .with_context(|| anyhow!(format_dbg!()))?;
787
788 if self.sim_params.trace_miss_opts == TraceMissOptions::Correct {
789 let i = *self.veh.state.i.get_fresh(|| format_dbg!())?;
790 let max_steps = self.sim_params.trace_miss_correct_max_steps.max(2) as usize;
791 let correction = calc_best_rendezvous(i, max_steps, &self.cyc, speed_ach_floored);
792 if correction.steps >= 2 {
793 self.cyc.speed[i] = speed_ach_floored;
799 self.cyc.modify_by_const_jerk_trajectory(
800 i + 1,
801 correction.steps,
802 correction.jerk_m_per_s3 * uc::MPS3,
803 correction.acceleration_m_per_s2 * uc::MPS2,
804 );
805 self.cyc.dist.clear();
806 self.cyc.elev.clear();
807 self.cyc.init().unwrap();
808 }
809 }
810
811 Ok(())
812 }
813
814 pub fn to_fastsim2(&self) -> anyhow::Result<fastsim_2::simdrive::RustSimDrive> {
815 let veh2 = self
816 .veh
817 .to_fastsim2()
818 .with_context(|| anyhow!(format_dbg!()))?;
819 let cyc2 = self
820 .cyc
821 .to_fastsim2()
822 .with_context(|| anyhow!(format_dbg!()))?;
823 Ok(fastsim_2::simdrive::RustSimDrive::new(cyc2, veh2))
824 }
825
826 pub fn clear(&mut self) {
827 self.veh.clear();
828 }
829}
830
831impl SetCumulative for SimDrive {
832 fn set_cumulative<F: Fn() -> String>(&mut self, dt: si::Time, loc: F) -> anyhow::Result<()> {
833 self.veh
834 .set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?;
835 Ok(())
836 }
837
838 fn reset_cumulative<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
839 self.veh
840 .reset_cumulative(|| format!("{}\n{}", loc(), format_dbg!()))?;
841 Ok(())
842 }
843}
844
845#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
846#[serde(deny_unknown_fields)]
847#[non_exhaustive]
848pub struct TraceMissTolerance {
850 tol_dist: si::Length,
853 tol_dist_frac: si::Ratio,
856 tol_speed: si::Velocity,
859 tol_speed_frac: si::Ratio,
862}
863
864impl TraceMissTolerance {
865 fn check_trace_miss(
866 &self,
867 cyc_speed: si::Velocity,
868 ach_speed: si::Velocity,
869 cyc_dist: si::Length,
870 ach_dist: si::Length,
871 ) -> anyhow::Result<()> {
872 ensure!(
873 cyc_speed - ach_speed < self.tol_speed,
874 "{}\n{}\n{}",
875 format_dbg!(cyc_speed),
876 format_dbg!(ach_speed),
877 format_dbg!(self.tol_speed)
878 );
879 if cyc_speed > self.tol_speed {
881 ensure!(
882 (cyc_speed - ach_speed) / cyc_speed < self.tol_speed_frac,
883 "{}\n{}\n{}",
884 format_dbg!(cyc_speed),
885 format_dbg!(ach_speed),
886 format_dbg!(self.tol_speed_frac)
887 )
888 }
889 ensure!(
890 (cyc_dist - ach_dist) < self.tol_dist,
891 "{}\n{}\n{}",
892 format_dbg!(cyc_dist),
893 format_dbg!(ach_dist),
894 format_dbg!(self.tol_dist)
895 );
896 if cyc_dist > self.tol_dist * 5.0 {
898 ensure!(
899 (cyc_dist - ach_dist) / cyc_dist < self.tol_dist_frac,
900 "{}\n{}\n{}",
901 format_dbg!(cyc_dist),
902 format_dbg!(ach_dist),
903 format_dbg!(self.tol_dist_frac)
904 )
905 }
906
907 Ok(())
908 }
909}
910impl SerdeAPI for TraceMissTolerance {}
911impl Init for TraceMissTolerance {}
912impl Default for TraceMissTolerance {
913 fn default() -> Self {
914 Self {
915 tol_dist: 100. * uc::M,
916 tol_dist_frac: 0.05 * uc::R,
917 tol_speed: 10. * uc::MPS,
918 tol_speed_frac: 0.5 * uc::R,
919 }
920 }
921}
922
923#[derive(
924 Clone, Default, Debug, Deserialize, Serialize, PartialEq, IsVariant, derive_more::From, TryInto,
925)]
926pub enum TraceMissOptions {
927 Allow,
929 AllowChecked,
931 #[default]
932 Error,
934 Correct,
936}
937
938impl SerdeAPI for TraceMissOptions {}
939impl Init for TraceMissOptions {}
940
941#[cfg(test)]
942mod tests {
943 use super::*;
944 use crate::vehicle::vehicle_model::tests::*;
945
946 #[test]
947 #[cfg(feature = "resources")]
948 fn test_sim_drive_conv() {
949 let _veh = mock_conv_veh();
950 let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
951 let mut sd = SimDrive::new(_veh, _cyc, Default::default());
952 sd.walk().unwrap();
953 assert!(
954 *sd.veh.state.i.get_fresh(String::new).unwrap() == sd.cyc.len_checked().unwrap() - 1
955 );
956 assert!(
957 *sd.veh
958 .fc()
959 .unwrap()
960 .state
961 .energy_fuel
962 .get_fresh(String::new)
963 .unwrap()
964 > si::Energy::ZERO
965 );
966 assert!(sd.veh.res().is_none());
967 }
968
969 #[test]
970 #[cfg(feature = "resources")]
971 fn test_sim_drive_hev() {
972 let _veh = mock_hev();
973 let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
974 let mut sd = SimDrive::new(_veh, _cyc, Default::default());
975 sd.walk().unwrap();
976 assert!(
977 *sd.veh.state.i.get_fresh(String::new).unwrap() == sd.cyc.len_checked().unwrap() - 1
978 );
979 assert!(
980 *sd.veh
981 .fc()
982 .unwrap()
983 .state
984 .energy_fuel
985 .get_fresh(String::new)
986 .unwrap()
987 > si::Energy::ZERO
988 );
989 assert!(
990 *sd.veh
991 .res()
992 .unwrap()
993 .state
994 .energy_out_chemical
995 .get_fresh(String::new)
996 .unwrap()
997 != si::Energy::ZERO
998 );
999 }
1000
1001 #[test]
1002 #[cfg(feature = "resources")]
1003 fn test_sim_drive_hev_thrml() {
1004 let _veh =
1005 Vehicle::from_resource("2021_Hyundai_Sonata_Hybrid_Blue_thrml.yaml", false).unwrap();
1006 let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
1007
1008 let te_amb_and_cab_and_batt_init_deg_c: Vec<(f64, f64)> = vec![
1009 (-6.7, -6.7),
1010 (5.0, 18.0),
1011 (22.0, 22.0),
1012 (25.0, 35.0),
1013 (45.0, 45.0),
1014 ];
1015 let te_amb: Vec<si::Temperature> = te_amb_and_cab_and_batt_init_deg_c
1016 .iter()
1017 .map(|t| (t.0 + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1018 .collect();
1019 let te_batt_and_cab_init: Vec<si::Temperature> = te_amb_and_cab_and_batt_init_deg_c
1020 .iter()
1021 .map(|t| (t.1 + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1022 .collect();
1023 let te_fc_init: Vec<si::Temperature> = [-6.7, 70.0, 90.0]
1024 .iter()
1025 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1026 .collect();
1027 for ((te_amb, te_init), te_fc_init) in
1028 te_amb.iter().zip(te_batt_and_cab_init).zip(te_fc_init)
1029 {
1030 let mut veh = _veh.clone();
1031
1032 veh.res_mut()
1033 .unwrap()
1034 .res_thrml_state_mut()
1035 .unwrap()
1036 .temperature
1037 .mark_stale();
1038 veh.res_mut()
1039 .unwrap()
1040 .res_thrml_state_mut()
1041 .unwrap()
1042 .temperature
1043 .update(te_init, || format_dbg!())
1044 .unwrap();
1045
1046 veh.res_mut()
1047 .unwrap()
1048 .res_thrml_state_mut()
1049 .unwrap()
1050 .temp_prev
1051 .mark_stale();
1052 veh.res_mut()
1053 .unwrap()
1054 .res_thrml_state_mut()
1055 .unwrap()
1056 .temp_prev
1057 .update(te_init, || format_dbg!())
1058 .unwrap();
1059 if let CabinOption::LumpedCabin(lc) = &mut veh.cabin {
1060 lc.state.temperature.mark_stale();
1061 lc.state
1062 .temperature
1063 .update(te_init, || format_dbg!())
1064 .unwrap();
1065 lc.state.temp_prev.mark_stale();
1066 lc.state
1067 .temp_prev
1068 .update(te_init, || format_dbg!())
1069 .unwrap();
1070 }
1071
1072 veh.fc_mut()
1073 .unwrap()
1074 .fc_thrml_state_mut()
1075 .unwrap()
1076 .temperature
1077 .mark_stale();
1078 veh.fc_mut()
1079 .unwrap()
1080 .fc_thrml_state_mut()
1081 .unwrap()
1082 .temperature
1083 .update(te_fc_init, || format_dbg!())
1084 .unwrap();
1085 let mut cyc = _cyc.clone();
1086 cyc.temp_amb_air = vec![*te_amb; cyc.len_checked().unwrap()];
1087 let mut sd = SimDrive::new(veh, cyc, Default::default());
1088 sd.walk()
1089 .with_context(|| {
1090 format!(
1091 "ambient temperature: {}*C\ninit temperature: {}",
1092 te_amb.get::<si::degree_celsius>(),
1093 te_init.get::<si::degree_celsius>()
1094 )
1095 })
1096 .unwrap();
1097 assert!(
1098 *sd.veh.state.i.get_fresh(String::new).unwrap()
1099 == sd.cyc.len_checked().unwrap() - 1
1100 );
1101 assert!(
1102 *sd.veh
1103 .fc()
1104 .unwrap()
1105 .state
1106 .energy_fuel
1107 .get_fresh(String::new)
1108 .unwrap()
1109 > si::Energy::ZERO
1110 );
1111 assert!(
1112 *sd.veh
1113 .res()
1114 .unwrap()
1115 .state
1116 .energy_out_chemical
1117 .get_fresh(String::new)
1118 .unwrap()
1119 != si::Energy::ZERO
1120 );
1121 }
1122 }
1123
1124 #[test]
1125 #[cfg(feature = "resources")]
1126 fn test_sim_drive_hev_thrml_soak() {
1128 let _veh =
1129 Vehicle::from_resource("2021_Hyundai_Sonata_Hybrid_Blue_thrml.yaml", false).unwrap();
1130 let mut cyc = Cycle::from_resource("udds.csv", false).unwrap();
1131 let mut soak_cyc_no_temp = cyc.clone();
1133 soak_cyc_no_temp
1134 .speed
1135 .iter_mut()
1136 .for_each(|v| *v = si::Velocity::ZERO);
1137
1138 let te_amb: Vec<si::Temperature> = [-6.7, -6.7, 38.0]
1139 .iter()
1140 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1141 .collect();
1142 let te_batt_and_cab_init: Vec<si::Temperature> = [-6.7, 22.0, 45.0]
1143 .iter()
1144 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1145 .collect();
1146 let te_fc_init: Vec<si::Temperature> = [-6.7, 70.0, 90.0]
1147 .iter()
1148 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1149 .collect();
1150 for ((te_amb, te_init), te_fc_init) in
1151 te_amb.iter().zip(te_batt_and_cab_init).zip(te_fc_init)
1152 {
1153 let prep_cyc = cyc
1154 .with_temp_amb_air(vec![*te_amb; cyc.len_checked().unwrap()])
1155 .unwrap();
1156 let soak_cyc = soak_cyc_no_temp
1157 .with_temp_amb_air(vec![*te_amb; cyc.len_checked().unwrap()])
1158 .unwrap();
1159 let test_cyc = cyc
1160 .with_temp_amb_air(vec![*te_amb; cyc.len_checked().unwrap()])
1161 .unwrap();
1162
1163 let mut veh = _veh.clone();
1164
1165 veh.res_mut()
1166 .unwrap()
1167 .res_thrml_state_mut()
1168 .unwrap()
1169 .temperature
1170 .mark_stale();
1171 veh.res_mut()
1172 .unwrap()
1173 .res_thrml_state_mut()
1174 .unwrap()
1175 .temperature
1176 .update(te_init, || format_dbg!())
1177 .unwrap();
1178
1179 veh.res_mut()
1180 .unwrap()
1181 .res_thrml_state_mut()
1182 .unwrap()
1183 .temp_prev
1184 .mark_stale();
1185 veh.res_mut()
1186 .unwrap()
1187 .res_thrml_state_mut()
1188 .unwrap()
1189 .temp_prev
1190 .update(te_init, || format_dbg!())
1191 .unwrap();
1192 if let CabinOption::LumpedCabin(lc) = &mut veh.cabin {
1193 lc.state.temperature.mark_stale();
1194 lc.state
1195 .temperature
1196 .update(te_init, || format_dbg!())
1197 .unwrap();
1198 lc.state.temp_prev.mark_stale();
1199 lc.state
1200 .temp_prev
1201 .update(te_init, || format_dbg!())
1202 .unwrap();
1203 }
1204
1205 veh.fc_mut()
1206 .unwrap()
1207 .fc_thrml_state_mut()
1208 .unwrap()
1209 .temperature
1210 .mark_stale();
1211 veh.fc_mut()
1212 .unwrap()
1213 .fc_thrml_state_mut()
1214 .unwrap()
1215 .temperature
1216 .update(te_fc_init, || format_dbg!())
1217 .unwrap();
1218
1219 dbg!("Running `sd_prep`");
1221 let mut sd_prep = SimDrive::new(veh, prep_cyc, None);
1222 sd_prep
1223 .walk()
1224 .with_context(|| {
1225 format!(
1226 "\nprep cycle:\nambient temperature: {}*C\ninit temperature: {}",
1227 te_amb.get::<si::degree_celsius>(),
1228 te_init.get::<si::degree_celsius>()
1229 )
1230 })
1231 .unwrap();
1232 assert!(
1233 *sd_prep.veh.state.i.get_fresh(String::new).unwrap()
1234 == sd_prep.cyc.len_checked().unwrap() - 1
1235 );
1236 sd_prep.reset_step(|| format_dbg!()).unwrap();
1237 sd_prep.veh.clear();
1238 sd_prep.reset_cumulative(|| format_dbg!()).unwrap();
1239
1240 dbg!("Running `sd_soak`");
1242 let mut sd_soak = SimDrive::new(
1243 sd_prep.veh.clone(),
1244 soak_cyc,
1245 Some(SimParams {
1246 ambient_thermal_soak: true,
1247 ..Default::default()
1248 }),
1249 );
1250 sd_soak
1251 .walk()
1252 .with_context(|| {
1253 format!(
1254 "\nsoak cycle:\nambient temperature: {}*C\ninit temperature: {}",
1255 te_amb.get::<si::degree_celsius>(),
1256 te_init.get::<si::degree_celsius>()
1257 )
1258 })
1259 .unwrap();
1260 assert!(
1261 *sd_soak.veh.state.i.get_fresh(String::new).unwrap()
1262 == sd_soak.cyc.len_checked().unwrap() - 1
1263 );
1264 sd_soak.reset_step(|| format_dbg!()).unwrap();
1265 sd_soak.veh.clear();
1266 sd_soak.reset_cumulative(|| format_dbg!()).unwrap();
1267
1268 dbg!("Running `sd_test`");
1270 let mut sd_test = SimDrive::new(sd_soak.veh.clone(), test_cyc, None);
1271 sd_test
1272 .walk()
1273 .with_context(|| {
1274 format!(
1275 "\ntest cycle:\nambient temperature: {}*C\ninit temperature: {}",
1276 te_amb.get::<si::degree_celsius>(),
1277 te_init.get::<si::degree_celsius>()
1278 )
1279 })
1280 .unwrap();
1281 assert!(
1282 *sd_test.veh.state.i.get_fresh(String::new).unwrap()
1283 == sd_test.cyc.len_checked().unwrap() - 1
1284 );
1285 sd_test.reset_step(|| format_dbg!()).unwrap();
1286 sd_test.veh.clear();
1287 sd_test.reset_cumulative(|| format_dbg!()).unwrap();
1288 }
1289 }
1290
1291 #[test]
1292 #[cfg(feature = "resources")]
1293 fn test_sim_drive_bev_thrml_soak() {
1295 let _veh = Vehicle::from_resource("2020 Chevrolet Bolt EV thrml.yaml", false).unwrap();
1296 let mut cyc = Cycle::from_resource("udds.csv", false).unwrap();
1297 let mut soak_cyc_no_temp = cyc.clone();
1299 soak_cyc_no_temp
1300 .speed
1301 .iter_mut()
1302 .for_each(|v| *v = si::Velocity::ZERO);
1303
1304 let te_amb: Vec<si::Temperature> = [-6.7, -6.7, 38.0]
1305 .iter()
1306 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1307 .collect();
1308 let te_batt_and_cab_init: Vec<si::Temperature> = [-6.7, 22.0, 45.0]
1309 .iter()
1310 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1311 .collect();
1312
1313 for (te_amb, te_init) in te_amb.iter().zip(te_batt_and_cab_init) {
1315 let prep_cyc = cyc
1316 .with_temp_amb_air(vec![*te_amb; cyc.len_checked().unwrap()])
1317 .unwrap();
1318 let soak_cyc = soak_cyc_no_temp
1319 .with_temp_amb_air(vec![*te_amb; cyc.len_checked().unwrap()])
1320 .unwrap();
1321 let test_cyc = cyc
1322 .with_temp_amb_air(vec![*te_amb; cyc.len_checked().unwrap()])
1323 .unwrap();
1324 let mut veh = _veh.clone();
1325
1326 veh.res_mut()
1327 .unwrap()
1328 .res_thrml_state_mut()
1329 .unwrap()
1330 .temperature
1331 .mark_stale();
1332 veh.res_mut()
1333 .unwrap()
1334 .res_thrml_state_mut()
1335 .unwrap()
1336 .temperature
1337 .update(te_init, || format_dbg!())
1338 .unwrap();
1339
1340 veh.res_mut()
1341 .unwrap()
1342 .res_thrml_state_mut()
1343 .unwrap()
1344 .temp_prev
1345 .mark_stale();
1346 veh.res_mut()
1347 .unwrap()
1348 .res_thrml_state_mut()
1349 .unwrap()
1350 .temp_prev
1351 .update(te_init, || format_dbg!())
1352 .unwrap();
1353
1354 if let CabinOption::LumpedCabin(lc) = &mut veh.cabin {
1356 lc.state.temperature.mark_stale();
1357 lc.state
1358 .temperature
1359 .update(te_init, || format_dbg!())
1360 .unwrap();
1361 lc.state.temp_prev.mark_stale();
1362 lc.state
1363 .temp_prev
1364 .update(te_init, || format_dbg!())
1365 .unwrap();
1366 }
1367
1368 dbg!("Running `sd_prep`");
1370 let mut sd_prep = SimDrive::new(veh, prep_cyc, None);
1371 sd_prep
1372 .walk()
1373 .with_context(|| {
1374 format!(
1375 "\nprep cycle:\nambient temperature: {}*C\ninit temperature: {}",
1376 te_amb.get::<si::degree_celsius>(),
1377 te_init.get::<si::degree_celsius>()
1378 )
1379 })
1380 .unwrap();
1381 assert!(
1382 *sd_prep.veh.state.i.get_fresh(String::new).unwrap()
1383 == sd_prep.cyc.len_checked().unwrap() - 1
1384 );
1385 sd_prep.reset_step(|| format_dbg!()).unwrap();
1386 sd_prep.veh.clear();
1387 sd_prep.reset_cumulative(|| format_dbg!()).unwrap();
1388
1389 dbg!("Running `sd_soak`");
1391 let mut sd_soak = SimDrive::new(
1392 sd_prep.veh.clone(),
1393 soak_cyc,
1394 Some(SimParams {
1395 ambient_thermal_soak: true,
1396 ..Default::default()
1397 }),
1398 );
1399 sd_soak
1400 .walk()
1401 .with_context(|| {
1402 format!(
1403 "\nsoak cycle:\nambient temperature: {}*C\ninit temperature: {}",
1404 te_amb.get::<si::degree_celsius>(),
1405 te_init.get::<si::degree_celsius>()
1406 )
1407 })
1408 .unwrap();
1409 assert!(
1410 *sd_soak.veh.state.i.get_fresh(String::new).unwrap()
1411 == sd_soak.cyc.len_checked().unwrap() - 1
1412 );
1413 sd_soak.reset_step(|| format_dbg!()).unwrap();
1414 sd_soak.veh.clear();
1415 sd_soak.reset_cumulative(|| format_dbg!()).unwrap();
1416
1417 dbg!("Running `sd_test`");
1419 let mut sd_test = SimDrive::new(sd_soak.veh.clone(), test_cyc, None);
1420 sd_test
1421 .walk()
1422 .with_context(|| {
1423 format!(
1424 "\ntest cycle:\nambient temperature: {}*C\ninit temperature: {}",
1425 te_amb.get::<si::degree_celsius>(),
1426 te_init.get::<si::degree_celsius>()
1427 )
1428 })
1429 .unwrap();
1430 assert!(
1431 *sd_test.veh.state.i.get_fresh(String::new).unwrap()
1432 == sd_test.cyc.len_checked().unwrap() - 1
1433 );
1434 sd_test.reset_step(|| format_dbg!()).unwrap();
1435 sd_test.veh.clear();
1436 sd_test.reset_cumulative(|| format_dbg!()).unwrap();
1437 }
1438 }
1439
1440 #[test]
1441 #[cfg(feature = "resources")]
1442 fn test_sim_drive_bev() {
1443 let _veh = mock_bev();
1444 let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
1445 let mut sd = SimDrive {
1446 veh: _veh,
1447 cyc: _cyc,
1448 sim_params: Default::default(),
1449 };
1450 sd.walk().unwrap();
1451 assert!(
1452 *sd.veh.state.i.get_fresh(String::new).unwrap() == sd.cyc.len_checked().unwrap() - 1
1453 );
1454 assert!(sd.veh.fc().is_none());
1455 assert!(
1456 *sd.veh
1457 .res()
1458 .unwrap()
1459 .state
1460 .energy_out_chemical
1461 .get_fresh(String::new)
1462 .unwrap()
1463 != si::Energy::ZERO
1464 );
1465 }
1466
1467 #[test]
1468 #[cfg(feature = "resources")]
1469 fn test_sim_drive_bev_thrml() {
1470 let _veh = Vehicle::from_resource("2020 Chevrolet Bolt EV thrml.yaml", false).unwrap();
1471 let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
1472
1473 let te_amb_and_cab_and_batt_init_deg_c: Vec<(f64, f64)> = vec![
1474 (-6.7, -6.7),
1475 (5.0, 18.0),
1476 (22.0, 22.0),
1477 (25.0, 35.0),
1478 (45.0, 45.0),
1479 ];
1480 let te_amb: Vec<si::Temperature> = te_amb_and_cab_and_batt_init_deg_c
1481 .iter()
1482 .map(|t| (t.0 + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1483 .collect();
1484 let te_batt_and_cab_init: Vec<si::Temperature> = te_amb_and_cab_and_batt_init_deg_c
1485 .iter()
1486 .map(|t| (t.1 + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1487 .collect();
1488 for (te_amb, te_init) in te_amb.iter().zip(te_batt_and_cab_init) {
1489 let mut veh = _veh.clone();
1490 veh.res_mut()
1491 .unwrap()
1492 .res_thrml_state_mut()
1493 .unwrap()
1494 .temperature
1495 .mark_stale();
1496 veh.res_mut()
1497 .unwrap()
1498 .res_thrml_state_mut()
1499 .unwrap()
1500 .temperature
1501 .update(te_init, || format_dbg!())
1502 .unwrap();
1503
1504 veh.res_mut()
1505 .unwrap()
1506 .res_thrml_state_mut()
1507 .unwrap()
1508 .temp_prev
1509 .mark_stale();
1510 veh.res_mut()
1511 .unwrap()
1512 .res_thrml_state_mut()
1513 .unwrap()
1514 .temp_prev
1515 .update(te_init, || format_dbg!())
1516 .unwrap();
1517
1518 if let CabinOption::LumpedCabin(lc) = &mut veh.cabin {
1519 lc.state.temperature.mark_stale();
1520 lc.state
1521 .temperature
1522 .update(te_init, || format_dbg!())
1523 .unwrap();
1524
1525 lc.state.temp_prev.mark_stale();
1526 lc.state
1527 .temp_prev
1528 .update(te_init, || format_dbg!())
1529 .unwrap();
1530 } else {
1531 panic!("cabin should have been configured");
1532 }
1533 let mut cyc = _cyc.clone();
1534 cyc.temp_amb_air = vec![*te_amb; cyc.len_checked().unwrap()];
1535 let mut sd = SimDrive::new(veh, cyc, Default::default());
1536 if let CabinOption::LumpedCabin(lc) = sd.veh.cabin.clone() {
1537 assert_eq!(
1538 *lc.state.temperature.get_fresh(|| format_dbg!()).unwrap(),
1539 te_init
1540 );
1541 } else {
1542 panic!();
1543 };
1544 sd.walk()
1545 .with_context(|| {
1546 format!(
1547 "ambient temperature: {}*C\ninit temperature: {}",
1548 te_amb.get::<si::degree_celsius>(),
1549 te_init.get::<si::degree_celsius>()
1550 )
1551 })
1552 .unwrap();
1553 assert!(
1554 *sd.veh.state.i.get_fresh(String::new).unwrap()
1555 == sd.cyc.len_checked().unwrap() - 1
1556 );
1557 assert!(sd.veh.fc().is_none());
1558 assert!(
1559 *sd.veh
1560 .res()
1561 .unwrap()
1562 .state
1563 .energy_out_chemical
1564 .get_fresh(String::new)
1565 .unwrap()
1566 != si::Energy::ZERO
1567 );
1568 sd.veh.reset_step(|| format_dbg!()).unwrap();
1569 sd.veh.state.time.mark_stale();
1570 sd.veh
1571 .state
1572 .time
1573 .update(si::Time::ZERO, || format_dbg!())
1574 .unwrap();
1575 assert!(*sd.veh.state.i.get_fresh(|| format_dbg!()).unwrap() == 0);
1576 sd.walk()
1577 .with_context(|| {
1578 format!(
1579 "ambient temperature: {}*C\ninit temperature: {}",
1580 te_amb.get::<si::degree_celsius>(),
1581 te_init.get::<si::degree_celsius>()
1582 )
1583 })
1584 .unwrap();
1585 sd.reset_cumulative(|| format_dbg!()).unwrap();
1586 assert_eq!(*sd.veh.state.i.get_fresh(|| format_dbg!()).unwrap(), 1369);
1587 }
1588 }
1589}