STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
chamber_geometry.py
Go to the documentation of this file.
1import numpy as np
2import matplotlib.pyplot as plt
3import sys
4from pathlib import Path
5
6try:
7 import ezdxf
8 HAS_EZDXF = True
9except ImportError:
10 HAS_EZDXF = False
11
12from engine.core.nozzle_solver import rao
13
14theta_default = np.pi/4
15force_coeffcient_default = 1.4
16diameter_exit_default = 4 / 39.37
17l_star_default = 1.27
18chamber_diameter_default = 3.4 / 39.37
19
20def area_exit_calc(diameter_exit=diameter_exit_default):
21 """
22 Calculate the area of the exit of the chamber.
23 Parameters:
24 - diameter_exit: The diameter of the exit of the chamber.
25 Calculate the area of the exit of the chamber.
26 """
27 return np.pi * (diameter_exit / 2) ** 2
28
29def expansion_ratio_calc(area_exit, area_throat):
30 """
31 Calculate the expansion ratio of the chamber.
32 Parameters:
33 - area_exit: The area of the exit of the chamber.
34 - area_throat: The area of the throat of the chamber.
35 Calculate the expansion ratio of the chamber.
36 """
37 return area_exit / area_throat
38
39def area_throat_calc(pc_design, thrust_design, force_coeffcient=force_coeffcient_default):
40 """
41 Calculate the area of the throat of the chamber.
42 Parameters:
43 - pc_design: The design chamber pressure.
44 - thrust_design: The design thrust.
45 - force_coeffcient: The force coeffcient.
46 Calculate the area of the throat of the chamber.
47 """
48 return thrust_design / (pc_design * force_coeffcient)
49
50def chamber_volume_calc(area_throat, l_star = l_star_default):
51 """
52 Parameters:
53 - l_star: The characteristic length of the chamber. (Get from config)
54 - area_throat: The area of the throat of the chamber.
55 Calculate the volume of the chamber.
56 """
57
58
59 return l_star * area_throat
60
61
62
63
64def contraction_ratio_calc(area_chamber, area_throat):
65 """
66 Calculate the contraction ratio of the chamber.
67 Parameters:
68 - area_chamber: The area of the chamber.
69 - area_throat: The area of the throat of the chamber.
70 Calculate the contraction ratio of the chamber.
71 """
72 return area_chamber / area_throat
73
74def area_chamber_calc(diameter_inner=chamber_diameter_default):
75 """
76 Calculate the area of the chamber.
77 Parameters:
78 - diameter_inner: The inner diameter of the chamber. = 3.4"
79 Calculate the area of the chamber.
80 """
81 return np.pi * (diameter_inner / 2) ** 2
82
83def chamber_diameter_calc(area_chamber):
84 """
85 Calculate the diameter of the chamber.
86 Parameters:
87 - area_chamber: The area of the chamber.
88 Calculate the diameter of the chamber.
89 """
90 return np.sqrt(4 * area_chamber / np.pi)
91
92def entrance_arc_volume(R_t, theta=theta_default, steps=4000):
93 """Solid-of-revolution volume of the 1.5*R_t nozzle entrance arc, face side of the throat.
94
95 The arc is centred at (0, 2.5*R_t) with radius 1.5*R_t and is swept from
96 t = -(pi/2 + theta) (tangent to the convergent cone) to t = -pi/2 (the throat), so
97
98 x(t) = 1.5 R_t cos t, y(t) = 1.5 R_t sin t + 2.5 R_t
99
100 and V = integral of pi*y^2 dx over that sweep. Integrated numerically because the closed
101 form is easy to get subtly wrong and this is called once per candidate, not per step.
102 """
103 t0 = -(np.pi / 2.0 + float(theta))
104 t = np.linspace(t0, -np.pi / 2.0, int(steps))
105 x = 1.5 * R_t * np.cos(t)
106 y = 1.5 * R_t * np.sin(t) + 2.5 * R_t
107 trapz = getattr(np, "trapezoid", None) or np.trapz
108 return float(trapz(np.pi * y * y, x))
109
110
111def chamber_length_calc(chamber_volume, area_throat, contraction_ratio, theta = theta_default):
112 """
113 Calculate the length of the chamber.
114 Parameters:
115 - chamber_volume: The volume of the chamber.
116 - area_throat: The area of the throat of the chamber.
117 - contraction_ratio: The contraction ratio of the chamber.
118 - theta: The angle of the chamber. = 45 degrees from -135deg nozzle entrance
119 Calculate the length of the chamber.
120 """
121 # Exact conical-frustum volume between A_c and A_t at half-angle `theta`:
122 # h = (R_c - R_t) * cot(theta)
123 # V_cone = (pi h / 3)(R_c^2 + R_c R_t + R_t^2)
124 # and with R_c = R_t * sqrt(eps) this closes to
125 # V_cone = (A_t R_t cot(theta) / 3) * (eps^(3/2) - 1).
126 #
127 # This previously used (eps^(1/3) - 1), which at a typical eps = 6.76 credits the
128 # convergent section with 13.6 cm^3 instead of 252.9 cm^3 -- 18.6x low. The generator
129 # then made the missing volume up in barrel length, so a commanded L* = 1.30 was drawn
130 # as a chamber whose integrated volume is L* = 1.43: 18.9 mm and 0.64 kg of barrel that
131 # the design never asked for. Verified against direct numerical integration of the
132 # generated contour (tests/test_chamber_geometry_volume.py).
133 # SECOND correction (2026-09-14): the cone does not reach R_t. It runs to the tangency
134 # radius of the 1.5*R_t entrance arc, r_tan = R_t*(1 + 1.5*(1 - cos(theta))), and the arc
135 # carries the rest. Crediting a straight frustum all the way to R_t under-counts the
136 # convergent volume by the arc's bulge, and the generator again made it up in barrel
137 # length -- 0.9-2.1% of L* depending on theta. Now: exact frustum R_c -> r_tan, plus the
138 # arc's own solid of revolution.
139 R_t = np.sqrt(area_throat / np.pi)
140 A_c = area_throat * contraction_ratio
141 R_c = np.sqrt(A_c / np.pi)
142 cot_theta = np.tan(np.pi / 2 - theta)
143
144 r_tan = R_t * (1.0 + 1.5 * (1.0 - np.cos(theta)))
145 if r_tan >= R_c: # arc alone spans the whole contraction
146 V_cone = 0.0
147 else:
148 h_cone = (R_c - r_tan) * cot_theta
149 V_cone = (np.pi * h_cone / 3.0) * (R_c ** 2 + R_c * r_tan + r_tan ** 2)
150
151 V_cone += entrance_arc_volume(R_t, theta)
152 return (chamber_volume - V_cone) / A_c
153
154
155def contraction_length_horizontal_calc(area_chamber, entrance_arc_start_y, theta=theta_default):
156
157 R_start = np.sqrt(area_chamber / np.pi)
158 R_end = entrance_arc_start_y
159 L_cone = (R_start - R_end) * np.tan(np.pi/2 - theta)
160 return L_cone
161
162
163
164def generate_nozzle(area_throat, area_exit, steps=200, theta=theta_default):
165 return rao(area_throat, area_exit, method="top", do_plot=False, steps=steps,
166 convergent_half_angle_rad=theta)
167
168
170 thrust_design, force_coeffcient=force_coeffcient_default,
171 diameter_inner=chamber_diameter_default,
172 diameter_exit=diameter_exit_default,
173 l_star = l_star_default,
174 do_plot=False,
175 color_segments=False,
176 steps=200,
177 export_dxf=None,
178 theta=theta_default):
179 """
180 Calculate the full chamber geometry including cylindrical section, contraction, and nozzle.
181
182 Parameters:
183 -----------
184 export_dxf : str or None
185 If provided, exports the contour to a DXF file at the specified path.
186 Example: 'chamber/chamber_contour.dxf'
187
188 Returns:
189 --------
190 pts : numpy array
191 Array of (x, y) points representing the full chamber contour
192 data : list of lists
193 Table data with metric and imperial units. First row contains headers:
194 ['Parameter', 'Metric Value', 'Metric Units', 'Imperial Value', 'Imperial Units']
195 Subsequent rows contain parameter data.
196 """
197 area_throat = area_throat_calc(pc_design, thrust_design, force_coeffcient)
198 area_exit = area_exit_calc(diameter_exit)
199 volume_chamber = chamber_volume_calc(area_throat, l_star)
200 area_chamber = area_chamber_calc(diameter_inner)
201 contraction_ratio = contraction_ratio_calc(area_chamber, area_throat)
202 nozzle_pts, nozzle_x_first, nozzle_y_first = generate_nozzle(area_throat, area_exit, steps=steps, theta=theta)
203 cylindrical_length = chamber_length_calc(volume_chamber, area_throat, contraction_ratio, theta)
204 contraction_length_horizontal = contraction_length_horizontal_calc(area_chamber, nozzle_y_first, theta)
205
206 # Barrel + cone. The 1.5*R_t entrance arc adds 1.5*R_t*sin(theta) before the throat:
207 # face to throat is the Huzel & Huang chamber length, reported separately.
208 total_chamber_length = cylindrical_length + contraction_length_horizontal
209 entrance_arc_length = 1.5 * np.sqrt(area_throat / np.pi) * np.sin(theta)
210 face_to_throat_length = total_chamber_length + entrance_arc_length
211
212 # Calculate chamber radius
213 r_c = np.sqrt(area_chamber / np.pi)
214
215 # Calculate where cylindrical section starts
216 # The 45° contraction line connects (x_cyl_start, r_c) to (nozzle_x_first, nozzle_y_first)
217 # For 45° line: y = r_c - (x - x_cyl_start) = r_c - x + x_cyl_start
218 # At connection: nozzle_y_first = r_c - nozzle_x_first + x_cyl_start
219 # General half-angle: the convergent runs from (x_cyl_start, r_c) down to
220 # (nozzle_x_first, nozzle_y_first) over a horizontal run of (r_c - y_first)*cot(theta).
221 # At theta = 45 deg this reduces to the old x_first + y_first - r_c.
222 _cot = np.tan(np.pi/2 - theta)
223 contraction_run = (r_c - nozzle_y_first) * _cot
224 x_cyl_start = nozzle_x_first - contraction_run
225
226 # Generate cylindrical section (constant radius)
227 x_cyl_end = x_cyl_start - cylindrical_length
228 x_cyl = np.linspace(x_cyl_end, x_cyl_start, steps)
229 y_cyl = np.full_like(x_cyl, r_c)
230
231 # Generate contraction section (45° line)
232 x_contraction = np.linspace(x_cyl_start, nozzle_x_first, steps)
233 # Straight convergent at half-angle theta: y = r_c - (x - x_cyl_start)*tan(theta)
234 y_contraction = r_c - (x_contraction - x_cyl_start) * np.tan(theta)
235
236 # Combine all sections: cylindrical -> contraction -> nozzle
237 # Note: nozzle_pts already includes all nozzle segments
238 chamber_pts = np.vstack([
239 np.column_stack((x_cyl, y_cyl)),
240 np.column_stack((x_contraction[1:-1], y_contraction[1:-1])), # both ends are shared vertices
241 nozzle_pts # Nozzle already starts at the connection point
242 ])
243
244 # Calculate additional diameters for table data
245 diameter_chamber = chamber_diameter_calc(area_chamber)
246 diameter_throat = np.sqrt(4 * area_throat / np.pi)
247 diameter_exit_calc = np.sqrt(4 * area_exit / np.pi)
248 expansion_ratio = expansion_ratio_calc(area_exit, area_throat)
249
250 # Conversion factors
251 m_to_in = 39.37
252 m2_to_in2 = 1550.0031
253 m3_to_in3 = 61023.7441
254 pa_to_psi = 0.000145038 # 1 Pa = 0.000145038 PSI
255 n_to_lbf = 0.224809 # 1 N = 0.224809 lbf
256
257 # Convert to imperial
258 volume_chamber_in3 = volume_chamber * m3_to_in3
259 area_chamber_in2 = area_chamber * m2_to_in2
260 diameter_chamber_in = diameter_chamber * m_to_in
261 area_throat_in2 = area_throat * m2_to_in2
262 diameter_throat_in = diameter_throat * m_to_in
263 area_exit_in2 = area_exit * m2_to_in2
264 diameter_exit_in = diameter_exit_calc * m_to_in
265 l_star_in = l_star * m_to_in
266 cylindrical_length_in = cylindrical_length * m_to_in
267 contraction_length_horizontal_in = contraction_length_horizontal * m_to_in
268 total_chamber_length_in = total_chamber_length * m_to_in
269 pc_design_psi = pc_design * pa_to_psi
270 thrust_design_lbf = thrust_design * n_to_lbf
271
272 # Create table data with metric and imperial units
273 table_data = [
274 ['Parameter', 'Metric Value', 'Metric Units', 'Imperial Value', 'Imperial Units'],
275 ['Design Pressure', f'{pc_design:.2e}', 'Pa', f'{pc_design_psi:.2f}', 'PSI'],
276 ['Design Thrust', f'{thrust_design:.2f}', 'N', f'{thrust_design_lbf:.2f}', 'lbf'],
277 ['Chamber Volume', f'{volume_chamber:.6e}', 'm³', f'{volume_chamber_in3:.4f}', 'in³'],
278 ['Chamber Area', f'{area_chamber:.6e}', 'm²', f'{area_chamber_in2:.4f}', 'in²'],
279 ['Chamber Diameter', f'{diameter_chamber:.6e}', 'm', f'{diameter_chamber_in:.4f}', 'in'],
280 ['Throat Area', f'{area_throat:.6e}', 'm²', f'{area_throat_in2:.6f}', 'in²'],
281 ['Throat Diameter', f'{diameter_throat:.6e}', 'm', f'{diameter_throat_in:.6f}', 'in'],
282 ['Exit Area', f'{area_exit:.6e}', 'm²', f'{area_exit_in2:.4f}', 'in²'],
283 ['Exit Diameter', f'{diameter_exit_calc:.6e}', 'm', f'{diameter_exit_in:.4f}', 'in'],
284 ['Expansion Ratio', f'{expansion_ratio:.4f}', '', f'{expansion_ratio:.4f}', ''],
285 ['Contraction Ratio', f'{contraction_ratio:.4f}', '', f'{contraction_ratio:.4f}', ''],
286 ['L*', f'{l_star:.4f}', 'm', f'{l_star_in:.4f}', 'in'],
287 ['Cylindrical Length', f'{cylindrical_length:.6e}', 'm', f'{cylindrical_length_in:.4f}', 'in'],
288 ['Contraction Length', f'{contraction_length_horizontal:.6e}', 'm', f'{contraction_length_horizontal_in:.4f}', 'in'],
289 ['Total Chamber Length', f'{total_chamber_length:.6e}', 'm', f'{total_chamber_length_in:.4f}', 'in'],
290 ['Face to Throat', f'{face_to_throat_length:.6e}', 'm', f'{face_to_throat_length * m_to_in:.4f}', 'in'],
291 ]
292
293 # Plot if requested
294 if do_plot:
295 # Create figure with two subplots: contour on top, table below
296 fig = plt.figure(figsize=(16, 14))
297 gs = fig.add_gridspec(2, 1, height_ratios=[2, 1], hspace=0.15)
298
299 # Top subplot for contour
300 ax = fig.add_subplot(gs[0])
301
302 if color_segments:
303 ax.plot(x_cyl, y_cyl, label='Cylindrical section', color='blue', linewidth=2)
304 ax.plot(x_contraction, y_contraction, label='Contraction (45°)', color='green', linewidth=2)
305 ax.plot(nozzle_pts[:, 0], nozzle_pts[:, 1], label='Nozzle', color='red', linewidth=2)
306 ax.legend()
307 else:
308 ax.plot(chamber_pts[:, 0], chamber_pts[:, 1], 'k-', linewidth=2)
309
310 # Stretch the plot vertically to show more detail in radius direction
311 # Get the data range to calculate aspect ratio (after plotting)
312 ax.relim()
313 ax.autoscale()
314 x_min, x_max = ax.get_xlim()
315 y_min, y_max = ax.get_ylim()
316 x_range = x_max - x_min
317 y_range = y_max - y_min
318
319 # Set aspect ratio to stretch y-direction (make it taller)
320 # A smaller aspect value stretches the y-axis
321 aspect_ratio = (x_range / y_range) * 0.2 # 0.3 factor stretches y-direction
322 ax.set_aspect(aspect_ratio, 'box')
323
324 # Add small padding to y-axis
325 ax.set_ylim(y_min - 0.05 * y_range, y_max + 0.05 * y_range)
326
327 ax.set_xlabel("Axial distance x (m)", fontsize=12)
328 ax.set_ylabel("Radius y (m)", fontsize=12)
329 ax.grid(True, alpha=0.3)
330 ax.set_title("Full Chamber Contour", fontsize=14, fontweight='bold')
331
332 # Bottom subplot for table
333 ax_table = fig.add_subplot(gs[1])
334 ax_table.axis('off')
335
336 # Create table
337 table = ax_table.table(cellText=table_data[1:], colLabels=table_data[0],
338 cellLoc='left', loc='center',
339 bbox=[0, 0, 1, 1])
340 table.auto_set_font_size(False)
341 table.set_fontsize(18)
342 table.scale(1.5, 4.5) # Much larger scale for better spacing
343
344 # Set column widths for better readability
345 col_widths = [0.22, 0.22, 0.12, 0.22, 0.12] # Wider columns
346 for i in range(5):
347 for j in range(len(table_data)):
348 table[(j, i)].set_width(col_widths[i])
349
350 # Style the header row
351 for i in range(5):
352 table[(0, i)].set_facecolor('#40466e')
353 table[(0, i)].set_text_props(weight='bold', color='white', size=20)
354 table[(0, i)].set_height(0.35) # Much taller header row
355
356 # Alternate row colors and style cells
357 for i in range(1, len(table_data)):
358 for j in range(5):
359 if i % 2 == 0:
360 table[(i, j)].set_facecolor('#f1f1f2')
361 else:
362 table[(i, j)].set_facecolor('white')
363 table[(i, j)].set_text_props(size=18)
364 table[(i, j)].set_height(0.30) # Much taller data rows
365
366 # Ensure directory exists before saving
367 output_path = Path('chamber/chamber_full_contour.png')
368 output_path.parent.mkdir(parents=True, exist_ok=True)
369 plt.savefig(str(output_path), dpi=150, bbox_inches='tight')
370 plt.close()
371
372 # Export to DXF if requested
373 if export_dxf is not None:
374 from engine.core.dxf_export import export_chamber_dxf
375 export_chamber_dxf(chamber_pts, export_dxf)
376
377 # Also return the lengths as a separate dictionary for easy access
378 lengths = {
379 'cylindrical': cylindrical_length,
380 'contraction': contraction_length_horizontal,
381 'total': total_chamber_length, # barrel + cone
382 'entrance_arc': entrance_arc_length,
383 'face_to_throat': face_to_throat_length,
384 }
385
386 return chamber_pts, table_data, lengths
387
388# Only run example if script is executed directly (not when imported)
389if __name__ == "__main__":
390 pts, data, lengths = chamber_geometry_calc(pc_design=2.068e6, thrust_design=6000, do_plot=True, color_segments=True, export_dxf='chamber/chamber_contour.dxf')
391
generate_nozzle(area_throat, area_exit, steps=200, theta=theta_default)
chamber_geometry_calc(pc_design, thrust_design, force_coeffcient=force_coeffcient_default, diameter_inner=chamber_diameter_default, diameter_exit=diameter_exit_default, l_star=l_star_default, do_plot=False, color_segments=False, steps=200, export_dxf=None, theta=theta_default)
area_exit_calc(diameter_exit=diameter_exit_default)
contraction_length_horizontal_calc(area_chamber, entrance_arc_start_y, theta=theta_default)
chamber_length_calc(chamber_volume, area_throat, contraction_ratio, theta=theta_default)
expansion_ratio_calc(area_exit, area_throat)
area_throat_calc(pc_design, thrust_design, force_coeffcient=force_coeffcient_default)
area_chamber_calc(diameter_inner=chamber_diameter_default)
entrance_arc_volume(R_t, theta=theta_default, steps=4000)
chamber_volume_calc(area_throat, l_star=l_star_default)
contraction_ratio_calc(area_chamber, area_throat)