From 4b271bf4d121e1765ab9e4f9cca7136b4308f5ee Mon Sep 17 00:00:00 2001 From: SPS Date: Fri, 12 Dec 2025 06:36:31 -0700 Subject: [PATCH] Fix dashboard aggregation across multiple runs --- docs/source/api_reference/simpm.dashboard.rst | 12 ++++ src/simpm/dashboard.py | 55 ++++++++++++++++--- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/docs/source/api_reference/simpm.dashboard.rst b/docs/source/api_reference/simpm.dashboard.rst index a9670fc..2d07011 100644 --- a/docs/source/api_reference/simpm.dashboard.rst +++ b/docs/source/api_reference/simpm.dashboard.rst @@ -34,3 +34,15 @@ Usage :undoc-members: :show-inheritance: :noindex: + + +Aggregating many runs +--------------------- + +When :func:`simpm.run` executes a factory multiple times (for example, in a +Monte Carlo experiment), the dashboard automatically aggregates the results. +Each replication is assigned a unique ``run_id`` and all entity, resource, and +environment logs are merged into a single dataset so the dashboard tables, +charts, and download buttons display *every* run—not just the last one. Use the +"Run" selector in the Overview panel to focus on a specific replication or pick +"All runs" to view the combined history. diff --git a/src/simpm/dashboard.py b/src/simpm/dashboard.py index a1e69d8..811aef2 100644 --- a/src/simpm/dashboard.py +++ b/src/simpm/dashboard.py @@ -1210,10 +1210,44 @@ def run(self, host: str = "127.0.0.1", port: int = 8050, async_mode: bool = Fals def _aggregate_envs_to_snapshot(envs: List[Any]) -> RunSnapshot: - """Aggregate multiple environments into a single RunSnapshot. - - Used when ``simpm.run`` has executed many replications via an env_factory. + """Aggregate multiple environments into a single :class:`RunSnapshot`. + + The dashboard can be launched after a Monte Carlo experiment where an + environment factory was executed many times. Each replication maintains its + own entities, resources, and log streams, but the dashboard should present + the complete set rather than only the final run. This helper collects a + snapshot per environment and *reindexes* every run to a unique ``run_id`` so + that run filters, tables, and plots operate on all runs consistently. """ + + def _with_run_id(snapshot: RunSnapshot, run_id: int) -> RunSnapshot: + """Deep-copy a snapshot while forcing ``run_id`` fields to ``run_id``. + + This keeps per-run data aligned with the combined ``run_history`` and + prevents multiple replications from reusing ``run_id=1`` (the default + when each environment only executes once). + """ + + def _set_run_id(obj: Any) -> Any: + if isinstance(obj, dict): + updated: dict[str, Any] = {} + for key, value in obj.items(): + if key == "run_id": + updated[key] = run_id + else: + updated[key] = _set_run_id(value) + return updated + if isinstance(obj, list): + return [_set_run_id(item) for item in obj] + return obj + + return RunSnapshot( + environment=_set_run_id(snapshot.environment), + entities=[_set_run_id(ent) for ent in snapshot.entities], + resources=[_set_run_id(res) for res in snapshot.resources], + logs=[_set_run_id(log) for log in snapshot.logs], + ) + snapshots: list[RunSnapshot] = [] for env in envs: if isinstance(env, RunSnapshot): @@ -1231,13 +1265,18 @@ def _aggregate_envs_to_snapshot(envs: List[Any]) -> RunSnapshot: all_logs: list[dict[str, Any]] = [] combined_run_history: list[dict[str, Any]] = [] - for snap in snapshots: - all_entities.extend(snap.entities) - all_resources.extend(snap.resources) - all_logs.extend(snap.logs) + for idx, snap in enumerate(snapshots, start=1): + remapped_snap = _with_run_id(snap, idx) + all_entities.extend(remapped_snap.entities) + all_resources.extend(remapped_snap.resources) + all_logs.extend(remapped_snap.logs) + rh = snap.environment.get("run_history") or [] if isinstance(rh, list): - combined_run_history.extend(rh) + for row in rh: + rh_row = dict(row) + rh_row["run_id"] = idx + combined_run_history.append(rh_row) # Re-index run_ids to 1..N for clarity in the dashboard for idx, rh in enumerate(combined_run_history, start=1):