18 state: ControllerState,
22 cfg: ControllerConfig,
23 engine_wrapper: Optional[EngineWrapper] =
None,
24 mdot_F: Optional[float] =
None,
25 mdot_O: Optional[float] =
None,
26 dt: Optional[float] =
None,
29 Update residual bounds w_bar and disturbance bias beta.
31 Computes residual between measured and predicted state, then updates
32 bounds using exponential moving average with inflation.
36 state : ControllerState
37 Controller state (mutated in place)
38 x_prev : np.ndarray, shape (N_STATE,)
40 x_meas : np.ndarray, shape (N_STATE,)
41 Measured state x_meas[k]
42 u_prev : np.ndarray, shape (N_CONTROL,)
43 Previous control u[k-1]
44 cfg : ControllerConfig
45 Controller configuration (contains rho, eta)
46 engine_wrapper : EngineWrapper, optional
47 Engine wrapper for computing mass flows (if not provided, uses mdot_F/mdot_O)
48 mdot_F : float, optional
49 Fuel mass flow [kg/s] (required if engine_wrapper not provided)
50 mdot_O : float, optional
51 Oxidizer mass flow [kg/s] (required if engine_wrapper not provided)
53 Time step [s] (defaults to cfg.dt)
57 Mutates state.w_bar and state.beta in place.
60 1. Compute predicted state: x_pred = f(x_prev, u_prev)
61 2. Compute residual: residual = x_meas - x_pred
62 3. Update bounds: w_bar = rho * w_bar + (1 - rho) * abs(residual)
63 4. Inflate: w_bar *= eta
64 5. Update bias (optional): beta = rho_beta * beta + (1 - rho_beta) * residual
70 if engine_wrapper
is not None:
73 from .dynamics
import IDX_P_U_F, IDX_P_U_O
74 eng_est = engine_wrapper.estimate_from_pressures(
78 mdot_F = eng_est.mdot_F
79 mdot_O = eng_est.mdot_O
81 if mdot_F
is None or mdot_O
is None:
82 raise ValueError(
"Must provide either engine_wrapper or both mdot_F and mdot_O")
85 from .dynamics
import DynamicsParams
86 params = DynamicsParams.from_config(cfg)
89 x_pred = step(x_prev, u_prev, dt, params, mdot_F, mdot_O)
92 residual = x_meas - x_pred
95 if not hasattr(state,
'w_bar_array')
or state.w_bar_array
is None:
97 if isinstance(state.w_bar, dict):
99 state.w_bar_array = np.zeros(N_STATE, dtype=np.float64)
101 state.w_bar_array = np.zeros(N_STATE, dtype=np.float64)
104 state.w_bar_array = cfg.rho * state.w_bar_array + (1 - cfg.rho) * np.abs(residual)
108 state.w_bar_array *= (1.0 + cfg.eta)
111 state.w_bar_array = np.maximum(state.w_bar_array, 0.0)
116 state.beta = rho_beta * state.beta + (1 - rho_beta) * np.mean(residual)
121 "P_copv",
"P_reg",
"P_u_F",
"P_u_O",
122 "P_d_F",
"P_d_O",
"V_u_F",
"V_u_O"
124 state.w_bar = {name: float(state.w_bar_array[i])
for i, name
in enumerate(state_names)}
133 params: DynamicsParams,
136) -> Tuple[np.ndarray, np.ndarray]:
138 Propagate uncertainty tube through dynamics.
140 Computes lower and upper bounds of state uncertainty after one step:
141 - x_lo_next = f(x_lo, u) - w_bar
142 - x_hi_next = f(x_hi, u) + w_bar
146 x_lo : np.ndarray, shape (N_STATE,)
147 Lower bound of state uncertainty
148 x_hi : np.ndarray, shape (N_STATE,)
149 Upper bound of state uncertainty
150 u : np.ndarray, shape (N_CONTROL,)
152 w_bar : np.ndarray, shape (N_STATE,)
153 Residual bounds (uncertainty per state component)
156 params : DynamicsParams
159 Fuel mass flow [kg/s]
161 Oxidizer mass flow [kg/s]
165 x_lo_next : np.ndarray, shape (N_STATE,)
166 Lower bound of next state uncertainty
167 x_hi_next : np.ndarray, shape (N_STATE,)
168 Upper bound of next state uncertainty
172 This implements a simple interval propagation. For more sophisticated
173 methods (e.g., zonotopes, polytopes), see advanced robust control literature.
176 if x_lo.shape != (N_STATE,)
or x_hi.shape != (N_STATE,):
177 raise ValueError(f
"State bounds must have shape ({N_STATE},), got {x_lo.shape} and {x_hi.shape}")
178 if w_bar.shape != (N_STATE,):
179 raise ValueError(f
"w_bar must have shape ({N_STATE},), got {w_bar.shape}")
182 if not np.all(x_lo <= x_hi):
183 raise ValueError(
"x_lo must be <= x_hi element-wise")
186 x_lo_next = step(x_lo, u, dt, params, mdot_F, mdot_O) - w_bar
189 x_hi_next = step(x_hi, u, dt, params, mdot_F, mdot_O) + w_bar
193 x_lo_next = np.minimum(x_lo_next, x_hi_next)
194 x_hi_next = np.maximum(x_lo_next, x_hi_next)
197 x_lo_next = np.maximum(x_lo_next, 0.0)
199 return x_lo_next, x_hi_next
204 Get w_bar as numpy array (per-state-component bounds).
208 state : ControllerState
213 w_bar : np.ndarray, shape (N_STATE,)
214 Residual bounds for each state component
216 if hasattr(state,
'w_bar_array')
and state.w_bar_array
is not None:
217 return state.w_bar_array.copy()
220 if isinstance(state.w_bar, dict):
222 "P_copv",
"P_reg",
"P_u_F",
"P_u_O",
223 "P_d_F",
"P_d_O",
"V_u_F",
"V_u_O"
225 w_bar_array = np.zeros(N_STATE, dtype=np.float64)
226 for i, name
in enumerate(state_names):
227 w_bar_array[i] = state.w_bar.get(name, 0.0)
231 return np.zeros(N_STATE, dtype=np.float64)
236 Set w_bar from numpy array.
240 state : ControllerState
241 Controller state (mutated in place)
242 w_bar : np.ndarray, shape (N_STATE,)
243 Residual bounds for each state component
245 if w_bar.shape != (N_STATE,):
246 raise ValueError(f
"w_bar must have shape ({N_STATE},), got {w_bar.shape}")
248 state.w_bar_array = w_bar.copy()
252 "P_copv",
"P_reg",
"P_u_F",
"P_u_O",
253 "P_d_F",
"P_d_O",
"V_u_F",
"V_u_O"
255 state.w_bar = {name: float(w_bar[i])
for i, name
in enumerate(state_names)}
Tuple[np.ndarray, np.ndarray] tube_propagate(np.ndarray x_lo, np.ndarray x_hi, np.ndarray u, np.ndarray w_bar, float dt, DynamicsParams params, float mdot_F, float mdot_O)
None update_bounds(ControllerState state, np.ndarray x_prev, np.ndarray x_meas, np.ndarray u_prev, ControllerConfig cfg, Optional[EngineWrapper] engine_wrapper=None, Optional[float] mdot_F=None, Optional[float] mdot_O=None, Optional[float] dt=None)