1use super::*;
2
3#[serde_api]
4#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, StateMethods, SetCumulative)]
5#[non_exhaustive]
6#[serde(deny_unknown_fields)]
7#[cfg_attr(feature = "pyo3", pyclass(module = "fastsim", subclass, eq))]
8pub struct BatteryElectricVehicle {
10 #[has_state]
11 pub res: ReversibleEnergyStorage,
12 #[has_state]
13 pub em: ElectricMachine,
14 #[has_state]
15 pub transmission: Transmission,
16 pub(crate) mass: Option<si::Mass>,
17}
18
19#[pyo3_api]
20impl BatteryElectricVehicle {}
21
22impl BatteryElectricVehicle {
23 pub fn new(
24 res: ReversibleEnergyStorage,
25 em: ElectricMachine,
26 transmission: Transmission,
27 mass: Option<si::Mass>,
28 ) -> anyhow::Result<Self> {
29 let mut bev = Self {
30 res,
31 em,
32 transmission,
33 mass,
34 };
35 bev.init()?;
36 Ok(bev)
37 }
38}
39
40impl Init for BatteryElectricVehicle {
41 fn init(&mut self) -> Result<(), Error> {
42 self.res
43 .init()
44 .map_err(|err| Error::InitError(format_dbg!(err)))?;
45 self.em
46 .init()
47 .map_err(|err| Error::InitError(format_dbg!(err)))?;
48 self.transmission
49 .init()
50 .map_err(|err| Error::InitError(format_dbg!(err)))?;
51 Ok(())
52 }
53}
54
55impl SerdeAPI for BatteryElectricVehicle {}
56
57impl Mass for BatteryElectricVehicle {
58 fn mass(&self) -> anyhow::Result<Option<si::Mass>> {
59 let derived_mass = self
60 .derived_mass()
61 .with_context(|| anyhow!(format_dbg!()))?;
62 match (derived_mass, self.mass) {
63 (Some(derived_mass), Some(set_mass)) => {
64 ensure!(
65 utils::almost_eq_uom(&set_mass, &derived_mass, None),
66 format!(
67 "{}",
68 format_dbg!(utils::almost_eq_uom(&set_mass, &derived_mass, None)),
69 )
70 );
71 Ok(Some(set_mass))
72 }
73 _ => Ok(self.mass.or(derived_mass)),
74 }
75 }
76
77 fn set_mass(
78 &mut self,
79 new_mass: Option<si::Mass>,
80 side_effect: MassSideEffect,
81 ) -> anyhow::Result<()> {
82 ensure!(
83 side_effect == MassSideEffect::None,
84 "At the powertrain level, only `MassSideEffect::None` is allowed"
85 );
86 let derived_mass = self
87 .derived_mass()
88 .with_context(|| anyhow!(format_dbg!()))?;
89 self.mass = match (new_mass, derived_mass) {
90 (Some(new_mass), Some(dm)) => {
92 if dm != new_mass {
93 self.expunge_mass_fields();
94 }
95 Some(new_mass)
96 }
97 (Some(new_mass), None) => Some(new_mass),
98 (None, Some(dm)) => Some(dm),
99 (None, None) => bail!(
100 "Not all mass fields in `{}` are set and no mass was provided.",
101 stringify!(BatteryElectricVehicle)
102 ),
103 };
104 ensure!(
105 self.mass > Some(0.0 * uc::KG),
106 "{} mass must be positive",
107 stringify!(BatteryElectricVehicle)
108 );
109 Ok(())
110 }
111
112 fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
113 let res_mass = self.res.mass().with_context(|| anyhow!(format_dbg!()))?;
114 let em_mass = self.em.mass().with_context(|| anyhow!(format_dbg!()))?;
115 let transmission_mass = self
116 .transmission
117 .mass()
118 .with_context(|| anyhow!(format_dbg!()))?;
119 match (res_mass, em_mass, transmission_mass) {
120 (Some(res_mass), Some(em_mass), Some(transmission_mass)) => {
121 Ok(Some(em_mass + res_mass + transmission_mass))
122 }
123 (None, None, None) => Ok(None),
124 _ => bail!(
125 "`{}` field masses are not consistently set to `Some` or `None`",
126 stringify!(BatteryElectricVehicle)
127 ),
128 }
129 }
130
131 fn expunge_mass_fields(&mut self) {
132 self.res.expunge_mass_fields();
133 self.em.expunge_mass_fields();
134 self.transmission.expunge_mass_fields();
135 self.mass = None;
136 }
137}
138
139impl HistoryMethods for BatteryElectricVehicle {
140 fn save_interval(&self) -> anyhow::Result<Option<usize>> {
141 bail!("`save_interval` is not implemented in BatteryElectricVehicle")
142 }
143 fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
144 self.res.set_save_interval(save_interval)?;
145 self.em.set_save_interval(save_interval)?;
146 self.transmission.set_save_interval(save_interval)?;
147 Ok(())
148 }
149 fn clear(&mut self) {
150 self.res.clear();
151 self.em.clear();
152 self.transmission.clear();
153 }
154}
155
156impl Powertrain for BatteryElectricVehicle {
157 fn solve(
158 &mut self,
159 pwr_out_req: si::Power,
160 _enabled: bool,
161 dt: si::Time,
162 ) -> anyhow::Result<Option<si::Power>> {
163 let pwr_in_transmission = self
164 .transmission
165 .solve(pwr_out_req, true, dt)
166 .with_context(|| format_dbg!())?
167 .with_context(|| format!("{}\nExpected `Some`", format_dbg!()))?;
168 let pwr_in_em = self
169 .em
170 .solve(pwr_in_transmission, true, dt)
171 .map_err(|err| anyhow::anyhow!(
172 format!(
173 "error at line {}: \ntransmission `pwr_out_req`: {} kW\n`self.transmission.state.pwr_out_fwd_max`: {} kW \n with originating error [{}]",
174 format_dbg!(),
175 pwr_out_req.get::<si::kilowatt>().format_eng(None),
176 self.transmission
177 .state
178 .pwr_out_fwd_max
179 .get_fresh(|| format_dbg!())
180 .unwrap()
181 .get::<si::kilowatt>()
182 .format_eng(None), err)))?
183 .with_context(|| format!("{}\nExpected `Some`", format_dbg!()))?;
184 self.res
185 .solve(pwr_in_em, dt)
186 .with_context(|| format_dbg!())?;
187 Ok(None)
188 }
189
190 fn get_curr_pwr_prop_out_max(&self) -> anyhow::Result<(si::Power, si::Power)> {
191 self.transmission
192 .get_curr_pwr_prop_out_max()
193 .with_context(|| format_dbg!())
194 }
195
196 fn set_curr_pwr_prop_out_max(
197 &mut self,
198 _pwr_upstream: (si::Power, si::Power),
199 pwr_aux: si::Power,
200 dt: si::Time,
201 _veh_state: &VehicleState,
202 ) -> anyhow::Result<()> {
203 let disch_buffer = si::Energy::ZERO;
205 let chrg_buffer = si::Energy::ZERO;
206 self.res
207 .set_curr_pwr_out_max(dt, disch_buffer, chrg_buffer)
208 .with_context(|| anyhow!(format_dbg!()))?;
209 self.res
210 .set_curr_pwr_prop_max(pwr_aux)
211 .with_context(|| anyhow!(format_dbg!()))?;
212 self.em
213 .set_curr_pwr_prop_out_max(
214 self.res
215 .get_curr_pwr_prop_out_max()
216 .with_context(|| format_dbg!())?,
217 f64::NAN * uc::W,
218 dt,
219 _veh_state,
220 )
221 .with_context(|| anyhow!(format_dbg!()))?;
222 self.transmission
223 .set_curr_pwr_prop_out_max(
224 self.em
225 .get_curr_pwr_prop_out_max()
226 .with_context(|| format_dbg!())?,
227 f64::NAN * uc::W,
228 dt,
229 _veh_state,
230 )
231 .with_context(|| anyhow!(format_dbg!()))?;
232
233 Ok(())
234 }
235
236 fn pwr_regen(&self) -> anyhow::Result<si::Power> {
238 self.transmission.pwr_regen().with_context(|| format_dbg!())
242 }
243}
244
245impl BatteryElectricVehicle {
246 pub fn solve_thermal(
253 &mut self,
254 te_amb: si::Temperature,
255 pwr_thrml_hvac_to_res: si::Power,
256 te_cab: Option<si::Temperature>,
257 dt: si::Time,
258 ) -> anyhow::Result<()> {
259 self.res
260 .solve_thermal(te_amb, pwr_thrml_hvac_to_res, te_cab, dt)
261 .with_context(|| format_dbg!())?;
262 Ok(())
263 }
264}