From 7cc1891f1da1d4b4cc5f975bd1c92340cf185e51 Mon Sep 17 00:00:00 2001 From: 14Richa Date: Fri, 2 Aug 2024 17:20:52 +0100 Subject: [PATCH 01/26] added elexonpy in requirement.txt file --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5927607..636c329 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,4 +18,4 @@ fsspec==2024.2.0 s3fs==2024.2.0 ocf_blosc2==0.0.4 zarr==2.14.2 - +elexonpy From bc0bafdec37c0770a844c4191ded287f7bb579c5 Mon Sep 17 00:00:00 2001 From: 14Richa Date: Fri, 2 Aug 2024 17:57:27 +0100 Subject: [PATCH 02/26] added elexon solar forecast --- src/forecast.py | 195 ++++++++++++++++++++++++++++++------------------ 1 file changed, 121 insertions(+), 74 deletions(-) diff --git a/src/forecast.py b/src/forecast.py index cf02e87..38a57cf 100644 --- a/src/forecast.py +++ b/src/forecast.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta, time, timezone import numpy as np +import pandas as pd import plotly.graph_objects as go import streamlit as st from nowcasting_datamodel.connection import DatabaseConnection @@ -17,6 +18,58 @@ get_colour_from_model_name, model_is_probabilistic, get_recent_available_model_names ) +from datetime import datetime, timedelta +from elexonpy.api_client import ApiClient +from elexonpy.api.generation_forecast_api import GenerationForecastApi + + +def elexon_forecast_data(api_func, start_date, end_date, process_type): + try: + response = api_func( + _from=start_date.isoformat(), + to=end_date.isoformat(), + process_type=process_type, + format="json", + ) + if not response.data: + return pd.DataFrame() + + df = pd.DataFrame([item.to_dict() for item in response.data]) + solar_df = df[df["business_type"] == "Solar generation"] + solar_df["start_time"] = pd.to_datetime(solar_df["start_time"]) + solar_df = solar_df.set_index("start_time") + + if not solar_df.empty: + solar_df = solar_df.resample("30T")["quantity"].sum().reset_index() + # Do not fill missing values; leave as NaN + solar_df.set_index("start_time", inplace=True) + solar_df = solar_df.reset_index() + + return solar_df + except Exception as e: + st.error(f"Error fetching data for process type '{process_type}': {e}") + return pd.DataFrame() + +def plot_elexon_forecast(fig, elexon_forecasts): + for process_type, df in elexon_forecasts.items(): + if not df.empty and not df["quantity"].isnull().all(): # Ensure there is valid data + # Check for missing data; here, missing data is where 'quantity' is NaN + missing_data_ratio = df["quantity"].isnull().sum() / len(df) + if missing_data_ratio > 0.1: # Adjust threshold as needed + st.warning(f"Data for '{process_type}' contains significant gaps and will not be plotted.") + continue + + fig.add_trace( + go.Scatter( + x=df["start_time"], + y=df["quantity"], + mode="lines", + name=f"Elexon Forecast ({process_type})", + line=dict(dash="dot"), + showlegend=True + ) + ) + class GSPLabeler: """A function class to add the GSP name to the GSP IDs""" @@ -29,116 +82,88 @@ def __call__(self, gsp_id): """Get GSP label""" i = self.gsp_ids.index(gsp_id) return f"{gsp_id}: {self.gsp_names[i]}" - -def forecast_page(): - """Main page for status""" +def forecast_page(): st.markdown( f'

{"National and GSP Forecasts"}

