STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
nozzle_angles.py
Go to the documentation of this file.
1"""
2Nozzle angle lookup table for Rao nozzle design.
3
4This table provides theta_n (nozzle entrance angle) and theta_e (nozzle exit angle)
5as functions of:
6- Nozzle expansion ratio (eps = area_exit / area_throat)
7- Bell percentage (60%, 70%, 80%, 90%, 100%)
8
9Data extracted from parabola angles graph.
10
11Usage:
12 from nozzle_angles import lookup_angles
13
14 theta_n, theta_e = lookup_angles(expansion_ratio=10.0, bell_percent=0.8)
15"""
16
17import numpy as np
18import matplotlib.pyplot as plt
19from scipy.interpolate import interp1d
20
21# Lookup table: [expansion_ratio, bell_percent] -> (theta_n, theta_e)
22# Expansion ratios sampled at key points (log scale: 3, 4, 5, 10, 20, 100)
23# Interpolation range: 3.0 to 100.0 (values outside this range are clamped)
24# Bell percentages: 0.60, 0.70, 0.80, 0.90, 1.00
25
26NOZZLE_ANGLES_TABLE = {
27 # Format: expansion_ratio: {bell_percent: (theta_n_deg, theta_e_deg)}
28 # Interpolation range: 3.0 to 100.0
29
30 4.0: {
31 0.60: (26.5, 20.8),
32 0.70: (23.9, 17.1),
33 0.80: (21.6, 13.95),
34 0.90: (20.0, 11.2),
35 1.00: (19.0, 9.1),
36 },
37 5.0: {
38 0.60: (28.0, 19.1),
39 0.70: (25.1, 15.95),
40 0.80: (23.0, 12.95),
41 0.90: (20.95, 10.5),
42 1.00: (19.9, 8.1),
43 },
44 10.0: {
45 0.60: (32.0, 16.05),
46 0.70: (28.3, 13.2),
47 0.80: (26.2, 10.15),
48 0.90: (24.1, 8.0),
49 1.00: (22.5, 6.05),
50 },
51 20.0: {
52 0.60: (34.9, 14.5),
53 0.70: (31.1, 12.0),
54 0.80: (28.8, 9.0),
55 0.90: (27.0, 7.0),
56 1.00: (25.1, 5.1),
57 },
58 100.0: {
59 0.60: (40.1, 12.1),
60 0.70: (36.2, 9.5),
61 0.80: (33.6, 6.95),
62 0.90: (32.6, 6.0),
63 1.00: (31.9, 4.1),
64 },
65}
66
67
68def lookup_angles(expansion_ratio, bell_percent=0.8):
69 """
70 Look up nozzle angles for given expansion ratio and bell percentage.
71
72 Parameters:
73 -----------
74 expansion_ratio : float
75 Nozzle expansion ratio (eps = area_exit / area_throat)
76 bell_percent : float
77 Bell percentage (0.60, 0.70, 0.80, 0.90, or 1.00)
78
79 Returns:
80 --------
81 theta_n_deg : float
82 Nozzle entrance angle in degrees
83 theta_e_deg : float
84 Nozzle exit angle in degrees
85 """
86 # Clamp expansion ratio to valid interpolation range [3.0, 100.0]
87 eps = max(3.0, min(100.0, expansion_ratio))
88
89 # Round bell_percent to nearest valid value
90 valid_percents = [0.60, 0.70, 0.80, 0.90, 1.00]
91 bell_pct = min(valid_percents, key=lambda x: abs(x - bell_percent))
92
93 # Get expansion ratio keys
94 eps_keys = sorted(NOZZLE_ANGLES_TABLE.keys())
95
96 # If exact match, return directly
97 if eps in eps_keys:
98 return NOZZLE_ANGLES_TABLE[eps][bell_pct]
99
100 # Interpolate between expansion ratios
101 if eps < eps_keys[0]:
102 # Extrapolate below minimum
103 eps1, eps2 = eps_keys[0], eps_keys[1]
104 theta_n1, theta_e1 = NOZZLE_ANGLES_TABLE[eps1][bell_pct]
105 theta_n2, theta_e2 = NOZZLE_ANGLES_TABLE[eps2][bell_pct]
106 # Linear interpolation in log space
107 log_eps = np.log(eps)
108 log_eps1 = np.log(eps1)
109 log_eps2 = np.log(eps2)
110 t = (log_eps - log_eps1) / (log_eps2 - log_eps1)
111 theta_n = theta_n1 + t * (theta_n2 - theta_n1)
112 theta_e = theta_e1 + t * (theta_e2 - theta_e1)
113 return theta_n, theta_e
114
115 if eps > eps_keys[-1]:
116 # Extrapolate above maximum
117 eps1, eps2 = eps_keys[-2], eps_keys[-1]
118 theta_n1, theta_e1 = NOZZLE_ANGLES_TABLE[eps1][bell_pct]
119 theta_n2, theta_e2 = NOZZLE_ANGLES_TABLE[eps2][bell_pct]
120 # Linear interpolation in log space
121 log_eps = np.log(eps)
122 log_eps1 = np.log(eps1)
123 log_eps2 = np.log(eps2)
124 t = (log_eps - log_eps1) / (log_eps2 - log_eps1)
125 theta_n = theta_n1 + t * (theta_n2 - theta_n1)
126 theta_e = theta_e1 + t * (theta_e2 - theta_e1)
127 return theta_n, theta_e
128
129 # Find bounding expansion ratios
130 for i in range(len(eps_keys) - 1):
131 if eps_keys[i] <= eps < eps_keys[i + 1]:
132 eps1, eps2 = eps_keys[i], eps_keys[i + 1]
133 theta_n1, theta_e1 = NOZZLE_ANGLES_TABLE[eps1][bell_pct]
134 theta_n2, theta_e2 = NOZZLE_ANGLES_TABLE[eps2][bell_pct]
135 # Linear interpolation in log space (since x-axis is logarithmic)
136 log_eps = np.log(eps)
137 log_eps1 = np.log(eps1)
138 log_eps2 = np.log(eps2)
139 t = (log_eps - log_eps1) / (log_eps2 - log_eps1)
140 theta_n = theta_n1 + t * (theta_n2 - theta_n1)
141 theta_e = theta_e1 + t * (theta_e2 - theta_e1)
142 return theta_n, theta_e
143
144 # Fallback (shouldn't reach here)
145 return NOZZLE_ANGLES_TABLE[eps_keys[-1]][bell_pct]
146
147
148def lookup_angles_interp_bell(area_throat, area_exit, bell_percent=0.8):
149 """
150 Look up nozzle angles with interpolation in both expansion ratio and bell percentage.
151
152 Parameters:
153 -----------
154 expansion_ratio : float
155 Nozzle expansion ratio (eps = area_exit / area_throat)
156 bell_percent : float
157 Bell percentage (0.60 to 1.00)
158
159 Returns:
160 --------
161 theta_n_deg : float
162 Nozzle entrance angle in degrees
163 theta_e_deg : float
164 Nozzle exit angle in degrees
165 """
166 expansion_ratio = area_exit / area_throat
167
168 # Clamp bell_percent to valid range
169 bell_pct = max(0.60, min(1.00, bell_percent))
170
171 # Get expansion ratio keys
172 eps_keys = sorted(NOZZLE_ANGLES_TABLE.keys())
173 valid_percents = [0.60, 0.70, 0.80, 0.90, 1.00]
174
175 # Clamp expansion ratio to valid interpolation range [3.0, 100.0]
176 eps = max(3.0, min(100.0, expansion_ratio))
177
178 # Find bounding bell percentages
179 if bell_pct in valid_percents:
180 # Exact match, use simple lookup
181 return lookup_angles(eps, bell_pct)
182
183 # Find bounding bell percentages
184 bell_low = None
185 bell_high = None
186 for i in range(len(valid_percents) - 1):
187 if valid_percents[i] <= bell_pct < valid_percents[i + 1]:
188 bell_low = valid_percents[i]
189 bell_high = valid_percents[i + 1]
190 break
191
192 if bell_low is None:
193 # At boundary
194 if bell_pct <= valid_percents[0]:
195 bell_low = bell_high = valid_percents[0]
196 else:
197 bell_low = bell_high = valid_percents[-1]
198
199 # Get angles at both bell percentages
200 theta_n_low, theta_e_low = lookup_angles(eps, bell_low)
201 theta_n_high, theta_e_high = lookup_angles(eps, bell_high)
202
203 # Interpolate in bell percentage
204 if bell_low == bell_high:
205 return theta_n_low, theta_e_low
206
207 t = (bell_pct - bell_low) / (bell_high - bell_low)
208 theta_n = theta_n_low + t * (theta_n_high - theta_n_low)
209 theta_e = theta_e_low + t * (theta_e_high - theta_e_low)
210
211 return theta_n, theta_e
212
213
214def plot_nozzle_angles(save_path=None, show_plot=True):
215 """
216 Plot the nozzle angle lookup table to match the original graph format.
217
218 Parameters:
219 -----------
220 save_path : str, optional
221 Path to save the plot (e.g., 'nozzle_angles_plot.png')
222 show_plot : bool
223 Whether to display the plot (default: True)
224
225 Returns:
226 --------
227 fig, ax : matplotlib figure and axes objects
228 """
229 # Define bell percentages and their colors
230 bell_percents = [0.60, 0.70, 0.80, 0.90, 1.00]
231 colors = {
232 0.60: '#FFD700', # Yellow/Gold
233 0.70: '#00FF00', # Green
234 0.80: '#87CEEB', # Light Blue/Sky Blue
235 0.90: '#000000', # Black
236 1.00: '#800080', # Purple
237 }
238
239 # Create expansion ratio array (log scale, dense for smooth curves)
240 # Interpolation range: 3.0 to 100.0
241 eps_min = 3.0
242 eps_max = 100.0
243 eps_plot = np.logspace(np.log10(eps_min), np.log10(eps_max), 200)
244
245 # Create figure
246 fig, ax = plt.subplots(figsize=(10, 6))
247
248 # Plot theta_n curves (upper group)
249 for bell_pct in bell_percents:
250 theta_n_values = [lookup_angles(eps, bell_pct)[0] for eps in eps_plot]
251 label = f'{int(bell_pct * 100)}%'
252 ax.semilogx(eps_plot, theta_n_values, color=colors[bell_pct],
253 linewidth=2, label=label)
254
255 # Plot theta_e curves (lower group)
256 for bell_pct in bell_percents:
257 theta_e_values = [lookup_angles(eps, bell_pct)[1] for eps in eps_plot]
258 label = f'{int(bell_pct * 100)}%'
259 ax.semilogx(eps_plot, theta_e_values, color=colors[bell_pct],
260 linewidth=2, label=label)
261
262 # Add labels and brackets for theta_n and theta_e groups
263 # Find approximate positions for labels
264 eps_label = 80.0
265 theta_n_mid = (lookup_angles(eps_label, 0.60)[0] + lookup_angles(eps_label, 1.00)[0]) / 2
266 theta_e_mid = (lookup_angles(eps_label, 0.60)[1] + lookup_angles(eps_label, 1.00)[1]) / 2
267
268 # Add vertical bracket and label for theta_n
269 ax.annotate('', xy=(eps_max * 1.15, theta_n_mid + 5),
270 xytext=(eps_max * 1.15, theta_n_mid - 5),
271 arrowprops=dict(arrowstyle='<->', lw=1.5, color='black'))
272 ax.text(eps_max * 1.2, theta_n_mid, r'$\theta_n$',
273 fontsize=14, va='center', ha='left')
274
275 # Add vertical bracket and label for theta_e
276 ax.annotate('', xy=(eps_max * 1.15, theta_e_mid + 3),
277 xytext=(eps_max * 1.15, theta_e_mid - 3),
278 arrowprops=dict(arrowstyle='<->', lw=1.5, color='black'))
279 ax.text(eps_max * 1.2, theta_e_mid, r'$\theta_e$',
280 fontsize=14, va='center', ha='left')
281
282 # Formatting
283 ax.set_xlabel('Nozzle expansion ratio', fontsize=12)
284 ax.set_ylabel('Parabola angles, degs', fontsize=12)
285 ax.set_xlim(eps_min, eps_max * 1.3) # Interpolation range: 3.0 to 100.0
286 ax.set_ylim(0, 45)
287 ax.grid(True, alpha=0.3, which='both')
288 ax.set_title('Parabola angles vs Nozzle expansion ratio', fontsize=14, fontweight='bold')
289
290 # Add legend (optional, but might be cluttered)
291 # ax.legend(title='Bell %', loc='upper left', ncol=5, fontsize=8)
292
293 # Add percentage labels near the curves (at expansion ratio ~50)
294 eps_label_pos = 50.0
295 for bell_pct in bell_percents:
296 theta_n_val = lookup_angles(eps_label_pos, bell_pct)[0]
297 theta_e_val = lookup_angles(eps_label_pos, bell_pct)[1]
298 label_text = f'{int(bell_pct * 100)}%'
299 ax.text(eps_label_pos, theta_n_val, label_text,
300 fontsize=8, ha='left', va='center', color=colors[bell_pct])
301 ax.text(eps_label_pos, theta_e_val, label_text,
302 fontsize=8, ha='left', va='center', color=colors[bell_pct])
303
304 plt.tight_layout()
305
306 if save_path:
307 plt.savefig(save_path, dpi=150, bbox_inches='tight')
308 print(f"Plot saved to {save_path}")
309
310 if show_plot:
311 plt.show()
312 else:
313 plt.close()
314
315 return fig, ax
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
lookup_angles(expansion_ratio, bell_percent=0.8)
plot_nozzle_angles(save_path=None, show_plot=True)
lookup_angles_interp_bell(area_throat, area_exit, bell_percent=0.8)