Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,672 changes: 1,072 additions & 600 deletions data/rte7000_tht/grids/grid_437818d6/graded_contingencies.json

Large diffs are not rendered by default.

2,188 changes: 1,383 additions & 805 deletions data/rte7000_tht/grids/grid_5384e039/graded_contingencies.json

Large diffs are not rendered by default.

4,184 changes: 2,824 additions & 1,360 deletions data/rte7000_tht/grids/grid_5fc376c7/graded_contingencies.json

Large diffs are not rendered by default.

4,775 changes: 3,145 additions & 1,630 deletions data/rte7000_tht/grids/grid_e4e81e29/graded_contingencies.json

Large diffs are not rendered by default.

12,837 changes: 8,433 additions & 4,404 deletions data/rte7000_tht/scenarios.json

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions expert_backend/services/simulation_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,25 @@ def compute_action_metrics(
except Exception as e:
logger.warning("compute_action_metrics: max_rho / overload calc failed: %s", e)

# Persistent N-state overloads: monitored lines already overloaded in the N
# base case that this action did NOT influence (excluded from the care mask
# by build_care_mask's ``pre_existing & not_impacted`` rule) and that remain
# overloaded after it. These are pre-existing constraints the contingency
# didn't cause and the remedial action can't be expected to clear — surfaced
# so the UI can explain a line staying red that the player cannot act on.
result["persistent_n_overloads"] = []
try:
wt = float(worsening_threshold)
in_scope = np.isin(action_names, list(lines_we_care_about)) & np.isin(
action_names, list(branches_with_limits)
)
pre_existing = base_rho >= 1.0
not_impacted = (action_rho >= base_rho * (1 - wt)) & (action_rho <= base_rho * (1 + wt))
persistent = in_scope & pre_existing & not_impacted & (action_rho >= 1.0)
result["persistent_n_overloads"] = action_names[persistent].tolist()
except Exception as e:
logger.debug("compute_action_metrics: persistent N-overload calc failed: %s", e)

return result


Expand Down
1 change: 1 addition & 0 deletions expert_backend/services/simulation_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ def simulate_manual_action(
"n_components": metrics["n_components_after"],
"non_convergence": non_convergence,
"lines_overloaded_after": sanitize_for_json(metrics["lines_overloaded_after"]),
"persistent_n_overloads": sanitize_for_json(metrics.get("persistent_n_overloads", [])),
"half_open_overloads": sanitize_for_json(half_open_overloads),
"is_estimated": False,
}
Expand Down
37 changes: 37 additions & 0 deletions expert_backend/tests/test_simulation_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,43 @@ def test_islanding_below_mw_threshold_is_filtered(self):
# The bare topology counter is still surfaced for diagnostics.
assert metrics["n_components_after"] == 2

def test_persistent_n_overloads_reports_uninfluenced_preexisting(self):
"""A line already overloaded in the N base case that the action does not
influence and that stays overloaded is reported in persistent_n_overloads
(and excluded from lines_overloaded_after)."""
metrics = compute_action_metrics(
obs=self._obs([0.5, 1.3, 0.5]), # L2 pre-existing overload in N
obs_simu_defaut=self._obs([1.2, 1.3, 0.5]), # contingency overloads L1
obs_simu_action=self._obs([0.8, 1.3, 0.5]), # action clears L1, L2 untouched
info_action={"exception": None},
lines_overloaded_ids=[0], # L1 is the influenced overload
lines_we_care_about={"L1", "L2", "L3"},
branches_with_limits={"L1", "L2", "L3"},
monitoring_factor=0.95,
worsening_threshold=0.02,
)
assert metrics["persistent_n_overloads"] == ["L2"]
assert "L2" not in metrics["lines_overloaded_after"] # not the player's fault
assert metrics["lines_overloaded_after"] == [] # influenced overload cleared

def test_persistent_n_overloads_empty_when_preexisting_is_influenced(self):
"""If the action MOVES the pre-existing line beyond the worsening band it
is 'influenced' — it belongs to lines_overloaded_after, not the persistent
(uninfluenced) bucket."""
metrics = compute_action_metrics(
obs=self._obs([0.5, 1.3, 0.5]),
obs_simu_defaut=self._obs([1.2, 1.3, 0.5]),
obs_simu_action=self._obs([0.8, 1.6, 0.5]), # action pushed L2 up >2% (influenced)
info_action={"exception": None},
lines_overloaded_ids=[0],
lines_we_care_about={"L1", "L2", "L3"},
branches_with_limits={"L1", "L2", "L3"},
monitoring_factor=0.95,
worsening_threshold=0.02,
)
assert metrics["persistent_n_overloads"] == []
assert "L2" in metrics["lines_overloaded_after"]

def test_rho_reduction_detected(self):
metrics = compute_action_metrics(
obs=self._obs([0.5, 0.5, 0.5]),
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/components/ActionCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,20 @@ describe('ActionCard', () => {
expect(screen.getByText(/12\.5 MW disconnected/)).toBeInTheDocument();
});

it('surfaces persistent pre-existing N overloads with an explanatory bubble', () => {
const details = { ...baseDetails, persistent_n_overloads: ['DARSEL61RASSU', 'FESSEL61VOGEL'] };
render(<ActionCard {...defaultProps} details={details} />);
const bubble = screen.getByTestId('action-card-act_1-persistent-n');
expect(bubble).toBeInTheDocument();
expect(bubble).toHaveTextContent(/2 pre-existing N overloads persist/);
expect(bubble.getAttribute('title')).toContain('DARSEL61RASSU');
});

it('omits the persistent-N bubble when there are none', () => {
render(<ActionCard {...defaultProps} details={{ ...baseDetails, persistent_n_overloads: [] }} />);
expect(screen.queryByTestId('action-card-act_1-persistent-n')).not.toBeInTheDocument();
});

it('marks the card with data-viewing="true" when isViewing is true', () => {
// The viewing-state signal is now a higher-saturation left-edge
// accent stripe + a `data-viewing` attribute on the card root,
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/components/ActionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,15 @@ const ActionCard: React.FC<ActionCardProps> = ({
🏝️ Islanding detected ({details.disconnected_mw?.toFixed(1)} MW disconnected)
</div>
)}
{details.persistent_n_overloads && details.persistent_n_overloads.length > 0 && (
<div
data-testid={`action-card-${id}-persistent-n`}
title={`These lines are already overloaded in the base (N) state — before any contingency — so this remedial action can't clear them: ${details.persistent_n_overloads.join(', ')}. They are not counted against solving this study.`}
style={{ fontSize: '11px', background: colors.warningSoft, color: colors.warningText, padding: '4px 8px', marginTop: '6px', borderRadius: '4px', border: `1px solid ${colors.warningBorder}`, cursor: 'help', display: 'inline-block' }}
>
ℹ️ {details.persistent_n_overloads.length} pre-existing N overload{details.persistent_n_overloads.length > 1 ? 's' : ''} persist (not caused by this contingency)
</div>
)}

{/* Progressive disclosure: description, parameter editors,
and per-line "Loading after" only render on the viewing
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/game/rte7000Presets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ describe('RTE7000 France THT scenario data', () => {
for (const t of RTE7000_TIERS) expect(t.label).toBeTruthy();
});

it('matches the graded database snapshot (656 = 453 easy / 79 medium / 124 hard)', () => {
expect(RTE7000_EASY.length).toBe(453);
expect(RTE7000_MEDIUM.length).toBe(79);
expect(RTE7000_HARD.length).toBe(124);
it('matches the graded database snapshot (655 = 457 easy / 80 medium / 118 hard)', () => {
expect(RTE7000_EASY.length).toBe(457);
expect(RTE7000_MEDIUM.length).toBe(80);
expect(RTE7000_HARD.length).toBe(118);
const total = RTE7000_EASY.length + RTE7000_MEDIUM.length + RTE7000_HARD.length;
expect(total).toBe(656);
expect(total).toBe(655);
});

it('every study is playable (network + actions + layout + contingency)', () => {
Expand Down
Loading
Loading