ExpertOp4Grid 0.3.2__tar.gz → 0.3.2.post3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/ExpertOp4Grid.egg-info/PKG-INFO +2 -2
  2. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/PKG-INFO +2 -2
  3. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/graphs/overflow_graph.py +46 -7
  4. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/interactive_html.py +69 -0
  5. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/tests/test_interactive_html.py +30 -0
  6. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/tests/test_overflow_graph.py +118 -0
  7. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/setup.py +2 -2
  8. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/ExpertOp4Grid.egg-info/SOURCES.txt +0 -0
  9. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/ExpertOp4Grid.egg-info/dependency_links.txt +0 -0
  10. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/ExpertOp4Grid.egg-info/entry_points.txt +0 -0
  11. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/ExpertOp4Grid.egg-info/not-zip-safe +0 -0
  12. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/ExpertOp4Grid.egg-info/requires.txt +0 -0
  13. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/ExpertOp4Grid.egg-info/top_level.txt +0 -0
  14. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/LICENSE +0 -0
  15. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/MANIFEST.in +0 -0
  16. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/README.md +0 -0
  17. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/Expert_rule_action_verification.py +0 -0
  18. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/__init__.py +0 -0
  19. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/__init__.py +0 -0
  20. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/alphadeesp.py +0 -0
  21. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/elements.py +0 -0
  22. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/graphs/__init__.py +0 -0
  23. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/graphs/constants.py +0 -0
  24. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/graphs/constrained_path.py +0 -0
  25. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/graphs/graph_consolidation.py +0 -0
  26. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/graphs/graph_utils.py +0 -0
  27. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/graphs/null_flow.py +0 -0
  28. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/graphs/null_flow_graph.py +0 -0
  29. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/graphs/power_flow_graph.py +0 -0
  30. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/graphs/shortest_paths.py +0 -0
  31. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/graphs/structured_overload_graph.py +0 -0
  32. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/graphsAndPaths.py +0 -0
  33. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/grid2op/Grid2opObservationLoader.py +0 -0
  34. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/grid2op/Grid2opSimulation.py +0 -0
  35. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/grid2op/__init__.py +0 -0
  36. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/grid2op/scoring.py +0 -0
  37. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/network.py +0 -0
  38. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/printer.py +0 -0
  39. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/pypownet/PypownetObservationLoader.py +0 -0
  40. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/pypownet/PypownetSimulation.py +0 -0
  41. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/pypownet/__init__.py +0 -0
  42. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/simulation.py +0 -0
  43. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/topo_applicator.py +0 -0
  44. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/topology_scorer.py +0 -0
  45. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/core/twin_nodes.py +0 -0
  46. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/expert_operator.py +0 -0
  47. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/main.py +0 -0
  48. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/ressources/config/config.ini +0 -0
  49. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/ressources/parameters/l2rpn_2019/chronics/a/datetimes.csv +0 -0
  50. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/ressources/parameters/l2rpn_2019/chronics/a/hazards.zip +0 -0
  51. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/ressources/parameters/l2rpn_2019/chronics/a/imaps.csv +0 -0
  52. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/ressources/parameters/l2rpn_2019/chronics/a/load_p.zip +0 -0
  53. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/ressources/parameters/l2rpn_2019/chronics/a/load_p_forecasted.zip +0 -0
  54. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/ressources/parameters/l2rpn_2019/chronics/a/load_q.zip +0 -0
  55. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/ressources/parameters/l2rpn_2019/chronics/a/load_q_forecasted.zip +0 -0
  56. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/ressources/parameters/l2rpn_2019/chronics/a/maintenance.zip +0 -0
  57. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/ressources/parameters/l2rpn_2019/chronics/a/prod_p.zip +0 -0
  58. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/ressources/parameters/l2rpn_2019/chronics/a/prod_p_forecasted.zip +0 -0
  59. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/ressources/parameters/l2rpn_2019/chronics/a/prods_p.csv +0 -0
  60. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/ressources/parameters/l2rpn_2019/chronics/a/simu_ids.csv +0 -0
  61. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/ressources/parameters/l2rpn_2019/config.py +0 -0
  62. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/ressources/parameters/l2rpn_2019/difficulty_levels.json +0 -0
  63. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/ressources/parameters/l2rpn_2019/grid.json +0 -0
  64. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/ressources/parameters/l2rpn_2019/grid_layout.json +0 -0
  65. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/ressources/parameters/l2rpn_2019/prods_charac.csv +0 -0
  66. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/tests/__init__.py +0 -0
  67. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/tests/alphadeesp_test.py +0 -0
  68. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/tests/graphs_test_helpers.py +0 -0
  69. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/tests/grid2op/__init__.py +0 -0
  70. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/tests/grid2op/grid2op_grid_test.py +0 -0
  71. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/tests/grid2op/test_integrations.py +0 -0
  72. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/tests/pypownet/__init__.py +0 -0
  73. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/tests/pypownet/grid_test.py +0 -0
  74. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/tests/pypownet/test_integrations.py +0 -0
  75. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/tests/test_alphadeesp_unit.py +0 -0
  76. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/tests/test_cli.py +0 -0
  77. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/tests/test_constrained_path.py +0 -0
  78. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/tests/test_expert_op.py +0 -0
  79. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/tests/test_expert_rules.py +0 -0
  80. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/tests/test_graph_utils.py +0 -0
  81. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/tests/test_graphs_package.py +0 -0
  82. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/tests/test_null_flow.py +0 -0
  83. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/tests/test_score_helpers.py +0 -0
  84. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/tests/test_shortest_paths.py +0 -0
  85. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/tests/test_topo_applicator.py +0 -0
  86. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/alphaDeesp/tests/test_topology_scorer.py +0 -0
  87. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/docs/DESCRIPTION.rst +0 -0
  88. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/docs/DETAILS.rst +0 -0
  89. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/docs/GETTING_STARTED.rst +0 -0
  90. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/docs/INSTALL.rst +0 -0
  91. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/docs/README.rst +0 -0
  92. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/docs/index.rst +0 -0
  93. {expertop4grid-0.3.2 → expertop4grid-0.3.2.post3}/setup.cfg +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ExpertOp4Grid
