STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
identify.py
Go to the documentation of this file.
1"""Online parameter identification for robust DDP controller.
2
3Uses Recursive Least Squares (RLS) with forgetting factor to identify:
4- alpha_F, alpha_O: Pressurization flow coefficients
5- tau_line_F, tau_line_O: Feed line time constants
6- copv_cF, copv_cO: COPV consumption coefficients
7"""
8
9from __future__ import annotations
10
11from typing import Optional
12import numpy as np
13
14from .data_models import ControllerConfig, ControllerState, Measurement
15from .dynamics import (
16 IDX_P_COPV,
17 IDX_P_REG,
18 IDX_P_U_F,
19 IDX_P_U_O,
20 IDX_P_D_F,
21 IDX_P_D_O,
22 IDX_V_U_F,
23 IDX_V_U_O,
24 IDX_U_F,
25 IDX_U_O,
26)
27
28
30 """Online parameter identifier using RLS with forgetting factor."""
31
32 def __init__(self, cfg: ControllerConfig, forgetting_factor: float = 0.99):
33 """
34 Initialize parameter identifier.
35
36 Parameters:
37 -----------
38 cfg : ControllerConfig
39 Controller configuration (contains initial parameter values)
40 forgetting_factor : float
41 RLS forgetting factor (0 < lambda < 1, closer to 1 = slower forgetting)
42 """
43 self.cfg = cfg
44 self.lambda_rls = forgetting_factor
45
46 # Initialize parameters (will be updated from cfg)
47 self.alpha_F = cfg.alpha_F
48 self.alpha_O = cfg.alpha_O
49 self.tau_line_F = cfg.tau_line_F
50 self.tau_line_O = cfg.tau_line_O
51 self.copv_cF = cfg.copv_cF
52 self.copv_cO = cfg.copv_cO
53
54 # RLS state for alpha_F
55 self.P_alpha_F = 1e6 # Covariance matrix (scalar for 1D)
56 self.theta_alpha_F = cfg.alpha_F # Parameter estimate
57
58 # RLS state for alpha_O
59 self.P_alpha_O = 1e6
60 self.theta_alpha_O = cfg.alpha_O
61
62 # RLS state for tau_line_F (identify 1/tau)
63 self.P_tau_F = 1e6
64 self.theta_tau_F = 1.0 / cfg.tau_line_F
65
66 # RLS state for tau_line_O (identify 1/tau)
67 self.P_tau_O = 1e6
68 self.theta_tau_O = 1.0 / cfg.tau_line_O
69
70 # RLS state for COPV coefficients (2D: cF, cO)
71 self.P_copv = 1e6 * np.eye(2) # 2x2 covariance matrix
72 self.theta_copv = np.array([cfg.copv_cF, cfg.copv_cO])
73
74 # Previous state for computing derivatives
75 self.x_prev: Optional[np.ndarray] = None
76 self.u_prev: Optional[np.ndarray] = None
77 self.t_prev: Optional[float] = None
78
79 # Parameter bounds
80 self.alpha_min, self.alpha_max = 0.1, 100.0 # [1/s]
81 self.tau_min, self.tau_max = 0.001, 1.0 # [s]
82 self.copv_c_min, self.copv_c_max = 1e3, 1e7 # [Pa/s per unit]
83
85 self,
86 state: ControllerState,
87 meas: Measurement,
88 cfg: ControllerConfig,
89 x_current: np.ndarray,
90 u_current: np.ndarray,
91 dt: float,
92 mdot_F: Optional[float] = None,
93 mdot_O: Optional[float] = None,
94 ) -> None:
95 """
96 Update identified parameters from measurements.
97
98 Parameters:
99 -----------
100 state : ControllerState
101 Controller state (not used, but kept for API consistency)
102 meas : Measurement
103 Current measurements
104 cfg : ControllerConfig
105 Controller configuration
106 x_current : np.ndarray
107 Current state vector
108 u_current : np.ndarray
109 Current control input
110 dt : float
111 Time step [s]
112 mdot_F : float, optional
113 Fuel mass flow [kg/s]
114 mdot_O : float, optional
115 Oxidizer mass flow [kg/s]
116 """
117 if self.x_prev is None:
118 # First call: store state and return
119 self.x_prev = x_current.copy()
120 self.u_prev = u_current.copy()
121 self.t_prev = meas.timestamp if hasattr(meas, 'timestamp') else 0.0
122 return
123
124 # Compute derivatives (finite difference)
125 dP_copv_dt = (x_current[IDX_P_COPV] - self.x_prev[IDX_P_COPV]) / dt
126 dP_u_F_dt = (x_current[IDX_P_U_F] - self.x_prev[IDX_P_U_F]) / dt
127 dP_u_O_dt = (x_current[IDX_P_U_O] - self.x_prev[IDX_P_U_O]) / dt
128 dP_d_F_dt = (x_current[IDX_P_D_F] - self.x_prev[IDX_P_D_F]) / dt
129 dP_d_O_dt = (x_current[IDX_P_D_O] - self.x_prev[IDX_P_D_O]) / dt
130
131 # Update alpha_F (pressurization coefficient)
132 self._update_alpha_F(
133 dP_u_F_dt, x_current, u_current, dt, mdot_F
134 )
135
136 # Update alpha_O (pressurization coefficient)
137 self._update_alpha_O(
138 dP_u_O_dt, x_current, u_current, dt, mdot_O
139 )
140
141 # Update tau_line_F (feed line time constant)
142 self._update_tau_line_F(dP_d_F_dt, x_current, dt)
143
144 # Update tau_line_O (feed line time constant)
145 self._update_tau_line_O(dP_d_O_dt, x_current, dt)
146
147 # Update COPV coefficients
148 self._update_copv_coefficients(dP_copv_dt, u_current, dt)
149
150 # Update previous state
151 self.x_prev = x_current.copy()
152 self.u_prev = u_current.copy()
153 self.t_prev = meas.timestamp if hasattr(meas, 'timestamp') else self.t_prev + dt
154
156 self,
157 dP_u_dt: float,
158 x: np.ndarray,
159 u: np.ndarray,
160 dt: float,
161 mdot_F: Optional[float],
162 ) -> None:
163 """Update alpha_F using RLS."""
164 P_reg = x[IDX_P_REG]
165 P_u = x[IDX_P_U_F]
166 u_F = u[IDX_U_F]
167 V_u = x[IDX_V_U_F]
168
169 # Only update when valve is on and there's headroom
170 if u_F < 0.1 or P_reg <= P_u:
171 return
172
173 # Model: dP_u/dt = alpha * u * (P_reg - P_u) - (P_u/V_u) * (mdot/rho)
174 # Rearrange: dP_u/dt + (P_u/V_u) * (mdot/rho) = alpha * u * (P_reg - P_u)
175
176 # Compute blowdown term
177 blowdown_term = 0.0
178 if V_u > 1e-10 and mdot_F is not None:
179 blowdown_term = (P_u / V_u) * (mdot_F / self.cfg.rho_F)
180
181 # Measurement: y = dP_u/dt + blowdown_term
182 y = dP_u_dt + blowdown_term
183
184 # Regressor: phi = u * (P_reg - P_u)
185 phi = u_F * (P_reg - P_u)
186
187 if abs(phi) < 1e-6: # Avoid division by zero
188 return
189
190 # RLS update
191 K = self.P_alpha_F * phi / (self.lambda_rls + self.P_alpha_F * phi ** 2)
192 error = y - self.theta_alpha_F * phi
193 self.theta_alpha_F += K * error
194 self.P_alpha_F = (1.0 / self.lambda_rls) * (self.P_alpha_F - K * phi * self.P_alpha_F)
195
196 # Bound and update parameter
197 self.theta_alpha_F = np.clip(self.theta_alpha_F, self.alpha_min, self.alpha_max)
198 self.alpha_F = self.theta_alpha_F
199
201 self,
202 dP_u_dt: float,
203 x: np.ndarray,
204 u: np.ndarray,
205 dt: float,
206 mdot_O: Optional[float],
207 ) -> None:
208 """Update alpha_O using RLS."""
209 P_reg = x[IDX_P_REG]
210 P_u = x[IDX_P_U_O]
211 u_O = u[IDX_U_O]
212 V_u = x[IDX_V_U_O]
213
214 # Only update when valve is on and there's headroom
215 if u_O < 0.1 or P_reg <= P_u:
216 return
217
218 # Compute blowdown term
219 blowdown_term = 0.0
220 if V_u > 1e-10 and mdot_O is not None:
221 blowdown_term = (P_u / V_u) * (mdot_O / self.cfg.rho_O)
222
223 # Measurement: y = dP_u/dt + blowdown_term
224 y = dP_u_dt + blowdown_term
225
226 # Regressor: phi = u * (P_reg - P_u)
227 phi = u_O * (P_reg - P_u)
228
229 if abs(phi) < 1e-6:
230 return
231
232 # RLS update
233 K = self.P_alpha_O * phi / (self.lambda_rls + self.P_alpha_O * phi ** 2)
234 error = y - self.theta_alpha_O * phi
235 self.theta_alpha_O += K * error
236 self.P_alpha_O = (1.0 / self.lambda_rls) * (self.P_alpha_O - K * phi * self.P_alpha_O)
237
238 # Bound and update parameter
239 self.theta_alpha_O = np.clip(self.theta_alpha_O, self.alpha_min, self.alpha_max)
240 self.alpha_O = self.theta_alpha_O
241
242 def _update_tau_line_F(self, dP_d_dt: float, x: np.ndarray, dt: float) -> None:
243 """Update tau_line_F using RLS."""
244 P_u = x[IDX_P_U_F]
245 P_d = x[IDX_P_D_F]
246
247 # Model: dP_d/dt = (P_u - P_d) / tau_line
248 # Rearrange: dP_d/dt = (1/tau_line) * (P_u - P_d)
249
250 # Regressor: phi = P_u - P_d
251 phi = P_u - P_d
252
253 if abs(phi) < 1e-6:
254 return
255
256 # Measurement: y = dP_d/dt
257 y = dP_d_dt
258
259 # RLS update (identify 1/tau)
260 K = self.P_tau_F * phi / (self.lambda_rls + self.P_tau_F * phi ** 2)
261 error = y - self.theta_tau_F * phi
262 self.theta_tau_F += K * error
263 self.P_tau_F = (1.0 / self.lambda_rls) * (self.P_tau_F - K * phi * self.P_tau_F)
264
265 # Convert to tau and bound
266 tau = 1.0 / max(abs(self.theta_tau_F), 1e-6) # Avoid division by zero
267 tau = np.clip(tau, self.tau_min, self.tau_max)
268 self.tau_line_F = tau
269 self.theta_tau_F = 1.0 / tau # Update theta to match bounded tau
270
271 def _update_tau_line_O(self, dP_d_dt: float, x: np.ndarray, dt: float) -> None:
272 """Update tau_line_O using RLS."""
273 P_u = x[IDX_P_U_O]
274 P_d = x[IDX_P_D_O]
275
276 # Regressor: phi = P_u - P_d
277 phi = P_u - P_d
278
279 if abs(phi) < 1e-6:
280 return
281
282 # Measurement: y = dP_d/dt
283 y = dP_d_dt
284
285 # RLS update (identify 1/tau)
286 K = self.P_tau_O * phi / (self.lambda_rls + self.P_tau_O * phi ** 2)
287 error = y - self.theta_tau_O * phi
288 self.theta_tau_O += K * error
289 self.P_tau_O = (1.0 / self.lambda_rls) * (self.P_tau_O - K * phi * self.P_tau_O)
290
291 # Convert to tau and bound
292 tau = 1.0 / max(abs(self.theta_tau_O), 1e-6)
293 tau = np.clip(tau, self.tau_min, self.tau_max)
294 self.tau_line_O = tau
295 self.theta_tau_O = 1.0 / tau
296
298 self,
299 dP_copv_dt: float,
300 u: np.ndarray,
301 dt: float,
302 ) -> None:
303 """Update COPV consumption coefficients using RLS."""
304 u_F = u[IDX_U_F]
305 u_O = u[IDX_U_O]
306
307 # Model: dP_copv/dt = -(cF*u_F + cO*u_O + loss)
308 # Rearrange: -dP_copv/dt = cF*u_F + cO*u_O + loss
309
310 # Measurement: y = -dP_copv/dt (excluding loss)
311 y = -dP_copv_dt - self.cfg.copv_loss
312
313 # Regressor: phi = [u_F, u_O]
314 phi = np.array([u_F, u_O])
315
316 if np.linalg.norm(phi) < 1e-6:
317 return
318
319 # RLS update (2D)
320 P_phi = self.P_copv @ phi
321 denominator = self.lambda_rls + phi @ P_phi
322 if abs(denominator) < 1e-10:
323 return
324
325 K = P_phi / denominator
326 error = y - self.theta_copv @ phi
327 self.theta_copv += K * error
328 self.P_copv = (1.0 / self.lambda_rls) * (self.P_copv - np.outer(K, P_phi))
329
330 # Bound parameters
331 self.theta_copv = np.clip(self.theta_copv, self.copv_c_min, self.copv_c_max)
332 self.copv_cF = self.theta_copv[0]
333 self.copv_cO = self.theta_copv[1]
334
335 def get_params(self) -> dict:
336 """Get current parameter estimates."""
337 return {
338 "alpha_F": self.alpha_F,
339 "alpha_O": self.alpha_O,
340 "tau_line_F": self.tau_line_F,
341 "tau_line_O": self.tau_line_O,
342 "copv_cF": self.copv_cF,
343 "copv_cO": self.copv_cO,
344 }
345
346 def update_config(self, cfg: ControllerConfig) -> None:
347 """Update config with identified parameters (slow update)."""
348 # Use exponential moving average to update slowly
349 update_rate = 0.01 # 1% per update
350
351 cfg.alpha_F = (1 - update_rate) * cfg.alpha_F + update_rate * self.alpha_F
352 cfg.alpha_O = (1 - update_rate) * cfg.alpha_O + update_rate * self.alpha_O
353 cfg.tau_line_F = (1 - update_rate) * cfg.tau_line_F + update_rate * self.tau_line_F
354 cfg.tau_line_O = (1 - update_rate) * cfg.tau_line_O + update_rate * self.tau_line_O
355 cfg.copv_cF = (1 - update_rate) * cfg.copv_cF + update_rate * self.copv_cF
356 cfg.copv_cO = (1 - update_rate) * cfg.copv_cO + update_rate * self.copv_cO
357
358
360 identifier: ParameterIdentifier,
361 state: ControllerState,
362 meas: Measurement,
363 cfg: ControllerConfig,
364 x_current: np.ndarray,
365 u_current: np.ndarray,
366 dt: float,
367 mdot_F: Optional[float] = None,
368 mdot_O: Optional[float] = None,
369) -> None:
370 """
371 Update identified parameters (convenience function).
372
373 Parameters:
374 -----------
375 identifier : ParameterIdentifier
376 Parameter identifier instance
377 state : ControllerState
378 Controller state
379 meas : Measurement
380 Current measurements
381 cfg : ControllerConfig
382 Controller configuration
383 x_current : np.ndarray
384 Current state vector
385 u_current : np.ndarray
386 Current control input
387 dt : float
388 Time step [s]
389 mdot_F : float, optional
390 Fuel mass flow [kg/s]
391 mdot_O : float, optional
392 Oxidizer mass flow [kg/s]
393 """
394 identifier.update_params(
395 state, meas, cfg, x_current, u_current, dt, mdot_F, mdot_O
396 )
397
398
399
#define max(a, b)
Definition Arduino.h:43
None _update_tau_line_F(self, float dP_d_dt, np.ndarray x, float dt)
Definition identify.py:242
None _update_tau_line_O(self, float dP_d_dt, np.ndarray x, float dt)
Definition identify.py:271
None update_params(self, ControllerState state, Measurement meas, ControllerConfig cfg, np.ndarray x_current, np.ndarray u_current, float dt, Optional[float] mdot_F=None, Optional[float] mdot_O=None)
Definition identify.py:94
__init__(self, ControllerConfig cfg, float forgetting_factor=0.99)
Definition identify.py:32
None _update_alpha_O(self, float dP_u_dt, np.ndarray x, np.ndarray u, float dt, Optional[float] mdot_O)
Definition identify.py:207
None _update_alpha_F(self, float dP_u_dt, np.ndarray x, np.ndarray u, float dt, Optional[float] mdot_F)
Definition identify.py:162
None _update_copv_coefficients(self, float dP_copv_dt, np.ndarray u, float dt)
Definition identify.py:302
None update_config(self, ControllerConfig cfg)
Definition identify.py:346
None update_params(ParameterIdentifier identifier, ControllerState state, Measurement meas, ControllerConfig cfg, np.ndarray x_current, np.ndarray u_current, float dt, Optional[float] mdot_F=None, Optional[float] mdot_O=None)
Definition identify.py:369