STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
reference.py
Go to the documentation of this file.
1"""Reference generation for robust DDP controller.
2
3Supports two modes:
4- A) Thrust command: user provides F_des(t) or piecewise schedule
5- B) Altitude command: compute desired thrust from PD guidance
6
7Computes feasible reference projection with bounds and slew rate limiting.
8"""
9
10from __future__ import annotations
11
12from typing import Optional, Union, List, Tuple, Dict, Any
13from dataclasses import dataclass
14import numpy as np
15
16from .data_models import NavState, Measurement, Command, CommandType, ControllerConfig
17from .engine_wrapper import EngineWrapper, EngineEstimate
18from .dynamics import IDX_P_D_F, IDX_P_D_O, IDX_P_REG, IDX_P_U_F, IDX_P_U_O
19
20
21@dataclass
23 """Reference trajectory for DDP controller."""
24 F_ref: np.ndarray # shape (N,) - Reference thrust sequence [N]
25 MR_ref: np.ndarray # shape (N,) - Reference mixture ratio sequence
26 F_min: np.ndarray # shape (N,) - Minimum feasible thrust [N]
27 F_max: np.ndarray # shape (N,) - Maximum feasible thrust [N]
28 feasible: np.ndarray # shape (N,) - Whether reference is feasible at each step
29
30
31# Constants
32GRAVITY = 9.81 # [m/s²]
33
34
36 nav: NavState,
37 meas: Measurement,
38 cmd: Command,
39 cfg: ControllerConfig,
40 horizon_N: int,
41 engine_wrapper: Optional[EngineWrapper] = None,
42 F_ref_prev: Optional[float] = None,
43 dt: Optional[float] = None,
44) -> Reference:
45 """
46 Build reference trajectory for DDP controller.
47
48 Parameters:
49 -----------
50 nav : NavState
51 Current navigation state (altitude, velocity, tilt, mass)
52 meas : Measurement
53 Current sensor measurements (pressures)
54 cmd : Command
55 Control command (thrust desired or altitude goal)
56 cfg : ControllerConfig
57 Controller configuration
58 horizon_N : int
59 Prediction horizon length
60 engine_wrapper : EngineWrapper, optional
61 Engine wrapper for estimating feasible thrust bounds
62 F_ref_prev : float, optional
63 Previous reference thrust (for slew rate limiting)
64 dt : float, optional
65 Time step [s] (defaults to cfg.dt)
66
67 Returns:
68 --------
69 ref : Reference
70 Reference trajectory with F_ref, MR_ref, bounds, and feasibility
71 """
72 if dt is None:
73 dt = cfg.dt
74
75 # Initialize arrays
76 F_ref = np.zeros(horizon_N, dtype=np.float64)
77 MR_ref = np.zeros(horizon_N, dtype=np.float64)
78 F_min = np.zeros(horizon_N, dtype=np.float64)
79 F_max = np.zeros(horizon_N, dtype=np.float64)
80 feasible = np.zeros(horizon_N, dtype=bool)
81
82 # Compute desired thrust based on command type
83 if cmd.command_type == CommandType.THRUST_DESIRED:
84 F_des_seq = _compute_thrust_command(cmd, horizon_N, dt)
85 elif cmd.command_type == CommandType.ALTITUDE_GOAL:
86 F_des_seq = _compute_altitude_command(
87 nav, cmd.altitude_goal, horizon_N, dt, cfg
88 )
89 else:
90 raise ValueError(f"Unknown command type: {cmd.command_type}")
91
92 # Compute feasible bounds at each step
93 # For now, use current state to estimate bounds (simplified)
94 # In full implementation, would propagate state forward
95 current_state = _measurement_to_state(meas)
96
97 # Estimate current engine performance
98 eng_est_current = None
99 if engine_wrapper is not None:
100 try:
101 # Use tank/ullage pressures (P_u) to get engine performance
102 # Engine expects tank pressures, not feed pressures
103 eng_est_current = engine_wrapper.estimate_from_pressures(
104 meas.P_u_fuel, meas.P_u_ox
105 )
106 except Exception:
107 pass
108
109 # Compute bounds (simplified - uses current state)
110 # In full implementation, would compute time-varying bounds
111 F_min_val, F_max_val = _estimate_thrust_bounds(
112 current_state, meas, cfg, engine_wrapper, eng_est_current
113 )
114
115 # Project to feasible reference with slew rate limiting
116 F_ref[0] = _project_thrust(
117 F_des_seq[0],
118 F_min_val,
119 F_max_val,
120 F_ref_prev if F_ref_prev is not None else F_des_seq[0],
121 dt,
122 cfg,
123 )
124
125 # Propagate forward (simplified - assumes bounds constant)
126 # In full implementation, would update bounds based on predicted state
127 for k in range(1, horizon_N):
128 F_ref[k] = _project_thrust(
129 F_des_seq[k],
130 F_min_val,
131 F_max_val,
132 F_ref[k - 1],
133 dt,
134 cfg,
135 )
136
137 # Set mixture ratio reference (default to mid-band)
138 MR_mid = (cfg.MR_min + cfg.MR_max) / 2.0
139 MR_ref[:] = MR_mid
140
141 # Set bounds (constant for now)
142 F_min[:] = F_min_val
143 F_max[:] = F_max_val
144
145 # Check feasibility
146 feasible[:] = (F_ref >= F_min) & (F_ref <= F_max)
147
148 return Reference(
149 F_ref=F_ref,
150 MR_ref=MR_ref,
151 F_min=F_min,
152 F_max=F_max,
153 feasible=feasible,
154 )
155
156
158 cmd: Command,
159 horizon_N: int,
160 dt: float,
161) -> np.ndarray:
162 """
163 Compute thrust command sequence from Command.
164
165 Parameters:
166 -----------
167 cmd : Command
168 Command with thrust_desired
169 horizon_N : int
170 Horizon length
171 dt : float
172 Time step [s]
173
174 Returns:
175 --------
176 F_des : np.ndarray, shape (horizon_N,)
177 Desired thrust sequence [N]
178 """
179 if cmd.command_type != CommandType.THRUST_DESIRED:
180 raise ValueError("Command must be THRUST_DESIRED")
181
182 if cmd.thrust_desired is None:
183 raise ValueError("thrust_desired must be provided")
184
185 F_des = np.zeros(horizon_N, dtype=np.float64)
186
187 if isinstance(cmd.thrust_desired, (int, float)):
188 # Constant thrust
189 F_des[:] = float(cmd.thrust_desired)
190 elif isinstance(cmd.thrust_desired, list):
191 # Piecewise schedule: list of (time, thrust) pairs
192 times = np.array([t for t, _ in cmd.thrust_desired])
193 thrusts = np.array([F for _, F in cmd.thrust_desired])
194
195 # Interpolate to horizon
196 t_horizon = np.arange(horizon_N) * dt
197
198 # Handle extrapolation: use first/last value
199 if len(times) == 0:
200 raise ValueError("thrust_desired list is empty")
201
202 # Simple linear interpolation
203 for k in range(horizon_N):
204 t_k = t_horizon[k]
205
206 # Find interval
207 if t_k <= times[0]:
208 F_des[k] = thrusts[0]
209 elif t_k >= times[-1]:
210 F_des[k] = thrusts[-1]
211 else:
212 # Linear interpolation
213 idx = np.searchsorted(times, t_k) - 1
214 t0, t1 = times[idx], times[idx + 1]
215 F0, F1 = thrusts[idx], thrusts[idx + 1]
216 F_des[k] = F0 + (F1 - F0) * (t_k - t0) / (t1 - t0)
217 else:
218 raise ValueError(f"Unsupported thrust_desired type: {type(cmd.thrust_desired)}")
219
220 return F_des
221
222
224 nav: NavState,
225 altitude_goal: float,
226 horizon_N: int,
227 dt: float,
228 cfg: ControllerConfig,
229) -> np.ndarray:
230 """
231 Compute desired thrust from PD guidance for altitude command.
232
233 Uses: a_cmd = kp*(h_goal - h) + kd*(vz_goal - vz)
234 F_des = m*(a_cmd + g) / cos(theta)
235
236 Parameters:
237 -----------
238 nav : NavState
239 Current navigation state
240 altitude_goal : float
241 Target altitude [m]
242 horizon_N : int
243 Horizon length
244 dt : float
245 Time step [s]
246 cfg : ControllerConfig
247 Controller configuration (for PD gains if available)
248
249 Returns:
250 --------
251 F_des : np.ndarray, shape (horizon_N,)
252 Desired thrust sequence [N]
253 """
254 # PD gains (default values, could be in cfg)
255 kp = getattr(cfg, 'altitude_kp', 0.5) # [1/s²]
256 kd = getattr(cfg, 'altitude_kd', 0.3) # [1/s]
257
258 # Target vertical velocity (zero for hover, or computed)
259 vz_goal = getattr(cfg, 'altitude_vz_goal', 0.0) # [m/s]
260
261 # Current state
262 h = nav.h
263 vz = nav.vz
264 theta = nav.theta
265
266 # Mass estimate (use provided or default)
267 if nav.mass_estimate is not None:
268 m = nav.mass_estimate
269 else:
270 # Default mass (could be in cfg)
271 m = getattr(cfg, 'vehicle_mass', 100.0) # [kg]
272
273 # Compute acceleration command
274 h_error = altitude_goal - h
275 vz_error = vz_goal - vz
276 a_cmd = kp * h_error + kd * vz_error
277
278 # Compute desired thrust
279 # F = m * (a_cmd + g) / cos(theta)
280 # Protect against division by zero (theta near 90 deg)
281 cos_theta = np.cos(theta)
282 if abs(cos_theta) < 0.1: # ~84 deg
283 cos_theta = np.sign(cos_theta) * 0.1 # Clamp to prevent extreme values
284
285 F_des_base = m * (a_cmd + GRAVITY) / cos_theta
286
287 # Clamp to reasonable range
288 F_des_base = np.clip(F_des_base, 0.0, 1e6) # [N]
289
290 # For horizon, use constant or decaying command
291 # Simple: constant for now (could add trajectory following)
292 F_des = np.ones(horizon_N, dtype=np.float64) * F_des_base
293
294 return F_des
295
296
298 state: np.ndarray,
299 meas: Measurement,
300 cfg: ControllerConfig,
301 engine_wrapper: Optional[EngineWrapper],
302 eng_est_current: Optional[EngineEstimate],
303) -> Tuple[float, float]:
304 """
305 Estimate feasible thrust bounds [F_min, F_max].
306
307 Parameters:
308 -----------
309 state : np.ndarray
310 Current state vector
311 meas : Measurement
312 Current measurements
313 cfg : ControllerConfig
314 Controller configuration
315 engine_wrapper : EngineWrapper, optional
316 Engine wrapper for performance estimation
317 eng_est_current : EngineEstimate, optional
318 Current engine estimate
319
320 Returns:
321 --------
322 F_min : float
323 Minimum feasible thrust [N]
324 F_max : float
325 Maximum feasible thrust [N]
326 """
327 # Minimum thrust: near zero (or minimum stable thrust)
328 F_min = 0.0
329
330 # Maximum thrust: depends on:
331 # 1. Current tank/ullage pressures (P_u_F, P_u_O) - used for engine evaluation
332 # Feed pressures (P_d_F, P_d_O) lag behind and are downstream
333 # 2. Actuation headroom (P_reg - P_u_i)
334 # 3. COPV pressure (for pressurization)
335
336 # Estimate from current engine performance
337 if eng_est_current is not None:
338 F_current = eng_est_current.F
339 else:
340 # Fallback: estimate from pressures (rough approximation)
341 # Note: Using feed pressures here is approximate; ideally would use tank pressures
342 # Simple model: F ~ sqrt(P * P) * constant
343 F_current = 1000.0 * np.sqrt(meas.P_u_fuel * meas.P_u_ox) / 1e6
344
345 # Check headroom for actuation
346 headroom_F = meas.P_reg - meas.P_u_fuel
347 headroom_O = meas.P_reg - meas.P_u_ox
348
349 # If headroom is low, limit maximum thrust
350 headroom_factor = 1.0
351 if headroom_F < cfg.headroom_dp_min:
352 headroom_factor = min(headroom_factor, headroom_F / cfg.headroom_dp_min)
353 if headroom_O < cfg.headroom_dp_min:
354 headroom_factor = min(headroom_factor, headroom_O / cfg.headroom_dp_min)
355
356 # Estimate maximum thrust
357 # Conservative: use current thrust * headroom_factor * safety margin
358 F_max = F_current * headroom_factor * 1.5 # 50% headroom
359
360 # Also check COPV pressure (need enough for pressurization)
361 if meas.P_copv < cfg.P_copv_min * 1.5: # Need some margin
362 # Reduce max thrust if COPV is low
363 copv_factor = (meas.P_copv - cfg.P_copv_min) / (cfg.P_copv_min * 0.5)
364 copv_factor = np.clip(copv_factor, 0.1, 1.0)
365 F_max *= copv_factor
366
367 # Clamp to reasonable range
368 F_max = max(F_max, F_min + 100.0) # At least 100 N range
369 F_max = min(F_max, 1e6) # Cap at 1 MN (very large engine)
370
371 return F_min, F_max
372
373
375 F_des: float,
376 F_min: float,
377 F_max: float,
378 F_prev: float,
379 dt: float,
380 cfg: ControllerConfig,
381) -> float:
382 """
383 Project desired thrust to feasible reference with slew rate limiting.
384
385 Uses sequential clipping:
386 1. Clamp to bounds: F = clip(F_des, [F_min, F_max])
387 2. Limit slew rate: F = clip(F, [F_prev - dF_max*dt, F_prev + dF_max*dt])
388
389 Parameters:
390 -----------
391 F_des : float
392 Desired thrust [N]
393 F_min : float
394 Minimum feasible thrust [N]
395 F_max : float
396 Maximum feasible thrust [N]
397 F_prev : float
398 Previous reference thrust [N]
399 dt : float
400 Time step [s]
401 cfg : ControllerConfig
402 Controller configuration
403
404 Returns:
405 --------
406 F_ref : float
407 Projected reference thrust [N]
408 """
409 # Maximum slew rate (default: 10 kN/s)
410 dF_max = getattr(cfg, 'thrust_slew_max', 10000.0) # [N/s]
411
412 # Step 1: Clamp to bounds
413 F_clamped = np.clip(F_des, F_min, F_max)
414
415 # Step 2: Limit slew rate
416 dF_max_step = dF_max * dt
417 F_ref = np.clip(F_clamped, F_prev - dF_max_step, F_prev + dF_max_step)
418
419 # Step 3: Re-clamp to bounds (in case slew limit pushed out)
420 F_ref = np.clip(F_ref, F_min, F_max)
421
422 return float(F_ref)
423
424
425def _measurement_to_state(meas: Measurement) -> np.ndarray:
426 """
427 Convert measurement to state vector (simplified).
428
429 Parameters:
430 -----------
431 meas : Measurement
432 Sensor measurements
433
434 Returns:
435 --------
436 state : np.ndarray, shape (N_STATE,)
437 State vector (partial, for bounds estimation)
438 """
439 from .dynamics import N_STATE
440
441 state = np.zeros(N_STATE, dtype=np.float64)
442 # Fill in pressure states (others would need additional info)
443 # This is a simplified version for bounds estimation
444 return state
445
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
Reference build_reference(NavState nav, Measurement meas, Command cmd, ControllerConfig cfg, int horizon_N, Optional[EngineWrapper] engine_wrapper=None, Optional[float] F_ref_prev=None, Optional[float] dt=None)
Definition reference.py:44
np.ndarray _compute_thrust_command(Command cmd, int horizon_N, float dt)
Definition reference.py:161
np.ndarray _compute_altitude_command(NavState nav, float altitude_goal, int horizon_N, float dt, ControllerConfig cfg)
Definition reference.py:229
Tuple[float, float] _estimate_thrust_bounds(np.ndarray state, Measurement meas, ControllerConfig cfg, Optional[EngineWrapper] engine_wrapper, Optional[EngineEstimate] eng_est_current)
Definition reference.py:303
float _project_thrust(float F_des, float F_min, float F_max, float F_prev, float dt, ControllerConfig cfg)
Definition reference.py:381
np.ndarray _measurement_to_state(Measurement meas)
Definition reference.py:425