3
- Version: 0.3.2
3
+ Version: 0.3.2.post3
4
4
  Summary: Expert analysis algorithm for solving overloads in a powergrid
5
5
  Home-page: https://github.com/marota/ExpertOp4Grid/
6
- Download-URL: https://github.com/marota/ExpertOp4Grid/archive/refs/tags/v0.3.2.tar.gz
6
+ Download-URL: https://github.com/marota/ExpertOp4Grid/archive/refs/tags/v0.3.2.post3.tar.gz
7
7
  Author: Antoine Marot
8
8
  Author-email: antoine.marot@rte-france.com
9
9
  License: Mozilla Public License 2.0 (MPL 2.0)
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ExpertOp4Grid
3
- Version: 0.3.2
3
+ Version: 0.3.2.post3
4
4
  Summary: Expert analysis algorithm for solving overloads in a powergrid
5
5
  Home-page: https://github.com/marota/ExpertOp4Grid/
6
- Download-URL: https://github.com/marota/ExpertOp4Grid/archive/refs/tags/v0.3.2.tar.gz
6
+ Download-URL: https://github.com/marota/ExpertOp4Grid/archive/refs/tags/v0.3.2.post3.tar.gz
7
7
  Author: Antoine Marot
8
8
  Author-email: antoine.marot@rte-france.com
9
9
  License: Mozilla Public License 2.0 (MPL 2.0)
@@ -41,6 +41,7 @@ class OverFlowGraph(NullFlowGraphMixin, GraphConsolidationMixin, PowerFlowGraph)
41
41
  df_overflow: pd.DataFrame,
42
42
  layout: Optional[List[Tuple[float, float]]] = None,
43
43
  float_precision: str = "%.2f",
44
+ extra_lines_to_cut: Optional[Iterable[int]] = None,
44
45
  ) -> None:
