STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
copv_solve.py
Go to the documentation of this file.
1import numpy as np
2import pandas as pd
3from scipy.interpolate import RegularGridInterpolator
4
5# ----------------------------
6# Z(P,T) utilities (unchanged)
7# ----------------------------
8def load_Z_lookup_table(csv_path):
9 df_z = pd.read_csv(csv_path)
10 T_vals = np.unique(np.sort(df_z["T_K"].values))
11 P_vals = np.unique(np.sort(df_z["P_Pa"].values))
12 pivot = df_z.pivot(index="T_K", columns="P_Pa", values="Z")
13 pivot = pivot.reindex(index=T_vals, columns=P_vals)
14 Z_grid = pivot.values
15 interp = RegularGridInterpolator((T_vals, P_vals), Z_grid,
16 bounds_error=False, fill_value=None)
17 return interp, T_vals, P_vals
18
19def Z_lookup(T_K, P_Pa, interp, default_Z=1.0):
20 T_arr = np.atleast_1d(T_K).astype(float)
21 P_arr = np.atleast_1d(P_Pa).astype(float)
22 if T_arr.size == 1 and P_arr.size > 1:
23 T_arr = np.full_like(P_arr, T_arr[0])
24 elif P_arr.size == 1 and T_arr.size > 1:
25 P_arr = np.full_like(T_arr, P_arr[0])
26 elif T_arr.size != P_arr.size:
27 raise ValueError("Temperature and pressure arrays must have matching sizes.")
28 pts = np.column_stack([T_arr, P_arr])
29 Z_vals = interp(pts)
30 Z_vals = np.where(np.isnan(Z_vals), default_Z, Z_vals)
31 if np.isscalar(T_K) and np.isscalar(P_Pa):
32 return float(Z_vals[0])
33 return Z_vals
34
35# ----------------------------
36# Core solver (updated)
37# ----------------------------
39 df,
40 config,
41 *,
42 n=1.2,
43 T0_K=300.0,
44 Tp_K=300.0,
45 use_real_gas=True,
46 n2_Z_csv="n2_Z_lookup.csv",
47 copv_volume_m3=None,
48 copv_P0_Pa=None,
49 tol=1e-4,
50 max_iter=60
51):
52 """
53 Updated solver that uses known COPV volume (config.press_tank.press_volume)
54 and initial ullage computed from initial propellant mass, prop tank volume, and density.
55
56 Provide either copv_volume_m3 OR copv_P0_Pa (exactly one).
57 """
58
59 assert (copv_volume_m3 is None) ^ (copv_P0_Pa is None), \
60 "Provide exactly one: copv_volume_m3 OR copv_P0_Pa."
61
62 # --- Extract constants from config (adapt field names if your config differs) ---
63 R = float(config.fluids['pressurant'].R) # J/(kg·K) for N2
64 rho_ox = float(config.fluids['oxidizer'].density) # kg/m^3
65
66 # Known COPV physical volume from config (the user said they have this)
67 # prefer explicit argument first, otherwise attempt to read from config
68 if copv_volume_m3 is None:
69 # try a few likely keys
70 if hasattr(config, "press_tank") and hasattr(config.press_tank, "press_volume"):
71 copv_volume_m3 = float(config.press_tank.press_volume)
72 elif hasattr(config, "press_tank") and hasattr(config.press_tank, "volume_m3"):
73 copv_volume_m3 = float(config.press_tank.volume_m3)
74 else:
75 raise KeyError("COPV volume not provided and not found in config.press_tank (press_volume).")
76 copv_volume_m3 = config.press_tank.press_volume
77
78 # Propellant tank geometry and initial prop mass used to compute ullage
79 # Attempt to read initial propellant mass and prop tank volume from config
80 # Adapt these field names if your config uses other keys.
81 try:
82 V_tank_total = float(config.propellant.tank_volume_m3)
83 except Exception:
84 # fallback: try press_tank.tank_volume
85 V_tank_total = float(getattr(config.press_tank, "tank_volume_m3", None) or 0.0)
86 try:
87 m_prop_initial = float(config.lox_tank.mass)
88 except Exception:
89 # fallback: try press_tank.initial_propellant_mass
90 m_prop_initial = float(getattr(config.press_tank, "initial_propellant_mass_kg", None) or 0.0)
91
92 if V_tank_total <= 0 or m_prop_initial <= 0:
93 raise KeyError("Need propellant tank total volume and initial propellant mass in config (e.g., config.propellant.tank_volume_m3 and config.propellant.initial_mass_kg).")
94
95 # initial ullage volume (gas volume) in prop tank at time zero
96 Vg0 = V_tank_total - (m_prop_initial / rho_ox)
97 if Vg0 <= 0:
98 raise ValueError("Computed initial ullage Vg0 <= 0. Check propellant mass, density, or tank volume inputs.")
99
100 # Conversion
101 psi_to_Pa = 6894.757293168
102
103 # --- Read time-series from df ---
104 time = np.asarray(df["time"].values, dtype=float)
105 mdot_o = np.asarray(df["mdot_O (kg/s)"].values, dtype=float)
106 # allow different column name variations for P tank psi
107 if "P_tank_O (psi)" in df.columns:
108 P_tank_psi = np.asarray(df["P_tank_O (psi)"].values, dtype=float)
109 elif "P_tank_O_psi" in df.columns:
110 P_tank_psi = np.asarray(df["P_tank_O_psi"].values, dtype=float)
111 else:
112 raise KeyError("Dataframe missing prop tank pressure column (expected 'P_tank_O (psi)')")
113
114 P_tank_Pa = P_tank_psi * psi_to_Pa
115
116 # Time step handling
117 dt = np.diff(time)
118 if dt.size == 0:
119 raise ValueError("Need multiple time samples.")
120 dt_full = np.concatenate(([dt[0]], dt))
121
122 # Propellant volume displaced per step → ullage growth (start from Vg0)
123 dV_ox = mdot_o * dt_full / rho_ox
124 Vg = Vg0 + np.cumsum(dV_ox)
125
126 # Z lookups
127 if use_real_gas:
128 interp, _, _ = load_Z_lookup_table(n2_Z_csv)
129 Zp = Z_lookup(Tp_K, P_tank_Pa, interp) # prop-tank Z over time
130 # do not set Z0 here; Z0 depends on P0 (unknown). We'll compute Z_H when needed.
131 else:
132 interp = None
133 Zp = np.ones_like(P_tank_Pa)
134
135 # Required prop-tank gas mass to maintain the given pressure trace
136 m_g_req = (P_tank_Pa * Vg) / (Zp * R * Tp_K)
137
138 # Delivered mass over each step (no-backflow assumption)
139 m_g_prev = np.concatenate(([m_g_req[0]], m_g_req[:-1]))
140 dm_deliv = m_g_req - m_g_prev
141 dm_deliv = np.maximum(dm_deliv, 0.0)
142 M_deliv = np.cumsum(dm_deliv)
143 M_total = M_deliv[-1]
144
145 # --- Helper: compute Z_H at given P_H (with fallback) ---
146 def estimate_Z_H(P_H):
147 if not use_real_gas:
148 return 1.0
149 # The interpolator EXTRAPOLATES outside the table (fill_value=None), so the table must cover
150 # COPV pressures: n2_Z_lookup.csv runs to 60 MPa (CoolProp; it stopped at 8.3 MPa before).
151 z = Z_lookup(T0_K, P_H, interp, default_Z=1.0)
152 return z
153
154 # --- HPS pressure from m_H, V_H, and P0 using fixed-point on Z_H ---
155 def compute_PH_from_mH(mH, V_H, P0):
156 """
157 Solve for P_H given mH, V_H, P0, T0_K and polytropic exponent n.
158 This solves: mH = P_H * V_H / (Z_H(P_H) * R * T_H)
159 with T_H = T0 * (P_H/P0)^{(n-1)/n}
160 and Z_H a function of P_H (estimated via estimate_Z_H).
161 We iterate a few times on P_H because Z_H depends on P_H.
162 """
163 # initial guess: assume Z_H = Z_at_P0
164 Z_guess = estimate_Z_H(P0)
165 # compute initial P_H estimate from explicit algebra assuming Z_guess
166 # P_H = ( (mH * Z_guess * R * T0) / (V_H * P0^{(n-1)/n}) )^n
167 prefactor = (mH * Z_guess * R * T0_K) / (V_H * (P0 ** ((n - 1.0) / n)))
168 # protect against negative/zero
169 prefactor = max(prefactor, 1e-20)
170 P_H = prefactor ** n
171
172 # iterate to update Z_H(P_H) then recompute P_H
173 for _ in range(8):
174 Z_new = estimate_Z_H(P_H)
175 if Z_new <= 0:
176 Z_new = Z_guess
177 prefactor = (mH * Z_new * R * T0_K) / (V_H * (P0 ** ((n - 1.0) / n)))
178 prefactor = max(prefactor, 1e-20)
179 P_H_new = prefactor ** n
180 # convergence check relative
181 if abs(P_H_new - P_H) / max(1.0, P_H_new) < 1e-6:
182 P_H = P_H_new
183 break
184 P_H = P_H_new
185 return P_H
186
187 # copv_pressure_trace now uses compute_PH_from_mH
188 def copv_pressure_trace(m0, V_H, P0):
189 mH = m0 - M_deliv
190 mH = np.maximum(mH, 1e-12)
191 PH = np.array([compute_PH_from_mH(mHi, V_H, P0) for mHi in mH])
192 return PH
193
194 # Feasibility solvers (updated to compute Z0 appropriately inside)
195 def solve_for_P0_given_VH(V_H):
196 P0_low = np.max(P_tank_Pa) # must be at least as large as peak prop-tank requirement
197 P0_high = max(P0_low * 3.0, 5.0e7)
198
199 def ok(P0):
200 # compute m0 from P0 and Z at P0
201 Z0_trial = estimate_Z_H(P0)
202 m0 = P0 * V_H / (Z0_trial * R * T0_K)
203 PH = copv_pressure_trace(m0, V_H, P0)
204 return np.min(PH - P_tank_Pa) >= 0.0
205
206 itg = 0
207 while not ok(P0_high) and itg < 30:
208 P0_high *= 1.6
209 itg += 1
210
211 lo, hi = P0_low, P0_high
212 for _ in range(max_iter):
213 mid = 0.5 * (lo + hi)
214 if ok(mid):
215 hi = mid
216 else:
217 lo = mid
218 if (hi - lo) / max(1.0, hi) < tol:
219 break
220
221 P0_star = hi
222 Z0_star = estimate_Z_H(P0_star)
223 m0_star = P0_star * V_H / (Z0_star * R * T0_K)
224 PH_star = copv_pressure_trace(m0_star, V_H, P0_star)
225 return P0_star, m0_star, PH_star
226
227 def solve_for_VH_given_P0(P0):
228 V_low = 1e-5
229 V_high = 0.5
230
231 def ok(VH):
232 Z0_trial = estimate_Z_H(P0)
233 m0 = P0 * VH / (Z0_trial * R * T0_K)
234 PH = copv_pressure_trace(m0, VH, P0)
235 return np.min(PH - P_tank_Pa) >= 0.0
236
237 itg = 0
238 while not ok(V_high) and itg < 30:
239 V_high *= 1.6
240 itg += 1
241
242 lo, hi = V_low, V_high
243 for _ in range(max_iter):
244 mid = 0.5 * (lo + hi)
245 if ok(mid):
246 hi = mid
247 else:
248 lo = mid
249 if (hi - lo) / max(1.0, hi) < tol:
250 break
251
252 V_star = hi
253 Z0_star = estimate_Z_H(P0)
254 m0_star = P0 * V_star / (Z0_star * R * T0_K)
255 PH_star = copv_pressure_trace(m0_star, V_star, P0)
256 return V_star, m0_star, PH_star
257
258 # --- Results bundle ---
259 results = {
260 "time_s": time,
261 "P_tank_Pa": P_tank_Pa,
262 "mdot_o_kg_s": mdot_o,
263 "Vg_m3": Vg,
264 "m_g_req_kg": m_g_req,
265 "dm_delivered_kg": dm_deliv,
266 "M_delivered_kg": M_deliv,
267 "total_delivered_mass_kg": float(M_total),
268 "Zp_trace": Zp,
269 "R_pressurant": R,
270 "T0_K": T0_K,
271 "Tp_K": Tp_K,
272 "n": n
273 }
274
275 if copv_volume_m3 is not None:
276 # user gave a COPV physical volume; solve for P0
277 P0_star, m0_star, PH_star = solve_for_P0_given_VH(copv_volume_m3)
278 results.update({
279 "mode": "sized_P0_given_VH",
280 "copv_volume_m3": float(copv_volume_m3),
281 "P0_Pa": float(P0_star),
282 "m0_kg": float(m0_star),
283 "PH_trace_Pa": PH_star,
284 "min_margin_Pa": float(np.min(PH_star - P_tank_Pa))
285 })
286 else:
287 # user provided P0 and we size VH
288 V_star, m0_star, PH_star = solve_for_VH_given_P0(copv_P0_Pa)
289 results.update({
290 "mode": "sized_VH_given_P0",
291 "P0_Pa": float(copv_P0_Pa),
292 "copv_volume_m3": float(V_star),
293 "m0_kg": float(m0_star),
294 "PH_trace_Pa": PH_star,
295 "min_margin_Pa": float(np.min(PH_star - P_tank_Pa))
296 })
297
298 return results
#define max(a, b)
Definition Arduino.h:43
static bool ok
Definition flight_e2e.c:435
load_Z_lookup_table(csv_path)
Definition copv_solve.py:8
Z_lookup(T_K, P_Pa, interp, default_Z=1.0)
Definition copv_solve.py:19
size_or_check_copv_for_polytropic_N2(df, config, *, n=1.2, T0_K=300.0, Tp_K=300.0, use_real_gas=True, n2_Z_csv="n2_Z_lookup.csv", copv_volume_m3=None, copv_P0_Pa=None, tol=1e-4, max_iter=60)
Definition copv_solve.py:51