1"""Safety filter for robust DDP controller.
3Given proposed actuation, computes reachable tube and checks constraint violations.
4If unsafe, finds best safe action from discrete candidate set.
7from __future__
import annotations
9from typing
import Optional, Tuple, List
12from .data_models
import ControllerConfig, ControllerState
13from .dynamics
import (
14 step, DynamicsParams, N_STATE, N_CONTROL,
15 IDX_U_F, IDX_U_O, IDX_P_U_F, IDX_P_U_O
17from .robustness
import tube_propagate, get_w_bar_array
18from .constraints
import is_safe, constraint_values
19from .engine_wrapper
import EngineWrapper, EngineEstimate
25 state: ControllerState,
26 cfg: ControllerConfig,
27 engine_wrapper: Optional[EngineWrapper] =
None,
28 F_ref: Optional[float] =
None,
29 MR_ref: Optional[float] =
None,
31 dt: Optional[float] =
None,
34 Filter proposed action to ensure safety.
36 Computes reachable tube from proposed action and checks constraints.
37 If unsafe, finds best safe action from discrete candidate set.
41 x : np.ndarray, shape (N_STATE,)
42 Current measured state
43 proposed : np.ndarray, shape (N_CONTROL,)
44 Proposed relaxed control [u_F, u_O] in [0, 1]
45 state : ControllerState
46 Controller state (for w_bar)
47 cfg : ControllerConfig
48 Controller configuration
49 engine_wrapper : EngineWrapper, optional
50 Engine wrapper for performance estimation
51 F_ref : float, optional
52 Reference thrust [N] (for cost computation)
53 MR_ref : float, optional
54 Reference mixture ratio (for cost computation)
56 Number of steps to propagate tube (default: 2)
58 Time step [s] (defaults to cfg.dt)
62 safe_action : np.ndarray, shape (N_CONTROL,)
63 Safe relaxed control (may be same as proposed if safe)
69 if x.shape != (N_STATE,):
70 raise ValueError(f
"x must have shape ({N_STATE},), got {x.shape}")
71 if proposed.shape != (N_CONTROL,):
72 raise ValueError(f
"proposed must have shape ({N_CONTROL},), got {proposed.shape}")
75 proposed = np.clip(proposed, 0.0, 1.0)
78 w_bar = get_w_bar_array(state)
81 if _is_action_safe(x, proposed, w_bar, cfg, engine_wrapper, num_steps, dt):
86 x, proposed, w_bar, cfg, engine_wrapper, F_ref, MR_ref, num_steps, dt
96 cfg: ControllerConfig,
97 engine_wrapper: Optional[EngineWrapper],
102 Check if action is safe by propagating tube and checking constraints.
112 cfg : ControllerConfig
113 Controller configuration
114 engine_wrapper : EngineWrapper, optional
117 Number of steps to propagate
124 True if action is safe, False otherwise
131 params = DynamicsParams.from_config(cfg)
134 for _
in range(num_steps):
139 x_lo, x_hi = tube_propagate(
140 x_lo, x_hi, u, w_bar, dt, params, mdot_F, mdot_O
153 cfg: ControllerConfig,
154 engine_wrapper: Optional[EngineWrapper],
157 Check if uncertainty tube violates hard constraints.
159 Checks worst-case (upper bound) for each constraint.
164 Lower bound of state tube
166 Upper bound of state tube
167 cfg : ControllerConfig
168 Controller configuration
169 engine_wrapper : EngineWrapper, optional
175 True if tube violates any hard constraint
179 if x_hi[0] < cfg.P_copv_min:
183 if x_hi[2] > cfg.P_u_max:
185 if x_hi[3] > cfg.P_u_max:
189 if engine_wrapper
is not None:
193 eng_est = engine_wrapper.estimate_from_pressures(x_hi[IDX_P_U_F], x_hi[IDX_P_U_O])
196 if eng_est.MR < cfg.MR_min
or eng_est.MR > cfg.MR_max:
200 dp_F = eng_est.injector_dp_F
201 dp_O = eng_est.injector_dp_O
204 if dp_F < cfg.injector_dp_frac * P_ch:
206 if dp_O < cfg.injector_dp_frac * P_ch:
217 proposed: np.ndarray,
219 cfg: ControllerConfig,
220 engine_wrapper: Optional[EngineWrapper],
221 F_ref: Optional[float],
222 MR_ref: Optional[float],
227 Find best safe action from discrete candidate set.
233 proposed : np.ndarray
234 Proposed action (unsafe)
237 cfg : ControllerConfig
238 Controller configuration
239 engine_wrapper : EngineWrapper, optional
241 F_ref : float, optional
243 MR_ref : float, optional
244 Reference mixture ratio
246 Number of steps for tube propagation
252 best_action : np.ndarray
253 Best safe action from candidate set
260 best_cost = float(
'inf')
262 for candidate
in candidates:
264 if not _is_action_safe(x, candidate, w_bar, cfg, engine_wrapper, num_steps, dt):
269 x, candidate, cfg, engine_wrapper, F_ref, MR_ref
275 best_action = candidate.copy()
278 if best_action
is None:
279 return np.zeros(N_CONTROL, dtype=np.float64)
286 Generate discrete action candidates.
288 For binary backend: {(0,0), (0,1), (1,0), (1,1)}
289 For PWM backend: quantized duty pairs based on duty_quantization.
293 cfg : ControllerConfig
294 Controller configuration
298 candidates : List[np.ndarray]
299 List of candidate actions
304 binary_candidates = [
305 np.array([0.0, 0.0]),
306 np.array([0.0, 1.0]),
307 np.array([1.0, 0.0]),
308 np.array([1.0, 1.0]),
310 candidates.extend(binary_candidates)
314 duty_step = cfg.duty_quantization
315 n_steps = int(1.0 / duty_step) + 1
319 pwm_step =
max(duty_step * 5, 0.1)
320 pwm_values = np.linspace(0.0, 1.0, int(1.0 / pwm_step) + 1)
323 for u_F
in [0.0, 0.5, 1.0]:
324 for u_O
in [0.0, 0.5, 1.0]:
325 candidate = np.array([u_F, u_O])
327 if not any(np.allclose(candidate, bc)
for bc
in binary_candidates):
328 candidates.append(candidate)
336 cfg: ControllerConfig,
337 engine_wrapper: Optional[EngineWrapper],
338 F_ref: Optional[float],
339 MR_ref: Optional[float],
342 Compute immediate cost for action.
344 Cost = tracking error + gas penalty
352 cfg : ControllerConfig
353 Controller configuration
354 engine_wrapper : EngineWrapper, optional
356 F_ref : float, optional
358 MR_ref : float, optional
359 Reference mixture ratio
369 P_copv_drop = cfg.copv_cF * u[IDX_U_F] + cfg.copv_cO * u[IDX_U_O] + cfg.copv_loss
370 cost += cfg.qGas * P_copv_drop
373 if engine_wrapper
is not None and (F_ref
is not None or MR_ref
is not None):
376 eng_est = engine_wrapper.estimate_from_pressures(x[IDX_P_U_F], x[IDX_P_U_O])
378 if F_ref
is not None:
379 F_error = eng_est.F - F_ref
380 cost += cfg.qF * F_error ** 2
382 if MR_ref
is not None:
383 MR_error = eng_est.MR - MR_ref
384 cost += cfg.qMR * MR_error ** 2
394 engine_wrapper: Optional[EngineWrapper],
395) -> Tuple[float, float]:
397 Estimate mass flows from state.
403 engine_wrapper : EngineWrapper, optional
409 Fuel mass flow [kg/s]
411 Oxidizer mass flow [kg/s]
413 if engine_wrapper
is not None:
416 eng_est = engine_wrapper.estimate_from_pressures(x[IDX_P_U_F], x[IDX_P_U_O])
417 mdot_F = eng_est.mdot_F
if np.isfinite(eng_est.mdot_F)
else 0.0
418 mdot_O = eng_est.mdot_O
if np.isfinite(eng_est.mdot_O)
else 0.0
419 return mdot_F, mdot_O
np.ndarray filter_action(np.ndarray x, np.ndarray proposed, ControllerState state, ControllerConfig cfg, Optional[EngineWrapper] engine_wrapper=None, Optional[float] F_ref=None, Optional[float] MR_ref=None, int num_steps=2, Optional[float] dt=None)
bool _tube_violates_constraints(np.ndarray x_lo, np.ndarray x_hi, ControllerConfig cfg, Optional[EngineWrapper] engine_wrapper)
bool _is_action_safe(np.ndarray x, np.ndarray u, np.ndarray w_bar, ControllerConfig cfg, Optional[EngineWrapper] engine_wrapper, int num_steps, float dt)
Tuple[float, float] _estimate_mass_flows(np.ndarray x, Optional[EngineWrapper] engine_wrapper)
List[np.ndarray] _generate_action_candidates(ControllerConfig cfg)
float _compute_action_cost(np.ndarray x, np.ndarray u, ControllerConfig cfg, Optional[EngineWrapper] engine_wrapper, Optional[float] F_ref, Optional[float] MR_ref)
np.ndarray _find_best_safe_action(np.ndarray x, np.ndarray proposed, np.ndarray w_bar, ControllerConfig cfg, Optional[EngineWrapper] engine_wrapper, Optional[float] F_ref, Optional[float] MR_ref, int num_steps, float dt)