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))]
11pub struct SimParams {
13 #[serde(default = "SimParams::def_ach_speed_max_iter")]
14 pub ach_speed_max_iter: u32,
17 #[serde(default = "SimParams::def_ach_speed_tol")]
18 pub ach_speed_tol: si::Ratio,
21 #[serde(default = "SimParams::def_ach_speed_solver_gain")]
22 pub ach_speed_solver_gain: f64,
24 #[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 #[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 #[pyo3(name = "walk_once")]
104 fn walk_once_py(&mut self) -> anyhow::Result<()> {
105 self.walk_once()
106 }
107
108 #[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 pub fn walk(&mut self) -> anyhow::Result<()> {
160 match self.veh.pt_type {
161 PowertrainType::HybridElectricVehicle(_) => {
162 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 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 self.veh = veh_init.clone();
252 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 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 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 self.veh
314 .solve_thermal(self.cyc.temp_amb_air[i], dt)
315 .with_context(|| format_dbg!())?;
316 self.veh
317 .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 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 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 * 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 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 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 }
525 TraceMissOptions::AllowChecked => {
526 }
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 let speed_guess = (1e-3 * uc::MPS).max(cyc_speed);
638 let max_iter = &self.sim_params.ach_speed_max_iter;
640 let xtol = &self.sim_params.ach_speed_tol;
641 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 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 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 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 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 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]
727pub struct TraceMissTolerance {
729 tol_dist: si::Length,
732 tol_dist_frac: si::Ratio,
735 tol_speed: si::Velocity,
738 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 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 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,
808 AllowChecked,
810 #[default]
811 Error,
813 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}