1pub mod params;
2pub mod roadload;
3
4pub use params::{SimParams, TraceMissOptions, TraceMissTolerance};
5use roadload::StepInfo;
6
7use super::drive_cycle::Cycle;
8use super::vehicle::Vehicle;
9use crate::drive_cycle::manipulation_utils::calc_best_rendezvous;
10use crate::imports::*;
11use crate::prelude::*;
12use crate::vehicle::conv::DfcoControls;
13
14#[serde_api]
15#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, StateMethods)]
16#[non_exhaustive]
17#[serde(deny_unknown_fields)]
18#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
19pub struct SimDrive {
20 #[has_state]
21 pub veh: Vehicle,
22 pub cyc: Cycle,
23 pub sim_params: SimParams,
24}
25
26#[pyo3_api]
27impl SimDrive {
28 #[new]
29 #[pyo3(signature = (veh, cyc, sim_params=None))]
30 fn __new__(veh: Vehicle, cyc: Cycle, sim_params: Option<SimParams>) -> anyhow::Result<Self> {
31 Ok(SimDrive::new(veh, cyc, sim_params))
32 }
33
34 #[pyo3(name = "run_once")]
36 fn run_once_py(&mut self) -> anyhow::Result<()> {
37 self.run_once()
38 }
39
40 #[pyo3(name = "run")]
44 fn run_py(&mut self) -> anyhow::Result<()> {
45 self.run()
46 }
47
48 #[pyo3(name = "walk_once")]
50 fn walk_once_py(&mut self, py: Python<'_>) -> anyhow::Result<()> {
51 Self::warn_deprecated_walk(py, "walk_once", "run_once");
52 self.run_once()
53 }
54
55 #[pyo3(name = "walk")]
57 fn walk_py(&mut self, py: Python<'_>) -> anyhow::Result<()> {
58 Self::warn_deprecated_walk(py, "walk", "run");
59 self.run()
60 }
61
62 #[pyo3(name = "reset")]
63 fn reset_py(&mut self) -> anyhow::Result<()> {
65 self.reset_cumulative(|| format_dbg!())?;
66 self.reset_step(|| format_dbg!())?;
67 self.clear();
68 Ok(())
69 }
70
71 #[pyo3(name = "clear")]
72 fn clear_py(&mut self) {
73 self.clear()
74 }
75
76 #[pyo3(name = "reset_step")]
77 fn reset_step_py(&mut self) -> anyhow::Result<()> {
78 self.reset_step(|| format_dbg!())
79 }
80
81 #[pyo3(name = "reset_cumulative")]
82 fn reset_cumulative_py(&mut self) -> anyhow::Result<()> {
83 self.reset_cumulative(|| format_dbg!())
84 }
85}
86
87impl SerdeAPI for SimDrive {}
88impl Init for SimDrive {
89 fn init(&mut self) -> Result<(), Error> {
90 self.veh
91 .init()
92 .map_err(|err| Error::InitError(format_dbg!(err)))?;
93 self.cyc
94 .init()
95 .map_err(|err| Error::InitError(format_dbg!(err)))?;
96 self.sim_params
97 .init()
98 .map_err(|err| Error::InitError(format_dbg!(err)))?;
99 Ok(())
100 }
101}
102
103impl SimDrive {
104 pub fn new(veh: Vehicle, cyc: Cycle, sim_params: Option<SimParams>) -> Self {
105 Self {
106 veh,
107 cyc,
108 sim_params: sim_params.unwrap_or_default(),
109 }
110 }
111
112 #[cfg(feature = "pyo3")]
114 fn warn_deprecated_walk(py: Python<'_>, old_name: &str, new_name: &str) {
115 if let Ok(warnings) = py.import("warnings") {
116 let msg =
117 format!("SimDrive.{old_name} is deprecated; use SimDrive.{new_name} instead.",);
118 let kwargs = PyDict::new(py);
119 let _ = kwargs.set_item("stacklevel", 2);
120 let _ = kwargs.set_item("category", py.get_type::<PyDeprecationWarning>());
121 let _ = warnings.call_method("warn", (msg,), Some(&kwargs));
122 }
123 }
124
125 pub fn run(&mut self) -> anyhow::Result<()> {
148 match self.veh.pt_type {
149 PowertrainType::HybridElectricVehicle(_) => {
150 let veh_init = self.veh.clone();
153 let res_mut = self.veh.res_mut().with_context(|| format_dbg!())?;
154 res_mut.state.soc.mark_stale();
155 res_mut
156 .state
157 .soc
158 .update(0.5 * (res_mut.min_soc + res_mut.max_soc), || format_dbg!())?;
159 loop {
160 self.veh
161 .hev_mut()
162 .with_context(|| format_dbg!())?
163 .soc_bal_iters
164 .mark_stale();
165 self.veh
166 .hev_mut()
167 .with_context(|| format_dbg!())?
168 .soc_bal_iters
169 .increment(1, || format_dbg!())?;
170 self.run_once().map_err(|err| {
171 anyhow::anyhow!(format!(
172 "HEV run_once failed at line {}\ntime step: {}\n with originating error: [{}]",
173 format_dbg!(),
174 self.veh.state.i,
175 err
176 ))
177 })?;
178 let soc_final = self.veh.res().with_context(|| format_dbg!())?.state.soc;
180 let soc_bal_iters =
182 self.veh.hev().with_context(|| format_dbg!())?.soc_bal_iters;
183 let res_per_fuel = *self
184 .veh
185 .res()
186 .with_context(|| format_dbg!())?
187 .state
188 .energy_out_chemical
189 .get_fresh(|| format_dbg!())?
190 / *self
191 .veh
192 .fc()
193 .with_context(|| format_dbg!())?
194 .state
195 .energy_fuel
196 .get_fresh(|| format_dbg!())?;
197 if self
198 .veh
199 .hev()
200 .with_context(|| format_dbg!())?
201 .soc_bal_iters
202 .get_fresh(|| format_dbg!())?
203 > &self
204 .veh
205 .hev()
206 .with_context(|| format_dbg!())?
207 .sim_params
208 .soc_balance_iter_err
209 {
210 bail!(
211 "{}\nSOC balancing surpassed sim_params.soc_balance_iter_err = {} iterations",
212 format_dbg!(),
213 self.veh.hev().with_context(|| format_dbg!())?.sim_params.soc_balance_iter_err,
214 );
215 }
216 if res_per_fuel.abs()
217 < self
218 .veh
219 .hev()
220 .with_context(|| format_dbg!())?
221 .sim_params
222 .res_per_fuel_lim
223 || !self
224 .veh
225 .hev()
226 .with_context(|| format_dbg!())?
227 .sim_params
228 .balance_soc
229 || self.sim_params.ambient_thermal_soak
230 {
231 break;
232 } else {
233 if let Some(&mut ref mut hev) = self.veh.hev_mut() {
235 if hev.sim_params.save_soc_bal_iters {
236 hev.soc_bal_iter_history.push(hev.clone());
237 hev.soc_bal_iters.mark_stale();
238 }
239 }
240 self.veh = veh_init.clone();
242 self.veh.res_mut().with_context(|| format_dbg!())?.state.soc = soc_final;
244 self.veh
246 .hev_mut()
247 .with_context(|| format_dbg!())?
248 .soc_bal_iters = soc_bal_iters;
249 }
250 }
251 }
252 PowertrainType::PlugInHybridElectricVehicle(_) => {
253 let res_mut = self.veh.res_mut().with_context(|| format_dbg!())?;
254 res_mut.state.soc.mark_stale();
255 res_mut
256 .state
257 .soc
258 .update(res_mut.max_soc, || format_dbg!())?;
259 self.run_once()?
260 }
261 PowertrainType::BatteryElectricVehicle(_) => {
262 let res_mut = self.veh.res_mut().with_context(|| format_dbg!())?;
263 res_mut.state.soc.mark_stale();
264 res_mut
265 .state
266 .soc
267 .update(res_mut.max_soc, || format_dbg!())?;
268 self.run_once()?
269 }
270 PowertrainType::ConventionalVehicle(_) => self.run_once()?,
271 }
272 Ok(())
273 }
274
275 #[deprecated(since = "3.1.0", note = "Use SimDrive::run instead")]
276 pub fn walk(&mut self) -> anyhow::Result<()> {
277 self.run()
278 }
279
280 pub fn run_once(&mut self) -> anyhow::Result<()> {
282 let len = &self.cyc.len_checked().with_context(|| format_dbg!())?;
283 ensure!(len >= &2, format_dbg!(len < &2));
284 self.save_state(|| format_dbg!())?;
285
286 self.veh.state.mass.mark_stale();
287 self.veh.state.mass.update(
288 self.veh
289 .mass()
290 .with_context(|| format_dbg!())?
291 .with_context(|| format_dbg!("Expected mass to have been set."))?,
292 || format_dbg!(),
293 )?;
294
295 let hvac: Option<HVACOption> = if self.sim_params.ambient_thermal_soak {
296 ensure!(
297 self.cyc.speed.iter().all(|s| *s == si::Velocity::ZERO),
298 format!(
299 "{}\nDuring thermal soak, cycle speed should always be zero",
300 format_dbg!()
301 )
302 );
303 if !self.veh.hvac.is_none() {
304 let hvac_some = Some(self.veh.hvac.clone());
306 self.veh.hvac = HVACOption::None;
307 hvac_some
308 } else {
309 None
310 }
311 } else {
312 None
313 };
314
315 loop {
316 self.check_and_reset(|| format_dbg!())?;
317 self.veh.state.mass.mark_fresh(|| format_dbg!())?;
318 if let Some(res) = self.veh.res_mut() {
319 res.state.soh.mark_fresh(|| format_dbg!())?;
320 }
321 self.step(|| format_dbg!())?;
322 self.solve_step().map_err(|err| {
323 anyhow::anyhow!(format!(
324 "solver step failed at line {}\ntime step: {}\n with originating error: [{}]",
325 format_dbg!(),
326 self.veh.state.i,
327 err
328 ))
329 })?;
330 self.save_state(|| format_dbg!())?;
331 if *self.veh.state.i.get_fresh(|| format_dbg!())? == len - 1 {
332 break;
333 }
334 }
335
336 if let Some(hvac) = hvac {
337 self.veh.hvac = hvac;
339 }
340
341 Ok(())
342 }
343
344 #[deprecated(since = "3.1.0", note = "Use SimDrive::run_once instead")]
345 pub fn walk_once(&mut self) -> anyhow::Result<()> {
346 self.run_once()
347 }
348
349 pub fn calc_dvdd(&self, speed_m_per_s: f64, grade: f64) -> anyhow::Result<f64> {
355 let v = speed_m_per_s;
356 if v <= 0.0 {
357 Ok(0.0)
358 } else {
359 let (atan_grade_sin, atan_grade_cos) = if grade == 0.0 {
360 (0.0, 1.0)
361 } else {
362 let atan_grade = grade.atan();
363 (atan_grade.sin(), atan_grade.cos())
364 };
365 let g = uc::ACC_GRAV.get::<si::meter_per_second_squared>();
366 let m = self
367 .veh
368 .mass
369 .with_context(|| {
370 format!(
371 "{}\nVehicle mass should have been set already.",
372 format_dbg!()
373 )
374 })?
375 .get::<si::kilogram>();
376 let rho_cdfa = self
377 .veh
378 .state
379 .air_density
380 .get_stale(|| format_dbg!())?
381 .get::<si::kilogram_per_cubic_meter>()
382 * self.veh.chassis.drag_coef.get::<si::ratio>()
383 * self.veh.chassis.frontal_area.get::<si::square_meter>();
384 let rrc = self.veh.chassis.wheel_rr_coef.get::<si::ratio>();
385 Ok(-((g / v) * (atan_grade_sin + rrc * atan_grade_cos)
386 + (0.5 * rho_cdfa * (1.0 / m) * v)))
387 }
388 }
389
390 pub fn solve_step(&mut self) -> anyhow::Result<()> {
392 let i = *self.veh.state.i.get_fresh(|| format_dbg!())?;
393 let time_prev = *self.veh.state.time.get_stale(|| format_dbg!())?;
394 ensure!(self.cyc.time.len() > i);
395 self.veh
396 .state
397 .time
398 .update(
399 *self.cyc.time.get(i).ok_or({
400 anyhow::anyhow!(format!(
401 "failed to get time for index {} at line {}",
402 i,
403 format_dbg!()
404 ))
405 })?,
406 || format_dbg!(),
407 )
408 .map_err(|err| {
409 anyhow::anyhow!(format!(
410 "updating time failed at line {}\ntime step: {}\n with originating error [{}]",
411 format_dbg!(),
412 self.veh.state.i,
413 err
414 ))
415 })?;
416 let dt = *self.veh.state.time.get_fresh(|| format_dbg!())? - time_prev;
417 self.veh
429 .solve_thermal(self.cyc.temp_amb_air[i], dt)
430 .with_context(|| format!("{}\n`self.veh.state.i`: {}", format_dbg!(), i))?;
431 match self.sim_params.ambient_thermal_soak {
432 false => {
433 self.veh
434 .set_curr_pwr_out_max(dt)
435 .with_context(|| anyhow!(format_dbg!()))?;
436 self.set_pwr_prop_for_speed(
437 self.cyc.speed[i],
438 *self.veh.state.speed_ach.get_stale(|| format_dbg!())?,
439 dt,
440 )
441 .with_context(|| anyhow!(format_dbg!()))?;
442 self.veh.state.pwr_tractive_for_cyc.update(
443 *self.veh.state.pwr_tractive.get_fresh(|| format_dbg!())?,
444 || format_dbg!(),
445 )?;
446 self.set_ach_speed(self.cyc.speed[i], self.cyc.dist[i], dt).map_err(|err| anyhow::anyhow!(format!(
447 "set_ach_speed failed at line {} with cycle speed {:?}, cyc dist {:?}, and dt {:?} and originating error {}",
448 format_dbg!(),
449 self.cyc.speed[i],
450 self.cyc.dist[i],
451 dt,
452 err
453 )))?;
454
455 match &mut self.veh.pt_type {
457 PowertrainType::HybridElectricVehicle(hev) => hev
458 .pt_cntrl
459 .handle_fc_on_causes_for_speed(self.cyc.speed[i])?,
460 PowertrainType::PlugInHybridElectricVehicle(hev) => hev
461 .pt_cntrl
462 .handle_fc_on_causes_for_speed(self.cyc.speed[i])?,
463 PowertrainType::ConventionalVehicle(conv) => {
464 conv.dfco_cntrl.state.vehicle_dynamics_prevent_dfco.update(
465 DfcoControls::is_dfco_disabled_due_to_veh_dynamics(
466 self.cyc.speed[i - 1],
467 self.cyc.speed[i],
468 dt,
469 conv.dfco_cntrl.dfco_enabled,
470 conv.dfco_cntrl.minimum_dfco_speed,
471 conv.dfco_cntrl.minimum_dfco_deceleration,
472 conv.dfco_cntrl.stopped_speed_threshold,
473 ),
474 || format_dbg!(),
475 )?;
476 conv.pt_cntrl
477 .handle_fc_on_causes_for_speed(self.cyc.speed[i])?
478 }
479 _ => (),
480 }
481 self.veh.solve_powertrain(dt).map_err(|err| {
482 anyhow::anyhow!(format!(
483 "solve_powertrain failed at line {} with originating error [{}]",
484 format_dbg!(),
485 err
486 ))
487 })?;
488 }
489 true => {
490 self.veh.mark_non_thermal_fresh()?;
491 }
492 }
493 self.set_cumulative(dt, || format_dbg!())?;
494 Ok(())
495 }
496
497 pub fn set_pwr_prop_for_speed(
502 &mut self,
503 speed: si::Velocity,
504 speed_prev: si::Velocity,
505 dt: si::Time,
506 ) -> anyhow::Result<()> {
507 let i = *self.veh.state.i.get_fresh(|| format_dbg!())?;
508 let vs = &mut self.veh.state;
509 let interp_pt_dist: &[f64] = match self.cyc.grade_interp {
514 Some(InterpolatorEnum::Interp0D(_)) => &[],
515 Some(InterpolatorEnum::Interp1D(_)) => {
516 &[vs.dist.get_fresh(|| format_dbg!())?.get::<si::meter>()]
517 }
518 _ => unreachable!(),
519 };
520 vs.grade_curr.update(
521 if *vs.cyc_met_overall.get_stale(|| format_dbg!())? {
522 *self
523 .cyc
524 .grade
525 .get(i)
526 .with_context(|| format_dbg!(self.cyc.grade.len()))?
527 } else {
528 uc::R
529 * self
530 .cyc
531 .grade_interp
532 .as_ref()
533 .with_context(|| format_dbg!("You might have somehow bypassed `init()`"))?
534 .interpolate(interp_pt_dist)
535 .with_context(|| format_dbg!())?
536 },
537 || format_dbg!(),
538 )?;
539 vs.elev_curr.update(
540 if *vs.cyc_met_overall.get_stale(|| format_dbg!())? {
541 *self.cyc.elev.get(i).with_context(|| format_dbg!())?
542 } else {
543 uc::M
544 * self
545 .cyc
546 .elev_interp
547 .as_ref()
548 .with_context(|| format_dbg!("You might have somehow bypassed `init()`"))?
549 .interpolate(interp_pt_dist)
550 .with_context(|| format_dbg!())?
551 },
552 || format_dbg!(),
553 )?;
554
555 vs.air_density.update(
556 if self.sim_params.f2_const_air_density {
557 1.2 * uc::KGPM3
558 } else {
559 let te_amb_air = {
560 let te_amb_air = self
561 .cyc
562 .temp_amb_air
563 .get(i)
564 .with_context(|| format_dbg!())?;
565 if *te_amb_air == *TE_STD_AIR {
566 None
567 } else {
568 Some(te_amb_air)
569 }
570 };
571 Air::get_density(
572 te_amb_air.copied(),
573 Some(*vs.elev_curr.get_fresh(|| format_dbg!())?),
574 )
575 },
576 || format_dbg!(),
577 )?;
578
579 let mass = self.veh.mass.with_context(|| {
580 format!(
581 "{}\nVehicle mass should have been set already.",
582 format_dbg!()
583 )
584 })?;
585 vs.pwr_accel.update(
586 mass / (2.0 * dt) * (speed.powi(P2::new()) - speed_prev.powi(P2::new())),
587 || format_dbg!(),
588 )?;
589 vs.pwr_ascent.update(
590 uc::ACC_GRAV
591 * *vs.grade_curr.get_fresh(|| format_dbg!())?
592 * mass
593 * (speed_prev + speed)
594 / 2.0,
595 || format_dbg!(),
596 )?;
597 vs.pwr_drag.update(
598 0.5
599 * Air::get_density(None, None)
601 * self.veh.chassis.drag_coef
602 * self.veh.chassis.frontal_area
603 * ((speed + speed_prev) / 2.0).powi(P3::new()),
604 || format_dbg!(),
605 )?;
606 vs.pwr_rr.update(
607 mass * uc::ACC_GRAV
608 * self.veh.chassis.wheel_rr_coef
609 * vs.grade_curr.get_fresh(|| format_dbg!())?.atan().cos()
610 * (speed_prev + speed)
611 / 2.,
612 || format_dbg!(),
613 )?;
614 vs.pwr_whl_inertia.update(
615 0.5 * self.veh.chassis.wheel_inertia
616 * self.veh.chassis.num_wheels as f64
617 * ((speed
618 / self
619 .veh
620 .chassis
621 .wheel_radius
622 .with_context(|| format_dbg!())?)
623 .powi(P2::new())
624 - (speed_prev
625 / self
626 .veh
627 .chassis
628 .wheel_radius
629 .with_context(|| format_dbg!())?)
630 .powi(P2::new()))
631 / self.cyc.dt_at_i(i).with_context(|| format_dbg!())?,
632 || format_dbg!(),
633 )?;
634
635 vs.pwr_tractive.update(
636 *vs.pwr_rr.get_fresh(|| format_dbg!())?
637 + *vs.pwr_whl_inertia.get_fresh(|| format_dbg!())?
638 + *vs.pwr_accel.get_fresh(|| format_dbg!())?
639 + *vs.pwr_ascent.get_fresh(|| format_dbg!())?
640 + *vs.pwr_drag.get_fresh(|| format_dbg!())?,
641 || format_dbg!(),
642 )?;
643 Ok(())
644 }
645
646 pub fn set_ach_speed(
651 &mut self,
652 cyc_speed: si::Velocity,
653 cyc_dist: si::Length,
654 dt: si::Time,
655 ) -> anyhow::Result<()> {
656 self.veh.state.cyc_met.update(
657 self.veh.state.pwr_tractive.get_fresh(|| format_dbg!())?
658 <= self
659 .veh
660 .state
661 .pwr_prop_fwd_max
662 .get_fresh(|| format_dbg!())?
663 && cyc_speed
664 <= *self
665 .veh
666 .state
667 .speed_trac_fwd_max
668 .get_fresh(|| format_dbg!())?,
669 || format_dbg!(),
670 )?;
671 self.veh.state.cyc_met_overall.update(
672 if !*self.veh.state.cyc_met.get_fresh(|| format_dbg!())? {
673 false
676 } else {
677 *self
678 .veh
679 .state
680 .cyc_met_overall
681 .clone()
682 .get_stale(|| format_dbg!())?
683 },
684 || format_dbg!(),
685 )?;
686 let speed_prev = *self.veh.state.speed_ach.get_stale(|| format_dbg!())?;
687 if *self.veh.state.cyc_met.get_fresh(|| format_dbg!())? {
688 self.veh
689 .state
690 .speed_ach
691 .update(cyc_speed, || format_dbg!())?;
692 return Ok(());
693 }
694 let step_info = StepInfo {
695 dt,
696 speed_prev,
697 cyc_speed,
698 grade_curr: *self.veh.state.grade_curr.get_fresh(|| format_dbg!())?,
699 air_density: *self.veh.state.air_density.get_fresh(|| format_dbg!())?,
700 mass: self.veh.mass.with_context(|| {
701 format!("{}\nMass should have been set before now", format_dbg!())
702 })?,
703 drag_coef: self.veh.chassis.drag_coef,
704 frontal_area: self.veh.chassis.frontal_area,
705 wheel_inertia: self.veh.chassis.wheel_inertia,
706 num_wheels: self.veh.chassis.num_wheels,
707 wheel_radius: self
708 .veh
709 .chassis
710 .wheel_radius
711 .with_context(|| format_dbg!())?,
712 wheel_rr_coef: self.veh.chassis.wheel_rr_coef,
713 pwr_prop_fwd_max: *self
714 .veh
715 .state
716 .pwr_prop_fwd_max
717 .get_fresh(|| format_dbg!())?,
718 };
719 let speed_ach = step_info.solve_for_speed(
720 self.sim_params.ach_speed_max_iter * 10,
721 self.sim_params.ach_speed_tol,
722 self.sim_params.ach_speed_solver_gain,
723 );
724 let speed_trac_max = *self
729 .veh
730 .state
731 .speed_trac_fwd_max
732 .get_fresh(|| format_dbg!())?;
733 let speed_ach = speed_ach.min(speed_trac_max);
734 let speed_ach_floored = {
735 if speed_ach == speed_prev {
741 (speed_ach - 1.0e-6 * uc::MPS).max(si::Velocity::ZERO)
742 } else {
743 speed_ach
744 }
745 };
746
747 self.veh
748 .state
749 .speed_ach
750 .update(speed_ach_floored, || format_dbg!())?;
751 self.veh.state.air_density.mark_stale();
757 self.veh.state.cyc_met.mark_stale();
758 self.veh.state.cyc_met_overall.mark_stale();
759 self.veh.state.elev_curr.mark_stale();
760 self.veh.state.grade_curr.mark_stale();
761 self.veh.state.pwr_accel.mark_stale();
762 self.veh.state.pwr_ascent.mark_stale();
763 self.veh.state.pwr_drag.mark_stale();
764 self.veh.state.pwr_rr.mark_stale();
765 self.veh.state.pwr_tractive.mark_stale();
766 self.veh.state.pwr_whl_inertia.mark_stale();
767 self.veh.state.speed_ach.mark_stale();
768
769 self.set_pwr_prop_for_speed(speed_ach_floored, speed_prev, dt)
771 .with_context(|| format_dbg!())?;
772 self.set_ach_speed(speed_ach, cyc_dist, dt)
773 .with_context(|| anyhow!(format_dbg!()))?;
774
775 match self.sim_params.trace_miss_opts {
776 TraceMissOptions::Allow => {
777 }
779 TraceMissOptions::AllowChecked => {
780 let ach_speed = *self.veh.state.speed_ach.get_fresh(|| format_dbg!())?;
782 let ach_dist = *self.veh.state.dist.get_stale(|| format_dbg!())? + ach_speed * dt; self.sim_params
784 .trace_miss_tol
785 .check_trace_miss(cyc_speed, ach_speed, cyc_dist, ach_dist)
786 .map_err(|e| {
787 anyhow!(
788 concat!(
789 "\n{}\n",
790 "{}\n",
791 "failed to meet speed trace\n",
792 " prescribed speed: {} mph\n",
793 " achieved speed: {} mph\n",
794 " pwr_tractive_for_cyc: {} kW\n",
795 " pwr_tractive: {} kW\n",
796 " pwr_prop_fwd_max: {} kW\n",
797 " pwr deficit: {} kW\n",
798 ),
799 format_dbg!(),
800 e,
801 cyc_speed.get::<si::mile_per_hour>(),
802 ach_speed.get::<si::mile_per_hour>(),
803 self.veh
804 .state
805 .pwr_tractive_for_cyc
806 .get_fresh(|| format_dbg!())
807 .unwrap()
808 .get::<si::kilowatt>(),
809 self.veh
810 .state
811 .pwr_tractive
812 .get_fresh(|| format_dbg!())
813 .unwrap()
814 .get::<si::kilowatt>(),
815 self.veh
816 .state
817 .pwr_prop_fwd_max
818 .get_fresh(|| format_dbg!())
819 .unwrap()
820 .get::<si::kilowatt>(),
821 (*self
822 .veh
823 .state
824 .pwr_tractive
825 .get_fresh(|| format_dbg!())
826 .unwrap()
827 - *self
828 .veh
829 .state
830 .pwr_prop_fwd_max
831 .get_fresh(|| format_dbg!())
832 .unwrap())
833 .get::<si::kilowatt>()
834 .format_eng(None),
835 )
836 })?;
837 }
838 TraceMissOptions::Error => {
839 let ach_speed = *self.veh.state.speed_ach.get_fresh(|| format_dbg!())?;
840 bail!(
841 concat!(
842 "{}\nfailed to meet speed trace\n",
843 " prescribed speed: {} mph\n",
844 " achieved speed: {} mph\n",
845 " pwr_tractive_for_cyc: {} kW\n",
846 " pwr_tractive: {} kW\n",
847 " pwr_prop_fwd_max: {} kW\n",
848 " pwr deficit: {} kW\n",
849 ),
850 format_dbg!(),
851 cyc_speed.get::<si::mile_per_hour>(),
852 ach_speed.get::<si::mile_per_hour>(),
853 self.veh
854 .state
855 .pwr_tractive_for_cyc
856 .get_fresh(|| format_dbg!())?
857 .get::<si::kilowatt>(),
858 self.veh
859 .state
860 .pwr_tractive
861 .get_fresh(|| format_dbg!())?
862 .get::<si::kilowatt>(),
863 self.veh
864 .state
865 .pwr_prop_fwd_max
866 .get_fresh(|| format_dbg!())?
867 .get::<si::kilowatt>(),
868 (*self.veh.state.pwr_tractive.get_fresh(|| format_dbg!())?
869 - *self
870 .veh
871 .state
872 .pwr_prop_fwd_max
873 .get_fresh(|| format_dbg!())?)
874 .get::<si::kilowatt>()
875 .format_eng(None)
876 )
877 }
878 TraceMissOptions::Correct => {
879 let i = *self.veh.state.i.get_fresh(|| format_dbg!())?;
882 let max_steps = self.sim_params.trace_miss_correct_max_steps.max(2) as usize;
883 let correction = calc_best_rendezvous(i, max_steps, &self.cyc, speed_ach_floored);
884 if correction.steps >= 2 {
885 self.cyc.speed[i] = speed_ach_floored;
891 self.cyc.modify_by_const_jerk_trajectory(
892 i + 1,
893 correction.steps,
894 correction.jerk_m_per_s3 * uc::MPS3,
895 correction.acceleration_m_per_s2 * uc::MPS2,
896 );
897 self.cyc.dist.clear();
898 self.cyc.elev.clear();
899 self.cyc.init().unwrap();
900 }
901 }
902 }
903
904 Ok(())
905 }
906
907 pub fn clear(&mut self) {
908 self.veh.clear();
909 }
910}
911
912impl SetCumulative for SimDrive {
913 fn set_cumulative<F: Fn() -> String>(&mut self, dt: si::Time, loc: F) -> anyhow::Result<()> {
914 self.veh
915 .set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?;
916 Ok(())
917 }
918
919 fn reset_cumulative<F: Fn() -> String>(&mut self, loc: F) -> anyhow::Result<()> {
920 self.veh
921 .reset_cumulative(|| format!("{}\n{}", loc(), format_dbg!()))?;
922 Ok(())
923 }
924}
925
926#[cfg(test)]
927mod tests {
928 use super::*;
929 use crate::vehicle::vehicle_model::tests::*;
930
931 #[test]
932 #[cfg(feature = "resources")]
933 fn test_sim_drive_conv() {
934 let _veh = Vehicle::from_resource("2012_Ford_Fusion.yaml", false).unwrap();
935 let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
936 let mut sd = SimDrive::new(_veh, _cyc, Default::default());
937 sd.run().unwrap();
938 assert!(
939 *sd.veh.state.i.get_fresh(String::new).unwrap() == sd.cyc.len_checked().unwrap() - 1
940 );
941 assert!(
942 *sd.veh
943 .fc()
944 .unwrap()
945 .state
946 .energy_fuel
947 .get_fresh(String::new)
948 .unwrap()
949 > si::Energy::ZERO
950 );
951 assert!(sd.veh.res().is_none());
952 }
953
954 #[test]
955 #[cfg(feature = "resources")]
956 fn test_sim_drive_hev() {
957 let _veh = Vehicle::from_resource("2016_TOYOTA_Prius_Two.yaml", false).unwrap();
958 let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
959 let mut sd = SimDrive::new(_veh, _cyc, Default::default());
960 sd.run().unwrap();
961 assert!(
962 *sd.veh.state.i.get_fresh(String::new).unwrap() == sd.cyc.len_checked().unwrap() - 1
963 );
964 assert!(
965 *sd.veh
966 .fc()
967 .unwrap()
968 .state
969 .energy_fuel
970 .get_fresh(String::new)
971 .unwrap()
972 > si::Energy::ZERO
973 );
974 assert!(
975 *sd.veh
976 .res()
977 .unwrap()
978 .state
979 .energy_out_chemical
980 .get_fresh(String::new)
981 .unwrap()
982 != si::Energy::ZERO
983 );
984 }
985
986 #[test]
987 #[cfg(feature = "resources")]
988 fn test_sim_drive_hev_thrml() {
989 let _veh =
990 Vehicle::from_resource("2021_Hyundai_Sonata_Hybrid_Blue_thrml.yaml", false).unwrap();
991 let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
992
993 let te_amb_and_cab_and_batt_init_deg_c: Vec<(f64, f64)> = vec![
994 (-6.7, -6.7),
995 (5.0, 18.0),
996 (22.0, 22.0),
997 (25.0, 35.0),
998 (45.0, 45.0),
999 ];
1000 let te_amb: Vec<si::Temperature> = te_amb_and_cab_and_batt_init_deg_c
1001 .iter()
1002 .map(|t| (t.0 + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1003 .collect();
1004 let te_batt_and_cab_init: Vec<si::Temperature> = te_amb_and_cab_and_batt_init_deg_c
1005 .iter()
1006 .map(|t| (t.1 + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1007 .collect();
1008 let te_fc_init: Vec<si::Temperature> = [-6.7, 70.0, 90.0]
1009 .iter()
1010 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1011 .collect();
1012 for ((te_amb, te_init), te_fc_init) in
1013 te_amb.iter().zip(te_batt_and_cab_init).zip(te_fc_init)
1014 {
1015 let mut veh = _veh.clone();
1016
1017 veh.res_mut()
1018 .unwrap()
1019 .res_thrml_state_mut()
1020 .unwrap()
1021 .temperature
1022 .mark_stale();
1023 veh.res_mut()
1024 .unwrap()
1025 .res_thrml_state_mut()
1026 .unwrap()
1027 .temperature
1028 .update(te_init, || format_dbg!())
1029 .unwrap();
1030
1031 veh.res_mut()
1032 .unwrap()
1033 .res_thrml_state_mut()
1034 .unwrap()
1035 .temp_prev
1036 .mark_stale();
1037 veh.res_mut()
1038 .unwrap()
1039 .res_thrml_state_mut()
1040 .unwrap()
1041 .temp_prev
1042 .update(te_init, || format_dbg!())
1043 .unwrap();
1044 if let CabinOption::LumpedCabin(lc) = &mut veh.cabin {
1045 lc.state.temperature.mark_stale();
1046 lc.state
1047 .temperature
1048 .update(te_init, || format_dbg!())
1049 .unwrap();
1050 lc.state.temp_prev.mark_stale();
1051 lc.state
1052 .temp_prev
1053 .update(te_init, || format_dbg!())
1054 .unwrap();
1055 }
1056
1057 veh.fc_mut()
1058 .unwrap()
1059 .fc_thrml_state_mut()
1060 .unwrap()
1061 .temperature
1062 .mark_stale();
1063 veh.fc_mut()
1064 .unwrap()
1065 .fc_thrml_state_mut()
1066 .unwrap()
1067 .temperature
1068 .update(te_fc_init, || format_dbg!())
1069 .unwrap();
1070 let mut cyc = _cyc.clone();
1071 cyc.temp_amb_air = vec![*te_amb; cyc.len_checked().unwrap()];
1072 let mut sd = SimDrive::new(veh, cyc, Default::default());
1073 sd.run()
1074 .with_context(|| {
1075 format!(
1076 "ambient temperature: {}*C\ninit temperature: {}",
1077 te_amb.get::<si::degree_celsius>(),
1078 te_init.get::<si::degree_celsius>()
1079 )
1080 })
1081 .unwrap();
1082 assert!(
1083 *sd.veh.state.i.get_fresh(String::new).unwrap()
1084 == sd.cyc.len_checked().unwrap() - 1
1085 );
1086 assert!(
1087 *sd.veh
1088 .fc()
1089 .unwrap()
1090 .state
1091 .energy_fuel
1092 .get_fresh(String::new)
1093 .unwrap()
1094 > si::Energy::ZERO
1095 );
1096 assert!(
1097 *sd.veh
1098 .res()
1099 .unwrap()
1100 .state
1101 .energy_out_chemical
1102 .get_fresh(String::new)
1103 .unwrap()
1104 != si::Energy::ZERO
1105 );
1106 }
1107 }
1108
1109 #[test]
1110 #[cfg(feature = "resources")]
1111 fn test_sim_drive_hev_thrml_soak() {
1113 let _veh =
1114 Vehicle::from_resource("2021_Hyundai_Sonata_Hybrid_Blue_thrml.yaml", false).unwrap();
1115 let mut cyc = Cycle::from_resource("udds.csv", false).unwrap();
1116 let mut soak_cyc_no_temp = cyc.clone();
1118 soak_cyc_no_temp
1119 .speed
1120 .iter_mut()
1121 .for_each(|v| *v = si::Velocity::ZERO);
1122
1123 let te_amb: Vec<si::Temperature> = [-6.7, -6.7, 38.0]
1124 .iter()
1125 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1126 .collect();
1127 let te_batt_and_cab_init: Vec<si::Temperature> = [-6.7, 22.0, 45.0]
1128 .iter()
1129 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1130 .collect();
1131 let te_fc_init: Vec<si::Temperature> = [-6.7, 70.0, 90.0]
1132 .iter()
1133 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1134 .collect();
1135 for ((te_amb, te_init), te_fc_init) in
1136 te_amb.iter().zip(te_batt_and_cab_init).zip(te_fc_init)
1137 {
1138 let prep_cyc = cyc
1139 .with_temp_amb_air(vec![*te_amb; cyc.len_checked().unwrap()])
1140 .unwrap();
1141 let soak_cyc = soak_cyc_no_temp
1142 .with_temp_amb_air(vec![*te_amb; cyc.len_checked().unwrap()])
1143 .unwrap();
1144 let test_cyc = cyc
1145 .with_temp_amb_air(vec![*te_amb; cyc.len_checked().unwrap()])
1146 .unwrap();
1147
1148 let mut veh = _veh.clone();
1149
1150 veh.res_mut()
1151 .unwrap()
1152 .res_thrml_state_mut()
1153 .unwrap()
1154 .temperature
1155 .mark_stale();
1156 veh.res_mut()
1157 .unwrap()
1158 .res_thrml_state_mut()
1159 .unwrap()
1160 .temperature
1161 .update(te_init, || format_dbg!())
1162 .unwrap();
1163
1164 veh.res_mut()
1165 .unwrap()
1166 .res_thrml_state_mut()
1167 .unwrap()
1168 .temp_prev
1169 .mark_stale();
1170 veh.res_mut()
1171 .unwrap()
1172 .res_thrml_state_mut()
1173 .unwrap()
1174 .temp_prev
1175 .update(te_init, || format_dbg!())
1176 .unwrap();
1177 if let CabinOption::LumpedCabin(lc) = &mut veh.cabin {
1178 lc.state.temperature.mark_stale();
1179 lc.state
1180 .temperature
1181 .update(te_init, || format_dbg!())
1182 .unwrap();
1183 lc.state.temp_prev.mark_stale();
1184 lc.state
1185 .temp_prev
1186 .update(te_init, || format_dbg!())
1187 .unwrap();
1188 }
1189
1190 veh.fc_mut()
1191 .unwrap()
1192 .fc_thrml_state_mut()
1193 .unwrap()
1194 .temperature
1195 .mark_stale();
1196 veh.fc_mut()
1197 .unwrap()
1198 .fc_thrml_state_mut()
1199 .unwrap()
1200 .temperature
1201 .update(te_fc_init, || format_dbg!())
1202 .unwrap();
1203
1204 dbg!("Running `sd_prep`");
1206 let mut sd_prep = SimDrive::new(veh, prep_cyc, None);
1207 sd_prep
1208 .run()
1209 .with_context(|| {
1210 format!(
1211 "\nprep cycle:\nambient temperature: {}*C\ninit temperature: {}",
1212 te_amb.get::<si::degree_celsius>(),
1213 te_init.get::<si::degree_celsius>()
1214 )
1215 })
1216 .unwrap();
1217 assert!(
1218 *sd_prep.veh.state.i.get_fresh(String::new).unwrap()
1219 == sd_prep.cyc.len_checked().unwrap() - 1
1220 );
1221 sd_prep.reset_step(|| format_dbg!()).unwrap();
1222 sd_prep.veh.clear();
1223 sd_prep.reset_cumulative(|| format_dbg!()).unwrap();
1224
1225 dbg!("Running `sd_soak`");
1227 let mut sd_soak = SimDrive::new(
1228 sd_prep.veh.clone(),
1229 soak_cyc,
1230 Some(SimParams {
1231 ambient_thermal_soak: true,
1232 ..Default::default()
1233 }),
1234 );
1235 sd_soak
1236 .run()
1237 .with_context(|| {
1238 format!(
1239 "\nsoak cycle:\nambient temperature: {}*C\ninit temperature: {}",
1240 te_amb.get::<si::degree_celsius>(),
1241 te_init.get::<si::degree_celsius>()
1242 )
1243 })
1244 .unwrap();
1245 assert!(
1246 *sd_soak.veh.state.i.get_fresh(String::new).unwrap()
1247 == sd_soak.cyc.len_checked().unwrap() - 1
1248 );
1249 sd_soak.reset_step(|| format_dbg!()).unwrap();
1250 sd_soak.veh.clear();
1251 sd_soak.reset_cumulative(|| format_dbg!()).unwrap();
1252
1253 dbg!("Running `sd_test`");
1255 let mut sd_test = SimDrive::new(sd_soak.veh.clone(), test_cyc, None);
1256 sd_test
1257 .run()
1258 .with_context(|| {
1259 format!(
1260 "\ntest cycle:\nambient temperature: {}*C\ninit temperature: {}",
1261 te_amb.get::<si::degree_celsius>(),
1262 te_init.get::<si::degree_celsius>()
1263 )
1264 })
1265 .unwrap();
1266 assert!(
1267 *sd_test.veh.state.i.get_fresh(String::new).unwrap()
1268 == sd_test.cyc.len_checked().unwrap() - 1
1269 );
1270 sd_test.reset_step(|| format_dbg!()).unwrap();
1271 sd_test.veh.clear();
1272 sd_test.reset_cumulative(|| format_dbg!()).unwrap();
1273 }
1274 }
1275
1276 #[test]
1277 #[cfg(feature = "resources")]
1278 fn test_sim_drive_bev_thrml_soak() {
1280 let _veh = Vehicle::from_resource("2020 Chevrolet Bolt EV thrml.yaml", false).unwrap();
1281 let mut cyc = Cycle::from_resource("udds.csv", false).unwrap();
1282 let mut soak_cyc_no_temp = cyc.clone();
1284 soak_cyc_no_temp
1285 .speed
1286 .iter_mut()
1287 .for_each(|v| *v = si::Velocity::ZERO);
1288
1289 let te_amb: Vec<si::Temperature> = [-6.7, -6.7, 38.0]
1290 .iter()
1291 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1292 .collect();
1293 let te_batt_and_cab_init: Vec<si::Temperature> = [-6.7, 22.0, 45.0]
1294 .iter()
1295 .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1296 .collect();
1297
1298 for (te_amb, te_init) in te_amb.iter().zip(te_batt_and_cab_init) {
1300 let prep_cyc = cyc
1301 .with_temp_amb_air(vec![*te_amb; cyc.len_checked().unwrap()])
1302 .unwrap();
1303 let soak_cyc = soak_cyc_no_temp
1304 .with_temp_amb_air(vec![*te_amb; cyc.len_checked().unwrap()])
1305 .unwrap();
1306 let test_cyc = cyc
1307 .with_temp_amb_air(vec![*te_amb; cyc.len_checked().unwrap()])
1308 .unwrap();
1309 let mut veh = _veh.clone();
1310
1311 veh.res_mut()
1312 .unwrap()
1313 .res_thrml_state_mut()
1314 .unwrap()
1315 .temperature
1316 .mark_stale();
1317 veh.res_mut()
1318 .unwrap()
1319 .res_thrml_state_mut()
1320 .unwrap()
1321 .temperature
1322 .update(te_init, || format_dbg!())
1323 .unwrap();
1324
1325 veh.res_mut()
1326 .unwrap()
1327 .res_thrml_state_mut()
1328 .unwrap()
1329 .temp_prev
1330 .mark_stale();
1331 veh.res_mut()
1332 .unwrap()
1333 .res_thrml_state_mut()
1334 .unwrap()
1335 .temp_prev
1336 .update(te_init, || format_dbg!())
1337 .unwrap();
1338
1339 if let CabinOption::LumpedCabin(lc) = &mut veh.cabin {
1341 lc.state.temperature.mark_stale();
1342 lc.state
1343 .temperature
1344 .update(te_init, || format_dbg!())
1345 .unwrap();
1346 lc.state.temp_prev.mark_stale();
1347 lc.state
1348 .temp_prev
1349 .update(te_init, || format_dbg!())
1350 .unwrap();
1351 }
1352
1353 dbg!("Running `sd_prep`");
1355 let mut sd_prep = SimDrive::new(veh, prep_cyc, None);
1356 sd_prep
1357 .run()
1358 .with_context(|| {
1359 format!(
1360 "\nprep cycle:\nambient temperature: {}*C\ninit temperature: {}",
1361 te_amb.get::<si::degree_celsius>(),
1362 te_init.get::<si::degree_celsius>()
1363 )
1364 })
1365 .unwrap();
1366 assert!(
1367 *sd_prep.veh.state.i.get_fresh(String::new).unwrap()
1368 == sd_prep.cyc.len_checked().unwrap() - 1
1369 );
1370 sd_prep.reset_step(|| format_dbg!()).unwrap();
1371 sd_prep.veh.clear();
1372 sd_prep.reset_cumulative(|| format_dbg!()).unwrap();
1373
1374 dbg!("Running `sd_soak`");
1376 let mut sd_soak = SimDrive::new(
1377 sd_prep.veh.clone(),
1378 soak_cyc,
1379 Some(SimParams {
1380 ambient_thermal_soak: true,
1381 ..Default::default()
1382 }),
1383 );
1384 sd_soak
1385 .run()
1386 .with_context(|| {
1387 format!(
1388 "\nsoak cycle:\nambient temperature: {}*C\ninit temperature: {}",
1389 te_amb.get::<si::degree_celsius>(),
1390 te_init.get::<si::degree_celsius>()
1391 )
1392 })
1393 .unwrap();
1394 assert!(
1395 *sd_soak.veh.state.i.get_fresh(String::new).unwrap()
1396 == sd_soak.cyc.len_checked().unwrap() - 1
1397 );
1398 sd_soak.reset_step(|| format_dbg!()).unwrap();
1399 sd_soak.veh.clear();
1400 sd_soak.reset_cumulative(|| format_dbg!()).unwrap();
1401
1402 dbg!("Running `sd_test`");
1404 let mut sd_test = SimDrive::new(sd_soak.veh.clone(), test_cyc, None);
1405 sd_test
1406 .run()
1407 .with_context(|| {
1408 format!(
1409 "\ntest cycle:\nambient temperature: {}*C\ninit temperature: {}",
1410 te_amb.get::<si::degree_celsius>(),
1411 te_init.get::<si::degree_celsius>()
1412 )
1413 })
1414 .unwrap();
1415 assert!(
1416 *sd_test.veh.state.i.get_fresh(String::new).unwrap()
1417 == sd_test.cyc.len_checked().unwrap() - 1
1418 );
1419 sd_test.reset_step(|| format_dbg!()).unwrap();
1420 sd_test.veh.clear();
1421 sd_test.reset_cumulative(|| format_dbg!()).unwrap();
1422 }
1423 }
1424
1425 #[test]
1426 #[cfg(feature = "resources")]
1427 fn test_sim_drive_bev() {
1428 let _veh = Vehicle::from_resource("2022_Renault_Zoe_ZE50_R135.yaml", false).unwrap();
1429 let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
1430 let mut sd = SimDrive {
1431 veh: _veh,
1432 cyc: _cyc,
1433 sim_params: Default::default(),
1434 };
1435 sd.run().unwrap();
1436 assert!(
1437 *sd.veh.state.i.get_fresh(String::new).unwrap() == sd.cyc.len_checked().unwrap() - 1
1438 );
1439 assert!(sd.veh.fc().is_none());
1440 assert!(
1441 *sd.veh
1442 .res()
1443 .unwrap()
1444 .state
1445 .energy_out_chemical
1446 .get_fresh(String::new)
1447 .unwrap()
1448 != si::Energy::ZERO
1449 );
1450 }
1451
1452 #[test]
1453 #[cfg(feature = "resources")]
1454 fn test_sim_drive_bev_thrml() {
1455 let _veh = Vehicle::from_resource("2020 Chevrolet Bolt EV thrml.yaml", false).unwrap();
1456 let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
1457
1458 let te_amb_and_cab_and_batt_init_deg_c: Vec<(f64, f64)> = vec![
1459 (-6.7, -6.7),
1460 (5.0, 18.0),
1461 (22.0, 22.0),
1462 (25.0, 35.0),
1463 (45.0, 45.0),
1464 ];
1465 let te_amb: Vec<si::Temperature> = te_amb_and_cab_and_batt_init_deg_c
1466 .iter()
1467 .map(|t| (t.0 + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1468 .collect();
1469 let te_batt_and_cab_init: Vec<si::Temperature> = te_amb_and_cab_and_batt_init_deg_c
1470 .iter()
1471 .map(|t| (t.1 + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1472 .collect();
1473 for (te_amb, te_init) in te_amb.iter().zip(te_batt_and_cab_init) {
1474 let mut veh = _veh.clone();
1475 veh.res_mut()
1476 .unwrap()
1477 .res_thrml_state_mut()
1478 .unwrap()
1479 .temperature
1480 .mark_stale();
1481 veh.res_mut()
1482 .unwrap()
1483 .res_thrml_state_mut()
1484 .unwrap()
1485 .temperature
1486 .update(te_init, || format_dbg!())
1487 .unwrap();
1488
1489 veh.res_mut()
1490 .unwrap()
1491 .res_thrml_state_mut()
1492 .unwrap()
1493 .temp_prev
1494 .mark_stale();
1495 veh.res_mut()
1496 .unwrap()
1497 .res_thrml_state_mut()
1498 .unwrap()
1499 .temp_prev
1500 .update(te_init, || format_dbg!())
1501 .unwrap();
1502
1503 if let CabinOption::LumpedCabin(lc) = &mut veh.cabin {
1504 lc.state.temperature.mark_stale();
1505 lc.state
1506 .temperature
1507 .update(te_init, || format_dbg!())
1508 .unwrap();
1509
1510 lc.state.temp_prev.mark_stale();
1511 lc.state
1512 .temp_prev
1513 .update(te_init, || format_dbg!())
1514 .unwrap();
1515 } else {
1516 panic!("cabin should have been configured");
1517 }
1518 let mut cyc = _cyc.clone();
1519 cyc.temp_amb_air = vec![*te_amb; cyc.len_checked().unwrap()];
1520 let mut sd = SimDrive::new(veh, cyc, Default::default());
1521 if let CabinOption::LumpedCabin(lc) = sd.veh.cabin.clone() {
1522 assert_eq!(
1523 *lc.state.temperature.get_fresh(|| format_dbg!()).unwrap(),
1524 te_init
1525 );
1526 } else {
1527 panic!();
1528 };
1529 sd.run()
1530 .with_context(|| {
1531 format!(
1532 "ambient temperature: {}*C\ninit temperature: {}",
1533 te_amb.get::<si::degree_celsius>(),
1534 te_init.get::<si::degree_celsius>()
1535 )
1536 })
1537 .unwrap();
1538 assert!(
1539 *sd.veh.state.i.get_fresh(String::new).unwrap()
1540 == sd.cyc.len_checked().unwrap() - 1
1541 );
1542 assert!(sd.veh.fc().is_none());
1543 assert!(
1544 *sd.veh
1545 .res()
1546 .unwrap()
1547 .state
1548 .energy_out_chemical
1549 .get_fresh(String::new)
1550 .unwrap()
1551 != si::Energy::ZERO
1552 );
1553 sd.veh.reset_step(|| format_dbg!()).unwrap();
1554 sd.veh.state.time.mark_stale();
1555 sd.veh
1556 .state
1557 .time
1558 .update(si::Time::ZERO, || format_dbg!())
1559 .unwrap();
1560 assert!(*sd.veh.state.i.get_fresh(|| format_dbg!()).unwrap() == 0);
1561 sd.run()
1562 .with_context(|| {
1563 format!(
1564 "ambient temperature: {}*C\ninit temperature: {}",
1565 te_amb.get::<si::degree_celsius>(),
1566 te_init.get::<si::degree_celsius>()
1567 )
1568 })
1569 .unwrap();
1570 sd.reset_cumulative(|| format_dbg!()).unwrap();
1571 assert_eq!(*sd.veh.state.i.get_fresh(|| format_dbg!()).unwrap(), 1369);
1572 }
1573 }
1574}