Skip to main content

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