STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
plot_lut.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""
3Plot controller LUT as heatmaps and slices.
4Produces a multi-panel figure showing F, duty_F, duty_O over the pressure/thrust grid.
5"""
6from __future__ import annotations
7
8import argparse
9import json
10from pathlib import Path
11
12import matplotlib.pyplot as plt
13import numpy as np
14
15
16def load_lut(npz_path: Path) -> dict:
17 """Load LUT from .npz, return dict with axes and data arrays."""
18 data = np.load(npz_path, allow_pickle=True)
19 meta_raw = data["meta"].tolist()
20 meta = json.loads(meta_raw)
21 axes_meta = meta.get("axes", [])
22
23 axes = {}
24 for ax in axes_meta:
25 name = ax["name"]
26 key = f"axes/{name}"
27 axes[name] = np.asarray(data[key], dtype=np.float64)
28
29 outputs = {}
30 for out_name in meta.get("outputs", []):
31 key = f"data/{out_name}"
32 if key in data:
33 outputs[out_name] = np.asarray(data[key], dtype=np.float64)
34
35 return {"axes": axes, "outputs": outputs, "meta": meta}
36
37
38def plot_policy_lut(lut: dict, out_path: Path) -> None:
39 """Create multi-panel plot for 4D policy LUT (P_u_fuel, P_u_ox, thrust_desired, MR_ref)."""
40 axes = lut["axes"]
41 outputs = lut["outputs"]
42
43 P_fuel = axes["P_u_fuel"] / 1e6 # MPa
44 P_ox = axes["P_u_ox"] / 1e6
45 thrust = axes["thrust_desired"]
46 MR_ref = axes["MR_ref"]
47
48 # Use MR_ref index 1 (middle, ~2.2)
49 mr_idx = len(MR_ref) // 2
50
51 n_thrust = len(thrust)
52 n_cols = 3
53 n_rows = (n_thrust + n_cols - 1) // n_cols
54 fig, axes_plt = plt.subplots(
55 n_rows, n_cols, figsize=(5 * n_cols, 4 * n_rows), sharex=True, sharey=True
56 )
57 axes_plt = np.atleast_2d(axes_plt).flatten()
58
59 # F (thrust) heatmaps for each thrust_desired
60 for i, t_val in enumerate(thrust):
61 ax = axes_plt[i]
62 F_slice = outputs["F"][:, :, i, mr_idx]
63 im = ax.pcolormesh(
64 P_fuel,
65 P_ox,
66 F_slice.T,
67 cmap="viridis",
68 shading="auto",
69 vmin=0,
70 vmax=np.nanmax(outputs["F"]) if np.any(np.isfinite(outputs["F"])) else 7000,
71 )
72 ax.set_title(f"F @ thrust_des={t_val:.0f} N, MR_ref={MR_ref[mr_idx]:.1f}")
73 ax.set_xlabel("P_u_fuel (MPa)")
74 ax.set_ylabel("P_u_ox (MPa)")
75 ax.set_aspect("equal")
76 plt.colorbar(im, ax=ax, label="F (N)")
77
78 for j in range(n_thrust, len(axes_plt)):
79 axes_plt[j].set_visible(False)
80 plt.suptitle("Controller LUT: Thrust (F) vs tank pressures", fontsize=12, y=1.02)
81 plt.tight_layout()
82 fig.savefig(out_path.with_suffix(".thrust.png"), dpi=150, bbox_inches="tight")
83 plt.close(fig)
84 print(f"Saved {out_path.with_suffix('.thrust.png')}")
85
86 # Duty cycle heatmaps: duty_F (row 0) and duty_O (row 1)
87 fig2, axes2 = plt.subplots(
88 2 * n_rows,
89 n_cols,
90 figsize=(5 * n_cols, 4 * 2 * n_rows),
91 sharex=True,
92 sharey=True,
93 )
94 axes2 = np.atleast_2d(axes2)
95 for i, t_val in enumerate(thrust):
96 r, c = i // n_cols, i % n_cols
97 ax_f = axes2[r, c]
98 ax_o = axes2[n_rows + r, c]
99 duty_F = outputs["duty_F"][:, :, i, mr_idx]
100 duty_O = outputs["duty_O"][:, :, i, mr_idx]
101 im_f = ax_f.pcolormesh(
102 P_fuel, P_ox, duty_F.T, cmap="plasma", shading="auto", vmin=0, vmax=1
103 )
104 ax_f.set_title(f"duty_F @ thrust_des={t_val:.0f} N")
105 ax_f.set_xlabel("P_u_fuel (MPa)")
106 ax_f.set_ylabel("P_u_ox (MPa)")
107 ax_f.set_aspect("equal")
108 plt.colorbar(im_f, ax=ax_f, label="duty_F")
109 im_o = ax_o.pcolormesh(
110 P_fuel, P_ox, duty_O.T, cmap="plasma", shading="auto", vmin=0, vmax=1
111 )
112 ax_o.set_title(f"duty_O @ thrust_des={t_val:.0f} N")
113 ax_o.set_xlabel("P_u_fuel (MPa)")
114 ax_o.set_ylabel("P_u_ox (MPa)")
115 ax_o.set_aspect("equal")
116 plt.colorbar(im_o, ax=ax_o, label="duty_O")
117 for j in range(n_thrust, n_rows * n_cols):
118 r, c = j // n_cols, j % n_cols
119 axes2[r, c].set_visible(False)
120 axes2[n_rows + r, c].set_visible(False)
121 plt.suptitle(
122 "Controller LUT: Fuel and LOX duty cycles vs tank pressures",
123 fontsize=12,
124 y=1.02,
125 )
126 plt.tight_layout()
127 fig2.savefig(out_path.with_suffix(".duty_fuel.png"), dpi=150, bbox_inches="tight")
128 plt.close(fig2)
129 print(f"Saved {out_path.with_suffix('.duty_fuel.png')}")
130
131 # MR_ref sweep: one thrust level, all MR_ref
132 n_mr = len(MR_ref)
133 t_idx = min(5, n_thrust - 1) # 6000 N or last if fewer
134 fig3, axes3 = plt.subplots(1, n_mr, figsize=(4 * n_mr, 4), sharex=True, sharey=True)
135 axes3 = np.atleast_1d(axes3)
136 for j, mr_val in enumerate(MR_ref):
137 ax = axes3[j]
138 F_slice = outputs["F"][:, :, t_idx, j]
139 im = ax.pcolormesh(
140 P_fuel,
141 P_ox,
142 F_slice.T,
143 cmap="viridis",
144 shading="auto",
145 vmin=0,
146 vmax=np.nanmax(outputs["F"]),
147 )
148 ax.set_title(f"F @ thrust_des={thrust[t_idx]:.0f} N, MR_ref={mr_val:.1f}")
149 ax.set_xlabel("P_u_fuel (MPa)")
150 ax.set_ylabel("P_u_ox (MPa)")
151 ax.set_aspect("equal")
152 plt.colorbar(im, ax=ax, label="F (N)")
153
154 plt.suptitle(
155 f"Controller LUT: Thrust vs MR_ref (thrust_desired={thrust[t_idx]:.0f} N)",
156 fontsize=12,
157 y=1.02,
158 )
159 plt.tight_layout()
160 fig3.savefig(out_path.with_suffix(".mr_sweep.png"), dpi=150, bbox_inches="tight")
161 plt.close(fig3)
162 print(f"Saved {out_path.with_suffix('.mr_sweep.png')}")
163
164
165def plot_engine_lut(lut: dict, out_path: Path) -> None:
166 """Plot 2D engine LUT (P_u_fuel, P_u_ox)."""
167 axes = lut["axes"]
168 outputs = lut["outputs"]
169
170 P_fuel = axes["P_u_fuel"] / 1e6
171 P_ox = axes["P_u_ox"] / 1e6
172
173 fig, axes_plt = plt.subplots(2, 2, figsize=(10, 9), sharex=True, sharey=True)
174
175 # F
176 ax = axes_plt[0, 0]
177 F = outputs["F"]
178 im = ax.pcolormesh(P_fuel, P_ox, F.T, cmap="viridis", shading="auto")
179 ax.set_title("Thrust F (N)")
180 ax.set_xlabel("P_u_fuel (MPa)")
181 ax.set_ylabel("P_u_ox (MPa)")
182 plt.colorbar(im, ax=ax)
183
184 # MR
185 ax = axes_plt[0, 1]
186 MR = outputs["MR"]
187 im = ax.pcolormesh(P_fuel, P_ox, MR.T, cmap="coolwarm", shading="auto")
188 ax.set_title("Mixture ratio MR")
189 ax.set_xlabel("P_u_fuel (MPa)")
190 ax.set_ylabel("P_u_ox (MPa)")
191 plt.colorbar(im, ax=ax)
192
193 # P_ch if present
194 if "P_ch" in outputs:
195 ax = axes_plt[1, 0]
196 Pc = outputs["P_ch"] / 1e6
197 im = ax.pcolormesh(P_fuel, P_ox, Pc.T, cmap="magma", shading="auto")
198 ax.set_title("Chamber pressure (MPa)")
199 ax.set_xlabel("P_u_fuel (MPa)")
200 ax.set_ylabel("P_u_ox (MPa)")
201 plt.colorbar(im, ax=ax)
202
203 # stability_score if present
204 if "stability_score" in outputs:
205 ax = axes_plt[1, 1]
206 stab = outputs["stability_score"]
207 im = ax.pcolormesh(
208 P_fuel, P_ox, stab.T, cmap="RdYlGn", shading="auto", vmin=0, vmax=1
209 )
210 ax.set_title("Stability score")
211 ax.set_xlabel("P_u_fuel (MPa)")
212 ax.set_ylabel("P_u_ox (MPa)")
213 plt.colorbar(im, ax=ax)
214
215 plt.suptitle("Engine LUT: Performance vs tank pressures", fontsize=12, y=1.02)
216 plt.tight_layout()
217 png_path = out_path.with_suffix(".png") if out_path.suffix != ".png" else out_path
218 fig.savefig(png_path, dpi=150, bbox_inches="tight")
219 plt.close(fig)
220 print(f"Saved {png_path}")
221
222
223def main() -> None:
224 parser = argparse.ArgumentParser(description="Plot controller/engine LUT")
225 parser.add_argument(
226 "--input",
227 "-i",
228 type=Path,
229 default=Path("output/lut/controller_policy_fsw.npz"),
230 help="Input .npz LUT path",
231 )
232 parser.add_argument(
233 "--output",
234 "-o",
235 type=Path,
236 default=None,
237 help="Output plot path (default: same as input with .png)",
238 )
239 parser.add_argument(
240 "--engine",
241 action="store_true",
242 help="Plot engine LUT (2D) instead of policy LUT (4D)",
243 )
244 args = parser.parse_args()
245
246 in_path = args.input.resolve()
247 if not in_path.exists():
248 raise FileNotFoundError(f"LUT not found: {in_path}")
249
250 out_path = args.output or in_path.with_suffix(".plots")
251 out_path = out_path.resolve()
252
253 lut = load_lut(in_path)
254
255 if args.engine or "thrust_desired" not in lut["axes"]:
256 plot_engine_lut(lut, out_path)
257 else:
258 plot_policy_lut(lut, out_path)
259
260
261if __name__ == "__main__":
262 main()
#define min(a, b)
Definition Arduino.h:42
static void flatten(const string_t &reference_string, const BasicJsonType &value, BasicJsonType &result)
Definition json.hpp:14540
dict load_lut(Path npz_path)
Definition plot_lut.py:16
None plot_engine_lut(dict lut, Path out_path)
Definition plot_lut.py:165
None plot_policy_lut(dict lut, Path out_path)
Definition plot_lut.py:38