STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
RobustDDPController.cpp
Go to the documentation of this file.
1#include "../include/control/RobustDDPController.hpp"
2
3#include <algorithm>
4#include <cmath>
5#include <iostream>
6#include <limits>
7#include <mutex>
8#include <numeric>
9
10namespace fsw {
11namespace control {
12
13// ═══════════════════════════════════════════════════════════════════════
14// CONSTRUCTION / INIT / RESET
15// ═══════════════════════════════════════════════════════════════════════
16
18 state_.x = Eigen::VectorXd::Zero(N_STATE);
19 state_.w_bar = Eigen::VectorXd::Zero(N_STATE);
20 state_.V_u_F = 0.01;
21 state_.V_u_O = 0.01;
22}
23
25
28 reset();
29 return true;
30}
31
33 std::lock_guard<std::mutex> lock(state_mutex_);
34 state_.x = Eigen::VectorXd::Zero(N_STATE);
35 state_.w_bar = Eigen::VectorXd::Zero(N_STATE);
36 state_.V_u_F = config_.V_u_F_init;
37 state_.V_u_O = config_.V_u_O_init;
38 x_prev_.reset();
39 u_prev_applied_.reset();
40 u_seq_prev_.reset();
41 tick_ = 0;
42}
43
44// ═══════════════════════════════════════════════════════════════════════
45// MAIN CONTROL STEP (§9 pipeline)
46// ═══════════════════════════════════════════════════════════════════════
47
48std::pair<RobustDDPController::ActuationCommand, RobustDDPController::Diagnostics>
49RobustDDPController::step(const Measurement& meas, const NavState& nav, const Command& cmd) {
50 std::lock_guard<std::mutex> lock(state_mutex_);
51
52 Diagnostics diag;
53 diag.safety_filtered = false;
54
55 // 1. Build state
56 Eigen::VectorXd x = buildState(meas);
57
58 // 2. Build reference
59 Reference ref = buildReference(nav, meas, cmd);
60 diag.F_ref = ref.F_ref.empty() ? 0.0 : ref.F_ref[0];
61 diag.MR_ref = ref.MR_ref.empty() ? 0.0 : ref.MR_ref[0];
62
63 // 3. Update robustness bounds w_bar ← ρ·w_bar + (1−ρ)·|x_meas − x_pred|
64 if (x_prev_.has_value() && u_prev_applied_.has_value()) {
65 auto eng = estimateEngine(x_prev_.value()(IDX_P_D_F), x_prev_.value()(IDX_P_D_O));
66 Eigen::VectorXd x_pred =
67 dynamicsStep(x_prev_.value(), u_prev_applied_.value(), eng.mdot_F, eng.mdot_O);
68 Eigen::VectorXd residual = (x - x_pred).cwiseAbs();
69 state_.w_bar = config_.rho * state_.w_bar + (1.0 - config_.rho) * residual;
70 state_.w_bar *= (1.0 + config_.eta);
71 }
72
73 // 4. Supervisory cutoff (§12)
74 if (shouldCutoff(nav)) {
75 diag.cutoff_active = true;
76 ActuationCommand act{};
77 act.duty_F = 0.0;
78 act.duty_O = 0.0;
79 act.u_F_on = false;
80 act.u_O_on = false;
81 act.valid = true;
82 x_prev_ = x;
83 u_prev_applied_ = Eigen::VectorXd::Zero(N_CONTROL);
84 ++tick_;
85 return {act, diag};
86 }
87
88 // 5. Solve — use enumerative MPC for binary valves (§10)
89 // then refine with DDP for PWM (§9)
90 Eigen::VectorXd u_binary = enumerativeMPC(x, ref, 0);
91
92 // Also try economic decision logic (§11) as candidate
93 Eigen::VectorXd u_econ = economicDecision(x, ref, 0);
94
95 // Pick best of enumerative and economic
96 // eng_bin used implicitly inside runningCost
97 double cost_bin = runningCost(x, u_binary, ref, 0,
98 u_prev_applied_.value_or(Eigen::VectorXd::Zero(N_CONTROL)));
99 double cost_econ =
100 runningCost(x, u_econ, ref, 0, u_prev_applied_.value_or(Eigen::VectorXd::Zero(N_CONTROL)));
101 Eigen::VectorXd u_relaxed = (cost_bin <= cost_econ) ? u_binary : u_econ;
102
103 // Optionally refine with DDP if horizon > 1
104 if (config_.N > 1 && config_.max_iterations > 0) {
105 Eigen::MatrixXd u_init(config_.N, N_CONTROL);
106 if (u_seq_prev_.has_value()) {
107 // Warm start: shift previous solution
108 u_init.topRows(config_.N - 1) = u_seq_prev_->bottomRows(config_.N - 1);
109 u_init.row(config_.N - 1) = u_seq_prev_->row(config_.N - 1);
110 } else {
111 for (int i = 0; i < config_.N; ++i)
112 u_init.row(i) = u_relaxed.transpose();
113 }
114
115 DDPSolution sol = solveDDP(x, ref, u_init);
116 diag.cost = sol.cost;
117 diag.solver_iters = sol.iterations;
118
119 // Only use DDP solution if it improves over greedy
120 double cost_ddp = runningCost(x, sol.u_seq.row(0).transpose(), ref, 0,
121 u_prev_applied_.value_or(Eigen::VectorXd::Zero(N_CONTROL)));
122 if (cost_ddp < cost_bin && cost_ddp < cost_econ) {
123 u_relaxed = sol.u_seq.row(0).transpose();
124 }
125 u_seq_prev_ = sol.u_seq;
126 }
127
128 // 6. Safety filter (§9.3)
129 Eigen::VectorXd u_safe = filterAction(x, u_relaxed, ref, 0);
130 if ((u_safe - u_relaxed).norm() > 1e-6)
131 diag.safety_filtered = true;
132
133 // 7. Actuation
135
136 // Engine estimate for diagnostics
138 diag.F_estimated = eng.F;
139 diag.MR_estimated = eng.MR;
140 diag.P_ch = eng.P_ch;
141
142 if (ref.type == CommandType::PRESSURE_TARGET && tick_ % 10 == 0) {
143 std::cout << "[PRESSURE_MODE] tick=" << tick_ << " P_f_d=" << x(IDX_P_D_F) / 6894.76
144 << "psi / " << cmd.P_fuel_target / 6894.76 << "psi" << " u_safe=[" << u_safe(0)
145 << ", " << u_safe(1) << "]" << " cost_bin=" << cost_bin
146 << " cost_econ=" << cost_econ << "\n";
147 }
148
149 // Store for next tick
150 x_prev_ = x;
151 u_prev_applied_ = u_safe;
152 ++tick_;
153
154 return {act, diag};
155}
156
158 std::lock_guard<std::mutex> lock(state_mutex_);
159 return state_;
160}
161
162// ═══════════════════════════════════════════════════════════════════════
163// DYNAMICS (§4: Blowdown Ullage Dynamics + §5: COPV Gas)
164// ═══════════════════════════════════════════════════════════════════════
165
166Eigen::VectorXd RobustDDPController::dynamicsStep(const Eigen::VectorXd& x,
167 const Eigen::VectorXd& u, double mdot_F,
168 double mdot_O) const {
169 const double dt = config_.dt;
170
171 double P_copv = x(IDX_P_COPV);
172 // P_reg derived from COPV, not directly from state
173 double P_u_F = x(IDX_P_U_F);
174 double P_u_O = x(IDX_P_U_O);
175 double P_d_F = x(IDX_P_D_F);
176 double P_d_O = x(IDX_P_D_O);
177 double V_u_F = x(IDX_V_U_F);
178 double V_u_O = x(IDX_V_U_O);
179 double m_copv = x(IDX_M_GAS_COPV);
180 double m_F = x(IDX_M_GAS_F);
181 double m_O = x(IDX_M_GAS_O);
182
183 double u_F = std::clamp(u(0), 0.0, 1.0);
184 double u_O = std::clamp(u(1), 0.0, 1.0);
185 double u_total = std::max(u_F, u_O);
186
187 const double T = config_.T_gas_init;
188 const double n = config_.n_polytropic;
189
190 // ── COPV gas flow (choked / subsonic) ──────────────────────────────
191 double P_reg_eff = (config_.reg_setpoint > 0 && P_copv >= config_.reg_setpoint)
192 ? config_.reg_setpoint
193 : config_.reg_ratio * P_copv;
194
195 double mdot_gas_reg = 0.0;
196 if (u_total > 1e-6 && P_copv > P_reg_eff) {
197 double pr_crit = std::pow(2.0 / (GAMMA_N2 + 1.0), GAMMA_N2 / (GAMMA_N2 - 1.0));
198 double pr = P_reg_eff / P_copv;
199 double Cd_reg = 0.7, A_reg = 2e-5;
200 if (pr < pr_crit) {
201 // choked
202 double C_star = std::sqrt(
203 GAMMA_N2 * std::pow(2.0 / (GAMMA_N2 + 1.0), (GAMMA_N2 + 1.0) / (GAMMA_N2 - 1.0)));
204 mdot_gas_reg = Cd_reg * A_reg * P_copv / std::sqrt(R_GAS_N2 * T) * C_star;
205 } else {
206 double t1 = (2.0 * GAMMA_N2) / ((GAMMA_N2 - 1.0) * R_GAS_N2 * T);
207 double t2 = std::pow(pr, 2.0 / GAMMA_N2) - std::pow(pr, (GAMMA_N2 + 1.0) / GAMMA_N2);
208 mdot_gas_reg = Cd_reg * A_reg * P_copv * std::sqrt(t1 * std::max(t2, 0.0));
209 }
210 mdot_gas_reg *= u_total;
211 mdot_gas_reg = std::clamp(mdot_gas_reg, 0.0, 0.2);
212 }
213
214 // ── Gas flow to tanks (proportional to valve duty) ─────────────────
215 auto calc_tank_flow = [&](double u_v, double P_tank) -> double {
216 if (u_v < 1e-6 || P_reg_eff <= P_tank)
217 return 0.0;
218 double pr = P_tank / P_reg_eff;
219 double pr_crit = std::pow(2.0 / (GAMMA_N2 + 1.0), GAMMA_N2 / (GAMMA_N2 - 1.0));
220 double Cd_v = 0.65, A_v = 5e-5;
221 double flow;
222 if (pr < pr_crit) {
223 double C_star = std::sqrt(
224 GAMMA_N2 * std::pow(2.0 / (GAMMA_N2 + 1.0), (GAMMA_N2 + 1.0) / (GAMMA_N2 - 1.0)));
225 flow = Cd_v * A_v * P_reg_eff / std::sqrt(R_GAS_N2 * T) * C_star;
226 } else {
227 double t1 = (2.0 * GAMMA_N2) / ((GAMMA_N2 - 1.0) * R_GAS_N2 * T);
228 double t2 = std::pow(pr, 2.0 / GAMMA_N2) - std::pow(pr, (GAMMA_N2 + 1.0) / GAMMA_N2);
229 flow = Cd_v * A_v * P_reg_eff * std::sqrt(t1 * std::max(t2, 0.0));
230 }
231 flow *= u_v;
232 // mass-conservation clamp
233 if (u_total > 1e-6 && mdot_gas_reg > 0.0)
234 flow = std::min(flow, mdot_gas_reg * u_v / u_total);
235 return std::max(flow, 0.0);
236 };
237
238 double mdot_gas_F = calc_tank_flow(u_F, P_u_F);
239 double mdot_gas_O = calc_tank_flow(u_O, P_u_O);
240
241 // COPV loss
242 double mdot_loss = std::clamp(config_.copv_loss * config_.V_copv / (R_GAS_N2 * T), 0.0, 0.01);
243 double mdot_total = mdot_gas_reg + mdot_loss;
244
245 // ── Update gas masses ──────────────────────────────────────────────
246 double m_copv_next = std::max(m_copv - dt * mdot_total, 0.0);
247 double m_F_next = std::max(m_F + dt * mdot_gas_F, 0.0);
248 double m_O_next = std::max(m_O + dt * mdot_gas_O, 0.0);
249
250 // ── COPV pressure (ideal gas + polytropic T) ───────────────────────
251 double P_copv_next = 0.0;
252 if (config_.V_copv > 1e-10) {
253 double T_copv = T;
254 if (m_copv > 1e-10) {
255 double rho_ratio = m_copv_next / m_copv;
256 T_copv = T * std::pow(std::max(rho_ratio, 1e-6), n - 1.0);
257 T_copv = std::clamp(T_copv, 200.0, 400.0);
258 }
259 P_copv_next = m_copv_next * R_GAS_N2 * T_copv / config_.V_copv;
260 }
261 P_copv_next = std::max(P_copv_next, 0.0);
262
263 // ── Regulator pressure ─────────────────────────────────────────────
264 double P_reg_next;
265 if (config_.reg_setpoint > 0 && P_copv_next >= config_.reg_setpoint)
266 P_reg_next = config_.reg_setpoint;
267 else
268 P_reg_next = config_.reg_ratio * P_copv_next;
269
270 // ── Ullage volume (§4.1) ───────────────────────────────────────────
271 double mdot_F_safe = std::max(mdot_F, 0.0);
272 double mdot_O_safe = std::max(mdot_O, 0.0);
273
274 // Pressure-dependent scaling for exponential blowdown
275 const double P_ref = 5e6;
276 double scale_F = std::sqrt(std::max(P_u_F, 1e5) / P_ref);
277 double scale_O = std::sqrt(std::max(P_u_O, 1e5) / P_ref);
278 scale_F = std::clamp(scale_F, 0.1, 2.0);
279 scale_O = std::clamp(scale_O, 0.1, 2.0);
280
281 if (u_total < 1e-6) { // pure blowdown — more aggressive
282 scale_F *= 1.5;
283 scale_O *= 1.5;
284 }
285
286 double V_u_F_next = V_u_F + dt * mdot_F_safe * scale_F / config_.rho_F;
287 double V_u_O_next = V_u_O + dt * mdot_O_safe * scale_O / config_.rho_O;
288 V_u_F_next = std::max(V_u_F_next, 0.0);
289 V_u_O_next = std::max(V_u_O_next, 0.0);
290
291 // ── Ullage pressures P = m·R·T / V ───────────────────────────────
292 auto ullage_P = [&](double m_gas, double V_u) {
293 if (V_u < 1e-10)
294 return 0.0;
295 return std::max(m_gas * R_GAS_N2 * T / V_u, 0.0);
296 };
297 double P_u_F_next = ullage_P(m_F_next, V_u_F_next);
298 double P_u_O_next = ullage_P(m_O_next, V_u_O_next);
299
300 // ── Feed pressures (first-order lag) ──────────────────────────────
301 double P_d_F_next = P_d_F + dt * (P_u_F - P_d_F) / (config_.tau_line_F * 2.0);
302 double P_d_O_next = P_d_O + dt * (P_u_O - P_d_O) / (config_.tau_line_O * 2.0);
303
304 // ── Assemble ───────────────────────────────────────────────────────
305 Eigen::VectorXd x_next(N_STATE);
306 x_next << P_copv_next, P_reg_next, P_u_F_next, P_u_O_next, P_d_F_next, P_d_O_next, V_u_F_next,
307 V_u_O_next, m_copv_next, m_F_next, m_O_next;
308 return x_next;
309}
310
311void RobustDDPController::linearize(const Eigen::VectorXd& x, const Eigen::VectorXd& u,
312 double mdot_F, double mdot_O, Eigen::MatrixXd& A,
313 Eigen::MatrixXd& B) const {
314 const double eps = 1e-6;
315 Eigen::VectorXd x_nom = dynamicsStep(x, u, mdot_F, mdot_O);
316
317 A.resize(N_STATE, N_STATE);
318 for (int i = 0; i < N_STATE; ++i) {
319 Eigen::VectorXd xp = x;
320 xp(i) += eps;
321 A.col(i) = (dynamicsStep(xp, u, mdot_F, mdot_O) - x_nom) / eps;
322 }
323
324 B.resize(N_STATE, N_CONTROL);
325 for (int i = 0; i < N_CONTROL; ++i) {
326 Eigen::VectorXd up = u;
327 up(i) = std::clamp(up(i) + eps, 0.0, 1.0);
328 B.col(i) = (dynamicsStep(x, up, mdot_F, mdot_O) - x_nom) / eps;
329 }
330}
331
332// ═══════════════════════════════════════════════════════════════════════
333// ENGINE MODEL (§6)
334// ═══════════════════════════════════════════════════════════════════════
335
337 double P_d_ox) const {
338 EngineEstimate est;
339
340 // Injector mass flow (§6.1): ṁ = Cd·A·√(2·ρ·ΔP)
341 // For a simple model, assume chamber pressure from supply pressures
342 // Iterative root-find (bracketed) for Pch: ṁF + ṁO = Pch·At / c*
343 double P_ch_lo = 0.0;
344 double P_ch_hi = std::min(P_d_fuel, P_d_ox);
345 if (P_ch_hi < 1e3) {
346 est.valid = false;
347 return est;
348 }
349
350 auto mdot_at_Pch = [&](double P_ch) -> std::pair<double, double> {
351 double dP_F = std::max(P_d_fuel - P_ch, 0.0);
352 double dP_O = std::max(P_d_ox - P_ch, 0.0);
353 double mF = config_.Cd_fuel * config_.A_inj_fuel * std::sqrt(2.0 * config_.rho_F * dP_F);
354 double mO = config_.Cd_ox * config_.A_inj_ox * std::sqrt(2.0 * config_.rho_O * dP_O);
355 return {mF, mO};
356 };
357
358 // Chamber closure: ṁF + ṁO = Pch · At / c* (§6.3)
359 auto residual = [&](double P_ch) -> double {
360 auto [mF, mO] = mdot_at_Pch(P_ch);
361 double mdot_total = mF + mO;
362 double mdot_nozzle = P_ch * config_.A_throat / config_.c_star;
363 return mdot_total - mdot_nozzle;
364 };
365
366 // Bisection root-find
367 for (int i = 0; i < 30; ++i) {
368 double P_mid = 0.5 * (P_ch_lo + P_ch_hi);
369 if (residual(P_mid) > 0)
370 P_ch_lo = P_mid;
371 else
372 P_ch_hi = P_mid;
373 if (P_ch_hi - P_ch_lo < 10.0)
374 break; // 10 Pa tolerance
375 }
376 double P_ch = 0.5 * (P_ch_lo + P_ch_hi);
377
378 auto [mF, mO] = mdot_at_Pch(P_ch);
379
380 est.P_ch = P_ch;
381 est.mdot_F = mF;
382 est.mdot_O = mO;
383 est.MR = (mF > 1e-8) ? mO / mF : 0.0;
384 est.F = config_.Cf * P_ch * config_.A_throat; // §6.4
385 est.dp_F = std::max(P_d_fuel - P_ch, 0.0);
386 est.dp_O = std::max(P_d_ox - P_ch, 0.0);
387 est.valid = (P_ch > 1e3 && std::isfinite(est.F));
388 return est;
389}
390
391// ═══════════════════════════════════════════════════════════════════════
392// REFERENCE GENERATION (§8)
393// ═══════════════════════════════════════════════════════════════════════
394
396 const Measurement& /* meas */,
397 const Command& cmd) const {
398 Reference ref;
399 ref.type = cmd.type;
400 ref.F_ref.resize(config_.N);
401 ref.MR_ref.resize(config_.N);
402 ref.P_fuel_ref.resize(config_.N);
403 ref.P_ox_ref.resize(config_.N);
404
405 double F_target = 0.0;
406 double MR_target = 2.0; // nominal MR
407 double P_f_target = 0.0;
408 double P_o_target = 0.0;
409
411 F_target = cmd.thrust_desired;
412 } else if (cmd.type == CommandType::PRESSURE_TARGET) {
413 F_target = cmd.thrust_desired; // Secondary, maybe 0
414 P_f_target = cmd.P_fuel_target;
415 P_o_target = cmd.P_ox_target;
416 } else {
417 // Altitude-based (§8.1): a_z = kp·(h*−h) + kd·(v*−vz)
418 double kp = 1.0, kd = 2.0;
419 double az_des = kp * (cmd.altitude_goal - nav.h) + kd * (0.0 - nav.vz);
420 double cos_theta = std::cos(nav.theta);
421 if (cos_theta < 0.1)
422 cos_theta = 0.1;
423 F_target = nav.mass * (az_des + G0) / cos_theta;
424 F_target = std::max(F_target, 0.0);
425 }
426
427 for (int i = 0; i < config_.N; ++i) {
428 ref.F_ref[i] = F_target;
429 ref.MR_ref[i] = MR_target;
430 ref.P_fuel_ref[i] = P_f_target;
431 ref.P_ox_ref[i] = P_o_target;
432 }
433 return ref;
434}
435
436// ═══════════════════════════════════════════════════════════════════════
437// BUILD STATE
438// ═══════════════════════════════════════════════════════════════════════
439
440Eigen::VectorXd RobustDDPController::buildState(const Measurement& meas) const {
441 Eigen::VectorXd x(N_STATE);
442 x(IDX_P_COPV) = meas.P_copv;
443 x(IDX_P_REG) = meas.P_reg;
444 x(IDX_P_U_F) = meas.P_u_fuel;
445 x(IDX_P_U_O) = meas.P_u_ox;
446 x(IDX_P_D_F) = meas.P_d_fuel;
447 x(IDX_P_D_O) = meas.P_d_ox;
448 x(IDX_V_U_F) = state_.V_u_F;
449 x(IDX_V_U_O) = state_.V_u_O;
450
451 const double T = config_.T_gas_init;
452 x(IDX_M_GAS_COPV) = meas.P_copv * config_.V_copv / (R_GAS_N2 * T);
453 x(IDX_M_GAS_F) = (state_.V_u_F > 1e-10) ? meas.P_u_fuel * state_.V_u_F / (R_GAS_N2 * T) : 0.0;
454 x(IDX_M_GAS_O) = (state_.V_u_O > 1e-10) ? meas.P_u_ox * state_.V_u_O / (R_GAS_N2 * T) : 0.0;
455 return x;
456}
457
458// ═══════════════════════════════════════════════════════════════════════
459// CONSTRAINTS (§7)
460// ═══════════════════════════════════════════════════════════════════════
461
462bool RobustDDPController::isStateSafe(const Eigen::VectorXd& x, CommandType cmd_type) const {
463 // §5.2 Hard gas constraint: P_copv ≥ P_copv_min
464 if (x(IDX_P_COPV) < config_.P_copv_min)
465 return false;
466 // §7 pressure bounds
467 if (x(IDX_P_U_F) > config_.P_u_max)
468 return false;
469 if (x(IDX_P_U_O) > config_.P_u_max)
470 return false;
471
472 // Safety checks on engine are somewhat coupled to thrust mode.
473 // If not valid, it's safe (e.g. pressure too low for combustion).
474 auto eng = estimateEngine(x(IDX_P_D_F), x(IDX_P_D_O));
475 if (!eng.valid)
476 return true; // can't evaluate → assume safe
477
478 if (cmd_type != CommandType::PRESSURE_TARGET) {
479 // §7.1 MR bounds
480 if (eng.MR < config_.MR_min || eng.MR > config_.MR_max)
481 return false;
482 // §7.2 Injector stiffness: ΔP ≥ ε·Pch
483 if (eng.dp_F < config_.injector_dp_frac * eng.P_ch)
484 return false;
485 if (eng.dp_O < config_.injector_dp_frac * eng.P_ch)
486 return false;
487 }
488
489 return true;
490}
491
492// ═══════════════════════════════════════════════════════════════════════
493// DDP SOLVER (§9 — iLQR-style finite-horizon DDP)
494// ═══════════════════════════════════════════════════════════════════════
495
496double RobustDDPController::runningCost(const Eigen::VectorXd& x, const Eigen::VectorXd& u,
497 const Reference& ref, int k,
498 const Eigen::VectorXd& u_prev) const {
499 auto eng = estimateEngine(x(IDX_P_D_F), x(IDX_P_D_O));
500 double cost = 0.0;
501
503 // Pressure control mode: penalize deviation from target tank pressures
504 double P_f_err = (x(IDX_P_D_F) - ref.P_fuel_ref[k]) / 1e5; // scale by 1 bar for tuning
505 double P_o_err = (x(IDX_P_D_O) - ref.P_ox_ref[k]) / 1e5;
506
507 // High penalty to overcome gas cost/switching cost
508 cost += 1e4 * P_f_err * P_f_err;
509 cost += 1e4 * P_o_err * P_o_err;
510 } else {
511 // Thrust/MR control mode
512 if (eng.valid) {
513 double F_ref = ref.F_ref[k];
514 double MR_ref = ref.MR_ref[k];
515 double F_err = (eng.F - F_ref) / std::max(std::abs(F_ref), 1.0);
516 double MR_err = (eng.MR - MR_ref) / std::max(std::abs(MR_ref), 1.0);
517 cost += config_.qF * F_err * F_err;
518 cost += config_.qMR * MR_err * MR_err;
519 } else if (ref.F_ref[k] > 0) {
520 cost += 1e4;
521 }
522 }
523
524 // Gas consumption
525 cost += config_.qGas * config_.dt *
526 (config_.copv_cF * u(0) + config_.copv_cO * u(1) + config_.copv_loss);
527
528 // Switching cost
529 cost += config_.qSwitch * (u - u_prev).squaredNorm();
530
531 // Soft constraint penalties
532 if (x(IDX_P_COPV) < config_.P_copv_min)
533 cost += 1e4 * std::pow(config_.P_copv_min - x(IDX_P_COPV), 2) / 1e12;
534 if (eng.valid) {
535 if (eng.MR < config_.MR_min)
536 cost += 1e4 * std::pow(config_.MR_min - eng.MR, 2);
537 if (eng.MR > config_.MR_max)
538 cost += 1e4 * std::pow(eng.MR - config_.MR_max, 2);
539 }
540 return cost;
541}
542
544 const Eigen::VectorXd& x0, const Reference& ref, const Eigen::MatrixXd& u_init) const {
545 const int N = config_.N;
546 DDPSolution sol;
547 sol.u_seq = u_init;
548
549 Eigen::MatrixXd x_seq(N + 1, N_STATE);
550 double best_cost = std::numeric_limits<double>::infinity();
551 Eigen::MatrixXd best_u = u_init;
552
553 double reg = 1e-4;
554 const double eps = 1e-6;
555
556 for (int iter = 0; iter < config_.max_iterations; ++iter) {
557 // ── Forward rollout ────────────────────────────────────────────
558 x_seq.row(0) = x0.transpose();
559 double total_cost = 0.0;
560 Eigen::VectorXd u_p = Eigen::VectorXd::Zero(N_CONTROL);
561
562 for (int k = 0; k < N; ++k) {
563 Eigen::VectorXd xk = x_seq.row(k).transpose();
564 Eigen::VectorXd uk = sol.u_seq.row(k).transpose();
565 auto eng = estimateEngine(xk(IDX_P_D_F), xk(IDX_P_D_O));
566 total_cost += runningCost(xk, uk, ref, k, u_p);
567 u_p = uk;
568 x_seq.row(k + 1) = dynamicsStep(xk, uk, eng.mdot_F, eng.mdot_O).transpose();
569 }
570
571 if (total_cost < best_cost) {
572 best_cost = total_cost;
573 best_u = sol.u_seq;
574 }
575
576 // Convergence
577 if (iter > 0 && std::abs(total_cost - sol.cost) < config_.convergence_tol) {
578 sol.converged = true;
579 break;
580 }
581 sol.cost = total_cost;
582
583 // ── Backward pass ──────────────────────────────────────────────
584 Eigen::VectorXd Vx = Eigen::VectorXd::Zero(N_STATE);
585 Eigen::MatrixXd Vxx = Eigen::MatrixXd::Zero(N_STATE, N_STATE);
586
587 std::vector<Eigen::VectorXd> k_gains(N, Eigen::VectorXd::Zero(N_CONTROL));
588 std::vector<Eigen::MatrixXd> K_gains(N, Eigen::MatrixXd::Zero(N_CONTROL, N_STATE));
589
590 for (int k = N - 1; k >= 0; --k) {
591 Eigen::VectorXd xk = x_seq.row(k).transpose();
592 Eigen::VectorXd uk = sol.u_seq.row(k).transpose();
593 auto eng = estimateEngine(xk(IDX_P_D_F), xk(IDX_P_D_O));
594
595 Eigen::MatrixXd A, B;
596 linearize(xk, uk, eng.mdot_F, eng.mdot_O, A, B);
597
598 // Cost derivatives via finite differences
599 Eigen::VectorXd u_pr = Eigen::VectorXd::Zero(N_CONTROL);
600 if (k > 0)
601 u_pr = sol.u_seq.row(k - 1).transpose();
602 double c0 = runningCost(xk, uk, ref, k, u_pr);
603
604 Eigen::VectorXd lx(N_STATE);
605 for (int i = 0; i < N_STATE; ++i) {
606 Eigen::VectorXd xp = xk;
607 xp(i) += eps;
608 lx(i) = (runningCost(xp, uk, ref, k, u_pr) - c0) / eps;
609 }
610 Eigen::VectorXd lu(N_CONTROL);
611 for (int i = 0; i < N_CONTROL; ++i) {
612 Eigen::VectorXd up = uk;
613 up(i) = std::clamp(up(i) + eps, 0.0, 1.0);
614 lu(i) = (runningCost(xk, up, ref, k, u_pr) - c0) / eps;
615 }
616
617 // Q-function
618 Eigen::VectorXd Qx = lx + A.transpose() * Vx;
619 Eigen::VectorXd Qu = lu + B.transpose() * Vx;
620 Eigen::MatrixXd Qxx = A.transpose() * Vxx * A;
621 Eigen::MatrixXd Quu = B.transpose() * Vxx * B;
622 Eigen::MatrixXd Qux = B.transpose() * Vxx * A;
623
624 // Robustification — inflate by w_bar²
625 Qxx += Eigen::VectorXd(state_.w_bar.array().square()).asDiagonal();
626
627 // Regularize Quu
628 Quu += (reg + 1e-6) * Eigen::MatrixXd::Identity(N_CONTROL, N_CONTROL);
629
630 // Gains
631 Eigen::MatrixXd Quu_inv = Quu.inverse();
632 k_gains[k] = -Quu_inv * Qu;
633 K_gains[k] = -Quu_inv * Qux;
634
635 // Value function update
636 Vx = Qx + K_gains[k].transpose() * Quu * k_gains[k] + K_gains[k].transpose() * Qu +
637 Qux.transpose() * k_gains[k];
638 Vxx = Qxx + K_gains[k].transpose() * Quu * K_gains[k] + K_gains[k].transpose() * Qux +
639 Qux.transpose() * K_gains[k];
640 Vxx = 0.5 * (Vxx + Vxx.transpose()); // symmetrise
641 }
642
643 // ── Forward line search ────────────────────────────────────────
644 double alpha = 1.0;
645 bool improved = false;
646 while (alpha > 1e-4) {
647 Eigen::MatrixXd u_new(N, N_CONTROL);
648 Eigen::MatrixXd x_new(N + 1, N_STATE);
649 x_new.row(0) = x0.transpose();
650 double c_new = 0.0;
651 Eigen::VectorXd u_p2 = Eigen::VectorXd::Zero(N_CONTROL);
652
653 for (int k = 0; k < N; ++k) {
654 Eigen::VectorXd dx = x_new.row(k).transpose() - x_seq.row(k).transpose();
655 Eigen::VectorXd du = alpha * k_gains[k] + K_gains[k] * dx;
656 Eigen::VectorXd uk_new = sol.u_seq.row(k).transpose() + du;
657 uk_new = uk_new.cwiseMax(0.0).cwiseMin(1.0); // clamp
658 u_new.row(k) = uk_new.transpose();
659
660 auto eng = estimateEngine(x_new.row(k)(IDX_P_D_F), x_new.row(k)(IDX_P_D_O));
661 c_new += runningCost(x_new.row(k).transpose(), uk_new, ref, k, u_p2);
662 u_p2 = uk_new;
663 x_new.row(k + 1) =
664 dynamicsStep(x_new.row(k).transpose(), uk_new, eng.mdot_F, eng.mdot_O)
665 .transpose();
666 }
667
668 if (c_new < total_cost) {
670 // std::cout << "[DDP Iter " << iter << "] accepted step (alpha=" << alpha << ")
671 // cost: " << c_new << " -> u=" << u_new.row(0) << std::endl;
672 }
673 sol.u_seq = u_new;
674 improved = true;
675 break;
676 }
677 alpha *= 0.5;
678 }
679
680 if (ref.type == CommandType::PRESSURE_TARGET && !improved) {
681 // std::cout << "[DDP Iter " << iter << "] rejected all steps (reg=" << reg << ")" <<
682 // std::endl;
683 }
684
685 if (!improved)
686 reg *= 5.0;
687 else
688 reg = std::max(reg / 5.0, 1e-6);
689 if (reg > 1e6)
690 break;
691
692 sol.iterations = iter + 1;
693 }
694
695 sol.u_seq = best_u;
696 sol.cost = best_cost;
697 sol.x_seq = x_seq;
698 return sol;
699}
700
701// ═══════════════════════════════════════════════════════════════════════
702// ENUMERATIVE ROBUST MPC (§10)
703//
704// Binary valves → only 4 actions: {(0,0),(0,1),(1,0),(1,1)}.
705// Enumerate admissible sequences (respecting dwell), propagate
706// worst-case blowdown + gas loss, reject infeasible, pick min cost.
707// ═══════════════════════════════════════════════════════════════════════
708
709Eigen::VectorXd RobustDDPController::enumerativeMPC(const Eigen::VectorXd& x0, const Reference& ref,
710 int k) const {
711 // Candidate binary actions
712 static const std::array<Eigen::Vector2d, 4> actions = {
713 {{0.0, 0.0}, {0.0, 1.0}, {1.0, 0.0}, {1.0, 1.0}}};
714
715 double best_cost = std::numeric_limits<double>::infinity();
716 Eigen::VectorXd best_u = Eigen::VectorXd::Zero(N_CONTROL);
717
718 Eigen::VectorXd u_prev = u_prev_applied_.value_or(Eigen::VectorXd::Zero(N_CONTROL));
719
720 // Look-ahead depth (tractable since 4^depth)
721 const int depth = std::min(config_.N, 3);
722
723 // Depth-1 enumeration with multi-step worst-case propagation
724 for (const auto& u_cand : actions) {
725 // Propagate worst-case over `depth` steps
726 Eigen::VectorXd x_wc = x0;
727 double total_cost = 0.0;
728 bool feasible = true;
729
730 for (int step = 0; step < depth; ++step) {
731 auto eng = estimateEngine(x_wc(IDX_P_D_F), x_wc(IDX_P_D_O));
732 total_cost += runningCost(x_wc, u_cand, ref, k, u_prev);
733
734 // Worst-case dynamics: nominal + w_bar disturbance (max adverse)
735 Eigen::VectorXd x_next = dynamicsStep(x_wc, u_cand, eng.mdot_F, eng.mdot_O);
736 // Inflate by w_bar for robustness (worst-case adversary)
737 x_next -= state_.w_bar; // worst-case direction
738 x_next = x_next.cwiseMax(0.0);
739
740 // Check feasibility (§9.3 constraints)
741 if (!isStateSafe(x_next, ref.type)) {
742 feasible = false;
743 break;
744 }
745 x_wc = x_next;
746 }
747
748 if (!feasible)
749 continue;
750
751 if (total_cost < best_cost) {
752 best_cost = total_cost;
753 best_u = u_cand;
754 }
755 }
756
758 // std::cout << "[EnumMPC] best cost=" << best_cost << " u=" << best_u.transpose() <<
759 // std::endl;
760 }
761
762 return best_u;
763}
764
765// ═══════════════════════════════════════════════════════════════════════
766// OPEN/CLOSE ECONOMIC DECISION (§11)
767//
768// Valve i opens if: qF·(F_ref − F)·ΔF_i ≥ λ_gas·Δm_c,i + λ_switch
769// ═══════════════════════════════════════════════════════════════════════
770
771Eigen::VectorXd RobustDDPController::economicDecision(const Eigen::VectorXd& x,
772 const Reference& ref, int k) const {
773 auto eng_base = estimateEngine(x(IDX_P_D_F), x(IDX_P_D_O));
774 double F_base = eng_base.valid ? eng_base.F : 0.0;
775
776 Eigen::VectorXd u_out = Eigen::VectorXd::Zero(N_CONTROL);
777
778 // Marginal thrust benefit for each valve
779 for (int i = 0; i < N_CONTROL; ++i) {
780 // Simulate opening valve i
781 Eigen::VectorXd u_test = Eigen::VectorXd::Zero(N_CONTROL);
782 u_test(i) = 1.0;
783
784 // One-step forward
785 auto eng_open = estimateEngine(x(IDX_P_D_F), x(IDX_P_D_O));
786 Eigen::VectorXd x_next = dynamicsStep(x, u_test, eng_open.mdot_F, eng_open.mdot_O);
787 auto eng_next = estimateEngine(x_next(IDX_P_D_F), x_next(IDX_P_D_O));
788 double F_open = eng_next.valid ? eng_next.F : F_base;
789
790 double delta_F = F_open - F_base;
791
792 // Marginal gas cost
793 double delta_mc = (i == 0) ? config_.copv_cF : config_.copv_cO;
794 delta_mc *= config_.dt;
795
796 // Decision rule (§11)
797 double benefit;
799 // High benefit if we are below the pressure target
800 double P_f_err = std::max(0.0, ref.P_fuel_ref[k] - x(IDX_P_D_F));
801 double P_o_err = std::max(0.0, ref.P_ox_ref[k] - x(IDX_P_D_O));
802 double err_pa = (i == 0) ? P_f_err : P_o_err;
803
804 // Scaled benefit: if we are >1 psi (6894 Pa) away, open it
805 // gas_cost is usually around ~2000-3000
806 benefit = err_pa;
807 } else {
808 benefit = config_.qF * (ref.F_ref[k] - F_base) * delta_F;
809 }
810 double gas_cost = config_.qGas * delta_mc + config_.qSwitch;
811
812 if (benefit >= gas_cost)
813 u_out(i) = 1.0;
814 }
815
816 return u_out;
817}
818
819// ═══════════════════════════════════════════════════════════════════════
820// SAFETY FILTER (§9.3 — tube propagation)
821// ═══════════════════════════════════════════════════════════════════════
822
823bool RobustDDPController::isActionSafe(const Eigen::VectorXd& x, const Eigen::VectorXd& u,
824 CommandType cmd_type, int num_steps) const {
825 Eigen::VectorXd x_lo = x;
826 Eigen::VectorXd x_hi = x;
827
828 for (int s = 0; s < num_steps; ++s) {
829 auto eng = estimateEngine(x_hi(IDX_P_D_F), x_hi(IDX_P_D_O));
830 Eigen::VectorXd x_lo_next = dynamicsStep(x_lo, u, eng.mdot_F, eng.mdot_O) - state_.w_bar;
831 Eigen::VectorXd x_hi_next = dynamicsStep(x_hi, u, eng.mdot_F, eng.mdot_O) + state_.w_bar;
832 x_lo_next = x_lo_next.cwiseMax(0.0);
833 x_lo = x_lo_next.cwiseMin(x_hi_next);
834 x_hi = x_lo_next.cwiseMax(x_hi_next);
835
836 if (!isStateSafe(x_hi, cmd_type))
837 return false;
838 }
839 return true;
840}
841
842Eigen::VectorXd RobustDDPController::filterAction(const Eigen::VectorXd& x,
843 const Eigen::VectorXd& u_proposed,
844 const Reference& ref, int k) const {
845 Eigen::VectorXd u_safe = u_proposed.cwiseMax(0.0).cwiseMin(1.0);
846
847 if (isActionSafe(x, u_safe, ref.type))
848 return u_safe;
849
850 // Find best safe action from binary candidates
851 static const std::array<Eigen::Vector2d, 9> candidates = {
852 {{0, 0}, {0, 0.5}, {0, 1}, {0.5, 0}, {0.5, 0.5}, {0.5, 1}, {1, 0}, {1, 0.5}, {1, 1}}};
853
854 double best_cost = std::numeric_limits<double>::infinity();
855 Eigen::VectorXd best_u = Eigen::VectorXd::Zero(N_CONTROL);
856 Eigen::VectorXd u_prev = u_prev_applied_.value_or(Eigen::VectorXd::Zero(N_CONTROL));
857
858 for (const auto& c : candidates) {
859 if (!isActionSafe(x, c, ref.type))
860 continue;
861 double cost = runningCost(x, c, ref, k, u_prev);
862 if (cost < best_cost) {
863 best_cost = cost;
864 best_u = c;
865 }
866 }
867 return best_u;
868}
869
870// ═══════════════════════════════════════════════════════════════════════
871// SUPERVISORY CUTOFF (§12)
872// Predict apogee: h_ap = h + vz² / (2g)
873// If h_ap ≥ h* − δ → u_F = u_O = 0
874// ═══════════════════════════════════════════════════════════════════════
875
877 if (config_.altitude_target < 0)
878 return false; // disabled
879 double h_apogee = nav.h + (nav.vz * nav.vz) / (2.0 * G0);
880 return h_apogee >= (config_.altitude_target - config_.cutoff_delta);
881}
882
883// ═══════════════════════════════════════════════════════════════════════
884// ACTUATION (quantization + dwell)
885// ═══════════════════════════════════════════════════════════════════════
886
888 const Eigen::VectorXd& u_relaxed) const {
890 double q = config_.duty_quantization;
891
892 // Quantize
893 act.duty_F = std::clamp(std::round(u_relaxed(0) / q) * q, 0.0, 1.0);
894 act.duty_O = std::clamp(std::round(u_relaxed(1) / q) * q, 0.0, 1.0);
895
896 // Binary threshold (for on/off solenoids)
897 act.u_F_on = act.duty_F > 0.5;
898 act.u_O_on = act.duty_O > 0.5;
899 act.valid = true;
900 return act;
901}
902
903} // namespace control
904} // namespace fsw
Eigen::VectorXd filterAction(const Eigen::VectorXd &x, const Eigen::VectorXd &u_proposed, const Reference &ref, int k) const
Eigen::VectorXd dynamicsStep(const Eigen::VectorXd &x, const Eigen::VectorXd &u, double mdot_F, double mdot_O) const
EngineEstimate estimateEngine(double P_d_fuel, double P_d_ox) const
void linearize(const Eigen::VectorXd &x, const Eigen::VectorXd &u, double mdot_F, double mdot_O, Eigen::MatrixXd &A, Eigen::MatrixXd &B) const
Eigen::VectorXd economicDecision(const Eigen::VectorXd &x, const Reference &ref, int k) const
std::optional< Eigen::VectorXd > x_prev_
Eigen::VectorXd enumerativeMPC(const Eigen::VectorXd &x0, const Reference &ref, int k) const
ActuationCommand computeActuation(const Eigen::VectorXd &u_relaxed) const
bool shouldCutoff(const NavState &nav) const
double runningCost(const Eigen::VectorXd &x, const Eigen::VectorXd &u, const Reference &ref, int k, const Eigen::VectorXd &u_prev) const
bool isStateSafe(const Eigen::VectorXd &x, CommandType cmd_type=CommandType::THRUST_DESIRED) const
Reference buildReference(const NavState &nav, const Measurement &meas, const Command &cmd) const
bool isActionSafe(const Eigen::VectorXd &x, const Eigen::VectorXd &u, CommandType cmd_type, int num_steps=2) const
bool initialize(const Config &config)
std::pair< ActuationCommand, Diagnostics > step(const Measurement &meas, const NavState &nav, const Command &cmd)
Execute one control step.
DDPSolution solveDDP(const Eigen::VectorXd &x0, const Reference &ref, const Eigen::MatrixXd &u_init) const
Eigen::VectorXd buildState(const Measurement &meas) const
std::optional< Eigen::MatrixXd > u_seq_prev_
std::optional< Eigen::VectorXd > u_prev_applied_
uint8_t reg
static osMutexId_t lock
Definition mode.c:11
constexpr int N_CONTROL
constexpr double G0
constexpr int IDX_M_GAS_O
constexpr int N_STATE
constexpr int IDX_P_D_O
constexpr int IDX_P_REG
constexpr int IDX_P_U_F
constexpr int IDX_P_COPV
constexpr double GAMMA_N2
constexpr int IDX_P_D_F
constexpr int IDX_M_GAS_F
constexpr int IDX_V_U_F
constexpr int IDX_P_U_O
constexpr int IDX_V_U_O
constexpr int IDX_M_GAS_COPV
constexpr double R_GAS_N2
Motorized ball valve position: raw ADC counts -> % open.
*brief user defined string literal for JSON values *sa std::size_t n
Definition json.hpp:24474