45
46
  if "line_name" not in df_overflow.columns:
46
47
  df_overflow["line_name"] = [
@@ -49,6 +50,16 @@ class OverFlowGraph(NullFlowGraphMixin, GraphConsolidationMixin, PowerFlowGraph)
49
50
  ]
50
51
 
51
52
  self.df = df_overflow
53
+ # Subset of ``lines_to_cut`` that the caller wants the cut-analysis
54
+ # to treat like overloads (so they get the same black/constrained
55
+ # styling and feed the structured-overload graph the same way) but
56
+ # WITHOUT being classified as overloads in the viewer's
57
+ # ``Overloads`` layer / ``is_overload`` flag. Used by callers who
58
+ # want the recommender to find actions that prevent flow increase
59
+ # on otherwise-healthy lines (ExpertAgent's ``additionalLinesToCut``
60
+ # semantic). ``None`` / empty means "no extras" — every cut line
61
+ # is a true overload, preserving the legacy behaviour.
62
+ self.extra_lines_cut = set(extra_lines_to_cut or [])
52
63
  super().__init__(topo, lines_to_cut, layout, float_precision)
53
64
 
54
65
  def build_graph(self) -> None:
@@ -74,14 +85,23 @@ class OverFlowGraph(NullFlowGraphMixin, GraphConsolidationMixin, PowerFlowGraph)
74
85
  )
75
86
 
76
87
  cols = ("idx_or", "idx_ex", "delta_flows", "gray_edges", "line_name")
88
+ # Operator-selected extras must NOT be coloured black: black is the
89
+ # visual signal for "overload contingency line" used by both the
90
+ # ``Overloads`` layer and the structured-overload analyser. We
91
+ # therefore strip extras from the cut list passed to ``_edge_color``
92
+ # so they keep their natural flow polarity colour (coral / blue).
93
+ # They stay marked ``is_constrained`` and ``is_extra_cut`` so the
94
+ # downstream layers can still find them by flag.
95
+ cut_for_colour = [idx for idx in lines_to_cut if idx not in self.extra_lines_cut]
77
96
  for i, (origin, extremity, reported_flow, gray_edge, line_name) in enumerate(
78
97
  zip(*(self.df[c] for c in cols))):
79
98
  self._add_overflow_edge(
80
99
  g, origin, extremity, reported_flow, line_name,
81
- color=self._edge_color(i, reported_flow, gray_edge, lines_to_cut),
100
+ color=self._edge_color(i, reported_flow, gray_edge, cut_for_colour),
82
101
  scaling_factor=scaling_factor,
83
102
  min_penwidth=min_penwidth,
84
- is_constrained=(i in lines_to_cut))
103
+ is_constrained=(i in lines_to_cut),
104
+ is_extra_cut=(i in self.extra_lines_cut))
85
105
 
86
106
  @staticmethod
87
107
  def _edge_color(
@@ -105,6 +125,7 @@ class OverFlowGraph(NullFlowGraphMixin, GraphConsolidationMixin, PowerFlowGraph)
105
125
  scaling_factor: float,
106
126
  min_penwidth: float,
107
127
  is_constrained: bool,
128
+ is_extra_cut: bool = False,
108
129
  ) -> None:
109
130
  """Add a single styled overflow edge to g."""
110
131
  fp = self.float_precision
@@ -119,6 +140,12 @@ class OverFlowGraph(NullFlowGraphMixin, GraphConsolidationMixin, PowerFlowGraph)
119
140
  }
120
141
  if is_constrained:
121
142
  attrs["constrained"] = True
143
+ if is_extra_cut:
144
+ # Operator-supplied extra cut — gets the black/constrained
145
+ # styling like a real overload but the viewer's "Overloads"
146
+ # layer must skip it. ``highlight_significant_line_loading``
147
+ # respects this flag when stamping ``is_overload``.
148
+ attrs["is_extra_cut"] = True
122
149
  g.add_edge(origin, extremity, **attrs)
123
150
 
124
151
  def keep_overloads_components(self) -> None:
