fastsim_core/
cycle.rs

1//! Module containing drive cycle struct and related functions.
2
3use std::collections::HashMap;
4
5// local
6use crate::imports::*;
7use crate::params::*;
8use crate::proc_macros::add_pyo3_api;
9#[cfg(feature = "pyo3")]
10use crate::pyo3imports::*;
11use crate::utils::*;
12
13#[cfg_attr(feature = "pyo3", pyfunction)]
14/// # Arguments
15/// - n: Int, number of time-steps away from rendezvous
16/// - d0: Num, distance of simulated vehicle, $\frac{m}{s}$
17/// - v0: Num, speed of simulated vehicle, $\frac{m}{s}$
18/// - dr: Num, distance of rendezvous point, $m$
19/// - vr: Num, speed of rendezvous point, $\frac{m}{s}$
20/// - dt: Num, step duration, $s$
21///
22/// # Returns
23/// (Tuple 'jerk_m__s3': Num, 'accel_m__s2': Num)
24/// - Constant jerk and acceleration for initial time step.
25pub fn calc_constant_jerk_trajectory(
26    n: usize,
27    d0: f64,
28    v0: f64,
29    dr: f64,
30    vr: f64,
31    dt: f64,
32) -> anyhow::Result<(f64, f64)> {
33    ensure!(n > 1);
34    ensure!(dr > d0);
35    let n = n as f64;
36    let ddr = dr - d0;
37    let dvr = vr - v0;
38    let k = (dvr - (2.0 * ddr / (n * dt)) + 2.0 * v0)
39        / (0.5 * n * (n - 1.0) * dt
40            - (1.0 / 3.0) * (n - 1.0) * (n - 2.0) * dt
41            - 0.5 * (n - 1.0) * dt * dt);
42    let a0 = ((ddr / dt)
43        - n * v0
44        - ((1.0 / 6.0) * n * (n - 1.0) * (n - 2.0) * dt + 0.25 * n * (n - 1.0) * dt * dt) * k)
45        / (0.5 * n * n * dt);
46    Ok((k, a0))
47}
48
49#[cfg_attr(feature = "pyo3", pyfunction)]
50/// Calculate distance (m) after n timesteps
51///
52/// INPUTS:
53/// - n: Int, numer of timesteps away to calculate
54/// - d0: Num, initial distance (m)
55/// - v0: Num, initial speed (m/s)
56/// - a0: Num, initial acceleration (m/s2)
57/// - k: Num, constant jerk
58/// - dt: Num, duration of a timestep (s)
59///
60/// NOTE:
61/// - this is the distance traveled from start (i.e., n=0) measured at sample point n
62/// RETURN: Num, the distance at n timesteps away (m)
63pub fn dist_for_constant_jerk(n: usize, d0: f64, v0: f64, a0: f64, k: f64, dt: f64) -> f64 {
64    let n = n as f64;
65    let term1 = dt
66        * ((n * v0)
67            + (0.5 * n * (n - 1.0) * a0 * dt)
68            + ((1.0 / 6.0) * k * dt * (n - 2.0) * (n - 1.0) * n));
69    let term2 = 0.5 * dt * dt * ((n * a0) + (0.5 * n * (n - 1.0) * k * dt));
70    d0 + term1 + term2
71}
72
73#[cfg_attr(feature = "pyo3", pyfunction)]
74/// Calculate speed (m/s) n timesteps away via a constant-jerk acceleration
75///
76/// INPUTS:
77/// - n: Int, numer of timesteps away to calculate
78/// - v0: Num, initial speed (m/s)
79/// - a0: Num, initial acceleration (m/s2)
80/// - k: Num, constant jerk
81/// - dt: Num, duration of a timestep (s)
82///
83/// NOTE:
84/// - this is the speed at sample n
85/// - if n == 0, speed is v0
86/// - if n == 1, speed is v0 + a0*dt, etc.
87///
88/// RETURN: Num, the speed n timesteps away (m/s)
89pub fn speed_for_constant_jerk(n: usize, v0: f64, a0: f64, k: f64, dt: f64) -> f64 {
90    let n = n as f64;
91    v0 + (n * a0 * dt) + (0.5 * n * (n - 1.0) * k * dt)
92}
93
94#[cfg_attr(feature = "pyo3", pyfunction)]
95/// Calculate the acceleration n timesteps away
96///
97/// INPUTS:
98/// - n: Int, number of times steps away to calculate
99/// - a0: Num, initial acceleration (m/s2)
100/// - k: Num, constant jerk (m/s3)
101/// - dt: Num, time-step duration in seconds
102///
103/// NOTE:
104/// - this is the constant acceleration over the time-step from sample n to sample n+1
105///
106/// RETURN: Num, the acceleration n timesteps away (m/s2)
107pub fn accel_for_constant_jerk(n: usize, a0: f64, k: f64, dt: f64) -> f64 {
108    let n = n as f64;
109    a0 + (n * k * dt)
110}
111
112/// Apply `accel_for_constant_jerk` to full
113pub fn accel_array_for_constant_jerk(nmax: usize, a0: f64, k: f64, dt: f64) -> Array1<f64> {
114    let mut accels = Vec::new();
115    for n in 0..nmax {
116        accels.push(accel_for_constant_jerk(n, a0, k, dt));
117    }
118    Array1::from_vec(accels)
119}
120
121/// Calculate the average speed per each step in m/s
122pub fn average_step_speeds(cyc: &RustCycle) -> Array1<f64> {
123    let mut result = Vec::with_capacity(cyc.len());
124    result.push(0.0);
125    for i in 1..cyc.len() {
126        result.push(0.5 * (cyc.mps[i] + cyc.mps[i - 1]));
127    }
128    Array1::from_vec(result)
129}
130
131/// Calculate the average step speed at step i in m/s
132/// (i.e., from sample point i-1 to i)
133pub fn average_step_speed_at(cyc: &RustCycle, i: usize) -> f64 {
134    0.5 * (cyc.mps[i] + cyc.mps[i - 1])
135}
136
137/// Sum of the distance traveled over each step using
138/// trapezoidal integration
139pub fn trapz_step_distances(cyc: &RustCycle) -> Array1<f64> {
140    average_step_speeds(cyc) * cyc.dt_s()
141}
142
143pub fn trapz_step_distances_primitive(time_s: &Array1<f64>, mps: &Array1<f64>) -> Array1<f64> {
144    let mut delta_dists_m = Vec::with_capacity(time_s.len());
145    delta_dists_m.push(0.0);
146    for i in 1..time_s.len() {
147        delta_dists_m.push((time_s[i] - time_s[i - 1]) * 0.5 * (mps[i] + mps[i - 1]));
148    }
149    Array1::from_vec(delta_dists_m)
150}
151
152/// The distance traveled from start at the beginning of step i
153/// (i.e., distance traveled up to sample point i-1)
154/// Distance is in meters.
155pub fn trapz_step_start_distance(cyc: &RustCycle, i: usize) -> f64 {
156    let mut dist_m = 0.0;
157    for i in 1..i {
158        dist_m += (cyc.time_s[i] - cyc.time_s[i - 1]) * 0.5 * (cyc.mps[i] + cyc.mps[i - 1]);
159    }
160    dist_m
161}
162
163/// The distance traveled during step i in meters
164/// (i.e., from sample point i-1 to i)
165pub fn trapz_distance_for_step(cyc: &RustCycle, i: usize) -> f64 {
166    average_step_speed_at(cyc, i) * cyc.dt_s_at_i(i)
167}
168
169/// Calculate the distance from step i_start to the start of step i_end
170/// (i.e., distance from sample point i_start-1 to i_end-1)
171pub fn trapz_distance_over_range(cyc: &RustCycle, i_start: usize, i_end: usize) -> f64 {
172    trapz_step_distances(cyc).slice(s![i_start..i_end]).sum()
173}
174
175/// Calculate the time in a cycle spent moving
176/// - stopped_speed_m_per_s: the speed above which we are considered to be moving
177///
178/// RETURN: the time spent moving in seconds
179pub fn time_spent_moving(cyc: &RustCycle, stopped_speed_m_per_s: Option<f64>) -> f64 {
180    let mut t_move_s = 0.0;
181    let stopped_speed_m_per_s = stopped_speed_m_per_s.unwrap_or(0.0);
182    for idx in 1..cyc.len() {
183        let dt = cyc.time_s[idx] - cyc.time_s[idx - 1];
184        let vavg = (cyc.mps[idx] + cyc.mps[idx - 1]) / 2.0;
185        if vavg > stopped_speed_m_per_s {
186            t_move_s += dt;
187        }
188    }
189    t_move_s
190}
191
192/// Split a cycle into an array of microtrips with one microtrip being a start
193/// to subsequent stop plus any idle (stopped time).
194/// Arguments:
195/// ----------
196/// cycle: drive cycle
197/// stop_speed_m__s: speed at which vehicle is considered stopped for trip
198///     separation
199/// keep_name: (optional) bool, if True and cycle contains "name", adds
200///     that name to all microtrips
201pub fn to_microtrips(cycle: &RustCycle, stop_speed_m_per_s: Option<f64>) -> Vec<RustCycle> {
202    let stop_speed_m_per_s = stop_speed_m_per_s.unwrap_or(1e-6);
203    let mut microtrips = Vec::new();
204    let ts = cycle.time_s.to_vec();
205    let vs = cycle.mps.to_vec();
206    let gs = cycle.grade.to_vec();
207    let rs = cycle.road_type.to_vec();
208    let mut mt_ts = Vec::new();
209    let mut mt_vs = Vec::new();
210    let mut mt_gs = Vec::new();
211    let mut mt_rs = Vec::new();
212    let mut moving = false;
213    for idx in 0..ts.len() {
214        let t = ts[idx];
215        let v = vs[idx];
216        let g = gs[idx];
217        let r = rs[idx];
218        if v > stop_speed_m_per_s && !moving && mt_ts.len() > 1 {
219            let last_idx = mt_ts.len() - 1;
220            let last_t = mt_ts[last_idx];
221            let last_v = mt_vs[last_idx];
222            let last_g = mt_gs[last_idx];
223            let last_r = mt_rs[last_idx];
224            mt_ts = mt_ts.iter().map(|t| -> f64 { t - mt_ts[0] }).collect();
225            microtrips.push(RustCycle {
226                time_s: Array::from_vec(mt_ts),
227                mps: Array::from_vec(mt_vs),
228                grade: Array::from_vec(mt_gs),
229                road_type: Array::from_vec(mt_rs),
230                name: cycle.name.clone(),
231                orphaned: false,
232            });
233            mt_ts = vec![last_t];
234            mt_vs = vec![last_v];
235            mt_gs = vec![last_g];
236            mt_rs = vec![last_r];
237        }
238        mt_ts.push(t);
239        mt_vs.push(v);
240        mt_gs.push(g);
241        mt_rs.push(r);
242        moving = v > stop_speed_m_per_s;
243    }
244    if !mt_ts.is_empty() {
245        mt_ts = mt_ts.iter().map(|t| -> f64 { t - mt_ts[0] }).collect();
246        microtrips.push(RustCycle {
247            time_s: Array::from_vec(mt_ts),
248            mps: Array::from_vec(mt_vs),
249            grade: Array::from_vec(mt_gs),
250            road_type: Array::from_vec(mt_rs),
251            name: cycle.name.clone(),
252            orphaned: false,
253        });
254    }
255    microtrips
256}
257
258/// Create distance and target speeds by microtrip
259/// This helper function splits a cycle up into microtrips and returns a list of 2-tuples of:
260/// (distance from start in meters, target speed in meters/second)
261/// - cyc: the cycle to operate on
262/// - blend_factor: float, from 0 to 1
263///     if 0, use average speed of the microtrip
264///     if 1, use average speed while moving (i.e., no stopped time)
265///     else something in between
266/// - min_target_speed_mps: float, the minimum target speed allowed (m/s)
267///
268/// RETURN: list of 2-tuple of (float, float) representing the distance of start of
269///     each microtrip and target speed for that microtrip
270///
271/// NOTE: target speed per microtrip is not allowed to be below min_target_speed_mps
272pub fn create_dist_and_target_speeds_by_microtrip(
273    cyc: &RustCycle,
274    blend_factor: f64,
275    min_target_speed_mps: f64,
276) -> Vec<(f64, f64)> {
277    let blend_factor = blend_factor.clamp(0.0, 1.0);
278    let mut dist_and_tgt_speeds = Vec::new();
279    // Split cycle into microtrips
280    let microtrips = to_microtrips(cyc, None);
281    let mut dist_at_start_of_microtrip_m = 0.0;
282    for mt_cyc in microtrips {
283        let mt_dist_m = mt_cyc.dist_m().sum();
284        let mt_time_s = mt_cyc.time_s.last().unwrap() - mt_cyc.time_s.first().unwrap();
285        let mt_moving_time_s = time_spent_moving(&mt_cyc, None);
286        let mt_avg_spd_m_per_s = if mt_time_s > 0.0 {
287            mt_dist_m / mt_time_s
288        } else {
289            0.0
290        };
291        let mt_moving_avg_spd_m_per_s = if mt_moving_time_s > 0.0 {
292            mt_dist_m / mt_moving_time_s
293        } else {
294            0.0
295        };
296        let mt_target_spd_m_per_s =
297            (blend_factor * (mt_moving_avg_spd_m_per_s - mt_avg_spd_m_per_s) + mt_avg_spd_m_per_s)
298                .min(mt_moving_avg_spd_m_per_s)
299                .max(mt_avg_spd_m_per_s);
300        if mt_dist_m > 0.0 {
301            dist_and_tgt_speeds.push((
302                dist_at_start_of_microtrip_m,
303                mt_target_spd_m_per_s.max(min_target_speed_mps),
304            ));
305            dist_at_start_of_microtrip_m += mt_dist_m;
306        }
307    }
308    dist_and_tgt_speeds
309}
310
311/// - cyc: fastsim.cycle.Cycle
312/// - absolute_time_s: float, the seconds to extend
313/// - time_fraction: float, the fraction of the original cycle time to add on
314/// - use_rust: bool, if True, return a RustCycle instance, else a normal Python Cycle
315/// RETURNS: fastsim.cycle.Cycle (or fastsimrust.RustCycle), the new cycle with stopped time appended
316/// NOTE: additional time is rounded to the nearest second
317pub fn extend_cycle(
318    cyc: &RustCycle,
319    absolute_time_s: Option<f64>, // =0.0,
320    time_fraction: Option<f64>,   // =0.0,
321) -> RustCycle {
322    let absolute_time_s = absolute_time_s.unwrap_or(0.0);
323    let time_fraction = time_fraction.unwrap_or(0.0);
324    let mut ts = cyc.time_s.to_vec();
325    let mut vs = cyc.mps.to_vec();
326    let mut gs = cyc.grade.to_vec();
327    let mut rs = cyc.road_type.to_vec();
328    let extra_time_s = (absolute_time_s + (time_fraction * ts.last().unwrap())).round() as i32;
329    if extra_time_s == 0 {
330        return cyc.clone();
331    }
332    let dt = 1;
333    let t_end = *ts.last().unwrap();
334    let mut idx = 1;
335    while dt * idx <= extra_time_s {
336        let dt_extra = (dt * idx) as f64;
337        ts.push(t_end + dt_extra);
338        vs.push(0.0);
339        gs.push(0.0);
340        rs.push(0.0);
341        idx += 1;
342    }
343    RustCycle {
344        time_s: Array::from_vec(ts),
345        mps: Array::from_vec(vs),
346        grade: Array::from_vec(gs),
347        road_type: Array::from_vec(rs),
348        name: cyc.name.clone(),
349        orphaned: false,
350    }
351}
352
353#[cfg(feature = "pyo3")]
354#[allow(unused)]
355pub fn register(_py: Python<'_>, m: &Bound<PyModule>) -> anyhow::Result<()> {
356    m.add_function(wrap_pyfunction!(calc_constant_jerk_trajectory, m)?)?;
357    m.add_function(wrap_pyfunction!(accel_for_constant_jerk, m)?)?;
358    m.add_function(wrap_pyfunction!(speed_for_constant_jerk, m)?)?;
359    m.add_function(wrap_pyfunction!(dist_for_constant_jerk, m)?)?;
360    Ok(())
361}
362
363#[derive(Default, PartialEq, Clone, Debug, Deserialize, Serialize)]
364pub struct RustCycleElement {
365    /// time [s]
366    #[serde(alias = "cycSecs")]
367    pub time_s: f64,
368    /// speed [m/s]
369    #[serde(alias = "cycMps")]
370    pub mps: f64,
371    /// grade [rise/run]
372    #[serde(alias = "cycGrade")]
373    pub grade: Option<f64>,
374    /// max possible charge rate from roadway
375    #[serde(alias = "cycRoadType")]
376    pub road_type: Option<f64>,
377}
378
379#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
380#[add_pyo3_api(
381    #[new]
382    pub fn __new__(
383        cyc: &RustCycle,
384    ) -> Self {
385        Self::new(cyc)
386    }
387)]
388pub struct RustCycleCache {
389    pub grade_all_zero: bool,
390    pub trapz_step_distances_m: Array1<f64>,
391    pub trapz_distances_m: Array1<f64>,
392    pub trapz_elevations_m: Array1<f64>,
393    pub stops: Array1<bool>,
394    interp_ds: Array1<f64>,
395    interp_is: Array1<f64>,
396    interp_hs: Array1<f64>,
397    grades: Array1<f64>,
398}
399
400impl SerdeAPI for RustCycleCache {}
401
402impl RustCycleCache {
403    pub fn new(cyc: &RustCycle) -> Self {
404        let tol = 1e-6;
405        let num_items = cyc.len();
406        let grade_all_zero = cyc.grade.iter().all(|g| *g == 0.0);
407        let trapz_step_distances_m = trapz_step_distances(cyc);
408        let trapz_distances_m = ndarrcumsum(&trapz_step_distances_m);
409        let trapz_elevations_m = if grade_all_zero {
410            Array::zeros(num_items)
411        } else {
412            let xs = Array::from_iter(
413                cyc.grade
414                    .iter()
415                    .zip(&trapz_step_distances_m)
416                    .map(|(g, dd)| g.atan().cos() * dd * g),
417            );
418            ndarrcumsum(&xs)
419        };
420        let stops = Array::from_iter(cyc.mps.iter().map(|v| v <= &tol));
421        let mut interp_ds = Vec::with_capacity(num_items);
422        let mut interp_is = Vec::with_capacity(num_items);
423        let mut interp_hs = Vec::with_capacity(num_items);
424        for idx in 0..num_items {
425            let d = trapz_distances_m[idx];
426            if interp_ds.is_empty() || d > *interp_ds.last().unwrap() {
427                interp_ds.push(d);
428                interp_is.push(idx as f64);
429                interp_hs.push(trapz_elevations_m[idx]);
430            }
431        }
432        let interp_ds = Array::from_vec(interp_ds);
433        let interp_is = Array::from_vec(interp_is);
434        let interp_hs = Array::from_vec(interp_hs);
435        Self {
436            grade_all_zero,
437            trapz_step_distances_m,
438            trapz_distances_m,
439            trapz_elevations_m,
440            stops,
441            interp_ds,
442            interp_is,
443            interp_hs,
444            grades: cyc.grade.clone(),
445        }
446    }
447
448    /// Interpolate the single-point grade at the given distance.
449    /// Assumes that the grade at i applies from sample point (i-1, i]
450    pub fn interp_grade(&self, dist_m: f64) -> anyhow::Result<f64> {
451        let v = if self.grade_all_zero {
452            0.0
453        } else if dist_m <= self.interp_ds[0] {
454            self.grades[0]
455        } else if dist_m > *self.interp_ds.last().unwrap() {
456            *self.grades.last().unwrap()
457        } else {
458            let raw_idx = interpolate(&dist_m, &self.interp_ds, &self.interp_is, false)
459                .with_context(|| format_dbg!())?;
460            let idx = raw_idx.ceil() as usize;
461            self.grades[idx]
462        };
463        Ok(v)
464    }
465
466    /// Interpolate the elevation at the given distance
467    pub fn interp_elevation(&self, dist_m: f64) -> anyhow::Result<f64> {
468        if self.grade_all_zero {
469            Ok(0.0)
470        } else {
471            interpolate(&dist_m, &self.interp_ds, &self.interp_hs, false)
472                .with_context(|| format_dbg!())
473        }
474    }
475}
476
477#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
478#[add_pyo3_api(
479    pub fn __len__(&self) -> usize {
480        self.len()
481    }
482
483    #[allow(clippy::type_complexity)]
484    pub fn __getnewargs__(&self) -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>, &str) {
485        (self.time_s.to_vec(), self.mps.to_vec(), self.grade.to_vec(), self.road_type.to_vec(), &self.name)
486    }
487
488    #[staticmethod]
489    #[pyo3(name = "from_csv")]
490    #[pyo3(signature = (filepath, skip_init=None))]
491    pub fn from_csv_py(filepath: &Bound<PyAny>, skip_init: Option<bool>) -> anyhow::Result<Self> {
492        Self::from_csv_file(PathBuf::extract_bound(filepath)?, skip_init.unwrap_or_default())
493    }
494
495    pub fn to_rust(&self) -> Self {
496        self.clone()
497    }
498
499    #[staticmethod]
500    #[pyo3(signature = (dict, skip_init=None))]
501    pub fn from_dict(dict: &Bound<PyDict>, skip_init: Option<bool>) -> PyResult<Self> {
502        let time_s = Array::from_vec(dict.get_item("time_s")?.with_context(|| format_dbg!())?.extract()?);
503        let cyc_len = time_s.len();
504        let mut cyc = Self {
505            time_s,
506            mps: Array::from_vec(dict.get_item("mps")?.with_context(|| format_dbg!())?.extract()?),
507            grade: if let Ok(Some(item_res)) = dict.get_item("grade") {
508                if let Ok(grade) = item_res.extract() {
509                    Array::from_vec(grade)
510                } else {
511                    Array::default(cyc_len)
512                }
513            } else {
514                Array::default(cyc_len)
515            },
516            road_type: if let Ok(Some(item_res)) = dict.get_item("road_type") {
517                if let Ok(road_type) = item_res.extract() {
518                    Array::from_vec(road_type)
519                } else {
520                    Array::default(cyc_len)
521                }
522            } else {
523                Array::default(cyc_len)
524            },
525            name: if let Ok(Some(item_res)) = dict.get_item("name") {
526                String::extract_bound(&item_res).unwrap_or_default()
527            } else {
528                Default::default()
529            },
530            orphaned: false,
531        };
532        if !skip_init.unwrap_or_default() {
533            cyc.init()?;
534        }
535        Ok(cyc)
536    }
537
538    pub fn to_dict<'py>(&self, py: Python<'py>) -> anyhow::Result<Bound<'py, PyDict>> {
539        let dict = PyDict::new(py);
540        dict.set_item("time_s", self.time_s.to_vec())?;
541        dict.set_item("mps", self.mps.to_vec())?;
542        dict.set_item("grade", self.grade.to_vec())?;
543        dict.set_item("road_type", self.road_type.to_vec())?;
544        dict.set_item("name", self.name.clone())?;
545        Ok(dict)
546    }
547
548    #[pyo3(name = "to_csv")]
549    pub fn to_csv_py(&self) -> PyResult<String> {
550        self.to_csv().map_err(|e| PyIOError::new_err(format!("{:?}", e)))
551    }
552
553    #[pyo3(name = "modify_by_const_jerk_trajectory")]
554    pub fn modify_by_const_jerk_trajectory_py(
555        &mut self,
556        idx: usize,
557        n: usize,
558        jerk_m_per_s3: f64,
559        accel0_m_per_s2: f64,
560    ) -> f64 {
561        self.modify_by_const_jerk_trajectory(idx, n, jerk_m_per_s3, accel0_m_per_s2)
562    }
563
564    #[pyo3(name = "modify_with_braking_trajectory")]
565    #[pyo3(signature = (brake_accel_m_per_s2, idx, dts_m=None))]
566    pub fn modify_with_braking_trajectory_py(
567        &mut self,
568        brake_accel_m_per_s2: f64,
569        idx: usize,
570        dts_m: Option<f64>
571    ) -> anyhow::Result<(f64, usize)> {
572        self.modify_with_braking_trajectory(brake_accel_m_per_s2, idx, dts_m)
573    }
574
575    #[pyo3(name = "calc_distance_to_next_stop_from")]
576    pub fn calc_distance_to_next_stop_from_py(&self, distance_m: f64) -> f64 {
577        self.calc_distance_to_next_stop_from(distance_m, None)
578    }
579
580    #[pyo3(name = "average_grade_over_range")]
581    pub fn average_grade_over_range_py(
582        &self,
583        distance_start_m: f64,
584        delta_distance_m: f64,
585    ) -> anyhow::Result<f64> {
586        self.average_grade_over_range(distance_start_m, delta_distance_m, None)
587    }
588
589    #[pyo3(name = "build_cache")]
590    pub fn build_cache_py(&self) -> RustCycleCache {
591        self.build_cache()
592    }
593
594    #[pyo3(name = "dt_s_at_i")]
595    pub fn dt_s_at_i_py(&self, i: usize) -> f64 {
596        if i == 0 {
597            0.0
598        } else {
599            self.dt_s_at_i(i)
600        }
601    }
602
603    #[getter]
604    pub fn get_mph(&self) -> Vec<f64> {
605        (&self.mps * crate::params::MPH_PER_MPS).to_vec()
606    }
607    #[setter]
608    pub fn set_mph(&mut self, new_value: Vec<f64>) {
609        self.mps = Array::from_vec(new_value) / MPH_PER_MPS;
610    }
611    #[getter]
612    /// array of time steps
613    pub fn get_dt_s(&self) -> Vec<f64> {
614        self.dt_s().to_vec()
615    }
616    #[getter]
617    /// distance for each time step based on final speed
618    pub fn get_dist_m(&self) -> Vec<f64> {
619        self.dist_m().to_vec()
620    }
621    #[getter]
622    pub fn get_delta_elev_m(&self) -> Vec<f64> {
623        self.delta_elev_m().to_vec()
624    }
625
626    #[pyo3(name = "list_resources")]
627    /// list available cycle resources
628    pub fn list_resources_py(&self) -> Vec<String> {
629        RustCycle::list_resources()
630    }
631)]
632/// Struct for containing:
633/// * time_s, cycle time, $s$
634/// * mps, vehicle speed, $\frac{m}{s}$
635/// * grade, road grade/slope, $\frac{rise}{run}$
636/// * road_type, $kW$
637/// * legacy, will likely change to road charging capacity
638///    * Another sublist.
639pub struct RustCycle {
640    /// array of time [s]
641    #[serde(alias = "cycSecs")]
642    pub time_s: Array1<f64>,
643    /// array of speed [m/s]
644    #[serde(alias = "cycMps")]
645    pub mps: Array1<f64>,
646    /// array of grade [rise/run]
647    #[serde(alias = "cycGrade")]
648    #[serde(default)]
649    pub grade: Array1<f64>,
650    /// array of max possible charge rate from roadway
651    #[serde(alias = "cycRoadType")]
652    #[serde(default)]
653    pub road_type: Array1<f64>,
654    pub name: String,
655    #[serde(skip)]
656    pub orphaned: bool,
657}
658
659impl SerdeAPI for RustCycle {
660    const ACCEPTED_BYTE_FORMATS: &'static [&'static str] = &["yaml", "json", "toml", "bin", "csv"];
661    const ACCEPTED_STR_FORMATS: &'static [&'static str] = &["yaml", "json", "toml", "csv"];
662    const RESOURCE_PREFIX: &'static str = "cycles";
663    const CACHE_FOLDER: &'static str = "cycles";
664
665    fn init(&mut self) -> anyhow::Result<()> {
666        self.init_checks()
667    }
668
669    fn to_writer<W: std::io::Write>(&self, mut wtr: W, format: &str) -> anyhow::Result<()> {
670        match format.trim_start_matches('.').to_lowercase().as_str() {
671            "yaml" | "yml" => serde_yaml::to_writer(wtr, self)?,
672            "json" => serde_json::to_writer(wtr, self)?,
673            "toml" => wtr.write_all(self.to_toml()?.as_bytes())?,
674            #[cfg(feature = "bincode")]
675            "bin" => bincode::serialize_into(wtr, self)?,
676            "csv" => {
677                let mut wtr = csv::Writer::from_writer(wtr);
678                for i in 0..self.len() {
679                    wtr.serialize(RustCycleElement {
680                        time_s: self.time_s[i],
681                        mps: self.mps[i],
682                        grade: Some(self.grade[i]),
683                        road_type: Some(self.road_type[i]),
684                    })?;
685                }
686                wtr.flush()?
687            }
688            _ => bail!(
689                "Unsupported format {format:?}, must be one of {:?}",
690                Self::ACCEPTED_BYTE_FORMATS
691            ),
692        }
693        Ok(())
694    }
695
696    fn to_str(&self, format: &str) -> anyhow::Result<String> {
697        Ok(
698            match format.trim_start_matches('.').to_lowercase().as_str() {
699                "yaml" | "yml" => self.to_yaml()?,
700                "json" => self.to_json()?,
701                "toml" => self.to_toml()?,
702                "csv" => self.to_csv()?,
703                _ => {
704                    bail!(
705                        "Unsupported format {format:?}, must be one of {:?}",
706                        Self::ACCEPTED_STR_FORMATS
707                    )
708                }
709            },
710        )
711    }
712
713    /// Note that using this method to instantiate a RustCycle from CSV, rather
714    /// than the `from_csv_str` method, sets the cycle name to an empty string
715    fn from_str<S: AsRef<str>>(contents: S, format: &str, skip_init: bool) -> anyhow::Result<Self> {
716        Ok(
717            match format.trim_start_matches('.').to_lowercase().as_str() {
718                "yaml" | "yml" => Self::from_yaml(contents, skip_init)?,
719                "json" => Self::from_json(contents, skip_init)?,
720                "toml" => Self::from_toml(contents, skip_init)?,
721                "csv" => Self::from_reader(contents.as_ref().as_bytes(), "csv", skip_init)?,
722                _ => bail!(
723                    "Unsupported format {format:?}, must be one of {:?}",
724                    Self::ACCEPTED_STR_FORMATS
725                ),
726            },
727        )
728    }
729
730    fn from_reader<R: std::io::Read>(
731        mut rdr: R,
732        format: &str,
733        skip_init: bool,
734    ) -> anyhow::Result<Self> {
735        let mut deserialized = match format.trim_start_matches('.').to_lowercase().as_str() {
736            "yaml" | "yml" => serde_yaml::from_reader(rdr)?,
737            "json" => serde_json::from_reader(rdr)?,
738            "toml" => {
739                let mut buf = String::new();
740                rdr.read_to_string(&mut buf)?;
741                Self::from_toml(buf, skip_init)?
742            }
743            #[cfg(feature = "bincode")]
744            "bin" => bincode::deserialize_from(rdr)?,
745            "csv" => {
746                // Create empty cycle to be populated
747                let mut cyc = Self::default();
748                let mut rdr = csv::Reader::from_reader(rdr);
749                for result in rdr.deserialize() {
750                    cyc.push(result?);
751                }
752                cyc
753            }
754            _ => {
755                bail!(
756                    "Unsupported format {format:?}, must be one of {:?}",
757                    Self::ACCEPTED_BYTE_FORMATS
758                )
759            }
760        };
761        if !skip_init {
762            deserialized.init()?;
763        }
764        Ok(deserialized)
765    }
766}
767
768impl TryFrom<HashMap<String, Vec<f64>>> for RustCycle {
769    type Error = anyhow::Error;
770
771    fn try_from(hashmap: HashMap<String, Vec<f64>>) -> anyhow::Result<Self> {
772        let time_s = Array::from_vec(
773            hashmap
774                .get("time_s")
775                .with_context(|| format!("`time_s` not in HashMap: {hashmap:?}"))?
776                .to_owned(),
777        );
778        let cyc_len = time_s.len();
779        let mut cyc = Self {
780            time_s,
781            mps: Array::from_vec(
782                hashmap
783                    .get("mps")
784                    .with_context(|| format!("`mps` not in HashMap: {hashmap:?}"))?
785                    .to_owned(),
786            ),
787            grade: Array::from_vec(
788                hashmap
789                    .get("grade")
790                    .unwrap_or(&vec![0.0; cyc_len])
791                    .to_owned(),
792            ),
793            road_type: Array::from_vec(
794                hashmap
795                    .get("road_type")
796                    .unwrap_or(&vec![0.0; cyc_len])
797                    .to_owned(),
798            ),
799            name: String::default(),
800            orphaned: false,
801        };
802        cyc.init()?;
803        Ok(cyc)
804    }
805}
806
807impl From<RustCycle> for HashMap<String, Vec<f64>> {
808    fn from(cyc: RustCycle) -> Self {
809        HashMap::from([
810            ("time_s".into(), cyc.time_s.to_vec()),
811            ("mps".into(), cyc.mps.to_vec()),
812            ("grade".into(), cyc.grade.to_vec()),
813            ("road_type".into(), cyc.road_type.to_vec()),
814        ])
815    }
816}
817
818/// pure Rust methods that need to be separate due to pymethods incompatibility
819impl RustCycle {
820    fn init_checks(&self) -> anyhow::Result<()> {
821        ensure!(!self.is_empty(), "Deserialized cycle is empty");
822        ensure!(self.is_sorted(), "Deserialized cycle is not sorted in time");
823        ensure!(
824            self.are_fields_equal_length(),
825            "Deserialized cycle has unequal field lengths\ntime_s: {}\nmps: {}\ngrade: {}\nroad_type: {}",
826            self.time_s.len(),
827            self.mps.len(),
828            self.grade.len(),
829            self.road_type.len(),
830        );
831        Ok(())
832    }
833
834    /// Load cycle from CSV file, parsing name from filepath
835    pub fn from_csv_file<P: AsRef<Path>>(filepath: P, skip_init: bool) -> anyhow::Result<Self> {
836        let filepath = filepath.as_ref();
837        let name = filepath
838            .file_stem()
839            .and_then(OsStr::to_str)
840            .with_context(|| format!("Could not parse cycle name from filepath: {filepath:?}"))?
841            .to_string();
842        let mut cyc = Self::from_file(filepath, skip_init)?;
843        cyc.name = name;
844        Ok(cyc)
845    }
846
847    /// Load cycle from CSV string
848    pub fn from_csv_str<S: AsRef<str>>(
849        csv_str: S,
850        name: String,
851        skip_init: bool,
852    ) -> anyhow::Result<Self> {
853        let mut cyc = Self::from_str(csv_str, "csv", skip_init)?;
854        cyc.name = name;
855        Ok(cyc)
856    }
857
858    /// Write (serialize) cycle to a CSV string
859    pub fn to_csv(&self) -> anyhow::Result<String> {
860        let mut buf = Vec::with_capacity(self.len());
861        self.to_writer(&mut buf, "csv")?;
862        Ok(String::from_utf8(buf)?)
863    }
864
865    pub fn build_cache(&self) -> RustCycleCache {
866        RustCycleCache::new(self)
867    }
868
869    pub fn push(&mut self, cyc_elem: RustCycleElement) {
870        self.time_s
871            .append(Axis(0), array![cyc_elem.time_s].view())
872            .unwrap();
873        self.mps
874            .append(Axis(0), array![cyc_elem.mps].view())
875            .unwrap();
876        if let Some(grade) = cyc_elem.grade {
877            self.grade.append(Axis(0), array![grade].view()).unwrap();
878        }
879        if let Some(road_type) = cyc_elem.road_type {
880            self.road_type
881                .append(Axis(0), array![road_type].view())
882                .unwrap();
883        }
884    }
885
886    pub fn len(&self) -> usize {
887        self.time_s.len()
888    }
889
890    pub fn is_empty(&self) -> bool {
891        self.len() == 0
892    }
893
894    pub fn is_sorted(&self) -> bool {
895        self.time_s
896            .as_slice()
897            .unwrap()
898            .windows(2)
899            .all(|window| window[0] < window[1])
900    }
901
902    pub fn are_fields_equal_length(&self) -> bool {
903        let cyc_len = self.len();
904        [self.mps.len(), self.grade.len(), self.road_type.len()]
905            .iter()
906            .all(|len| len == &cyc_len)
907    }
908
909    pub fn test_cyc() -> Self {
910        Self {
911            time_s: Array::range(0.0, 10.0, 1.0),
912            mps: Array::range(0.0, 10.0, 1.0),
913            grade: Array::zeros(10),
914            road_type: Array::zeros(10),
915            name: String::from("test"),
916            orphaned: false,
917        }
918    }
919
920    /// Returns the average grade over the given range of distances
921    /// - distance_start_m: non-negative-number, the distance at start of evaluation area (m)
922    /// - delta_distance_m: non-negative-number, the distance traveled from distance_start_m (m)
923    /// RETURN: number, the average grade (rise over run) over the given distance range
924    /// Note: grade is assumed to be constant from just after the previous sample point
925    /// until the current sample point. That is, grade\[i\] applies over the range of
926    /// distances, d, from (d\[i - 1\], d\[i\])
927    pub fn average_grade_over_range(
928        &self,
929        distance_start_m: f64,
930        delta_distance_m: f64,
931        cache: Option<&RustCycleCache>,
932    ) -> anyhow::Result<f64> {
933        let tol = 1e-6;
934        match &cache {
935            Some(rcc) => {
936                let v = if rcc.grade_all_zero {
937                    0.0
938                } else if delta_distance_m <= tol {
939                    rcc.interp_grade(distance_start_m)?
940                } else {
941                    let e0 = rcc.interp_elevation(distance_start_m)?;
942                    let e1 = rcc.interp_elevation(distance_start_m + delta_distance_m)?;
943                    ((e1 - e0) / delta_distance_m).asin().tan()
944                };
945                Ok(v)
946            }
947            None => {
948                let grade_all_zero = {
949                    let mut all0 = true;
950                    for idx in 0..self.len() {
951                        if self.grade[idx] != 0.0 {
952                            all0 = false;
953                            break;
954                        }
955                    }
956                    all0
957                };
958                let v = if grade_all_zero {
959                    0.0
960                } else {
961                    let delta_dists = trapz_step_distances(self);
962                    let trapz_distances_m = ndarrcumsum(&delta_dists);
963                    if delta_distance_m <= tol {
964                        if distance_start_m <= trapz_distances_m[0] {
965                            return Ok(self.grade[0]);
966                        }
967                        let max_idx = self.len() - 1;
968                        if distance_start_m > trapz_distances_m[max_idx] {
969                            return Ok(self.grade[max_idx]);
970                        }
971                        for idx in 1..self.time_s.len() {
972                            if distance_start_m > trapz_distances_m[idx - 1]
973                                && distance_start_m <= trapz_distances_m[idx]
974                            {
975                                return Ok(self.grade[idx]);
976                            }
977                        }
978                        self.grade[max_idx]
979                    } else {
980                        // NOTE: we use the following instead of delta_elev_m in order to use
981                        // more precise trapezoidal distance and elevation at sample points.
982                        // This also uses the fully accurate trig functions in case we have large slope angles.
983                        let trapz_elevations_m = ndarrcumsum(
984                            &(self.grade.mapv(|g| g.atan().cos()) * delta_dists * &self.grade),
985                        );
986                        let e0 = interpolate(
987                            &distance_start_m,
988                            &trapz_distances_m,
989                            &trapz_elevations_m,
990                            false,
991                        )
992                        .with_context(|| format_dbg!())?;
993                        let e1 = interpolate(
994                            &(distance_start_m + delta_distance_m),
995                            &trapz_distances_m,
996                            &trapz_elevations_m,
997                            false,
998                        )
999                        .with_context(|| format_dbg!())?;
1000                        ((e1 - e0) / delta_distance_m).asin().tan()
1001                    }
1002                };
1003                Ok(v)
1004            }
1005        }
1006    }
1007
1008    /// Calculate the distance to next stop from `distance_m`
1009    /// - distance_m: non-negative-number, the current distance from start (m)
1010    /// # RETURN: returns the distance to the next stop from distance_m
1011    /// # NOTE: distance may be negative if we're beyond the last stop
1012    pub fn calc_distance_to_next_stop_from(
1013        &self,
1014        distance_m: f64,
1015        cache: Option<&RustCycleCache>,
1016    ) -> f64 {
1017        let tol = 1e-6;
1018        match cache {
1019            Some(rcc) => {
1020                for (&dist, &v) in rcc.trapz_distances_m.iter().zip(self.mps.iter()) {
1021                    if (v < tol) && (dist > (distance_m + tol)) {
1022                        return dist - distance_m;
1023                    }
1024                }
1025                rcc.trapz_distances_m.last().unwrap() - distance_m
1026            }
1027            None => {
1028                let ds = ndarrcumsum(&trapz_step_distances(self));
1029                for (&dist, &v) in ds.iter().zip(self.mps.iter()) {
1030                    if (v < tol) && (dist > (distance_m + tol)) {
1031                        return dist - distance_m;
1032                    }
1033                }
1034                ds.last().unwrap() - distance_m
1035            }
1036        }
1037    }
1038
1039    /// Modifies the cycle using the given constant-jerk trajectory parameters
1040    /// - idx: non-negative integer, the point in the cycle to initiate
1041    ///   modification (note: THIS point is modified since trajectory should be
1042    ///   calculated from idx-1)
1043    /// - n: non-negative integer, the number of steps ahead
1044    /// - jerk_m__s3: number, the "Jerk" associated with the trajectory (m/s3)
1045    /// - accel0_m__s2: number, the initial acceleration (m/s2)
1046    /// NOTE:
1047    /// - modifies cyc in place to hit any critical rendezvous_points by a trajectory adjustment
1048    /// - CAUTION: NOT ROBUST AGAINST VARIABLE DURATION TIME-STEPS
1049    /// RETURN: Number, final modified speed (m/s)
1050    pub fn modify_by_const_jerk_trajectory(
1051        &mut self,
1052        i: usize,
1053        n: usize,
1054        jerk_m_per_s3: f64,
1055        accel0_m_per_s2: f64,
1056    ) -> f64 {
1057        if n == 0 {
1058            return 0.0;
1059        }
1060        let num_samples = self.mps.len();
1061        if i >= num_samples {
1062            if num_samples > 0 {
1063                return self.mps[num_samples - 1];
1064            }
1065            return 0.0;
1066        }
1067        let v0 = self.mps[i - 1];
1068        let dt = self.dt_s_at_i(i);
1069        let mut v = v0;
1070        for ni in 1..(n + 1) {
1071            let idx_to_set = (i - 1) + ni;
1072            if idx_to_set >= num_samples {
1073                break;
1074            }
1075            v = speed_for_constant_jerk(ni, v0, accel0_m_per_s2, jerk_m_per_s3, dt);
1076            self.mps[idx_to_set] = max(v, 0.0);
1077        }
1078        v
1079    }
1080
1081    /// Add a braking trajectory that would cover the same distance as the given constant brake deceleration
1082    /// - brake_accel_m__s2: negative number, the braking acceleration (m/s2)
1083    /// - idx: non-negative integer, the index where to initiate the stop trajectory, start of the step (i in FASTSim)
1084    /// - dts_m: None | float: if given, this is the desired distance-to-stop in meters. If not given, it is
1085    ///     calculated based on braking deceleration.
1086    /// RETURN: (non-negative-number, positive-integer)
1087    /// - the final speed of the modified trajectory (m/s)
1088    /// - the number of time-steps required to complete the braking maneuver
1089    /// NOTE:
1090    /// - modifies the cycle in place for the braking trajectory
1091    pub fn modify_with_braking_trajectory(
1092        &mut self,
1093        brake_accel_m_per_s2: f64,
1094        i: usize,
1095        dts_m: Option<f64>,
1096    ) -> anyhow::Result<(f64, usize)> {
1097        ensure!(brake_accel_m_per_s2 < 0.0);
1098        if i >= self.time_s.len() {
1099            return Ok((*self.mps.last().unwrap(), 0));
1100        }
1101        let v0 = self.mps[i - 1];
1102        let dt = self.dt_s_at_i(i);
1103        // distance-to-stop (m)
1104        let dts_m = match dts_m {
1105            Some(value) => {
1106                if value > 0.0 {
1107                    value
1108                } else {
1109                    -0.5 * v0 * v0 / brake_accel_m_per_s2
1110                }
1111            }
1112            None => -0.5 * v0 * v0 / brake_accel_m_per_s2,
1113        };
1114        if dts_m <= 0.0 {
1115            return Ok((v0, 0));
1116        }
1117        // time-to-stop (s)
1118        let tts_s = -v0 / brake_accel_m_per_s2;
1119        // number of steps to take
1120        let n = (tts_s / dt).round() as usize;
1121        let n = if n < 2 { 2 } else { n }; // need at least 2 steps
1122        let (jerk_m_per_s3, accel_m_per_s2) =
1123            calc_constant_jerk_trajectory(n, 0.0, v0, dts_m, 0.0, dt)?;
1124        Ok((
1125            self.modify_by_const_jerk_trajectory(i, n, jerk_m_per_s3, accel_m_per_s2),
1126            n,
1127        ))
1128    }
1129
1130    /// rust-internal time steps
1131    pub fn dt_s(&self) -> Array1<f64> {
1132        diff(&self.time_s)
1133    }
1134
1135    /// rust-internal time steps at i
1136    pub fn dt_s_at_i(&self, i: usize) -> f64 {
1137        self.time_s[i] - self.time_s[i - 1]
1138    }
1139
1140    /// distance covered in each time step
1141    pub fn dist_m(&self) -> Array1<f64> {
1142        &self.mps * self.dt_s()
1143    }
1144
1145    /// get mph from self.mps
1146    pub fn mph_at_i(&self, i: usize) -> f64 {
1147        self.mps[i] * MPH_PER_MPS
1148    }
1149
1150    /// elevation change w.r.t. to initial
1151    pub fn delta_elev_m(&self) -> Array1<f64> {
1152        ndarrcumsum(&(self.dist_m() * &self.grade))
1153    }
1154}
1155
1156pub struct PassingInfo {
1157    /// True if first cycle passes the second
1158    pub has_collision: bool,
1159    /// the index where first cycle passes the second
1160    pub idx: usize,
1161    /// the number of time-steps until idx from i
1162    pub num_steps: usize,
1163    /// the starting distance of the first cycle at i
1164    pub start_distance_m: f64,
1165    /// the distance (m) traveled of the second cycle when first passes
1166    pub distance_m: f64,
1167    /// the starting speed (m/s) of the first cycle at i
1168    pub start_speed_m_per_s: f64,
1169    /// the speed (m/s) of the second cycle when first passes
1170    pub speed_m_per_s: f64,
1171    /// the time step duration throught the passing investigation
1172    pub time_step_duration_s: f64,
1173}
1174
1175/// Reports back information of the first point where cyc passes cyc0, starting at
1176/// step i until the next stop of cyc.
1177/// - cyc: fastsim.Cycle, the proposed cycle of the vehicle under simulation
1178/// - cyc0: fastsim.Cycle, the reference/lead vehicle/shadow cycle to compare with
1179/// - i: int, the time-step index to consider
1180/// - dist_tol_m: float, the distance tolerance away from lead vehicle to be seen as
1181///     "deviated" from the reference/shadow trace (m)
1182/// RETURNS: PassingInfo
1183pub fn detect_passing(
1184    cyc: &RustCycle,
1185    cyc0: &RustCycle,
1186    i: usize,
1187    dist_tol_m: Option<f64>,
1188) -> PassingInfo {
1189    if i >= cyc.len() {
1190        return PassingInfo {
1191            has_collision: false,
1192            idx: 0,
1193            num_steps: 0,
1194            start_distance_m: 0.0,
1195            distance_m: 0.0,
1196            start_speed_m_per_s: 0.0,
1197            speed_m_per_s: 0.0,
1198            time_step_duration_s: 1.0,
1199        };
1200    }
1201    let zero_speed_tol_m_per_s = 1e-6;
1202    let dist_tol_m = dist_tol_m.unwrap_or(0.1);
1203    let mut v0 = cyc.mps[i - 1];
1204    let d0 = trapz_step_start_distance(cyc, i);
1205    let mut v0_lv = cyc0.mps[i - 1];
1206    let d0_lv = trapz_step_start_distance(cyc0, i);
1207    let mut d = d0;
1208    let mut d_lv = d0_lv;
1209    let mut rendezvous_idx = None;
1210    let mut rendezvous_num_steps = 0;
1211    let mut rendezvous_distance_m = 0.0;
1212    let mut rendezvous_speed_m_per_s = 0.0;
1213    for di in 0..(cyc.mps.len() - i) {
1214        let idx = i + di;
1215        let v = cyc.mps[idx];
1216        let v_lv = cyc0.mps[idx];
1217        let vavg = (v + v0) * 0.5;
1218        let vavg_lv = (v_lv + v0_lv) * 0.5;
1219        let dd = vavg * cyc.dt_s_at_i(idx);
1220        let dd_lv = vavg_lv * cyc0.dt_s_at_i(idx);
1221        d += dd;
1222        d_lv += dd_lv;
1223        let dtlv = d_lv - d;
1224        v0 = v;
1225        v0_lv = v_lv;
1226        if di > 0 && dtlv < -dist_tol_m {
1227            rendezvous_idx = Some(idx);
1228            rendezvous_num_steps = di + 1;
1229            rendezvous_distance_m = d_lv;
1230            rendezvous_speed_m_per_s = v_lv;
1231            break;
1232        }
1233        if v <= zero_speed_tol_m_per_s {
1234            break;
1235        }
1236    }
1237    PassingInfo {
1238        has_collision: rendezvous_idx.is_some(),
1239        idx: rendezvous_idx.unwrap_or(0),
1240        num_steps: rendezvous_num_steps,
1241        start_distance_m: d0,
1242        distance_m: rendezvous_distance_m,
1243        start_speed_m_per_s: cyc.mps[i - 1],
1244        speed_m_per_s: rendezvous_speed_m_per_s,
1245        time_step_duration_s: cyc.dt_s_at_i(i),
1246    }
1247}
1248
1249#[cfg(test)]
1250mod tests {
1251    use super::*;
1252
1253    #[test]
1254    fn test_dist() {
1255        let cyc = RustCycle::test_cyc();
1256        assert_eq!(cyc.dist_m().sum(), 45.0);
1257    }
1258
1259    #[test]
1260    fn test_average_speeds_and_distances() {
1261        let cyc = RustCycle {
1262            time_s: array![0.0, 10.0, 30.0, 34.0, 40.0],
1263            mps: array![0.0, 10.0, 10.0, 0.0, 0.0],
1264            grade: Array::zeros(5),
1265            road_type: Array::zeros(5),
1266            name: String::from("test"),
1267            orphaned: false,
1268        };
1269        let avg_mps = average_step_speeds(&cyc);
1270        let expected_avg_mps = Array::from_vec(vec![0.0, 5.0, 10.0, 5.0, 0.0]);
1271        assert_eq!(expected_avg_mps.len(), avg_mps.len());
1272        for (expected, actual) in expected_avg_mps.iter().zip(avg_mps.iter()) {
1273            assert_eq!(expected, actual);
1274        }
1275        let dist_m = trapz_step_distances(&cyc);
1276        let expected_dist_m = Array::from_vec(vec![0.0, 50.0, 200.0, 20.0, 0.0]);
1277        assert_eq!(expected_dist_m.len(), dist_m.len());
1278        for (expected, actual) in expected_dist_m.iter().zip(dist_m.iter()) {
1279            assert_eq!(expected, actual);
1280        }
1281    }
1282
1283    #[test]
1284    fn test_loading_a_cycle_from_the_filesystem() {
1285        let cyc_file_path = resources_path().join("cycles/udds.csv");
1286        let expected_udds_length = 1370;
1287        let cyc = RustCycle::from_csv_file(cyc_file_path, false).unwrap();
1288        let num_entries = cyc.len();
1289        assert_eq!(cyc.name, String::from("udds"));
1290        assert!(num_entries > 0);
1291        assert_eq!(num_entries, cyc.len());
1292        assert_eq!(num_entries, cyc.mps.len());
1293        assert_eq!(num_entries, cyc.grade.len());
1294        assert_eq!(num_entries, cyc.road_type.len());
1295        assert_eq!(num_entries, expected_udds_length);
1296    }
1297
1298    #[test]
1299    fn test_str_serde() {
1300        let cyc = RustCycle::test_cyc();
1301        for format in RustCycle::ACCEPTED_STR_FORMATS {
1302            let csv_str = cyc.to_str(format).unwrap();
1303            RustCycle::from_str(&csv_str, format, false).unwrap();
1304        }
1305    }
1306}