120 params: DynamicsParams,
125 Discrete-time dynamics step.
129 x : np.ndarray, shape (N_STATE,)
130 Current state: [P_copv, P_reg, P_u_F, P_u_O, P_d_F, P_d_O, V_u_F, V_u_O, m_gas_copv, m_gas_F, m_gas_O]
131 u : np.ndarray, shape (N_CONTROL,)
132 Control input: [u_F, u_O] in [0, 1]
135 params : DynamicsParams
138 Fuel mass flow rate [kg/s] (from engine physics)
140 Oxidizer mass flow rate [kg/s] (from engine physics)
144 x_next : np.ndarray, shape (N_STATE,)
149 raise ValueError(f
"State vector must have at least 8 elements, got {len(x)}")
152 P_copv = x[IDX_P_COPV]
162 if len(x) > IDX_M_GAS_COPV:
163 m_gas_copv = x[IDX_M_GAS_COPV]
164 m_gas_F = x[IDX_M_GAS_F]
165 m_gas_O = x[IDX_M_GAS_O]
169 T_copv_init = getattr(params,
'T_gas_copv_initial', params.T_gas)
170 T_F_init = getattr(params,
'T_gas_F_initial', params.T_gas)
171 T_O_init = getattr(params,
'T_gas_O_initial', 250.0)
172 m_gas_copv = (P_copv * params.V_copv * params.Z_gas) / (params.R_gas * T_copv_init)
173 m_gas_F = (P_u_F * V_u_F * params.Z_gas) / (params.R_gas * T_F_init)
if V_u_F > 1e-10
else 0.0
174 m_gas_O = (P_u_O * V_u_O * params.Z_gas) / (params.R_gas * T_O_init)
if V_u_O > 1e-10
else 0.0
189 if not getattr(params,
'_temp_initialized',
False):
190 params._T_copv_0 = getattr(params,
'T_gas_copv_initial', params.T_gas)
191 params._T_F_0 = getattr(params,
'T_gas_F_initial', params.T_gas)
192 params._T_O_0 = getattr(params,
'T_gas_O_initial', 250.0)
193 params._m_copv_0 = m_gas_copv
194 params._m_F_0 = m_gas_F
195 params._m_O_0 = m_gas_O
196 params._V_F_0 = V_u_F
197 params._V_O_0 = V_u_O
198 params._temp_initialized =
True
203 use_polytropic = getattr(params,
'use_polytropic',
True)
204 n_poly = getattr(params,
'n_polytropic', 1.2)
208 if params._m_copv_0 > 1e-10
and params.V_copv > 1e-10:
209 rho_copv_0 = params._m_copv_0 / params.V_copv
210 rho_copv = m_gas_copv / params.V_copv
if params.V_copv > 1e-10
else rho_copv_0
211 if rho_copv_0 > 1e-10:
212 T_copv = params._T_copv_0 * (rho_copv / rho_copv_0) ** (n_poly - 1.0)
213 T_copv =
max(200.0,
min(400.0, T_copv))
215 T_copv = params._T_copv_0
217 T_copv = params._T_copv_0
220 if params._m_F_0 > 1e-10
and params._V_F_0 > 1e-10:
221 rho_F_0 = params._m_F_0 / params._V_F_0
222 rho_F = m_gas_F / V_u_F
if V_u_F > 1e-10
else rho_F_0
224 T_gas_F = params._T_F_0 * (rho_F / rho_F_0) ** (n_poly - 1.0)
225 T_gas_F =
max(200.0,
min(400.0, T_gas_F))
227 T_gas_F = params._T_F_0
229 T_gas_F = params._T_F_0
232 if params._m_O_0 > 1e-10
and params._V_O_0 > 1e-10:
233 rho_O_0 = params._m_O_0 / params._V_O_0
234 rho_O = m_gas_O / V_u_O
if V_u_O > 1e-10
else rho_O_0
236 T_gas_O = params._T_O_0 * (rho_O / rho_O_0) ** (n_poly - 1.0)
237 T_gas_O =
max(200.0,
min(400.0, T_gas_O))
239 T_gas_O = params._T_O_0
241 T_gas_O = params._T_O_0
244 T_copv = params._T_copv_0
245 T_gas_F = params._T_F_0
246 T_gas_O = params._T_O_0
249 u_F = np.clip(u[IDX_U_F], 0.0, 1.0)
250 u_O = np.clip(u[IDX_U_O], 0.0, 1.0)
259 gamma_gas = params.gamma_gas
260 Cd_regulator = params.Cd_regulator
261 Cd_valve = params.Cd_valve
263 A_regulator = params.A_regulator
264 A_valve_F = params.A_valve_F
265 A_valve_O = params.A_valve_O
271 u_total =
max(u_F, u_O)
276 if params.reg_setpoint
is not None:
278 reg_output_pressure = params.reg_setpoint
279 can_supply = P_copv >= params.reg_setpoint
282 reg_output_pressure =
min(P_copv, P_reg)
283 can_supply = P_copv > P_reg
287 if u_total > 1e-6
and can_supply:
291 if params.reg_setpoint
is not None:
292 P_downstream = params.reg_setpoint
298 pr_crit = (2.0 / (gamma_gas + 1.0)) ** (gamma_gas / (gamma_gas - 1.0))
299 pr_ratio = P_downstream / P_copv
if P_copv > 0
else 0.0
301 if pr_ratio < pr_crit:
304 C_star = np.sqrt(gamma_gas * (2.0 / (gamma_gas + 1.0)) ** ((gamma_gas + 1.0) / (gamma_gas - 1.0)))
305 mdot_gas_to_reg_max = Cd_regulator * A_regulator * P_copv / np.sqrt(params.R_gas * params.T_gas) * C_star
310 term1 = (2.0 * gamma_gas) / ((gamma_gas - 1.0) * params.R_gas * T_copv)
311 term2 = pr_ratio ** (2.0 / gamma_gas) - pr_ratio ** ((gamma_gas + 1.0) / gamma_gas)
312 mdot_gas_to_reg_max = Cd_regulator * A_regulator * P_copv * np.sqrt(term1 * term2)
316 mdot_gas_to_reg = mdot_gas_to_reg_max * u_total
319 mdot_gas_to_reg =
max(0.0,
min(mdot_gas_to_reg, 0.2))
321 mdot_gas_to_reg = 0.0
369 if params.reg_setpoint
is not None:
370 if P_copv >= params.reg_setpoint:
371 P_reg_effective = params.reg_setpoint
373 P_reg_effective = P_copv
375 P_reg_effective = P_reg
378 if u_F > 1e-6
and P_reg_effective > P_u_F:
380 pr_ratio_F = P_u_F / P_reg_effective
if P_reg_effective > 0
else 0.0
381 pr_crit = (2.0 / (gamma_gas + 1.0)) ** (gamma_gas / (gamma_gas - 1.0))
383 if pr_ratio_F < pr_crit:
387 C_star = np.sqrt(gamma_gas * (2.0 / (gamma_gas + 1.0)) ** ((gamma_gas + 1.0) / (gamma_gas - 1.0)))
388 mdot_gas_F_max = Cd_valve * A_valve_F * P_reg_effective / np.sqrt(params.R_gas * T_reg_flow) * C_star
392 term1 = (2.0 * gamma_gas) / ((gamma_gas - 1.0) * params.R_gas * T_reg_flow)
393 term2 = pr_ratio_F ** (2.0 / gamma_gas) - pr_ratio_F ** ((gamma_gas + 1.0) / gamma_gas)
394 mdot_gas_F_max = Cd_valve * A_valve_F * P_reg_effective * np.sqrt(term1 * term2)
398 mdot_gas_F = mdot_gas_F_max * u_F
401 if u_total > 1e-6
and mdot_gas_to_reg > 0:
403 mdot_gas_F =
min(mdot_gas_F, mdot_gas_to_reg * u_F / u_total)
405 mdot_gas_F =
max(mdot_gas_F, 0.0)
410 if u_O > 1e-6
and P_reg_effective > P_u_O:
412 pr_ratio_O = P_u_O / P_reg_effective
if P_reg_effective > 0
else 0.0
413 pr_crit = (2.0 / (gamma_gas + 1.0)) ** (gamma_gas / (gamma_gas - 1.0))
415 if pr_ratio_O < pr_crit:
418 C_star = np.sqrt(gamma_gas * (2.0 / (gamma_gas + 1.0)) ** ((gamma_gas + 1.0) / (gamma_gas - 1.0)))
419 mdot_gas_O_max = Cd_valve * A_valve_O * P_reg_effective / np.sqrt(params.R_gas * T_reg_flow) * C_star
423 term1 = (2.0 * gamma_gas) / ((gamma_gas - 1.0) * params.R_gas * T_reg_flow)
424 term2 = pr_ratio_O ** (2.0 / gamma_gas) - pr_ratio_O ** ((gamma_gas + 1.0) / gamma_gas)
425 mdot_gas_O_max = Cd_valve * A_valve_O * P_reg_effective * np.sqrt(term1 * term2)
429 mdot_gas_O = mdot_gas_O_max * u_O
432 if u_total > 1e-6
and mdot_gas_to_reg > 0:
434 mdot_gas_O =
min(mdot_gas_O, mdot_gas_to_reg * u_O / u_total)
436 mdot_gas_O =
max(mdot_gas_O, 0.0)
443 mdot_gas_loss = (params.copv_loss * params.V_copv) / (params.R_gas * params.T_gas)
444 mdot_gas_loss =
max(0.0,
min(mdot_gas_loss, 0.01))
449 mdot_gas_total = mdot_gas_to_reg + mdot_gas_loss
452 mdot_gas_to_tanks = mdot_gas_F + mdot_gas_O
453 if mdot_gas_to_tanks > mdot_gas_to_reg + 1e-6:
455 if mdot_gas_to_reg > 1e-6:
456 scale_factor = mdot_gas_to_reg / mdot_gas_to_tanks
457 mdot_gas_F = mdot_gas_F * scale_factor
458 mdot_gas_O = mdot_gas_O * scale_factor
464 m_gas_copv_next = m_gas_copv - dt * mdot_gas_total
465 m_gas_copv_next =
max(m_gas_copv_next, 0.0)
472 if params.V_copv > 1e-10:
474 if use_polytropic
and params._m_copv_0 > 1e-10:
475 rho_copv_next = m_gas_copv_next / params.V_copv
476 rho_copv_0 = params._m_copv_0 / params.V_copv
477 if rho_copv_0 > 1e-10:
478 T_copv_next = params._T_copv_0 * (rho_copv_next / rho_copv_0) ** (n_poly - 1.0)
479 T_copv_next =
max(200.0,
min(400.0, T_copv_next))
485 P_copv_next = (m_gas_copv_next * params.R_gas * T_copv_next) / (params.V_copv * params.Z_gas)
488 P_copv_next =
max(P_copv_next, 0.0)
500 if params.reg_setpoint
is not None:
502 if P_copv >= params.reg_setpoint:
504 base_pressure = params.reg_setpoint
515 oscillation_amplitude = 0.06 * params.reg_setpoint * u_total
519 control_sum = u_F + u_O
520 oscillation = oscillation_amplitude * np.sin(control_sum * np.pi * 2.0)
521 P_reg_next = base_pressure + oscillation
523 P_reg_next =
max(base_pressure * 0.9,
min(base_pressure * 1.1, P_reg_next))
525 P_reg_next = base_pressure
532 P_reg_next = params.reg_ratio * P_copv
534 P_reg_next =
min(P_reg_next, P_copv)
555 mdot_F_valid =
max(0.0, mdot_F)
if np.isfinite(mdot_F)
else 0.0
556 mdot_O_valid =
max(0.0, mdot_O)
if np.isfinite(mdot_O)
else 0.0
576 mdot_scale_F = np.sqrt(
max(P_u_F, 1e5) / P_ref_F)
577 mdot_scale_O = np.sqrt(
max(P_u_O, 1e5) / P_ref_O)
581 mdot_scale_F =
max(0.1,
min(2.0, mdot_scale_F))
582 mdot_scale_O =
max(0.1,
min(2.0, mdot_scale_O))
588 mdot_F_pressure_dependent = mdot_F_valid * mdot_scale_F
589 mdot_O_pressure_dependent = mdot_O_valid * mdot_scale_O
601 blowdown_factor = 1.5
602 mdot_F_pressure_dependent = mdot_F_pressure_dependent * blowdown_factor
603 mdot_O_pressure_dependent = mdot_O_pressure_dependent * blowdown_factor
617 V_u_F_next = V_u_F + dt * mdot_F_pressure_dependent / params.rho_F
618 V_u_O_next = V_u_O + dt * mdot_O_pressure_dependent / params.rho_O
621 V_u_F_next =
max(V_u_F_next, 0.0)
622 V_u_O_next =
max(V_u_O_next, 0.0)
627 if mdot_F_pressure_dependent > 1e-6
and V_u_F_next <= V_u_F:
629 V_u_F_next = V_u_F + dt * mdot_F_pressure_dependent / params.rho_F
630 if mdot_O_pressure_dependent > 1e-6
and V_u_O_next <= V_u_O:
632 V_u_O_next = V_u_O + dt * mdot_O_pressure_dependent / params.rho_O
653 m_gas_F_next = m_gas_F + dt * mdot_gas_F
654 m_gas_F_next =
max(m_gas_F_next, 0.0)
676 if V_u_F_next > 1e-10:
678 if use_polytropic
and params._m_F_0 > 1e-10
and params._V_F_0 > 1e-10:
679 rho_F_next = m_gas_F_next / V_u_F_next
680 rho_F_0 = params._m_F_0 / params._V_F_0
682 T_gas_F_next = params._T_F_0 * (rho_F_next / rho_F_0) ** (n_poly - 1.0)
683 T_gas_F_next =
max(200.0,
min(400.0, T_gas_F_next))
685 T_gas_F_next = T_gas_F
687 T_gas_F_next = T_gas_F
693 P_u_F_next = (m_gas_F_next * params.R_gas * T_gas_F_next) / (V_u_F_next * params.Z_gas)
700 if V_u_F_next > V_u_F
and mdot_F_pressure_dependent > 1e-6:
709 P_u_F_next =
max(P_u_F_next, 0.0)
714 m_gas_O_next = m_gas_O + dt * mdot_gas_O
715 m_gas_O_next =
max(m_gas_O_next, 0.0)
727 if V_u_O_next > 1e-10:
729 if use_polytropic
and params._m_O_0 > 1e-10
and params._V_O_0 > 1e-10:
730 rho_O_next = m_gas_O_next / V_u_O_next
731 rho_O_0 = params._m_O_0 / params._V_O_0
733 T_gas_O_next = params._T_O_0 * (rho_O_next / rho_O_0) ** (n_poly - 1.0)
734 T_gas_O_next =
max(200.0,
min(400.0, T_gas_O_next))
736 T_gas_O_next = T_gas_O
738 T_gas_O_next = T_gas_O
742 P_u_O_next = (m_gas_O_next * params.R_gas * T_gas_O_next) / (V_u_O_next * params.Z_gas)
749 if V_u_O_next > V_u_O
and mdot_O_pressure_dependent > 1e-6:
757 P_u_O_next =
max(P_u_O_next, 0.0)
769 tau_line_F_effective = params.tau_line_F * 2.0
770 tau_line_O_effective = params.tau_line_O * 2.0
772 P_d_F_next = P_d_F + dt * (P_u_F - P_d_F) / tau_line_F_effective
773 P_d_O_next = P_d_O + dt * (P_u_O - P_d_O) / tau_line_O_effective
791 if len(x_next) != N_STATE:
792 raise ValueError(f
"Output state vector must have {N_STATE} elements, got {len(x_next)}")