Skip to content

Commit 266e46d

Browse files
authored
Merge pull request #75 from cyiallou/fix/no-data-failure
Add simple NoDataAvailableError exception for missing data handling
2 parents 44fd7ab + acbbc1a commit 266e46d

File tree

2 files changed

+114
-77
lines changed

2 files changed

+114
-77
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@
1313

1414
## Bug Fixes
1515

16-
<!-- Here goes notable bug fixes that are worth a special mention or explanation -->
16+
- Introduced `NoDataAvailableError` exception to represent situations where no data is available and to skip such cases during workflow execution and plotting.

src/frequenz/lib/notebooks/solar/maintenance/solar_maintenance_app.py

Lines changed: 113 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ class SolarAnalysisData:
119119
)
120120

121121

122+
class NoDataAvailableError(Exception):
123+
"""Raised when there is no available data."""
124+
125+
122126
# pylint: disable=too-many-statements, too-many-branches, too-many-locals
123127
async def run_workflow(user_config_changes: dict[str, Any]) -> SolarAnalysisData:
124128
"""Run the Solar Maintenance App workflow.
@@ -142,6 +146,7 @@ async def run_workflow(user_config_changes: dict[str, Any]) -> SolarAnalysisData
142146
`stat_profile_view_col_to_plot`) are hardcoded.
143147
- If the timezone of the data does not match the timezone in the
144148
configuration.
149+
NoDataAvailableError: If no reporting data is available.
145150
"""
146151
config, all_client_site_info = _load_and_validate_config(user_config_changes)
147152

@@ -193,12 +198,19 @@ async def run_workflow(user_config_changes: dict[str, Any]) -> SolarAnalysisData
193198
)
194199
reporting_data_higher_fs = await retrieve_data(reporting_config)
195200

196-
weather_data = transform_weather_data(
197-
data=weather_data,
198-
weather_feature_names_mapping=config.weather_feature_names_mapping,
199-
time_zone=config.time_zone,
200-
verbose=config.verbose,
201-
)
201+
if reporting_data.empty and reporting_data_higher_fs.empty:
202+
raise NoDataAvailableError("No reporting data available. Cannot proceed.")
203+
204+
if not weather_data.empty:
205+
weather_data = transform_weather_data(
206+
data=weather_data,
207+
weather_feature_names_mapping=config.weather_feature_names_mapping,
208+
time_zone=config.time_zone,
209+
verbose=config.verbose,
210+
)
211+
lat_lon_pairs = _create_lat_lon_pairs(
212+
weather_data["latitude"].unique(), weather_data["longitude"].unique()
213+
)
202214

203215
reporting_data = transform_reporting_data(
204216
data=reporting_data,
@@ -224,10 +236,6 @@ async def run_workflow(user_config_changes: dict[str, Any]) -> SolarAnalysisData
224236
lambda x: abs(x) if np.issubdtype(type(x), np.number) else x
225237
)
226238

