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 self.veh.state.time.update(
443 *self.cyc.time.get(i).with_context(|| format_dbg!())?,
444 || format_dbg!(),
445 )?;
446 let dt = *self.veh.state.time.get_fresh(|| format_dbg!())? - time_prev;
447 self.veh
459 .solve_thermal(self.cyc.temp_amb_air[i], dt)
460 .with_context(|| format!("{}\n`self.veh.state.i`: {}", format_dbg!(), i))?;
461 match self.sim_params.ambient_thermal_soak {
462 false => {
463 self.veh
464 .set_curr_pwr_out_max(dt)
465 .with_context(|| anyhow!(format_dbg!()))?;
466 self.set_pwr_prop_for_speed(
467 self.cyc.speed[i],
468 *self.veh.state.speed_ach.get_stale(|| format_dbg!())?,
469 dt,
470 )
471 .with_context(|| anyhow!(format_dbg!()))?;
472 self.veh.state.pwr_tractive_for_cyc.update(
473 *self.veh.state.pwr_tractive.get_fresh(|| format_dbg!())?,
474 || format_dbg!(),
475 )?;
476 self.set_ach_speed(self.cyc.speed[i], dt)
477 .with_context(|| anyhow!(format_dbg!()))?;
478 if self.sim_params.trace_miss_opts.is_allow_checked() {
479 self.sim_params.trace_miss_tol.check_trace_miss(
480 self.cyc.speed[i],
481 *self.veh.state.speed_ach.get_fresh(|| format_dbg!())?,
482 self.cyc.dist[i],
483 *self.veh.state.dist.get_fresh(|| format_dbg!())?,
484 )?;
485 }
486 self.veh
487 .solve_powertrain(dt)
488 .with_context(|| anyhow!(format_dbg!()))?;
489 }
490 true => {
491 self.veh.mark_non_thermal_fresh()?;
492 }
493 }
494 self.set_cumulative(dt, || format_dbg!())?;
495 Ok(())
496 }
497
498 pub fn set_pwr_prop_for_speed(
503 &mut self,
504 speed: si::Velocity,
505 speed_prev: si::Velocity,
506 dt: si::Time,
507 ) -> anyhow::Result<()> {
508 let i = *self.veh.state.i.get_fresh(|| format_dbg!())?;
509 let vs = &mut self.veh.state;
510 let interp_pt_dist: &[f64] = match self.cyc.grade_interp {
515 Some(InterpolatorEnum::Interp0D(_)) => &[],
516 Some(InterpolatorEnum::Interp1D(_)) => {
517 &[vs.dist.get_fresh(|| format_dbg!())?.get::<si::meter>()]
518 }
519 _ => unreachable!(),
520 };
521 vs.grade_curr.update(
522 if *vs.cyc_met_overall.get_stale(|| format_dbg!())? {
523 *self
524 .cyc
525 .grade
526 .get(i)
527 .with_context(|| format_dbg!(self.cyc.grade.len()))?
528 } else {
529 uc::R
530 * self
531 .cyc
532 .grade_interp
533 .as_ref()
534 .with_context(|| format_dbg!("You might have somehow bypassed `init()`"))?
535 .interpolate(interp_pt_dist)
536 .with_context(|| format_dbg!())?
537 },
538 || format_dbg!(),
539 )?;
540 vs.elev_curr.update(
541 if *vs.cyc_met_overall.get_stale(|| format_dbg!())? {
542 *self.cyc.elev.get(i).with_context(|| format_dbg!())?
543 } else {
544 uc::M
545 * self
546 .cyc
547 .elev_interp
548 .as_ref()
549 .with_context(|| format_dbg!("You might have somehow bypassed `init()`"))?
550 .interpolate(interp_pt_dist)
551 .with_context(|| format_dbg!())?
552 },
553 || format_dbg!(),
554 )?;
555
556 vs.air_density.update(
557 if self.sim_params.f2_const_air_density {
558 1.2 * uc::KGPM3
559 } else {
560 let te_amb_air = {
561 let te_amb_air = self
562 .cyc
563 .temp_amb_air
564 .get(i)
565 .with_context(|| format_dbg!())?;
566 if *te_amb_air == *TE_STD_AIR {
567 None
568 } else {
569 Some(te_amb_air)
570 }
571 };
572 Air::get_density(
573 te_amb_air.copied(),
574 Some(*vs.elev_curr.get_fresh(|| format_dbg!())?),
575 )
576 },
577 || format_dbg!(),
578 )?;
579
580 let mass = self.veh.mass.with_context(|| {
581 format!(
582 "{}\nVehicle mass should have been set already.",
583 format_dbg!()
584 )
585 })?;
586 vs.pwr_accel.update(
587 mass / (2.0 * dt) * (speed.powi(P2::new()) - speed_prev.powi(P2::new())),
588 || format_dbg!(),
589 )?;
590 vs.pwr_ascent.update(
591 uc::ACC_GRAV
592 * *vs.grade_curr.get_fresh(|| format_dbg!())?
593 * mass
594 * (speed_prev + speed)
595 / 2.0,
596 || format_dbg!(),
597 )?;
598 vs.pwr_drag.update(
599 0.5
600 * Air::get_density(None, None)
602 * self.veh.chassis.drag_coef
603 * self.veh.chassis.frontal_area
604 * ((speed + speed_prev) / 2.0).powi(P3::new()),
605 || format_dbg!(),
606 )?;
607 vs.pwr_rr.update(
608 mass * uc::ACC_GRAV
609 * self.veh.chassis.wheel_rr_coef
610 * vs.grade_curr.get_fresh(|| format_dbg!())?.atan().cos()
611 * (speed_prev + speed)
612 / 2.,
613 || format_dbg!(),
614 )?;
615 vs.pwr_whl_inertia.update(
616 0.5 * self.veh.chassis.wheel_inertia
617 * self.veh.chassis.num_wheels as f64
618 * ((speed
619 / self
620 .veh
621 .chassis
622 .wheel_radius
623 .with_context(|| format_dbg!())?)
624 .powi(P2::new())
625 - (speed_prev
626 / self
627 .veh
628 .chassis
629 .wheel_radius
630 .with_context(|| format_dbg!())?)
631 .powi(P2::new()))
632 / self.cyc.dt_at_i(i).with_context(|| format_dbg!())?,
633 || format_dbg!(),
634 )?;
635
636 vs.pwr_tractive.update(
637 *vs.pwr_rr.get_fresh(|| format_dbg!())?
638 + *vs.pwr_whl_inertia.get_fresh(|| format_dbg!())?
639 + *vs.pwr_accel.get_fresh(|| format_dbg!())?
640 + *vs.pwr_ascent.get_fresh(|| format_dbg!())?
641 + *vs.pwr_drag.get_fresh(|| format_dbg!())?,
642 || format_dbg!(),
643 )?;
644 Ok(())
645 }
646
647 pub fn set_ach_speed(&mut self, cyc_speed: si::Velocity, dt: si::Time) -> anyhow::Result<()> {
652 let vs = &mut self.veh.state;
653 vs.cyc_met.update(
654 vs.pwr_tractive.get_fresh(|| format_dbg!())?
655 <= vs.pwr_prop_fwd_max.get_fresh(|| format_dbg!())?,
656 || format_dbg!(),
657 )?;
658 vs.cyc_met_overall.update(
659 if !*vs.cyc_met.get_fresh(|| format_dbg!())? {
660 false
663 } else {
664 *vs.cyc_met_overall.get_stale(|| format_dbg!())?
665 },
666 || format_dbg!(),
667 )?;
668 let veh = &mut self.veh;
669 let speed_prev = *veh.state.speed_ach.get_stale(|| format_dbg!())?;
670 if *veh.state.cyc_met.get_fresh(|| format_dbg!())? {
671 veh.state.speed_ach.update(cyc_speed, || format_dbg!())?;
672 return Ok(());
673 } else {
674 match self.sim_params.trace_miss_opts {
675 TraceMissOptions::Allow => {
676 }
678 TraceMissOptions::AllowChecked => {
679 }
681 TraceMissOptions::Error => bail!(
682 "{}\nFailed to meet speed trace.
683prescribed speed: {} mph
684prev speed_ach: {} mph
685pwr_tractive_for_cyc: {} kW
686pwr_tractive: {} kW
687pwr_prop_fwd_max: {} kW,
688pwr deficit: {} kW
689",
690 format_dbg!(),
691 cyc_speed.get::<si::mile_per_hour>(),
692 veh.state
693 .speed_ach
694 .get_stale(|| format_dbg!())?
695 .get::<si::mile_per_hour>(),
696 veh.state
697 .pwr_tractive_for_cyc
698 .get_fresh(|| format_dbg!())?
699 .get::<si::kilowatt>(),
700 veh.state
701 .pwr_tractive
702 .get_fresh(|| format_dbg!())?
703 .get::<si::kilowatt>(),
704 veh.state
705 .pwr_prop_fwd_max
706 .get_fresh(|| format_dbg!())?
707 .get::<si::kilowatt>(),
708 (*veh.state.pwr_tractive.get_fresh(|| format_dbg!())?
709 - *veh.state.pwr_prop_fwd_max.get_fresh(|| format_dbg!())?)
710 .get::<si::kilowatt>()
711 .format_eng(None)
712 ),
713 TraceMissOptions::Correct => {
714 }
719 }
720 }
721 let vs = &mut self.veh.state;
722 let step_info = StepInfo {
723 dt,
724 speed_prev,
725 cyc_speed,
726 grade_curr: *vs.grade_curr.get_fresh(|| format_dbg!())?,
727 air_density: *vs.air_density.get_fresh(|| format_dbg!())?,
728 mass: self.veh.mass.with_context(|| {
729 format!("{}\nMass should have been set before now", format_dbg!())
730 })?,
731 drag_coef: self.veh.chassis.drag_coef,
732 frontal_area: self.veh.chassis.frontal_area,
733 wheel_inertia: self.veh.chassis.wheel_inertia,
734 num_wheels: self.veh.chassis.num_wheels,
735 wheel_radius: self
736 .veh
737 .chassis
738 .wheel_radius
739 .with_context(|| format_dbg!())?,
740 wheel_rr_coef: self.veh.chassis.wheel_rr_coef,
741 pwr_prop_fwd_max: *vs.pwr_prop_fwd_max.get_fresh(|| format_dbg!())?,
742 };
743 let speed_ach = step_info.solve_for_speed(
744 self.sim_params.ach_speed_max_iter * 10,
745 self.sim_params.ach_speed_tol,
746 self.sim_params.ach_speed_solver_gain,
747 );
748 let speed_ach_floored = {
749 let v = ((speed_ach.get::<si::meter_per_second>() * 10.0).floor() / 10.0) * uc::MPS;
753 if v == speed_ach {
756 (v - 0.1 * uc::MPS).max(si::Velocity::ZERO)
757 } else {
758 v
759 }
760 };
761
762 vs.speed_ach.update(speed_ach_floored, || format_dbg!())?;
763 vs.air_density.mark_stale();
769 vs.cyc_met.mark_stale();
770 vs.cyc_met_overall.mark_stale();
771 vs.elev_curr.mark_stale();
772 vs.grade_curr.mark_stale();
773 vs.pwr_accel.mark_stale();
774 vs.pwr_ascent.mark_stale();
775 vs.pwr_drag.mark_stale();
776 vs.pwr_rr.mark_stale();
777 vs.pwr_tractive.mark_stale();
778 vs.pwr_whl_inertia.mark_stale();
779 vs.speed_ach.mark_stale();
780
781 self.set_pwr_prop_for_speed(speed_ach_floored, speed_prev, dt)
783 .with_context(|| format_dbg!())?;
784 self.set_ach_speed(speed_ach, dt)
785 .with_context(|| anyhow!(format_dbg!()))?;
786
787 if self.sim_params.trace_miss_opts == TraceMissOptions::Correct {
788 let i = *self.veh.state.i.get_fresh(|| format_dbg!())?;
789 let max_steps = self.sim_params.trace_miss_correct_max_steps.max(2) as usize;
790 let correction = calc_best_rendezvous(i, max_steps, &self.cyc, speed_ach_floored);
791 if correction.steps >= 2 {
792 self.cyc.speed[i] = speed_ach_floored;
798 self.cyc.modify_by_const_jerk_trajectory(
799 i + 1,
800 correction.steps,
801 correction.jerk_m_per_s3 * uc::MPS3,
802 correction.acceleration_m_per_s2 * uc::MPS2,
803 );
804 self.cyc.dist.clear();
805 self.cyc.elev.clear();
806 self.cyc.init().unwrap();
807 }
808 }
809
810 Ok(())
811 }
812
813 pub fn to_fastsim2(&self) -> anyhow::Result<fastsim_2::simdrive::RustSimDrive> {
814 let veh2 = self
815 .veh
816 .to_fastsim2()
817 .with_context(|| anyhow!(format_dbg!()))?;
818 let cyc2 = self
819 .cyc
820 .to_fastsim2()
821 .with_context(|| anyhow!(format_dbg!()))?;
822 Ok(fastsim_2::simdrive::RustSimDrive::new(cyc2, veh2))
823 }
824
825 pub fn clear(&mut self) {
826 self.veh.clear();
827 }
828}
829
830impl SetCumulative for SimDrive {
831 fn set_cumulative<F: Fn() -> String>(&mut self, dt: si::Time, loc: F) -> anyhow::Result<()> {
832 self.veh
833 .set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?;
834 Ok(())
835 }
836
837 fn reset_cumulative<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
838 self.veh
839 .reset_cumulative(|| format!("{}\n{}", loc(), format_dbg!()))?;
840 Ok(())
841 }
842}
843
844#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
845#[serde(deny_unknown_fields)]
846#[non_exhaustive]
847pub struct TraceMissTolerance {
849 tol_dist: si::Length,
852 tol_dist_frac: si::Ratio,
855 tol_speed: si::Velocity,
858 tol_speed_frac: si::Ratio,
861}
862
863impl TraceMissTolerance {
864 fn check_trace_miss(
865 &self,
866 cyc_speed: si::Velocity,
867 ach_speed: si::Velocity,
868 cyc_dist: si::Length,
869 ach_dist: si::Length,
870 ) -> anyhow::Result<()> {
871 ensure!(
872 cyc_speed - ach_speed < self.tol_speed,
873 "{}\n{}\n{}",
874 format_dbg!(cyc_speed),
875 format_dbg!(ach_speed),
876 format_dbg!(self.tol_speed)
877 );
878 if cyc_speed > self.tol_speed {
880 ensure!(
881 (cyc_speed - ach_speed) / cyc_speed < self.tol_speed_frac,
882 "{}\n{}\n{}",
883 format_dbg!(cyc_speed),
884 format_dbg!(ach_speed),
885 format_dbg!(self.tol_speed_frac)
886 )
887 }
888 ensure!(
889 (cyc_dist - ach_dist) < self.tol_dist,
890 "{}\n{}\n{}",
891 format_dbg!(cyc_dist),
892 format_dbg!(ach_dist),
893 format_dbg!(self.tol_dist)
894 );
895 if cyc_dist > self.tol_dist * 5.0 {
897 ensure!(
898 (cyc_dist - ach_dist) / cyc_dist < self.tol_dist_frac,
899 "{}\n{}\n{}",
900 format_dbg!(cyc_dist),
901 format_dbg!(ach_dist),
902 format_dbg!(self.tol_dist_frac)
903 )
904 }
905
906 Ok(())
907 }
908}
909impl SerdeAPI for TraceMissTolerance {}
910impl Init for TraceMissTolerance {}
911impl Default for TraceMissTolerance {
912 fn default() -> Self {
913 Self {
914 tol_dist: 100. * uc::M,
915 tol_dist_frac: 0.05 * uc::R,
916 tol_speed: 10. * uc::MPS,
917 tol_speed_frac: 0.5 * uc::R,
918 }
919 }
920}
921
922#[derive(
923 Clone, Default, Debug, Deserialize, Serialize, PartialEq, IsVariant, derive_more::From, TryInto,
924)]
925pub enum TraceMissOptions {
926 Allow,
928 AllowChecked,
930 #[default]
931 Error,
933 Correct,
935}
936
937impl SerdeAPI for TraceMissOptions {}
938impl Init for TraceMissOptions {}
939
940#[cfg(test)]
941mod tests {
942 use super::*;
943 use crate::vehicle::vehicle_model::tests::*;
944
945 #[test]
946 #[cfg(feature = "resources")]
947 fn test_sim_drive_conv() {
948 let _veh = mock_conv_veh();
949 let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
950 let mut sd = SimDrive::new(_veh, _cyc, Default::default());
951 sd.walk().unwrap();
952 assert!(
953 *sd.veh.state.i.get_fresh(String::new).unwrap() == sd.cyc.len_checked().unwrap() - 1
954 );
955 assert!(
956 *sd.veh
957 .fc()
958 .unwrap()
959 .state
960 .energy_fuel
961 .get_fresh(String::new)
962 .unwrap()
963 > si::Energy::ZERO
964 );
965 assert!(sd.veh.res().is_none());
966 }
967
968 #[test]
969 #[cfg(feature = "resources")]
970 fn test_sim_drive_hev() {
971 let _veh = mock_hev();
972 let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
973 let mut sd = SimDrive::new(_veh, _cyc, Default::default());
974 sd.walk().unwrap();
975 assert!(
976 *sd.veh.state.i.get_fresh(String::new).unwrap() == sd.cyc.len_checked().unwrap() - 1
977 );
978 assert!(
979 *sd.veh
980 .fc()
981 .unwrap()
982 .state
983 .energy_fuel
984 .get_fresh(String::new)
985 .unwrap()
986 > si::Energy::ZERO
987 );
988 assert!(
989 *sd.veh
990 .res()
991 .unwrap()
992 .state
993 .energy_out_chemical
994 .get_fresh(String::new)
995 .unwrap()
996 != si::Energy::ZERO
997 );
998 }
999
1000 #[test]
1001 #[cfg(feature = "resources")]
1002 fn test_sim_drive_hev_thrml() {
1003 let _veh =
1004 Vehicle::from_resource("2021_Hyundai_Sonata_Hybrid_Blue_thrml.yaml", false).unwrap();
1005 let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
1006
1007 let te_amb_and_cab_and_batt_init_deg_c: Vec<(f64, f64)> = vec![
1008 (-6.7, -6.7),
1009 (5.0, 18.0),
1010 (22.0, 22.0),
1011 (25.0, 35.0),
1012 (45.0, 45.0),
1013 ];
1014 let te_amb: Vec<si::Temperature> = te_amb_and_cab_and_batt_init_deg_c
1015 .iter()
1016 .map(|t| (t.0 + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1017 .collect();
1018 let te_batt_and_cab_init: Vec<si::Temperature> = te_amb_and_cab_and_batt_init_deg_c
1019 .iter()
1020 .map(|t| (t.1 + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1021 .collect();
1022 let te_fc_init: Vec<si::Temperature> = [-6.7, 70.0, 90.0]
1023 .iter()
1024 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1025 .collect();
1026 for ((te_amb, te_init), te_fc_init) in
1027 te_amb.iter().zip(te_batt_and_cab_init).zip(te_fc_init)
1028 {
1029 let mut veh = _veh.clone();
1030
1031 veh.res_mut()
1032 .unwrap()
1033 .res_thrml_state_mut()
1034 .unwrap()
1035 .temperature
1036 .mark_stale();
1037 veh.res_mut()
1038 .unwrap()
1039 .res_thrml_state_mut()
1040 .unwrap()
1041 .temperature
1042 .update(te_init, || format_dbg!())
1043 .unwrap();
1044
1045 veh.res_mut()
1046 .unwrap()
1047 .res_thrml_state_mut()
1048 .unwrap()
1049 .temp_prev
1050 .mark_stale();
1051 veh.res_mut()
1052 .unwrap()
1053 .res_thrml_state_mut()
1054 .unwrap()
1055 .temp_prev
1056 .update(te_init, || format_dbg!())
1057 .unwrap();
1058 if let CabinOption::LumpedCabin(lc) = &mut veh.cabin {
1059 lc.state.temperature.mark_stale();
1060 lc.state
1061 .temperature
1062 .update(te_init, || format_dbg!())
1063 .unwrap();
1064 lc.state.temp_prev.mark_stale();
1065 lc.state
1066 .temp_prev
1067 .update(te_init, || format_dbg!())
1068 .unwrap();
1069 }
1070
1071 veh.fc_mut()
1072 .unwrap()
1073 .fc_thrml_state_mut()
1074 .unwrap()
1075 .temperature
1076 .mark_stale();
1077 veh.fc_mut()
1078 .unwrap()
1079 .fc_thrml_state_mut()
1080 .unwrap()
1081 .temperature
1082 .update(te_fc_init, || format_dbg!())
1083 .unwrap();
1084 let mut cyc = _cyc.clone();
1085 cyc.temp_amb_air = vec![*te_amb; cyc.len_checked().unwrap()];
1086 let mut sd = SimDrive::new(veh, cyc, Default::default());
1087 sd.walk()
1088 .with_context(|| {
1089 format!(
1090 "ambient temperature: {}*C\ninit temperature: {}",
1091 te_amb.get::<si::degree_celsius>(),
1092 te_init.get::<si::degree_celsius>()
1093 )
1094 })
1095 .unwrap();
1096 assert!(
1097 *sd.veh.state.i.get_fresh(String::new).unwrap()
1098 == sd.cyc.len_checked().unwrap() - 1
1099 );
1100 assert!(
1101 *sd.veh
1102 .fc()
1103 .unwrap()
1104 .state
1105 .energy_fuel
1106 .get_fresh(String::new)
1107 .unwrap()
1108 > si::Energy::ZERO
1109 );
1110 assert!(
1111 *sd.veh
1112 .res()
1113 .unwrap()
1114 .state
1115 .energy_out_chemical
1116 .get_fresh(String::new)
1117 .unwrap()
1118 != si::Energy::ZERO
1119 );
1120 }
1121 }
1122
1123 #[test]
1124 #[cfg(feature = "resources")]
1125 fn test_sim_drive_hev_thrml_soak() {
1127 let _veh =
1128 Vehicle::from_resource("2021_Hyundai_Sonata_Hybrid_Blue_thrml.yaml", false).unwrap();
1129 let mut cyc = Cycle::from_resource("udds.csv", false).unwrap();
1130 let mut soak_cyc_no_temp = cyc.clone();
1132 soak_cyc_no_temp
1133 .speed
1134 .iter_mut()
1135 .for_each(|v| *v = si::Velocity::ZERO);
1136
1137 let te_amb: Vec<si::Temperature> = [-6.7, -6.7, 38.0]
1138 .iter()
1139 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1140 .collect();
1141 let te_batt_and_cab_init: Vec<si::Temperature> = [-6.7, 22.0, 45.0]
1142 .iter()
1143 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1144 .collect();
1145 let te_fc_init: Vec<si::Temperature> = [-6.7, 70.0, 90.0]
1146 .iter()
1147 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1148 .collect();
1149 for ((te_amb, te_init), te_fc_init) in
1150 te_amb.iter().zip(te_batt_and_cab_init).zip(te_fc_init)
1151 {
1152 let prep_cyc = cyc
1153 .with_temp_amb_air(vec![*te_amb; cyc.len_checked().unwrap()])
1154 .unwrap();
1155 let soak_cyc = soak_cyc_no_temp
1156 .with_temp_amb_air(vec![*te_amb; cyc.len_checked().unwrap()])
1157 .unwrap();
1158 let test_cyc = cyc
1159 .with_temp_amb_air(vec![*te_amb; cyc.len_checked().unwrap()])
1160 .unwrap();
1161
1162 let mut veh = _veh.clone();
1163
1164 veh.res_mut()
1165 .unwrap()
1166 .res_thrml_state_mut()
1167 .unwrap()
1168 .temperature
1169 .mark_stale();
1170 veh.res_mut()
1171 .unwrap()
1172 .res_thrml_state_mut()
1173 .unwrap()
1174 .temperature
1175 .update(te_init, || format_dbg!())
1176 .unwrap();
1177
1178 veh.res_mut()
1179 .unwrap()
1180 .res_thrml_state_mut()
1181 .unwrap()
1182 .temp_prev
1183 .mark_stale();
1184 veh.res_mut()
1185 .unwrap()
1186 .res_thrml_state_mut()
1187 .unwrap()
1188 .temp_prev
1189 .update(te_init, || format_dbg!())
1190 .unwrap();
1191 if let CabinOption::LumpedCabin(lc) = &mut veh.cabin {
1192 lc.state.temperature.mark_stale();
1193 lc.state
1194 .temperature
1195 .update(te_init, || format_dbg!())
1196 .unwrap();
1197 lc.state.temp_prev.mark_stale();
1198 lc.state
1199 .temp_prev
1200 .update(te_init, || format_dbg!())
1201 .unwrap();
1202 }
1203
1204 veh.fc_mut()
1205 .unwrap()
1206 .fc_thrml_state_mut()
1207 .unwrap()
1208 .temperature
1209 .mark_stale();
1210 veh.fc_mut()
1211 .unwrap()
1212 .fc_thrml_state_mut()
1213 .unwrap()
1214 .temperature
1215 .update(te_fc_init, || format_dbg!())
1216 .unwrap();
1217
1218 dbg!("Running `sd_prep`");
1220 let mut sd_prep = SimDrive::new(veh, prep_cyc, None);
1221 sd_prep
1222 .walk()
1223 .with_context(|| {
1224 format!(
1225 "\nprep cycle:\nambient temperature: {}*C\ninit temperature: {}",
1226 te_amb.get::<si::degree_celsius>(),
1227 te_init.get::<si::degree_celsius>()
1228 )
1229 })
1230 .unwrap();
1231 assert!(
1232 *sd_prep.veh.state.i.get_fresh(String::new).unwrap()
1233 == sd_prep.cyc.len_checked().unwrap() - 1
1234 );
1235 sd_prep.reset_step(|| format_dbg!()).unwrap();
1236 sd_prep.veh.clear();
1237 sd_prep.reset_cumulative(|| format_dbg!()).unwrap();
1238
1239 dbg!("Running `sd_soak`");
1241 let mut sd_soak = SimDrive::new(
1242 sd_prep.veh.clone(),
1243 soak_cyc,
1244 Some(SimParams {
1245 ambient_thermal_soak: true,
1246 ..Default::default()
1247 }),
1248 );
1249 sd_soak
1250 .walk()
1251 .with_context(|| {
1252 format!(
1253 "\nsoak cycle:\nambient temperature: {}*C\ninit temperature: {}",
1254 te_amb.get::<si::degree_celsius>(),
1255 te_init.get::<si::degree_celsius>()
1256 )
1257 })
1258 .unwrap();
1259 assert!(
1260 *sd_soak.veh.state.i.get_fresh(String::new).unwrap()
1261 == sd_soak.cyc.len_checked().unwrap() - 1
1262 );
1263 sd_soak.reset_step(|| format_dbg!()).unwrap();
1264 sd_soak.veh.clear();
1265 sd_soak.reset_cumulative(|| format_dbg!()).unwrap();
1266
1267 dbg!("Running `sd_test`");
1269 let mut sd_test = SimDrive::new(sd_soak.veh.clone(), test_cyc, None);
1270 sd_test
1271 .walk()
1272 .with_context(|| {
1273 format!(
1274 "\ntest cycle:\nambient temperature: {}*C\ninit temperature: {}",
1275 te_amb.get::<si::degree_celsius>(),
1276 te_init.get::<si::degree_celsius>()
1277 )
1278 })
1279 .unwrap();
1280 assert!(
1281 *sd_test.veh.state.i.get_fresh(String::new).unwrap()
1282 == sd_test.cyc.len_checked().unwrap() - 1
1283 );
1284 sd_test.reset_step(|| format_dbg!()).unwrap();
1285 sd_test.veh.clear();
1286 sd_test.reset_cumulative(|| format_dbg!()).unwrap();
1287 }
1288 }
1289
1290 #[test]
1291 #[cfg(feature = "resources")]
1292 fn test_sim_drive_bev_thrml_soak() {
1294 let _veh = Vehicle::from_resource("2020 Chevrolet Bolt EV thrml.yaml", false).unwrap();
1295 let mut cyc = Cycle::from_resource("udds.csv", false).unwrap();
1296 let mut soak_cyc_no_temp = cyc.clone();
1298 soak_cyc_no_temp
1299 .speed
1300 .iter_mut()
1301 .for_each(|v| *v = si::Velocity::ZERO);
1302
1303 let te_amb: Vec<si::Temperature> = [-6.7, -6.7, 38.0]
1304 .iter()
1305 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1306 .collect();
1307 let te_batt_and_cab_init: Vec<si::Temperature> = [-6.7, 22.0, 45.0]
1308 .iter()
1309 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1310 .collect();
1311
1312 for (te_amb, te_init) in te_amb.iter().zip(te_batt_and_cab_init) {
1314 let prep_cyc = cyc
1315 .with_temp_amb_air(vec![*te_amb; cyc.len_checked().unwrap()])
1316 .unwrap();
1317 let soak_cyc = soak_cyc_no_temp
1318 .with_temp_amb_air(vec![*te_amb; cyc.len_checked().unwrap()])
1319 .unwrap();
1320 let test_cyc = cyc
1321 .with_temp_amb_air(vec![*te_amb; cyc.len_checked().unwrap()])
1322 .unwrap();
1323 let mut veh = _veh.clone();
1324
1325 veh.res_mut()
1326 .unwrap()
1327 .res_thrml_state_mut()
1328 .unwrap()
1329 .temperature
1330 .mark_stale();
1331 veh.res_mut()
1332 .unwrap()
1333 .res_thrml_state_mut()
1334 .unwrap()
1335 .temperature
1336 .update(te_init, || format_dbg!())
1337 .unwrap();
1338
1339 veh.res_mut()
1340 .unwrap()
1341 .res_thrml_state_mut()
1342 .unwrap()
1343 .temp_prev
1344 .mark_stale();
1345 veh.res_mut()
1346 .unwrap()
1347 .res_thrml_state_mut()
1348 .unwrap()
1349 .temp_prev
1350 .update(te_init, || format_dbg!())
1351 .unwrap();
1352
1353 if let CabinOption::LumpedCabin(lc) = &mut veh.cabin {
1355 lc.state.temperature.mark_stale();
1356 lc.state
1357 .temperature
1358 .update(te_init, || format_dbg!())
1359 .unwrap();
1360 lc.state.temp_prev.mark_stale();
1361 lc.state
1362 .temp_prev
1363 .update(te_init, || format_dbg!())
1364 .unwrap();
1365 }
1366
1367 dbg!("Running `sd_prep`");
1369 let mut sd_prep = SimDrive::new(veh, prep_cyc, None);
1370 sd_prep
1371 .walk()
1372 .with_context(|| {
1373 format!(
1374 "\nprep cycle:\nambient temperature: {}*C\ninit temperature: {}",
1375 te_amb.get::<si::degree_celsius>(),
1376 te_init.get::<si::degree_celsius>()
1377 )
1378 })
1379 .unwrap();
1380 assert!(
1381 *sd_prep.veh.state.i.get_fresh(String::new).unwrap()
1382 == sd_prep.cyc.len_checked().unwrap() - 1
1383 );
1384 sd_prep.reset_step(|| format_dbg!()).unwrap();
1385 sd_prep.veh.clear();
1386 sd_prep.reset_cumulative(|| format_dbg!()).unwrap();
1387
1388 dbg!("Running `sd_soak`");
1390 let mut sd_soak = SimDrive::new(
1391 sd_prep.veh.clone(),
1392 soak_cyc,
1393 Some(SimParams {
1394 ambient_thermal_soak: true,
1395 ..Default::default()
1396 }),
1397 );
1398 sd_soak
1399 .walk()
1400 .with_context(|| {
1401 format!(
1402 "\nsoak cycle:\nambient temperature: {}*C\ninit temperature: {}",
1403 te_amb.get::<si::degree_celsius>(),
1404 te_init.get::<si::degree_celsius>()
1405 )
1406 })
1407 .unwrap();
1408 assert!(
1409 *sd_soak.veh.state.i.get_fresh(String::new).unwrap()
1410 == sd_soak.cyc.len_checked().unwrap() - 1
1411 );
1412 sd_soak.reset_step(|| format_dbg!()).unwrap();
1413 sd_soak.veh.clear();
1414 sd_soak.reset_cumulative(|| format_dbg!()).unwrap();
1415
1416 dbg!("Running `sd_test`");
1418 let mut sd_test = SimDrive::new(sd_soak.veh.clone(), test_cyc, None);
1419 sd_test
1420 .walk()
1421 .with_context(|| {
1422 format!(
1423 "\ntest cycle:\nambient temperature: {}*C\ninit temperature: {}",
1424 te_amb.get::<si::degree_celsius>(),
1425 te_init.get::<si::degree_celsius>()
1426 )
1427 })
1428 .unwrap();
1429 assert!(
1430 *sd_test.veh.state.i.get_fresh(String::new).unwrap()
1431 == sd_test.cyc.len_checked().unwrap() - 1
1432 );
1433 sd_test.reset_step(|| format_dbg!()).unwrap();
1434 sd_test.veh.clear();
1435 sd_test.reset_cumulative(|| format_dbg!()).unwrap();
1436 }
1437 }
1438
1439 #[test]
1440 #[cfg(feature = "resources")]
1441 fn test_sim_drive_bev() {
1442 let _veh = mock_bev();
1443 let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
1444 let mut sd = SimDrive {
1445 veh: _veh,
1446 cyc: _cyc,
1447 sim_params: Default::default(),
1448 };
1449 sd.walk().unwrap();
1450 assert!(
1451 *sd.veh.state.i.get_fresh(String::new).unwrap() == sd.cyc.len_checked().unwrap() - 1
1452 );
1453 assert!(sd.veh.fc().is_none());
1454 assert!(
1455 *sd.veh
1456 .res()
1457 .unwrap()
1458 .state
1459 .energy_out_chemical
1460 .get_fresh(String::new)
1461 .unwrap()
1462 != si::Energy::ZERO
1463 );
1464 }
1465
1466 #[test]
1467 #[cfg(feature = "resources")]
1468 fn test_sim_drive_bev_thrml() {
1469 let _veh = Vehicle::from_resource("2020 Chevrolet Bolt EV thrml.yaml", false).unwrap();
1470 let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
1471
1472 let te_amb_and_cab_and_batt_init_deg_c: Vec<(f64, f64)> = vec![
1473 (-6.7, -6.7),
1474 (5.0, 18.0),
1475 (22.0, 22.0),
1476 (25.0, 35.0),
1477 (45.0, 45.0),
1478 ];
1479 let te_amb: Vec<si::Temperature> = te_amb_and_cab_and_batt_init_deg_c
1480 .iter()
1481 .map(|t| (t.0 + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1482 .collect();
1483 let te_batt_and_cab_init: Vec<si::Temperature> = te_amb_and_cab_and_batt_init_deg_c
1484 .iter()
1485 .map(|t| (t.1 + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1486 .collect();
1487 for (te_amb, te_init) in te_amb.iter().zip(te_batt_and_cab_init) {
1488 let mut veh = _veh.clone();
1489 veh.res_mut()
1490 .unwrap()
1491 .res_thrml_state_mut()
1492 .unwrap()
1493 .temperature
1494 .mark_stale();
1495 veh.res_mut()
1496 .unwrap()
1497 .res_thrml_state_mut()
1498 .unwrap()
1499 .temperature
1500 .update(te_init, || format_dbg!())
1501 .unwrap();
1502
1503 veh.res_mut()
1504 .unwrap()
1505 .res_thrml_state_mut()
1506 .unwrap()
1507 .temp_prev
1508 .mark_stale();
1509 veh.res_mut()
1510 .unwrap()
1511 .res_thrml_state_mut()
1512 .unwrap()
1513 .temp_prev
1514 .update(te_init, || format_dbg!())
1515 .unwrap();
1516
1517 if let CabinOption::LumpedCabin(lc) = &mut veh.cabin {
1518 lc.state.temperature.mark_stale();
1519 lc.state
1520 .temperature
1521 .update(te_init, || format_dbg!())
1522 .unwrap();
1523
1524 lc.state.temp_prev.mark_stale();
1525 lc.state
1526 .temp_prev
1527 .update(te_init, || format_dbg!())
1528 .unwrap();
1529 } else {
1530 panic!("cabin should have been configured");
1531 }
1532 let mut cyc = _cyc.clone();
1533 cyc.temp_amb_air = vec![*te_amb; cyc.len_checked().unwrap()];
1534 let mut sd = SimDrive::new(veh, cyc, Default::default());
1535 if let CabinOption::LumpedCabin(lc) = sd.veh.cabin.clone() {
1536 assert_eq!(
1537 *lc.state.temperature.get_fresh(|| format_dbg!()).unwrap(),
1538 te_init
1539 );
1540 } else {
1541 panic!();
1542 };
1543 sd.walk()
1544 .with_context(|| {
1545 format!(
1546 "ambient temperature: {}*C\ninit temperature: {}",
1547 te_amb.get::<si::degree_celsius>(),
1548 te_init.get::<si::degree_celsius>()
1549 )
1550 })
1551 .unwrap();
1552 assert!(
1553 *sd.veh.state.i.get_fresh(String::new).unwrap()
1554 == sd.cyc.len_checked().unwrap() - 1
1555 );
1556 assert!(sd.veh.fc().is_none());
1557 assert!(
1558 *sd.veh
1559 .res()
1560 .unwrap()
1561 .state
1562 .energy_out_chemical
1563 .get_fresh(String::new)
1564 .unwrap()
1565 != si::Energy::ZERO
1566 );
1567 sd.veh.reset_step(|| format_dbg!()).unwrap();
1568 sd.veh.state.time.mark_stale();
1569 sd.veh
1570 .state
1571 .time
1572 .update(si::Time::ZERO, || format_dbg!())
1573 .unwrap();
1574 assert!(*sd.veh.state.i.get_fresh(|| format_dbg!()).unwrap() == 0);
1575 sd.walk()
1576 .with_context(|| {
1577 format!(
1578 "ambient temperature: {}*C\ninit temperature: {}",
1579 te_amb.get::<si::degree_celsius>(),
1580 te_init.get::<si::degree_celsius>()
1581 )
1582 })
1583 .unwrap();
1584 sd.reset_cumulative(|| format_dbg!()).unwrap();
1585 assert_eq!(*sd.veh.state.i.get_fresh(|| format_dbg!()).unwrap(), 1369);
1586 }
1587 }
1588}