Repository navigation
Surface target max rho on user-selected overloads in pair estimation - #114
Merged
Merged
Conversation
…imulate-only) Load shedding and renewable-curtailment actions were blocked from selection in the Explore Pairs combination picker because the superposition-theorem estimator does not support them. Their simulation, however, is still well-defined — the previous block prevented a perfectly valid combined study. - Drop the selection-time restriction in CombinedActionsModal so any action pair can be picked, including LS/RC. - Surface a persistent "Simulate Combined" button in ExplorePairsTab (both the no-preview state and the comparison card), independent from the estimation flow. Estimate remains disabled when an LS/RC action is among the selected pair and explains why via tooltip and button label. - Update the associated Vitest specs. https://claude.ai/code/session_01HBuTAKw7NKbfoRREE4XFU2
…after simulate-only Previously the Estimate + Simulate buttons sat inside the comparison-card header, and the card was gated on a successful estimate. This made the simulate-only path (required for load shedding / curtailment pairs) feel bolted on — after Simulate Combined there was nowhere to read the result unless a preview happened to exist. - Remove both buttons from the card header. - Render the card whenever there is something to show: an estimate, a running simulation, or a simulation result. The Estimated Effect column is skipped when no preview exists, so the simulate-only case shows a single "Simulation Result" column. - Surface "Simulate Combined" as a full-width button above the card while an estimate is visible but no simulation has been run yet; hide it once the simulation feedback lands. - Keep the two-button no-preview area (Estimate + Simulate Combined) below the action table for the initial state only. - Tighten the title: "Explore Pairs Comparison" when an estimate is present, "Simulation Result" when only simulation exists. https://claude.ai/code/session_01HBuTAKw7NKbfoRREE4XFU2
…ed betas Estimating a pair via Explore Pairs (POST /api/compute-superposition) could return very different betas than the same pair's entry in the step2-computed "Computed Pairs" view (e.g. [3.193, 1.479] vs the pre-computed [1.10, 0.92], sometimes flagged as "unreliable — betas outside [-2.0, 3.0]"). Two causes, both corrected: 1. `obs_start` drift. The on-demand path fetched a fresh N-1 observation via `env.get_obs()` after `_get_n1_variant`. Each preceding `simulate_manual_action` used grid2op's `obs.simulate(action, keep_variant=True)`, which can mutate the shared N-1 variant — so the fresh fetch drifted from the step1 baseline that step2's discovery used. Prefer the N-1 observation captured in `_analysis_context.obs_simu_defaut` when available; fall back to `env.get_obs()` only when no context exists. 2. Monitoring-set mismatch. `_superposition_lines_overloaded` only read `_analysis_context["lines_overloaded"]`, a key populated exclusively by `restore_analysis_context` (session reload). After a fresh analysis the step1 context exposes the overload set as `lines_overloaded_ids` / `lines_overloaded_names`, so the lookup always missed and the code recomputed from `obs_start.rho` — producing a monitoring set diverging from what step2 used, and thus a different `max_rho_line` / `max_rho` for the same pair. Extend the lookup order to prefer `lines_overloaded_ids`, then `lines_overloaded_names`, and finally the legacy `lines_overloaded` key. Also adds: - Unit tests pinning context-obs reuse, fallback to fresh fetch, and the new lookup ordering in `_superposition_lines_overloaded`. - A real-data regression test in `test_combined_actions_scenario.py` asserting on-demand betas / max_rho agree (within 1e-3) with the step2 pre-computed entry for an identical pair; the test module is already skipped when pypowsybl / expert_op4grid_recommender are not installed. - Follow-up Vitest coverage for the prior UI refactor (top Simulate click routing, disabled state during simulation, card title flip between "Explore Pairs Comparison" and "Simulation Result"). https://claude.ai/code/session_01HBuTAKw7NKbfoRREE4XFU2
…load set When Run Analysis pre-computes a combined pair, the library picks max_rho_line as the globally highest linearised rho across every monitored line — so it can warn on new overloads the pair might introduce. But on lines far from either action the superposition linearisation is inaccurate: it can put an arbitrary far-from- contingency line (e.g. "LOUHAL31PYMON at 81.9%") at the top of the scan even when the actual simulation finds the max on a different, unrelated line (e.g. "BOCTOL71N.SE5 at 52%"). The estimate and the simulation end up pointing at different lines at different values, both below the monitoring factor — confusing operators who are really asking "did the pair resolve my overloads?". This change preserves the global-max warning semantics (required by `test_superposition_max_rho_filtering_regression`) and adds a side-channel target_max_rho / target_max_rho_line, scoped to the user-selected `lines_overloaded_ids`. Both the on-demand Explore Pairs re-estimation and the step2 Computed Pairs view get the new fields. - `simulation_helpers.compute_target_max_rho` — shared helper that picks (rho, line) over the overloaded-id set only. - `SimulationMixin._augment_superposition_result` — emits target_* alongside max_*. - `AnalysisMixin._augment_combined_actions_with_target_max_rho` — post-processes each library-populated pair with the same helper, leaving the library's max_rho_line untouched. - Frontend `CombinedAction` type gains target_max_rho / target_max_rho_line (both optional). - Explore Pairs comparison card shows a small "Target overload: X% on LINE" line under the estimated effect when the target line differs from the global estimated line. - Computed Pairs table shows a "target: X% on LINE" subtitle under the Line (Est.) cell with the same condition. Tests: 4 new backend specs (on-demand emits target fields, shape stability without analysis context, step2 augment with target fields, no-op without context) + 2 new frontend specs on each UI surface. 37/37 backend superposition tests pass, 1156/1156 frontend Vitest specs pass, lint + typecheck clean. https://claude.ai/code/session_01HBuTAKw7NKbfoRREE4XFU2
The previous commit that added `target_max_rho` augmentation
accidentally deleted the `enriched_actions` block in
`AnalysisMixin.run_analysis_step2`, producing a runtime
`NameError: name 'enriched_actions' is not defined` the moment the
final `{type: "result"}` event tried to include the enriched action
feed — surfaced to the frontend as "Backend Error in Analysis
Resolution".
Restored the `_enrich_actions` call and the combined-id filter.
Added a regression test that drives the real
`AnalysisMixin.run_analysis_step2` generator end-to-end (not through
the endpoint-level mock seam used by `test_split_analysis`) and
asserts a typed `pdf` + `result` event pair with no `error` event.
The previous split-analysis test mocked the whole
`recommender_service.run_analysis_step2` call so it could not have
caught a body-level NameError — the new test closes that gap.
https://claude.ai/code/session_01HBuTAKw7NKbfoRREE4XFU2
…pair path Symptom: in the Computed Pairs table, the "Simulated Line" column can land on the contingency line itself (e.g. P.SAOL31RONCI at 52.3% after running analysis for that contingency) — physically impossible in N-1 where that line is disconnected and rho must be 0. Root cause: `simulate_manual_action` calls `_fetch_n_and_n1_observations` which only invokes `set_working_variant` when a cache MISSES (lines 314-330 of simulation_mixin.py). When both caches hit — the common case after step1 has already primed them — the working variant is left on whatever the previous caller positioned. The N branch a few lines above tends to set it to N when the N cache misses while the N-1 cache hits, so the net effect after the helper returns is "variant = N". The subsequent `obs_simu_defaut.simulate(action, keep_variant=True, ...)` then applies the combined action ON TOP OF the CURRENT working variant in-place — so the action runs against the N state, not N-1, and the result includes a non-zero rho on the contingency line. Fix: re-pin the working variant to the N-1 id (`_get_n1_variant`) immediately before the `.simulate()` call. This is cheap (variant lookup is cached) and defensive: it guarantees the simulation runs against N-1 regardless of what the cache-hit path or any upstream caller did with the variant. Regression test (test_superposition_service.py) drives `simulate_manual_action` with both N and N-1 caches primed and tracks the order of `set_working_variant` calls vs the `.simulate` call — asserts the last variant set BEFORE simulate is the N-1 id. Also updates `test_cache_invalidation_on_contingency_switch` to account for the extra `_get_n1_variant` lookup per call (was 2, now 3). Note: I was unable to reproduce the symptom locally — this sandbox doesn't have pypowsybl / expert_op4grid_recommender installed. The fix is derived from reading the cache / variant flow and matches the exact observed symptom (contingency line appearing as simulated max). Please re-run the Computed Pairs flow with config_small_grid.json + P.SAOL31RONCI to confirm. https://claude.ai/code/session_01HBuTAKw7NKbfoRREE4XFU2
The "Simulated Max Rho" column in the Combine Actions → Computed Pairs modal diverged from "Max Loading (Est.)" by up to ~46 points on the same line (e.g. 64.9% vs 52.0% on BOCTOL71N.SE5). The library internal verify `_verify_pair_max_rho_by_simulation` reports mean |gap| < 0.01, so the estimator is sound — the divergence is entirely in the backend's `simulate_manual_action` path. Root cause: `_fetch_n_and_n1_observations` calls `env.get_obs()` after `n.set_working_variant(n1)`, but the grid2op ↔ pypowsybl env bridge does not re-sync the returned observation to the current working variant. On the small test grid with contingency P.SAOL31RONCI, the backend's "N-1" obs carried rho=0.43 on the contingency line itself (it should be ≈0 in N-1), i.e. it was really the N state. The combined action then simulated against the wrong baseline → large rho drift on every downstream line. Fix: prefer the (obs, obs_simu_defaut) pair already captured by step1 and stored on `_analysis_context` — exactly the pattern `compute_superposition` already uses via `_obs_n1_from_context`. Do NOT overwrite `obs_simu_defaut._variant_id`: the library stamped it at step1 with its own kept variant, and `pypowsybl_backend.observation.simulate` clones from `self._variant_id` directly. The pre-simulate working-variant re-pin is now gated on the fallback branch only — the context path is variant-safe without it. Limitation carried over: session reload (`restore_analysis_context` doesn't serialize obs objects) and direct `/api/simulate-manual-action` without a prior step1 still exercise the stale-fetch fallback. Deeper fix requires repairing the env re-sync on `set_working_variant` — tracked separately. Validation on contingency P.SAOL31RONCI (top 15 pairs): est − backend_sim mean|gap|: 0.3461 → 0.0011 est − backend_sim max |gap|: 0.4735 → 0.0102 lib_sim − backend_sim mean|gap|: 0.3452 → 0.0000 line match est vs backend_sim: 15/15 Tests: adds two regressions in `test_cache_synchronization.py` covering the context-prefer path (get_obs never called, rho_before propagated from the context obs) and the fallback path (two get_obs calls + N-1 re-pin fired). Also ships the end-to-end diagnostic script `scripts/test_estimation_vs_simulation_small_grid.py`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ombined pair
The Computed Pairs card for a combined action like
`load_shedding_BEON3 TR311+reco_GEN.PY762` rendered only the
load-shedding voltage level as a clickable badge; the reco'd line
was dropped. Same behaviour for curtailment + reco, or any
combination where one leg is a topology action and the other is
load-shedding / curtailment. Pairs of the same kind
(two load sheddings, two topology actions) rendered correctly.
Root cause: `renderBadges()` in ActionCard.tsx used an
`if (isLoadShedding) { … } else if (isRenewableCurtailment) { … }
else { topology extraction }` chain. The presence of a single
load-shedding or curtailment detail short-circuited to its own
branch, skipping the topology extraction that would have surfaced
the reco / disco / coupling sub-action's line or VL.
Fix: collect badges cumulatively. Load-shedding VLs, curtailment
VLs, topology-derived VLs, topology-derived lines, and the
single-equipment fallback each contribute to a shared `badges`
array with a shared de-dup `vlSet`, so a combined pair always
gets one badge per sub-action. This mirrors the split-and-evaluate-
per-part pattern commit 150fd2a applied to `getActionTargetLines`
in utils/svg/highlights.ts.
Tests: two new regressions in ActionCard.test.tsx cover
`load_shedding + reco` and `curtail + reco`, asserting both the
LS/RC voltage level and the reco line are rendered as clickable
`<button>` elements.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
marota
pushed a commit
that referenced
this pull request
Apr 24, 2026
Follow-up to the root/frontend/backend CLAUDE.md + README refresh. Audits the remaining project docs against PRs merged 2026-04-21 → 2026-04-23 and updates the eight that had actually drifted. frontend/README.md (HIGH) Replaced the stock Vite-template boilerplate with a real frontend orientation: npm scripts, source-tree map (hooks, components, utils incl. the utils/svg/* split from PR #104), pointer to the auto-generated standalone bundle, testing + lint commands, and cross-refs to CLAUDE.md / PARITY_AUDIT.md / docs/README.md. expert_backend/tests/CLAUDE.md (MEDIUM) Full rewrite of the test-file inventory: added the PR #104 decomposition suites (test_simulation_helpers.py, test_analysis_helpers.py, test_diagram_helpers.py), the patch- endpoint coverage (test_diagram_patch_helpers.py, test_n1_diagram_fast_path.py) from PR #108, the regression guards (test_resimulate_regression.py, test_second_contingency_reset.py, test_get_n1_variant_clones_from_n_state.py, test_configurable_mw.py) that were added since 2026-04-11, and the dynamic reco_* reconnection path from PR #110. Replaced the frontend inventory with a structural summary matching the current ~60-file Vitest layout, listed the scripts/pypsa_eur pytest coverage, and documented the tests removed by PR #103 / #104 (test_ui_regressions.py, standaloneInterface.test.ts, cssRegression.test.ts). docs/features/combined-actions.md (MEDIUM) Added a "Recent updates (PR #114, release 0.6.5)" section covering LS/curtailment in combined pairs, Simulate Combined moved out of the card + clickable sub-action badges, target_max_rho on the user-selected overload set, and Explore- Pairs re-estimation aligned with the pre-computed betas. Rewrote the "Standalone Interface" section to describe the auto-generated bundle, and dropped the checklist line that required manual standalone_interface.html mirroring. Added a row to the key-files table for the ActionCard sub-action badges and the ComputedPairsTable / ExplorePairsTab split. docs/features/state-reset-and-confirmation-dialogs.md (MEDIUM) Replaced the standalone_interface.html section with a pointer to the auto-generated bundle, and expanded the "What reset() clears" list to match the current RecommenderService.reset() implementation (drain order + fast-path caches + _layout_cache + NAD-prefetch state, with the add-a-new-cache-goes-here guardrail). docs/features/detachable-viz-tabs.md (MEDIUM) Removed the "standalone_interface.html mirror" row and the follow-up caveat that said the single-file interface doesn't support detaching — both are obsolete now that the bundle is auto-generated from the React tree. docs/architecture/app-refactoring-plan.md (MEDIUM) Added a "Status: SHIPPED" banner at the top summarising both refactor waves (Phase 1 via PR #74, Phase 2 hook extraction via PR #109) and pointing readers at the current source of truth (frontend/CLAUDE.md + CHANGELOG.md 0.6.5). Preserved the original plan below the banner for historical value. docs/architecture/phase2-state-management-optimization.md (LOW) Added a "Status: Partially shipped" banner — memoization + React.memo pass shipped under PR #75, superseded in part by PR #109 (useN1Fetch / useDiagramHighlights extractions), orchestrator hooks still deferred. docs/features/frontend-ui-improvements.md (LOW) Scope header now names `frontend/dist-standalone/standalone.html` (auto-generated, PR #101) instead of the decommissioned hand-maintained file. https://claude.ai/code/session_01RsvHjjFbAauF3NvD9BNT5V
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR adds "target max rho" metrics to combined action pair estimation, allowing the UI to surface the pair's effect on the user-selected overloaded lines alongside the global max rho. This addresses a regression where linearisation error could place the global max on an unrelated line, obscuring the pair's actual impact on the contingency being resolved.
Key Changes
Backend (expert_backend/)
simulation_mixin.py:
compute_superposition()to reuse the N-1 observation from analysis context (obs_simu_defaut) instead of fetching a fresh one, ensuring numerical consistency with pre-computed pairs (Grid2Op'ssimulate()can mutate the shared N-1 variant)simulate_manual_action()immediately beforeobs.simulate()to prevent simulation against the N state when observation caches hit_superposition_lines_overloaded()to prefer step1-populated context keys (lines_overloaded_ids,lines_overloaded_names) over session-reload keys, keeping on-demand re-estimation aligned with pre-computed discoveryanalysis_mixin.py:
_augment_combined_actions_with_target_max_rho()method to enrich pre-computed pairs with target metrics scoped to user-selected overloadsrun_analysis_step2()after discovery completessimulation_helpers.py:
compute_target_max_rho()helper to calculate max rho/line over a subset of lines, returning(0.0, "N/A")when no overloads are availableFrontend (frontend/src/)
ExplorePairsTab.tsx:
ExplorePairsTab.test.tsx:
ComputedPairsTable.tsx/test.tsx:
target_max_rhoandtarget_max_rho_linefields toComputedPairEntryinterfaceCombinedActionsModal.tsx:
target_max_rhoandtarget_max_rho_linefrom backend response to modal datatypes.ts:
CombinedActioninterface with target max rho fieldsTests
compute_superposition()matches pre-computed pair values_get_n1_variantcall from explicit pinningNotable Implementation Details
max_rhoscan to catch newly-introduced overloads while adding a scoped "target" metric for the contingency's actual overloads"N/A"sentinel when no target info available, simplifying frontend branching logichttps://claude.ai/code/session_01HBuTAKw7NKbfoRREE4XFU2