hpr_aero/blunt_tip.rs
1//! A blunt or vertical nose tip faster than sound: modified Newtonian pressures on the cap, handed
2//! over to the second-order shock-expansion method ([`crate::shock_expansion`]) where the surface
3//! slope falls to the steepest wedge an attached shock can turn. After C. M. Jackson Jr.,
4//! W. C. Sawyer and R. S. Smith, *A Method for Determining Surface Pressures on Blunt Bodies of
5//! Revolution at Small Angles of Attack in Supersonic Flow*, NASA TN D-4865 (1968) ([J68]).
6//!
7//! **Flown faster than sound** since the milestone
8//! [M1.8e7](https://nrdptel.github.io/hpr-sim/decisions-and-roadmap.html#m1-8e7), for noses whose
9//! tip is vertical (power-series noses with `n < 1`, Haack series, elliptical noses) through
10//! [`crate::model::SupersonicBody`]. The guide's
11//! [Blunt tips](https://nrdptel.github.io/hpr-sim/physics/aero.html#blunt-tips) explains it and
12//! how it was checked.
13//!
14//! ```
15//! use hpr_aero::blunt_tip::{handover_angle_rad, newtonian_loading, pitot_pressure_ratio};
16//!
17//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
18//! // Behind a normal shock at Mach 2 the pitot pressure is 5.640 times the free stream's
19//! // (NACA Report 1135, Table II).
20//! assert!((pitot_pressure_ratio(2.0)? - 5.6404).abs() < 1e-4);
21//! // The cap hands over where its slope falls to the steepest wedge an attached shock turns
22//! // at Mach 1.5, 12.1°.
23//! assert!((handover_angle_rad(1.5)?.to_degrees() - 12.11).abs() < 0.01);
24//! // Where the cap's slope is 45° its loading is C_p,max/2, 0.829 at Mach 2.
25//! let loading = newtonian_loading(2.0, 45f64.to_radians())?;
26//! assert!((loading - 0.8286).abs() < 1e-4);
27//! # Ok(())
28//! # }
29//! ```
30//!
31//! **The cap.** Near a blunt tip the shock stands off the body and the flow behind it is subsonic,
32//! where the shock-expansion method, which marches supersonic flow from a pointed tip, can't
33//! start. The report's cap is modified Newtonian (eq. 1, p. 5):
34//!
35//! `p_s/p₀ = (p_t2/p₀ − 1) sin²δ + 1`,
36//!
37//! with `δ` the surface's slope to the wind and `p_t2` the pitot pressure behind a normal shock
38//! (the Rayleigh pitot formula, NACA Report 1135, 1953, eq. 100, p. 619):
39//!
40//! `p_t2/p₀ = [(γ + 1)M²/2]^(γ/(γ − 1)) · [(γ + 1)/(2γM² − (γ − 1))]^(1/(γ − 1))`.
41//!
42//! In coefficient form `C_p = C_p,max sin²δ`, `C_p,max = (p_t2/p₀ − 1)/(γM²/2)`.
43//!
44//! **The handover.** The shock-expansion method starts "at the point where the surface slope is
45//! the same as that required for shock attachment to a two-dimensional wedge at the free-stream
46//! Mach number", chosen "simply because it gave the best agreement with the available data in the
47//! low supersonic-speed range" (p. 5): the wedge's largest deflection `δ_max`, from the shock angle
48//! of NACA Report 1135 eq. 168 (p. 624),
49//!
50//! `sin²θ = [(γ + 1)M²/4 − 1 + √((γ + 1)((γ + 1)M⁴/16 + (γ − 1)M²/2 + 1))]/(γM²)`,
51//!
52//! turned into a deflection by eq. 138 (p. 621), `tan δ = 2 cot θ (M² sin²θ − 1)/(2 + M²(γ + 1 −
53//! 2 sin²θ))`. It is 12.1° at Mach 1.5 and 22.97° at Mach 2. hpr hands over at the lesser of
54//! `δ_max` and 24° ([`MAX_HANDOVER_RAD`]), so from Mach 2.06 up the cap reaches further aft than
55//! the report's. The cap is a cap because the method reads the tangent cone's normal-force slope
56//! at the handover, and those tables ([`crate::shock_expansion::cone_normal_force_slope`]) once
57//! stopped at TN 3527 Fig. 2's 24°. They now reach 30° ([`CONE_TABLE_CAP_RAD`]), and the
58//! handover doesn't, because the march does not carry it there yet: measured over the whole
59//! sweep in [ADR-043][adr-043], a 30° handover reads nearer TN D-4865's own sphere-cone at every
60//! row where a cap binds at all, and on the committed Arcas Robin nose above about Mach 4 it puts
61//! most of the march's elements into `η < 0`
62//! ([issue #108](https://github.com/nrdptel/hpr-sim/issues/108)), where the answer moves with the
63//! element count. No cap above 24° holds its answer to Mach 5, and the failure isn't orderly in
64//! the cap: 28° is the worst of the four measured. [`handover_angle_capped_rad`] takes the cap as
65//! a parameter so both ends are measured rather than argued.
66//!
67//! **The flow behind it: hpr's choice, not the report's.** hpr starts TN 3527's march at the
68//! handover as the method starts at a pointed vertex: with the flow on the cone tangent to the
69//! body there (Taylor–Maccoll), that cone's loading `tan δ (dC_N/dα)_tc`, and no pressure gradient
70//! (TN 3527 sketch (a), p. 6). The report starts it from the Newtonian pressure and Mach number
71//! instead (eq. 2 and p. 5). Read that way at `α → 0`, the march on the committed Arcas Robin nose
72//! reduces elements from Mach 2.96 ([issue #81](https://github.com/nrdptel/hpr-sim/issues/81),
73//! the method's open question there), so its answer changes as elements are added, and
74//! fails from Mach 3.96, where the Newtonian pressure at the handover lies below the tangent
75//! cone's; the tangent cone's start holds to Mach 5 and converges. [`crate::shock_expansion::HandoverStart`] keeps the report's start to compare,
76//! and the decision record on it, [ADR-038][adr-038], gives both readings' numbers.
77//!
78//! **The loading at `α → 0`.** On the cap, TN 3527's loading form ([`crate::shock_expansion`]),
79//! whose `C_Nα = (2π/A_ref) ∫ Λ r dx` makes `Λ` half the windward meridian's `∂C_p/∂α`, takes
80//! the wind's slope `δ + α cos φ` in `C_p = C_p,max sin²δ`: `Λ = C_p,max sin δ cos δ`
81//! ([`newtonian_loading`]). A hemisphere then carries `C_Nα = C_p,max/2`, its Newtonian drag turned
82//! into the body's axes, as it must. Behind the handover the loading is the method's. The handover
83//! itself is held where it sits on the body, as TN 3527 holds every other point; the report's
84//! equivalent bodies turn the body about the sphere's center, sliding the handover along the
85//! surface, and that term is left out. Nothing here measures what it is worth.
86//!
87//! **What it covers.** The report checked spherical caps only: a sphere-cone (a 0.175-diameter
88//! nose radius on an 11.5° cone) and a sphere on a flared body, from Mach 1.50 to 4.63 and up to
89//! 12°, calling its results "adequate engineering estimates … except where flow separation or
90//! detached secondary shock waves are present" (p. 13). A power-series, Haack or elliptical nose
91//! has no sphere at its tip; applying the slope rule to it is an extrapolation, stated. The
92//! wedge's deflection, the pitot pressure and the Newtonian pressure are exact for a perfect gas
93//! with `γ = 1.4`; the loading is only as good as Newtonian theory on the cap. A cap that shrinks
94//! to nothing doesn't reach the cone it sits on, because the march keeps its start cone's total
95//! pressure ([issue #101](https://github.com/nrdptel/hpr-sim/issues/101)), and near the join's
96//! start the cap can cover half a slender nose, far more than the report's own.
97//!
98//! [J68]: https://ntrs.nasa.gov/citations/19690000884
99//! [adr-038]: https://github.com/nrdptel/hpr-sim/blob/main/docs/DECISIONS.md#adr-038-blunt-and-vertical-nose-tips-faster-than-sound-by-a-newtonian-cap-the-method-started-from-the-tangent-cone-2026-09-19
100//! [adr-043]: https://github.com/nrdptel/hpr-sim/blob/main/docs/DECISIONS.md#adr-043-the-blunt-tips-handover-cap-what-it-is-worth-and-what-stops-it-moving-2026-09-20
101
102use crate::afterbody::GAMMA;
103use crate::error::AeroError;
104
105/// The steepest slope the cap hands over at, as hpr flies it: 24°. It was TN 3527 Fig. 2's
106/// steepest tangent cone, the steepest whose normal-force slope the method could read; since the
107/// milestone [M1.8e11](https://nrdptel.github.io/hpr-sim/decisions-and-roadmap.html#m1-8e11),
108/// which took the cone slopes to 30°, the tables reach [`CONE_TABLE_CAP_RAD`], and 24° is kept
109/// because that is as far as the march carries the handover, not as far as the tables do
110/// ([ADR-043: what the handover's cap is worth][adr-043]).
111///
112/// [adr-043]: https://github.com/nrdptel/hpr-sim/blob/main/docs/DECISIONS.md#adr-043-the-blunt-tips-handover-cap-what-it-is-worth-and-what-stops-it-moving-2026-09-20
113pub const MAX_HANDOVER_RAD: f64 = 24.0 * std::f64::consts::PI / 180.0;
114
115/// The steepest cap the method can be given, 30°: the steepest cone its normal-force slopes
116/// cover ([`crate::shock_expansion::cone_normal_force_slope`]), where NASA SP-3007's tables stop
117/// ("cone angles from 2.5° to 30°", Foreword, p. iii). A handover steeper than this has no
118/// tangent cone to start the march from.
119pub const CONE_TABLE_CAP_RAD: f64 = 30.0 * std::f64::consts::PI / 180.0;
120
121/// The Mach number must be finite and above 1: the cap stands behind a normal shock.
122fn check_mach(mach: f64) -> Result<(), AeroError> {
123 if mach.is_finite() && mach > 1.0 {
124 Ok(())
125 } else {
126 Err(AeroError::Domain {
127 what: "Mach number of a blunt tip's cap",
128 value: mach,
129 })
130 }
131}
132
133/// A slope to the wind within `[0, π/2]`.
134fn check_slope(slope_rad: f64) -> Result<(), AeroError> {
135 if slope_rad.is_finite() && (0.0..=std::f64::consts::FRAC_PI_2).contains(&slope_rad) {
136 Ok(())
137 } else {
138 Err(AeroError::Domain {
139 what: "surface slope on a blunt tip's cap",
140 value: slope_rad,
141 })
142 }
143}
144
145/// The pitot pressure behind a normal shock over the free stream's, `p_t2/p₀`, at Mach `mach`
146/// (the Rayleigh pitot formula, NACA Report 1135 eq. 100, p. 619; TN D-4865 eq. 1).
147///
148/// # Errors
149///
150/// [`AeroError::Domain`] for a Mach number that isn't finite and above 1, or so large (past about
151/// 1e76) that the ratio overflows.
152pub fn pitot_pressure_ratio(mach: f64) -> Result<f64, AeroError> {
153 check_mach(mach)?;
154 let ratio = pitot(mach);
155 if ratio.is_finite() {
156 Ok(ratio)
157 } else {
158 Err(AeroError::Domain {
159 what: "Mach number of a blunt tip's cap, too large for the pitot pressure",
160 value: mach,
161 })
162 }
163}
164
165fn pitot(mach: f64) -> f64 {
166 let m2 = mach * mach;
167 let g = GAMMA;
168 (0.5 * (g + 1.0) * m2).powf(g / (g - 1.0))
169 * ((g + 1.0) / (2.0 * g * m2 - (g - 1.0))).powf(1.0 / (g - 1.0))
170}
171
172/// The largest angle a two-dimensional wedge can turn the flow at Mach `mach` behind an attached
173/// shock, rad: the shock angle of NACA Report 1135 eq. 168 (p. 624) put into eq. 138 (p. 621).
174///
175/// # Errors
176///
177/// [`AeroError::Domain`] for a Mach number that isn't finite and above 1.
178pub fn wedge_detachment_angle_rad(mach: f64) -> Result<f64, AeroError> {
179 check_mach(mach)?;
180 let g = GAMMA;
181 // Both equations divided through by M² (and M⁴ under the root), so nothing overflows.
182 let i2 = 1.0 / (mach * mach);
183 let root = ((g + 1.0) * ((g + 1.0) / 16.0 + 0.5 * (g - 1.0) * i2 + i2 * i2)).sqrt();
184 let sin2 = ((0.25 * (g + 1.0) - i2 + root) / g).min(1.0);
185 let cot = ((1.0 - sin2) / sin2).sqrt();
186 let tan = 2.0 * cot * (sin2 - i2) / (2.0 * i2 + g + 1.0 - 2.0 * sin2);
187 Ok(tan.atan())
188}
189
190/// The slope at which the cap hands over to the shock-expansion method at Mach `mach`, rad: the
191/// lesser of the wedge's largest deflection ([`wedge_detachment_angle_rad`], TN D-4865 p. 5) and
192/// [`MAX_HANDOVER_RAD`], the cap hpr flies.
193///
194/// # Errors
195///
196/// As [`wedge_detachment_angle_rad`].
197pub fn handover_angle_rad(mach: f64) -> Result<f64, AeroError> {
198 handover_angle_capped_rad(mach, MAX_HANDOVER_RAD)
199}
200
201/// The handover slope at Mach `mach` under a cap of `cap_rad` rather than the flown
202/// [`MAX_HANDOVER_RAD`], rad: the lesser of the wedge's largest deflection and the cap. What
203/// [ADR-043][adr-043] sweeps; a body takes it through
204/// [`crate::shock_expansion::ShockExpansionBody::with_handover_cap_rad`].
205///
206/// # Errors
207///
208/// - As [`wedge_detachment_angle_rad`] for the Mach number.
209/// - [`AeroError::Domain`] for a cap outside `(0, `[`CONE_TABLE_CAP_RAD`]`]`, where the march has
210/// no tangent cone to start from.
211///
212/// [adr-043]: https://github.com/nrdptel/hpr-sim/blob/main/docs/DECISIONS.md#adr-043-the-blunt-tips-handover-cap-what-it-is-worth-and-what-stops-it-moving-2026-09-20
213pub fn handover_angle_capped_rad(mach: f64, cap_rad: f64) -> Result<f64, AeroError> {
214 if !(cap_rad.is_finite() && cap_rad > 0.0 && cap_rad <= CONE_TABLE_CAP_RAD) {
215 return Err(AeroError::Domain {
216 what: "cap on a blunt tip's handover slope",
217 value: cap_rad,
218 });
219 }
220 Ok(wedge_detachment_angle_rad(mach)?.min(cap_rad))
221}
222
223/// Modified Newtonian `C_p,max = (p_t2/p₀ − 1)/(γM²/2)` at Mach `mach`.
224///
225/// # Errors
226///
227/// As [`pitot_pressure_ratio`].
228pub fn newtonian_pressure_coefficient_max(mach: f64) -> Result<f64, AeroError> {
229 Ok((pitot_pressure_ratio(mach)? - 1.0) / (0.5 * GAMMA * mach * mach))
230}
231
232/// The cap's surface pressure over the free stream's where its slope to the wind is `slope_rad`
233/// (TN D-4865 eq. 1): `(p_t2/p₀ − 1) sin²δ + 1`.
234///
235/// # Errors
236///
237/// [`AeroError::Domain`] for a Mach number that isn't finite and above 1, or a slope outside
238/// `[0, π/2]`.
239pub fn newtonian_pressure_ratio(mach: f64, slope_rad: f64) -> Result<f64, AeroError> {
240 check_slope(slope_rad)?;
241 let sin = slope_rad.sin();
242 Ok((pitot_pressure_ratio(mach)? - 1.0) * sin * sin + 1.0)
243}
244
245/// The cap's surface Mach number where its pressure is `pressure_ratio` times the free stream's,
246/// isentropic from the pitot pressure (TN D-4865 eq. 2); zero at the stagnation point. The
247/// report's march starts from it ([`crate::shock_expansion::HandoverStart::Newtonian`]).
248///
249/// # Errors
250///
251/// [`AeroError::Domain`] for a Mach number that isn't finite and above 1, or a pressure that
252/// isn't positive and at most the pitot pressure.
253pub fn newtonian_surface_mach(mach: f64, pressure_ratio: f64) -> Result<f64, AeroError> {
254 let pitot = pitot_pressure_ratio(mach)?;
255 if !(pressure_ratio.is_finite() && pressure_ratio > 0.0 && pressure_ratio <= pitot) {
256 return Err(AeroError::Domain {
257 what: "surface pressure ratio on a blunt tip's cap",
258 value: pressure_ratio,
259 });
260 }
261 let g = GAMMA;
262 let m2 = 2.0 / (g - 1.0) * ((pressure_ratio / pitot).powf(-(g - 1.0) / g) - 1.0);
263 Ok(m2.max(0.0).sqrt())
264}
265
266/// The cap's loading at `α → 0` where its slope to the axis is `slope_rad`, `Λ = C_p,max sin δ
267/// cos δ`: half the windward `∂C_p/∂α` of `C_p = C_p,max sin²(δ + α cos φ)`, in the units of
268/// TN 3527's loading ([`crate::shock_expansion`]).
269///
270/// # Errors
271///
272/// As [`newtonian_pressure_ratio`].
273pub fn newtonian_loading(mach: f64, slope_rad: f64) -> Result<f64, AeroError> {
274 check_slope(slope_rad)?;
275 Ok(newtonian_pressure_coefficient_max(mach)? * slope_rad.sin() * slope_rad.cos())
276}
277
278/// [`newtonian_loading`] from the slope `dr/dx` itself, which may be infinite (a vertical tip):
279/// `sin δ cos δ = 1/(t + 1/t)` with `t = dr/dx`, zero at both ends. `c_p_max` is
280/// [`newtonian_pressure_coefficient_max`].
281pub(crate) fn newtonian_loading_at_slope(c_p_max: f64, slope: f64) -> f64 {
282 let t = slope.abs();
283 if t == 0.0 || t.is_infinite() {
284 0.0
285 } else {
286 c_p_max / (t + 1.0 / t)
287 }
288}
289
290/// The loading just behind a handover that holds still in the wind, `Λ = λ/(γM²)`, with
291/// `λ = 2γp/sin 2μ` at the handover's pressure `pressure_ratio` (times the free stream's) and
292/// surface Mach number `surface_mach`, and `M` the free stream's: the Prandtl–Meyer flow's
293/// `∂p/∂ν` over the free stream's `γM²/2`, halved as TN 3527's loading is. It is TN D-4865's
294/// equivalent bodies (eqs. 4a and 4b, the body turned about the sphere's center) read at `α → 0`,
295/// hpr's reading ([`crate::shock_expansion::HandoverStart::Newtonian`]).
296///
297/// # Errors
298///
299/// [`AeroError::Domain`] for a free-stream or surface Mach number that isn't finite and above 1,
300/// or a pressure that isn't finite and positive.
301pub fn handover_loading(
302 mach: f64,
303 pressure_ratio: f64,
304 surface_mach: f64,
305) -> Result<f64, AeroError> {
306 check_mach(mach)?;
307 if !(surface_mach.is_finite() && surface_mach > 1.0) {
308 return Err(AeroError::Domain {
309 what: "surface Mach number at a blunt tip's handover",
310 value: surface_mach,
311 });
312 }
313 if !(pressure_ratio.is_finite() && pressure_ratio > 0.0) {
314 return Err(AeroError::Domain {
315 what: "surface pressure ratio at a blunt tip's handover",
316 value: pressure_ratio,
317 });
318 }
319 let m2 = surface_mach * surface_mach;
320 let lambda = GAMMA * pressure_ratio * m2 / (m2 - 1.0).sqrt();
321 Ok(lambda / (GAMMA * mach * mach))
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327 use std::f64::consts::FRAC_PI_2;
328
329 fn close(got: f64, want: f64, tol: f64, what: &str) {
330 assert!(
331 (got - want).abs() <= tol,
332 "{what}: got {got}, want {want} ± {tol}"
333 );
334 }
335
336 /// NACA Report 1135, Table II (normal shock, γ = 1.4): `p_t2/p₁` at M = 1.5, 2, 3 and 5.
337 #[test]
338 fn the_pitot_pressure_is_rayleighs() {
339 close(pitot_pressure_ratio(1.5).unwrap(), 3.413, 5e-4, "M 1.5");
340 close(pitot_pressure_ratio(2.0).unwrap(), 5.640, 5e-4, "M 2");
341 close(pitot_pressure_ratio(3.0).unwrap(), 12.06, 5e-3, "M 3");
342 close(pitot_pressure_ratio(5.0).unwrap(), 32.65, 5e-3, "M 5");
343 // At Mach 1 the shock vanishes: the pitot pressure is the isentropic total, 1.8929.
344 close(pitot(1.0), 1.892_929, 1e-6, "M 1");
345 }
346
347 /// NACA Report 1135, chart 2: the largest wedge deflection is 12.1° at Mach 1.5, 22.97° at
348 /// Mach 2, 34.07° at Mach 3 and 41.1° at Mach 5, rising to 45.6° as Mach → ∞.
349 #[test]
350 fn the_wedge_detaches_where_naca_1135_says() {
351 let deg = |m: f64| wedge_detachment_angle_rad(m).unwrap().to_degrees();
352 close(deg(1.5), 12.11, 0.01, "M 1.5");
353 close(deg(2.0), 22.97, 0.01, "M 2");
354 close(deg(3.0), 34.07, 0.01, "M 3");
355 close(deg(5.0), 41.12, 0.02, "M 5");
356 close(deg(1e6), 45.58, 0.01, "M → ∞");
357 close(deg(1e200), 45.58, 0.01, "M → ∞, no overflow");
358 assert!(matches!(
359 pitot_pressure_ratio(1e200),
360 Err(AeroError::Domain { .. })
361 ));
362 // Near Mach 1 it falls to zero like (M² − 1)^(3/2).
363 assert!(deg(1.0 + 1e-9) < 1e-9);
364 // It rises with Mach.
365 let mut last = 0.0;
366 for i in 1..400 {
367 let d = deg(1.0 + 0.02 * f64::from(i));
368 assert!(d > last);
369 last = d;
370 }
371 }
372
373 /// Below the cap the handover is the wedge's own detachment angle, above it the cap. The
374 /// flown cap, 24°, binds from Mach 2.06; the steepest the tables carry, 30°, from Mach 2.52.
375 #[test]
376 fn the_handover_is_the_wedges_until_the_cap_binds() {
377 close(
378 handover_angle_rad(1.5).unwrap(),
379 wedge_detachment_angle_rad(1.5).unwrap(),
380 0.0,
381 "M 1.5",
382 );
383 assert_eq!(handover_angle_rad(3.0).unwrap(), MAX_HANDOVER_RAD);
384 // Where each cap starts to bind, bisected against the wedge's own angle.
385 let binds_from = |cap: f64| {
386 let (mut low, mut high) = (1.0_f64, 20.0_f64);
387 for _ in 0..200 {
388 let mid = 0.5 * (low + high);
389 if wedge_detachment_angle_rad(mid).unwrap() < cap {
390 low = mid;
391 } else {
392 high = mid;
393 }
394 }
395 0.5 * (low + high)
396 };
397 close(binds_from(MAX_HANDOVER_RAD), 2.0614, 5e-4, "24° binds from");
398 close(
399 binds_from(CONE_TABLE_CAP_RAD),
400 2.5192,
401 5e-4,
402 "30° binds from",
403 );
404 // Under a cap of its own the handover is the lesser of the two, and the cap can't ask for
405 // a cone the tables don't carry.
406 assert_eq!(
407 handover_angle_capped_rad(3.0, CONE_TABLE_CAP_RAD).unwrap(),
408 CONE_TABLE_CAP_RAD
409 );
410 close(
411 handover_angle_capped_rad(1.5, CONE_TABLE_CAP_RAD).unwrap(),
412 wedge_detachment_angle_rad(1.5).unwrap(),
413 0.0,
414 "M 1.5, 30° cap",
415 );
416 for bad in [0.0, -1.0, f64::NAN, CONE_TABLE_CAP_RAD * 1.000_001] {
417 assert!(
418 matches!(
419 handover_angle_capped_rad(3.0, bad),
420 Err(AeroError::Domain { .. })
421 ),
422 "a cap of {bad} should be refused"
423 );
424 }
425 }
426
427 #[test]
428 fn newtonian_pressures() {
429 // At the stagnation point the pitot pressure; at zero slope the free stream's.
430 let pitot = pitot_pressure_ratio(2.0).unwrap();
431 close(
432 newtonian_pressure_ratio(2.0, FRAC_PI_2).unwrap(),
433 pitot,
434 1e-12,
435 "p",
436 );
437 close(newtonian_pressure_ratio(2.0, 0.0).unwrap(), 1.0, 0.0, "p₀");
438 // C_p,max at Mach 2: (5.6404 − 1)/2.8.
439 close(
440 newtonian_pressure_coefficient_max(2.0).unwrap(),
441 (5.640_4 - 1.0) / 2.8,
442 1e-4,
443 "C_p,max",
444 );
445 assert!(matches!(
446 newtonian_pressure_ratio(2.0, 2.0),
447 Err(AeroError::Domain { .. })
448 ));
449 assert!(matches!(
450 pitot_pressure_ratio(1.0),
451 Err(AeroError::Domain { .. })
452 ));
453 }
454
455 /// A hemisphere's Newtonian loading integrates to `C_Nα = C_p,max/2` on its base: the drag
456 /// `C_p,max/2` of a Newtonian hemisphere turned into body axes (the force on a sphere passes
457 /// through its center, so it lies along the wind).
458 #[test]
459 fn a_hemisphere_carries_its_drag_turned() {
460 let mach = 3.0;
461 let c_p_max = newtonian_pressure_coefficient_max(mach).unwrap();
462 // With r = sin θ, x = 1 − cos θ (unit radius) the slope is cot θ.
463 let n = 20_000;
464 let mut sum = 0.0;
465 for i in 0..n {
466 let theta = FRAC_PI_2 * (f64::from(i) + 0.5) / f64::from(n);
467 let slope = 1.0 / theta.tan();
468 let load = newtonian_loading_at_slope(c_p_max, slope);
469 let r = theta.sin();
470 // dx = sin θ dθ.
471 sum += load * r * theta.sin() * FRAC_PI_2 / f64::from(n);
472 }
473 // C_Nα = (2π/π) ∫ Λ r dx on the unit base.
474 close(2.0 * sum, 0.5 * c_p_max, 1e-8, "C_Nα");
475 close(
476 newtonian_loading(mach, 0.3).unwrap(),
477 newtonian_loading_at_slope(c_p_max, 0.3f64.tan()),
478 1e-15,
479 "Λ",
480 );
481 assert_eq!(newtonian_loading_at_slope(c_p_max, f64::INFINITY), 0.0);
482 assert_eq!(newtonian_loading_at_slope(c_p_max, 0.0), 0.0);
483 }
484
485 /// A Newtonian cone of half-angle δ carries `C_Nα = C_p,max cos²δ` on its base, the classical
486 /// result; TN 3527's loading form gives it from `Λ = C_p,max sin δ cos δ`: `C_Nα = Λ/tan δ`.
487 #[test]
488 fn a_newtonian_cone_carries_c_p_max_cos_squared() {
489 let delta = 0.2_f64;
490 let load = newtonian_loading(2.5, delta).unwrap();
491 let c_p_max = newtonian_pressure_coefficient_max(2.5).unwrap();
492 close(
493 load / delta.tan(),
494 c_p_max * delta.cos().powi(2),
495 1e-14,
496 "cone",
497 );
498 }
499
500 #[test]
501 fn newtonian_surface_mach_numbers() {
502 let pitot = pitot_pressure_ratio(2.0).unwrap();
503 close(newtonian_surface_mach(2.0, pitot).unwrap(), 0.0, 1e-12, "M");
504 // Expanded to the free stream's pressure through the normal shock's total, the surface
505 // flow is slower than the free stream.
506 let m = newtonian_surface_mach(2.0, 1.0).unwrap();
507 assert!(m > 1.5 && m < 2.0, "{m}");
508 assert!(matches!(
509 newtonian_surface_mach(2.0, pitot * 1.01),
510 Err(AeroError::Domain { .. })
511 ));
512 }
513
514 /// Behind a handover fixed in the wind the loading is the Prandtl–Meyer flow's: a turn `dν`
515 /// lowers the pressure by `λ dν`, checked by a finite difference of the expansion itself.
516 #[test]
517 fn the_handover_loading_is_the_expansions() {
518 use crate::afterbody::{inverse_prandtl_meyer, prandtl_meyer};
519 let mach = 2.3;
520 let delta = handover_angle_rad(mach).unwrap();
521 let p = newtonian_pressure_ratio(mach, delta).unwrap();
522 let m = newtonian_surface_mach(mach, p).unwrap();
523 let total = pitot_pressure_ratio(mach).unwrap();
524 let static_at = |nu: f64| {
525 let ms = inverse_prandtl_meyer(nu);
526 total / (1.0 + 0.2 * ms * ms).powf(3.5)
527 };
528 let h = 1e-6;
529 let nu = prandtl_meyer(m);
530 // A turn smaller by ε raises the pressure by λε, C_p by 2λε/(γM²); Λ is half that.
531 let dp = (static_at(nu - h) - static_at(nu + h)) / (2.0 * h);
532 let want = dp / (GAMMA * mach * mach);
533 close(
534 handover_loading(mach, p, m).unwrap(),
535 want,
536 1e-6 * want,
537 "Λ",
538 );
539 assert!(matches!(
540 handover_loading(mach, p, 1.0),
541 Err(AeroError::Domain { .. })
542 ));
543 }
544}