STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
safety_filter.py
Go to the documentation of this file.
1"""Safety filter for robust DDP controller.
2
3Given proposed actuation, computes reachable tube and checks constraint violations.
4If unsafe, finds best safe action from discrete candidate set.
5"""
6
7from __future__ import annotations
8
9from typing import Optional, Tuple, List
10import numpy as np
11
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
16)
17from .robustness import tube_propagate, get_w_bar_array
18from .constraints import is_safe, constraint_values
19from .engine_wrapper import EngineWrapper, EngineEstimate
20
21
23 x: np.ndarray,
24 proposed: np.ndarray,
25 state: ControllerState,
26 cfg: ControllerConfig,
27 engine_wrapper: Optional[EngineWrapper] = None,
28 F_ref: Optional[float] = None,
29 MR_ref: Optional[float] = None,
30 num_steps: int = 2,
31 dt: Optional[float] = None,
32) -> np.ndarray:
33 """
34 Filter proposed action to ensure safety.
35
36 Computes reachable tube from proposed action and checks constraints.
37 If unsafe, finds best safe action from discrete candidate set.
38
39 Parameters:
40 -----------
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)
55 num_steps : int
56 Number of steps to propagate tube (default: 2)
57 dt : float, optional
58 Time step [s] (defaults to cfg.dt)
59
60 Returns:
61 --------
62 safe_action : np.ndarray, shape (N_CONTROL,)
63 Safe relaxed control (may be same as proposed if safe)
64 """
65 if dt is None:
66 dt = cfg.dt
67
68 # Validate inputs
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}")
73
74 # Clamp proposed to [0, 1]
75 proposed = np.clip(proposed, 0.0, 1.0)
76
77 # Get residual bounds
78 w_bar = get_w_bar_array(state)
79
80 # Check if proposed action is safe
81 if _is_action_safe(x, proposed, w_bar, cfg, engine_wrapper, num_steps, dt):
82 return proposed
83
84 # Proposed action is unsafe: find best safe alternative
85 safe_action = _find_best_safe_action(
86 x, proposed, w_bar, cfg, engine_wrapper, F_ref, MR_ref, num_steps, dt
87 )
88
89 return safe_action
90
91
93 x: np.ndarray,
94 u: np.ndarray,
95 w_bar: np.ndarray,
96 cfg: ControllerConfig,
97 engine_wrapper: Optional[EngineWrapper],
98 num_steps: int,
99 dt: float,
100) -> bool:
101 """
102 Check if action is safe by propagating tube and checking constraints.
103
104 Parameters:
105 -----------
106 x : np.ndarray
107 Current state
108 u : np.ndarray
109 Control action
110 w_bar : np.ndarray
111 Residual bounds
112 cfg : ControllerConfig
113 Controller configuration
114 engine_wrapper : EngineWrapper, optional
115 Engine wrapper
116 num_steps : int
117 Number of steps to propagate
118 dt : float
119 Time step
120
121 Returns:
122 --------
123 safe : bool
124 True if action is safe, False otherwise
125 """
126 # Initialize tube
127 x_lo = x.copy()
128 x_hi = x.copy()
129
130 # Create dynamics parameters
131 params = DynamicsParams.from_config(cfg)
132
133 # Propagate tube for num_steps
134 for _ in range(num_steps):
135 # Estimate engine performance for mass flows
136 mdot_F, mdot_O = _estimate_mass_flows(x_hi, engine_wrapper)
137
138 # Propagate tube
139 x_lo, x_hi = tube_propagate(
140 x_lo, x_hi, u, w_bar, dt, params, mdot_F, mdot_O
141 )
142
143 # Check if tube violates constraints
144 if _tube_violates_constraints(x_lo, x_hi, cfg, engine_wrapper):
145 return False
146
147 return True
148
149
151 x_lo: np.ndarray,
152 x_hi: np.ndarray,
153 cfg: ControllerConfig,
154 engine_wrapper: Optional[EngineWrapper],
155) -> bool:
156 """
157 Check if uncertainty tube violates hard constraints.
158
159 Checks worst-case (upper bound) for each constraint.
160
161 Parameters:
162 -----------
163 x_lo : np.ndarray
164 Lower bound of state tube
165 x_hi : np.ndarray
166 Upper bound of state tube
167 cfg : ControllerConfig
168 Controller configuration
169 engine_wrapper : EngineWrapper, optional
170 Engine wrapper
171
172 Returns:
173 --------
174 violates : bool
175 True if tube violates any hard constraint
176 """
177 # Check worst-case (upper bound) for pressure constraints
178 # COPV minimum
179 if x_hi[0] < cfg.P_copv_min: # P_copv
180 return True
181
182 # Ullage maximum
183 if x_hi[2] > cfg.P_u_max: # P_u_F
184 return True
185 if x_hi[3] > cfg.P_u_max: # P_u_O
186 return True
187
188 # Check engine constraints (need engine estimate)
189 if engine_wrapper is not None:
190 try:
191 # Use upper bound feed pressures for worst-case
192 # Use tank/ullage pressures (P_u) to get engine performance
193 eng_est = engine_wrapper.estimate_from_pressures(x_hi[IDX_P_U_F], x_hi[IDX_P_U_O])
194
195 # Check MR bounds
196 if eng_est.MR < cfg.MR_min or eng_est.MR > cfg.MR_max:
197 return True
198
199 # Check injector stiffness
200 dp_F = eng_est.injector_dp_F
201 dp_O = eng_est.injector_dp_O
202 P_ch = eng_est.P_ch
203
204 if dp_F < cfg.injector_dp_frac * P_ch:
205 return True
206 if dp_O < cfg.injector_dp_frac * P_ch:
207 return True
208 except Exception:
209 # If engine estimation fails, assume unsafe
210 return True
211
212 return False
213
214
216 x: np.ndarray,
217 proposed: np.ndarray,
218 w_bar: np.ndarray,
219 cfg: ControllerConfig,
220 engine_wrapper: Optional[EngineWrapper],
221 F_ref: Optional[float],
222 MR_ref: Optional[float],
223 num_steps: int,
224 dt: float,
225) -> np.ndarray:
226 """
227 Find best safe action from discrete candidate set.
228
229 Parameters:
230 -----------
231 x : np.ndarray
232 Current state
233 proposed : np.ndarray
234 Proposed action (unsafe)
235 w_bar : np.ndarray
236 Residual bounds
237 cfg : ControllerConfig
238 Controller configuration
239 engine_wrapper : EngineWrapper, optional
240 Engine wrapper
241 F_ref : float, optional
242 Reference thrust
243 MR_ref : float, optional
244 Reference mixture ratio
245 num_steps : int
246 Number of steps for tube propagation
247 dt : float
248 Time step
249
250 Returns:
251 --------
252 best_action : np.ndarray
253 Best safe action from candidate set
254 """
255 # Generate discrete candidates
256 candidates = _generate_action_candidates(cfg)
257
258 # Evaluate each candidate
259 best_action = None
260 best_cost = float('inf')
261
262 for candidate in candidates:
263 # Check if candidate is safe
264 if not _is_action_safe(x, candidate, w_bar, cfg, engine_wrapper, num_steps, dt):
265 continue # Skip unsafe candidates
266
267 # Compute cost
269 x, candidate, cfg, engine_wrapper, F_ref, MR_ref
270 )
271
272 # Update best
273 if cost < best_cost:
274 best_cost = cost
275 best_action = candidate.copy()
276
277 # If no safe candidate found, return safest (all zeros)
278 if best_action is None:
279 return np.zeros(N_CONTROL, dtype=np.float64)
280
281 return best_action
282
283
284def _generate_action_candidates(cfg: ControllerConfig) -> List[np.ndarray]:
285 """
286 Generate discrete action candidates.
287
288 For binary backend: {(0,0), (0,1), (1,0), (1,1)}
289 For PWM backend: quantized duty pairs based on duty_quantization.
290
291 Parameters:
292 -----------
293 cfg : ControllerConfig
294 Controller configuration
295
296 Returns:
297 --------
298 candidates : List[np.ndarray]
299 List of candidate actions
300 """
301 candidates = []
302
303 # Binary candidates (always include)
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]),
309 ]
310 candidates.extend(binary_candidates)
311
312 # PWM candidates (quantized)
313 # Generate grid based on duty_quantization
314 duty_step = cfg.duty_quantization
315 n_steps = int(1.0 / duty_step) + 1
316
317 # Sample grid (don't include all combinations to avoid explosion)
318 # Use coarser grid for PWM candidates
319 pwm_step = max(duty_step * 5, 0.1) # At least 10% steps
320 pwm_values = np.linspace(0.0, 1.0, int(1.0 / pwm_step) + 1)
321
322 # Add some PWM candidates (not all combinations)
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])
326 # Check if not already in binary candidates
327 if not any(np.allclose(candidate, bc) for bc in binary_candidates):
328 candidates.append(candidate)
329
330 return candidates
331
332
334 x: np.ndarray,
335 u: np.ndarray,
336 cfg: ControllerConfig,
337 engine_wrapper: Optional[EngineWrapper],
338 F_ref: Optional[float],
339 MR_ref: Optional[float],
340) -> float:
341 """
342 Compute immediate cost for action.
343
344 Cost = tracking error + gas penalty
345
346 Parameters:
347 -----------
348 x : np.ndarray
349 Current state
350 u : np.ndarray
351 Control action
352 cfg : ControllerConfig
353 Controller configuration
354 engine_wrapper : EngineWrapper, optional
355 Engine wrapper
356 F_ref : float, optional
357 Reference thrust
358 MR_ref : float, optional
359 Reference mixture ratio
360
361 Returns:
362 --------
363 cost : float
364 Action cost
365 """
366 cost = 0.0
367
368 # Gas consumption penalty
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
371
372 # Tracking error (if references provided)
373 if engine_wrapper is not None and (F_ref is not None or MR_ref is not None):
374 try:
375 # Use tank/ullage pressures (P_u) to get engine performance
376 eng_est = engine_wrapper.estimate_from_pressures(x[IDX_P_U_F], x[IDX_P_U_O])
377
378 if F_ref is not None:
379 F_error = eng_est.F - F_ref
380 cost += cfg.qF * F_error ** 2
381
382 if MR_ref is not None:
383 MR_error = eng_est.MR - MR_ref
384 cost += cfg.qMR * MR_error ** 2
385 except Exception:
386 # If estimation fails, add penalty
387 cost += 1e6
388
389 return cost
390
391
393 x: np.ndarray,
394 engine_wrapper: Optional[EngineWrapper],
395) -> Tuple[float, float]:
396 """
397 Estimate mass flows from state.
398
399 Parameters:
400 -----------
401 x : np.ndarray
402 State vector
403 engine_wrapper : EngineWrapper, optional
404 Engine wrapper
405
406 Returns:
407 --------
408 mdot_F : float
409 Fuel mass flow [kg/s]
410 mdot_O : float
411 Oxidizer mass flow [kg/s]
412 """
413 if engine_wrapper is not None:
414 try:
415 # Use tank/ullage pressures (P_u) to get engine performance
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
420 except Exception:
421 pass
422
423 # Fallback: use simple model or zeros
424 return 0.0, 0.0
425
#define max(a, b)
Definition Arduino.h:43
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)