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: InterpolatorEnumOwned<f64>,
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<InterpolatorEnumOwned<f64>>,
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#[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                                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        // maximum power in forward direction is minimum of component `pwr_out_max` parameter or time-varying max
205        // power based on what the ReversibleEnergyStorage can provide
206        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        // maximum power in backward direction is minimum of component `pwr_out_max` parameter or time-varying max
217        // power in bacward direction (i.e. regen) based on what the ReversibleEnergyStorage can provide
218        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    /// Solves for this powertrain system/component efficiency and sets/returns power input required.
227    /// # Arguments
228    /// - `pwr_out_req`: propulsion-related power output required
229    /// - `dt`: simulation time step size
230    pub fn get_pwr_in_req(
231        &mut self,
232        pwr_out_req: si::Power,
233        _dt: si::Time,
234    ) -> anyhow::Result<si::Power> {
235        //TODO: update this function to use `pwr_mech_regen_out_max`
236        ensure!(
237            pwr_out_req.abs() <= self.pwr_out_max,
238            format!(
239                "{}\nedrv required power ({} kW) exceeds static max power ({} kW)",
240                format_dbg!(pwr_out_req.abs() <= self.pwr_out_max),
241                pwr_out_req.get::<si::kilowatt>().format_eng(Some(9)),
242                self.pwr_out_max.get::<si::kilowatt>().format_eng(Some(9))
243            ),
244        );
245        ensure!(
246            almost_le_uom(&pwr_out_req , self.state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?, None),
247            format!(
248                "{}\nedrv required propulsion power ({} kW) exceeds current max propulsion power ({} kW) by {} kW",
249                format_dbg!(pwr_out_req <= *self.state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?),
250                pwr_out_req.get::<si::kilowatt>().format_eng(Some(6)),
251                self.state
252                    .pwr_mech_fwd_out_max
253                    .get_fresh(|| format_dbg!())?
254                    .get::<si::kilowatt>()
255                    .format_eng(Some(6)),
256                    (pwr_out_req - *self.state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?).get::<si::kilowatt>().format_eng(Some(6))
257            ),
258        );
259        if pwr_out_req < si::Power::ZERO {
260            ensure!(
261                almost_le_uom(
262                    &pwr_out_req.abs(),
263                    self.state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?,
264                    None
265                ),
266                format!(
267                    "{}\nedrv charge power ({:.6} kW) exceeds current max charge power ({:.6} kW)",
268                    format_dbg!(),
269                    -pwr_out_req.get::<si::kilowatt>(),
270                    self.state
271                        .pwr_mech_regen_max
272                        .get_fresh(|| format_dbg!())?
273                        .get::<si::kilowatt>()
274                ),
275            );
276        }
277
278        self.state
279            .pwr_out_req
280            .update(pwr_out_req, || format_dbg!())?;
281
282        // ensuring eff_interp_fwd has Extrapolate set to Error before calculating self.state.eff
283        self.eff_interp_achieved
284            .set_extrapolate(Extrapolate::Error)?;
285
286        self.state.eff.update(
287            uc::R
288                * match &self.eff_interp_achieved {
289                    InterpolatorEnum::Interp1D(interp) => interp
290                        .interpolate(&[{
291                            let pwr = |pwr_uncorrected: f64| -> anyhow::Result<f64> {
292                                Ok({
293                                    if interp.data.grid[0]
294                                        .first()
295                                        .with_context(|| anyhow!(format_dbg!()))?
296                                        >= &0.
297                                    {
298                                        pwr_uncorrected.max(0.)
299                                    } else {
300                                        pwr_uncorrected
301                                    }
302                                })
303                            };
304                            pwr((pwr_out_req / self.pwr_out_max).get::<si::ratio>())?
305                        }])
306                        .with_context(|| {
307                            anyhow!(
308                                "{}\n failed to calculate {}",
309                                format_dbg!(),
310                                stringify!(self.state.eff)
311                            )
312                        })?,
313                    _ => {
314                        return Err(Error::InitError(format_dbg!(
315                            "Only 1-D interpolators are supported"
316                        ))
317                        .into())
318                    }
319                },
320            || format_dbg!(),
321        )?;
322        // `pwr_mech_prop_out` is `pwr_out_req` unless `pwr_out_req` is more negative than `pwr_mech_regen_max`,
323        // in which case, excess is handled by `pwr_mech_dyn_brake`
324        self.state.pwr_mech_prop_out.update(
325            pwr_out_req.max(-*self.state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?),
326            || format_dbg!(),
327        )?;
328
329        self.state.pwr_mech_dyn_brake.update(
330            -(pwr_out_req - *self.state.pwr_mech_prop_out.get_fresh(|| format_dbg!())?),
331            || format_dbg!(),
332        )?;
333        ensure!(
334            *self.state.pwr_mech_dyn_brake.get_fresh(|| format_dbg!())? >= si::Power::ZERO,
335            "Mech Dynamic Brake Power cannot be below 0.0"
336        );
337
338        // if pwr_out_req is negative, need to multiply by eff
339        self.state.pwr_elec_prop_in.update(
340            if pwr_out_req > si::Power::ZERO {
341                *self.state.pwr_mech_prop_out.get_fresh(|| format_dbg!())?
342                    / *self.state.eff.get_fresh(|| format_dbg!())?
343            } else {
344                *self.state.pwr_mech_prop_out.get_fresh(|| format_dbg!())?
345                    * *self.state.eff.get_fresh(|| format_dbg!())?
346            },
347            || format_dbg!(),
348        )?;
349
350        self.state.pwr_elec_dyn_brake.update(
351            *self.state.pwr_mech_dyn_brake.get_fresh(|| format_dbg!())?
352                * *self.state.eff.get_fresh(|| format_dbg!())?,
353            || format_dbg!(),
354        )?;
355
356        // loss does not account for dynamic braking
357        self.state.pwr_loss.update(
358            (*self.state.pwr_mech_prop_out.get_fresh(|| format_dbg!())?
359                - *self.state.pwr_elec_prop_in.get_fresh(|| format_dbg!())?)
360            .abs(),
361            || format_dbg!(),
362        )?;
363
364        Ok(*self.state.pwr_elec_prop_in.get_fresh(|| format_dbg!())?)
365    }
366}
367
368impl SerdeAPI for ElectricMachine {}
369impl Init for ElectricMachine {
370    fn init(&mut self) -> Result<(), Error> {
371        let _ = self
372            .mass()
373            .map_err(|err| Error::InitError(format_dbg!(err)))?;
374        let _ = check_interp_frac_data(match &mut self.eff_interp_achieved  {
375                InterpolatorEnum::Interp1D(interp) => interp.data.grid[0].as_slice().ok_or(Error::Other("Cannot convert to slice".to_string()))?, _ => {
376            return Err(Error::InitError(format_dbg!(
377                "Only 1-D interpolators are supported"
378            )))
379        }}, InterpRange::Either)
380            .map_err(|err|
381                Error::InitError(format!(
382                    "{}\nInvalid values for `ElectricMachine::pwr_out_frac_interp`; must range from [-1..1] or [0..1].",
383                    format_dbg!(err)
384                )
385             ))?;
386        self.state
387            .init()
388            .map_err(|err| Error::InitError(format_dbg!(err)))?;
389        // sets eff_interp_bwd to eff_interp_fwd, but changes the x-value.
390        // TODO: what should the default strategy be for eff_interp_bwd?
391        let eff_interp_at_max_input = match &self.eff_interp_achieved {
392            InterpolatorEnum::Interp1D(interp) => {
393                InterpolatorEnum::new_1d(
394                    interp.data.grid[0]
395                        .iter()
396                        .zip(&interp.data.values)
397                        .map(|(x, y)| x / y)
398                        .collect(),
399                    interp.data.values.clone(),
400                    // TODO: should these be set to be the same as eff_interp_fwd,
401                    // as currently is done, or should they be set to be specific
402                    // Extrapolate and Strategy types?
403                    interp.strategy.clone(),
404                    interp.extrapolate.clone(),
405                )
406            }
407            _ => unimplemented!(),
408        }
409        .map_err(|e| Error::NinterpError(e.to_string()))?;
410        self.eff_interp_at_max_input = Some(eff_interp_at_max_input);
411        Ok(())
412    }
413}
414impl HistoryMethods for ElectricMachine {
415    fn save_interval(&self) -> anyhow::Result<Option<usize>> {
416        Ok(self.save_interval)
417    }
418    fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
419        self.save_interval = save_interval;
420        Ok(())
421    }
422    fn clear(&mut self) {
423        self.history.clear();
424    }
425}
426
427impl Mass for ElectricMachine {
428    fn mass(&self) -> anyhow::Result<Option<si::Mass>> {
429        let derived_mass = self
430            .derived_mass()
431            .with_context(|| anyhow!(format_dbg!()))?;
432        if let (Some(derived_mass), Some(set_mass)) = (derived_mass, self.mass) {
433            ensure!(
434                utils::almost_eq_uom(&set_mass, &derived_mass, None),
435                format!(
436                    "{}",
437                    format_dbg!(utils::almost_eq_uom(&set_mass, &derived_mass, None)),
438                )
439            );
440        }
441        Ok(self.mass)
442    }
443
444    fn set_mass(
445        &mut self,
446        new_mass: Option<si::Mass>,
447        side_effect: MassSideEffect,
448    ) -> anyhow::Result<()> {
449        let derived_mass = self
450            .derived_mass()
451            .with_context(|| anyhow!(format_dbg!()))?;
452        if let (Some(derived_mass), Some(new_mass)) = (derived_mass, new_mass) {
453            if derived_mass != new_mass {
454                match side_effect {
455                    MassSideEffect::Extensive => {
456                        self.pwr_out_max = self.specific_pwr.with_context(|| {
457                            format!(
458                                "{}\nExpected `self.specific_pwr` to be `Some`.",
459                                format_dbg!()
460                            )
461                        })? * new_mass;
462                    }
463                    MassSideEffect::Intensive => {
464                        self.specific_pwr = Some(self.pwr_out_max / new_mass);
465                    }
466                    MassSideEffect::None => {
467                        self.specific_pwr = None;
468                    }
469                }
470            }
471        } else if new_mass.is_none() {
472            self.specific_pwr = None;
473        }
474        self.mass = new_mass;
475        Ok(())
476    }
477
478    fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
479        Ok(self
480            .specific_pwr
481            .map(|specific_pwr| self.pwr_out_max / specific_pwr))
482    }
483
484    fn expunge_mass_fields(&mut self) {
485        self.specific_pwr = None;
486        self.mass = None;
487    }
488}
489
490impl ElectricMachine {
491    /// Returns max value of `eff_interp_fwd`
492    pub fn get_eff_fwd_max(&self) -> anyhow::Result<&f64> {
493        // since efficiency is all f64 between 0 and 1, NEG_INFINITY is safe
494        self.eff_interp_achieved.max()
495    }
496
497    /// Returns max value of `eff_interp_bwd`
498    pub fn get_eff_max_bwd(&self) -> anyhow::Result<&f64> {
499        self.eff_interp_at_max_input
500            .as_ref()
501            .with_context(|| "eff_interp_bwd should be Some by this point.")?
502            .max()
503    }
504
505    /// Scales eff_interp_fwd and eff_interp_bwd by ratio of new `eff_max` per current calculated max
506    pub fn set_eff_fwd_max(&mut self, eff_max: f64) -> anyhow::Result<()> {
507        if (0.0..=1.0).contains(&eff_max) {
508            let old_max_fwd = *self.get_eff_fwd_max()?;
509            let old_max_bwd = *self.get_eff_max_bwd()?;
510            match &mut self.eff_interp_achieved {
511                InterpolatorEnum::Interp1D(interp) => {
512                    interp.data.values = interp
513                        .data
514                        .values
515                        .iter()
516                        .map(|x| x * eff_max / old_max_fwd)
517                        .collect::<Array1<_>>();
518                }
519                _ => bail!("{}\n", "Only `InterpolatorEnum::Interp1D` is allowed."),
520            }
521            match &mut self.eff_interp_at_max_input {
522                Some(InterpolatorEnum::Interp1D(interp)) => {
523                    interp.data.values = interp
524                        .data
525                        .values
526                        .iter()
527                        .map(|x| x * eff_max / old_max_bwd)
528                        .collect::<Array1<_>>();
529                }
530                _ => bail!("{}\n", "Only `InterpolatorEnum::Interp1D` is allowed. eff_interp_bwd should be Some by this point."),
531            }
532            Ok(())
533        } else {
534            Err(anyhow!(
535                "`eff_max` ({:.3}) must be between 0.0 and 1.0",
536                eff_max,
537            ))
538        }
539    }
540
541    /// Returns min value of `eff_interp_fwd`
542    pub fn get_eff_min_fwd(&self) -> anyhow::Result<&f64> {
543        self.eff_interp_achieved.min()
544    }
545
546    /// Returns min value of `eff_interp_at_max_input`
547    pub fn get_eff_min_at_max_input(&self) -> anyhow::Result<&f64> {
548        self.eff_interp_at_max_input
549            .as_ref()
550            .context("eff_interp_bwd should be Some by this point")?
551            .min()
552    }
553
554    /// Max value of `eff_interp_fwd` minus min value of `eff_interp_fwd`.
555    pub fn get_eff_fwd_range(&self) -> anyhow::Result<f64> {
556        Ok(self.get_eff_fwd_max()? - self.get_eff_min_fwd()?)
557    }
558
559    /// Max value of `eff_interp_bwd` minus min value of `eff_interp_bwd`.
560    pub fn get_eff_range_bwd(&self) -> anyhow::Result<f64> {
561        Ok(self.get_eff_max_bwd()? - self.get_eff_min_at_max_input()?)
562    }
563
564    /// Scales values of `eff_interp_fwd.f_x` and `eff_interp_bwd.f_x` without changing max such that max - min
565    /// is equal to new range.  Will change max if needed to ensure no values are
566    /// less than zero.
567    pub fn set_eff_fwd_range(&mut self, eff_range: f64) -> anyhow::Result<()> {
568        let eff_max_fwd = self.get_eff_fwd_max()?.to_owned();
569        let eff_max_bwd = self.get_eff_max_bwd()?.to_owned();
570        if eff_range == 0.0 {
571            let f_x_fwd = vec![
572                eff_max_fwd;
573                match &self.eff_interp_achieved {
574                    InterpolatorEnum::Interp1D(interp) => interp.data.values.len(),
575                    _ => {
576                        return Err(Error::InitError(format_dbg!(
577                            "Only 1-D interpolators are supported"
578                        ))
579                        .into());
580                    }
581                }
582            ];
583            match &mut self.eff_interp_achieved {
584                InterpolatorEnum::Interp1D(interp) => interp.data.values = Array::from_vec(f_x_fwd),
585                _ => {
586                    return Err(Error::InitError(format_dbg!(
587                        "Only 1-D interpolators are supported"
588                    ))
589                    .into());
590                }
591            };
592            let f_x_bwd = vec![
593                eff_max_bwd;
594                match &self.eff_interp_at_max_input {
595                    Some(interp) => {
596                        match interp {
597                            InterpolatorEnum::Interp1D(interp) => interp.data.values.len(),
598                            _ => {
599                                return Err(Error::InitError(format_dbg!(
600                                    "Only 1-D interpolators are supported"
601                                ))
602                                .into());
603                            }
604                        }
605                    }
606                    None => bail!("eff_interp_bwd should be Some by this point."),
607                }
608            ];
609            self.eff_interp_at_max_input
610                .as_mut()
611                .map(|interpolator| match interpolator {
612                    InterpolatorEnum::Interp1D(interp) => {
613                        interp.data.values = Array::from_vec(f_x_bwd);
614                        Ok(())
615                    }
616                    _ => {
617                        return Err(Error::InitError(format_dbg!(
618                            "Only 1-D interpolators are supported"
619                        )));
620                    }
621                })
622                .transpose()?;
623            Ok(())
624        } else if (0.0..=1.0).contains(&eff_range) {
625            let old_min = self.get_eff_min_fwd()?;
626            let old_range = self.get_eff_fwd_max()? - old_min;
627            if old_range == 0.0 {
628                return Err(anyhow!(
629                    "`eff_range` is already zero so it cannot be modified."
630                ));
631            }
632            match &mut self.eff_interp_achieved {
633                InterpolatorEnum::Interp1D(interp) => {
634                    interp.data.values = interp
635                        .data
636                        .values
637                        .iter()
638                        .map(|x| eff_max_fwd + (x - eff_max_fwd) * eff_range / old_range)
639                        .collect();
640                    interp.validate()?;
641                }
642                _ => bail!("{}\n", "Only `InterpolatorEnum::Interp1D` is allowed."),
643            }
644            if self.get_eff_min_fwd()? < &0. {
645                let x_neg = *self.get_eff_min_fwd()?;
646                match &mut self.eff_interp_achieved {
647                    InterpolatorEnum::Interp1D(interp) => {
648                        interp.data.values.map_inplace(|x| *x -= x_neg);
649                        interp.validate()?;
650                    }
651                    _ => bail!("{}\n", "Only `InterpolatorEnum::Interp1D` is allowed."),
652                }
653            }
654            if self.get_eff_fwd_max()? > &1.0 {
655                return Err(anyhow!(format!(
656                    "`eff_max` ({:.3}) must be no greater than 1.0",
657                    self.get_eff_fwd_max()?
658                )));
659            }
660            let old_min = self.get_eff_min_at_max_input()?;
661            let old_range = self.get_eff_max_bwd()? - old_min;
662            if old_range == 0.0 {
663                return Err(anyhow!(
664                    "`eff_range` is already zero so it cannot be modified."
665                ));
666            }
667
668            //TODO
669            match &mut self.eff_interp_at_max_input {
670                Some(InterpolatorEnum::Interp1D(interp)) => {
671                    interp.data.values = interp
672                        .data
673                        .values
674                        .iter()
675                        .map(|x| eff_max_bwd + (x - eff_max_bwd) * eff_range / old_range)
676                        .collect();
677                }
678                _ => bail!("TODO"),
679            }
680
681            if self.get_eff_min_at_max_input()? < &0.0 {
682                let x_neg = *self.get_eff_min_at_max_input()?;
683                self.eff_interp_at_max_input
684                    .as_mut()
685                    .map(|interpolator| match interpolator {
686                        InterpolatorEnum::Interp1D(interp) => {
687                            interp.data.values.map_inplace(|x| *x -= x_neg);
688                            interp.validate()?;
689                            Ok(())
690                        }
691                        _ => bail!("Only `InterpolatorEnum::Interp1D` is allowed."),
692                    })
693                    .transpose()?;
694            }
695            if self.get_eff_max_bwd()? > &1.0 {
696                return Err(anyhow!(format!(
697                    "`eff_max` ({:.3}) must be no greater than 1.0",
698                    self.get_eff_max_bwd()?
699                )));
700            }
701            Ok(())
702        } else {
703            Err(anyhow!(format!(
704                "`eff_range` ({:.3}) must be between 0.0 and 1.0",
705                eff_range,
706            )))
707        }
708    }
709}
710
711#[serde_api]
712#[derive(
713    Clone,
714    Debug,
715    Default,
716    Deserialize,
717    Serialize,
718    PartialEq,
719    HistoryVec,
720    StateMethods,
721    SetCumulative,
722)]
723#[non_exhaustive]
724#[serde(default)]
725#[serde(deny_unknown_fields)]
726#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
727
728pub struct ElectricMachineState {
729    /// time step index
730    pub i: TrackedState<usize>,
731    /// Component efficiency based on current power demand.
732    pub eff: TrackedState<si::Ratio>,
733    // Component limits
734    /// Maximum possible positive traction power.
735    pub pwr_mech_fwd_out_max: TrackedState<si::Power>,
736    /// efficiency in forward direction at max possible input power from `FuelConverter` and `ReversibleEnergyStorage`
737    pub eff_fwd_at_max_input: TrackedState<si::Ratio>,
738    /// Maximum possible regeneration power going to ReversibleEnergyStorage.
739    pub pwr_mech_regen_max: TrackedState<si::Power>,
740    /// efficiency in backward direction at max possible input power from `FuelConverter` and `ReversibleEnergyStorage`
741    pub eff_at_max_regen: TrackedState<si::Ratio>,
742
743    // Current values
744    /// Raw power requirement from boundary conditions
745    pub pwr_out_req: TrackedState<si::Power>,
746    /// Integral of [Self::pwr_out_req]
747    pub energy_out_req: TrackedState<si::Energy>,
748    /// Electrical power to propulsion from ReversibleEnergyStorage and Generator.
749    /// negative value indicates regenerative braking
750    pub pwr_elec_prop_in: TrackedState<si::Power>,
751    /// Integral of [Self::pwr_elec_prop_in]
752    pub energy_elec_prop_in: TrackedState<si::Energy>,
753    /// Mechanical power to propulsion, corrected by efficiency, from ReversibleEnergyStorage and Generator.
754    /// Negative value indicates regenerative braking.
755    pub pwr_mech_prop_out: TrackedState<si::Power>,
756    /// Integral of [Self::pwr_mech_prop_out]
757    pub energy_mech_prop_out: TrackedState<si::Energy>,
758    /// Mechanical power from dynamic braking.  Positive value indicates braking; this should be zero otherwise.
759    pub pwr_mech_dyn_brake: TrackedState<si::Power>,
760    /// Integral of [Self::pwr_mech_dyn_brake]
761    pub energy_mech_dyn_brake: TrackedState<si::Energy>,
762    /// Electrical power from dynamic braking, dissipated as heat.
763    pub pwr_elec_dyn_brake: TrackedState<si::Power>,
764    /// Integral of [Self::pwr_elec_dyn_brake]
765    pub energy_elec_dyn_brake: TrackedState<si::Energy>,
766    /// Power lost in regeneratively converting mechanical power to power that can be absorbed by the battery.
767    pub pwr_loss: TrackedState<si::Power>,
768    /// Integral of [Self::pwr_loss]
769    pub energy_loss: TrackedState<si::Energy>,
770}
771
772#[pyo3_api]
773impl ElectricMachineState {}
774
775impl Init for ElectricMachineState {}
776impl SerdeAPI for ElectricMachineState {}