1"""Mach number solver for isentropic nozzle flow.
3This module provides the area-Mach relation solver used throughout the nozzle
4calculations. Consolidates previously duplicated Newton-Raphson implementations.
7 The area-Mach relation for isentropic flow:
8 A/A* = (1/M) × [(2/(γ+1)) × (1 + (γ-1)/2 × M²)]^((γ+1)/(2(γ-1)))
10 For a given area ratio ε = A/A* > 1, there are two solutions:
11 1. Subsonic solution (M < 1)
12 2. Supersonic solution (M > 1)
15from __future__
import annotations
18from dataclasses
import dataclass
19from typing
import Optional, Tuple
25 """Result from Mach number solver."""
35 Calculate area ratio A/A* for given Mach number and gamma.
37 A/A* = (1/M) × [(2/(γ+1)) × (1 + (γ-1)/2 × M²)]^((γ+1)/(2(γ-1)))
42 Mach number (must be > 0)
44 Specific heat ratio (must be > 1)
51 if M <= 0
or gamma <= 1.0:
52 raise ValueError(f
"Invalid inputs: M={M}, gamma={gamma}")
54 term = (2.0 / (gamma + 1.0)) * (1.0 + (gamma - 1.0) / 2.0 * M**2)
55 exponent = (gamma + 1.0) / (2.0 * (gamma - 1.0))
56 A_Astar = (1.0 / M) * (term ** exponent)
63 Calculate derivative d(A/A*)/dM for Newton-Raphson solver.
71 A_Astar : float, optional
72 Pre-calculated A/A* (for efficiency)
77 Derivative of area ratio with respect to Mach number
84 numerator = 2.0 * (M**2 - 1.0)
85 denominator = M * (2.0 + (gamma - 1.0) * M**2)
87 return A_Astar * (numerator / denominator)
92 Estimate initial Mach number for given area ratio.
97 Area ratio A/A* (must be >= 1)
101 Whether to estimate the supersonic solution (M > 1) or subsonic (M < 1)
106 Initial guess for Mach number
111 raise ValueError(f
"Area ratio must be >= 1: eps={eps}")
113 raise ValueError(f
"Gamma must be > 1: gamma={gamma}")
122 p = (gamma - 1.0) / 2.0
123 prefactor = ((gamma + 1.0) / (gamma - 1.0)) ** ((gamma + 1.0) / 4.0)
124 M_guess = prefactor * (eps ** p)
127 M_guess = 1.0 + np.sqrt(2.0 * (eps - 1.0) / (gamma + 1.0))
130 M_guess = 1.0 + 0.5 * (eps - 1.0)
133 return max(M_guess, 1.0 + 1e-6)
141 prefactor = (2.0 / (gamma + 1.0)) ** ((gamma + 1.0) / (2.0 * (gamma - 1.0)))
142 M_guess = prefactor / eps
145 M_guess = 1.0 - 0.5 * np.sqrt(2.0 * (eps - 1.0) / (gamma + 1.0))
148 return np.clip(M_guess, 1e-6, 1.0 - 1e-6)
154 supersonic: bool =
True,
155 tolerance: float = 1e-10,
156 max_iterations: int = 50,
157 initial_guess: Optional[float] =
None,
158) -> MachSolverResult:
160 Solve for Mach number given area ratio using Newton-Raphson.
162 Finds either the supersonic (M > 1) or subsonic (M < 1) solution.
167 Area ratio A/A* (must be >= 1)
169 Specific heat ratio (must be > 1)
171 Whether to find the supersonic or subsonic solution
173 Convergence tolerance for residual
175 Maximum Newton-Raphson iterations
176 initial_guess : float, optional
177 Initial guess for M (if None, uses estimate_initial_mach)
181 result : MachSolverResult
182 Solver result with M, convergence info, and diagnostics
189 raise ValueError(f
"Area ratio must be >= 1: eps={eps}")
191 raise ValueError(f
"Gamma must be > 1: gamma={gamma}")
194 return MachSolverResult(M=1.0, converged=
True, iterations=0, error=0.0, A_Astar_actual=1.0)
197 if initial_guess
is not None:
198 if supersonic
and initial_guess > 1.0:
200 elif not supersonic
and initial_guess < 1.0:
210 for iteration
in range(max_iterations):
213 error = A_Astar - eps
216 if abs(error) < tolerance:
220 iterations=iteration + 1,
222 A_Astar_actual=A_Astar,
229 if abs(dA_dM) < 1e-12:
235 if error > 0: M *= 0.99
238 if error > 0: M *= 1.01
243 step = np.clip(step, -0.5 * M, 0.5 * M)
260 converged=abs(error) < tolerance * 10,
261 iterations=max_iterations,
262 error=abs(A_Astar_final - eps),
263 A_Astar_actual=A_Astar_final,
270 supersonic: bool =
True,
271) -> Tuple[float, bool]:
273 Robust wrapper for Mach solver with automatic fallback.
282 Whether to find the supersonic or subsonic solution
289 Whether solver converged successfully
293 if eps > 0.999: eps = 1.0
295 warnings.warn(f
"Invalid area ratio: eps={eps}. Using sonic condition.")
299 warnings.warn(f
"Invalid gamma: gamma={gamma}. Using fallback.")
300 return 2.0
if supersonic
else 0.1,
False
305 if not result.converged:
307 f
"Mach solver did not fully converge: |error| = {result.error:.2e} "
308 f
"after {result.iterations} iterations. M={result.M:.4f}"
312 if supersonic
and result.M <= 1.0:
313 return 1.0 + 1e-6,
False
314 if not supersonic
and result.M >= 1.0:
315 return 1.0 - 1e-6,
False
317 return result.M, result.converged
319 except Exception
as e:
320 warnings.warn(f
"Mach solver failed: {e}. Using fallback.")
321 return 2.0
if supersonic
else 0.1,
False
327) -> Tuple[float, bool]:
329 Backward compatible wrapper for supersonic exit Mach solver.
338) -> MachSolverResult:
340 Backward compatible wrapper for supersonic exit Mach solver.
float estimate_initial_mach(float eps, float gamma, bool supersonic=True)
float calculate_area_mach_derivative(float M, float gamma, Optional[float] A_Astar=None)
float calculate_area_mach_ratio(float M, float gamma)
MachSolverResult solve_exit_mach_from_area_ratio(float eps, float gamma, **kwargs)
Tuple[float, bool] solve_mach_robust(float eps, float gamma, bool supersonic=True)
Tuple[float, bool] solve_exit_mach_robust(float eps, float gamma)
MachSolverResult solve_mach_from_area_ratio(float eps, float gamma, bool supersonic=True, float tolerance=1e-10, int max_iterations=50, Optional[float] initial_guess=None)