42 u_seq_init: np.ndarray,
45 cfg: ControllerConfig,
46 dynamics_params: DynamicsParams,
47 engine_wrapper: EngineWrapper,
48 w_bar: Optional[np.ndarray] =
None,
49 alpha_init: float = 2.0,
50 alpha_min: float = 1e-5,
51 reg_init: float = 1e-4,
52 reg_factor: float = 5.0,
53 use_robustification: bool =
True,
54 gamma_robust: float = 1.0,
57 Solve finite-horizon DDP (iLQR-style) for robust control.
61 x0 : np.ndarray, shape (N_STATE,)
63 u_seq_init : np.ndarray, shape (N, N_CONTROL)
64 Initial control sequence
65 F_ref : np.ndarray, shape (N,)
66 Reference thrust sequence [N]
67 MR_ref : np.ndarray, shape (N,)
68 Reference mixture ratio sequence
69 cfg : ControllerConfig
70 Controller configuration
71 dynamics_params : DynamicsParams
73 engine_wrapper : EngineWrapper
74 Engine wrapper for mass flow estimation
75 w_bar : np.ndarray, optional, shape (N_STATE,)
76 Residual bounds for robustification (if None, uses zeros)
78 Initial line search step size
80 Minimum line search step size
82 Initial Levenberg-Marquardt regularization
84 Regularization increase/decrease factor
85 use_robustification : bool
86 Whether to add robustification term to value function
88 Robustification weight (for diag(gamma*w_bar^2) term)
92 solution : DDPSolution
93 DDP solution with optimal control sequence and trajectory
99 w_bar = np.zeros(N_STATE, dtype=np.float64)
102 if u_seq_init.shape != (N, N_CONTROL):
103 raise ValueError(f
"u_seq_init must have shape ({N}, {N_CONTROL}), got {u_seq_init.shape}")
104 if F_ref.shape != (N,):
105 raise ValueError(f
"F_ref must have shape ({N},), got {F_ref.shape}")
106 if MR_ref.shape != (N,):
107 raise ValueError(f
"MR_ref must have shape ({N},), got {MR_ref.shape}")
110 u_seq = u_seq_init.copy()
111 best_u_seq = u_seq.copy()
112 best_objective = float(
'inf')
117 best_eng_estimates =
None
118 best_constraint_violations =
None
121 constraint_violations_all = []
122 eng_estimates_all = []
125 for iteration
in range(cfg.max_iterations):
127 x_seq, eng_estimates, objective, constraint_violations =
forward_rollout(
128 x0, u_seq, F_ref, MR_ref, cfg, dynamics_params, engine_wrapper, dt
131 eng_estimates_all.append(eng_estimates)
132 constraint_violations_all.append(constraint_violations)
135 if objective < best_objective
or best_x_seq
is None:
136 best_objective = objective
137 best_u_seq = u_seq.copy()
138 best_x_seq = x_seq.copy()
139 best_eng_estimates = eng_estimates.copy()
140 best_constraint_violations = constraint_violations.copy()
144 improvement = abs(objective - prev_objective)
145 if improvement < cfg.convergence_tol:
151 prev_objective = objective
155 x_seq, u_seq, eng_estimates, F_ref, MR_ref, cfg, dynamics_params,
156 engine_wrapper, dt, reg, w_bar, use_robustification, gamma_robust
161 x0, u_seq, k_seq, K_seq, x_seq, F_ref, MR_ref, cfg, dynamics_params,
162 engine_wrapper, dt, alpha_init, alpha_min
168 cost_improvement = objective - cost_new
169 if cost_new < objective
or cost_improvement > -0.01 * abs(objective):
171 reg =
max(reg / reg_factor, reg_init)
179 u_seq = u_seq + 0.01 * (np.random.rand(*u_seq.shape) - 0.5)
180 u_seq = np.clip(u_seq, 0.0, 1.0)
184 if best_x_seq
is None:
186 best_x_seq, best_eng_estimates, best_objective, best_constraint_violations =
forward_rollout(
187 x0, best_u_seq, F_ref, MR_ref, cfg, dynamics_params, engine_wrapper, dt
194 eng_estimates=best_eng_estimates,
195 objective=best_objective,
196 iterations=iteration + 1,
198 constraint_violations=best_constraint_violations,
200 "final_regularization": reg,
201 "final_alpha": alpha
if 'alpha' in locals()
else alpha_init,
211 cfg: ControllerConfig,
212 params: DynamicsParams,
213 engine_wrapper: EngineWrapper,
215) -> Tuple[np.ndarray, List[EngineEstimate], float, List[Dict[str, float]]]:
217 Forward rollout: compute trajectory with current control sequence.
221 x_seq : np.ndarray, shape (N+1, N_STATE)
223 eng_estimates : List[EngineEstimate]
224 Engine estimates per step
227 constraint_violations : List[Dict[str, float]]
228 Constraint violations per step
231 x_seq = np.zeros((N + 1, N_STATE), dtype=np.float64)
235 constraint_violations = []
245 eng_est = engine_wrapper.estimate_from_pressures(
249 eng_estimates.append(eng_est)
252 constraints = constraint_values(x_k, eng_est, cfg)
253 constraint_violations.append(constraints)
257 x_k, u_seq[k], eng_est, F_ref[k], MR_ref[k], cfg, constraints, dt
262 mdot_F = eng_est.mdot_F
if np.isfinite(eng_est.mdot_F)
else 0.0
263 mdot_O = eng_est.mdot_O
if np.isfinite(eng_est.mdot_O)
else 0.0
265 x_seq[k + 1] = step(x_k, u_seq[k], dt, params, mdot_F, mdot_O)
267 return x_seq, eng_estimates, total_cost, constraint_violations
273 eng_est: EngineEstimate,
276 cfg: ControllerConfig,
277 constraints: Dict[str, float],
281 Compute running cost at one time step.
283 Cost = qF*(F - F_ref)^2 + qMR*(MR - MR_ref)^2 + qGas*(P_copv_drop) + qSwitch*||u - u_prev||^2
284 + constraint_penalties + invalid_estimate_penalties
289 F_valid = np.isfinite(eng_est.F)
and eng_est.F >= 0
290 MR_valid = np.isfinite(eng_est.MR)
and eng_est.MR > 0
291 estimate_valid = F_valid
and MR_valid
294 if F_valid
and np.isfinite(F_ref):
295 F_error = eng_est.F - F_ref
298 F_error_normalized = F_error /
max(abs(F_ref), 1.0)
301 cost += cfg.qF * F_error_normalized ** 2 * 2.0
303 cost += cfg.qF * F_error_normalized ** 2
304 elif not F_valid
and np.isfinite(F_ref)
and F_ref > 0:
307 invalid_penalty = 1e4 * (1.0 + F_ref / 1000.0)
308 cost += invalid_penalty
311 if MR_valid
and np.isfinite(MR_ref):
312 MR_error = eng_est.MR - MR_ref
314 MR_error_normalized = MR_error /
max(abs(MR_ref), 1.0)
315 cost += cfg.qMR * MR_error_normalized ** 2
316 elif not MR_valid
and np.isfinite(MR_ref)
and MR_ref > 0:
318 invalid_penalty = 1e4 * (1.0 + MR_ref)
319 cost += invalid_penalty
322 if not estimate_valid:
323 if (np.isfinite(F_ref)
and F_ref > 0)
or (np.isfinite(MR_ref)
and MR_ref > 0):
334 cost += cfg.qGas * P_copv_drop
342 constraint_penalty_weight = 1e4
344 "copv_min",
"ullage_max_F",
"ullage_max_O",
346 "injector_stiffness_F",
"injector_stiffness_O",
349 for key
in hard_constraints:
350 if key
in constraints:
351 violation = constraints[key]
354 violation_normalized = violation /
max(abs(violation), 1.0)
355 cost += constraint_penalty_weight * violation_normalized ** 2
363 eng_estimates: List[EngineEstimate],
366 cfg: ControllerConfig,
367 params: DynamicsParams,
368 engine_wrapper: EngineWrapper,
372 use_robustification: bool,
374) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
376 Backward pass: compute feedforward k and feedback K gains.
380 k_seq : np.ndarray, shape (N, N_CONTROL)
382 K_seq : np.ndarray, shape (N, N_CONTROL, N_STATE)
384 Vx : np.ndarray, shape (N_STATE,)
385 Value function gradient at initial state
386 Vxx : np.ndarray, shape (N_STATE, N_STATE)
387 Value function Hessian at initial state
392 Vx = np.zeros(N_STATE, dtype=np.float64)
393 Vxx = np.zeros((N_STATE, N_STATE), dtype=np.float64)
396 k_seq = np.zeros((N, N_CONTROL), dtype=np.float64)
397 K_seq = np.zeros((N, N_CONTROL, N_STATE), dtype=np.float64)
400 for k
in range(N - 1, -1, -1):
403 eng_est = eng_estimates[k]
406 mdot_F = eng_est.mdot_F
if np.isfinite(eng_est.mdot_F)
else 0.0
407 mdot_O = eng_est.mdot_O
if np.isfinite(eng_est.mdot_O)
else 0.0
410 A_k, B_k = linearize(x_k, u_k, dt, params, mdot_F, mdot_O)
414 x_k, u_k, eng_est, F_ref[k], MR_ref[k], cfg, dt, engine_wrapper
420 Qxx = lxx + A_k.T @ Vxx @ A_k
421 Quu = luu + B_k.T @ Vxx @ B_k
422 Qux = lux + B_k.T @ Vxx @ A_k
425 if use_robustification:
427 Qxx += gamma_robust * np.diag(w_bar ** 2)
429 Quu += gamma_robust * np.diag(np.mean(w_bar ** 2) * np.ones(N_CONTROL))
433 min_reg =
max(reg, 1e-6)
434 Quu_reg = Quu + min_reg * np.eye(N_CONTROL)
438 max_cholesky_attempts = 5
439 cholesky_reg = min_reg
440 for attempt
in range(max_cholesky_attempts):
443 np.linalg.cholesky(Quu_reg)
445 except np.linalg.LinAlgError:
448 Quu_reg = Quu + cholesky_reg * np.eye(N_CONTROL)
452 cond_num = np.linalg.cond(Quu_reg)
455 extra_reg =
max(cholesky_reg * 0.1, 1e-4)
456 Quu_reg += extra_reg * np.eye(N_CONTROL)
459 Quu_reg += 1e-4 * np.eye(N_CONTROL)
465 Quu_inv = np.linalg.inv(Quu_reg)
468 except np.linalg.LinAlgError:
470 Quu_inv = np.linalg.pinv(Quu_reg)
477 k_k = k_k * gain_scale
478 K_k = K_k * gain_scale
484 Vx = Qx + K_k.T @ Quu_reg @ k_k + K_k.T @ Qu + Qux.T @ k_k
485 Vxx = Qxx + K_k.T @ Quu_reg @ K_k + K_k.T @ Qux + Qux.T @ K_k
488 Vxx = (Vxx + Vxx.T) / 2
493 np.linalg.cholesky(Vxx + 1e-8 * np.eye(N_STATE))
494 except np.linalg.LinAlgError:
498 eigenvals, eigenvecs = np.linalg.eigh(Vxx)
499 eigenvals = np.maximum(eigenvals, 0.0)
500 Vxx = eigenvecs @ np.diag(eigenvals) @ eigenvecs.T
501 except np.linalg.LinAlgError:
503 Vxx = Vxx + 1e-6 * np.eye(N_STATE)
505 np.fill_diagonal(Vxx, np.maximum(np.diag(Vxx), 1e-8))
507 return k_seq, K_seq, Vx, Vxx
513 eng_est: EngineEstimate,
516 cfg: ControllerConfig,
518 engine_wrapper: Optional[EngineWrapper] =
None,
519) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
521 Compute cost derivatives via finite differences.
525 engine_wrapper : EngineWrapper, optional
526 Engine wrapper for re-estimating engine performance when state changes.
527 If None, uses approximation (assumes eng_est doesn't change).
531 lx : np.ndarray, shape (N_STATE,)
532 Cost gradient w.r.t. state
533 lu : np.ndarray, shape (N_CONTROL,)
534 Cost gradient w.r.t. control
535 lxx : np.ndarray, shape (N_STATE, N_STATE)
536 Cost Hessian w.r.t. state
537 luu : np.ndarray, shape (N_CONTROL, N_CONTROL)
538 Cost Hessian w.r.t. control
539 lux : np.ndarray, shape (N_CONTROL, N_STATE)
540 Cost cross-derivative
542 from .constraints
import constraint_values
547 constraints_nom = constraint_values(x, eng_est, cfg)
548 cost_nom =
running_cost(x, u, eng_est, F_ref, MR_ref, cfg, constraints_nom, dt)
551 lx = np.zeros(N_STATE, dtype=np.float64)
552 for i
in range(N_STATE):
558 if engine_wrapper
is not None and i
in [IDX_P_U_F, IDX_P_U_O]:
559 eng_est_pert = engine_wrapper.estimate_from_pressures(
560 x_pert[IDX_P_U_F], x_pert[IDX_P_U_O]
563 eng_est_pert = eng_est
565 constraints_pert = constraint_values(x_pert, eng_est_pert, cfg)
566 cost_pert =
running_cost(x_pert, u, eng_est_pert, F_ref, MR_ref, cfg, constraints_pert, dt)
568 lx[i] = (cost_pert - cost_nom) / eps
575 lu = np.zeros(N_CONTROL, dtype=np.float64)
576 for i
in range(N_CONTROL):
579 u_pert[i] = np.clip(u_pert[i], 0.0, 1.0)
581 constraints_pert = constraint_values(x, eng_est, cfg)
582 cost_pert =
running_cost(x, u_pert, eng_est, F_ref, MR_ref, cfg, constraints_pert, dt)
584 lu[i] = (cost_pert - cost_nom) / eps
587 lxx = np.zeros((N_STATE, N_STATE), dtype=np.float64)
589 for i
in range(N_STATE):
597 if engine_wrapper
is not None and i
in [IDX_P_U_F, IDX_P_U_O]:
598 eng_est_pert1 = engine_wrapper.estimate_from_pressures(
599 x_pert1[IDX_P_U_F], x_pert1[IDX_P_U_O]
601 eng_est_pert2 = engine_wrapper.estimate_from_pressures(
602 x_pert2[IDX_P_U_F], x_pert2[IDX_P_U_O]
605 eng_est_pert1 = eng_est
606 eng_est_pert2 = eng_est
608 constraints_pert1 = constraint_values(x_pert1, eng_est_pert1, cfg)
609 constraints_pert2 = constraint_values(x_pert2, eng_est_pert2, cfg)
611 cost_pert1 =
running_cost(x_pert1, u, eng_est_pert1, F_ref, MR_ref, cfg, constraints_pert1, dt)
612 cost_pert2 =
running_cost(x_pert2, u, eng_est_pert2, F_ref, MR_ref, cfg, constraints_pert2, dt)
614 lxx[i, i] = (cost_pert1 - 2 * cost_nom + cost_pert2) / (eps ** 2)
617 luu = np.zeros((N_CONTROL, N_CONTROL), dtype=np.float64)
618 for i
in range(N_CONTROL):
620 u_pert1[i] = np.clip(u_pert1[i] + eps, 0.0, 1.0)
622 u_pert2[i] = np.clip(u_pert2[i] - eps, 0.0, 1.0)
624 constraints_pert1 = constraint_values(x, eng_est, cfg)
625 constraints_pert2 = constraint_values(x, eng_est, cfg)
627 cost_pert1 =
running_cost(x, u_pert1, eng_est, F_ref, MR_ref, cfg, constraints_pert1, dt)
628 cost_pert2 =
running_cost(x, u_pert2, eng_est, F_ref, MR_ref, cfg, constraints_pert2, dt)
630 luu[i, i] = (cost_pert1 - 2 * cost_nom + cost_pert2) / (eps ** 2)
633 lux = np.zeros((N_CONTROL, N_STATE), dtype=np.float64)
635 return lx, lu, lxx, luu, lux
643 x_seq_nom: np.ndarray,
646 cfg: ControllerConfig,
647 params: DynamicsParams,
648 engine_wrapper: EngineWrapper,
652) -> Tuple[np.ndarray, float, float]:
654 Forward line search: find best step size alpha.
658 u_seq_new : np.ndarray
663 Cost of new trajectory
667 x0, u_seq, F_ref, MR_ref, cfg, params, engine_wrapper, dt
675 best_u_seq = u_seq.copy()
679 alpha =
min(alpha_init * 5.0, 5.0)
683 alpha =
min(alpha_init * 10.0, 10.0)
685 while alpha >= alpha_min:
687 u_seq_new = np.zeros_like(u_seq)
688 x_seq_new = np.zeros((len(u_seq) + 1, N_STATE), dtype=np.float64)
689 x_seq_new[0] = x0.copy()
691 for k
in range(len(u_seq)):
693 dx = x_seq_new[k] - x_seq_nom_full[k]
694 du = alpha * k_seq[k] + K_seq[k] @ dx
700 u_new = np.clip(u_seq[k] + du_scaled, 0.0, 1.0)
705 eng_est = engine_wrapper.estimate_from_pressures(
706 x_seq_new[k][IDX_P_U_F], x_seq_new[k][IDX_P_U_O]
708 mdot_F = eng_est.mdot_F
if np.isfinite(eng_est.mdot_F)
else 0.0
709 mdot_O = eng_est.mdot_O
if np.isfinite(eng_est.mdot_O)
else 0.0
711 x_seq_new[k + 1] = step(x_seq_new[k], u_new, dt, params, mdot_F, mdot_O)
715 x0, u_seq_new, F_ref, MR_ref, cfg, params, engine_wrapper, dt
719 improvement = best_cost - cost_new
720 if improvement > 1e-8:
723 best_u_seq = u_seq_new.copy()
726 if improvement > 1e-6:
734 return best_u_seq, best_alpha, best_cost
Tuple[np.ndarray, float, float] forward_line_search(np.ndarray x0, np.ndarray u_seq, np.ndarray k_seq, np.ndarray K_seq, np.ndarray x_seq_nom, np.ndarray F_ref, np.ndarray MR_ref, ControllerConfig cfg, DynamicsParams params, EngineWrapper engine_wrapper, float dt, float alpha_init, float alpha_min)
DDPSolution solve_ddp(np.ndarray x0, np.ndarray u_seq_init, np.ndarray F_ref, np.ndarray MR_ref, ControllerConfig cfg, DynamicsParams dynamics_params, EngineWrapper engine_wrapper, Optional[np.ndarray] w_bar=None, float alpha_init=2.0, float alpha_min=1e-5, float reg_init=1e-4, float reg_factor=5.0, bool use_robustification=True, float gamma_robust=1.0)
Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray] backward_pass(np.ndarray x_seq, np.ndarray u_seq, List[EngineEstimate] eng_estimates, np.ndarray F_ref, np.ndarray MR_ref, ControllerConfig cfg, DynamicsParams params, EngineWrapper engine_wrapper, float dt, float reg, np.ndarray w_bar, bool use_robustification, float gamma_robust)