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 ConventionalVehicle {
10 pub fs: FuelStorage,
11 #[has_state]
12 pub fc: FuelConverter,
13 #[has_state]
14 pub transmission: Transmission,
15 pub(crate) mass: Option<si::Mass>,
16 pub alt_eff: si::Ratio,
18}
19
20#[pyo3_api]
21impl ConventionalVehicle {}
22
23impl SerdeAPI for ConventionalVehicle {}
24impl Init for ConventionalVehicle {
25 fn init(&mut self) -> Result<(), Error> {
26 self.fc
27 .init()
28 .map_err(|err| Error::InitError(format_dbg!(err)))?;
29 self.fs
30 .init()
31 .map_err(|err| Error::InitError(format_dbg!(err)))?;
32 self.transmission
33 .init()
34 .map_err(|err| Error::InitError(format_dbg!(err)))?;
35 Ok(())
36 }
37}
38
39impl HistoryMethods for ConventionalVehicle {
40 fn save_interval(&self) -> anyhow::Result<Option<usize>> {
41 bail!("`save_interval` is not implemented in ConventionalVehicle")
42 }
43 fn set_save_interval(&mut self, save_interval: Option<usize>) -> anyhow::Result<()> {
44 self.fc.set_save_interval(save_interval)?;
46 self.transmission.set_save_interval(save_interval)?;
47 Ok(())
48 }
49 fn clear(&mut self) {
50 self.fc.clear();
51 self.transmission.clear();
52 }
53}
54
55impl Powertrain for Box<ConventionalVehicle> {
56 fn set_curr_pwr_prop_out_max(
57 &mut self,
58 _pwr_upstream: (si::Power, si::Power),
59 pwr_aux: si::Power,
60 dt: si::Time,
61 _veh_state: &VehicleState,
62 ) -> anyhow::Result<()> {
63 self.fc
65 .set_curr_pwr_out_max(dt)
66 .with_context(|| anyhow!(format_dbg!()))?;
67 self.fc
68 .set_curr_pwr_prop_max(pwr_aux / self.alt_eff)
69 .with_context(|| anyhow!(format_dbg!()))?;
70 self.transmission
71 .set_curr_pwr_prop_out_max(
72 (
73 *self.fc.state.pwr_prop_max.get_fresh(|| format_dbg!())?,
74 si::Power::ZERO,
75 ),
76 f64::NAN * uc::W,
77 dt,
78 _veh_state,
79 )
80 .with_context(|| format_dbg!())?;
81 Ok(())
82 }
83
84 fn get_curr_pwr_prop_out_max(&self) -> anyhow::Result<(si::Power, si::Power)> {
85 self.transmission
86 .get_curr_pwr_prop_out_max()
87 .with_context(|| format_dbg!())
88 }
89
90 fn solve(
91 &mut self,
92 pwr_out_req: si::Power,
93 _enabled: bool,
94 dt: si::Time,
95 ) -> anyhow::Result<Option<si::Power>> {
96 ensure!(pwr_out_req >= si::Power::ZERO, format_dbg!());
98 ensure!(almost_le_uom(
99 &pwr_out_req,
100 self.transmission
101 .state
102 .pwr_out_fwd_max
103 .get_fresh(|| format_dbg!())?,
104 None
105 ));
106 ensure!(almost_le_uom(
107 &pwr_out_req,
108 self.transmission
109 .state
110 .pwr_out_fwd_max
111 .get_fresh(|| format_dbg!())?,
112 None
113 ));
114 let enabled = true; let pwr_in_transmission = self
116 .transmission
117 .solve(pwr_out_req, true, dt)
118 .with_context(|| format_dbg!())?
119 .with_context(|| format!("{}\nExpected `Some`", format_dbg!()))?;
120 self.fc
121 .solve(pwr_in_transmission, enabled, dt)
122 .with_context(|| anyhow!(format_dbg!()))?;
123 Ok(None)
124 }
125
126 fn pwr_regen(&self) -> anyhow::Result<si::Power> {
127 Ok(si::Power::ZERO)
128 }
129}
130
131impl ConventionalVehicle {
132 pub fn solve_thermal(
133 &mut self,
134 te_amb: si::Temperature,
135 pwr_thrml_fc_to_cab: Option<si::Power>,
136 veh_state: &mut VehicleState,
137 dt: si::Time,
138 ) -> anyhow::Result<()> {
139 self.fc
140 .solve_thermal(te_amb, pwr_thrml_fc_to_cab, veh_state, dt)
141 }
142}
143
144impl TryFrom<&fastsim_2::vehicle::RustVehicle> for ConventionalVehicle {
145 type Error = anyhow::Error;
146 fn try_from(f2veh: &fastsim_2::vehicle::RustVehicle) -> anyhow::Result<ConventionalVehicle> {
147 let conv = ConventionalVehicle {
148 fs: {
149 let mut fs = FuelStorage {
150 pwr_out_max: f2veh.fs_max_kw * uc::KW,
151 pwr_ramp_lag: f2veh.fs_secs_to_peak_pwr * uc::S,
152 energy_capacity: f2veh.fs_kwh * uc::KWH,
153 specific_energy: Some(
154 super::vehicle_model::FUEL_LHV_MJ_PER_KG * uc::MJ / uc::KG,
155 ),
156 mass: None,
157 };
158 fs.set_mass(None, MassSideEffect::None)
159 .with_context(|| anyhow!(format_dbg!()))?;
160 fs
161 },
162 fc: FuelConverter::try_from(f2veh.clone())?,
163 transmission: Transmission::try_from(f2veh.clone())?,
164 mass: None,
165 alt_eff: f2veh.alt_eff * uc::R,
166 };
167 Ok(conv)
168 }
169}
170
171impl Mass for ConventionalVehicle {
172 fn mass(&self) -> anyhow::Result<Option<si::Mass>> {
173 let derived_mass = self
174 .derived_mass()
175 .with_context(|| anyhow!(format_dbg!()))?;
176 match (derived_mass, self.mass) {
177 (Some(derived_mass), Some(set_mass)) => {
178 ensure!(
179 utils::almost_eq_uom(&set_mass, &derived_mass, None),
180 format!(
181 "{}",
182 format_dbg!(utils::almost_eq_uom(&set_mass, &derived_mass, None)),
183 )
184 );
185 Ok(Some(set_mass))
186 }
187 _ => Ok(self.mass.or(derived_mass)),
188 }
189 }
190
191 fn set_mass(
192 &mut self,
193 new_mass: Option<si::Mass>,
194 side_effect: MassSideEffect,
195 ) -> anyhow::Result<()> {
196 ensure!(
197 side_effect == MassSideEffect::None,
198 "At the powertrain level, only `MassSideEffect::None` is allowed"
199 );
200 let derived_mass = self
201 .derived_mass()
202 .with_context(|| anyhow!(format_dbg!()))?;
203 self.mass = match new_mass {
204 Some(new_mass) => {
206 if let Some(dm) = derived_mass {
207 if dm != new_mass {
208 self.expunge_mass_fields();
209 }
210 }
211 Some(new_mass)
212 }
213 None => Some(derived_mass.with_context(|| {
215 format!(
216 "Not all mass fields in `{}` are set and no mass was provided.",
217 stringify!(ConventionalVehicle)
218 )
219 })?),
220 };
221 Ok(())
222 }
223
224 fn derived_mass(&self) -> anyhow::Result<Option<si::Mass>> {
225 let fc_mass = self.fc.mass().with_context(|| anyhow!(format_dbg!()))?;
226 let fs_mass = self.fs.mass().with_context(|| anyhow!(format_dbg!()))?;
227 let transmission_mass = self
228 .transmission
229 .mass()
230 .with_context(|| anyhow!(format_dbg!()))?;
231 match (fc_mass, fs_mass, transmission_mass) {
232 (Some(fc_mass), Some(fs_mass), Some(transmission_mass)) => {
233 Ok(Some(fc_mass + fs_mass + transmission_mass))
234 }
235 (None, None, None) => Ok(None),
236 _ => bail!(
237 "`{}` field masses are not consistently set to `Some` or `None`",
238 stringify!(ConventionalVehicle)
239 ),
240 }
241 }
242
243 fn expunge_mass_fields(&mut self) {
244 self.fc.expunge_mass_fields();
245 self.fs.expunge_mass_fields();
246 self.transmission.expunge_mass_fields();
247 self.mass = None;
248 }
249}