hpr_sim/rail.rs
1//! The launch rail: its direction, length and friction, and where a design's rail buttons or launch
2//! lugs leave it.
3//!
4//! The rocket starts with its aft end (the aft end of its body or its aft-most nozzle exit,
5//! whichever is further aft) at the foot of the rail, its axis along the rail. It is guided, with
6//! one degree of freedom along the rail, until the aft edge of its aft-most guide passes the top
7//! of the rail. A rocket with no rail buttons or lugs is guided until its aft end passes the top,
8//! as from a tower. Between the forward guide leaving and the aft guide leaving, a real rocket can
9//! pivot about the aft guide ("tip-off"); that rotation is not modeled, and the rocket leaves the
10//! rail with no angular velocity.
11//!
12//! Friction is Coulomb friction, `μ |N|`, with `N` the rail's reaction perpendicular to the rail:
13//! the component of gravity, the aerodynamic force and the variable-mass terms across the rocket's
14//! axis. It opposes the motion along the rail, and on the pad it holds the rocket until the force
15//! along the rail exceeds it.
16//!
17//! Method: `docs/physics/flight.md`.
18
19use hpr_core::frames::LaunchAngles;
20use hpr_core::{DQuat, DVec3};
21use hpr_design::{Assembly, Part};
22use serde::{Deserialize, Serialize};
23
24use crate::error::SimError;
25
26/// A launch rail.
27#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
28#[serde(deny_unknown_fields)]
29pub struct Rail {
30 /// The rail's length from its foot, where the rocket's aft end starts, to its top, m.
31 pub length_m: f64,
32 /// The heading of the rail, clockwise from true north, rad (`frames::LaunchAngles`).
33 pub azimuth_rad: f64,
34 /// The rail's angle above the horizon, rad (`π/2` is vertical).
35 pub elevation_rad: f64,
36 /// The rocket's roll on the rail: the turn of `x_B` about the rail axis, rad.
37 pub roll_rad: f64,
38 /// The Coulomb friction coefficient between the guides and the rail. There is no default
39 /// value in the sources; zero means a frictionless rail.
40 pub friction_coefficient: f64,
41}
42
43impl Rail {
44 /// A frictionless vertical rail of length `length_m`.
45 #[must_use]
46 pub fn vertical(length_m: f64) -> Self {
47 Self {
48 length_m,
49 azimuth_rad: 0.0,
50 elevation_rad: std::f64::consts::FRAC_PI_2,
51 roll_rad: 0.0,
52 friction_coefficient: 0.0,
53 }
54 }
55
56 /// Checks the length, angles and friction.
57 ///
58 /// # Errors
59 ///
60 /// [`SimError::Domain`] for a non-positive length, a negative friction coefficient, an
61 /// elevation outside `(0, π/2]` or a non-finite angle.
62 pub fn validate(&self) -> Result<(), SimError> {
63 let checks = [
64 ("rail length", self.length_m, self.length_m > 0.0),
65 (
66 "rail friction coefficient",
67 self.friction_coefficient,
68 self.friction_coefficient >= 0.0,
69 ),
70 (
71 "rail elevation",
72 self.elevation_rad,
73 self.elevation_rad > 0.0 && self.elevation_rad <= std::f64::consts::FRAC_PI_2,
74 ),
75 ("rail azimuth", self.azimuth_rad, true),
76 ("rail roll", self.roll_rad, true),
77 ];
78 for (what, value, ok) in checks {
79 if !value.is_finite() || !ok {
80 return Err(SimError::Domain { what, value });
81 }
82 }
83 Ok(())
84 }
85
86 /// The rocket's attitude on the rail.
87 #[must_use]
88 pub fn attitude(&self) -> DQuat {
89 LaunchAngles {
90 azimuth_rad: self.azimuth_rad,
91 elevation_rad: self.elevation_rad,
92 roll_rad: self.roll_rad,
93 }
94 .to_quaternion()
95 }
96
97 /// The unit vector along the rail, up, in the launch frame.
98 #[must_use]
99 pub fn direction_enu(&self) -> DVec3 {
100 self.attitude().mul_vec3(DVec3::Z)
101 }
102}
103
104/// Where a design meets the rail, as stations (m aft of the nose tip).
105#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
106pub struct Guides {
107 /// The rocket's aft end: the aft end of its body, or its aft-most nozzle exit if that is
108 /// further aft. It starts at the foot of the rail.
109 pub aft_station_m: f64,
110 /// The aft edge of the forward-most guide (a rail button's aft edge, or a lug's aft end), or
111 /// `None` without guides.
112 pub first_guide_station_m: Option<f64>,
113 /// The aft edge of the aft-most guide, or `None` without guides.
114 pub last_guide_station_m: Option<f64>,
115}
116
117impl Guides {
118 /// The guides of an assembled design: every rail button of every row (each `outer_diameter`
119 /// long, `spacing` apart) and every launch lug (each `length` long).
120 #[must_use]
121 pub fn of(assembly: &Assembly) -> Self {
122 let body_aft = assembly.layout.length_m;
123 let aft_station_m = assembly
124 .motors
125 .iter()
126 .map(|motor| motor.nozzle_station_m())
127 .fold(body_aft, f64::max);
128 let mut first: Option<f64> = None;
129 let mut last: Option<f64> = None;
130 for component in &assembly.layout.components {
131 let (count, spacing, extent) = match &component.part {
132 Part::RailButton(button) => {
133 (button.count, button.spacing_m, button.outer_diameter_m)
134 }
135 Part::LaunchLug(lug) => (lug.count, lug.spacing_m, lug.length_m),
136 _ => continue,
137 };
138 for k in 0..count {
139 let aft_edge = component.fore_station_m + f64::from(k) * spacing + extent;
140 first = Some(first.map_or(aft_edge, |f| f.min(aft_edge)));
141 last = Some(last.map_or(aft_edge, |l| l.max(aft_edge)));
142 }
143 }
144 Self {
145 aft_station_m,
146 first_guide_station_m: first,
147 last_guide_station_m: last,
148 }
149 }
150
151 /// How far the rocket travels along a rail of `rail_length_m` before its last guide leaves the
152 /// top: `L − (s_aft − s_guide)`, with the aft end standing in for the guide without guides.
153 #[must_use]
154 pub fn exit_travel_m(&self, rail_length_m: f64) -> f64 {
155 let guide = self.last_guide_station_m.unwrap_or(self.aft_station_m);
156 rail_length_m - (self.aft_station_m - guide)
157 }
158
159 /// How far the rocket travels before its first guide leaves the top.
160 #[must_use]
161 pub fn first_guide_exit_travel_m(&self, rail_length_m: f64) -> f64 {
162 let guide = self.first_guide_station_m.unwrap_or(self.aft_station_m);
163 rail_length_m - (self.aft_station_m - guide)
164 }
165}
166
167#[cfg(test)]
168mod tests;