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 self.state.eff_fwd_at_max_input.update(
144 uc::R
145 * self
146 .eff_interp_at_max_input
147 .as_ref()
148 .map(|interpolator| {
149 interpolator
150 .interpolate(&[abs_checked_x_val(
151 (*pwr_in_fwd_lim / self.pwr_out_max).get::<si::ratio>(),
152 match interpolator {
153 InterpolatorEnum::Interp1D(interp) => interp.data.grid[0]
154 .as_slice()
155 .ok_or_else(|| anyhow!(format_dbg!()))?,
156 _ => bail!("Only `InterpolatorEnum::Interp1D` is allowed."),
157 },
158 )?])
159 .map_err(|e| anyhow!(e))
160 })
161 .ok_or(anyhow!(
162 "eff_interp_bwd is None, which should never be the case at this point."
163 ))?
164 .with_context(|| {
165 anyhow!(
166 "{}\n failed to calculate {}",
167 format_dbg!(),
168 stringify!(eff_pos)
169 )
170 })?,
171 || format_dbg!(),
172 )?;
173 self.state.eff_at_max_regen.update(
174 uc::R
175 * self
176 .eff_interp_at_max_input
177 .as_ref()
178 .map(|interpolator| {
179 interpolator
180 .interpolate(&[abs_checked_x_val(
181 (*pwr_in_bwd_lim / self.pwr_out_max).get::<si::ratio>(),
182 match interpolator {
183 InterpolatorEnum::Interp1D(interp) => interp.data.grid[0]
184 .as_slice()
185 .ok_or_else(|| anyhow!(format_dbg!()))?,
186 _ => bail!("Only `InterpolatorEnum::Interp1D` is allowed."),
187 },
188 )?])
189 .map_err(|e| anyhow!(e))
190 })
191 .ok_or(anyhow!(
192 "eff_interp_bwd is None, which should never be the case at this point."
193 ))?
194 .with_context(|| {
195 anyhow!(
196 "{}\n failed to calculate {}",
197 format_dbg!(),
198 stringify!(eff_neg)
199 )
200 })?,
201 || format_dbg!(),
202 )?;
203
204 self.state.pwr_mech_fwd_out_max.update(
207 self.pwr_out_max.min(
208 *pwr_in_fwd_lim
209 * *self
210 .state
211 .eff_fwd_at_max_input
212 .get_fresh(|| format_dbg!())?,
213 ),
214 || format_dbg!(),
215 )?;
216 self.state.pwr_mech_regen_max.update(
219 self.pwr_out_max
220 .min(*pwr_in_bwd_lim / *self.state.eff_at_max_regen.get_fresh(|| format_dbg!())?),
221 || format_dbg!(),
222 )?;
223 Ok(())
224 }
225
226 fn get_curr_pwr_prop_out_max(&self) -> anyhow::Result<(si::Power, si::Power)> {
227 Ok((
228 *self
229 .state
230 .pwr_mech_fwd_out_max
231 .get_fresh(|| format_dbg!())?,
232 *self.state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?,
233 ))
234 }
235
236 fn solve(
241 &mut self,
242 pwr_out_req: si::Power,
243 _enabled: bool,
244 _dt: si::Time,
245 ) -> anyhow::Result<Option<si::Power>> {
246 if pwr_out_req > si::Power::ZERO {
247 ensure!(
248 pwr_out_req <= self.pwr_out_max,
249 format!(
250 "{}\nedrv required power ({} kW) exceeds static max power ({} kW)",
251 format_dbg!(),
252 pwr_out_req.get::<si::kilowatt>().format_eng(Some(9)),
253 self.pwr_out_max.get::<si::kilowatt>().format_eng(Some(9))
254 ),
255 );
256 }
257 ensure!(
259 almost_le_uom(&pwr_out_req , self.state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?, None),
260 format!(
261 "{}\nedrv required propulsion power ({} kW) exceeds current max propulsion power ({} kW) by {} kW",
262 format_dbg!(pwr_out_req <= *self.state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?),
263 pwr_out_req.get::<si::kilowatt>().format_eng(Some(6)),
264 self.state
265 .pwr_mech_fwd_out_max
266 .get_fresh(|| format_dbg!())?
267 .get::<si::kilowatt>()
268 .format_eng(Some(6)),
269 (pwr_out_req - *self.state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?).get::<si::kilowatt>().format_eng(Some(6))
270 ),
271 );
272 if pwr_out_req < si::Power::ZERO {
273 ensure!(
274 almost_le_uom(
275 &pwr_out_req.abs(),
276 self.state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?,
277 None
278 ),
279 format!(
280 "{}\nedrv charge power ({:.6} kW) exceeds current max charge power ({:.6} kW)",
281 format_dbg!(),
282 -pwr_out_req.get::<si::kilowatt>(),
283 self.state
284 .pwr_mech_regen_max
285 .get_fresh(|| format_dbg!())?
286 .get::<si::kilowatt>()
287 ),
288 );
289 }
290
291 self.state
292 .pwr_out_req
293 .update(pwr_out_req, || format_dbg!())?;
294
295 self.eff_interp_achieved
297 .set_extrapolate(Extrapolate::Error)?;
298
299 self.state.eff.update(
300 uc::R
301 * match &self.eff_interp_achieved {
302 InterpolatorEnum::Interp1D(interp) => interp
303 .interpolate(&[{
304 let pwr = |pwr_uncorrected: f64| -> anyhow::Result<f64> {
305 Ok({
306 if interp.data.grid[0]
307 .first()
308 .with_context(|| anyhow!(format_dbg!()))?
309 >= &0.
310 {
311 pwr_uncorrected.max(0.)
312 } else {
313 pwr_uncorrected
314 }
315 })
316 };
317 pwr((pwr_out_req / self.pwr_out_max).get::<si::ratio>())?
318 }])
319 .with_context(|| {
320 anyhow!(
321 "{}\n failed to calculate {}",
322 format_dbg!(),
323 stringify!(self.state.eff)
324 )
325 })?,
326 _ => {
327 return Err(Error::InitError(format_dbg!(
328 "Only 1-D interpolators are supported"
329 ))
330 .into())
331 }
332 },
333 || format_dbg!(),
334 )?;
335 self.state.pwr_mech_prop_out.update(
338 pwr_out_req.max(-*self.state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?),
339 || format_dbg!(),
340 )?;
341
342 self.state.pwr_mech_dyn_brake.update(
343 -(pwr_out_req - *self.state.pwr_mech_prop_out.get_fresh(|| format_dbg!())?),
344 || format_dbg!(),
345 )?;
346 ensure!(
347 *self.state.pwr_mech_dyn_brake.get_fresh(|| format_dbg!())? >= si::Power::ZERO,
348 "Mech Dynamic Brake Power cannot be below 0.0"
349 );
350
351 self.state.pwr_elec_prop_in.update(
353 if pwr_out_req > si::Power::ZERO {
354 *self.state.pwr_mech_prop_out.get_fresh(|| format_dbg!())?
355 / *self.state.eff.get_fresh(|| format_dbg!())?
356 } else {
357 *self.state.pwr_mech_prop_out.get_fresh(|| format_dbg!())?
358 * *self.state.eff.get_fresh(|| format_dbg!())?
359 },
360 || format_dbg!(),
361 )?;
362
363 self.state.pwr_elec_dyn_brake.update(
364 *self.state.pwr_mech_dyn_brake.get_fresh(|| format_dbg!())?
365 * *self.state.eff.get_fresh(|| format_dbg!())?,
366 || format_dbg!(),
367 )?;
368
369 self.state.pwr_loss.update(
371 (*self.state.pwr_mech_prop_out.get_fresh(|| format_dbg!())?
372 - *self.state.pwr_elec_prop_in.get_fresh(|| format_dbg!())?)
373 .abs(),
374 || format_dbg!(),
375 )?;
376
377 Ok(Some(
378 *self.state.pwr_elec_prop_in.get_fresh(|| format_dbg!())?,
379 ))
380 }
381
382 fn pwr_regen(&self) -> anyhow::Result<si::Power> {
383 Ok(-self
384 .state
385 .pwr_mech_dyn_brake
386 .get_fresh(|| format_dbg!())?
387 .max(si::Power::ZERO))
388 }
389}
390
391impl SerdeAPI for ElectricMachine {}
392impl Init for ElectricMachine {
393 fn init(&mut self) -> Result<(), Error> {
394 let _ = self
395 .mass()
396 .map_err(|err| Error::InitError(format_dbg!(err)))?;
397 let _ = check_interp_frac_data(match &mut self.eff_interp_achieved {
398 InterpolatorEnum::Interp1D(interp) => interp.data.grid[0].as_slice().ok_or(Error::Other("Cannot convert to slice".to_string()))?, _ => {
399 return Err(Error::InitError(format_dbg!(
400 "Only 1-D interpolators are supported"
401 )))
402 }}, InterpRange::Either)
403 .map_err(|err|
404 Error::InitError(format!(
405 "{}\nInvalid values for `ElectricMachine::pwr_out_frac_interp`; must range from [-1..1] or [0..1].",
406 format_dbg!(err)
407 )
408 ))?;
409 self.state
410 .init()
411 .map_err(|err| Error::InitError(format_dbg!(err)))?;
412 let eff_interp_at_max_input = match &self.eff_interp_achieved {
415 InterpolatorEnum::Interp1D(interp) => {
416 InterpolatorEnum::new_1d(
417 interp.data.grid[0]
418 .iter()
419 .zip(&interp.data.values)
420 .map(|(x, y)| x / y)
421 .collect(),
422 interp.data.values.clone(),
423 interp.strategy.clone(),
427 interp.extrapolate,
428 )
429 }
430 _ => unimplemented!(),
431 }
432 .map_err(|e| Error::NinterpError(e.to_string()))?;
433 self.eff_interp_at_max_input = Some(eff_interp_at_max_input);
434 Ok(())
435 }
436}
437impl HistoryMethods for ElectricMachine {
438 fn save_interval(&self) -> anyhow::Result<Option<usize>> {
439 Ok(self.save_interval)
440 }
441 fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
442 self.save_interval = save_interval;
443 Ok(())
444 }
445 fn clear(&mut self) {
446 self.history.clear();
447 }
448}
449
450impl Mass for ElectricMachine {
451 fn mass(&self) -> anyhow::Result<Option<si::Mass>> {
452 let derived_mass = self
453 .derived_mass()
454 .with_context(|| anyhow!(format_dbg!()))?;
455 if let (Some(derived_mass), Some(set_mass)) = (derived_mass, self.mass) {
456 ensure!(
457 utils::almost_eq_uom(&set_mass, &derived_mass, None),
458 format!(
459 "{}",
460 format_dbg!(utils::almost_eq_uom(&set_mass, &derived_mass, None)),
461 )
462 );
463 }
464 Ok(self.mass)
465 }
466
467 fn set_mass(
468 &mut self,
469 new_mass: Option<si::Mass>,
470 side_effect: MassSideEffect,
471 ) -> anyhow::Result<()> {
472 let derived_mass = self
473 .derived_mass()
474 .with_context(|| anyhow!(format_dbg!()))?;
475 if let (Some(derived_mass), Some(new_mass)) = (derived_mass, new_mass) {
476 if derived_mass != new_mass {
477 match side_effect {
478 MassSideEffect::Extensive => {
479 self.pwr_out_max = self.specific_pwr.with_context(|| {
480 format!(
481 "{}\nExpected `self.specific_pwr` to be `Some`.",
482 format_dbg!()
483 )
484 })? * new_mass;
485 }
486 MassSideEffect::Intensive => {
487 self.specific_pwr = Some(self.pwr_out_max / new_mass);
488 }
489 MassSideEffect::None => {
490 self.specific_pwr = None;
491 }
492 }
493 }
494 } else if new_mass.is_none() {
495 self.specific_pwr = None;
496 }
497 self.mass = new_mass;
498 Ok(())
499 }
500
501 fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
502 Ok(self
503 .specific_pwr
504 .map(|specific_pwr| self.pwr_out_max / specific_pwr))
505 }
506
507 fn expunge_mass_fields(&mut self) {
508 self.specific_pwr = None;
509 self.mass = None;
510 }
511}
512
513impl TryFrom<EMBuilder> for ElectricMachine {
514 type Error = anyhow::Error;
515 fn try_from(em_builder: EMBuilder) -> anyhow::Result<ElectricMachine> {
516 let mut em = ElectricMachine {
517 eff_interp_achieved: em_builder.eff_interp_achieved.clone(),
518 eff_interp_at_max_input: None,
519 pwr_out_max: em_builder.pwr_out_max,
520 specific_pwr: None,
521 mass: None,
522 save_interval: Some(1),
523 state: Default::default(),
524 history: Default::default(),
525 };
526 em.init()?;
527
528 Ok(em)
529 }
530}
531
532impl ElectricMachine {
533 pub fn get_eff_fwd_max(&self) -> anyhow::Result<&f64> {
535 self.eff_interp_achieved.max()
537 }
538
539 pub fn get_eff_max_bwd(&self) -> anyhow::Result<&f64> {
541 self.eff_interp_at_max_input
542 .as_ref()
543 .with_context(|| "eff_interp_bwd should be Some by this point.")?
544 .max()
545 }
546
547 pub fn set_eff_fwd_max(&mut self, eff_max: f64) -> anyhow::Result<()> {
549 if (0.0..=1.0).contains(&eff_max) {
550 let old_max_fwd = *self.get_eff_fwd_max()?;
551 let old_max_bwd = *self.get_eff_max_bwd()?;
552 match &mut self.eff_interp_achieved {
553 InterpolatorEnum::Interp1D(interp) => {
554 interp.data.values = interp
555 .data
556 .values
557 .iter()
558 .map(|x| x * eff_max / old_max_fwd)
559 .collect::<Array1<_>>();
560 }
561 _ => bail!("{}\n", "Only `InterpolatorEnum::Interp1D` is allowed."),
562 }
563 match &mut self.eff_interp_at_max_input {
564 Some(InterpolatorEnum::Interp1D(interp)) => {
565 interp.data.values = interp
566 .data
567 .values
568 .iter()
569 .map(|x| x * eff_max / old_max_bwd)
570 .collect::<Array1<_>>();
571 }
572 _ => bail!("{}\n", "Only `InterpolatorEnum::Interp1D` is allowed. eff_interp_bwd should be Some by this point."),
573 }
574 Ok(())
575 } else {
576 Err(anyhow!(
577 "`eff_max` ({:.3}) must be between 0.0 and 1.0",
578 eff_max,
579 ))
580 }
581 }
582
583 pub fn get_eff_min_fwd(&self) -> anyhow::Result<&f64> {
585 self.eff_interp_achieved.min()
586 }
587
588 pub fn get_eff_min_at_max_input(&self) -> anyhow::Result<&f64> {
590 self.eff_interp_at_max_input
591 .as_ref()
592 .context("eff_interp_bwd should be Some by this point")?
593 .min()
594 }
595
596 pub fn get_eff_fwd_range(&self) -> anyhow::Result<f64> {
598 Ok(self.get_eff_fwd_max()? - self.get_eff_min_fwd()?)
599 }
600
601 pub fn get_eff_range_bwd(&self) -> anyhow::Result<f64> {
603 Ok(self.get_eff_max_bwd()? - self.get_eff_min_at_max_input()?)
604 }
605
606 pub fn set_eff_fwd_range(&mut self, eff_range: f64) -> anyhow::Result<()> {
610 let eff_max_fwd = self.get_eff_fwd_max()?.to_owned();
611 let eff_max_bwd = self.get_eff_max_bwd()?.to_owned();
612 if eff_range == 0.0 {
613 let f_x_fwd = vec![
614 eff_max_fwd;
615 match &self.eff_interp_achieved {
616 InterpolatorEnum::Interp1D(interp) => interp.data.values.len(),
617 _ => {
618 return Err(Error::InitError(format_dbg!(
619 "Only 1-D interpolators are supported"
620 ))
621 .into());
622 }
623 }
624 ];
625 match &mut self.eff_interp_achieved {
626 InterpolatorEnum::Interp1D(interp) => interp.data.values = Array::from_vec(f_x_fwd),
627 _ => {
628 return Err(Error::InitError(format_dbg!(
629 "Only 1-D interpolators are supported"
630 ))
631 .into());
632 }
633 };
634 let f_x_bwd = vec![
635 eff_max_bwd;
636 match &self.eff_interp_at_max_input {
637 Some(interp) => {
638 match interp {
639 InterpolatorEnum::Interp1D(interp) => interp.data.values.len(),
640 _ => {
641 return Err(Error::InitError(format_dbg!(
642 "Only 1-D interpolators are supported"
643 ))
644 .into());
645 }
646 }
647 }
648 None => bail!("eff_interp_bwd should be Some by this point."),
649 }
650 ];
651 self.eff_interp_at_max_input
652 .as_mut()
653 .map(|interpolator| match interpolator {
654 InterpolatorEnum::Interp1D(interp) => {
655 interp.data.values = Array::from_vec(f_x_bwd);
656 Ok(())
657 }
658 _ => Err(Error::InitError(format_dbg!(
659 "Only 1-D interpolators are supported"
660 ))),
661 })
662 .transpose()?;
663 Ok(())
664 } else if (0.0..=1.0).contains(&eff_range) {
665 let old_min = self.get_eff_min_fwd()?;
666 let old_range = self.get_eff_fwd_max()? - old_min;
667 if old_range == 0.0 {
668 return Err(anyhow!(
669 "`eff_range` is already zero so it cannot be modified."
670 ));
671 }
672 match &mut self.eff_interp_achieved {
673 InterpolatorEnum::Interp1D(interp) => {
674 interp.data.values = interp
675 .data
676 .values
677 .iter()
678 .map(|x| eff_max_fwd + (x - eff_max_fwd) * eff_range / old_range)
679 .collect();
680 interp.validate()?;
681 }
682 _ => bail!("{}\n", "Only `InterpolatorEnum::Interp1D` is allowed."),
683 }
684 if self.get_eff_min_fwd()? < &0. {
685 let x_neg = *self.get_eff_min_fwd()?;
686 match &mut self.eff_interp_achieved {
687 InterpolatorEnum::Interp1D(interp) => {
688 interp.data.values.map_inplace(|x| *x -= x_neg);
689 interp.validate()?;
690 }
691 _ => bail!("{}\n", "Only `InterpolatorEnum::Interp1D` is allowed."),
692 }
693 }
694 if self.get_eff_fwd_max()? > &1.0 {
695 return Err(anyhow!(format!(
696 "`eff_max` ({:.3}) must be no greater than 1.0",
697 self.get_eff_fwd_max()?
698 )));
699 }
700 let old_min = self.get_eff_min_at_max_input()?;
701 let old_range = self.get_eff_max_bwd()? - old_min;
702 if old_range == 0.0 {
703 return Err(anyhow!(
704 "`eff_range` is already zero so it cannot be modified."
705 ));
706 }
707
708 match &mut self.eff_interp_at_max_input {
710 Some(InterpolatorEnum::Interp1D(interp)) => {
711 interp.data.values = interp
712 .data
713 .values
714 .iter()
715 .map(|x| eff_max_bwd + (x - eff_max_bwd) * eff_range / old_range)
716 .collect();
717 }
718 _ => bail!("TODO"),
719 }
720
721 if self.get_eff_min_at_max_input()? < &0.0 {
722 let x_neg = *self.get_eff_min_at_max_input()?;
723 self.eff_interp_at_max_input
724 .as_mut()
725 .map(|interpolator| match interpolator {
726 InterpolatorEnum::Interp1D(interp) => {
727 interp.data.values.map_inplace(|x| *x -= x_neg);
728 interp.validate()?;
729 Ok(())
730 }
731 _ => bail!("Only `InterpolatorEnum::Interp1D` is allowed."),
732 })
733 .transpose()?;
734 }
735 if self.get_eff_max_bwd()? > &1.0 {
736 return Err(anyhow!(format!(
737 "`eff_max` ({:.3}) must be no greater than 1.0",
738 self.get_eff_max_bwd()?
739 )));
740 }
741 Ok(())
742 } else {
743 Err(anyhow!(format!(
744 "`eff_range` ({:.3}) must be between 0.0 and 1.0",
745 eff_range,
746 )))
747 }
748 }
749}
750
751impl TryFrom<fastsim_2::vehicle::RustVehicle> for ElectricMachine {
752 type Error = anyhow::Error;
753 fn try_from(f2veh: fastsim_2::vehicle::RustVehicle) -> Result<ElectricMachine, anyhow::Error> {
754 Ok(EMBuilder {
755 eff_interp_achieved: {
756 let short_perc_out_vec =
758 vec![0.0, 0.02, 0.04, 0.06, 0.08, 0.1, 0.2, 0.4, 0.6, 0.8, 1.0];
759 InterpolatorEnum::new_1d(
761 short_perc_out_vec.clone().into(),
762 {
763 let mc_full_eff = Array1::from_vec(f2veh.mc_full_eff_array.clone());
766 ensure!(mc_full_eff.len() == 101);
767 let shortener = Interp1D::new(
768 fastsim_2::params::MC_PERC_OUT_ARRAY.to_vec().into(),
769 mc_full_eff,
770 strategy::Linear,
771 Extrapolate::Error,
772 )
773 .with_context(|| format_dbg!())?;
774 let mut short_eff: Vec<f64> = short_perc_out_vec
775 .iter()
776 .map(|x| shortener.interpolate(&[*x]).unwrap())
777 .collect();
778 short_eff[0] = short_eff[1];
779 short_eff.into()
780 },
781 strategy::Linear,
782 Extrapolate::Error,
783 )
784 }
785 .with_context(|| {
786 format!(
787 "{}\n{}",
788 format_dbg!(f2veh.mc_full_eff_array.len()),
789 format_dbg!(f2veh.mc_perc_out_array.len())
790 )
791 })?,
792 pwr_out_max: f2veh.mc_max_kw * uc::KW,
793 }
794 .try_into()
795 .with_context(|| format_dbg!())?)
796 }
797}
798
799#[serde_api]
800#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
801#[serde(deny_unknown_fields)]
802#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
803pub struct EMBuilder {
805 pub eff_interp_achieved: InterpolatorEnumOwned<f64>,
809 pub pwr_out_max: si::Power,
815}
816
817#[allow(dead_code)]
818impl EMBuilder {
819 fn with_save_interval(&self, save_interval: Option<usize>) -> anyhow::Result<ElectricMachine> {
820 let mut em: ElectricMachine = self.clone().try_into()?;
821 em.save_interval = save_interval;
822 Ok(em)
823 }
824
825 fn with_state(&self, state: ElectricMachineState) -> anyhow::Result<ElectricMachine> {
826 let mut em: ElectricMachine = self.clone().try_into()?;
827 em.state = state;
828 Ok(em)
829 }
830}
831
832#[serde_api]
833#[derive(
834 Clone,
835 Debug,
836 Default,
837 Deserialize,
838 Serialize,
839 PartialEq,
840 HistoryVec,
841 StateMethods,
842 SetCumulative,
843)]
844#[non_exhaustive]
845#[serde(default)]
846#[serde(deny_unknown_fields)]
847#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
848
849pub struct ElectricMachineState {
850 pub i: TrackedState<usize>,
852 pub eff: TrackedState<si::Ratio>,
854 pub pwr_mech_fwd_out_max: TrackedState<si::Power>,
857 pub eff_fwd_at_max_input: TrackedState<si::Ratio>,
859 pub pwr_mech_regen_max: TrackedState<si::Power>,
861 pub eff_at_max_regen: TrackedState<si::Ratio>,
863
864 pub pwr_out_req: TrackedState<si::Power>,
867 pub energy_out_req: TrackedState<si::Energy>,
869 pub pwr_elec_prop_in: TrackedState<si::Power>,
872 pub energy_elec_prop_in: TrackedState<si::Energy>,
874 pub pwr_mech_prop_out: TrackedState<si::Power>,
877 pub energy_mech_prop_out: TrackedState<si::Energy>,
879 pub pwr_mech_dyn_brake: TrackedState<si::Power>,
881 pub energy_mech_dyn_brake: TrackedState<si::Energy>,
883 pub pwr_elec_dyn_brake: TrackedState<si::Power>,
885 pub energy_elec_dyn_brake: TrackedState<si::Energy>,
887 pub pwr_loss: TrackedState<si::Power>,
889 pub energy_loss: TrackedState<si::Energy>,
891}
892
893#[pyo3_api]
894impl ElectricMachineState {}
895
896impl Init for ElectricMachineState {}
897impl SerdeAPI for ElectricMachineState {}