fastsim_core/vehicle/powertrain/
electric_machine.rs

1//! Module for electric machine (i.e. bidirectional electromechanical device), generator, or motor
2
3use 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))]
14/// Struct for modeling electric machines.  This lumps performance and efficiency of motor and power
15/// electronics.
16pub struct ElectricMachine {
17    /// Efficiency interpolator corresponding to achieved output power
18    ///
19    /// Note that the Extrapolate field of this variable is changed in [Self::get_pwr_in_req]
20    pub eff_interp_achieved: Interpolator,
21    /// Efficiency interpolator corresponding to max input power
22    /// If `None`, will be set during [Self::init].
23    ///
24    /// Note that the Extrapolate field of this variable is changed in [Self::set_curr_pwr_prop_out_max]
25    pub eff_interp_at_max_input: Option<Interpolator>,
26    /// Electrical input power fraction array at which efficiencies are evaluated.
27    /// Calculated during runtime if not provided.
28    // /// this will disappear and instead be in eff_interp_bwd
29    // pub pwr_in_frac_interp: Vec<f64>,
30    /// ElectricMachine maximum output power \[W\]
31    pub pwr_out_max: si::Power,
32    /// ElectricMachine specific power
33    pub specific_pwr: Option<si::SpecificPower>,
34    /// ElectricMachine mass
35    pub(in super::super) mass: Option<si::Mass>,
36    /// Time step interval between saves. 1 is a good option. If None, no saving occurs.
37    pub save_interval: Option<usize>,
38    /// struct for tracking current state
39    #[serde(default)]
40    pub state: ElectricMachineState,
41    /// Custom vector of [Self::state]
42    #[serde(
43        default,
44        skip_serializing_if = "ElectricMachineStateHistoryVec::is_empty"
45    )]
46    pub history: ElectricMachineStateHistoryVec,
47}
48
49#[named_struct_pyo3_api]
50impl ElectricMachine {
51    // #[new]
52    // fn __new__(
53    //     pwr_out_frac_interp: Vec<f64>,
54    //     eff_interp: Vec<f64>,
55    //     pwr_out_max_watts: f64,
56    //     save_interval: Option<usize>,
57    // ) -> anyhow::Result<Self> {
58    //     Self::new(
59    //         pwr_out_frac_interp,
60    //         eff_interp,
61    //         pwr_out_max_watts,
62    //         save_interval,
63    //     )
64    // }
65
66    // #[setter]
67    // pub fn set_eff_interp(&mut self, new_value: Vec<f64>) -> anyhow::Result<()> {
68    //     self.eff_interp = new_value;
69    //     self.set_pwr_in_frac_interp()
70    // }
71
72    #[getter("eff_fwd_max")]
73    fn get_eff_max_fwd_py(&self) -> PyResult<f64> {
74        Ok(self.get_eff_fwd_max()?)
75    }
76
77    #[setter("__eff_fwd_max")]
78    fn set_eff_fwd_max_py(&mut self, eff_max: f64) -> PyResult<()> {
79        self.set_eff_fwd_max(eff_max)?;
80        Ok(())
81    }
82
83    #[getter("eff_min_fwd")]
84    fn get_eff_min_fwd_py(&self) -> PyResult<f64> {
85        Ok(self.get_eff_min_fwd()?)
86    }
87
88    #[getter("eff_fwd_range")]
89    fn get_eff_fwd_range_py(&self) -> PyResult<f64> {
90        Ok(self.get_eff_fwd_range()?)
91    }
92
93    #[setter("__eff_fwd_range")]
94    fn set_eff_fwd_range_py(&mut self, eff_range: f64) -> PyResult<()> {
95        self.set_eff_fwd_range(eff_range)?;
96        Ok(())
97    }
98}
99
100impl ElectricMachine {
101    /// Returns maximum possible positive and negative propulsion-related powers
102    /// this component/system can produce, accounting for any aux-related power
103    /// required.
104    /// # Arguments
105    /// - `pwr_in_fwd_lim`: positive-propulsion-related power available to this
106    ///    component. Positive values indicate that the upstream component can supply
107    ///    positive tractive power.
108    /// - `pwr_in_bwd_lim`: negative-propulsion-related power available to this
109    ///     component. Zero means no power can be sent to upstream compnents and positive
110    ///     values indicate upstream components can absorb energy.
111    /// - `pwr_aux`: aux-related power required from this component
112    /// - `dt`: simulation time step size
113    pub fn set_curr_pwr_prop_out_max(
114        &mut self,
115        pwr_in_fwd_lim: si::Power,
116        pwr_in_bwd_lim: si::Power,
117        _dt: si::Time,
118    ) -> anyhow::Result<()> {
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        // ensuring Extrapolate is Clamp in preparation for calculating eff_pos
135
136        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                                interpolator.x().map_err(|e| anyhow!(e))?,
153                            )?])
154                            .map_err(|e| anyhow!(e))
155                    })
156                    .ok_or(anyhow!(
157                        "eff_interp_bwd is None, which should never be the case at this point."
158                    ))?
159                    .with_context(|| {
160                        anyhow!(
161                            "{}\n failed to calculate {}",
162                            format_dbg!(),
163                            stringify!(eff_pos)
164                        )
165                    })?,
166            || format_dbg!(),
167        )?;
168        self.state.eff_at_max_regen.update(
169            uc::R
170                * self
171                    .eff_interp_at_max_input
172                    .as_ref()
173                    .map(|interpolator| {
174                        interpolator
175                            .interpolate(&[abs_checked_x_val(
176                                (pwr_in_bwd_lim / self.pwr_out_max).get::<si::ratio>(),
177                                interpolator.x().map_err(|e| anyhow!(e))?,
178                            )?])
179                            .map_err(|e| anyhow!(e))
180                    })
181                    .ok_or(anyhow!(
182                        "eff_interp_bwd is None, which should never be the case at this point."
183                    ))?
184                    .with_context(|| {
185                        anyhow!(
186                            "{}\n failed to calculate {}",
187                            format_dbg!(),
188                            stringify!(eff_neg)
189                        )
190                    })?,
191            || format_dbg!(),
192        )?;
193
194        // maximum power in forward direction is minimum of component `pwr_out_max` parameter or time-varying max
195        // power based on what the ReversibleEnergyStorage can provide
196        self.state.pwr_mech_fwd_out_max.update(
197            self.pwr_out_max.min(
198                pwr_in_fwd_lim
199                    * *self
200                        .state
201                        .eff_fwd_at_max_input
202                        .get_fresh(|| format_dbg!())?,
203            ),
204            || format_dbg!(),
205        )?;
206        // maximum power in backward direction is minimum of component `pwr_out_max` parameter or time-varying max
207        // power in bacward direction (i.e. regen) based on what the ReversibleEnergyStorage can provide
208        self.state.pwr_mech_regen_max.update(
209            self.pwr_out_max
210                .min(pwr_in_bwd_lim / *self.state.eff_at_max_regen.get_fresh(|| format_dbg!())?),
211            || format_dbg!(),
212        )?;
213        Ok(())
214    }
215
216    /// Solves for this powertrain system/component efficiency and sets/returns power input required.
217    /// # Arguments
218    /// - `pwr_out_req`: propulsion-related power output required
219    /// - `dt`: simulation time step size
220    pub fn get_pwr_in_req(
221        &mut self,
222        pwr_out_req: si::Power,
223        _dt: si::Time,
224    ) -> anyhow::Result<si::Power> {
225        //TODO: update this function to use `pwr_mech_regen_out_max`
226        ensure!(
227            pwr_out_req.abs() <= self.pwr_out_max,
228            format!(
229                "{}\nedrv required power ({} kW) exceeds static max power ({} kW)",
230                format_dbg!(pwr_out_req.abs() <= self.pwr_out_max),
231                pwr_out_req.get::<si::kilowatt>().format_eng(Some(9)),
232                self.pwr_out_max.get::<si::kilowatt>().format_eng(Some(9))
233            ),
234        );
235        ensure!(
236            almost_le_uom(&pwr_out_req , self.state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?, None),
237            format!(
238                "{}\nedrv required propulsion power ({} kW) exceeds current max propulsion power ({} kW) by {} kW",
239                format_dbg!(pwr_out_req <= *self.state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?),
240                pwr_out_req.get::<si::kilowatt>().format_eng(Some(6)),
241                self.state
242                    .pwr_mech_fwd_out_max
243                    .get_fresh(|| format_dbg!())?
244                    .get::<si::kilowatt>()
245                    .format_eng(Some(6)),
246                    (pwr_out_req - *self.state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?).get::<si::kilowatt>().format_eng(Some(6))
247            ),
248        );
249        if pwr_out_req < si::Power::ZERO {
250            ensure!(
251                almost_le_uom(
252                    &pwr_out_req.abs(),
253                    self.state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?,
254                    None
255                ),
256                format!(
257                    "{}\nedrv charge power ({:.6} kW) exceeds current max charge power ({:.6} kW)",
258                    format_dbg!(),
259                    -pwr_out_req.get::<si::kilowatt>(),
260                    self.state
261                        .pwr_mech_regen_max
262                        .get_fresh(|| format_dbg!())?
263                        .get::<si::kilowatt>()
264                ),
265            );
266        }
267
268        self.state
269            .pwr_out_req
270            .update(pwr_out_req, || format_dbg!())?;
271
272        // ensuring eff_interp_fwd has Extrapolate set to Error before calculating self.state.eff
273        self.eff_interp_achieved
274            .set_extrapolate(Extrapolate::Error)?;
275
276        self.state.eff.update(
277            uc::R
278                * self
279                    .eff_interp_achieved
280                    .interpolate(
281                        &[{
282                            let pwr = |pwr_uncorrected: f64| -> anyhow::Result<f64> {
283                                Ok({
284                                    if self
285                                        .eff_interp_achieved
286                                        .x()?
287                                        .first()
288                                        .with_context(|| anyhow!(format_dbg!()))?
289                                        >= &0.
290                                    {
291                                        pwr_uncorrected.max(0.)
292                                    } else {
293                                        pwr_uncorrected
294                                    }
295                                })
296                            };
297                            pwr((pwr_out_req / self.pwr_out_max).get::<si::ratio>())?
298                        }], // &self.eff_interp_fwd.x()?,
299                            // &self.eff_interp_fwd,
300                            // Extrapolate::Error,
301                    )
302                    .with_context(|| {
303                        anyhow!(
304                            "{}\n failed to calculate {}",
305                            format_dbg!(),
306                            stringify!(self.state.eff)
307                        )
308                    })?,
309            || format_dbg!(),
310        )?;
311
312        // `pwr_mech_prop_out` is `pwr_out_req` unless `pwr_out_req` is more negative than `pwr_mech_regen_max`,
313        // in which case, excess is handled by `pwr_mech_dyn_brake`
314        self.state.pwr_mech_prop_out.update(
315            pwr_out_req.max(-*self.state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?),
316            || format_dbg!(),
317        )?;
318
319        self.state.pwr_mech_dyn_brake.update(
320            -(pwr_out_req - *self.state.pwr_mech_prop_out.get_fresh(|| format_dbg!())?),
321            || format_dbg!(),
322        )?;
323        ensure!(
324            *self.state.pwr_mech_dyn_brake.get_fresh(|| format_dbg!())? >= si::Power::ZERO,
325            "Mech Dynamic Brake Power cannot be below 0.0"
326        );
327
328        // if pwr_out_req is negative, need to multiply by eff
329        self.state.pwr_elec_prop_in.update(
330            if pwr_out_req > si::Power::ZERO {
331                *self.state.pwr_mech_prop_out.get_fresh(|| format_dbg!())?
332                    / *self.state.eff.get_fresh(|| format_dbg!())?
333            } else {
334                *self.state.pwr_mech_prop_out.get_fresh(|| format_dbg!())?
335                    * *self.state.eff.get_fresh(|| format_dbg!())?
336            },
337            || format_dbg!(),
338        )?;
339
340        self.state.pwr_elec_dyn_brake.update(
341            *self.state.pwr_mech_dyn_brake.get_fresh(|| format_dbg!())?
342                * *self.state.eff.get_fresh(|| format_dbg!())?,
343            || format_dbg!(),
344        )?;
345
346        // loss does not account for dynamic braking
347        self.state.pwr_loss.update(
348            (*self.state.pwr_mech_prop_out.get_fresh(|| format_dbg!())?
349                - *self.state.pwr_elec_prop_in.get_fresh(|| format_dbg!())?)
350            .abs(),
351            || format_dbg!(),
352        )?;
353
354        Ok(*self.state.pwr_elec_prop_in.get_fresh(|| format_dbg!())?)
355    }
356}
357
358impl SerdeAPI for ElectricMachine {}
359impl Init for ElectricMachine {
360    fn init(&mut self) -> Result<(), Error> {
361        let _ = self
362            .mass()
363            .map_err(|err| Error::InitError(format_dbg!(err)))?;
364        let _ = check_interp_frac_data(self.eff_interp_achieved.x()?, InterpRange::Either)
365            .map_err(|err|
366                Error::InitError(format!(
367                    "{}\nInvalid values for `ElectricMachine::pwr_out_frac_interp`; must range from [-1..1] or [0..1].",
368                    format_dbg!(err)
369                )
370             ))?;
371        self.state
372            .init()
373            .map_err(|err| Error::InitError(format_dbg!(err)))?;
374        // sets eff_interp_bwd to eff_interp_fwd, but changes the x-value.
375        // TODO: what should the default strategy be for eff_interp_bwd?
376        let eff_interp_at_max_input = Interpolator::new_1d(
377            self.eff_interp_achieved
378                .x()?
379                .iter()
380                .zip(self.eff_interp_achieved.f_x()?)
381                .map(|(x, y)| x / y)
382                .collect(),
383            self.eff_interp_achieved.f_x()?.to_owned(),
384            // TODO: should these be set to be the same as eff_interp_fwd,
385            // as currently is done, or should they be set to be specific
386            // Extrapolate and Strategy types?
387            self.eff_interp_achieved.strategy()?.to_owned(),
388            self.eff_interp_achieved.extrapolate()?.to_owned(),
389        )
390        .map_err(ninterp::error::Error::from)?;
391        self.eff_interp_at_max_input = Some(eff_interp_at_max_input);
392        Ok(())
393    }
394}
395impl HistoryMethods for ElectricMachine {
396    fn save_interval(&self) -> anyhow::Result<Option<usize>> {
397        Ok(self.save_interval)
398    }
399    fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
400        self.save_interval = save_interval;
401        Ok(())
402    }
403    fn clear(&mut self) {
404        self.history.clear();
405    }
406}
407
408impl Mass for ElectricMachine {
409    fn mass(&self) -> anyhow::Result<Option<si::Mass>> {
410        let derived_mass = self
411            .derived_mass()
412            .with_context(|| anyhow!(format_dbg!()))?;
413        if let (Some(derived_mass), Some(set_mass)) = (derived_mass, self.mass) {
414            ensure!(
415                utils::almost_eq_uom(&set_mass, &derived_mass, None),
416                format!(
417                    "{}",
418                    format_dbg!(utils::almost_eq_uom(&set_mass, &derived_mass, None)),
419                )
420            );
421        }
422        Ok(self.mass)
423    }
424
425    fn set_mass(
426        &mut self,
427        new_mass: Option<si::Mass>,
428        side_effect: MassSideEffect,
429    ) -> anyhow::Result<()> {
430        let derived_mass = self
431            .derived_mass()
432            .with_context(|| anyhow!(format_dbg!()))?;
433        if let (Some(derived_mass), Some(new_mass)) = (derived_mass, new_mass) {
434            if derived_mass != new_mass {
435                match side_effect {
436                    MassSideEffect::Extensive => {
437                        self.pwr_out_max = self.specific_pwr.with_context(|| {
438                            format!(
439                                "{}\nExpected `self.specific_pwr` to be `Some`.",
440                                format_dbg!()
441                            )
442                        })? * new_mass;
443                    }
444                    MassSideEffect::Intensive => {
445                        self.specific_pwr = Some(self.pwr_out_max / new_mass);
446                    }
447                    MassSideEffect::None => {
448                        self.specific_pwr = None;
449                    }
450                }
451            }
452        } else if new_mass.is_none() {
453            self.specific_pwr = None;
454        }
455        self.mass = new_mass;
456        Ok(())
457    }
458
459    fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
460        Ok(self
461            .specific_pwr
462            .map(|specific_pwr| self.pwr_out_max / specific_pwr))
463    }
464
465    fn expunge_mass_fields(&mut self) {
466        self.specific_pwr = None;
467        self.mass = None;
468    }
469}
470
471impl ElectricMachine {
472    /// Returns max value of `eff_interp_fwd`
473    pub fn get_eff_fwd_max(&self) -> anyhow::Result<f64> {
474        // since efficiency is all f64 between 0 and 1, NEG_INFINITY is safe
475        Ok(self
476            .eff_interp_achieved
477            .f_x()?
478            .iter()
479            .fold(f64::NEG_INFINITY, |acc, curr| acc.max(*curr)))
480    }
481
482    /// Returns max value of `eff_interp_bwd`
483    pub fn get_eff_max_bwd(&self) -> anyhow::Result<f64> {
484        // since efficiency is all f64 between 0 and 1, NEG_INFINITY is safe
485        Ok(match self.eff_interp_at_max_input.as_ref() {
486            Some(interp) => interp
487                .f_x()?
488                .iter()
489                .fold(f64::NEG_INFINITY, |acc, curr| acc.max(*curr)),
490            None => bail!("eff_interp_bwd should be Some by this point."),
491        })
492    }
493
494    /// Scales eff_interp_fwd and eff_interp_bwd by ratio of new `eff_max` per current calculated max
495    pub fn set_eff_fwd_max(&mut self, eff_max: f64) -> anyhow::Result<()> {
496        if (0.0..=1.0).contains(&eff_max) {
497            let old_max_fwd = self.get_eff_fwd_max()?;
498            let old_max_bwd = self.get_eff_max_bwd()?;
499            let f_x_fwd = self.eff_interp_achieved.f_x()?.to_owned();
500            match &mut self.eff_interp_achieved {
501                interp @ Interpolator::Interp1D(..) => {
502                    interp.set_f_x(f_x_fwd.iter().map(|x| x * eff_max / old_max_fwd).collect())?;
503                }
504                _ => bail!("{}\n", "Only `Interpolator::Interp1D` is allowed."),
505            }
506            let f_x_bwd = self
507                .eff_interp_at_max_input
508                .as_ref()
509                .ok_or(anyhow!(
510                    "eff_interp_bwd is None, which should never be the case at this point."
511                ))?
512                .f_x()?
513                .to_owned();
514            match &mut self.eff_interp_at_max_input {
515                Some(interp @ Interpolator::Interp1D(..)) => {
516                    interp.set_f_x(
517                        f_x_bwd
518                            .iter()
519                            .map(|x| x * eff_max / old_max_bwd)
520                            .collect(),
521                    )?;
522                }
523                _ => bail!("{}\n", "Only `Interpolator::Interp1D` is allowed. eff_interp_bwd should be Some by this point."),
524            }
525            Ok(())
526        } else {
527            Err(anyhow!(
528                "`eff_max` ({:.3}) must be between 0.0 and 1.0",
529                eff_max,
530            ))
531        }
532    }
533
534    /// Returns min value of `eff_interp_fwd`
535    pub fn get_eff_min_fwd(&self) -> anyhow::Result<f64> {
536        // since efficiency is all f64 between 0 and 1, NEG_INFINITY is safe
537        Ok(self
538            .eff_interp_achieved
539            .f_x()
540            .with_context(|| "eff_interp_fwd does not have f_x field")?
541            .iter()
542            .fold(f64::INFINITY, |acc, curr| acc.min(*curr)))
543    }
544
545    /// Returns min value of `eff_interp_bwd`
546    pub fn get_eff_min_at_max_input(&self) -> anyhow::Result<f64> {
547        // since efficiency is all f64 between 0 and 1, NEG_INFINITY is safe
548        Ok(self
549            .eff_interp_at_max_input
550            .as_ref()
551            .ok_or(anyhow!("eff_interp_bwd should be Some by this point."))?
552            .f_x()
553            .with_context(|| "eff_interp_bwd does not have f_x field")?
554            .iter()
555            .fold(f64::INFINITY, |acc, curr| acc.min(*curr)))
556    }
557
558    /// Max value of `eff_interp_fwd` minus min value of `eff_interp_fwd`.
559    pub fn get_eff_fwd_range(&self) -> anyhow::Result<f64> {
560        Ok(self.get_eff_fwd_max()? - self.get_eff_min_fwd()?)
561    }
562
563    /// Max value of `eff_interp_bwd` minus min value of `eff_interp_bwd`.
564    pub fn get_eff_range_bwd(&self) -> anyhow::Result<f64> {
565        Ok(self.get_eff_max_bwd()? - self.get_eff_min_at_max_input()?)
566    }
567
568    /// Scales values of `eff_interp_fwd.f_x` and `eff_interp_bwd.f_x` without changing max such that max - min
569    /// is equal to new range.  Will change max if needed to ensure no values are
570    /// less than zero.
571    pub fn set_eff_fwd_range(&mut self, eff_range: f64) -> anyhow::Result<()> {
572        let eff_max_fwd = self.get_eff_fwd_max()?;
573        let eff_max_bwd = self.get_eff_max_bwd()?;
574        if eff_range == 0.0 {
575            let f_x_fwd = vec![
576                eff_max_fwd;
577                self.eff_interp_achieved
578                    .f_x()
579                    .with_context(|| "eff_interp_fwd does not have f_x field")?
580                    .len()
581            ];
582            self.eff_interp_achieved.set_f_x(f_x_fwd)?;
583            let f_x_bwd = vec![
584                eff_max_bwd;
585                match &self.eff_interp_at_max_input {
586                    Some(interp) => {
587                        interp
588                            .f_x()
589                            .with_context(|| "eff_interp_bwd does not have f_x field")?
590                            .len()
591                    }
592                    None => bail!("eff_interp_bwd should be Some by this point."),
593                }
594            ];
595            self.eff_interp_at_max_input
596                .as_mut()
597                .map(|interpolator| interpolator.set_f_x(f_x_bwd))
598                .transpose()?;
599            Ok(())
600        } else if (0.0..=1.0).contains(&eff_range) {
601            let old_min = self.get_eff_min_fwd()?;
602            let old_range = self.get_eff_fwd_max()? - old_min;
603            if old_range == 0.0 {
604                return Err(anyhow!(
605                    "`eff_range` is already zero so it cannot be modified."
606                ));
607            }
608            let f_x_fwd = self.eff_interp_achieved.f_x()?.to_owned();
609            match &mut self.eff_interp_achieved {
610                interp @ Interpolator::Interp1D(..) => {
611                    interp.set_f_x(
612                        f_x_fwd
613                            .iter()
614                            .map(|x| eff_max_fwd + (x - eff_max_fwd) * eff_range / old_range)
615                            .collect(),
616                    )?;
617                }
618                _ => bail!("{}\n", "Only `Interpolator::Interp1D` is allowed."),
619            }
620            if self.get_eff_min_fwd()? < 0.0 {
621                let x_neg = self.get_eff_min_fwd()?;
622                let f_x_fwd = self.eff_interp_achieved.f_x()?.to_owned();
623                match &mut self.eff_interp_achieved {
624                    interp @ Interpolator::Interp1D(..) => {
625                        interp.set_f_x(f_x_fwd.iter().map(|x| x - x_neg).collect())?;
626                    }
627                    _ => bail!("{}\n", "Only `Interpolator::Interp1D` is allowed."),
628                }
629            }
630            if self.get_eff_fwd_max()? > 1.0 {
631                return Err(anyhow!(format!(
632                    "`eff_max` ({:.3}) must be no greater than 1.0",
633                    self.get_eff_fwd_max()?
634                )));
635            }
636            let old_min = self.get_eff_min_at_max_input()?;
637            let old_range = self.get_eff_max_bwd()? - old_min;
638            if old_range == 0.0 {
639                return Err(anyhow!(
640                    "`eff_range` is already zero so it cannot be modified."
641                ));
642            }
643
644            let new_f_x: Vec<f64> = self
645                .eff_interp_at_max_input
646                .as_ref()
647                .ok_or(anyhow!("eff_interp_bwd should be Some by this point."))?
648                .f_x()?
649                .iter()
650                .map(|x| eff_max_bwd + (x - eff_max_bwd) * eff_range / old_range)
651                .collect();
652
653            self.eff_interp_at_max_input
654                .as_mut()
655                .map(|interpolator| interpolator.set_f_x(new_f_x))
656                .transpose()?;
657
658            if self.get_eff_min_at_max_input()? < 0.0 {
659                let x_neg = self.get_eff_min_at_max_input()?;
660                let new_f_x: Vec<f64> = self
661                    .eff_interp_at_max_input
662                    .as_ref()
663                    .ok_or(anyhow!("eff_interp_bwd should be Some by this point."))?
664                    .f_x()?
665                    .iter()
666                    .map(|x| x - x_neg)
667                    .collect();
668                self.eff_interp_at_max_input
669                    .as_mut()
670                    .map(|interpolator| interpolator.set_f_x(new_f_x))
671                    .transpose()?;
672            }
673            if self.get_eff_max_bwd()? > 1.0 {
674                return Err(anyhow!(format!(
675                    "`eff_max` ({:.3}) must be no greater than 1.0",
676                    self.get_eff_max_bwd()?
677                )));
678            }
679            Ok(())
680        } else {
681            Err(anyhow!(format!(
682                "`eff_range` ({:.3}) must be between 0.0 and 1.0",
683                eff_range,
684            )))
685        }
686    }
687}
688
689#[serde_api]
690#[derive(
691    Clone,
692    Debug,
693    Default,
694    Deserialize,
695    Serialize,
696    PartialEq,
697    HistoryVec,
698    StateMethods,
699    SetCumulative,
700)]
701#[non_exhaustive]
702#[serde(default)]
703#[serde(deny_unknown_fields)]
704#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
705
706pub struct ElectricMachineState {
707    /// time step index
708    pub i: TrackedState<usize>,
709    /// Component efficiency based on current power demand.
710    pub eff: TrackedState<si::Ratio>,
711    // Component limits
712    /// Maximum possible positive traction power.
713    pub pwr_mech_fwd_out_max: TrackedState<si::Power>,
714    /// efficiency in forward direction at max possible input power from `FuelConverter` and `ReversibleEnergyStorage`
715    pub eff_fwd_at_max_input: TrackedState<si::Ratio>,
716    /// Maximum possible regeneration power going to ReversibleEnergyStorage.
717    pub pwr_mech_regen_max: TrackedState<si::Power>,
718    /// efficiency in backward direction at max possible input power from `FuelConverter` and `ReversibleEnergyStorage`
719    pub eff_at_max_regen: TrackedState<si::Ratio>,
720
721    // Current values
722    /// Raw power requirement from boundary conditions
723    pub pwr_out_req: TrackedState<si::Power>,
724    /// Integral of [Self::pwr_out_req]
725    pub energy_out_req: TrackedState<si::Energy>,
726    /// Electrical power to propulsion from ReversibleEnergyStorage and Generator.
727    /// negative value indicates regenerative braking
728    pub pwr_elec_prop_in: TrackedState<si::Power>,
729    /// Integral of [Self::pwr_elec_prop_in]
730    pub energy_elec_prop_in: TrackedState<si::Energy>,
731    /// Mechanical power to propulsion, corrected by efficiency, from ReversibleEnergyStorage and Generator.
732    /// Negative value indicates regenerative braking.
733    pub pwr_mech_prop_out: TrackedState<si::Power>,
734    /// Integral of [Self::pwr_mech_prop_out]
735    pub energy_mech_prop_out: TrackedState<si::Energy>,
736    /// Mechanical power from dynamic braking.  Positive value indicates braking; this should be zero otherwise.
737    pub pwr_mech_dyn_brake: TrackedState<si::Power>,
738    /// Integral of [Self::pwr_mech_dyn_brake]
739    pub energy_mech_dyn_brake: TrackedState<si::Energy>,
740    /// Electrical power from dynamic braking, dissipated as heat.
741    pub pwr_elec_dyn_brake: TrackedState<si::Power>,
742    /// Integral of [Self::pwr_elec_dyn_brake]
743    pub energy_elec_dyn_brake: TrackedState<si::Energy>,
744    /// Power lost in regeneratively converting mechanical power to power that can be absorbed by the battery.
745    pub pwr_loss: TrackedState<si::Power>,
746    /// Integral of [Self::pwr_loss]
747    pub energy_loss: TrackedState<si::Energy>,
748}
749
750#[named_struct_pyo3_api]
751impl ElectricMachineState {}
752
753impl Init for ElectricMachineState {}
754impl SerdeAPI for ElectricMachineState {}