STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
chamber_geometry_visualizer.py
Go to the documentation of this file.
1"""Clear chamber geometry visualization showing the actual structure.
2
3Structure:
4- Gas boundary (inner surface where hot gas flows)
5- Ablative liner (chamber region, NOT throat)
6- Graphite insert (ONLY at throat region, small axial length)
7- Stainless steel case (outer structure)
8- Nozzle (diverging section after throat)
9"""
10
11from __future__ import annotations
12
13from typing import Dict, Optional, Tuple
14import numpy as np
15import plotly.graph_objects as go
16from engine.pipeline.config_schemas import PintleEngineConfig, AblativeCoolingConfig, GraphiteInsertConfig
17from engine.pipeline.chamber_geometry_fixed import calculate_chamber_geometry_fixed
18
19
21 L_chamber: float,
22 D_chamber: float,
23 D_throat: float,
24 L_nozzle: float = 0.0,
25 expansion_ratio: float = 10.0,
26 ablative_config: Optional[AblativeCoolingConfig] = None,
27 graphite_config: Optional[GraphiteInsertConfig] = None,
28 recession_chamber: float = 0.0,
29 recession_graphite: float = 0.0,
30 n_points: int = 200,
31) -> Dict[str, np.ndarray]:
32 """
33 Calculate chamber geometry - delegates to fixed version.
34 """
35 return calculate_chamber_geometry_fixed(
36 L_chamber=L_chamber,
37 D_chamber=D_chamber,
38 D_throat=D_throat,
39 L_nozzle=L_nozzle,
40 expansion_ratio=expansion_ratio,
41 ablative_config=ablative_config,
42 graphite_config=graphite_config,
43 recession_chamber=recession_chamber,
44 recession_graphite=recession_graphite,
45 n_points=n_points,
46 )
47
48
50 L_chamber: float,
51 D_chamber: float,
52 D_throat: float,
53 L_nozzle: float = 0.0,
54 expansion_ratio: float = 10.0,
55 ablative_config: Optional[AblativeCoolingConfig] = None,
56 graphite_config: Optional[GraphiteInsertConfig] = None,
57 recession_chamber: float = 0.0,
58 recession_graphite: float = 0.0,
59 n_points: int = 200,
60) -> Dict[str, np.ndarray]:
61 """
62 Calculate clear chamber geometry showing actual structure.
63
64 Structure:
65 - 0 to L_chamber: Chamber (ablative liner)
66 - L_chamber: Throat (graphite insert, small axial extent)
67 - L_chamber to L_chamber+L_nozzle: Nozzle (diverging, can be ablative or other)
68
69 Parameters:
70 -----------
71 L_chamber : float
72 Chamber length [m] (from injector to throat)
73 D_chamber : float
74 Chamber inner diameter [m]
75 D_throat : float
76 Throat diameter [m]
77 L_nozzle : float
78 Nozzle length [m] (diverging section after throat)
79 expansion_ratio : float
80 Nozzle expansion ratio (A_exit / A_throat)
81 ablative_config : AblativeCoolingConfig, optional
82 Ablative configuration
83 graphite_config : GraphiteInsertConfig, optional
84 Graphite configuration
85 recession_chamber : float
86 Cumulative ablative recession [m]
87 recession_graphite : float
88 Cumulative graphite recession [m]
89 n_points : int
90 Number of points for geometry
91
92 Returns:
93 --------
94 geometry : dict
95 - positions: Axial positions [m]
96 - R_gas: Gas boundary radius [m]
97 - R_ablative_outer: Ablative outer radius [m] (chamber region only)
98 - R_graphite_outer: Graphite outer radius [m] (throat region only)
99 - R_stainless: Stainless steel outer radius [m]
100 - R_nozzle_gas: Nozzle gas boundary radius [m]
101 - throat_position: Throat axial position [m]
102 - graphite_start: Graphite insert start position [m]
103 - graphite_end: Graphite insert end position [m]
104 """
105 # Calculate nozzle exit diameter
106 A_throat = np.pi * (D_throat / 2.0) ** 2
107 A_exit = A_throat * expansion_ratio
108 D_exit = np.sqrt(4.0 * A_exit / np.pi)
109
110 # Total length
111 L_total = L_chamber + L_nozzle
112
113 # Create positions
114 positions = np.linspace(0.0, L_total, n_points)
115
116 # Throat position
117 throat_pos = L_chamber
118
119 # Gas boundary (chamber + nozzle)
120 R_gas = np.zeros_like(positions)
121 for i, x in enumerate(positions):
122 if x <= throat_pos:
123 # Chamber: constant diameter
124 R_gas[i] = (D_chamber / 2.0) + recession_chamber
125 else:
126 # Nozzle: diverging (linear for simplicity)
127 x_nozzle = x - throat_pos
128 R_throat = D_throat / 2.0
129 R_exit = D_exit / 2.0
130 if L_nozzle > 0:
131 R_gas[i] = R_throat + (R_exit - R_throat) * (x_nozzle / L_nozzle)
132 else:
133 R_gas[i] = R_throat
134
135 # Ablative liner (chamber region only, NOT throat or nozzle)
136 # Variable thickness based on local heat flux and recession
137 if ablative_config and ablative_config.enabled:
138 # Base thickness after recession
139 base_thickness = max(
140 ablative_config.initial_thickness - recession_chamber,
141 0.0
142 )
143
144 # Variable thickness: can vary along chamber based on heat flux
145 # For now, use constant thickness (can be made variable with heat flux profile)
146 ablative_thickness_profile = np.full(n_points, base_thickness)
147
148 R_ablative_outer = np.zeros_like(positions)
149 for i, x in enumerate(positions):
150 if x < throat_pos: # Chamber region only
151 # Ablative outer = gas boundary + variable thickness
152 R_ablative_outer[i] = R_gas[i] + ablative_thickness_profile[i]
153 else:
154 # No ablative in throat/nozzle region
155 R_ablative_outer[i] = R_gas[i]
156 else:
157 R_ablative_outer = R_gas.copy()
158
159 # Graphite insert (throat region only, variable thickness conforming to chamber)
160 if graphite_config and graphite_config.enabled:
161 # Get graphite axial length
162 graphite_axial_half_length = getattr(graphite_config, 'axial_half_length', None)
163 if graphite_axial_half_length is None or graphite_axial_half_length <= 0:
164 # Ratio is configurable (graphite_insert.axial_half_length_ratio, default 0.75);
165 # 0.75 was hardcoded here. An explicit axial_half_length overrides it upstream.
166 _gr = locals().get('graphite_config') or locals().get('config')
167 _ratio = float(getattr(getattr(_gr, 'graphite_insert', _gr), 'axial_half_length_ratio', 0.75) or 0.75)
168 graphite_axial_half_length = _ratio * D_throat
169
170 graphite_start = max(throat_pos - graphite_axial_half_length, 0.0)
171 graphite_end = min(throat_pos + graphite_axial_half_length, L_total)
172
173 # Calculate variable thickness graphite conforming to chamber geometry
174 # Import variable thickness module
175 try:
176 from engine.pipeline.thermal.graphite_variable_thickness import calculate_graphite_geometry_conforming
177
178 # Create heat flux profile (simplified - could be from actual analysis)
179 # Higher heat flux near throat
180 heat_flux_profile = np.zeros_like(positions)
181 for i, x in enumerate(positions):
182 if graphite_start <= x <= graphite_end:
183 # Heat flux peaks at throat
184 distance_from_throat = abs(x - throat_pos)
185 heat_flux_profile[i] = 1e6 * np.exp(-distance_from_throat / (0.1 * D_throat))
186 else:
187 heat_flux_profile[i] = 0.0
188
189 # Calculate conforming graphite geometry
190 graphite_geometry = calculate_graphite_geometry_conforming(
191 L_chamber=L_chamber,
192 D_chamber=D_chamber,
193 D_throat=D_throat,
194 positions_chamber=positions,
195 R_gas_chamber=R_gas,
196 heat_flux_chamber=heat_flux_profile,
197 graphite_config=graphite_config,
198 graphite_start=graphite_start,
199 graphite_end=graphite_end,
200 recession_graphite=recession_graphite,
201 )
202
203 # Use variable thickness profile
204 R_graphite_outer = np.zeros_like(positions)
205 for i, x in enumerate(positions):
206 if graphite_start <= x <= graphite_end:
207 # Find corresponding point in graphite geometry
208 if len(graphite_geometry["positions"]) > 0:
209 idx = np.argmin(np.abs(graphite_geometry["positions"] - x))
210 if idx < len(graphite_geometry["R_outer"]):
211 R_graphite_outer[i] = graphite_geometry["R_outer"][idx]
212 else:
213 # Fallback: use gas radius + base thickness
214 R_graphite_outer[i] = R_gas[i] + max(graphite_config.initial_thickness - recession_graphite, 0.0)
215 else:
216 R_graphite_outer[i] = R_gas[i] + max(graphite_config.initial_thickness - recession_graphite, 0.0)
217 else:
218 # No graphite outside throat region
219 R_graphite_outer[i] = R_gas[i]
220 except ImportError:
221 # Fallback to uniform thickness
222 graphite_thickness = max(graphite_config.initial_thickness - recession_graphite, 0.0)
223 R_graphite_outer = np.zeros_like(positions)
224 for i, x in enumerate(positions):
225 if graphite_start <= x <= graphite_end:
226 # Graphite region: conforms to gas boundary + thickness
227 R_graphite_outer[i] = R_gas[i] + graphite_thickness
228 else:
229 R_graphite_outer[i] = R_gas[i]
230 else:
231 R_graphite_outer = R_gas.copy()
232 graphite_start = throat_pos
233 graphite_end = throat_pos
234
235 # Stainless steel case (CONSTANT OUTER DIAMETER)
236 # The outer diameter must be constant - stainless does NOT follow internal contour
237 # Find the maximum inner radius across ALL positions
238 R_inner_max = np.maximum(R_ablative_outer, R_graphite_outer)
239 R_inner_max_global = np.max(R_inner_max) # Maximum inner radius anywhere
240
241 # Stainless steel thickness
242 stainless_thickness = 0.002 # Default 2mm (constant)
243
244 # Constant outer radius = max inner radius + thickness
245 # This creates a truly constant outer diameter
246 R_stainless_constant = R_inner_max_global + stainless_thickness
247
248 # Stainless outer radius is constant everywhere
249 R_stainless = np.full_like(positions, R_stainless_constant)
250
251 return {
252 "positions": positions,
253 "R_gas": R_gas,
254 "R_ablative_outer": R_ablative_outer,
255 "R_graphite_outer": R_graphite_outer,
256 "R_stainless": R_stainless,
257 "throat_position": throat_pos,
258 "graphite_start": graphite_start,
259 "graphite_end": graphite_end,
260 "D_chamber": D_chamber,
261 "D_throat": D_throat,
262 "D_exit": D_exit,
263 }
264
265
266def plot_chamber_geometry_clear(geometry: Dict[str, np.ndarray], config: Optional[PintleEngineConfig] = None) -> go.Figure:
267 """
268 Create clear plotly figure showing chamber geometry structure.
269
270 Shows:
271 - Gas boundary (orange)
272 - Ablative liner (brown, chamber region only)
273 - Graphite insert (black, throat region only)
274 - Stainless steel (gray, outer)
275 - Throat marker (red)
276 """
277 positions = geometry["positions"]
278 R_gas = geometry["R_gas"]
279 R_ablative = geometry["R_ablative_outer"]
280 R_graphite = geometry["R_graphite_outer"]
281 R_stainless = geometry["R_stainless"]
282 throat_pos = geometry["throat_position"]
283 graphite_start = geometry["graphite_start"]
284 graphite_end = geometry["graphite_end"]
285
286 fig = go.Figure()
287
288 # Gas boundary (top)
289 fig.add_trace(go.Scatter(
290 x=positions,
291 y=R_gas,
292 mode='lines',
293 name='Gas Boundary',
294 line=dict(color='orange', width=3),
295 fill='tozeroy',
296 fillcolor='rgba(255, 165, 0, 0.1)',
297 ))
298
299 # Gas boundary (bottom)
300 fig.add_trace(go.Scatter(
301 x=positions,
302 y=-R_gas,
303 mode='lines',
304 name='Gas Boundary (Lower)',
305 line=dict(color='orange', width=3),
306 fill='tozeroy',
307 fillcolor='rgba(255, 165, 0, 0.1)',
308 showlegend=False,
309 ))
310
311 # Ablative liner (chamber region only)
312 # Find chamber region (before throat)
313 chamber_mask = positions < throat_pos
314 if np.any(chamber_mask):
315 chamber_positions = positions[chamber_mask]
316 chamber_ablative = R_ablative[chamber_mask]
317
318 fig.add_trace(go.Scatter(
319 x=chamber_positions,
320 y=chamber_ablative,
321 mode='lines',
322 name='Ablative Liner (Chamber)',
323 line=dict(color='brown', width=2, dash='dash'),
324 fill='tonexty',
325 fillcolor='rgba(139, 69, 19, 0.3)',
326 ))
327
328 fig.add_trace(go.Scatter(
329 x=chamber_positions,
330 y=-chamber_ablative,
331 mode='lines',
332 name='Ablative (Lower)',
333 line=dict(color='brown', width=2, dash='dash'),
334 fill='tonexty',
335 fillcolor='rgba(139, 69, 19, 0.3)',
336 showlegend=False,
337 ))
338
339 # Graphite insert (throat region only)
340 graphite_mask = (positions >= graphite_start) & (positions <= graphite_end)
341 if np.any(graphite_mask):
342 graphite_positions = positions[graphite_mask]
343 graphite_outer = R_graphite[graphite_mask]
344
345 fig.add_trace(go.Scatter(
346 x=graphite_positions,
347 y=graphite_outer,
348 mode='lines',
349 name='Graphite Insert (Throat)',
350 line=dict(color='black', width=3),
351 fill='tonexty',
352 fillcolor='rgba(0, 0, 0, 0.4)',
353 ))
354
355 fig.add_trace(go.Scatter(
356 x=graphite_positions,
357 y=-graphite_outer,
358 mode='lines',
359 name='Graphite (Lower)',
360 line=dict(color='black', width=3),
361 fill='tonexty',
362 fillcolor='rgba(0, 0, 0, 0.4)',
363 showlegend=False,
364 ))
365
366 # Stainless steel (outer) - constant outer diameter
367 # Find what's underneath (ablative or graphite)
368 R_under_stainless = np.maximum(R_ablative, R_graphite)
369
370 # Stainless steel fills from inner to outer (constant outer)
371 fig.add_trace(go.Scatter(
372 x=positions,
373 y=R_stainless,
374 mode='lines',
375 name='Stainless Steel Case',
376 line=dict(color='gray', width=2, dash='dot'),
377 fill='tonexty',
378 fillcolor='rgba(128, 128, 128, 0.2)',
379 ))
380
381 fig.add_trace(go.Scatter(
382 x=positions,
383 y=-R_stainless,
384 mode='lines',
385 name='Stainless (Lower)',
386 line=dict(color='gray', width=2, dash='dot'),
387 fill='tonexty',
388 fillcolor='rgba(128, 128, 128, 0.2)',
389 showlegend=False,
390 ))
391
392 # Throat marker
393 R_throat = geometry["D_throat"] / 2.0
394 fig.add_trace(go.Scatter(
395 x=[throat_pos],
396 y=[R_throat],
397 mode='markers',
398 name='Throat',
399 marker=dict(size=15, color='red', symbol='circle', line=dict(width=3, color='darkred')),
400 ))
401
402 fig.add_trace(go.Scatter(
403 x=[throat_pos],
404 y=[-R_throat],
405 mode='markers',
406 marker=dict(size=15, color='red', symbol='circle', line=dict(width=3, color='darkred')),
407 showlegend=False,
408 ))
409
410 # Centerline
411 fig.add_trace(go.Scatter(
412 x=[positions[0], positions[-1]],
413 y=[0, 0],
414 mode='lines',
415 name='Centerline',
416 line=dict(color='gray', width=1, dash='dash'),
417 showlegend=False,
418 ))
419
420 # Calculate data ranges for proper 1:1 aspect ratio
421 x_min, x_max = positions.min(), positions.max()
422 y_max = max(np.max(R_stainless), np.max(R_gas))
423 y_min = -y_max # Symmetric about centerline
424
425 x_range = x_max - x_min
426 y_range = y_max - y_min
427
428 # Add 5% padding
429 x_pad = 0.05 * x_range
430 y_pad = 0.05 * y_range
431
432 fig.update_layout(
433 title="Chamber Geometry: Gas → Ablative (Chamber) → Graphite (Throat) → Stainless Steel",
434 xaxis_title="Axial Position [m]",
435 yaxis_title="Radius [m]",
436 height=600,
437 showlegend=True,
438 # Force 1:1 aspect ratio with explicit ranges
439 xaxis=dict(
440 range=[x_min - x_pad, x_max + x_pad],
441 constrain="domain",
442 ),
443 yaxis=dict(
444 range=[y_min - y_pad, y_max + y_pad],
445 scaleanchor="x",
446 scaleratio=1,
447 constrain="domain",
448 ),
449 )
450
451 return fig
452
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
Dict[str, np.ndarray] calculate_chamber_geometry_clear_OLD(float L_chamber, float D_chamber, float D_throat, float L_nozzle=0.0, float expansion_ratio=10.0, Optional[AblativeCoolingConfig] ablative_config=None, Optional[GraphiteInsertConfig] graphite_config=None, float recession_chamber=0.0, float recession_graphite=0.0, int n_points=200)
go.Figure plot_chamber_geometry_clear(Dict[str, np.ndarray] geometry, Optional[PintleEngineConfig] config=None)
Dict[str, np.ndarray] calculate_chamber_geometry_clear(float L_chamber, float D_chamber, float D_throat, float L_nozzle=0.0, float expansion_ratio=10.0, Optional[AblativeCoolingConfig] ablative_config=None, Optional[GraphiteInsertConfig] graphite_config=None, float recession_chamber=0.0, float recession_graphite=0.0, int n_points=200)