Skip to main content

fastsim_core/simdrive/
mod.rs

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