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