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))]
17pub struct Cycle {
19 #[serde(default, skip_serializing_if = "String::is_empty")]
21 pub name: String,
22 pub init_elev: Option<si::Length>,
26 pub time: Vec<si::Time>,
28 #[serde(alias = "speed_mps")]
30 pub speed: Vec<si::Velocity>,
31 #[serde(default, skip_serializing_if = "Vec::is_empty")]
34 pub dist: Vec<si::Length>,
35 #[serde(default, skip_serializing_if = "Vec::is_empty")]
37 pub grade: Vec<si::Ratio>,
38 #[serde(default, skip_serializing_if = "Vec::is_empty")]
42 pub elev: Vec<si::Length>,
43 #[serde(default, skip_serializing_if = "Vec::is_empty")]
45 pub pwr_max_chrg: Vec<si::Power>,
46 #[serde(default, skip_serializing_if = "Vec::is_empty")]
48 pub temp_amb_air: Vec<si::Temperature>,
49 #[serde(default, skip_serializing_if = "Vec::is_empty")]
51 pub pwr_solar_load: Vec<si::Power>,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub grade_interp: Option<InterpolatorEnumOwned<f64>>,
56 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 self.init_elev.unwrap(),
220 |elev, (grade, dist)| {
221 *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 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 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 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 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 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 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 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 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 pub fn is_empty(&self) -> anyhow::Result<bool> {
515 Ok(self.len_checked().with_context(|| format_dbg!())? == 0)
516 }
517
518 pub fn push(&mut self, element: CycleElement) -> anyhow::Result<()> {
520 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 pub fn extend(&mut self, vec: Vec<CycleElement>) -> anyhow::Result<()> {
546 self.time.extend(vec.iter().map(|x| x.time).clone());
547 todo!();
548 }
572
573 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 #[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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn build_cache(&self) -> CycleCache {
1027 CycleCache::new(self)
1028 }
1029
1030 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 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 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 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 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 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 let tts_s = -v0 / brake_accel_m_per_s2;
1294 let n = (tts_s / dt).round() as usize;
1296 let n = if n < 2 { 2 } else { n }; 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 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 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 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))]
1527pub struct CycleElement {
1529 #[serde(alias = "cycSecs")]
1531 pub time: si::Time,
1532 #[serde(alias = "speed_mps", alias = "cycMps")]
1534 pub speed: si::Velocity,
1535 #[serde(alias = "cycGrade")]
1539 pub grade: Option<si::Ratio>,
1540 pub pwr_max_charge: Option<si::Power>,
1543 pub temp_amb_air: Option<si::Temperature>,
1546 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.] .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] .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 let actual = cyc.extend_time(absolute_time, time_fraction);
1774 assert_eq!(actual, expected);
1775 }
1776
1777 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; let v0 = 4.0; let dr = 65.0; let vr = 5.0; let dt = 10.0; 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 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 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}