fastsim_core/drive_cycle/
manipulation_utils.rs

1use crate::drive_cycle::Cycle;
2use crate::imports::*;
3
4/// Rendezvous Trajectory that uses a constant-jerk trajectory to rendezvous
5/// with another trace in distance/time.
6pub struct RendezvousTrajectory {
7    pub found_trajectory: bool,
8    pub idx: usize,
9    pub n: usize,
10    pub full_brake_steps: usize,
11    pub jerk_m_per_s3: f64,
12    pub accel0_m_per_s2: f64,
13    pub accel_spread: f64,
14}
15
16/// Coasting trajectory that describes the characteristics of a time/speed coast.
17pub struct CoastTrajectory {
18    pub found_trajectory: bool,
19    pub distance_to_stop_via_coast_m: f64,
20    pub start_idx: usize,
21    pub speed_m_per_s: Option<Vec<f64>>,
22    pub distance_to_brake_m: Option<f64>,
23}
24
25#[serde_api]
26#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
27#[non_exhaustive]
28#[serde(deny_unknown_fields)]
29#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
30/// Data describing a trajectory with a "constant jerk"
31pub struct ConstantJerkTrajectory {
32    /// number of steps in the trajectory
33    pub steps: usize,
34    /// initial elapsed distance at trajectory start (m)
35    pub distance_m: f64,
36    /// initial speed of the trajectory (m/s)
37    pub speed_m_per_s: f64,
38    /// initial acceleration of the trajectory (m/s2)
39    pub acceleration_m_per_s2: f64,
40    /// constant jerk of the trajectory (m/s3)
41    pub jerk_m_per_s3: f64,
42    /// duration of a single step in seconds
43    pub step_duration_s: f64,
44}
45
46impl SerdeAPI for ConstantJerkTrajectory {}
47impl Init for ConstantJerkTrajectory {}
48
49impl ConstantJerkTrajectory {
50    /// Create a constant-jerk trajectory.
51    /// - n: the number of steps to use
52    /// - d0: the starting distance (m)
53    /// - v0: the starting speed (m/s)
54    /// - dr: the rendezvous distance (m)
55    /// - vr: the rendezvous speed (m/s)
56    /// - dt: constant step duration (s)
57    ///
58    /// RETURN: a ConstantJerkTrajectory
59    pub fn from_speed_and_distance_targets(
60        n: usize,
61        d0: f64,
62        v0: f64,
63        dr: f64,
64        vr: f64,
65        dt: f64,
66    ) -> ConstantJerkTrajectory {
67        assert!(n > 1);
68        assert!(dr > d0);
69        let n_orig = n;
70        let n = n as f64;
71        let ddr = dr - d0;
72        let dvr = vr - v0;
73        let k = (dvr - (2.0 * ddr / (n * dt)) + 2.0 * v0)
74            / (0.5 * n * (n - 1.0) * dt
75                - (1.0 / 3.0) * (n - 1.0) * (n - 2.0) * dt
76                - 0.5 * (n - 1.0) * dt * dt);
77        let a0 = ((ddr / dt)
78            - n * v0
79            - ((1.0 / 6.0) * n * (n - 1.0) * (n - 2.0) * dt + 0.25 * n * (n - 1.0) * dt * dt) * k)
80            / (0.5 * n * n * dt);
81        ConstantJerkTrajectory {
82            steps: n_orig,
83            distance_m: d0,
84            speed_m_per_s: v0,
85            acceleration_m_per_s2: a0,
86            jerk_m_per_s3: k,
87            step_duration_s: dt,
88        }
89    }
90    /// Calculate the distance traveled by the end of the nth step in m.
91    pub fn distance_at_step(&self, n: usize) -> f64 {
92        let n = n as f64;
93        let d0 = self.distance_m;
94        let v0 = self.speed_m_per_s;
95        let a0 = self.acceleration_m_per_s2;
96        let k = self.jerk_m_per_s3;
97        let dt = self.step_duration_s;
98        let term1 = dt
99            * ((n * v0)
100                + (0.5 * n * (n - 1.0) * a0 * dt)
101                + ((1.0 / 6.0) * k * dt * (n - 2.0) * (n - 1.0) * n));
102        let term2 = 0.5 * dt * dt * ((n * a0) + (0.5 * n * (n - 1.0) * k * dt));
103        d0 + term1 + term2
104    }
105    /// Calculate the ending distance of the trajectory in m.
106    pub fn end_distance(&self) -> f64 {
107        self.distance_at_step(self.steps)
108    }
109    /// Calculate the ending speed for the nth step in m/s.
110    pub fn speed_at_step(&self, n: usize) -> f64 {
111        let n = n as f64;
112        let v0 = self.speed_m_per_s;
113        let a0 = self.acceleration_m_per_s2;
114        let k = self.jerk_m_per_s3;
115        let dt = self.step_duration_s;
116        v0 + (n * a0 * dt) + (0.5 * n * (n - 1.0) * k * dt)
117    }
118    /// Calculate the ending speed in m/s.
119    pub fn end_speed(&self) -> f64 {
120        self.speed_at_step(self.steps)
121    }
122    /// Calculate the acceleration for step n in m/s2.
123    pub fn acceleration_at_step(&self, n: usize) -> f64 {
124        let n = n as f64;
125        let a0 = self.acceleration_m_per_s2;
126        let k = self.jerk_m_per_s3;
127        let dt = self.step_duration_s;
128        a0 + (n * k * dt)
129    }
130    /// Calculate the acceleration at end of the trajectory in m/s2.
131    pub fn end_acceleration(&self) -> f64 {
132        self.acceleration_at_step(self.steps)
133    }
134    /// Calculate and return a vector of all of the step-wise accelerations for
135    /// the trajectory in m/s2.
136    pub fn all_accelerations(&self) -> Vec<f64> {
137        let mut accels = Vec::with_capacity(self.steps);
138        for n_idx in 0..self.steps {
139            accels.push(self.acceleration_at_step(n_idx));
140        }
141        accels
142    }
143    /// Calculate and return the maximum acceleration over the trajectory in m/s2.
144    pub fn maximum_acceleration(&self) -> f64 {
145        let accels = self.all_accelerations();
146        *accels.max().unwrap_or(&0.0)
147    }
148}
149
150#[cfg_attr(feature = "pyo3", pyfunction)]
151/// Calculate constant-Jerk trajectory.
152/// - n: number of time steps away from rendezvous point
153/// - d0: distance from start of simulated vehicle ($m$)
154/// - v0: speed of simulated vehicle ($\frac{m}{s}$)
155/// - dr: distance from start of rendezvous point ($m$)
156/// - vr: speed to hit at rendezvous point ($\frac{m}{s}$)
157/// - dt: time-step duration ($s$)
158/// RETURN: constant jerk and acceleration for first time step.
159pub fn calc_constant_jerk_trajectory(
160    n: usize,
161    d0: f64,
162    v0: f64,
163    dr: f64,
164    vr: f64,
165    dt: f64,
166) -> ConstantJerkTrajectory {
167    assert!(n > 1);
168    assert!(dr > d0);
169    assert!(v0 >= 0.0);
170    assert!(vr >= 0.0);
171    assert!(dt > 0.0);
172    ConstantJerkTrajectory::from_speed_and_distance_targets(n, d0, v0, dr, vr, dt)
173}
174
175#[cfg_attr(feature = "pyo3", pyfunction)]
176/// Calculate distance (m) after n timesteps
177/// - n: number of timesteps away to calculate
178/// - d0: initial distance (m)
179/// - v0: initial speed (m/s)
180/// - a0: initial acceleration (m/s2)
181/// - k: constant jerk (m/s3)
182/// - dt: duration of a timestep (s)
183/// RETURN: distance a n timesteps away (m)
184/// NOTE: this is the distance traveled from start (i.e., when n=0)
185/// measured at sample point n
186pub fn dist_for_constant_jerk(n: usize, d0: f64, v0: f64, a0: f64, k: f64, dt: f64) -> f64 {
187    let trajectory = ConstantJerkTrajectory {
188        steps: n,
189        distance_m: d0,
190        speed_m_per_s: v0,
191        acceleration_m_per_s2: a0,
192        jerk_m_per_s3: k,
193        step_duration_s: dt,
194    };
195    trajectory.end_distance()
196}
197
198#[cfg_attr(feature = "pyo3", pyfunction)]
199/// Calculate speed (m/s) n timesteps away via a constant-jerk acceleration
200/// - n: number of timesteps away to calculate
201/// - v0: initial speed (m/s)
202/// - a0: initial acceleration (m/s2)
203/// - k: constant jerk (m/s3)
204/// - dt: duration of a time step (s)
205/// RETURN: the speed n timesteps away (m/s)
206/// NOTES:
207/// - this is the speed at sample-point n
208/// - if n == 0, speed is v0
209/// - if n == 1, speed is v* + a0 * dt, etc.
210pub fn speed_for_constant_jerk(n: usize, v0: f64, a0: f64, k: f64, dt: f64) -> f64 {
211    let trajectory = ConstantJerkTrajectory {
212        steps: n,
213        distance_m: 0.0,
214        speed_m_per_s: v0,
215        acceleration_m_per_s2: a0,
216        jerk_m_per_s3: k,
217        step_duration_s: dt,
218    };
219    trajectory.end_speed()
220}
221
222#[cfg_attr(feature = "pyo3", pyfunction)]
223/// Calculate the acceleration n timesteps away (m/s2)
224/// - n: number of time steps away to calculate
225/// - a0: initial acceleration (m/s2)
226/// - k: constant jerk (m/s3)
227/// - dt: time-step duration (s)
228/// RETURN: the acceleration n timesteps away (m/s)
229/// NOTES:
230/// - this is the constant accerlation over the time-step from sample n to n+1
231pub fn accel_for_constant_jerk(n: usize, a0: f64, k: f64, dt: f64) -> f64 {
232    let trajectory = ConstantJerkTrajectory {
233        steps: n,
234        distance_m: 0.0,
235        speed_m_per_s: 0.0,
236        acceleration_m_per_s2: a0,
237        jerk_m_per_s3: k,
238        step_duration_s: dt,
239    };
240    trajectory.end_acceleration()
241}
242
243#[cfg_attr(feature = "pyo3", pyfunction)]
244/// Apply accel_for_constant_jerk to derive full array of accelerations.
245/// - n: number of time steps away to calculate
246/// - a0: initial acceleration (m/s2)
247/// - k: constant jerk (m/s3)
248/// - dt: time-step duration (s)
249/// RETURN: the accelerations for each timestep up to n timesteps away (m/s)
250pub fn accel_array_for_constant_jerk(n: usize, a0: f64, k: f64, dt: f64) -> Vec<f64> {
251    let trajectory = ConstantJerkTrajectory {
252        steps: n,
253        distance_m: 0.0,
254        speed_m_per_s: 0.0,
255        acceleration_m_per_s2: a0,
256        jerk_m_per_s3: k,
257        step_duration_s: dt,
258    };
259    trajectory.all_accelerations()
260}
261
262/// Return the average step speeds of the cycle as vector of velicities.
263/// NOTE: the average speed from sample i-1 to i will appear as entry i.
264/// - cyc: an instance of the cycle to get average step speeds for.
265///
266/// RETURN: vector of velocities representing average step speeds.
267pub fn average_step_speeds(cyc: &Cycle) -> Vec<si::Velocity> {
268    cyc.average_step_speeds()
269}
270
271/// Calculate the average step speed at step i
272/// (i.e., from sample point i-1 to i)
273pub fn average_step_speed_at(cyc: &Cycle, i: usize) -> si::Velocity {
274    cyc.average_step_speed_at(i)
275}
276
277/// The distances traveled over each step using trapezoidal
278/// integration.
279pub fn trapz_step_distances(cyc: &Cycle) -> Vec<si::Length> {
280    cyc.trapz_step_distances()
281}
282
283/// The distance traveled from start to the beginning of step i
284/// (i.e., distance traveled up to sample point i-1)
285pub fn trapz_step_start_distance(cyc: &Cycle, i: usize) -> si::Length {
286    cyc.trapz_step_start_distance(i)
287}
288
289/// The distance traveled during the given step
290/// (i.e., distance from sample point i-1 to i for step i)
291pub fn trapz_distance_for_step(cyc: &Cycle, i: usize) -> si::Length {
292    cyc.trapz_distance_for_step(i)
293}
294
295/// Calculate the distance from step i_start to the start of step i_end
296/// (i.e., distance from sample point i_start - 1 to i_end - 1)
297pub fn trapz_distance_over_range(cyc: &Cycle, i_start: usize, i_end: usize) -> si::Length {
298    cyc.trapz_distance_over_range(i_start, i_end)
299}
300
301/// Calculate the time in a cycle spent moving
302/// - stopped_speed_m_per_s: the speed above which we are considered to be moving
303///
304/// RETURN: the time spent moving in seconds
305pub fn time_spent_moving(cyc: &Cycle, stopped_speed: Option<si::Velocity>) -> si::Time {
306    cyc.time_spent_moving(stopped_speed)
307}
308
309/// Create distance and target speeds by microtrip.
310/// Used to set target speeds for each microtrip.
311/// - cyc: the cycle to operate on
312/// - stop_speed: speed at or below which we consider the vehicle "stopped"
313/// - blend_factor: a ratio between 0 and 1. At "0", we use the average speed
314///   including stopped time; at "1" we use only the speed while moving to set
315///   the average.
316/// - min_target_speed: the minimum speed we allow a vehicle to drop down to
317///   over a microtrip
318///
319/// RETURN vector of distance and speed targets. The interpretation is that
320/// "at or above" the given distance, the given speed will be in effect as
321/// the target speed (until we pass the next entry's distance).
322pub fn create_distance_and_target_speeds_by_microtrip(
323    cyc: &Cycle,
324    stop_speed: Option<si::Velocity>,
325    blend_factor: f64,
326    min_target_speed: si::Velocity,
327) -> Vec<(si::Length, si::Velocity)> {
328    cyc.distance_and_target_speeds_by_microtrip(stop_speed, blend_factor, min_target_speed)
329}
330
331/// Extend the cycle's time.
332/// - absolute_time: an absolute time value
333/// - time_fraction: extend by the given fraction of cycle's current time
334///
335/// RETURN: new cycle with time extended by both the absolute
336///         and fraction values
337///
338/// NOTE: absolute_time and time_faction are optional. Pass None to
339/// remove them from the equation.
340pub fn extend_cycle_time(
341    cyc: &Cycle,
342    absolute_time: Option<si::Time>,
343    time_fraction: Option<si::Ratio>,
344) -> Cycle {
345    cyc.extend_time(absolute_time, time_fraction)
346}
347
348#[serde_api]
349#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
350#[non_exhaustive]
351#[serde(deny_unknown_fields)]
352#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
353pub struct PassingInfo {
354    /// True if first cycle passes the second; NOTE: was `has_collision`
355    pub passing_detected: bool,
356    /// the index where first cycle passes the second; NOTE: was `idx`
357    pub index: usize,
358    /// the number of time-steps until index from i
359    pub num_steps: usize,
360    /// the starting distance of the first cycle at i
361    pub start_distance: si::Length,
362    /// the distance traveled of the second cycle when the first passes
363    pub distance: si::Length,
364    /// the starting speed of the first cycle at i
365    pub start_speed: si::Velocity,
366    /// the speed of the second cycle when first passes
367    pub speed: si::Velocity,
368    /// the (assumed constant) time step duration throughout the passing investigation
369    pub time_step_duration: si::Time,
370}
371
372impl PassingInfo {
373    /// Create a new PassingInfo struct from a cycle and reference cycle.
374    /// - cyc: the cycle to detect passing for
375    /// - cyc0: the reference cycle / lead vehicle / shadow cycle to compare cyc with
376    /// - i: the time-step index for the start of consideration
377    /// - distance_tolerance: the distance away from the lead vehicle at or above which
378    ///   we consider ourselves "deviated" or "no longer following" the reference trace
379    ///
380    /// RETURN: a PassingInfo structure
381    pub fn from(
382        cyc: &Cycle,
383        cyc_ref: &Cycle,
384        i: usize,
385        distance_tolerance: Option<si::Length>,
386    ) -> Self {
387        let i = std::cmp::max(i, 1);
388        if i >= cyc.time.len() {
389            return Self {
390                passing_detected: false,
391                index: 0,
392                num_steps: 0,
393                start_distance: 0.0 * uc::M,
394                distance: 0.0 * uc::M,
395                start_speed: 0.0 * uc::MPS,
396                speed: 0.0 * uc::MPS,
397                time_step_duration: 1.0 * uc::S,
398            };
399        }
400        let zero_speed_tol = 1e-6 * uc::MPS;
401        let distance_tol = distance_tolerance.unwrap_or(0.1 * uc::M);
402        let mut v0 = cyc.speed[i - 1];
403        let d0 = cyc.trapz_step_start_distance(i);
404        let mut v0_lv = cyc_ref.speed[i - 1];
405        let d0_lv = cyc_ref.trapz_step_start_distance(i);
406        let mut d = d0;
407        let mut d_lv = d0_lv;
408        let mut rendezvous_index = None;
409        let mut rendezvous_num_steps = 0;
410        let mut rendezvous_distance = 0.0 * uc::M;
411        let mut rendezvous_speed = 0.0 * uc::MPS;
412        for di in 0..(cyc.speed.len() - i) {
413            let idx = i + di;
414            // cycle current speed
415            let v = cyc.speed[idx];
416            // lead vehicle current speed
417            let v_lv = cyc_ref.speed[idx];
418            // cycle average speed for step
419            let vavg = (v + v0) * 0.5;
420            // lead vehicle average speed for step
421            let vavg_lv = (v_lv + v0_lv) * 0.5;
422            // time step duration
423            let dt = cyc.time[idx] - cyc.time[idx - 1];
424            // delta distance for step
425            let dd = vavg * dt;
426            // time step duration for lead vehicle
427            let dt_lv = cyc_ref.time[idx] - cyc_ref.time[idx - 1];
428            // delta distance for lead vehicle for step
429            let dd_lv = vavg_lv * dt_lv;
430            // total distance from start
431            d += dd;
432            // total distance from start for lead vehicle
433            d_lv += dd_lv;
434            // distance to lead vehicle
435            let dtlv = d_lv - d;
436            v0 = v;
437            v0_lv = v_lv;
438            if di > 0 && dtlv < -distance_tol {
439                rendezvous_index = Some(idx);
440                rendezvous_num_steps = di + 1;
441                rendezvous_distance = d_lv;
442                rendezvous_speed = v_lv;
443                break;
444            }
445            if v <= zero_speed_tol {
446                break;
447            }
448        }
449        Self {
450            passing_detected: rendezvous_index.is_some(),
451            index: rendezvous_index.unwrap_or(0),
452            num_steps: rendezvous_num_steps,
453            start_distance: d0,
454            distance: rendezvous_distance,
455            start_speed: cyc.speed[i - 1],
456            speed: rendezvous_speed,
457            time_step_duration: cyc.time[i] - cyc.time[i - 1],
458        }
459    }
460}
461
462#[serde_api]
463#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
464#[non_exhaustive]
465#[serde(deny_unknown_fields)]
466#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
467pub struct CycleCache {
468    /// flag to indicate if cycle has all-zero grade (i.e., flat) or not
469    pub grade_all_zero: bool,
470    /// distance traveled over each time-step of the cycle
471    pub trapz_step_distances_m: Vec<f64>,
472    /// distances from start at each sample point
473    pub trapz_distances_m: Vec<f64>,
474    /// elevations at each sample point
475    pub trapz_elevations_m: Vec<f64>,
476    /// an array of flags indicating whether stopped (true) or not (false)
477    pub stops: Vec<bool>,
478    /// interpolation_distances
479    interp_ds: Vec<f64>,
480    /// interpolation of indices
481    interp_is: Vec<f64>,
482    /// interpolation of heights (i.e., elevations)
483    interp_hs: Vec<f64>,
484    /// grades where g[i] applies from distance [i, i+1)
485    grades: Vec<f64>,
486    /// interpolator for index by distance
487    interp_index_by_dist: InterpolatorEnumOwned<f64>,
488    /// interpolator for elevation by distance
489    interp_elev_by_dist: InterpolatorEnumOwned<f64>,
490}
491
492impl Default for CycleCache {
493    fn default() -> Self {
494        Self {
495            grade_all_zero: false,
496            trapz_step_distances_m: Default::default(),
497            trapz_distances_m: Default::default(),
498            trapz_elevations_m: Default::default(),
499            stops: Default::default(),
500            interp_ds: Default::default(),
501            interp_is: Default::default(),
502            interp_hs: Default::default(),
503            grades: Default::default(),
504            interp_index_by_dist: InterpolatorEnum::new_0d(0.0),
505            interp_elev_by_dist: InterpolatorEnum::new_0d(0.0),
506        }
507    }
508}
509
510impl Init for CycleCache {}
511
512impl SerdeAPI for CycleCache {}
513
514impl CycleCache {
515    /// Create a new cycle cache from a cycle.
516    pub fn new(cyc: &Cycle) -> Self {
517        let tol = 1e-6;
518        let num_items = cyc.time.len();
519        let grade_all_zero = cyc.grade.is_empty() || cyc.grade.iter().all(|g| *g == 0.0 * uc::R);
520        let trapz_step_distances_m: Vec<f64> = cyc
521            .trapz_step_distances()
522            .iter()
523            .map(|dd| dd.get::<si::meter>())
524            .collect();
525        debug_assert!(trapz_step_distances_m.len() == num_items);
526        let trapz_distances_m: Vec<f64> = {
527            let mut ds = Vec::with_capacity(num_items);
528            let mut d = 0.0;
529            for dd in &trapz_step_distances_m {
530                d += *dd;
531                ds.push(d);
532            }
533            ds
534        };
535        debug_assert!(trapz_distances_m.len() == num_items);
536        let trapz_elevations_m = if grade_all_zero {
537            let h = cyc.init_elev.unwrap_or(0.0 * uc::M).get::<si::meter>();
538            vec![h; num_items]
539        } else {
540            let dhs: Vec<f64> = cyc
541                .grade
542                .iter()
543                .zip(&trapz_step_distances_m)
544                .map(|(g, dd)| {
545                    let gr = g.get::<si::ratio>();
546                    gr.atan().cos() * dd * gr
547                })
548                .collect();
549            let mut hs = Vec::with_capacity(num_items);
550            let mut h = cyc.init_elev.unwrap_or(0.0 * uc::M).get::<si::meter>();
551            for dh in &dhs {
552                h += *dh;
553                hs.push(h);
554            }
555            hs
556        };
557        debug_assert!(trapz_elevations_m.len() == num_items);
558        let stops = cyc
559            .speed
560            .iter()
561            .map(|v| v.get::<si::meter_per_second>() <= tol)
562            .collect();
563        let mut interp_ds = Vec::with_capacity(num_items);
564        let mut interp_is = Vec::with_capacity(num_items);
565        let mut interp_hs = Vec::with_capacity(num_items);
566        for idx in 0..num_items {
567            let d = trapz_distances_m[idx];
568            let h = trapz_elevations_m[idx];
569            if interp_ds.is_empty() || d > *interp_ds.last().unwrap() {
570                interp_ds.push(d);
571                interp_is.push(idx as f64);
572                interp_hs.push(h);
573            }
574        }
575        let grades: Vec<f64> = cyc.grade.iter().map(|g| g.get::<si::ratio>()).collect();
576        debug_assert!(grades.len() == num_items);
577        let interp_index_by_dist = InterpolatorEnum::new_1d(
578            interp_ds.clone().into(),
579            interp_is.clone().into(),
580            strategy::RightNearest,
581            Extrapolate::Clamp,
582        )
583        .unwrap();
584        let interp_elev_by_dist = InterpolatorEnum::new_1d(
585            interp_ds.clone().into(),
586            interp_hs.clone().into(),
587            strategy::Linear,
588            Extrapolate::Clamp,
589        )
590        .unwrap();
591        Self {
592            grade_all_zero,
593            trapz_step_distances_m,
594            trapz_distances_m,
595            trapz_elevations_m,
596            stops,
597            interp_ds,
598            interp_is,
599            interp_hs,
600            grades,
601            interp_index_by_dist,
602            interp_elev_by_dist,
603        }
604    }
605
606    /// Interpolate the single-point grade at the given distance.
607    /// Assumes that the grade at i applies from sample point (i-1, i]
608    pub fn interp_grade(&self, dist_m: f64) -> f64 {
609        if self.grade_all_zero {
610            0.0
611        } else if dist_m <= self.interp_ds[0] {
612            self.grades[0]
613        } else if dist_m > *self.interp_ds.last().expect("interp_ds.len()>0") {
614            *self.grades.last().unwrap()
615        } else {
616            // NOTE: interp strategy is right nearest; equal to linear + ceil()
617            let idx = self.interp_index_by_dist.interpolate(&[dist_m]).unwrap();
618            self.grades[idx as usize]
619        }
620    }
621
622    /// Interpolate the elevation at the given distance
623    pub fn interp_elevation(&self, dist_m: f64) -> f64 {
624        if self.grade_all_zero {
625            0.0
626        } else {
627            self.interp_elev_by_dist.interpolate(&[dist_m]).unwrap()
628        }
629    }
630}
631
632/// Calculate a rendezvous trajectory for re-rendezvous with reference cycle.
633/// - i: the index where speed has changed from reference
634/// - max_steps: the maximum number of time-steps ahead that a rendezvous will be considered. Minimum is 2.
635/// - cyc: the reference cycle
636/// - speed_ach: the current best effort speed that deviates from reference cycle at i.
637///
638/// RESULT: returns a RendezvousTrajectory which describes a constant-jerk trajectory path
639/// that will rendezvous with the reference trace between n=2 and max_steps steps. The trajectory
640/// chosen will have the smallest peak acceleration of all options investigated.
641pub fn calc_best_rendezvous(
642    i: usize,
643    max_steps: usize,
644    cyc: &Cycle,
645    speed_ach: si::Velocity,
646) -> ConstantJerkTrajectory {
647    let max_steps = (cyc.time.len() - i).min(max_steps);
648    let i = i.clamp(1, cyc.time.len() - 1);
649    let dt = cyc.time[i] - cyc.time[i - 1];
650    let start_distance = 0.5 * (speed_ach + cyc.speed[i - 1]) * dt;
651    let mut best = ConstantJerkTrajectory {
652        steps: 0,
653        distance_m: start_distance.get::<si::meter>(),
654        speed_m_per_s: speed_ach.get::<si::meter_per_second>(),
655        acceleration_m_per_s2: 0.0,
656        jerk_m_per_s3: 0.0,
657        step_duration_s: dt.get::<si::second>(),
658    };
659    if max_steps < 2 {
660        return best;
661    }
662    let mut rendezvous_distance = 0.5 * (cyc.speed[i] + cyc.speed[i - 1]) * dt;
663    let mut max_accel_m_per_s2 = 100.0;
664    for n in 1..max_steps {
665        let j = i + n;
666        let dt = cyc.time[j] - cyc.time[j - 1];
667        rendezvous_distance += 0.5 * (cyc.speed[j] + cyc.speed[j - 1]) * dt;
668        if n >= 2 {
669            let candidate = ConstantJerkTrajectory::from_speed_and_distance_targets(
670                n,
671                start_distance.get::<si::meter>(),
672                speed_ach.get::<si::meter_per_second>(),
673                rendezvous_distance.get::<si::meter>(),
674                cyc.speed[j].get::<si::meter_per_second>(),
675                dt.get::<si::second>(),
676            );
677            let candidate_max_accel_m_per_s2 = candidate.maximum_acceleration();
678            if candidate_max_accel_m_per_s2 < max_accel_m_per_s2 {
679                max_accel_m_per_s2 = candidate_max_accel_m_per_s2;
680                best = candidate;
681            }
682        }
683    }
684    best
685}
686
687#[cfg(test)]
688mod tests {
689    use super::*;
690    use crate::drive_cycle::Cycle;
691
692    fn make_triangle_cycle() -> Cycle {
693        Cycle {
694            name: String::from("Triangle"),
695            init_elev: None,
696            time: vec![0.0 * uc::S, 10.0 * uc::S, 20.0 * uc::S, 30.0 * uc::S],
697            speed: vec![0.0 * uc::MPS, 4.0 * uc::MPS, 0.0 * uc::MPS, 0.0 * uc::MPS],
698            dist: vec![],
699            grade: vec![],
700            elev: vec![],
701            pwr_max_chrg: vec![],
702            grade_interp: Default::default(),
703            elev_interp: Default::default(),
704            temp_amb_air: Default::default(),
705            pwr_solar_load: Default::default(),
706        }
707    }
708    fn make_test_trajectory() -> ConstantJerkTrajectory {
709        let n = 2;
710        let d0_m = 0.0;
711        let v0_m_per_s = 0.0;
712        let dr_m = 2.0;
713        let vr_m_per_s = 2.0;
714        let dt_s = 1.0;
715        ConstantJerkTrajectory::from_speed_and_distance_targets(
716            n, d0_m, v0_m_per_s, dr_m, vr_m_per_s, dt_s,
717        )
718    }
719    #[test]
720    fn test_calc_const_jerk_trajectory() {
721        let actual = make_test_trajectory();
722        let expected = ConstantJerkTrajectory {
723            steps: 2,
724            distance_m: 0.0,
725            speed_m_per_s: 0.0,
726            acceleration_m_per_s2: 1.0,
727            jerk_m_per_s3: 0.0,
728            step_duration_s: 1.0,
729        };
730        assert_eq!(actual, expected);
731    }
732    #[test]
733    fn test_dist_for_constant_jerk() {
734        let trajectory = make_test_trajectory();
735        let expected = 2.0; // meters
736        let actual = trajectory.end_distance();
737        assert_eq!(actual, expected);
738    }
739    #[test]
740    fn test_speed_for_constant_jerk() {
741        let n = 2;
742        let v0_m_per_s = 0.0;
743        let a0_m_per_s2 = 1.0;
744        let k_m_per_s3 = 0.0;
745        let dt_s = 1.0;
746        let expected = 2.0;
747        let actual = speed_for_constant_jerk(n, v0_m_per_s, a0_m_per_s2, k_m_per_s3, dt_s);
748        assert_eq!(actual, expected);
749    }
750    #[test]
751    fn test_accel_for_constant_jerk() {
752        let n = 2;
753        let a0_m_per_s2 = 1.0;
754        let k_m_per_s3 = 0.0;
755        let dt_s = 1.0;
756        let expected = 1.0;
757        let actual = accel_for_constant_jerk(n, a0_m_per_s2, k_m_per_s3, dt_s);
758        assert_eq!(actual, expected);
759    }
760    #[test]
761    fn test_accel_array_for_constant_jerk() {
762        let n = 2;
763        let a0_m_per_s2 = 1.0;
764        let k_m_per_s3 = 0.0;
765        let dt_s = 1.0;
766        let expected = [1.0, 1.0];
767        let actual = accel_array_for_constant_jerk(n, a0_m_per_s2, k_m_per_s3, dt_s);
768        assert_eq!(actual.len(), expected.len());
769        for i in 0..expected.len() {
770            assert_eq!(actual[i], expected[i]);
771        }
772    }
773    #[test]
774    fn test_average_step_speeds() {
775        let cyc = make_triangle_cycle();
776        let expected = [0.0 * uc::MPS, 2.0 * uc::MPS, 2.0 * uc::MPS, 0.0 * uc::MPS];
777        let actual = average_step_speeds(&cyc);
778        assert_eq!(actual.len(), expected.len());
779        for i in 0..expected.len() {
780            assert_eq!(actual[i], expected[i]);
781        }
782    }
783
784    #[test]
785    fn test_average_step_speed_at() {
786        let cyc = make_triangle_cycle();
787        let expected = 2.0 * uc::MPS;
788        let actual = average_step_speed_at(&cyc, 1);
789        assert_eq!(actual, expected);
790    }
791
792    #[test]
793    fn test_trapz_step_distances() {
794        let cyc = make_triangle_cycle();
795        let expected = [0.0 * uc::M, 20.0 * uc::M, 20.0 * uc::M, 0.0 * uc::M];
796        let actual = trapz_step_distances(&cyc);
797        assert_eq!(actual.len(), expected.len());
798        for i in 0..expected.len() {
799            assert_eq!(actual[i], expected[i]);
800        }
801    }
802
803    #[test]
804    fn test_trapz_step_start_distance() {
805        let cyc = make_triangle_cycle();
806        let expected = 40.0 * uc::M;
807        // NOTE: using '30' tests we can overshoot the step index with no problem.
808        let actual = trapz_step_start_distance(&cyc, 30);
809        assert_eq!(actual, expected);
810    }
811
812    #[test]
813    fn test_trapz_distance_for_step() {
814        let cyc = make_triangle_cycle();
815        let expected = 20.0 * uc::M;
816        let actual = trapz_distance_for_step(&cyc, 1);
817        assert_eq!(actual, expected);
818    }
819
820    #[test]
821    fn test_trapz_distance_over_range() {
822        let cyc = make_triangle_cycle();
823        let expected = 40.0 * uc::M;
824        // NOTE: the high end step is meant to test out-of-bounds indices.
825        let actual = trapz_distance_over_range(&cyc, 0, 1000);
826        assert_eq!(actual, expected);
827    }
828
829    #[test]
830    fn test_time_spent_moving() {
831        let cyc = make_triangle_cycle();
832        let expected = 20.0 * uc::S;
833        let actual = time_spent_moving(&cyc, None);
834        assert_eq!(actual, expected);
835    }
836
837    #[test]
838    fn test_create_distance_and_target_speeds_by_microtrip() {
839        let cyc = make_triangle_cycle();
840        let expected = [(0.0 * uc::M, (40.0 / 30.0) * uc::MPS)];
841        let v0 = 0.0 * uc::MPS;
842        let actual = create_distance_and_target_speeds_by_microtrip(&cyc, None, 0.0, v0);
843        assert_eq!(actual.len(), expected.len());
844        for i in 0..expected.len() {
845            assert_eq!(actual[i].0, expected[i].0);
846            assert_eq!(actual[i].1, expected[i].1);
847        }
848    }
849
850    #[test]
851    fn test_extending_cycle_time() {
852        let cyc = make_triangle_cycle();
853        let expected = {
854            let mut c = Cycle {
855                name: cyc.name.clone(),
856                init_elev: None,
857                time: vec![
858                    0.0 * uc::S,
859                    10.0 * uc::S,
860                    20.0 * uc::S,
861                    30.0 * uc::S,
862                    31.0 * uc::S,
863                    32.0 * uc::S,
864                    33.0 * uc::S,
865                    34.0 * uc::S,
866                    35.0 * uc::S,
867                ],
868                speed: vec![
869                    0.0 * uc::MPS,
870                    4.0 * uc::MPS,
871                    0.0 * uc::MPS,
872                    0.0 * uc::MPS,
873                    0.0 * uc::MPS,
874                    0.0 * uc::MPS,
875                    0.0 * uc::MPS,
876                    0.0 * uc::MPS,
877                    0.0 * uc::MPS,
878                ],
879                dist: vec![],
880                grade: vec![],
881                elev: vec![],
882                pwr_max_chrg: vec![],
883                grade_interp: cyc.grade_interp.clone(),
884                elev_interp: cyc.elev_interp.clone(),
885                temp_amb_air: Default::default(),
886                pwr_solar_load: Default::default(),
887            };
888            c.init().unwrap();
889            c
890        };
891        let actual = extend_cycle_time(&cyc, Some(2.0 * uc::S), Some(0.10 * uc::R));
892        assert_eq!(actual, expected);
893    }
894
895    #[test]
896    fn test_passing_info() {
897        let c = {
898            // travels 300 m
899            let mut cyc = Cycle {
900                name: String::from("Main Cycle"),
901                time: vec![
902                    0.0 * uc::S,
903                    10.0 * uc::S,
904                    20.0 * uc::S,
905                    30.0 * uc::S,
906                    40.0 * uc::S,
907                ],
908                speed: vec![
909                    0.0 * uc::MPS,
910                    10.0 * uc::MPS,
911                    10.0 * uc::MPS,
912                    10.0 * uc::MPS,
913                    0.0 * uc::MPS,
914                ],
915                grade: vec![],
916                dist: vec![],
917                elev: vec![],
918                init_elev: Some(0.0 * uc::M),
919                pwr_max_chrg: vec![],
920                pwr_solar_load: vec![],
921                temp_amb_air: vec![],
922                grade_interp: Default::default(),
923                elev_interp: Default::default(),
924            };
925            cyc.init().unwrap();
926            cyc
927        };
928        let c_lead = {
929            // travels 250 m
930            let mut cyc = Cycle {
931                name: String::from("Lead Vehicle"),
932                time: vec![
933                    0.0 * uc::S,
934                    10.0 * uc::S,
935                    20.0 * uc::S,
936                    30.0 * uc::S,
937                    40.0 * uc::S,
938                ],
939                speed: vec![
940                    0.0 * uc::MPS,
941                    10.0 * uc::MPS,
942                    10.0 * uc::MPS,
943                    5.0 * uc::MPS,
944                    0.0 * uc::MPS,
945                ],
946                grade: vec![],
947                dist: vec![],
948                elev: vec![],
949                init_elev: Some(0.0 * uc::M),
950                pwr_max_chrg: vec![],
951                pwr_solar_load: vec![],
952                temp_amb_air: vec![],
953                grade_interp: Default::default(),
954                elev_interp: Default::default(),
955            };
956            cyc.init().unwrap();
957            cyc
958        };
959        let expected = PassingInfo {
960            passing_detected: true,
961            index: 3,
962            num_steps: 3,
963            start_distance: 0.0 * uc::M,
964            distance: 225.0 * uc::M,
965            start_speed: 0.0 * uc::MPS,
966            speed: 5.0 * uc::MPS,
967            time_step_duration: 10.0 * uc::S,
968        };
969        let actual = PassingInfo::from(&c, &c_lead, 1, None);
970        assert_eq!(actual, expected);
971    }
972
973    #[test]
974    fn test_making_interp() {
975        let interp = InterpolatorEnum::new_1d(
976            array![0.0, 2.0, 4.0],
977            array![0.0, 4.0, 8.0],
978            strategy::Linear,
979            Extrapolate::Clamp,
980        )
981        .unwrap();
982        let value = interp.interpolate(&[1.0]).unwrap();
983        let expected = 2.0;
984        assert_eq!(value, expected);
985    }
986
987    #[test]
988    fn test_calc_best_rendezvous() {
989        let cyc = {
990            let mut c = Cycle {
991                name: String::from("Trapezoidal Trace"),
992                init_elev: None,
993                time: vec![
994                    0.0 * uc::S,
995                    1.0 * uc::S,
996                    2.0 * uc::S,
997                    3.0 * uc::S,
998                    4.0 * uc::S,
999                    5.0 * uc::S,
1000                    6.0 * uc::S,
1001                    7.0 * uc::S,
1002                    8.0 * uc::S,
1003                ],
1004                speed: vec![
1005                    0.0 * uc::MPS,
1006                    0.0 * uc::MPS,
1007                    8.0 * uc::MPS,
1008                    8.0 * uc::MPS,
1009                    8.0 * uc::MPS,
1010                    8.0 * uc::MPS,
1011                    8.0 * uc::MPS,
1012                    0.0 * uc::MPS,
1013                    0.0 * uc::MPS,
1014                ],
1015                dist: vec![],
1016                grade: vec![],
1017                elev: vec![],
1018                pwr_max_chrg: vec![],
1019                temp_amb_air: vec![],
1020                pwr_solar_load: vec![],
1021                grade_interp: None,
1022                elev_interp: None,
1023            };
1024            c.init().unwrap();
1025            c
1026        };
1027        let i = 2;
1028        let max_steps = 4;
1029        let speed_ach = 4.0 * uc::MPS;
1030        let result = calc_best_rendezvous(i, max_steps, &cyc, speed_ach);
1031        assert!(result.steps >= 2);
1032        let expected_distance_m = 4.0 + 8.0 * (result.steps as f64);
1033        let actual_distance_m = result.end_distance();
1034        assert_eq!(actual_distance_m, expected_distance_m);
1035    }
1036}