STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
recession_animation_stable.py
Go to the documentation of this file.
1"""Stable recession animation that doesn't crash.
2
3This version uses a simpler approach to avoid crashes.
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 = 20,
21) -> go.Figure:
22 """
23 Create stable animated figure showing recession over time.
24
25 Uses static figure with time slider instead of frames to avoid crashes.
26 """
27 if len(geometry_history) == 0:
28 raise ValueError("No geometry history provided")
29
30 n_steps = len(time_history)
31
32 # Limit frames aggressively to prevent crashes
33 if n_steps > max_frames:
34 step = max(1, n_steps // max_frames)
35 indices = np.arange(0, n_steps, step)
36 time_history = time_history[indices]
37 geometry_history = [geometry_history[i] for i in indices if i < len(geometry_history)]
38 n_steps = len(time_history)
39
40 if n_steps == 0 or len(geometry_history) == 0:
41 raise ValueError("No valid time steps or geometry data")
42
43 # Get positions from first valid geometry
44 positions = None
45 for geom in geometry_history:
46 pos = geom.get("positions", [])
47 if len(pos) > 0:
48 positions = np.array(pos)
49 break
50
51 if positions is None or len(positions) == 0:
52 raise ValueError("No positions found in geometry data")
53
54 # Prepare all data
55 all_R_gas = []
56 all_R_ablative = []
57 all_recession = []
58
59 for geometry in geometry_history:
60 try:
61 pos = np.array(geometry.get("positions", positions))
62 if len(pos) != len(positions):
63 if len(pos) > 1:
64 pos = np.interp(positions, np.linspace(0, 1, len(pos)), pos)
65 else:
66 pos = positions
67
68 # Get diameters
69 D_gas_list = geometry.get("D_gas_chamber", [])
70 if isinstance(D_gas_list, (list, np.ndarray)):
71 D_gas = np.array(D_gas_list)
72 if len(D_gas) == 1:
73 D_gas = np.full_like(positions, float(D_gas[0]))
74 elif len(D_gas) != len(positions):
75 D_gas = np.full_like(positions, float(D_gas[0]) if len(D_gas) > 0 else 0.1)
76 else:
77 D_gas = np.full_like(positions, float(D_gas_list) if D_gas_list else 0.1)
78
79 D_ablative_list = geometry.get("D_ablative_outer", D_gas)
80 if isinstance(D_ablative_list, (list, np.ndarray)):
81 D_ablative = np.array(D_ablative_list)
82 if len(D_ablative) == 1:
83 D_ablative = np.full_like(positions, float(D_ablative[0]))
84 elif len(D_ablative) != len(positions):
85 D_ablative = np.full_like(positions, float(D_ablative[0]) if len(D_ablative) > 0 else D_gas[0])
86 else:
87 D_ablative = np.full_like(positions, float(D_ablative_list) if D_ablative_list else D_gas[0])
88
89 recession_list = geometry.get("recession", [])
90 if isinstance(recession_list, (list, np.ndarray)):
91 recession = np.array(recession_list)
92 if len(recession) == 1:
93 recession = np.full_like(positions, float(recession[0]))
94 elif len(recession) != len(positions):
95 recession = np.full_like(positions, float(recession[0]) if len(recession) > 0 else 0.0)
96 else:
97 recession = np.full_like(positions, float(recession_list) if recession_list else 0.0)
98
99 R_gas = D_gas / 2.0
100 R_ablative = D_ablative / 2.0
101
102 all_R_gas.append(R_gas)
103 all_R_ablative.append(R_ablative)
104 all_recession.append(recession)
105 except Exception:
106 continue
107
108 if len(all_R_gas) == 0:
109 raise ValueError("No valid geometry data after processing")
110
111 # Use static figure with slider instead of frames (more stable)
112 fig = make_subplots(
113 rows=2, cols=1,
114 subplot_titles=("Chamber Cross-Section", "Cumulative Recession"),
115 vertical_spacing=0.15,
116 row_heights=[0.7, 0.3],
117 )
118
119 # Add initial data (frame 0)
120 first_data = {
121 "R_gas": all_R_gas[0] if len(all_R_gas) > 0 else np.zeros_like(positions),
122 "R_ablative": all_R_ablative[0] if len(all_R_ablative) > 0 else np.zeros_like(positions),
123 "recession": all_recession[0] if len(all_recession) > 0 else np.zeros_like(positions),
124 }
125
126 fig.add_trace(go.Scatter(
127 x=positions,
128 y=first_data["R_gas"],
129 mode='lines',
130 name='Gas Boundary',
131 line=dict(color='orange', width=3),
132 ), row=1, col=1)
133
134 fig.add_trace(go.Scatter(
135 x=positions,
136 y=-first_data["R_gas"],
137 mode='lines',
138 name='Gas Boundary (Lower)',
139 line=dict(color='orange', width=3),
140 showlegend=False,
141 ), row=1, col=1)
142
143 fig.add_trace(go.Scatter(
144 x=positions,
145 y=first_data["R_ablative"],
146 mode='lines',
147 name='Ablative Outer',
148 line=dict(color='brown', width=2, dash='dash'),
149 fill='tonexty',
150 fillcolor='rgba(139, 69, 19, 0.3)',
151 ), row=1, col=1)
152
153 fig.add_trace(go.Scatter(
154 x=positions,
155 y=-first_data["R_ablative"],
156 mode='lines',
157 name='Ablative Outer (Lower)',
158 line=dict(color='brown', width=2, dash='dash'),
159 fill='tonexty',
160 fillcolor='rgba(139, 69, 19, 0.3)',
161 showlegend=False,
162 ), row=1, col=1)
163
164 fig.add_trace(go.Scatter(
165 x=positions,
166 y=first_data["recession"] * 1e6,
167 mode='lines',
168 name='Cumulative Recession',
169 line=dict(color='red', width=2),
170 ), row=2, col=1)
171
172 # Create frames (limited to prevent crashes)
173 frames = []
174 n_frames = min(len(all_R_gas), n_steps, max_frames)
175
176 for i in range(n_frames):
177 if i >= len(all_R_gas) or i >= len(all_R_ablative) or i >= len(all_recession):
178 continue
179
180 frame_data = [
181 go.Scatter(x=positions, y=all_R_gas[i], mode='lines', line=dict(color='orange', width=3)),
182 go.Scatter(x=positions, y=-all_R_gas[i], mode='lines', line=dict(color='orange', width=3), showlegend=False),
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)'),
185 go.Scatter(x=positions, y=-all_R_ablative[i], mode='lines', line=dict(color='brown', width=2, dash='dash'),
186 fill='tonexty', fillcolor='rgba(139, 69, 19, 0.3)', showlegend=False),
187 go.Scatter(x=positions, y=all_recession[i] * 1e6, mode='lines', line=dict(color='red', width=2)),
188 ]
189
190 frames.append(go.Frame(
191 data=frame_data,
192 name=str(i),
193 traces=[0, 1, 2, 3, 4]
194 ))
195
196 if len(frames) == 0:
197 # Fallback: just show static plot
198 fig.update_layout(
199 title="Ablative Recession (Static View)",
200 height=800,
201 )
202 else:
203 fig.frames = frames
204
205 fig.update_layout(
206 title="Ablative Recession Over Time",
207 height=800,
208 updatemenus=[{
209 "type": "buttons",
210 "showactive": False,
211 "buttons": [
212 {
213 "label": "Play",
214 "method": "animate",
215 "args": [None, {
216 "frame": {"duration": 500, "redraw": True},
217 "fromcurrent": True,
218 }]
219 },
220 {
221 "label": "Pause",
222 "method": "animate",
223 "args": [[None], {
224 "frame": {"duration": 0, "redraw": False},
225 "mode": "immediate",
226 }]
227 }
228 ],
229 }],
230 sliders=[{
231 "active": 0,
232 "currentvalue": {"prefix": "Time: ", "suffix": " s"},
233 "steps": [
234 {
235 "args": [[str(i)], {"frame": {"duration": 300, "redraw": True}, "mode": "immediate"}],
236 "label": f"{t:.2f}",
237 "method": "animate"
238 }
239 for i, t in enumerate(time_history[:len(frames)])
240 ]
241 }]
242 )
243
244 fig.update_xaxes(title_text="Axial Position [m]", row=1, col=1)
245 fig.update_yaxes(title_text="Radius [m]", row=1, col=1, scaleanchor="x", scaleratio=1)
246 fig.update_xaxes(title_text="Axial Position [m]", row=2, col=1)
247 fig.update_yaxes(title_text="Recession [mm]", row=2, col=1)
248
249 return fig
250
#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=20)