@@ -191,6 +218,7 @@ class OverFlowGraph(NullFlowGraphMixin, GraphConsolidationMixin, PowerFlowGraph)
191
218
  edge_names = nx.get_edge_attributes(self.g, "name")
192
219
  edge_colors = nx.get_edge_attributes(self.g, "color")
193
220
  edge_x_labels = nx.get_edge_attributes(self.g, "label")
221
+ edge_extra_cut = nx.get_edge_attributes(self.g, "is_extra_cut")
194
222
  label_font_color = {edge: "black" for edge in edge_names.keys()}
195
223
  color_label_highlight = "darkred"
196
224
 
@@ -204,18 +232,29 @@ class OverFlowGraph(NullFlowGraphMixin, GraphConsolidationMixin, PowerFlowGraph)
204
232
  current_edge_color = edge_colors[edge]
205
233
  before = dict_line_loading[edge_name]["before"]
206
234
  after = dict_line_loading[edge_name]["after"]
235
+ is_extra = bool(edge_extra_cut.get(edge, False))
207
236
 
208
237
  # Every entry in dict_line_loading is a monitored / low-
209
238
  # margin line; the black ones are additionally overloads.
210
- is_monitored_attrs[edge] = True
211
- if current_edge_color == "black":
212
- edge_x_labels[edge] = f'< {current_x_label} <BR/> <B>{before}%</B> → {after}%>'
213
- is_overload_attrs[edge] = True
239
+ # Operator-selected extras (``is_extra_cut``) are kept out
240
+ # of both flags so the viewer's ``Overloads`` and
241
+ # ``Low margin lines`` layers reflect the recommender's
242
+ # detected state, not user-supplied targets.
243
+ if not is_extra:
244
+ is_monitored_attrs[edge] = True
245
+ if current_edge_color == "black":
246
+ edge_x_labels[edge] = f'< {current_x_label} <BR/> <B>{before}%</B> → {after}%>'
247
+ is_overload_attrs[edge] = True
248
+ else:
249
+ edge_x_labels[edge] = f'< {current_x_label} <BR/> {before}% → <B>{after}%</B> >'
250
+ edge_colors[edge] = f'"{current_edge_color}:yellow:{current_edge_color}"'
214
251
  else:
252
+ # Extras keep their natural flow colour; only the
253
+ # ``before → 0%`` annotation surfaces the cut so the
254
+ # operator sees how their choice materialises.
215
255
  edge_x_labels[edge] = f'< {current_x_label} <BR/> {before}% → <B>{after}%</B> >'
216
256
 
217
257
  label_font_color[edge] = color_label_highlight
218
- edge_colors[edge] = f'"{current_edge_color}:yellow:{current_edge_color}"'
219
258
 
220
259
  nx.set_edge_attributes(self.g, edge_x_labels, "label")
221
260
  nx.set_edge_attributes(self.g, label_font_color, "fontcolor")
@@ -73,9 +73,34 @@ _SEMANTIC_LAYERS: List[Dict[str, str]] = [
73
73
  {"key": "in_red_loop", "label": "Red-loop paths", "swatch": "red-loop", "scope": "both"},
74
74
  {"key": "is_overload", "label": "Overloads", "swatch": "overload", "scope": "edge"},
75
75
  {"key": "is_monitored", "label": "Low margin lines", "swatch": "monitored", "scope": "edge"},
76
+ # Operator-supplied extras (ExpertAgent's `additionalLinesToCut`):
77
+ # cut in the analysis like overloads but rendered with their
78
+ # natural flow colour and excluded from the Overloads /
79
+ # Low margin lines layers. Surfaced as a dedicated layer so the
80
+ # operator can still see how their choice materialised.
81
+ {"key": "is_extra_cut", "label": "Extra lines to prevent flow increase", "swatch": "extra-cut", "scope": "edge"},
76
82
  {"key": "is_hub", "label": "Hubs", "swatch": "diamond", "scope": "node"},
77
83
  ]
78
84
 
