STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
mach_solver.py
Go to the documentation of this file.
1"""Mach number solver for isentropic nozzle flow.
2
3This module provides the area-Mach relation solver used throughout the nozzle
4calculations. Consolidates previously duplicated Newton-Raphson implementations.
5
6Physics:
7 The area-Mach relation for isentropic flow:
8 A/A* = (1/M) × [(2/(γ+1)) × (1 + (γ-1)/2 × M²)]^((γ+1)/(2(γ-1)))
9
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)
13"""
14
15from __future__ import annotations
16
17import warnings
18from dataclasses import dataclass
19from typing import Optional, Tuple
20import numpy as np
21
22
23@dataclass
25 """Result from Mach number solver."""
26 M: float # Mach number solution
27 converged: bool # Whether solver converged
28 iterations: int # Number of iterations used
29 error: float # Final residual error
30 A_Astar_actual: float # Actual area ratio achieved
31
32
33def calculate_area_mach_ratio(M: float, gamma: float) -> float:
34 """
35 Calculate area ratio A/A* for given Mach number and gamma.
36
37 A/A* = (1/M) × [(2/(γ+1)) × (1 + (γ-1)/2 × M²)]^((γ+1)/(2(γ-1)))
38
39 Parameters
40 ----------
41 M : float
42 Mach number (must be > 0)
43 gamma : float
44 Specific heat ratio (must be > 1)
45
46 Returns
47 -------
48 A_Astar : float
49 Area ratio A/A*
50 """
51 if M <= 0 or gamma <= 1.0:
52 raise ValueError(f"Invalid inputs: M={M}, gamma={gamma}")
53
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)
57
58 return A_Astar
59
60
61def calculate_area_mach_derivative(M: float, gamma: float, A_Astar: Optional[float] = None) -> float:
62 """
63 Calculate derivative d(A/A*)/dM for Newton-Raphson solver.
64
65 Parameters
66 ----------
67 M : float
68 Mach number
69 gamma : float
70 Specific heat ratio
71 A_Astar : float, optional
72 Pre-calculated A/A* (for efficiency)
73
74 Returns
75 -------
76 dA_dM : float
77 Derivative of area ratio with respect to Mach number
78 """
79 if A_Astar is None:
80 A_Astar = calculate_area_mach_ratio(M, gamma)
81
82 # Simplified and more stable derivative:
83 # d(A/A*)/dM = (A/A*) * 2 * (M^2 - 1) / (M * (2 + (gamma-1)*M^2))
84 numerator = 2.0 * (M**2 - 1.0)
85 denominator = M * (2.0 + (gamma - 1.0) * M**2)
86
87 return A_Astar * (numerator / denominator)
88
89
90def estimate_initial_mach(eps: float, gamma: float, supersonic: bool = True) -> float:
91 """
92 Estimate initial Mach number for given area ratio.
93
94 Parameters
95 ----------
96 eps : float
97 Area ratio A/A* (must be >= 1)
98 gamma : float
99 Specific heat ratio
100 supersonic : bool
101 Whether to estimate the supersonic solution (M > 1) or subsonic (M < 1)
102
103 Returns
104 -------
105 M_guess : float
106 Initial guess for Mach number
107 """
108 if eps < 1.0:
109 if eps > 0.999: # Handle floating point near 1.0
110 return 1.0
111 raise ValueError(f"Area ratio must be >= 1: eps={eps}")
112 if gamma <= 1.0:
113 raise ValueError(f"Gamma must be > 1: gamma={gamma}")
114
115 if eps == 1.0:
116 return 1.0
117
118 if supersonic:
119 if eps > 10.0:
120 # Large expansion ratio - supersonic asymptotic formula
121 # Derived from M >> 1 limit: M ~ eps^((g-1)/2) * prefactor
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)
125 elif eps > 1.5:
126 # Moderate expansion ratio - improved approximation
127 M_guess = 1.0 + np.sqrt(2.0 * (eps - 1.0) / (gamma + 1.0))
128 else:
129 # Small expansion ratio (near 1) - linear approximation
130 M_guess = 1.0 + 0.5 * (eps - 1.0)
131
132 # Ensure supersonic (M > 1)
133 return max(M_guess, 1.0 + 1e-6)
134 else:
135 # Subsonic guess (M < 1)
136 # For M << 1, A/A* ~ (prefactor/M)
137 # For M near 1, eps ~ 1 + 0.5 * (gamma+1) * (M-1)^2 / 2 ? No.
138 # Simple approximation for subsonic:
139 if eps > 2.0:
140 # Very small M approximation: A/A* ~ 1/M * (2/(g+1))^((g+1)/(2(g-1)))
141 prefactor = (2.0 / (gamma + 1.0)) ** ((gamma + 1.0) / (2.0 * (gamma - 1.0)))
142 M_guess = prefactor / eps
143 else:
144 # Near sonic: eps ~ 1 + 0.5 * (1-M)^2 * (gamma+1)/2? No, simpler:
145 M_guess = 1.0 - 0.5 * np.sqrt(2.0 * (eps - 1.0) / (gamma + 1.0))
146
147 # Ensure subsonic (M < 1)
148 return np.clip(M_guess, 1e-6, 1.0 - 1e-6)
149
150
152 eps: float,
153 gamma: float,
154 supersonic: bool = True,
155 tolerance: float = 1e-10,
156 max_iterations: int = 50,
157 initial_guess: Optional[float] = None,
158) -> MachSolverResult:
159 """
160 Solve for Mach number given area ratio using Newton-Raphson.
161
162 Finds either the supersonic (M > 1) or subsonic (M < 1) solution.
163
164 Parameters
165 ----------
166 eps : float
167 Area ratio A/A* (must be >= 1)
168 gamma : float
169 Specific heat ratio (must be > 1)
170 supersonic : bool
171 Whether to find the supersonic or subsonic solution
172 tolerance : float
173 Convergence tolerance for residual
174 max_iterations : int
175 Maximum Newton-Raphson iterations
176 initial_guess : float, optional
177 Initial guess for M (if None, uses estimate_initial_mach)
178
179 Returns
180 -------
181 result : MachSolverResult
182 Solver result with M, convergence info, and diagnostics
183 """
184 # Validate inputs
185 if eps < 1.0:
186 if eps > 0.999:
187 eps = 1.0
188 else:
189 raise ValueError(f"Area ratio must be >= 1: eps={eps}")
190 if gamma <= 1.0:
191 raise ValueError(f"Gamma must be > 1: gamma={gamma}")
192
193 if eps == 1.0:
194 return MachSolverResult(M=1.0, converged=True, iterations=0, error=0.0, A_Astar_actual=1.0)
195
196 # Initial guess
197 if initial_guess is not None:
198 if supersonic and initial_guess > 1.0:
199 M = initial_guess
200 elif not supersonic and initial_guess < 1.0:
201 M = initial_guess
202 else:
203 M = estimate_initial_mach(eps, gamma, supersonic)
204 else:
205 M = estimate_initial_mach(eps, gamma, supersonic)
206
207 # Newton-Raphson iteration
208 error = float('inf')
209
210 for iteration in range(max_iterations):
211 # Calculate A/A* and error
212 A_Astar = calculate_area_mach_ratio(M, gamma)
213 error = A_Astar - eps
214
215 # Check convergence
216 if abs(error) < tolerance:
217 return MachSolverResult(
218 M=M,
219 converged=True,
220 iterations=iteration + 1,
221 error=abs(error),
222 A_Astar_actual=A_Astar,
223 )
224
225 # Calculate derivative
226 dA_dM = calculate_area_mach_derivative(M, gamma, A_Astar)
227
228 # Newton step with safeguards
229 if abs(dA_dM) < 1e-12:
230 # Derivative too small - use bisection-like fallback
231 # Since f(M) has a minimum at M=1:
232 # Supersonic: f'(M) > 0, increasing M increases f(M)
233 # Subsonic: f'(M) < 0, increasing M decreases f(M)
234 if supersonic:
235 if error > 0: M *= 0.99
236 else: M *= 1.01
237 else:
238 if error > 0: M *= 1.01
239 else: M *= 0.99
240 else:
241 step = error / dA_dM
242 # Limit step size to prevent overshoot
243 step = np.clip(step, -0.5 * M, 0.5 * M)
244 M = M - step
245
246 # Ensure Mach number remains in the correct regime
247 if supersonic:
248 if M <= 1.0:
249 M = 1.0 + 1e-6
250 else:
251 if M >= 1.0:
252 M = 1.0 - 1e-6
253 if M <= 0:
254 M = 1e-6
255
256 # Did not converge within max_iterations
257 A_Astar_final = calculate_area_mach_ratio(M, gamma)
258 return MachSolverResult(
259 M=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,
264 )
265
266
268 eps: float,
269 gamma: float,
270 supersonic: bool = True,
271) -> Tuple[float, bool]:
272 """
273 Robust wrapper for Mach solver with automatic fallback.
274
275 Parameters
276 ----------
277 eps : float
278 Area ratio A/A*
279 gamma : float
280 Specific heat ratio
281 supersonic : bool
282 Whether to find the supersonic or subsonic solution
283
284 Returns
285 -------
286 M : float
287 Mach number solution
288 success : bool
289 Whether solver converged successfully
290 """
291 # Validate basic physics
292 if eps < 1.0:
293 if eps > 0.999: eps = 1.0
294 else:
295 warnings.warn(f"Invalid area ratio: eps={eps}. Using sonic condition.")
296 return 1.0, False
297
298 if gamma <= 1.0:
299 warnings.warn(f"Invalid gamma: gamma={gamma}. Using fallback.")
300 return 2.0 if supersonic else 0.1, False
301
302 try:
303 result = solve_mach_from_area_ratio(eps, gamma, supersonic)
304
305 if not result.converged:
306 warnings.warn(
307 f"Mach solver did not fully converge: |error| = {result.error:.2e} "
308 f"after {result.iterations} iterations. M={result.M:.4f}"
309 )
310
311 # Validate result is in correct regime
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
316
317 return result.M, result.converged
318
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
322
323
325 eps: float,
326 gamma: float,
327) -> Tuple[float, bool]:
328 """
329 Backward compatible wrapper for supersonic exit Mach solver.
330 """
331 return solve_mach_robust(eps, gamma, supersonic=True)
332
333
335 eps: float,
336 gamma: float,
337 **kwargs
338) -> MachSolverResult:
339 """
340 Backward compatible wrapper for supersonic exit Mach solver.
341 """
342 return solve_mach_from_area_ratio(eps, gamma, supersonic=True, **kwargs)
#define max(a, b)
Definition Arduino.h:43
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)