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(default)]
43    pub history: ElectricMachineStateHistoryVec,
44}
45
46#[pyo3_api]
47impl ElectricMachine {
48    // #[new]
49    // fn __new__(
50    //     pwr_out_frac_interp: Vec<f64>,
51    //     eff_interp: Vec<f64>,
52    //     pwr_out_max_watts: f64,
53    //     save_interval: Option<usize>,
54    // ) -> anyhow::Result<Self> {
55    //     Self::new(
56    //         pwr_out_frac_interp,
57    //         eff_interp,
58    //         pwr_out_max_watts,
59    //         save_interval,
60    //     )
61    // }
62
63    // #[setter]
64    // pub fn set_eff_interp(&mut self, new_value: Vec<f64>) -> anyhow::Result<()> {
65    //     self.eff_interp = new_value;
66    //     self.set_pwr_in_frac_interp()
67    // }
68
69    #[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    /// Returns maximum possible positive and negative propulsion-related powers
99    /// this component/system can produce, accounting for any aux-related power
100    /// required.
101    /// # Arguments
102    /// - `pwr_in_fwd_lim`: positive-propulsion-related power available to this
103    ///   component. Positive values indicate that the upstream component can supply
104    ///   positive tractive power.
105    /// - `pwr_in_bwd_lim`: negative-propulsion-related power available to this
106    ///   component. Zero means no power can be sent to upstream compnents and positive
107    ///   values indicate upstream components can absorb energy.
108    /// - `pwr_aux`: aux-related power required from this component
109    /// - `dt`: simulation time step size
110    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        // 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        let raw_tractive_lookup_ratio = (*pwr_in_fwd_lim / self.pwr_out_max).get::<si::ratio>();
144        let raw_regen_lookup_ratio = (*pwr_in_bwd_lim / self.pwr_out_max).get::<si::ratio>();
145        self.state.eff_fwd_at_max_input.update(
146            uc::R
147                * self
148                    .eff_interp_at_max_input
149                    .as_ref()
150                    .map(|interpolator| {
151                        interpolator
152                            .interpolate(&[abs_checked_x_val(
153                                raw_tractive_lookup_ratio,
154                                match interpolator {
155                                    InterpolatorEnum::Interp1D(interp) => interp.data.grid[0]
156                                        .as_slice()
157                                        .ok_or_else(|| anyhow!(format_dbg!()))?,
158                                    _ => bail!("Only `InterpolatorEnum::Interp1D` is allowed."),
159                                },
160                            )?])
161                            .map_err(|e| anyhow!(e))
162                    })
163                    .ok_or(anyhow!(
164                        "eff_interp_bwd is None, which should never be the case at this point."
165                    ))?
166                    .with_context(|| {
167                        anyhow!(
168                            "{}\n failed to calculate {}",
169                            format_dbg!(),
170                            stringify!(eff_pos)
171                        )
172                    })?,
173            || format_dbg!(),
174        )?;
175        self.state.eff_at_max_regen.update(
176            uc::R
177                * self
178                    .eff_interp_at_max_input
179                    .as_ref()
180                    .map(|interpolator| {
181                        interpolator
182                            .interpolate(&[abs_checked_x_val(
183                                raw_regen_lookup_ratio,
184                                match interpolator {
185                                    InterpolatorEnum::Interp1D(interp) => interp.data.grid[0]
186                                        .as_slice()
187                                        .ok_or_else(|| anyhow!(format_dbg!()))?,
188                                    _ => bail!("Only `InterpolatorEnum::Interp1D` is allowed."),
189                                },
190                            )?])
191                            .map_err(|e| anyhow!(e))
192                    })
193                    .ok_or(anyhow!(
194                        "eff_interp_bwd is None, which should never be the case at this point."
195                    ))?
196                    .with_context(|| {
197                        anyhow!(
198                            "{}\n failed to calculate {}",
199                            format_dbg!(),
200                            stringify!(eff_neg)
201                        )
202                    })?,
203            || format_dbg!(),
204        )?;
205
206        // maximum power in forward direction is minimum of component `pwr_out_max` parameter or time-varying max
207        // power based on what the ReversibleEnergyStorage can provide
208        self.state.pwr_mech_fwd_out_max.update(
209            self.pwr_out_max.min(
210                *pwr_in_fwd_lim
211                    * *self
212                        .state
213                        .eff_fwd_at_max_input
214                        .get_fresh(|| format_dbg!())?,
215            ),
216            || format_dbg!(),
217        )?;
218        // maximum power in backward direction is minimum of component `pwr_out_max` parameter or time-varying max
219        // power in bacward direction (i.e. regen) based on what the ReversibleEnergyStorage can provide
220        self.state.pwr_mech_regen_max.update(
221            self.pwr_out_max
222                .min(*pwr_in_bwd_lim / *self.state.eff_at_max_regen.get_fresh(|| format_dbg!())?),
223            || format_dbg!(),
224        )?;
225        Ok(())
226    }
227
228    fn get_curr_pwr_prop_out_max(&self) -> anyhow::Result<(si::Power, si::Power)> {
229        Ok((
230            *self
231                .state
232                .pwr_mech_fwd_out_max
233                .get_fresh(|| format_dbg!())?,
234            *self.state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?,
235        ))
236    }
237
238    /// Solves for this powertrain system/component efficiency and sets/returns power input required.
239    /// # Arguments
240    /// - `pwr_out_req`: propulsion-related power output required
241    /// - `dt`: simulation time step size
242    fn solve(
243        &mut self,
244        pwr_out_req: si::Power,
245        _enabled: bool,
246        _dt: si::Time,
247    ) -> anyhow::Result<Option<si::Power>> {
248        if pwr_out_req > si::Power::ZERO {
249            ensure!(
250                pwr_out_req <= self.pwr_out_max,
251                format!(
252                    "{}\nedrv required power ({} kW) exceeds static max power ({} kW)",
253                    format_dbg!(),
254                    pwr_out_req.get::<si::kilowatt>().format_eng(Some(9)),
255                    self.pwr_out_max.get::<si::kilowatt>().format_eng(Some(9))
256                ),
257            );
258        }
259        // not needed during negative traction because friction braking is still included
260        ensure!(
261            almost_le_uom(&pwr_out_req , self.state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?, None),
262            format!(
263                "{}\nedrv required propulsion power ({} kW) exceeds current max propulsion power ({} kW) by {} kW",
264                format_dbg!(pwr_out_req <= *self.state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?),
265                pwr_out_req.get::<si::kilowatt>().format_eng(Some(6)),
266                self.state
267                    .pwr_mech_fwd_out_max
268                    .get_fresh(|| format_dbg!())?
269                    .get::<si::kilowatt>()
270                    .format_eng(Some(6)),
271                    (pwr_out_req - *self.state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?).get::<si::kilowatt>().format_eng(Some(6))
272            ),
273        );
274        if pwr_out_req < si::Power::ZERO {
275            ensure!(
276                almost_le_uom(
277                    &pwr_out_req.abs(),
278                    self.state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?,
279                    None
280                ),
281                format!(
282                    "{}\nedrv charge power ({:.6} kW) exceeds current max charge power ({:.6} kW)",
283                    format_dbg!(),
284                    -pwr_out_req.get::<si::kilowatt>(),
285                    self.state
286                        .pwr_mech_regen_max
287                        .get_fresh(|| format_dbg!())?
288                        .get::<si::kilowatt>()
289                ),
290            );
291        }
292
293        self.state
294            .pwr_out_req
295            .update(pwr_out_req, || format_dbg!())?;
296
297        // `pwr_mech_prop_out` is `pwr_out_req` unless `pwr_out_req` is more negative than `pwr_mech_regen_max`,
298        // in which case, excess is handled by `pwr_mech_dyn_brake`
299        self.state.pwr_mech_prop_out.update(
300            pwr_out_req.max(-*self.state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?),
301            || format_dbg!(),
302        )?;
303
304        let is_max_output = pwr_out_req
305            == *self
306                .state
307                .pwr_mech_fwd_out_max
308                .get_fresh(|| format_dbg!())?;
309
310        // ensuring eff_interp_fwd has Extrapolate set to Error before calculating self.state.eff
311        self.eff_interp_achieved
312            .set_extrapolate(Extrapolate::Error)?;
313
314        let raw_lookup_pwr_ratio = (pwr_out_req / self.pwr_out_max).get::<si::ratio>();
315        let calculated_eff = uc::R
316            * match &self.eff_interp_achieved {
317                InterpolatorEnum::Interp1D(interp) => interp
318                    .interpolate(&[{
319                        let pwr = |pwr_uncorrected: f64| -> anyhow::Result<f64> {
320                            Ok({
321                                if interp.data.grid[0]
322                                    .first()
323                                    .with_context(|| anyhow!(format_dbg!()))?
324                                    >= &0.
325                                {
326                                    pwr_uncorrected.max(0.)
327                                } else {
328                                    pwr_uncorrected
329                                }
330                            })
331                        };
332                        pwr(raw_lookup_pwr_ratio)?
333                    }])
334                    .with_context(|| {
335                        anyhow!(
336                            "{}\n failed to calculate {}",
337                            format_dbg!(),
338                            stringify!(self.state.eff)
339                        )
340                    })?,
341                _ => {
342                    return Err(Error::InitError(format_dbg!(
343                        "Only 1-D interpolators are supported"
344                    ))
345                    .into())
346                }
347            };
348        let eff_value = if is_max_output {
349            if pwr_out_req >= si::Power::ZERO {
350                *self
351                    .state
352                    .eff_fwd_at_max_input
353                    .get_fresh(|| format_dbg!())?
354            } else {
355                *self.state.eff_at_max_regen.get_fresh(|| format_dbg!())?
356            }
357        } else {
358            calculated_eff
359        };
360        ensure!(eff_value >= si::Ratio::ZERO && eff_value <= 1.0 * uc::R);
361        self.state.eff.update(eff_value, || format_dbg!())?;
362
363        self.state.pwr_mech_dyn_brake.update(
364            -(pwr_out_req - *self.state.pwr_mech_prop_out.get_fresh(|| format_dbg!())?),
365            || format_dbg!(),
366        )?;
367        ensure!(
368            *self.state.pwr_mech_dyn_brake.get_fresh(|| format_dbg!())? >= si::Power::ZERO,
369            "Mech Dynamic Brake Power cannot be below 0.0"
370        );
371
372        // if pwr_out_req is negative, need to multiply by eff
373        self.state.pwr_elec_prop_in.update(
374            if pwr_out_req > si::Power::ZERO {
375                *self.state.pwr_mech_prop_out.get_fresh(|| format_dbg!())?
376                    / *self.state.eff.get_fresh(|| format_dbg!())?
377            } else {
378                *self.state.pwr_mech_prop_out.get_fresh(|| format_dbg!())?
379                    * *self.state.eff.get_fresh(|| format_dbg!())?
380            },
381            || format_dbg!(),
382        )?;
383
384        self.state.pwr_elec_dyn_brake.update(
385            *self.state.pwr_mech_dyn_brake.get_fresh(|| format_dbg!())?
386                * *self.state.eff.get_fresh(|| format_dbg!())?,
387            || format_dbg!(),
388        )?;
389
390        // loss does not account for dynamic braking
391        self.state.pwr_loss.update(
392            (*self.state.pwr_mech_prop_out.get_fresh(|| format_dbg!())?
393                - *self.state.pwr_elec_prop_in.get_fresh(|| format_dbg!())?)
394            .abs(),
395            || format_dbg!(),
396        )?;
397
398        Ok(Some(
399            *self.state.pwr_elec_prop_in.get_fresh(|| format_dbg!())?,
400        ))
401    }
402
403    fn pwr_regen(&self) -> anyhow::Result<si::Power> {
404        Ok(-self
405            .state
406            .pwr_mech_dyn_brake
407            .get_fresh(|| format_dbg!())?
408            .max(si::Power::ZERO))
409    }
410}
411
412impl SerdeAPI for ElectricMachine {}
413impl Init for ElectricMachine {
414    fn init(&mut self) -> Result<(), Error> {
415        let _ = self
416            .mass()
417            .map_err(|err| Error::InitError(format_dbg!(err)))?;
418        let _ = check_interp_frac_data(match &mut self.eff_interp_achieved  {
419                InterpolatorEnum::Interp1D(interp) => interp.data.grid[0].as_slice().ok_or(Error::Other("Cannot convert to slice".to_string()))?, _ => {
420            return Err(Error::InitError(format_dbg!(
421                "Only 1-D interpolators are supported"
422            )))
423        }}, InterpRange::Either)
424            .map_err(|err|
425                Error::InitError(format!(
426                    "{}\nInvalid values for `ElectricMachine::pwr_out_frac_interp`; must range from [-1..1] or [0..1].",
427                    format_dbg!(err)
428                )
429             ))?;
430        self.state
431            .init()
432            .map_err(|err| Error::InitError(format_dbg!(err)))?;
433        // sets eff_interp_bwd to eff_interp_fwd, but changes the x-value.
434        // TODO: what should the default strategy be for eff_interp_bwd?
435        let eff_interp_at_max_input = match &self.eff_interp_achieved {
436            InterpolatorEnum::Interp1D(interp) => {
437                InterpolatorEnum::new_1d(
438                    interp.data.grid[0]
439                        .iter()
440                        .zip(&interp.data.values)
441                        .map(|(x, y)| x / y)
442                        .collect(),
443                    interp.data.values.clone(),
444                    // TODO: should these be set to be the same as eff_interp_fwd,
445                    // as currently is done, or should they be set to be specific
446                    // Extrapolate and Strategy types?
447                    interp.strategy.clone(),
448                    interp.extrapolate,
449                )
450            }
451            _ => unimplemented!(),
452        }
453        .map_err(|e| Error::NinterpError(e.to_string()))?;
454        self.eff_interp_at_max_input = Some(eff_interp_at_max_input);
455        Ok(())
456    }
457}
458impl HistoryMethods for ElectricMachine {
459    fn save_interval(&self) -> anyhow::Result<Option<usize>> {
460        Ok(self.save_interval)
461    }
462    fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
463        self.save_interval = save_interval;
464        Ok(())
465    }
466    fn clear(&mut self) {
467        self.history.clear();
468    }
469}
470
471impl Mass for ElectricMachine {
472    fn mass(&self) -> anyhow::Result<Option<si::Mass>> {
473        let derived_mass = self
474            .derived_mass()
475            .with_context(|| anyhow!(format_dbg!()))?;
476        if let (Some(derived_mass), Some(set_mass)) = (derived_mass, self.mass) {
477            ensure!(
478                utils::almost_eq_uom(&set_mass, &derived_mass, None),
479                format!(
480                    "{}",
481                    format_dbg!(utils::almost_eq_uom(&set_mass, &derived_mass, None)),
482                )
483            );
484        }
485        Ok(self.mass)
486    }
487
488    fn set_mass(
489        &mut self,
490        new_mass: Option<si::Mass>,
491        side_effect: MassSideEffect,
492    ) -> anyhow::Result<()> {
493        let derived_mass = self
494            .derived_mass()
495            .with_context(|| anyhow!(format_dbg!()))?;
496        if let (Some(derived_mass), Some(new_mass)) = (derived_mass, new_mass) {
497            if derived_mass != new_mass {
498                match side_effect {
499                    MassSideEffect::Extensive => {
500                        self.pwr_out_max = self.specific_pwr.with_context(|| {
501                            format!(
502                                "{}\nExpected `self.specific_pwr` to be `Some`.",
503                                format_dbg!()
504                            )
505                        })? * new_mass;
506                    }
507                    MassSideEffect::Intensive => {
508                        self.specific_pwr = Some(self.pwr_out_max / new_mass);
509                    }
510                    MassSideEffect::None => {
511                        self.specific_pwr = None;
512                    }
513                }
514            }
515        } else if new_mass.is_none() {
516            self.specific_pwr = None;
517        }
518        self.mass = new_mass;
519        Ok(())
520    }
521
522    fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
523        Ok(self
524            .specific_pwr
525            .map(|specific_pwr| self.pwr_out_max / specific_pwr))
526    }
527
528    fn expunge_mass_fields(&mut self) {
529        self.specific_pwr = None;
530        self.mass = None;
531    }
532}
533
534impl TryFrom<EMBuilder> for ElectricMachine {
535    type Error = anyhow::Error;
536    fn try_from(em_builder: EMBuilder) -> anyhow::Result<ElectricMachine> {
537        let mut em = ElectricMachine {
538            eff_interp_achieved: em_builder.eff_interp_achieved.clone(),
539            eff_interp_at_max_input: None,
540            pwr_out_max: em_builder.pwr_out_max,
541            specific_pwr: None,
542            mass: None,
543            save_interval: Some(1),
544            state: Default::default(),
545            history: Default::default(),
546        };
547        em.init()?;
548
549        Ok(em)
550    }
551}
552
553impl ElectricMachine {
554    /// Returns max value of `eff_interp_fwd`
555    pub fn get_eff_fwd_max(&self) -> anyhow::Result<&f64> {
556        // since efficiency is all f64 between 0 and 1, NEG_INFINITY is safe
557        self.eff_interp_achieved.max()
558    }
559
560    /// Returns max value of `eff_interp_bwd`
561    pub fn get_eff_max_bwd(&self) -> anyhow::Result<&f64> {
562        self.eff_interp_at_max_input
563            .as_ref()
564            .with_context(|| "eff_interp_bwd should be Some by this point.")?
565            .max()
566    }
567
568    /// Scales eff_interp_fwd and eff_interp_bwd by ratio of new `eff_max` per current calculated max
569    pub fn set_eff_fwd_max(&mut self, eff_max: f64) -> anyhow::Result<()> {
570        if (0.0..=1.0).contains(&eff_max) {
571            let old_max_fwd = *self.get_eff_fwd_max()?;
572            let old_max_bwd = *self.get_eff_max_bwd()?;
573            match &mut self.eff_interp_achieved {
574                InterpolatorEnum::Interp1D(interp) => {
575                    interp.data.values = interp
576                        .data
577                        .values
578                        .iter()
579                        .map(|x| x * eff_max / old_max_fwd)
580                        .collect::<Array1<_>>();
581                }
582                _ => bail!("{}\n", "Only `InterpolatorEnum::Interp1D` is allowed."),
583            }
584            match &mut self.eff_interp_at_max_input {
585                Some(InterpolatorEnum::Interp1D(interp)) => {
586                    interp.data.values = interp
587                        .data
588                        .values
589                        .iter()
590                        .map(|x| x * eff_max / old_max_bwd)
591                        .collect::<Array1<_>>();
592                }
593                _ => bail!("{}\n", "Only `InterpolatorEnum::Interp1D` is allowed. eff_interp_bwd should be Some by this point."),
594            }
595            Ok(())
596        } else {
597            Err(anyhow!(
598                "`eff_max` ({:.3}) must be between 0.0 and 1.0",
599                eff_max,
600            ))
601        }
602    }
603
604    /// Returns min value of `eff_interp_fwd`
605    pub fn get_eff_min_fwd(&self) -> anyhow::Result<&f64> {
606        self.eff_interp_achieved.min()
607    }
608
609    /// Returns min value of `eff_interp_at_max_input`
610    pub fn get_eff_min_at_max_input(&self) -> anyhow::Result<&f64> {
611        self.eff_interp_at_max_input
612            .as_ref()
613            .context("eff_interp_bwd should be Some by this point")?
614            .min()
615    }
616
617    /// Max value of `eff_interp_fwd` minus min value of `eff_interp_fwd`.
618    pub fn get_eff_fwd_range(&self) -> anyhow::Result<f64> {
619        Ok(self.get_eff_fwd_max()? - self.get_eff_min_fwd()?)
620    }
621
622    /// Max value of `eff_interp_bwd` minus min value of `eff_interp_bwd`.
623    pub fn get_eff_range_bwd(&self) -> anyhow::Result<f64> {
624        Ok(self.get_eff_max_bwd()? - self.get_eff_min_at_max_input()?)
625    }
626
627    /// Scales values of `eff_interp_fwd.f_x` and `eff_interp_bwd.f_x` without changing max such that max - min
628    /// is equal to new range.  Will change max if needed to ensure no values are
629    /// less than zero.
630    pub fn set_eff_fwd_range(&mut self, eff_range: f64) -> anyhow::Result<()> {
631        let eff_max_fwd = self.get_eff_fwd_max()?.to_owned();
632        let eff_max_bwd = self.get_eff_max_bwd()?.to_owned();
633        if eff_range == 0.0 {
634            let f_x_fwd = vec![
635                eff_max_fwd;
636                match &self.eff_interp_achieved {
637                    InterpolatorEnum::Interp1D(interp) => interp.data.values.len(),
638                    _ => {
639                        return Err(Error::InitError(format_dbg!(
640                            "Only 1-D interpolators are supported"
641                        ))
642                        .into());
643                    }
644                }
645            ];
646            match &mut self.eff_interp_achieved {
647                InterpolatorEnum::Interp1D(interp) => interp.data.values = Array::from_vec(f_x_fwd),
648                _ => {
649                    return Err(Error::InitError(format_dbg!(
650                        "Only 1-D interpolators are supported"
651                    ))
652                    .into());
653                }
654            };
655            let f_x_bwd = vec![
656                eff_max_bwd;
657                match &self.eff_interp_at_max_input {
658                    Some(interp) => {
659                        match interp {
660                            InterpolatorEnum::Interp1D(interp) => interp.data.values.len(),
661                            _ => {
662                                return Err(Error::InitError(format_dbg!(
663                                    "Only 1-D interpolators are supported"
664                                ))
665                                .into());
666                            }
667                        }
668                    }
669                    None => bail!("eff_interp_bwd should be Some by this point."),
670                }
671            ];
672            self.eff_interp_at_max_input
673                .as_mut()
674                .map(|interpolator| match interpolator {
675                    InterpolatorEnum::Interp1D(interp) => {
676                        interp.data.values = Array::from_vec(f_x_bwd);
677                        Ok(())
678                    }
679                    _ => Err(Error::InitError(format_dbg!(
680                        "Only 1-D interpolators are supported"
681                    ))),
682                })
683                .transpose()?;
684            Ok(())
685        } else if (0.0..=1.0).contains(&eff_range) {
686            let old_min = self.get_eff_min_fwd()?;
687            let old_range = self.get_eff_fwd_max()? - old_min;
688            if old_range == 0.0 {
689                return Err(anyhow!(
690                    "`eff_range` is already zero so it cannot be modified."
691                ));
692            }
693            match &mut self.eff_interp_achieved {
694                InterpolatorEnum::Interp1D(interp) => {
695                    interp.data.values = interp
696                        .data
697                        .values
698                        .iter()
699                        .map(|x| eff_max_fwd + (x - eff_max_fwd) * eff_range / old_range)
700                        .collect();
701                    interp.validate()?;
702                }
703                _ => bail!("{}\n", "Only `InterpolatorEnum::Interp1D` is allowed."),
704            }
705            if self.get_eff_min_fwd()? < &0. {
706                let x_neg = *self.get_eff_min_fwd()?;
707                match &mut self.eff_interp_achieved {
708                    InterpolatorEnum::Interp1D(interp) => {
709                        interp.data.values.map_inplace(|x| *x -= x_neg);
710                        interp.validate()?;
711                    }
712                    _ => bail!("{}\n", "Only `InterpolatorEnum::Interp1D` is allowed."),
713                }
714            }
715            if self.get_eff_fwd_max()? > &1.0 {
716                return Err(anyhow!(format!(
717                    "`eff_max` ({:.3}) must be no greater than 1.0",
718                    self.get_eff_fwd_max()?
719                )));
720            }
721            let old_min = self.get_eff_min_at_max_input()?;
722            let old_range = self.get_eff_max_bwd()? - old_min;
723            if old_range == 0.0 {
724                return Err(anyhow!(
725                    "`eff_range` is already zero so it cannot be modified."
726                ));
727            }
728
729            //TODO
730            match &mut self.eff_interp_at_max_input {
731                Some(InterpolatorEnum::Interp1D(interp)) => {
732                    interp.data.values = interp
733                        .data
734                        .values
735                        .iter()
736                        .map(|x| eff_max_bwd + (x - eff_max_bwd) * eff_range / old_range)
737                        .collect();
738                }
739                _ => bail!("TODO"),
740            }
741
742            if self.get_eff_min_at_max_input()? < &0.0 {
743                let x_neg = *self.get_eff_min_at_max_input()?;
744                self.eff_interp_at_max_input
745                    .as_mut()
746                    .map(|interpolator| match interpolator {
747                        InterpolatorEnum::Interp1D(interp) => {
748                            interp.data.values.map_inplace(|x| *x -= x_neg);
749                            interp.validate()?;
750                            Ok(())
751                        }
752                        _ => bail!("Only `InterpolatorEnum::Interp1D` is allowed."),
753                    })
754                    .transpose()?;
755            }
756            if self.get_eff_max_bwd()? > &1.0 {
757                return Err(anyhow!(format!(
758                    "`eff_max` ({:.3}) must be no greater than 1.0",
759                    self.get_eff_max_bwd()?
760                )));
761            }
762            Ok(())
763        } else {
764            Err(anyhow!(format!(
765                "`eff_range` ({:.3}) must be between 0.0 and 1.0",
766                eff_range,
767            )))
768        }
769    }
770}
771
772impl TryFrom<fastsim_2::vehicle::RustVehicle> for ElectricMachine {
773    type Error = anyhow::Error;
774    fn try_from(f2veh: fastsim_2::vehicle::RustVehicle) -> Result<ElectricMachine, anyhow::Error> {
775        Ok(EMBuilder {
776            eff_interp_achieved: {
777                // fastsim-2's hard-coded short vector of percent of peak power
778                let short_perc_out_vec =
779                    vec![0.0, 0.02, 0.04, 0.06, 0.08, 0.1, 0.2, 0.4, 0.6, 0.8, 1.0];
780                // `InterpolatorEnum` for fastsim-3
781                InterpolatorEnum::new_1d(
782                    short_perc_out_vec.clone().into(),
783                    {
784                        // convert 101 element f2 array to shorter f2 array and use
785                        // linear rather than left-nearest interpolation
786                        let mc_full_eff = Array1::from_vec(f2veh.mc_full_eff_array.clone());
787                        ensure!(mc_full_eff.len() == 101);
788                        let shortener = Interp1D::new(
789                            fastsim_2::params::MC_PERC_OUT_ARRAY.to_vec().into(),
790                            mc_full_eff,
791                            strategy::Linear,
792                            Extrapolate::Error,
793                        )
794                        .with_context(|| format_dbg!())?;
795                        let mut short_eff: Vec<f64> = short_perc_out_vec
796                            .iter()
797                            .map(|x| shortener.interpolate(&[*x]).unwrap())
798                            .collect();
799                        short_eff[0] = short_eff[1];
800                        short_eff.into()
801                    },
802                    strategy::Linear,
803                    Extrapolate::Error,
804                )
805            }
806            .with_context(|| {
807                format!(
808                    "{}\n{}",
809                    format_dbg!(f2veh.mc_full_eff_array.len()),
810                    format_dbg!(f2veh.mc_perc_out_array.len())
811                )
812            })?,
813            pwr_out_max: f2veh.mc_max_kw * uc::KW,
814        }
815        .try_into()
816        .with_context(|| format_dbg!())?)
817    }
818}
819
820#[serde_api]
821#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
822#[serde(deny_unknown_fields)]
823#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
824/// Builder for [ElectricMachine].  Use this to instantiate EM with minimal parameterization
825pub struct EMBuilder {
826    /// Efficiency interpolator corresponding to achieved output power
827    ///
828    /// Note that the Extrapolate field of this variable is changed in [Self::get_pwr_in_req]
829    pub eff_interp_achieved: InterpolatorEnumOwned<f64>,
830    /// Electrical input power fraction array at which efficiencies are evaluated.
831    /// Calculated during runtime if not provided.
832    // /// this will disappear and instead be in eff_interp_bwd
833    // pub pwr_in_frac_interp: Vec<f64>,
834    /// ElectricMachine maximum output power \[W\]
835    pub pwr_out_max: si::Power,
836}
837
838#[allow(dead_code)]
839impl EMBuilder {
840    fn with_save_interval(&self, save_interval: Option<usize>) -> anyhow::Result<ElectricMachine> {
841        let mut em: ElectricMachine = self.clone().try_into()?;
842        em.save_interval = save_interval;
843        Ok(em)
844    }
845
846    fn with_state(&self, state: ElectricMachineState) -> anyhow::Result<ElectricMachine> {
847        let mut em: ElectricMachine = self.clone().try_into()?;
848        em.state = state;
849        Ok(em)
850    }
851}
852
853#[serde_api]
854#[derive(
855    Clone,
856    Debug,
857    Default,
858    Deserialize,
859    Serialize,
860    PartialEq,
861    HistoryVec,
862    StateMethods,
863    SetCumulative,
864)]
865#[non_exhaustive]
866#[serde(default)]
867#[serde(deny_unknown_fields)]
868#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
869
870pub struct ElectricMachineState {
871    /// time step index
872    pub i: TrackedState<usize>,
873    /// Component efficiency based on current power demand.
874    pub eff: TrackedState<si::Ratio>,
875    // Component limits
876    /// Maximum possible positive traction power.
877    pub pwr_mech_fwd_out_max: TrackedState<si::Power>,
878    /// efficiency in forward direction at max possible input power from `FuelConverter` and `ReversibleEnergyStorage`
879    pub eff_fwd_at_max_input: TrackedState<si::Ratio>,
880    /// Maximum possible regeneration power going to ReversibleEnergyStorage.
881    pub pwr_mech_regen_max: TrackedState<si::Power>,
882    /// efficiency in backward direction at max possible input power from `FuelConverter` and `ReversibleEnergyStorage`
883    pub eff_at_max_regen: TrackedState<si::Ratio>,
884
885    // Current values
886    /// Raw power requirement from boundary conditions
887    pub pwr_out_req: TrackedState<si::Power>,
888    /// Integral of [Self::pwr_out_req]
889    pub energy_out_req: TrackedState<si::Energy>,
890    /// Electrical power to propulsion from ReversibleEnergyStorage and Generator.
891    /// negative value indicates regenerative braking
892    pub pwr_elec_prop_in: TrackedState<si::Power>,
893    /// Integral of [Self::pwr_elec_prop_in]
894    pub energy_elec_prop_in: TrackedState<si::Energy>,
895    /// Mechanical power to propulsion, corrected by efficiency, from ReversibleEnergyStorage and Generator.
896    /// Negative value indicates regenerative braking.
897    pub pwr_mech_prop_out: TrackedState<si::Power>,
898    /// Integral of [Self::pwr_mech_prop_out]
899    pub energy_mech_prop_out: TrackedState<si::Energy>,
900    /// Mechanical power from dynamic braking.  Positive value indicates braking; this should be zero otherwise.
901    pub pwr_mech_dyn_brake: TrackedState<si::Power>,
902    /// Integral of [Self::pwr_mech_dyn_brake]
903    pub energy_mech_dyn_brake: TrackedState<si::Energy>,
904    /// Electrical power from dynamic braking, dissipated as heat.
905    pub pwr_elec_dyn_brake: TrackedState<si::Power>,
906    /// Integral of [Self::pwr_elec_dyn_brake]
907    pub energy_elec_dyn_brake: TrackedState<si::Energy>,
908    /// Power lost in regeneratively converting mechanical power to power that can be absorbed by the battery.
909    pub pwr_loss: TrackedState<si::Power>,
910    /// Integral of [Self::pwr_loss]
911    pub energy_loss: TrackedState<si::Energy>,
912}
913
914#[pyo3_api]
915impl ElectricMachineState {}
916
917impl Init for ElectricMachineState {}
918impl SerdeAPI for ElectricMachineState {}