hpr_aero/supersonic_boattail.rs
1//! A conical boattail's share of the normal force faster than sound, measured: W. D. Washington
2//! and W. Pettis Jr., *Boattail Effects on Static Stability at Small Angles of Attack*, U.S. Army
3//! Missile Command report RD-TM-68-5 (1968; DTIC AD-695658, approved for public release).
4//!
5//! Washington and Pettis mounted a model's aft section on its own balance and measured it with a
6//! boattail and as a plain cylinder of the same length, at Mach 1.75 to 4.5 (models T0 to T4), and
7//! a whole model with and without a boattail (No. 1) from Mach 0.8 to 4.5, Mach 0.8 to 1.5 of it
8//! in a second tunnel (pp. 1 to 2). The
9//! boattail's increment, `ΔC_Nα = C_Nα(with) − C_Nα(without)` (eq. 1), correlates with
10//!
11//! `ΔC_Nα / [1 − (D_B/D)²] = F(√|M² − 1| / (L_B/D))` (Fig. 5, printed p. 8),
12//!
13//! per degree on the cylinder's area `πD²/4`, with `D` the cylinder's diameter, `D_B` the
14//! boattail's base diameter and `L_B` its length. Its center of pressure is "approximately 50
15//! percent of its length" (abstract), from about 43% at Mach 2 to 64% at 4.5 (Fig. 6, printed
16//! p. 9). hpr reads both figures by hand from the page images ([`WP_PARAMETERS`],
17//! [`WP_SLOPE_PER_DEG`], [`WP_CENTER_MACHS`], [`WP_CENTER_FRACTION`]).
18//!
19//! **What it covers.** The models were conical boattails "with diameter ratios of 0.72 to 0.86
20//! and angles from 4 to 10 degrees" (p. 1), 0.82 to 1.18 diameters long, behind a cylinder; the
21//! correlation's points reach the peak near zero argument, with the next near 0.3 and the rest
22//! out to about 5.4; the curve is drawn to 6.1. Past 6.1 hpr
23//! holds its end, an extrapolation, as is any boattail steeper, shorter, **longer** or narrower
24//! than those tested: a long one reads the curve near zero argument, which comes from the
25//! report's lowest supersonic runs. The data scatter about the curve by up to about 15%.
26//! Slender-body theory gives a boattail `2[(D_B/D)² − 1]` per radian at any Mach number (Munk's
27//! value, which the report plots for comparison at subsonic speeds, p. 3). The measured curve
28//! runs from 0.23 to 1.58 times it, crossing at an argument of 0.635; it is 0.24 to 0.47 of it at
29//! the Arcas Robin's Mach numbers.
30//!
31//! See `docs/physics/aero.md` (*The body faster than sound in a flight*).
32
33use std::f64::consts::PI;
34
35use crate::error::{AeroError, check_dimension};
36
37/// The argument of Fig. 5's correlation, `√(M² − 1)/(L_B/D)`, at [`WP_SLOPE_PER_DEG`]'s rows.
38pub const WP_PARAMETERS: [f64; 24] = [
39 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0,
40 4.5, 5.0, 5.5, 6.0, 6.1,
41];
42
43/// `ΔC_Nα / [1 − (D_B/D)²]`, per degree on the cylinder's area, at [`WP_PARAMETERS`]: RD-TM-68-5
44/// Fig. 5, the supersonic side of the faired curve, read by tracing it on a 300-dpi scan to about
45/// ±0.0003 per degree (±0.02 per radian). At 0 (Mach 1) the curve peaks near −0.055, past the
46/// axis's last tick; from there to about 1.2 it follows model No. 1's points, the report's lowest
47/// supersonic runs. Their Mach numbers are not read off here: the report gives model No. 1 as
48/// Mach 0.8 to 4.5, 0.8 to 1.5 of it in the AEDC tunnel (p. 1).
49pub const WP_SLOPE_PER_DEG: [f64; 24] = [
50 -0.05500, -0.05291, -0.05080, -0.04818, -0.04486, -0.04100, -0.03681, -0.03131, -0.02531,
51 -0.02186, -0.01985, -0.01876, -0.01726, -0.01614, -0.01429, -0.01299, -0.01181, -0.01089,
52 -0.01013, -0.00946, -0.00886, -0.00841, -0.00820, -0.00816,
53];
54
55/// The Mach numbers of [`WP_CENTER_FRACTION`].
56pub const WP_CENTER_MACHS: [f64; 8] = [1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 4.8];
57
58/// The boattail's center of pressure as a share of its length from its fore end, at
59/// [`WP_CENTER_MACHS`]: the middle of RD-TM-68-5 Fig. 6's band (printed p. 9), read by tracing
60/// its two edges to about ±0.01; the band is about ±0.03 wide. Held outside Mach 1.5 to 4.8.
61pub const WP_CENTER_FRACTION: [f64; 8] = [0.430, 0.430, 0.449, 0.479, 0.524, 0.580, 0.643, 0.684];
62
63/// Linear interpolation in `xs` (increasing), holding the end values outside them.
64fn held_linear(xs: &[f64], ys: &[f64], x: f64) -> f64 {
65 let x = x.clamp(xs[0], xs[xs.len() - 1]);
66 let i = xs.partition_point(|&c| c <= x).clamp(1, xs.len() - 1);
67 let w = (x - xs[i - 1]) / (xs[i] - xs[i - 1]);
68 (1.0 - w) * ys[i - 1] + w * ys[i]
69}
70
71/// Fig. 5's argument, `√(M² − 1)/(L_B/D)`, for a boattail `length_over_diameter` long in its fore
72/// diameters at Mach `mach` (at least 1).
73pub fn wp_parameter(mach: f64, length_over_diameter: f64) -> f64 {
74 (mach * mach - 1.0).max(0.0).sqrt() / length_over_diameter
75}
76
77/// A conical boattail's normal-force slope increment at `α → 0`, per radian on the area
78/// `π fore_radius_m²`, at Mach `mach` (at least 1): negative, from Fig. 5's correlation
79/// ([`WP_SLOPE_PER_DEG`]).
80///
81/// # Errors
82///
83/// [`AeroError::Domain`] for a Mach number below 1 or not finite, a length that isn't finite and
84/// positive, an aft radius that isn't below the fore radius (not a boattail) or is negative, or
85/// dimensions whose ratios overflow.
86pub fn wp_slope(
87 mach: f64,
88 fore_radius_m: f64,
89 aft_radius_m: f64,
90 length_m: f64,
91) -> Result<f64, AeroError> {
92 if !(mach.is_finite() && mach >= 1.0) {
93 return Err(AeroError::Domain {
94 what: "Mach number for the boattail correlation",
95 value: mach,
96 });
97 }
98 check_dimension("boattail length", length_m, false)?;
99 check_dimension("boattail fore radius", fore_radius_m, false)?;
100 if !(aft_radius_m.is_finite() && aft_radius_m >= 0.0 && aft_radius_m < fore_radius_m) {
101 return Err(AeroError::Domain {
102 what: "boattail aft radius",
103 value: aft_radius_m,
104 });
105 }
106 let ratio = aft_radius_m / fore_radius_m;
107 let per_deg = held_linear(
108 &WP_PARAMETERS,
109 &WP_SLOPE_PER_DEG,
110 wp_parameter(mach, length_m / (2.0 * fore_radius_m)),
111 );
112 let slope = per_deg * (180.0 / PI) * (1.0 - ratio * ratio);
113 // Finite inputs whose ratios overflow (an enormous Mach number over an enormous length) leave
114 // no parameter to read the curve at.
115 if slope.is_finite() {
116 Ok(slope)
117 } else {
118 Err(AeroError::Domain {
119 what: "boattail correlation parameter",
120 value: wp_parameter(mach, length_m / (2.0 * fore_radius_m)),
121 })
122 }
123}
124
125/// The boattail's center of pressure at Mach `mach`, as a share of its length from its fore end
126/// ([`WP_CENTER_FRACTION`]).
127pub fn wp_center_fraction(mach: f64) -> f64 {
128 let m = if mach.is_nan() {
129 WP_CENTER_MACHS[0]
130 } else {
131 mach
132 };
133 held_linear(&WP_CENTER_MACHS, &WP_CENTER_FRACTION, m)
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 const INCH: f64 = 0.0254;
141
142 #[test]
143 fn overflowing_ratios_are_refused() {
144 // Finite inputs, but `√(M² − 1)` and `L/D` both overflow and their ratio is NaN.
145 assert!(matches!(
146 wp_slope(1e200, 1e-300, 0.0, 1e300),
147 Err(AeroError::Domain { .. })
148 ));
149 }
150
151 #[test]
152 fn the_tables_are_well_formed() {
153 assert!(WP_PARAMETERS.windows(2).all(|w| w[0] < w[1]));
154 // The curve rises (toward zero) all the way from its peak at Mach 1.
155 assert!(WP_SLOPE_PER_DEG.windows(2).all(|w| w[0] < w[1]));
156 assert!(WP_CENTER_MACHS.windows(2).all(|w| w[0] < w[1]));
157 assert!(WP_CENTER_FRACTION.windows(2).all(|w| w[0] <= w[1]));
158 // Munk's (slender-body) value, −2 per radian, is −0.0349 per degree: the dashed line
159 // on Fig. 5 at the subsonic side. The supersonic curve falls below it in size from
160 // just past Mach 1.
161 let munk = -2.0 * PI / 180.0;
162 assert!((munk - -0.034_91).abs() < 1e-5);
163 assert!(WP_SLOPE_PER_DEG[0] < munk && WP_SLOPE_PER_DEG[7] > munk);
164 }
165
166 /// The Arcas Robin's 15° boattail (TN D-4014 Fig. 1(a): 2.25 in to 1.308 in across over
167 /// 1.757 in) at the report's Mach numbers, pinned, and between the two theories M1.8e5's
168 /// research note brackets it with: footnote 8's −0.177 to −0.026 and slender-body theory's
169 /// −1.324 per radian on the body's cross-section.
170 #[test]
171 fn the_arcas_robins_boattail_lies_between_the_two_theories() {
172 let (fore, aft, length) = (1.125 * INCH, 0.654 * INCH, 1.757 * INCH);
173 let slender = 2.0 * ((aft / fore).powi(2) - 1.0);
174 assert!((slender - -1.324).abs() < 2e-3);
175 let pinned = [
176 (1.5, -0.6219),
177 (1.8, -0.5538),
178 (2.3, -0.4791),
179 (2.96, -0.4092),
180 (3.96, -0.3403),
181 (4.63, -0.3144),
182 ];
183 for (mach, want) in pinned {
184 let got = wp_slope(mach, fore, aft, length).unwrap();
185 assert!((got - want).abs() < 1e-4, "Mach {mach}: {got}");
186 assert!(got > slender && got < -0.177, "Mach {mach}: {got}");
187 }
188 }
189
190 #[test]
191 fn it_is_continuous_and_held_past_the_curve() {
192 let (fore, aft) = (0.05, 0.04);
193 // From Mach 1.2, where hpr's supersonic join starts. At Mach 1 itself the argument
194 // `√(M² − 1)` rises with infinite slope, so a step of 1e-9 moves it by about 4e-5.
195 for mach in [1.2, 1.5, 2.0, 3.0, 4.0, 4.99] {
196 let a = wp_slope(mach, fore, aft, 0.1).unwrap();
197 let b = wp_slope(mach + 1e-9, fore, aft, 0.1).unwrap();
198 assert!((a - b).abs() < 1e-8, "Mach {mach}");
199 }
200 // A short boattail at Mach 5: past the curve's end, held.
201 let short = wp_slope(5.0, fore, aft, 0.005).unwrap();
202 let end = -0.00816 * (180.0 / PI) * (1.0 - 0.64);
203 assert!((short - end).abs() < 1e-12);
204 assert_eq!(wp_center_fraction(1.2), 0.430);
205 assert_eq!(wp_center_fraction(6.0), 0.684);
206 }
207
208 #[test]
209 fn it_refuses_what_isnt_a_supersonic_boattail() {
210 assert!(wp_slope(0.9, 0.05, 0.04, 0.1).is_err());
211 assert!(wp_slope(f64::NAN, 0.05, 0.04, 0.1).is_err());
212 assert!(wp_slope(2.0, 0.05, 0.05, 0.1).is_err());
213 assert!(wp_slope(2.0, 0.05, 0.06, 0.1).is_err());
214 assert!(wp_slope(2.0, 0.05, -0.01, 0.1).is_err());
215 assert!(wp_slope(2.0, 0.05, 0.04, 0.0).is_err());
216 // To a point: the full base area, still negative and finite.
217 assert!(wp_slope(2.0, 0.05, 0.0, 0.1).unwrap() < 0.0);
218 }
219}