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