diff --git a/MTB.pfd b/MTB.pfd index 1dcc1f4..91b0860 100644 Binary files a/MTB.pfd and b/MTB.pfd differ diff --git a/MTB.pslx b/MTB.pslx index c5a78d3..41b909d 100644 --- a/MTB.pslx +++ b/MTB.pslx @@ -23,7 +23,7 @@ - + @@ -37998,7 +37998,7 @@ if ($type > 0) then endif]]> - + @@ -38043,9 +38043,9 @@ endif]]> - + - + @@ -38053,7 +38053,7 @@ endif]]> - + @@ -39256,7 +39256,7 @@ It is often necessary to scale or adjust the reference signals and mode selectio - + @@ -40352,7 +40352,7 @@ $out = mtb_return_value #ENDIF]]> - + @@ -40859,7 +40859,7 @@ $out = mtb_return_value - + @@ -42799,7 +42799,7 @@ $out = mtb_return_value - + @@ -44240,7 +44240,7 @@ $out = mtb_return_value - + @@ -44718,7 +44718,7 @@ $out = mtb_return_value - + @@ -46259,7 +46259,7 @@ $out = mtb_return_value - + @@ -49583,7 +49583,7 @@ $iqneg = $ineg * Sin(v_neg_p-i_neg_p)]]> - + @@ -50491,7 +50491,7 @@ $iqneg = $ineg * Sin(v_neg_p-i_neg_p)]]> - + @@ -59795,15 +59795,15 @@ $iqneg = $ineg * Sin(v_neg_p-i_neg_p)]]> - - + + - + diff --git a/execute_pf.py b/execute_pf.py index f62a590..2dd947d 100644 --- a/execute_pf.py +++ b/execute_pf.py @@ -79,7 +79,7 @@ def script_GetInt(script : pf.ComPython, name : str) -> Optional[int]: else: return None -def connectPF() -> Tuple[pf.Application, pf.IntPrj, pf.ComPython]: +def connectPF() -> Tuple[pf.Application, pf.IntPrj, pf.ComPython, int]: ''' Connects to the powerfactory application and returns the application, project and this script object. ''' @@ -91,6 +91,10 @@ def connectPF() -> Tuple[pf.Application, pf.IntPrj, pf.ComPython]: app.PrintInfo(f'Powerfactory application connected externally. Executable: {sys.executable}') app.PrintInfo(f'Imported powerfactory module from {pf.__file__}') + version : str = pf.__version__ + pfVersion = 2000 + int(version.split('.')[0]) + app.PrintInfo(f'Powerfactory version registred: {pfVersion}') + project : Optional[pf.IntPrj] = app.GetActiveProject() #type: ignore if DEBUG: @@ -106,7 +110,7 @@ def connectPF() -> Tuple[pf.Application, pf.IntPrj, pf.ComPython]: thisScript : pf.ComPython = networkData.SearchObject('MTB\\MTB\\execute.ComPython') #type: ignore assert thisScript is not None - return app, project, thisScript + return app, project, thisScript, pfVersion def resetProjectUnits(project : pf.IntPrj) -> None: ''' @@ -150,81 +154,32 @@ def setupResFiles(app : pf.Application, script : pf.ComPython, root : pf.DataObj elmRes.AddVariable(measurementBlock, 's:ppoc_pu') elmRes.AddVariable(measurementBlock, 's:qpoc_pu') - mtb_s_pref_pu = root.SearchObject('mtb_s_pref_pu.ElmDsl') - assert mtb_s_pref_pu is not None - elmRes.AddVariable(mtb_s_pref_pu, 's:yo') - - mtb_s_qref = root.SearchObject('mtb_s_qref.ElmDsl') - assert mtb_s_qref is not None - elmRes.AddVariable(mtb_s_qref, 's:yo') - - mtb_s_qref_q_pu = root.SearchObject('mtb_s_qref_q_pu.ElmDsl') - assert mtb_s_qref_q_pu is not None - elmRes.AddVariable(mtb_s_qref_q_pu, 's:yo') - - mtb_s_qref_qu_pu = root.SearchObject('mtb_s_qref_qu_pu.ElmDsl') - assert mtb_s_qref_qu_pu is not None - elmRes.AddVariable(mtb_s_qref_qu_pu, 's:yo') - - mtb_s_qref_pf = root.SearchObject('mtb_s_qref_pf.ElmDsl') - assert mtb_s_qref_pf is not None - elmRes.AddVariable(mtb_s_qref_pf, 's:yo') - - mtb_s_qref_3 = root.SearchObject('mtb_s_qref_3.ElmDsl') - assert mtb_s_qref_3 is not None - elmRes.AddVariable(mtb_s_qref_3, 's:yo') - - mtb_s_qref_4 = root.SearchObject('mtb_s_qref_4.ElmDsl') - assert mtb_s_qref_4 is not None - elmRes.AddVariable(mtb_s_qref_4, 's:yo') - - mtb_s_qref_5 = root.SearchObject('mtb_s_qref_5.ElmDsl') - assert mtb_s_qref_5 is not None - elmRes.AddVariable(mtb_s_qref_5, 's:yo') - - mtb_s_qref_6 = root.SearchObject('mtb_s_qref_6.ElmDsl') - assert mtb_s_qref_6 is not None - elmRes.AddVariable(mtb_s_qref_6, 's:yo') - - mtb_s_1 = root.SearchObject('mtb_s_1.ElmDsl') - assert mtb_s_1 is not None - elmRes.AddVariable(mtb_s_1, 's:yo') - - mtb_s_2 = root.SearchObject('mtb_s_2.ElmDsl') - assert mtb_s_2 is not None - elmRes.AddVariable(mtb_s_2, 's:yo') - - mtb_s_3 = root.SearchObject('mtb_s_3.ElmDsl') - assert mtb_s_3 is not None - elmRes.AddVariable(mtb_s_3, 's:yo') - - mtb_s_4 = root.SearchObject('mtb_s_4.ElmDsl') - assert mtb_s_4 is not None - elmRes.AddVariable(mtb_s_4, 's:yo') - - mtb_s_5 = root.SearchObject('mtb_s_5.ElmDsl') - assert mtb_s_5 is not None - elmRes.AddVariable(mtb_s_5, 's:yo') - - mtb_s_6 = root.SearchObject('mtb_s_6.ElmDsl') - assert mtb_s_6 is not None - elmRes.AddVariable(mtb_s_6, 's:yo') - - mtb_s_7 = root.SearchObject('mtb_s_7.ElmDsl') - assert mtb_s_7 is not None - elmRes.AddVariable(mtb_s_7, 's:yo') - - mtb_s_8 = root.SearchObject('mtb_s_8.ElmDsl') - assert mtb_s_8 is not None - elmRes.AddVariable(mtb_s_8, 's:yo') - - mtb_s_9 = root.SearchObject('mtb_s_9.ElmDsl') - assert mtb_s_9 is not None - elmRes.AddVariable(mtb_s_9, 's:yo') - - mtb_s_10 = root.SearchObject('mtb_s_10.ElmDsl') - assert mtb_s_10 is not None - elmRes.AddVariable(mtb_s_10, 's:yo') + signals = [ + 'mtb_s_pref_pu.ElmDsl', + 'mtb_s_qref.ElmDsl', + 'mtb_s_qref_q_pu.ElmDsl', + 'mtb_s_qref_qu_pu.ElmDsl', + 'mtb_s_qref_pf.ElmDsl', + 'mtb_s_qref_3.ElmDsl', + 'mtb_s_qref_4.ElmDsl', + 'mtb_s_qref_5.ElmDsl', + 'mtb_s_qref_6.ElmDsl', + 'mtb_s_1.ElmDsl', + 'mtb_s_2.ElmDsl', + 'mtb_s_3.ElmDsl', + 'mtb_s_4.ElmDsl', + 'mtb_s_5.ElmDsl', + 'mtb_s_6.ElmDsl', + 'mtb_s_7.ElmDsl', + 'mtb_s_8.ElmDsl', + 'mtb_s_9.ElmDsl', + 'mtb_s_10.ElmDsl' + ] + + for signal in signals: + signalObj = root.SearchObject(signal) + assert signalObj is not None + elmRes.AddVariable(signalObj, 's:yo') # Include measurement objects and set alias for i in range(1, 100): @@ -381,7 +336,7 @@ def convertToConfStr(param : str, signal : str) -> str: def main(): # Connect to Powerfactory - app, project, thisScript = connectPF() + app, project, thisScript, pfVersion = connectPF() # Check if any studycase is active currentStudyCase : Optional[pf.IntCase] = app.GetActiveStudyCase() #type: ignore @@ -524,12 +479,15 @@ def main(): if onlySetup == 0: taskAuto.Execute() - for studycase in studycases: - studycase.Activate() - setupPlots(app, root) - app.WriteChangesToDb() - studycase.Deactivate() - app.WriteChangesToDb() + if pfVersion >= 2024: + for studycase in studycases: + studycase.Activate() + setupPlots(app, root) + app.WriteChangesToDb() + studycase.Deactivate() + app.WriteChangesToDb() + else: + app.PrintWarn('Plot setup not supported for PowerFactory versions older than 2024.') # Create post run backup postBackup = script_GetInt(thisScript, 'Post_run_backup') diff --git a/execute_pscad.py b/execute_pscad.py index 4becb7c..c1ed9f5 100644 --- a/execute_pscad.py +++ b/execute_pscad.py @@ -78,14 +78,14 @@ def outToCsv(srcPath : str, dstPath : str): open(dstPath, 'w') as csv: csv.writelines(','.join(line.split()) +'\n' for line in out) -def moveFiles(srcPath : str, dstPath : str, types : List[str]) -> None: +def moveFiles(srcPath : str, dstPath : str, types : List[str], suffix : str = '') -> None: ''' Moves files of the specified types from srcPath to dstPath. ''' for file in os.listdir(srcPath): _, typ = os.path.splitext(file) if typ in types: - shutil.move(os.path.join(srcPath, file), os.path.join(dstPath, file)) + shutil.move(os.path.join(srcPath, file), os.path.join(dstPath, file + suffix)) def taskIdToRank(csvPath : str, projectName : str, emtCases : List[cs.Case]): ''' @@ -94,17 +94,20 @@ def taskIdToRank(csvPath : str, projectName : str, emtCases : List[cs.Case]): for file in os.listdir(csvPath): _, fileName = os.path.split(file) root, typ = os.path.splitext(fileName) - if typ == '.csv' or typ == '.inf': - parts = root.split('_') - if len(parts) > 1 and parts[0] == projectName and parts[1].isnumeric(): - taskId = int(parts[1]) + if typ == '.csv_taskid' or typ == '.inf_taskid' and root.startswith(projectName + '_'): + suffix = root[len(projectName) + 1:] + parts = suffix.split('_') + if len(parts) > 0 and parts[0].isnumeric(): + taskId = int(parts[0]) if taskId - 1 < len(emtCases): - parts[1] = str(emtCases[int(parts[1]) - 1].rank) - newName = '_'.join(parts) - print(f'Renaming {fileName} to {newName + typ}') - os.rename(os.path.join(csvPath, fileName), os.path.join(csvPath, newName + typ)) + parts[0] = str(emtCases[taskId - 1].rank) + newName = projectName + '_' + '_'.join(parts) + typ.replace('_taskid', '') + print(f'Renaming {fileName} to {newName}') + os.rename(os.path.join(csvPath, fileName), os.path.join(csvPath, newName)) else: print(f'WARNING: {fileName} has a task ID that is out of bounds. Ignoring file.') + else: + print(f'WARNING: {fileName} has an invalid task ID. Ignoring file.') def cleanUpOutFiles(buildPath : str, projectName : str) -> str: ''' @@ -136,7 +139,7 @@ def cleanUpOutFiles(buildPath : str, projectName : str) -> str: #Move .csv and .inf files away from build folder into output folder csvFolder = os.path.join(outputFolder, resultsFolder) os.mkdir(csvFolder) - moveFiles(buildPath, csvFolder, ['.csv', '.inf']) + moveFiles(buildPath, csvFolder, ['.csv', '.inf'], '_taskid') #Move .out file away from build folder outFolder = os.path.join(buildPath, resultsFolder) @@ -245,7 +248,7 @@ def main(): print() taskIdToRank(csvFolder, plantSettings.Projectname, emtCases) - print('execute.py finished at:', datetime.now().strftime('%Y-%m-%d %H:%M:%S')) + print('execute.py finished at: ', datetime.now().strftime('%m-%d %H:%M:%S')) if __name__ == '__main__': main() diff --git a/plotter/config.ini b/plotter/config.ini index e2c393f..52c0280 100644 --- a/plotter/config.ini +++ b/plotter/config.ini @@ -1,14 +1,15 @@ [config] resultsDir = results -columns = 3 genHTML = True -genJPEG = True +genImage = True +imageFormat = png +htmlColumns = 1 +imageColumns = 3 threads = 10 pfFlatTime = 0.1 pscadInitTime = 3.5 optionalCasesheet = ../testcases.xlsx [Simulation data paths] -EMT = C:\EMT -RMS = C:\RMS - +EMT = C:\Users\MKT.ENERGINET\Desktop\MTB\MTB_04092024154118 +RMS = C:\Users\MKT.ENERGINET\Desktop\MTB\export diff --git a/plotter/down_sampling_method.py b/plotter/down_sampling_method.py index b95dab0..5f168b4 100644 --- a/plotter/down_sampling_method.py +++ b/plotter/down_sampling_method.py @@ -1,13 +1,12 @@ from enum import Enum - class DownSamplingMethod(Enum): GRADIENT = 1 AMOUNT = 2 NO_DOWN_SAMPLING = 3 @classmethod - def from_string(cls, string): + def from_string(cls, string : str): try: return cls[string.upper()] except KeyError: diff --git a/plotter/figureSetup.csv b/plotter/figureSetup.csv index b1d70ff..6893d7a 100644 --- a/plotter/figureSetup.csv +++ b/plotter/figureSetup.csv @@ -1,13 +1,13 @@ figure;title;units;emt_signal_1;emt_signal_2;emt_signal_3;rms_signal_1;rms_signal_2;rms_signal_3;down_sampling_method;gradient_threshold;include_in_case;exclude_in_case -1;Vpp;pu;meas_Vab_pu;meas_Vbc_pu;meas_Vca_pu;##meas\s:Vab_pu;##meas\s:Vbc_pu;##meas\s:Vbc_pu;gradient;0.5;; -2;Vpg;pu;meas_Vag_pu;meas_Vbg_pu;meas_Vcg_pu;##meas\s:Vag_pu;##meas\s:Vbg_pu;##meas\s:Vbg_pu;gradient;0.5;; -3;Vseq;pu;fft_pos_Vmag_pu;fft_neg_Vmag_pu;;##meas\s:pos_Vmag_pu;##meas\s:neg_Vmag_pu;;gradient;0.5;; -4;Itotal;pu;meas_Ia_pu;meas_Ib_pu;meas_Ic_pu;##meas\s:Ia_pu;##meas\s:Ib_pu;##meas\s:Ic_pu;gradient;0.5;; -5;Iactive;pu;fft_pos_Id_pu;fft_neg_Id_pu;;##meas\s:pos_Id_pu;##meas\s:neg_Id_pu;;gradient;0.5;; -6;Ireactive;pu;fft_pos_Iq_pu;fft_neg_Iq_pu;;##meas\s:pos_Iq_pu;##meas\s:neg_Iq_pu;;gradient;0.5;; -7;Ppoc;pu;meas_ppoc_pu;mtb_s_pref_pu;;##meas\s:ppoc_pu;;;gradient;0.5;; -8;Qpoc;pu;meas_qpoc_pu;mtb_s_qref_pu;;##meas\s:qpoc_pu;;;gradient;0.5;; -9;F;Hz;pll_f_hz;;;##meas\s:f_hz;;;gradient;0.5;; +1;Vpp;pu;meas_Vab_pu;meas_Vbc_pu;meas_Vca_pu;meas\s:Vab_pu;meas\s:Vbc_pu;meas\s:Vca_pu;gradient;0.5;; +2;Vpg;pu;meas_Vag_pu;meas_Vbg_pu;meas_Vcg_pu;meas\s:Vag_pu;meas\s:Vbg_pu;meas\s:Vcg_pu;gradient;0.5;; +3;Vseq;pu;fft_pos_Vmag_pu;fft_neg_Vmag_pu;;meas\s:pos_Vmag_pu;meas\s:neg_Vmag_pu;;gradient;0.5;; +4;Itotal;pu;meas_Ia_pu;meas_Ib_pu;meas_Ic_pu;meas\s:Ia_pu;meas\s:Ib_pu;meas\s:Ic_pu;gradient;0.5;; +5;Iactive;pu;fft_pos_Id_pu;fft_neg_Id_pu;;meas\s:pos_Id_pu;meas\s:neg_Id_pu;;gradient;0.5;; +6;Ireactive;pu;fft_pos_Iq_pu;fft_neg_Iq_pu;;meas\s:pos_Iq_pu;meas\s:neg_Iq_pu;;gradient;0.5;; +7;Ppoc;pu;meas_ppoc_pu;mtb_s_pref_pu;;meas\s:ppoc_pu;;;gradient;0.5;; +8;Qpoc;pu;meas_qpoc_pu;mtb_s_qref;;meas\s:qpoc_pu;;;gradient;0.5;; +9;F;Hz;pll_f_hz;;;meas\s:f_hz;;;gradient;0.5;; 10;Id_pll;pu;pll_pos_Id_pu;pll_neg_Id_pu;;;;;gradient;0.5;; 11;Iq_pll;pu;pll_pos_Iq_pu;pll_neg_Iq_pu;;;;;gradient;0.5;; -12;Terminal;pu;unit_fft_pos_Id_pu;unit_fft_pos_Iq_pu;unit_fft_pos_Vmag_pu;##Unit_1\m:i1P:bus1 in p.u.;##Unit_1\m:i1Q:bus1 in p.u.;##Unit_1\n:u1:bus1 in p.u.;gradient;0.5;; \ No newline at end of file +12;Terminal;pu;unit_fft_pos_Id_pu;unit_fft_pos_Iq_pu;unit_fft_pos_Vmag_pu;Unit_1\m:i1P:bus1 in p.u.;Unit_1\m:i1Q:bus1 in p.u.;Unit_1\n:u1:bus1 in p.u.;gradient;0.5;; \ No newline at end of file diff --git a/plotter/plotter.py b/plotter/plotter.py index 758c9f5..3a7806d 100644 --- a/plotter/plotter.py +++ b/plotter/plotter.py @@ -47,13 +47,6 @@ class ResultType(Enum): RMS = 0 EMT = 1 - @classmethod - def from_string(cls, string : str): - try: - return cls[string.upper()] - except KeyError: - raise ValueError(f'{string} is not a valid {cls.__name__}') - class Figure: def __init__(self, id : int, @@ -85,26 +78,34 @@ def __init__(self, self.exclude_in_case : List[int] = exclude_in_case class Result: - def __init__(self, typ : ResultType, rank : int, name : str, bulkname : str, fullpath : str, group : str) -> None: + def __init__(self, typ : ResultType, rank : int, projectName : str, bulkname : str, fullpath : str, group : str) -> None: self.typ = typ self.rank = rank - self.name = name + self.projectName = projectName self.bulkname = bulkname self.fullpath = fullpath self.group = group + self.shorthand = f'{group}\\{projectName}' class ReadConfig: def __init__(self) -> None: cp = ConfigParser() cp.read('config.ini') parsedConf = cp['config'] - self.resultsDir = parsedConf['resultsDir'] - self.columns = parsedConf.getint('columns') + self.resultsDir = parsedConf['resultsDir'] self.genHTML = parsedConf.getboolean('genHTML') - self.genJPEG = parsedConf.getboolean('genJPEG') + self.genImage = parsedConf.getboolean('genImage') + self.htmlColumns = parsedConf.getint('htmlColumns') + assert self.htmlColumns > 0 or not self.genHTML + self.imageColumns = parsedConf.getint('imageColumns') + assert self.imageColumns > 0 or not self.genImage + self.imageFormat = parsedConf['imageFormat'] self.threads = parsedConf.getint('threads') + assert self.threads > 0 self.pfFlatTIme = parsedConf.getfloat('pfFlatTime') + assert self.pfFlatTIme >= 0.1 self.pscadInitTime = parsedConf.getfloat('pscadInitTime') + assert self.pscadInitTime >= 1.0 self.optionalCasesheet = parsedConf['optionalCasesheet'] self.simDataDirs : List[Tuple[str, str]] = list() simPaths = cp.items('Simulation data paths') @@ -188,7 +189,7 @@ def readFigureSetup(filePath : str) -> Dict[int, List[Figure]]: for inc in fig.include_in_case: if not inc in figDict.keys(): figDict[inc] = defaultSetup.copy() - defaultSetup.append(fig) + figDict[inc].append(fig) else: for exc in fig.exclude_in_case: if not exc in figDict.keys(): @@ -204,19 +205,19 @@ def idFile(filePath: str) -> Tuple[Union[ResultType, None], Union[int, None], Un match = re.match(r'^(\w+?)_([0-9]+).(inf|csv)$', fileName.lower()) if match: rank = int(match.group(2)) - name = match.group(1) + projectName = match.group(1) bulkName = join(path, match.group(1)) fullpath = filePath with open(filePath, 'r') as file: firstLine = file.readline() if match.group(3) == 'inf' and firstLine.startswith('PGB(1)'): fileType = ResultType.EMT - return (fileType, rank, name, bulkName, fullpath) + return (fileType, rank, projectName, bulkName, fullpath) elif match.group(3) == 'csv': secondLine = file.readline() if secondLine.startswith(r'"b:tnow in s"'): fileType = ResultType.RMS - return (fileType, rank, name, bulkName, fullpath) + return (fileType, rank, projectName, bulkName, fullpath) return (None, None, None, None, None) def mapResultFiles(config : ReadConfig) -> Dict[int, List[Result]]: @@ -233,14 +234,16 @@ def mapResultFiles(config : ReadConfig) -> Dict[int, List[Result]]: for file in files: group = file[0] fullpath = file[1] - typ, rank, name, bulkName, fullpath = idFile(fullpath) + typ, rank, projectName, bulkName, fullpath = idFile(fullpath) + if typ is None: continue assert rank is not None - assert name is not None + assert projectName is not None assert bulkName is not None assert fullpath is not None - newResult = Result(typ, rank, name, bulkName, fullpath, group) + + newResult = Result(typ, rank, projectName, bulkName, fullpath, group) if rank in results.keys(): results[rank].append(newResult) @@ -305,11 +308,38 @@ def loadEMT(infFile : str) -> pd.DataFrame: print(f"Loaded {infFile}, length = {df['time'].iloc[-1]}s") #type: ignore return df -def addResults( plotlyFigure : go.Figure, +def colorMap(results: Dict[int, List[Result]]) -> Dict[str, List[str]]: + ''' + Select colors for the given projects. Return a dictionary with the project name as key and a list of colors as value. + ''' + colors = ['#e6194B', '#3cb44b', '#ffe119', '#4363d8', '#f58231', '#911eb4', '#42d4f4', '#f032e6', '#bfef45', '#fabed4', '#469990', '#dcbeff', '#9A6324', '#fffac8', '#800000', '#aaffc3', '#808000', '#ffd8b1', '#000075', '#a9a9a9', '#000000'] + + projects : Set[str] = set() + + for rank in results.keys(): + for result in results[rank]: + projects.add(result.shorthand) + + cMap : Dict[str, List[str]] = dict() + + if len(list(projects)) > 2: + i = 0 + for p in list(projects): + cMap[p] = [colors[i % len(colors)]] * 3 + i += 1 + return cMap + else: + i = 0 + for p in list(projects): + cMap[p] = colors[i:i+3] + i += 3 + return cMap + +def addResults( plots : List[go.Figure], typ: ResultType, data: pd.DataFrame, figures: List[Figure], - name: str, + resultName: str, file: str, #Only for error messages colors: Dict[str, List[str]], nColumns: int, @@ -319,15 +349,30 @@ def addResults( plotlyFigure : go.Figure, Add result to plot. ''' + assert nColumns > 0 + + if nColumns > 1: + plotlyFigure = plots[0] + else: + assert len(plots) == len(figures) + + rowPos = 1 + colPos = 1 + fi = -1 for figure in figures: + fi += 1 + + if nColumns == 1: + plotlyFigure = plots[fi] + else: + rowPos = (fi // nColumns) + 1 + colPos = (fi % nColumns) + 1 + downsampling_method = figure.down_sampling_method - rowPos = (figure.id - 1) // nColumns + 1 - colPos = (figure.id - 1) % nColumns + 1 - traces = 0 for sig in range(1,4): signalKey = typ.name.lower() - rawSigName = getattr(figure, f'{signalKey}_signal_{sig}') + rawSigName : str = getattr(figure, f'{signalKey}_signal_{sig}') if typ == ResultType.RMS: while rawSigName.startswith('#'): @@ -341,6 +386,8 @@ def addResults( plotlyFigure : go.Figure, else: sigColumn = rawSigName + displayName = f'{resultName}:{rawSigName.split(" ")[0]}' + timeColName = 'time' if typ == ResultType.EMT else data.columns[0] timeoffset = pfFlatTIme if typ == ResultType.RMS else pscadInitTime @@ -351,27 +398,28 @@ def addResults( plotlyFigure : go.Figure, x_value, y_value = sampling_functions.downsample_based_on_gradient(x_value, y_value, figure.gradient_threshold) #type: ignore elif downsampling_method == DownSamplingMethod.AMOUNT: x_value, y_value = sampling_functions.down_sample(x_value, y_value) #type: ignore + plotlyFigure.add_trace( #type: ignore go.Scatter( x=x_value, y=y_value, - line_color=colors[name][traces], - name=f"{name}:{rawSigName}", - legendgroup=name, + line_color=colors[resultName][traces], + name=displayName, + legendgroup=resultName if nColumns > 1 else displayName, showlegend=True ), row=rowPos, col=colPos ) traces += 1 elif sigColumn != '': - print(f"Signal '{rawSigName}' not recognized in resultfile '{file}'") + print(f'Signal "{rawSigName}" not recognized in resultfile: {file}') plotlyFigure.add_trace( #type: ignore go.Scatter( x=None, y=None, - line_color=colors[name][traces], - name=f"{name}:{rawSigName} (Unknown)", - legendgroup=name, + line_color=colors[resultName][traces], + name=f'{displayName} (Unknown)', + legendgroup=resultName if nColumns > 1 else displayName, showlegend=True ), row=rowPos, col=colPos @@ -382,30 +430,16 @@ def addResults( plotlyFigure : go.Figure, title_text='Time[s]', row=rowPos, col=colPos ) + if nColumns == 1: + yaxisTitle = f'[{figure.units}]' + else: + yaxisTitle = f'{figure.title}[{figure.units}]' + plotlyFigure.update_yaxes( #type: ignore - title_text=f"{figure.title}[{figure.units}]", + title_text=yaxisTitle , row=rowPos, col=colPos ) -def colorMap(names: List[str]) -> Dict[str, List[str]]: - ''' - Select colors for the given projects. Return a dictionary with the project name as key and a list of colors as value. - ''' - colors = ['#e6194B', '#3cb44b', '#ffe119', '#4363d8', '#f58231', '#911eb4', '#42d4f4', '#f032e6', '#bfef45', '#fabed4', '#469990', '#dcbeff', '#9A6324', '#fffac8', '#800000', '#aaffc3', '#808000', '#ffd8b1', '#000075', '#a9a9a9', '#000000'] - cMap : Dict[str, List[str]] = dict() - if len(names) > 2: - i = 0 - for p in names: - cMap[p] = [colors[i % len(colors)]] * 3 - i += 1 - return cMap - else: - i = 0 - for p in names: - cMap[p] = colors[i:i+3] - i += 3 - return cMap - def drawPlot( rank : int, resultDict : Dict[int, List[Result]], figureDict : Dict[int, List[Figure]], @@ -414,7 +448,7 @@ def drawPlot( rank : int, config : ReadConfig): ''' - Draws plot. + Draws plots for html and static image export. ''' print(f'Drawing plot for rank {rank}.') @@ -426,11 +460,37 @@ def drawPlot( rank : int, return figurePath = join(config.resultsDir, str(rank)) - nrows = ceil(len(figureList)/config.columns) - plot = make_subplots(rows = nrows, cols = config.columns) - plot.update_layout(title_text = caseDict[rank]) #type: ignore + htmlPlots : List[go.Figure] = list() + imagePlots : List[go.Figure] = list() + + lst : List[Tuple[int, List[go.Figure]]]= [] + + if config.genHTML: + lst.append((config.htmlColumns, htmlPlots)) + if config.genImage: + lst.append((config.imageColumns, imagePlots)) + + for columnNr, plotList in lst: + if columnNr == 1: + for fig in figureList: + plotList.append(make_subplots()) + plotList[-1].update_layout(title_text = fig.title, height = 500, #type: ignore + legend=dict( + orientation="h", + yanchor="top", + y=1.22, + xanchor="left", + x = 0.12, + )) + elif columnNr > 1: + plotList.append(make_subplots(rows = ceil(len(figureList)/columnNr), cols = columnNr)) + plotList[-1].update_layout(height = 500 * ceil(len(figureList)/columnNr)) #type: ignore + if plotList == imagePlots: + plotList[-1].update_layout(title_text = caseDict[rank]) #type: ignore + + for result in resultList: if result.typ == ResultType.RMS: resultData : pd.DataFrame = pd.read_csv(result.fullpath, sep=';',decimal=',',header=[0,1]) #type: ignore @@ -439,41 +499,59 @@ def drawPlot( rank : int, else: continue - addResults(plot, result.typ, resultData, figureList, result.group, result.fullpath, colorMap, config.columns, config.pfFlatTIme, config.pscadInitTime) - + if config.genHTML: + addResults(htmlPlots, result.typ, resultData, figureList, result.shorthand, result.fullpath, colorMap, config.htmlColumns, config.pfFlatTIme, config.pscadInitTime) + + if config.genImage: + addResults(imagePlots, result.typ, resultData, figureList, result.shorthand, result.fullpath, colorMap, config.imageColumns, config.pfFlatTIme, config.pscadInitTime) + if config.genHTML: - create_html(plot, figurePath, config) + create_html(htmlPlots, figurePath, caseDict[rank], config) + print(f'Exported plot for rank {rank} to {figurePath}.html') - if config.genJPEG: - plot.write_image('{}.jpeg'.format(figurePath), width=500*nrows, height=500*config.columns) #type: ignore - plot.write_image('{}.png'.format(figurePath), width=500*nrows, height=500*config.columns) #type: ignore + if config.genImage: + imagePlots[0].write_image(f'{figurePath}.{config.imageFormat}', height = 500 * ceil(len(figureList)/columnNr), width = 500*config.imageColumns) #type: ignore + print(f'Exported plot for rank {rank} to {figurePath}.{config.imageFormat}') print(f'Plot for rank {rank} done.') -def create_html(plotlyFigure : go.Figure, path : str, config : ReadConfig): - - additional_text = """ -
""" +def create_html(plots : List[go.Figure], path : str, title : str, config : ReadConfig) -> None: + source_list = '
' + source_list += '

Source data:

' for group in config.simDataDirs: - additional_text += f"

{group[0]} = {group[1]}

" + source_list += f'

{group[0]} = {group[1]}

' + + source_list += '
' + + if config.htmlColumns == 1: + figur_links = '
' + figur_links += '

Figures:

' + for p in plots: + plot_title : str = p['layout']['title']['text'] #type: ignore + figur_links += f'{plot_title} ' + + figur_links += '
' + else: + figur_links = '' + + html_content = '

' + title + '

' - additional_text += """

