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