3ec09f7296
PiperOrigin-RevId: 868229512 Change-Id: I790bc08fc8b0745583a2f92d9ee2c5a19ba558ea
241 lines
7.3 KiB
Python
241 lines
7.3 KiB
Python
# Copyright 2026 DeepMind Technologies Limited
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
# ==============================================================================
|
|
|
|
"""Parameter table report section."""
|
|
|
|
from typing import Any
|
|
|
|
from mujoco.sysid._src import parameter
|
|
from mujoco.sysid.report.sections.base import ReportSection
|
|
import numpy as np
|
|
|
|
|
|
class ParametersTable(ReportSection):
|
|
"""Displays a table of identified, initial, and nominal parameters."""
|
|
|
|
def __init__(
|
|
self,
|
|
title: str,
|
|
opt_params: parameter.ParameterDict,
|
|
initial_params: parameter.ParameterDict,
|
|
anchor: str = "",
|
|
collapsible: bool = True,
|
|
):
|
|
super().__init__(collapsible=collapsible)
|
|
self._title = title
|
|
self._anchor = anchor
|
|
self._opt_params = opt_params
|
|
self._initial_params = initial_params
|
|
self._nominal_params = initial_params.copy()
|
|
self._nominal_params.reset()
|
|
|
|
self._x_nominal = self._nominal_params.as_vector()
|
|
self._x_hat = self._opt_params.as_vector()
|
|
self._x_initial = self._initial_params.as_vector()
|
|
|
|
@property
|
|
def title(self) -> str:
|
|
return self._title
|
|
|
|
@property
|
|
def anchor(self) -> str:
|
|
return self._anchor
|
|
|
|
@property
|
|
def template_filename(self) -> str:
|
|
"""Tells the builder to look for 'parameters_table.html'."""
|
|
return "parameters_table.html"
|
|
|
|
def header_includes(self) -> set[str]:
|
|
return {""}
|
|
|
|
def get_context(self) -> dict[str, Any]:
|
|
# Get the vector of non-frozen parameters
|
|
non_frozen_vector = self._opt_params.as_vector()
|
|
|
|
if len(self._x_hat) != non_frozen_vector.size:
|
|
raise ValueError(
|
|
"Parameter vector lengths don't match. Expected"
|
|
f" {non_frozen_vector.size}, got {len(self._x_hat)}"
|
|
)
|
|
|
|
if self._x_nominal is not None:
|
|
if len(self._x_nominal) != non_frozen_vector.size:
|
|
raise ValueError(
|
|
"Parameter vector lengths don't match. Expected"
|
|
f" {non_frozen_vector.size}, got {len(self._x_nominal)}"
|
|
)
|
|
|
|
if self._x_initial is not None:
|
|
if len(self._x_initial) != non_frozen_vector.size:
|
|
raise ValueError(
|
|
"Parameter vector lengths don't match. Expected"
|
|
f" {non_frozen_vector.size}, got {len(self._x_initial)}"
|
|
)
|
|
|
|
# Compute error metrics if nominal exists
|
|
if self._x_nominal is not None:
|
|
if self._x_hat.size > 0:
|
|
rel_errors = np.abs(self._x_hat - self._x_nominal) / (
|
|
np.abs(self._x_nominal) + 1e-8
|
|
)
|
|
overall_rmse = np.sqrt(np.mean((self._x_hat - self._x_nominal) ** 2))
|
|
abs_errors = np.abs(self._x_hat - self._x_nominal)
|
|
else:
|
|
rel_errors = np.array([])
|
|
overall_rmse = 0.0
|
|
abs_errors = np.array([])
|
|
else:
|
|
rel_errors = None
|
|
overall_rmse = None
|
|
abs_errors = None
|
|
|
|
def create_table_row(param_name, val, lb, ub, idx=None, is_frozen=False):
|
|
"""Create a dict for a table row."""
|
|
error_class = ""
|
|
|
|
# Bounds check
|
|
if not is_frozen:
|
|
# Check if on boundary
|
|
if (abs(val - lb) < 1e-8 + 1e-3 * abs(lb)) or (
|
|
abs(val - ub) < 1e-8 + 1e-3 * abs(ub)
|
|
):
|
|
error_class = "pt_on_boundary"
|
|
|
|
# If not on boundary, check errors if nominal exists
|
|
elif rel_errors is not None and idx is not None:
|
|
rel_error = rel_errors[idx]
|
|
if rel_error < 0.02:
|
|
error_class = "pt_small_error"
|
|
elif rel_error < 0.1:
|
|
error_class = "pt_medium_error"
|
|
else:
|
|
error_class = "pt_large_error"
|
|
else:
|
|
error_class = "pt_frozen"
|
|
|
|
row = {
|
|
"name": param_name + ("*" if is_frozen else ""),
|
|
"pred": val,
|
|
"lower_bound": lb,
|
|
"upper_bound": ub,
|
|
"error_class": error_class,
|
|
"is_frozen": is_frozen,
|
|
}
|
|
|
|
if self._x_initial is not None:
|
|
if is_frozen:
|
|
# Initial for frozen is assumed same as val
|
|
row["initial"] = val
|
|
elif idx is not None:
|
|
row["initial"] = self._x_initial[idx]
|
|
|
|
if self._x_nominal is not None:
|
|
if is_frozen:
|
|
# Nominal for frozen is assumed same as val
|
|
row.update({
|
|
"nominal": val,
|
|
"abs_err": 0.0,
|
|
"rel_err": 0.0,
|
|
})
|
|
elif (
|
|
idx is not None
|
|
and abs_errors is not None
|
|
and rel_errors is not None
|
|
):
|
|
row.update({
|
|
"nominal": self._x_nominal[idx],
|
|
"abs_err": abs_errors[idx],
|
|
"rel_err": rel_errors[idx],
|
|
})
|
|
|
|
return row
|
|
|
|
# Build table data.
|
|
table_data = []
|
|
non_frozen_idx = 0 # Index for non-frozen parameters in the arrays
|
|
|
|
for param_name, param in self._opt_params.parameters.items():
|
|
# Get bounds for this specific parameter
|
|
p_min, p_max = param.get_bounds()
|
|
|
|
if param.size == 1:
|
|
flat_value = (
|
|
param.value if np.isscalar(param.value) else param.value.item()
|
|
)
|
|
lb = p_min.item()
|
|
ub = p_max.item()
|
|
|
|
if param.frozen:
|
|
table_data.append(
|
|
create_table_row(param_name, flat_value, lb, ub, is_frozen=True)
|
|
)
|
|
else:
|
|
val = self._x_hat[non_frozen_idx]
|
|
table_data.append(
|
|
create_table_row(param_name, val, lb, ub, idx=non_frozen_idx)
|
|
)
|
|
non_frozen_idx += 1
|
|
else:
|
|
for i in range(param.size):
|
|
if param.shape == (param.size,):
|
|
element_name = f"{param_name}[{i}]"
|
|
val_frozen = param.value[i]
|
|
lb = p_min[i]
|
|
ub = p_max[i]
|
|
else:
|
|
multi_idx = np.unravel_index(i, param.shape)
|
|
idx_str = ",".join(str(x) for x in multi_idx)
|
|
element_name = f"{param_name}[{idx_str}]"
|
|
val_frozen = param.value[multi_idx]
|
|
lb = p_min[multi_idx]
|
|
ub = p_max[multi_idx]
|
|
|
|
if param.frozen:
|
|
table_data.append(
|
|
create_table_row(
|
|
element_name, val_frozen, lb, ub, is_frozen=True
|
|
)
|
|
)
|
|
else:
|
|
val = self._x_hat[non_frozen_idx]
|
|
table_data.append(
|
|
create_table_row(element_name, val, lb, ub, idx=non_frozen_idx)
|
|
)
|
|
non_frozen_idx += 1
|
|
|
|
headers = ["Parameter"]
|
|
|
|
if self._x_initial is not None:
|
|
headers.append("Initial")
|
|
|
|
if self._x_nominal is not None:
|
|
headers.append("Nominal")
|
|
|
|
headers.append("Identified")
|
|
headers.extend(["Lower Bound", "Upper Bound"])
|
|
|
|
if self._x_nominal is not None:
|
|
headers.extend(["Absolute Change", "Relative Change"])
|
|
|
|
return {
|
|
"title": self._title,
|
|
"headers": headers,
|
|
"table_data": table_data,
|
|
"overall_rmse": overall_rmse,
|
|
"has_initial": self._x_initial is not None,
|
|
"has_nominal": self._x_nominal is not None,
|
|
}
|