STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
recession_animation_fixed.py
Go to the documentation of this file.
1"""Fixed animated visualization of ablative recession over time.
2
3This version fixes the animation issues and prevents tab restarts.
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
13from engine.pipeline.localized_ablation import calculate_impingement_zones
14
15
17 time_history: np.ndarray,
18 geometry_history: List[Dict],
19 config: PintleEngineConfig,
20 impingement_data: Optional[Dict] = None,
21 max_frames: int = 50,
22) -> go.Figure:
23 """
24 Create animated plotly figure showing recession over time.
25
26 Fixed version that:
27 - Limits number of frames to prevent crashes
28 - Handles missing data gracefully
29 - Uses simpler animation structure
30 """
31 n_steps = len(time_history)
32
33 # Limit frames to prevent crashes
34 if n_steps > max_frames:
35 step = n_steps // max_frames
36 indices = np.arange(0, n_steps, step)
37 time_history = time_history[indices]
38 geometry_history = [geometry_history[i] for i in indices]
39 n_steps = len(time_history)
40
41 if n_steps == 0:
42 raise ValueError("No time steps available for animation")
43
44 # Get initial geometry
45 initial_geometry = geometry_history[0]
46 positions = np.array(initial_geometry.get("positions", []))
47
48 if len(positions) == 0:
49 raise ValueError("No positions in geometry data")
50
51 # Create figure
52 fig = make_subplots(
53 rows=2, cols=1,
54 subplot_titles=("Chamber Cross-Section", "Recession Rate"),
55 vertical_spacing=0.15,
56 row_heights=[0.7, 0.3],
57 )
58
59 # Prepare data arrays for all frames
60 all_R_gas = []
61 all_R_ablative = []
62 all_recession_rates = []
63
64 for geometry in geometry_history:
65 try:
66 positions = np.array(geometry.get("positions", []))
67 if len(positions) == 0:
68 continue
69
70 # Handle D_gas_chamber - can be array or single value
71 D_gas_raw = geometry.get("D_gas_chamber", [])
72 if isinstance(D_gas_raw, (list, np.ndarray)):
73 D_gas = np.array(D_gas_raw)
74 if len(D_gas) != len(positions):
75 # If lengths don't match, use first value or interpolate
76 if len(D_gas) == 1:
77 D_gas = np.full_like(positions, float(D_gas[0]))
78 else:
79 D_gas = np.full_like(positions, float(D_gas[0])) # Use first value
80 else:
81 D_gas = np.full_like(positions, float(D_gas_raw) if D_gas_raw else 0.1)
82
83 # Handle D_ablative_outer
84 D_ablative_raw = geometry.get("D_ablative_outer", D_gas)
85 if isinstance(D_ablative_raw, (list, np.ndarray)):
86 D_ablative = np.array(D_ablative_raw)
87 if len(D_ablative) != len(positions):
88 if len(D_ablative) == 1:
89 D_ablative = np.full_like(positions, float(D_ablative[0]))
90 else:
91 D_ablative = np.full_like(positions, float(D_ablative[0]))
92 else:
93 D_ablative = np.full_like(positions, float(D_ablative_raw) if D_ablative_raw else D_gas[0])
94
95 # Handle recession
96 recession_raw = geometry.get("recession", [])
97 if isinstance(recession_raw, (list, np.ndarray)):
98 recession = np.array(recession_raw)
99 if len(recession) != len(positions):
100 if len(recession) == 1:
101 recession = np.full_like(positions, float(recession[0]))
102 else:
103 recession = np.full_like(positions, float(recession[0]))
104 else:
105 recession = np.full_like(positions, float(recession_raw) if recession_raw else 0.0)
106
107 R_gas = D_gas / 2.0
108 R_ablative = D_ablative / 2.0
109
110 # Calculate recession rate (derivative with respect to time, not position)
111 # For now, just show cumulative recession
112 recession_rate = np.zeros_like(positions) # Will be calculated from time history if needed
113
114 all_R_gas.append(R_gas)
115 all_R_ablative.append(R_ablative)
116 all_recession_rates.append(recession_rate)
117 except Exception as e:
118 # Skip this geometry if there's an error
119 continue
120
121 # Use first frame positions for all
122 positions = np.array(geometry_history[0].get("positions", []))
123
124 # Add initial traces (will be updated by animation)
125 fig.add_trace(go.Scatter(
126 x=positions,
127 y=all_R_gas[0] if len(all_R_gas) > 0 else np.zeros_like(positions),
128 mode='lines',
129 name='Gas Boundary',
130 line=dict(color='orange', width=3),
131 ), row=1, col=1)
132
133 fig.add_trace(go.Scatter(
134 x=positions,
135 y=-all_R_gas[0] if len(all_R_gas) > 0 else np.zeros_like(positions),
136 mode='lines',
137 name='Gas Boundary (Lower)',
138 line=dict(color='orange', width=3),
139 showlegend=False,
140 ), row=1, col=1)
141
142 fig.add_trace(go.Scatter(
143 x=positions,
144 y=all_R_ablative[0] if len(all_R_ablative) > 0 else np.zeros_like(positions),
145 mode='lines',
146 name='Ablative Outer',
147 line=dict(color='brown', width=2, dash='dash'),
148 fill='tonexty',
149 fillcolor='rgba(139, 69, 19, 0.3)',
150 ), row=1, col=1)
151
152 fig.add_trace(go.Scatter(
153 x=positions,
154 y=-all_R_ablative[0] if len(all_R_ablative) > 0 else np.zeros_like(positions),
155 mode='lines',
156 name='Ablative Outer (Lower)',
157 line=dict(color='brown', width=2, dash='dash'),
158 fill='tonexty',
159 fillcolor='rgba(139, 69, 19, 0.3)',
160 showlegend=False,
161 ), row=1, col=1)
162
163 # Recession rate trace
164 fig.add_trace(go.Scatter(
165 x=positions,
166 y=all_recession_rates[0] if len(all_recession_rates) > 0 else np.zeros_like(positions),
167 mode='lines',
168 name='Recession Rate',
169 line=dict(color='red', width=2),
170 ), row=2, col=1)
171
172 # Create frames
173 frames = []
174 for i in range(n_steps):
175 if i >= len(all_R_gas) or i >= len(all_R_ablative) or i >= len(all_recession_rates):
176 continue
177
178 frame_data = [
179 go.Scatter(x=positions, y=all_R_gas[i], mode='lines', line=dict(color='orange', width=3)),
180 go.Scatter(x=positions, y=-all_R_gas[i], mode='lines', line=dict(color='orange', width=3), showlegend=False),
181 go.Scatter(x=positions, y=all_R_ablative[i], mode='lines', line=dict(color='brown', width=2, dash='dash'),
182 fill='tonexty', fillcolor='rgba(139, 69, 19, 0.3)'),
183 go.Scatter(x=positions, y=-all_R_ablative[i], mode='lines', line=dict(color='brown', width=2, dash='dash'),
184 fill='tonexty', fillcolor='rgba(139, 69, 19, 0.3)', showlegend=False),
185 go.Scatter(x=positions, y=all_recession_rates[i], mode='lines', line=dict(color='red', width=2)),
186 ]
187
188 frames.append(go.Frame(
189 data=frame_data,
190 name=str(i),
191 traces=[0, 1, 2, 3, 4]
192 ))
193
194 fig.frames = frames
195
196 # Update layout
197 fig.update_layout(
198 title="Ablative Recession Over Time",
199 height=800,
200 updatemenus=[{
201 "type": "buttons",
202 "showactive": False,
203 "buttons": [
204 {
205 "label": "Play",
206 "method": "animate",
207 "args": [None, {
208 "frame": {"duration": 200, "redraw": True},
209 "fromcurrent": True,
210 "transition": {"duration": 100}
211 }]
212 },
213 {
214 "label": "Pause",
215 "method": "animate",
216 "args": [[None], {
217 "frame": {"duration": 0, "redraw": False},
218 "mode": "immediate",
219 "transition": {"duration": 0}
220 }]
221 }
222 ],
223 }],
224 sliders=[{
225 "active": 0,
226 "currentvalue": {
227 "prefix": "Time: ",
228 "suffix": " s",
229 },
230 "steps": [
231 {
232 "args": [[str(i)], {
233 "frame": {"duration": 100, "redraw": True},
234 "mode": "immediate",
235 "transition": {"duration": 100}
236 }],
237 "label": f"{t:.2f}",
238 "method": "animate"
239 }
240 for i, t in enumerate(time_history[:len(frames)])
241 ]
242 }]
243 )
244
245 # Update axes
246 fig.update_xaxes(title_text="Axial Position [m]", row=1, col=1)
247 fig.update_yaxes(title_text="Radius [m]", row=1, col=1, scaleanchor="x", scaleratio=1)
248 fig.update_xaxes(title_text="Axial Position [m]", row=2, col=1)
249 fig.update_yaxes(title_text="Recession Rate [mm/s]", row=2, col=1)
250
251 return fig
252
go.Figure create_recession_animation(np.ndarray time_history, List[Dict] geometry_history, PintleEngineConfig config, Optional[Dict] impingement_data=None, int max_frames=50)