85
+ # Threshold below which a node's ``value`` (prod − load, in MW) is
86
+ # treated as "no real prod/load here". Build-time conventions in the
87
+ # upstream simulators tag every node with ``prod_or_load="load"`` and
88
+ # ``value="0.0"`` even when no consumption exists, so a strict
89
+ # ``prod_or_load == "load"`` test would flood the layer with empty
90
+ # nodes. The 1 MW floor matches operator practice.
91
+ _PROD_LOAD_VALUE_FLOOR_MW = 1.0
92
+
93
+ # Per-kind config for the value-based node layers. Matched against the
94
+ # ``prod_or_load`` attribute set by ``build_nodes`` upstream
95
+ # (alphaDeesp/core/graphs/power_flow_graph.py and the simulator-specific
96
+ # build_nodes_v2 helpers). Each entry produces a single layer in the
97
+ # "Individual entities properties" section, populated only with the
98
+ # nodes whose absolute ``value`` clears ``_PROD_LOAD_VALUE_FLOOR_MW``.
99
+ _VALUE_NODE_LAYERS: List[Dict[str, str]] = [
100
+ {"key": "prod", "label": "Production nodes", "swatch": "prod-node"},
101
+ {"key": "load", "label": "Consumption nodes", "swatch": "load-node"},
102
+ ]
103
+
79
104
  # Per-layer-key section assignment. The JS renders one ``<h3>`` per
80
105
  # section in the order the sections are first encountered.
81
106
  _LAYER_SECTIONS: Dict[str, str] = {
@@ -85,10 +110,14 @@ _LAYER_SECTIONS: Dict[str, str] = {
85
110
  # Individual entities properties — per-edge / per-node flags.
86
111
  "semantic:is_overload": _SECTION_PROPERTIES,
87
112
  "semantic:is_monitored": _SECTION_PROPERTIES,
113
+ "semantic:is_extra_cut": _SECTION_PROPERTIES,
88
114
  "semantic:is_hub": _SECTION_PROPERTIES,
89
115
  "style:dashed": _SECTION_PROPERTIES,
90
116
  "style:dotted": _SECTION_PROPERTIES,
91
117
  "style:tapered": _SECTION_PROPERTIES,
118
+ # Value-based node layers — see _VALUE_NODE_LAYERS / build_nodes.
119
+ "node:prod": _SECTION_PROPERTIES,
120
+ "node:load": _SECTION_PROPERTIES,
92
121
  # Flow polarity buckets.
93
122
  "color:coral": _SECTION_FLOWS,
94
123
  "color:blue": _SECTION_FLOWS,
@@ -309,6 +338,40 @@ def _build_layer_index(
309
338
  "edges": bucket["edges"],
310
339
  })
311
340
 
341
+ # Value-based node layers (Production / Consumption). Built from
342
+ # the ``prod_or_load`` attribute upstream tagged on every node by
343
+ # ``build_nodes`` — see _VALUE_NODE_LAYERS. The white-coloured
344
+ # zero-balance nodes carry ``prod_or_load="load"`` AND
345
+ # ``value="0.0"`` upstream by convention; the 1 MW floor filters
346
+ # them out so the operator's "Consumption nodes" toggle doesn't
347
+ # also tag every passive substation.
348
+ if nodes:
349
+ value_buckets: Dict[str, List[str]] = {
350
+ cfg["key"]: [] for cfg in _VALUE_NODE_LAYERS
351
+ }
352
+ for n in nodes:
353
+ kind = n["attrs"].get("prod_or_load")
354
+ if kind not in value_buckets:
355
+ continue
356
+ try:
357
+ magnitude = abs(float(n["attrs"].get("value", "0")))
358
+ except (TypeError, ValueError):
359
+ continue
360
+ if magnitude < _PROD_LOAD_VALUE_FLOOR_MW:
361
+ continue
362
+ value_buckets[kind].append(n["name"])
363
+ for cfg in _VALUE_NODE_LAYERS:
364
+ ids = value_buckets[cfg["key"]]
365
+ if not ids:
366
+ continue
367
+ raw_layers.append({
368
+ "key": f"node:{cfg['key']}",
369
+ "label": cfg["label"],
370
+ "swatch": cfg["swatch"],
371
+ "nodes": ids,
372
+ "edges": [],
373
+ })
374
+
312
375
  # Drop layers without a section assignment (e.g. ``color:black``,
313
376
  # ``color:gray``, ``color:darkred`` — historically redundant
314
377
  # buckets). Then group by section in the canonical order so the
@@ -637,6 +700,12 @@ const MODEL = __MODEL_JSON__;
637
700
  if (swatch === 'constrained-path') return '<svg viewBox="0 0 14 6"><line x1="0" y1="3" x2="14" y2="3" stroke="black" stroke-width="2"/></svg>';
638
701
  if (swatch === 'overload') return '<svg viewBox="0 0 14 6"><line x1="0" y1="3" x2="14" y2="3" stroke="black" stroke-width="2.5"/><line x1="0" y1="3" x2="14" y2="3" stroke="yellow" stroke-width="0.8"/></svg>';
639
702
  if (swatch === 'monitored') return '<svg viewBox="0 0 14 6"><line x1="0" y1="3" x2="14" y2="3" stroke="coral" stroke-width="2.5"/><line x1="0" y1="3" x2="14" y2="3" stroke="yellow" stroke-width="0.8"/></svg>';
703
+ if (swatch === 'extra-cut') return '<svg viewBox="0 0 14 6"><line x1="0" y1="3" x2="14" y2="3" stroke="blue" stroke-width="2" stroke-dasharray="3 2"/></svg>';
704
+ // Match the upstream node fillcolors set in build_nodes:
705
+ // prod (prod_minus_load > 0) → coral
706
+ // load (prod_minus_load < 0) → lightblue
707
+ if (swatch === 'prod-node') return '<svg viewBox="0 0 10 10"><circle cx="5" cy="5" r="4" fill="coral" stroke="#444" stroke-width="0.6"/></svg>';
708
+ if (swatch === 'load-node') return '<svg viewBox="0 0 10 10"><circle cx="5" cy="5" r="4" fill="lightblue" stroke="#444" stroke-width="0.6"/></svg>';
640
709
  return '';
641
710
  }
