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 Init for BatteryElectricVehicle {
23 fn init(&mut self) -> Result<(), Error> {
24 self.res
25 .init()
26 .map_err(|err| Error::InitError(format_dbg!(err)))?;
27 self.em
28 .init()
29 .map_err(|err| Error::InitError(format_dbg!(err)))?;
30 self.transmission
31 .init()
32 .map_err(|err| Error::InitError(format_dbg!(err)))?;
33 Ok(())
34 }
35}
36
37impl SerdeAPI for BatteryElectricVehicle {}
38
39impl Mass for BatteryElectricVehicle {
40 fn mass(&self) -> anyhow::Result<Option<si::Mass>> {
41 let derived_mass = self
42 .derived_mass()
43 .with_context(|| anyhow!(format_dbg!()))?;
44 match (derived_mass, self.mass) {
45 (Some(derived_mass), Some(set_mass)) => {
46 ensure!(
47 utils::almost_eq_uom(&set_mass, &derived_mass, None),
48 format!(
49 "{}",
50 format_dbg!(utils::almost_eq_uom(&set_mass, &derived_mass, None)),
51 )
52 );
53 Ok(Some(set_mass))
54 }
55 _ => Ok(self.mass.or(derived_mass)),
56 }
57 }
58
59 fn set_mass(
60 &mut self,
61 new_mass: Option<si::Mass>,
62 side_effect: MassSideEffect,
63 ) -> anyhow::Result<()> {
64 ensure!(
65 side_effect == MassSideEffect::None,
66 "At the powertrain level, only `MassSideEffect::None` is allowed"
67 );
68 let derived_mass = self
69 .derived_mass()
70 .with_context(|| anyhow!(format_dbg!()))?;
71 self.mass = match new_mass {
72 Some(new_mass) => {
74 if let Some(dm) = derived_mass {
75 if dm != new_mass {
76 self.expunge_mass_fields();
77 }
78 }
79 Some(new_mass)
80 }
81 None => Some(derived_mass.with_context(|| {
83 format!(
84 "Not all mass fields in `{}` are set and no mass was provided.",
85 stringify!(BatteryElectricVehicle)
86 )
87 })?),
88 };
89 Ok(())
90 }
91
92 fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
93 let res_mass = self.res.mass().with_context(|| anyhow!(format_dbg!()))?;
94 let em_mass = self.em.mass().with_context(|| anyhow!(format_dbg!()))?;
95 let transmission_mass = self
96 .transmission
97 .mass()
98 .with_context(|| anyhow!(format_dbg!()))?;
99 match (res_mass, em_mass, transmission_mass) {
100 (Some(res_mass), Some(em_mass), Some(transmission_mass)) => {
101 Ok(Some(em_mass + res_mass + transmission_mass))
102 }
103 (None, None, None) => Ok(None),
104 _ => bail!(
105 "`{}` field masses are not consistently set to `Some` or `None`",
106 stringify!(BatteryElectricVehicle)
107 ),
108 }
109 }
110
111 fn expunge_mass_fields(&mut self) {
112 self.res.expunge_mass_fields();
113 self.em.expunge_mass_fields();
114 self.transmission.expunge_mass_fields();
115 self.mass = None;
116 }
117}
118
119impl HistoryMethods for BatteryElectricVehicle {
120 fn save_interval(&self) -> anyhow::Result<Option<usize>> {
121 bail!("`save_interval` is not implemented in BatteryElectricVehicle")
122 }
123 fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
124 self.res.set_save_interval(save_interval)?;
125 self.em.set_save_interval(save_interval)?;
126 self.transmission.set_save_interval(save_interval)?;
127 Ok(())
128 }
129 fn clear(&mut self) {
130 self.res.clear();
131 self.em.clear();
132 self.transmission.clear();
133 }
134}
135
136impl Powertrain for BatteryElectricVehicle {
137 fn solve(
138 &mut self,
139 pwr_out_req: si::Power,
140 _enabled: bool,
141 dt: si::Time,
142 ) -> anyhow::Result<Option<si::Power>> {
143 let pwr_in_transmission = self
144 .transmission
145 .solve(pwr_out_req, true, dt)
146 .with_context(|| format_dbg!())?
147 .with_context(|| format!("{}\nExpected `Some`", format_dbg!()))?;
148 let pwr_in_em = self
149 .em
150 .solve(pwr_in_transmission, true, dt)
151 .with_context(|| {
152 format!(
153 "{}\ntransmission `pwr_out_req`: {} kW\n`self.transmission.state.pwr_out_fwd_max`: {} kW",
154 format_dbg!(),
155 pwr_out_req.get::<si::kilowatt>().format_eng(None),
156 self.transmission
157 .state
158 .pwr_out_fwd_max
159 .get_fresh(|| format_dbg!())
160 .unwrap()
161 .get::<si::kilowatt>()
162 .format_eng(None)
163 )
164 })?
165 .with_context(|| format!("{}\nExpected `Some`", format_dbg!()))?;
166 self.res
167 .solve(pwr_in_em, dt)
168 .with_context(|| format_dbg!())?;
169 Ok(None)
170 }
171
172 fn get_curr_pwr_prop_out_max(&self) -> anyhow::Result<(si::Power, si::Power)> {
173 self.transmission
174 .get_curr_pwr_prop_out_max()
175 .with_context(|| format_dbg!())
176 }
177
178 fn set_curr_pwr_prop_out_max(
179 &mut self,
180 _pwr_upstream: (si::Power, si::Power),
181 pwr_aux: si::Power,
182 dt: si::Time,
183 _veh_state: &VehicleState,
184 ) -> anyhow::Result<()> {
185 let disch_buffer = si::Energy::ZERO;
187 let chrg_buffer = si::Energy::ZERO;
188 self.res
189 .set_curr_pwr_out_max(dt, disch_buffer, chrg_buffer)
190 .with_context(|| anyhow!(format_dbg!()))?;
191 self.res
192 .set_curr_pwr_prop_max(pwr_aux)
193 .with_context(|| anyhow!(format_dbg!()))?;
194 self.em
195 .set_curr_pwr_prop_out_max(
196 self.res
197 .get_curr_pwr_prop_out_max()
198 .with_context(|| format_dbg!())?,
199 f64::NAN * uc::W,
200 dt,
201 _veh_state,
202 )
203 .with_context(|| anyhow!(format_dbg!()))?;
204 self.transmission
205 .set_curr_pwr_prop_out_max(
206 self.em
207 .get_curr_pwr_prop_out_max()
208 .with_context(|| format_dbg!())?,
209 f64::NAN * uc::W,
210 dt,
211 _veh_state,
212 )
213 .with_context(|| anyhow!(format_dbg!()))?;
214
215 Ok(())
216 }
217
218 fn pwr_regen(&self) -> anyhow::Result<si::Power> {
220 self.transmission.pwr_regen().with_context(|| format_dbg!())
224 }
225}
226
227impl BatteryElectricVehicle {
228 pub fn solve_thermal(
235 &mut self,
236 te_amb: si::Temperature,
237 pwr_thrml_hvac_to_res: si::Power,
238 te_cab: Option<si::Temperature>,
239 dt: si::Time,
240 ) -> anyhow::Result<()> {
241 self.res
242 .solve_thermal(te_amb, pwr_thrml_hvac_to_res, te_cab, dt)
243 .with_context(|| format_dbg!())?;
244 Ok(())
245 }
246}
247
248impl TryFrom<&fastsim_2::vehicle::RustVehicle> for BatteryElectricVehicle {
249 type Error = anyhow::Error;
250 fn try_from(f2veh: &fastsim_2::vehicle::RustVehicle) -> anyhow::Result<BatteryElectricVehicle> {
251 let bev = BatteryElectricVehicle {
252 res: ReversibleEnergyStorage::try_from(f2veh.clone()).with_context(|| format_dbg!())?,
253 em: ElectricMachine {
254 state: Default::default(),
255 eff_interp_achieved: InterpolatorEnum::new_1d(
256 f2veh.mc_pwr_out_perc.clone(),
257 f2veh.mc_eff_array.clone(),
258 strategy::Linear,
259 Extrapolate::Error,
260 )?,
261 eff_interp_at_max_input: Some(InterpolatorEnum::new_1d(
262 f2veh
265 .mc_pwr_out_perc
266 .iter()
267 .zip(f2veh.mc_eff_array.iter())
268 .map(|(x, y)| x / y)
269 .collect(),
270 f2veh.mc_eff_array.clone(),
271 strategy::Linear,
272 Extrapolate::Error,
273 )?),
274 pwr_out_max: f2veh.mc_max_kw * uc::KW,
275 specific_pwr: None,
276 mass: None,
277 save_interval: Some(1),
278 history: Default::default(),
279 },
280 transmission: Transmission::try_from(f2veh.clone())?,
281 mass: None,
282 };
283 Ok(bev)
284 }
285}