Generated with Energinets Model Testbench

""" + html_content += source_list + html_content += figur_links - html_content = plotlyFigure.to_html(full_html=False, include_plotlyjs='cdn') #type: ignore + for p in plots: + plot_title : str = p['layout']['title']['text'] #type: ignore + html_content += f'
' + p.to_html(full_html=False, include_plotlyjs='cdn') + '
'#type: ignore - full_html_content = f""" + full_html_content = f''' - - - - {html_content} - {additional_text} +

Generated with Energinets Model Testbench

- """ + ''' with open(f'{path}.html', 'w') as file: file.write(full_html_content) @@ -503,17 +581,19 @@ def readCasesheet(casesheetPath : str) -> Dict[int, str]: def main() -> None: config = ReadConfig() - resultDict = mapResultFiles(config) - figureDict = readFigureSetup('figureSetup.csv') - caseDict = readCasesheet(config.optionalCasesheet) + print('Starting plotter main thread') - uniqueGroups : Set[str] = set() + #Output config + print('Configuration:') + for setting in config.__dict__: + print(f'\t{setting}: {config.__dict__[setting]}') - for rank in resultDict.keys(): - for result in resultDict[rank]: - uniqueGroups.add(result.group) + print() - colorSchemeMap = colorMap(list(uniqueGroups)) + resultDict = mapResultFiles(config) + figureDict = readFigureSetup('figureSetup.csv') + caseDict = readCasesheet(config.optionalCasesheet) + colorSchemeMap = colorMap(resultDict) if not exists(config.resultsDir): makedirs(config.resultsDir) @@ -534,15 +614,19 @@ def main() -> None: while len(sched) > 0: for t in inProg: if not t.is_alive(): + print(f'Thread {t.native_id} finished') inProg.remove(t) while len(inProg) < config.threads and len(sched) > 0: nextThread = sched.pop() nextThread.start() + print(f'Started thread {nextThread.native_id}') inProg.append(nextThread) time.sleep(0.5) + print('Finished plotter main thread') + if __name__ == "__main__": main() diff --git a/powerfactory.pyi b/powerfactory.pyi index 558febc..0a0b14b 100644 --- a/powerfactory.pyi +++ b/powerfactory.pyi @@ -599,3 +599,5 @@ class PltLinebarplot(DataObject): class PltDataseries(DataObject) def AddCurve(self, __element : DataObject, __varname : str, __datasource : Optional[DataObject] = None) -> None: ... + +__version__ : str = '' \ No newline at end of file