Files
Mujoco_WASM/python/mujoco/sysid/report/sections/parameters.py
T
Kevin Zakka 146a5c08f7 System identification toolbox for MuJoCo.
This resulted from a lengthy collaboration with @kevinzakka, @jonathanembleyriches, @nimrod-gileadi, @gizemozd, @quagla, and @yuval.
2026-02-09 12:12:24 -05:00

219 lines
6.5 KiB
Python

from typing import Any
import numpy as np
from mujoco.sysid._src import parameter
from mujoco.sysid.report.sections.base import ReportSection
class ParametersTable(ReportSection):
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,
}