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
140impl SerdeAPI for SimDrive {}
141impl Init for SimDrive {
142    fn init(&mut self) -> Result<(), Error> {
143        self.veh
144            .init()
145            .map_err(|err| Error::InitError(format_dbg!(err)))?;
146        self.cyc
147            .init()
148            .map_err(|err| Error::InitError(format_dbg!(err)))?;
149        self.sim_params
150            .init()
151            .map_err(|err| Error::InitError(format_dbg!(err)))?;
152        Ok(())
153    }
154}
155
156impl SimDrive {
157    pub fn new(veh: Vehicle, cyc: Cycle, sim_params: Option<SimParams>) -> Self {
158        Self {
159            veh,
160            cyc,
161            sim_params: sim_params.unwrap_or_default(),
162        }
163    }
164
165    // # TODO:
166    // ## Features
167    // - [ ] regen limiting curve during speeds approaching zero per f2 -- less urgent
168    // - [ ] ability to manipulate friction/regen brake split based on required braking
169    //       power -- new feature -- move this to enum
170    // - [x] make enum `EngineOnCause::{AlreadyOn, TooCold,
171    //       PowerDemand}` and save it in a vec or some such for when there are
172    //       multiple causes -- new feature
173
174    /// Run vehicle simulation, and, if applicable, apply powertrain-specific
175    /// corrections (e.g. iterate `walk` until SOC balance is achieved -- i.e. initial
176    /// and final SOC are nearly identical)
177    pub fn walk(&mut self) -> anyhow::Result<()> {
178        match self.veh.pt_type {
179            PowertrainType::HybridElectricVehicle(_) => {
180                // Net battery energy used per amount of fuel used
181                // clone initial vehicle to preserve starting state (TODO: figure out if this is a huge CPU burden)
182                let veh_init = self.veh.clone();
183                loop {
184                    self.veh
185                        .hev_mut()
186                        .with_context(|| format_dbg!())?
187                        .soc_bal_iters
188                        .mark_stale();
189                    self.veh
190                        .hev_mut()
191                        .with_context(|| format_dbg!())?
192                        .soc_bal_iters
193                        .increment(1, || format_dbg!())?;
194                    self.walk_once().with_context(|| format_dbg!())?;
195                    let soc_final = self
196                        .veh
197                        .res()
198                        .with_context(|| format_dbg!())?
199                        .state
200                        .soc
201                        .clone();
202                    let res_per_fuel = *self
203                        .veh
204                        .res()
205                        .with_context(|| format_dbg!())?
206                        .state
207                        .energy_out_chemical
208                        .get_fresh(|| format_dbg!())?
209                        / *self
210                            .veh
211                            .fc()
212                            .with_context(|| format_dbg!())?
213                            .state
214                            .energy_fuel
215                            .get_fresh(|| format_dbg!())?;
216                    if self
217                        .veh
218                        .hev()
219                        .with_context(|| format_dbg!())?
220                        .soc_bal_iters
221                        .get_fresh(|| format_dbg!())?
222                        > &self
223                            .veh
224                            .hev()
225                            .with_context(|| format_dbg!())?
226                            .sim_params
227                            .soc_balance_iter_err
228                    {
229                        bail!(
230                            "{}",
231                            format_dbg!((
232                                self.veh
233                                    .hev()
234                                    .with_context(|| format_dbg!())?
235                                    .soc_bal_iters
236                                    .clone(),
237                                self.veh
238                                    .hev()
239                                    .with_context(|| format_dbg!())?
240                                    .sim_params
241                                    .soc_balance_iter_err
242                            ))
243                        );
244                    }
245                    if res_per_fuel.abs()
246                        < self
247                            .veh
248                            .hev()
249                            .with_context(|| format_dbg!())?
250                            .sim_params
251                            .res_per_fuel_lim
252                        || !self
253                            .veh
254                            .hev()
255                            .with_context(|| format_dbg!())?
256                            .sim_params
257                            .balance_soc
258                        || self.sim_params.ambient_thermal_soak
259                    {
260                        break;
261                    } else {
262                        // prep for another iteration
263                        if let Some(&mut ref mut hev) = self.veh.hev_mut() {
264                            if hev.sim_params.save_soc_bal_iters {
265                                hev.soc_bal_iter_history.push(hev.clone());
266                                hev.soc_bal_iters.mark_stale();
267                            }
268                        }
269                        // reset vehicle to initial state
270                        self.veh = veh_init.clone();
271                        // start SOC at previous final value
272                        self.veh.res_mut().with_context(|| format_dbg!())?.state.soc = soc_final;
273                    }
274                }
275            }
276            _ => self.walk_once()?,
277        }
278        Ok(())
279    }
280
281    /// Run vehicle simulation once
282    pub fn walk_once(&mut self) -> anyhow::Result<()> {
283        let len = &self.cyc.len_checked().with_context(|| format_dbg!())?;
284        ensure!(len >= &2, format_dbg!(len < &2));
285        self.save_state(|| format_dbg!())?;
286
287        self.veh.state.mass.mark_stale();
288        self.veh.state.mass.update(
289            self.veh
290                .mass()
291                .with_context(|| format_dbg!())?
292                .with_context(|| format_dbg!("Expected mass to have been set."))?,
293            || format_dbg!(),
294        )?;
295
296        loop {
297            self.check_and_reset(|| format_dbg!())?;
298            self.veh.state.mass.mark_fresh(|| format_dbg!())?;
299            if let Some(res) = self.veh.res_mut() {
300                res.state.soh.mark_fresh(|| format_dbg!())?;
301            }
302            self.step(|| format_dbg!())?;
303            self.solve_step()
304                .with_context(|| format!("{}\ntime step: {:?}", format_dbg!(), self.veh.state.i))?;
305            self.save_state(|| format_dbg!())?;
306            if *self.veh.state.i.get_fresh(|| format_dbg!())? == len - 1 {
307                break;
308            }
309        }
310        Ok(())
311    }
312
313    /// Calculates the derivative dv/dd (change in speed by change in distance)
314    /// - speed_m_per_s: the speed at which to evaluate dv/dd (m/s)
315    /// - grade: the road grade as a decimal fraction
316    ///
317    /// RETURN: number, the dv/dd for these conditions
318    pub fn calc_dvdd(&self, speed_m_per_s: f64, grade: f64) -> anyhow::Result<f64> {
319        let v = speed_m_per_s;
320        if v <= 0.0 {
321            Ok(0.0)
322        } else {
323            let (atan_grade_sin, atan_grade_cos) = if grade == 0.0 {
324                (0.0, 1.0)
325            } else {
326                let atan_grade = grade.atan();
327                (atan_grade.sin(), atan_grade.cos())
328            };
329            let g = uc::ACC_GRAV.get::<si::meter_per_second_squared>();
330            let m = self
331                .veh
332                .mass
333                .with_context(|| {
334                    format!(
335                        "{}\nVehicle mass should have been set already.",
336                        format_dbg!()
337                    )
338                })?
339                .get::<si::kilogram>();
340            let rho_cdfa = self
341                .veh
342                .state
343                .air_density
344                .get_stale(|| format_dbg!())?
345                .get::<si::kilogram_per_cubic_meter>()
346                * self.veh.chassis.drag_coef.get::<si::ratio>()
347                * self.veh.chassis.frontal_area.get::<si::square_meter>();
348            let rrc = self.veh.chassis.wheel_rr_coef.get::<si::ratio>();
349            Ok(-1.0
350                * ((g / v) * (atan_grade_sin + rrc * atan_grade_cos)
351                    + (0.5 * rho_cdfa * (1.0 / m) * v)))
352        }
353    }
354
355    /// Solves current time step
356    pub fn solve_step(&mut self) -> anyhow::Result<()> {
357        let i = *self.veh.state.i.get_fresh(|| format_dbg!())?;
358        let time_prev = *self.veh.state.time.get_stale(|| format_dbg!())?;
359        self.veh
360            .state
361            .time
362            .update(self.cyc.time[i], || format_dbg!())?;
363        let dt = *self.veh.state.time.get_fresh(|| format_dbg!())? - time_prev;
364        // maybe make controls like:
365        // ```
366        // pub enum HVACAuxPriority {
367        //     /// Prioritize [ReversibleEnergyStorage] thermal management
368        //     ReversibleEnergyStorage
369        //     /// Prioritize [Cabin] and [ReversibleEnergyStorage] proportionally to their requests
370        //     Proportional
371        // }
372        // ```
373
374        // `solve_thermal` must happen before the other methods because it impacts aux power demand
375        self.veh
376            .solve_thermal(self.cyc.temp_amb_air[i], dt)
377            .with_context(|| format_dbg!())?;
378        match self.sim_params.ambient_thermal_soak {
379            false => {
380                self.veh
381                    .set_curr_pwr_out_max(dt)
382                    .with_context(|| anyhow!(format_dbg!()))?;
383                self.set_pwr_prop_for_speed(
384                    self.cyc.speed[i],
385                    *self.veh.state.speed_ach.get_stale(|| format_dbg!())?,
386                    dt,
387                )
388                .with_context(|| anyhow!(format_dbg!()))?;
389                self.veh.state.pwr_tractive_for_cyc.update(
390                    *self.veh.state.pwr_tractive.get_fresh(|| format_dbg!())?,
391                    || format_dbg!(),
392                )?;
393                self.set_ach_speed(self.cyc.speed[i], dt)
394                    .with_context(|| anyhow!(format_dbg!()))?;
395                if self.sim_params.trace_miss_opts.is_allow_checked() {
396                    self.sim_params.trace_miss_tol.check_trace_miss(
397                        self.cyc.speed[i],
398                        *self.veh.state.speed_ach.get_fresh(|| format_dbg!())?,
399                        self.cyc.dist[i],
400                        *self.veh.state.dist.get_fresh(|| format_dbg!())?,
401                    )?;
402                }
403                self.veh
404                    .solve_powertrain(dt)
405                    .with_context(|| anyhow!(format_dbg!()))?;
406            }
407            true => {
408                self.veh.mark_non_thermal_fresh()?;
409            }
410        }
411        self.set_cumulative(dt, || format_dbg!())?;
412        Ok(())
413    }
414
415    /// Sets power required for given prescribed speed
416    /// # Arguments
417    /// - `speed`: prescribed or achieved speed
418    /// - `dt`: simulation time step size
419    pub fn set_pwr_prop_for_speed(
420        &mut self,
421        speed: si::Velocity,
422        speed_prev: si::Velocity,
423        dt: si::Time,
424    ) -> anyhow::Result<()> {
425        let i = *self.veh.state.i.get_fresh(|| format_dbg!())?;
426        let vs = &mut self.veh.state;
427        // TODO: get @mokeefe to give this a serious look and think about grade alignment issues that may arise
428        // TODO: memo-ize this
429        //     - if we get back on trace or nearly back on trace, revert to just using the index
430        //     - we can also shorten the x and y values by removing stuff that's already happened
431        let interp_pt_dist: &[f64] = match self.cyc.grade_interp {
432            Some(InterpolatorEnum::Interp0D(_)) => &[],
433            Some(InterpolatorEnum::Interp1D(_)) => {
434                &[vs.dist.get_fresh(|| format_dbg!())?.get::<si::meter>()]
435            }
436            _ => unreachable!(),
437        };
438        vs.grade_curr.update(
439            if *vs.cyc_met_overall.get_stale(|| format_dbg!())? {
440                *self
441                    .cyc
442                    .grade
443                    .get(i)
444                    .with_context(|| format_dbg!(self.cyc.grade.len()))?
445            } else {
446                uc::R
447                    * self
448                        .cyc
449                        .grade_interp
450                        .as_ref()
451                        .with_context(|| format_dbg!("You might have somehow bypassed `init()`"))?
452                        .interpolate(interp_pt_dist)
453                        .with_context(|| format_dbg!())?
454            },
455            || format_dbg!(),
456        )?;
457        vs.elev_curr.update(
458            if *vs.cyc_met_overall.get_stale(|| format_dbg!())? {
459                *self.cyc.elev.get(i).with_context(|| format_dbg!())?
460            } else {
461                uc::M
462                    * self
463                        .cyc
464                        .elev_interp
465                        .as_ref()
466                        .with_context(|| format_dbg!("You might have somehow bypassed `init()`"))?
467                        .interpolate(interp_pt_dist)
468                        .with_context(|| format_dbg!())?
469            },
470            || format_dbg!(),
471        )?;
472
473        vs.air_density.update(
474            if self.sim_params.f2_const_air_density {
475                1.2 * uc::KGPM3
476            } else {
477                let te_amb_air = {
478                    let te_amb_air = self
479                        .cyc
480                        .temp_amb_air
481                        .get(i)
482                        .with_context(|| format_dbg!())?;
483                    if *te_amb_air == *TE_STD_AIR {
484                        None
485                    } else {
486                        Some(te_amb_air)
487                    }
488                };
489                Air::get_density(
490                    te_amb_air.copied(),
491                    Some(*vs.elev_curr.get_fresh(|| format_dbg!())?),
492                )
493            },
494            || format_dbg!(),
495        )?;
496
497        let mass = self.veh.mass.with_context(|| {
498            format!(
499                "{}\nVehicle mass should have been set already.",
500                format_dbg!()
501            )
502        })?;
503        vs.pwr_accel.update(
504            mass / (2.0 * dt)
505                * (speed.powi(typenum::P2::new()) - speed_prev.powi(typenum::P2::new())),
506            || format_dbg!(),
507        )?;
508        vs.pwr_ascent.update(
509            uc::ACC_GRAV
510                * *vs.grade_curr.get_fresh(|| format_dbg!())?
511                * mass
512                * (speed_prev + speed)
513                / 2.0,
514            || format_dbg!(),
515        )?;
516        vs.pwr_drag.update(
517            0.5
518            // TODO: feed in elevation
519            * Air::get_density(None, None)
520            * self.veh.chassis.drag_coef
521            * self.veh.chassis.frontal_area
522            * ((speed + speed_prev) / 2.0).powi(typenum::P3::new()),
523            || format_dbg!(),
524        )?;
525        vs.pwr_rr.update(
526            mass * uc::ACC_GRAV
527                * self.veh.chassis.wheel_rr_coef
528                * vs.grade_curr.get_fresh(|| format_dbg!())?.atan().cos()
529                * (speed_prev + speed)
530                / 2.,
531            || format_dbg!(),
532        )?;
533        vs.pwr_whl_inertia.update(
534            0.5 * self.veh.chassis.wheel_inertia
535                * self.veh.chassis.num_wheels as f64
536                * ((speed
537                    / self
538                        .veh
539                        .chassis
540                        .wheel_radius
541                        .with_context(|| format_dbg!())?)
542                .powi(typenum::P2::new())
543                    - (speed_prev
544                        / self
545                            .veh
546                            .chassis
547                            .wheel_radius
548                            .with_context(|| format_dbg!())?)
549                    .powi(typenum::P2::new()))
550                / self.cyc.dt_at_i(i).with_context(|| format_dbg!())?,
551            || format_dbg!(),
552        )?;
553
554        vs.pwr_tractive.update(
555            *vs.pwr_rr.get_fresh(|| format_dbg!())?
556                + *vs.pwr_whl_inertia.get_fresh(|| format_dbg!())?
557                + *vs.pwr_accel.get_fresh(|| format_dbg!())?
558                + *vs.pwr_ascent.get_fresh(|| format_dbg!())?
559                + *vs.pwr_drag.get_fresh(|| format_dbg!())?,
560            || format_dbg!(),
561        )?;
562        Ok(())
563    }
564
565    /// Sets achieved speed based on known current max power
566    /// # Arguments
567    /// - `cyc_speed`: prescribed speed
568    /// - `dt`: simulation time step size
569    pub fn set_ach_speed(&mut self, cyc_speed: si::Velocity, dt: si::Time) -> anyhow::Result<()> {
570        let vs = &mut self.veh.state;
571        vs.cyc_met.update(
572            vs.pwr_tractive.get_fresh(|| format_dbg!())?
573                <= vs.pwr_prop_fwd_max.get_fresh(|| format_dbg!())?,
574            || format_dbg!(),
575        )?;
576        vs.cyc_met_overall.update(
577            if !*vs.cyc_met.get_fresh(|| format_dbg!())? {
578                // if current power demand is not met, then this becomes false for
579                // the rest of the cycle and should not be manipulated anywhere else
580                false
581            } else {
582                *vs.cyc_met_overall.get_stale(|| format_dbg!())?
583            },
584            || format_dbg!(),
585        )?;
586        let veh = &mut self.veh;
587        let speed_prev = *veh.state.speed_ach.get_stale(|| format_dbg!())?;
588        if *veh.state.cyc_met.get_fresh(|| format_dbg!())? {
589            veh.state.speed_ach.update(cyc_speed, || format_dbg!())?;
590            return Ok(());
591        } else {
592            match self.sim_params.trace_miss_opts {
593                TraceMissOptions::Allow => {
594                    // do nothing because `set_ach_speed` should be allowed to proceed to handle this
595                }
596                TraceMissOptions::AllowChecked => {
597                    // this will be handled later
598                }
599                TraceMissOptions::Error => bail!(
600                    "{}\nFailed to meet speed trace.
601prescribed speed: {} mph
602prev speed_ach: {} mph
603pwr_tractive_for_cyc: {} kW
604pwr_tractive: {} kW
605pwr_prop_fwd_max: {} kW,
606pwr deficit: {} kW
607",
608                    format_dbg!(),
609                    cyc_speed.get::<si::mile_per_hour>(),
610                    veh.state
611                        .speed_ach
612                        .get_stale(|| format_dbg!())?
613                        .get::<si::mile_per_hour>(),
614                    veh.state
615                        .pwr_tractive_for_cyc
616                        .get_fresh(|| format_dbg!())?
617                        .get::<si::kilowatt>(),
618                    veh.state
619                        .pwr_tractive
620                        .get_fresh(|| format_dbg!())?
621                        .get::<si::kilowatt>(),
622                    veh.state
623                        .pwr_prop_fwd_max
624                        .get_fresh(|| format_dbg!())?
625                        .get::<si::kilowatt>(),
626                    (*veh.state.pwr_tractive.get_fresh(|| format_dbg!())?
627                        - *veh.state.pwr_prop_fwd_max.get_fresh(|| format_dbg!())?)
628                    .get::<si::kilowatt>()
629                    .format_eng(None)
630                ),
631                TraceMissOptions::Correct => {
632                    // We will correct the deviation from trace by modifying the cycle to re-rendezvous with a later time/distance.
633                    // In so doing, we will use a less agressive roadload.
634                    // NOTE: actual correction occurs later but we need to calculate
635                    // the achieved speed first.
636                }
637            }
638        }
639        let vs = &mut self.veh.state;
640        let step_info = StepInfo {
641            dt,
642            speed_prev,
643            cyc_speed,
644            grade_curr: *vs.grade_curr.get_fresh(|| format_dbg!())?,
645            air_density: *vs.air_density.get_fresh(|| format_dbg!())?,
646            mass: self.veh.mass.with_context(|| {
647                format!("{}\nMass should have been set before now", format_dbg!())
648            })?,
649            drag_coef: self.veh.chassis.drag_coef,
650            frontal_area: self.veh.chassis.frontal_area,
651            wheel_inertia: self.veh.chassis.wheel_inertia,
652            num_wheels: self.veh.chassis.num_wheels,
653            wheel_radius: self
654                .veh
655                .chassis
656                .wheel_radius
657                .with_context(|| format_dbg!())?,
658            wheel_rr_coef: self.veh.chassis.wheel_rr_coef,
659            pwr_prop_fwd_max: *vs.pwr_prop_fwd_max.get_fresh(|| format_dbg!())?,
660        };
661        let speed_ach = step_info.solve_for_speed(
662            self.sim_params.ach_speed_max_iter * 10,
663            self.sim_params.ach_speed_tol,
664            self.sim_params.ach_speed_solver_gain,
665        );
666        let speed_ach_floored = {
667            // NOTE: what we are doing here is "flooring" the speed to the nearest tength of a m/s.
668            // The purpose is to slightly reduce the target speed below the max power threshold
669            // to prevent float precision issues from sending us right back into trace miss.
670            let v = ((speed_ach.get::<si::meter_per_second>() * 10.0).floor() / 10.0) * uc::MPS;
671            // NOTE: if after "flooring" we happen to exactly be the same as
672            // previous, we subtract off a tenth of a m/s but prevent going below 0 m/s.
673            if v == speed_ach {
674                (v - 0.1 * uc::MPS).max(si::Velocity::ZERO)
675            } else {
676                v
677            }
678        };
679
680        vs.speed_ach.update(speed_ach_floored, || format_dbg!())?;
681        // NOTE: need to reset tracked state to allow
682        // for calling set_pwr_prop_for_speed(.) again this step.
683        // set_pwr_prop_for_speed has already been called so the
684        // following variables have already been set fresh but need
685        // to be re-iterated.
686        vs.air_density.mark_stale();
687        vs.cyc_met.mark_stale();
688        vs.cyc_met_overall.mark_stale();
689        vs.elev_curr.mark_stale();
690        vs.grade_curr.mark_stale();
691        vs.pwr_accel.mark_stale();
692        vs.pwr_ascent.mark_stale();
693        vs.pwr_drag.mark_stale();
694        vs.pwr_rr.mark_stale();
695        vs.pwr_tractive.mark_stale();
696        vs.pwr_whl_inertia.mark_stale();
697        vs.speed_ach.mark_stale();
698
699        // Rerun again to ensure we have updated achieved speed and state
700        self.set_pwr_prop_for_speed(speed_ach_floored, speed_prev, dt)
701            .with_context(|| format_dbg!())?;
702        self.set_ach_speed(speed_ach, dt)
703            .with_context(|| anyhow!(format_dbg!()))?;
704
705        if self.sim_params.trace_miss_opts == TraceMissOptions::Correct {
706            let i = *self.veh.state.i.get_fresh(|| format_dbg!())?;
707            let max_steps = self.sim_params.trace_miss_correct_max_steps.max(2) as usize;
708            let correction = calc_best_rendezvous(i, max_steps, &self.cyc, speed_ach_floored);
709            if correction.steps >= 2 {
710                // NOTE: in theory, grade could be slightly
711                // off with this deviation from trace. However, since we
712                // rendezvous in a small number of time steps, it should be
713                // close. The call again to init() should correct distance
714                // and elevation calculations.
715                self.cyc.speed[i] = speed_ach_floored;
716                self.cyc.modify_by_const_jerk_trajectory(
717                    i + 1,
718                    correction.steps,
719                    correction.jerk_m_per_s3 * uc::MPS3,
720                    correction.acceleration_m_per_s2 * uc::MPS2,
721                );
722                self.cyc.dist.clear();
723                self.cyc.elev.clear();
724                self.cyc.init().unwrap();
725            }
726        }
727
728        Ok(())
729    }
730
731    pub fn to_fastsim2(&self) -> anyhow::Result<fastsim_2::simdrive::RustSimDrive> {
732        let veh2 = self
733            .veh
734            .to_fastsim2()
735            .with_context(|| anyhow!(format_dbg!()))?;
736        let cyc2 = self
737            .cyc
738            .to_fastsim2()
739            .with_context(|| anyhow!(format_dbg!()))?;
740        Ok(fastsim_2::simdrive::RustSimDrive::new(cyc2, veh2))
741    }
742}
743
744impl SetCumulative for SimDrive {
745    fn set_cumulative<F: Fn() -> String>(&mut self, dt: si::Time, loc: F) -> anyhow::Result<()> {
746        self.veh
747            .set_cumulative(dt, || format!("{}\n{}", loc(), format_dbg!()))?;
748        Ok(())
749    }
750}
751
752#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
753#[serde(deny_unknown_fields)]
754#[non_exhaustive]
755// NOTE: consider embedding this in TraceMissOptions::AllowChecked
756pub struct TraceMissTolerance {
757    /// if the vehicle falls this far behind trace in terms of absolute
758    /// difference and [TraceMissOptions::is_allow_checked], fail
759    tol_dist: si::Length,
760    /// if the vehicle falls this far behind trace in terms of fractional
761    /// difference and [TraceMissOptions::is_allow_checked], fail
762    tol_dist_frac: si::Ratio,
763    /// if the vehicle falls this far behind instantaneous speed and
764    /// [TraceMissOptions::is_allow_checked], fail
765    tol_speed: si::Velocity,
766    /// if the vehicle falls this far behind instantaneous speed in terms of
767    /// fractional difference and [TraceMissOptions::is_allow_checked], fail
768    tol_speed_frac: si::Ratio,
769}
770
771impl TraceMissTolerance {
772    fn check_trace_miss(
773        &self,
774        cyc_speed: si::Velocity,
775        ach_speed: si::Velocity,
776        cyc_dist: si::Length,
777        ach_dist: si::Length,
778    ) -> anyhow::Result<()> {
779        ensure!(
780            cyc_speed - ach_speed < self.tol_speed,
781            "{}\n{}\n{}",
782            format_dbg!(cyc_speed),
783            format_dbg!(ach_speed),
784            format_dbg!(self.tol_speed)
785        );
786        // if condition to prevent divide-by-zero errors
787        if cyc_speed > self.tol_speed {
788            ensure!(
789                (cyc_speed - ach_speed) / cyc_speed < self.tol_speed_frac,
790                "{}\n{}\n{}",
791                format_dbg!(cyc_speed),
792                format_dbg!(ach_speed),
793                format_dbg!(self.tol_speed_frac)
794            )
795        }
796        ensure!(
797            (cyc_dist - ach_dist) < self.tol_dist,
798            "{}\n{}\n{}",
799            format_dbg!(cyc_dist),
800            format_dbg!(ach_dist),
801            format_dbg!(self.tol_dist)
802        );
803        // if condition to prevent checking early in cycle
804        if cyc_dist > self.tol_dist * 5.0 {
805            ensure!(
806                (cyc_dist - ach_dist) / cyc_dist < self.tol_dist_frac,
807                "{}\n{}\n{}",
808                format_dbg!(cyc_dist),
809                format_dbg!(ach_dist),
810                format_dbg!(self.tol_dist_frac)
811            )
812        }
813
814        Ok(())
815    }
816}
817impl SerdeAPI for TraceMissTolerance {}
818impl Init for TraceMissTolerance {}
819impl Default for TraceMissTolerance {
820    fn default() -> Self {
821        Self {
822            tol_dist: 100. * uc::M,
823            tol_dist_frac: 0.05 * uc::R,
824            tol_speed: 10. * uc::MPS,
825            tol_speed_frac: 0.5 * uc::R,
826        }
827    }
828}
829
830#[derive(
831    Clone, Default, Debug, Deserialize, Serialize, PartialEq, IsVariant, derive_more::From, TryInto,
832)]
833pub enum TraceMissOptions {
834    /// Allow trace miss without any fanfare
835    Allow,
836    /// Allow trace miss within error tolerance
837    AllowChecked,
838    #[default]
839    /// Error out when trace miss happens
840    Error,
841    /// Correct trace miss with driver model that catches up
842    Correct,
843}
844
845impl SerdeAPI for TraceMissOptions {}
846impl Init for TraceMissOptions {}
847
848#[cfg(test)]
849mod tests {
850    use super::*;
851    use crate::vehicle::vehicle_model::tests::*;
852
853    #[test]
854    #[cfg(feature = "resources")]
855    fn test_sim_drive_conv() {
856        let _veh = mock_conv_veh();
857        let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
858        let mut sd = SimDrive::new(_veh, _cyc, Default::default());
859        sd.walk().unwrap();
860        assert!(
861            *sd.veh.state.i.get_fresh(String::new).unwrap() == sd.cyc.len_checked().unwrap() - 1
862        );
863        assert!(
864            *sd.veh
865                .fc()
866                .unwrap()
867                .state
868                .energy_fuel
869                .get_fresh(String::new)
870                .unwrap()
871                > si::Energy::ZERO
872        );
873        assert!(sd.veh.res().is_none());
874    }
875
876    #[test]
877    #[cfg(feature = "resources")]
878    fn test_sim_drive_hev() {
879        let _veh = mock_hev();
880        let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
881        let mut sd = SimDrive::new(_veh, _cyc, Default::default());
882        sd.walk().unwrap();
883        assert!(
884            *sd.veh.state.i.get_fresh(String::new).unwrap() == sd.cyc.len_checked().unwrap() - 1
885        );
886        assert!(
887            *sd.veh
888                .fc()
889                .unwrap()
890                .state
891                .energy_fuel
892                .get_fresh(String::new)
893                .unwrap()
894                > si::Energy::ZERO
895        );
896        assert!(
897            *sd.veh
898                .res()
899                .unwrap()
900                .state
901                .energy_out_chemical
902                .get_fresh(String::new)
903                .unwrap()
904                != si::Energy::ZERO
905        );
906    }
907
908    #[test]
909    #[cfg(feature = "resources")]
910    fn test_sim_drive_hev_thrml() {
911        let _veh = Vehicle::from_resource("2021_Hyundai_Sonata_Hybrid_Blue.yaml", false).unwrap();
912        let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
913
914        let te_amb: Vec<si::Temperature> = [-6.7, -6.7, 38.0]
915            .iter()
916            .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
917            .collect();
918        let te_batt_and_cab_init: Vec<si::Temperature> = [-6.7, 22.0, 45.0]
919            .iter()
920            .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
921            .collect();
922        let te_fc_init: Vec<si::Temperature> = [-6.7, 70.0, 90.0]
923            .iter()
924            .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
925            .collect();
926        for ((te_amb, te_init), te_fc_init) in
927            te_amb.iter().zip(te_batt_and_cab_init).zip(te_fc_init)
928        {
929            let mut veh = _veh.clone();
930
931            veh.res_mut()
932                .unwrap()
933                .res_thrml_state_mut()
934                .unwrap()
935                .temperature
936                .mark_stale();
937            veh.res_mut()
938                .unwrap()
939                .res_thrml_state_mut()
940                .unwrap()
941                .temperature
942                .update(te_init, || format_dbg!())
943                .unwrap();
944
945            veh.res_mut()
946                .unwrap()
947                .res_thrml_state_mut()
948                .unwrap()
949                .temp_prev
950                .mark_stale();
951            veh.res_mut()
952                .unwrap()
953                .res_thrml_state_mut()
954                .unwrap()
955                .temp_prev
956                .update(te_init, || format_dbg!())
957                .unwrap();
958            if let CabinOption::LumpedCabin(lc) = &mut veh.cabin {
959                lc.state.temperature.mark_stale();
960                lc.state
961                    .temperature
962                    .update(te_init, || format_dbg!())
963                    .unwrap();
964                lc.state.temp_prev.mark_stale();
965                lc.state
966                    .temp_prev
967                    .update(te_init, || format_dbg!())
968                    .unwrap();
969            }
970
971            veh.fc_mut()
972                .unwrap()
973                .fc_thrml_state_mut()
974                .unwrap()
975                .temperature
976                .mark_stale();
977            veh.fc_mut()
978                .unwrap()
979                .fc_thrml_state_mut()
980                .unwrap()
981                .temperature
982                .update(te_fc_init, || format_dbg!())
983                .unwrap();
984            let mut cyc = _cyc.clone();
985            cyc.temp_amb_air = vec![*te_amb; cyc.len_checked().unwrap()];
986            let mut sd = SimDrive::new(veh, cyc, Default::default());
987            sd.walk()
988                .with_context(|| {
989                    format!(
990                        "ambient temperature: {}*C\ninit temperature: {}",
991                        te_amb.get::<si::degree_celsius>(),
992                        te_init.get::<si::degree_celsius>()
993                    )
994                })
995                .unwrap();
996            assert!(
997                *sd.veh.state.i.get_fresh(String::new).unwrap()
998                    == sd.cyc.len_checked().unwrap() - 1
999            );
1000            assert!(
1001                *sd.veh
1002                    .fc()
1003                    .unwrap()
1004                    .state
1005                    .energy_fuel
1006                    .get_fresh(String::new)
1007                    .unwrap()
1008                    > si::Energy::ZERO
1009            );
1010            assert!(
1011                *sd.veh
1012                    .res()
1013                    .unwrap()
1014                    .state
1015                    .energy_out_chemical
1016                    .get_fresh(String::new)
1017                    .unwrap()
1018                    != si::Energy::ZERO
1019            );
1020        }
1021    }
1022
1023    #[test]
1024    #[cfg(feature = "resources")]
1025    fn test_sim_drive_hev_thrml_soak() {
1026        let _veh = Vehicle::from_resource("2021_Hyundai_Sonata_Hybrid_Blue.yaml", false).unwrap();
1027        let mut cyc = Cycle::from_resource("udds.csv", false).unwrap();
1028        // zero out speed
1029        cyc.speed.iter_mut().for_each(|v| *v = si::Velocity::ZERO);
1030
1031        let te_amb: Vec<si::Temperature> = [-6.7, -6.7, 38.0]
1032            .iter()
1033            .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1034            .collect();
1035        let te_batt_and_cab_init: Vec<si::Temperature> = [-6.7, 22.0, 45.0]
1036            .iter()
1037            .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1038            .collect();
1039        let te_fc_init: Vec<si::Temperature> = [-6.7, 70.0, 90.0]
1040            .iter()
1041            .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1042            .collect();
1043        for ((te_amb, te_init), te_fc_init) in
1044            te_amb.iter().zip(te_batt_and_cab_init).zip(te_fc_init)
1045        {
1046            let mut veh = _veh.clone();
1047
1048            veh.res_mut()
1049                .unwrap()
1050                .res_thrml_state_mut()
1051                .unwrap()
1052                .temperature
1053                .mark_stale();
1054            veh.res_mut()
1055                .unwrap()
1056                .res_thrml_state_mut()
1057                .unwrap()
1058                .temperature
1059                .update(te_init, || format_dbg!())
1060                .unwrap();
1061
1062            veh.res_mut()
1063                .unwrap()
1064                .res_thrml_state_mut()
1065                .unwrap()
1066                .temp_prev
1067                .mark_stale();
1068            veh.res_mut()
1069                .unwrap()
1070                .res_thrml_state_mut()
1071                .unwrap()
1072                .temp_prev
1073                .update(te_init, || format_dbg!())
1074                .unwrap();
1075            if let CabinOption::LumpedCabin(lc) = &mut veh.cabin {
1076                lc.state.temperature.mark_stale();
1077                lc.state
1078                    .temperature
1079                    .update(te_init, || format_dbg!())
1080                    .unwrap();
1081                lc.state.temp_prev.mark_stale();
1082                lc.state
1083                    .temp_prev
1084                    .update(te_init, || format_dbg!())
1085                    .unwrap();
1086            }
1087
1088            veh.fc_mut()
1089                .unwrap()
1090                .fc_thrml_state_mut()
1091                .unwrap()
1092                .temperature
1093                .mark_stale();
1094            veh.fc_mut()
1095                .unwrap()
1096                .fc_thrml_state_mut()
1097                .unwrap()
1098                .temperature
1099                .update(te_fc_init, || format_dbg!())
1100                .unwrap();
1101            let mut cyc = cyc.clone();
1102            cyc.temp_amb_air = vec![*te_amb; cyc.len_checked().unwrap()];
1103            let mut sd = SimDrive::new(
1104                veh,
1105                cyc,
1106                Some(SimParams {
1107                    ambient_thermal_soak: true,
1108                    ..Default::default()
1109                }),
1110            );
1111            sd.walk()
1112                .with_context(|| {
1113                    format!(
1114                        "ambient temperature: {}*C\ninit temperature: {}",
1115                        te_amb.get::<si::degree_celsius>(),
1116                        te_init.get::<si::degree_celsius>()
1117                    )
1118                })
1119                .unwrap();
1120            assert!(
1121                *sd.veh.state.i.get_fresh(String::new).unwrap()
1122                    == sd.cyc.len_checked().unwrap() - 1
1123            );
1124        }
1125    }
1126
1127    #[test]
1128    #[cfg(feature = "resources")]
1129    fn test_sim_drive_bev() {
1130        let _veh = mock_bev();
1131        let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
1132        let mut sd = SimDrive {
1133            veh: _veh,
1134            cyc: _cyc,
1135            sim_params: Default::default(),
1136        };
1137        sd.walk().unwrap();
1138        assert!(
1139            *sd.veh.state.i.get_fresh(String::new).unwrap() == sd.cyc.len_checked().unwrap() - 1
1140        );
1141        assert!(sd.veh.fc().is_none());
1142        assert!(
1143            *sd.veh
1144                .res()
1145                .unwrap()
1146                .state
1147                .energy_out_chemical
1148                .get_fresh(String::new)
1149                .unwrap()
1150                != si::Energy::ZERO
1151        );
1152    }
1153
1154    #[test]
1155    #[cfg(feature = "resources")]
1156    fn test_sim_drive_bev_thrml() {
1157        let _veh = Vehicle::from_resource("2020 Chevrolet Bolt EV.yaml", false).unwrap();
1158        let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
1159
1160        let te_amb: Vec<si::Temperature> = [-6.7, -6.7, 38.0]
1161            .iter()
1162            .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1163            .collect();
1164        let te_batt_and_cab_init: Vec<si::Temperature> = [-6.7, 22.0, 45.0]
1165            .iter()
1166            .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1167            .collect();
1168        for (te_amb, te_init) in te_amb.iter().zip(te_batt_and_cab_init) {
1169            let mut veh = _veh.clone();
1170            veh.res_mut()
1171                .unwrap()
1172                .res_thrml_state_mut()
1173                .unwrap()
1174                .temperature
1175                .mark_stale();
1176            veh.res_mut()
1177                .unwrap()
1178                .res_thrml_state_mut()
1179                .unwrap()
1180                .temperature
1181                .update(te_init, || format_dbg!())
1182                .unwrap();
1183
1184            veh.res_mut()
1185                .unwrap()
1186                .res_thrml_state_mut()
1187                .unwrap()
1188                .temp_prev
1189                .mark_stale();
1190            veh.res_mut()
1191                .unwrap()
1192                .res_thrml_state_mut()
1193                .unwrap()
1194                .temp_prev
1195                .update(te_init, || format_dbg!())
1196                .unwrap();
1197
1198            if let CabinOption::LumpedCabin(lc) = &mut veh.cabin {
1199                lc.state.temperature.mark_stale();
1200                lc.state
1201                    .temperature
1202                    .update(te_init, || format_dbg!())
1203                    .unwrap();
1204
1205                lc.state.temp_prev.mark_stale();
1206                lc.state
1207                    .temp_prev
1208                    .update(te_init, || format_dbg!())
1209                    .unwrap();
1210            } else {
1211                panic!("cabin should have been configured");
1212            }
1213            let mut cyc = _cyc.clone();
1214            cyc.temp_amb_air = vec![*te_amb; cyc.len_checked().unwrap()];
1215            let mut sd = SimDrive::new(veh, cyc, Default::default());
1216            if let CabinOption::LumpedCabin(lc) = sd.veh.cabin.clone() {
1217                assert_eq!(
1218                    *lc.state.temperature.get_fresh(|| format_dbg!()).unwrap(),
1219                    te_init
1220                );
1221            } else {
1222                panic!();
1223            };
1224            sd.walk()
1225                .with_context(|| {
1226                    format!(
1227                        "ambient temperature: {}*C\ninit temperature: {}",
1228                        te_amb.get::<si::degree_celsius>(),
1229                        te_init.get::<si::degree_celsius>()
1230                    )
1231                })
1232                .unwrap();
1233            assert!(
1234                *sd.veh.state.i.get_fresh(String::new).unwrap()
1235                    == sd.cyc.len_checked().unwrap() - 1
1236            );
1237            assert!(sd.veh.fc().is_none());
1238            assert!(
1239                *sd.veh
1240                    .res()
1241                    .unwrap()
1242                    .state
1243                    .energy_out_chemical
1244                    .get_fresh(String::new)
1245                    .unwrap()
1246                    != si::Energy::ZERO
1247            );
1248            sd.veh.reset_step(|| format_dbg!()).unwrap();
1249            sd.veh.state.time.mark_stale();
1250            sd.veh
1251                .state
1252                .time
1253                .update(si::Time::ZERO, || format_dbg!())
1254                .unwrap();
1255            assert!(*sd.veh.state.i.get_fresh(|| format_dbg!()).unwrap() == 0);
1256            sd.walk()
1257                .with_context(|| {
1258                    format!(
1259                        "ambient temperature: {}*C\ninit temperature: {}",
1260                        te_amb.get::<si::degree_celsius>(),
1261                        te_init.get::<si::degree_celsius>()
1262                    )
1263                })
1264                .unwrap();
1265            assert_eq!(*sd.veh.state.i.get_fresh(|| format_dbg!()).unwrap(), 1369);
1266        }
1267    }
1268}