STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
policy_lut.py
Go to the documentation of this file.
1"""Controller policy look-up table (LUT) for DDP stack.
2
3Precomputes optimal control u = [u_F, u_O] over a grid of (P_u_F, P_u_O, F_ref, MR_ref)
4and provides fast runtime interpolation. Use when online DDP is too slow.
5"""
6
7from __future__ import annotations
8
9from pathlib import Path
10from typing import Optional, Tuple, List
11import numpy as np
12
13from .dynamics import N_CONTROL, IDX_P_U_F, IDX_P_U_O
14
15
17 axes: List[np.ndarray],
18 u_grid: np.ndarray,
19 bounds_mode: str = "clip",
20) -> "RegularGridInterpolator":
21 """Create RegularGridInterpolator for u lookup."""
22 from scipy.interpolate import RegularGridInterpolator
23
24 return RegularGridInterpolator(
25 axes,
26 u_grid,
27 method="linear",
28 bounds_error=False,
29 fill_value=None,
30 )
31
32
34 """Look-up table for controller policy u = f(P_u_F, P_u_O, F_ref, MR_ref)."""
35
37 self,
38 axes: List[np.ndarray],
39 u_grid: np.ndarray,
40 bounds_mode: str = "clip",
41 ):
42 """
43 Parameters:
44 -----------
45 axes : List[np.ndarray]
46 Grid axes [P_u_F, P_u_O, F_ref, MR_ref], each 1D sorted
47 u_grid : np.ndarray
48 Control values shape (n_P_u_F, n_P_u_O, n_F_ref, n_MR_ref, N_CONTROL)
49 bounds_mode : str
50 "clip" = clamp query to grid bounds; "nearest" = use nearest boundary value
51 """
52 self.axes = axes
53 self.u_grid = u_grid
54 self.bounds_mode = bounds_mode
55
56 # Build interpolator for each control dimension
58 axes, u_grid[..., 0], bounds_mode
59 )
61 axes, u_grid[..., 1], bounds_mode
62 )
63
64 def lookup(
65 self,
66 P_u_F: float,
67 P_u_O: float,
68 F_ref: float,
69 MR_ref: float,
70 ) -> np.ndarray:
71 """
72 Interpolate optimal control at query point.
73
74 Parameters:
75 -----------
76 P_u_F : float
77 Fuel ullage pressure [Pa]
78 P_u_O : float
79 Oxidizer ullage pressure [Pa]
80 F_ref : float
81 Reference thrust [N]
82 MR_ref : float
83 Reference mixture ratio
84
85 Returns:
86 --------
87 u : np.ndarray, shape (N_CONTROL,)
88 [u_F, u_O] in [0, 1]
89 """
90 point = np.array([[P_u_F, P_u_O, F_ref, MR_ref]], dtype=np.float64)
91
92 if self.bounds_mode == "clip":
93 for i, ax in enumerate(self.axes):
94 point[0, i] = np.clip(point[0, i], ax.min(), ax.max())
95
96 u_F = float(self._interp_u_F(point)[0])
97 u_O = float(self._interp_u_O(point)[0])
98
99 # Handle NaN from extrapolation
100 if not np.isfinite(u_F):
101 u_F = 0.1
102 if not np.isfinite(u_O):
103 u_O = 0.1
104
105 u = np.array([u_F, u_O], dtype=np.float64)
106 return np.clip(u, 0.0, 1.0)
107
108 def save(self, filepath: str) -> None:
109 """Save LUT to .npz file."""
110 path = Path(filepath)
111 path.parent.mkdir(parents=True, exist_ok=True)
112
113 np.savez_compressed(
114 path,
115 axes_0=self.axes[0],
116 axes_1=self.axes[1],
117 axes_2=self.axes[2],
118 axes_3=self.axes[3],
119 u_grid=self.u_grid,
120 bounds_mode=np.array([self.bounds_mode], dtype=object),
121 )
122
123 @classmethod
124 def load(cls, filepath: str) -> "PolicyLUT":
125 """Load LUT from .npz file."""
126 data = np.load(filepath, allow_pickle=True)
127 axes = [
128 data["axes_0"],
129 data["axes_1"],
130 data["axes_2"],
131 data["axes_3"],
132 ]
133 u_grid = data["u_grid"]
134 bounds_mode = str(data["bounds_mode"][0]) if "bounds_mode" in data else "clip"
135 return cls(axes=axes, u_grid=u_grid, bounds_mode=bounds_mode)
136
137 @property
138 def grid_shape(self) -> Tuple[int, ...]:
139 """Return grid shape (n_P_u_F, n_P_u_O, n_F_ref, n_MR_ref)."""
140 return self.u_grid.shape[:-1]
__init__(self, List[np.ndarray] axes, np.ndarray u_grid, str bounds_mode="clip")
Definition policy_lut.py:41
"PolicyLUT" load(cls, str filepath)
np.ndarray lookup(self, float P_u_F, float P_u_O, float F_ref, float MR_ref)
Definition policy_lut.py:70
"RegularGridInterpolator" _make_interpolator(List[np.ndarray] axes, np.ndarray u_grid, str bounds_mode="clip")
Definition policy_lut.py:20