1use super::*;
4
5#[allow(unused_imports)]
6#[cfg(feature = "pyo3")]
7use crate::pyo3::*;
8
9#[serde_api]
10#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, StateMethods, SetCumulative)]
11#[non_exhaustive]
12#[serde(deny_unknown_fields)]
13#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
14pub struct ElectricMachine {
17 pub eff_interp_achieved: InterpolatorEnumOwned<f64>,
21 pub eff_interp_at_max_input: Option<InterpolatorEnumOwned<f64>>,
26 pub pwr_out_max: si::Power,
32 pub specific_pwr: Option<si::SpecificPower>,
34 pub(in super::super) mass: Option<si::Mass>,
36 pub save_interval: Option<usize>,
38 #[serde(default)]
40 pub state: ElectricMachineState,
41 #[serde(default)]
43 pub history: ElectricMachineStateHistoryVec,
44}
45
46#[pyo3_api]
47impl ElectricMachine {
48 #[getter("eff_fwd_max")]
70 fn get_eff_max_fwd_py(&self) -> PyResult<f64> {
71 Ok(*self.get_eff_fwd_max()?)
72 }
73
74 #[setter("__eff_fwd_max")]
75 fn set_eff_fwd_max_py(&mut self, eff_max: f64) -> PyResult<()> {
76 self.set_eff_fwd_max(eff_max)?;
77 Ok(())
78 }
79
80 #[getter("eff_min_fwd")]
81 fn get_eff_min_fwd_py(&self) -> PyResult<f64> {
82 Ok(*self.get_eff_min_fwd()?)
83 }
84
85 #[getter("eff_fwd_range")]
86 fn get_eff_fwd_range_py(&self) -> PyResult<f64> {
87 Ok(self.get_eff_fwd_range()?)
88 }
89
90 #[setter("__eff_fwd_range")]
91 fn set_eff_fwd_range_py(&mut self, eff_range: f64) -> PyResult<()> {
92 self.set_eff_fwd_range(eff_range)?;
93 Ok(())
94 }
95}
96
97impl Powertrain for ElectricMachine {
98 fn set_curr_pwr_prop_out_max(
111 &mut self,
112 pwr_upstream: (si::Power, si::Power),
113 _pwr_aux: si::Power,
114 _dt: si::Time,
115 _veh_state: &VehicleState,
116 ) -> anyhow::Result<()> {
117 let pwr_in_fwd_lim = &pwr_upstream.0;
118 let pwr_in_bwd_lim = &pwr_upstream.1;
119 ensure!(
120 pwr_in_fwd_lim >= &si::Power::ZERO,
121 "`{}` ({} W) must be greater than or equal to zero for `{}`",
122 stringify!(pwr_in_fwd_lim),
123 pwr_in_fwd_lim.get::<si::watt>().format_eng(None),
124 stringify!(ElectricMachine::get_curr_pwr_prop_out_max)
125 );
126 ensure!(
127 pwr_in_bwd_lim >= &si::Power::ZERO,
128 "`{}` ({} W) must be greater than or equal to zero for `{}`",
129 stringify!(pwr_in_bwd_lim),
130 pwr_in_bwd_lim.get::<si::watt>().format_eng(None),
131 stringify!(ElectricMachine::get_curr_pwr_prop_out_max)
132 );
133
134 self.eff_interp_at_max_input
137 .as_mut()
138 .with_context(|| {
139 "eff_interp_bwd is None, which should never be the case at this point."
140 })?
141 .set_extrapolate(Extrapolate::Clamp)?;
142
143 let raw_tractive_lookup_ratio = (*pwr_in_fwd_lim / self.pwr_out_max).get::<si::ratio>();
144 let raw_regen_lookup_ratio = (*pwr_in_bwd_lim / self.pwr_out_max).get::<si::ratio>();
145 self.state.eff_fwd_at_max_input.update(
146 uc::R
147 * self
148 .eff_interp_at_max_input
149 .as_ref()
150 .map(|interpolator| {
151 interpolator
152 .interpolate(&[abs_checked_x_val(
153 raw_tractive_lookup_ratio,
154 match interpolator {
155 InterpolatorEnum::Interp1D(interp) => interp.data.grid[0]
156 .as_slice()
157 .ok_or_else(|| anyhow!(format_dbg!()))?,
158 _ => bail!("Only `InterpolatorEnum::Interp1D` is allowed."),
159 },
160 )?])
161 .map_err(|e| anyhow!(e))
162 })
163 .ok_or(anyhow!(
164 "eff_interp_bwd is None, which should never be the case at this point."
165 ))?
166 .with_context(|| {
167 anyhow!(
168 "{}\n failed to calculate {}",
169 format_dbg!(),
170 stringify!(eff_pos)
171 )
172 })?,
173 || format_dbg!(),
174 )?;
175 self.state.eff_at_max_regen.update(
176 uc::R
177 * self
178 .eff_interp_at_max_input
179 .as_ref()
180 .map(|interpolator| {
181 interpolator
182 .interpolate(&[abs_checked_x_val(
183 raw_regen_lookup_ratio,
184 match interpolator {
185 InterpolatorEnum::Interp1D(interp) => interp.data.grid[0]
186 .as_slice()
187 .ok_or_else(|| anyhow!(format_dbg!()))?,
188 _ => bail!("Only `InterpolatorEnum::Interp1D` is allowed."),
189 },
190 )?])
191 .map_err(|e| anyhow!(e))
192 })
193 .ok_or(anyhow!(
194 "eff_interp_bwd is None, which should never be the case at this point."
195 ))?
196 .with_context(|| {
197 anyhow!(
198 "{}\n failed to calculate {}",
199 format_dbg!(),
200 stringify!(eff_neg)
201 )
202 })?,
203 || format_dbg!(),
204 )?;
205
206 self.state.pwr_mech_fwd_out_max.update(
209 self.pwr_out_max.min(
210 *pwr_in_fwd_lim
211 * *self
212 .state
213 .eff_fwd_at_max_input
214 .get_fresh(|| format_dbg!())?,
215 ),
216 || format_dbg!(),
217 )?;
218 self.state.pwr_mech_regen_max.update(
221 self.pwr_out_max
222 .min(*pwr_in_bwd_lim / *self.state.eff_at_max_regen.get_fresh(|| format_dbg!())?),
223 || format_dbg!(),
224 )?;
225 Ok(())
226 }
227
228 fn get_curr_pwr_prop_out_max(&self) -> anyhow::Result<(si::Power, si::Power)> {
229 Ok((
230 *self
231 .state
232 .pwr_mech_fwd_out_max
233 .get_fresh(|| format_dbg!())?,
234 *self.state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?,
235 ))
236 }
237
238 fn solve(
243 &mut self,
244 pwr_out_req: si::Power,
245 _enabled: bool,
246 _dt: si::Time,
247 ) -> anyhow::Result<Option<si::Power>> {
248 if pwr_out_req > si::Power::ZERO {
249 ensure!(
250 pwr_out_req <= self.pwr_out_max,
251 format!(
252 "{}\nedrv required power ({} kW) exceeds static max power ({} kW)",
253 format_dbg!(),
254 pwr_out_req.get::<si::kilowatt>().format_eng(Some(9)),
255 self.pwr_out_max.get::<si::kilowatt>().format_eng(Some(9))
256 ),
257 );
258 }
259 ensure!(
261 almost_le_uom(&pwr_out_req , self.state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?, None),
262 format!(
263 "{}\nedrv required propulsion power ({} kW) exceeds current max propulsion power ({} kW) by {} kW",
264 format_dbg!(pwr_out_req <= *self.state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?),
265 pwr_out_req.get::<si::kilowatt>().format_eng(Some(6)),
266 self.state
267 .pwr_mech_fwd_out_max
268 .get_fresh(|| format_dbg!())?
269 .get::<si::kilowatt>()
270 .format_eng(Some(6)),
271 (pwr_out_req - *self.state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?).get::<si::kilowatt>().format_eng(Some(6))
272 ),
273 );
274 if pwr_out_req < si::Power::ZERO {
275 ensure!(
276 almost_le_uom(
277 &pwr_out_req.abs(),
278 self.state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?,
279 None
280 ),
281 format!(
282 "{}\nedrv charge power ({:.6} kW) exceeds current max charge power ({:.6} kW)",
283 format_dbg!(),
284 -pwr_out_req.get::<si::kilowatt>(),
285 self.state
286 .pwr_mech_regen_max
287 .get_fresh(|| format_dbg!())?
288 .get::<si::kilowatt>()
289 ),
290 );
291 }
292
293 self.state
294 .pwr_out_req
295 .update(pwr_out_req, || format_dbg!())?;
296
297 self.state.pwr_mech_prop_out.update(
300 pwr_out_req.max(-*self.state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?),
301 || format_dbg!(),
302 )?;
303
304 let is_max_output = pwr_out_req
305 == *self
306 .state
307 .pwr_mech_fwd_out_max
308 .get_fresh(|| format_dbg!())?;
309
310 self.eff_interp_achieved
312 .set_extrapolate(Extrapolate::Error)?;
313
314 let raw_lookup_pwr_ratio = (pwr_out_req / self.pwr_out_max).get::<si::ratio>();
315 let calculated_eff = uc::R
316 * match &self.eff_interp_achieved {
317 InterpolatorEnum::Interp1D(interp) => interp
318 .interpolate(&[{
319 let pwr = |pwr_uncorrected: f64| -> anyhow::Result<f64> {
320 Ok({
321 if interp.data.grid[0]
322 .first()
323 .with_context(|| anyhow!(format_dbg!()))?
324 >= &0.
325 {
326 pwr_uncorrected.max(0.)
327 } else {
328 pwr_uncorrected
329 }
330 })
331 };
332 pwr(raw_lookup_pwr_ratio)?
333 }])
334 .with_context(|| {
335 anyhow!(
336 "{}\n failed to calculate {}",
337 format_dbg!(),
338 stringify!(self.state.eff)
339 )
340 })?,
341 _ => {
342 return Err(Error::InitError(format_dbg!(
343 "Only 1-D interpolators are supported"
344 ))
345 .into())
346 }
347 };
348 let eff_value = if is_max_output {
349 if pwr_out_req >= si::Power::ZERO {
350 *self
351 .state
352 .eff_fwd_at_max_input
353 .get_fresh(|| format_dbg!())?
354 } else {
355 *self.state.eff_at_max_regen.get_fresh(|| format_dbg!())?
356 }
357 } else {
358 calculated_eff
359 };
360 ensure!(eff_value >= si::Ratio::ZERO && eff_value <= 1.0 * uc::R);
361 self.state.eff.update(eff_value, || format_dbg!())?;
362
363 self.state.pwr_mech_dyn_brake.update(
364 -(pwr_out_req - *self.state.pwr_mech_prop_out.get_fresh(|| format_dbg!())?),
365 || format_dbg!(),
366 )?;
367 ensure!(
368 *self.state.pwr_mech_dyn_brake.get_fresh(|| format_dbg!())? >= si::Power::ZERO,
369 "Mech Dynamic Brake Power cannot be below 0.0"
370 );
371
372 self.state.pwr_elec_prop_in.update(
374 if pwr_out_req > si::Power::ZERO {
375 *self.state.pwr_mech_prop_out.get_fresh(|| format_dbg!())?
376 / *self.state.eff.get_fresh(|| format_dbg!())?
377 } else {
378 *self.state.pwr_mech_prop_out.get_fresh(|| format_dbg!())?
379 * *self.state.eff.get_fresh(|| format_dbg!())?
380 },
381 || format_dbg!(),
382 )?;
383
384 self.state.pwr_elec_dyn_brake.update(
385 *self.state.pwr_mech_dyn_brake.get_fresh(|| format_dbg!())?
386 * *self.state.eff.get_fresh(|| format_dbg!())?,
387 || format_dbg!(),
388 )?;
389
390 self.state.pwr_loss.update(
392 (*self.state.pwr_mech_prop_out.get_fresh(|| format_dbg!())?
393 - *self.state.pwr_elec_prop_in.get_fresh(|| format_dbg!())?)
394 .abs(),
395 || format_dbg!(),
396 )?;
397
398 Ok(Some(
399 *self.state.pwr_elec_prop_in.get_fresh(|| format_dbg!())?,
400 ))
401 }
402
403 fn pwr_regen(&self) -> anyhow::Result<si::Power> {
404 Ok(-self
405 .state
406 .pwr_mech_dyn_brake
407 .get_fresh(|| format_dbg!())?
408 .max(si::Power::ZERO))
409 }
410}
411
412impl SerdeAPI for ElectricMachine {}
413impl Init for ElectricMachine {
414 fn init(&mut self) -> Result<(), Error> {
415 let _ = self
416 .mass()
417 .map_err(|err| Error::InitError(format_dbg!(err)))?;
418 let _ = check_interp_frac_data(match &mut self.eff_interp_achieved {
419 InterpolatorEnum::Interp1D(interp) => interp.data.grid[0].as_slice().ok_or(Error::Other("Cannot convert to slice".to_string()))?, _ => {
420 return Err(Error::InitError(format_dbg!(
421 "Only 1-D interpolators are supported"
422 )))
423 }}, InterpRange::Either)
424 .map_err(|err|
425 Error::InitError(format!(
426 "{}\nInvalid values for `ElectricMachine::pwr_out_frac_interp`; must range from [-1..1] or [0..1].",
427 format_dbg!(err)
428 )
429 ))?;
430 self.state
431 .init()
432 .map_err(|err| Error::InitError(format_dbg!(err)))?;
433 let eff_interp_at_max_input = match &self.eff_interp_achieved {
436 InterpolatorEnum::Interp1D(interp) => {
437 InterpolatorEnum::new_1d(
438 interp.data.grid[0]
439 .iter()
440 .zip(&interp.data.values)
441 .map(|(x, y)| x / y)
442 .collect(),
443 interp.data.values.clone(),
444 interp.strategy.clone(),
448 interp.extrapolate,
449 )
450 }
451 _ => unimplemented!(),
452 }
453 .map_err(|e| Error::NinterpError(e.to_string()))?;
454 self.eff_interp_at_max_input = Some(eff_interp_at_max_input);
455 Ok(())
456 }
457}
458impl HistoryMethods for ElectricMachine {
459 fn save_interval(&self) -> anyhow::Result<Option<usize>> {
460 Ok(self.save_interval)
461 }
462 fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
463 self.save_interval = save_interval;
464 Ok(())
465 }
466 fn clear(&mut self) {
467 self.history.clear();
468 }
469}
470
471impl Mass for ElectricMachine {
472 fn mass(&self) -> anyhow::Result<Option<si::Mass>> {
473 let derived_mass = self
474 .derived_mass()
475 .with_context(|| anyhow!(format_dbg!()))?;
476 if let (Some(derived_mass), Some(set_mass)) = (derived_mass, self.mass) {
477 ensure!(
478 utils::almost_eq_uom(&set_mass, &derived_mass, None),
479 format!(
480 "{}",
481 format_dbg!(utils::almost_eq_uom(&set_mass, &derived_mass, None)),
482 )
483 );
484 }
485 Ok(self.mass)
486 }
487
488 fn set_mass(
489 &mut self,
490 new_mass: Option<si::Mass>,
491 side_effect: MassSideEffect,
492 ) -> anyhow::Result<()> {
493 let derived_mass = self
494 .derived_mass()
495 .with_context(|| anyhow!(format_dbg!()))?;
496 if let (Some(derived_mass), Some(new_mass)) = (derived_mass, new_mass) {
497 if derived_mass != new_mass {
498 match side_effect {
499 MassSideEffect::Extensive => {
500 self.pwr_out_max = self.specific_pwr.with_context(|| {
501 format!(
502 "{}\nExpected `self.specific_pwr` to be `Some`.",
503 format_dbg!()
504 )
505 })? * new_mass;
506 }
507 MassSideEffect::Intensive => {
508 self.specific_pwr = Some(self.pwr_out_max / new_mass);
509 }
510 MassSideEffect::None => {
511 self.specific_pwr = None;
512 }
513 }
514 }
515 } else if new_mass.is_none() {
516 self.specific_pwr = None;
517 }
518 self.mass = new_mass;
519 Ok(())
520 }
521
522 fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
523 Ok(self
524 .specific_pwr
525 .map(|specific_pwr| self.pwr_out_max / specific_pwr))
526 }
527
528 fn expunge_mass_fields(&mut self) {
529 self.specific_pwr = None;
530 self.mass = None;
531 }
532}
533
534impl TryFrom<EMBuilder> for ElectricMachine {
535 type Error = anyhow::Error;
536 fn try_from(em_builder: EMBuilder) -> anyhow::Result<ElectricMachine> {
537 let mut em = ElectricMachine {
538 eff_interp_achieved: em_builder.eff_interp_achieved.clone(),
539 eff_interp_at_max_input: None,
540 pwr_out_max: em_builder.pwr_out_max,
541 specific_pwr: None,
542 mass: None,
543 save_interval: Some(1),
544 state: Default::default(),
545 history: Default::default(),
546 };
547 em.init()?;
548
549 Ok(em)
550 }
551}
552
553impl ElectricMachine {
554 pub fn get_eff_fwd_max(&self) -> anyhow::Result<&f64> {
556 self.eff_interp_achieved.max()
558 }
559
560 pub fn get_eff_max_bwd(&self) -> anyhow::Result<&f64> {
562 self.eff_interp_at_max_input
563 .as_ref()
564 .with_context(|| "eff_interp_bwd should be Some by this point.")?
565 .max()
566 }
567
568 pub fn set_eff_fwd_max(&mut self, eff_max: f64) -> anyhow::Result<()> {
570 if (0.0..=1.0).contains(&eff_max) {
571 let old_max_fwd = *self.get_eff_fwd_max()?;
572 let old_max_bwd = *self.get_eff_max_bwd()?;
573 match &mut self.eff_interp_achieved {
574 InterpolatorEnum::Interp1D(interp) => {
575 interp.data.values = interp
576 .data
577 .values
578 .iter()
579 .map(|x| x * eff_max / old_max_fwd)
580 .collect::<Array1<_>>();
581 }
582 _ => bail!("{}\n", "Only `InterpolatorEnum::Interp1D` is allowed."),
583 }
584 match &mut self.eff_interp_at_max_input {
585 Some(InterpolatorEnum::Interp1D(interp)) => {
586 interp.data.values = interp
587 .data
588 .values
589 .iter()
590 .map(|x| x * eff_max / old_max_bwd)
591 .collect::<Array1<_>>();
592 }
593 _ => bail!("{}\n", "Only `InterpolatorEnum::Interp1D` is allowed. eff_interp_bwd should be Some by this point."),
594 }
595 Ok(())
596 } else {
597 Err(anyhow!(
598 "`eff_max` ({:.3}) must be between 0.0 and 1.0",
599 eff_max,
600 ))
601 }
602 }
603
604 pub fn get_eff_min_fwd(&self) -> anyhow::Result<&f64> {
606 self.eff_interp_achieved.min()
607 }
608
609 pub fn get_eff_min_at_max_input(&self) -> anyhow::Result<&f64> {
611 self.eff_interp_at_max_input
612 .as_ref()
613 .context("eff_interp_bwd should be Some by this point")?
614 .min()
615 }
616
617 pub fn get_eff_fwd_range(&self) -> anyhow::Result<f64> {
619 Ok(self.get_eff_fwd_max()? - self.get_eff_min_fwd()?)
620 }
621
622 pub fn get_eff_range_bwd(&self) -> anyhow::Result<f64> {
624 Ok(self.get_eff_max_bwd()? - self.get_eff_min_at_max_input()?)
625 }
626
627 pub fn set_eff_fwd_range(&mut self, eff_range: f64) -> anyhow::Result<()> {
631 let eff_max_fwd = self.get_eff_fwd_max()?.to_owned();
632 let eff_max_bwd = self.get_eff_max_bwd()?.to_owned();
633 if eff_range == 0.0 {
634 let f_x_fwd = vec![
635 eff_max_fwd;
636 match &self.eff_interp_achieved {
637 InterpolatorEnum::Interp1D(interp) => interp.data.values.len(),
638 _ => {
639 return Err(Error::InitError(format_dbg!(
640 "Only 1-D interpolators are supported"
641 ))
642 .into());
643 }
644 }
645 ];
646 match &mut self.eff_interp_achieved {
647 InterpolatorEnum::Interp1D(interp) => interp.data.values = Array::from_vec(f_x_fwd),
648 _ => {
649 return Err(Error::InitError(format_dbg!(
650 "Only 1-D interpolators are supported"
651 ))
652 .into());
653 }
654 };
655 let f_x_bwd = vec![
656 eff_max_bwd;
657 match &self.eff_interp_at_max_input {
658 Some(interp) => {
659 match interp {
660 InterpolatorEnum::Interp1D(interp) => interp.data.values.len(),
661 _ => {
662 return Err(Error::InitError(format_dbg!(
663 "Only 1-D interpolators are supported"
664 ))
665 .into());
666 }
667 }
668 }
669 None => bail!("eff_interp_bwd should be Some by this point."),
670 }
671 ];
672 self.eff_interp_at_max_input
673 .as_mut()
674 .map(|interpolator| match interpolator {
675 InterpolatorEnum::Interp1D(interp) => {
676 interp.data.values = Array::from_vec(f_x_bwd);
677 Ok(())
678 }
679 _ => Err(Error::InitError(format_dbg!(
680 "Only 1-D interpolators are supported"
681 ))),
682 })
683 .transpose()?;
684 Ok(())
685 } else if (0.0..=1.0).contains(&eff_range) {
686 let old_min = self.get_eff_min_fwd()?;
687 let old_range = self.get_eff_fwd_max()? - old_min;
688 if old_range == 0.0 {
689 return Err(anyhow!(
690 "`eff_range` is already zero so it cannot be modified."
691 ));
692 }
693 match &mut self.eff_interp_achieved {
694 InterpolatorEnum::Interp1D(interp) => {
695 interp.data.values = interp
696 .data
697 .values
698 .iter()
699 .map(|x| eff_max_fwd + (x - eff_max_fwd) * eff_range / old_range)
700 .collect();
701 interp.validate()?;
702 }
703 _ => bail!("{}\n", "Only `InterpolatorEnum::Interp1D` is allowed."),
704 }
705 if self.get_eff_min_fwd()? < &0. {
706 let x_neg = *self.get_eff_min_fwd()?;
707 match &mut self.eff_interp_achieved {
708 InterpolatorEnum::Interp1D(interp) => {
709 interp.data.values.map_inplace(|x| *x -= x_neg);
710 interp.validate()?;
711 }
712 _ => bail!("{}\n", "Only `InterpolatorEnum::Interp1D` is allowed."),
713 }
714 }
715 if self.get_eff_fwd_max()? > &1.0 {
716 return Err(anyhow!(format!(
717 "`eff_max` ({:.3}) must be no greater than 1.0",
718 self.get_eff_fwd_max()?
719 )));
720 }
721 let old_min = self.get_eff_min_at_max_input()?;
722 let old_range = self.get_eff_max_bwd()? - old_min;
723 if old_range == 0.0 {
724 return Err(anyhow!(
725 "`eff_range` is already zero so it cannot be modified."
726 ));
727 }
728
729 match &mut self.eff_interp_at_max_input {
731 Some(InterpolatorEnum::Interp1D(interp)) => {
732 interp.data.values = interp
733 .data
734 .values
735 .iter()
736 .map(|x| eff_max_bwd + (x - eff_max_bwd) * eff_range / old_range)
737 .collect();
738 }
739 _ => bail!("TODO"),
740 }
741
742 if self.get_eff_min_at_max_input()? < &0.0 {
743 let x_neg = *self.get_eff_min_at_max_input()?;
744 self.eff_interp_at_max_input
745 .as_mut()
746 .map(|interpolator| match interpolator {
747 InterpolatorEnum::Interp1D(interp) => {
748 interp.data.values.map_inplace(|x| *x -= x_neg);
749 interp.validate()?;
750 Ok(())
751 }
752 _ => bail!("Only `InterpolatorEnum::Interp1D` is allowed."),
753 })
754 .transpose()?;
755 }
756 if self.get_eff_max_bwd()? > &1.0 {
757 return Err(anyhow!(format!(
758 "`eff_max` ({:.3}) must be no greater than 1.0",
759 self.get_eff_max_bwd()?
760 )));
761 }
762 Ok(())
763 } else {
764 Err(anyhow!(format!(
765 "`eff_range` ({:.3}) must be between 0.0 and 1.0",
766 eff_range,
767 )))
768 }
769 }
770}
771
772impl TryFrom<fastsim_2::vehicle::RustVehicle> for ElectricMachine {
773 type Error = anyhow::Error;
774 fn try_from(f2veh: fastsim_2::vehicle::RustVehicle) -> Result<ElectricMachine, anyhow::Error> {
775 Ok(EMBuilder {
776 eff_interp_achieved: {
777 let short_perc_out_vec =
779 vec![0.0, 0.02, 0.04, 0.06, 0.08, 0.1, 0.2, 0.4, 0.6, 0.8, 1.0];
780 InterpolatorEnum::new_1d(
782 short_perc_out_vec.clone().into(),
783 {
784 let mc_full_eff = Array1::from_vec(f2veh.mc_full_eff_array.clone());
787 ensure!(mc_full_eff.len() == 101);
788 let shortener = Interp1D::new(
789 fastsim_2::params::MC_PERC_OUT_ARRAY.to_vec().into(),
790 mc_full_eff,
791 strategy::Linear,
792 Extrapolate::Error,
793 )
794 .with_context(|| format_dbg!())?;
795 let mut short_eff: Vec<f64> = short_perc_out_vec
796 .iter()
797 .map(|x| shortener.interpolate(&[*x]).unwrap())
798 .collect();
799 short_eff[0] = short_eff[1];
800 short_eff.into()
801 },
802 strategy::Linear,
803 Extrapolate::Error,
804 )
805 }
806 .with_context(|| {
807 format!(
808 "{}\n{}",
809 format_dbg!(f2veh.mc_full_eff_array.len()),
810 format_dbg!(f2veh.mc_perc_out_array.len())
811 )
812 })?,
813 pwr_out_max: f2veh.mc_max_kw * uc::KW,
814 }
815 .try_into()
816 .with_context(|| format_dbg!())?)
817 }
818}
819
820#[serde_api]
821#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
822#[serde(deny_unknown_fields)]
823#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
824pub struct EMBuilder {
826 pub eff_interp_achieved: InterpolatorEnumOwned<f64>,
830 pub pwr_out_max: si::Power,
836}
837
838#[allow(dead_code)]
839impl EMBuilder {
840 fn with_save_interval(&self, save_interval: Option<usize>) -> anyhow::Result<ElectricMachine> {
841 let mut em: ElectricMachine = self.clone().try_into()?;
842 em.save_interval = save_interval;
843 Ok(em)
844 }
845
846 fn with_state(&self, state: ElectricMachineState) -> anyhow::Result<ElectricMachine> {
847 let mut em: ElectricMachine = self.clone().try_into()?;
848 em.state = state;
849 Ok(em)
850 }
851}
852
853#[serde_api]
854#[derive(
855 Clone,
856 Debug,
857 Default,
858 Deserialize,
859 Serialize,
860 PartialEq,
861 HistoryVec,
862 StateMethods,
863 SetCumulative,
864)]
865#[non_exhaustive]
866#[serde(default)]
867#[serde(deny_unknown_fields)]
868#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
869
870pub struct ElectricMachineState {
871 pub i: TrackedState<usize>,
873 pub eff: TrackedState<si::Ratio>,
875 pub pwr_mech_fwd_out_max: TrackedState<si::Power>,
878 pub eff_fwd_at_max_input: TrackedState<si::Ratio>,
880 pub pwr_mech_regen_max: TrackedState<si::Power>,
882 pub eff_at_max_regen: TrackedState<si::Ratio>,
884
885 pub pwr_out_req: TrackedState<si::Power>,
888 pub energy_out_req: TrackedState<si::Energy>,
890 pub pwr_elec_prop_in: TrackedState<si::Power>,
893 pub energy_elec_prop_in: TrackedState<si::Energy>,
895 pub pwr_mech_prop_out: TrackedState<si::Power>,
898 pub energy_mech_prop_out: TrackedState<si::Energy>,
900 pub pwr_mech_dyn_brake: TrackedState<si::Power>,
902 pub energy_mech_dyn_brake: TrackedState<si::Energy>,
904 pub pwr_elec_dyn_brake: TrackedState<si::Power>,
906 pub energy_elec_dyn_brake: TrackedState<si::Energy>,
908 pub pwr_loss: TrackedState<si::Power>,
910 pub energy_loss: TrackedState<si::Energy>,
912}
913
914#[pyo3_api]
915impl ElectricMachineState {}
916
917impl Init for ElectricMachineState {}
918impl SerdeAPI for ElectricMachineState {}