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