Files
Mujoco_WASM/python/mujoco/sysid/report/defaults.py
T
2026-02-11 00:22:56 -05:00

292 lines
9.1 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.
# ==============================================================================
"""Default report generation for system identification results."""
from collections.abc import Sequence
import pathlib
from mujoco.sysid._src import model_modifier
from mujoco.sysid._src import parameter
from mujoco.sysid._src.optimize import calculate_intervals
from mujoco.sysid._src.residual import BuildModelFn
from mujoco.sysid._src.trajectory import ModelSequences
from mujoco.sysid.report.builder import ReportBuilder
from mujoco.sysid.report.sections.covariance import Covariance
from mujoco.sysid.report.sections.optimization_trace import OptimizationTrace
from mujoco.sysid.report.sections.parameters import ParametersTable
from mujoco.sysid.report.sections.signals import SignalReport
import numpy as np
import scipy.optimize as scipy_optimize
def default_report(
models_sequences: Sequence[ModelSequences],
initial_params: parameter.ParameterDict,
opt_params: parameter.ParameterDict,
residual_fn,
opt_result: scipy_optimize.OptimizeResult,
title="SysID",
save_path=None,
build_model: BuildModelFn | None = model_modifier.apply_param_modifiers,
generate_videos=True,
) -> ReportBuilder:
"""Returns a ReportBuilder containing experiment results.
Users needing a custom report can copy and modify this code.
"""
from mujoco.sysid.report.sections.group import GroupSection
from mujoco.sysid.report.sections.insights import AutomatedInsights
from mujoco.sysid.report.sections.parameter_distribution import ParameterDistribution
from mujoco.sysid.report.sections.row import RowSection
from mujoco.sysid.report.sections.video import generate_video_from_trajectories
from mujoco.sysid.report.sections.video import VideoPlayer
####################################
# Build report
# Sections:
# Fit
# Parameter tables
# Confidence intervals
# Extras: Optimization trace
####################################
rb = ReportBuilder(title)
if generate_videos:
# 1. Video Player
if save_path is None:
raise ValueError("save_path is required when generate_videos=True")
if build_model is None:
raise ValueError("build_model is required when generate_videos=True")
# Collect ALL trajectories from all model sequences
all_trajectories = []
for model_sequences in models_sequences:
for traj in model_sequences.measured_rollout:
all_trajectories.append(traj)
# Use first model's spec for rendering
model_spec_to_render = models_sequences[0].spec
video_dir = pathlib.Path(save_path)
video_dir.mkdir(parents=True, exist_ok=True)
# Video 1: All (Initial + Nominal + Optimized)
# all trajectories concatenated
video_all_path = video_dir / "video_all.mp4"
generate_video_from_trajectories(
initial_params=initial_params,
opt_params=opt_params,
_build_model=build_model,
trajectories=all_trajectories,
model_spec=model_spec_to_render,
output_filepath=video_all_path,
fps=60,
)
# Video 2: Initial + Nominal (no optimized)
video_init_path = video_dir / "video_init.mp4"
generate_video_from_trajectories(
initial_params=initial_params,
opt_params=opt_params,
_build_model=build_model,
trajectories=all_trajectories,
model_spec=model_spec_to_render,
output_filepath=video_init_path,
render_opt=False,
fps=60,
)
# Video 3: Optimized + Nominal (no initial)
video_opt_path = video_dir / "video_opt.mp4"
generate_video_from_trajectories(
initial_params=initial_params,
opt_params=opt_params,
_build_model=build_model,
trajectories=all_trajectories,
model_spec=model_spec_to_render,
output_filepath=video_opt_path,
render_initial=False,
fps=60,
)
video_all_section = VideoPlayer(
title="All Models",
video_filepath=video_all_path,
anchor="visual_run_all",
autoplay=True,
muted=True,
width="100%",
height=None,
caption=(
"<span class='color-initial'>Initial</span>, <span"
" class='color-nominal'>Nominal</span>, <span"
" class='color-optimized'>Optimized</span>"
),
)
video_init_section = VideoPlayer(
title="Initial vs Nominal",
video_filepath=video_init_path,
anchor="visual_run_init",
autoplay=True,
muted=True,
width="100%",
height=None,
caption=(
"<span class='color-initial'>Initial</span>, <span"
" class='color-nominal'>Nominal</span>"
),
)
video_opt_section = VideoPlayer(
title="Optimized vs Nominal",
video_filepath=video_opt_path,
anchor="visual_run_opt",
autoplay=True,
muted=True,
width="100%",
height=None,
caption=(
"<span class='color-nominal'>Nominal</span>, <span"
" class='color-optimized'>Optimized</span>"
),
)
rb.add_section(
RowSection(
title="Visual Comparison",
sections=[video_all_section, video_init_section, video_opt_section],
anchor="visual_comparison",
description=(
"Visual comparison of the system identification results. The"
" nominal model is shown in green, the initial model in red,"
" and the optimized model in blue."
),
)
)
# 2. Automated Insights (Logs)
rb.add_section(AutomatedInsights("Automated Insights", opt_params))
# 3. Parameters Table (Unified)
rb.add_section(
ParametersTable(
"Parameters", opt_params, initial_params, anchor="Parameters"
)
)
# 4. Control Signals (per sequence, grouped like observations)
# Get predictions for initial solution.
names = [
f"{model_sequences.name}\n{sequence}"
for model_sequences in models_sequences
for sequence in model_sequences.sequence_name
]
_, pred0s, _ = residual_fn(
initial_params.as_vector(), initial_params, return_pred_all=True
)
residuals_star, preds_star, records_star = residual_fn(
opt_params.as_vector(), opt_params, return_pred_all=True
)
assert build_model is not None
model_hat = build_model(initial_params, models_sequences[0].spec)
# Build control signal reports for each sequence
control_reports = []
seq_idx = 0
for model_sequences in models_sequences:
for i, seq_name in enumerate(model_sequences.sequence_name):
ctrl_ts = model_sequences.control[i]
name = f"{model_sequences.name}\n{seq_name}"
control_reports.append(
SignalReport(
f"Sequence: {name}",
model_hat,
title_prefix="",
ts_dict={"control": ctrl_ts},
collapsible=True,
)
)
seq_idx += 1
rb.add_section(
GroupSection("Control Signals", control_reports, anchor="control_signals")
)
# 5. Observation Signals
observation_reports = []
for name, pred, record, pred0 in zip(
names, preds_star, records_star, pred0s, strict=True
):
obs_dict = {"initial": pred0[0], "nominal": record[0], "fitted": pred[0]}
observation_reports.append(
SignalReport(
f"Sequence: {name}",
model_hat,
title_prefix="",
ts_dict=obs_dict,
collapsible=True,
)
)
rb.add_section(
GroupSection(
"Observation Signals", observation_reports, anchor="observations"
)
)
covariance, intervals = calculate_intervals(residuals_star, opt_result.jac)
# 6. Parameter Distribution
rb.add_section(
ParameterDistribution(
title="Parameter Distribution",
opt_params=opt_params,
initial_params=initial_params,
confidence_intervals=intervals,
anchor="param_dist",
)
)
rb.add_section(
Covariance(
title="Covariance and Correlation",
anchor="cov",
covariance=covariance,
parameter_dict=opt_params,
)
)
# Add diagnostic optimization trace plots.
if "extras" in opt_result:
# Add to the report.
rb.add_section(
OptimizationTrace(
title="Optimization Trace",
anchor="opt",
objective=opt_result.extras.get("objective"),
candidate=opt_result.extras.get("candidate"),
bounds=opt_params.get_bounds(),
param_names=opt_params.get_non_frozen_parameter_names(),
)
)
rb.build()
if save_path:
rb.save(save_path / "report.html")
return rb