31 data = np.load(path, allow_pickle=
True)
33 if "meta" not in data:
34 raise ValueError(f
"LUT file {path} is missing required 'meta' entry")
36 meta_raw = data[
"meta"].tolist()
37 meta: Dict[str, Any] = json.loads(meta_raw)
39 axes_meta: Sequence[Mapping[str, Any]] = meta.get(
"axes", [])
40 self.
_axes: List[LoadedAxis] = []
41 for axis_meta
in axes_meta:
42 name = axis_meta[
"name"]
46 f
"LUT file {path} missing axis array for '{name}' (key '{key}')"
48 values = np.asarray(data[key], dtype=float)
53 units=axis_meta.get(
"units",
""),
54 description=axis_meta.get(
"description",
""),
61 outputs_meta: Iterable[str] = meta.get(
"outputs", [])
63 expected_shape: tuple[int, ...] |
None =
None
65 for out_name
in outputs_meta:
66 key = f
"data/{out_name}"
69 f
"LUT file {path} missing data array for output '{out_name}' (key '{key}')"
71 arr = np.asarray(data[key], dtype=float)
72 if expected_shape
is None:
73 expected_shape = arr.shape
74 elif arr.shape != expected_shape:
76 f
"All output arrays must have the same shape. "
77 f
"Got {arr.shape} for '{out_name}', expected {expected_shape}."
82 axis_sizes = tuple(len(axis.values)
for axis
in self.
_axes)
83 if expected_shape
is not None and expected_shape != axis_sizes:
85 f
"LUT data shape {expected_shape} does not match axes sizes {axis_sizes}"
99 def evaluate(self, point: Mapping[str, float]) -> Dict[str, float]:
101 Evaluate the LUT at an arbitrary point using multilinear interpolation.
106 Mapping from axis name -> coordinate value in that axis' units.
107 All axes defined in the LUT must be present; extra keys are ignored.
112 Mapping from output name -> interpolated value.
115 raise ValueError(
"LUT has no axes defined")
118 indices: List[tuple[int, int]] = []
119 weights: List[tuple[float, float]] = []
121 for axis
in self.
_axes:
122 if axis.name
not in point:
124 f
"Point is missing coordinate for axis '{axis.name}'. "
125 f
"Required axes: {list(self._axis_name_to_index.keys())}"
128 x = float(point[axis.name])
132 hi = int(np.searchsorted(grid, x))
144 t = float((x - x0) / (x1 - x0))
152 indices.append((lo, hi))
153 weights.append((1.0 - t, t))
155 n_axes = len(self.
_axes)
158 f
"Too many axes for multilinear interpolation: {n_axes} (max 20)"
161 result: Dict[str, float] = {name: 0.0
for name
in self.
_outputs.keys()}
164 n_corners = 1 << n_axes
165 for corner
in range(n_corners):
169 for axis_idx
in range(n_axes):
170 lo, hi = indices[axis_idx]
171 w0, w1 = weights[axis_idx]
172 if (corner >> axis_idx) & 1:
185 idx_tuple = tuple(idx)
186 for out_name, arr
in self.
_outputs.items():
189 result[out_name] += w * float(val)