STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
nozzle_solver.py
Go to the documentation of this file.
1import numpy as np
2import matplotlib.pyplot as plt
3from scipy.optimize import root
4from engine.core.nozzle_angles import lookup_angles_interp_bell
5
6
7# -----------------------------
8# Rotated parabola (Garcia Eq. 7)
9# -----------------------------
10def rotated_parabola_xy(t, m, theta, h, k):
11 """
12 x(t) = t(-m sinθ t + cosθ) + h
13 y(t) = t( m cosθ t + sinθ) + k
14 """
15 t = np.asarray(t)
16 s = np.sin(theta)
17 c = np.cos(theta)
18
19 x = t * (-m * s * t + c) + h
20 y = t * ( m * c * t + s) + k
21 return x, y
22
23
24def solve_rotated_parabola_params(Nx, Ny, Ex, Ey, theta_n, theta_e):
25 """
26 Solve for m, h, k, psi, t5, t6 such that the rotated parabola goes
27 through N and E with the correct slopes at each end.
28 """
29
30 def equations(u):
31 m, h, k, psi, t5, t6 = u
32 s = np.sin(psi)
33 c = np.cos(psi)
34
35 def xy(t):
36 x = -m * s * t**2 + c * t + h
37 y = m * c * t**2 + s * t + k
38 return x, y
39
40 def slope(t):
41 dxdt = -2 * m * s * t + c
42 dydt = 2 * m * c * t + s
43 return dydt / dxdt
44
45 x5, y5 = xy(t5)
46 x6, y6 = xy(t6)
47
48 eq1 = x5 - Nx
49 eq2 = y5 - Ny
50 eq3 = x6 - Ex
51 eq4 = y6 - Ey
52 eq5 = slope(t5) - np.tan(theta_n)
53 eq6 = slope(t6) - np.tan(theta_e)
54 return [eq1, eq2, eq3, eq4, eq5, eq6]
55
56 # crude initial guess
57 L = Ex - Nx if Ex != Nx else 1.0
58 m0 = (Ey - Ny) / (L**2)
59 h0 = Nx
60 k0 = Ny
61 psi0 = 0.5 * (theta_n + theta_e)
62 t50 = 0.0
63 t60 = L
64
65 u0 = [m0, h0, k0, psi0, t50, t60]
66 sol = root(equations, u0, method="hybr")
67
68 if not sol.success:
69 raise RuntimeError(f"Parabola solve failed: {sol.message}")
70
71 return sol.x # m, h, k, psi, t5, t6
72
73
74# -----------------------------
75# Placeholder for true Rao MoC nozzle
76# -----------------------------
77def generate_rao_moc_contour(Nx, Ny, Ex, Ey, theta_n, theta_e,
78 steps=200, gamma=1.23):
79 """
80 TRUE Rao Method-of-Characteristics nozzle contour placeholder.
81 """
82 raise NotImplementedError(
83 "generate_rao_moc_contour is not implemented. "
84 "Use method='top' for a standard Thrust-Optimized Parabolic "
85 "approximation of Rao, or 'garcia' for the rotated-parabola form."
86 )
87
88
89# -----------------------------
90# Main Rao-style nozzle generator
91# -----------------------------
92def rao(area_throat,
93 area_exit,
94 bell_percent=0.8,
95 steps=200,
96 do_plot=True,
97 color_segments=False,
98 method="garcia",
99 gamma=1.23,
100 convergent_half_angle_rad=np.pi / 4.0):
101 """
102 method:
103 - "top" : Thrust-Optimized Parabolic (quadratic Bézier N-Q-E)
104 - "garcia" : rotated/canted parabola (RRS Garcia 2023)
105 - "rao_moc" : placeholder hook for a true Rao MoC solver
106 """
107
108 eps = area_exit / area_throat
109 deg2rad = lambda d: d * np.pi / 180.0
110
111 # Look up angles using interpolation
112 theta_n_degree, theta_e_degree = lookup_angles_interp_bell(
113 area_throat, area_exit, bell_percent
114 )
115 theta_n = deg2rad(theta_n_degree)
116 theta_e = deg2rad(theta_e_degree)
117
118 r_t = np.sqrt(area_throat / np.pi)
119 r_e = np.sqrt(area_exit / np.pi)
120
121 # ----- Entrance arc: 1.5 Rt, tangent to the convergent cone
122 # The arc's wall slope at parameter t is -cot(t), and a cone of half-angle
123 # theta_c has slope -tan(theta_c), so the two meet tangentially only when the
124 # arc starts at t = -(90 deg + theta_c). This was hardcoded to -135 deg, which
125 # is tangency with a 45 deg cone and ONLY a 45 deg cone -- any other convergent
126 # angle met the throat arc at a slope discontinuity, i.e. a corner in the wall
127 # right where the gas accelerates hardest. Default is unchanged at 45 deg.
128 _theta_c = float(convergent_half_angle_rad)
129 theta1 = np.linspace(-(np.pi / 2.0 + _theta_c), deg2rad(-90), steps)
130 x1 = 1.5 * r_t * np.cos(theta1)
131 y1 = 1.5 * r_t * np.sin(theta1) + 1.5 * r_t + r_t
132
133 # ----- Throat arc: 0.382 Rt
134 theta2 = np.linspace(deg2rad(-90), theta_n - deg2rad(90), steps)
135 x2 = 0.382 * r_t * np.cos(theta2)
136 y2 = 0.382 * r_t * np.sin(theta2) + 0.382 * r_t + r_t
137
138 # Start of bell (point N)
139 Nx, Ny = x2[-1], y2[-1]
140
141 # End of bell (point E)
142 L_noz = bell_percent * ((np.sqrt(eps) - 1.0) * r_t / np.tan(deg2rad(15.0)))
143 Ex = L_noz
144 Ey = r_e
145
146 method = method.lower()
147 param_text = ""
148
149 # -----------------------
150 # Bell section by method
151 # -----------------------
152 if method == "top":
153 # Classic Thrust-Optimized Parabolic (quadratic Bézier)
154 m1 = np.tan(theta_n)
155 m2 = np.tan(theta_e)
156 c1 = Ny - m1 * Nx
157 c2 = Ey - m2 * Ex
158
159 Qx = (c2 - c1) / (m1 - m2)
160 Qy = (m1 * c2 - m2 * c1) / (m1 - m2)
161
162 t = np.linspace(0.0, 1.0, steps)
163 x3 = (1 - t)**2 * Nx + 2*(1 - t)*t*Qx + t**2 * Ex
164 y3 = (1 - t)**2 * Ny + 2*(1 - t)*t*Qy + t**2 * Ey
165
166 param_text = (
167 "TOP Bézier bell\n"
168 f"N = ({Nx:.4g}, {Ny:.4g})\n"
169 f"Q = ({Qx:.4g}, {Qy:.4g})\n"
170 f"E = ({Ex:.4g}, {Ey:.4g})\n"
171 f"θ_n = {theta_n_degree:.2f}°, θ_e = {theta_e_degree:.2f}°\n"
172 f"ε = {eps:.4g}, L_noz = {L_noz:.4g}"
173 )
174
175 elif method == "garcia":
176 # Garcia canted, rotated parabola
177 m, h, k, psi, t5, t6 = solve_rotated_parabola_params(
178 Nx, Ny, Ex, Ey, theta_n, theta_e
179 )
180 t_vals = np.linspace(t5, t6, steps)
181 x3, y3 = rotated_parabola_xy(t_vals, m=m, theta=psi, h=h, k=k)
182
183 param_text = (
184 "Garcia rotated parabola\n"
185 f"N = ({Nx:.4g}, {Ny:.4g})\n"
186 f"E = ({Ex:.4g}, {Ey:.4g})\n"
187 f"θ_n = {theta_n_degree:.2f}°, θ_e = {theta_e_degree:.2f}°\n"
188 f"m = {m:.4g}, h = {h:.4g}, k = {k:.4g}\n"
189 f"ψ = {np.degrees(psi):.2f}°\n"
190 f"t5 = {t5:.4g}, t6 = {t6:.4g}"
191 )
192
193 elif method == "rao_moc":
194 # True Rao MoC (not implemented – placeholder)
196 Nx, Ny, Ex, Ey, theta_n, theta_e,
197 steps=steps, gamma=gamma
198 )
199 param_text = (
200 "Rao MoC (placeholder)\n"
201 f"N = ({Nx:.4g}, {Ny:.4g})\n"
202 f"E = ({Ex:.4g}, {Ey:.4g})\n"
203 f"θ_n = {theta_n_degree:.2f}°, θ_e = {theta_e_degree:.2f}°\n"
204 f"ε = {eps:.4g}, L_noz = {L_noz:.4g}\n"
205 "MoC contour not yet implemented"
206 )
207 else:
208 raise ValueError(f"Unknown method '{method}'. "
209 "Use 'top', 'garcia', or 'rao_moc'.")
210
211 # ---- Combine all pieces
212 pts = np.vstack([
213 np.column_stack((x1, y1)),
214 np.column_stack((x2[1:], y2[1:])),
215 np.column_stack((x3[1:], y3[1:]))
216 ])
217
218 # ---- Plot
219 if do_plot:
220 plt.figure(figsize=(8, 4))
221
222 if color_segments:
223 plt.plot(x1, y1, label='Entrance arc', color='red')
224 plt.plot(x2, y2, label='Throat arc', color='green')
225 plt.plot(x3, y3, label=f'Bell ({method})', color='blue')
226 plt.legend()
227 else:
228 plt.plot(pts[:, 0], pts[:, 1], 'k-', linewidth=2)
229
230 plt.gca().set_aspect('equal', 'box')
231 plt.xlabel("Axial distance x")
232 plt.ylabel("Radius y")
233 plt.grid(True, alpha=0.3)
234 plt.title(f"Rao nozzle – {method.upper()}")
235
236 # Add parameter text box on the figure
237 plt.gcf().text(
238 0.02, 0.02, param_text,
239 fontsize=8,
240 va="bottom", ha="left",
241 bbox=dict(boxstyle="round", facecolor="white", alpha=0.8)
242 )
243
244
245
246 # NEW:
247 plt.savefig('nozzle_models/rao/rao_nozzle.png', dpi=150, bbox_inches='tight')
248 plt.close()
249
250 # Get x value of first point
251 x_first = pts[0, 0]
252 y_first = pts[0, 1]
253
254 return pts, x_first, y_first
255
256
257# Example usage (commented out to avoid running on import)
258# pts, x_first = rao(0.00156235266901, 0.00831498636119,
259# method="top", do_plot=True, color_segments=True)
260# print(f"First point x value: {x_first}")
rotated_parabola_xy(t, m, theta, h, k)
rao(area_throat, area_exit, bell_percent=0.8, steps=200, do_plot=True, color_segments=False, method="garcia", gamma=1.23, convergent_half_angle_rad=np.pi/4.0)
generate_rao_moc_contour(Nx, Ny, Ex, Ey, theta_n, theta_e, steps=200, gamma=1.23)
solve_rotated_parabola_params(Nx, Ny, Ex, Ey, theta_n, theta_e)