227-
lat_lon_pairs = _create_lat_lon_pairs(
228-
weather_data["latitude"].unique(), weather_data["longitude"].unique()
229-
)
230-
231239
# display the results for each microgrid separately
232240
production_legend_label = tm.translate("production")
233241
patch_label = tm.translate("current value")
@@ -285,6 +293,10 @@ async def run_workflow(user_config_changes: dict[str, Any]) -> SolarAnalysisData
285293
reporting_data_higher_fs, col_text, verbose=config.verbose
286294
)
287295
normalisation_factor = 1
296+
if data.empty:
297+
reason = NoDataAvailableError(f"No data available for microgrid ID {mid}.")
298+
print(f"{type(reason).__name__}: {reason} Skipping...")
299+
continue
288300
timezone = str(pd.to_datetime(data.index).tzinfo)
289301
if timezone != config.time_zone.key:
290302
raise ValueError("Timezone mismatch.")
@@ -302,25 +314,31 @@ async def run_workflow(user_config_changes: dict[str, Any]) -> SolarAnalysisData
302314
[k for k in config.baseline_models if k != "weather-based-forecast"],
303315
)
304316
if "weather-based-forecast" in config.baseline_models:
305-
closest_grid_point = _find_closest_grid_point(
306-
all_client_site_info[mid]["latitude"],
307-
all_client_site_info[mid]["longitude"],
308-
lat_lon_pairs,
309-
)
310-
prediction_models.update(
311-
prepare_prediction_models(
312-
weather_data[
313-
(weather_data["latitude"] == closest_grid_point[0])
314-
& (weather_data["longitude"] == closest_grid_point[1])
315-
& (
316-
weather_data["validity_ts"]
317-
<= config.end_timestamp + datetime.timedelta(hours=1)
318-
)
319-
],
320-
model_specs,
321-
["weather-based-forecast"],
317+
if weather_data.empty:
318+
reason = NoDataAvailableError("No weather data available.")
319+
print(
320+
f"{type(reason).__name__}: {reason} Skipping weather-based-forecast model."
321+
)
322+
else:
323+
closest_grid_point = _find_closest_grid_point(
324+
all_client_site_info[mid]["latitude"],
325+
all_client_site_info[mid]["longitude"],
326+
lat_lon_pairs,
327+
)
328+
prediction_models.update(
329+
prepare_prediction_models(
330+
weather_data[
331+
(weather_data["latitude"] == closest_grid_point[0])
332+
& (weather_data["longitude"] == closest_grid_point[1])
333+
& (
334+
weather_data["validity_ts"]
335+
<= config.end_timestamp + datetime.timedelta(hours=1)
336+
)
337+
],
338+
model_specs,
339+
["weather-based-forecast"],
340+
)
322341
)
323-
)
324342
# NOTE: the below is a hack until PV lib simulation is properly set up
325343
# (i.e. needs user input for the PV system parameters)
326344
if "simulation" in prediction_models:
@@ -397,8 +415,12 @@ async def run_workflow(user_config_changes: dict[str, Any]) -> SolarAnalysisData
397415
rolling_view_average = RollingPreparer(rolling_view_average_config).prepare(
398416
data[[long_term_view_col_to_plot]]
399417
)
400-
rolling_view_real_time = RollingPreparer(rolling_view_real_time_config).prepare(
401-
data_higher_fs[real_time_view_col_to_plot] / normalisation_factor
418+
rolling_view_real_time = (
419+
pd.DataFrame()
420+
if data_higher_fs.empty
421+
else RollingPreparer(rolling_view_real_time_config).prepare(
422+
data_higher_fs[real_time_view_col_to_plot] / normalisation_factor
423+
)
402424
)
403425
daily_production_view = DailyPreparer(daily_plot_config).prepare(data)
404426
statistical_view = ProfilePreparer(statistical_plot_config).prepare(data)
@@ -481,54 +503,67 @@ async def run_workflow(user_config_changes: dict[str, Any]) -> SolarAnalysisData
481503
fig=figures_and_axes["fig_real_time"]["figure"],
482504
ax=figures_and_axes["fig_real_time"]["axes"][0],
483505
)
484-
if plot_settings["show_annotation"]:
485-
if len(real_time_view_col_to_plot) == 1:
486-
for col in real_time_view_col_to_plot:
487-
recent_y = rolling_view_real_time[str(col)].iloc[-2]
488-
_annotate_last_point(
489-
figures_and_axes["fig_real_time"]["axes"][0], recent_y
490-
)
491-
patch = Patch(
492-
color=plot_settings["patch_colour"], label=patch_label
493-
)
494-
figures_and_axes["fig_real_time"]["axes"][0].set_ylabel(real_time_view_ylabel)
495-
496-
if plot_settings["legend_update_on"] == "figure":
497-
_legend_kwargs_copy = plot_settings["legend_kwargs"].copy()
498-
# divide legend labels into groups of 2 if needed
499-
_legend_kwargs_copy["ncol"] = max(
500-
_legend_kwargs_copy["ncol"],
501-
(
502-
len(
503-
figures_and_axes["fig_real_time"]["axes"][
504-
0
505-
].get_legend_handles_labels()[1]
506-
)
507-
+ 1
508-
)
509-
// 2,
510-
)
506+
if data_higher_fs.empty:
507+
reason = NoDataAvailableError("No data available for real-time view.")
508+
print(f"{reason} Skipping this plot.")
509+
print(f"{type(reason).__name__}: {reason}. Skipping this plot.")
510+
# the plotter automatically hides the axis when data is empty
511+
# but we need to deal with the figure itself
512+
# we can safely clear the figure like this because it only plots rolling_view_real_time
513+
figures_and_axes["fig_real_time"]["figure"].clf()
511514
else:
512-
_legend_kwargs_copy = plot_settings["legend_kwargs"]
513-
plot_manager.update_legend(
514-
fig_id="fig_real_time",
515-
axs=[figures_and_axes["fig_real_time"]["axes"][0]],
516-
on=plot_settings["legend_update_on"],
517-
modifications={
518-
"additional_items": (
519-
[(patch, patch_label)] if plot_settings["show_annotation"] else None
520-
),
521-
"replace_label": {
522-
str(col): (
523-
tm.translate("component_{value}", value=col)
524-
if config.split_real_time_view_per_inverter
525-
else production_legend_label
515+
if plot_settings["show_annotation"]:
516+
if len(real_time_view_col_to_plot) == 1:
517+
for col in real_time_view_col_to_plot:
518+
recent_y = rolling_view_real_time[str(col)].iloc[-2]
519+
_annotate_last_point(
520+
figures_and_axes["fig_real_time"]["axes"][0], recent_y
521+
)
522+
patch = Patch(
523+
color=plot_settings["patch_colour"], label=patch_label
524+
)
525+
figures_and_axes["fig_real_time"]["axes"][0].set_ylabel(
526+
real_time_view_ylabel
527+
)
528+
529+
if plot_settings["legend_update_on"] == "figure":
530+
_legend_kwargs_copy = plot_settings["legend_kwargs"].copy()
531+
# divide legend labels into groups of 2 if needed
532+
_legend_kwargs_copy["ncol"] = max(
533+
_legend_kwargs_copy["ncol"],
534+
(
535+
len(
536+
figures_and_axes["fig_real_time"]["axes"][
537+
0
538+
].get_legend_handles_labels()[1]
539+
)
540+
+ 1
526541
)
527-
for col in real_time_view_col_to_plot
542+
// 2,
543+
)
544+
else:
545+
_legend_kwargs_copy = plot_settings["legend_kwargs"]
546+
plot_manager.update_legend(
547+
fig_id="fig_real_time",
548+
axs=[figures_and_axes["fig_real_time"]["axes"][0]],
549+
on=plot_settings["legend_update_on"],
550+
modifications={
551+
"additional_items": (
552+
[(patch, patch_label)]
553+
if plot_settings["show_annotation"]
554+
else None
555+
),
556+
"replace_label": {
557+
str(col): (
558+
tm.translate("component_{value}", value=col)
559+
if config.split_real_time_view_per_inverter
560+
else production_legend_label
561+
)
562+
for col in real_time_view_col_to_plot
563+
},
528564
},
529-
},
530-
**_legend_kwargs_copy,
531-
)
565+
**_legend_kwargs_copy,
566+
)
532567
# ------------------- #
533568

534569
# --- plot the statistical production profile --- #
@@ -798,7 +833,9 @@ async def run_workflow(user_config_changes: dict[str, Any]) -> SolarAnalysisData
798833
for fig in figures_and_axes.keys():
799834
plot_manager.adjust_axes_spacing(fig_id=fig, pixels=100.0)
800835

801-
output.real_time_view[mid] = rolling_view_real_time
836+
output.real_time_view[mid] = (
837+
pd.DataFrame() if data_higher_fs.empty else rolling_view_real_time
838+
)
802839
output.rolling_view_short_term[mid] = rolling_view_short_term
803840
output.rolling_view_long_term[mid] = rolling_view_long_term
804841
output.rolling_view_average[mid] = rolling_view_average

0 commit comments

Comments
 (0)