fastsim_core/
drive_cycle.rs

1pub mod maneuvers;
2pub mod manipulation_utils;
3
4use crate::drive_cycle::manipulation_utils::{
5    speed_for_constant_jerk, ConstantJerkTrajectory, CycleCache,
6};
7use crate::imports::*;
8use crate::prelude::*;
9use fastsim_2::cycle::RustCycle as Cycle2;
10use std::cmp;
11
12#[serde_api]
13#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
14#[non_exhaustive]
15#[serde(deny_unknown_fields)]
16#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
17/// Container
18pub struct Cycle {
19    /// Name of cycle (can be left empty)
20    #[serde(default, skip_serializing_if = "String::is_empty")]
21    pub name: String,
22    // TODO: either write or automate generation of getter and setter for this
23    // TODO: put the above TODO in github issue for all fields with `Option<...>` type
24    /// inital elevation
25    pub init_elev: Option<si::Length>,
26    /// simulation time
27    pub time: Vec<si::Time>,
28    /// prescribed speed
29    #[serde(alias = "speed_mps")]
30    pub speed: Vec<si::Velocity>,
31    // TODO: consider trapezoidal integration scheme
32    /// calculated prescribed distance based on RHS integral of time and speed
33    #[serde(default, skip_serializing_if = "Vec::is_empty")]
34    pub dist: Vec<si::Length>,
35    /// road grade (expressed as a decimal, not percent)
36    #[serde(default, skip_serializing_if = "Vec::is_empty")]
37    pub grade: Vec<si::Ratio>,
38    // TODO: consider trapezoidal integration scheme
39    // TODO: @mokeefe, please check out how elevation is handled
40    /// calculated prescribed elevation based on RHS integral distance and grade
41    #[serde(default, skip_serializing_if = "Vec::is_empty")]
42    pub elev: Vec<si::Length>,
43    /// road charging/discharing capacity
44    #[serde(default, skip_serializing_if = "Vec::is_empty")]
45    pub pwr_max_chrg: Vec<si::Power>,
46    /// ambient air temperature w.r.t. to time (rather than spatial position)
47    #[serde(default, skip_serializing_if = "Vec::is_empty")]
48    pub temp_amb_air: Vec<si::Temperature>,
49    /// solar heat load w.r.t. to time (rather than spatial position)
50    #[serde(default, skip_serializing_if = "Vec::is_empty")]
51    pub pwr_solar_load: Vec<si::Power>,
52    // TODO: add provision for optional time-varying aux load
53    /// grade interpolator
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub grade_interp: Option<InterpolatorEnumOwned<f64>>,
56    /// elevation interpolator
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub elev_interp: Option<InterpolatorEnumOwned<f64>>,
59}
60
61#[pyo3_api]
62impl Cycle {
63    #[pyo3(name = "len")]
64    /// return the length of the cycle
65    fn len_py(&self) -> PyResult<usize> {
66        Ok(self.len_checked()?)
67    }
68
69    #[pyo3(name = "to_microtrips", signature=(stop_speed_m_per_s=None))]
70    /// convert cycle to a list of microtrips.
71    /// If stop speed is specified, it signifies the speed at or below which
72    /// a vehicle should be considered as stopped. This can be useful when
73    /// processing real-world data.
74    fn to_microtrips_py(&self, stop_speed_m_per_s: Option<f64>) -> PyResult<Vec<Cycle>> {
75        let stop_speed = stop_speed_m_per_s.map(|v| v * uc::MPS);
76        Ok(self.to_microtrips(stop_speed))
77    }
78
79    #[pyo3(name = "extend_time", signature=(absolute_time_s=None, time_fraction=None))]
80    /// extend cycle with idle time.
81    /// This is useful when a cycle's duration needs to be extended.
82    /// - absolute_time_s: optional time to extend the cycle
83    /// - time_fraction: optional fraction of cycle's duration to add to cycle.
84    ///
85    /// NOTE: if both absolute time and time fraction are specified, they
86    /// both add to extend the cycle. For example, if we have a 100 s cycle
87    /// and specify an absolute_time_s of 10 and time_fraction of 0.5, the
88    /// resulting cycle will have a duration of 160 s = 100.0 + (10 + 100.0 * 0.5)
89    fn extend_time_py(
90        &mut self,
91        absolute_time_s: Option<f64>,
92        time_fraction: Option<f64>,
93    ) -> PyResult<Cycle> {
94        let absolute_time = absolute_time_s.map(|t| t * uc::S);
95        let time_fraction = time_fraction.map(|f| f * uc::R);
96        Ok(self.extend_time(absolute_time, time_fraction))
97    }
98
99    #[pyo3(name = "dt_at_i")]
100    /// time step duration at step i.
101    pub fn dt_at_i_py(&self, i: usize) -> PyResult<f64> {
102        let i = std::cmp::max(1, i);
103        let dt = if i < self.time.len() {
104            self.time[i].get::<si::second>() - self.time[i - 1].get::<si::second>()
105        } else {
106            0.0
107        };
108        Ok(dt)
109    }
110
111    #[pyo3(name = "ending_idle_time_s")]
112    /// calculate and return the ending "idle" time of a cycle.
113    /// "Idle" time is defined as the amount of contiguous time
114    /// at the end of a cycle where the vehicle is not moving.
115    pub fn ending_idle_time_py(&self) -> PyResult<f64> {
116        let dt_end_idle = self.ending_idle_time();
117        Ok(dt_end_idle.get::<si::second>())
118    }
119
120    #[pyo3(name = "trim_ending_idle", signature=(idle_to_keep_s=None))]
121    /// trim ending "idle" time from a cycle.
122    /// The "idle" time is the time the vehicle is not moving.
123    /// - idle_to_keep_s: the amount of time to keep
124    ///
125    /// NOTE: if idle_to_keep_s is specified, the ending idle duration
126    /// will be UP TO this idle_to_keep_s amount but could be less if
127    /// there is insufficient idle time.
128    pub fn trim_ending_idle_py(&self, idle_to_keep_s: Option<f64>) -> PyResult<Cycle> {
129        let idle_to_keep = idle_to_keep_s.map(|idle| idle * uc::S);
130        Ok(self.trim_ending_idle(idle_to_keep))
131    }
132
133    #[pyo3(name = "average_speed_m_per_s", signature=(while_moving=None))]
134    /// calculate and return the average speed of the cycle in (m/s).
135    /// - while_moving: if specified and true, calculate the speed only
136    ///   while the vehicle is moving. Otherwise, calculate the average speed
137    ///   including stopped time.
138    pub fn average_speed_py(&self, while_moving: Option<bool>) -> PyResult<f64> {
139        let while_moving = while_moving.unwrap_or(false);
140        let vavg = self.average_speed(while_moving);
141        Ok(vavg.get::<si::meter_per_second>())
142    }
143
144    #[pyo3(name = "average_step_speeds_m_per_s")]
145    /// calculate and return the average speeds per time-step in (m/s).
146    pub fn average_step_speeds_py(&self) -> PyResult<Vec<f64>> {
147        Ok(self
148            .average_step_speeds()
149            .iter()
150            .map(|v| v.get::<si::meter_per_second>())
151            .collect())
152    }
153
154    #[pyo3(name = "average_step_speed_in_m_per_s_at")]
155    /// calculate the average step speed at the given step in (m/s).
156    pub fn average_step_speed_at_py(&self, i: usize) -> PyResult<f64> {
157        Ok(self.average_step_speed_at(i).get::<si::meter_per_second>())
158    }
159
160    #[pyo3(name = "resample")]
161    /// create a new cycle with the values resampled to the given time-step
162    /// duration.
163    pub fn resample_py(&self, time_step_s: f64) -> PyResult<Cycle> {
164        let time_step = time_step_s.max(0.01) * uc::S;
165        Ok(self.resample(time_step))
166    }
167}
168
169lazy_static! {
170    pub static ref ELEV_DEFAULT: si::Length = 400. * uc::FT;
171}
172
173impl Init for Cycle {
174    /// Sets `self.dist` and `self.elev`
175    /// # Assumptions
176    /// - if `init_elev.is_none()`, then defaults to [static@ELEV_DEFAULT]
177    fn init(&mut self) -> Result<(), Error> {
178        let _ = self
179            .len_checked()
180            .map_err(|err| Error::InitError(format_dbg!(err)))?;
181
182        if !self.temp_amb_air.is_empty() {
183            if self.temp_amb_air.len() != self.time.len() {
184                return Err(Error::InitError(format_dbg!()));
185            }
186        } else {
187            self.temp_amb_air = vec![*TE_STD_AIR; self.time.len()];
188        }
189
190        // calculate distance from RHS integral of speed and time
191        self.dist = {
192            self.time
193                .diff()
194                .iter()
195                .zip(&self.speed)
196                .scan(0. * uc::M, |dist, (dt, speed)| {
197                    *dist += *dt * *speed;
198                    Some(*dist)
199                })
200                .collect()
201        };
202
203        // populate grade if not provided
204        if self.grade.is_empty() {
205            self.grade = vec![
206                si::Ratio::ZERO;
207                self.len_checked()
208                    .map_err(|err| Error::InitError(format_dbg!(err)))?
209            ]
210        };
211        // calculate elevation from RHS integral of grade and distance
212        self.init_elev = self.init_elev.or_else(|| Some(*ELEV_DEFAULT));
213        self.elev = self
214            .grade
215            .iter()
216            .zip(&self.dist)
217            .scan(
218                // already guaranteed to be `Some`
219                self.init_elev.unwrap(),
220                |elev, (grade, dist)| {
221                    // TODO: Kyle, check this
222                    *elev += *dist * *grade;
223                    Some(*elev)
224                },
225            )
226            .collect();
227        let g0 = if !self.grade.is_empty() {
228            self.grade[0]
229        } else {
230            0.0 * uc::R
231        };
232        if self.grade.iter().all(|&g| g != g0) {
233            self.grade_interp = Some(
234                InterpolatorEnum::new_1d(
235                    self.dist.iter().map(|x| x.get::<si::meter>()).collect(),
236                    self.grade.iter().map(|y| y.get::<si::ratio>()).collect(),
237                    strategy::Linear,
238                    Extrapolate::Error,
239                )
240                .map_err(|e| Error::NinterpError(e.to_string()))?,
241            );
242
243            self.elev_interp = Some(
244                InterpolatorEnum::new_1d(
245                    self.dist.iter().map(|x| x.get::<si::meter>()).collect(),
246                    self.elev.iter().map(|y| y.get::<si::meter>()).collect(),
247                    strategy::Linear,
248                    Extrapolate::Error,
249                )
250                .map_err(|e| Error::NinterpError(e.to_string()))?,
251            );
252        } else {
253            self.grade_interp = Some(InterpolatorEnum::new_0d(g0.get::<si::ratio>()));
254            self.elev_interp = Some(InterpolatorEnum::new_0d(
255                self.init_elev.unwrap().get::<si::meter>(),
256            ));
257        }
258
259        Ok(())
260    }
261}
262
263impl SerdeAPI for Cycle {
264    const ACCEPTED_BYTE_FORMATS: &'static [&'static str] = &[
265        #[cfg(feature = "csv")]
266        "csv",
267        #[cfg(feature = "json")]
268        "json",
269        #[cfg(feature = "msgpack")]
270        "msgpack",
271        #[cfg(feature = "toml")]
272        "toml",
273        #[cfg(feature = "yaml")]
274        "yaml",
275    ];
276    const ACCEPTED_STR_FORMATS: &'static [&'static str] = &[
277        #[cfg(feature = "csv")]
278        "csv",
279        #[cfg(feature = "json")]
280        "json",
281        #[cfg(feature = "toml")]
282        "toml",
283        #[cfg(feature = "yaml")]
284        "yaml",
285    ];
286    #[cfg(feature = "resources")]
287    const RESOURCES_SUBDIR: &'static str = "cycles";
288
289    /// Write (serialize) an object into anything that implements [`std::io::Write`]
290    ///
291    /// # Arguments:
292    ///
293    /// * `wtr` - The writer into which to write object data
294    /// * `format` - The target format, any of those listed in [`ACCEPTED_BYTE_FORMATS`](`SerdeAPI::ACCEPTED_BYTE_FORMATS`)
295    ///
296    fn to_writer<W: std::io::Write>(&self, mut wtr: W, format: &str) -> Result<(), Error> {
297        match format.trim_start_matches('.').to_lowercase().as_str() {
298            #[cfg(feature = "csv")]
299            "csv" => {
300                let mut wtr = csv::Writer::from_writer(wtr);
301                for i in 0..self
302                    .len_checked()
303                    .map_err(|err| Error::SerdeError(format_dbg!(err)))?
304                {
305                    wtr.serialize(CycleElement {
306                        // unchecked indexing should be ok because of `self.len()`
307                        time: self.time[i],
308                        speed: self.speed[i],
309                        grade: if !self.grade.is_empty() {
310                            Some(self.grade[i])
311                        } else {
312                            None
313                        },
314                        pwr_max_charge: if !self.pwr_max_chrg.is_empty() {
315                            Some(self.pwr_max_chrg[i])
316                        } else {
317                            None
318                        },
319                        temp_amb_air: if !self.temp_amb_air.is_empty() {
320                            Some(self.temp_amb_air[i])
321                        } else {
322                            None
323                        },
324                        pwr_solar_load: if !self.pwr_solar_load.is_empty() {
325                            Some(self.pwr_solar_load[i])
326                        } else {
327                            None
328                        },
329                    })
330                    .map_err(|err| Error::SerdeError(format_dbg!(err)))?;
331                }
332                wtr.flush()
333                    .map_err(|err| Error::SerdeError(format_dbg!(err)))?
334            }
335            #[cfg(feature = "json")]
336            "json" => serde_json::to_writer(wtr, self)
337                .map_err(|err| Error::SerdeError(format_dbg!(err)))?,
338            #[cfg(feature = "toml")]
339            "toml" => {
340                let toml_string = self
341                    .to_toml()
342                    .map_err(|err| Error::SerdeError(format_dbg!(err)))?;
343                wtr.write_all(toml_string.as_bytes())
344                    .map_err(|err| Error::SerdeError(format_dbg!(err)))?;
345            }
346            #[cfg(feature = "yaml")]
347            "yaml" | "yml" => serde_yaml::to_writer(wtr, self)
348                .map_err(|err| Error::SerdeError(format_dbg!(err)))?,
349            _ => Err(Error::SerdeError(format!(
350                "Unsupported format {format:?}, must be one of {:?}",
351                Self::ACCEPTED_BYTE_FORMATS,
352            )))?,
353        }
354        Ok(())
355    }
356
357    /// Deserialize an object from anything that implements [`std::io::Read`]
358    ///
359    /// # Arguments:
360    ///
361    /// * `rdr` - The reader from which to read object data
362    /// * `format` - The source format, any of those listed in [`ACCEPTED_BYTE_FORMATS`](`SerdeAPI::ACCEPTED_BYTE_FORMATS`)
363    ///
364    fn from_reader<R: std::io::Read>(
365        rdr: &mut R,
366        format: &str,
367        skip_init: bool,
368    ) -> Result<Self, Error> {
369        let mut deserialized: Self =
370            match format.trim_start_matches('.').to_lowercase().as_str() {
371                #[cfg(feature = "csv")]
372                "csv" => {
373                    // Create empty cycle to be populated
374                    let mut cyc = Self::default();
375                    let mut rdr = csv::Reader::from_reader(rdr);
376                    for result in rdr.deserialize() {
377                        cyc.push(result.map_err(|err| Error::SerdeError(format_dbg!(err)))?)
378                            .map_err(|err| Error::SerdeError(format!("{err}")))?;
379                    }
380                    cyc
381                }
382                #[cfg(feature = "json")]
383                "json" => serde_json::from_reader(rdr)
384                    .map_err(|err| Error::SerdeError(format!("{err}")))?,
385                #[cfg(feature = "toml")]
386                "toml" => {
387                    let mut buf = String::new();
388                    rdr.read_to_string(&mut buf)
389                        .map_err(|err| Error::SerdeError(format_dbg!(err)))?;
390                    Self::from_toml(buf, skip_init)
391                        .map_err(|err| Error::SerdeError(format_dbg!(err)))?
392                }
393                #[cfg(feature = "yaml")]
394                "yaml" | "yml" => serde_yaml::from_reader(rdr)
395                    .map_err(|err| Error::SerdeError(format_dbg!(err)))?,
396                _ => {
397                    return Err(Error::SerdeError(format!(
398                        "Unsupported format {format:?}, must be one of {:?}",
399                        Self::ACCEPTED_BYTE_FORMATS
400                    )))
401                }
402            };
403        if !skip_init {
404            deserialized.init()?;
405        }
406        Ok(deserialized)
407    }
408
409    /// Write (serialize) an object into a string
410    ///
411    /// # Arguments:
412    ///
413    /// * `format` - The target format, any of those listed in [`ACCEPTED_STR_FORMATS`](`SerdeAPI::ACCEPTED_STR_FORMATS`)
414    ///
415    fn to_str(&self, format: &str) -> anyhow::Result<String> {
416        match format.trim_start_matches('.').to_lowercase().as_str() {
417            #[cfg(feature = "csv")]
418            "csv" => self.to_csv(),
419            #[cfg(feature = "json")]
420            "json" => self.to_json(),
421            #[cfg(feature = "toml")]
422            "toml" => self.to_toml(),
423            #[cfg(feature = "yaml")]
424            "yaml" | "yml" => self.to_yaml(),
425            _ => bail!(
426                "Unsupported format {format:?}, must be one of {:?}",
427                Self::ACCEPTED_STR_FORMATS
428            ),
429        }
430    }
431
432    /// Read (deserialize) an object from a string
433    ///
434    /// # Arguments:
435    ///
436    /// * `contents` - The string containing the object data
437    /// * `format` - The source format, any of those listed in [`ACCEPTED_STR_FORMATS`](`SerdeAPI::ACCEPTED_STR_FORMATS`)
438    ///
439    fn from_str<S: AsRef<str>>(contents: S, format: &str, skip_init: bool) -> anyhow::Result<Self> {
440        Ok(
441            match format.trim_start_matches('.').to_lowercase().as_str() {
442                #[cfg(feature = "csv")]
443                "csv" => Self::from_csv(contents, skip_init)?,
444                #[cfg(feature = "json")]
445                "json" => Self::from_json(contents, skip_init)?,
446                #[cfg(feature = "toml")]
447                "toml" => Self::from_toml(contents, skip_init)?,
448                #[cfg(feature = "yaml")]
449                "yaml" | "yml" => Self::from_yaml(contents, skip_init)?,
450                _ => bail!(
451                    "Unsupported format {format:?}, must be one of {:?}",
452                    Self::ACCEPTED_STR_FORMATS
453                ),
454            },
455        )
456    }
457}
458
459impl Cycle {
460    /// rust-internal time steps at i
461    pub fn dt_at_i(&self, i: usize) -> anyhow::Result<si::Time> {
462        Ok(*self.time.get(i).with_context(|| format_dbg!())?
463            - *self.time.get(i - 1).with_context(|| format_dbg!())?)
464    }
465
466    /// return the length of the cycle
467    pub fn len_checked(&self) -> anyhow::Result<usize> {
468        ensure!(
469            self.time.len() == self.speed.len(),
470            format!(
471                "{}\n`time` and `speed` fields do not have same `len()`",
472                format_dbg!()
473            )
474        );
475        ensure!(
476            self.dist.is_empty() || self.time.len() == self.dist.len(),
477            format!(
478                "{}\n`time` and `dist` fields do not have same `len()`",
479                format_dbg!()
480            )
481        );
482        ensure!(
483            self.grade.is_empty() || self.time.len() == self.grade.len(),
484            format!(
485                "{}\n`time` and `grade` fields do not have same `len()`",
486                format_dbg!()
487            )
488        );
489        ensure!(
490            self.elev.is_empty() || self.grade.len() == self.elev.len(),
491            format!(
492                "{}\n`grade` and `elev` fields do not have same `len()`",
493                format_dbg!()
494            )
495        );
496        ensure!(
497            self.pwr_max_chrg.is_empty() || self.time.len() == self.pwr_max_chrg.len(),
498            format!(
499                "{}\n`time` and `pwr_max_chrg` fields do not have same `len()`",
500                format_dbg!()
501            )
502        );
503        ensure!(
504            self.temp_amb_air.is_empty() || self.time.len() == self.temp_amb_air.len(),
505            format!(
506                "{}\n`time` and `temp_amb_air` fields do not have same `len()`",
507                format_dbg!()
508            )
509        );
510        Ok(self.time.len())
511    }
512
513    /// return true if the cycle is empty, else false
514    pub fn is_empty(&self) -> anyhow::Result<bool> {
515        Ok(self.len_checked().with_context(|| format_dbg!())? == 0)
516    }
517
518    /// append the given cycle element
519    pub fn push(&mut self, element: CycleElement) -> anyhow::Result<()> {
520        // TODO: maybe automate generation of this function as derive macro
521        // TODO: maybe automate `ensure!` that all vec fields are same length before returning result
522        // TODO: make sure all fields are being updated as appropriate
523        self.time.push(element.time);
524        self.speed.push(element.speed);
525        match element.grade {
526            Some(grade) => self.grade.push(grade),
527            None => self.grade.push(si::Ratio::ZERO),
528        }
529        match element.pwr_max_charge {
530            Some(pwr_max_chrg) => self.pwr_max_chrg.push(pwr_max_chrg),
531            None => self.pwr_max_chrg.push(si::Power::ZERO),
532        }
533        match element.temp_amb_air {
534            Some(temp_amb_air) => self.temp_amb_air.push(temp_amb_air),
535            None => self.temp_amb_air.push(*TE_STD_AIR),
536        }
537        match element.pwr_solar_load {
538            Some(pwr_solar_load) => self.pwr_solar_load.push(pwr_solar_load),
539            None => self.pwr_solar_load.push(si::Power::ZERO),
540        }
541        Ok(())
542    }
543
544    /// extend the cycle by a vector of elements
545    pub fn extend(&mut self, vec: Vec<CycleElement>) -> anyhow::Result<()> {
546        self.time.extend(vec.iter().map(|x| x.time).clone());
547        todo!();
548        // self.time.extend(vec.iter().map(|x| x.time).clone());
549        // match (&mut self.grade, vec.grade) {
550        //     (Some(grade_mut), Some(grade)) => grade_mut.push(grade),
551        //     (None, Some(_)) => {
552        //         bail!("Element and Cycle `grade` fields must both be `Some` or `None`")
553        //     }
554        //     (Some(_), None) => {
555        //         bail!("Element and Cycle `grade` fields must both be `Some` or `None`")
556        //     }
557        //     _ => {}
558        // }
559        // match (&mut self.pwr_max_chrg, vec.pwr_max_charge) {
560        //     (Some(pwr_max_chrg_mut), Some(pwr_max_chrg)) => pwr_max_chrg_mut.push(pwr_max_chrg),
561        //     (None, Some(_)) => {
562        //         bail!("Element and Cycle `pwr_max_chrg` fields must both be `Some` or `None`")
563        //     }
564        //     (Some(_), None) => {
565        //         bail!("Element and Cycle `pwr_max_chrg` fields must both be `Some` or `None`")
566        //     }
567        //     _ => {}
568        // }
569        // self.speed.push(vec.speed);
570        // Ok(())
571    }
572
573    /// trim the cycle to the given start_idx and end_idx.
574    ///
575    /// NOTE: ending cycle will include start_idx but NOT end_idx
576    pub fn trim(&mut self, start_idx: Option<usize>, end_idx: Option<usize>) -> anyhow::Result<()> {
577        let start_idx = start_idx.unwrap_or_default();
578        let len = self.len_checked().with_context(|| format_dbg!())?;
579        let end_idx = end_idx.unwrap_or(len);
580        ensure!(end_idx <= len, format_dbg!(end_idx <= len));
581
582        self.time = self.time[start_idx..end_idx].to_vec();
583        self.speed = self.speed[start_idx..end_idx].to_vec();
584        Ok(())
585    }
586
587    /// Write (serialize) cycle to a CSV string
588    #[cfg(feature = "csv")]
589    pub fn to_csv(&self) -> anyhow::Result<String> {
590        let mut buf = Vec::with_capacity(self.len_checked().with_context(|| format_dbg!())?);
591        self.to_writer(&mut buf, "csv")?;
592        Ok(String::from_utf8(buf)?)
593    }
594
595    /// Read (deserialize) an object from a CSV string
596    ///
597    /// # Arguments
598    ///
599    /// * `json_str` - JSON-formatted string to deserialize from
600    ///
601    #[cfg(feature = "csv")]
602    fn from_csv<S: AsRef<str>>(csv_str: S, skip_init: bool) -> anyhow::Result<Self> {
603        let mut csv_de = Self::from_reader(&mut csv_str.as_ref().as_bytes(), "csv", skip_init)?;
604        if !skip_init {
605            csv_de.init()?;
606        }
607        Ok(csv_de)
608    }
609
610    pub fn to_fastsim2(&self) -> anyhow::Result<Cycle2> {
611        let cyc2 = Cycle2 {
612            name: self.name.clone(),
613            time_s: self.time.iter().map(|t| t.get::<si::second>()).collect(),
614            mps: self
615                .speed
616                .iter()
617                .map(|s| s.get::<si::meter_per_second>())
618                .collect(),
619            grade: self.grade.iter().map(|g| g.get::<si::ratio>()).collect(),
620            orphaned: false,
621            road_type: vec![0.; self.len_checked().with_context(|| format_dbg!())?].into(),
622        };
623
624        Ok(cyc2)
625    }
626
627    /// convert cycle to a vector of CycleElement
628    pub fn to_elements(&self) -> Vec<CycleElement> {
629        let mut result = Vec::with_capacity(self.time.len());
630        for idx in 0..self.time.len() {
631            let element = CycleElement {
632                time: self.time[idx],
633                speed: self.speed[idx],
634                grade: if self.grade.is_empty() {
635                    None
636                } else {
637                    Some(self.grade[idx])
638                },
639                pwr_max_charge: if self.pwr_max_chrg.is_empty() {
640                    None
641                } else {
642                    Some(self.pwr_max_chrg[idx])
643                },
644                temp_amb_air: if self.temp_amb_air.is_empty() {
645                    None
646                } else {
647                    Some(self.temp_amb_air[idx])
648                },
649                pwr_solar_load: if self.pwr_solar_load.is_empty() {
650                    None
651                } else {
652                    Some(self.pwr_solar_load[idx])
653                },
654            };
655            result.push(element);
656        }
657        result
658    }
659
660    /// Convert cycle into a vector of "microtrips".
661    /// A microtrip is a start to a subsequent stop plus any idle time.
662    /// - stop_speed: the speed at or below which vehicle is considered "stopped"
663    ///
664    /// RETURN: vector of cycles with each cycle being a "microtrip".
665    pub fn to_microtrips(&self, stop_speed: Option<si::Velocity>) -> Vec<Cycle> {
666        let stop_speed = stop_speed.unwrap_or(1e-6 * uc::MPS);
667        let mut microtrips = Vec::new();
668        let mut current = Cycle {
669            name: self.name.clone(),
670            init_elev: self.init_elev,
671            time: vec![],
672            speed: vec![],
673            dist: vec![],
674            grade: vec![],
675            elev: vec![],
676            pwr_max_chrg: vec![],
677            temp_amb_air: vec![],
678            pwr_solar_load: vec![],
679            grade_interp: self.grade_interp.clone(),
680            elev_interp: self.elev_interp.clone(),
681        };
682        let elements = self.to_elements();
683        let mut moving: bool = false;
684        for element in &elements {
685            if element.speed > stop_speed && !moving && current.time.len() > 1 {
686                current.init().unwrap();
687                let last_idx = current.time.len() - 1;
688                let last_time = current.time[last_idx];
689                let last_speed = current.speed[last_idx];
690                let last_grade = if last_idx >= current.grade.len() {
691                    None
692                } else {
693                    Some(current.grade[last_idx])
694                };
695                let last_elevation = if last_idx >= current.elev.len() {
696                    None
697                } else {
698                    Some(current.elev[last_idx])
699                };
700                let last_temperature = if last_idx >= current.temp_amb_air.len() {
701                    None
702                } else {
703                    Some(current.temp_amb_air[last_idx])
704                };
705                let last_solar_load = if last_idx >= current.pwr_solar_load.len() {
706                    None
707                } else {
708                    Some(current.pwr_solar_load[last_idx])
709                };
710                let last_charge_power = if last_idx >= current.pwr_max_chrg.len() {
711                    None
712                } else {
713                    Some(current.pwr_max_chrg[last_idx])
714                };
715                current.time = current.time.iter().map(|t| *t - current.time[0]).collect();
716                microtrips.push(current.clone());
717                current = Cycle {
718                    name: self.name.clone(),
719                    init_elev: last_elevation,
720                    time: vec![last_time],
721                    speed: vec![last_speed],
722                    dist: vec![],
723                    grade: if let Some(g) = last_grade {
724                        vec![g]
725                    } else {
726                        vec![]
727                    },
728                    elev: vec![],
729                    pwr_max_chrg: if let Some(p) = last_charge_power {
730                        vec![p]
731                    } else {
732                        vec![]
733                    },
734                    temp_amb_air: if let Some(temp) = last_temperature {
735                        vec![temp]
736                    } else {
737                        vec![]
738                    },
739                    pwr_solar_load: if let Some(p) = last_solar_load {
740                        vec![p]
741                    } else {
742                        vec![]
743                    },
744                    grade_interp: self.grade_interp.clone(),
745                    elev_interp: self.elev_interp.clone(),
746                };
747            }
748            current
749                .push(element.clone())
750                .expect("Push shouldn't have an error path");
751            moving = element.speed > stop_speed;
752        }
753        if current.time.len() > 1 {
754            current.time = current.time.iter().map(|t| *t - current.time[0]).collect();
755            current.init().unwrap();
756            microtrips.push(current.clone());
757        }
758        microtrips
759    }
760
761    /// Determine average speed of cycle.
762    /// -- while_moving: if true, only takes average while moving
763    ///
764    /// RETURN: average speed
765    pub fn average_speed(&self, while_moving: bool) -> si::Velocity {
766        let mut d = si::Length::ZERO;
767        let mut t = si::Time::ZERO;
768        for idx in 1..self.speed.len() {
769            let dt = self.time[idx] - self.time[idx - 1];
770            let vavg = 0.5 * (self.speed[idx] + self.speed[idx - 1]);
771            let dd = vavg * dt;
772            let no_move = (dd.get::<si::meter>().ceil() as i32) == 0;
773            d += dd;
774            t += if while_moving && no_move {
775                si::Time::ZERO
776            } else {
777                dt
778            };
779        }
780        if t > si::Time::ZERO {
781            d / t
782        } else {
783            si::Velocity::ZERO
784        }
785    }
786
787    /// Return the average step speeds of the cycle as vector of velicities.
788    /// NOTE: the average speed from sample i-1 to i will appear as entry i.
789    /// RETURN: vector of velocities representing average step speeds.
790    pub fn average_step_speeds(&self) -> Vec<si::Velocity> {
791        let mut result = Vec::with_capacity(self.time.len());
792        result.push(0.0 * uc::MPS);
793        for i in 1..self.time.len() {
794            result.push(0.5 * (self.speed[i] + self.speed[i - 1]));
795        }
796        result
797    }
798
799    /// Calculate the average step speed at step i
800    /// (i.e., from sample point i-1 to i)
801    pub fn average_step_speed_at(&self, i: usize) -> si::Velocity {
802        if i >= self.speed.len() {
803            return 0.0 * uc::MPS;
804        }
805        0.5 * (self.speed[i] + self.speed[i - 1])
806    }
807
808    /// The distances traveled over each step using trapezoidal
809    /// integration.
810    pub fn trapz_step_distances(&self) -> Vec<si::Length> {
811        let mut result = Vec::with_capacity(self.time.len());
812        result.push(0.0 * uc::M);
813        for i in 1..self.time.len() {
814            let step_time = self.time[i] - self.time[i - 1];
815            let average_speed = 0.5 * (self.speed[i] + self.speed[i - 1]);
816            result.push(step_time * average_speed);
817        }
818        result
819    }
820
821    /// The elevation climb each step using trapezoidal integration.
822    pub fn trapz_step_elevations(&self) -> Vec<si::Length> {
823        let mut result = Vec::with_capacity(self.time.len());
824        result.push(0.0 * uc::M);
825        for i in 1..self.time.len() {
826            let step_time = self.time[i].get::<si::second>() - self.time[i - 1].get::<si::second>();
827            let average_speed = 0.5
828                * (self.speed[i].get::<si::meter_per_second>()
829                    + self.speed[i - 1].get::<si::meter_per_second>());
830            let step_dist = step_time * average_speed;
831            let gr = self.grade[i].get::<si::ratio>();
832            let dh = gr.atan().cos() * step_dist * gr;
833            result.push(dh * uc::M);
834        }
835        result
836    }
837
838    /// The distance traveled from start to the beginning of step i
839    /// (i.e., distance traveled up to sample point i-1)
840    pub fn trapz_step_start_distance(&self, step: usize) -> si::Length {
841        let mut distance = 0.0 * uc::M;
842        let step_max = cmp::min(step, self.time.len());
843        for i in 1..step_max {
844            let step_time = self.time[i] - self.time[i - 1];
845            let average_speed = 0.5 * (self.speed[i] + self.speed[i - 1]);
846            distance += step_time * average_speed;
847        }
848        distance
849    }
850
851    /// The distance traveled during the given step
852    /// (i.e., distance from sample point i-1 to i for step i)
853    pub fn trapz_distance_for_step(&self, step: usize) -> si::Length {
854        let average_speed = self.average_step_speed_at(step);
855        let elapsed_time = self.time[step] - self.time[step - 1];
856        average_speed * elapsed_time
857    }
858
859    /// Calculate the distance from step i_start to the start of step i_end
860    /// (i.e., distance from sample point i_start - 1 to i_end - 1)
861    pub fn trapz_distance_over_range(&self, step0: usize, step1: usize) -> si::Length {
862        let distances = self.trapz_step_distances();
863        let last_i = cmp::max(distances.len() - 1, 0);
864        let i_start = cmp::min(step0, last_i);
865        let i_end = cmp::min(step1, last_i);
866        let mut distance = 0.0 * uc::M;
867        for d in &distances[cmp::min(i_start, i_end)..cmp::max(i_start, i_end)] {
868            distance += *d;
869        }
870        distance
871    }
872
873    /// Calculate the time in a cycle spent moving
874    /// - stopped_speed_m_per_s: the speed above which we are considered to be moving
875    ///
876    /// RETURN: the time spent moving in seconds
877    pub fn time_spent_moving(&self, stopped_speed: Option<si::Velocity>) -> si::Time {
878        let stop_speed = stopped_speed.unwrap_or(0.0 * uc::MPS);
879        let mut result = 0.0 * uc::S;
880        for i in 1..self.time.len() {
881            let step_time = self.time[i] - self.time[i - 1];
882            if self.speed[i] > stop_speed || self.speed[i - 1] > stop_speed {
883                result += step_time;
884            }
885        }
886        result
887    }
888
889    /// Create distance and target speeds by microtrip.
890    /// Splits cycle into microtrips and returns a list of
891    /// 2-tuples of:
892    /// (distance from start in meters, target speed in m/s)
893    /// The distance is measured to the start of the microtrip.
894    ///
895    /// # Parameters
896    ///
897    /// * `blend_factor`: from 0.0 to 1.0
898    ///    - if 0.0, use the average speed of the microtrip
899    ///    - if 1.0, use the average speed while moving (i.e., no stopped time)
900    ///    - otherwise, something in between
901    /// * `min_target_speed`: the minimum target speed allowed
902    ///
903    /// # Result
904    ///
905    /// List of 2-tuple of (distance from start, target speed).
906    /// A tuple represents the distance from start of the start
907    /// of the given microtrip and its target speed.
908    ///
909    /// # Notes
910    ///
911    /// * target speed per microtrip is not allowed to be
912    ///   below the `min_target_speed`
913    pub fn distance_and_target_speeds_by_microtrip(
914        &self,
915        stop_speed: Option<si::Velocity>,
916        blend_factor: f64,
917        min_target_speed: si::Velocity,
918    ) -> Vec<(si::Length, si::Velocity)> {
919        let blend_factor = blend_factor.clamp(0.0, 1.0);
920        let mut result = Vec::new();
921        let microtrips = self.to_microtrips(stop_speed);
922        let mut distance_at_start = 0.0 * uc::M;
923        let t0 = 0.0 * uc::S;
924        let v0 = 0.0 * uc::MPS;
925        let d0 = 0.0 * uc::M;
926        for mt in microtrips {
927            let distance = mt
928                .trapz_step_distances()
929                .iter()
930                .fold(0.0 * uc::M, |total, dist| total + *dist);
931            let last_index = cmp::max(mt.time.len() - 1, 0);
932            let end_time = mt.time[last_index];
933            let start_time = mt.time[0];
934            let total_time = end_time - start_time;
935            let moving_time = mt.time_spent_moving(stop_speed);
936            let average_speed = if total_time > t0 {
937                distance / total_time
938            } else {
939                v0
940            };
941            let moving_average_speed = if moving_time > t0 {
942                distance / moving_time
943            } else {
944                v0
945            };
946            let target_speed =
947                blend_factor * (moving_average_speed - average_speed) + average_speed;
948            let target_speed = if target_speed > min_target_speed {
949                target_speed
950            } else {
951                min_target_speed
952            };
953            if distance > d0 {
954                result.push((distance_at_start, target_speed));
955                distance_at_start += distance;
956            }
957        }
958        result
959    }
960
961    /// Add idle time to Cycle.
962    /// By "idle" time, we mean "stopped" time (i.e., vehicle not moving).
963    pub fn extend_time(
964        &self,
965        absolute_time: Option<si::Time>,
966        time_fraction: Option<si::Ratio>,
967    ) -> Cycle {
968        let absolute_time = absolute_time.unwrap_or(0.0 * uc::S);
969        let time_fraction = time_fraction.unwrap_or(0.0 * uc::R);
970        let mut ts = self.time.clone();
971        let mut vs = self.speed.clone();
972        let mut gs = self.grade.clone();
973        let mut ps = self.pwr_max_chrg.clone();
974        let mut temps = self.temp_amb_air.clone();
975        let mut ss = self.pwr_solar_load.clone();
976        let t_end = *ts.last().unwrap();
977        let extra_time_s = (absolute_time.get::<si::second>()
978            + time_fraction.get::<si::ratio>() * t_end.get::<si::second>())
979        .round() as i32;
980        if extra_time_s == 0 {
981            return self.clone();
982        }
983        let dt = 1.0 * uc::S;
984        let dt_s = dt.get::<si::second>();
985        let mut idx = 1;
986        loop {
987            let dt_extra_s = dt_s * idx as f64;
988            if dt_extra_s > extra_time_s as f64 {
989                break;
990            }
991            ts.push(t_end + dt_extra_s * uc::S);
992            vs.push(0.0 * uc::MPS);
993            if !gs.is_empty() {
994                gs.push(0.0 * uc::R);
995            }
996            if !ps.is_empty() {
997                ps.push(*ps.last().unwrap());
998            }
999            if !temps.is_empty() {
1000                temps.push(*temps.last().unwrap());
1001            }
1002            if !ss.is_empty() {
1003                ss.push(*ss.last().unwrap());
1004            }
1005            idx += 1;
1006        }
1007        let mut cyc = Cycle {
1008            name: self.name.clone(),
1009            init_elev: self.init_elev,
1010            time: ts,
1011            speed: vs,
1012            dist: vec![],
1013            grade: gs,
1014            elev: vec![],
1015            pwr_max_chrg: vec![],
1016            grade_interp: self.grade_interp.clone(),
1017            elev_interp: self.elev_interp.clone(),
1018            temp_amb_air: temps,
1019            pwr_solar_load: ss,
1020        };
1021        cyc.init().unwrap();
1022        cyc
1023    }
1024
1025    /// Create a cache object for faster computations on Cycle.
1026    pub fn build_cache(&self) -> CycleCache {
1027        CycleCache::new(self)
1028    }
1029
1030    /// Returns the average grade over the given range of distances.
1031    /// - distance_start: the distance at start of evaluation area
1032    /// - delta_distance: distance traveled from distance_start
1033    /// - cache: optional CycleCache which can save computation time
1034    ///
1035    /// RETURN: average grade (rise over run) for the given range.
1036    ///
1037    /// NOTE: grade is assumed to be constant from just after the
1038    /// previous sample point until the current sample point (inclusive).
1039    /// That is, grade[i] applies from distance, d, of (d[i - 1], d[i]]
1040    pub fn average_grade_over_range(
1041        &self,
1042        distance_start: si::Length,
1043        delta_distance: si::Length,
1044        cache: Option<&CycleCache>,
1045    ) -> si::Ratio {
1046        let tol = 1e-6;
1047        match &cache {
1048            Some(cc) => {
1049                let dd_m = delta_distance.get::<si::meter>();
1050                if cc.grade_all_zero {
1051                    0.0 * uc::R
1052                } else if dd_m <= tol {
1053                    let dist_m = distance_start.get::<si::meter>();
1054                    cc.interp_grade(dist_m) * uc::R
1055                } else {
1056                    let dist0_m = distance_start.get::<si::meter>();
1057                    let dist1_m = dist0_m + dd_m;
1058                    let e0 = cc.interp_elevation(dist0_m);
1059                    let e1 = cc.interp_elevation(dist1_m);
1060                    ((e1 - e0) / dd_m).asin().tan() * uc::R
1061                }
1062            }
1063            None => {
1064                let zero_grade = 0.0 * uc::R;
1065                let grade_all_zero = {
1066                    let mut all0 = true;
1067                    for idx in 0..self.grade.len() {
1068                        if self.grade[idx] != zero_grade {
1069                            all0 = false;
1070                            break;
1071                        }
1072                    }
1073                    all0
1074                };
1075                if grade_all_zero {
1076                    0.0 * uc::R
1077                } else {
1078                    let delta_dists_m: Vec<f64> = self
1079                        .trapz_step_distances()
1080                        .iter()
1081                        .map(|dd| dd.get::<si::meter>())
1082                        .collect();
1083                    let trapz_distances_m = {
1084                        let mut d = 0.0;
1085                        let mut result = Vec::with_capacity(delta_dists_m.len());
1086                        for dd in &delta_dists_m {
1087                            d += *dd;
1088                            result.push(d);
1089                        }
1090                        result
1091                    };
1092                    let dist0_m = distance_start.get::<si::meter>();
1093                    let dd_m = delta_distance.get::<si::meter>();
1094                    let dist1_m = dist0_m + dd_m;
1095                    if dd_m < tol {
1096                        if dist0_m < trapz_distances_m[0] {
1097                            return self.grade[0];
1098                        }
1099                        let max_idx = self.grade.len() - 1;
1100                        if dist0_m > trapz_distances_m[max_idx] {
1101                            return self.grade[max_idx];
1102                        }
1103                        for idx in 1..self.time.len() {
1104                            if dist0_m > trapz_distances_m[idx - 1]
1105                                && dist0_m <= trapz_distances_m[idx]
1106                            {
1107                                return self.grade[idx];
1108                            }
1109                        }
1110                        self.grade[max_idx]
1111                    } else {
1112                        // NOTE: we use the following instead of delta_elev_m
1113                        // as it uses more precise trapezoidal diatance and
1114                        // elevation at sample points. This also uses the
1115                        // fully accurate trig functions in case we have large
1116                        // slope angles. This level of rigor may be overkill.
1117                        let trapz_elevations_m = {
1118                            let delta_elevs_m: Vec<f64> = self
1119                                .grade
1120                                .iter()
1121                                .zip(delta_dists_m)
1122                                .map(|(g, dd)| {
1123                                    let gr = g.get::<si::ratio>();
1124                                    gr.atan().cos() * dd * gr
1125                                })
1126                                .collect();
1127                            let mut result = Vec::with_capacity(delta_elevs_m.len());
1128                            let mut elev_m = 0.0;
1129                            for de in &delta_elevs_m {
1130                                elev_m += *de;
1131                                result.push(elev_m);
1132                            }
1133                            result
1134                        };
1135                        let interp: InterpolatorEnum<ndarray::OwnedRepr<f64>> =
1136                            InterpolatorEnum::new_1d(
1137                                trapz_distances_m.clone().into(),
1138                                trapz_elevations_m.clone().into(),
1139                                strategy::Linear,
1140                                Extrapolate::Clamp,
1141                            )
1142                            .unwrap();
1143                        let e0_m = interp.interpolate(&[dist0_m]).unwrap();
1144                        let e1_m = interp.interpolate(&[dist1_m]).unwrap();
1145                        ((e1_m - e0_m) / dd_m).asin().tan() * uc::R
1146                    }
1147                }
1148            }
1149        }
1150    }
1151
1152    /// Calculate the distance to next stop from `distance`.
1153    /// - distance: the distance to calculate distance-to-stop from
1154    ///
1155    /// RETURN: returns the distance to the next stop from `distance`
1156    ///
1157    /// NOTE: distance may be negative if we're beyond the last stop
1158    pub fn calc_distance_to_next_stop_from(
1159        &self,
1160        distance: si::Length,
1161        cache: Option<&CycleCache>,
1162    ) -> si::Length {
1163        let tol = 1e-6;
1164        let distance_m = distance.get::<si::meter>();
1165        match cache {
1166            Some(cc) => {
1167                for (&d_m, &v) in cc.trapz_distances_m.iter().zip(self.speed.iter()) {
1168                    let v_mps = v.get::<si::meter_per_second>();
1169                    if (v_mps < tol) && (d_m > (distance_m + tol)) {
1170                        return (d_m - distance_m) * uc::M;
1171                    }
1172                }
1173                (*cc.trapz_distances_m.last().unwrap_or(&0.0) * uc::M) - distance
1174            }
1175            None => {
1176                let ds_m = {
1177                    let mut result = Vec::with_capacity(self.time.len());
1178                    let mut d_m = 0.0;
1179                    for dd in self.trapz_step_distances() {
1180                        let dd_m = dd.get::<si::meter>();
1181                        d_m += dd_m;
1182                        result.push(d_m);
1183                    }
1184                    result
1185                };
1186                for (&d_m, &v) in ds_m.iter().zip(self.speed.iter()) {
1187                    let v_mps = v.get::<si::meter_per_second>();
1188                    if (v_mps < tol) && (d_m > (distance_m + tol)) {
1189                        return (d_m - distance_m) * uc::M;
1190                    }
1191                }
1192                *ds_m.last().unwrap_or(&0.0) * uc::M
1193            }
1194        }
1195    }
1196
1197    /// Modify the cycle using the given constant-jerk trajectory.
1198    /// - i: the index into the cycle to initiate modification
1199    ///   NOTE: THIS point is modified as trajectory is calculated as
1200    ///   starting at i-1
1201    /// - n: the number of steps ahead
1202    /// - jerk: the jerk (deriviative of acceleration with time)
1203    /// - accel0: the starting accelartion
1204    ///
1205    /// NOTE:
1206    /// - modifies the cycle in-place. Purpose is to allow hitting
1207    ///   a rendezvous point in time/speed in the future.
1208    /// - CAUTION: not robust against variable duration time-steps
1209    ///
1210    /// RETURN: the final modified speed
1211    pub fn modify_by_const_jerk_trajectory(
1212        &mut self,
1213        i: usize,
1214        n: usize,
1215        jerk: si::Jerk,
1216        accel0: si::Acceleration,
1217    ) -> si::Velocity {
1218        if n == 0 {
1219            return si::Velocity::ZERO;
1220        }
1221        let jerk_m_per_s3 = jerk.get::<si::meter_per_second_cubed>();
1222        let accel0_m_per_s2 = accel0.get::<si::meter_per_second_squared>();
1223        let num_samples = self.speed.len();
1224        if i >= num_samples {
1225            if num_samples > 0 {
1226                return self.speed[num_samples - 1];
1227            }
1228            return si::Velocity::ZERO;
1229        }
1230        let v0 = self.speed[i - 1].get::<si::meter_per_second>();
1231        let dt = (self.time[i] - self.time[i - 1]).get::<si::second>();
1232        let mut v = v0;
1233        for ni in 1..(n + 1) {
1234            let idx_to_set = (i - 1) + ni;
1235            if idx_to_set >= num_samples {
1236                break;
1237            }
1238            v = speed_for_constant_jerk(ni, v0, accel0_m_per_s2, jerk_m_per_s3, dt);
1239            self.speed[idx_to_set] = v.max(0.0) * uc::MPS;
1240        }
1241        self.init().unwrap();
1242        v * uc::MPS
1243    }
1244
1245    /// Modify cycle to add a braking trajectory that would cover the same
1246    /// distance as the given constant brake deceleration.
1247    /// - brake_accel: the brake acceleration (m/s2); must be negative
1248    /// - i: index where to initiate the stop trectory; start of the step
1249    /// - desired_distance_to_stop: the desired distance to stop within. If
1250    ///   not provided, it is calculated based on the braking deceleration.
1251    ///
1252    /// RETURN: (final speed of modified trajectory, number of steps to complete)
1253    /// - the final speed should be zero ideally
1254    /// - the number of time-steps required to complete the braking maneuver
1255    ///
1256    /// NOTE:
1257    /// - modifies the cycle in-place.
1258    pub fn modify_with_braking_trajectory(
1259        &mut self,
1260        brake_accel: si::Acceleration,
1261        i: usize,
1262        desired_distance_to_stop: Option<si::Length>,
1263    ) -> (si::Velocity, usize) {
1264        let brake_accel = if brake_accel > si::Acceleration::ZERO {
1265            -brake_accel
1266        } else {
1267            brake_accel
1268        };
1269        assert!(brake_accel < si::Acceleration::ZERO);
1270        if i >= self.time.len() {
1271            return (*self.speed.last().unwrap(), 0);
1272        }
1273        let i = if i < 1 { 1 } else { i };
1274        let v0 = self.speed[i - 1].get::<si::meter_per_second>();
1275        let dt = (self.time[i] - self.time[i - 1]).get::<si::second>();
1276        let brake_accel_m_per_s2 = brake_accel.get::<si::meter_per_second_squared>();
1277        // distance-to-stop (m)
1278        let dts_m = match desired_distance_to_stop {
1279            Some(value) => {
1280                let result = value.get::<si::meter>();
1281                if result > 0.0 {
1282                    result
1283                } else {
1284                    -0.5 * v0 * v0 / brake_accel_m_per_s2
1285                }
1286            }
1287            None => -0.5 * v0 * v0 / brake_accel_m_per_s2,
1288        };
1289        if dts_m <= 0.0 {
1290            return (v0 * uc::MPS, 0);
1291        }
1292        // time-to-stop (s)
1293        let tts_s = -v0 / brake_accel_m_per_s2;
1294        // number of steps to stop
1295        let n = (tts_s / dt).round() as usize;
1296        let n = if n < 2 { 2 } else { n }; // need at least 2 steps
1297        let traj =
1298            ConstantJerkTrajectory::from_speed_and_distance_targets(n, 0.0, v0, dts_m, 0.0, dt);
1299        let v_final = self.modify_by_const_jerk_trajectory(
1300            i,
1301            n,
1302            traj.jerk_m_per_s3 * uc::MPS3,
1303            traj.acceleration_m_per_s2 * uc::MPS2,
1304        );
1305        (v_final, n)
1306    }
1307
1308    /// Report the stopped time (i.e., idle) at the end of a cycle.
1309    ///
1310    /// RESULT: time vehicle is at zero speed at cycle end
1311    pub fn ending_idle_time(&self) -> si::Time {
1312        let mut result = si::Time::ZERO;
1313        let vzero = si::Velocity::ZERO;
1314        for idx in (1..self.time.len()).rev() {
1315            let v0 = self.speed[idx - 1];
1316            let v1 = self.speed[idx];
1317            if v0 != vzero || v1 != vzero {
1318                break;
1319            } else {
1320                let dt = self.time[idx] - self.time[idx - 1];
1321                result += dt;
1322            }
1323        }
1324        result
1325    }
1326
1327    /// Remove idel time at end of cycle except for the optionally
1328    /// specified duration.
1329    /// - idle_to_keep: optional duration of idle time to keep. Default is 0 s
1330    ///
1331    /// RESULT: a new cycle with idle time trimmed.
1332    pub fn trim_ending_idle(&self, idle_to_keep: Option<si::Time>) -> Cycle {
1333        let idle_to_keep = idle_to_keep.unwrap_or(si::Time::ZERO).max(si::Time::ZERO);
1334        let vzero = si::Velocity::ZERO;
1335        let mut idle_start_idx = 0;
1336        for idx in (1..self.time.len()).rev() {
1337            let v0 = self.speed[idx - 1];
1338            let v1 = self.speed[idx];
1339            if v0 != vzero || v1 != vzero {
1340                idle_start_idx = idx + 1;
1341                break;
1342            }
1343        }
1344        if idle_start_idx >= self.time.len() {
1345            return self.clone();
1346        }
1347        let end_idx = if idle_to_keep == si::Time::ZERO {
1348            idle_start_idx
1349        } else {
1350            let mut dt_idle = si::Time::ZERO;
1351            let mut idx_drop = idle_start_idx;
1352            for idx in idle_start_idx..self.time.len() {
1353                let dt = self.time[idx] - self.time[idx - 1];
1354                dt_idle += dt;
1355                if dt_idle > idle_to_keep {
1356                    idx_drop = idx;
1357                    break;
1358                }
1359            }
1360            idx_drop
1361        };
1362        let mut cyc = Cycle {
1363            name: self.name.clone(),
1364            time: self.time[0..end_idx].to_vec(),
1365            speed: self.speed[0..end_idx].to_vec(),
1366            init_elev: self.init_elev,
1367            grade: if self.grade.is_empty() {
1368                vec![]
1369            } else {
1370                self.grade[0..end_idx].to_vec()
1371            },
1372            dist: vec![],
1373            elev: vec![],
1374            pwr_max_chrg: if self.pwr_max_chrg.is_empty() {
1375                vec![]
1376            } else {
1377                self.pwr_max_chrg[0..end_idx].to_vec()
1378            },
1379            temp_amb_air: if self.temp_amb_air.is_empty() {
1380                vec![]
1381            } else {
1382                self.temp_amb_air[0..end_idx].to_vec()
1383            },
1384            pwr_solar_load: if self.pwr_solar_load.is_empty() {
1385                vec![]
1386            } else {
1387                self.pwr_solar_load[0..end_idx].to_vec()
1388            },
1389            grade_interp: None,
1390            elev_interp: None,
1391        };
1392        cyc.init().unwrap();
1393        cyc
1394    }
1395
1396    /// Resample cycle to a lower or higher frequency.
1397    /// - dt: the new step duration.
1398    ///
1399    /// RETURN: cycle
1400    /// NOTE: a value of dt <= 0 s implies to just clone the current cycle
1401    /// "as is"
1402    pub fn resample(&self, dt: si::Time) -> Cycle {
1403        if dt <= si::Time::ZERO {
1404            return self.clone();
1405        }
1406        let mut t = si::Time::ZERO;
1407        let speed_interp: InterpolatorEnum<OwnedRepr<f64>> = InterpolatorEnum::new_1d(
1408            self.time.iter().map(|x| x.get::<si::second>()).collect(),
1409            self.speed
1410                .iter()
1411                .map(|y| y.get::<si::meter_per_second>())
1412                .collect(),
1413            strategy::Linear,
1414            Extrapolate::Clamp,
1415        )
1416        .unwrap();
1417        let grade_interp: InterpolatorEnum<OwnedRepr<f64>> = InterpolatorEnum::new_1d(
1418            self.time.iter().map(|x| x.get::<si::second>()).collect(),
1419            self.grade.iter().map(|y| y.get::<si::ratio>()).collect(),
1420            strategy::RightNearest,
1421            Extrapolate::Clamp,
1422        )
1423        .unwrap();
1424        let temp_interp: Option<InterpolatorEnum<OwnedRepr<f64>>> =
1425            if self.temp_amb_air.len() == self.time.len() {
1426                Some(
1427                    InterpolatorEnum::new_1d(
1428                        self.time.iter().map(|t| t.get::<si::second>()).collect(),
1429                        self.temp_amb_air
1430                            .iter()
1431                            .map(|temp| temp.get::<si::kelvin_abs>())
1432                            .collect(),
1433                        strategy::Linear,
1434                        Extrapolate::Clamp,
1435                    )
1436                    .unwrap(),
1437                )
1438            } else {
1439                None
1440            };
1441        let solar_interp: Option<InterpolatorEnum<OwnedRepr<f64>>> =
1442            if self.pwr_solar_load.len() == self.time.len() {
1443                Some(
1444                    InterpolatorEnum::new_1d(
1445                        self.time.iter().map(|t| t.get::<si::second>()).collect(),
1446                        self.pwr_solar_load
1447                            .iter()
1448                            .map(|p| p.get::<si::kilowatt>())
1449                            .collect(),
1450                        strategy::Linear,
1451                        Extrapolate::Clamp,
1452                    )
1453                    .unwrap(),
1454                )
1455            } else {
1456                None
1457            };
1458        let chg_pwr_interp: Option<InterpolatorEnum<OwnedRepr<f64>>> =
1459            if self.pwr_max_chrg.len() == self.time.len() {
1460                Some(
1461                    InterpolatorEnum::new_1d(
1462                        self.time.iter().map(|t| t.get::<si::second>()).collect(),
1463                        self.pwr_max_chrg
1464                            .iter()
1465                            .map(|p| p.get::<si::kilowatt>())
1466                            .collect(),
1467                        strategy::Linear,
1468                        Extrapolate::Clamp,
1469                    )
1470                    .unwrap(),
1471                )
1472            } else {
1473                None
1474            };
1475        let mut ts = vec![];
1476        let mut vs = vec![];
1477        let mut gs = vec![];
1478        let mut pwr_chg = vec![];
1479        let mut temps = vec![];
1480        let mut solars = vec![];
1481        while t <= self.time[self.time.len() - 1] {
1482            ts.push(t);
1483            let t0 = t.get::<si::second>();
1484            let v = speed_interp.interpolate(&[t0]).unwrap();
1485            vs.push(v * uc::MPS);
1486            let g = grade_interp.interpolate(&[t0]).unwrap();
1487            gs.push(g * uc::R);
1488            if let Some(ref interp) = chg_pwr_interp {
1489                let pchg = interp.interpolate(&[t0]).unwrap();
1490                pwr_chg.push(pchg * uc::KW);
1491            }
1492            if let Some(ref interp) = temp_interp {
1493                let temp = interp.interpolate(&[t0]).unwrap();
1494                temps.push(temp * uc::KELVIN);
1495            }
1496            if let Some(ref interp) = solar_interp {
1497                let solar = interp.interpolate(&[t0]).unwrap();
1498                solars.push(solar * uc::KW);
1499            }
1500            t += dt;
1501        }
1502
1503        let mut cyc = Cycle {
1504            name: self.name.clone(),
1505            init_elev: self.init_elev,
1506            time: ts,
1507            speed: vs,
1508            dist: vec![],
1509            grade: gs,
1510            elev: vec![],
1511            pwr_max_chrg: pwr_chg,
1512            temp_amb_air: temps,
1513            pwr_solar_load: solars,
1514            grade_interp: None,
1515            elev_interp: None,
1516        };
1517        cyc.init().unwrap();
1518        cyc
1519    }
1520}
1521
1522#[serde_api]
1523#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)]
1524#[non_exhaustive]
1525#[serde(deny_unknown_fields)]
1526#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
1527/// Element of `Cycle`.  Used for vec-like operations.
1528pub struct CycleElement {
1529    /// simulation time \[s\]
1530    #[serde(alias = "cycSecs")]
1531    pub time: si::Time,
1532    /// simulation power \[W\]
1533    #[serde(alias = "speed_mps", alias = "cycMps")]
1534    pub speed: si::Velocity,
1535    // `dist` is not included here because it is derived in `Init::init`
1536    // TODO: make `fastsim_api` handle Option or write custom getter/setter
1537    /// road grade
1538    #[serde(alias = "cycGrade")]
1539    pub grade: Option<si::Ratio>,
1540    // `elev` is not included here because it is derived in `Init::init`
1541    /// road charging/discharing capacity
1542    pub pwr_max_charge: Option<si::Power>,
1543    // TODO: make sure all fields in cycle are represented here, as appropriate
1544    /// ambient air temperature w.r.t. to time (rather than spatial position)
1545    pub temp_amb_air: Option<si::Temperature>,
1546    /// solar heat load w.r.t. to time (rather than spatial position)
1547    pub pwr_solar_load: Option<si::Power>,
1548}
1549
1550impl SerdeAPI for CycleElement {}
1551impl Init for CycleElement {}
1552
1553#[pyo3_api]
1554impl CycleElement {}
1555
1556#[cfg(test)]
1557mod tests {
1558    use super::{manipulation_utils::ConstantJerkTrajectory, *};
1559    fn mock_cyc_len_2() -> Cycle {
1560        let mut cyc = Cycle {
1561            name: String::new(),
1562            init_elev: None,
1563            time: (0..=2).map(|x| (x as f64) * uc::S).collect(),
1564            speed: (0..=2).map(|x| (x as f64) * uc::MPS).collect(),
1565            dist: vec![],
1566            grade: (0..=2).map(|x| (x as f64 * uc::R) / 100.).collect(),
1567            elev: vec![],
1568            pwr_max_chrg: vec![],
1569            grade_interp: Default::default(),
1570            elev_interp: Default::default(),
1571            temp_amb_air: Default::default(),
1572            pwr_solar_load: Default::default(),
1573        };
1574        cyc.init().unwrap();
1575        cyc
1576    }
1577
1578    fn make_two_triangles_cycle() -> Cycle {
1579        let mut cyc = Cycle {
1580            name: String::from("Two Triangles"),
1581            init_elev: Some(0.0 * uc::M),
1582            time: vec![
1583                0.0 * uc::S,
1584                10.0 * uc::S,
1585                20.0 * uc::S,
1586                30.0 * uc::S,
1587                40.0 * uc::S,
1588                50.0 * uc::S,
1589            ],
1590            speed: vec![
1591                0.0 * uc::MPS,
1592                4.0 * uc::MPS,
1593                0.0 * uc::MPS,
1594                0.0 * uc::MPS,
1595                5.0 * uc::MPS,
1596                0.0 * uc::MPS,
1597            ],
1598            dist: vec![],
1599            grade: vec![
1600                0.0 * uc::R,
1601                0.0 * uc::R,
1602                0.0 * uc::R,
1603                0.0 * uc::R,
1604                0.01 * uc::R,
1605                0.01 * uc::R,
1606            ],
1607            elev: vec![],
1608            pwr_max_chrg: vec![],
1609            grade_interp: Default::default(),
1610            elev_interp: Default::default(),
1611            temp_amb_air: Default::default(),
1612            pwr_solar_load: Default::default(),
1613        };
1614        cyc.init().unwrap();
1615        cyc
1616    }
1617
1618    #[test]
1619    fn test_init() {
1620        let cyc = mock_cyc_len_2();
1621        assert_eq!(
1622            cyc.dist,
1623            [0., 1., 3.] // meters
1624                .iter()
1625                .map(|x| *x * uc::M)
1626                .collect::<Vec<si::Length>>()
1627        );
1628        assert_eq!(
1629            cyc.elev,
1630            [121.92, 121.93, 121.99000000000001] // meters
1631                .iter()
1632                .map(|x| *x * uc::M)
1633                .collect::<Vec<si::Length>>()
1634        );
1635    }
1636
1637    #[test]
1638    fn test_to_elements() {
1639        let cyc = mock_cyc_len_2();
1640        let elements = cyc.to_elements();
1641        assert_eq!(elements.len(), 3);
1642        assert_eq!(elements[0].time, 0.0 * uc::S);
1643        assert_eq!(elements[2].time, cyc.time[2]);
1644        assert_eq!(elements[2].speed, cyc.speed[2]);
1645        assert_eq!(elements[2].grade.unwrap(), 0.02 * uc::R);
1646        assert!(elements[2].pwr_max_charge.is_none());
1647        assert_eq!(elements[2].temp_amb_air.unwrap(), *TE_STD_AIR);
1648        assert!(elements[2].pwr_solar_load.is_none());
1649    }
1650
1651    #[test]
1652    fn test_to_microtrips() {
1653        let cyc = make_two_triangles_cycle();
1654        let actual = cyc.to_microtrips(Some(0.01 * uc::MPH));
1655        assert_eq!(actual.len(), 2);
1656        let cyc0 = &actual[0];
1657        assert_eq!(
1658            cyc0.time,
1659            vec![0.0 * uc::S, 10.0 * uc::S, 20.0 * uc::S, 30.0 * uc::S]
1660        );
1661        assert_eq!(
1662            cyc0.speed,
1663            vec![0.0 * uc::MPS, 4.0 * uc::MPS, 0.0 * uc::MPS, 0.0 * uc::MPS]
1664        );
1665        assert_eq!(
1666            cyc0.grade,
1667            vec![0.0 * uc::R, 0.0 * uc::R, 0.0 * uc::R, 0.0 * uc::R]
1668        );
1669        let cyc1 = &actual[1];
1670        assert_eq!(cyc1.time, vec![0.0 * uc::S, 10.0 * uc::S, 20.0 * uc::S]);
1671        assert_eq!(
1672            cyc1.speed,
1673            vec![0.0 * uc::MPS, 5.0 * uc::MPS, 0.0 * uc::MPS]
1674        );
1675        assert_eq!(cyc1.grade, vec![0.0 * uc::R, 0.01 * uc::R, 0.01 * uc::R]);
1676    }
1677
1678    #[test]
1679    fn test_distance_and_target_speeds_by_microtrip() {
1680        let cyc = make_two_triangles_cycle();
1681        let expected = [
1682            (0.0 * uc::M, (40.0 / 20.0) * uc::MPS),
1683            (40.0 * uc::M, (50.0 / 20.0) * uc::MPS),
1684        ];
1685        let actual = cyc.distance_and_target_speeds_by_microtrip(None, 1.0, 0.0 * uc::MPS);
1686        assert_eq!(actual.len(), expected.len());
1687        for i in 0..expected.len() {
1688            assert_eq!(actual[i].0, expected[i].0);
1689            assert_eq!(actual[i].1, expected[i].1);
1690        }
1691        let expected = [
1692            (0.0 * uc::M, (40.0 / 30.0) * uc::MPS),
1693            (40.0 * uc::M, (50.0 / 20.0) * uc::MPS),
1694        ];
1695        let actual = cyc.distance_and_target_speeds_by_microtrip(None, 0.0, 0.0 * uc::MPS);
1696        assert_eq!(actual.len(), expected.len());
1697        for i in 0..expected.len() {
1698            assert_eq!(actual[i].0, expected[i].0);
1699            assert_eq!(actual[i].1, expected[i].1);
1700        }
1701    }
1702
1703    #[test]
1704    fn test_extending_cycle_time() {
1705        let cyc = make_two_triangles_cycle();
1706        let expected = {
1707            let mut c = Cycle {
1708                name: String::from("Two Triangles"),
1709                init_elev: Some(0.0 * uc::M),
1710                time: vec![
1711                    0.0 * uc::S,
1712                    10.0 * uc::S,
1713                    20.0 * uc::S,
1714                    30.0 * uc::S,
1715                    40.0 * uc::S,
1716                    50.0 * uc::S,
1717                    51.0 * uc::S,
1718                    52.0 * uc::S,
1719                    53.0 * uc::S,
1720                    54.0 * uc::S,
1721                    55.0 * uc::S,
1722                    56.0 * uc::S,
1723                    57.0 * uc::S,
1724                    58.0 * uc::S,
1725                ],
1726                speed: vec![
1727                    0.0 * uc::MPS,
1728                    4.0 * uc::MPS,
1729                    0.0 * uc::MPS,
1730                    0.0 * uc::MPS,
1731                    5.0 * uc::MPS,
1732                    0.0 * uc::MPS,
1733                    0.0 * uc::MPS,
1734                    0.0 * uc::MPS,
1735                    0.0 * uc::MPS,
1736                    0.0 * uc::MPS,
1737                    0.0 * uc::MPS,
1738                    0.0 * uc::MPS,
1739                    0.0 * uc::MPS,
1740                    0.0 * uc::MPS,
1741                ],
1742                dist: vec![],
1743                grade: vec![
1744                    0.0 * uc::R,
1745                    0.0 * uc::R,
1746                    0.0 * uc::R,
1747                    0.0 * uc::R,
1748                    0.01 * uc::R,
1749                    0.01 * uc::R,
1750                    0.0 * uc::R,
1751                    0.0 * uc::R,
1752                    0.0 * uc::R,
1753                    0.0 * uc::R,
1754                    0.0 * uc::R,
1755                    0.0 * uc::R,
1756                    0.0 * uc::R,
1757                    0.0 * uc::R,
1758                ],
1759                elev: vec![],
1760                pwr_max_chrg: vec![],
1761                grade_interp: Default::default(),
1762                elev_interp: Default::default(),
1763                temp_amb_air: Default::default(),
1764                pwr_solar_load: Default::default(),
1765            };
1766            c.init().unwrap();
1767            c
1768        };
1769        let absolute_time = Some(3.0 * uc::S);
1770        let time_fraction = Some(0.10 * uc::R);
1771        // extend by 3 s and 10% of existing time (i.e., 5 s)
1772        // = extend by 8 s
1773        let actual = cyc.extend_time(absolute_time, time_fraction);
1774        assert_eq!(actual, expected);
1775    }
1776
1777    /// Round the given number n to the given number of digits
1778    /// - n: the number to round
1779    /// - digits: the digits to round or defaults to 2; if not positive,
1780    fn round(n: f64, digits: Option<i32>) -> f64 {
1781        let digits = digits.unwrap_or(2);
1782        let digits = if digits < 0 { 0 } else { digits };
1783        let multiplier = 10.0_f64.powi(digits);
1784        (n * multiplier).round() / multiplier
1785    }
1786
1787    #[test]
1788    fn cycle_step_distances_are_as_expected() {
1789        let c = make_two_triangles_cycle();
1790        let expected = [
1791            0.0 * uc::M,
1792            20.0 * uc::M,
1793            20.0 * uc::M,
1794            0.0 * uc::M,
1795            25.0 * uc::M,
1796            25.0 * uc::M,
1797        ];
1798        let actual = c.trapz_step_distances();
1799        assert_eq!(actual.len(), expected.len());
1800        for i in 0..expected.len() {
1801            assert_eq!(actual[i], expected[i], "differ at step {i}");
1802        }
1803    }
1804
1805    #[test]
1806    fn cycle_elevations_are_as_expected() {
1807        let c = make_two_triangles_cycle();
1808        let dh = 0.01_f64.atan().cos() * 25.0_f64 * 0.01_f64;
1809        let expected = [
1810            0.0 * uc::M,
1811            0.0 * uc::M,
1812            0.0 * uc::M,
1813            0.0 * uc::M,
1814            dh * uc::M,
1815            dh * uc::M,
1816        ];
1817        let actual = c.trapz_step_elevations();
1818        assert_eq!(actual.len(), expected.len());
1819        for i in 0..expected.len() {
1820            assert_eq!(actual[i], expected[i], "differ at step {i}");
1821        }
1822    }
1823
1824    #[test]
1825    fn cycle_cache_yields_same_results() {
1826        let c = make_two_triangles_cycle();
1827        let cache = c.build_cache();
1828        let dist_m = 0.0;
1829        let e0_expected = 0.0;
1830        let e0_actual = cache.interp_elevation(dist_m);
1831        assert_eq!(e0_actual, e0_expected);
1832        let dist_m = 65.0;
1833        let e1_expected = 0.01_f64.atan().cos() * 25.0_f64 * 0.01_f64;
1834        let e1_actual = cache.interp_elevation(dist_m);
1835        assert_eq!(e1_actual, e1_expected);
1836    }
1837
1838    #[test]
1839    fn average_grade_over_range_is_correct() {
1840        let c = make_two_triangles_cycle();
1841        let cache = c.build_cache();
1842        let d0 = 40.0 * uc::M;
1843        let dd = 50.0 * uc::M;
1844        let expected0 = 0.01 * uc::R;
1845        let actual00 = c.average_grade_over_range(d0, dd, None);
1846        let actual00 = round(actual00.get::<si::ratio>(), Some(6)) * uc::R;
1847        assert_eq!(actual00, expected0);
1848        let actual01 = c.average_grade_over_range(d0, dd, Some(&cache));
1849        let actual01 = round(actual01.get::<si::ratio>(), Some(6)) * uc::R;
1850        assert_eq!(actual01, expected0);
1851    }
1852
1853    #[test]
1854    fn distance_to_next_stop_is_correct() {
1855        let c = make_two_triangles_cycle();
1856        let cache = c.build_cache();
1857        let d = 20.0 * uc::M;
1858        let expected = 20.0 * uc::M;
1859        let actual = c.calc_distance_to_next_stop_from(d, None);
1860        assert_eq!(actual, expected);
1861        let actual = c.calc_distance_to_next_stop_from(d, Some(&cache));
1862        assert_eq!(actual, expected);
1863        let d = 65.0 * uc::M;
1864        let expected = 25.0 * uc::M;
1865        let actual = c.calc_distance_to_next_stop_from(d, None);
1866        assert_eq!(actual, expected);
1867        let actual = c.calc_distance_to_next_stop_from(d, Some(&cache));
1868        assert_eq!(actual, expected);
1869        let d = 0.0 * uc::M;
1870        let expected = 40.0 * uc::M;
1871        let actual = c.calc_distance_to_next_stop_from(d, None);
1872        assert_eq!(actual, expected);
1873        let actual = c.calc_distance_to_next_stop_from(d, Some(&cache));
1874        assert_eq!(actual, expected);
1875    }
1876
1877    #[test]
1878    fn modifying_a_cycle_with_trajectory() {
1879        let c0 = make_two_triangles_cycle();
1880        let mut c = c0.clone();
1881        let n = 3;
1882        let d0 = 20.0; // units: m
1883        let v0 = 4.0; // units: m/s
1884        let dr = 65.0; // units: m
1885        let vr = 5.0; // units: m/s
1886        let dt = 10.0; // units: s
1887        let traj = ConstantJerkTrajectory::from_speed_and_distance_targets(n, d0, v0, dr, vr, dt);
1888        c.modify_by_const_jerk_trajectory(
1889            2,
1890            n,
1891            traj.jerk_m_per_s3 * uc::MPS3,
1892            traj.acceleration_m_per_s2 * uc::MPS2,
1893        );
1894        let expected = {
1895            let mut cyc = Cycle {
1896                name: String::from("Two Triangles"),
1897                init_elev: Some(0.0 * uc::M),
1898                time: vec![
1899                    0.0 * uc::S,
1900                    10.0 * uc::S,
1901                    20.0 * uc::S,
1902                    30.0 * uc::S,
1903                    40.0 * uc::S,
1904                    50.0 * uc::S,
1905                ],
1906                speed: vec![
1907                    0.0 * uc::MPS,
1908                    4.0 * uc::MPS,
1909                    traj.speed_at_step(1) * uc::MPS,
1910                    traj.speed_at_step(2) * uc::MPS,
1911                    5.0 * uc::MPS,
1912                    0.0 * uc::MPS,
1913                ],
1914                dist: vec![],
1915                grade: vec![
1916                    0.0 * uc::R,
1917                    0.0 * uc::R,
1918                    0.0 * uc::R,
1919                    0.0 * uc::R,
1920                    0.01 * uc::R,
1921                    0.01 * uc::R,
1922                ],
1923                elev: vec![],
1924                pwr_max_chrg: vec![],
1925                grade_interp: Default::default(),
1926                elev_interp: Default::default(),
1927                temp_amb_air: Default::default(),
1928                pwr_solar_load: Default::default(),
1929            };
1930            cyc.init().expect("initializaiton should not throw");
1931            cyc
1932        };
1933        assert_eq!(c.time.len(), expected.time.len());
1934        assert_eq!(c.speed.len(), expected.speed.len());
1935        assert_eq!(c.dist.len(), expected.dist.len());
1936        assert_eq!(c.grade.len(), expected.grade.len());
1937        for idx in 0..c.speed.len() {
1938            assert_eq!(c.time[idx], expected.time[idx]);
1939            assert_eq!(c.speed[idx], expected.speed[idx]);
1940            assert_eq!(c.dist[idx], expected.dist[idx]);
1941            assert_eq!(c.grade[idx], expected.grade[idx]);
1942        }
1943    }
1944
1945    #[test]
1946    pub fn modify_with_braking_trajectory() {
1947        let mut actual = {
1948            let mut cyc = Cycle {
1949                name: String::from("Test"),
1950                init_elev: Some(0.0 * uc::M),
1951                time: vec![
1952                    0.0 * uc::S,
1953                    1.0 * uc::S,
1954                    2.0 * uc::S,
1955                    3.0 * uc::S,
1956                    4.0 * uc::S,
1957                    5.0 * uc::S,
1958                ],
1959                speed: vec![
1960                    0.0 * uc::MPS,
1961                    4.0 * uc::MPS,
1962                    4.0 * uc::MPS,
1963                    1.0 * uc::MPS,
1964                    1.0 * uc::MPS,
1965                    0.0 * uc::MPS,
1966                ],
1967                dist: vec![],
1968                grade: vec![],
1969                elev: vec![],
1970                pwr_max_chrg: vec![],
1971                grade_interp: Default::default(),
1972                elev_interp: Default::default(),
1973                temp_amb_air: Default::default(),
1974                pwr_solar_load: Default::default(),
1975            };
1976            cyc.init().expect("initializaiton should not throw");
1977            cyc
1978        };
1979        let precision = Some(6);
1980        let (v_end, n_steps) =
1981            actual.modify_with_braking_trajectory((-4.0 / 3.0) * uc::MPS2, 3, Some(4.0 * uc::M));
1982        let v_end = round(v_end.get::<si::meter_per_second>(), precision);
1983        assert_eq!(v_end, 0.0);
1984        assert_eq!(n_steps, 3);
1985        let expected = {
1986            let n = 3;
1987            let d0 = 0.0;
1988            let v0 = 4.0;
1989            let dr = 4.0;
1990            let vr = 0.0;
1991            let dt = 1.0;
1992            let traj =
1993                ConstantJerkTrajectory::from_speed_and_distance_targets(n, d0, v0, dr, vr, dt);
1994            let mut cyc = Cycle {
1995                name: String::from("Test"),
1996                init_elev: Some(0.0 * uc::M),
1997                time: vec![
1998                    0.0 * uc::S,
1999                    1.0 * uc::S,
2000                    2.0 * uc::S,
2001                    3.0 * uc::S,
2002                    4.0 * uc::S,
2003                    5.0 * uc::S,
2004                ],
2005                speed: vec![
2006                    0.0 * uc::MPS,
2007                    4.0 * uc::MPS,
2008                    4.0 * uc::MPS,
2009                    traj.speed_at_step(1) * uc::MPS,
2010                    traj.speed_at_step(2) * uc::MPS,
2011                    traj.speed_at_step(3) * uc::MPS,
2012                ],
2013                dist: vec![],
2014                grade: vec![],
2015                elev: vec![],
2016                pwr_max_chrg: vec![],
2017                grade_interp: Default::default(),
2018                elev_interp: Default::default(),
2019                temp_amb_air: Default::default(),
2020                pwr_solar_load: Default::default(),
2021            };
2022            cyc.init().expect("initializaiton should not throw");
2023            cyc
2024        };
2025        assert_eq!(actual.time.len(), expected.time.len());
2026        for i in 0..actual.time.len() {
2027            let at = round(actual.time[i].get::<si::second>(), precision);
2028            let et = round(expected.time[i].get::<si::second>(), precision);
2029            let av = round(actual.speed[i].get::<si::meter_per_second>(), precision);
2030            let ev = round(expected.speed[i].get::<si::meter_per_second>(), precision);
2031            let ad = round(actual.dist[i].get::<si::meter>(), precision);
2032            let ed = round(expected.dist[i].get::<si::meter>(), precision);
2033            assert_eq!(at, et, "time@t={et}&i={i}");
2034            assert_eq!(av, ev, "speed@t={et}&i={i}");
2035            assert_eq!(ad, ed, "dist@t={et}&i={i}");
2036        }
2037    }
2038
2039    #[test]
2040    pub fn test_trim() {
2041        let c = make_two_triangles_cycle();
2042        let cyc = c.extend_time(Some(10.0 * uc::S), None);
2043        let dt_idle = cyc.ending_idle_time();
2044        assert_eq!(dt_idle, 10.0 * uc::S);
2045        // NOTE: extend_time adds time by 1.0 s increments so 10 points
2046        assert_eq!(cyc.time.len(), c.time.len() + 10);
2047        assert_eq!(*cyc.time.iter().last().unwrap(), 60.0 * uc::S);
2048        let cyc_trimmed = cyc.trim_ending_idle(None);
2049        assert_eq!(cyc_trimmed.time.len(), c.time.len());
2050    }
2051    type StructWithResources = Cycle;
2052
2053    #[test]
2054    fn test_resources() {
2055        let resource_list = StructWithResources::list_resources().unwrap();
2056        assert!(!resource_list.is_empty());
2057
2058        // verify that resources can all load
2059        for resource in resource_list {
2060            StructWithResources::from_resource(resource.clone(), false)
2061                .with_context(|| format_dbg!(resource))
2062                .unwrap();
2063        }
2064    }
2065
2066    #[test]
2067    fn test_resample() {
2068        let cyc0 = {
2069            let mut c = Cycle {
2070                name: String::from("a test"),
2071                time: vec![0.0 * uc::S, 10.0 * uc::S, 20.0 * uc::S],
2072                speed: vec![0.0 * uc::MPS, 10.0 * uc::MPS, 0.0 * uc::MPS],
2073                grade: vec![0.01 * uc::R, 0.01 * uc::R, -0.01 * uc::R],
2074                init_elev: None,
2075                dist: vec![],
2076                elev: vec![],
2077                pwr_max_chrg: vec![],
2078                temp_amb_air: vec![],
2079                pwr_solar_load: vec![],
2080                grade_interp: None,
2081                elev_interp: None,
2082            };
2083            c.init().unwrap();
2084            c
2085        };
2086        let cyc1 = cyc0.resample(1.0 * uc::S);
2087        assert_eq!(21, cyc1.time.len());
2088        assert_eq!(
2089            cyc1.time[cyc1.time.len() - 1],
2090            cyc0.time[cyc0.time.len() - 1]
2091        );
2092        assert_eq!(cyc1.time[0], cyc0.time[0]);
2093        assert_eq!(cyc1.time[0], 0.0 * uc::S);
2094        assert_eq!(cyc1.time[5], 5.0 * uc::S);
2095        assert_eq!(cyc1.speed[5], 5.0 * uc::MPS);
2096        assert_eq!(cyc1.grade[5], 0.01 * uc::R);
2097        assert_eq!(cyc1.time[10], 10.0 * uc::S);
2098        assert_eq!(cyc1.speed[10], 10.0 * uc::MPS);
2099        assert_eq!(cyc1.grade[10], 0.01 * uc::R);
2100        assert_eq!(cyc1.time[11], 11.0 * uc::S);
2101        assert_eq!(cyc1.speed[11], 9.0 * uc::MPS);
2102        assert_eq!(cyc1.grade[11], -0.01 * uc::R);
2103        assert_eq!(cyc1.time[20], 20.0 * uc::S);
2104        assert_eq!(cyc1.speed[20], 0.0 * uc::MPS);
2105        assert_eq!(cyc1.grade[20], -0.01 * uc::R);
2106    }
2107}