121 backend: ExecutionBackend = ExecutionBackend.PWM,
124 Execute one control step.
129 Current sensor measurements
131 Current navigation state
133 Control command (thrust desired or altitude goal)
134 backend : ExecutionBackend
135 Actuation backend (PWM or BINARY)
139 actuation_cmd : ActuationCommand
140 Actuation command for solenoids
142 Additional diagnostics (DDP solution, reference, etc.)
148 ref = build_reference(
153 horizon_N=self.
cfg.N,
173 eng_est_prelim =
None
178 meas.P_u_fuel, meas.P_u_ox
182 np.isfinite(eng_est_prelim.F)
and eng_est_prelim.F >= 0
and
183 np.isfinite(eng_est_prelim.MR)
and eng_est_prelim.MR > 0
189 F_ref_current = ref.F_ref[0]
if len(ref.F_ref) > 0
else 0.0
190 MR_ref_current = ref.MR_ref[0]
if len(ref.MR_ref) > 0
else 0.0
191 has_nonzero_ref = (np.isfinite(F_ref_current)
and F_ref_current > 0)
or \
192 (np.isfinite(MR_ref_current)
and MR_ref_current > 0)
194 if has_nonzero_ref
and not engine_valid:
200 f
"Controller step {self.tick}: Non-zero reference (F_ref={F_ref_current:.1f} N, "
201 f
"MR_ref={MR_ref_current:.3f}) but engine estimate is invalid. "
202 f
"Pressures: P_u_F={meas.P_u_fuel/1e6:.2f} MPa, P_u_O={meas.P_u_ox/1e6:.2f} MPa. "
203 f
"DDP will use penalty-based optimization.",
207 w_bar = get_w_bar_array(self.
state)
212 x[IDX_P_U_F], x[IDX_P_U_O], F_ref_current, MR_ref_current
214 u_seq = np.tile(u_relaxed, (self.
cfg.N, 1))
215 from .ddp_solver
import DDPSolution
218 x_seq=np.tile(x, (self.
cfg.N + 1, 1)),
223 constraint_violations=[{}] * self.
cfg.N,
224 diagnostics={
"source":
"policy_lut"},
228 solution = solve_ddp(
230 u_seq_init=u_seq_init,
237 use_robustification=
True,
239 u_relaxed = solution.u_seq[0].copy()
243 eng_est_current =
None
247 meas.P_u_fuel, meas.P_u_ox
252 F_ref_current = ref.F_ref[0]
if len(ref.F_ref) > 0
else 0.0
253 if eng_est_current
is not None and np.isfinite(eng_est_current.F)
and F_ref_current > 0:
254 F_error = eng_est_current.F - F_ref_current
256 if F_error < -500.0
and np.max(u_relaxed) < 0.3:
258 u_relaxed = np.clip(u_relaxed + 0.2, 0.0, 1.0)
261 actuation_cmd = compute_actuation(
270 u_safe = filter_action(
276 F_ref=ref.F_ref[0]
if len(ref.F_ref) > 0
else None,
277 MR_ref=ref.MR_ref[0]
if len(ref.MR_ref) > 0
else None,
283 if not np.allclose(u_safe, u_relaxed):
284 actuation_cmd = compute_actuation(
293 update_state_dwell_timers(self.
state, actuation_cmd)
305 meas.P_u_fuel, meas.P_u_ox
312 if eng_est
is not None:
313 constraints = constraint_values(x, eng_est, self.
cfg)
316 if self.
logger is not None:
320 timestamp=meas.timestamp
if hasattr(meas,
'timestamp')
else self.
tick * self.
cfg.dt,
323 constraints=constraints,
324 u_proposed=u_relaxed,
326 actuation_cmd=actuation_cmd,
328 F_ref=ref.F_ref[0]
if len(ref.F_ref) > 0
else None,
329 MR_ref=ref.MR_ref[0]
if len(ref.MR_ref) > 0
else None,
338 "u_relaxed": u_relaxed.copy(),
339 "u_safe": u_safe.copy(),
340 "solution": solution,
342 "w_bar": w_bar.copy(),
343 "constraint_violations": solution.constraint_violations[0]
if solution.constraint_violations
else {},
345 "F_hat": float(eng_est.F)
if eng_est
is not None and np.isfinite(eng_est.F)
else 0.0,
346 "MR_hat": float(eng_est.MR)
if eng_est
is not None and np.isfinite(eng_est.MR)
else 0.0,
347 "P_ch": float(eng_est.P_ch)
if eng_est
is not None and np.isfinite(eng_est.P_ch)
else 0.0,
348 "F_ref": float(ref.F_ref[0])
if len(ref.F_ref) > 0
else 0.0,
349 "MR_ref": float(ref.MR_ref[0])
if len(ref.MR_ref) > 0
else 0.0,
352 return actuation_cmd, diagnostics
356 Build full state vector from measurements and internal ullage volumes.
365 x : np.ndarray, shape (N_STATE,)
366 Full state vector including gas masses
368 from .dynamics
import IDX_M_GAS_COPV, IDX_M_GAS_F, IDX_M_GAS_O
370 x = np.zeros(N_STATE, dtype=np.float64)
372 x[IDX_P_COPV] = meas.P_copv
373 x[IDX_P_REG] = meas.P_reg
374 x[IDX_P_U_F] = meas.P_u_fuel
375 x[IDX_P_U_O] = meas.P_u_ox
376 x[IDX_P_D_F] = meas.P_d_fuel
377 x[IDX_P_D_O] = meas.P_d_ox
378 x[IDX_V_U_F] = self.
V_u_F
379 x[IDX_V_U_O] = self.
V_u_O
388 x[IDX_M_GAS_COPV] = (meas.P_copv * V_copv * Z_gas) / (R_gas * T_gas)
389 x[IDX_M_GAS_F] = (meas.P_u_fuel * self.
V_u_F * Z_gas) / (R_gas * T_gas)
if self.
V_u_F > 1e-10
else 0.0
390 x[IDX_M_GAS_O] = (meas.P_u_ox * self.
V_u_O * Z_gas) / (R_gas * T_gas)
if self.
V_u_O > 1e-10
else 0.0