fastsim_core/
simdrive.rs

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))]
16/// Solver parameters
17pub struct SimParams {
18    #[serde(default = "SimParams::def_ach_speed_max_iter")]
19    /// max number of iterations allowed in setting achieved speed when trace
20    /// cannot be achieved
21    pub ach_speed_max_iter: u32,
22    #[serde(default = "SimParams::def_ach_speed_tol")]
23    /// tolerance in change in speed guess in setting achieved speed when trace
24    /// cannot be achieved
25    pub ach_speed_tol: si::Ratio,
26    #[serde(default = "SimParams::def_ach_speed_solver_gain")]
27    /// Newton method gain for setting achieved speed
28    pub ach_speed_solver_gain: f64,
29    // TODO: plumb this up to actually do something
30    /// When implemented, this will set the tolerance on how much trace miss
31    /// is allowed
32    #[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    /// the maximum number of steps in which to re-rendezvous with reference
38    /// trace after a trace miss. Note: this field only applies when
39    /// trace_miss_opts is set to TraceMissOptions::Correct. Note: must
40    /// be 2 or greater. Defaults to 6.
41    pub trace_miss_correct_max_steps: u32,
42    /// whether to use FASTSim-2 style air density
43    #[serde(default = "SimParams::def_f2_const_air_density")]
44    pub f2_const_air_density: bool,
45    /// if true, vehicle is totally inactive except for thermal models
46    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    /// Run vehicle simulation once
121    #[pyo3(name = "walk_once")]
122    fn walk_once_py(&mut self) -> anyhow::Result<()> {
123        self.walk_once()
124    }
125
126    /// Run vehicle simulation, and, if applicable, apply powertrain-specific
127    /// corrections (e.g. iterate `walk` until SOC balance is achieved -- i.e. initial
128    /// and final SOC are nearly identical)
129    #[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    /// Compines [Self::reset_cumulative], [Self::reset_step], [Self::clear]
141    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    // # TODO:
190    // ## Features
191    // - [ ] regen limiting curve during speeds approaching zero per f2 -- less urgent
192    // - [ ] ability to manipulate friction/regen brake split based on required braking
193    //       power -- new feature -- move this to enum
194    // - [x] make enum `EngineOnCause::{AlreadyOn, TooCold,
195    //       PowerDemand}` and save it in a vec or some such for when there are
196    //       multiple causes -- new feature
197
198    /// Run vehicle simulation, and, if applicable, apply powertrain-specific
199    /// corrections:
200    /// - for HEV, set initial SOC to mean of min and max SOC, and then iterate
201    ///   `walk` until SOC balance is achieved -- i.e. initial and final SOC are
202    ///   nearly identical
203    /// - for PHEV, set initial SOC to max SOC, and then simulate once
204    /// - for BEV, set initial SOC to max SOC, and then simulate once
205    /// - for Conv, simulate once
206    ///
207    /// # Important Considerations
208    /// If you need to run a [ReversibleEnergyStorage]-equipped vehicle for
209    /// only one iteration without modifying the initial SOC, then run the
210    /// [Self::walk_once] method directly
211    pub fn walk(&mut self) -> anyhow::Result<()> {
212        match self.veh.pt_type {
213            PowertrainType::HybridElectricVehicle(_) => {
214                // Net battery energy used per amount of fuel used
215                // clone initial vehicle to preserve starting state (TODO: figure out if this is a huge CPU burden)
216                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                        // prep for another iteration
303                        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                        // reset vehicle to initial state
310                        self.veh = veh_init.clone();
311                        // start SOC at previous final value
312                        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    /// Run vehicle simulation once
340    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                // turn off HVAC if vehicle is not active
364                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            // reset original hvac
391            self.veh.hvac = hvac;
392        }
393
394        Ok(())
395    }
396
397    /// Calculates the derivative dv/dd (change in speed by change in distance)
398    /// - speed_m_per_s: the speed at which to evaluate dv/dd (m/s)
399    /// - grade: the road grade as a decimal fraction
400    ///
401    /// RETURN: number, the dv/dd for these conditions
402    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    /// Solves current time step
439    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        // maybe make controls like:
448        // ```
449        // pub enum HVACAuxPriority {
450        //     /// Prioritize [ReversibleEnergyStorage] thermal management
451        //     ReversibleEnergyStorage
452        //     /// Prioritize [Cabin] and [ReversibleEnergyStorage] proportionally to their requests
453        //     Proportional
454        // }
455        // ```
456
457        // `solve_thermal` must happen before the other methods because it impacts aux power demand
458        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    /// Sets power required for given prescribed speed
499    /// # Arguments
500    /// - `speed`: prescribed or achieved speed
501    /// - `dt`: simulation time step size
502    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        // TODO: get @mokeefe to give this a serious look and think about grade alignment issues that may arise
511        // TODO: memo-ize this
512        //     - if we get back on trace or nearly back on trace, revert to just using the index
513        //     - we can also shorten the x and y values by removing stuff that's already happened
514        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            // TODO: feed in elevation
601            * 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    /// Sets achieved speed based on known current max power
648    /// # Arguments
649    /// - `cyc_speed`: prescribed speed
650    /// - `dt`: simulation time step size
651    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                // if current power demand is not met, then this becomes false for
661                // the rest of the cycle and should not be manipulated anywhere else
662                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                    // do nothing because `set_ach_speed` should be allowed to proceed to handle this
677                }
678                TraceMissOptions::AllowChecked => {
679                    // this will be handled later
680                }
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                    // We will correct the deviation from trace by modifying the cycle to re-rendezvous with a later time/distance.
715                    // In so doing, we will use a less agressive roadload.
716                    // NOTE: actual correction occurs later but we need to calculate
717                    // the achieved speed first.
718                }
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            // NOTE: what we are doing here is "flooring" the speed to the nearest tenth of a m/s.
750            // The purpose is to slightly reduce the target speed below the max power threshold
751            // to prevent float precision issues from sending us right back into trace miss.
752            let v = ((speed_ach.get::<si::meter_per_second>() * 10.0).floor() / 10.0) * uc::MPS;
753            // NOTE: if after "flooring" we happen to exactly be the same as
754            // previous, we subtract off a tenth of a m/s but prevent going below 0 m/s.
755            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        // NOTE: need to reset tracked state to allow
764        // for calling set_pwr_prop_for_speed(.) again this step.
765        // set_pwr_prop_for_speed has already been called so the
766        // following variables have already been set fresh but need
767        // to be re-iterated.
768        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        // Rerun again to ensure we have updated achieved speed and state
782        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                // NOTE: in theory, grade could be slightly
793                // off with this deviation from trace. However, since we
794                // rendezvous in a small number of time steps, it should be
795                // close. The call again to init() should correct distance
796                // and elevation calculations.
797                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]
847// NOTE: consider embedding this in TraceMissOptions::AllowChecked
848pub struct TraceMissTolerance {
849    /// if the vehicle falls this far behind trace in terms of absolute
850    /// difference and [TraceMissOptions::is_allow_checked], fail
851    tol_dist: si::Length,
852    /// if the vehicle falls this far behind trace in terms of fractional
853    /// difference and [TraceMissOptions::is_allow_checked], fail
854    tol_dist_frac: si::Ratio,
855    /// if the vehicle falls this far behind instantaneous speed and
856    /// [TraceMissOptions::is_allow_checked], fail
857    tol_speed: si::Velocity,
858    /// if the vehicle falls this far behind instantaneous speed in terms of
859    /// fractional difference and [TraceMissOptions::is_allow_checked], fail
860    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 condition to prevent divide-by-zero errors
879        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 condition to prevent checking early in cycle
896        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 trace miss without any fanfare
927    Allow,
928    /// Allow trace miss within error tolerance
929    AllowChecked,
930    #[default]
931    /// Error out when trace miss happens
932    Error,
933    /// Correct trace miss with driver model that catches up
934    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    /// Simulate prep cycle, soak cycle, and test cycle with thermal effects
1126    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        // zero out speed in soak cyc
1131        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            // simulate prep cycle
1219            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            // simulate soak cycle
1240            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            // simulate test cycle
1268            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    /// Simulate prep cycle, soak cycle, and test cycle with thermal effects
1293    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        // zero out speed in soak cyc
1297        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        // sweep ambient and initial conditions
1313        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            // setup initial conditions
1354            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            // simulate prep cycle
1368            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            // simulate soak cycle
1389            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            // simulate test cycle
1417            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}