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])
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])
46 n2_Z_csv="n2_Z_lookup.csv",
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.
56 Provide either copv_volume_m3 OR copv_P0_Pa (exactly one).
59 assert (copv_volume_m3
is None) ^ (copv_P0_Pa
is None), \
60 "Provide exactly one: copv_volume_m3 OR copv_P0_Pa."
63 R = float(config.fluids[
'pressurant'].R)
64 rho_ox = float(config.fluids[
'oxidizer'].density)
68 if copv_volume_m3
is None:
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)
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
82 V_tank_total = float(config.propellant.tank_volume_m3)
85 V_tank_total = float(getattr(config.press_tank,
"tank_volume_m3",
None)
or 0.0)
87 m_prop_initial = float(config.lox_tank.mass)
90 m_prop_initial = float(getattr(config.press_tank,
"initial_propellant_mass_kg",
None)
or 0.0)
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).")
96 Vg0 = V_tank_total - (m_prop_initial / rho_ox)
98 raise ValueError(
"Computed initial ullage Vg0 <= 0. Check propellant mass, density, or tank volume inputs.")
101 psi_to_Pa = 6894.757293168
104 time = np.asarray(df[
"time"].values, dtype=float)
105 mdot_o = np.asarray(df[
"mdot_O (kg/s)"].values, dtype=float)
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)
112 raise KeyError(
"Dataframe missing prop tank pressure column (expected 'P_tank_O (psi)')")
114 P_tank_Pa = P_tank_psi * psi_to_Pa
119 raise ValueError(
"Need multiple time samples.")
120 dt_full = np.concatenate(([dt[0]], dt))
123 dV_ox = mdot_o * dt_full / rho_ox
124 Vg = Vg0 + np.cumsum(dV_ox)
129 Zp =
Z_lookup(Tp_K, P_tank_Pa, interp)
133 Zp = np.ones_like(P_tank_Pa)
136 m_g_req = (P_tank_Pa * Vg) / (Zp * R * Tp_K)
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]
146 def estimate_Z_H(P_H):
151 z =
Z_lookup(T0_K, P_H, interp, default_Z=1.0)
155 def compute_PH_from_mH(mH, V_H, P0):
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.
164 Z_guess = estimate_Z_H(P0)
167 prefactor = (mH * Z_guess * R * T0_K) / (V_H * (P0 ** ((n - 1.0) / n)))
169 prefactor =
max(prefactor, 1e-20)
174 Z_new = estimate_Z_H(P_H)
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
181 if abs(P_H_new - P_H) /
max(1.0, P_H_new) < 1e-6:
188 def copv_pressure_trace(m0, V_H, P0):
190 mH = np.maximum(mH, 1e-12)
191 PH = np.array([compute_PH_from_mH(mHi, V_H, P0)
for mHi
in mH])
195 def solve_for_P0_given_VH(V_H):
196 P0_low = np.max(P_tank_Pa)
197 P0_high =
max(P0_low * 3.0, 5.0e7)
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
207 while not ok(P0_high)
and itg < 30:
211 lo, hi = P0_low, P0_high
212 for _
in range(max_iter):
213 mid = 0.5 * (lo + hi)
218 if (hi - lo) /
max(1.0, hi) < tol:
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
227 def solve_for_VH_given_P0(P0):
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
238 while not ok(V_high)
and itg < 30:
242 lo, hi = V_low, V_high
243 for _
in range(max_iter):
244 mid = 0.5 * (lo + hi)
249 if (hi - lo) /
max(1.0, hi) < tol:
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
261 "P_tank_Pa": P_tank_Pa,
262 "mdot_o_kg_s": mdot_o,
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),
275 if copv_volume_m3
is not None:
277 P0_star, m0_star, PH_star = solve_for_P0_given_VH(copv_volume_m3)
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))
288 V_star, m0_star, PH_star = solve_for_VH_given_P0(copv_P0_Pa)
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))