642
711
  function swatchStyle(swatch) {
@@ -167,6 +167,35 @@ def test_layer_index_emits_semantic_layers_from_source_flags():
167
167
  assert set(by_key["semantic:is_monitored"]["nodes"]) == {"A", "B"}
168
168
 
169
169
 
170
+ def test_layer_index_emits_extra_cut_layer_with_endpoints():
171
+ """``is_extra_cut`` is an edge-only semantic flag; like the other
172
+ edge-only layers it must include the endpoint nodes so the
173
+ substations stay visible when the operator ticks it on alone, and
174
+ the layer must be assigned to the "Properties" section."""
175
+ edges = [
176
+ {"id": "edge1", "source": "A", "target": "B",
177
+ "attrs": {"color": "blue", "is_extra_cut": "True"}},
178
+ {"id": "edge2", "source": "B", "target": "C",
179
+ "attrs": {"color": "coral"}},
180
+ ]
181
+ nodes = [
182
+ {"name": "A", "attrs": {}},
183
+ {"name": "B", "attrs": {}},
184
+ {"name": "C", "attrs": {}},
185
+ ]
186
+ layers = _build_layer_index(edges, nodes)
187
+ by_key = {l["key"]: l for l in layers}
188
+
189
+ assert "semantic:is_extra_cut" in by_key
190
+ layer = by_key["semantic:is_extra_cut"]
191
+ assert layer["edges"] == ["edge1"]
192
+ assert set(layer["nodes"]) == {"A", "B"}
193
+ assert layer["swatch"] == "extra-cut"
194
+ assert layer["label"] == "Extra lines to prevent flow increase"
195
+ # Section assignment matches the other per-entity property layers.
196
+ assert layer["section"] == "Individual entities properties"
197
+
198
+
170
199
  def test_layer_index_skips_semantic_layer_when_no_match():
171
200
  """No noise: empty semantic buckets do NOT produce a layer entry."""
172
201
  edges = [
@@ -177,6 +206,7 @@ def test_layer_index_skips_semantic_layer_when_no_match():
177
206
  keys = {l["key"] for l in _build_layer_index(edges, nodes)}
178
207
  assert "semantic:is_hub" not in keys
179
208
  assert "semantic:in_red_loop" not in keys
209
+ assert "semantic:is_extra_cut" not in keys
180
210
  assert "color:coral" in keys
181
211
 
182
212
 
@@ -352,6 +352,124 @@ class TestOverFlowGraphScaling:
352
352
  assert penwidth == 1.5
353
353
 
354
354
 
355
+ # ──────────────────────────────────────────────────────────────────────
356
+ # extra_lines_to_cut: operator-supplied extras keep their natural flow
357
+ # colour, are flagged ``is_extra_cut`` (alongside ``constrained``), and
358
+ # stay out of the ``is_overload`` / ``is_monitored`` semantic layers.
359
+ # ──────────────────────────────────────────────────────────────────────
360
+
361
+
362
+ def _three_line_df():
363
+ """L1 positive overload, L2 negative extra-cut, L3 healthy positive."""
364
+ return pd.DataFrame({
365
+ "idx_or": [0, 1, 2],
366
+ "idx_ex": [1, 2, 0],
367
+ "delta_flows": [1000.0, -100.0, 50.0],
368
+ "gray_edges": [False, False, False],
369
+ "line_name": ["L1", "L2", "L3"],
370
+ })
371
+
372
+
373
+ def _edge_by_name(g, name):
374
+ for u, v, k, data in g.edges(keys=True, data=True):
375
+ if data.get("name") == name:
376
+ return (u, v, k), data
377
+ raise AssertionError(f"edge {name!r} not found")
378
+
379
+
380
+ class TestExtraLinesToCut:
381
+
382
+ def test_default_extras_is_empty(self):
383
+ ofg = OverFlowGraph(_basic_topo(3), [0, 1], _three_line_df())
384
+ assert ofg.extra_lines_cut == set()
385
+
386
+ def test_extras_stored_as_set(self):
387
+ ofg = OverFlowGraph(
388
+ _basic_topo(3), [0, 1], _three_line_df(),
389
+ extra_lines_to_cut=[1, 1],
390
+ )
391
+ assert ofg.extra_lines_cut == {1}
392
+
393
+ def test_extras_keep_natural_flow_colour(self):
394
+ """An extra-cut line never gets the black overload colour — it
395
+ keeps coral / blue based on its delta-flow polarity."""
396
+ ofg = OverFlowGraph(
397
+ _basic_topo(3), [0, 1], _three_line_df(),
398
+ extra_lines_to_cut=[1],
399
+ )
400
+ _, l1 = _edge_by_name(ofg.g, "L1") # in lines_to_cut, NOT extra
401
+ _, l2 = _edge_by_name(ofg.g, "L2") # in lines_to_cut AND extra
402
+ _, l3 = _edge_by_name(ofg.g, "L3") # not cut at all
403
+ assert l1["color"] == "black"
404
+ assert l2["color"] == "blue" # natural — delta_flows = -100
405
+ assert l3["color"] == "coral" # natural — delta_flows = +50
406
+
407
+ def test_extras_are_constrained_and_flagged(self):
408
+ ofg = OverFlowGraph(
409
+ _basic_topo(3), [0, 1], _three_line_df(),
410
+ extra_lines_to_cut=[1],
411
+ )
412
+ _, l1 = _edge_by_name(ofg.g, "L1")
413
+ _, l2 = _edge_by_name(ofg.g, "L2")
414
+ _, l3 = _edge_by_name(ofg.g, "L3")
415
+ # Real overload: constrained, not extra.
416
+ assert l1.get("constrained") is True
417
+ assert "is_extra_cut" not in l1
418
+ # Extra cut: both flags set so downstream layers can find it.
419
+ assert l2.get("constrained") is True
420
+ assert l2.get("is_extra_cut") is True
421
+ # Untouched line carries neither flag.
422
+ assert "constrained" not in l3
423
+ assert "is_extra_cut" not in l3
424
+
425
+ def test_extras_skipped_in_overload_and_monitored(self):
426
+ """``highlight_significant_line_loading`` must not stamp
427
+ ``is_overload`` / ``is_monitored`` on extras, must not yellow-tint
428
+ their colour, but should still annotate the edge label."""
429
+ ofg = OverFlowGraph(
430
+ _basic_topo(3), [0, 1], _three_line_df(),
431
+ extra_lines_to_cut=[1],
432
+ )
433
+ ofg.highlight_significant_line_loading({
434
+ "L1": {"before": 110, "after": 80},
435
+ "L2": {"before": 90, "after": 0},
436
+ "L3": {"before": 75, "after": 60},
437
+ })
438
+ _, l1 = _edge_by_name(ofg.g, "L1")
439
+ _, l2 = _edge_by_name(ofg.g, "L2")
440
+ _, l3 = _edge_by_name(ofg.g, "L3")
441
+
442
+ # L1 is a real overload: yellow-tinted, both flags set.
443
+ assert l1["color"] == '"black:yellow:black"'
444
+ assert l1.get("is_overload") is True
445
+ assert l1.get("is_monitored") is True
446
+
447
+ # L2 is the extra cut: keeps natural blue (no yellow tint), no
448
+ # is_overload / is_monitored, but the loading annotation still
449
+ # fires so the operator sees how their choice materialises.
450
+ assert l2["color"] == "blue"
451
+ assert l2.get("is_overload") is None
452
+ assert l2.get("is_monitored") is None
453
+ assert "90% → <B>0%</B>" in l2["label"]
454
+
455
+ # L3 is a low-margin line (not overload, not extra).
456
+ assert l3["color"] == '"coral:yellow:coral"'
457
+ assert l3.get("is_overload") is None
458
+ assert l3.get("is_monitored") is True
459
+
460
+ def test_legacy_behaviour_when_no_extras(self):
461
+ """Without ``extra_lines_to_cut`` the contingency lines render
462
+ black and get tagged as overloads — the legacy contract."""
463
+ ofg = OverFlowGraph(_basic_topo(3), [0], _three_line_df())
464
+ ofg.highlight_significant_line_loading({
465
+ "L1": {"before": 110, "after": 80},
466
+ })
467
+ _, l1 = _edge_by_name(ofg.g, "L1")
468
+ assert l1["color"] == '"black:yellow:black"'
469
+ assert l1.get("is_overload") is True
470
+ assert l1.get("is_extra_cut") is None
471
+
472
+
355
473
  # ──────────────────────────────────────────────────────────────────────
356
474
  # detect_edges_to_keep (full method)
357
475
  # ──────────────────────────────────────────────────────────────────────
@@ -28,7 +28,7 @@ pkgs = {
28
28
  }
29
29
 
30
30
  setup(name='ExpertOp4Grid',
31
- version='0.3.2',
31
+ version='0.3.2.post3',
32
32
  description='Expert analysis algorithm for solving overloads in a powergrid',
33
33
  long_description_content_type="text/markdown",
34
34
  python_requires=">=3.9",
@@ -50,7 +50,7 @@ setup(name='ExpertOp4Grid',
50
50
  author='Antoine Marot',
51
51
  author_email='antoine.marot@rte-france.com',
52
52
  url="https://github.com/marota/ExpertOp4Grid/",
53
- download_url = 'https://github.com/marota/ExpertOp4Grid/archive/refs/tags/v0.3.2.tar.gz',
53
+ download_url = 'https://github.com/marota/ExpertOp4Grid/archive/refs/tags/v0.3.2.post3.tar.gz',
54
54
  license='Mozilla Public License 2.0 (MPL 2.0)',
55
55
  packages=setuptools.find_packages(),
56
56
  extras_require=pkgs["extras"],