fastsim_core/
drive_cycle.rs

1use crate::imports::*;
2use crate::prelude::*;
3#[cfg(feature = "pyo3")]
4use crate::resources;
5use fastsim_2::cycle::RustCycle as Cycle2;
6
7#[serde_api]
8#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
9#[non_exhaustive]
10#[serde(deny_unknown_fields)]
11#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
12/// Container
13pub struct Cycle {
14    /// Name of cycle (can be left empty)
15    #[serde(default, skip_serializing_if = "String::is_empty")]
16    pub name: String,
17    // TODO: either write or automate generation of getter and setter for this
18    // TODO: put the above TODO in github issue for all fields with `Option<...>` type
19    /// inital elevation
20    pub init_elev: Option<si::Length>,
21    /// simulation time
22    pub time: Vec<si::Time>,
23    /// prescribed speed
24    #[serde(alias = "speed_mps")]
25    pub speed: Vec<si::Velocity>,
26    // TODO: consider trapezoidal integration scheme
27    /// calculated prescribed distance based on RHS integral of time and speed
28    #[serde(default, skip_serializing_if = "Vec::is_empty")]
29    pub dist: Vec<si::Length>,
30    /// road grade (expressed as a decimal, not percent)
31    #[serde(default, skip_serializing_if = "Vec::is_empty")]
32    pub grade: Vec<si::Ratio>,
33    // TODO: consider trapezoidal integration scheme
34    // TODO: @mokeefe, please check out how elevation is handled
35    /// calculated prescribed elevation based on RHS integral distance and grade
36    #[serde(default, skip_serializing_if = "Vec::is_empty")]
37    pub elev: Vec<si::Length>,
38    /// road charging/discharing capacity
39    #[serde(default, skip_serializing_if = "Vec::is_empty")]
40    pub pwr_max_chrg: Vec<si::Power>,
41    /// ambient air temperature w.r.t. to time (rather than spatial position)
42    #[serde(default, skip_serializing_if = "Vec::is_empty")]
43    pub temp_amb_air: Vec<si::Temperature>,
44    /// solar heat load w.r.t. to time (rather than spatial position)
45    #[serde(default, skip_serializing_if = "Vec::is_empty")]
46    pub pwr_solar_load: Vec<si::Power>,
47    // TODO: add provision for optional time-varying aux load
48    /// grade interpolator
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub grade_interp: Option<Interpolator>,
51    /// elevation interpolator
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub elev_interp: Option<Interpolator>,
54}
55
56#[named_struct_pyo3_api]
57impl Cycle {
58    #[pyo3(name = "list_resources")]
59    #[staticmethod]
60    /// list available cycle resources
61    fn list_resources_py() -> Vec<String> {
62        resources::list_resources(Self::RESOURCE_PREFIX)
63    }
64
65    #[pyo3(name = "len")]
66    fn len_py(&self) -> PyResult<usize> {
67        Ok(self.len_checked()?)
68    }
69}
70
71lazy_static! {
72    pub static ref ELEV_DEFAULT: si::Length = 400. * uc::FT;
73}
74
75impl Init for Cycle {
76    /// Sets `self.dist` and `self.elev`
77    /// # Assumptions
78    /// - if `init_elev.is_none()`, then defaults to [static@ELEV_DEFAULT]
79    fn init(&mut self) -> Result<(), Error> {
80        let _ = self
81            .len_checked()
82            .map_err(|err| Error::InitError(format_dbg!(err)))?;
83
84        if !self.temp_amb_air.is_empty() {
85            if self.temp_amb_air.len() != self.time.len() {
86                return Err(Error::InitError(format_dbg!()));
87            }
88        } else {
89            self.temp_amb_air = vec![*TE_STD_AIR; self.time.len()];
90        }
91
92        // calculate distance from RHS integral of speed and time
93        self.dist = {
94            self.time
95                .diff()
96                .iter()
97                .zip(&self.speed)
98                .scan(0. * uc::M, |dist, (dt, speed)| {
99                    *dist += *dt * *speed;
100                    Some(*dist)
101                })
102                .collect()
103        };
104
105        // populate grade if not provided
106        if self.grade.is_empty() {
107            self.grade = vec![
108                si::Ratio::ZERO;
109                self.len_checked()
110                    .map_err(|err| Error::InitError(format_dbg!(err)))?
111            ]
112        };
113        // calculate elevation from RHS integral of grade and distance
114        self.init_elev = self.init_elev.or_else(|| Some(*ELEV_DEFAULT));
115        self.elev = self
116            .grade
117            .iter()
118            .zip(&self.dist)
119            .scan(
120                // already guaranteed to be `Some`
121                self.init_elev.unwrap(),
122                |elev, (grade, dist)| {
123                    // TODO: Kyle, check this
124                    *elev += *dist * *grade;
125                    Some(*elev)
126                },
127            )
128            .collect();
129        let g0 = self.grade[0];
130        if self.grade.iter().all(|&g| g != g0) {
131            self.grade_interp = Some(
132                Interpolator::new_1d(
133                    self.dist.iter().map(|x| x.get::<si::meter>()).collect(),
134                    self.grade.iter().map(|y| y.get::<si::ratio>()).collect(),
135                    Strategy::Linear,
136                    Extrapolate::Error,
137                )
138                .map_err(ninterp::error::Error::from)?,
139            );
140
141            self.elev_interp = Some(
142                Interpolator::new_1d(
143                    self.dist.iter().map(|x| x.get::<si::meter>()).collect(),
144                    self.elev.iter().map(|y| y.get::<si::meter>()).collect(),
145                    Strategy::Linear,
146                    Extrapolate::Error,
147                )
148                .map_err(ninterp::error::Error::from)?,
149            );
150        } else {
151            self.grade_interp = Some(Interpolator::Interp0D(g0.get::<si::ratio>()));
152            self.elev_interp = Some(Interpolator::Interp0D(
153                self.init_elev.unwrap().get::<si::meter>(),
154            ));
155        }
156
157        Ok(())
158    }
159}
160
161impl SerdeAPI for Cycle {
162    const ACCEPTED_BYTE_FORMATS: &'static [&'static str] = &[
163        #[cfg(feature = "csv")]
164        "csv",
165        #[cfg(feature = "json")]
166        "json",
167        #[cfg(feature = "toml")]
168        "toml",
169        #[cfg(feature = "yaml")]
170        "yaml",
171    ];
172    const ACCEPTED_STR_FORMATS: &'static [&'static str] = &[
173        #[cfg(feature = "csv")]
174        "csv",
175        #[cfg(feature = "json")]
176        "json",
177        #[cfg(feature = "toml")]
178        "toml",
179        #[cfg(feature = "yaml")]
180        "yaml",
181    ];
182    #[cfg(feature = "resources")]
183    const RESOURCE_PREFIX: &'static str = "cycles";
184
185    /// Write (serialize) an object into anything that implements [`std::io::Write`]
186    ///
187    /// # Arguments:
188    ///
189    /// * `wtr` - The writer into which to write object data
190    /// * `format` - The target format, any of those listed in [`ACCEPTED_BYTE_FORMATS`](`SerdeAPI::ACCEPTED_BYTE_FORMATS`)
191    ///
192    fn to_writer<W: std::io::Write>(&self, mut wtr: W, format: &str) -> Result<(), Error> {
193        match format.trim_start_matches('.').to_lowercase().as_str() {
194            #[cfg(feature = "csv")]
195            "csv" => {
196                let mut wtr = csv::Writer::from_writer(wtr);
197                for i in 0..self
198                    .len_checked()
199                    .map_err(|err| Error::SerdeError(format_dbg!(err)))?
200                {
201                    wtr.serialize(CycleElement {
202                        // unchecked indexing should be ok because of `self.len()`
203                        time: self.time[i],
204                        speed: self.speed[i],
205                        grade: if !self.grade.is_empty() {
206                            Some(self.grade[i])
207                        } else {
208                            None
209                        },
210                        pwr_max_charge: if !self.pwr_max_chrg.is_empty() {
211                            Some(self.pwr_max_chrg[i])
212                        } else {
213                            None
214                        },
215                        temp_amb_air: if !self.temp_amb_air.is_empty() {
216                            Some(self.temp_amb_air[i])
217                        } else {
218                            None
219                        },
220                        pwr_solar_load: if !self.pwr_solar_load.is_empty() {
221                            Some(self.pwr_solar_load[i])
222                        } else {
223                            None
224                        },
225                    })
226                    .map_err(|err| Error::SerdeError(format_dbg!(err)))?;
227                }
228                wtr.flush()
229                    .map_err(|err| Error::SerdeError(format_dbg!(err)))?
230            }
231            #[cfg(feature = "json")]
232            "json" => serde_json::to_writer(wtr, self)
233                .map_err(|err| Error::SerdeError(format_dbg!(err)))?,
234            #[cfg(feature = "toml")]
235            "toml" => {
236                let toml_string = self
237                    .to_toml()
238                    .map_err(|err| Error::SerdeError(format_dbg!(err)))?;
239                wtr.write_all(toml_string.as_bytes())
240                    .map_err(|err| Error::SerdeError(format_dbg!(err)))?;
241            }
242            #[cfg(feature = "yaml")]
243            "yaml" | "yml" => serde_yaml::to_writer(wtr, self)
244                .map_err(|err| Error::SerdeError(format_dbg!(err)))?,
245            _ => Err(Error::SerdeError(format!(
246                "Unsupported format {format:?}, must be one of {:?}",
247                Self::ACCEPTED_BYTE_FORMATS,
248            )))?,
249        }
250        Ok(())
251    }
252
253    /// Deserialize an object from anything that implements [`std::io::Read`]
254    ///
255    /// # Arguments:
256    ///
257    /// * `rdr` - The reader from which to read object data
258    /// * `format` - The source format, any of those listed in [`ACCEPTED_BYTE_FORMATS`](`SerdeAPI::ACCEPTED_BYTE_FORMATS`)
259    ///
260    fn from_reader<R: std::io::Read>(
261        rdr: &mut R,
262        format: &str,
263        skip_init: bool,
264    ) -> Result<Self, Error> {
265        let mut deserialized: Self =
266            match format.trim_start_matches('.').to_lowercase().as_str() {
267                #[cfg(feature = "csv")]
268                "csv" => {
269                    // Create empty cycle to be populated
270                    let mut cyc = Self::default();
271                    let mut rdr = csv::Reader::from_reader(rdr);
272                    for result in rdr.deserialize() {
273                        cyc.push(result.map_err(|err| Error::SerdeError(format_dbg!(err)))?)
274                            .map_err(|err| Error::SerdeError(format!("{err}")))?;
275                    }
276                    cyc
277                }
278                #[cfg(feature = "json")]
279                "json" => serde_json::from_reader(rdr)
280                    .map_err(|err| Error::SerdeError(format!("{err}")))?,
281                #[cfg(feature = "toml")]
282                "toml" => {
283                    let mut buf = String::new();
284                    rdr.read_to_string(&mut buf)
285                        .map_err(|err| Error::SerdeError(format_dbg!(err)))?;
286                    Self::from_toml(buf, skip_init)
287                        .map_err(|err| Error::SerdeError(format_dbg!(err)))?
288                }
289                #[cfg(feature = "yaml")]
290                "yaml" | "yml" => serde_yaml::from_reader(rdr)
291                    .map_err(|err| Error::SerdeError(format_dbg!(err)))?,
292                _ => {
293                    return Err(Error::SerdeError(format!(
294                        "Unsupported format {format:?}, must be one of {:?}",
295                        Self::ACCEPTED_BYTE_FORMATS
296                    )))
297                }
298            };
299        if !skip_init {
300            deserialized.init()?;
301        }
302        Ok(deserialized)
303    }
304
305    /// Write (serialize) an object into a string
306    ///
307    /// # Arguments:
308    ///
309    /// * `format` - The target format, any of those listed in [`ACCEPTED_STR_FORMATS`](`SerdeAPI::ACCEPTED_STR_FORMATS`)
310    ///
311    fn to_str(&self, format: &str) -> anyhow::Result<String> {
312        match format.trim_start_matches('.').to_lowercase().as_str() {
313            #[cfg(feature = "csv")]
314            "csv" => self.to_csv(),
315            #[cfg(feature = "json")]
316            "json" => self.to_json(),
317            #[cfg(feature = "toml")]
318            "toml" => self.to_toml(),
319            #[cfg(feature = "yaml")]
320            "yaml" | "yml" => self.to_yaml(),
321            _ => bail!(
322                "Unsupported format {format:?}, must be one of {:?}",
323                Self::ACCEPTED_STR_FORMATS
324            ),
325        }
326    }
327
328    /// Read (deserialize) an object from a string
329    ///
330    /// # Arguments:
331    ///
332    /// * `contents` - The string containing the object data
333    /// * `format` - The source format, any of those listed in [`ACCEPTED_STR_FORMATS`](`SerdeAPI::ACCEPTED_STR_FORMATS`)
334    ///
335    fn from_str<S: AsRef<str>>(contents: S, format: &str, skip_init: bool) -> anyhow::Result<Self> {
336        Ok(
337            match format.trim_start_matches('.').to_lowercase().as_str() {
338                #[cfg(feature = "csv")]
339                "csv" => Self::from_csv(contents, skip_init)?,
340                #[cfg(feature = "json")]
341                "json" => Self::from_json(contents, skip_init)?,
342                #[cfg(feature = "toml")]
343                "toml" => Self::from_toml(contents, skip_init)?,
344                #[cfg(feature = "yaml")]
345                "yaml" | "yml" => Self::from_yaml(contents, skip_init)?,
346                _ => bail!(
347                    "Unsupported format {format:?}, must be one of {:?}",
348                    Self::ACCEPTED_STR_FORMATS
349                ),
350            },
351        )
352    }
353}
354
355impl Cycle {
356    /// rust-internal time steps at i
357    pub fn dt_at_i(&self, i: usize) -> anyhow::Result<si::Time> {
358        Ok(*self.time.get(i).with_context(|| format_dbg!())?
359            - *self.time.get(i - 1).with_context(|| format_dbg!())?)
360    }
361
362    pub fn len_checked(&self) -> anyhow::Result<usize> {
363        ensure!(
364            self.time.len() == self.speed.len(),
365            format!(
366                "{}\n`time` and `speed` fields do not have same `len()`",
367                format_dbg!()
368            )
369        );
370        ensure!(
371            self.dist.is_empty() || self.time.len() == self.dist.len(),
372            format!(
373                "{}\n`time` and `dist` fields do not have same `len()`",
374                format_dbg!()
375            )
376        );
377        ensure!(
378            self.grade.is_empty() || self.time.len() == self.grade.len(),
379            format!(
380                "{}\n`time` and `grade` fields do not have same `len()`",
381                format_dbg!()
382            )
383        );
384        ensure!(
385            self.elev.is_empty() || self.grade.len() == self.elev.len(),
386            format!(
387                "{}\n`grade` and `elev` fields do not have same `len()`",
388                format_dbg!()
389            )
390        );
391        ensure!(
392            self.pwr_max_chrg.is_empty() || self.time.len() == self.pwr_max_chrg.len(),
393            format!(
394                "{}\n`time` and `pwr_max_chrg` fields do not have same `len()`",
395                format_dbg!()
396            )
397        );
398        ensure!(
399            self.temp_amb_air.is_empty() || self.time.len() == self.temp_amb_air.len(),
400            format!(
401                "{}\n`time` and `temp_amb_air` fields do not have same `len()`",
402                format_dbg!()
403            )
404        );
405        Ok(self.time.len())
406    }
407
408    pub fn is_empty(&self) -> anyhow::Result<bool> {
409        Ok(self.len_checked().with_context(|| format_dbg!())? == 0)
410    }
411
412    pub fn push(&mut self, element: CycleElement) -> anyhow::Result<()> {
413        // TODO: maybe automate generation of this function as derive macro
414        // TODO: maybe automate `ensure!` that all vec fields are same length before returning result
415        // TODO: make sure all fields are being updated as appropriate
416        self.time.push(element.time);
417        self.speed.push(element.speed);
418        match element.grade {
419            Some(grade) => self.grade.push(grade),
420            None => self.grade.push(si::Ratio::ZERO),
421        }
422        match element.pwr_max_charge {
423            Some(pwr_max_chrg) => self.pwr_max_chrg.push(pwr_max_chrg),
424            None => self.pwr_max_chrg.push(si::Power::ZERO),
425        }
426        match element.temp_amb_air {
427            Some(temp_amb_air) => self.temp_amb_air.push(temp_amb_air),
428            None => self.temp_amb_air.push(*TE_STD_AIR),
429        }
430        match element.pwr_solar_load {
431            Some(pwr_solar_load) => self.pwr_solar_load.push(pwr_solar_load),
432            None => self.pwr_solar_load.push(si::Power::ZERO),
433        }
434        Ok(())
435    }
436
437    pub fn extend(&mut self, vec: Vec<CycleElement>) -> anyhow::Result<()> {
438        self.time.extend(vec.iter().map(|x| x.time).clone());
439        todo!();
440        // self.time.extend(vec.iter().map(|x| x.time).clone());
441        // match (&mut self.grade, vec.grade) {
442        //     (Some(grade_mut), Some(grade)) => grade_mut.push(grade),
443        //     (None, Some(_)) => {
444        //         bail!("Element and Cycle `grade` fields must both be `Some` or `None`")
445        //     }
446        //     (Some(_), None) => {
447        //         bail!("Element and Cycle `grade` fields must both be `Some` or `None`")
448        //     }
449        //     _ => {}
450        // }
451        // match (&mut self.pwr_max_chrg, vec.pwr_max_charge) {
452        //     (Some(pwr_max_chrg_mut), Some(pwr_max_chrg)) => pwr_max_chrg_mut.push(pwr_max_chrg),
453        //     (None, Some(_)) => {
454        //         bail!("Element and Cycle `pwr_max_chrg` fields must both be `Some` or `None`")
455        //     }
456        //     (Some(_), None) => {
457        //         bail!("Element and Cycle `pwr_max_chrg` fields must both be `Some` or `None`")
458        //     }
459        //     _ => {}
460        // }
461        // self.speed.push(vec.speed);
462        // Ok(())
463    }
464
465    pub fn trim(&mut self, start_idx: Option<usize>, end_idx: Option<usize>) -> anyhow::Result<()> {
466        let start_idx = start_idx.unwrap_or_default();
467        let len = self.len_checked().with_context(|| format_dbg!())?;
468        let end_idx = end_idx.unwrap_or(len);
469        ensure!(end_idx <= len, format_dbg!(end_idx <= len));
470
471        self.time = self.time[start_idx..end_idx].to_vec();
472        self.speed = self.speed[start_idx..end_idx].to_vec();
473        Ok(())
474    }
475
476    /// Write (serialize) cycle to a CSV string
477    #[cfg(feature = "csv")]
478    pub fn to_csv(&self) -> anyhow::Result<String> {
479        let mut buf = Vec::with_capacity(self.len_checked().with_context(|| format_dbg!())?);
480        self.to_writer(&mut buf, "csv")?;
481        Ok(String::from_utf8(buf)?)
482    }
483
484    /// Read (deserialize) an object from a CSV string
485    ///
486    /// # Arguments
487    ///
488    /// * `json_str` - JSON-formatted string to deserialize from
489    ///
490    #[cfg(feature = "csv")]
491    fn from_csv<S: AsRef<str>>(csv_str: S, skip_init: bool) -> anyhow::Result<Self> {
492        let mut csv_de = Self::from_reader(&mut csv_str.as_ref().as_bytes(), "csv", skip_init)?;
493        if !skip_init {
494            csv_de.init()?;
495        }
496        Ok(csv_de)
497    }
498
499    pub fn to_fastsim2(&self) -> anyhow::Result<Cycle2> {
500        let cyc2 = Cycle2 {
501            name: self.name.clone(),
502            time_s: self.time.iter().map(|t| t.get::<si::second>()).collect(),
503            mps: self
504                .speed
505                .iter()
506                .map(|s| s.get::<si::meter_per_second>())
507                .collect(),
508            grade: self.grade.iter().map(|g| g.get::<si::ratio>()).collect(),
509            orphaned: false,
510            road_type: vec![0.; self.len_checked().with_context(|| format_dbg!())?].into(),
511        };
512
513        Ok(cyc2)
514    }
515}
516
517#[serde_api]
518#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)]
519#[non_exhaustive]
520#[serde(deny_unknown_fields)]
521#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
522/// Element of `Cycle`.  Used for vec-like operations.
523pub struct CycleElement {
524    /// simulation time \[s\]
525    #[serde(alias = "cycSecs")]
526    pub time: si::Time,
527    /// simulation power \[W\]
528    #[serde(alias = "speed_mps", alias = "cycMps")]
529    pub speed: si::Velocity,
530    // `dist` is not included here because it is derived in `Init::init`
531    // TODO: make `fastsim_api` handle Option or write custom getter/setter
532    /// road grade
533    #[serde(alias = "cycGrade")]
534    pub grade: Option<si::Ratio>,
535    // `elev` is not included here because it is derived in `Init::init`
536    /// road charging/discharing capacity
537    pub pwr_max_charge: Option<si::Power>,
538    // TODO: make sure all fields in cycle are represented here, as appropriate
539    /// ambient air temperature w.r.t. to time (rather than spatial position)
540    pub temp_amb_air: Option<si::Temperature>,
541    /// solar heat load w.r.t. to time (rather than spatial position)
542    pub pwr_solar_load: Option<si::Power>,
543}
544
545impl SerdeAPI for CycleElement {}
546impl Init for CycleElement {}
547
548#[named_struct_pyo3_api]
549impl CycleElement {}
550
551#[cfg(test)]
552mod tests {
553    use super::*;
554    fn mock_cyc_len_2() -> Cycle {
555        let mut cyc = Cycle {
556            name: String::new(),
557            init_elev: None,
558            time: (0..=2).map(|x| (x as f64) * uc::S).collect(),
559            speed: (0..=2).map(|x| (x as f64) * uc::MPS).collect(),
560            dist: vec![],
561            grade: (0..=2).map(|x| (x as f64 * uc::R) / 100.).collect(),
562            elev: vec![],
563            pwr_max_chrg: vec![],
564            grade_interp: Default::default(),
565            elev_interp: Default::default(),
566            temp_amb_air: Default::default(),
567            pwr_solar_load: Default::default(),
568        };
569        cyc.init().unwrap();
570        cyc
571    }
572
573    #[test]
574    fn test_init() {
575        let cyc = mock_cyc_len_2();
576        assert_eq!(
577            cyc.dist,
578            [0., 1., 3.] // meters
579                .iter()
580                .map(|x| *x * uc::M)
581                .collect::<Vec<si::Length>>()
582        );
583        assert_eq!(
584            cyc.elev,
585            [121.92, 121.93, 121.99000000000001] // meters
586                .iter()
587                .map(|x| *x * uc::M)
588                .collect::<Vec<si::Length>>()
589        );
590    }
591}