1"""Reference generation for robust DDP controller.
4- A) Thrust command: user provides F_des(t) or piecewise schedule
5- B) Altitude command: compute desired thrust from PD guidance
7Computes feasible reference projection with bounds and slew rate limiting.
10from __future__
import annotations
12from typing
import Optional, Union, List, Tuple, Dict, Any
13from dataclasses
import dataclass
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
23 """Reference trajectory for DDP controller."""
39 cfg: ControllerConfig,
41 engine_wrapper: Optional[EngineWrapper] =
None,
42 F_ref_prev: Optional[float] =
None,
43 dt: Optional[float] =
None,
46 Build reference trajectory for DDP controller.
51 Current navigation state (altitude, velocity, tilt, mass)
53 Current sensor measurements (pressures)
55 Control command (thrust desired or altitude goal)
56 cfg : ControllerConfig
57 Controller configuration
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)
65 Time step [s] (defaults to cfg.dt)
70 Reference trajectory with F_ref, MR_ref, bounds, and feasibility
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)
83 if cmd.command_type == CommandType.THRUST_DESIRED:
85 elif cmd.command_type == CommandType.ALTITUDE_GOAL:
87 nav, cmd.altitude_goal, horizon_N, dt, cfg
90 raise ValueError(f
"Unknown command type: {cmd.command_type}")
98 eng_est_current =
None
99 if engine_wrapper
is not None:
103 eng_est_current = engine_wrapper.estimate_from_pressures(
104 meas.P_u_fuel, meas.P_u_ox
112 current_state, meas, cfg, engine_wrapper, eng_est_current
120 F_ref_prev
if F_ref_prev
is not None else F_des_seq[0],
127 for k
in range(1, horizon_N):
138 MR_mid = (cfg.MR_min + cfg.MR_max) / 2.0
146 feasible[:] = (F_ref >= F_min) & (F_ref <= F_max)
163 Compute thrust command sequence from Command.
168 Command with thrust_desired
176 F_des : np.ndarray, shape (horizon_N,)
177 Desired thrust sequence [N]
179 if cmd.command_type != CommandType.THRUST_DESIRED:
180 raise ValueError(
"Command must be THRUST_DESIRED")
182 if cmd.thrust_desired
is None:
183 raise ValueError(
"thrust_desired must be provided")
185 F_des = np.zeros(horizon_N, dtype=np.float64)
187 if isinstance(cmd.thrust_desired, (int, float)):
189 F_des[:] = float(cmd.thrust_desired)
190 elif isinstance(cmd.thrust_desired, list):
192 times = np.array([t
for t, _
in cmd.thrust_desired])
193 thrusts = np.array([F
for _, F
in cmd.thrust_desired])
196 t_horizon = np.arange(horizon_N) * dt
200 raise ValueError(
"thrust_desired list is empty")
203 for k
in range(horizon_N):
208 F_des[k] = thrusts[0]
209 elif t_k >= times[-1]:
210 F_des[k] = thrusts[-1]
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)
218 raise ValueError(f
"Unsupported thrust_desired type: {type(cmd.thrust_desired)}")
225 altitude_goal: float,
228 cfg: ControllerConfig,
231 Compute desired thrust from PD guidance for altitude command.
233 Uses: a_cmd = kp*(h_goal - h) + kd*(vz_goal - vz)
234 F_des = m*(a_cmd + g) / cos(theta)
239 Current navigation state
240 altitude_goal : float
246 cfg : ControllerConfig
247 Controller configuration (for PD gains if available)
251 F_des : np.ndarray, shape (horizon_N,)
252 Desired thrust sequence [N]
255 kp = getattr(cfg,
'altitude_kp', 0.5)
256 kd = getattr(cfg,
'altitude_kd', 0.3)
259 vz_goal = getattr(cfg,
'altitude_vz_goal', 0.0)
267 if nav.mass_estimate
is not None:
268 m = nav.mass_estimate
271 m = getattr(cfg,
'vehicle_mass', 100.0)
274 h_error = altitude_goal - h
275 vz_error = vz_goal - vz
276 a_cmd = kp * h_error + kd * vz_error
281 cos_theta = np.cos(theta)
282 if abs(cos_theta) < 0.1:
283 cos_theta = np.sign(cos_theta) * 0.1
285 F_des_base = m * (a_cmd + GRAVITY) / cos_theta
288 F_des_base = np.clip(F_des_base, 0.0, 1e6)
292 F_des = np.ones(horizon_N, dtype=np.float64) * F_des_base
300 cfg: ControllerConfig,
301 engine_wrapper: Optional[EngineWrapper],
302 eng_est_current: Optional[EngineEstimate],
303) -> Tuple[float, float]:
305 Estimate feasible thrust bounds [F_min, F_max].
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
323 Minimum feasible thrust [N]
325 Maximum feasible thrust [N]
337 if eng_est_current
is not None:
338 F_current = eng_est_current.F
343 F_current = 1000.0 * np.sqrt(meas.P_u_fuel * meas.P_u_ox) / 1e6
346 headroom_F = meas.P_reg - meas.P_u_fuel
347 headroom_O = meas.P_reg - meas.P_u_ox
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)
358 F_max = F_current * headroom_factor * 1.5
361 if meas.P_copv < cfg.P_copv_min * 1.5:
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)
368 F_max =
max(F_max, F_min + 100.0)
369 F_max =
min(F_max, 1e6)
380 cfg: ControllerConfig,
383 Project desired thrust to feasible reference with slew rate limiting.
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])
394 Minimum feasible thrust [N]
396 Maximum feasible thrust [N]
398 Previous reference thrust [N]
401 cfg : ControllerConfig
402 Controller configuration
407 Projected reference thrust [N]
410 dF_max = getattr(cfg,
'thrust_slew_max', 10000.0)
413 F_clamped = np.clip(F_des, F_min, F_max)
416 dF_max_step = dF_max * dt
417 F_ref = np.clip(F_clamped, F_prev - dF_max_step, F_prev + dF_max_step)
420 F_ref = np.clip(F_ref, F_min, F_max)
427 Convert measurement to state vector (simplified).
436 state : np.ndarray, shape (N_STATE,)
437 State vector (partial, for bounds estimation)
439 from .dynamics
import N_STATE
441 state = np.zeros(N_STATE, dtype=np.float64)
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)
np.ndarray _compute_thrust_command(Command cmd, int horizon_N, float dt)
np.ndarray _compute_altitude_command(NavState nav, float altitude_goal, int horizon_N, float dt, ControllerConfig cfg)
Tuple[float, float] _estimate_thrust_bounds(np.ndarray state, Measurement meas, ControllerConfig cfg, Optional[EngineWrapper] engine_wrapper, Optional[EngineEstimate] eng_est_current)
float _project_thrust(float F_des, float F_min, float F_max, float F_prev, float dt, ControllerConfig cfg)
np.ndarray _measurement_to_state(Measurement meas)