STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
constraints.py
Go to the documentation of this file.
1"""Constraint checking for robust DDP controller.
2
3Provides hard constraint violations and soft constraint margins.
4"""
5
6from __future__ import annotations
7
8from typing import Dict, Any, Optional
9import numpy as np
10
11from .data_models import ControllerConfig
12from .engine_wrapper import EngineEstimate
13from .dynamics import (
14 IDX_P_COPV,
15 IDX_P_REG,
16 IDX_P_U_F,
17 IDX_P_U_O,
18 IDX_P_D_F,
19 IDX_P_D_O,
20)
21
22
24 x: np.ndarray,
25 eng_est: EngineEstimate,
26 cfg: ControllerConfig,
27) -> bool:
28 """
29 Check if state satisfies all hard constraints.
30
31 Parameters:
32 -----------
33 x : np.ndarray, shape (N_STATE,)
34 State vector: [P_copv, P_reg, P_u_F, P_u_O, P_d_F, P_d_O, V_u_F, V_u_O]
35 eng_est : EngineEstimate
36 Engine performance estimate
37 cfg : ControllerConfig
38 Controller configuration
39
40 Returns:
41 --------
42 safe : bool
43 True if all constraints are satisfied, False otherwise
44 """
45 constraint_vals = constraint_values(x, eng_est, cfg)
46
47 # Check all hard constraints (positive = violation)
48 hard_constraints = [
49 "copv_min",
50 "ullage_max_F",
51 "ullage_max_O",
52 "MR_min",
53 "MR_max",
54 "injector_stiffness_F",
55 "injector_stiffness_O",
56 ]
57
58 for key in hard_constraints:
59 if key in constraint_vals and constraint_vals[key] > 0:
60 return False
61
62 return True
63
64
66 x: np.ndarray,
67 eng_est: EngineEstimate,
68 cfg: ControllerConfig,
69) -> Dict[str, float]:
70 """
71 Compute constraint violation margins and soft constraint distances.
72
73 Positive values indicate constraint violations (unsafe).
74 Negative values indicate constraint satisfaction with margin.
75
76 Parameters:
77 -----------
78 x : np.ndarray, shape (N_STATE,)
79 State vector: [P_copv, P_reg, P_u_F, P_u_O, P_d_F, P_d_O, V_u_F, V_u_O]
80 eng_est : EngineEstimate
81 Engine performance estimate
82 cfg : ControllerConfig
83 Controller configuration
84
85 Returns:
86 --------
87 constraints : dict
88 Dictionary with constraint values:
89
90 Hard constraints (positive = violation):
91 - "copv_min": P_copv_min - P_copv [Pa]
92 - "ullage_max_F": P_u_F - P_u_max [Pa]
93 - "ullage_max_O": P_u_O - P_u_max [Pa]
94 - "MR_min": MR_min - MR [dimensionless]
95 - "MR_max": MR - MR_max [dimensionless]
96 - "injector_stiffness_F": eps_i * P_ch - (P_d_F - P_ch) [Pa]
97 - "injector_stiffness_O": eps_i * P_ch - (P_d_O - P_ch) [Pa]
98
99 Soft constraints (margins, negative = safe with margin):
100 - "copv_margin": P_copv - P_copv_min [Pa] (distance above minimum)
101 - "ullage_margin_F": P_u_max - P_u_F [Pa] (distance below maximum)
102 - "ullage_margin_O": P_u_max - P_u_O [Pa] (distance below maximum)
103 - "MR_margin_low": MR - MR_min [dimensionless] (distance above minimum)
104 - "MR_margin_high": MR_max - MR [dimensionless] (distance below maximum)
105 - "injector_stiffness_margin_F": (P_d_F - P_ch) - eps_i * P_ch [Pa]
106 - "injector_stiffness_margin_O": (P_d_O - P_ch) - eps_i * P_ch [Pa]
107
108 Headroom flags (boolean-like, 1.0 = insufficient, 0.0 = sufficient):
109 - "headroom_insufficient_F": 1.0 if (P_reg - P_u_F) < dp_min, else 0.0
110 - "headroom_insufficient_O": 1.0 if (P_reg - P_u_O) < dp_min, else 0.0
111 - "headroom_margin_F": (P_reg - P_u_F) - dp_min [Pa]
112 - "headroom_margin_O": (P_reg - P_u_O) - dp_min [Pa]
113 """
114 # Extract state
115 P_copv = x[IDX_P_COPV]
116 P_reg = x[IDX_P_REG]
117 P_u_F = x[IDX_P_U_F]
118 P_u_O = x[IDX_P_U_O]
119 P_d_F = x[IDX_P_D_F]
120 P_d_O = x[IDX_P_D_O]
121
122 # Extract engine estimate
123 P_ch = eng_est.P_ch
124 MR = eng_est.MR
125
126 constraints = {}
127
128 # 1. COPV minimum pressure constraint
129 # Constraint: P_copv >= P_copv_min
130 # Violation: P_copv_min - P_copv (positive if violated)
131 copv_violation = cfg.P_copv_min - P_copv
132 constraints["copv_min"] = copv_violation
133
134 # Soft constraint margin (distance above minimum)
135 constraints["copv_margin"] = P_copv - cfg.P_copv_min
136
137 # 2. Ullage maximum pressure constraints
138 # Constraint: P_u_F <= P_u_max, P_u_O <= P_u_max
139 # Violation: P_u_F - P_u_max (positive if violated)
140 ullage_violation_F = P_u_F - cfg.P_u_max
141 ullage_violation_O = P_u_O - cfg.P_u_max
142 constraints["ullage_max_F"] = ullage_violation_F
143 constraints["ullage_max_O"] = ullage_violation_O
144
145 # Soft constraint margins (distance below maximum)
146 constraints["ullage_margin_F"] = cfg.P_u_max - P_u_F
147 constraints["ullage_margin_O"] = cfg.P_u_max - P_u_O
148
149 # 3. Mixture ratio constraints
150 # Constraint: MR_min <= MR <= MR_max
151 if np.isfinite(MR):
152 # Lower bound violation: MR_min - MR (positive if violated)
153 MR_violation_low = cfg.MR_min - MR
154 constraints["MR_min"] = MR_violation_low
155
156 # Upper bound violation: MR - MR_max (positive if violated)
157 MR_violation_high = MR - cfg.MR_max
158 constraints["MR_max"] = MR_violation_high
159
160 # Soft constraint margins
161 constraints["MR_margin_low"] = MR - cfg.MR_min
162 constraints["MR_margin_high"] = cfg.MR_max - MR
163 else:
164 # MR is NaN - treat as violation
165 constraints["MR_min"] = np.inf
166 constraints["MR_max"] = np.inf
167 constraints["MR_margin_low"] = -np.inf
168 constraints["MR_margin_high"] = -np.inf
169
170 # 4. Injector stiffness constraints
171 # Constraint: (P_d_i - P_ch) >= eps_i * P_ch
172 # Rearranged: P_d_i >= P_ch * (1 + eps_i)
173 # Violation: eps_i * P_ch - (P_d_i - P_ch) (positive if violated)
174 if np.isfinite(P_ch) and P_ch > 0:
175 # Fuel injector stiffness
176 injector_dp_F = eng_est.injector_dp_F
177 if np.isfinite(injector_dp_F):
178 required_dp_F = cfg.eps_i * P_ch
179 stiffness_violation_F = required_dp_F - injector_dp_F
180 constraints["injector_stiffness_F"] = stiffness_violation_F
181 constraints["injector_stiffness_margin_F"] = injector_dp_F - required_dp_F
182 else:
183 constraints["injector_stiffness_F"] = np.inf
184 constraints["injector_stiffness_margin_F"] = -np.inf
185
186 # Oxidizer injector stiffness
187 injector_dp_O = eng_est.injector_dp_O
188 if np.isfinite(injector_dp_O):
189 required_dp_O = cfg.eps_i * P_ch
190 stiffness_violation_O = required_dp_O - injector_dp_O
191 constraints["injector_stiffness_O"] = stiffness_violation_O
192 constraints["injector_stiffness_margin_O"] = injector_dp_O - required_dp_O
193 else:
194 constraints["injector_stiffness_O"] = np.inf
195 constraints["injector_stiffness_margin_O"] = -np.inf
196 else:
197 # P_ch is invalid - treat as violation
198 constraints["injector_stiffness_F"] = np.inf
199 constraints["injector_stiffness_O"] = np.inf
200 constraints["injector_stiffness_margin_F"] = -np.inf
201 constraints["injector_stiffness_margin_O"] = -np.inf
202
203 # 5. Headroom constraints (for actuation effectiveness)
204 # Constraint: (P_reg - P_u_i) >= dp_min for effective pressurization
205 # Flag: 1.0 if insufficient headroom, 0.0 if sufficient
206 headroom_F = P_reg - P_u_F
207 headroom_O = P_reg - P_u_O
208
209 constraints["headroom_insufficient_F"] = 1.0 if headroom_F < cfg.headroom_dp_min else 0.0
210 constraints["headroom_insufficient_O"] = 1.0 if headroom_O < cfg.headroom_dp_min else 0.0
211
212 # Soft constraint margins (distance above minimum headroom)
213 constraints["headroom_margin_F"] = headroom_F - cfg.headroom_dp_min
214 constraints["headroom_margin_O"] = headroom_O - cfg.headroom_dp_min
215
216 return constraints
217
218
220 constraints: Dict[str, float],
221) -> Dict[str, Any]:
222 """
223 Get human-readable summary of constraint status.
224
225 Parameters:
226 -----------
227 constraints : dict
228 Output from constraint_values()
229
230 Returns:
231 --------
232 summary : dict
233 Summary with:
234 - "safe": bool (all constraints satisfied)
235 - "violations": list of violated constraint names
236 - "margins": dict of constraint margins
237 - "headroom_flags": dict of headroom insufficiency flags
238 """
239 violations = []
240
241 hard_constraints = [
242 "copv_min",
243 "ullage_max_F",
244 "ullage_max_O",
245 "MR_min",
246 "MR_max",
247 "injector_stiffness_F",
248 "injector_stiffness_O",
249 ]
250
251 for key in hard_constraints:
252 if key in constraints:
253 val = constraints[key]
254 if np.isfinite(val) and val > 0:
255 violations.append(key)
256 elif not np.isfinite(val):
257 violations.append(f"{key}_invalid")
258
259 safe = len(violations) == 0
260
261 # Extract margins
262 margins = {
263 "copv": constraints.get("copv_margin", np.nan),
264 "ullage_F": constraints.get("ullage_margin_F", np.nan),
265 "ullage_O": constraints.get("ullage_margin_O", np.nan),
266 "MR_low": constraints.get("MR_margin_low", np.nan),
267 "MR_high": constraints.get("MR_margin_high", np.nan),
268 "injector_stiffness_F": constraints.get("injector_stiffness_margin_F", np.nan),
269 "injector_stiffness_O": constraints.get("injector_stiffness_margin_O", np.nan),
270 "headroom_F": constraints.get("headroom_margin_F", np.nan),
271 "headroom_O": constraints.get("headroom_margin_O", np.nan),
272 }
273
274 # Extract headroom flags
275 headroom_flags = {
276 "insufficient_F": constraints.get("headroom_insufficient_F", 0.0) > 0.5,
277 "insufficient_O": constraints.get("headroom_insufficient_O", 0.0) > 0.5,
278 }
279
280 return {
281 "safe": safe,
282 "violations": violations,
283 "margins": margins,
284 "headroom_flags": headroom_flags,
285 }
286
287
288
Dict[str, float] constraint_values(np.ndarray x, EngineEstimate eng_est, ControllerConfig cfg)
Dict[str, Any] get_constraint_summary(Dict[str, float] constraints)
bool is_safe(np.ndarray x, EngineEstimate eng_est, ControllerConfig cfg)