fastsim_core/
simdrive.rs

1use super::drive_cycle::Cycle;
2use super::vehicle::Vehicle;
3use crate::imports::*;
4use crate::prelude::*;
5
6#[serde_api]
7#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, StateMethods)]
8#[non_exhaustive]
9#[serde(deny_unknown_fields)]
10#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
11/// Solver parameters
12pub struct SimParams {
13    #[serde(default = "SimParams::def_ach_speed_max_iter")]
14    /// max number of iterations allowed in setting achieved speed when trace
15    /// cannot be achieved
16    pub ach_speed_max_iter: u32,
17    #[serde(default = "SimParams::def_ach_speed_tol")]
18    /// tolerance in change in speed guess in setting achieved speed when trace
19    /// cannot be achieved
20    pub ach_speed_tol: si::Ratio,
21    #[serde(default = "SimParams::def_ach_speed_solver_gain")]
22    /// Newton method gain for setting achieved speed
23    pub ach_speed_solver_gain: f64,
24    // TODO: plumb this up to actually do something
25    /// When implemented, this will set the tolerance on how much trace miss
26    /// is allowed
27    #[serde(default = "SimParams::def_trace_miss_tol")]
28    pub trace_miss_tol: TraceMissTolerance,
29    #[serde(default = "SimParams::def_trace_miss_opts")]
30    pub trace_miss_opts: TraceMissOptions,
31    /// whether to use FASTSim-2 style air density
32    #[serde(default = "SimParams::def_f2_const_air_density")]
33    pub f2_const_air_density: bool,
34}
35
36#[named_struct_pyo3_api]
37impl SimParams {
38    #[staticmethod]
39    #[pyo3(name = "default")]
40    fn default_py() -> Self {
41        Self::default()
42    }
43}
44
45impl SimParams {
46    fn def_ach_speed_max_iter() -> u32 {
47        Self::default().ach_speed_max_iter
48    }
49    fn def_ach_speed_tol() -> si::Ratio {
50        Self::default().ach_speed_tol
51    }
52    fn def_ach_speed_solver_gain() -> f64 {
53        Self::default().ach_speed_solver_gain
54    }
55    fn def_trace_miss_tol() -> TraceMissTolerance {
56        Self::default().trace_miss_tol
57    }
58    fn def_trace_miss_opts() -> TraceMissOptions {
59        Self::default().trace_miss_opts
60    }
61    fn def_f2_const_air_density() -> bool {
62        Self::default().f2_const_air_density
63    }
64}
65
66impl SerdeAPI for SimParams {}
67impl Init for SimParams {}
68
69impl Default for SimParams {
70    fn default() -> Self {
71        Self {
72            ach_speed_max_iter: 3,
73            ach_speed_tol: 1.0e-3 * uc::R,
74            ach_speed_solver_gain: 0.9,
75            trace_miss_tol: Default::default(),
76            trace_miss_opts: Default::default(),
77            f2_const_air_density: true,
78        }
79    }
80}
81
82#[serde_api]
83#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, StateMethods)]
84#[non_exhaustive]
85#[serde(deny_unknown_fields)]
86#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
87pub struct SimDrive {
88    #[has_state]
89    pub veh: Vehicle,
90    pub cyc: Cycle,
91    pub sim_params: SimParams,
92}
93
94#[named_struct_pyo3_api]
95impl SimDrive {
96    #[new]
97    #[pyo3(signature = (veh, cyc, sim_params=None))]
98    fn __new__(veh: Vehicle, cyc: Cycle, sim_params: Option<SimParams>) -> anyhow::Result<Self> {
99        Ok(SimDrive::new(veh, cyc, sim_params))
100    }
101
102    /// Run vehicle simulation once
103    #[pyo3(name = "walk_once")]
104    fn walk_once_py(&mut self) -> anyhow::Result<()> {
105        self.walk_once()
106    }
107
108    /// Run vehicle simulation, and, if applicable, apply powertrain-specific
109    /// corrections (e.g. iterate `walk` until SOC balance is achieved -- i.e. initial
110    /// and final SOC are nearly identical)
111    #[pyo3(name = "walk")]
112    fn walk_py(&mut self) -> anyhow::Result<()> {
113        self.walk()
114    }
115
116    #[pyo3(name = "to_fastsim2")]
117    fn to_fastsim2_py(&self) -> anyhow::Result<fastsim_2::simdrive::RustSimDrive> {
118        self.to_fastsim2()
119    }
120}
121
122impl SerdeAPI for SimDrive {}
123impl Init for SimDrive {
124    fn init(&mut self) -> Result<(), Error> {
125        self.veh
126            .init()
127            .map_err(|err| Error::InitError(format_dbg!(err)))?;
128        self.cyc
129            .init()
130            .map_err(|err| Error::InitError(format_dbg!(err)))?;
131        self.sim_params
132            .init()
133            .map_err(|err| Error::InitError(format_dbg!(err)))?;
134        Ok(())
135    }
136}
137
138impl SimDrive {
139    pub fn new(veh: Vehicle, cyc: Cycle, sim_params: Option<SimParams>) -> Self {
140        Self {
141            veh,
142            cyc,
143            sim_params: sim_params.unwrap_or_default(),
144        }
145    }
146
147    // # TODO:
148    // ## Features
149    // - [ ] regen limiting curve during speeds approaching zero per f2 -- less urgent
150    // - [ ] ability to manipulate friction/regen brake split based on required braking
151    //       power -- new feature -- move this to enum
152    // - [x] make enum `EngineOnCause::{AlreadyOn, TooCold,
153    //       PowerDemand}` and save it in a vec or some such for when there are
154    //       multiple causes -- new feature
155
156    /// Run vehicle simulation, and, if applicable, apply powertrain-specific
157    /// corrections (e.g. iterate `walk` until SOC balance is achieved -- i.e. initial
158    /// and final SOC are nearly identical)
159    pub fn walk(&mut self) -> anyhow::Result<()> {
160        match self.veh.pt_type {
161            PowertrainType::HybridElectricVehicle(_) => {
162                // Net battery energy used per amount of fuel used
163                // clone initial vehicle to preserve starting state (TODO: figure out if this is a huge CPU burden)
164                let veh_init = self.veh.clone();
165                loop {
166                    self.veh
167                        .hev_mut()
168                        .with_context(|| format_dbg!())?
169                        .soc_bal_iters
170                        .mark_stale();
171                    self.veh
172                        .hev_mut()
173                        .with_context(|| format_dbg!())?
174                        .soc_bal_iters
175                        .increment(1, || format_dbg!())?;
176                    self.walk_once().with_context(|| format_dbg!())?;
177                    let soc_final = self
178                        .veh
179                        .res()
180                        .with_context(|| format_dbg!())?
181                        .state
182                        .soc
183                        .clone();
184                    let res_per_fuel = *self
185                        .veh
186                        .res()
187                        .with_context(|| format_dbg!())?
188                        .state
189                        .energy_out_chemical
190                        .get_fresh(|| format_dbg!())?
191                        / *self
192                            .veh
193                            .fc()
194                            .with_context(|| format_dbg!())?
195                            .state
196                            .energy_fuel
197                            .get_fresh(|| format_dbg!())?;
198                    if self
199                        .veh
200                        .hev()
201                        .with_context(|| format_dbg!())?
202                        .soc_bal_iters
203                        .get_fresh(|| format_dbg!())?
204                        > &self
205                            .veh
206                            .hev()
207                            .with_context(|| format_dbg!())?
208                            .sim_params
209                            .soc_balance_iter_err
210                    {
211                        bail!(
212                            "{}",
213                            format_dbg!((
214                                self.veh
215                                    .hev()
216                                    .with_context(|| format_dbg!())?
217                                    .soc_bal_iters
218                                    .clone(),
219                                self.veh
220                                    .hev()
221                                    .with_context(|| format_dbg!())?
222                                    .sim_params
223                                    .soc_balance_iter_err
224                            ))
225                        );
226                    }
227                    if res_per_fuel.abs()
228                        < self
229                            .veh
230                            .hev()
231                            .with_context(|| format_dbg!())?
232                            .sim_params
233                            .res_per_fuel_lim
234                        || !self
235                            .veh
236                            .hev()
237                            .with_context(|| format_dbg!())?
238                            .sim_params
239                            .balance_soc
240                    {
241                        break;
242                    } else {
243                        // prep for another iteration
244                        if let Some(&mut ref mut hev) = self.veh.hev_mut() {
245                            if hev.sim_params.save_soc_bal_iters {
246                                hev.soc_bal_iter_history.push(hev.clone());
247                                hev.soc_bal_iters.mark_stale();
248                            }
249                        }
250                        // reset vehicle to initial state
251                        self.veh = veh_init.clone();
252                        // start SOC at previous final value
253                        self.veh.res_mut().with_context(|| format_dbg!())?.state.soc = soc_final;
254                    }
255                }
256            }
257            _ => self.walk_once()?,
258        }
259        Ok(())
260    }
261
262    /// Run vehicle simulation once
263    pub fn walk_once(&mut self) -> anyhow::Result<()> {
264        let len = &self.cyc.len_checked().with_context(|| format_dbg!())?;
265        ensure!(len >= &2, format_dbg!(len < &2));
266        self.save_state(|| format_dbg!())?;
267
268        self.veh.state.mass.mark_stale();
269        self.veh.state.mass.update(
270            self.veh
271                .mass()
272                .with_context(|| format_dbg!())?
273                .with_context(|| format_dbg!("Expected mass to have been set."))?,
274            || format_dbg!(),
275        )?;
276
277        loop {
278            self.check_and_reset(|| format_dbg!())?;
279            self.veh.state.mass.mark_fresh(|| format_dbg!())?;
280            if let Some(res) = self.veh.res_mut() {
281                res.state.soh.mark_fresh(|| format_dbg!())?;
282            }
283            self.step(|| format_dbg!())?;
284            self.solve_step()
285                .with_context(|| format!("{}\ntime step: {:?}", format_dbg!(), self.veh.state.i))?;
286            self.save_state(|| format_dbg!())?;
287            if *self.veh.state.i.get_fresh(|| format_dbg!())? == len - 1 {
288                break;
289            }
290        }
291        Ok(())
292    }
293
294    /// Solves current time step
295    pub fn solve_step(&mut self) -> anyhow::Result<()> {
296        let i = *self.veh.state.i.get_fresh(|| format_dbg!())?;
297        self.veh
298            .state
299            .time
300            .update(self.cyc.time[i], || format_dbg!())?;
301        let dt = self.cyc.dt_at_i(i)?;
302        // maybe make controls like:
303        // ```
304        // pub enum HVACAuxPriority {
305        //     /// Prioritize [ReversibleEnergyStorage] thermal management
306        //     ReversibleEnergyStorage
307        //     /// Prioritize [Cabin] and [ReversibleEnergyStorage] proportionally to their requests
308        //     Proportional
309        // }
310        // ```
311
312        // `solve_thermal` must happen before the other methods because it impacts aux power demand
313        self.veh
314            .solve_thermal(self.cyc.temp_amb_air[i], dt)
315            .with_context(|| format_dbg!())?;
316        self.veh
317            // TODO: feed in something different for first arg when CAVs stuff is active @MOK
318            .set_curr_pwr_out_max(dt)
319            .with_context(|| anyhow!(format_dbg!()))?;
320        self.set_pwr_prop_for_speed(
321            self.cyc.speed[i],
322            *self.veh.state.speed_ach.get_stale(|| format_dbg!())?,
323            dt,
324        )
325        .with_context(|| anyhow!(format_dbg!()))?;
326        self.veh.state.pwr_tractive_for_cyc.update(
327            *self.veh.state.pwr_tractive.get_fresh(|| format_dbg!())?,
328            || format_dbg!(),
329        )?;
330        self.set_ach_speed(self.cyc.speed[i], dt)
331            .with_context(|| anyhow!(format_dbg!()))?;
332        if self.sim_params.trace_miss_opts.is_allow_checked() {
333            self.sim_params.trace_miss_tol.check_trace_miss(
334                self.cyc.speed[i],
335                *self.veh.state.speed_ach.get_fresh(|| format_dbg!())?,
336                self.cyc.dist[i],
337                *self.veh.state.dist.get_fresh(|| format_dbg!())?,
338            )?;
339        }
340        self.veh
341            .solve_powertrain(dt)
342            .with_context(|| anyhow!(format_dbg!()))?;
343        self.veh.set_cumulative(dt)?;
344        Ok(())
345    }
346
347    /// Sets power required for given prescribed speed
348    /// # Arguments
349    /// - `speed`: prescribed or achieved speed
350    /// - `dt`: simulation time step size
351    pub fn set_pwr_prop_for_speed(
352        &mut self,
353        speed: si::Velocity,
354        speed_prev: si::Velocity,
355        dt: si::Time,
356    ) -> anyhow::Result<()> {
357        let i = *self.veh.state.i.get_fresh(|| format_dbg!())?;
358        let vs = &mut self.veh.state;
359        // TODO: get @mokeefe to give this a serious look and think about grade alignment issues that may arise
360        let interp_pt_dist: &[f64] = match self.cyc.grade_interp {
361            Some(Interpolator::Interp0D(..)) => &[],
362            Some(Interpolator::Interp1D(..)) => {
363                &[vs.dist.get_fresh(|| format_dbg!())?.get::<si::meter>()]
364            }
365            _ => unreachable!(),
366        };
367        vs.grade_curr.update(
368            if *vs.cyc_met_overall.get_stale(|| format_dbg!())? {
369                *self
370                    .cyc
371                    .grade
372                    .get(i)
373                    .with_context(|| format_dbg!(self.cyc.grade.len()))?
374            } else {
375                uc::R
376                    * self
377                        .cyc
378                        .grade_interp
379                        .as_ref()
380                        .with_context(|| format_dbg!("You might have somehow bypassed `init()`"))?
381                        .interpolate(interp_pt_dist)
382                        .with_context(|| format_dbg!())?
383            },
384            || format_dbg!(),
385        )?;
386        vs.elev_curr.update(
387            if *vs.cyc_met_overall.get_stale(|| format_dbg!())? {
388                *self.cyc.elev.get(i).with_context(|| format_dbg!())?
389            } else {
390                uc::M
391                    * self
392                        .cyc
393                        .elev_interp
394                        .as_ref()
395                        .with_context(|| format_dbg!("You might have somehow bypassed `init()`"))?
396                        .interpolate(interp_pt_dist)
397                        .with_context(|| format_dbg!())?
398            },
399            || format_dbg!(),
400        )?;
401
402        vs.air_density.update(
403            if self.sim_params.f2_const_air_density {
404                1.2 * uc::KGPM3
405            } else {
406                let te_amb_air = {
407                    let te_amb_air = self
408                        .cyc
409                        .temp_amb_air
410                        .get(i)
411                        .with_context(|| format_dbg!())?;
412                    if *te_amb_air == *TE_STD_AIR {
413                        None
414                    } else {
415                        Some(te_amb_air)
416                    }
417                };
418                Air::get_density(
419                    te_amb_air.copied(),
420                    Some(*vs.elev_curr.get_fresh(|| format_dbg!())?),
421                )
422            },
423            || format_dbg!(),
424        )?;
425
426        let mass = self.veh.mass.with_context(|| {
427            format!(
428                "{}\nVehicle mass should have been set already.",
429                format_dbg!()
430            )
431        })?;
432        vs.pwr_accel.update(
433            mass / (2.0 * dt)
434                * (speed.powi(typenum::P2::new()) - speed_prev.powi(typenum::P2::new())),
435            || format_dbg!(),
436        )?;
437        vs.pwr_ascent.update(
438            uc::ACC_GRAV
439                * *vs.grade_curr.get_fresh(|| format_dbg!())?
440                * mass
441                * (speed_prev + speed)
442                / 2.0,
443            || format_dbg!(),
444        )?;
445        vs.pwr_drag.update(
446            0.5
447            // TODO: feed in elevation
448            * Air::get_density(None, None)
449            * self.veh.chassis.drag_coef
450            * self.veh.chassis.frontal_area
451            * ((speed + speed_prev) / 2.0).powi(typenum::P3::new()),
452            || format_dbg!(),
453        )?;
454        vs.pwr_rr.update(
455            mass * uc::ACC_GRAV
456                * self.veh.chassis.wheel_rr_coef
457                * vs.grade_curr.get_fresh(|| format_dbg!())?.atan().cos()
458                * (speed_prev + speed)
459                / 2.,
460            || format_dbg!(),
461        )?;
462        vs.pwr_whl_inertia.update(
463            0.5 * self.veh.chassis.wheel_inertia
464                * self.veh.chassis.num_wheels as f64
465                * ((speed
466                    / self
467                        .veh
468                        .chassis
469                        .wheel_radius
470                        .with_context(|| format_dbg!())?)
471                .powi(typenum::P2::new())
472                    - (speed_prev
473                        / self
474                            .veh
475                            .chassis
476                            .wheel_radius
477                            .with_context(|| format_dbg!())?)
478                    .powi(typenum::P2::new()))
479                / self.cyc.dt_at_i(i).with_context(|| format_dbg!())?,
480            || format_dbg!(),
481        )?;
482
483        vs.pwr_tractive.update(
484            *vs.pwr_rr.get_fresh(|| format_dbg!())?
485                + *vs.pwr_whl_inertia.get_fresh(|| format_dbg!())?
486                + *vs.pwr_accel.get_fresh(|| format_dbg!())?
487                + *vs.pwr_ascent.get_fresh(|| format_dbg!())?
488                + *vs.pwr_drag.get_fresh(|| format_dbg!())?,
489            || format_dbg!(),
490        )?;
491        Ok(())
492    }
493
494    /// Sets achieved speed based on known current max power
495    /// # Arguments
496    /// - `cyc_speed`: prescribed speed
497    /// - `dt`: simulation time step size
498    pub fn set_ach_speed(&mut self, cyc_speed: si::Velocity, dt: si::Time) -> anyhow::Result<()> {
499        let vs = &mut self.veh.state;
500        vs.cyc_met.update(
501            vs.pwr_tractive.get_fresh(|| format_dbg!())?
502                <= vs.pwr_prop_fwd_max.get_fresh(|| format_dbg!())?,
503            || format_dbg!(),
504        )?;
505        vs.cyc_met_overall.update(
506            if !*vs.cyc_met.get_fresh(|| format_dbg!())? {
507                // if current power demand is not met, then this becomes false for
508                // the rest of the cycle and should not be manipulated anywhere else
509                false
510            } else {
511                *vs.cyc_met_overall.get_stale(|| format_dbg!())?
512            },
513            || format_dbg!(),
514        )?;
515        let veh = &mut self.veh;
516        let speed_prev = *veh.state.speed_ach.get_stale(|| format_dbg!())?;
517        if *veh.state.cyc_met.get_fresh(|| format_dbg!())? {
518            veh.state.speed_ach.update(cyc_speed, || format_dbg!())?;
519            return Ok(());
520        } else {
521            match self.sim_params.trace_miss_opts {
522                TraceMissOptions::Allow => {
523                    // do nothing because `set_ach_speed` should be allowed to proceed to handle this
524                }
525                TraceMissOptions::AllowChecked => {
526                    // this will be handled later
527                }
528                TraceMissOptions::Error => bail!(
529                    "{}\nFailed to meet speed trace.
530prescribed speed: {} mph
531prev speed_ach: {} mph
532pwr_tractive_for_cyc: {} kW
533pwr_tractive: {} kW
534pwr_prop_fwd_max: {} kW,
535pwr deficit: {} kW
536",
537                    format_dbg!(),
538                    cyc_speed.get::<si::mile_per_hour>(),
539                    veh.state
540                        .speed_ach
541                        .get_stale(|| format_dbg!())?
542                        .get::<si::mile_per_hour>(),
543                    veh.state
544                        .pwr_tractive_for_cyc
545                        .get_fresh(|| format_dbg!())?
546                        .get::<si::kilowatt>(),
547                    veh.state
548                        .pwr_tractive
549                        .get_fresh(|| format_dbg!())?
550                        .get::<si::kilowatt>(),
551                    veh.state
552                        .pwr_prop_fwd_max
553                        .get_fresh(|| format_dbg!())?
554                        .get::<si::kilowatt>(),
555                    (*veh.state.pwr_tractive.get_fresh(|| format_dbg!())?
556                        - *veh.state.pwr_prop_fwd_max.get_fresh(|| format_dbg!())?)
557                    .get::<si::kilowatt>()
558                    .format_eng(None)
559                ),
560                TraceMissOptions::Correct => todo!(),
561            }
562        }
563        let vs = &mut self.veh.state;
564        let mass = self
565            .veh
566            .mass
567            .with_context(|| format!("{}\nMass should have been set before now", format_dbg!()))?;
568
569        let drag3 = 1.0 / 16.0
570            * *vs.air_density.get_fresh(|| format_dbg!())?
571            * self.veh.chassis.drag_coef
572            * self.veh.chassis.frontal_area;
573        let accel2 = 0.5 * mass / dt;
574        let drag2 = 3.0 / 16.0
575            * *vs.air_density.get_fresh(|| format_dbg!())?
576            * self.veh.chassis.drag_coef
577            * self.veh.chassis.frontal_area
578            * speed_prev;
579        let wheel2 = 0.5 * self.veh.chassis.wheel_inertia * self.veh.chassis.num_wheels as f64
580            / (dt
581                * self
582                    .veh
583                    .chassis
584                    .wheel_radius
585                    .with_context(|| format_dbg!())?
586                    .powi(typenum::P2::new()));
587        let drag1 = 3.0 / 16.0
588            * *vs.air_density.get_fresh(|| format_dbg!())?
589            * self.veh.chassis.drag_coef
590            * self.veh.chassis.frontal_area
591            * vs.speed_ach
592                .get_fresh(|| format_dbg!())?
593                .powi(typenum::P2::new());
594        let roll1 = 0.5
595            * mass
596            * uc::ACC_GRAV
597            * self.veh.chassis.wheel_rr_coef
598            * vs.grade_curr.get_fresh(|| format_dbg!())?.atan().cos();
599        let ascent1 =
600            0.5 * uc::ACC_GRAV * vs.grade_curr.get_fresh(|| format_dbg!())?.atan().sin() * mass;
601        let accel0 = -0.5 * mass * speed_prev.powi(typenum::P2::new()) / dt;
602        let drag0 = 1.0 / 16.0
603            * *vs.air_density.get_fresh(|| format_dbg!())?
604            * self.veh.chassis.drag_coef
605            * self.veh.chassis.frontal_area
606            * speed_prev.powi(typenum::P3::new());
607        let roll0 = 0.5
608            * mass
609            * uc::ACC_GRAV
610            * self.veh.chassis.wheel_rr_coef
611            * vs.grade_curr.get_fresh(|| format_dbg!())?.atan().cos()
612            * speed_prev;
613        let ascent0 = 0.5
614            * uc::ACC_GRAV
615            * vs.grade_curr.get_fresh(|| format_dbg!())?.atan().sin()
616            * mass
617            * speed_prev;
618        let wheel0 = -0.5
619            * self.veh.chassis.wheel_inertia
620            * self.veh.chassis.num_wheels as f64
621            * speed_prev.powi(typenum::P2::new())
622            / (dt
623                * self
624                    .veh
625                    .chassis
626                    .wheel_radius
627                    .with_context(|| format_dbg!())?
628                    .powi(typenum::P2::new()));
629
630        let t3 = drag3;
631        let t2 = accel2 + drag2 + wheel2;
632        let t1 = drag1 + roll1 + ascent1;
633        let t0 = (accel0 + drag0 + roll0 + ascent0 + wheel0)
634            - *vs.pwr_prop_fwd_max.get_fresh(|| format_dbg!())?;
635
636        // initial guess
637        let speed_guess = (1e-3 * uc::MPS).max(cyc_speed);
638        // stop criteria
639        let max_iter = &self.sim_params.ach_speed_max_iter;
640        let xtol = &self.sim_params.ach_speed_tol;
641        // solver gain
642        let g = &self.sim_params.ach_speed_solver_gain;
643        let pwr_err_fn = |speed_guess: si::Velocity| -> si::Power {
644            t3 * speed_guess.powi(typenum::P3::new())
645                + t2 * speed_guess.powi(typenum::P2::new())
646                + t1 * speed_guess
647                + t0
648        };
649        let pwr_err_per_speed_guess_fn = |speed_guess: si::Velocity| {
650            3.0 * t3 * speed_guess.powi(typenum::P2::new()) + 2.0 * t2 * speed_guess + t1
651        };
652        let pwr_err = pwr_err_fn(speed_guess);
653        if almost_eq_uom(&pwr_err, &(0. * uc::W), Some(1e-6)) {
654            // TODO: maybe change to `updated_unchecked`
655            vs.speed_ach.update(cyc_speed, || format_dbg!())?;
656            return Ok(());
657        }
658        let pwr_err_per_speed_guess = pwr_err_per_speed_guess_fn(speed_guess);
659        let new_speed_guess = pwr_err - speed_guess * pwr_err_per_speed_guess;
660        let mut speed_guesses = vec![speed_guess];
661        let mut pwr_errs = vec![pwr_err];
662        let mut d_pwr_err_per_d_speed_guesses = vec![pwr_err_per_speed_guess];
663        let mut new_speed_guesses = vec![new_speed_guess];
664        // speed achieved iteration counter
665        let mut spd_ach_iter_counter = 1;
666        let mut converged = pwr_err <= si::Power::ZERO;
667        let mut speed_ach: si::Velocity = Default::default();
668        while &spd_ach_iter_counter < max_iter && !converged {
669            let speed_guess = *speed_guesses.iter().last().with_context(|| format_dbg!())?
670                * (1.0 - g)
671                - *g * *new_speed_guesses
672                    .iter()
673                    .last()
674                    .with_context(|| format_dbg!())?
675                    / d_pwr_err_per_d_speed_guesses[speed_guesses.len() - 1];
676            let pwr_err = pwr_err_fn(speed_guess);
677            let pwr_err_per_speed_guess = pwr_err_per_speed_guess_fn(speed_guess);
678            let new_speed_guess = pwr_err - speed_guess * pwr_err_per_speed_guess;
679            speed_guesses.push(speed_guess);
680            pwr_errs.push(pwr_err);
681            d_pwr_err_per_d_speed_guesses.push(pwr_err_per_speed_guess);
682            new_speed_guesses.push(new_speed_guess);
683            // is the fractional change between previous and current speed guess smaller than `xtol`
684            converged = &((*speed_guesses.iter().last().with_context(|| format_dbg!())?
685                - speed_guesses[speed_guesses.len() - 2])
686                / speed_guesses[speed_guesses.len() - 2])
687                .abs()
688                < xtol;
689            spd_ach_iter_counter += 1;
690
691            // TODO: verify that assuming `speed_guesses.iter().last()` is the correct solution
692            speed_ach = speed_guesses
693                .last()
694                .with_context(|| format_dbg!("should have had at least one element"))?
695                .max(0.0 * uc::MPS);
696        }
697
698        vs.speed_ach.update(speed_ach, || format_dbg!())?;
699
700        // Run it again to make sure it has been updated for achieved speed
701        self.set_pwr_prop_for_speed(
702            *self.veh.state.speed_ach.get_fresh(|| format_dbg!())?,
703            speed_prev,
704            dt,
705        )
706        .with_context(|| format_dbg!())?;
707
708        Ok(())
709    }
710
711    pub fn to_fastsim2(&self) -> anyhow::Result<fastsim_2::simdrive::RustSimDrive> {
712        let veh2 = self
713            .veh
714            .to_fastsim2()
715            .with_context(|| anyhow!(format_dbg!()))?;
716        let cyc2 = self
717            .cyc
718            .to_fastsim2()
719            .with_context(|| anyhow!(format_dbg!()))?;
720        Ok(fastsim_2::simdrive::RustSimDrive::new(cyc2, veh2))
721    }
722}
723
724#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, StateMethods)]
725#[serde(deny_unknown_fields)]
726#[non_exhaustive]
727// NOTE: consider embedding this in TraceMissOptions::AllowChecked
728pub struct TraceMissTolerance {
729    /// if the vehicle falls this far behind trace in terms of absolute
730    /// difference and [TraceMissOptions::is_allow_checked], fail
731    tol_dist: si::Length,
732    /// if the vehicle falls this far behind trace in terms of fractional
733    /// difference and [TraceMissOptions::is_allow_checked], fail
734    tol_dist_frac: si::Ratio,
735    /// if the vehicle falls this far behind instantaneous speed and
736    /// [TraceMissOptions::is_allow_checked], fail
737    tol_speed: si::Velocity,
738    /// if the vehicle falls this far behind instantaneous speed in terms of
739    /// fractional difference and [TraceMissOptions::is_allow_checked], fail
740    tol_speed_frac: si::Ratio,
741}
742
743impl TraceMissTolerance {
744    fn check_trace_miss(
745        &self,
746        cyc_speed: si::Velocity,
747        ach_speed: si::Velocity,
748        cyc_dist: si::Length,
749        ach_dist: si::Length,
750    ) -> anyhow::Result<()> {
751        ensure!(
752            cyc_speed - ach_speed < self.tol_speed,
753            "{}\n{}\n{}",
754            format_dbg!(cyc_speed),
755            format_dbg!(ach_speed),
756            format_dbg!(self.tol_speed)
757        );
758        // if condition to prevent divide-by-zero errors
759        if cyc_speed > self.tol_speed {
760            ensure!(
761                (cyc_speed - ach_speed) / cyc_speed < self.tol_speed_frac,
762                "{}\n{}\n{}",
763                format_dbg!(cyc_speed),
764                format_dbg!(ach_speed),
765                format_dbg!(self.tol_speed_frac)
766            )
767        }
768        ensure!(
769            (cyc_dist - ach_dist) < self.tol_dist,
770            "{}\n{}\n{}",
771            format_dbg!(cyc_dist),
772            format_dbg!(ach_dist),
773            format_dbg!(self.tol_dist)
774        );
775        // if condition to prevent checking early in cycle
776        if cyc_dist > self.tol_dist * 5.0 {
777            ensure!(
778                (cyc_dist - ach_dist) / cyc_dist < self.tol_dist_frac,
779                "{}\n{}\n{}",
780                format_dbg!(cyc_dist),
781                format_dbg!(ach_dist),
782                format_dbg!(self.tol_dist_frac)
783            )
784        }
785
786        Ok(())
787    }
788}
789impl SerdeAPI for TraceMissTolerance {}
790impl Init for TraceMissTolerance {}
791impl Default for TraceMissTolerance {
792    fn default() -> Self {
793        Self {
794            tol_dist: 100. * uc::M,
795            tol_dist_frac: 0.05 * uc::R,
796            tol_speed: 10. * uc::MPS,
797            tol_speed_frac: 0.5 * uc::R,
798        }
799    }
800}
801
802#[derive(
803    Clone, Default, Debug, Deserialize, Serialize, PartialEq, IsVariant, derive_more::From, TryInto,
804)]
805pub enum TraceMissOptions {
806    /// Allow trace miss without any fanfare
807    Allow,
808    /// Allow trace miss within error tolerance
809    AllowChecked,
810    #[default]
811    /// Error out when trace miss happens
812    Error,
813    /// Correct trace miss with driver model that catches up
814    Correct,
815}
816
817impl SerdeAPI for TraceMissOptions {}
818impl Init for TraceMissOptions {}
819
820#[cfg(test)]
821mod tests {
822    use super::*;
823    use crate::vehicle::vehicle_model::tests::*;
824
825    #[test]
826    #[cfg(feature = "resources")]
827    fn test_sim_drive_conv() {
828        let _veh = mock_conv_veh();
829        let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
830        let mut sd = SimDrive::new(_veh, _cyc, Default::default());
831        sd.walk().unwrap();
832        assert!(
833            *sd.veh.state.i.get_fresh(String::new).unwrap() == sd.cyc.len_checked().unwrap() - 1
834        );
835        assert!(
836            *sd.veh
837                .fc()
838                .unwrap()
839                .state
840                .energy_fuel
841                .get_fresh(String::new)
842                .unwrap()
843                > si::Energy::ZERO
844        );
845        assert!(sd.veh.res().is_none());
846    }
847
848    #[test]
849    #[cfg(feature = "resources")]
850    fn test_sim_drive_hev() {
851        let _veh = mock_hev();
852        let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
853        let mut sd = SimDrive::new(_veh, _cyc, Default::default());
854        sd.walk().unwrap();
855        assert!(
856            *sd.veh.state.i.get_fresh(String::new).unwrap() == sd.cyc.len_checked().unwrap() - 1
857        );
858        assert!(
859            *sd.veh
860                .fc()
861                .unwrap()
862                .state
863                .energy_fuel
864                .get_fresh(String::new)
865                .unwrap()
866                > si::Energy::ZERO
867        );
868        assert!(
869            *sd.veh
870                .res()
871                .unwrap()
872                .state
873                .energy_out_chemical
874                .get_fresh(String::new)
875                .unwrap()
876                != si::Energy::ZERO
877        );
878    }
879
880    #[test]
881    #[cfg(feature = "resources")]
882    fn test_sim_drive_hev_thrml() {
883        let _veh = Vehicle::from_resource("2021_Hyundai_Sonata_Hybrid_Blue.yaml", false).unwrap();
884        let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
885
886        let te_amb: Vec<si::Temperature> = [-6.7, -6.7, 38.0]
887            .iter()
888            .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
889            .collect();
890        let te_batt_and_cab_init: Vec<si::Temperature> = [-6.7, 22.0, 45.0]
891            .iter()
892            .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
893            .collect();
894        let te_fc_init: Vec<si::Temperature> = [-6.7, 70.0, 90.0]
895            .iter()
896            .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
897            .collect();
898        for ((te_amb, te_init), te_fc_init) in
899            te_amb.iter().zip(te_batt_and_cab_init).zip(te_fc_init)
900        {
901            let mut veh = _veh.clone();
902
903            veh.res_mut()
904                .unwrap()
905                .res_thrml_state_mut()
906                .unwrap()
907                .temperature
908                .mark_stale();
909            veh.res_mut()
910                .unwrap()
911                .res_thrml_state_mut()
912                .unwrap()
913                .temperature
914                .update(te_init, || format_dbg!())
915                .unwrap();
916
917            veh.res_mut()
918                .unwrap()
919                .res_thrml_state_mut()
920                .unwrap()
921                .temp_prev
922                .mark_stale();
923            veh.res_mut()
924                .unwrap()
925                .res_thrml_state_mut()
926                .unwrap()
927                .temp_prev
928                .update(te_init, || format_dbg!())
929                .unwrap();
930            if let CabinOption::LumpedCabin(lc) = &mut veh.cabin {
931                lc.state.temperature.mark_stale();
932                lc.state
933                    .temperature
934                    .update(te_init, || format_dbg!())
935                    .unwrap();
936                lc.state.temp_prev.mark_stale();
937                lc.state
938                    .temp_prev
939                    .update(te_init, || format_dbg!())
940                    .unwrap();
941            }
942
943            veh.fc_mut()
944                .unwrap()
945                .fc_thrml_state_mut()
946                .unwrap()
947                .temperature
948                .mark_stale();
949            veh.fc_mut()
950                .unwrap()
951                .fc_thrml_state_mut()
952                .unwrap()
953                .temperature
954                .update(te_fc_init, || format_dbg!())
955                .unwrap();
956            let mut cyc = _cyc.clone();
957            cyc.temp_amb_air = vec![*te_amb; cyc.len_checked().unwrap()];
958            let mut sd = SimDrive::new(veh, cyc, Default::default());
959            sd.walk()
960                .with_context(|| {
961                    format!(
962                        "ambient temperature: {}*C\ninit temperature: {}",
963                        te_amb.get::<si::degree_celsius>(),
964                        te_init.get::<si::degree_celsius>()
965                    )
966                })
967                .unwrap();
968            assert!(
969                *sd.veh.state.i.get_fresh(String::new).unwrap()
970                    == sd.cyc.len_checked().unwrap() - 1
971            );
972            assert!(
973                *sd.veh
974                    .fc()
975                    .unwrap()
976                    .state
977                    .energy_fuel
978                    .get_fresh(String::new)
979                    .unwrap()
980                    > si::Energy::ZERO
981            );
982            assert!(
983                *sd.veh
984                    .res()
985                    .unwrap()
986                    .state
987                    .energy_out_chemical
988                    .get_fresh(String::new)
989                    .unwrap()
990                    != si::Energy::ZERO
991            );
992        }
993    }
994
995    #[test]
996    #[cfg(feature = "resources")]
997    fn test_sim_drive_bev() {
998        let _veh = mock_bev();
999        let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
1000        let mut sd = SimDrive {
1001            veh: _veh,
1002            cyc: _cyc,
1003            sim_params: Default::default(),
1004        };
1005        sd.walk().unwrap();
1006        assert!(
1007            *sd.veh.state.i.get_fresh(String::new).unwrap() == sd.cyc.len_checked().unwrap() - 1
1008        );
1009        assert!(sd.veh.fc().is_none());
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    #[test]
1023    #[cfg(feature = "resources")]
1024    fn test_sim_drive_bev_thrml() {
1025        let _veh = Vehicle::from_resource("2020 Chevrolet Bolt EV.yaml", false).unwrap();
1026        let _cyc = Cycle::from_resource("udds.csv", false).unwrap();
1027
1028        let te_amb: Vec<si::Temperature> = [-6.7, -6.7, 38.0]
1029            .iter()
1030            .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1031            .collect();
1032        let te_batt_and_cab_init: Vec<si::Temperature> = [-6.7, 22.0, 45.0]
1033            .iter()
1034            .map(|t| (*t + uc::CELSIUS_TO_KELVIN) * uc::KELVIN)
1035            .collect();
1036        for (te_amb, te_init) in te_amb.iter().zip(te_batt_and_cab_init) {
1037            let mut veh = _veh.clone();
1038            veh.res_mut()
1039                .unwrap()
1040                .res_thrml_state_mut()
1041                .unwrap()
1042                .temperature
1043                .mark_stale();
1044            veh.res_mut()
1045                .unwrap()
1046                .res_thrml_state_mut()
1047                .unwrap()
1048                .temperature
1049                .update(te_init, || format_dbg!())
1050                .unwrap();
1051
1052            veh.res_mut()
1053                .unwrap()
1054                .res_thrml_state_mut()
1055                .unwrap()
1056                .temp_prev
1057                .mark_stale();
1058            veh.res_mut()
1059                .unwrap()
1060                .res_thrml_state_mut()
1061                .unwrap()
1062                .temp_prev
1063                .update(te_init, || format_dbg!())
1064                .unwrap();
1065
1066            if let CabinOption::LumpedCabin(lc) = &mut veh.cabin {
1067                lc.state.temperature.mark_stale();
1068                lc.state
1069                    .temperature
1070                    .update(te_init, || format_dbg!())
1071                    .unwrap();
1072
1073                lc.state.temp_prev.mark_stale();
1074                lc.state
1075                    .temp_prev
1076                    .update(te_init, || format_dbg!())
1077                    .unwrap();
1078            }
1079            let mut cyc = _cyc.clone();
1080            cyc.temp_amb_air = vec![*te_amb; cyc.len_checked().unwrap()];
1081            let mut sd = SimDrive::new(veh, cyc, Default::default());
1082            sd.walk()
1083                .with_context(|| {
1084                    format!(
1085                        "ambient temperature: {}*C\ninit temperature: {}",
1086                        te_amb.get::<si::degree_celsius>(),
1087                        te_init.get::<si::degree_celsius>()
1088                    )
1089                })
1090                .unwrap();
1091            assert!(
1092                *sd.veh.state.i.get_fresh(String::new).unwrap()
1093                    == sd.cyc.len_checked().unwrap() - 1
1094            );
1095            assert!(sd.veh.fc().is_none());
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}