1use std::collections::{BTreeMap, BTreeSet};
41
42use hpr_core::{DMat3, DVec3};
43use serde::{Deserialize, Serialize};
44
45use crate::config::{Configuration, MotorMount};
46use crate::error::DesignError;
47use crate::finish::Finish;
48use crate::fins::{FinSet, TubeFinSet};
49use crate::mass::{MassProperties, Placement};
50use crate::parts::{
51 BodyTube, CenteringRing, InnerTube, LaunchLug, MassComponent, NoseCone, Packing, Parachute,
52 PodSet, RailButton, ShockCord, Streamer, Transition,
53};
54use crate::shapes::check_dimension;
55use crate::solids::Wall;
56
57pub const LENGTH_TOLERANCE_M: f64 = 1e-9;
60
61#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
64#[serde(deny_unknown_fields)]
65pub struct Rocket {
66 #[serde(default)]
68 pub name: String,
69 pub stages: Vec<Stage>,
72 #[serde(default)]
74 pub reference_diameter: ReferenceDiameter,
75 #[serde(default)]
77 pub configurations: Vec<Configuration>,
78}
79
80#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
83#[serde(deny_unknown_fields)]
84pub struct Stage {
85 pub id: String,
87 #[serde(default)]
89 pub name: String,
90 pub components: Vec<Component>,
92 #[serde(
95 default,
96 skip_serializing_if = "Overrides::is_empty",
97 deserialize_with = "object_only"
98 )]
99 pub overrides: Overrides,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub drag_override: Option<DragOverride>,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub parallel: Option<ParallelStage>,
108}
109
110#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
122#[serde(deny_unknown_fields)]
123pub struct ParallelStage {
124 pub on: String,
126 pub position: Position,
129 pub pods: PodSet,
131}
132
133fn object_only<'de, D, T>(deserializer: D) -> Result<T, D::Error>
138where
139 D: serde::Deserializer<'de>,
140 T: Deserialize<'de>,
141{
142 struct ObjectOnly<T>(std::marker::PhantomData<T>);
143 impl<'de, T: Deserialize<'de>> serde::de::Visitor<'de> for ObjectOnly<T> {
144 type Value = T;
145 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
146 formatter.write_str("an object")
147 }
148 fn visit_map<A: serde::de::MapAccess<'de>>(self, map: A) -> Result<T, A::Error> {
149 T::deserialize(serde::de::value::MapAccessDeserializer::new(map))
150 }
151 }
152 deserializer.deserialize_map(ObjectOnly(std::marker::PhantomData))
153}
154
155#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
157#[serde(deny_unknown_fields)]
158pub struct Component {
159 pub id: String,
161 #[serde(default)]
163 pub name: String,
164 pub part: Part,
166 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub position: Option<Position>,
170 #[serde(default, skip_serializing_if = "Vec::is_empty")]
172 pub auto: Vec<AutoDimension>,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub motor_mount: Option<MotorMount>,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
179 pub finish: Option<Finish>,
180 #[serde(
182 default,
183 skip_serializing_if = "Overrides::is_empty",
184 deserialize_with = "object_only"
185 )]
186 pub overrides: Overrides,
187 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
190 pub overrides_include_children: bool,
191 #[serde(default, skip_serializing_if = "Option::is_none")]
194 pub drag_override: Option<DragOverride>,
195 #[serde(default, skip_serializing_if = "Vec::is_empty")]
197 pub children: Vec<Component>,
198}
199
200impl Component {
201 pub fn length_m(&self) -> f64 {
204 match self.part {
205 Part::PodSet(_) => self
207 .children
208 .iter()
209 .fold(0.0, |length, c| length + c.part.length_m()),
210 _ => self.part.length_m(),
211 }
212 }
213}
214
215#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
217#[serde(rename_all = "snake_case")]
218#[non_exhaustive]
219pub enum Part {
220 NoseCone(NoseCone),
222 BodyTube(BodyTube),
224 Transition(Transition),
226 InnerTube(InnerTube),
228 CenteringRing(CenteringRing),
230 FinSet(FinSet),
232 TubeFinSet(TubeFinSet),
234 LaunchLug(LaunchLug),
236 RailButton(RailButton),
238 PodSet(PodSet),
241 MassComponent(MassComponent),
243 Parachute(Parachute),
245 Streamer(Streamer),
247 ShockCord(ShockCord),
249}
250
251#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253enum Role {
254 Body,
256 External,
258 Internal,
260}
261
262impl Part {
263 pub fn kind_name(&self) -> &'static str {
265 match self {
266 Self::NoseCone(_) => "nose_cone",
267 Self::BodyTube(_) => "body_tube",
268 Self::Transition(_) => "transition",
269 Self::InnerTube(_) => "inner_tube",
270 Self::CenteringRing(_) => "centering_ring",
271 Self::FinSet(_) => "fin_set",
272 Self::TubeFinSet(_) => "tube_fin_set",
273 Self::LaunchLug(_) => "launch_lug",
274 Self::RailButton(_) => "rail_button",
275 Self::PodSet(_) => "pod_set",
276 Self::MassComponent(_) => "mass_component",
277 Self::Parachute(_) => "parachute",
278 Self::Streamer(_) => "streamer",
279 Self::ShockCord(_) => "shock_cord",
280 }
281 }
282
283 fn role(&self) -> Role {
284 match self {
285 Self::NoseCone(_) | Self::BodyTube(_) | Self::Transition(_) => Role::Body,
286 Self::FinSet(_)
287 | Self::TubeFinSet(_)
288 | Self::LaunchLug(_)
289 | Self::RailButton(_)
290 | Self::PodSet(_) => Role::External,
291 Self::InnerTube(_)
292 | Self::CenteringRing(_)
293 | Self::MassComponent(_)
294 | Self::Parachute(_)
295 | Self::Streamer(_)
296 | Self::ShockCord(_) => Role::Internal,
297 }
298 }
299
300 pub fn is_body(&self) -> bool {
302 self.role() == Role::Body
303 }
304
305 pub fn is_external(&self) -> bool {
308 self.role() == Role::External
309 }
310
311 pub fn length_m(&self) -> f64 {
317 let row =
318 |count: u32, one: f64, spacing: f64| one + spacing * f64::from(count.saturating_sub(1));
319 match self {
320 Self::NoseCone(p) => p.length_m,
321 Self::BodyTube(p) => p.length_m,
322 Self::Transition(p) => p.length_m,
323 Self::InnerTube(p) => p.length_m,
324 Self::CenteringRing(p) => p.length_m,
325 Self::FinSet(p) => p.planform.root_chord_m(),
326 Self::TubeFinSet(p) => p.length_m,
327 Self::LaunchLug(p) => row(p.count, p.length_m, p.spacing_m),
328 Self::RailButton(p) => row(p.count, p.outer_diameter_m, p.spacing_m),
329 Self::PodSet(_) => 0.0,
330 Self::MassComponent(p) => p.packing.length_m,
331 Self::Parachute(p) => p.packing.length_m,
332 Self::Streamer(p) => p.packing.length_m,
333 Self::ShockCord(p) => p.packing.length_m,
334 }
335 }
336
337 pub fn packing(&self) -> Option<&Packing> {
339 match self {
340 Self::MassComponent(p) => Some(&p.packing),
341 Self::Parachute(p) => Some(&p.packing),
342 Self::Streamer(p) => Some(&p.packing),
343 Self::ShockCord(p) => Some(&p.packing),
344 _ => None,
345 }
346 }
347
348 pub fn fore_radius_m(&self) -> Option<f64> {
350 match self {
351 Self::NoseCone(_) => Some(0.0),
352 Self::BodyTube(p) => Some(p.outer_radius_m),
353 Self::Transition(p) => Some(p.fore_radius_m),
354 _ => None,
355 }
356 }
357
358 pub fn aft_radius_m(&self) -> Option<f64> {
360 match self {
361 Self::NoseCone(p) => Some(p.base_radius_m),
362 Self::BodyTube(p) => Some(p.outer_radius_m),
363 Self::Transition(p) => Some(p.aft_radius_m),
364 _ => None,
365 }
366 }
367
368 pub fn max_radius_m(&self) -> Result<Option<f64>, DesignError> {
374 Ok(match self {
375 Self::NoseCone(p) => Some(p.profile()?.max_radius_m()),
376 Self::BodyTube(p) => Some(p.outer_radius_m),
377 Self::Transition(p) => Some(p.profile()?.max_radius_m()),
378 _ => None,
379 })
380 }
381
382 pub fn inner_radius_m(&self) -> Option<f64> {
384 match self {
385 Self::BodyTube(p) => Some(p.outer_radius_m - p.thickness_m),
386 Self::InnerTube(p) => Some(p.outer_radius_m - p.thickness_m),
387 _ => None,
388 }
389 }
390
391 pub fn axis_offset_m(&self) -> [f64; 2] {
395 let turned = |r: f64, angle: f64| [r * angle.cos(), r * angle.sin()];
396 match self {
397 Self::InnerTube(p) => turned(p.radial_offset_m, p.angle_rad),
398 Self::MassComponent(p) => turned(p.packing.radial_offset_m, p.packing.angle_rad),
399 Self::Parachute(p) => turned(p.packing.radial_offset_m, p.packing.angle_rad),
400 Self::Streamer(p) => turned(p.packing.radial_offset_m, p.packing.angle_rad),
401 Self::ShockCord(p) => turned(p.packing.radial_offset_m, p.packing.angle_rad),
402 _ => [0.0, 0.0],
403 }
404 }
405
406 pub fn outer_radius_about_axis_m(&self) -> Option<f64> {
409 match self {
410 Self::InnerTube(p) => Some(p.outer_radius_m),
411 Self::CenteringRing(p) => Some(p.outer_radius_m),
412 Self::MassComponent(p) => Some(p.packing.radius_m),
413 Self::Parachute(p) => Some(p.packing.radius_m),
414 Self::Streamer(p) => Some(p.packing.radius_m),
415 Self::ShockCord(p) => Some(p.packing.radius_m),
416 _ => None,
417 }
418 }
419
420 pub fn reach_from_m(&self, axis: [f64; 2]) -> Option<f64> {
423 let [x, y] = self.axis_offset_m();
424 let tubes = match self {
425 Self::InnerTube(tube) if !tube.cluster_m.is_empty() => tube.cluster_m.as_slice(),
426 _ => &[[0.0, 0.0]],
427 };
428 self.outer_radius_about_axis_m().map(|r| {
429 tubes
430 .iter()
431 .map(|&[u, v]| (x + u - axis[0]).hypot(y + v - axis[1]) + r)
432 .fold(f64::NEG_INFINITY, f64::max)
433 })
434 }
435
436 pub fn mass_properties(
444 &self,
445 body_radius_m: Option<f64>,
446 ) -> Result<MassProperties, DesignError> {
447 let body = || {
448 body_radius_m.ok_or_else(|| {
449 DesignError::Geometry(format!(
450 "a {} needs the radius of the body it sits on",
451 self.kind_name()
452 ))
453 })
454 };
455 match self {
456 Self::NoseCone(p) => p.mass_properties(),
457 Self::BodyTube(p) => p.mass_properties(),
458 Self::Transition(p) => p.mass_properties(),
459 Self::InnerTube(p) => p.mass_properties(),
460 Self::CenteringRing(p) => p.mass_properties(),
461 Self::FinSet(p) => p.mass_properties(body()?),
462 Self::TubeFinSet(p) => p.mass_properties(body()?),
463 Self::LaunchLug(p) => p.mass_properties(body()?),
464 Self::RailButton(p) => p.mass_properties(body()?),
465 Self::PodSet(p) => p.pods().map(|_| MassProperties::ZERO),
466 Self::MassComponent(p) => p.mass_properties(),
467 Self::Parachute(p) => p.mass_properties(),
468 Self::Streamer(p) => p.mass_properties(),
469 Self::ShockCord(p) => p.mass_properties(),
470 }
471 }
472}
473
474#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
476#[serde(tag = "from", rename_all = "snake_case", deny_unknown_fields)]
477pub enum Position {
478 Top {
480 #[serde(default)]
482 aft_offset_m: f64,
483 },
484 Middle {
486 #[serde(default)]
488 aft_offset_m: f64,
489 },
490 Bottom {
492 #[serde(default)]
494 aft_offset_m: f64,
495 },
496 After {
499 #[serde(default)]
501 aft_offset_m: f64,
502 },
503 Absolute {
505 station_m: f64,
507 },
508}
509
510#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema)]
512#[serde(rename_all = "snake_case")]
513#[non_exhaustive]
514pub enum AutoDimension {
515 BaseRadius,
517 OuterRadius,
532 ForeRadius,
534 AftRadius,
536 ShoulderRadius,
538 ForeShoulderRadius,
540 AftShoulderRadius,
542 InnerRadius,
545 PackedRadius,
548}
549
550impl AutoDimension {
551 pub fn name(self) -> &'static str {
553 match self {
554 Self::BaseRadius => "base_radius",
555 Self::OuterRadius => "outer_radius",
556 Self::ForeRadius => "fore_radius",
557 Self::AftRadius => "aft_radius",
558 Self::ShoulderRadius => "shoulder_radius",
559 Self::ForeShoulderRadius => "fore_shoulder_radius",
560 Self::AftShoulderRadius => "aft_shoulder_radius",
561 Self::InnerRadius => "inner_radius",
562 Self::PackedRadius => "packed_radius",
563 }
564 }
565
566 fn applies_to(self, part: &Part) -> bool {
567 use AutoDimension as A;
568 matches!(
569 (self, part),
570 (A::BaseRadius | A::ShoulderRadius, Part::NoseCone(_))
571 | (
572 A::OuterRadius,
573 Part::BodyTube(_)
574 | Part::CenteringRing(_)
575 | Part::InnerTube(_)
576 | Part::TubeFinSet(_)
577 )
578 | (
579 A::ForeRadius | A::AftRadius | A::ForeShoulderRadius | A::AftShoulderRadius,
580 Part::Transition(_)
581 )
582 | (A::InnerRadius, Part::CenteringRing(_))
583 | (
584 A::PackedRadius,
585 Part::MassComponent(_)
586 | Part::Parachute(_)
587 | Part::Streamer(_)
588 | Part::ShockCord(_)
589 )
590 )
591 }
592}
593
594#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize, schemars::JsonSchema)]
608#[serde(deny_unknown_fields)]
609pub struct Overrides {
610 #[serde(default, skip_serializing_if = "Option::is_none")]
612 pub mass_kg: Option<f64>,
613 #[serde(default, skip_serializing_if = "Option::is_none")]
615 pub cg_aft_m: Option<f64>,
616 #[serde(default, skip_serializing_if = "Option::is_none")]
620 pub cg_xy_m: Option<[f64; 2]>,
621 #[serde(default, skip_serializing_if = "Option::is_none")]
624 pub inertia: Option<InertiaOverride>,
625}
626
627#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
647#[serde(deny_unknown_fields)]
648pub struct DragOverride {
649 pub coefficient: f64,
652 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
655 pub include_children: bool,
656}
657
658#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
661#[serde(deny_unknown_fields)]
662pub struct InertiaOverride {
663 pub xx_kg_m2: f64,
665 pub yy_kg_m2: f64,
667 pub zz_kg_m2: f64,
669 #[serde(default)]
671 pub xy_kg_m2: f64,
672 #[serde(default)]
674 pub xz_kg_m2: f64,
675 #[serde(default)]
677 pub yz_kg_m2: f64,
678}
679
680impl InertiaOverride {
681 pub fn axisymmetric(axial_kg_m2: f64, transverse_kg_m2: f64) -> Self {
683 Self {
684 xx_kg_m2: transverse_kg_m2,
685 yy_kg_m2: transverse_kg_m2,
686 zz_kg_m2: axial_kg_m2,
687 xy_kg_m2: 0.0,
688 xz_kg_m2: 0.0,
689 yz_kg_m2: 0.0,
690 }
691 }
692
693 pub fn tensor(&self) -> DMat3 {
695 DMat3::from_cols(
696 DVec3::new(self.xx_kg_m2, self.xy_kg_m2, self.xz_kg_m2),
697 DVec3::new(self.xy_kg_m2, self.yy_kg_m2, self.yz_kg_m2),
698 DVec3::new(self.xz_kg_m2, self.yz_kg_m2, self.zz_kg_m2),
699 )
700 }
701}
702
703impl Overrides {
704 pub fn sets_center(&self) -> bool {
706 self.cg_aft_m.is_some() || self.cg_xy_m.is_some()
707 }
708
709 pub fn sets_axial_center(&self) -> bool {
711 self.cg_aft_m.is_some()
712 }
713
714 pub fn is_empty(&self) -> bool {
716 self.mass_kg.is_none()
717 && self.cg_aft_m.is_none()
718 && self.cg_xy_m.is_none()
719 && self.inertia.is_none()
720 }
721
722 pub fn apply(
731 &self,
732 mass: MassProperties,
733 fore_station_m: f64,
734 ) -> Result<MassProperties, DesignError> {
735 if self.is_empty() {
736 return Ok(mass);
737 }
738 let mut out = mass;
739 if let Some(m) = self.mass_kg {
740 check_dimension("mass override (kg)", m, true)?;
741 out = if out.mass_kg > 0.0 {
742 out.scaled(m / out.mass_kg)
743 } else {
744 MassProperties::point(m, out.cg_m)
745 };
746 }
747 if let Some(aft) = self.cg_aft_m {
748 if !aft.is_finite() {
749 return Err(DesignError::Domain {
750 what: "center-of-mass override (m)",
751 value: aft,
752 });
753 }
754 out.cg_m.z = -(fore_station_m + aft);
755 }
756 if let Some([x, y]) = self.cg_xy_m {
757 for value in [x, y] {
758 if !value.is_finite() {
759 return Err(DesignError::Domain {
760 what: "off-axis center-of-mass override (m)",
761 value,
762 });
763 }
764 }
765 out.cg_m.x = x;
766 out.cg_m.y = y;
767 }
768 if let Some(inertia) = self.inertia {
769 out.inertia_kg_m2 = inertia.tensor();
770 }
771 out.validate()?;
772 Ok(out)
773 }
774}
775
776#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
778#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
779#[non_exhaustive]
780pub enum ReferenceDiameter {
781 Maximum {},
784 NoseBase {},
786 Custom {
788 diameter_m: f64,
790 },
791}
792
793impl Default for ReferenceDiameter {
794 fn default() -> Self {
795 Self::Maximum {}
796 }
797}
798
799#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
801pub struct PlacedComponent {
802 pub id: String,
804 pub stage: usize,
806 pub parent: Option<usize>,
809 pub part: Part,
811 pub fore_station_m: f64,
813 pub length_m: f64,
815 #[serde(default)]
817 pub finish: Finish,
818 pub body_radius_m: Option<f64>,
820 pub motor_mount: Option<MotorMount>,
822 pub own: MassProperties,
824 pub with_children: MassProperties,
826 #[serde(default, alias = "centre_overridden")]
829 pub center_overridden: bool,
830 #[serde(default = "one_copy")]
838 pub copies: Vec<Placement>,
839 #[serde(default, skip_serializing_if = "Option::is_none")]
841 pub drag_override: Option<DragOverride>,
842}
843
844fn one_copy() -> Vec<Placement> {
846 vec![Placement::HERE]
847}
848
849impl PlacedComponent {
850 pub fn aft_station_m(&self) -> f64 {
852 self.fore_station_m + self.length_m
853 }
854
855 pub fn contents_copies(&self) -> Result<Vec<Placement>, DesignError> {
865 let repeats = repeats(&self.part)?;
866 Ok(self
867 .copies
868 .iter()
869 .flat_map(|copy| repeats.iter().map(move |inner| copy.after(inner)))
870 .collect())
871 }
872}
873
874fn repeats(part: &Part) -> Result<Vec<Placement>, DesignError> {
877 match part {
878 Part::InnerTube(tube) => Ok(tube.tubes_m()?.into_iter().map(Placement::moved).collect()),
879 Part::PodSet(pods) => pods.pods(),
880 _ => Ok(one_copy()),
881 }
882}
883
884#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
886pub struct PlacedStage {
887 pub id: String,
889 pub fore_station_m: f64,
891 pub aft_station_m: f64,
893 pub mass: MassProperties,
895 #[serde(default, alias = "centre_overridden")]
898 pub center_overridden: bool,
899 #[serde(default, skip_serializing_if = "Option::is_none")]
901 pub drag_override: Option<DragOverride>,
902 #[serde(default, skip_serializing_if = "Option::is_none")]
905 pub hung_on: Option<usize>,
906}
907
908#[derive(Debug, Clone, PartialEq)]
911pub(crate) struct StageMasses {
912 masses: Vec<Vec<MassProperties>>,
914 ends: Vec<Option<(f64, f64)>>,
916 length_m: f64,
918}
919
920#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
922pub struct Layout {
923 pub components: Vec<PlacedComponent>,
925 pub stages: Vec<PlacedStage>,
927 pub length_m: f64,
930 pub reference_diameter_m: f64,
932 pub structure: MassProperties,
934}
935
936impl Layout {
937 pub fn find(&self, id: &str) -> Option<(usize, &PlacedComponent)> {
939 self.components.iter().enumerate().find(|(_, c)| c.id == id)
940 }
941
942 pub fn body(&self) -> impl Iterator<Item = &PlacedComponent> {
944 self.components.iter().filter(|c| c.parent.is_none())
945 }
946
947 pub fn pod_set_of(&self, index: usize) -> Option<usize> {
951 let mut parent = self.components.get(index)?.parent;
952 for _ in 0..self.components.len() {
953 let at = parent?;
954 let component = self.components.get(at)?;
955 if matches!(component.part, Part::PodSet(_)) {
956 return Some(at);
957 }
958 parent = component.parent;
959 }
960 None
961 }
962
963 pub fn motor_pod_sets(&self) -> Vec<usize> {
967 let mut sets = Vec::new();
968 for (index, component) in self.components.iter().enumerate() {
969 if component.motor_mount.is_none() {
970 continue;
971 }
972 if let Some(set) = self.pod_set_of(index)
973 && !sets.contains(&set)
974 {
975 sets.push(set);
976 }
977 }
978 sets
979 }
980
981 pub fn reference_area_m2(&self) -> f64 {
983 0.25 * std::f64::consts::PI * self.reference_diameter_m * self.reference_diameter_m
984 }
985}
986
987#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
990pub struct UnresolvableRadius {
991 pub stage: usize,
993 pub component: usize,
995 pub id: String,
997 pub dimension: AutoDimension,
1000}
1001
1002impl Rocket {
1003 pub fn layout(&self) -> Result<Layout, DesignError> {
1021 let (components, masses) = self.placed_components()?;
1022 self.staged_layout(components, &masses)
1023 }
1024
1025 pub(crate) fn placed_components(
1028 &self,
1029 ) -> Result<(Vec<PlacedComponent>, StageMasses), DesignError> {
1030 if self.stages.is_empty() {
1031 return Err(DesignError::Tree {
1032 id: self.name.clone(),
1033 message: "a rocket needs at least one stage".to_owned(),
1034 });
1035 }
1036 let mut ids = BTreeSet::new();
1037 for stage in &self.stages {
1038 if stage.components.is_empty() {
1039 return Err(tree(&stage.id, "a stage needs at least one body component"));
1040 }
1041 if stage.parallel.is_some() {
1042 continue;
1044 }
1045 unique(&mut ids, &stage.id)?;
1046 for component in &stage.components {
1047 check_node(&mut ids, component, None, 0, false)?;
1048 }
1049 }
1050 let mut hung = Hung::default();
1051 for ((k, stage), carrier) in self.stages.iter().enumerate().zip(self.carriers()?) {
1052 let (Some(parallel), Some(carrier)) = (&stage.parallel, carrier) else {
1053 continue;
1054 };
1055 let on = &self.stages[carrier.stage].components[carrier.component];
1056 if matches!(parallel.position, Position::After { .. }) {
1057 return Err(tree(
1058 &stage.id,
1059 "a parallel stage is placed along the tube it hangs on, not after a sibling",
1060 ));
1061 }
1062 if !self.stages[carrier.stage].overrides.is_empty()
1066 || (on.overrides_include_children && !on.overrides.is_empty())
1067 {
1068 return Err(tree(
1069 &stage.id,
1070 format!(
1071 "a parallel stage hangs on `{}`, under a mass override that covers what \
1072 the tube or its stage holds, which doesn't say whether it covers the \
1073 parallel stage",
1074 on.id
1075 ),
1076 ));
1077 }
1078 let pods = parallel_pods(stage, parallel);
1079 check_node(&mut ids, &pods, Some(&on.part), 1, false)?;
1080 hung.stages
1081 .entry(on.id.clone())
1082 .or_default()
1083 .push((k, pods));
1084 }
1085
1086 let nodes: Vec<(usize, &Component)> = self
1087 .stages
1088 .iter()
1089 .enumerate()
1090 .filter(|(_, stage)| stage.parallel.is_none())
1091 .flat_map(|(k, stage)| stage.components.iter().map(move |c| (k, c)))
1092 .collect();
1093 let mut parts: Vec<Part> = nodes.iter().map(|(_, c)| c.part.clone()).collect();
1094 let autos: Vec<&[AutoDimension]> = nodes.iter().map(|(_, c)| c.auto.as_slice()).collect();
1095 let ids: Vec<&str> = nodes.iter().map(|(_, c)| c.id.as_str()).collect();
1096 resolve_body_radii(&mut parts, &autos, &ids)?;
1097 resolve_shoulders(&mut parts, &autos, &ids)?;
1098
1099 let mut components = Vec::new();
1100 let mut stage_masses: Vec<Vec<MassProperties>> = vec![Vec::new(); self.stages.len()];
1101 let mut stage_ends: Vec<Option<(f64, f64)>> = vec![None; self.stages.len()];
1102 let mut station = 0.0;
1103 for ((stage, node), part) in nodes.iter().zip(parts) {
1104 let length = part.length_m();
1105 check_dimension("body component length", length, false)
1106 .map_err(|e| within(&node.id, e))?;
1107 let placed = place(&part, None, station, &node.id)?;
1108 let index = components.len();
1109 components.push(PlacedComponent {
1110 id: node.id.clone(),
1111 stage: *stage,
1112 parent: None,
1113 part,
1114 fore_station_m: station,
1115 length_m: length,
1116 finish: node.finish.unwrap_or_default(),
1117 body_radius_m: None,
1118 motor_mount: node.motor_mount,
1119 own: placed,
1120 with_children: placed,
1121 center_overridden: node.overrides.sets_axial_center(),
1122 copies: one_copy(),
1123 drag_override: node.drag_override,
1124 });
1125 let with_children = finish(&mut components, index, node, &mut hung)?;
1126 stage_masses[*stage].push(with_children);
1127 let ends = stage_ends[*stage].get_or_insert((station, station));
1128 ends.1 = station + length;
1129 station += length;
1130 }
1131 for (k, mass, ends) in hung.placed {
1132 stage_masses[k].push(mass);
1133 stage_ends[k] = Some(ends);
1134 }
1135 Ok((
1136 components,
1137 StageMasses {
1138 masses: stage_masses,
1139 ends: stage_ends,
1140 length_m: station,
1141 },
1142 ))
1143 }
1144
1145 pub(crate) fn staged_layout(
1149 &self,
1150 components: Vec<PlacedComponent>,
1151 masses: &StageMasses,
1152 ) -> Result<Layout, DesignError> {
1153 let mut stages = Vec::with_capacity(self.stages.len());
1154 let carriers = self.carriers()?;
1155 for ((stage, masses), ends) in self.stages.iter().zip(&masses.masses).zip(&masses.ends) {
1156 let (fore, aft) = ends.unwrap_or((0.0, 0.0));
1157 let mass = stage
1158 .overrides
1159 .apply(MassProperties::combine(masses), fore)
1160 .map_err(|e| within(&stage.id, e))?;
1161 let hung_on = carriers[stages.len()].map(|carrier| carrier.stage);
1162 stages.push(PlacedStage {
1163 id: stage.id.clone(),
1164 fore_station_m: fore,
1165 aft_station_m: aft,
1166 mass,
1167 center_overridden: stage.overrides.sets_axial_center(),
1168 drag_override: stage.drag_override,
1169 hung_on,
1170 });
1171 }
1172 let structure = MassProperties::combine(stages.iter().map(|s| &s.mass));
1173 let reference_diameter_m = self.reference_diameter_m(&components)?;
1174 Ok(Layout {
1175 components,
1176 stages,
1177 length_m: masses.length_m,
1178 reference_diameter_m,
1179 structure,
1180 })
1181 }
1182
1183 pub fn unresolvable_body_radii(&self) -> Vec<UnresolvableRadius> {
1199 let nodes: Vec<(usize, usize, &Component)> = self
1200 .stages
1201 .iter()
1202 .enumerate()
1203 .filter(|(_, stage)| stage.parallel.is_none())
1204 .flat_map(|(k, stage)| {
1205 stage
1206 .components
1207 .iter()
1208 .enumerate()
1209 .map(move |(i, c)| (k, i, c))
1210 })
1211 .collect();
1212 let parts: Vec<&Part> = nodes.iter().map(|(_, _, c)| &c.part).collect();
1213 let autos: Vec<&[AutoDimension]> =
1214 nodes.iter().map(|(_, _, c)| c.auto.as_slice()).collect();
1215 let (fore, aft) = sweep_body_radii(&parts, &autos);
1216 let mut unresolvable = Vec::new();
1217 for (n, &(stage, component, node)) in nodes.iter().enumerate() {
1218 let mut add = |dimension| {
1219 unresolvable.push(UnresolvableRadius {
1220 stage,
1221 component,
1222 id: node.id.clone(),
1223 dimension,
1224 });
1225 };
1226 match node.part {
1227 Part::NoseCone(_) if aft[n].is_none() => add(AutoDimension::BaseRadius),
1228 Part::BodyTube(_) if aft[n].is_none() => add(AutoDimension::OuterRadius),
1229 Part::Transition(_) => {
1230 if fore[n].is_none() {
1231 add(AutoDimension::ForeRadius);
1232 }
1233 if aft[n].is_none() {
1234 add(AutoDimension::AftRadius);
1235 }
1236 }
1237 _ => {}
1238 }
1239 }
1240 unresolvable
1241 }
1242
1243 pub fn fill_unresolvable_body_radii(&mut self, radius_m: f64) -> Vec<UnresolvableRadius> {
1251 let unresolvable = self.unresolvable_body_radii();
1252 for radius in &unresolvable {
1253 let component = &mut self.stages[radius.stage].components[radius.component];
1254 match (&mut component.part, radius.dimension) {
1255 (Part::NoseCone(p), AutoDimension::BaseRadius) => p.base_radius_m = radius_m,
1256 (Part::BodyTube(p), AutoDimension::OuterRadius) => p.outer_radius_m = radius_m,
1257 (Part::Transition(p), AutoDimension::ForeRadius) => p.fore_radius_m = radius_m,
1258 (Part::Transition(p), AutoDimension::AftRadius) => p.aft_radius_m = radius_m,
1259 _ => continue,
1261 }
1262 component.auto.retain(|auto| *auto != radius.dimension);
1263 }
1264 unresolvable
1265 }
1266
1267 fn reference_diameter_m(&self, components: &[PlacedComponent]) -> Result<f64, DesignError> {
1268 let diameter = match self.reference_diameter {
1269 ReferenceDiameter::Maximum {} => {
1270 let mut widest = 0.0f64;
1271 for c in components.iter().filter(|c| c.parent.is_none()) {
1272 if let Some(r) = c.part.max_radius_m()? {
1273 widest = widest.max(r);
1274 }
1275 }
1276 2.0 * widest
1277 }
1278 ReferenceDiameter::NoseBase {} => components
1280 .iter()
1281 .filter(|c| c.parent.is_none())
1282 .find_map(|c| match &c.part {
1283 Part::NoseCone(nose) => Some(2.0 * nose.base_radius_m),
1284 _ => None,
1285 })
1286 .ok_or_else(|| {
1287 tree(
1288 &self.name,
1289 "the reference diameter is the nose base, but there is no nose cone",
1290 )
1291 })?,
1292 ReferenceDiameter::Custom { diameter_m } => diameter_m,
1293 };
1294 check_dimension("reference diameter (m)", diameter, false)?;
1295 Ok(diameter)
1296 }
1297}
1298
1299fn within(id: &str, error: DesignError) -> DesignError {
1301 DesignError::InComponent {
1302 id: id.to_owned(),
1303 source: Box::new(error),
1304 }
1305}
1306
1307pub const MAX_DEPTH: usize = 32;
1311
1312fn tree(id: &str, message: impl Into<String>) -> DesignError {
1313 DesignError::Tree {
1314 id: id.to_owned(),
1315 message: message.into(),
1316 }
1317}
1318
1319fn unique(ids: &mut BTreeSet<String>, id: &str) -> Result<(), DesignError> {
1320 if id.is_empty() || !ids.insert(id.to_owned()) {
1321 return Err(DesignError::DuplicateId(id.to_owned()));
1322 }
1323 Ok(())
1324}
1325
1326fn check_node(
1329 ids: &mut BTreeSet<String>,
1330 node: &Component,
1331 parent: Option<&Part>,
1332 depth: usize,
1333 in_pod: bool,
1334) -> Result<(), DesignError> {
1335 unique(ids, &node.id)?;
1336 if let Some(finish) = node.finish {
1337 finish.roughness_m().map_err(|e| within(&node.id, e))?;
1338 }
1339 if depth >= MAX_DEPTH {
1340 return Err(tree(
1341 &node.id,
1342 format!("components nest more than {MAX_DEPTH} deep"),
1343 ));
1344 }
1345 let kind = node.part.kind_name();
1346 match (parent, node.part.role()) {
1347 (None | Some(Part::PodSet(_)), Role::Body) => {
1348 if node.position.is_some() {
1349 return Err(tree(
1350 &node.id,
1351 "a body component stacks and takes no position",
1352 ));
1353 }
1354 }
1355 (Some(Part::PodSet(_)), _) => {
1356 return Err(tree(
1357 &node.id,
1358 format!("a pod holds body components; attach a {kind} to one of them"),
1359 ));
1360 }
1361 (None, _) => {
1362 return Err(tree(
1363 &node.id,
1364 format!("a {kind} can't be a body component; attach it to one"),
1365 ));
1366 }
1367 (Some(_), Role::Body) => {
1368 return Err(tree(
1369 &node.id,
1370 format!("a {kind} is a body component; list it in a stage"),
1371 ));
1372 }
1373 (Some(parent), Role::External)
1376 if !(matches!(parent, Part::BodyTube(_))
1377 || matches!(node.part, Part::FinSet(_))
1378 && matches!(parent, Part::NoseCone(_) | Part::Transition(_))) =>
1379 {
1380 return Err(tree(
1381 &node.id,
1382 format!(
1383 "a {kind} attaches to a body tube{}, not a {}",
1384 if matches!(node.part, Part::FinSet(_)) {
1385 ", a nose cone or a transition"
1386 } else {
1387 ""
1388 },
1389 parent.kind_name()
1390 ),
1391 ));
1392 }
1393 (Some(parent), Role::Internal)
1394 if !(parent.is_body() || matches!(parent, Part::InnerTube(_))) =>
1395 {
1396 return Err(tree(
1397 &node.id,
1398 format!("a {kind} can't go inside a {}", parent.kind_name()),
1399 ));
1400 }
1401 (Some(_), _) => {
1402 if node.position.is_none() {
1403 return Err(tree(&node.id, "an attached part needs a position"));
1404 }
1405 }
1406 }
1407 for auto in &node.auto {
1408 if !auto.applies_to(&node.part) {
1409 return Err(tree(
1410 &node.id,
1411 format!("a {kind} has no automatic {} dimension", auto.name()),
1412 ));
1413 }
1414 }
1415 if matches!(node.part, Part::PodSet(_)) {
1416 if in_pod {
1417 return Err(tree(&node.id, "a pod set can't hang from a pod"));
1418 }
1419 if !node.overrides.is_empty() && !node.overrides_include_children {
1420 return Err(tree(
1421 &node.id,
1422 "a pod set weighs nothing of its own, so an override on it must cover its pods \
1423 (`overrides_include_children`)",
1424 ));
1425 }
1426 }
1427 if node.motor_mount.is_some() && !matches!(node.part, Part::BodyTube(_) | Part::InnerTube(_)) {
1428 return Err(tree(&node.id, format!("a {kind} can't be a motor mount")));
1429 }
1430 if !node.children.is_empty()
1431 && !(node.part.is_body() || matches!(node.part, Part::InnerTube(_) | Part::PodSet(_)))
1432 {
1433 return Err(tree(
1434 &node.id,
1435 format!("a {kind} can't have attached parts"),
1436 ));
1437 }
1438 for child in &node.children {
1439 let in_pod = in_pod || matches!(node.part, Part::PodSet(_));
1440 check_node(ids, child, Some(&node.part), depth + 1, in_pod)?;
1441 }
1442 Ok(())
1443}
1444
1445fn resolve_body_radii(
1448 parts: &mut [Part],
1449 autos: &[&[AutoDimension]],
1450 ids: &[&str],
1451) -> Result<(), DesignError> {
1452 let (fore, aft) = sweep_body_radii(parts, autos);
1453 for (i, part) in parts.iter_mut().enumerate() {
1454 let missing = || {
1455 tree(
1456 ids[i],
1457 "an automatic radius has no fixed radius among its neighbours to take",
1458 )
1459 };
1460 match part {
1461 Part::NoseCone(p) => p.base_radius_m = aft[i].ok_or_else(missing)?,
1462 Part::BodyTube(p) => p.outer_radius_m = aft[i].ok_or_else(missing)?,
1463 Part::Transition(p) => {
1464 p.fore_radius_m = fore[i].ok_or_else(missing)?;
1465 p.aft_radius_m = aft[i].ok_or_else(missing)?;
1466 }
1467 _ => {}
1468 }
1469 }
1470 Ok(())
1471}
1472
1473fn sweep_body_radii<P: std::borrow::Borrow<Part>>(
1483 parts: &[P],
1484 autos: &[&[AutoDimension]],
1485) -> (Vec<Option<f64>>, Vec<Option<f64>>) {
1486 let n = parts.len();
1487 let is_auto = |i: usize, a: AutoDimension| autos[i].contains(&a);
1488 let mut fore: Vec<Option<f64>> = Vec::with_capacity(n);
1489 let mut aft: Vec<Option<f64>> = Vec::with_capacity(n);
1490 for (i, part) in parts.iter().enumerate() {
1491 let (f, a) = match part.borrow() {
1492 Part::NoseCone(p) => (
1493 Some(0.0),
1494 (!is_auto(i, AutoDimension::BaseRadius)).then_some(p.base_radius_m),
1495 ),
1496 Part::BodyTube(p) => {
1497 let r = (!is_auto(i, AutoDimension::OuterRadius)).then_some(p.outer_radius_m);
1498 (r, r)
1499 }
1500 Part::Transition(p) => (
1501 (!is_auto(i, AutoDimension::ForeRadius)).then_some(p.fore_radius_m),
1502 (!is_auto(i, AutoDimension::AftRadius)).then_some(p.aft_radius_m),
1503 ),
1504 _ => (None, None),
1505 };
1506 fore.push(f);
1507 aft.push(a);
1508 }
1509 loop {
1510 let mut progress = false;
1511 for i in 0..n {
1512 let previous_aft = if i > 0 { aft[i - 1] } else { None };
1513 let next_fore = fore.get(i + 1).copied().flatten();
1514 match parts[i].borrow() {
1515 Part::NoseCone(_) => {
1516 if aft[i].is_none()
1517 && let Some(r) = next_fore
1518 {
1519 aft[i] = Some(r);
1520 progress = true;
1521 }
1522 }
1523 Part::BodyTube(_) => {
1524 if fore[i].is_none()
1525 && let Some(r) = previous_aft
1526 {
1527 fore[i] = Some(r);
1528 aft[i] = Some(r);
1529 progress = true;
1530 }
1531 }
1532 Part::Transition(_) => {
1533 if fore[i].is_none()
1534 && let Some(r) = previous_aft
1535 {
1536 fore[i] = Some(r);
1537 progress = true;
1538 }
1539 if aft[i].is_none()
1540 && let Some(r) = next_fore
1541 {
1542 aft[i] = Some(r);
1543 progress = true;
1544 }
1545 }
1546 _ => {}
1547 }
1548 }
1549 if progress {
1550 continue;
1551 }
1552 let fallback = (0..n).find(|&i| {
1553 matches!(parts[i].borrow(), Part::BodyTube(_))
1554 && fore[i].is_none()
1555 && fore.get(i + 1).copied().flatten().is_some()
1556 });
1557 match fallback {
1558 Some(i) => {
1559 fore[i] = fore[i + 1];
1560 aft[i] = fore[i + 1];
1561 }
1562 None => break,
1563 }
1564 }
1565 (fore, aft)
1566}
1567
1568fn resolve_shoulders(
1570 parts: &mut [Part],
1571 autos: &[&[AutoDimension]],
1572 ids: &[&str],
1573) -> Result<(), DesignError> {
1574 let inner = |parts: &[Part], i: Option<usize>| -> Option<f64> {
1575 match i.and_then(|i| parts.get(i)) {
1576 Some(Part::BodyTube(t)) => Some(t.outer_radius_m - t.thickness_m),
1577 _ => None,
1578 }
1579 };
1580 for i in 0..parts.len() {
1581 let previous = inner(parts, i.checked_sub(1));
1582 let next = inner(parts, Some(i + 1));
1583 let fit = |shoulder: &mut Option<crate::parts::Shoulder>,
1584 radius: Option<f64>,
1585 which: &str|
1586 -> Result<(), DesignError> {
1587 let Some(shoulder) = shoulder else {
1588 return Err(tree(
1589 ids[i],
1590 format!("an automatic {which} shoulder radius needs a shoulder"),
1591 ));
1592 };
1593 shoulder.outer_radius_m = radius.ok_or_else(|| {
1594 tree(
1595 ids[i],
1596 format!("an automatic {which} shoulder radius needs a body tube there"),
1597 )
1598 })?;
1599 Ok(())
1600 };
1601 match &mut parts[i] {
1602 Part::NoseCone(p) if autos[i].contains(&AutoDimension::ShoulderRadius) => {
1603 fit(&mut p.shoulder, next, "aft")?;
1604 }
1605 Part::Transition(p) => {
1606 if autos[i].contains(&AutoDimension::ForeShoulderRadius) {
1607 fit(&mut p.fore_shoulder, previous, "forward")?;
1608 }
1609 if autos[i].contains(&AutoDimension::AftShoulderRadius) {
1610 fit(&mut p.aft_shoulder, next, "aft")?;
1611 }
1612 }
1613 _ => {}
1614 }
1615 }
1616 Ok(())
1617}
1618
1619fn place(
1621 part: &Part,
1622 body_radius_m: Option<f64>,
1623 fore_station_m: f64,
1624 id: &str,
1625) -> Result<MassProperties, DesignError> {
1626 let mass = part
1627 .mass_properties(body_radius_m)
1628 .map_err(|e| within(id, e))?;
1629 Ok(mass.translated(DVec3::new(0.0, 0.0, -fore_station_m)))
1630}
1631
1632fn packed_for_override(
1642 part: &Part,
1643 overrides: &Overrides,
1644 mass: MassProperties,
1645 fore_station_m: f64,
1646) -> Result<MassProperties, DesignError> {
1647 match (overrides.mass_kg, part.packing()) {
1648 (Some(mass_kg), Some(packing)) => {
1651 check_dimension("mass override (kg)", mass_kg, true)?;
1652 Ok(packing
1653 .place(mass_kg)?
1654 .translated(DVec3::new(0.0, 0.0, -fore_station_m)))
1655 }
1656 _ => Ok(mass),
1657 }
1658}
1659
1660#[derive(Default)]
1663struct Hung {
1664 stages: BTreeMap<String, Vec<(usize, Component)>>,
1667 placed: Vec<(usize, MassProperties, (f64, f64))>,
1669}
1670
1671fn parallel_pods(stage: &Stage, parallel: &ParallelStage) -> Component {
1675 Component {
1676 id: stage.id.clone(),
1677 name: stage.name.clone(),
1678 part: Part::PodSet(parallel.pods.clone()),
1679 position: Some(parallel.position),
1680 auto: Vec::new(),
1681 motor_mount: None,
1682 finish: None,
1683 overrides: Overrides::default(),
1684 overrides_include_children: false,
1685 drag_override: None,
1686 children: stage.components.clone(),
1687 }
1688}
1689
1690#[derive(Debug, Clone, Copy)]
1692struct Carrier {
1693 stage: usize,
1694 component: usize,
1695}
1696
1697impl Rocket {
1698 fn carriers(&self) -> Result<Vec<Option<Carrier>>, DesignError> {
1706 let mut tubes: BTreeMap<&str, Carrier> = BTreeMap::new();
1707 let mut carriers = Vec::with_capacity(self.stages.len());
1708 for (k, stage) in self.stages.iter().enumerate() {
1709 let Some(parallel) = &stage.parallel else {
1710 for (component, c) in stage.components.iter().enumerate() {
1711 if matches!(c.part, Part::BodyTube(_)) {
1712 tubes.entry(&c.id).or_insert(Carrier {
1713 stage: k,
1714 component,
1715 });
1716 }
1717 }
1718 carriers.push(None);
1719 continue;
1720 };
1721 let carrier = tubes.get(parallel.on.as_str()).copied().ok_or_else(|| {
1722 tree(
1723 &stage.id,
1724 format!(
1725 "a parallel stage hangs on a body tube of an axial stage before it, and \
1726 `{}` is none",
1727 parallel.on
1728 ),
1729 )
1730 })?;
1731 carriers.push(Some(carrier));
1732 }
1733 Ok(carriers)
1734 }
1735}
1736
1737fn finish(
1747 components: &mut Vec<PlacedComponent>,
1748 index: usize,
1749 node: &Component,
1750 hung: &mut Hung,
1751) -> Result<MassProperties, DesignError> {
1752 let hanging: Vec<(usize, Component)> = hung.stages.get(&node.id).cloned().unwrap_or_default();
1755 let children: Vec<(&Component, Option<usize>)> = node
1756 .children
1757 .iter()
1758 .map(|child| (child, None))
1759 .chain(hanging.iter().map(|(k, pods)| (pods, Some(*k))))
1760 .collect();
1761 let parent = &components[index];
1762 let (p_fore, p_length, stage) = (parent.fore_station_m, parent.length_m, parent.stage);
1763 let (p_kind, p_inner) = (parent.part.kind_name(), parent.part.inner_radius_m());
1764 let p_axis = parent.part.axis_offset_m();
1765 let pod = matches!(parent.part, Part::PodSet(_));
1767 let p_tubes = repeats(&parent.part).map_err(|e| within(&node.id, e))?;
1770 let p_copies = parent.copies.clone();
1771 let p_contents = parent.contents_copies().map_err(|e| within(&node.id, e))?;
1772 let p_tube_radius = match &parent.part {
1773 Part::BodyTube(tube) => Some(tube.outer_radius_m),
1774 _ => None,
1775 };
1776 let p_surface = match &parent.part {
1778 Part::NoseCone(nose) => Some(nose.profile()),
1779 Part::Transition(part) => Some(part.profile()),
1780 _ => None,
1781 }
1782 .transpose()
1783 .map_err(|e| within(&node.id, e))?;
1784 let shell = |wall: &Wall| match wall {
1787 Wall::Shell { thickness_m } => Some(*thickness_m),
1788 Wall::Filled {} => None,
1789 };
1790 let p_narrowing = match &parent.part {
1791 Part::NoseCone(nose) => shell(&nose.wall).map(|t| nose.profile().map(|p| (p, t))),
1792 Part::Transition(part) => shell(&part.wall).map(|t| part.profile().map(|p| (p, t))),
1793 _ => None,
1794 }
1795 .transpose()
1796 .map_err(|e| within(&node.id, e))?;
1797 let own = if node.overrides_include_children {
1798 parent.own
1799 } else {
1800 packed_for_override(&parent.part, &node.overrides, parent.own, p_fore)
1801 .and_then(|own| node.overrides.apply(own, p_fore))
1802 .map_err(|e| within(&node.id, e))?
1803 };
1804
1805 let mut stations = Vec::with_capacity(children.len());
1808 let mut previous_aft = None;
1809 for &(child, _) in &children {
1810 let length = child.length_m();
1811 check_dimension("attached part length", length, true).map_err(|e| within(&child.id, e))?;
1812 let fore = if pod {
1813 previous_aft.unwrap_or(p_fore)
1814 } else {
1815 match child.position {
1816 Some(Position::Top { aft_offset_m }) => p_fore + aft_offset_m,
1817 Some(Position::Middle { aft_offset_m }) => {
1818 p_fore + 0.5 * (p_length - length) + aft_offset_m
1819 }
1820 Some(Position::Bottom { aft_offset_m }) => {
1821 p_fore + p_length - length + aft_offset_m
1822 }
1823 Some(Position::After { aft_offset_m }) => {
1824 previous_aft.unwrap_or(p_fore) + aft_offset_m
1825 }
1826 Some(Position::Absolute { station_m }) => station_m,
1827 None => return Err(tree(&child.id, "an attached part needs a position")),
1828 }
1829 };
1830 if !fore.is_finite() {
1831 return Err(within(
1832 &child.id,
1833 DesignError::Domain {
1834 what: "attached part position (m)",
1835 value: fore,
1836 },
1837 ));
1838 }
1839 stations.push((fore, length));
1840 previous_aft = Some(fore + length);
1841 }
1842
1843 let bore = |id: &str| {
1844 p_inner.ok_or_else(|| {
1845 tree(
1846 id,
1847 format!("an automatic radius needs a tube's inner radius, and a {p_kind} has none"),
1848 )
1849 })
1850 };
1851 let bore_over = |id: &str, (fore, length): (f64, f64)| match &p_narrowing {
1859 Some((profile, wall_m)) => {
1860 let at = |station: f64| profile.radius_m(station - p_fore);
1861 Ok((at(fore).min(at(fore + length)) - wall_m).max(0.0))
1862 }
1863 None => bore(id),
1864 };
1865 let mut resolved: Vec<Part> = children.iter().map(|(c, _)| c.part.clone()).collect();
1873 if pod {
1874 let autos: Vec<&[AutoDimension]> =
1876 node.children.iter().map(|c| c.auto.as_slice()).collect();
1877 let ids: Vec<&str> = node.children.iter().map(|c| c.id.as_str()).collect();
1878 resolve_body_radii(&mut resolved, &autos, &ids)?;
1879 resolve_shoulders(&mut resolved, &autos, &ids)?;
1880 }
1881 for ((part, &(child, _)), &station) in resolved.iter_mut().zip(&children).zip(&stations) {
1882 if child.auto.contains(&AutoDimension::OuterRadius) {
1883 match part {
1884 Part::CenteringRing(ring) => ring.outer_radius_m = bore_over(&child.id, station)?,
1885 Part::InnerTube(tube) => {
1888 tube.outer_radius_m = bore_over(&child.id, station)?;
1889 tube.thickness_m = tube.thickness_m.min(tube.outer_radius_m);
1890 }
1891 Part::TubeFinSet(tubes) => {
1895 let body_radius_m = p_tube_radius.ok_or_else(|| {
1896 tree(
1897 &child.id,
1898 "an automatic tube fin radius needs a body tube to ring",
1899 )
1900 })?;
1901 tubes.outer_radius_m =
1902 crate::TubeFinSet::closing_radius_m(body_radius_m, tubes.count);
1903 tubes.thickness_m = tubes.thickness_m.min(tubes.outer_radius_m);
1904 }
1905 _ => {}
1906 }
1907 }
1908 }
1909
1910 let mut parts = vec![own];
1911 for (k, &(child, hung_stage)) in children.iter().enumerate() {
1912 let (fore, length) = stations[k];
1913 let mut part = resolved[k].clone();
1914 let parent_inner = || bore(&child.id);
1915 let packed = |packing: &mut crate::parts::Packing| -> Result<(), DesignError> {
1917 let (x, y) = (
1918 packing.radial_offset_m * packing.angle_rad.cos(),
1919 packing.radial_offset_m * packing.angle_rad.sin(),
1920 );
1921 let room = parent_inner()? - (x - p_axis[0]).hypot(y - p_axis[1]);
1922 if !(room.is_finite() && room > 0.0) {
1923 return Err(tree(
1924 &child.id,
1925 "an automatic packed radius needs a radial offset inside the parent's bore",
1926 ));
1927 }
1928 packing.radius_m = room;
1929 Ok(())
1930 };
1931 for auto in &child.auto {
1932 match (auto, &mut part) {
1933 (AutoDimension::OuterRadius, _) => {}
1935 (AutoDimension::InnerRadius, Part::CenteringRing(ring)) => {
1936 let aft = fore + length;
1937 let outer_radius_m = ring.outer_radius_m;
1943 ring.inner_radius_m = resolved
1944 .iter()
1945 .zip(&stations)
1946 .filter_map(|(sibling, &(s_fore, s_length))| match sibling {
1947 Part::InnerTube(tube)
1948 if tube.radial_offset_m == 0.0
1949 && tube.outer_radius_m < outer_radius_m
1950 && s_fore.max(fore) < (s_fore + s_length).min(aft) =>
1951 {
1952 Some(tube.outer_radius_m)
1953 }
1954 _ => None,
1955 })
1956 .fold(0.0, f64::max);
1957 }
1958 (AutoDimension::PackedRadius, Part::MassComponent(p)) => packed(&mut p.packing)?,
1959 (AutoDimension::PackedRadius, Part::Parachute(p)) => packed(&mut p.packing)?,
1960 (AutoDimension::PackedRadius, Part::Streamer(p)) => packed(&mut p.packing)?,
1961 (AutoDimension::PackedRadius, Part::ShockCord(p)) => packed(&mut p.packing)?,
1962 _ => {}
1963 }
1964 }
1965 let body_radius_m = match (&part, &p_surface) {
1968 (Part::FinSet(fins), Some(surface)) => Some(
1969 fins.root_radius_on(surface, fore - p_fore)
1970 .map_err(|e| within(&child.id, e))?,
1971 ),
1972 _ if part.is_external() => {
1973 if let Part::FinSet(fins) = &part {
1974 fins.check_level_root().map_err(|e| within(&child.id, e))?;
1975 }
1976 Some(
1977 p_tube_radius
1978 .ok_or_else(|| tree(&child.id, "external parts attach to a body tube"))?,
1979 )
1980 }
1981 _ => None,
1982 };
1983 let placed = place(&part, body_radius_m, fore, &child.id)?;
1985 let child_index = components.len();
1986 components.push(PlacedComponent {
1987 id: child.id.clone(),
1988 stage: hung_stage.unwrap_or(stage),
1989 parent: Some(index),
1990 part,
1991 fore_station_m: fore,
1992 length_m: length,
1993 finish: child.finish.unwrap_or_default(),
1994 body_radius_m,
1995 motor_mount: child.motor_mount,
1996 own: placed,
1997 with_children: placed,
1998 center_overridden: child.overrides.sets_axial_center(),
1999 copies: p_contents.clone(),
2000 drag_override: child.drag_override,
2001 });
2002 let mass = MassProperties::placed(finish(components, child_index, child, hung)?, &p_tubes);
2003 match hung_stage {
2004 Some(k) => hung.placed.push((k, mass, (fore, fore + length))),
2005 None => parts.push(mass),
2006 }
2007 }
2008
2009 let mut with_children = MassProperties::combine(&parts);
2010 if node.overrides_include_children {
2011 with_children = packed_for_override(
2012 &components[index].part,
2013 &node.overrides,
2014 with_children,
2015 p_fore,
2016 )
2017 .and_then(|mass| node.overrides.apply(mass, p_fore))
2018 .map_err(|e| within(&node.id, e))?;
2019 }
2020 components[index].own = MassProperties::placed(own, &p_copies);
2021 components[index].with_children = MassProperties::placed(with_children, &p_copies);
2022 Ok(with_children)
2023}
2024
2025#[cfg(test)]
2026mod tests {
2027 use proptest::prelude::*;
2028
2029 use super::*;
2030 use crate::parts::Shoulder;
2031 use crate::testing::{
2032 attached, body, bottom, fins, inner_tube, mass_component, nose, ring, rocket, stage,
2033 three_fin_rocket, top, tube,
2034 };
2035
2036 fn close(got: f64, want: f64, tol: f64, what: &str) {
2037 assert!((got - want).abs() <= tol, "{what}: {got} vs {want}");
2038 }
2039
2040 fn station(layout: &Layout, id: &str) -> f64 {
2041 layout.find(id).unwrap().1.fore_station_m
2042 }
2043
2044 #[test]
2047 fn stacks_body_components_and_places_attached_parts() {
2048 let mut airframe = body("airframe", tube(0.8, 0.03, 0.001));
2049 let mut coupler = attached("coupler", inner_tube(0.2, 0.029, 0.001), top(0.1));
2050 coupler.children = vec![attached(
2051 "bay",
2052 mass_component(0.1, 0.05, 0.02),
2053 Position::After { aft_offset_m: 0.02 },
2054 )];
2055 airframe.children = vec![
2056 coupler,
2057 attached(
2058 "middle",
2059 mass_component(0.1, 0.1, 0.02),
2060 Position::Middle { aft_offset_m: 0.02 },
2061 ),
2062 attached("bottom", mass_component(0.1, 0.1, 0.02), bottom(-0.01)),
2063 attached(
2064 "after",
2065 mass_component(0.1, 0.004, 0.02),
2066 Position::After {
2067 aft_offset_m: 0.001,
2068 },
2069 ),
2070 attached(
2071 "absolute",
2072 mass_component(0.1, 0.05, 0.02),
2073 Position::Absolute { station_m: 0.4 },
2074 ),
2075 ];
2076 let design = rocket(vec![
2077 stage("upper", vec![body("nose", nose(0.2, 0.03)), airframe]),
2078 stage(
2079 "booster",
2080 vec![
2081 body(
2082 "interstage",
2083 Part::Transition(Transition {
2084 shape: crate::NoseShape::Conical {},
2085 clipped: false,
2086 length_m: 0.1,
2087 fore_radius_m: 0.03,
2088 aft_radius_m: 0.04,
2089 wall: crate::Wall::Shell { thickness_m: 0.002 },
2090 fore_shoulder: None,
2091 aft_shoulder: None,
2092 material: crate::testing::cardboard(),
2093 }),
2094 ),
2095 body("booster-tube", tube(0.5, 0.04, 0.001)),
2096 ],
2097 ),
2098 ]);
2099 let layout = design.layout().unwrap();
2100 let tol = 1e-15;
2101 close(station(&layout, "nose"), 0.0, tol, "nose");
2102 close(station(&layout, "airframe"), 0.2, tol, "airframe");
2103 close(station(&layout, "coupler"), 0.3, tol, "top");
2104 close(station(&layout, "bay"), 0.32, tol, "after, first child");
2106 close(station(&layout, "middle"), 0.2 + 0.35 + 0.02, tol, "middle");
2107 close(station(&layout, "bottom"), 1.0 - 0.1 - 0.01, tol, "bottom");
2108 close(station(&layout, "after"), 0.99 + 0.001, tol, "after");
2109 close(station(&layout, "absolute"), 0.4, tol, "absolute");
2110 close(station(&layout, "interstage"), 1.0, tol, "second stage");
2111 close(station(&layout, "booster-tube"), 1.1, tol, "booster tube");
2112 close(layout.length_m, 1.6, tol, "length");
2113 let [upper, booster] = &layout.stages[..] else {
2114 panic!("two stages")
2115 };
2116 close(upper.fore_station_m, 0.0, tol, "upper fore");
2117 close(upper.aft_station_m, 1.0, tol, "upper aft");
2118 close(booster.fore_station_m, 1.0, tol, "booster fore");
2119 close(booster.aft_station_m, 1.6, tol, "booster aft");
2120 let (bay, placed) = layout.find("bay").unwrap();
2121 assert_eq!(
2122 layout.components[placed.parent.unwrap()].id,
2123 "coupler",
2124 "parent"
2125 );
2126 assert_eq!(layout.components[bay].stage, 0);
2127 assert_eq!(layout.find("booster-tube").unwrap().1.stage, 1);
2128 let own = mass_component(0.1, 0.05, 0.02)
2130 .mass_properties(None)
2131 .unwrap();
2132 close(placed.own.cg_m.z, own.cg_m.z - 0.32, 1e-15, "placed center");
2133 let sum: f64 = layout.body().map(|c| c.with_children.mass_kg).sum();
2135 close(layout.structure.mass_kg, sum, 1e-15, "structure mass");
2136 }
2137
2138 #[test]
2144 fn a_fin_set_on_a_nose_cone_follows_its_surface() {
2145 let fin = |end_h: f64| {
2146 let mut set = match fins(0.05, 0.04) {
2147 Part::FinSet(set) => set,
2148 _ => unreachable!("`fins` makes a fin set"),
2149 };
2150 set.planform = crate::FinPlanform::Freeform {
2151 points_m: vec![[0.0, 0.0], [0.03, 0.04], [0.05, end_h]],
2152 root_m: Vec::new(),
2153 };
2154 set
2155 };
2156 let design = |child: Component, on_the_nose: bool| {
2157 let mut nose_cone = body("nose", nose(0.2, 0.03));
2158 let mut airframe = body("airframe", tube(0.6, 0.03, 0.001));
2159 if on_the_nose {
2160 nose_cone.children = vec![child];
2161 } else {
2162 airframe.children = vec![child];
2163 }
2164 rocket(vec![stage("only", vec![nose_cone, airframe])])
2165 };
2166 let layout = design(
2167 attached("cockpit", Part::FinSet(fin(0.0075)), bottom(0.0)),
2168 true,
2169 )
2170 .layout()
2171 .unwrap();
2172 let (_, placed) = layout.find("cockpit").unwrap();
2173 close(placed.fore_station_m, 0.15, 1e-15, "root leading edge");
2174 close(placed.body_radius_m.unwrap(), 0.0225, 1e-15, "body radius");
2175 close(
2176 placed.own.mass_kg,
2177 fin(0.0075).mass_properties(0.0225).unwrap().mass_kg,
2178 1e-15,
2179 "mass",
2180 );
2181 let refused = |child: Component, on_the_nose: bool, says: &str| {
2182 let err = design(child, on_the_nose).layout().unwrap_err().to_string();
2183 assert!(err.contains(says), "{says}: {err}");
2184 };
2185 refused(
2186 attached("rising", Part::FinSet(fin(0.0075)), bottom(0.0)),
2187 false,
2188 "needs a level root",
2189 );
2190 refused(
2191 attached("level", fins(0.05, 0.04), bottom(0.0)),
2192 true,
2193 "stands",
2194 );
2195 refused(
2196 attached("short", Part::FinSet(fin(0.0075)), bottom(0.01)),
2197 true,
2198 "must stay on it",
2199 );
2200 let lug = Part::LaunchLug(crate::LaunchLug {
2201 length_m: 0.03,
2202 outer_radius_m: 0.003,
2203 thickness_m: 0.0005,
2204 angle_rad: 0.0,
2205 count: 1,
2206 spacing_m: 0.0,
2207 material: crate::testing::cardboard(),
2208 });
2209 refused(
2210 attached("lug", lug, bottom(0.0)),
2211 true,
2212 "a launch_lug attaches to a body tube, not a nose_cone",
2213 );
2214 }
2215
2216 #[test]
2220 fn automatic_radii_follow_neighbours_across_stages() {
2221 let auto = |mut c: Component, dims: &[AutoDimension]| {
2222 c.auto = dims.to_vec();
2223 c
2224 };
2225 let transition = Part::Transition(Transition {
2226 shape: crate::NoseShape::Conical {},
2227 clipped: false,
2228 length_m: 0.1,
2229 fore_radius_m: 0.0,
2230 aft_radius_m: 0.04,
2231 wall: crate::Wall::Filled {},
2232 fore_shoulder: Some(Shoulder {
2233 length_m: 0.05,
2234 outer_radius_m: 0.0,
2235 thickness_m: 0.002,
2236 capped: false,
2237 }),
2238 aft_shoulder: None,
2239 material: crate::testing::cardboard(),
2240 });
2241 let mut nose_cone = auto(body("nose", nose(0.2, 0.0)), &[AutoDimension::BaseRadius]);
2242 if let Part::NoseCone(n) = &mut nose_cone.part {
2243 n.shoulder = Some(Shoulder {
2244 length_m: 0.05,
2245 outer_radius_m: 0.0,
2246 thickness_m: 0.002,
2247 capped: true,
2248 });
2249 }
2250 nose_cone.auto.push(AutoDimension::ShoulderRadius);
2251 let design = rocket(vec![
2252 stage(
2253 "upper",
2254 vec![nose_cone, body("upper-tube", tube(0.5, 0.03, 0.001))],
2255 ),
2256 stage(
2257 "booster",
2258 vec![
2259 auto(
2260 body("interstage", transition),
2261 &[AutoDimension::ForeRadius, AutoDimension::ForeShoulderRadius],
2262 ),
2263 auto(
2264 body("booster-tube", tube(0.5, 0.0, 0.001)),
2265 &[AutoDimension::OuterRadius],
2266 ),
2267 ],
2268 ),
2269 ]);
2270 let layout = design.layout().unwrap();
2271 let part = |id: &str| layout.find(id).unwrap().1.part.clone();
2272 let Part::NoseCone(n) = part("nose") else {
2273 panic!()
2274 };
2275 assert_eq!(n.base_radius_m, 0.03);
2276 assert_eq!(n.shoulder.unwrap().outer_radius_m, 0.03 - 0.001);
2277 let Part::Transition(t) = part("interstage") else {
2278 panic!()
2279 };
2280 assert_eq!(t.fore_radius_m, 0.03);
2281 assert_eq!(t.fore_shoulder.unwrap().outer_radius_m, 0.03 - 0.001);
2282 let Part::BodyTube(b) = part("booster-tube") else {
2283 panic!()
2284 };
2285 assert_eq!(b.outer_radius_m, 0.04);
2286
2287 let design = rocket(vec![stage(
2289 "only",
2290 vec![
2291 auto(body("nose", nose(0.2, 0.0)), &[AutoDimension::BaseRadius]),
2292 auto(
2293 body("a", tube(0.3, 0.0, 0.001)),
2294 &[AutoDimension::OuterRadius],
2295 ),
2296 body("b", tube(0.3, 0.05, 0.001)),
2297 ],
2298 )]);
2299 let layout = design.layout().unwrap();
2300 assert_eq!(layout.find("a").unwrap().1.part.aft_radius_m(), Some(0.05));
2301 assert_eq!(
2302 layout.find("nose").unwrap().1.part.aft_radius_m(),
2303 Some(0.05)
2304 );
2305 let design = rocket(vec![stage(
2307 "only",
2308 vec![
2309 body("nose", nose(0.2, 0.02)),
2310 auto(
2311 body("a", tube(0.3, 0.0, 0.001)),
2312 &[AutoDimension::OuterRadius],
2313 ),
2314 body("b", tube(0.3, 0.05, 0.001)),
2315 ],
2316 )]);
2317 let layout = design.layout().unwrap();
2318 assert_eq!(layout.find("a").unwrap().1.part.aft_radius_m(), Some(0.02));
2319
2320 let design = rocket(vec![stage(
2322 "only",
2323 vec![
2324 auto(body("nose", nose(0.2, 0.0)), &[AutoDimension::BaseRadius]),
2325 auto(
2326 body("a", tube(0.3, 0.0, 0.001)),
2327 &[AutoDimension::OuterRadius],
2328 ),
2329 ],
2330 )]);
2331 assert!(matches!(
2332 design.layout(),
2333 Err(DesignError::Tree { ref id, .. }) if id == "nose"
2334 ));
2335 }
2336
2337 fn cone_transition(fore_radius_m: f64, aft_radius_m: f64) -> Part {
2339 Part::Transition(Transition {
2340 shape: crate::NoseShape::Conical {},
2341 clipped: false,
2342 length_m: 0.1,
2343 fore_radius_m,
2344 aft_radius_m,
2345 wall: crate::Wall::Shell { thickness_m: 0.001 },
2346 fore_shoulder: None,
2347 aft_shoulder: None,
2348 material: crate::testing::cardboard(),
2349 })
2350 }
2351
2352 #[test]
2358 fn unresolvable_body_radii_are_the_chain_with_nothing_fixed() {
2359 let auto = |mut c: Component, dims: &[AutoDimension]| {
2360 c.auto = dims.to_vec();
2361 c
2362 };
2363 let mut design = rocket(vec![stage(
2364 "only",
2365 vec![
2366 auto(body("nose", nose(0.3, 0.0)), &[AutoDimension::BaseRadius]),
2367 auto(
2368 body("upper", tube(0.5, 0.0, 0.002)),
2369 &[AutoDimension::OuterRadius],
2370 ),
2371 auto(
2372 body("shoulder", cone_transition(0.0, 0.022)),
2373 &[AutoDimension::ForeRadius],
2374 ),
2375 body("lower", tube(0.45, 0.022, 0.0018)),
2376 ],
2377 )]);
2378 let listed = |design: &Rocket| -> Vec<(usize, usize, String, AutoDimension)> {
2379 design
2380 .unresolvable_body_radii()
2381 .into_iter()
2382 .map(|r| (r.stage, r.component, r.id, r.dimension))
2383 .collect()
2384 };
2385 let chain = vec![
2386 (0, 0, "nose".to_owned(), AutoDimension::BaseRadius),
2387 (0, 1, "upper".to_owned(), AutoDimension::OuterRadius),
2388 (0, 2, "shoulder".to_owned(), AutoDimension::ForeRadius),
2389 ];
2390 assert_eq!(listed(&design), chain);
2391 assert!(design.layout().is_err());
2392
2393 let filled = design.fill_unresolvable_body_radii(0.025);
2394 let filled: Vec<_> = filled
2395 .into_iter()
2396 .map(|r| (r.stage, r.component, r.id, r.dimension))
2397 .collect();
2398 assert_eq!(filled, chain);
2399 assert!(listed(&design).is_empty());
2400 assert!(
2401 design.stages[0]
2402 .components
2403 .iter()
2404 .all(|c| c.auto.is_empty())
2405 );
2406 let layout = design.layout().unwrap();
2407 let radius = |id: &str| layout.find(id).unwrap().1.part.aft_radius_m();
2408 assert_eq!(radius("nose"), Some(0.025));
2409 assert_eq!(radius("upper"), Some(0.025));
2410 let Part::Transition(t) = &layout.find("shoulder").unwrap().1.part else {
2411 panic!("a transition")
2412 };
2413 assert_eq!((t.fore_radius_m, t.aft_radius_m), (0.025, 0.022));
2414
2415 let mut design = three_fin_rocket();
2417 let before = design.clone();
2418 assert!(design.unresolvable_body_radii().is_empty());
2419 assert!(design.fill_unresolvable_body_radii(0.025).is_empty());
2420 assert_eq!(design, before);
2421 design.layout().unwrap();
2422 }
2423
2424 #[test]
2427 fn ring_and_packed_radii_come_from_parent_and_siblings() {
2428 let layout = three_fin_rocket().layout().unwrap();
2429 let part = |id: &str| layout.find(id).unwrap().1.part.clone();
2430 for id in ["ring-fore", "ring-aft"] {
2431 let Part::CenteringRing(r) = part(id) else {
2432 panic!()
2433 };
2434 assert_eq!(r.outer_radius_m, 0.027 - 0.0015, "{id}");
2435 assert_eq!(r.inner_radius_m, 0.020, "{id}");
2436 }
2437 let Part::Parachute(p) = part("chute") else {
2438 panic!()
2439 };
2440 assert_eq!(p.packing.radius_m, 0.027 - 0.0015);
2441
2442 let mut design = three_fin_rocket();
2443 let airframe = &mut design.stages[0].components[1];
2444 airframe.children[1].position = Some(top(0.1));
2446 let layout = design.layout().unwrap();
2447 let Part::CenteringRing(r) = layout.find("ring-fore").unwrap().1.part.clone() else {
2448 panic!()
2449 };
2450 assert_eq!(r.inner_radius_m, 0.0);
2451 }
2452
2453 #[test]
2457 fn an_automatic_tube_fin_radius_closes_the_ring_around_its_body() {
2458 let tubes = |count: u32, thickness_m: f64| {
2459 Part::TubeFinSet(crate::TubeFinSet {
2460 count,
2461 length_m: 0.1,
2462 outer_radius_m: 0.0,
2463 thickness_m,
2464 base_angle_rad: 0.0,
2465 material: crate::Material::bulk("cardboard", 680.0),
2466 })
2467 };
2468 let with = |part: Part, auto: bool, on: usize| {
2469 let mut design = three_fin_rocket();
2470 let mut child = attached("tubes", part, bottom(0.0));
2471 if auto {
2472 child.auto = vec![AutoDimension::OuterRadius];
2473 }
2474 design.stages[0].components[on].children.push(child);
2475 design.layout()
2476 };
2477 let resolved = |layout: &Layout| match &layout.find("tubes").unwrap().1.part {
2478 Part::TubeFinSet(set) => (set.outer_radius_m, set.thickness_m),
2479 other => panic!("{other:?}"),
2480 };
2481 let layout = with(tubes(6, 0.001), true, 1).unwrap();
2482 let (r, t) = resolved(&layout);
2483 close(r, 0.027, 1e-15, "six");
2484 assert_eq!(t, 0.001);
2485 let mut stated = tubes(6, 0.001);
2486 if let Part::TubeFinSet(set) = &mut stated {
2487 set.outer_radius_m = r;
2488 }
2489 let same = with(stated, false, 1).unwrap();
2490 assert_eq!(
2491 layout.find("tubes").unwrap().1.own,
2492 same.find("tubes").unwrap().1.own
2493 );
2494 let (r, _) = resolved(&with(tubes(4, 0.001), true, 1).unwrap());
2495 close(r, 0.027 * (1.0 + 2f64.sqrt()), 1e-15, "four");
2496 let (r, _) = resolved(&with(tubes(2, 0.001), true, 1).unwrap());
2497 assert_eq!(r, 0.027);
2498 let (r, t) = resolved(&with(tubes(6, 0.05), true, 1).unwrap());
2499 assert_eq!(t, r);
2500 let error = with(tubes(6, 0.001), true, 0).unwrap_err().to_string();
2502 assert!(error.contains("attaches to a body tube"), "{error}");
2503 }
2504
2505 #[test]
2510 fn an_automatic_radius_inside_a_nose_is_its_bore_at_the_narrow_end() {
2511 let with = |part: Part, position: Position| {
2512 let mut design = three_fin_rocket();
2513 let mut child = attached("inside", part, position);
2514 child.auto = vec![AutoDimension::OuterRadius];
2515 design.stages[0].components[0].children = vec![child];
2516 design.layout()
2517 };
2518 let radius = |layout: &Layout| match &layout.find("inside").unwrap().1.part {
2519 Part::InnerTube(tube) => (tube.outer_radius_m, tube.thickness_m),
2520 Part::CenteringRing(ring) => (ring.outer_radius_m, ring.inner_radius_m),
2521 other => panic!("{other:?}"),
2522 };
2523 let coupler = || inner_tube(0.1, 0.0, 0.001);
2524 let (r, t) = radius(&with(coupler(), bottom(0.0)).unwrap());
2526 close(r, 0.135 * 0.1 - 0.002, 1e-17, "at the bottom");
2527 assert_eq!(t, 0.001);
2528 let (r, _) = radius(&with(coupler(), bottom(0.05)).unwrap());
2530 close(r, 0.135 * 0.15 - 0.002, 1e-17, "past the base");
2531 let (r, bore) = radius(&with(ring(0.006, 0.0, 0.005), bottom(0.0)).unwrap());
2533 close(r, 0.135 * 0.194 - 0.002, 1e-17, "a ring");
2534 assert_eq!(bore, 0.005);
2535 let (r, t) = radius(&with(inner_tube(0.1, 0.0, 0.02), bottom(0.0)).unwrap());
2537 assert_eq!(t, r);
2538 let error = with(coupler(), top(0.0)).unwrap_err();
2540 assert!(
2541 matches!(
2542 &error,
2543 DesignError::InComponent { id, source }
2544 if id == "inside"
2545 && matches!(**source, DesignError::Domain { what: "outer radius", value } if value == 0.0)
2546 ),
2547 "{error:?}"
2548 );
2549 let mut design = three_fin_rocket();
2551 if let Part::NoseCone(nose) = &mut design.stages[0].components[0].part {
2552 nose.wall = Wall::Filled {};
2553 }
2554 let mut child = attached("inside", coupler(), bottom(0.0));
2555 child.auto = vec![AutoDimension::OuterRadius];
2556 design.stages[0].components[0].children = vec![child];
2557 let error = design.layout().unwrap_err().to_string();
2558 assert!(error.contains("a nose_cone has none"), "{error}");
2559
2560 let transition = Part::Transition(Transition {
2563 shape: crate::shapes::NoseShape::Conical {},
2564 clipped: false,
2565 length_m: 0.1,
2566 fore_radius_m: 0.03,
2567 aft_radius_m: 0.02,
2568 wall: Wall::Shell { thickness_m: 0.002 },
2569 fore_shoulder: None,
2570 aft_shoulder: None,
2571 material: crate::testing::cardboard(),
2572 });
2573 let mut part = body("transition", transition);
2574 let mut child = attached("inside", inner_tube(0.05, 0.0, 0.001), top(0.0));
2575 child.auto = vec![AutoDimension::OuterRadius];
2576 part.children = vec![child];
2577 let design = rocket(vec![stage(
2578 "only",
2579 vec![body("tube", tube(0.3, 0.03, 0.001)), part],
2580 )]);
2581 let (r, _) = radius(&design.layout().unwrap());
2582 close(r, 0.025 - 0.002, 1e-17, "a transition");
2583 }
2584
2585 #[test]
2591 fn a_cluster_repeats_what_it_holds_in_every_tube() {
2592 let three: Vec<[f64; 2]> = [90.0_f64, 210.0, 330.0]
2593 .iter()
2594 .map(|a| [0.02 * a.to_radians().cos(), 0.02 * a.to_radians().sin()])
2595 .collect();
2596 let with_block = |cluster_m: Vec<[f64; 2]>| {
2597 let mut design = crate::testing::three_fin_rocket();
2598 let mount = &mut design.stages[0].components[1].children[0];
2599 if let Part::InnerTube(tube) = &mut mount.part {
2600 tube.cluster_m = cluster_m;
2601 }
2602 mount.children = vec![attached("block", inner_tube(0.01, 0.019, 0.005), top(0.0))];
2603 design
2604 };
2605 let single = with_block(Vec::new()).layout().unwrap();
2606 let clustered_design = with_block(three.clone());
2607 let clustered = clustered_design.layout().unwrap();
2608 let (_, one) = single.find("block").unwrap();
2609 let (_, block) = clustered.find("block").unwrap();
2610 assert_eq!(one.copies, [Placement::HERE]);
2611 let moved: Vec<Placement> = three.iter().copied().map(Placement::moved).collect();
2612 assert_eq!(block.copies, moved);
2613 close(
2614 block.own.mass_kg,
2615 3.0 * one.own.mass_kg,
2616 1e-15,
2617 "three blocks",
2618 );
2619 assert!(block.own.cg_m.truncate().length() < 1e-17);
2620 let (_, tube) = single.find("mmt").unwrap();
2621 let extra = 2.0 * (tube.own.mass_kg + one.own.mass_kg);
2622 close(
2623 clustered.structure.mass_kg,
2624 single.structure.mass_kg + extra,
2625 1e-14,
2626 "structure",
2627 );
2628 let (_, mount) = clustered.find("mmt").unwrap();
2629 assert_eq!(mount.contents_copies().unwrap(), moved);
2630 let (_, ring) = clustered.find("ring-fore").unwrap();
2631 let Part::CenteringRing(ring) = &ring.part else {
2632 panic!("a ring");
2633 };
2634 assert_eq!(ring.inner_radius_m, 0.02);
2635 let findings = crate::checks::check(&clustered_design).unwrap();
2636 for id in ["ring-fore", "ring-aft"] {
2637 assert!(
2638 findings.contains(&crate::Finding::RingOverlapsInnerTube {
2639 ring: id.to_owned(),
2640 tube: "mmt".to_owned(),
2641 }),
2642 "{findings:?}"
2643 );
2644 }
2645 assert!(
2647 findings.iter().any(|f| matches!(
2648 f,
2649 crate::Finding::InternalPartWiderThanParent { component, reach_m, .. }
2650 if component == "mmt" && (reach_m - 0.04).abs() < 1e-15
2651 )),
2652 "{findings:?}"
2653 );
2654 assert!(
2655 !crate::checks::check(&with_block(Vec::new()))
2656 .unwrap()
2657 .iter()
2658 .any(|f| matches!(f, crate::Finding::RingOverlapsInnerTube { .. }))
2659 );
2660 }
2661
2662 fn podded(count: u32, angle_rad: f64) -> Rocket {
2666 let mut design = three_fin_rocket();
2667 let mut pod_tube = body("pod-tube", tube(0.3, 0.012, 0.001));
2668 pod_tube.children = vec![attached(
2669 "pod-mass",
2670 mass_component(0.05, 0.1, 0.008),
2671 top(0.02),
2672 )];
2673 let mut pods = attached(
2674 "pods",
2675 Part::PodSet(PodSet {
2676 count,
2677 radial_offset_m: 0.05,
2678 angle_rad,
2679 }),
2680 top(0.1),
2681 );
2682 pods.children = vec![pod_tube];
2683 design.stages[0].components[1].children.push(pods);
2684 design
2685 }
2686
2687 fn boosted(count: u32, angle_rad: f64) -> Rocket {
2689 let mut design = podded(count, angle_rad);
2690 let pods = design.stages[0].components[1]
2691 .children
2692 .pop()
2693 .expect("podded hangs its pods last");
2694 let Part::PodSet(set) = pods.part else {
2695 unreachable!("podded's last child is its pod set")
2696 };
2697 let mut boosters = stage("pods", pods.children);
2698 boosters.parallel = Some(ParallelStage {
2699 on: "airframe".to_owned(),
2700 position: pods.position.expect("an attached part has a position"),
2701 pods: set,
2702 });
2703 design.stages.push(boosters);
2704 design
2705 }
2706
2707 #[test]
2712 fn a_parallel_stage_lays_out_as_a_pod_set_of_its_own_stage() {
2713 for (count, angle_rad) in [(2, 0.0), (1, 0.3), (3, 1.0)] {
2714 let pods = podded(count, angle_rad).layout().unwrap();
2715 let boosters = boosted(count, angle_rad).layout().unwrap();
2716 assert_eq!(boosters.components.len(), pods.components.len());
2717 let own_stage = ["pods", "pod-tube", "pod-mass"];
2718 for (got, want) in boosters.components.iter().zip(&pods.components) {
2719 assert_eq!(got.id, want.id, "depth-first order kept");
2720 let theirs = own_stage.contains(&got.id.as_str());
2721 assert_eq!(got.stage, usize::from(theirs), "{}'s stage", got.id);
2722 let mut same = got.clone();
2723 same.stage = want.stage;
2724 if got.id == "airframe" {
2725 let (_, set) = pods.find("pods").unwrap();
2728 close(
2729 got.with_children.mass_kg,
2730 want.with_children.mass_kg - set.with_children.mass_kg,
2731 1e-15,
2732 "the tube without its pods",
2733 );
2734 same.with_children = want.with_children;
2735 }
2736 assert_eq!(&same, want, "{}", got.id);
2737 }
2738 let (_, set) = pods.find("pods").unwrap();
2739 assert_eq!(boosters.stages.len(), 2);
2740 let stage = &boosters.stages[1];
2741 assert_eq!(stage.id, "pods");
2742 assert_eq!(stage.hung_on, Some(0));
2743 assert_eq!(boosters.stages[0].hung_on, None);
2744 assert_eq!(stage.mass, set.with_children);
2745 assert_eq!(
2746 (stage.fore_station_m, stage.aft_station_m),
2747 (set.fore_station_m, set.aft_station_m())
2748 );
2749 assert_eq!(boosters.length_m, pods.length_m, "the pods don't stack");
2750 close(
2751 boosters.structure.mass_kg,
2752 pods.structure.mass_kg,
2753 1e-15,
2754 "structure mass",
2755 );
2756 close(
2757 (boosters.structure.cg_m - pods.structure.cg_m).length(),
2758 0.0,
2759 1e-15,
2760 "structure center",
2761 );
2762 let inertia = boosters.structure.inertia_kg_m2 - pods.structure.inertia_kg_m2;
2763 for column in 0..3 {
2764 close(
2765 inertia.col(column).length(),
2766 0.0,
2767 1e-15,
2768 "structure inertia",
2769 );
2770 }
2771 }
2772
2773 let boosters = boosted(2, 0.0);
2775 let tube_kg = 790.0 * std::f64::consts::PI * (0.012_f64.powi(2) - 0.011_f64.powi(2)) * 0.3;
2776 close(
2777 boosters.layout().unwrap().stages[1].mass.mass_kg,
2778 2.0 * (tube_kg + 0.05),
2779 1e-15,
2780 "the stage's mass",
2781 );
2782 let page = include_str!("../../../docs/physics/design.md");
2784 let block = page
2785 .split("\n## Parallel stages\n")
2786 .nth(1)
2787 .and_then(|stages| stages.split("```json\n").nth(1))
2788 .and_then(|json| json.split("```").next())
2789 .expect("the Parallel stages section has a JSON block");
2790 let mut written = boosters.stages[1].clone();
2791 written.components[0].children.clear();
2792 assert_eq!(
2793 serde_json::from_str::<Stage>(block).unwrap(),
2794 written,
2795 "{block}"
2796 );
2797 }
2798
2799 #[test]
2802 fn a_parallel_stage_hangs_on_a_body_tube_of_an_earlier_axial_stage() {
2803 let refused = |design: &Rocket, needle: &str| match design.layout() {
2804 Err(DesignError::Tree { id, message }) => {
2805 assert_eq!(id, "pods");
2806 assert!(message.contains(needle), "{message}");
2807 }
2808 other => panic!("expected a tree error naming `{needle}`, got {other:?}"),
2809 };
2810 let hangs = "hangs on a body tube of an axial stage before it";
2811 let on = |on: &str| {
2812 let mut design = boosted(2, 0.0);
2813 design.stages[1].parallel.as_mut().unwrap().on = on.to_owned();
2814 design
2815 };
2816 refused(&on("nose"), hangs);
2817 refused(&on("fins"), hangs);
2818 refused(&on("nowhere"), hangs);
2819 refused(&on("pod-tube"), hangs);
2821 let mut first = boosted(2, 0.0);
2822 first.stages.swap(0, 1);
2823 refused(&first, hangs);
2824 let mut nested = boosted(2, 0.0);
2826 let mut more = nested.stages[1].clone();
2827 more.id = "more".to_owned();
2828 more.components[0].id = "more-tube".to_owned();
2829 more.components[0].children[0].id = "more-mass".to_owned();
2830 more.parallel.as_mut().unwrap().on = "pod-tube".to_owned();
2831 nested.stages.push(more);
2832 match nested.layout() {
2833 Err(DesignError::Tree { id, message }) => {
2834 assert_eq!(id, "more");
2835 assert!(message.contains(hangs), "{message}");
2836 }
2837 other => panic!("expected a tree error, got {other:?}"),
2838 }
2839 let mut after = boosted(2, 0.0);
2841 after.stages[1].parallel.as_mut().unwrap().position = Position::After { aft_offset_m: 0.0 };
2842 refused(&after, "not after a sibling");
2843 let covered = "under a mass override that covers what the tube or its stage holds";
2846 let heavy = Overrides {
2847 mass_kg: Some(1.0),
2848 ..Overrides::default()
2849 };
2850 let mut staged = boosted(2, 0.0);
2851 staged.stages[0].overrides = heavy;
2852 refused(&staged, covered);
2853 let mut tube = boosted(2, 0.0);
2854 tube.stages[0].components[1].overrides = heavy;
2855 tube.layout().unwrap();
2856 tube.stages[0].components[1].overrides_include_children = true;
2857 refused(&tube, covered);
2858 let mut loose = boosted(2, 0.0);
2860 loose.stages[1].components[0] = attached("pod-tube", fins(0.1, 0.06), top(0.0));
2861 assert!(loose.layout().is_err());
2862 let mut twice = boosted(2, 0.0);
2864 twice.stages[1].id = "airframe".to_owned();
2865 assert!(matches!(twice.layout(), Err(DesignError::DuplicateId(_))));
2866 }
2867
2868 #[test]
2871 fn a_pod_is_its_stack_repeated_with_its_parallel_axis_term() {
2872 use std::f64::consts::{FRAC_PI_2, PI};
2873 let (rho, length, ro, ri) = (790.0, 0.3, 0.012, 0.011);
2875 let m_t = rho * PI * (ro * ro - ri * ri) * length;
2876 let (ax_t, tr_t) = (
2877 m_t * (ro * ro + ri * ri) / 2.0,
2878 m_t * (3.0 * (ro * ro + ri * ri) + length * length) / 12.0,
2879 );
2880 let z_t = 0.3 + 0.15;
2881 let (m_c, l_c, r_c) = (0.05, 0.1, 0.008);
2882 let (ax_c, tr_c) = (
2883 m_c * r_c * r_c / 2.0,
2884 m_c * (3.0 * r_c * r_c + l_c * l_c) / 12.0,
2885 );
2886 let z_c = 0.3 + 0.02 + 0.05;
2887 let m1 = m_t + m_c;
2888 let z1 = (m_t * z_t + m_c * z_c) / m1;
2889 let axial1 = ax_t + ax_c;
2890 let transverse1 = tr_t + m_t * (z_t - z1).powi(2) + tr_c + m_c * (z_c - z1).powi(2);
2891 let d = 0.05;
2892
2893 let base = three_fin_rocket().layout().unwrap();
2895 let two = podded(2, 0.0).layout().unwrap();
2896 let (_, pods) = two.find("pods").unwrap();
2897 let mass = pods.with_children;
2898 close(mass.mass_kg, 2.0 * m1, 1e-15, "two pods' mass");
2899 close(mass.cg_m.x, 0.0, 1e-16, "center x");
2900 close(mass.cg_m.y, 0.0, 1e-16, "center y");
2901 close(-mass.cg_m.z, z1, 1e-15, "center station");
2902 let i = mass.inertia_kg_m2;
2903 close(i.col(0).x, 2.0 * transverse1, 1e-15, "I_xx");
2905 close(i.col(1).y, 2.0 * (transverse1 + m1 * d * d), 1e-15, "I_yy");
2906 close(i.col(2).z, 2.0 * (axial1 + m1 * d * d), 1e-15, "I_zz");
2907 for (k, product) in [i.col(1).x, i.col(2).x, i.col(2).y].into_iter().enumerate() {
2908 close(product, 0.0, 1e-16, &format!("product {k}"));
2909 }
2910 close(pods.length_m, 0.3, 1e-15, "the pod's extent");
2911 assert_eq!(pods.own.mass_kg, 0.0);
2912 let (_, pod_tube) = two.find("pod-tube").unwrap();
2913 close(pod_tube.fore_station_m, 0.3, 1e-15, "pod tube station");
2914 assert_eq!(pod_tube.copies.len(), 2);
2915 close(pod_tube.copies[1].offset_m[0], -d, 1e-16, "second pod x");
2916 assert_eq!(pod_tube.copies[1].roll_rad, PI);
2917 close(
2918 two.structure.mass_kg,
2919 base.structure.mass_kg + 2.0 * m1,
2920 1e-14,
2921 "structure",
2922 );
2923
2924 let one = podded(1, FRAC_PI_2).layout().unwrap();
2927 let (_, pod) = one.find("pods").unwrap();
2928 let mass = pod.with_children;
2929 close(mass.mass_kg, m1, 1e-15, "one pod's mass");
2930 close(mass.cg_m.x, 0.0, 1e-16, "center x");
2931 close(mass.cg_m.y, d, 1e-16, "center y");
2932 let i = mass.inertia_kg_m2;
2933 close(i.col(0).x, transverse1, 1e-15, "I_xx about its center");
2934 close(i.col(2).z, axial1, 1e-15, "I_zz about its center");
2935 let about_origin = mass.inertia_about(DVec3::ZERO);
2936 close(about_origin.col(2).y, m1 * d * z1, 1e-15, "I_yz");
2937 close(
2938 about_origin.col(2).z,
2939 axial1 + m1 * d * d,
2940 1e-15,
2941 "I_zz about the axis",
2942 );
2943 close(
2944 one.structure.cg_m.y,
2945 (base.structure.mass_kg * base.structure.cg_m.y + m1 * d)
2946 / (base.structure.mass_kg + m1),
2947 1e-15,
2948 "the rocket's center across the axis",
2949 );
2950 }
2951
2952 #[test]
2958 fn what_a_pod_holds_turns_with_it_and_overrides_keep_their_scope() {
2959 use std::f64::consts::{FRAC_PI_2, PI};
2960 let (d, e) = (0.05, 0.003);
2961 let off_axis = |count: u32, angle_rad: f64| {
2962 let mut design = podded(count, angle_rad);
2963 let pod_tube = &mut design.stages[0].components[1].children[6].children[0];
2964 if let Part::MassComponent(mass) = &mut pod_tube.children[0].part {
2965 mass.packing.radial_offset_m = e;
2966 }
2967 design
2968 };
2969 let (rho, length, ro, ri) = (790.0, 0.3, 0.012, 0.011);
2970 let m_t = rho * PI * (ro * ro - ri * ri) * length;
2971 let m_c = 0.05;
2972
2973 let two = off_axis(2, 0.0).layout().unwrap();
2974 let pods = two.find("pods").unwrap().1.with_children;
2975 close(pods.cg_m.x, 0.0, 1e-16, "the pair's center x");
2976 close(pods.cg_m.y, 0.0, 1e-16, "the pair's center y");
2977 let roll = pods
2978 .inertia_about(DVec3::new(0.0, 0.0, pods.cg_m.z))
2979 .col(2)
2980 .z;
2981 let hand = 2.0
2982 * (m_t * (ro * ro + ri * ri) / 2.0
2983 + m_t * d * d
2984 + m_c * 0.008 * 0.008 / 2.0
2985 + m_c * (d + e) * (d + e));
2986 close(roll, hand, 1e-15, "the pair's roll inertia");
2987
2988 let one = off_axis(1, FRAC_PI_2).layout().unwrap();
2989 let (_, mass) = one.find("pod-mass").unwrap();
2990 close(mass.own.cg_m.x, 0.0, 1e-16, "the turned mass's x");
2991 close(mass.own.cg_m.y, d + e, 1e-16, "the turned mass's y");
2992
2993 let mut design = podded(1, FRAC_PI_2);
2995 let pod_tube = &mut design.stages[0].components[1].children[6].children[0];
2996 pod_tube.overrides.cg_xy_m = Some([0.01, 0.0]);
2997 let layout = design.layout().unwrap();
2998 let (_, tube) = layout.find("pod-tube").unwrap();
2999 close(tube.own.cg_m.x, 0.0, 1e-16, "the overridden center's x");
3000 close(
3001 tube.own.cg_m.y,
3002 d + 0.01,
3003 1e-16,
3004 "the overridden center's y",
3005 );
3006
3007 let set_to = |mass_kg: f64, on_set: bool| {
3008 let mut design = podded(2, 0.0);
3009 let pods = &mut design.stages[0].components[1].children[6];
3010 if on_set {
3011 pods.overrides.mass_kg = Some(mass_kg);
3012 pods.overrides_include_children = true;
3013 } else {
3014 pods.children[0].children[0].overrides.mass_kg = Some(mass_kg);
3015 }
3016 let layout = design.layout().unwrap();
3017 layout.find("pods").unwrap().1.with_children.mass_kg
3018 };
3019 close(set_to(0.3, true), 0.3, 1e-15, "an override on the pod set");
3020 close(
3021 set_to(0.1, false),
3022 2.0 * (m_t + 0.1),
3023 1e-15,
3024 "an override in each pod",
3025 );
3026 }
3027
3028 #[test]
3030 fn a_pod_s_nose_is_not_the_reference_nose() {
3031 let mut design = podded(2, 0.0);
3032 let pods = &mut design.stages[0].components[1].children[6];
3033 pods.children.insert(0, body("pod-nose", nose(0.06, 0.012)));
3034 design.stages[0].components.remove(0);
3035 design.reference_diameter = ReferenceDiameter::NoseBase {};
3036 let error = design.layout().unwrap_err().to_string();
3037 assert!(error.contains("there is no nose cone"), "{error}");
3038 }
3039
3040 #[test]
3044 fn pods_stack_hold_motors_and_refuse_the_wrong_trees() {
3045 let mut design = podded(3, 0.0);
3046 let pods = &mut design.stages[0].components[1].children[6];
3047 pods.position = Some(bottom(0.2));
3049 let mut pod_nose = body("pod-nose", nose(0.06, 0.0));
3050 pod_nose.auto = vec![AutoDimension::BaseRadius];
3051 pods.children.insert(0, pod_nose);
3052 pods.children[1].motor_mount = Some(MotorMount { overhang_m: 0.005 });
3053 design.configurations.push(Configuration {
3054 id: "pods".to_owned(),
3055 name: String::new(),
3056 motors: vec![crate::testing::motor("pod-tube", 0.018, 0.1)],
3057 });
3058 let layout = design.layout().unwrap();
3059 let (_, set) = layout.find("pods").unwrap();
3060 close(set.length_m, 0.36, 1e-15, "the pods' extent");
3061 let (_, cone) = layout.find("pod-nose").unwrap();
3062 let Part::NoseCone(cone_part) = &cone.part else {
3063 panic!("a nose cone");
3064 };
3065 assert_eq!(cone_part.base_radius_m, 0.012);
3066 close(cone.fore_station_m, 0.84, 1e-15, "pod nose station");
3067 close(station(&layout, "pod-tube"), 0.9, 1e-15, "pod tube station");
3068 let three = PodSet {
3069 count: 3,
3070 radial_offset_m: 0.05,
3071 angle_rad: 0.0,
3072 }
3073 .pods()
3074 .unwrap();
3075 assert_eq!(cone.copies, three);
3076 assert_eq!(layout.find("pod-mass").unwrap().1.copies, three);
3077
3078 let assembly = design.assemble("pods").unwrap();
3079 assert_eq!(assembly.motors.len(), 3);
3080 for (motor, pod) in assembly.motors.iter().zip(&three) {
3081 assert_eq!([motor.nozzle_m.x, motor.nozzle_m.y], pod.offset_m);
3082 close(motor.nozzle_station_m(), 1.2 + 0.005, 1e-15, "nozzle");
3083 }
3084
3085 let findings = crate::checks::check(&design).unwrap();
3087 assert!(
3088 !findings.iter().any(|f| format!("{f:?}").contains("pod")),
3089 "{findings:?}"
3090 );
3091
3092 let page = include_str!("../../../docs/physics/design.md");
3094 let block = page
3095 .split("\n## Pods\n")
3096 .nth(1)
3097 .and_then(|pods| pods.split("```json\n").nth(1))
3098 .and_then(|json| json.split("```").next())
3099 .expect("the Pods section has a JSON block");
3100 let mut written = podded(2, 0.0).stages[0].components[1].children[6].clone();
3101 written.children[0].children.clear();
3102 assert_eq!(
3103 serde_json::from_str::<Component>(block).unwrap(),
3104 written,
3105 "{block}"
3106 );
3107
3108 let json = serde_json::to_string(&design).unwrap();
3109 assert!(json.contains("\"pod_set\""));
3110 assert_eq!(serde_json::from_str::<Rocket>(&json).unwrap(), design);
3111
3112 let refused = |edit: &dyn Fn(&mut Component), want: &str| {
3113 let mut design = podded(2, 0.0);
3114 edit(&mut design.stages[0].components[1].children[6]);
3115 let error = design.layout().unwrap_err().to_string();
3116 assert!(error.contains(want), "{error}");
3117 };
3118 refused(
3119 &|pods| {
3120 pods.children
3121 .push(attached("fin", fins(0.1, 0.05), top(0.0)))
3122 },
3123 "a pod holds body components",
3124 );
3125 refused(
3126 &|pods| pods.children[0].position = Some(top(0.0)),
3127 "stacks and takes no position",
3128 );
3129 refused(
3130 &|pods| {
3131 if let Part::PodSet(set) = &mut pods.part {
3132 set.count = 0;
3133 }
3134 },
3135 "pod count",
3136 );
3137 refused(
3138 &|pods| {
3139 if let Part::PodSet(set) = &mut pods.part {
3140 set.radial_offset_m = f64::NAN;
3141 }
3142 },
3143 "pod radial offset",
3144 );
3145 refused(
3146 &|pods| {
3147 if let Part::PodSet(set) = &mut pods.part {
3148 set.count = PodSet::MAX_COUNT + 1;
3149 }
3150 },
3151 "pod count (1 to 64)",
3152 );
3153 refused(
3154 &|pods| pods.overrides.mass_kg = Some(0.2),
3155 "an override on it must cover its pods",
3156 );
3157 refused(
3158 &|pods| {
3159 let mut inner = pods.clone();
3160 inner.id = "inner-pods".to_owned();
3161 inner.children[0].id = "inner-tube".to_owned();
3162 inner.children[0].children[0].id = "inner-mass".to_owned();
3163 pods.children[0].children.push(inner);
3164 },
3165 "a pod set can't hang from a pod",
3166 );
3167 let mut on_nose = podded(2, 0.0);
3168 let pods = on_nose.stages[0].components[1].children.pop().unwrap();
3169 on_nose.stages[0].components[0].children.push(pods);
3170 let error = on_nose.layout().unwrap_err().to_string();
3171 assert!(error.contains("attaches to a body tube"), "{error}");
3172 }
3173
3174 #[test]
3180 fn a_pod_of_no_length_holds_its_parts_at_the_pod_s_axis() {
3181 use std::f64::consts::PI;
3182 let (rho, length, ro, ri, d) = (790.0, 0.05, 0.004, 0.0035, 0.05);
3183 let lug = LaunchLug {
3184 length_m: length,
3185 outer_radius_m: ro,
3186 thickness_m: ro - ri,
3187 angle_rad: PI,
3188 count: 1,
3189 spacing_m: 0.0,
3190 material: crate::testing::cardboard(),
3191 };
3192 let mut phantom = body("phantom", tube(0.0, 0.0, 0.0));
3193 phantom.children = vec![attached(
3194 "pod-lug",
3195 Part::LaunchLug(lug),
3196 Position::Middle { aft_offset_m: 0.0 },
3197 )];
3198 let mut pods = attached(
3199 "pods",
3200 Part::PodSet(PodSet {
3201 count: 2,
3202 radial_offset_m: d,
3203 angle_rad: 0.0,
3204 }),
3205 top(0.1),
3206 );
3207 pods.children = vec![phantom];
3208 let mut design = three_fin_rocket();
3209 design.stages[0].components[1].children.push(pods);
3210 let base = three_fin_rocket().layout().unwrap();
3211 let layout = design.layout().unwrap();
3212
3213 let (_, set) = layout.find("pods").unwrap();
3214 assert_eq!(set.length_m, 0.0);
3215 let (_, phantom) = layout.find("phantom").unwrap();
3216 assert_eq!(phantom.own.mass_kg, 0.0);
3217 close(phantom.fore_station_m, 0.3, 1e-15, "the phantom's station");
3218 close(
3220 station(&layout, "pod-lug"),
3221 0.3 - 0.5 * length,
3222 1e-15,
3223 "lug station",
3224 );
3225 let m = rho * PI * (ro * ro - ri * ri) * length;
3226 let axial = m * (ro * ro + ri * ri) / 2.0;
3227 let transverse = m * (3.0 * (ro * ro + ri * ri) + length * length) / 12.0;
3228 let r = d - ro;
3229 let mass = set.with_children;
3230 close(mass.mass_kg, 2.0 * m, 1e-18, "two lugs' mass");
3231 close(mass.cg_m.x, 0.0, 1e-16, "center x");
3232 close(mass.cg_m.y, 0.0, 1e-16, "center y");
3233 close(-mass.cg_m.z, 0.3, 1e-15, "center station");
3234 let i = mass.inertia_kg_m2;
3235 close(i.col(0).x, 2.0 * transverse, 1e-18, "I_xx");
3236 close(i.col(1).y, 2.0 * (transverse + m * r * r), 1e-18, "I_yy");
3237 close(i.col(2).z, 2.0 * (axial + m * r * r), 1e-18, "I_zz");
3238 let (_, lug) = layout.find("pod-lug").unwrap();
3240 close(lug.own.cg_m.x, 0.0, 1e-16, "the pair's center");
3241 let Part::LaunchLug(one) = &lug.part else {
3242 unreachable!("the lug")
3243 };
3244 let one = MassProperties::placed(one.mass_properties(0.0).unwrap(), &lug.copies[..1]);
3245 close(one.cg_m.x, r, 1e-16, "the first lug across the axis");
3246
3247 let mut empty = design.clone();
3248 let pods = empty.stages[0].components[1].children.last_mut();
3249 pods.expect("the pod set").children.clear();
3250 let layout = empty.layout().unwrap();
3251 let (_, set) = layout.find("pods").unwrap();
3252 assert!(set.length_m.is_sign_positive());
3253 assert_eq!(set.with_children.mass_kg, 0.0);
3254 assert_eq!(set.with_children.inertia_kg_m2, DMat3::ZERO);
3255 assert_eq!(layout.structure, base.structure);
3256
3257 let mut flat = three_fin_rocket();
3259 flat.stages[0]
3260 .components
3261 .push(body("flat", tube(0.0, 0.0, 0.0)));
3262 let error = flat.layout().unwrap_err();
3263 assert!(
3264 matches!(
3265 error,
3266 DesignError::InComponent { ref id, ref source }
3267 if id == "flat" && matches!(**source, DesignError::Domain {
3268 what: "body component length",
3269 ..
3270 })
3271 ),
3272 "{error:?}"
3273 );
3274 }
3275
3276 #[test]
3280 fn an_override_inside_a_cluster_is_each_copy_s() {
3281 let three: Vec<[f64; 2]> = [90.0_f64, 210.0, 330.0]
3282 .iter()
3283 .map(|a| [0.02 * a.to_radians().cos(), 0.02 * a.to_radians().sin()])
3284 .collect();
3285 let with = |part: Part, mass_kg: Option<f64>| {
3286 let mut design = crate::testing::three_fin_rocket();
3287 let mount = &mut design.stages[0].components[1].children[0];
3288 if let Part::InnerTube(tube) = &mut mount.part {
3289 tube.cluster_m = three.clone();
3290 }
3291 let mut inside = attached("inside", part, top(0.0));
3292 inside.overrides.mass_kg = mass_kg;
3293 mount.children = vec![inside];
3294 design.layout().unwrap()
3295 };
3296 for part in [
3297 inner_tube(0.01, 0.019, 0.005),
3298 mass_component(0.05, 0.04, 0.015),
3299 ] {
3300 let free = with(part.clone(), None);
3301 let (_, one) = free.find("inside").unwrap();
3302 let each_kg = one.own.mass_kg / 3.0;
3303 let same = with(part.clone(), Some(each_kg));
3304 let (a, b) = (&free.structure, &same.structure);
3305 close(b.mass_kg, a.mass_kg, 1e-15, "structure mass");
3306 assert!(
3307 (b.cg_m - a.cg_m).length() < 1e-15,
3308 "{:?} vs {:?}",
3309 b.cg_m,
3310 a.cg_m
3311 );
3312 for (x, y) in [
3313 (a.inertia_kg_m2.x_axis, b.inertia_kg_m2.x_axis),
3314 (a.inertia_kg_m2.y_axis, b.inertia_kg_m2.y_axis),
3315 (a.inertia_kg_m2.z_axis, b.inertia_kg_m2.z_axis),
3316 ] {
3317 assert!((x - y).length() < 1e-15, "{x:?} vs {y:?}");
3318 }
3319 let doubled = with(part, Some(2.0 * each_kg));
3320 let (_, inside) = doubled.find("inside").unwrap();
3321 close(inside.own.mass_kg, 6.0 * each_kg, 1e-15, "doubled");
3322 assert!(inside.own.cg_m.truncate().length() < 1e-17);
3323 }
3324 }
3325
3326 #[test]
3328 fn tree_structure_matches_parts_placed_by_hand() {
3329 let design = three_fin_rocket();
3330 let layout = design.layout().unwrap();
3331 let bore = 0.027 - 0.0015;
3332 let placed = |part: Part, body_radius: Option<f64>, s: f64| {
3333 part.mass_properties(body_radius)
3334 .unwrap()
3335 .translated(DVec3::new(0.0, 0.0, -s))
3336 };
3337 let mut chute = design.stages[0].components[1].children[4].part.clone();
3338 if let Part::Parachute(p) = &mut chute {
3339 p.packing.radius_m = bore;
3340 }
3341 let parts = [
3342 placed(nose(0.2, 0.027), None, 0.0),
3343 placed(tube(0.8, 0.027, 0.0015), None, 0.2),
3344 placed(inner_tube(0.3, 0.020, 0.001), None, 0.7),
3345 placed(ring(0.006, bore, 0.020), None, 1.0 - 0.006 - 0.25),
3346 placed(ring(0.006, bore, 0.020), None, 1.0 - 0.006 - 0.02),
3347 placed(fins(0.1, 0.06), Some(0.027), 0.9),
3348 placed(chute, None, 0.25),
3349 placed(
3350 design.stages[0].components[1].children[5].part.clone(),
3351 Some(0.027),
3352 0.2 + 0.5 * (0.8 - 0.05),
3353 ),
3354 ];
3355 let want = MassProperties::combine(&parts);
3356 let got = layout.structure;
3357 close(got.mass_kg, want.mass_kg, 1e-15 * want.mass_kg, "mass");
3358 assert!(
3359 (got.cg_m - want.cg_m).length() < 1e-14,
3360 "{:?} vs {:?}",
3361 got.cg_m,
3362 want.cg_m
3363 );
3364 let scale = want
3365 .inertia_kg_m2
3366 .to_cols_array()
3367 .iter()
3368 .fold(0.0f64, |m, v| m.max(v.abs()));
3369 let diff = (got.inertia_kg_m2 - want.inertia_kg_m2)
3370 .to_cols_array()
3371 .iter()
3372 .fold(0.0f64, |m, v| m.max(v.abs()));
3373 assert!(diff < 1e-13 * scale, "{diff:e}");
3374 assert_eq!(layout.stages[0].mass, got);
3375 }
3376
3377 fn tensor_close(a: DMat3, b: DMat3, rel: f64) {
3378 let scale = b.to_cols_array().iter().fold(0.0f64, |m, v| m.max(v.abs()));
3379 let diff = (a - b)
3380 .to_cols_array()
3381 .iter()
3382 .fold(0.0f64, |m, v| m.max(v.abs()));
3383 assert!(diff <= rel * scale, "{a:?}\nvs\n{b:?}");
3384 }
3385
3386 #[test]
3389 fn overrides_rescale_move_and_replace() {
3390 let bay = || attached("bay", mass_component(0.4, 0.1, 0.02), top(0.3));
3391 let mut airframe = body("airframe", tube(0.8, 0.03, 0.001));
3392 airframe.children = vec![bay()];
3393 let plain = rocket(vec![stage(
3394 "s",
3395 vec![body("nose", nose(0.2, 0.03)), airframe.clone()],
3396 )]);
3397 let base = plain.layout().unwrap();
3398 let (_, tube_plain) = base.find("airframe").unwrap();
3399 let (_, bay_plain) = base.find("bay").unwrap();
3400
3401 let mut overridden = airframe.clone();
3403 overridden.overrides.mass_kg = Some(2.0 * tube_plain.own.mass_kg);
3404 let design = rocket(vec![stage(
3405 "s",
3406 vec![body("nose", nose(0.2, 0.03)), overridden.clone()],
3407 )]);
3408 let layout = design.layout().unwrap();
3409 let (_, t) = layout.find("airframe").unwrap();
3410 close(
3411 t.own.mass_kg,
3412 2.0 * tube_plain.own.mass_kg,
3413 1e-15,
3414 "scaled mass",
3415 );
3416 assert!((t.own.cg_m - tube_plain.own.cg_m).length() < 1e-15);
3417 tensor_close(
3418 t.own.inertia_kg_m2,
3419 tube_plain.own.inertia_kg_m2 * 2.0,
3420 1e-14,
3421 );
3422 let with = MassProperties::combine([&t.own, &bay_plain.with_children]);
3423 assert_eq!(t.with_children, with, "the child is still added");
3424
3425 overridden.overrides = Overrides {
3427 mass_kg: Some(1.5),
3428 cg_aft_m: Some(0.35),
3429 cg_xy_m: Some([0.001, -0.002]),
3430 inertia: Some(InertiaOverride::axisymmetric(0.001, 0.08)),
3431 };
3432 overridden.overrides_include_children = true;
3433 let design = rocket(vec![stage(
3434 "s",
3435 vec![body("nose", nose(0.2, 0.03)), overridden.clone()],
3436 )]);
3437 let layout = design.layout().unwrap();
3438 let (_, t) = layout.find("airframe").unwrap();
3439 assert_eq!(t.own, tube_plain.own, "own mass untouched");
3440 close(t.with_children.mass_kg, 1.5, 0.0, "mass");
3441 close(
3442 t.with_children.cg_m.z,
3443 -(0.2 + 0.35),
3444 1e-15,
3445 "center from the tube's forward end",
3446 );
3447 assert_eq!(
3448 (t.with_children.cg_m.x, t.with_children.cg_m.y),
3449 (0.001, -0.002)
3450 );
3451 assert_eq!(
3452 t.with_children.inertia_kg_m2,
3453 DMat3::from_diagonal(DVec3::new(0.08, 0.08, 0.001))
3454 );
3455
3456 let mut design = design;
3458 design.stages[0].overrides = Overrides {
3459 mass_kg: Some(3.0),
3460 cg_aft_m: Some(0.5),
3461 cg_xy_m: None,
3462 inertia: None,
3463 };
3464 let layout = design.layout().unwrap();
3465 let stage_mass = layout.stages[0].mass;
3466 close(stage_mass.mass_kg, 3.0, 0.0, "stage mass");
3467 close(stage_mass.cg_m.z, -0.5, 1e-15, "stage center");
3468 let bodies: Vec<MassProperties> = layout.body().map(|c| c.with_children).collect();
3469 let unscaled = MassProperties::combine(&bodies);
3470 assert!(unscaled.cg_m.x.abs() > 1e-5);
3472 assert_eq!(
3473 (stage_mass.cg_m.x, stage_mass.cg_m.y),
3474 (unscaled.cg_m.x, unscaled.cg_m.y)
3475 );
3476 tensor_close(
3477 stage_mass.inertia_kg_m2,
3478 unscaled.inertia_kg_m2 * (3.0 / unscaled.mass_kg),
3479 1e-14,
3480 );
3481 assert_eq!(layout.structure, stage_mass);
3482
3483 let center = DVec3::new(0.0, 0.0, -0.4);
3485 let point = Overrides {
3486 mass_kg: Some(0.25),
3487 ..Overrides::default()
3488 }
3489 .apply(MassProperties::point(0.0, center), 0.0)
3490 .unwrap();
3491 assert_eq!(point, MassProperties::point(0.25, center));
3492
3493 for covering in [false, true] {
3496 let mut airframe = airframe.clone();
3497 airframe.children = vec![attached(
3498 "ballast",
3499 mass_component(0.0, 0.1, 0.02),
3500 top(0.3),
3501 )];
3502 airframe.children[0].overrides.mass_kg = Some(0.25);
3503 airframe.children[0].overrides_include_children = covering;
3504 let design = rocket(vec![stage(
3505 "s",
3506 vec![body("nose", nose(0.2, 0.03)), airframe],
3507 )]);
3508 let layout = design.layout().unwrap();
3509 let (_, b) = layout.find("ballast").unwrap();
3510 let b = if covering { b.with_children } else { b.own };
3511 assert_eq!(b.mass_kg, 0.25);
3512 close(b.cg_m.z, -(0.2 + 0.3 + 0.05), 1e-15, "packing's center");
3513 let (r, l) = (0.02_f64, 0.1_f64);
3514 close(
3515 b.inertia_kg_m2.z_axis.z,
3516 0.5 * 0.25 * r * r,
3517 1e-15,
3518 "roll, m r²/2",
3519 );
3520 close(
3521 b.inertia_kg_m2.x_axis.x,
3522 0.25 * (3.0 * r * r + l * l) / 12.0,
3523 1e-15,
3524 "pitch, m (3r² + l²)/12",
3525 );
3526 }
3527
3528 let in_stage = |design: &Rocket| match design.layout() {
3530 Err(DesignError::InComponent { id, source }) if id == "s" => *source,
3531 other => panic!("{other:?}"),
3532 };
3533 let mut bad = plain.clone();
3534 bad.stages[0].overrides.mass_kg = Some(-1.0);
3535 assert!(matches!(in_stage(&bad), DesignError::Domain { .. }));
3536 let mut bad = plain.clone();
3537 bad.stages[0].overrides.inertia = Some(InertiaOverride::axisymmetric(1.0, 0.1));
3538 assert!(matches!(in_stage(&bad), DesignError::UnphysicalInertia(_)));
3539 for broken in [
3540 Overrides {
3541 cg_aft_m: Some(f64::NAN),
3542 ..Overrides::default()
3543 },
3544 Overrides {
3545 cg_xy_m: Some([0.0, f64::INFINITY]),
3546 ..Overrides::default()
3547 },
3548 ] {
3549 let mut bad = plain.clone();
3550 bad.stages[0].overrides = broken;
3551 assert!(matches!(in_stage(&bad), DesignError::Domain { .. }));
3552 }
3553 let mut bad = plain.clone();
3554 bad.stages[0].overrides.inertia = Some(InertiaOverride {
3555 xy_kg_m2: f64::NAN,
3556 ..InertiaOverride::axisymmetric(0.1, 1.0)
3557 });
3558 assert!(matches!(in_stage(&bad), DesignError::UnphysicalInertia(_)));
3559 let mut zero = plain.clone();
3561 zero.stages[0].overrides.mass_kg = Some(0.0);
3562 assert_eq!(zero.layout().unwrap().structure.inertia_kg_m2, DMat3::ZERO);
3563 let mut bad = plain;
3564 bad.stages[0].overrides.mass_kg = Some(0.0);
3565 bad.stages[0].overrides.inertia = Some(InertiaOverride::axisymmetric(1.0, 5.0));
3566 assert!(matches!(in_stage(&bad), DesignError::UnphysicalInertia(_)));
3567 }
3568
3569 #[test]
3571 fn malformed_trees_are_refused() {
3572 let base = || {
3573 rocket(vec![stage(
3574 "s",
3575 vec![
3576 body("nose", nose(0.2, 0.03)),
3577 body("tube", tube(0.5, 0.03, 0.001)),
3578 ],
3579 )])
3580 };
3581 let tree_error = |design: Rocket, want: &str| match design.layout() {
3582 Err(DesignError::Tree { id, .. }) => assert_eq!(id, want),
3583 other => panic!("{want}: {other:?}"),
3584 };
3585
3586 let mut d = base();
3587 d.stages[0].components[1].children =
3588 vec![attached("mmt", inner_tube(0.2, 0.01, 0.001), top(0.0))];
3589 d.stages[0].components[1].children[0].children =
3590 vec![attached("fins", fins(0.1, 0.05), top(0.0))];
3591 tree_error(d, "fins");
3592
3593 let mut d = base();
3596 let lug = Part::LaunchLug(crate::LaunchLug {
3597 length_m: 0.03,
3598 outer_radius_m: 0.003,
3599 thickness_m: 0.0005,
3600 angle_rad: 0.0,
3601 count: 1,
3602 spacing_m: 0.0,
3603 material: crate::testing::cardboard(),
3604 });
3605 d.stages[0].components[0].children = vec![attached("lug", lug, top(0.0))];
3606 tree_error(d, "lug");
3607 let mut d = base();
3608 d.stages[0].components[0].children = vec![attached("fins", fins(0.1, 0.05), top(0.0))];
3609 match d.layout() {
3610 Err(DesignError::InComponent { id, source }) => {
3611 assert_eq!(id, "fins");
3612 assert!(
3613 matches!(&*source, DesignError::Geometry(m) if m.contains("stands")),
3614 "{source}"
3615 );
3616 }
3617 other => panic!("fins on the nose: {other:?}"),
3618 }
3619
3620 let mut d = base();
3621 d.stages[0].components[1].children =
3622 vec![attached("inner-body", tube(0.1, 0.02, 0.001), top(0.0))];
3623 tree_error(d, "inner-body");
3624
3625 let mut d = base();
3626 d.stages[0]
3627 .components
3628 .push(body("ballast", mass_component(0.1, 0.1, 0.01)));
3629 tree_error(d, "ballast");
3630
3631 let mut d = base();
3632 d.stages[0].components[1].children = vec![body("unplaced", mass_component(0.1, 0.1, 0.01))];
3633 tree_error(d, "unplaced");
3634
3635 let mut d = base();
3636 d.stages[0].components[1].position = Some(top(0.0));
3637 tree_error(d, "tube");
3638
3639 let mut d = base();
3640 d.stages[0].components[1].auto = vec![AutoDimension::BaseRadius];
3641 tree_error(d, "tube");
3642
3643 let mut d = base();
3644 d.stages[0].components[1].children =
3645 vec![attached("ring", ring(0.005, 0.02, 0.0), top(0.0))];
3646 d.stages[0].components[1].children[0].motor_mount = Some(MotorMount::default());
3647 tree_error(d, "ring");
3648
3649 let mut d = base();
3650 d.stages[0].components[1].children =
3651 vec![attached("bay", mass_component(0.1, 0.1, 0.01), top(0.0))];
3652 d.stages[0].components[1].children[0].children =
3653 vec![attached("x", mass_component(0.1, 0.1, 0.01), top(0.0))];
3654 tree_error(d, "bay");
3655
3656 let mut d = base();
3657 d.stages[0].components[0].children =
3658 vec![attached("weight", mass_component(0.1, 0.05, 0.0), top(0.0))];
3659 d.stages[0].components[0].children[0].auto = vec![AutoDimension::PackedRadius];
3660 tree_error(d, "weight");
3661
3662 let mut d = base();
3663 d.stages.push(stage("empty", Vec::new()));
3664 tree_error(d, "empty");
3665
3666 let mut d = base();
3667 d.stages[0].components[1].id = "nose".to_owned();
3668 assert_eq!(d.layout(), Err(DesignError::DuplicateId("nose".to_owned())));
3669 let mut d = base();
3670 d.stages[0].components[1].id = String::new();
3671 assert!(matches!(d.layout(), Err(DesignError::DuplicateId(_))));
3672
3673 let mut d = base();
3674 d.stages.clear();
3675 assert!(matches!(d.layout(), Err(DesignError::Tree { .. })));
3676 }
3677
3678 #[test]
3681 fn nested_overrides_apply_deepest_first() {
3682 let mut airframe = body("airframe", tube(0.8, 0.03, 0.001));
3683 let mut bay = attached("bay", mass_component(0.4, 0.1, 0.02), top(0.3));
3684 bay.overrides.mass_kg = Some(1.0);
3685 airframe.children = vec![bay];
3686 let plain = rocket(vec![stage("s", vec![airframe.clone()])]);
3687 let layout = plain.layout().unwrap();
3688 let (_, t) = layout.find("airframe").unwrap();
3689 let (_, b) = layout.find("bay").unwrap();
3690 assert_eq!(b.own.mass_kg, 1.0);
3691 close(
3692 t.with_children.mass_kg,
3693 t.own.mass_kg + 1.0,
3694 1e-15,
3695 "child override inside",
3696 );
3697
3698 airframe.overrides.mass_kg = Some(3.0);
3699 airframe.overrides_include_children = true;
3700 let layout = rocket(vec![stage("s", vec![airframe])]).layout().unwrap();
3701 let (_, t2) = layout.find("airframe").unwrap();
3702 close(
3703 t2.with_children.mass_kg,
3704 3.0,
3705 0.0,
3706 "parent override over the total",
3707 );
3708 close(
3710 t2.with_children.cg_m.z,
3711 t.with_children.cg_m.z,
3712 1e-15,
3713 "center kept",
3714 );
3715 }
3716
3717 #[test]
3721 fn aft_radii_and_resolution_errors() {
3722 let boattail = |aft_shoulder: bool| {
3723 let mut c = body(
3724 "flare",
3725 Part::Transition(Transition {
3726 shape: crate::NoseShape::Conical {},
3727 clipped: false,
3728 length_m: 0.1,
3729 fore_radius_m: 0.03,
3730 aft_radius_m: 0.0,
3731 wall: crate::Wall::Shell { thickness_m: 0.002 },
3732 fore_shoulder: None,
3733 aft_shoulder: aft_shoulder.then_some(Shoulder {
3734 length_m: 0.04,
3735 outer_radius_m: 0.0,
3736 thickness_m: 0.002,
3737 capped: false,
3738 }),
3739 material: crate::testing::cardboard(),
3740 }),
3741 );
3742 c.auto = vec![AutoDimension::AftRadius, AutoDimension::AftShoulderRadius];
3743 c
3744 };
3745 let with = |flare: Component, last: Component| {
3746 rocket(vec![stage(
3747 "s",
3748 vec![
3749 body("nose", nose(0.2, 0.03)),
3750 body("upper", tube(0.5, 0.03, 0.001)),
3751 flare,
3752 last,
3753 ],
3754 )])
3755 };
3756 let layout = with(boattail(true), body("lower", tube(0.4, 0.04, 0.0015)))
3757 .layout()
3758 .unwrap();
3759 let Part::Transition(t) = layout.find("flare").unwrap().1.part.clone() else {
3760 panic!()
3761 };
3762 assert_eq!(t.aft_radius_m, 0.04);
3763 assert_eq!(t.aft_shoulder.unwrap().outer_radius_m, 0.04 - 0.0015);
3764
3765 let tree_id = |result: Result<Layout, DesignError>| match result {
3766 Err(DesignError::Tree { id, .. }) => id,
3767 other => panic!("{other:?}"),
3768 };
3769 let lower = || body("lower", tube(0.4, 0.04, 0.0015));
3771 assert_eq!(tree_id(with(boattail(false), lower()).layout()), "flare");
3772 let mut cone = boattail(true);
3774 cone.id = "cone".to_owned();
3775 cone.auto = vec![AutoDimension::AftShoulderRadius];
3776 if let Part::Transition(t) = &mut cone.part {
3777 t.aft_radius_m = 0.02;
3778 }
3779 assert_eq!(tree_id(with(boattail(true), cone).layout()), "flare");
3780
3781 let mut no_nose = with(boattail(true), lower());
3782 no_nose.stages[0].components.remove(0);
3783 no_nose.reference_diameter = ReferenceDiameter::NoseBase {};
3784 assert!(matches!(no_nose.layout(), Err(DesignError::Tree { .. })));
3785
3786 let nest = |n: usize| {
3788 let mut deepest = attached("t0", inner_tube(0.1, 0.02, 0.001), top(0.0));
3789 for k in 1..n {
3790 let mut outer = attached(&format!("t{k}"), inner_tube(0.1, 0.02, 0.001), top(0.0));
3791 outer.children = vec![deepest];
3792 deepest = outer;
3793 }
3794 let mut airframe = body("airframe", tube(0.5, 0.03, 0.001));
3795 airframe.children = vec![deepest];
3796 rocket(vec![stage("s", vec![airframe])])
3797 };
3798 nest(MAX_DEPTH - 1).layout().unwrap();
3799 assert_eq!(tree_id(nest(MAX_DEPTH).layout()), "t0");
3800
3801 let bad = with(boattail(true), body("lower", tube(0.4, 0.04, -0.001)));
3803 assert!(matches!(
3804 bad.layout(),
3805 Err(DesignError::InComponent { ref id, ref source })
3806 if id == "lower" && matches!(**source, DesignError::Domain { .. })
3807 ));
3808 let mut bad = with(boattail(true), lower());
3809 bad.stages[0].components[1].children = vec![attached(
3810 "lost",
3811 mass_component(0.1, 0.1, 0.01),
3812 top(f64::NAN),
3813 )];
3814 assert!(matches!(
3815 bad.layout(),
3816 Err(DesignError::InComponent { ref id, .. }) if id == "lost"
3817 ));
3818 }
3819
3820 #[test]
3823 fn packed_radius_leaves_room_for_the_offset() {
3824 let mut design = three_fin_rocket();
3825 if let Part::Parachute(chute) = &mut design.stages[0].components[1].children[4].part {
3826 chute.packing.radial_offset_m = 0.005;
3827 }
3828 let layout = design.layout().unwrap();
3829 let Part::Parachute(chute) = layout.find("chute").unwrap().1.part.clone() else {
3830 panic!()
3831 };
3832 close(
3833 chute.packing.radius_m,
3834 0.0255 - 0.005,
3835 1e-15,
3836 "packed radius",
3837 );
3838 assert!(crate::checks::check(&design).unwrap().is_empty());
3839 if let Part::Parachute(chute) = &mut design.stages[0].components[1].children[4].part {
3840 chute.packing.radial_offset_m = 0.03;
3841 }
3842 assert!(matches!(
3843 design.layout(),
3844 Err(DesignError::Tree { ref id, .. }) if id == "chute"
3845 ));
3846 }
3847
3848 #[test]
3849 fn design_round_trips_through_json() {
3850 let design = three_fin_rocket();
3851 let text = serde_json::to_string_pretty(&design).unwrap();
3852 let back: Rocket = serde_json::from_str(&text).unwrap();
3853 assert_eq!(back, design);
3854 assert_eq!(back.layout().unwrap(), design.layout().unwrap());
3855 let bad = text.replacen("\"auto\"", "\"autos\"", 1);
3857 assert!(serde_json::from_str::<Rocket>(&bad).is_err());
3858 }
3859
3860 type SpineSpec = Vec<(usize, bool, bool, u8, u8)>;
3863
3864 fn random_spine(spec: &SpineSpec, split: usize) -> Rocket {
3865 let mut components = Vec::new();
3866 for (k, &(kind, fore_auto, aft_auto, fore_cm, aft_cm)) in spec.iter().enumerate() {
3867 let (fore, aft) = (f64::from(fore_cm) * 0.01, f64::from(aft_cm) * 0.01);
3868 let id = format!("c{k}");
3869 let mut c = match kind {
3870 0 if k == 0 => body(&id, nose(0.2, aft)),
3871 2 => body(&id, cone_transition(fore, aft)),
3872 _ => body(&id, tube(0.3, aft, 0.001)),
3873 };
3874 c.auto = match &c.part {
3875 Part::NoseCone(_) if aft_auto => vec![AutoDimension::BaseRadius],
3876 Part::BodyTube(_) if aft_auto => vec![AutoDimension::OuterRadius],
3877 Part::Transition(_) => [
3878 (fore_auto, AutoDimension::ForeRadius),
3879 (aft_auto, AutoDimension::AftRadius),
3880 ]
3881 .into_iter()
3882 .filter_map(|(on, dimension)| on.then_some(dimension))
3883 .collect(),
3884 _ => Vec::new(),
3885 };
3886 components.push(c);
3887 }
3888 let split = split.clamp(1, components.len());
3890 let aft = components.split_off(split);
3891 let mut stages = vec![stage("upper", components)];
3892 if !aft.is_empty() {
3893 stages.push(stage("lower", aft));
3894 }
3895 rocket(stages)
3896 }
3897
3898 proptest! {
3899 #[test]
3903 fn unresolvable_radii_are_exactly_what_layout_refuses(
3904 spec in proptest::collection::vec((0usize..3, any::<bool>(), any::<bool>(), 1u8..5, 1u8..5), 1..7),
3905 split in 1usize..7,
3906 ) {
3907 let mut design = random_spine(&spec, split);
3908 let unresolvable = design.unresolvable_body_radii();
3909 prop_assert_eq!(unresolvable.is_empty(), design.layout().is_ok(), "{:?}", unresolvable);
3910
3911 let body = |design: &Rocket| -> (Vec<Part>, Vec<Vec<AutoDimension>>) {
3912 design
3913 .stages
3914 .iter()
3915 .flat_map(|s| s.components.iter())
3916 .map(|c| (c.part.clone(), c.auto.clone()))
3917 .unzip()
3918 };
3919 let (parts, autos) = body(&design);
3920 let autos: Vec<&[AutoDimension]> = autos.iter().map(Vec::as_slice).collect();
3921 let (fore, aft) = sweep_body_radii(&parts, &autos);
3922
3923 let filled = design.fill_unresolvable_body_radii(0.025);
3924 prop_assert_eq!(&filled, &unresolvable);
3925 prop_assert!(design.unresolvable_body_radii().is_empty());
3926 let layout = design.layout();
3927 prop_assert!(layout.is_ok(), "{:?}", layout.as_ref().err());
3928 let layout = layout.unwrap();
3929 for (n, placed) in layout.body().enumerate() {
3930 let (f, a) = match &placed.part {
3931 Part::NoseCone(p) => (None, Some(p.base_radius_m)),
3932 Part::BodyTube(p) => (Some(p.outer_radius_m), Some(p.outer_radius_m)),
3933 Part::Transition(p) => (Some(p.fore_radius_m), Some(p.aft_radius_m)),
3934 _ => (None, None),
3935 };
3936 if let (Some(before), Some(after)) = (fore[n], f) {
3938 prop_assert_eq!(before, after, "forward radius of {}", placed.id);
3939 }
3940 if let (Some(before), Some(after)) = (aft[n], a) {
3941 prop_assert_eq!(before, after, "aft radius of {}", placed.id);
3942 }
3943 }
3944 }
3945
3946 #[test]
3950 fn structure_is_the_mass_weighted_sum_and_slides_rigidly(
3951 parts in proptest::collection::vec((0.01f64..2.0, 0.0f64..0.7, 0.0f64..0.02, -3.0f64..3.0), 1..6),
3952 d in -0.1f64..0.1,
3953 ) {
3954 let build = |shift: f64| {
3955 let mut airframe = body("airframe", tube(1.0, 0.03, 0.001));
3956 airframe.children = parts
3957 .iter()
3958 .enumerate()
3959 .map(|(k, &(m, s, r, angle))| {
3960 let mut c = attached(&format!("m{k}"), mass_component(m, 0.05, 0.005), top(s + shift));
3961 if let Part::MassComponent(p) = &mut c.part {
3962 p.packing.radial_offset_m = r;
3963 p.packing.angle_rad = angle;
3964 }
3965 c
3966 })
3967 .collect();
3968 if let Part::BodyTube(t) = &mut airframe.part {
3970 t.material = crate::Material::bulk("none", 0.0);
3971 }
3972 rocket(vec![stage("s", vec![airframe])]).layout().unwrap()
3973 };
3974 let layout = build(0.0);
3975 let total: f64 = parts.iter().map(|p| p.0).sum();
3976 prop_assert!((layout.structure.mass_kg - total).abs() <= 1e-12 * total);
3977 let moment: f64 = parts.iter().map(|&(m, s, _, _)| m * -(s + 0.025)).sum();
3978 prop_assert!((layout.structure.cg_m.z - moment / total).abs() <= 1e-12);
3979 let slid = build(d);
3980 prop_assert!((slid.structure.cg_m.z - (layout.structure.cg_m.z - d)).abs() <= 1e-12);
3981 let scale = layout.structure.inertia_kg_m2.to_cols_array().iter().fold(1e-12f64, |m, v| m.max(v.abs()));
3982 let diff = (slid.structure.inertia_kg_m2 - layout.structure.inertia_kg_m2)
3983 .to_cols_array()
3984 .iter()
3985 .fold(0.0f64, |m, v| m.max(v.abs()));
3986 prop_assert!(diff <= 1e-9 * scale, "{diff:e} of {scale:e}");
3987 }
3988 }
3989
3990 #[test]
3994 fn a_layout_with_the_old_uk_key_reads_the_same() {
3995 let mut design = three_fin_rocket();
3996 design.stages[0].overrides.cg_aft_m = Some(0.4);
3997 design.stages[0].components[0].overrides.cg_aft_m = Some(0.1);
3998 let layout = design.layout().unwrap();
3999 assert!(layout.stages[0].center_overridden);
4000 let text = serde_json::to_string(&layout).unwrap();
4001 let old = text.replace("\"center_overridden\"", "\"centre_overridden\"");
4002 assert!(!old.contains("\"center_overridden\""));
4003 assert!(
4004 old.matches("\"centre_overridden\":true").count() >= 2,
4005 "a part and a stage: {old}"
4006 );
4007 assert_eq!(serde_json::from_str::<Layout>(&old).unwrap(), layout);
4008 }
4009}