STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
copv_flight_helpers.py
Go to the documentation of this file.
1"""COPV and Flight Simulation Helper Functions.
2
3This module contains:
4- COPV pressure curve calculation
5- Flight simulation execution
6"""
7
8from __future__ import annotations
9
10from typing import Dict, Any
11import numpy as np
12import pandas as pd
13
14from engine.pipeline.config_schemas import PintleEngineConfig
15
16
18 time_array: np.ndarray,
19 mdot_O: np.ndarray,
20 mdot_F: np.ndarray,
21 P_tank_O: np.ndarray,
22 P_tank_F: np.ndarray,
23 config: PintleEngineConfig,
24 copv_volume_m3: float,
25 T0_K: float = 260.0,
26 Tp_K: float = 260.0,
27) -> Dict[str, Any]:
28 """
29 Calculate COPV pressure curve using polytropic blowdown model.
30
31 Uses the same method as the COPV tab with user-specified temperatures (260K).
32 """
33 try:
34 from copv.copv_solve_both import size_or_check_copv_for_polytropic_N2
35
36 psi_to_Pa = 6894.757293168
37 df = pd.DataFrame({
38 "time": time_array,
39 "mdot_O (kg/s)": mdot_O,
40 "mdot_F (kg/s)": mdot_F,
41 "P_tank_O (psi)": P_tank_O / psi_to_Pa,
42 "P_tank_F (psi)": P_tank_F / psi_to_Pa,
43 })
44
45 copv_results = size_or_check_copv_for_polytropic_N2(
46 df=df,
47 config=config,
48 n=1.2,
49 T0_K=T0_K,
50 Tp_K=Tp_K,
51 use_real_gas=False,
52 copv_volume_m3=copv_volume_m3,
53 branch_temperatures_K={
54 "oxidizer": Tp_K,
55 "fuel": Tp_K,
56 },
57 )
58
59 return {
60 "success": True,
61 "time": time_array,
62 "copv_pressure_Pa": copv_results.get("PH_trace_Pa", np.zeros_like(time_array)),
63 "copv_pressure_psi": copv_results.get("PH_trace_Pa", np.zeros_like(time_array)) / psi_to_Pa,
64 "initial_pressure_Pa": copv_results.get("P0_Pa", 0),
65 "initial_mass_kg": copv_results.get("m0_kg", 0),
66 "total_delivered_kg": copv_results.get("total_delivered_mass_kg", 0),
67 "min_margin_psi": copv_results.get("min_margin_Pa", 0) / psi_to_Pa,
68 }
69 except Exception as e:
70 # Fallback: simple pressure estimate
71 P_max = max(np.max(P_tank_O), np.max(P_tank_F))
72 copv_P0 = P_max * 1.15
73 n = 1.2
74 mdot_total = mdot_O + mdot_F
75 mass_consumed = np.cumsum(mdot_total * np.gradient(time_array))
76 copv_pressure = copv_P0 * (1 - 0.3 * mass_consumed / (mass_consumed[-1] + 0.001))
77
78 return {
79 "success": False,
80 "error": str(e),
81 "time": time_array,
82 "copv_pressure_Pa": copv_pressure,
83 "copv_pressure_psi": copv_pressure / 6894.757293168,
84 "initial_pressure_Pa": copv_P0,
85 "initial_mass_kg": 0.5,
86 "total_delivered_kg": 0.3,
87 "min_margin_psi": 50.0,
88 }
89
90
92 config: PintleEngineConfig,
93 pressure_curves: Dict[str, np.ndarray],
94 burn_time: float,
95) -> Dict[str, Any]:
96 """Run flight simulation on the optimized engine.
97
98 Uses the existing setup_flight function from flight_sim.py.
99
100 Args:
101 config: Engine configuration (will be updated with burn_time)
102 pressure_curves: Dict with 'time', 'thrust', 'mdot_O', 'mdot_F' arrays
103 burn_time: Actual burn time to use (may be truncated from original)
104
105 Returns:
106 Dict with flight simulation results
107 """
108 try:
109 from ui.flight_sim import setup_flight
110 from scipy.interpolate import interp1d
111 import copy
112
113 # Create a copy to avoid modifying the original config
114 config_copy = copy.deepcopy(config)
115
116 # CRITICAL: Update config's burn_time to match the actual (possibly truncated) burn_time
117 # This ensures setup_flight uses the correct burn_time for tank discretization
118 # Ensure burn_time is a float, not an interp1d or other object
119 if hasattr(burn_time, '__call__'):
120 # burn_time is a callable (interp1d) - this shouldn't happen but handle it
121 burn_time_float = float(burn_time(0)) # Just use the value at t=0 as fallback
122 else:
123 burn_time_float = float(burn_time)
124 if hasattr(config_copy, 'thrust') and hasattr(config_copy.thrust, 'burn_time'):
125 config_copy.thrust.burn_time = burn_time_float
126
127 # Extract arrays and ensure they're numpy arrays (not interp1d objects)
128 # Handle the case where pressure_curves values might be interp1d objects
129 time_raw = pressure_curves["time"]
130 thrust_raw = pressure_curves["thrust"]
131 mdot_O_raw = pressure_curves["mdot_O"]
132 mdot_F_raw = pressure_curves["mdot_F"]
133
134 # Helper function to convert callable/interp1d to array
135 def to_array(val, time_samples):
136 """Convert a value (array, callable, or interp1d) to a numpy array."""
137 if val is None:
138 return np.array([], dtype=float)
139 elif hasattr(val, '__call__'):
140 # It's a callable (interp1d), sample it
141 return np.asarray([float(val(t)) for t in time_samples], dtype=float)
142 else:
143 return np.asarray(val, dtype=float)
144
145 # First, get time array (may need to generate samples if it's callable)
146 if hasattr(time_raw, '__call__'):
147 # Time is callable - generate time samples
148 time_samples = np.linspace(0, burn_time_float, max(100, int(burn_time_float * 100)))
149 time_array = time_samples
150 else:
151 time_array = np.asarray(time_raw, dtype=float)
152 time_samples = time_array
153
154 # Convert other arrays using the time samples
155 thrust_array = to_array(thrust_raw, time_samples)
156 mdot_O_array = to_array(mdot_O_raw, time_samples)
157 mdot_F_array = to_array(mdot_F_raw, time_samples)
158
159 # Ensure arrays are 1D and same length
160 time_array = time_array.flatten()
161 thrust_array = thrust_array.flatten()
162 mdot_O_array = mdot_O_array.flatten()
163 mdot_F_array = mdot_F_array.flatten()
164
165 # Ensure all arrays have the same length
166 min_len = min(len(time_array), len(thrust_array), len(mdot_O_array), len(mdot_F_array))
167 if min_len == 0:
168 raise ValueError("Pressure curves arrays are empty")
169
170 time_array = time_array[:min_len]
171 thrust_array = thrust_array[:min_len]
172 mdot_O_array = mdot_O_array[:min_len]
173 mdot_F_array = mdot_F_array[:min_len]
174
175 # Convert arrays to interpolation functions for setup_flight
176 # setup_flight expects Functions or callables, not arrays
177 thrust_func = interp1d(time_array, thrust_array, kind='linear', fill_value=0, bounds_error=False)
178 mdot_O_func = interp1d(time_array, mdot_O_array, kind='linear', fill_value=0, bounds_error=False)
179 mdot_F_func = interp1d(time_array, mdot_F_array, kind='linear', fill_value=0, bounds_error=False)
180
181 # Call the existing setup_flight function from flight_sim.py
182 # This reuses all the smart logic: tank underfill detection, truncation, etc.
183 result = setup_flight(config_copy, thrust_func, mdot_O_func, mdot_F_func, plot_results=False)
184
185 apogee = result.get("apogee", 0)
186 max_velocity = result.get("max_velocity", 0)
187
188 # Validate results - if apogee is suspiciously low, something went wrong
189 if apogee < 10.0:
190 # Check if thrust curve is valid
191 max_thrust = np.max(thrust_array) if len(thrust_array) > 0 else 0
192 initial_thrust = thrust_array[0] if len(thrust_array) > 0 else 0
193
194 # Check masses
195 lox_mass = getattr(config_copy.lox_tank, 'mass', 0) if hasattr(config_copy, 'lox_tank') else 0
196 fuel_mass = getattr(config_copy.fuel_tank, 'mass', 0) if hasattr(config_copy, 'fuel_tank') else 0
197
198 error_msg = (
199 f"Apogee is suspiciously low ({apogee:.1f}m). "
200 f"Diagnostics: max_thrust={max_thrust:.1f}N, initial_thrust={initial_thrust:.1f}N, "
201 f"lox_mass={lox_mass:.2f}kg, fuel_mass={fuel_mass:.2f}kg, burn_time={burn_time_float:.2f}s"
202 )
203
204 return {
205 "success": False,
206 "error": error_msg,
207 "apogee": apogee,
208 "max_velocity": max_velocity,
209 "flight_time": result.get("flight_time", 0),
210 "flight_obj": result.get("flight", None),
211 }
212
213 return {
214 "success": True,
215 "apogee": apogee,
216 "max_velocity": max_velocity,
217 "flight_time": result.get("flight_time", 0),
218 "flight_obj": result.get("flight", None),
219 "truncation_info": result.get("truncation_info", {}), # Pass through truncation info
220 }
221 except ValueError as e:
222 error_str = str(e)
223 # Translate RocketPy's internal Function domain error into a user-friendly message
224 if "must be within the domain of the Function" in error_str:
225 # This is a RocketPy internal error about function composition bounds
226 # Usually caused by tank fill levels exceeding geometry constraints
227 return {
228 "success": False,
229 "error": "Tank simulation error: propellant mass may exceed tank capacity. Try reducing LOX or fuel mass slightly.",
230 "apogee": 0,
231 "max_velocity": 0,
232 }
233 # For other ValueErrors, return the actual error
234 return {
235 "success": False,
236 "error": error_str,
237 "apogee": 0,
238 "max_velocity": 0,
239 }
240 except Exception as e:
241 return {
242 "success": False,
243 "error": str(e),
244 "apogee": 0,
245 "max_velocity": 0,
246 }
247
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
Dict[str, Any] run_flight_simulation(PintleEngineConfig config, Dict[str, np.ndarray] pressure_curves, float burn_time)
Dict[str, Any] calculate_copv_pressure_curve(np.ndarray time_array, np.ndarray mdot_O, np.ndarray mdot_F, np.ndarray P_tank_O, np.ndarray P_tank_F, PintleEngineConfig config, float copv_volume_m3, float T0_K=260.0, float Tp_K=260.0)