Skip to content

Commit 4a3d2dd

Browse files
authored
feat: add benchmark section to report (#20)
1 parent daaacf2 commit 4a3d2dd

File tree

6 files changed

+410
-41
lines changed

6 files changed

+410
-41
lines changed

action.yml

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,10 @@ runs:
119119
--config_file ${{ inputs.snakemake_config }} \
120120
--main_command "${{ inputs.main_command }}" \
121121
--pre_command "${{ inputs.pre_command }}"
122-
122+
123123
124124
shell: bash
125-
125+
126126
- name: Upload artifacts (logs)
127127
if: ${{ inputs.step == 'run-self-hosted-validation' }}
128128
uses: actions/upload-artifact@v4
@@ -300,7 +300,6 @@ runs:
300300
mkdir -p _validation-images/main
301301
302302
# Copy plots
303-
echo "Plots: ${plots_array[@]}"
304303
for plotpath in "${plots_array[@]}"
305304
do
306305
subpath="${plotpath%/*}"
@@ -315,6 +314,23 @@ runs:
315314
cp "$HOME/artifacts/results/feature/results/${PREFIX_FEATURE}/${subpath}/${plot}" "_validation-images/feature/${subpath}/" || true # ignore if run failed
316315
done
317316
317+
# Get benchmark plot list (from benchmark script)
318+
read -a plots_array_benchmark <<< "$(python scripts/plot_benchmarks.py)"
319+
320+
mkdir -p _validation-images/benchmarks
321+
322+
# Copy benchmark plots
323+
for plot in "${plots_array_benchmark[@]}"
324+
do
325+
echo "Copying benchmark plot: ${plot}
326+
327+
# Create directories
328+
mkdir -p "_validation-images/benchmarks
329+
330+
cp "${plot}" "_validation-images/benchmarks" || true # ignore if run failed
331+
cp "${plot}" "_validation-images/benchmarks" || true # ignore if run failed
332+
done
333+
318334
# Add plots to repo branch
319335
echo "Adding plots to repo branch"
320336
git add _validation-images

requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
numpy
22
pandas
3-
openpyxl
3+
openpyxl
4+
matplotlib
5+
seaborn

scripts/draft_comment.py

Lines changed: 94 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@
99
import re
1010
from dataclasses import dataclass
1111
from pathlib import Path
12-
from typing import Any
1312

1413
import numpy as np
1514
import pandas as pd
1615
from metrics import min_max_normalized_mae, normalized_root_mean_square_error
1716
from numpy.typing import ArrayLike
17+
from utils import get_env_var
1818

1919

2020
def create_numeric_mask(arr: ArrayLike) -> np.ndarray:
@@ -35,17 +35,6 @@ def create_numeric_mask(arr: ArrayLike) -> np.ndarray:
3535
return np.vectorize(lambda x: isinstance(x, (int, float)) and np.isfinite(x))(arr)
3636

3737

38-
def get_env_var(var_name: str, default: Any = None) -> Any:
39-
"""Get environment variable or raise an error if not set and no default provided."""
40-
value = os.getenv(var_name, default)
41-
if value == "" and default is None:
42-
msg = f"The environment variable '{var_name}' is not set."
43-
raise OSError(msg)
44-
if str(value).lower() in ["true", "false"]:
45-
return str(value).lower() == "true"
46-
return value
47-
48-
4938
@dataclass
5039
class CommentData:
5140
"""Class to store data for comment generation."""
@@ -69,6 +58,13 @@ class CommentData:
6958

7059
_sucessfull_run = None
7160

61+
def __init__(self):
62+
"""Initialize comment data class."""
63+
self.plots_base_url = (
64+
f"https://raw.githubusercontent.com/lkstrp/"
65+
f"pypsa-validator/{self.plots_hash}/_validation-images/"
66+
)
67+
7268
def errors(self, branch_type: str) -> list:
7369
"""Return errors for branch type."""
7470
if branch_type not in ["main", "feature"]:
@@ -115,6 +111,7 @@ def sucessfull_run(self) -> bool:
115111

116112

117113
def get_deviation_df(df1: pd.DataFrame, df2: pd.DataFrame) -> pd.DataFrame:
114+
"""Calculate deviation dataframe between two dataframes."""
118115
nrmse_series = df1.apply(
119116
lambda row: normalized_root_mean_square_error(
120117
row.values,
@@ -138,11 +135,27 @@ def get_deviation_df(df1: pd.DataFrame, df2: pd.DataFrame) -> pd.DataFrame:
138135
return deviation_df
139136

140137

138+
def create_details_block(summary: str, content: str) -> str:
139+
"""Wrap content in a details block (if content is not empty)."""
140+
if content:
141+
return (
142+
f"<details>\n"
143+
f" <summary>{summary}</summary>\n"
144+
f"{content}"
145+
f"</details>\n"
146+
f"\n"
147+
f"\n"
148+
)
149+
else:
150+
return ""
151+
152+
141153
class RunSuccessfull(CommentData):
142154
"""Class to generate successfull run component."""
143155

144156
def __init__(self):
145-
"""Initialize class."""
157+
"""Initialize successfull run component."""
158+
super().__init__()
146159
self.dir_main = [
147160
file
148161
for file in (self.dir_artifacts / "results/main/results").iterdir()
@@ -173,8 +186,6 @@ def __init__(self):
173186

174187
self._variables_deviation_df = None
175188

176-
self.plots_base_url = f"https://raw.githubusercontent.com/lkstrp/pypsa-validator/{self.plots_hash}/_validation-images/"
177-
178189
# Status strings for file comparison table
179190
STATUS_FILE_MISSING = " :warning: Missing"
180191
STATUS_EQUAL = ":white_check_mark: Equal"
@@ -191,6 +202,7 @@ def __init__(self):
191202

192203
@property
193204
def variables_deviation_df(self):
205+
"""Get the deviation dataframe for variables."""
194206
if self._variables_deviation_df is not None:
195207
return self._variables_deviation_df
196208
vars1 = pd.read_excel(self.dir_main / self.VARIABLES_FILE)
@@ -216,6 +228,7 @@ def variables_deviation_df(self):
216228

217229
@property
218230
def variables_plot_strings(self):
231+
"""Return list of variable plot strings."""
219232
plots = (
220233
self.variables_deviation_df.index.to_series()
221234
.apply(lambda x: re.sub(r"[ |/]", "-", x))
@@ -226,6 +239,7 @@ def variables_plot_strings(self):
226239

227240
@property
228241
def variables_comparison(self) -> str:
242+
"""Return variables comparison table."""
229243
if (
230244
not (self.dir_main / self.VARIABLES_FILE).exists()
231245
or not (self.dir_feature / self.VARIABLES_FILE).exists()
@@ -247,6 +261,7 @@ def variables_comparison(self) -> str:
247261

248262
@property
249263
def changed_variables_plots(self) -> str:
264+
"""Return plots for variables that have changed significantly."""
250265
if (
251266
not (self.dir_main / self.VARIABLES_FILE).exists()
252267
or not (self.dir_feature / self.VARIABLES_FILE).exists()
@@ -279,8 +294,8 @@ def plots_table(self) -> str:
279294
url_b = self.plots_base_url + "feature/" + plot
280295
rows.append(
281296
[
282-
f'<img src="{url_a}" alt="Image not found in results">',
283-
f'<img src="{url_b}" alt="Image not found in results">',
297+
f'<img src="{url_a}" alt="Image not available">',
298+
f'<img src="{url_b}" alt="Image not available">',
284299
]
285300
)
286301

@@ -460,20 +475,6 @@ def files_table(self) -> str:
460475
@property
461476
def body(self) -> str:
462477
"""Body text for successfull run."""
463-
464-
def create_details_block(summary: str, content: str) -> str:
465-
if content:
466-
return (
467-
f"<details>\n"
468-
f" <summary>{summary}</summary>\n"
469-
f"{content}"
470-
f"</details>\n"
471-
f"\n"
472-
f"\n"
473-
)
474-
else:
475-
return ""
476-
477478
if self.variables_comparison and self.changed_variables_plots:
478479
if self.variables_deviation_df.empty:
479480
variables_txt = (
@@ -489,7 +490,8 @@ def create_details_block(summary: str, content: str) -> str:
489490
)
490491
elif self.variables_comparison or self.changed_variables_plots:
491492
raise ValueError(
492-
"Both variables_comparison and changed_variables_plots must be set or unset."
493+
"Both variables_comparison and changed_variables_plots must be set or "
494+
"unset."
493495
)
494496
else:
495497
variables_txt = ""
@@ -508,6 +510,10 @@ def __call__(self) -> str:
508510
class RunFailed(CommentData):
509511
"""Class to generate failed run component."""
510512

513+
def __init__(self):
514+
"""Initialize failed run component."""
515+
super().__init__()
516+
511517
def body(self) -> str:
512518
"""Body text for failed run."""
513519
main_errors = self.errors("main")
@@ -539,9 +545,45 @@ def __call__(self) -> str:
539545
return self.body()
540546

541547

548+
class ModelMetrics(CommentData):
549+
"""Class to generate model metrics component."""
550+
551+
def __init__(self):
552+
"""Initialize model metrics component."""
553+
super().__init__()
554+
555+
@property
556+
def benchmark_plots(self) -> str:
557+
"""Benchmark plots."""
558+
"execution_time.png", "memory_peak.png", "memory_scatter.png"
559+
return (
560+
f'<img src="{self.plots_base_url}benchmarks/execution_time.png" '
561+
'alt="Image not available">\n'
562+
f'<img src="{self.plots_base_url}benchmarks/memory_peak.png" '
563+
'alt="Image not available">\n'
564+
f'<img src="{self.plots_base_url}benchmarks/memory_scatter.png" '
565+
'alt="Image not available">\n'
566+
)
567+
568+
def body(self) -> str:
569+
"""Body text for Model Metrics."""
570+
return (
571+
f"**Model Metrics**\n"
572+
f"{create_details_block('Benchmarks', self.benchmark_plots)}\n"
573+
)
574+
575+
def __call__(self) -> str:
576+
"""Return text for model metrics component."""
577+
return self.body()
578+
579+
542580
class Comment(CommentData):
543581
"""Class to generate pypsa validator comment for GitHub PRs."""
544582

583+
def __init__(self) -> None:
584+
"""Initialize comment class. It will put all text components together."""
585+
super().__init__()
586+
545587
@property
546588
def header(self) -> str:
547589
"""
@@ -603,7 +645,15 @@ def subtext(self) -> str:
603645
f"Last updated on `{time}`."
604646
)
605647

606-
def needed_plots(self):
648+
def dynamic_plots(self) -> str:
649+
"""
650+
Return a list of dynamic results plots needed for the comment.
651+
652+
Returns
653+
-------
654+
str: Space separated list of dynamic plots.
655+
656+
"""
607657
if self.sucessfull_run:
608658
body_sucessfull = RunSuccessfull()
609659
plots_string = " ".join(body_sucessfull.variables_plot_strings)
@@ -613,13 +663,15 @@ def needed_plots(self):
613663

614664
def __repr__(self) -> str:
615665
"""Return full formatted comment."""
666+
body_benchmarks = ModelMetrics()
616667
if self.sucessfull_run:
617668
body_sucessfull = RunSuccessfull()
618669

619670
return (
620671
f"{self.header}"
621672
f"{self.config_diff if self.git_diff_config else ''}"
622673
f"{body_sucessfull()}"
674+
f"{body_benchmarks()}"
623675
f"{self.subtext}"
624676
)
625677

@@ -630,11 +682,19 @@ def __repr__(self) -> str:
630682
f"{self.header}"
631683
f"{body_failed()}"
632684
f"{self.config_diff if self.git_diff_config else ''}"
685+
f"{body_benchmarks()}"
633686
f"{self.subtext}"
634687
)
635688

636689

637690
def main():
691+
"""
692+
Run draft comment script.
693+
694+
Command line interface for the draft comment script. Use no arguments to print the
695+
comment, or use the "plots" argument to print the dynamic plots which will be needed
696+
for the comment.
697+
"""
638698
parser = argparse.ArgumentParser(description="Process some comments.")
639699
parser.add_argument(
640700
"command", nargs="?", default="", help='Command to run, e.g., "plots".'
@@ -644,7 +704,7 @@ def main():
644704
comment = Comment()
645705

646706
if args.command == "plots":
647-
print(comment.needed_plots())
707+
print(comment.dynamic_plots())
648708

649709
else:
650710
print(comment) # noqa T201

scripts/metrics.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
"""
2-
Helper module for calculating evaluation metrics.
3-
"""
1+
"""Helper module for calculating evaluation metrics."""
42

53
import numpy as np
64
from numpy.typing import ArrayLike
@@ -64,6 +62,10 @@ def mean_absolute_percentage_error(
6462
Predicted values
6563
epsilon : float, optional (default=1e-9)
6664
Small value to avoid division by zero
65+
aggregate : bool, optional (default=True)
66+
If True, return the mean MAPE. Otherwise, return an array of individual MAPEs
67+
ignore_inf : bool, optional (default=True)
68+
If True, ignore infinite values in the calculation
6769
6870
Returns
6971
-------
@@ -111,6 +113,8 @@ def normalized_root_mean_square_error(
111113
If True, ignore infinite values in the calculation
112114
normalization : str, optional (default='min-max')
113115
Method of normalization. Options: 'mean', 'range', 'iqr', 'min-max'
116+
fill_na : float, optional (default=0)
117+
Value to replace NaN values
114118
epsilon : float, optional (default=1e-9)
115119
Small value to add to normalization factor to avoid division by zero
116120

0 commit comments

Comments
 (0)