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))]
12pub struct Cycle {
14 #[serde(default, skip_serializing_if = "String::is_empty")]
16 pub name: String,
17 pub init_elev: Option<si::Length>,
21 pub time: Vec<si::Time>,
23 #[serde(alias = "speed_mps")]
25 pub speed: Vec<si::Velocity>,
26 #[serde(default, skip_serializing_if = "Vec::is_empty")]
29 pub dist: Vec<si::Length>,
30 #[serde(default, skip_serializing_if = "Vec::is_empty")]
32 pub grade: Vec<si::Ratio>,
33 #[serde(default, skip_serializing_if = "Vec::is_empty")]
37 pub elev: Vec<si::Length>,
38 #[serde(default, skip_serializing_if = "Vec::is_empty")]
40 pub pwr_max_chrg: Vec<si::Power>,
41 #[serde(default, skip_serializing_if = "Vec::is_empty")]
43 pub temp_amb_air: Vec<si::Temperature>,
44 #[serde(default, skip_serializing_if = "Vec::is_empty")]
46 pub pwr_solar_load: Vec<si::Power>,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub grade_interp: Option<Interpolator>,
51 #[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 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 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 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 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 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 self.init_elev.unwrap(),
122 |elev, (grade, dist)| {
123 *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 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 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 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 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 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 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 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 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 }
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 #[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 #[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))]
522pub struct CycleElement {
524 #[serde(alias = "cycSecs")]
526 pub time: si::Time,
527 #[serde(alias = "speed_mps", alias = "cycMps")]
529 pub speed: si::Velocity,
530 #[serde(alias = "cycGrade")]
534 pub grade: Option<si::Ratio>,
535 pub pwr_max_charge: Option<si::Power>,
538 pub temp_amb_air: Option<si::Temperature>,
541 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.] .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] .iter()
587 .map(|x| *x * uc::M)
588 .collect::<Vec<si::Length>>()
589 );
590 }
591}