Source code for openswmm_gymnasium.viz.figures.hypervolume_trace

"""
Normalised single-point hypervolume per episode.

@author: Caleb Buahin
@copyright: Copyright (c) 2026 Caleb Buahin
@license: MIT
"""

from __future__ import annotations

from collections.abc import Sequence

import numpy as np
import plotly.graph_objects as go

from openswmm_gymnasium.scoring import normalized_hypervolume
from openswmm_gymnasium.viz.trajectory import TrajectoryRun


[docs] def hypervolume_trace( run: TrajectoryRun, *, ideal: Sequence[float], reference: Sequence[float], title: str = "Normalized hypervolume", ) -> go.Figure: """Per-episode single-point normalised HV, in C{[0, 1]}. Each episode contributes one point — its cumulative cost vector — against the supplied C{ideal} and C{reference} for normalisation. @param run: Multi-episode run. @type run: L{TrajectoryRun} @param ideal: Per-objective best-case cost (minimisation). @type ideal: sequence of float @param reference: Per-objective nadir cost. @type reference: sequence of float @param title: Figure title. @type title: str @rtype: L{plotly.graph_objects.Figure} """ fig = go.Figure() if len(run) == 0: return fig.update_layout(title=title) ideal_a = np.asarray(ideal, dtype=float) ref_a = np.asarray(reference, dtype=float) mat = run.cumulative_cost_matrix() if mat.shape[1] != ideal_a.shape[0]: raise ValueError( f"Number of cost components ({mat.shape[1]}) does not match " f"ideal/reference length ({ideal_a.shape[0]})" ) nhv = np.array( [normalized_hypervolume(mat[i : i + 1, :], ideal_a, ref_a) for i in range(mat.shape[0])], dtype=float, ) fig.add_trace( go.Scatter( x=np.arange(1, nhv.size + 1), y=nhv, mode="lines+markers", name="HV", ) ) fig.update_layout( title=title, xaxis_title="episode", yaxis_title="normalised HV", yaxis_range=[0.0, 1.0], ) return fig