', unsafe_allow_html=True, ) - + st.sidebar.subheader("Select Forecast Model") - + connection = DatabaseConnection(url=os.environ["DB_URL"], echo=True) with connection.get_session() as session: - - # Add dropdown to select GSP region locations = get_all_locations(session=session) locations = [Location.from_orm(loc) for loc in locations if loc.gsp_id < 318] gsp_ids = [loc.gsp_id for loc in locations] gsp_names = [loc.region_name for loc in locations] - + gsp_labeler = GSPLabeler(gsp_ids, gsp_names) - + gsp_id = st.sidebar.selectbox( - "Select a region", - gsp_ids, - index=0, + "Select a region", + gsp_ids, + index=0, format_func=gsp_labeler ) - - # Get effective capacity of selected GSP + capacity_mw = locations[gsp_ids.index(gsp_id)].installed_capacity_mw - - # Find recent available models available_models = get_recent_available_model_names(session) - - # Add selection for models selected_models = st.sidebar.multiselect("Select models", available_models, ["pvnet_v2"]) - - # If any selected models are probabilistic add checkbox to show quantiles selected_prob_models = [model for model in selected_models if model_is_probabilistic(model)] - - if len(selected_prob_models)>0: + + if len(selected_prob_models) > 0: show_prob = st.sidebar.checkbox("Show Probabilities Forecast", value=False) else: show_prob = False - - if gsp_id!=0 and ("National_xg" in selected_models): + + if gsp_id != 0 and ("National_xg" in selected_models): selected_models.remove("National_xg") st.sidebar.warning("National_xg only available for National forecast.") - - # Add selection for adjuster - use_adjuster = st.sidebar.radio("Use adjuster", [True, False], index=1) - # Add selection for forecast type + use_adjuster = st.sidebar.radio("Use adjuster", [True, False], index=1) forecast_type = st.sidebar.radio( "Forecast Type", ["Now", "Creation Time", "Forecast Horizon"], index=0 ) - + now = datetime.now() today = now.date() yesterday = today - timedelta(days=1) - + if forecast_type == "Now": start_datetimes = [today - timedelta(days=2)] end_datetimes = [None] - + elif forecast_type == "Creation Time": - # Add calendar to select start date - defaults to yesterday - date_sel = st.sidebar.date_input("Forecast creation date:", yesterday) - - # Add dropdown selection of init-times dt_sel = datetime.combine(date_sel, time(0, 0)) initial_times = [dt_sel - timedelta(days=1) + timedelta(hours=3 * i) for i in range(8)] initial_times += [dt_sel + timedelta(minutes=30 * i) for i in range(48)] - select_init_times = st.sidebar.multiselect( "Forecast creation time", initial_times, [initial_times[x] for x in [14, 20, 26, 32, 38]], ) - select_init_times = sorted(select_init_times) - start_datetimes = select_init_times end_datetimes = [t + timedelta(days=2) for t in select_init_times] elif forecast_type == "Forecast Horizon": - # Add calendar and time selections for datetime date_sel = st.sidebar.date_input("Forecast start date:", yesterday) time_sel = st.sidebar.time_input("Forecast start time", time(0, 0)) - dt_sel = datetime.combine(date_sel, time_sel) start_datetimes = [dt_sel] end_datetimes = [dt_sel + timedelta(days=2)] - - # Add selection for horizon - # 0-8 hours in 30 mintue chunks, 8-36 hours in 3 hour chunks forecast_horizon = st.sidebar.selectbox( "Forecast Horizon (mins)", list(range(0, 480, 30)) + list(range(480, 36 * 60, 180)), 0, ) - # Get the data to plot forecast_per_model = {} for model in selected_models: for start_dt, end_dt in zip(start_datetimes, end_datetimes): - if forecast_type == "Now": forecast_values = get_forecast_values_latest( session=session, @@ -147,7 +172,7 @@ def forecast_page(): start_datetime=start_dt, ) label = model - + elif forecast_type == "Creation Time": forecast_values = get_forecast_values( session=session, @@ -158,7 +183,7 @@ def forecast_page(): only_return_latest=True, ) label = f"{model} {start_dt}" - + elif forecast_type == "Forecast Horizon": forecast_values = get_forecast_values( session=session, @@ -171,7 +196,6 @@ def forecast_page(): ) label = model - # Make ForecastValue objects with _properties attribute and maybe adjust forecast_per_model[label] = [] for f in forecast_values: forecast_value = ForecastValue.from_orm(f) @@ -180,14 +204,38 @@ def forecast_page(): forecast_value = forecast_value.adjust(limit=1000) forecast_per_model[label].append(forecast_value) - - # Get pvlive values pvlive_data, pvlive_gsp_sum_dayafter, pvlive_gsp_sum_inday = get_pvlive_data( end_datetimes[0], gsp_id, session, start_datetimes[0] ) - # Make figure - fig = go.Figure( + # Initialize Elexon API client + api_client = ApiClient() + forecast_api = GenerationForecastApi(api_client) + forecast_generation_wind_and_solar_day_ahead_get = ( + forecast_api.forecast_generation_wind_and_solar_day_ahead_get + ) + + # Sidebar inputs for Elexon forecast + st.sidebar.subheader("Elexon Solar Forecast") + elexon_start_date = st.sidebar.date_input("Elexon Forecast Start Date", datetime.utcnow() - timedelta(days=3)) + elexon_end_date = st.sidebar.date_input("Elexon Forecast End Date", datetime.utcnow() + timedelta(days=3)) + process_types = ["Day Ahead", "Intraday Process", "Intraday Total"] + + if elexon_start_date < elexon_end_date: + elexon_forecasts = {} + for process_type in process_types: + elexon_forecasts[process_type] = elexon_forecast_data( + forecast_generation_wind_and_solar_day_ahead_get, + elexon_start_date, + elexon_end_date, + process_type + ) + else: + st.error("Elexon end date must be after the start date.") + + + + fig_forecast = go.Figure( layout=go.Layout( title=go.layout.Title(text="Latest Forecast"), xaxis=go.layout.XAxis(title=go.layout.xaxis.Title(text="Date")), @@ -195,26 +243,25 @@ def forecast_page(): legend=go.layout.Legend(title=go.layout.legend.Title(text="Chart Legend")), ) ) - - # Plot PVLive values and the forecasts - plot_pvlive(fig, gsp_id, pvlive_data, pvlive_gsp_sum_dayafter, pvlive_gsp_sum_inday) - plot_forecasts(fig, forecast_per_model, selected_prob_models, show_prob) - if end_datetimes[0] is None or now <= max(end_datetimes): - # Add vertical line to indicate now - fig.add_trace( - go.Scatter( - x=[now, now], - y=[0, capacity_mw], - mode="lines", - name="now", - line=dict(color="red", width=4, dash="dash"), - showlegend=False, - ) + plot_pvlive(fig_forecast, gsp_id, pvlive_data, pvlive_gsp_sum_dayafter, pvlive_gsp_sum_inday) + plot_forecasts(fig_forecast, forecast_per_model, selected_prob_models, show_prob) + + st.plotly_chart(fig_forecast, theme="streamlit") + + # Second figure: Elexon Forecast Chart + fig_elexon = go.Figure( + layout=go.Layout( + title=go.layout.Title(text="Elexon Forecast"), + xaxis=go.layout.XAxis(title=go.layout.xaxis.Title(text="Date")), + yaxis=go.layout.YAxis(title=go.layout.yaxis.Title(text="Quantity")), + legend=go.layout.Legend(title=go.layout.legend.Title(text="Chart Legend")), ) + ) - st.plotly_chart(fig, theme="streamlit") + plot_elexon_forecast(fig_elexon, elexon_forecasts) + st.plotly_chart(fig_elexon, theme="streamlit") def plot_pvlive(fig, gsp_id, pvlive_data, pvlive_gsp_sum_dayafter, pvlive_gsp_sum_inday): # pvlive on the chart @@ -227,7 +274,7 @@ def plot_pvlive(fig, gsp_id, pvlive_data, pvlive_gsp_sum_dayafter, pvlive_gsp_su line["dash"] = "dash" fig.add_trace(go.Scatter(x=x, y=y, mode="lines", name=k, line=line)) - + # pvlive gsp sum dictionary of values and chart for national forecast if gsp_id == 0: pvlive_gsp_sum_data = {} @@ -241,7 +288,7 @@ def plot_pvlive(fig, gsp_id, pvlive_data, pvlive_gsp_sum_dayafter, pvlive_gsp_su for k, v in pvlive_gsp_sum_data.items(): x = [i.datetime_utc for i in v] y = [i.solar_generation_kw / 1000 for i in v] - + line = {"color": get_colour_from_model_name(k)} if k == "PVLive GSP Sum Estimate": line["dash"] = "dash" @@ -273,7 +320,7 @@ def plot_forecasts(fig, forecast_per_model, selected_prob_models, show_prob): y=y, mode="lines", name=model, - line=dict(color=get_colour_from_model_name(model)), + line=dict(color=get_colour_from_model_name(model)), opacity=opacity, hovertemplate="
%{x}
" + "%{y:.2f}MW", legendgroup=model, @@ -300,7 +347,7 @@ def plot_forecasts(fig, forecast_per_model, selected_prob_models, show_prob): showlegend=False, ) ) - + fig.add_trace( go.Scatter( x=x, @@ -313,8 +360,8 @@ def plot_forecasts(fig, forecast_per_model, selected_prob_models, show_prob): showlegend=False, ) ) - - + + except Exception as e: print(e) print("Could not add plevel to chart") From 21905990773a8bfb33f127c80698a8841907f491 Mon Sep 17 00:00:00 2001 From: 14Richa Date: Thu, 8 Aug 2024 11:29:26 +0100 Subject: [PATCH 03/26] added elexon solar graoh to forecast page --- src/forecast.py | 202 ++++++++++++++++++++++++++---------------------- 1 file changed, 108 insertions(+), 94 deletions(-) diff --git a/src/forecast.py b/src/forecast.py index 38a57cf..e379d2b 100644 --- a/src/forecast.py +++ b/src/forecast.py @@ -1,7 +1,6 @@ import os from datetime import datetime, timedelta, time, timezone import numpy as np - import pandas as pd import plotly.graph_objects as go import streamlit as st @@ -17,60 +16,10 @@ from plots.utils import ( get_colour_from_model_name, model_is_probabilistic, get_recent_available_model_names ) - -from datetime import datetime, timedelta +from datetime import datetime, timedelta, time from elexonpy.api_client import ApiClient from elexonpy.api.generation_forecast_api import GenerationForecastApi - -def elexon_forecast_data(api_func, start_date, end_date, process_type): - try: - response = api_func( - _from=start_date.isoformat(), - to=end_date.isoformat(), - process_type=process_type, - format="json", - ) - if not response.data: - return pd.DataFrame() - - df = pd.DataFrame([item.to_dict() for item in response.data]) - solar_df = df[df["business_type"] == "Solar generation"] - solar_df["start_time"] = pd.to_datetime(solar_df["start_time"]) - solar_df = solar_df.set_index("start_time") - - if not solar_df.empty: - solar_df = solar_df.resample("30T")["quantity"].sum().reset_index() - # Do not fill missing values; leave as NaN - solar_df.set_index("start_time", inplace=True) - solar_df = solar_df.reset_index() - - return solar_df - except Exception as e: - st.error(f"Error fetching data for process type '{process_type}': {e}") - return pd.DataFrame() - -def plot_elexon_forecast(fig, elexon_forecasts): - for process_type, df in elexon_forecasts.items(): - if not df.empty and not df["quantity"].isnull().all(): # Ensure there is valid data - # Check for missing data; here, missing data is where 'quantity' is NaN - missing_data_ratio = df["quantity"].isnull().sum() / len(df) - if missing_data_ratio > 0.1: # Adjust threshold as needed - st.warning(f"Data for '{process_type}' contains significant gaps and will not be plotted.") - continue - - fig.add_trace( - go.Scatter( - x=df["start_time"], - y=df["quantity"], - mode="lines", - name=f"Elexon Forecast ({process_type})", - line=dict(dash="dot"), - showlegend=True - ) - ) - - class GSPLabeler: """A function class to add the GSP name to the GSP IDs""" def __init__(self, gsp_ids, gsp_names): @@ -109,8 +58,12 @@ def forecast_page(): ) capacity_mw = locations[gsp_ids.index(gsp_id)].installed_capacity_mw + + available_models = get_recent_available_model_names(session) + selected_models = st.sidebar.multiselect("Select models", available_models, ["pvnet_v2"]) + selected_prob_models = [model for model in selected_models if model_is_probabilistic(model)] if len(selected_prob_models) > 0: @@ -123,6 +76,7 @@ def forecast_page(): st.sidebar.warning("National_xg only available for National forecast.") use_adjuster = st.sidebar.radio("Use adjuster", [True, False], index=1) + forecast_type = st.sidebar.radio( "Forecast Type", ["Now", "Creation Time", "Forecast Horizon"], index=0 ) @@ -136,25 +90,33 @@ def forecast_page(): end_datetimes = [None] elif forecast_type == "Creation Time": + + date_sel = st.sidebar.date_input("Forecast creation date:", yesterday) + dt_sel = datetime.combine(date_sel, time(0, 0)) initial_times = [dt_sel - timedelta(days=1) + timedelta(hours=3 * i) for i in range(8)] initial_times += [dt_sel + timedelta(minutes=30 * i) for i in range(48)] + select_init_times = st.sidebar.multiselect( "Forecast creation time", initial_times, [initial_times[x] for x in [14, 20, 26, 32, 38]], ) + select_init_times = sorted(select_init_times) + start_datetimes = select_init_times end_datetimes = [t + timedelta(days=2) for t in select_init_times] elif forecast_type == "Forecast Horizon": date_sel = st.sidebar.date_input("Forecast start date:", yesterday) time_sel = st.sidebar.time_input("Forecast start time", time(0, 0)) + dt_sel = datetime.combine(date_sel, time_sel) start_datetimes = [dt_sel] end_datetimes = [dt_sel + timedelta(days=2)] + forecast_horizon = st.sidebar.selectbox( "Forecast Horizon (mins)", list(range(0, 480, 30)) + list(range(480, 36 * 60, 180)), @@ -208,34 +170,7 @@ def forecast_page(): end_datetimes[0], gsp_id, session, start_datetimes[0] ) - # Initialize Elexon API client - api_client = ApiClient() - forecast_api = GenerationForecastApi(api_client) - forecast_generation_wind_and_solar_day_ahead_get = ( - forecast_api.forecast_generation_wind_and_solar_day_ahead_get - ) - - # Sidebar inputs for Elexon forecast - st.sidebar.subheader("Elexon Solar Forecast") - elexon_start_date = st.sidebar.date_input("Elexon Forecast Start Date", datetime.utcnow() - timedelta(days=3)) - elexon_end_date = st.sidebar.date_input("Elexon Forecast End Date", datetime.utcnow() + timedelta(days=3)) - process_types = ["Day Ahead", "Intraday Process", "Intraday Total"] - - if elexon_start_date < elexon_end_date: - elexon_forecasts = {} - for process_type in process_types: - elexon_forecasts[process_type] = elexon_forecast_data( - forecast_generation_wind_and_solar_day_ahead_get, - elexon_start_date, - elexon_end_date, - process_type - ) - else: - st.error("Elexon end date must be after the start date.") - - - - fig_forecast = go.Figure( + fig = go.Figure( layout=go.Layout( title=go.layout.Title(text="Latest Forecast"), xaxis=go.layout.XAxis(title=go.layout.xaxis.Title(text="Date")), @@ -244,24 +179,103 @@ def forecast_page(): ) ) - plot_pvlive(fig_forecast, gsp_id, pvlive_data, pvlive_gsp_sum_dayafter, pvlive_gsp_sum_inday) - plot_forecasts(fig_forecast, forecast_per_model, selected_prob_models, show_prob) + plot_pvlive(fig, gsp_id, pvlive_data, pvlive_gsp_sum_dayafter, pvlive_gsp_sum_inday) + plot_forecasts(fig, forecast_per_model, selected_prob_models, show_prob) - st.plotly_chart(fig_forecast, theme="streamlit") + if end_datetimes[0] is None or now <= max(end_datetimes): + fig.add_trace( + go.Scatter( + x=[now, now], + y=[0, capacity_mw], + mode="lines", + name="now", + line=dict(color="red", width=4, dash="dash"), + showlegend=False, + ) + ) - # Second figure: Elexon Forecast Chart - fig_elexon = go.Figure( - layout=go.Layout( - title=go.layout.Title(text="Elexon Forecast"), - xaxis=go.layout.XAxis(title=go.layout.xaxis.Title(text="Date")), - yaxis=go.layout.YAxis(title=go.layout.yaxis.Title(text="Quantity")), - legend=go.layout.Legend(title=go.layout.legend.Title(text="Chart Legend")), + st.plotly_chart(fig, theme="streamlit") + + # Elexon Solar Forecast + st.title("Elexon Solar Forecast") + + start_datetime_utc = st.date_input("Start Date", datetime.utcnow() - timedelta(days=3)) + end_datetime_utc = st.date_input("End Date", datetime.utcnow() + timedelta(days=3)) + + if start_datetime_utc < end_datetime_utc: + # Fetch data for each process type + process_types = ["Day Ahead", "Intraday Process", "Intraday Total"] + colors = ["red", "blue", "green"] # Colors for each process type + forecasts = [fetch_forecast_data(forecast_generation_wind_and_solar_day_ahead_get, start_datetime_utc, end_datetime_utc, pt) for pt in process_types] + + fig = go.Figure() + for i, (forecast, color) in enumerate(zip(forecasts, colors)): + if forecast.empty: + st.write(f"No data available for process type: {process_types[i]}") + continue + + # Remove NaNs and zero values + forecast = forecast[forecast["quantity"].notna() & (forecast["quantity"] > 0)] + + full_time_range = pd.date_range(start=start_datetime_utc, end=end_datetime_utc, freq='30T', tz=forecast["start_time"].dt.tz) + full_time_df = pd.DataFrame(full_time_range, columns=['start_time']) + + forecast = full_time_df.merge(forecast, on='start_time', how='left') + + fig.add_trace(go.Scatter( + x=forecast["start_time"], + y=forecast["quantity"], + mode='lines', + name=process_types[i], + line=dict(color=color), + connectgaps=False + )) + + fig.update_layout( + title="Elexon Solar Forecast", + xaxis_title="Date and Time", + yaxis_title="Forecast (MW)", + xaxis=dict( + tickformat='%Y-%m-%d %H:%M', + tickangle=45 + ), + legend_title="Process Type" ) - ) - plot_elexon_forecast(fig_elexon, elexon_forecasts) + st.plotly_chart(fig) - st.plotly_chart(fig_elexon, theme="streamlit") +# Function to fetch and process data +def fetch_forecast_data(api_func, start_date, end_date, process_type): + try: + response = api_func( + _from=start_date.isoformat(), + to=end_date.isoformat(), + process_type=process_type, + format="json", + ) + if not response.data: + return pd.DataFrame() + + df = pd.DataFrame([item.to_dict() for item in response.data]) + solar_df = df[df["business_type"] == "Solar generation"] + solar_df["start_time"] = pd.to_datetime(solar_df["start_time"]) + solar_df = solar_df.set_index("start_time") + + # Only resample if there's data + if not solar_df.empty: + solar_df = solar_df.resample("30T")["quantity"].sum().reset_index() + + return solar_df + except Exception as e: + st.error(f"Error fetching data for process type '{process_type}': {e}") + return pd.DataFrame() + +# Initialize Elexon API client +api_client = ApiClient() +forecast_api = GenerationForecastApi(api_client) +forecast_generation_wind_and_solar_day_ahead_get = ( + forecast_api.forecast_generation_wind_and_solar_day_ahead_get +) def plot_pvlive(fig, gsp_id, pvlive_data, pvlive_gsp_sum_dayafter, pvlive_gsp_sum_inday): # pvlive on the chart @@ -400,4 +414,4 @@ def get_pvlive_data(end_datetime, gsp_id, session, start_datetime): pvlive_data = {} pvlive_data["PVLive Initial Estimate"] = [GSPYield.from_orm(f) for f in pvlive_inday] pvlive_data["PVLive Updated Estimate"] = [GSPYield.from_orm(f) for f in pvlive_dayafter] - return pvlive_data, pvlive_gsp_sum_dayafter, pvlive_gsp_sum_inday + return pvlive_data, pvlive_gsp_sum_dayafter, pvlive_gsp_sum_inday \ No newline at end of file From 5c4a1c073259e69cfb478f44da9ef0dfff61c989 Mon Sep 17 00:00:00 2001 From: 14Richa Date: Fri, 9 Aug 2024 11:12:08 +0100 Subject: [PATCH 04/26] added start date and end date in side bar --- src/forecast.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/forecast.py b/src/forecast.py index e379d2b..eeef81a 100644 --- a/src/forecast.py +++ b/src/forecast.py @@ -34,6 +34,7 @@ def __call__(self, gsp_id): def forecast_page(): + """Main page for status""" st.markdown( f'

{"National and GSP Forecasts"}

', unsafe_allow_html=True, @@ -59,7 +60,6 @@ def forecast_page(): capacity_mw = locations[gsp_ids.index(gsp_id)].installed_capacity_mw - available_models = get_recent_available_model_names(session) selected_models = st.sidebar.multiselect("Select models", available_models, ["pvnet_v2"]) @@ -91,9 +91,7 @@ def forecast_page(): elif forecast_type == "Creation Time": - date_sel = st.sidebar.date_input("Forecast creation date:", yesterday) - dt_sel = datetime.combine(date_sel, time(0, 0)) initial_times = [dt_sel - timedelta(days=1) + timedelta(hours=3 * i) for i in range(8)] initial_times += [dt_sel + timedelta(minutes=30 * i) for i in range(48)] @@ -198,9 +196,9 @@ def forecast_page(): # Elexon Solar Forecast st.title("Elexon Solar Forecast") - - start_datetime_utc = st.date_input("Start Date", datetime.utcnow() - timedelta(days=3)) - end_datetime_utc = st.date_input("End Date", datetime.utcnow() + timedelta(days=3)) + st.sidebar.subheader("Select Elexon Forecast Dates") + start_datetime_utc = st.sidebar.date_input("Start Date", datetime.utcnow() - timedelta(days=3)) + end_datetime_utc = st.sidebar.date_input("End Date", datetime.utcnow() + timedelta(days=3)) if start_datetime_utc < end_datetime_utc: # Fetch data for each process type From a3fa81c58a2ebf3498baa22d7a50a62733df76aa Mon Sep 17 00:00:00 2001 From: 14Richa Date: Mon, 12 Aug 2024 15:48:34 +0100 Subject: [PATCH 05/26] fixed elexon graph --- src/forecast.py | 33 ++++++--------------------------- 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/src/forecast.py b/src/forecast.py index eeef81a..0e138d8 100644 --- a/src/forecast.py +++ b/src/forecast.py @@ -16,7 +16,6 @@ from plots.utils import ( get_colour_from_model_name, model_is_probabilistic, get_recent_available_model_names ) -from datetime import datetime, timedelta, time from elexonpy.api_client import ApiClient from elexonpy.api.generation_forecast_api import GenerationForecastApi @@ -32,7 +31,6 @@ def __call__(self, gsp_id): i = self.gsp_ids.index(gsp_id) return f"{gsp_id}: {self.gsp_names[i]}" - def forecast_page(): """Main page for status""" st.markdown( @@ -192,10 +190,7 @@ def forecast_page(): ) ) - st.plotly_chart(fig, theme="streamlit") - - # Elexon Solar Forecast - st.title("Elexon Solar Forecast") + # Fetch and plot Elexon Solar Forecast data st.sidebar.subheader("Select Elexon Forecast Dates") start_datetime_utc = st.sidebar.date_input("Start Date", datetime.utcnow() - timedelta(days=3)) end_datetime_utc = st.sidebar.date_input("End Date", datetime.utcnow() + timedelta(days=3)) @@ -203,11 +198,10 @@ def forecast_page(): if start_datetime_utc < end_datetime_utc: # Fetch data for each process type process_types = ["Day Ahead", "Intraday Process", "Intraday Total"] - colors = ["red", "blue", "green"] # Colors for each process type + line_styles = ["solid", "dash", "dot"] forecasts = [fetch_forecast_data(forecast_generation_wind_and_solar_day_ahead_get, start_datetime_utc, end_datetime_utc, pt) for pt in process_types] - fig = go.Figure() - for i, (forecast, color) in enumerate(zip(forecasts, colors)): + for i, (forecast, line_style) in enumerate(zip(forecasts, line_styles)): if forecast.empty: st.write(f"No data available for process type: {process_types[i]}") continue @@ -217,30 +211,18 @@ def forecast_page(): full_time_range = pd.date_range(start=start_datetime_utc, end=end_datetime_utc, freq='30T', tz=forecast["start_time"].dt.tz) full_time_df = pd.DataFrame(full_time_range, columns=['start_time']) - forecast = full_time_df.merge(forecast, on='start_time', how='left') fig.add_trace(go.Scatter( x=forecast["start_time"], y=forecast["quantity"], mode='lines', - name=process_types[i], - line=dict(color=color), + name=f"Elexon {process_types[i]}", + line=dict(color='#318CE7', dash=line_style), connectgaps=False )) - fig.update_layout( - title="Elexon Solar Forecast", - xaxis_title="Date and Time", - yaxis_title="Forecast (MW)", - xaxis=dict( - tickformat='%Y-%m-%d %H:%M', - tickangle=45 - ), - legend_title="Process Type" - ) - - st.plotly_chart(fig) + st.plotly_chart(fig, theme="streamlit") # Function to fetch and process data def fetch_forecast_data(api_func, start_date, end_date, process_type): @@ -309,7 +291,6 @@ def plot_pvlive(fig, gsp_id, pvlive_data, pvlive_gsp_sum_dayafter, pvlive_gsp_su go.Scatter(x=x, y=y, mode="lines", name=k, line=line, visible="legendonly") ) - def plot_forecasts(fig, forecast_per_model, selected_prob_models, show_prob): index_forecast_per_model = 0 @@ -373,13 +354,11 @@ def plot_forecasts(fig, forecast_per_model, selected_prob_models, show_prob): ) ) - except Exception as e: print(e) print("Could not add plevel to chart") raise e - def get_pvlive_data(end_datetime, gsp_id, session, start_datetime): pvlive_inday = get_gsp_yield( session=session, From fc987461a586dbc9670a95d925a06319d8f68856 Mon Sep 17 00:00:00 2001 From: 14Richa Date: Mon, 12 Aug 2024 16:48:07 +0100 Subject: [PATCH 06/26] added comments --- src/forecast.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/forecast.py b/src/forecast.py index 0e138d8..13e938c 100644 --- a/src/forecast.py +++ b/src/forecast.py @@ -42,6 +42,7 @@ def forecast_page(): connection = DatabaseConnection(url=os.environ["DB_URL"], echo=True) with connection.get_session() as session: + # Add dropdown to select GSP region locations = get_all_locations(session=session) locations = [Location.from_orm(loc) for loc in locations if loc.gsp_id < 318] gsp_ids = [loc.gsp_id for loc in locations] @@ -56,12 +57,14 @@ def forecast_page(): format_func=gsp_labeler ) - capacity_mw = locations[gsp_ids.index(gsp_id)].installed_capacity_mw + # Get effective capacity of selected GSP + capacity_mw = locations[gsp_ids.index(gsp_id)].installed_capacity_mw + # Find recent available models available_models = get_recent_available_model_names(session) - + # Add selection for models selected_models = st.sidebar.multiselect("Select models", available_models, ["pvnet_v2"]) - + # If any selected models are probabilistic add checkbox to show quantiles selected_prob_models = [model for model in selected_models if model_is_probabilistic(model)] if len(selected_prob_models) > 0: @@ -72,9 +75,9 @@ def forecast_page(): if gsp_id != 0 and ("National_xg" in selected_models): selected_models.remove("National_xg") st.sidebar.warning("National_xg only available for National forecast.") - + # Add selection for adjuster use_adjuster = st.sidebar.radio("Use adjuster", [True, False], index=1) - + # Add selection for forecast type forecast_type = st.sidebar.radio( "Forecast Type", ["Now", "Creation Time", "Forecast Horizon"], index=0 ) @@ -88,8 +91,9 @@ def forecast_page(): end_datetimes = [None] elif forecast_type == "Creation Time": - + # Add calendar to select start date - defaults to yesterday date_sel = st.sidebar.date_input("Forecast creation date:", yesterday) + # Add dropdown selection of init-times dt_sel = datetime.combine(date_sel, time(0, 0)) initial_times = [dt_sel - timedelta(days=1) + timedelta(hours=3 * i) for i in range(8)] initial_times += [dt_sel + timedelta(minutes=30 * i) for i in range(48)] @@ -106,6 +110,7 @@ def forecast_page(): end_datetimes = [t + timedelta(days=2) for t in select_init_times] elif forecast_type == "Forecast Horizon": + # Add calendar and time selections for datetime date_sel = st.sidebar.date_input("Forecast start date:", yesterday) time_sel = st.sidebar.time_input("Forecast start time", time(0, 0)) @@ -113,12 +118,14 @@ def forecast_page(): start_datetimes = [dt_sel] end_datetimes = [dt_sel + timedelta(days=2)] + # Add selection for horizon + # 0-8 hours in 30 mintue chunks, 8-36 hours in 3 hour chunks forecast_horizon = st.sidebar.selectbox( "Forecast Horizon (mins)", list(range(0, 480, 30)) + list(range(480, 36 * 60, 180)), 0, ) - + # Get the data to plot forecast_per_model = {} for model in selected_models: for start_dt, end_dt in zip(start_datetimes, end_datetimes): From 6ce89d448559207a0d5f81309064ed453d35c123 Mon Sep 17 00:00:00 2001 From: 14Richa Date: Mon, 12 Aug 2024 16:51:22 +0100 Subject: [PATCH 07/26] added comments --- src/forecast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/forecast.py b/src/forecast.py index 13e938c..217fd60 100644 --- a/src/forecast.py +++ b/src/forecast.py @@ -213,7 +213,7 @@ def forecast_page(): st.write(f"No data available for process type: {process_types[i]}") continue - # Remove NaNs and zero values + # Remove NaNs and zero values to ensure clean data for plotting forecast = forecast[forecast["quantity"].notna() & (forecast["quantity"] > 0)] full_time_range = pd.date_range(start=start_datetime_utc, end=end_datetime_utc, freq='30T', tz=forecast["start_time"].dt.tz) From f0a367826ed68bcbd3c508e6a96239628a41233b Mon Sep 17 00:00:00 2001 From: 14Richa Date: Mon, 12 Aug 2024 16:53:20 +0100 Subject: [PATCH 08/26] added comments --- src/forecast.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/forecast.py b/src/forecast.py index 217fd60..074b1ba 100644 --- a/src/forecast.py +++ b/src/forecast.py @@ -161,6 +161,7 @@ def forecast_page(): ) label = model + # Make ForecastValue objects with _properties attribute and maybe adjust forecast_per_model[label] = [] for f in forecast_values: forecast_value = ForecastValue.from_orm(f) @@ -168,11 +169,11 @@ def forecast_page(): if use_adjuster: forecast_value = forecast_value.adjust(limit=1000) forecast_per_model[label].append(forecast_value) - + # Get pvlive values pvlive_data, pvlive_gsp_sum_dayafter, pvlive_gsp_sum_inday = get_pvlive_data( end_datetimes[0], gsp_id, session, start_datetimes[0] ) - + # Make figure fig = go.Figure( layout=go.Layout( title=go.layout.Title(text="Latest Forecast"), @@ -181,11 +182,12 @@ def forecast_page(): legend=go.layout.Legend(title=go.layout.legend.Title(text="Chart Legend")), ) ) - + # Plot PVLive values and the forecasts plot_pvlive(fig, gsp_id, pvlive_data, pvlive_gsp_sum_dayafter, pvlive_gsp_sum_inday) plot_forecasts(fig, forecast_per_model, selected_prob_models, show_prob) if end_datetimes[0] is None or now <= max(end_datetimes): + # Add vertical line to indicate now fig.add_trace( go.Scatter( x=[now, now], From 0c1376b59e9d5f32812856decc3c827012b4e8d7 Mon Sep 17 00:00:00 2001 From: 14Richa Date: Mon, 12 Aug 2024 17:24:48 +0100 Subject: [PATCH 09/26] fixed the date issue --- src/forecast.py | 87 ++++++++++++++++++++++++++++++------------------- 1 file changed, 54 insertions(+), 33 deletions(-) diff --git a/src/forecast.py b/src/forecast.py index 074b1ba..e3f603c 100644 --- a/src/forecast.py +++ b/src/forecast.py @@ -199,39 +199,60 @@ def forecast_page(): ) ) - # Fetch and plot Elexon Solar Forecast data - st.sidebar.subheader("Select Elexon Forecast Dates") - start_datetime_utc = st.sidebar.date_input("Start Date", datetime.utcnow() - timedelta(days=3)) - end_datetime_utc = st.sidebar.date_input("End Date", datetime.utcnow() + timedelta(days=3)) - - if start_datetime_utc < end_datetime_utc: - # Fetch data for each process type - process_types = ["Day Ahead", "Intraday Process", "Intraday Total"] - line_styles = ["solid", "dash", "dot"] - forecasts = [fetch_forecast_data(forecast_generation_wind_and_solar_day_ahead_get, start_datetime_utc, end_datetime_utc, pt) for pt in process_types] - - for i, (forecast, line_style) in enumerate(zip(forecasts, line_styles)): - if forecast.empty: - st.write(f"No data available for process type: {process_types[i]}") - continue - - # Remove NaNs and zero values to ensure clean data for plotting - forecast = forecast[forecast["quantity"].notna() & (forecast["quantity"] > 0)] - - full_time_range = pd.date_range(start=start_datetime_utc, end=end_datetime_utc, freq='30T', tz=forecast["start_time"].dt.tz) - full_time_df = pd.DataFrame(full_time_range, columns=['start_time']) - forecast = full_time_df.merge(forecast, on='start_time', how='left') - - fig.add_trace(go.Scatter( - x=forecast["start_time"], - y=forecast["quantity"], - mode='lines', - name=f"Elexon {process_types[i]}", - line=dict(color='#318CE7', dash=line_style), - connectgaps=False - )) - - st.plotly_chart(fig, theme="streamlit") + # start_datetimes is not empty + if not start_datetimes: + st.error("No start dates available. Please check your date inputs.") + else: + start_datetime_utc = start_datetimes[0] + # Check if end_datetimes is empty or the last element is None + if not end_datetimes or end_datetimes[-1] is None: + # Ensure start_datetime_utc is a datetime object + if isinstance(start_datetime_utc, datetime): + # Set end_datetime to 7 days after start_datetime or to current date + end_datetime_utc = max(start_datetime_utc + timedelta(days=7), datetime.utcnow()) + else: + # If start_datetime_utc is a date object, convert it to datetime + start_datetime_utc = datetime.combine(start_datetime_utc, datetime.min.time()) + end_datetime_utc = max(start_datetime_utc + timedelta(days=7), datetime.utcnow()) + + else: + end_datetime_utc = end_datetimes[-1] + if isinstance(end_datetime_utc, datetime.date): + end_datetime_utc = datetime.combine(end_datetime_utc, datetime.min.time()) + + # Check if both dates are valid + if start_datetime_utc is not None and end_datetime_utc is not None: + if start_datetime_utc < end_datetime_utc: + process_types = ["Day Ahead", "Intraday Process", "Intraday Total"] + line_styles = ["solid", "dash", "dot"] + forecasts = [fetch_forecast_data(forecast_generation_wind_and_solar_day_ahead_get, start_datetime_utc, end_datetime_utc, pt) for pt in process_types] + + for i, (forecast, line_style) in enumerate(zip(forecasts, line_styles)): + if forecast.empty: + st.write(f"No data available for process type: {process_types[i]}") + continue + + # Remove NaNs and zero values to ensure clean data for plotting + forecast = forecast[forecast["quantity"].notna() & (forecast["quantity"] > 0)] + + full_time_range = pd.date_range(start=start_datetime_utc, end=end_datetime_utc, freq='30T', tz=forecast["start_time"].dt.tz) + full_time_df = pd.DataFrame(full_time_range, columns=['start_time']) + forecast = full_time_df.merge(forecast, on='start_time', how='left') + + fig.add_trace(go.Scatter( + x=forecast["start_time"], + y=forecast["quantity"], + mode='lines', + name=f"Elexon {process_types[i]}", + line=dict(color='#318CE7', dash=line_style), + connectgaps=False + )) + + st.plotly_chart(fig, theme="streamlit") + else: + st.error("Start date must be before end date.") + else: + st.error("Invalid date selection. Please check your inputs.") # Function to fetch and process data def fetch_forecast_data(api_func, start_date, end_date, process_type): From eeb03252dcad9f6385e9145d974a282a7ad631ce Mon Sep 17 00:00:00 2001 From: 14Richa Date: Mon, 12 Aug 2024 17:29:15 +0100 Subject: [PATCH 10/26] added code of Initialize Elexon API client in the elexon block --- src/forecast.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/forecast.py b/src/forecast.py index e3f603c..fe2988e 100644 --- a/src/forecast.py +++ b/src/forecast.py @@ -199,7 +199,7 @@ def forecast_page(): ) ) - # start_datetimes is not empty + # Ensure start_datetimes is not empty if not start_datetimes: st.error("No start dates available. Please check your date inputs.") else: @@ -214,15 +214,22 @@ def forecast_page(): # If start_datetime_utc is a date object, convert it to datetime start_datetime_utc = datetime.combine(start_datetime_utc, datetime.min.time()) end_datetime_utc = max(start_datetime_utc + timedelta(days=7), datetime.utcnow()) - else: end_datetime_utc = end_datetimes[-1] + # Ensure end_datetime_utc is a datetime object if isinstance(end_datetime_utc, datetime.date): end_datetime_utc = datetime.combine(end_datetime_utc, datetime.min.time()) # Check if both dates are valid if start_datetime_utc is not None and end_datetime_utc is not None: if start_datetime_utc < end_datetime_utc: + + # Initialize Elexon API client + api_client = ApiClient() + forecast_api = GenerationForecastApi(api_client) + forecast_generation_wind_and_solar_day_ahead_get = forecast_api.forecast_generation_wind_and_solar_day_ahead_get + + # Fetch data for each process type process_types = ["Day Ahead", "Intraday Process", "Intraday Total"] line_styles = ["solid", "dash", "dot"] forecasts = [fetch_forecast_data(forecast_generation_wind_and_solar_day_ahead_get, start_datetime_utc, end_datetime_utc, pt) for pt in process_types] @@ -280,12 +287,12 @@ def fetch_forecast_data(api_func, start_date, end_date, process_type): st.error(f"Error fetching data for process type '{process_type}': {e}") return pd.DataFrame() -# Initialize Elexon API client -api_client = ApiClient() -forecast_api = GenerationForecastApi(api_client) -forecast_generation_wind_and_solar_day_ahead_get = ( - forecast_api.forecast_generation_wind_and_solar_day_ahead_get -) +# # Initialize Elexon API client +# api_client = ApiClient() +# forecast_api = GenerationForecastApi(api_client) +# forecast_generation_wind_and_solar_day_ahead_get = ( +# forecast_api.forecast_generation_wind_and_solar_day_ahead_get +# ) def plot_pvlive(fig, gsp_id, pvlive_data, pvlive_gsp_sum_dayafter, pvlive_gsp_sum_inday): # pvlive on the chart From 873b26357b99f694776ad14ee52db80b69672e1e Mon Sep 17 00:00:00 2001 From: 14Richa Date: Mon, 12 Aug 2024 17:37:58 +0100 Subject: [PATCH 11/26] remove comment --- src/forecast.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/forecast.py b/src/forecast.py index fe2988e..ebcd417 100644 --- a/src/forecast.py +++ b/src/forecast.py @@ -287,13 +287,6 @@ def fetch_forecast_data(api_func, start_date, end_date, process_type): st.error(f"Error fetching data for process type '{process_type}': {e}") return pd.DataFrame() -# # Initialize Elexon API client -# api_client = ApiClient() -# forecast_api = GenerationForecastApi(api_client) -# forecast_generation_wind_and_solar_day_ahead_get = ( -# forecast_api.forecast_generation_wind_and_solar_day_ahead_get -# ) - def plot_pvlive(fig, gsp_id, pvlive_data, pvlive_gsp_sum_dayafter, pvlive_gsp_sum_inday): # pvlive on the chart for k, v in pvlive_data.items(): From aa3375fec974509d901f5834d8215daa46a1b189 Mon Sep 17 00:00:00 2001 From: 14Richa Date: Mon, 12 Aug 2024 18:09:57 +0100 Subject: [PATCH 12/26] refactored the code , added a new function --- src/forecast.py | 136 +++++++++++++++++++++++++++--------------------- 1 file changed, 76 insertions(+), 60 deletions(-) diff --git a/src/forecast.py b/src/forecast.py index ebcd417..3a0983f 100644 --- a/src/forecast.py +++ b/src/forecast.py @@ -199,67 +199,46 @@ def forecast_page(): ) ) - # Ensure start_datetimes is not empty - if not start_datetimes: - st.error("No start dates available. Please check your date inputs.") - else: - start_datetime_utc = start_datetimes[0] - # Check if end_datetimes is empty or the last element is None - if not end_datetimes or end_datetimes[-1] is None: - # Ensure start_datetime_utc is a datetime object - if isinstance(start_datetime_utc, datetime): - # Set end_datetime to 7 days after start_datetime or to current date - end_datetime_utc = max(start_datetime_utc + timedelta(days=7), datetime.utcnow()) - else: - # If start_datetime_utc is a date object, convert it to datetime - start_datetime_utc = datetime.combine(start_datetime_utc, datetime.min.time()) - end_datetime_utc = max(start_datetime_utc + timedelta(days=7), datetime.utcnow()) - else: - end_datetime_utc = end_datetimes[-1] - # Ensure end_datetime_utc is a datetime object - if isinstance(end_datetime_utc, datetime.date): - end_datetime_utc = datetime.combine(end_datetime_utc, datetime.min.time()) - - # Check if both dates are valid - if start_datetime_utc is not None and end_datetime_utc is not None: - if start_datetime_utc < end_datetime_utc: - - # Initialize Elexon API client - api_client = ApiClient() - forecast_api = GenerationForecastApi(api_client) - forecast_generation_wind_and_solar_day_ahead_get = forecast_api.forecast_generation_wind_and_solar_day_ahead_get - - # Fetch data for each process type - process_types = ["Day Ahead", "Intraday Process", "Intraday Total"] - line_styles = ["solid", "dash", "dot"] - forecasts = [fetch_forecast_data(forecast_generation_wind_and_solar_day_ahead_get, start_datetime_utc, end_datetime_utc, pt) for pt in process_types] - - for i, (forecast, line_style) in enumerate(zip(forecasts, line_styles)): - if forecast.empty: - st.write(f"No data available for process type: {process_types[i]}") - continue - - # Remove NaNs and zero values to ensure clean data for plotting - forecast = forecast[forecast["quantity"].notna() & (forecast["quantity"] > 0)] - - full_time_range = pd.date_range(start=start_datetime_utc, end=end_datetime_utc, freq='30T', tz=forecast["start_time"].dt.tz) - full_time_df = pd.DataFrame(full_time_range, columns=['start_time']) - forecast = full_time_df.merge(forecast, on='start_time', how='left') - - fig.add_trace(go.Scatter( - x=forecast["start_time"], - y=forecast["quantity"], - mode='lines', - name=f"Elexon {process_types[i]}", - line=dict(color='#318CE7', dash=line_style), - connectgaps=False - )) - - st.plotly_chart(fig, theme="streamlit") - else: - st.error("Start date must be before end date.") + start_datetime_utc, end_datetime_utc = determine_start_and_end_datetimes(start_datetimes, end_datetimes) + + if start_datetime_utc and end_datetime_utc: + if start_datetime_utc < end_datetime_utc: + # Initialize Elexon API client + api_client = ApiClient() + forecast_api = GenerationForecastApi(api_client) + forecast_generation_wind_and_solar_day_ahead_get = forecast_api.forecast_generation_wind_and_solar_day_ahead_get + + # Fetch data for each process type + process_types = ["Day Ahead", "Intraday Process", "Intraday Total"] + line_styles = ["solid", "dash", "dot"] + forecasts = [fetch_forecast_data(forecast_generation_wind_and_solar_day_ahead_get, start_datetime_utc, end_datetime_utc, pt) for pt in process_types] + + for i, (forecast, line_style) in enumerate(zip(forecasts, line_styles)): + if forecast.empty: + st.write(f"No data available for process type: {process_types[i]}") + continue + + # Remove NaNs and zero values to ensure clean data for plotting + forecast = forecast[forecast["quantity"].notna() & (forecast["quantity"] > 0)] + + full_time_range = pd.date_range(start=start_datetime_utc, end=end_datetime_utc, freq='30T', tz=forecast["start_time"].dt.tz) + full_time_df = pd.DataFrame(full_time_range, columns=['start_time']) + forecast = full_time_df.merge(forecast, on='start_time', how='left') + + fig.add_trace(go.Scatter( + x=forecast["start_time"], + y=forecast["quantity"], + mode='lines', + name=f"Elexon {process_types[i]}", + line=dict(color='#318CE7', dash=line_style), + connectgaps=False + )) + + st.plotly_chart(fig, theme="streamlit") else: - st.error("Invalid date selection. Please check your inputs.") + st.error("Start date must be before end date.") + else: + st.error("Invalid date selection. Please check your inputs.") # Function to fetch and process data def fetch_forecast_data(api_func, start_date, end_date, process_type): @@ -287,6 +266,43 @@ def fetch_forecast_data(api_func, start_date, end_date, process_type): st.error(f"Error fetching data for process type '{process_type}': {e}") return pd.DataFrame() +def determine_start_and_end_datetimes(start_datetimes, end_datetimes): + """ + Determines the start and end datetime in UTC. + + Parameters: + - start_datetimes: list of datetime or date objects + - end_datetimes: list of datetime or date objects + + Returns: + - start_datetime_utc: datetime object in UTC + - end_datetime_utc: datetime object in UTC + """ + if not start_datetimes: + st.error("No start dates available. Please check your date inputs.") + return None, None + + start_datetime_utc = start_datetimes[0] + + # Check if end_datetimes is empty or the last element is None + if not end_datetimes or end_datetimes[-1] is None: + # start_datetime_utc is a datetime object + if isinstance(start_datetime_utc, datetime): + # Set end_datetime to 7 days after start_datetime or to current date + end_datetime_utc = max(start_datetime_utc + timedelta(days=7), datetime.utcnow()) + else: + # If start_datetime_utc is a date object, convert it to datetime + start_datetime_utc = datetime.combine(start_datetime_utc, datetime.min.time()) + end_datetime_utc = max(start_datetime_utc + timedelta(days=7), datetime.utcnow()) + else: + end_datetime_utc = end_datetimes[-1] + # end_datetime_utc is a datetime object + if isinstance(end_datetime_utc, datetime.date): + end_datetime_utc = datetime.combine(end_datetime_utc, datetime.min.time()) + + return start_datetime_utc, end_datetime_utc + + def plot_pvlive(fig, gsp_id, pvlive_data, pvlive_gsp_sum_dayafter, pvlive_gsp_sum_inday): # pvlive on the chart for k, v in pvlive_data.items(): From 1bb587b84ffcda0eb7368176fee96f4b7a5a8799 Mon Sep 17 00:00:00 2001 From: 14Richa Date: Tue, 13 Aug 2024 20:43:25 +0100 Subject: [PATCH 13/26] made some changes in datermine start time function --- src/forecast.py | 119 +++++++++++++++++++++++++----------------------- 1 file changed, 61 insertions(+), 58 deletions(-) diff --git a/src/forecast.py b/src/forecast.py index 3a0983f..d8fd39b 100644 --- a/src/forecast.py +++ b/src/forecast.py @@ -1,5 +1,5 @@ import os -from datetime import datetime, timedelta, time, timezone +from datetime import date, datetime, timedelta, time, timezone import numpy as np import pandas as pd import plotly.graph_objects as go @@ -202,43 +202,45 @@ def forecast_page(): start_datetime_utc, end_datetime_utc = determine_start_and_end_datetimes(start_datetimes, end_datetimes) if start_datetime_utc and end_datetime_utc: - if start_datetime_utc < end_datetime_utc: - # Initialize Elexon API client - api_client = ApiClient() - forecast_api = GenerationForecastApi(api_client) - forecast_generation_wind_and_solar_day_ahead_get = forecast_api.forecast_generation_wind_and_solar_day_ahead_get - - # Fetch data for each process type - process_types = ["Day Ahead", "Intraday Process", "Intraday Total"] - line_styles = ["solid", "dash", "dot"] - forecasts = [fetch_forecast_data(forecast_generation_wind_and_solar_day_ahead_get, start_datetime_utc, end_datetime_utc, pt) for pt in process_types] - - for i, (forecast, line_style) in enumerate(zip(forecasts, line_styles)): - if forecast.empty: - st.write(f"No data available for process type: {process_types[i]}") - continue - - # Remove NaNs and zero values to ensure clean data for plotting - forecast = forecast[forecast["quantity"].notna() & (forecast["quantity"] > 0)] - - full_time_range = pd.date_range(start=start_datetime_utc, end=end_datetime_utc, freq='30T', tz=forecast["start_time"].dt.tz) - full_time_df = pd.DataFrame(full_time_range, columns=['start_time']) - forecast = full_time_df.merge(forecast, on='start_time', how='left') - - fig.add_trace(go.Scatter( - x=forecast["start_time"], - y=forecast["quantity"], - mode='lines', - name=f"Elexon {process_types[i]}", - line=dict(color='#318CE7', dash=line_style), - connectgaps=False - )) - - st.plotly_chart(fig, theme="streamlit") - else: - st.error("Start date must be before end date.") + # Initialize Elexon API client + api_client = ApiClient() + forecast_api = GenerationForecastApi(api_client) + forecast_generation_wind_and_solar_day_ahead_get = forecast_api.forecast_generation_wind_and_solar_day_ahead_get + # Fetch data for each process type + process_types = ["Day Ahead", "Intraday Process", "Intraday Total"] + line_styles = ["solid", "dash", "dot"] + forecasts = [ + fetch_forecast_data( + forecast_generation_wind_and_solar_day_ahead_get, + start_datetime_utc, + end_datetime_utc, + pt + ) for pt in process_types + ] + + for i, (forecast, line_style) in enumerate(zip(forecasts, line_styles)): + if forecast.empty: + st.write(f"No data available for process type: {process_types[i]}") + continue + # Remove NaNs and zero values to ensure clean data for plotting + forecast = forecast[forecast["quantity"].notna() & (forecast["quantity"] > 0)] + + full_time_range = pd.date_range(start=start_datetime_utc, end=end_datetime_utc, freq='30T', tz=forecast["start_time"].dt.tz) + full_time_df = pd.DataFrame(full_time_range, columns=['start_time']) + forecast = full_time_df.merge(forecast, on='start_time', how='left') + + fig.add_trace(go.Scatter( + x=forecast["start_time"], + y=forecast["quantity"], + mode='lines', + name=f"Elexon {process_types[i]}", + line=dict(color='#318CE7', dash=line_style), + connectgaps=False + )) + + st.plotly_chart(fig, theme="streamlit") else: - st.error("Invalid date selection. Please check your inputs.") + st.error("Invalid date range. Start date must be before end date.") # Function to fetch and process data def fetch_forecast_data(api_func, start_date, end_date, process_type): @@ -269,39 +271,40 @@ def fetch_forecast_data(api_func, start_date, end_date, process_type): def determine_start_and_end_datetimes(start_datetimes, end_datetimes): """ Determines the start and end datetime in UTC. - Parameters: - start_datetimes: list of datetime or date objects - end_datetimes: list of datetime or date objects - Returns: - start_datetime_utc: datetime object in UTC - end_datetime_utc: datetime object in UTC """ - if not start_datetimes: - st.error("No start dates available. Please check your date inputs.") - return None, None + now = datetime.utcnow() - start_datetime_utc = start_datetimes[0] - - # Check if end_datetimes is empty or the last element is None - if not end_datetimes or end_datetimes[-1] is None: - # start_datetime_utc is a datetime object - if isinstance(start_datetime_utc, datetime): - # Set end_datetime to 7 days after start_datetime or to current date - end_datetime_utc = max(start_datetime_utc + timedelta(days=7), datetime.utcnow()) - else: - # If start_datetime_utc is a date object, convert it to datetime - start_datetime_utc = datetime.combine(start_datetime_utc, datetime.min.time()) - end_datetime_utc = max(start_datetime_utc + timedelta(days=7), datetime.utcnow()) + # Determine start_datetime_utc + if start_datetimes: + start_datetime_utc = start_datetimes[0] else: + start_datetime_utc = now + + # Ensure start_datetime_utc is a datetime object + if isinstance(start_datetime_utc, date) and not isinstance(start_datetime_utc, datetime): + start_datetime_utc = datetime.combine(start_datetime_utc, datetime.min.time()) + + # Determine end_datetime_utc + if end_datetimes and end_datetimes[-1]: end_datetime_utc = end_datetimes[-1] - # end_datetime_utc is a datetime object - if isinstance(end_datetime_utc, datetime.date): - end_datetime_utc = datetime.combine(end_datetime_utc, datetime.min.time()) + else: + end_datetime_utc = start_datetime_utc + timedelta(days=7) - return start_datetime_utc, end_datetime_utc + # Ensure end_datetime_utc is a datetime object + if isinstance(end_datetime_utc, date) and not isinstance(end_datetime_utc, datetime): + end_datetime_utc = datetime.combine(end_datetime_utc, datetime.min.time()) + # Check if start is before end + if start_datetime_utc >= end_datetime_utc: + return None, None + + return start_datetime_utc, end_datetime_utc def plot_pvlive(fig, gsp_id, pvlive_data, pvlive_gsp_sum_dayafter, pvlive_gsp_sum_inday): # pvlive on the chart From d444b58bfd6cbfbc18fb471ee4746d1fe1e927b0 Mon Sep 17 00:00:00 2001 From: 14Richa Date: Tue, 13 Aug 2024 21:00:05 +0100 Subject: [PATCH 14/26] fised date --- src/forecast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/forecast.py b/src/forecast.py index d8fd39b..b762bfd 100644 --- a/src/forecast.py +++ b/src/forecast.py @@ -284,7 +284,7 @@ def determine_start_and_end_datetimes(start_datetimes, end_datetimes): if start_datetimes: start_datetime_utc = start_datetimes[0] else: - start_datetime_utc = now + start_datetime_utc = now - timedelta(days=2) # Ensure start_datetime_utc is a datetime object if isinstance(start_datetime_utc, date) and not isinstance(start_datetime_utc, datetime): From 6b9661f607fa97fc24f0699a08fb3255143caf11 Mon Sep 17 00:00:00 2001 From: 14Richa Date: Tue, 13 Aug 2024 21:06:45 +0100 Subject: [PATCH 15/26] changed if statements to asserts --- src/forecast.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/forecast.py b/src/forecast.py index b762bfd..b9e11a7 100644 --- a/src/forecast.py +++ b/src/forecast.py @@ -300,9 +300,7 @@ def determine_start_and_end_datetimes(start_datetimes, end_datetimes): if isinstance(end_datetime_utc, date) and not isinstance(end_datetime_utc, datetime): end_datetime_utc = datetime.combine(end_datetime_utc, datetime.min.time()) - # Check if start is before end - if start_datetime_utc >= end_datetime_utc: - return None, None + assert start_datetime_utc < end_datetime_utc, "Start datetime must be before end datetime." return start_datetime_utc, end_datetime_utc From a54a7d67daef074c65e9242afe92e79c6f937455 Mon Sep 17 00:00:00 2001 From: 14Richa Date: Tue, 13 Aug 2024 21:34:28 +0100 Subject: [PATCH 16/26] added new file for clearity --- src/forecast.py | 63 ++----------------------------------- src/plots/elexon_plots.py | 65 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 61 deletions(-) create mode 100644 src/plots/elexon_plots.py diff --git a/src/forecast.py b/src/forecast.py index b9e11a7..8df1300 100644 --- a/src/forecast.py +++ b/src/forecast.py @@ -19,6 +19,8 @@ from elexonpy.api_client import ApiClient from elexonpy.api.generation_forecast_api import GenerationForecastApi +from plots.elexon_plots import fetch_forecast_data, determine_start_and_end_datetimes + class GSPLabeler: """A function class to add the GSP name to the GSP IDs""" def __init__(self, gsp_ids, gsp_names): @@ -242,67 +244,6 @@ def forecast_page(): else: st.error("Invalid date range. Start date must be before end date.") -# Function to fetch and process data -def fetch_forecast_data(api_func, start_date, end_date, process_type): - try: - response = api_func( - _from=start_date.isoformat(), - to=end_date.isoformat(), - process_type=process_type, - format="json", - ) - if not response.data: - return pd.DataFrame() - - df = pd.DataFrame([item.to_dict() for item in response.data]) - solar_df = df[df["business_type"] == "Solar generation"] - solar_df["start_time"] = pd.to_datetime(solar_df["start_time"]) - solar_df = solar_df.set_index("start_time") - - # Only resample if there's data - if not solar_df.empty: - solar_df = solar_df.resample("30T")["quantity"].sum().reset_index() - - return solar_df - except Exception as e: - st.error(f"Error fetching data for process type '{process_type}': {e}") - return pd.DataFrame() - -def determine_start_and_end_datetimes(start_datetimes, end_datetimes): - """ - Determines the start and end datetime in UTC. - Parameters: - - start_datetimes: list of datetime or date objects - - end_datetimes: list of datetime or date objects - Returns: - - start_datetime_utc: datetime object in UTC - - end_datetime_utc: datetime object in UTC - """ - now = datetime.utcnow() - - # Determine start_datetime_utc - if start_datetimes: - start_datetime_utc = start_datetimes[0] - else: - start_datetime_utc = now - timedelta(days=2) - - # Ensure start_datetime_utc is a datetime object - if isinstance(start_datetime_utc, date) and not isinstance(start_datetime_utc, datetime): - start_datetime_utc = datetime.combine(start_datetime_utc, datetime.min.time()) - - # Determine end_datetime_utc - if end_datetimes and end_datetimes[-1]: - end_datetime_utc = end_datetimes[-1] - else: - end_datetime_utc = start_datetime_utc + timedelta(days=7) - - # Ensure end_datetime_utc is a datetime object - if isinstance(end_datetime_utc, date) and not isinstance(end_datetime_utc, datetime): - end_datetime_utc = datetime.combine(end_datetime_utc, datetime.min.time()) - - assert start_datetime_utc < end_datetime_utc, "Start datetime must be before end datetime." - - return start_datetime_utc, end_datetime_utc def plot_pvlive(fig, gsp_id, pvlive_data, pvlive_gsp_sum_dayafter, pvlive_gsp_sum_inday): # pvlive on the chart diff --git a/src/plots/elexon_plots.py b/src/plots/elexon_plots.py new file mode 100644 index 0000000..c660734 --- /dev/null +++ b/src/plots/elexon_plots.py @@ -0,0 +1,65 @@ +import pandas as pd +from datetime import datetime, date, timedelta +from plotly import graph_objects as go +import streamlit as st + +def fetch_forecast_data(api_func, start_date, end_date, process_type): + try: + response = api_func( + _from=start_date.isoformat(), + to=end_date.isoformat(), + process_type=process_type, + format="json", + ) + if not response.data: + return pd.DataFrame() + + df = pd.DataFrame([item.to_dict() for item in response.data]) + solar_df = df[df["business_type"] == "Solar generation"] + solar_df["start_time"] = pd.to_datetime(solar_df["start_time"]) + solar_df = solar_df.set_index("start_time") + + # Resample if there's data + if not solar_df.empty: + solar_df = solar_df.resample("30T")["quantity"].sum().reset_index() + + return solar_df + except Exception as e: + st.error(f"Error fetching data for process type '{process_type}': {e}") + return pd.DataFrame() + +def determine_start_and_end_datetimes(start_datetimes, end_datetimes): + """ + Determines the start and end datetime in UTC. + Parameters: + - start_datetimes: list of datetime or date objects + - end_datetimes: list of datetime or date objects + Returns: + - start_datetime_utc: datetime object in UTC + - end_datetime_utc: datetime object in UTC + """ + now = datetime.utcnow() + + # Determine start_datetime_utc + if start_datetimes: + start_datetime_utc = start_datetimes[0] + else: + start_datetime_utc = now - timedelta(days=2) + # Ensure start_datetime_utc is a datetime object + if isinstance(start_datetime_utc, date) and not isinstance(start_datetime_utc, datetime): + start_datetime_utc = datetime.combine(start_datetime_utc, datetime.min.time()) + + # Determine end_datetime_utc + if end_datetimes and end_datetimes[-1]: + end_datetime_utc = end_datetimes[-1] + else: + end_datetime_utc = start_datetime_utc + timedelta(days=7) + + # Ensure end_datetime_utc is a datetime object + if isinstance(end_datetime_utc, date) and not isinstance(end_datetime_utc, datetime): + end_datetime_utc = datetime.combine(end_datetime_utc, datetime.min.time()) + + # Assert that start is before end + assert start_datetime_utc < end_datetime_utc, "Start datetime must be before end datetime." + + return start_datetime_utc, end_datetime_utc \ No newline at end of file From fb9ddc66d0956e0a67557149ec0cc3c3303fe6a4 Mon Sep 17 00:00:00 2001 From: 14Richa Date: Wed, 14 Aug 2024 10:50:50 +0100 Subject: [PATCH 17/26] added description of function and type hint --- src/plots/elexon_plots.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/plots/elexon_plots.py b/src/plots/elexon_plots.py index c660734..f708ee9 100644 --- a/src/plots/elexon_plots.py +++ b/src/plots/elexon_plots.py @@ -1,9 +1,22 @@ +from typing import Callable import pandas as pd from datetime import datetime, date, timedelta from plotly import graph_objects as go import streamlit as st -def fetch_forecast_data(api_func, start_date, end_date, process_type): +def fetch_forecast_data(api_func: Callable, start_date: datetime, end_date: datetime, process_type: str) -> pd.DataFrame: + """ + Fetches forecast data from an API and processes it. + + Parameters: + api_func (Callable): The API function to call for fetching data. + start_date (datetime): The start date for the data fetch. + end_date (datetime): The end date for the data fetch. + process_type (str): The type of process for which data is being fetched. + + Returns: + pd.DataFrame: A DataFrame containing the processed solar generation data. + """ try: response = api_func( _from=start_date.isoformat(), From 029d50a2ca9c7b0d64ca968fda1f201ddfcb334c Mon Sep 17 00:00:00 2001 From: 14Richa Date: Wed, 14 Aug 2024 10:52:38 +0100 Subject: [PATCH 18/26] added type hint --- src/plots/elexon_plots.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/plots/elexon_plots.py b/src/plots/elexon_plots.py index f708ee9..b9eae67 100644 --- a/src/plots/elexon_plots.py +++ b/src/plots/elexon_plots.py @@ -1,4 +1,4 @@ -from typing import Callable +from typing import Callable, List, Tuple, Union import pandas as pd from datetime import datetime, date, timedelta from plotly import graph_objects as go @@ -41,7 +41,10 @@ def fetch_forecast_data(api_func: Callable, start_date: datetime, end_date: date st.error(f"Error fetching data for process type '{process_type}': {e}") return pd.DataFrame() -def determine_start_and_end_datetimes(start_datetimes, end_datetimes): +def determine_start_and_end_datetimes( + start_datetimes: List[Union[datetime, date]], + end_datetimes: List[Union[datetime, date]] +) -> Tuple[datetime, datetime]: """ Determines the start and end datetime in UTC. Parameters: From fc705c0456f97bcfd409b4bbc804aa8297f3c9f9 Mon Sep 17 00:00:00 2001 From: 14Richa Date: Wed, 14 Aug 2024 11:05:58 +0100 Subject: [PATCH 19/26] added elexon plot function --- src/forecast.py | 47 +++----------------------------- src/plots/elexon_plots.py | 57 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 44 deletions(-) diff --git a/src/forecast.py b/src/forecast.py index 8df1300..33b0a26 100644 --- a/src/forecast.py +++ b/src/forecast.py @@ -19,7 +19,7 @@ from elexonpy.api_client import ApiClient from elexonpy.api.generation_forecast_api import GenerationForecastApi -from plots.elexon_plots import fetch_forecast_data, determine_start_and_end_datetimes +from plots.elexon_plots import add_elexon_plot class GSPLabeler: """A function class to add the GSP name to the GSP IDs""" @@ -201,49 +201,10 @@ def forecast_page(): ) ) - start_datetime_utc, end_datetime_utc = determine_start_and_end_datetimes(start_datetimes, end_datetimes) - - if start_datetime_utc and end_datetime_utc: - # Initialize Elexon API client - api_client = ApiClient() - forecast_api = GenerationForecastApi(api_client) - forecast_generation_wind_and_solar_day_ahead_get = forecast_api.forecast_generation_wind_and_solar_day_ahead_get - # Fetch data for each process type - process_types = ["Day Ahead", "Intraday Process", "Intraday Total"] - line_styles = ["solid", "dash", "dot"] - forecasts = [ - fetch_forecast_data( - forecast_generation_wind_and_solar_day_ahead_get, - start_datetime_utc, - end_datetime_utc, - pt - ) for pt in process_types - ] - - for i, (forecast, line_style) in enumerate(zip(forecasts, line_styles)): - if forecast.empty: - st.write(f"No data available for process type: {process_types[i]}") - continue - # Remove NaNs and zero values to ensure clean data for plotting - forecast = forecast[forecast["quantity"].notna() & (forecast["quantity"] > 0)] - - full_time_range = pd.date_range(start=start_datetime_utc, end=end_datetime_utc, freq='30T', tz=forecast["start_time"].dt.tz) - full_time_df = pd.DataFrame(full_time_range, columns=['start_time']) - forecast = full_time_df.merge(forecast, on='start_time', how='left') - - fig.add_trace(go.Scatter( - x=forecast["start_time"], - y=forecast["quantity"], - mode='lines', - name=f"Elexon {process_types[i]}", - line=dict(color='#318CE7', dash=line_style), - connectgaps=False - )) - - st.plotly_chart(fig, theme="streamlit") - else: - st.error("Invalid date range. Start date must be before end date.") + # Call the function to add Elexon plot and capture the returned figure + fig = add_elexon_plot(fig, start_datetimes, end_datetimes) + st.plotly_chart(fig, theme="streamlit") def plot_pvlive(fig, gsp_id, pvlive_data, pvlive_gsp_sum_dayafter, pvlive_gsp_sum_inday): # pvlive on the chart diff --git a/src/plots/elexon_plots.py b/src/plots/elexon_plots.py index b9eae67..17e02f3 100644 --- a/src/plots/elexon_plots.py +++ b/src/plots/elexon_plots.py @@ -1,8 +1,63 @@ -from typing import Callable, List, Tuple, Union +from typing import Callable, List, Optional, Tuple, Union import pandas as pd from datetime import datetime, date, timedelta from plotly import graph_objects as go import streamlit as st +from elexonpy.api_client import ApiClient +from elexonpy.api.generation_forecast_api import GenerationForecastApi + +def add_elexon_plot(fig: go.Figure, start_datetimes: List[Optional[datetime]], end_datetimes: List[Optional[datetime]]) -> go.Figure: + """ + Adds Elexon forecast data to the given Plotly figure. + + Parameters: + - fig (go.Figure): The Plotly figure to which the Elexon data will be added. + - start_datetimes (List[Optional[datetime]]): List of start datetimes for the forecast. + - end_datetimes (List[Optional[datetime]]): List of end datetimes for the forecast. + + Returns: + - go.Figure: The modified Plotly figure with Elexon data added. + """ + start_datetime_utc, end_datetime_utc = determine_start_and_end_datetimes(start_datetimes, end_datetimes) + + if start_datetime_utc and end_datetime_utc: + # Initialize Elexon API client + api_client = ApiClient() + forecast_api = GenerationForecastApi(api_client) + forecast_generation_wind_and_solar_day_ahead_get = forecast_api.forecast_generation_wind_and_solar_day_ahead_get + # Fetch data for each process type + process_types = ["Day Ahead", "Intraday Process", "Intraday Total"] + line_styles = ["solid", "dash", "dot"] + forecasts = [ + fetch_forecast_data( + forecast_generation_wind_and_solar_day_ahead_get, + start_datetime_utc, + end_datetime_utc, + pt + ) for pt in process_types + ] + + for i, (forecast, line_style) in enumerate(zip(forecasts, line_styles)): + if forecast.empty: + continue + # Remove NaNs and zero values to ensure clean data for plotting + forecast = forecast[forecast["quantity"].notna() & (forecast["quantity"] > 0)] + + full_time_range = pd.date_range(start=start_datetime_utc, end=end_datetime_utc, freq='30T', tz=forecast["start_time"].dt.tz) + full_time_df = pd.DataFrame(full_time_range, columns=['start_time']) + forecast = full_time_df.merge(forecast, on='start_time', how='left') + + fig.add_trace(go.Scatter( + x=forecast["start_time"], + y=forecast["quantity"], + mode='lines', + name=f"Elexon {process_types[i]}", + line=dict(color='#318CE7', dash=line_style), + connectgaps=False + )) + + return fig + def fetch_forecast_data(api_func: Callable, start_date: datetime, end_date: datetime, process_type: str) -> pd.DataFrame: """ From 00a86578d16dc9899f20a3b9f335271d3cf945fc Mon Sep 17 00:00:00 2001 From: 14Richa Date: Wed, 14 Aug 2024 21:38:22 +0100 Subject: [PATCH 20/26] remove unwanted imports --- src/forecast.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/forecast.py b/src/forecast.py index 33b0a26..2ee3aa8 100644 --- a/src/forecast.py +++ b/src/forecast.py @@ -16,8 +16,6 @@ from plots.utils import ( get_colour_from_model_name, model_is_probabilistic, get_recent_available_model_names ) -from elexonpy.api_client import ApiClient -from elexonpy.api.generation_forecast_api import GenerationForecastApi from plots.elexon_plots import add_elexon_plot From 3ad3d7ea772b0287fd2bce09d403aad7019cb63a Mon Sep 17 00:00:00 2001 From: 14Richa Date: Wed, 14 Aug 2024 21:40:07 +0100 Subject: [PATCH 21/26] run black command for line length --- src/forecast.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/forecast.py b/src/forecast.py index 2ee3aa8..b3dafe8 100644 --- a/src/forecast.py +++ b/src/forecast.py @@ -14,13 +14,17 @@ from nowcasting_datamodel.read.read_gsp import get_gsp_yield, get_gsp_yield_sum from plots.utils import ( - get_colour_from_model_name, model_is_probabilistic, get_recent_available_model_names + get_colour_from_model_name, + model_is_probabilistic, + get_recent_available_model_names, ) from plots.elexon_plots import add_elexon_plot + class GSPLabeler: """A function class to add the GSP name to the GSP IDs""" + def __init__(self, gsp_ids, gsp_names): """A function class to add the GSP name to the GSP IDs""" self.gsp_ids = gsp_ids @@ -31,6 +35,7 @@ def __call__(self, gsp_id): i = self.gsp_ids.index(gsp_id) return f"{gsp_id}: {self.gsp_names[i]}" + def forecast_page(): """Main page for status""" st.markdown( @@ -50,13 +55,7 @@ def forecast_page(): gsp_labeler = GSPLabeler(gsp_ids, gsp_names) - gsp_id = st.sidebar.selectbox( - "Select a region", - gsp_ids, - index=0, - format_func=gsp_labeler - ) - + gsp_id = st.sidebar.selectbox("Select a region", gsp_ids, index=0, format_func=gsp_labeler) # Get effective capacity of selected GSP capacity_mw = locations[gsp_ids.index(gsp_id)].installed_capacity_mw @@ -131,10 +130,7 @@ def forecast_page(): for start_dt, end_dt in zip(start_datetimes, end_datetimes): if forecast_type == "Now": forecast_values = get_forecast_values_latest( - session=session, - gsp_id=gsp_id, - model_name=model, - start_datetime=start_dt, + session=session, gsp_id=gsp_id, model_name=model, start_datetime=start_dt ) label = model @@ -204,6 +200,7 @@ def forecast_page(): st.plotly_chart(fig, theme="streamlit") + def plot_pvlive(fig, gsp_id, pvlive_data, pvlive_gsp_sum_dayafter, pvlive_gsp_sum_inday): # pvlive on the chart for k, v in pvlive_data.items(): @@ -238,6 +235,7 @@ def plot_pvlive(fig, gsp_id, pvlive_data, pvlive_gsp_sum_dayafter, pvlive_gsp_su go.Scatter(x=x, y=y, mode="lines", name=k, line=line, visible="legendonly") ) + def plot_forecasts(fig, forecast_per_model, selected_prob_models, show_prob): index_forecast_per_model = 0 @@ -306,6 +304,7 @@ def plot_forecasts(fig, forecast_per_model, selected_prob_models, show_prob): print("Could not add plevel to chart") raise e + def get_pvlive_data(end_datetime, gsp_id, session, start_datetime): pvlive_inday = get_gsp_yield( session=session, @@ -338,4 +337,4 @@ def get_pvlive_data(end_datetime, gsp_id, session, start_datetime): pvlive_data = {} pvlive_data["PVLive Initial Estimate"] = [GSPYield.from_orm(f) for f in pvlive_inday] pvlive_data["PVLive Updated Estimate"] = [GSPYield.from_orm(f) for f in pvlive_dayafter] - return pvlive_data, pvlive_gsp_sum_dayafter, pvlive_gsp_sum_inday \ No newline at end of file + return pvlive_data, pvlive_gsp_sum_dayafter, pvlive_gsp_sum_inday From 1bf3780f18df4bd56837efb6b5ec7690fc99ebda Mon Sep 17 00:00:00 2001 From: 14Richa Date: Wed, 14 Aug 2024 21:41:29 +0100 Subject: [PATCH 22/26] run black command --- src/plots/elexon_plots.py | 61 +++++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/src/plots/elexon_plots.py b/src/plots/elexon_plots.py index 17e02f3..fd2e40a 100644 --- a/src/plots/elexon_plots.py +++ b/src/plots/elexon_plots.py @@ -6,7 +6,12 @@ from elexonpy.api_client import ApiClient from elexonpy.api.generation_forecast_api import GenerationForecastApi -def add_elexon_plot(fig: go.Figure, start_datetimes: List[Optional[datetime]], end_datetimes: List[Optional[datetime]]) -> go.Figure: + +def add_elexon_plot( + fig: go.Figure, + start_datetimes: List[Optional[datetime]], + end_datetimes: List[Optional[datetime]], +) -> go.Figure: """ Adds Elexon forecast data to the given Plotly figure. @@ -18,13 +23,17 @@ def add_elexon_plot(fig: go.Figure, start_datetimes: List[Optional[datetime]], e Returns: - go.Figure: The modified Plotly figure with Elexon data added. """ - start_datetime_utc, end_datetime_utc = determine_start_and_end_datetimes(start_datetimes, end_datetimes) + start_datetime_utc, end_datetime_utc = determine_start_and_end_datetimes( + start_datetimes, end_datetimes + ) if start_datetime_utc and end_datetime_utc: # Initialize Elexon API client api_client = ApiClient() forecast_api = GenerationForecastApi(api_client) - forecast_generation_wind_and_solar_day_ahead_get = forecast_api.forecast_generation_wind_and_solar_day_ahead_get + forecast_generation_wind_and_solar_day_ahead_get = ( + forecast_api.forecast_generation_wind_and_solar_day_ahead_get + ) # Fetch data for each process type process_types = ["Day Ahead", "Intraday Process", "Intraday Total"] line_styles = ["solid", "dash", "dot"] @@ -33,8 +42,9 @@ def add_elexon_plot(fig: go.Figure, start_datetimes: List[Optional[datetime]], e forecast_generation_wind_and_solar_day_ahead_get, start_datetime_utc, end_datetime_utc, - pt - ) for pt in process_types + pt, + ) + for pt in process_types ] for i, (forecast, line_style) in enumerate(zip(forecasts, line_styles)): @@ -43,23 +53,32 @@ def add_elexon_plot(fig: go.Figure, start_datetimes: List[Optional[datetime]], e # Remove NaNs and zero values to ensure clean data for plotting forecast = forecast[forecast["quantity"].notna() & (forecast["quantity"] > 0)] - full_time_range = pd.date_range(start=start_datetime_utc, end=end_datetime_utc, freq='30T', tz=forecast["start_time"].dt.tz) - full_time_df = pd.DataFrame(full_time_range, columns=['start_time']) - forecast = full_time_df.merge(forecast, on='start_time', how='left') - - fig.add_trace(go.Scatter( - x=forecast["start_time"], - y=forecast["quantity"], - mode='lines', - name=f"Elexon {process_types[i]}", - line=dict(color='#318CE7', dash=line_style), - connectgaps=False - )) + full_time_range = pd.date_range( + start=start_datetime_utc, + end=end_datetime_utc, + freq="30T", + tz=forecast["start_time"].dt.tz, + ) + full_time_df = pd.DataFrame(full_time_range, columns=["start_time"]) + forecast = full_time_df.merge(forecast, on="start_time", how="left") + + fig.add_trace( + go.Scatter( + x=forecast["start_time"], + y=forecast["quantity"], + mode="lines", + name=f"Elexon {process_types[i]}", + line=dict(color="#318CE7", dash=line_style), + connectgaps=False, + ) + ) return fig -def fetch_forecast_data(api_func: Callable, start_date: datetime, end_date: datetime, process_type: str) -> pd.DataFrame: +def fetch_forecast_data( + api_func: Callable, start_date: datetime, end_date: datetime, process_type: str +) -> pd.DataFrame: """ Fetches forecast data from an API and processes it. @@ -96,9 +115,9 @@ def fetch_forecast_data(api_func: Callable, start_date: datetime, end_date: date st.error(f"Error fetching data for process type '{process_type}': {e}") return pd.DataFrame() + def determine_start_and_end_datetimes( - start_datetimes: List[Union[datetime, date]], - end_datetimes: List[Union[datetime, date]] + start_datetimes: List[Union[datetime, date]], end_datetimes: List[Union[datetime, date]] ) -> Tuple[datetime, datetime]: """ Determines the start and end datetime in UTC. @@ -133,4 +152,4 @@ def determine_start_and_end_datetimes( # Assert that start is before end assert start_datetime_utc < end_datetime_utc, "Start datetime must be before end datetime." - return start_datetime_utc, end_datetime_utc \ No newline at end of file + return start_datetime_utc, end_datetime_utc From c94353a6394336c023dd2327ea52abe23e96034a Mon Sep 17 00:00:00 2001 From: 14Richa Date: Thu, 15 Aug 2024 11:35:30 +0100 Subject: [PATCH 23/26] added test file for elexonplot --- tests/test_elexon_plot.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 tests/test_elexon_plot.py diff --git a/tests/test_elexon_plot.py b/tests/test_elexon_plot.py new file mode 100644 index 0000000..1726c24 --- /dev/null +++ b/tests/test_elexon_plot.py @@ -0,0 +1,23 @@ +import pandas as pd +import pytest +from datetime import datetime + + +from plots.elexon_plots import determine_start_and_end_datetimes + +def test_determine_start_and_end_datetimes_no_input(): + # Test with no input + now = datetime.utcnow() + start, end = determine_start_and_end_datetimes([], []) + assert start < now, "Start time should be before current time." + assert end > start, "End time should be after start time." + +def test_determine_start_and_end_datetimes_with_start_only(): + start_date = datetime(2024, 8, 1) + start, end = determine_start_and_end_datetimes([start_date], []) + assert start == start_date, "Start time should match provided start_date." + assert end > start, "End time should be 7 days after the start time." + +def test_determine_start_and_end_datetimes_with_invalid_dates(): + with pytest.raises(AssertionError): + determine_start_and_end_datetimes([datetime(2024, 8, 10)], [datetime(2024, 8, 5)]) From 1e2bd89b3f8ce5fb1e4a98978dddb0173639a908 Mon Sep 17 00:00:00 2001 From: 14Richa Date: Thu, 15 Aug 2024 11:39:24 +0100 Subject: [PATCH 24/26] added test file for elexonplot --- tests/test_elexon_plot.py | 55 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/tests/test_elexon_plot.py b/tests/test_elexon_plot.py index 1726c24..0563cfb 100644 --- a/tests/test_elexon_plot.py +++ b/tests/test_elexon_plot.py @@ -1,9 +1,9 @@ +from unittest.mock import Mock, patch import pandas as pd import pytest from datetime import datetime - - -from plots.elexon_plots import determine_start_and_end_datetimes +from plotly import graph_objects as go +from plots.elexon_plots import add_elexon_plot, determine_start_and_end_datetimes, fetch_forecast_data def test_determine_start_and_end_datetimes_no_input(): # Test with no input @@ -21,3 +21,52 @@ def test_determine_start_and_end_datetimes_with_start_only(): def test_determine_start_and_end_datetimes_with_invalid_dates(): with pytest.raises(AssertionError): determine_start_and_end_datetimes([datetime(2024, 8, 10)], [datetime(2024, 8, 5)]) + +def test_fetch_forecast_data_empty_response(): + # Mock the API function to return an empty response + mock_api_func = Mock() + mock_api_func.return_value.data = [] + + result = fetch_forecast_data(mock_api_func, datetime(2024, 8, 1), datetime(2024, 8, 2), "Day Ahead") + assert result.empty, "Result should be an empty DataFrame" + +def test_fetch_forecast_data_api_failure(): + # Mock the API function to raise an exception + mock_api_func = Mock(side_effect=Exception("API failure")) + + result = fetch_forecast_data(mock_api_func, datetime(2024, 8, 1), datetime(2024, 8, 2), "Day Ahead") + assert result.empty, "Result should be an empty DataFrame in case of an API failure" + +@patch('plots.elexon_plots.fetch_forecast_data') +def test_add_elexon_plot_with_data(mock_fetch): + # Mock fetch_forecast_data to return a non-empty DataFrame + mock_fetch.return_value = pd.DataFrame({ + "start_time": pd.date_range("2024-08-01", periods=3, freq="30T"), + "quantity": [100, 200, 150] + }) + + # Create an empty Plotly figure + fig = go.Figure() + + start_datetime = [datetime(2024, 8, 1)] + end_datetime = [datetime(2024, 8, 2)] + updated_fig = add_elexon_plot(fig, start_datetime, end_datetime) + + # Assert + assert len(updated_fig.data) > 0, "Figure should have traces added" + assert updated_fig.data[0].name.startswith("Elexon"), "Trace should be labeled as Elexon" + assert updated_fig.data[0].line.dash == "solid", "Line style should be solid for the first trace" + +@patch('plots.elexon_plots.fetch_forecast_data') +def test_add_elexon_plot_no_data(mock_fetch): + # Mock fetch_forecast_data to return an empty DataFrame + mock_fetch.return_value = pd.DataFrame() + + # Create an empty Plotly figure + fig = go.Figure() + start_datetime = [datetime(2024, 8, 1)] + end_datetime = [datetime(2024, 8, 2)] + updated_fig = add_elexon_plot(fig, start_datetime, end_datetime) + + # Assert + assert len(updated_fig.data) == 0, "Figure should have no traces added if no data is available" \ No newline at end of file From 1f339541cfb2979fe2365dbe1759b6a275cc92bf Mon Sep 17 00:00:00 2001 From: 14Richa Date: Thu, 15 Aug 2024 13:16:26 +0100 Subject: [PATCH 25/26] added integration test --- tests/test_elexon_plot.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/tests/test_elexon_plot.py b/tests/test_elexon_plot.py index 0563cfb..18e0ee4 100644 --- a/tests/test_elexon_plot.py +++ b/tests/test_elexon_plot.py @@ -4,6 +4,8 @@ from datetime import datetime from plotly import graph_objects as go from plots.elexon_plots import add_elexon_plot, determine_start_and_end_datetimes, fetch_forecast_data +from elexonpy.api_client import ApiClient +from elexonpy.api.generation_forecast_api import GenerationForecastApi def test_determine_start_and_end_datetimes_no_input(): # Test with no input @@ -69,4 +71,29 @@ def test_add_elexon_plot_no_data(mock_fetch): updated_fig = add_elexon_plot(fig, start_datetime, end_datetime) # Assert - assert len(updated_fig.data) == 0, "Figure should have no traces added if no data is available" \ No newline at end of file + assert len(updated_fig.data) == 0, "Figure should have no traces added if no data is available" + +@pytest.mark.integration +def test_fetch_forecast_data_integration(): + # Initialize the actual API client and the function to be tested + api_client = ApiClient() + forecast_api = GenerationForecastApi(api_client) + forecast_generation_wind_and_solar_day_ahead_get = forecast_api.forecast_generation_wind_and_solar_day_ahead_get + + # Define the start and end date for fetching the data + start_date = datetime(2024, 8, 1) + end_date = datetime(2024, 8, 2) + + # Call the function with real data + result = fetch_forecast_data(forecast_generation_wind_and_solar_day_ahead_get, start_date, end_date, "Day Ahead") + + # Assertions to check the returned DataFrame + assert isinstance(result, pd.DataFrame), "Result should be a DataFrame" + + # If data exists for the given dates, the DataFrame shouldn't be empty + if not result.empty: + assert "start_time" in result.columns, "DataFrame should contain 'start_time' column" + assert result["quantity"].notna().all(), "Quantity values should not be NaN" + else: + # If the DataFrame is empty, it indicates no data was returned for the given date range + print("No data returned for the given date range.") \ No newline at end of file From a20f718f2cf69eb73a0e1f859ba4f900184405a3 Mon Sep 17 00:00:00 2001 From: 14Richa Date: Thu, 15 Aug 2024 13:27:54 +0100 Subject: [PATCH 26/26] refactored the test --- tests/test_elexon_plot.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/test_elexon_plot.py b/tests/test_elexon_plot.py index 18e0ee4..c93c732 100644 --- a/tests/test_elexon_plot.py +++ b/tests/test_elexon_plot.py @@ -89,11 +89,7 @@ def test_fetch_forecast_data_integration(): # Assertions to check the returned DataFrame assert isinstance(result, pd.DataFrame), "Result should be a DataFrame" - - # If data exists for the given dates, the DataFrame shouldn't be empty - if not result.empty: - assert "start_time" in result.columns, "DataFrame should contain 'start_time' column" - assert result["quantity"].notna().all(), "Quantity values should not be NaN" - else: - # If the DataFrame is empty, it indicates no data was returned for the given date range - print("No data returned for the given date range.") \ No newline at end of file + assert not result.empty, "DataFrame should not be empty" + assert "start_time" in result.columns, "DataFrame should contain 'start_time' column" + assert "quantity" in result.columns, "DataFrame should contain 'quantity' column" + assert result["quantity"].notna().all(), "Quantity values should not be NaN" \ No newline at end of file