1use 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))]
14pub struct ElectricMachine {
17 pub eff_interp_achieved: Interpolator,
21 pub eff_interp_at_max_input: Option<Interpolator>,
26 pub pwr_out_max: si::Power,
32 pub specific_pwr: Option<si::SpecificPower>,
34 pub(in super::super) mass: Option<si::Mass>,
36 pub save_interval: Option<usize>,
38 #[serde(default)]
40 pub state: ElectricMachineState,
41 #[serde(
43 default,
44 skip_serializing_if = "ElectricMachineStateHistoryVec::is_empty"
45 )]
46 pub history: ElectricMachineStateHistoryVec,
47}
48
49#[named_struct_pyo3_api]
50impl ElectricMachine {
51 #[getter("eff_fwd_max")]
73 fn get_eff_max_fwd_py(&self) -> PyResult<f64> {
74 Ok(self.get_eff_fwd_max()?)
75 }
76
77 #[setter("__eff_fwd_max")]
78 fn set_eff_fwd_max_py(&mut self, eff_max: f64) -> PyResult<()> {
79 self.set_eff_fwd_max(eff_max)?;
80 Ok(())
81 }
82
83 #[getter("eff_min_fwd")]
84 fn get_eff_min_fwd_py(&self) -> PyResult<f64> {
85 Ok(self.get_eff_min_fwd()?)
86 }
87
88 #[getter("eff_fwd_range")]
89 fn get_eff_fwd_range_py(&self) -> PyResult<f64> {
90 Ok(self.get_eff_fwd_range()?)
91 }
92
93 #[setter("__eff_fwd_range")]
94 fn set_eff_fwd_range_py(&mut self, eff_range: f64) -> PyResult<()> {
95 self.set_eff_fwd_range(eff_range)?;
96 Ok(())
97 }
98}
99
100impl ElectricMachine {
101 pub fn set_curr_pwr_prop_out_max(
114 &mut self,
115 pwr_in_fwd_lim: si::Power,
116 pwr_in_bwd_lim: si::Power,
117 _dt: si::Time,
118 ) -> anyhow::Result<()> {
119 ensure!(
120 pwr_in_fwd_lim >= si::Power::ZERO,
121 "`{}` ({} W) must be greater than or equal to zero for `{}`",
122 stringify!(pwr_in_fwd_lim),
123 pwr_in_fwd_lim.get::<si::watt>().format_eng(None),
124 stringify!(ElectricMachine::get_curr_pwr_prop_out_max)
125 );
126 ensure!(
127 pwr_in_bwd_lim >= si::Power::ZERO,
128 "`{}` ({} W) must be greater than or equal to zero for `{}`",
129 stringify!(pwr_in_bwd_lim),
130 pwr_in_bwd_lim.get::<si::watt>().format_eng(None),
131 stringify!(ElectricMachine::get_curr_pwr_prop_out_max)
132 );
133
134 self.eff_interp_at_max_input
137 .as_mut()
138 .with_context(|| {
139 "eff_interp_bwd is None, which should never be the case at this point."
140 })?
141 .set_extrapolate(Extrapolate::Clamp)?;
142
143 self.state.eff_fwd_at_max_input.update(
144 uc::R
145 * self
146 .eff_interp_at_max_input
147 .as_ref()
148 .map(|interpolator| {
149 interpolator
150 .interpolate(&[abs_checked_x_val(
151 (pwr_in_fwd_lim / self.pwr_out_max).get::<si::ratio>(),
152 interpolator.x().map_err(|e| anyhow!(e))?,
153 )?])
154 .map_err(|e| anyhow!(e))
155 })
156 .ok_or(anyhow!(
157 "eff_interp_bwd is None, which should never be the case at this point."
158 ))?
159 .with_context(|| {
160 anyhow!(
161 "{}\n failed to calculate {}",
162 format_dbg!(),
163 stringify!(eff_pos)
164 )
165 })?,
166 || format_dbg!(),
167 )?;
168 self.state.eff_at_max_regen.update(
169 uc::R
170 * self
171 .eff_interp_at_max_input
172 .as_ref()
173 .map(|interpolator| {
174 interpolator
175 .interpolate(&[abs_checked_x_val(
176 (pwr_in_bwd_lim / self.pwr_out_max).get::<si::ratio>(),
177 interpolator.x().map_err(|e| anyhow!(e))?,
178 )?])
179 .map_err(|e| anyhow!(e))
180 })
181 .ok_or(anyhow!(
182 "eff_interp_bwd is None, which should never be the case at this point."
183 ))?
184 .with_context(|| {
185 anyhow!(
186 "{}\n failed to calculate {}",
187 format_dbg!(),
188 stringify!(eff_neg)
189 )
190 })?,
191 || format_dbg!(),
192 )?;
193
194 self.state.pwr_mech_fwd_out_max.update(
197 self.pwr_out_max.min(
198 pwr_in_fwd_lim
199 * *self
200 .state
201 .eff_fwd_at_max_input
202 .get_fresh(|| format_dbg!())?,
203 ),
204 || format_dbg!(),
205 )?;
206 self.state.pwr_mech_regen_max.update(
209 self.pwr_out_max
210 .min(pwr_in_bwd_lim / *self.state.eff_at_max_regen.get_fresh(|| format_dbg!())?),
211 || format_dbg!(),
212 )?;
213 Ok(())
214 }
215
216 pub fn get_pwr_in_req(
221 &mut self,
222 pwr_out_req: si::Power,
223 _dt: si::Time,
224 ) -> anyhow::Result<si::Power> {
225 ensure!(
227 pwr_out_req.abs() <= self.pwr_out_max,
228 format!(
229 "{}\nedrv required power ({} kW) exceeds static max power ({} kW)",
230 format_dbg!(pwr_out_req.abs() <= self.pwr_out_max),
231 pwr_out_req.get::<si::kilowatt>().format_eng(Some(9)),
232 self.pwr_out_max.get::<si::kilowatt>().format_eng(Some(9))
233 ),
234 );
235 ensure!(
236 almost_le_uom(&pwr_out_req , self.state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?, None),
237 format!(
238 "{}\nedrv required propulsion power ({} kW) exceeds current max propulsion power ({} kW) by {} kW",
239 format_dbg!(pwr_out_req <= *self.state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?),
240 pwr_out_req.get::<si::kilowatt>().format_eng(Some(6)),
241 self.state
242 .pwr_mech_fwd_out_max
243 .get_fresh(|| format_dbg!())?
244 .get::<si::kilowatt>()
245 .format_eng(Some(6)),
246 (pwr_out_req - *self.state.pwr_mech_fwd_out_max.get_fresh(|| format_dbg!())?).get::<si::kilowatt>().format_eng(Some(6))
247 ),
248 );
249 if pwr_out_req < si::Power::ZERO {
250 ensure!(
251 almost_le_uom(
252 &pwr_out_req.abs(),
253 self.state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?,
254 None
255 ),
256 format!(
257 "{}\nedrv charge power ({:.6} kW) exceeds current max charge power ({:.6} kW)",
258 format_dbg!(),
259 -pwr_out_req.get::<si::kilowatt>(),
260 self.state
261 .pwr_mech_regen_max
262 .get_fresh(|| format_dbg!())?
263 .get::<si::kilowatt>()
264 ),
265 );
266 }
267
268 self.state
269 .pwr_out_req
270 .update(pwr_out_req, || format_dbg!())?;
271
272 self.eff_interp_achieved
274 .set_extrapolate(Extrapolate::Error)?;
275
276 self.state.eff.update(
277 uc::R
278 * self
279 .eff_interp_achieved
280 .interpolate(
281 &[{
282 let pwr = |pwr_uncorrected: f64| -> anyhow::Result<f64> {
283 Ok({
284 if self
285 .eff_interp_achieved
286 .x()?
287 .first()
288 .with_context(|| anyhow!(format_dbg!()))?
289 >= &0.
290 {
291 pwr_uncorrected.max(0.)
292 } else {
293 pwr_uncorrected
294 }
295 })
296 };
297 pwr((pwr_out_req / self.pwr_out_max).get::<si::ratio>())?
298 }], )
302 .with_context(|| {
303 anyhow!(
304 "{}\n failed to calculate {}",
305 format_dbg!(),
306 stringify!(self.state.eff)
307 )
308 })?,
309 || format_dbg!(),
310 )?;
311
312 self.state.pwr_mech_prop_out.update(
315 pwr_out_req.max(-*self.state.pwr_mech_regen_max.get_fresh(|| format_dbg!())?),
316 || format_dbg!(),
317 )?;
318
319 self.state.pwr_mech_dyn_brake.update(
320 -(pwr_out_req - *self.state.pwr_mech_prop_out.get_fresh(|| format_dbg!())?),
321 || format_dbg!(),
322 )?;
323 ensure!(
324 *self.state.pwr_mech_dyn_brake.get_fresh(|| format_dbg!())? >= si::Power::ZERO,
325 "Mech Dynamic Brake Power cannot be below 0.0"
326 );
327
328 self.state.pwr_elec_prop_in.update(
330 if pwr_out_req > si::Power::ZERO {
331 *self.state.pwr_mech_prop_out.get_fresh(|| format_dbg!())?
332 / *self.state.eff.get_fresh(|| format_dbg!())?
333 } else {
334 *self.state.pwr_mech_prop_out.get_fresh(|| format_dbg!())?
335 * *self.state.eff.get_fresh(|| format_dbg!())?
336 },
337 || format_dbg!(),
338 )?;
339
340 self.state.pwr_elec_dyn_brake.update(
341 *self.state.pwr_mech_dyn_brake.get_fresh(|| format_dbg!())?
342 * *self.state.eff.get_fresh(|| format_dbg!())?,
343 || format_dbg!(),
344 )?;
345
346 self.state.pwr_loss.update(
348 (*self.state.pwr_mech_prop_out.get_fresh(|| format_dbg!())?
349 - *self.state.pwr_elec_prop_in.get_fresh(|| format_dbg!())?)
350 .abs(),
351 || format_dbg!(),
352 )?;
353
354 Ok(*self.state.pwr_elec_prop_in.get_fresh(|| format_dbg!())?)
355 }
356}
357
358impl SerdeAPI for ElectricMachine {}
359impl Init for ElectricMachine {
360 fn init(&mut self) -> Result<(), Error> {
361 let _ = self
362 .mass()
363 .map_err(|err| Error::InitError(format_dbg!(err)))?;
364 let _ = check_interp_frac_data(self.eff_interp_achieved.x()?, InterpRange::Either)
365 .map_err(|err|
366 Error::InitError(format!(
367 "{}\nInvalid values for `ElectricMachine::pwr_out_frac_interp`; must range from [-1..1] or [0..1].",
368 format_dbg!(err)
369 )
370 ))?;
371 self.state
372 .init()
373 .map_err(|err| Error::InitError(format_dbg!(err)))?;
374 let eff_interp_at_max_input = Interpolator::new_1d(
377 self.eff_interp_achieved
378 .x()?
379 .iter()
380 .zip(self.eff_interp_achieved.f_x()?)
381 .map(|(x, y)| x / y)
382 .collect(),
383 self.eff_interp_achieved.f_x()?.to_owned(),
384 self.eff_interp_achieved.strategy()?.to_owned(),
388 self.eff_interp_achieved.extrapolate()?.to_owned(),
389 )
390 .map_err(ninterp::error::Error::from)?;
391 self.eff_interp_at_max_input = Some(eff_interp_at_max_input);
392 Ok(())
393 }
394}
395impl HistoryMethods for ElectricMachine {
396 fn save_interval(&self) -> anyhow::Result<Option<usize>> {
397 Ok(self.save_interval)
398 }
399 fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
400 self.save_interval = save_interval;
401 Ok(())
402 }
403 fn clear(&mut self) {
404 self.history.clear();
405 }
406}
407
408impl Mass for ElectricMachine {
409 fn mass(&self) -> anyhow::Result<Option<si::Mass>> {
410 let derived_mass = self
411 .derived_mass()
412 .with_context(|| anyhow!(format_dbg!()))?;
413 if let (Some(derived_mass), Some(set_mass)) = (derived_mass, self.mass) {
414 ensure!(
415 utils::almost_eq_uom(&set_mass, &derived_mass, None),
416 format!(
417 "{}",
418 format_dbg!(utils::almost_eq_uom(&set_mass, &derived_mass, None)),
419 )
420 );
421 }
422 Ok(self.mass)
423 }
424
425 fn set_mass(
426 &mut self,
427 new_mass: Option<si::Mass>,
428 side_effect: MassSideEffect,
429 ) -> anyhow::Result<()> {
430 let derived_mass = self
431 .derived_mass()
432 .with_context(|| anyhow!(format_dbg!()))?;
433 if let (Some(derived_mass), Some(new_mass)) = (derived_mass, new_mass) {
434 if derived_mass != new_mass {
435 match side_effect {
436 MassSideEffect::Extensive => {
437 self.pwr_out_max = self.specific_pwr.with_context(|| {
438 format!(
439 "{}\nExpected `self.specific_pwr` to be `Some`.",
440 format_dbg!()
441 )
442 })? * new_mass;
443 }
444 MassSideEffect::Intensive => {
445 self.specific_pwr = Some(self.pwr_out_max / new_mass);
446 }
447 MassSideEffect::None => {
448 self.specific_pwr = None;
449 }
450 }
451 }
452 } else if new_mass.is_none() {
453 self.specific_pwr = None;
454 }
455 self.mass = new_mass;
456 Ok(())
457 }
458
459 fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
460 Ok(self
461 .specific_pwr
462 .map(|specific_pwr| self.pwr_out_max / specific_pwr))
463 }
464
465 fn expunge_mass_fields(&mut self) {
466 self.specific_pwr = None;
467 self.mass = None;
468 }
469}
470
471impl ElectricMachine {
472 pub fn get_eff_fwd_max(&self) -> anyhow::Result<f64> {
474 Ok(self
476 .eff_interp_achieved
477 .f_x()?
478 .iter()
479 .fold(f64::NEG_INFINITY, |acc, curr| acc.max(*curr)))
480 }
481
482 pub fn get_eff_max_bwd(&self) -> anyhow::Result<f64> {
484 Ok(match self.eff_interp_at_max_input.as_ref() {
486 Some(interp) => interp
487 .f_x()?
488 .iter()
489 .fold(f64::NEG_INFINITY, |acc, curr| acc.max(*curr)),
490 None => bail!("eff_interp_bwd should be Some by this point."),
491 })
492 }
493
494 pub fn set_eff_fwd_max(&mut self, eff_max: f64) -> anyhow::Result<()> {
496 if (0.0..=1.0).contains(&eff_max) {
497 let old_max_fwd = self.get_eff_fwd_max()?;
498 let old_max_bwd = self.get_eff_max_bwd()?;
499 let f_x_fwd = self.eff_interp_achieved.f_x()?.to_owned();
500 match &mut self.eff_interp_achieved {
501 interp @ Interpolator::Interp1D(..) => {
502 interp.set_f_x(f_x_fwd.iter().map(|x| x * eff_max / old_max_fwd).collect())?;
503 }
504 _ => bail!("{}\n", "Only `Interpolator::Interp1D` is allowed."),
505 }
506 let f_x_bwd = self
507 .eff_interp_at_max_input
508 .as_ref()
509 .ok_or(anyhow!(
510 "eff_interp_bwd is None, which should never be the case at this point."
511 ))?
512 .f_x()?
513 .to_owned();
514 match &mut self.eff_interp_at_max_input {
515 Some(interp @ Interpolator::Interp1D(..)) => {
516 interp.set_f_x(
517 f_x_bwd
518 .iter()
519 .map(|x| x * eff_max / old_max_bwd)
520 .collect(),
521 )?;
522 }
523 _ => bail!("{}\n", "Only `Interpolator::Interp1D` is allowed. eff_interp_bwd should be Some by this point."),
524 }
525 Ok(())
526 } else {
527 Err(anyhow!(
528 "`eff_max` ({:.3}) must be between 0.0 and 1.0",
529 eff_max,
530 ))
531 }
532 }
533
534 pub fn get_eff_min_fwd(&self) -> anyhow::Result<f64> {
536 Ok(self
538 .eff_interp_achieved
539 .f_x()
540 .with_context(|| "eff_interp_fwd does not have f_x field")?
541 .iter()
542 .fold(f64::INFINITY, |acc, curr| acc.min(*curr)))
543 }
544
545 pub fn get_eff_min_at_max_input(&self) -> anyhow::Result<f64> {
547 Ok(self
549 .eff_interp_at_max_input
550 .as_ref()
551 .ok_or(anyhow!("eff_interp_bwd should be Some by this point."))?
552 .f_x()
553 .with_context(|| "eff_interp_bwd does not have f_x field")?
554 .iter()
555 .fold(f64::INFINITY, |acc, curr| acc.min(*curr)))
556 }
557
558 pub fn get_eff_fwd_range(&self) -> anyhow::Result<f64> {
560 Ok(self.get_eff_fwd_max()? - self.get_eff_min_fwd()?)
561 }
562
563 pub fn get_eff_range_bwd(&self) -> anyhow::Result<f64> {
565 Ok(self.get_eff_max_bwd()? - self.get_eff_min_at_max_input()?)
566 }
567
568 pub fn set_eff_fwd_range(&mut self, eff_range: f64) -> anyhow::Result<()> {
572 let eff_max_fwd = self.get_eff_fwd_max()?;
573 let eff_max_bwd = self.get_eff_max_bwd()?;
574 if eff_range == 0.0 {
575 let f_x_fwd = vec![
576 eff_max_fwd;
577 self.eff_interp_achieved
578 .f_x()
579 .with_context(|| "eff_interp_fwd does not have f_x field")?
580 .len()
581 ];
582 self.eff_interp_achieved.set_f_x(f_x_fwd)?;
583 let f_x_bwd = vec![
584 eff_max_bwd;
585 match &self.eff_interp_at_max_input {
586 Some(interp) => {
587 interp
588 .f_x()
589 .with_context(|| "eff_interp_bwd does not have f_x field")?
590 .len()
591 }
592 None => bail!("eff_interp_bwd should be Some by this point."),
593 }
594 ];
595 self.eff_interp_at_max_input
596 .as_mut()
597 .map(|interpolator| interpolator.set_f_x(f_x_bwd))
598 .transpose()?;
599 Ok(())
600 } else if (0.0..=1.0).contains(&eff_range) {
601 let old_min = self.get_eff_min_fwd()?;
602 let old_range = self.get_eff_fwd_max()? - old_min;
603 if old_range == 0.0 {
604 return Err(anyhow!(
605 "`eff_range` is already zero so it cannot be modified."
606 ));
607 }
608 let f_x_fwd = self.eff_interp_achieved.f_x()?.to_owned();
609 match &mut self.eff_interp_achieved {
610 interp @ Interpolator::Interp1D(..) => {
611 interp.set_f_x(
612 f_x_fwd
613 .iter()
614 .map(|x| eff_max_fwd + (x - eff_max_fwd) * eff_range / old_range)
615 .collect(),
616 )?;
617 }
618 _ => bail!("{}\n", "Only `Interpolator::Interp1D` is allowed."),
619 }
620 if self.get_eff_min_fwd()? < 0.0 {
621 let x_neg = self.get_eff_min_fwd()?;
622 let f_x_fwd = self.eff_interp_achieved.f_x()?.to_owned();
623 match &mut self.eff_interp_achieved {
624 interp @ Interpolator::Interp1D(..) => {
625 interp.set_f_x(f_x_fwd.iter().map(|x| x - x_neg).collect())?;
626 }
627 _ => bail!("{}\n", "Only `Interpolator::Interp1D` is allowed."),
628 }
629 }
630 if self.get_eff_fwd_max()? > 1.0 {
631 return Err(anyhow!(format!(
632 "`eff_max` ({:.3}) must be no greater than 1.0",
633 self.get_eff_fwd_max()?
634 )));
635 }
636 let old_min = self.get_eff_min_at_max_input()?;
637 let old_range = self.get_eff_max_bwd()? - old_min;
638 if old_range == 0.0 {
639 return Err(anyhow!(
640 "`eff_range` is already zero so it cannot be modified."
641 ));
642 }
643
644 let new_f_x: Vec<f64> = self
645 .eff_interp_at_max_input
646 .as_ref()
647 .ok_or(anyhow!("eff_interp_bwd should be Some by this point."))?
648 .f_x()?
649 .iter()
650 .map(|x| eff_max_bwd + (x - eff_max_bwd) * eff_range / old_range)
651 .collect();
652
653 self.eff_interp_at_max_input
654 .as_mut()
655 .map(|interpolator| interpolator.set_f_x(new_f_x))
656 .transpose()?;
657
658 if self.get_eff_min_at_max_input()? < 0.0 {
659 let x_neg = self.get_eff_min_at_max_input()?;
660 let new_f_x: Vec<f64> = self
661 .eff_interp_at_max_input
662 .as_ref()
663 .ok_or(anyhow!("eff_interp_bwd should be Some by this point."))?
664 .f_x()?
665 .iter()
666 .map(|x| x - x_neg)
667 .collect();
668 self.eff_interp_at_max_input
669 .as_mut()
670 .map(|interpolator| interpolator.set_f_x(new_f_x))
671 .transpose()?;
672 }
673 if self.get_eff_max_bwd()? > 1.0 {
674 return Err(anyhow!(format!(
675 "`eff_max` ({:.3}) must be no greater than 1.0",
676 self.get_eff_max_bwd()?
677 )));
678 }
679 Ok(())
680 } else {
681 Err(anyhow!(format!(
682 "`eff_range` ({:.3}) must be between 0.0 and 1.0",
683 eff_range,
684 )))
685 }
686 }
687}
688
689#[serde_api]
690#[derive(
691 Clone,
692 Debug,
693 Default,
694 Deserialize,
695 Serialize,
696 PartialEq,
697 HistoryVec,
698 StateMethods,
699 SetCumulative,
700)]
701#[non_exhaustive]
702#[serde(default)]
703#[serde(deny_unknown_fields)]
704#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
705
706pub struct ElectricMachineState {
707 pub i: TrackedState<usize>,
709 pub eff: TrackedState<si::Ratio>,
711 pub pwr_mech_fwd_out_max: TrackedState<si::Power>,
714 pub eff_fwd_at_max_input: TrackedState<si::Ratio>,
716 pub pwr_mech_regen_max: TrackedState<si::Power>,
718 pub eff_at_max_regen: TrackedState<si::Ratio>,
720
721 pub pwr_out_req: TrackedState<si::Power>,
724 pub energy_out_req: TrackedState<si::Energy>,
726 pub pwr_elec_prop_in: TrackedState<si::Power>,
729 pub energy_elec_prop_in: TrackedState<si::Energy>,
731 pub pwr_mech_prop_out: TrackedState<si::Power>,
734 pub energy_mech_prop_out: TrackedState<si::Energy>,
736 pub pwr_mech_dyn_brake: TrackedState<si::Power>,
738 pub energy_mech_dyn_brake: TrackedState<si::Energy>,
740 pub pwr_elec_dyn_brake: TrackedState<si::Power>,
742 pub energy_elec_dyn_brake: TrackedState<si::Energy>,
744 pub pwr_loss: TrackedState<si::Power>,
746 pub energy_loss: TrackedState<si::Energy>,
748}
749
750#[named_struct_pyo3_api]
751impl ElectricMachineState {}
752
753impl Init for ElectricMachineState {}
754impl SerdeAPI for ElectricMachineState {}