61 P_tank_O_psi: np.ndarray,
62 P_tank_F_psi: np.ndarray,
63 run_copv: bool =
True,
64 lox_mass_kg: Optional[np.ndarray] =
None,
65 fuel_mass_kg: Optional[np.ndarray] =
None,
67 """Compute time-series results from pressure profiles.
69 This is a standalone version that doesn't depend on Streamlit.
71 P_tank_O_pa = np.asarray(P_tank_O_psi) * PSI_TO_PA
72 P_tank_F_pa = np.asarray(P_tank_F_psi) * PSI_TO_PA
75 ablative_cfg = runner.config.ablative_cooling
77 ablative_cfg
is not None
78 and ablative_cfg.enabled
79 and getattr(ablative_cfg,
'track_geometry_evolution',
False)
84 results = runner.evaluate_arrays_with_time(
88 use_coupled_solver=
True,
91 results = runner.evaluate_arrays(P_tank_O_pa, P_tank_F_pa)
95 "time": np.asarray(times).tolist(),
96 "P_tank_O_psi": np.asarray(P_tank_O_psi, dtype=float).tolist(),
97 "P_tank_F_psi": np.asarray(P_tank_F_psi, dtype=float).tolist(),
99 "Pc_psi": np.asarray(results[
"Pc"], dtype=float) * PA_TO_PSI,
100 "thrust_kN": np.asarray(results[
"F"], dtype=float) / 1000.0,
101 "Isp_s": np.asarray(results[
"Isp"], dtype=float),
102 "MR": np.asarray(results[
"MR"], dtype=float),
103 "mdot_O_kg_s": np.asarray(results[
"mdot_O"], dtype=float),
104 "mdot_F_kg_s": np.asarray(results[
"mdot_F"], dtype=float),
105 "mdot_total_kg_s": np.asarray(results[
"mdot_total"], dtype=float),
106 "cstar_actual_m_s": np.asarray(results[
"cstar_actual"], dtype=float),
107 "gamma": np.asarray(results[
"gamma"], dtype=float),
117 def to_float_array(arr):
119 return np.asarray(arr, dtype=float)
121 return np.zeros(len(times))
123 if lox_mass_kg
is not None and len(lox_mass_kg) == len(times):
124 lox_m = to_float_array(lox_mass_kg)
125 mask_lox = lox_m <= 1e-4
129 mask = mask | mask_lox
131 if fuel_mass_kg
is not None and len(fuel_mass_kg) == len(times):
132 fuel_m = to_float_array(fuel_mass_kg)
133 mask_fuel = fuel_m <= 1e-4
137 mask = mask | mask_fuel
141 n_masked = np.sum(mask)
144 logging.info(f
"[TIMESERIES] Masking {n_masked}/{len(times)} points due to flameout.")
149 "Pc_psi",
"thrust_kN",
"Isp_s",
"MR",
150 "mdot_O_kg_s",
"mdot_F_kg_s",
"mdot_total_kg_s",
153 for key
in metrics_to_mask:
154 if key
in result_data:
156 arr = np.array(result_data[key], dtype=float)
158 result_data[key] = arr
162 for k, v
in result_data.items():
163 if isinstance(v, np.ndarray):
164 result_data[k] = v.tolist()
167 if "Cd_O" in results:
168 result_data[
"Cd_O"] = np.asarray(results[
"Cd_O"], dtype=float).tolist()
169 if "Cd_F" in results:
170 result_data[
"Cd_F"] = np.asarray(results[
"Cd_F"], dtype=float).tolist()
173 diagnostics_list = results.get(
"diagnostics", [])
174 n_points = len(times)
177 delta_P_injector_O_psi = []
178 delta_P_injector_F_psi = []
179 recession_rate_ablative_um_s = []
180 recession_rate_graphite_thermal_um_s = []
181 recession_rate_graphite_oxidation_um_s = []
184 for i, diag
in enumerate(diagnostics_list):
185 if not isinstance(diag, dict):
186 delta_P_injector_O_psi.append(np.nan)
187 delta_P_injector_F_psi.append(np.nan)
188 recession_rate_ablative_um_s.append(np.nan)
189 recession_rate_graphite_thermal_um_s.append(np.nan)
190 recession_rate_graphite_oxidation_um_s.append(np.nan)
198 injector_pressure = diag.get(
"injector_pressure")
199 src = injector_pressure
if isinstance(injector_pressure, dict)
and injector_pressure
else diag
202 v = src.get(f
"delta_p_injector_{side}")
203 return v
if v
is not None else src.get(f
"delta_P_injector_{side}")
208 if delta_P_O
is not None:
209 delta_P_injector_O_psi.append(float(delta_P_O * PA_TO_PSI))
211 delta_P_injector_O_psi.append(np.nan)
213 if delta_P_F
is not None:
214 delta_P_injector_F_psi.append(float(delta_P_F * PA_TO_PSI))
216 delta_P_injector_F_psi.append(np.nan)
219 cooling = diag.get(
"cooling", {})
220 ablative = cooling.get(
"ablative", {})
if cooling
else {}
221 graphite = cooling.get(
"graphite", {})
if cooling
else {}
224 if ablative
and isinstance(ablative, dict):
225 recession_rate = ablative.get(
"recession_rate")
226 if recession_rate
is not None:
227 recession_rate_ablative_um_s.append(float(recession_rate * 1e6))
229 recession_rate_ablative_um_s.append(np.nan)
231 recession_rate_ablative_um_s.append(np.nan)
234 if graphite
and isinstance(graphite, dict)
and graphite.get(
"enabled",
False):
235 thermal_rate = graphite.get(
"recession_rate_thermal")
236 oxidation_rate = graphite.get(
"oxidation_rate")
237 if thermal_rate
is not None:
238 recession_rate_graphite_thermal_um_s.append(float(thermal_rate * 1e6))
240 recession_rate_graphite_thermal_um_s.append(np.nan)
241 if oxidation_rate
is not None:
242 recession_rate_graphite_oxidation_um_s.append(float(oxidation_rate * 1e6))
244 recession_rate_graphite_oxidation_um_s.append(np.nan)
246 recession_rate_graphite_thermal_um_s.append(np.nan)
247 recession_rate_graphite_oxidation_um_s.append(np.nan)
251 axial_positions =
None
252 heat_flux_profiles = []
253 wall_temp_profiles = []
256 ablative_axial_positions =
None
257 ablative_q_incident_profiles = []
258 ablative_q_conv_profiles = []
259 ablative_q_rad_profiles = []
260 ablative_q_net_profiles = []
261 ablative_throat_index = -1
265 logger = logging.getLogger(__name__)
266 if diagnostics_list
and len(diagnostics_list) > 0
and isinstance(diagnostics_list[0], dict):
267 first_diag = diagnostics_list[0]
268 cooling_data = first_diag.get(
"cooling", {})
269 logger.info(f
"[TIMESERIES DEBUG] First diagnostic 'cooling' keys: {list(cooling_data.keys()) if cooling_data else 'no cooling'}")
270 if cooling_data
and "ablative" in cooling_data:
271 abl_data = cooling_data[
"ablative"]
272 logger.info(f
"[TIMESERIES DEBUG] ablative keys: {list(abl_data.keys()) if isinstance(abl_data, dict) else 'not a dict'}")
273 if isinstance(abl_data, dict):
274 logger.info(f
"[TIMESERIES DEBUG] ablative segment_x length: {len(abl_data.get('segment_x', []))}")
276 for i, diag
in enumerate(diagnostics_list):
277 if not isinstance(diag, dict):
278 heat_flux_profiles.append([])
279 wall_temp_profiles.append([])
280 ablative_q_incident_profiles.append([])
281 ablative_q_conv_profiles.append([])
282 ablative_q_rad_profiles.append([])
283 ablative_q_net_profiles.append([])
286 cooling = diag.get(
"cooling", {})
287 regen = cooling.get(
"regen", {})
if cooling
else {}
288 ablative = cooling.get(
"ablative", {})
if cooling
else {}
291 if regen
and isinstance(regen, dict):
292 segment_positions = regen.get(
"segment_positions", [])
293 segment_heat_flux = regen.get(
"segment_heat_flux", [])
294 segment_wall_temps = regen.get(
"segment_wall_temperatures", [])
297 if axial_positions
is None and segment_positions:
298 axial_positions = [float(p)
for p
in segment_positions]
300 heat_flux_profiles.append([float(q)
for q
in segment_heat_flux]
if segment_heat_flux
else [])
301 wall_temp_profiles.append([float(t)
for t
in segment_wall_temps]
if segment_wall_temps
else [])
303 heat_flux_profiles.append([])
304 wall_temp_profiles.append([])
307 if ablative
and isinstance(ablative, dict):
308 segment_x = ablative.get(
"segment_x", [])
309 segment_q_incident = ablative.get(
"segment_q_incident", [])
310 segment_q_conv = ablative.get(
"segment_q_conv", [])
311 segment_q_rad = ablative.get(
"segment_q_rad", [])
312 segment_q_net = ablative.get(
"segment_q_net", [])
313 throat_idx = ablative.get(
"throat_index", -1)
316 if ablative_axial_positions
is None and segment_x:
317 ablative_axial_positions = [float(p)
for p
in segment_x]
318 ablative_throat_index = throat_idx
320 ablative_q_incident_profiles.append([float(q)
for q
in segment_q_incident]
if segment_q_incident
else [])
321 ablative_q_conv_profiles.append([float(q)
for q
in segment_q_conv]
if segment_q_conv
else [])
322 ablative_q_rad_profiles.append([float(q)
for q
in segment_q_rad]
if segment_q_rad
else [])
323 ablative_q_net_profiles.append([float(q)
for q
in segment_q_net]
if segment_q_net
else [])
325 ablative_q_incident_profiles.append([])
326 ablative_q_conv_profiles.append([])
327 ablative_q_rad_profiles.append([])
328 ablative_q_net_profiles.append([])
334 recession_cumulative_chamber_um =
None
335 recession_cumulative_throat_um =
None
337 if "Lstar" in results:
338 Lstar_array = np.asarray(results[
"Lstar"], dtype=float)
339 if len(Lstar_array) == n_points:
340 Lstar_mm = (Lstar_array * 1000.0).tolist()
342 if "V_chamber" in results:
343 V_chamber_array = np.asarray(results[
"V_chamber"], dtype=float)
344 if len(V_chamber_array) == n_points:
345 V_chamber_m3 = V_chamber_array.tolist()
347 if "A_throat" in results:
348 A_throat_array = np.asarray(results[
"A_throat"], dtype=float)
349 if len(A_throat_array) == n_points:
350 A_throat_m2 = A_throat_array.tolist()
352 if "recession_chamber" in results:
353 recession_array = np.asarray(results[
"recession_chamber"], dtype=float)
354 if len(recession_array) == n_points:
355 recession_cumulative_chamber_um = (recession_array * 1e6).tolist()
357 if "recession_throat" in results:
358 recession_array = np.asarray(results[
"recession_throat"], dtype=float)
359 if len(recession_array) == n_points:
360 recession_cumulative_throat_um = (recession_array * 1e6).tolist()
363 recession_cumulative_ablative_mm =
None
364 recession_cumulative_graphite_thermal_mm =
None
365 recession_cumulative_graphite_oxidation_mm =
None
367 if any(np.isfinite(recession_rate_ablative_um_s)):
368 times_array = np.asarray(times)
369 dt = np.diff(times_array, prepend=times_array[0]
if len(times_array) > 0
else 0.0)
370 recession_rate_m_s = np.asarray(recession_rate_ablative_um_s, dtype=float) * 1e-6
371 recession_rate_m_s = np.nan_to_num(recession_rate_m_s, nan=0.0)
372 cumulative = np.cumsum(recession_rate_m_s * dt) * 1000.0
373 recession_cumulative_ablative_mm = cumulative.tolist()
375 if any(np.isfinite(recession_rate_graphite_thermal_um_s)):
376 times_array = np.asarray(times)
377 dt = np.diff(times_array, prepend=times_array[0]
if len(times_array) > 0
else 0.0)
378 recession_rate_m_s = np.asarray(recession_rate_graphite_thermal_um_s, dtype=float) * 1e-6
379 recession_rate_m_s = np.nan_to_num(recession_rate_m_s, nan=0.0)
380 cumulative = np.cumsum(recession_rate_m_s * dt) * 1000.0
381 recession_cumulative_graphite_thermal_mm = cumulative.tolist()
383 if any(np.isfinite(recession_rate_graphite_oxidation_um_s)):
384 times_array = np.asarray(times)
385 dt = np.diff(times_array, prepend=times_array[0]
if len(times_array) > 0
else 0.0)
386 recession_rate_m_s = np.asarray(recession_rate_graphite_oxidation_um_s, dtype=float) * 1e-6
387 recession_rate_m_s = np.nan_to_num(recession_rate_m_s, nan=0.0)
388 cumulative = np.cumsum(recession_rate_m_s * dt) * 1000.0
389 recession_cumulative_graphite_oxidation_mm = cumulative.tolist()
392 if delta_P_injector_O_psi
and any(np.isfinite(delta_P_injector_O_psi)):
393 result_data[
"delta_P_injector_O_psi"] = delta_P_injector_O_psi
394 if delta_P_injector_F_psi
and any(np.isfinite(delta_P_injector_F_psi)):
395 result_data[
"delta_P_injector_F_psi"] = delta_P_injector_F_psi
397 if Lstar_mm
and any(np.isfinite(Lstar_mm)):
398 result_data[
"Lstar_mm"] = Lstar_mm
400 if recession_rate_ablative_um_s
and any(np.isfinite(recession_rate_ablative_um_s)):
401 result_data[
"recession_rate_ablative_um_s"] = recession_rate_ablative_um_s
402 if recession_rate_graphite_thermal_um_s
and any(np.isfinite(recession_rate_graphite_thermal_um_s)):
403 result_data[
"recession_rate_graphite_thermal_um_s"] = recession_rate_graphite_thermal_um_s
404 if recession_rate_graphite_oxidation_um_s
and any(np.isfinite(recession_rate_graphite_oxidation_um_s)):
405 result_data[
"recession_rate_graphite_oxidation_um_s"] = recession_rate_graphite_oxidation_um_s
407 if recession_cumulative_ablative_mm
and any(np.isfinite(recession_cumulative_ablative_mm)):
408 result_data[
"recession_cumulative_ablative_mm"] = recession_cumulative_ablative_mm
409 if recession_cumulative_graphite_thermal_mm
and any(np.isfinite(recession_cumulative_graphite_thermal_mm)):
410 result_data[
"recession_cumulative_graphite_thermal_mm"] = recession_cumulative_graphite_thermal_mm
411 if recession_cumulative_graphite_oxidation_mm
and any(np.isfinite(recession_cumulative_graphite_oxidation_mm)):
412 result_data[
"recession_cumulative_graphite_oxidation_mm"] = recession_cumulative_graphite_oxidation_mm
414 if recession_cumulative_chamber_um
and any(np.isfinite(recession_cumulative_chamber_um)):
415 result_data[
"recession_cumulative_chamber_um"] = recession_cumulative_chamber_um
416 if recession_cumulative_throat_um
and any(np.isfinite(recession_cumulative_throat_um)):
417 result_data[
"recession_cumulative_throat_um"] = recession_cumulative_throat_um
419 if V_chamber_m3
and any(np.isfinite(V_chamber_m3))
and len(V_chamber_m3) > 0:
420 result_data[
"V_chamber_m3"] = V_chamber_m3
421 result_data[
"V_chamber_initial_m3"] = float(V_chamber_m3[0])
422 if A_throat_m2
and any(np.isfinite(A_throat_m2))
and len(A_throat_m2) > 0:
423 result_data[
"A_throat_m2"] = A_throat_m2
424 result_data[
"A_throat_initial_m2"] = float(A_throat_m2[0])
429 result_data[
"axial_positions_m"] = axial_positions
430 result_data[
"heat_flux_profiles_w_m2"] = heat_flux_profiles
431 result_data[
"wall_temp_profiles_k"] = wall_temp_profiles
436 logger = logging.getLogger(__name__)
437 logger.info(f
"[TIMESERIES] ablative_axial_positions: {ablative_axial_positions is not None}, length={len(ablative_axial_positions) if ablative_axial_positions else 0}")
438 logger.info(f
"[TIMESERIES] ablative_q_incident_profiles length: {len(ablative_q_incident_profiles)}")
439 if ablative_q_incident_profiles
and len(ablative_q_incident_profiles) > 0:
440 non_empty = sum(1
for p
in ablative_q_incident_profiles
if p
and len(p) > 0)
441 logger.info(f
"[TIMESERIES] ablative_q_incident_profiles non-empty profiles: {non_empty}/{len(ablative_q_incident_profiles)}")
443 if ablative_axial_positions:
444 result_data[
"ablative_axial_positions_m"] = ablative_axial_positions
445 result_data[
"ablative_q_incident_profiles_w_m2"] = ablative_q_incident_profiles
446 result_data[
"ablative_q_conv_profiles_w_m2"] = ablative_q_conv_profiles
447 result_data[
"ablative_q_rad_profiles_w_m2"] = ablative_q_rad_profiles
448 result_data[
"ablative_q_net_profiles_w_m2"] = ablative_q_net_profiles
449 result_data[
"ablative_throat_index"] = ablative_throat_index
450 logger.info(f
"[TIMESERIES] Added ablative heat flux data to result_data")
452 logger.info(f
"[TIMESERIES] NO ablative_axial_positions - skipping ablative data")
455 thrust_arr = np.asarray(results[
"F"], dtype=float) / 1000.0
456 Pc_arr = np.asarray(results[
"Pc"], dtype=float) * PA_TO_PSI
457 Isp_arr = np.asarray(results[
"Isp"], dtype=float)
458 mdot_arr = np.asarray(results[
"mdot_total"], dtype=float)
461 "avg_thrust_kN": float(np.nanmean(thrust_arr)),
462 "peak_thrust_kN": float(np.nanmax(thrust_arr)),
463 "min_thrust_kN": float(np.nanmin(thrust_arr)),
464 "avg_Pc_psi": float(np.nanmean(Pc_arr)),
465 "peak_Pc_psi": float(np.nanmax(Pc_arr)),
466 "avg_Isp_s": float(np.nanmean(Isp_arr)),
467 "total_impulse_kNs": float(np.trapezoid(thrust_arr, times)
if hasattr(np,
"trapezoid")
else np.trapz(thrust_arr, times)),
468 "total_propellant_kg": float(np.trapezoid(mdot_arr, times)
if hasattr(np,
"trapezoid")
else np.trapz(mdot_arr, times)),
469 "burn_time_s": float(times[-1] - times[0])
if len(times) > 1
else 0.0,
477 cea_clamped = [bool(isinstance(dg, dict)
and dg.get(
"cea_clamped"))
for dg
in diagnostics_list]
478 if len(cea_clamped) == n_points
and mask
is not None:
479 cea_clamped = [c
and not bool(m)
for c, m
in zip(cea_clamped, mask)]
481 first = cea_clamped.index(
True)
482 dg = diagnostics_list[first]
483 at = {
"Pc": np.asarray(results[
"Pc"], dtype=float)[first],
"MR": np.asarray(results[
"MR"], dtype=float)[first]}
484 result_data[
"cea_clamped"] = cea_clamped
485 summary[
"cea_clamped_points"] = int(sum(cea_clamped))
486 summary[
"cea_clamped_axes"] = sorted(
487 {a
for c, d
in zip(cea_clamped, diagnostics_list)
if c
for a
in (d.get(
"cea_clamped")
or ())}
489 summary[
"cea_clamped_note"] = (
490 f
"{sum(cea_clamped)} of {n_points} points read CEA off the table edge; "
491 f
"first at t = {float(np.asarray(times)[first]):.2f} s: {cea_off_table(dg, at)}"
498 mdot_O_arr = np.asarray(results[
"mdot_O"], dtype=float)
499 mdot_F_arr = np.asarray(results[
"mdot_F"], dtype=float)
500 if hasattr(np,
"trapezoid"):
501 total_lox_kg = float(np.trapezoid(mdot_O_arr, times))
502 total_fuel_kg = float(np.trapezoid(mdot_F_arr, times))
504 total_lox_kg = float(np.trapz(mdot_O_arr, times))
505 total_fuel_kg = float(np.trapz(mdot_F_arr, times))
507 summary[
"lox_propellant_kg"] = total_lox_kg
508 summary[
"fuel_propellant_kg"] = total_fuel_kg
517 times_arr = np.asarray(times, dtype=float)
519 for side, tank_attr, label, given, mdot, total
in (
520 (
"lox",
"lox_tank",
"LOX", lox_mass_kg, mdot_O_arr, total_lox_kg),
521 (
"fuel",
"fuel_tank",
"Fuel", fuel_mass_kg, mdot_F_arr, total_fuel_kg),
523 if given
is not None and len(given) == len(times):
524 result_data[f
"{side}_mass_remaining_kg"] = to_float_array(given).tolist()
526 mdot_clean = np.nan_to_num(np.asarray(mdot, dtype=float), nan=0.0)
527 cumulative = np.zeros(len(times_arr))
528 for i
in range(1, len(times_arr)):
529 dt = times_arr[i] - times_arr[i - 1]
530 cumulative[i] = cumulative[i - 1] + 0.5 * (mdot_clean[i - 1] + mdot_clean[i]) * dt
534 result_data[f
"{side}_mass_remaining_kg"] = (total - cumulative).tolist()
536 result_data[f
"{side}_mass_remaining_kg"] = np.maximum(loaded - cumulative, 0.0).tolist()
537 summary[f
"{side}_loaded_kg"] = loaded
538 if cumulative[-1] > loaded:
539 t_dry = float(np.interp(loaded, cumulative, times_arr))
540 summary[f
"{side}_short_kg"] = float(cumulative[-1] - loaded)
541 summary[f
"{side}_depletion_time_s"] = t_dry
543 f
"{label} runs dry at t = {t_dry:.2f} s: the curve needs {cumulative[-1]:.2f} kg, "
544 f
"{loaded:.2f} kg is loaded"
547 summary[
"propellant_short_note"] =
"; ".join(short_notes)
558 result_data[
"mdot_O_kg_s"],
559 result_data[
"mdot_F_kg_s"],
566 result_data[
"copv_pressure_psi"] = copv_results[
"copv_pressure_psi"]
569 summary[
"copv_initial_pressure_psi"] = copv_results[
"copv_initial_pressure_psi"]
570 summary[
"copv_initial_mass_kg"] = copv_results[
"copv_initial_mass_kg"]
571 summary[
"copv_min_margin_psi"] = copv_results[
"copv_min_margin_psi"]
572 summary[
"copv_volume_L"] = copv_results[
"copv_volume_L"]
573 summary[
"copv_mode"] = copv_results[
"copv_mode"]
574 summary[
"copv_required_pressure_psi"] = copv_results[
"copv_required_pressure_psi"]
575 summary[
"copv_required_mass_kg"] = copv_results[
"copv_required_mass_kg"]
577 need = getattr(getattr(runner.config,
"design_requirements",
None),
"regulator_min_differential_psi",
None)
578 summary[
"copv_margin_needed_psi"] = float(need)
if need
is not None else None
585 result_data[
"correlation_matrix"] = correlation_data[
"matrix"]
586 result_data[
"correlation_labels"] = correlation_data[
"labels"]
588 return result_data, summary
945 _, runner = session.app_state.snapshot()
949 detail=
"No config loaded. Upload a config file first."
953 if request.blowdown_mode:
954 if request.lox_initial_pressure_psi
is None:
957 detail=
"lox_initial_pressure_psi is required when blowdown_mode=true"
959 if request.fuel_initial_pressure_psi
is None:
962 detail=
"fuel_initial_pressure_psi is required when blowdown_mode=true"
966 if not request.lox_segments
or not request.fuel_segments:
969 detail=
"Segments are required when blowdown_mode=false"
974 times = np.linspace(0, request.duration_s, request.n_points)
976 if request.blowdown_mode:
981 V_lox, m_lox_init, _, V_fuel, m_fuel_init, _ = resolve_blowdown_tank_state(
983 total_propellant_kg=request.total_propellant_kg,
984 of_ratio=request.of_ratio,
985 lox_tank_volume_m3=request.lox_tank_volume_m3,
986 lox_ullage_volume_m3=request.lox_ullage_volume_m3,
987 fuel_tank_volume_m3=request.fuel_tank_volume_m3,
988 fuel_ullage_volume_m3=request.fuel_ullage_volume_m3,
990 except ValueError
as exc:
991 raise HTTPException(status_code=400, detail=str(exc))
from exc
996 def engine_evaluator(P_lox_Pa: float, P_fuel_Pa: float):
1000 res = runner.evaluate(
1005 return res[
"mdot_O"], res[
"mdot_F"]
1011 blowdown_results = simulate_coupled_blowdown(
1013 evaluate_engine_fn=engine_evaluator,
1014 P_lox_initial_Pa=request.lox_initial_pressure_psi * PSI_TO_PA,
1015 P_fuel_initial_Pa=request.fuel_initial_pressure_psi * PSI_TO_PA,
1016 config=runner.config,
1017 R_pressurant=296.803,
1022 n2_Z_csv=N2_Z_LOOKUP_CSV,
1023 total_propellant_kg=request.total_propellant_kg,
1024 of_ratio=request.of_ratio,
1025 lox_tank_volume_m3=request.lox_tank_volume_m3,
1026 lox_ullage_volume_m3=request.lox_ullage_volume_m3,
1027 fuel_tank_volume_m3=request.fuel_tank_volume_m3,
1028 fuel_ullage_volume_m3=request.fuel_ullage_volume_m3,
1032 lox_curve_pa = blowdown_results[
"lox"][
"P_Pa"]
1033 fuel_curve_pa = blowdown_results[
"fuel"][
"P_Pa"]
1034 lox_curve_psi = lox_curve_pa * PA_TO_PSI
1035 fuel_curve_psi = fuel_curve_pa * PA_TO_PSI
1038 lox_mass_kg = blowdown_results[
"lox"][
"m_prop_kg"]
1039 fuel_mass_kg = blowdown_results[
"fuel"][
"m_prop_kg"]
1048 lox_mass_kg=lox_mass_kg,
1049 fuel_mass_kg=fuel_mass_kg,
1052 summary[
"blowdown_initial_lox_mass_kg"] = float(m_lox_init)
1053 summary[
"blowdown_initial_fuel_mass_kg"] = float(m_fuel_init)
1054 summary[
"blowdown_lox_tank_volume_m3"] = float(V_lox)
1055 summary[
"blowdown_fuel_tank_volume_m3"] = float(V_fuel)
1056 summary[
"blowdown_initial_lox_ullage_m3"] = float(V_lox - m_lox_init / float(runner.config.fluids[
"oxidizer"].density))
1057 summary[
"blowdown_initial_fuel_ullage_m3"] = float(V_fuel - m_fuel_init / float(runner.config.fluids[
"fuel"].density))
1068 lox_curve_pa = generate_pressure_curve_from_segments(
1070 n_points=request.n_points, allow_rise=
True, exact_ends=
True,
1072 fuel_curve_pa = generate_pressure_curve_from_segments(
1074 n_points=request.n_points, allow_rise=
True, exact_ends=
True,
1078 lox_curve_psi = lox_curve_pa * PA_TO_PSI
1079 fuel_curve_psi = fuel_curve_pa * PA_TO_PSI
1093 lox_curve_preview=lox_curve_psi.tolist(),
1094 fuel_curve_preview=fuel_curve_psi.tolist(),
1097 except Exception
as e:
1098 raise HTTPException(
1100 detail=f
"Segment-based evaluation failed: {str(e)}"