diff --git a/expert_backend/services/recommender_service.py b/expert_backend/services/recommender_service.py index 5b7fa87e..e1ecf39f 100644 --- a/expert_backend/services/recommender_service.py +++ b/expert_backend/services/recommender_service.py @@ -947,22 +947,72 @@ def _get_monitoring_parameters(self, obs): return lines_we_care_about, branches_with_limits - def _generate_diagram(self, network): + def _load_layout(self): + """Load layout DataFrame from grid_layout.json if available.""" + import pandas as pd + import json + + layout_file = getattr(config, 'LAYOUT_FILE_PATH', None) + if layout_file and layout_file.exists(): + try: + with open(layout_file, 'r') as f: + layout_data = json.load(f) + records = [{'id': k, 'x': v[0], 'y': v[1]} for k, v in layout_data.items()] + return pd.DataFrame(records).set_index('id') + except Exception as e: + print(f"Warning: Could not load layout: {e}") + return None + + def _default_nad_parameters(self): + """Return default NadParameters for diagram generation.""" + from pypowsybl.network import NadParameters + return NadParameters( + edge_name_displayed=False, + id_displayed=False, + edge_info_along_edge=True, + power_value_precision=1, + angle_value_precision=0, + current_value_precision=1, + voltage_value_precision=0, + bus_legend=True, + substation_description_displayed=True + ) + + def _generate_diagram(self, network, voltage_level_ids=None, depth=0): """Generate NAD and return svg + metadata dict.""" from pypowsybl_jupyter.util import _get_svg_string, _get_svg_metadata + import time + + print(f"[RECO] Generating diagram (VLs={voltage_level_ids}, depth={depth})...") + t0 = time.time() - diagram = network.get_network_area_diagram() + df_layout = self._load_layout() + npars = self._default_nad_parameters() + + kwargs = dict(nad_parameters=npars) + if df_layout is not None: + kwargs['fixed_positions'] = df_layout + if voltage_level_ids is not None: + kwargs['voltage_level_ids'] = voltage_level_ids + kwargs['depth'] = depth + + diagram = network.get_network_area_diagram(**kwargs) + t1 = time.time() svg = _get_svg_string(diagram) + t2 = time.time() meta = _get_svg_metadata(diagram) + t3 = time.time() + print(f"[RECO] Diagram generated: NAD {t1-t0:.2f}s, SVG {t2-t1:.2f}s, Meta {t3-t2:.2f}s (SVG length={len(svg)})") + return { "svg": svg, "metadata": meta, } - def get_network_diagram(self): + def get_network_diagram(self, voltage_level_ids=None, depth=0): import pypowsybl as pp n = self._get_base_network() original_variant = n.get_working_variant_id() @@ -970,7 +1020,7 @@ def get_network_diagram(self): n.set_working_variant(n_variant_id) try: - diagram = self._generate_diagram(n) + diagram = self._generate_diagram(n, voltage_level_ids=voltage_level_ids, depth=depth) diagram["lines_overloaded"] = self._get_overloaded_lines(n, lines_we_care_about=self._get_lines_we_care_about()) # Cache N-state element currents for N-1 comparison self._n_state_currents = self._get_element_max_currents(n) @@ -978,7 +1028,7 @@ def get_network_diagram(self): finally: n.set_working_variant(original_variant) # Restore original variant - def get_n1_diagram(self, disconnected_element: str): + def get_n1_diagram(self, disconnected_element: str, voltage_level_ids=None, depth=0): import pypowsybl as pp import time @@ -991,14 +1041,19 @@ def get_n1_diagram(self, disconnected_element: str): n.set_working_variant(n1_variant_id) try: + # Check convergence — partial AC results are still better than DC # (DC only computes angles/power, not voltage magnitudes). # We need to re-run AC to get the results object for status + t0 = time.time() params = create_olf_rte_parameter() results = self._run_ac_with_fallback(n, params) converged = any(r.status.name == 'CONVERGED' for r in results) lf_status = results[0].status.name if results else "UNKNOWN" + if not converged: + print(f"Warning: AC load flow did not converge for N-1 ({disconnected_element}): {lf_status}") + print(f"[RECO] N-1 LF check {disconnected_element}: {time.time()-t0:.2f}s") - diagram = self._generate_diagram(n) + diagram = self._generate_diagram(n, voltage_level_ids=voltage_level_ids, depth=depth) diagram["lf_converged"] = converged diagram["lf_status"] = lf_status diff --git a/expert_backend/tests/test_recommender_regressions.py b/expert_backend/tests/test_recommender_regressions.py new file mode 100644 index 00000000..4cf4e8af --- /dev/null +++ b/expert_backend/tests/test_recommender_regressions.py @@ -0,0 +1,86 @@ +# Copyright (c) 2025-2026, RTE (https://www.rte-france.com) +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Co-Study4Grid a Power Grid Study tool Assistant Interface to help solve contigencies for a grid state under study. + +import unittest +from unittest.mock import MagicMock, patch +import pandas as pd +import json +from pathlib import Path + +# Add the project path to sys.path +import sys +project_root = "/Users/antoine/Dev/Co-Study4Grid" +if project_root not in sys.path: + sys.path.append(project_root) + +# Mock config BEFORE importing +mock_config = MagicMock() +mock_config.LAYOUT_FILE_PATH = Path("/tmp/grid_layout.json") +sys.modules["expert_op4grid_recommender.config"] = mock_config + +from expert_backend.services.recommender_service import RecommenderService + +class TestRecommenderRegressions(unittest.TestCase): + + def setUp(self): + self.service = RecommenderService() + + def test_load_layout_exists(self): + """Test that _load_layout is present and handles missing file.""" + self.assertTrue(hasattr(self.service, '_load_layout')) + # With non-existent file path + mock_config.LAYOUT_FILE_PATH = Path("/non/existent/path") + layout = self.service._load_layout() + self.assertIsNone(layout) + + def test_default_nad_parameters_exists(self): + """Test that _default_nad_parameters is present.""" + self.assertTrue(hasattr(self.service, '_default_nad_parameters')) + with patch('pypowsybl.network.NadParameters', return_value=MagicMock()) as mock_nad: + params = self.service._default_nad_parameters() + self.assertIsNotNone(params) + mock_nad.assert_called_once() + + def test_generate_diagram_parameters(self): + """Test that _generate_diagram passes parameters correctly.""" + mock_network = MagicMock() + + # Patch dependencies + with patch.object(self.service, '_load_layout', return_value=None), \ + patch.object(self.service, '_default_nad_parameters', return_value=MagicMock()), \ + patch('pypowsybl_jupyter.util._get_svg_string', return_value=""), \ + patch('pypowsybl_jupyter.util._get_svg_metadata', return_value={}): + + # Case 1: Minimal call + self.service._generate_diagram(mock_network) + _, kwargs = mock_network.get_network_area_diagram.call_args + self.assertIn('nad_parameters', kwargs) + self.assertNotIn('voltage_level_ids', kwargs) + + # Case 2: With VL filtering + self.service._generate_diagram(mock_network, voltage_level_ids=["VL1"], depth=2) + _, kwargs = mock_network.get_network_area_diagram.call_args + self.assertEqual(kwargs['voltage_level_ids'], ["VL1"]) + self.assertEqual(kwargs['depth'], 2) + + def test_diagram_getters_signatures(self): + """Test that getters have the correct signatures to prevent regression.""" + import inspect + + # get_network_diagram(self, voltage_level_ids=None, depth=0) + sig = inspect.signature(self.service.get_network_diagram) + self.assertIn('voltage_level_ids', sig.parameters) + self.assertIn('depth', sig.parameters) + + # get_n1_diagram(self, disconnected_element: str, voltage_level_ids=None, depth=0) + sig = inspect.signature(self.service.get_n1_diagram) + self.assertIn('disconnected_element', sig.parameters) + self.assertIn('voltage_level_ids', sig.parameters) + self.assertIn('depth', sig.parameters) + +if __name__ == '__main__': + unittest.main() diff --git a/expert_backend/tests/test_ui_regressions.py b/expert_backend/tests/test_ui_regressions.py new file mode 100644 index 00000000..d2f99f39 --- /dev/null +++ b/expert_backend/tests/test_ui_regressions.py @@ -0,0 +1,33 @@ +# Copyright (c) 2025-2026, RTE (https://www.rte-france.com) +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Co-Study4Grid a Power Grid Study tool Assistant Interface to help solve contigencies for a grid state under study. + +import unittest +import os + +class TestUIRegressions(unittest.TestCase): + + def test_critical_ui_strings(self): + """Verify that standalone_interface.html contains fixed regressions.""" + # Use relative path from this test file + test_dir = os.path.dirname(os.path.abspath(__file__)) + ui_file = os.path.abspath(os.path.join(test_dir, "../../standalone_interface.html")) + self.assertTrue(os.path.exists(ui_file), f"File not found: {ui_file}") + + with open(ui_file, 'r') as f: + content = f.read() + + # 1. Flow deltas check + self.assertIn('!flowDeltas && !assetDeltas', content, "Regression: check for assetDeltas missing in standalone_interface.html") + + # 2. Action highlight class + self.assertIn('sld-highlight-action', content, "Regression: highlight class 'sld-highlight-action' missing in standalone_interface.html") + + # 3. Placeholder text + self.assertIn('Select an action card to view its network variant', content, "Regression: placeholder text 'Select an action card to view its network variant' missing in standalone_interface.html") + +if __name__ == '__main__': + unittest.main() diff --git a/standalone_interface.html b/standalone_interface.html index 72978ec0..ba7032a7 100644 --- a/standalone_interface.html +++ b/standalone_interface.html @@ -2766,7 +2766,7 @@ const reactiveDeltas = vlOverlay.reactive_flow_deltas; const assetDeltas = vlOverlay.asset_deltas; - if (!flowDeltas) return; + if (!flowDeltas && !assetDeltas) return; // Build equipmentId -> [svgId, ...] mapping from SLD metadata. // pypowsybl SLD metadata uses 'nodes' (for lines, transformers, breakers, @@ -4260,7 +4260,7 @@
: (selectedActionId ?
Failed to load diagram for action {selectedActionId}
- :
Select an action to view its variant diagram
+ :
Select an action card to view its network variant
) ) }