STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
build_policy_lut.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Build controller policy LUT by running DDP offline over a grid.
3
4Usage:
5 cd engine_sim && python scripts/build_policy_lut.py [options]
6
7Options:
8 --controller-config PATH Controller YAML (default: configs/robust_ddp_default.yaml)
9 --engine-config PATH Engine YAML (default: configs/default.yaml)
10 --output PATH Output .npz file (default: configs/policy_lut.npz)
11 --grid P_F P_O F MR Grid resolution: n_P_u_F n_P_u_O n_F_ref n_MR_ref (default: 6 6 8 5)
12"""
13
14from __future__ import annotations
15
16import argparse
17import sys
18from pathlib import Path
19
20import numpy as np
21
22# Add engine_sim root to path
23_ENGINE_SIM_ROOT = Path(__file__).resolve().parent.parent
24sys.path.insert(0, str(_ENGINE_SIM_ROOT))
25
26from engine.control.robust_ddp.config_loader import load_config as load_controller_config
28 DynamicsParams,
29 N_STATE,
30 N_CONTROL,
31 IDX_P_COPV,
32 IDX_P_REG,
33 IDX_P_U_F,
34 IDX_P_U_O,
35 IDX_P_D_F,
36 IDX_P_D_O,
37 IDX_V_U_F,
38 IDX_V_U_O,
39 IDX_M_GAS_COPV,
40 IDX_M_GAS_F,
41 IDX_M_GAS_O,
42)
43from engine.control.robust_ddp.ddp_solver import solve_ddp
44from engine.control.robust_ddp.robustness import get_w_bar_array
45from engine.control.robust_ddp.data_models import ControllerState
46from engine.control.robust_ddp.policy_lut import PolicyLUT
47from engine.pipeline.io import load_config as load_engine_config
48
49
50# Grid bounds (operating envelope)
51P_U_F_MIN = 2.0e6 # 2 MPa
52P_U_F_MAX = 10.0e6 # 10 MPa
53P_U_O_MIN = 2.0e6
54P_U_O_MAX = 10.0e6
55F_REF_MIN = 0.0
56F_REF_MAX = 5000.0 # 5 kN
57MR_REF_MIN = 1.5
58MR_REF_MAX = 3.0
59
60
62 P_u_F: float,
63 P_u_O: float,
64 cfg,
65 dynamics_params,
66) -> np.ndarray:
67 """Build full state vector from ullage pressures."""
68 R_gas = 296.8
69 T_gas = 293.0
70 Z_gas = 1.0
71 V_copv = getattr(dynamics_params, "V_copv", 0.006)
72 V_u_F = 0.01
73 V_u_O = 0.01
74
75 P_copv = getattr(cfg, "reg_setpoint", 6.89476e6) * 1.5 if cfg.reg_setpoint else 15e6
76 P_reg = cfg.reg_setpoint if cfg.reg_setpoint else P_copv * cfg.reg_ratio
77 P_d_F = P_u_F * 0.95
78 P_d_O = P_u_O * 0.95
79
80 m_gas_copv = (P_copv * V_copv * Z_gas) / (R_gas * T_gas)
81 m_gas_F = (P_u_F * V_u_F * Z_gas) / (R_gas * T_gas) if V_u_F > 1e-10 else 0.0
82 m_gas_O = (P_u_O * V_u_O * Z_gas) / (R_gas * T_gas) if V_u_O > 1e-10 else 0.0
83
84 return np.array([
85 P_copv, P_reg, P_u_F, P_u_O, P_d_F, P_d_O,
86 V_u_F, V_u_O,
87 m_gas_copv, m_gas_F, m_gas_O,
88 ], dtype=np.float64)
89
90
91def main() -> int:
92 parser = argparse.ArgumentParser(description="Build DDP controller policy LUT")
93 parser.add_argument(
94 "--controller-config",
95 type=str,
96 default=str(_ENGINE_SIM_ROOT / "configs" / "robust_ddp_default.yaml"),
97 help="Controller config YAML",
98 )
99 parser.add_argument(
100 "--engine-config",
101 type=str,
102 default=str(_ENGINE_SIM_ROOT / "configs" / "default.yaml"),
103 help="Engine config YAML",
104 )
105 parser.add_argument(
106 "--output",
107 type=str,
108 default=str(_ENGINE_SIM_ROOT / "configs" / "policy_lut.npz"),
109 help="Output LUT .npz path",
110 )
111 parser.add_argument(
112 "--grid",
113 type=int,
114 nargs=4,
115 default=[6, 6, 8, 5],
116 metavar=("P_F", "P_O", "F", "MR"),
117 help="Grid resolution (n_P_u_F n_P_u_O n_F_ref n_MR_ref)",
118 )
119 parser.add_argument(
120 "--max-iterations",
121 type=int,
122 default=None,
123 help="Override DDP max iterations (default: use config)",
124 )
125 args = parser.parse_args()
126
127 print("Loading configs...")
128 cfg = load_controller_config(args.controller_config)
129 if args.max_iterations is not None:
130 cfg.max_iterations = args.max_iterations
131 print(f" Overriding max_iterations={args.max_iterations}")
132 engine_config = load_engine_config(args.engine_config)
133
134 from engine.control.robust_ddp.engine_wrapper import EngineWrapper
135 engine_wrapper = EngineWrapper(engine_config)
136 dynamics_params = DynamicsParams.from_config(cfg)
137
138 n_P_F, n_P_O, n_F, n_MR = args.grid
139 axes = [
140 np.linspace(P_U_F_MIN, P_U_F_MAX, n_P_F),
141 np.linspace(P_U_O_MIN, P_U_O_MAX, n_P_O),
142 np.linspace(F_REF_MIN, F_REF_MAX, n_F),
143 np.linspace(MR_REF_MIN, MR_REF_MAX, n_MR),
144 ]
145
146 u_grid = np.zeros((n_P_F, n_P_O, n_F, n_MR, N_CONTROL), dtype=np.float64)
147 state = ControllerState()
148 w_bar = get_w_bar_array(state)
149
150 total = n_P_F * n_P_O * n_F * n_MR
151 done = 0
152
153 print(f"Building LUT: grid {n_P_F}x{n_P_O}x{n_F}x{n_MR} = {total} points")
154 print("Axes: P_u_F [MPa], P_u_O [MPa], F_ref [kN], MR_ref")
155
156 for i, P_u_F in enumerate(axes[0]):
157 for j, P_u_O in enumerate(axes[1]):
158 for k, F_ref in enumerate(axes[2]):
159 for m, MR_ref in enumerate(axes[3]):
161 P_u_F, P_u_O, cfg, dynamics_params
162 )
163 F_ref_seq = np.full(cfg.N, F_ref, dtype=np.float64)
164 MR_ref_seq = np.full(cfg.N, MR_ref, dtype=np.float64)
165 u_init = np.full((cfg.N, N_CONTROL), 0.2, dtype=np.float64)
166
167 try:
168 solution = solve_ddp(
169 x0=x0,
170 u_seq_init=u_init,
171 F_ref=F_ref_seq,
172 MR_ref=MR_ref_seq,
173 cfg=cfg,
174 dynamics_params=dynamics_params,
175 engine_wrapper=engine_wrapper,
176 w_bar=w_bar,
177 use_robustification=True,
178 )
179 u_grid[i, j, k, m, :] = solution.u_seq[0]
180 except Exception as e:
181 u_grid[i, j, k, m, :] = 0.1 # Fallback
182 if done < 5:
183 print(f" Warning at ({i},{j},{k},{m}): {e}")
184
185 done += 1
186 if done % 100 == 0 or done == total:
187 print(f" Progress: {done}/{total} ({100*done/total:.1f}%)")
188
189 lut = PolicyLUT(axes=axes, u_grid=u_grid, bounds_mode="clip")
190 lut.save(args.output)
191 print(f"Saved LUT to {args.output}")
192 return 0
193
194
195if __name__ == "__main__":
196 sys.exit(main())
np.ndarray build_state_from_pressures(float P_u_F, float P_u_O, cfg, dynamics_params)