Skip to main content

fastsim_core/simdrive/
params.rs

1use super::*;
2
3#[serde_api]
4#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
5#[non_exhaustive]
6#[serde(deny_unknown_fields)]
7#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
8/// Solver parameters
9pub struct SimParams {
10    #[serde(default = "SimParams::def_ach_speed_max_iter")]
11    /// max number of iterations allowed in setting achieved speed when trace
12    /// cannot be achieved
13    pub ach_speed_max_iter: u32,
14    #[serde(default = "SimParams::def_ach_speed_tol")]
15    /// tolerance in change in speed guess in setting achieved speed when trace
16    /// cannot be achieved
17    pub ach_speed_tol: si::Ratio,
18    #[serde(default = "SimParams::def_ach_speed_solver_gain")]
19    /// Newton method gain for setting achieved speed
20    pub ach_speed_solver_gain: f64,
21    // TODO: plumb this up to actually do something
22    /// When implemented, this will set the tolerance on how much trace miss
23    /// is allowed
24    #[serde(default = "SimParams::def_trace_miss_tol")]
25    pub trace_miss_tol: TraceMissTolerance,
26    #[serde(default = "SimParams::def_trace_miss_opts")]
27    pub trace_miss_opts: TraceMissOptions,
28    #[serde(default = "SimParams::def_trace_miss_correct_max_steps")]
29    /// the maximum number of steps in which to re-rendezvous with reference
30    /// trace after a trace miss. Note: this field only applies when
31    /// trace_miss_opts is set to TraceMissOptions::Correct. Note: must
32    /// be 2 or greater. Defaults to 6.
33    pub trace_miss_correct_max_steps: u32,
34    /// whether to use FASTSim-2 style air density
35    #[serde(default = "SimParams::def_f2_const_air_density")]
36    pub f2_const_air_density: bool,
37    /// if true, vehicle is totally inactive except for thermal models
38    pub ambient_thermal_soak: bool,
39}
40
41#[pyo3_api]
42impl SimParams {
43    #[staticmethod]
44    #[pyo3(name = "default")]
45    fn default_py() -> Self {
46        Self::default()
47    }
48}
49
50impl SimParams {
51    fn def_ach_speed_max_iter() -> u32 {
52        Self::default().ach_speed_max_iter
53    }
54    fn def_ach_speed_tol() -> si::Ratio {
55        Self::default().ach_speed_tol
56    }
57    fn def_ach_speed_solver_gain() -> f64 {
58        Self::default().ach_speed_solver_gain
59    }
60    fn def_trace_miss_tol() -> TraceMissTolerance {
61        Self::default().trace_miss_tol
62    }
63    fn def_trace_miss_opts() -> TraceMissOptions {
64        Self::default().trace_miss_opts
65    }
66    fn def_trace_miss_correct_max_steps() -> u32 {
67        Self::default().trace_miss_correct_max_steps
68    }
69    fn def_f2_const_air_density() -> bool {
70        Self::default().f2_const_air_density
71    }
72}
73
74impl SerdeAPI for SimParams {}
75impl Init for SimParams {}
76
77impl Default for SimParams {
78    fn default() -> Self {
79        Self {
80            ach_speed_max_iter: 3,
81            ach_speed_tol: 1.0e-3 * uc::R,
82            ach_speed_solver_gain: 0.9,
83            trace_miss_tol: Default::default(),
84            trace_miss_opts: Default::default(),
85            trace_miss_correct_max_steps: 6,
86            f2_const_air_density: true,
87            ambient_thermal_soak: false,
88        }
89    }
90}
91
92#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
93#[serde(deny_unknown_fields)]
94#[non_exhaustive]
95pub struct TraceMissTolerance {
96    /// if the vehicle falls this far behind trace in terms of absolute
97    /// difference and [TraceMissOptions::is_allow_checked], fail
98    pub tol_dist: si::Length,
99    /// if the vehicle falls this far behind trace in terms of fractional
100    /// difference and [TraceMissOptions::is_allow_checked], fail
101    pub tol_dist_frac: si::Ratio,
102    /// if the vehicle falls this far behind instantaneous speed and
103    /// [TraceMissOptions::is_allow_checked], fail
104    pub tol_speed: si::Velocity,
105    /// if the vehicle falls this far behind instantaneous speed in terms of
106    /// fractional difference and [TraceMissOptions::is_allow_checked], fail
107    pub tol_speed_frac: si::Ratio,
108}
109
110impl TraceMissTolerance {
111    pub fn check_trace_miss(
112        &self,
113        cyc_speed: si::Velocity,
114        ach_speed: si::Velocity,
115        cyc_dist: si::Length,
116        ach_dist: si::Length,
117    ) -> anyhow::Result<()> {
118        ensure!(
119            (cyc_speed - ach_speed).abs() < self.tol_speed,
120            concat!(
121                "trace miss: achieved speed misses prescribed speed\n",
122                "    achieved speed: {:?}\n",
123                "    prescribed speed: {:?}\n",
124                "    exceeds allowed tolerance: {:?}",
125            ),
126            ach_speed,
127            cyc_speed,
128            self.tol_speed,
129        );
130        // if condition to prevent divide-by-zero errors
131        if cyc_speed > self.tol_speed {
132            ensure!(
133                (cyc_speed - ach_speed).abs() / cyc_speed < self.tol_speed_frac,
134                concat!(
135                    "trace miss: achieved speed misses prescribed speed (fractional)\n",
136                    "    achieved speed: {:?}\n",
137                    "    prescribed speed: {:?}\n",
138                    "    exceeds allowed fractional tolerance: {:?}",
139                ),
140                ach_speed,
141                cyc_speed,
142                self.tol_speed_frac
143            )
144        }
145        ensure!(
146            (cyc_dist - ach_dist).abs() < self.tol_dist,
147            concat!(
148                "trace miss: achieved distance misses prescribed distance\n",
149                "    achieved distance: {:?}\n",
150                "    prescribed distance: {:?}\n",
151                "    exceeds allowed tolerance: {:?}",
152            ),
153            ach_dist,
154            cyc_dist,
155            self.tol_dist
156        );
157        // if condition to prevent checking early in cycle
158        if cyc_dist > self.tol_dist * 5.0 {
159            ensure!(
160                (cyc_dist - ach_dist).abs() / cyc_dist < self.tol_dist_frac,
161                concat!(
162                    "trace miss: achieved distance misses prescribed distance (fractional)\n",
163                    "    achieved distance: {:?}\n",
164                    "    prescribed distance: {:?}\n",
165                    "    exceeds allowed fractional tolerance: {:?}",
166                ),
167                ach_dist,
168                cyc_dist,
169                self.tol_dist_frac
170            )
171        }
172
173        Ok(())
174    }
175}
176impl SerdeAPI for TraceMissTolerance {}
177impl Init for TraceMissTolerance {}
178impl Default for TraceMissTolerance {
179    fn default() -> Self {
180        Self {
181            tol_dist: 100. * uc::M,
182            tol_dist_frac: 0.05 * uc::R,
183            tol_speed: 10. * uc::MPS,
184            tol_speed_frac: 0.5 * uc::R,
185        }
186    }
187}
188
189#[derive(
190    Clone, Default, Debug, Deserialize, Serialize, PartialEq, IsVariant, derive_more::From, TryInto,
191)]
192pub enum TraceMissOptions {
193    /// Allow trace miss without any fanfare
194    Allow,
195    /// Allow trace miss within error tolerance
196    AllowChecked,
197    // /// Show warning when any trace miss happens
198    // #[default]
199    // Warn,
200    // /// Show warning when trace miss outside tolerance happens
201    #[default]
202    // WarnChecked,
203    /// Throw error when trace miss happens
204    Error,
205    /// Correct trace miss with driver model that catches up
206    Correct,
207}
208
209impl SerdeAPI for TraceMissOptions {}
210impl Init for TraceMissOptions {}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    #[cfg(feature = "resources")]
218    #[cfg(feature = "yaml")]
219    fn test_trace_miss_allow() {
220        let mut veh =
221            crate::vehicle::Vehicle::from_resource("2012_Ford_Fusion.yaml", false).unwrap();
222        veh.mass = Some(10000.0 * uc::KG);
223        let params = SimParams {
224            trace_miss_opts: TraceMissOptions::Allow,
225            ..Default::default()
226        };
227        let cyc = crate::drive_cycle::CYC_ACCEL.clone();
228        let mut sim = SimDrive::new(veh, cyc, Some(params));
229        assert!(sim.run().is_ok());
230    }
231
232    #[test]
233    #[cfg(feature = "resources")]
234    #[cfg(feature = "yaml")]
235    fn test_trace_miss_allowchecked() {
236        let mut veh =
237            crate::vehicle::Vehicle::from_resource("2012_Ford_Fusion.yaml", false).unwrap();
238        veh.mass = Some(10000.0 * uc::KG);
239        let cyc = crate::drive_cycle::CYC_ACCEL.clone();
240        // misses default tolerances
241        let params = SimParams {
242            trace_miss_opts: TraceMissOptions::AllowChecked,
243            ..Default::default()
244        };
245        let mut sim = SimDrive::new(veh.clone(), cyc.clone(), Some(params));
246        assert!(sim.run().is_err());
247        // meets modified tolerances
248        let params = SimParams {
249            trace_miss_opts: TraceMissOptions::AllowChecked,
250            trace_miss_tol: TraceMissTolerance {
251                tol_dist: 1e6 * uc::M,
252                tol_dist_frac: 10.0 * uc::R,
253                tol_speed: *cyc.speed.max().unwrap(),
254                tol_speed_frac: 1.0 * uc::R,
255            },
256            ..Default::default()
257        };
258        let mut sim = SimDrive::new(veh.clone(), cyc.clone(), Some(params));
259        sim.run().unwrap();
260        // misses mixed tolerances
261        let params = SimParams {
262            trace_miss_opts: TraceMissOptions::AllowChecked,
263            trace_miss_tol: TraceMissTolerance {
264                tol_dist_frac: 10.0 * uc::R,
265                tol_speed: *cyc.speed.max().unwrap(),
266                tol_speed_frac: 1.0 * uc::R,
267                ..Default::default()
268            },
269            ..Default::default()
270        };
271        let mut sim = SimDrive::new(veh.clone(), cyc.clone(), Some(params));
272        assert!(sim.run().is_err());
273        // misses mixed tolerances
274        let params = SimParams {
275            trace_miss_opts: TraceMissOptions::AllowChecked,
276            trace_miss_tol: TraceMissTolerance {
277                tol_dist: 100. * uc::M,
278                tol_dist_frac: 0.05 * uc::R,
279                tol_speed_frac: 0.5 * uc::R,
280                ..Default::default()
281            },
282            ..Default::default()
283        };
284        let mut sim = SimDrive::new(veh.clone(), cyc.clone(), Some(params));
285        assert!(sim.run().is_err());
286        // misses mixed tolerances
287        let params = SimParams {
288            trace_miss_opts: TraceMissOptions::AllowChecked,
289            trace_miss_tol: TraceMissTolerance {
290                tol_dist: 100. * uc::M,
291                tol_dist_frac: 0.05 * uc::R,
292                tol_speed: 10. * uc::MPS,
293                ..Default::default()
294            },
295            ..Default::default()
296        };
297        let mut sim = SimDrive::new(veh.clone(), cyc.clone(), Some(params));
298        assert!(sim.run().is_err());
299    }
300
301    // TODO: implement when TraceMissOptions::Warn is implemented
302    // #[test]
303    // #[cfg(feature = "yaml")]
304    // fn test_trace_miss_warn() {
305    //     let mut veh =
306    //         crate::vehicle::Vehicle::from_resource("2012_Ford_Fusion.yaml", false).unwrap();
307    //     veh.mass = Some(10000.0 * uc::KG);
308    //     let params = SimParams {
309    //         trace_miss_opts: TraceMissOptions::Warn,
310    //         ..Default::default()
311    //     };
312    //     let cyc = crate::drive_cycle::CYC_ACCEL.clone();
313    //     let mut sim = SimDrive::new(veh, cyc, Some(params));
314    //     todo!();
315    // }
316
317    #[test]
318    #[cfg(feature = "resources")]
319    #[cfg(feature = "yaml")]
320    fn test_trace_miss_error() {
321        let mut veh =
322            crate::vehicle::Vehicle::from_resource("2012_Ford_Fusion.yaml", false).unwrap();
323        veh.mass = Some(10000.0 * uc::KG);
324        let params = SimParams {
325            trace_miss_opts: TraceMissOptions::Error,
326            ..Default::default()
327        };
328        let cyc = crate::drive_cycle::CYC_ACCEL.clone();
329        let mut sim = SimDrive::new(veh, cyc, Some(params));
330        assert!(sim.run().is_err());
331    }
332
333    // TODO: why does sim.cyc.speed have spikes? sim.veh.history.speed_ach seems reasonable
334    #[test]
335    #[cfg(feature = "resources")]
336    #[cfg(feature = "yaml")]
337    fn test_trace_miss_correct() {
338        let mut veh =
339            crate::vehicle::Vehicle::from_resource("2012_Ford_Fusion.yaml", false).unwrap();
340        veh.mass = Some(10000.0 * uc::KG);
341        let params = SimParams {
342            trace_miss_opts: TraceMissOptions::Correct,
343            ..Default::default()
344        };
345        let cyc = crate::drive_cycle::CYC_ACCEL.clone();
346        let mut sim = SimDrive::new(veh, cyc, Some(params));
347        sim.run().unwrap();
348    }
349}