Skip to main content

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;