Running the bundled example app¶
Running the example app¶
Note
Installing with optional dependencies for hosting the basic app is done by
pip install fbmc-linearisation-analysis[app-template]
This package comes bundled with an example app. It just needs a function to read data from internal CNECs, but this defaults to None and can be ignored.
The app is streamlit and the minimal configuration is to import the app function into a file, e.g appfile.py:
from fbmc_quality.linearisation_error_app import app
if __name__ == '__main__':
app()
This app can then be run from the commandline with:
streamlit run appfile.py
Which will start the streamlit dashboard.
If you have a function that reads data from internal CNECs it must return a dataframe with columns flow and fmax. It takes as argument the date-time range (start-end), and the name of the CNEC as it appears in JAO.
Repeating the setup as above with this function:
from fbmc_quality.linearisation_error_app import app
from XXX import internal_cnec_function
if __name__ == '__main__':
app(internal_cnec_function)
Example app¶
1import logging
2from datetime import date, timedelta
3from typing import Callable, Optional
4
5import numpy as np
6import pandas as pd
7import plotly.express as px
8import plotly.graph_objects as go
9import plotly.io as pio
10import streamlit as st
11from dotenv import load_dotenv
12from joblib import Parallel
13from pandas import NaT
14from pkg_resources import declare_namespace
15from pytz import timezone
16
17from fbmc_quality.dataframe_schemas.schemas import JaoData
18
19# from fbmc_quality.linearisation_analysis.process_data import get_from_to_bz_from_name
20from fbmc_quality.entsoe_data.fetch_entsoe_data import get_from_to_bz_from_name
21from fbmc_quality.enums.bidding_zones import BiddingZonesEnum
22from fbmc_quality.jao_data import get_cnec_id_from_name
23from fbmc_quality.jao_data.fetch_jao_data import create_uuid_from_string
24from fbmc_quality.linearisation_analysis import (
25 JaoDataAndNPS,
26 compute_cnec_vulnerability_to_err,
27 compute_linearisation_error,
28 compute_linearised_flow,
29 fetch_jao_data_basecase_nps_and_observed_nps,
30 load_data_for_corridor_cnec,
31 load_data_for_internal_cnec,
32)
33from fbmc_quality.linearisation_analysis.process_data import align_by_index_overlap
34from fbmc_quality.plotting.flow_map import compute_flow_geo_frame, draw_flow_map_figure, get_european_nps
35
36load_dotenv()
37logging.basicConfig(level="INFO")
38
39st.set_page_config(layout="wide")
40PARALLEL_CONTEXT = Parallel()
41
42SHADOW_CNECS = [
43 "13791_325 65% 420 Namsos-Ogndal + 30% 420 Namsos-Hofstad + 300 Tunnsjødal-Verdal",
44 "13791_325 65% 420 Namsos-Ogndal + 40% 420 Namsos-Hofstad + 300 Tunnsjødal-Verdal",
45 "15319_10 420 Sylling-Rjukan + 420 Hasle-Rød + 300 Sylling-Flesaker + 300 Tegneby-Flesaker",
46 "15319_182 25% 420 Rjukan-Kvilldal + 300 Mauranger-Blåfalli",
47 "L150_11 40% 420 Hasle-Tegneby + Hasle T6 Transformator P",
48 "13791_325 15% 420 Hasle-Rød + 300 Mauranger-Blåfalli",
49 "15290_10 40% 420 Høyanger-Sogndal + 300 Øvre Vinstra-Fåberg",
50 "14310_11 55% 300 Blåfalli-Sauda + 300 Husnes-Børtveit",
51 "13791_10 300 Mauranger-Blåfalli",
52 "13791_11 40% 300 Øvre Vinstra-Fåberg + 420 Moskog-Høyanger",
53 "15315_11 40% 300 Minne-Frogner + 300 Roa-Ulven",
54 "L4_11 40% 420 Tegneby-Hasle + 300 Røykås-Tegneby",
55 "13791_325 65% 420 Rød-Grenland + 300 Rød-Porsgrunn",
56]
57
58
59@st.cache_data
60def get_data(start, end, _deanonymizer):
61 if isinstance(start, date) and isinstance(end, date):
62 if start > end:
63 return None
64
65 data_load_state = st.text("Loading data...")
66 data = fetch_jao_data_basecase_nps_and_observed_nps(start, end)
67 if _deanonymizer is not None:
68 jaodata = data.jaoData
69 jaodata[JaoData.cnecName] = jaodata[JaoData.cnecName].apply(_deanonymizer)
70 jaodata[JaoData.cnec_id] = jaodata.apply(
71 lambda row: create_uuid_from_string(row[JaoData.cnecName] + row[JaoData.contName]), axis=1
72 )
73 data = JaoDataAndNPS(jaodata, data.basecaseNPs, data.observedNPs)
74
75 data_load_state.text("Loading data...done!")
76 return data
77
78
79class DataContainer:
80 def __init__(
81 self, data: JaoDataAndNPS, internal_cnec_func: Callable[[date, date, str], pd.DataFrame | None] | None
82 ):
83 self.data = data
84 self.internal_cnec_func = internal_cnec_func
85
86 @st.cache_data
87 def get_cnec_data(_self, selected_name: str, start, end):
88 data_load_state = st.text("Loading CNEC data...")
89
90 from_bz, to_bz = get_from_to_bz_from_name(selected_name)
91 if from_bz is None or to_bz is None:
92 if _self.internal_cnec_func is not None:
93 cnec_data = load_data_for_internal_cnec(selected_name, _self.internal_cnec_func, _self.data)
94 else:
95 st.error(f"No function for reading internal CNECs supplied, and no BZ found for {selected_name}")
96 cnec_data = None
97 else:
98 cnec_data = load_data_for_corridor_cnec(selected_name, _self.data)
99 data_load_state.text("Loading CNEC data...done!")
100 return cnec_data
101
102
103def get_names(data: JaoDataAndNPS) -> "pd.Series[pd.StringDtype]":
104 return pd.Series(data.jaoData[JaoData.cnecName].unique())
105
106
107@st.cache_data
108def get_data_for_all_cnecs(_internal_cnec_func, names: list[str | pd.StringDtype], start, end):
109 cnec_data = _internal_cnec_func(start, end, names)
110 return cnec_data
111
112
113def app(
114 internal_cnec_func: Callable[[date, date, str | list[str]], pd.DataFrame | dict[str, pd.DataFrame] | None]
115 | None = None,
116 deanonymizer: Callable[[str], str] | None = None,
117):
118 load_dotenv()
119
120 pio.templates.default = "ggplot2"
121 st.title("Linearisation Error Explorer")
122
123 if internal_cnec_func is not None:
124 tabs = st.tabs(["Single CNEC Analysis", "Period all cnec Analysis"])
125 single, all_cnecs = tabs
126 single_cnec_analysis(internal_cnec_func, deanonymizer, single)
127 all_cnecs_analysis(internal_cnec_func, deanonymizer, all_cnecs)
128 else:
129 single_cnec_analysis(None, deanonymizer, st)
130
131
132def draw_flow_map(time: pd.Timestamp, data: JaoDataAndNPS, european_nps: dict[BiddingZonesEnum, pd.Series], st_col):
133 with st_col.status("Plotting Flow..."):
134 st.write("Computing Flow for MTU...")
135 geo_df, obs_flow, fb_flow = compute_flow_geo_frame(time, data.jaoData, data.observedNPs, european_nps)
136 st.write("Drawing Flow map for MTU...")
137 fig = draw_flow_map_figure(
138 geo_df,
139 obs_flow,
140 fb_flow,
141 )
142 st.write("Rendering map...")
143 st_col.plotly_chart(fig, use_container_width=True)
144
145
146def all_cnecs_analysis(
147 internal_cnec_func: Callable[[date, date, list[str]], dict[str, pd.DataFrame] | None] | None = None,
148 deanonymizer: Callable[[str, Optional[bool]], str] | None = None,
149 st=None,
150):
151 if st is None:
152 return
153
154 start = st.date_input("Start Date", value=None, max_value=date.today() - timedelta(2), key="single_start")
155 end = st.date_input("End Date", value=None, max_value=date.today() - timedelta(1), key="single_end")
156 utc = timezone("utc")
157 start = utc.localize(pd.Timestamp(start))
158 end = utc.localize(pd.Timestamp(end))
159
160 st.text(
161 """
162 Plot of the Vulnerability score for all CNECs in a period.
163 The x axis shows if the FB process consistently under or overestimated the flow.
164 When computing the Linearisation Error, the flow is capped to the max. flow allowable at the CNEC.
165 The Vulnerability Score is calculated as:
166 """
167 )
168
169 st.latex(r"\text{Relative} \hspace{0.1in} V=\frac{F_{obs} - min(F_{fb-max}, F_{fb})}{F_{limit} - F_{obs}}")
170 names = None
171 data = None
172 all_cnec_data = None
173 overallocated_capacity = None
174 underallocated_capacity = None
175
176 if start is not NaT and end is not NaT:
177 data = get_data(start, end, deanonymizer)
178 if data is not None:
179 names = list(get_names(data))
180
181 if names is not None:
182 all_cnec_data = get_data_for_all_cnecs(internal_cnec_func, names, start, end)
183
184 if all_cnec_data is not None and data is not None:
185 too_much_allocated_capacity = []
186 too_little_allocated_capacity = []
187
188 for cnec_name, frame in all_cnec_data.items():
189 try:
190 cnec_id = get_cnec_id_from_name(cnec_name, data.jaoData)
191
192 if ("fmax" not in frame.columns) or ("flow" not in frame.columns):
193 raise ValueError('The internal cnec function must return a frame with columns "flow" and "fmax"')
194
195 cnec_data = data.jaoData.xs(cnec_id, level=JaoData.cnec_id)
196 overlap = align_by_index_overlap(cnec_data, frame, data.observedNPs)
197 frame = frame.loc[overlap]
198 vuln_cnec_data = compute_cnec_vulnerability_to_err(
199 cnec_data.loc[overlap],
200 data.observedNPs.loc[overlap],
201 frame["flow"].loc[overlap],
202 frame["fmax"].loc[overlap],
203 )
204 except:
205 continue
206
207 mtus_above_threshold = 100 * (vuln_cnec_data["vulnerability_score"] > 1).sum() / len(vuln_cnec_data)
208 median_above_zero = vuln_cnec_data["vulnerability_score"][
209 vuln_cnec_data["vulnerability_score"] > 0
210 ].median()
211
212 too_much_allocated_capacity.append(
213 {
214 "mtus_above_threshod": mtus_above_threshold,
215 "median_above_zero": median_above_zero,
216 "cnec": cnec_name,
217 "Significant Shadow Price": cnec_name in SHADOW_CNECS,
218 "Significant Domain Limit": (cnec_data[JaoData.nonRedundant].sum() / len(cnec_data)) > 0.1,
219 }
220 )
221
222 mtus_below_threshold = 100 * (vuln_cnec_data["vulnerability_score"] < -1).sum() / len(vuln_cnec_data)
223 median_below_zero = vuln_cnec_data["vulnerability_score"][
224 vuln_cnec_data["vulnerability_score"] < 0
225 ].median()
226 too_little_allocated_capacity.append(
227 {
228 "mtus_below_threshod": mtus_below_threshold,
229 "median_below_zero": median_below_zero,
230 "cnec": cnec_name,
231 "Significant Shadow Price": cnec_name in SHADOW_CNECS,
232 "Significant Domain Limit": (cnec_data[JaoData.nonRedundant].sum() / len(cnec_data)) > 0.1,
233 }
234 )
235
236 overallocated_capacity = pd.DataFrame(too_much_allocated_capacity)
237 overallocated_capacity = overallocated_capacity[
238 (overallocated_capacity["mtus_above_threshod"] > 0) | (overallocated_capacity["median_above_zero"] > 0.7)
239 ]
240 underallocated_capacity = pd.DataFrame(too_little_allocated_capacity)
241 underallocated_capacity = underallocated_capacity[
242 (underallocated_capacity["mtus_below_threshod"] > 0) | (underallocated_capacity["median_below_zero"] < -0.7)
243 ]
244
245 if overallocated_capacity is not None and underallocated_capacity is not None:
246 # st.dataframe(overallocated_capacity)
247 # st.dataframe(underallocated_capacity)
248
249 # fig_over = go.Figure()
250 # fig_over.add_trace(
251 # go.Scatter(
252 # x=overallocated_capacity["median_above_zero"],
253 # y=overallocated_capacity["mtus_above_threshod"],
254 # text=overallocated_capacity["cnec"],
255 # mode="markers",
256 # )
257 # )
258 fig_over = px.scatter(
259 overallocated_capacity,
260 x="median_above_zero",
261 y="mtus_above_threshod",
262 hover_data=["cnec"],
263 color="Significant Shadow Price",
264 symbol="Significant Domain Limit",
265 )
266
267 fig_over.update_layout(
268 title="CNECs that may have caused overloads",
269 xaxis_title="Median Vulnerability Score - for MTUS with V > 0 ",
270 yaxis_title=r"% of Active MTUS with Vulnerability score > 1",
271 font=dict(
272 size=24, # Set the font size here
273 ),
274 xaxis=dict(tickfont=dict(size=14)), # Change the size value as needed
275 yaxis=dict(tickfont=dict(size=14)), # Change the size value as needed
276 )
277 st.plotly_chart(fig_over, use_container_width=True)
278 filename = f"{start}-to-{end}-overloadrisk.html"
279 st.download_button("Download Overestimate Plot as HTML", fig_over.to_html(), file_name=filename)
280
281 # fig_under = go.Figure()
282 # fig_under.add_trace(
283 # go.Scatter(
284 # x=underallocated_capacity["median_below_zero"],
285 # y=underallocated_capacity["mtus_below_threshod"],
286 # text=underallocated_capacity["cnec"],
287 # mode="markers",
288 # )
289 # )
290
291 fig_under = px.scatter(
292 underallocated_capacity,
293 x="median_below_zero",
294 y="mtus_below_threshod",
295 hover_data=["cnec"],
296 color="Significant Shadow Price",
297 symbol="Significant Domain Limit",
298 )
299 fig_under.update_layout(
300 title="CNECs that may have caused too tight capacity restrictions",
301 xaxis_title="Median Vulnerability Score - for MTUS with V < 0 ",
302 yaxis_title=r"% of Active MTUS with Vulnerability score < -1",
303 font=dict(
304 size=24, # Set the font size here
305 ),
306 xaxis=dict(tickfont=dict(size=14)), # Change the size value as needed
307 yaxis=dict(tickfont=dict(size=14)), # Change the size value as needed
308 )
309 st.plotly_chart(fig_under, use_container_width=True)
310 filename = f"{start}-to-{end}-underallocaterisk.html"
311 st.download_button("Download Underestimate Plot as HTML", fig_under.to_html(), file_name=filename)
312
313
314def single_cnec_analysis(internal_cnec_func, deanonymizer, st):
315 col1, col2 = st.columns(2)
316 lin_err_from_cnec(internal_cnec_func, deanonymizer, col1, col2)
317
318
319def lin_err_from_cnec(internal_cnec_func, deanonymizer, st, map_st):
320 start = st.date_input("Start Date", value=None, max_value=date.today() - timedelta(2))
321 end = st.date_input("End Date", value=None, max_value=date.today() - timedelta(1))
322 utc = timezone("utc")
323 start = utc.localize(pd.Timestamp(start))
324 end = utc.localize(pd.Timestamp(end))
325 cnec_data = None
326 cnec_data_container = None
327 selected_name = None
328 data = None
329 new_fmax = st.number_input("Replace Fmax in calculations with Number")
330
331 if start is not NaT and end is not NaT:
332 data = get_data(start, end, deanonymizer)
333
334 if data is not None:
335 cnec_data_container = DataContainer(data, internal_cnec_func)
336 selected_name = st.selectbox(
337 "Which CNEC do you want to plot for?", get_names(data), index=None, placeholder="Search for CNEC..."
338 )
339
340 if selected_name is not None and cnec_data_container is not None:
341 cnec_data = cnec_data_container.get_cnec_data(selected_name, start, end)
342
343 if cnec_data is not None and data is not None:
344 lin_err = compute_linearisation_error(
345 cnec_data.cnecData, cnec_data.observedNPs, cnec_data.observed_flow["flow"]
346 )
347 lin_err_frame = pd.DataFrame(
348 {
349 "Linearisation Error": lin_err,
350 "Observed Flow": cnec_data.observed_flow["flow"],
351 "Linearised Flow": compute_linearised_flow(cnec_data.cnecData, cnec_data.observedNPs),
352 }
353 )
354
355 fig = px.density_contour(
356 lin_err_frame,
357 x="Observed Flow",
358 y="Linearised Flow",
359 marginal_x="box",
360 marginal_y="box",
361 width=600,
362 height=600,
363 title="Linearisation Error distribution",
364 )
365
366 reset_lin_err = lin_err_frame.reset_index()
367 new_frame = pd.melt(
368 reset_lin_err, id_vars=["time"], value_vars=[col for col in reset_lin_err.columns if col != "time"]
369 )
370 lineplot = px.line(
371 new_frame,
372 x="time",
373 y="value",
374 color="variable",
375 labels={"x": "Date", "y": "Flow and Linearisation Error"},
376 title="Linearisation Error timeseries",
377 )
378 fmax = (
379 cnec_data.observed_flow["fmax"]
380 if "fmax" in cnec_data.observed_flow.columns
381 else cnec_data.cnecData[JaoData.fmax]
382 )
383 fmax = fmax if not new_fmax else np.full_like(cnec_data.cnecData.index, new_fmax)
384 lineplot.add_trace(go.Scatter(x=cnec_data.observed_flow.index, y=fmax, name="Fmax", line=dict(dash="dash")))
385 st.plotly_chart(lineplot)
386
387 selected_time = map_st.selectbox("Select MTU to view flow", cnec_data.observed_flow.index)
388 if selected_time is not None:
389 try:
390 european_nps = get_european_nps(start, end)
391 draw_flow_map(selected_time, data, european_nps, map_st)
392 except Exception as e:
393 st.error(f"Drawing map failed with {e}")
394
395 fig.update_layout(
396 font=dict(
397 size=16, # Set the font size here
398 )
399 )
400 fig.update_traces(line={"width": 2})
401 st.plotly_chart(fig)
402
403 fig = px.box(
404 lin_err_frame,
405 x=lin_err_frame.index.date,
406 y="Linearisation Error",
407 labels={"x": "Date", "y": "Linearisation Error"},
408 )
409 fig.update_layout(title="Linearisation Error Boxplot per Day")
410 st.plotly_chart(fig)
411
412 vulnerability_frame = compute_cnec_vulnerability_to_err(
413 cnec_data.cnecData, cnec_data.observedNPs, cnec_data.observed_flow["flow"], fmax
414 )
415 reset_vuln_frame = vulnerability_frame.reset_index()
416 new_vuln_frame = pd.melt(
417 reset_vuln_frame, id_vars=["time"], value_vars=[col for col in reset_vuln_frame.columns if col != "time"]
418 )
419
420 fmax_mean = cnec_data.cnecData[JaoData.fmax].mean()
421 vuln_lineplot = px.line(
422 new_vuln_frame,
423 x="time",
424 y="value",
425 color="variable",
426 labels={"x": "Date", "y": "Score value"},
427 title=f"Vulnerability and Reliability against Fmax ~ {fmax_mean}",
428 )
429 st.plotly_chart(vuln_lineplot)
430
431
432if __name__ == "__main__":
433 app()