STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
recession_animation_working.py
Go to the documentation of this file.
1"""Working animated visualization of ablative recession over time.
2
3This version properly handles all data structures and creates working animations.
4"""
5
6from __future__ import annotations
7
8from typing import Dict, List, Optional
9import numpy as np
10import plotly.graph_objects as go
11from plotly.subplots import make_subplots
12from engine.pipeline.config_schemas import PintleEngineConfig
13
14
16 time_history: np.ndarray,
17 geometry_history: List[Dict],
18 config: PintleEngineConfig,
19 impingement_data: Optional[Dict] = None,
20 max_frames: int = 30,
21) -> go.Figure:
22 """
23 Create animated plotly figure showing recession over time.
24
25 This version:
26 - Properly handles all data structures
27 - Creates working animations without crashes
28 - Shows all layers (gas, ablative, graphite, stainless)
29 """
30 if len(geometry_history) == 0:
31 raise ValueError("No geometry history provided")
32
33 n_steps = len(time_history)
34
35 # Limit frames to prevent crashes
36 if n_steps > max_frames:
37 step = max(1, n_steps // max_frames)
38 indices = np.arange(0, n_steps, step)
39 time_history = time_history[indices]
40 geometry_history = [geometry_history[i] for i in indices if i < len(geometry_history)]
41 n_steps = len(time_history)
42
43 if n_steps == 0 or len(geometry_history) == 0:
44 raise ValueError("No valid time steps or geometry data")
45
46 # Get positions from first valid geometry
47 positions = None
48 for geom in geometry_history:
49 pos = geom.get("positions", [])
50 if len(pos) > 0:
51 positions = np.array(pos)
52 break
53
54 if positions is None or len(positions) == 0:
55 raise ValueError("No positions found in geometry data")
56
57 # Create figure
58 fig = make_subplots(
59 rows=2, cols=1,
60 subplot_titles=("Chamber Cross-Section", "Cumulative Recession"),
61 vertical_spacing=0.15,
62 row_heights=[0.7, 0.3],
63 )
64
65 # Prepare data for all frames
66 all_data = []
67
68 for i, geometry in enumerate(geometry_history):
69 try:
70 # Get positions (use first geometry's positions as reference)
71 pos = np.array(geometry.get("positions", positions))
72 if len(pos) != len(positions):
73 # Interpolate to match reference positions
74 if len(pos) > 1:
75 pos = np.interp(positions, np.linspace(0, 1, len(pos)), pos)
76 else:
77 pos = positions
78
79 # Get diameters
80 D_gas_list = geometry.get("D_gas_chamber", [])
81 if isinstance(D_gas_list, (list, np.ndarray)):
82 D_gas = np.array(D_gas_list)
83 if len(D_gas) == 1:
84 D_gas = np.full_like(positions, float(D_gas[0]))
85 elif len(D_gas) != len(positions):
86 if len(D_gas) > 1:
87 D_gas = np.interp(positions, np.linspace(0, 1, len(D_gas)), D_gas)
88 else:
89 D_gas = np.full_like(positions, float(D_gas[0]) if len(D_gas) > 0 else 0.1)
90 else:
91 D_gas = np.full_like(positions, float(D_gas_list) if D_gas_list else 0.1)
92
93 D_ablative_list = geometry.get("D_ablative_outer", D_gas)
94 if isinstance(D_ablative_list, (list, np.ndarray)):
95 D_ablative = np.array(D_ablative_list)
96 if len(D_ablative) == 1:
97 D_ablative = np.full_like(positions, float(D_ablative[0]))
98 elif len(D_ablative) != len(positions):
99 if len(D_ablative) > 1:
100 D_ablative = np.interp(positions, np.linspace(0, 1, len(D_ablative)), D_ablative)
101 else:
102 D_ablative = D_gas
103 else:
104 D_ablative = np.full_like(positions, float(D_ablative_list) if D_ablative_list else D_gas[0])
105
106 # Get recession
107 recession_list = geometry.get("recession", [])
108 if isinstance(recession_list, (list, np.ndarray)):
109 recession = np.array(recession_list)
110 if len(recession) == 1:
111 recession = np.full_like(positions, float(recession[0]))
112 elif len(recession) != len(positions):
113 if len(recession) > 1:
114 recession = np.interp(positions, np.linspace(0, 1, len(recession)), recession)
115 else:
116 recession = np.zeros_like(positions)
117 else:
118 recession = np.full_like(positions, float(recession_list) if recession_list else 0.0)
119
120 # Convert to radii
121 R_gas = D_gas / 2.0
122 R_ablative = D_ablative / 2.0
123
124 all_data.append({
125 "R_gas": R_gas,
126 "R_ablative": R_ablative,
127 "recession": recession,
128 })
129 except Exception as e:
130 # Skip invalid geometries
131 continue
132
133 if len(all_data) == 0:
134 raise ValueError("No valid geometry data after processing")
135
136 # Ensure all arrays have same length
137 n_pos = len(positions)
138 for data in all_data:
139 if len(data["R_gas"]) != n_pos:
140 data["R_gas"] = np.full(n_pos, data["R_gas"][0] if len(data["R_gas"]) > 0 else 0.05)
141 if len(data["R_ablative"]) != n_pos:
142 data["R_ablative"] = np.full(n_pos, data["R_ablative"][0] if len(data["R_ablative"]) > 0 else 0.06)
143 if len(data["recession"]) != n_pos:
144 data["recession"] = np.full(n_pos, data["recession"][0] if len(data["recession"]) > 0 else 0.0)
145
146 # Add initial traces
147 first_data = all_data[0]
148 fig.add_trace(go.Scatter(
149 x=positions,
150 y=first_data["R_gas"],
151 mode='lines',
152 name='Gas Boundary',
153 line=dict(color='orange', width=3),
154 ), row=1, col=1)
155
156 fig.add_trace(go.Scatter(
157 x=positions,
158 y=-first_data["R_gas"],
159 mode='lines',
160 name='Gas Boundary (Lower)',
161 line=dict(color='orange', width=3),
162 showlegend=False,
163 ), row=1, col=1)
164
165 fig.add_trace(go.Scatter(
166 x=positions,
167 y=first_data["R_ablative"],
168 mode='lines',
169 name='Ablative Outer',
170 line=dict(color='brown', width=2, dash='dash'),
171 fill='tonexty',
172 fillcolor='rgba(139, 69, 19, 0.3)',
173 ), row=1, col=1)
174
175 fig.add_trace(go.Scatter(
176 x=positions,
177 y=-first_data["R_ablative"],
178 mode='lines',
179 name='Ablative Outer (Lower)',
180 line=dict(color='brown', width=2, dash='dash'),
181 fill='tonexty',
182 fillcolor='rgba(139, 69, 19, 0.3)',
183 showlegend=False,
184 ), row=1, col=1)
185
186 # Recession trace
187 fig.add_trace(go.Scatter(
188 x=positions,
189 y=first_data["recession"] * 1e6, # Convert to mm
190 mode='lines',
191 name='Cumulative Recession',
192 line=dict(color='red', width=2),
193 ), row=2, col=1)
194
195 # Create frames
196 frames = []
197 for i in range(min(len(all_data), n_steps)):
198 if i >= len(all_data):
199 break
200
201 data = all_data[i]
202
203 frame_data = [
204 go.Scatter(x=positions, y=data["R_gas"], mode='lines',
205 line=dict(color='orange', width=3)),
206 go.Scatter(x=positions, y=-data["R_gas"], mode='lines',
207 line=dict(color='orange', width=3), showlegend=False),
208 go.Scatter(x=positions, y=data["R_ablative"], mode='lines',
209 line=dict(color='brown', width=2, dash='dash'),
210 fill='tonexty', fillcolor='rgba(139, 69, 19, 0.3)'),
211 go.Scatter(x=positions, y=-data["R_ablative"], mode='lines',
212 line=dict(color='brown', width=2, dash='dash'),
213 fill='tonexty', fillcolor='rgba(139, 69, 19, 0.3)', showlegend=False),
214 go.Scatter(x=positions, y=data["recession"] * 1e6, mode='lines',
215 line=dict(color='red', width=2)),
216 ]
217
218 frames.append(go.Frame(
219 data=frame_data,
220 name=str(i),
221 traces=[0, 1, 2, 3, 4]
222 ))
223
224 if len(frames) == 0:
225 raise ValueError("No valid frames created")
226
227 fig.frames = frames
228
229 # Update layout
230 fig.update_layout(
231 title="Ablative Recession Over Time",
232 height=800,
233 updatemenus=[{
234 "type": "buttons",
235 "showactive": False,
236 "buttons": [
237 {
238 "label": "Play",
239 "method": "animate",
240 "args": [None, {
241 "frame": {"duration": 300, "redraw": True},
242 "fromcurrent": True,
243 "transition": {"duration": 150}
244 }]
245 },
246 {
247 "label": "Pause",
248 "method": "animate",
249 "args": [[None], {
250 "frame": {"duration": 0, "redraw": False},
251 "mode": "immediate",
252 "transition": {"duration": 0}
253 }]
254 }
255 ],
256 }],
257 sliders=[{
258 "active": 0,
259 "currentvalue": {
260 "prefix": "Time: ",
261 "suffix": " s",
262 },
263 "steps": [
264 {
265 "args": [[str(i)], {
266 "frame": {"duration": 150, "redraw": True},
267 "mode": "immediate",
268 "transition": {"duration": 150}
269 }],
270 "label": f"{t:.2f}",
271 "method": "animate"
272 }
273 for i, t in enumerate(time_history[:len(frames)])
274 ]
275 }]
276 )
277
278 # Update axes
279 fig.update_xaxes(title_text="Axial Position [m]", row=1, col=1)
280 fig.update_yaxes(title_text="Radius [m]", row=1, col=1, scaleanchor="x", scaleratio=1)
281 fig.update_xaxes(title_text="Axial Position [m]", row=2, col=1)
282 fig.update_yaxes(title_text="Recession [mm]", row=2, col=1)
283
284 return fig
285
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
go.Figure create_recession_animation(np.ndarray time_history, List[Dict] geometry_history, PintleEngineConfig config, Optional[Dict] impingement_data=None, int max_frames=30)