99import re
1010from dataclasses import dataclass
1111from pathlib import Path
12- from typing import Any
1312
1413import numpy as np
1514import pandas as pd
1615from metrics import min_max_normalized_mae , normalized_root_mean_square_error
1716from numpy .typing import ArrayLike
17+ from utils import get_env_var
1818
1919
2020def 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
5039class 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
117113def 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+
141153class 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:
508510class 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+
542580class 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
637690def 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
0 commit comments