1use std::fmt;
56use std::sync::Arc;
57
58use crate::drag::{ComponentDrag, Drag, DragConditions};
59use crate::error::AeroError;
60use crate::model::{AeroModel, Flow};
61
62pub trait DragModel: fmt::Debug + Send + Sync {
78 fn zero_lift_drag(&self, query: &DragQuery<'_>) -> Result<f64, AeroError>;
89}
90
91#[derive(Clone, Copy)]
95pub struct DragQuery<'a> {
96 flow: &'a Flow,
97 conditions: &'a DragConditions,
98 model: &'a AeroModel,
99}
100
101impl<'a> DragQuery<'a> {
102 pub(crate) fn new(
105 flow: &'a Flow,
106 conditions: &'a DragConditions,
107 model: &'a AeroModel,
108 ) -> Self {
109 Self {
110 flow,
111 conditions,
112 model,
113 }
114 }
115
116 pub fn flow(&self) -> &Flow {
119 self.flow
120 }
121
122 pub fn mach(&self) -> f64 {
124 self.flow.mach
125 }
126
127 pub fn conditions(&self) -> &DragConditions {
131 self.conditions
132 }
133
134 pub fn reference_area_m2(&self) -> f64 {
136 self.model.reference_area_m2()
137 }
138
139 pub fn length_m(&self) -> f64 {
142 self.model.length_m()
143 }
144
145 pub fn buildup(&self) -> Result<Drag, AeroError> {
153 self.model.buildup_drag(self.flow, self.conditions)
154 }
155
156 pub fn buildup_components(&self) -> Result<Vec<ComponentDrag>, AeroError> {
163 self.model.buildup_components(self.flow, self.conditions)
164 }
165}
166
167impl fmt::Debug for DragQuery<'_> {
168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170 f.debug_struct("DragQuery")
171 .field("flow", self.flow)
172 .field("conditions", self.conditions)
173 .finish_non_exhaustive()
174 }
175}
176
177#[derive(Clone)]
181pub(crate) struct SharedDragModel(pub(crate) Arc<dyn DragModel>);
182
183impl PartialEq for SharedDragModel {
184 fn eq(&self, other: &Self) -> bool {
185 Arc::ptr_eq(&self.0, &other.0)
186 }
187}
188
189impl fmt::Debug for SharedDragModel {
190 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191 self.0.fmt(f)
192 }
193}
194
195impl serde::Serialize for SharedDragModel {
196 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
197 serializer.collect_str(&format_args!("{:?}", self.0))
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use std::sync::Mutex;
204
205 use super::*;
206 use crate::table::DragTable;
207 use crate::testing::finned_rocket;
208
209 fn model() -> AeroModel {
210 AeroModel::new(&finned_rocket(3).layout().unwrap()).unwrap()
211 }
212
213 #[derive(Debug)]
215 struct Buildup;
216
217 impl DragModel for Buildup {
218 fn zero_lift_drag(&self, query: &DragQuery<'_>) -> Result<f64, AeroError> {
219 Ok(query.buildup()?.zero_lift_coefficient)
220 }
221 }
222
223 #[derive(Debug)]
225 struct Constant(f64);
226
227 impl DragModel for Constant {
228 fn zero_lift_drag(&self, _query: &DragQuery<'_>) -> Result<f64, AeroError> {
229 Ok(self.0)
230 }
231 }
232
233 #[derive(Debug, Default)]
235 struct Spy(Mutex<Vec<DragConditions>>);
236
237 impl DragModel for Spy {
238 fn zero_lift_drag(&self, query: &DragQuery<'_>) -> Result<f64, AeroError> {
239 self.0.lock().unwrap().push(*query.conditions());
240 Ok(0.5)
241 }
242 }
243
244 #[test]
245 fn a_model_handing_back_the_buildup_changes_nothing() {
246 let own = model();
247 let custom = own.clone().with_drag_model(Buildup);
248 for mach in [0.0, 0.3, 0.95, 1.2, 2.5, 4.9] {
249 for alpha_deg in [0.0, 4.0, 17.0, 60.0, 120.0] {
250 let flow = Flow::new(mach, f64::to_radians(alpha_deg), 0.3);
251 for conditions in [
252 DragConditions::coasting(5.0e6),
253 DragConditions::thrusting(5.0e6, 0.0005),
254 ] {
255 assert_eq!(
256 custom.drag(&flow, &conditions).unwrap(),
257 Drag {
258 friction: 0.0,
259 pressure: 0.0,
260 base: 0.0,
261 parasitic: 0.0,
262 ..own.drag(&flow, &conditions).unwrap()
263 },
264 "Mach {mach}, {alpha_deg}°"
265 );
266 assert_eq!(
267 custom.buildup_drag(&flow, &conditions).unwrap(),
268 own.drag(&flow, &conditions).unwrap()
269 );
270 }
271 }
272 }
273 let past = Flow::axial(5.0);
276 let conditions = DragConditions::coasting(5.0e6);
277 let error = custom.drag(&past, &conditions).unwrap_err();
278 assert!(
279 matches!(&error, AeroError::DragModel { source }
280 if matches!(**source, AeroError::Mach { mach, .. } if mach == 5.0)),
281 "{error:?}"
282 );
283 }
284
285 #[test]
286 fn a_constant_model_flies_as_a_constant_table() {
287 let table = DragTable::from_csv("0,0.45\n1,0.45\n", None).unwrap();
288 let with_table = model().with_drag_table(table);
289 let with_model = model().with_drag_model(Constant(0.45));
290 for mach in [0.0, 0.5, 3.0, 7.0] {
291 for alpha_deg in [0.0, 10.0, 170.0] {
292 let flow = Flow::new(mach, f64::to_radians(alpha_deg), 0.0);
293 let conditions = DragConditions::coasting(1.0e6);
294 let from_table = with_table.drag(&flow, &conditions).unwrap();
295 let from_model = with_model.drag(&flow, &conditions).unwrap();
296 assert_eq!(
297 from_model.zero_lift_coefficient,
298 from_table.zero_lift_coefficient
299 );
300 assert_eq!(from_model.axial_coefficient, from_table.axial_coefficient);
301 assert!(from_model.table.is_none());
302 }
303 }
304 }
305
306 #[test]
310 fn a_drag_scale_multiplies_the_buildup_a_table_and_a_model() {
311 let table = DragTable::from_csv("0,0.45\n1,0.45\n", None).unwrap();
312 let conditions = DragConditions::thrusting(5.0e6, 0.0005);
313 let flow = Flow::new(0.6, f64::to_radians(7.0), 0.2);
314 for own in [
315 model(),
316 model().with_drag_table(table),
317 model().with_drag_model(Constant(0.7)),
318 ] {
319 let plain = own.drag(&flow, &conditions).unwrap();
320 assert_eq!(own.drag_scale(), 1.0);
321 assert_eq!(
322 own.clone()
323 .with_drag_scale(1.0)
324 .unwrap()
325 .drag(&flow, &conditions)
326 .unwrap(),
327 plain
328 );
329 let scaled_model = own.clone().with_drag_scale(1.25).unwrap();
330 assert_eq!(scaled_model.drag_scale(), 1.25);
331 let scaled = scaled_model.drag(&flow, &conditions).unwrap();
332 assert!(plain.zero_lift_coefficient > 0.0);
333 assert_eq!(
334 scaled.zero_lift_coefficient,
335 1.25 * plain.zero_lift_coefficient
336 );
337 assert_eq!(scaled.friction, 1.25 * plain.friction);
338 assert_eq!(scaled.pressure, 1.25 * plain.pressure);
339 assert_eq!(scaled.base, 1.25 * plain.base);
340 assert_eq!(scaled.parasitic, 1.25 * plain.parasitic);
341 let ratio = scaled.axial_coefficient / plain.axial_coefficient;
342 assert!((ratio - 1.25).abs() < 1e-15, "{ratio}");
343 assert_eq!(scaled.table, plain.table);
344 assert_eq!(
345 scaled_model.buildup_drag(&flow, &conditions).unwrap(),
346 own.buildup_drag(&flow, &conditions).unwrap()
347 );
348 }
349 for bad in [-0.1, f64::NAN, f64::INFINITY] {
350 let error = model().with_drag_scale(bad).unwrap_err();
351 assert!(
352 matches!(error, AeroError::Domain { what: "drag scale", value }
353 if value.to_bits() == bad.to_bits()),
354 "{error:?}"
355 );
356 }
357 }
358
359 #[test]
360 fn a_model_and_a_table_replace_each_other() {
361 let table = DragTable::from_csv("0,0.45\n1,0.45\n", None).unwrap();
362 let model_last = model()
363 .with_drag_table(table.clone())
364 .with_drag_model(Constant(0.7));
365 assert!(model_last.drag_table().is_none() && model_last.drag_model().is_some());
366 let table_last = model()
367 .with_drag_model(Constant(0.7))
368 .with_drag_table(table);
369 assert!(table_last.drag_model().is_none() && table_last.drag_table().is_some());
370 let conditions = DragConditions::coasting(1.0e6);
371 let flow = Flow::axial(0.5);
372 let drag = |m: &AeroModel| m.drag(&flow, &conditions).unwrap().zero_lift_coefficient;
373 assert_eq!(drag(&model_last), 0.7);
374 assert_eq!(drag(&table_last), 0.45);
375 }
376
377 #[test]
378 fn a_model_is_asked_the_conditions_the_buildup_reads() {
379 let spy = Arc::new(Spy::default());
380 let asked = || spy.0.lock().unwrap().pop().unwrap();
381 let conditions =
382 DragConditions::thrusting(1.0e6, 0.0005).with_pod_motors([0.0, 0.0002, 0.0, 0.0]);
383 let flow = Flow::axial(0.5);
384 model()
385 .with_shared_drag_model(spy.clone())
386 .drag(&flow, &conditions)
387 .unwrap();
388 assert_eq!(asked(), conditions);
389 model()
392 .with_full_base_drag_under_power()
393 .with_shared_drag_model(spy.clone())
394 .drag(&flow, &conditions)
395 .unwrap();
396 let read = asked();
397 assert!(read.thrusting);
398 assert_eq!(read.thrusting_motor_area_m2, 0.0);
399 assert_eq!(
400 read.thrusting_pod_motor_areas_m2,
401 [0.0; crate::MOTOR_POD_SETS]
402 );
403 }
404
405 #[test]
406 fn a_coefficient_that_is_negative_or_not_finite_is_refused() {
407 let conditions = DragConditions::coasting(1.0e6);
408 let flow = Flow::axial(0.5);
409 for bad in [-0.01, f64::NAN, f64::INFINITY] {
410 let error = model()
411 .with_drag_model(Constant(bad))
412 .drag(&flow, &conditions)
413 .unwrap_err();
414 assert!(
415 matches!(error, AeroError::Domain { what, value }
416 if what == "zero-lift drag coefficient from a drag model"
417 && value.to_bits() == bad.to_bits()),
418 "{error:?}"
419 );
420 }
421 let zero = model().with_drag_model(Constant(0.0));
423 assert_eq!(
424 zero.drag(&flow, &conditions).unwrap().axial_coefficient,
425 0.0
426 );
427 let custom = model().with_drag_model(Constant(0.5));
429 for mach in [-0.1, f64::NAN] {
430 let error = custom.drag(&Flow::axial(mach), &conditions).unwrap_err();
431 assert!(
432 matches!(error, AeroError::Domain { what, .. }
433 if what == "Mach number for a drag model"),
434 "{error:?}"
435 );
436 }
437 let error = custom
438 .drag(&Flow::new(0.5, -0.1, 0.0), &conditions)
439 .unwrap_err();
440 assert!(
441 matches!(error, AeroError::Domain { value, .. } if value == -0.1),
442 "{error:?}"
443 );
444 }
445
446 #[test]
447 fn a_models_own_error_is_passed_on() {
448 #[derive(Debug)]
449 struct Refuses;
450 impl DragModel for Refuses {
451 fn zero_lift_drag(&self, query: &DragQuery<'_>) -> Result<f64, AeroError> {
452 Err(AeroError::Domain {
453 what: "Mach number past the model's data",
454 value: query.mach(),
455 })
456 }
457 }
458 let error = model()
459 .with_drag_model(Refuses)
460 .drag(&Flow::axial(0.5), &DragConditions::coasting(1.0e6))
461 .unwrap_err();
462 assert!(
463 matches!(&error, AeroError::DragModel { source }
464 if **source == AeroError::Domain {
465 what: "Mach number past the model's data",
466 value: 0.5,
467 }),
468 "{error:?}"
469 );
470 assert_eq!(
471 error.to_string(),
472 "drag model: Mach number past the model's data is outside its domain: 0.5"
473 );
474 }
475
476 #[test]
477 fn models_holding_the_same_drag_model_are_equal() {
478 let custom = model().with_drag_model(Constant(0.5));
479 assert_eq!(custom.clone(), custom);
480 assert_ne!(model().with_drag_model(Constant(0.5)), custom);
481 assert_ne!(model(), custom);
482 let shared: Arc<dyn DragModel> = Arc::new(Constant(0.5));
483 assert_eq!(
484 model().with_shared_drag_model(Arc::clone(&shared)),
485 model().with_shared_drag_model(Arc::clone(&shared))
486 );
487 assert!(Arc::ptr_eq(
488 model()
489 .with_shared_drag_model(Arc::clone(&shared))
490 .drag_model()
491 .unwrap(),
492 &shared
493 ));
494 }
495
496 #[test]
497 fn a_query_says_what_the_flight_asks() {
498 let rocket = model();
499 let (flow, conditions) = (Flow::new(0.2, 0.1, 0.3), DragConditions::coasting(1.0e6));
500 let query = DragQuery::new(&flow, &conditions, &rocket);
501 assert_eq!(query.mach(), 0.2);
502 assert_eq!(*query.flow(), flow);
503 assert_eq!(*query.conditions(), conditions);
504 assert_eq!(query.reference_area_m2(), rocket.reference_area_m2());
505 assert_eq!(query.length_m(), rocket.length_m());
506 assert_eq!(
507 query.buildup_components().unwrap(),
508 rocket.buildup_components(&flow, &conditions).unwrap()
509 );
510 let text = format!("{query:?}");
512 assert!(text.starts_with("DragQuery { flow: Flow {") && text.ends_with(", .. }"));
513 }
514
515 #[test]
516 fn an_inspected_model_names_its_drag_model() {
517 let text = serde_json::to_string(&model().with_drag_model(Constant(0.45))).unwrap();
518 assert!(text.contains(r#""drag_model":"Constant(0.45)""#), "{text}");
519 let own = serde_json::to_string(&model()).unwrap();
520 assert!(!own.contains("drag_model"));
521 }
522}