-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathexecute_pscad.py
More file actions
409 lines (338 loc) · 15.3 KB
/
execute_pscad.py
File metadata and controls
409 lines (338 loc) · 15.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
'''
Executes the Powerplant model testbench in PSCAD.
'''
from __future__ import annotations
import os
import sys
try:
LOG_FILE = open('execute_pscad.log', 'w')
except:
print('Failed to open log file. Logging to file disabled.')
LOG_FILE = None #type: ignore
def print(*args): #type: ignore
'''
Overwrites the print function to also write to a log file.
'''
outputString = ''.join(map(str, args)) + '\n' #type: ignore
sys.stdout.write(outputString)
if LOG_FILE:
LOG_FILE.write(outputString)
LOG_FILE.flush()
if __name__ == '__main__':
print('Python', sys.version)
#Ensure right working directory
executePath = os.path.abspath(__file__)
executeFolder = os.path.dirname(executePath)
os.chdir(executeFolder)
if not executeFolder in sys.path:
sys.path.append(executeFolder)
print(f'CWD: {executeFolder}')
print('sys.path:')
for path in sys.path:
if path != '':
print(f'\t{path}')
from configparser import ConfigParser
config = ConfigParser()
config.read('config.ini')
sheetPath = config.get('General', 'Casesheet path', fallback='testcases.xlsx').strip()
exportPath = config.get('General', 'Export folder', fallback='export').strip()
pythonPath = config.get('Python', 'Python path', fallback='').strip()
volley = config.getint('PSCAD', 'Volley', fallback=16)
traceAffinity = config.getboolean('PSCAD', 'Tracing', fallback=False)
stateAnimation = config.getboolean('PSCAD', 'State animation', fallback=False)
onlyInUseChannels = config.getboolean('PSCAD', 'Only in use channels', fallback=True)
disableAllUnusedPGBs = config.getboolean('PSCAD', 'Disable all unused PGBs', fallback=True)
fortranVersion = config.get('PSCAD', 'Fortran version').strip()
workspacePath = config.get('PSCAD', 'Workspace').strip()
if pythonPath:
sys.path.append(pythonPath)
from datetime import datetime
import shutil
import psutil #type: ignore
from typing import List, Optional
import pandas as pd
import warnings
import sim_interface as si
import case_setup as cs
from pscad_update_ums import updateUMs
from pscad_synchronize_pgbs import getSignalsFromFigureSetup, validateFigureSetupAgainstWorkspace, synchronizePGBsInProject
# To suppress openpyxl warning messages
warnings.filterwarnings("ignore", category=UserWarning, module="openpyxl")
try:
import mhi.pscad
except ImportError:
print("Could not import mhi.pscad. Make sure PSCAD Automation Library is installed and available in your Python environment.")
sys.exit(1)
def connectPSCAD() -> mhi.pscad.PSCAD:
pid = os.getpid()
ports = [con.laddr.port for con in psutil.net_connections() if con.status == psutil.CONN_LISTEN and con.pid == pid] #type: ignore
if len(ports) == 0: #type: ignore
print('No PSCAD listening ports found!\n')
return None
elif len(ports) > 1: #type: ignore
print('WARNING: Multiple PSCAD listening ports found. Using the first one.')
try:
pscad = mhi.pscad.connect(port = ports[0]) #type: ignore
except (AttributeError, Exception) as e:
print(f"Connection failed: {e}. Proceeding to launch new instance.\n")
return None
return pscad
def startPSCAD():
# Launch PSCAD
print('Starting PSCAD v5.0.2\n')
pscad = mhi.pscad.launch(version='5.0.2',
silence=True,
splash=False,
minimize=True,
load_user_profile=False)
if pscad:
## PSCAD Licence management
# Release certificate if already exists
pscad.release_certificate()
# Lets try to get a license, query server for list of available licenses.
# Grab the first license found and use the certificate to license PSCAD
if(pscad.logged_in() == True):
certs = pscad.get_available_certificates()
if len(certs) > 0:
# finding a license with open instances
for cert in list(certs.values()):
if cert.meets([('EMTDC Instances', volley)]):
print('Acquiring Certificate Now! : %s', str(cert))
pscad.get_certificate(cert)
print('PSCAD should have a license now\n')
break
if pscad.licensed() == False:
print(f'All PSCAD Licenses that meet the volley requirement of {volley}, are in use right now!')
else:
print('No certificate licenses available on server')
print('Starting PSCAD in unlicensed mode')
else:
print('You must log in (top right on PSCAD) and then restart script')
## Set some PSCAD settings - can only be done with a valid license
pscad_options = {'start_page_startup':False,
'cl_use_advanced': True}
pscad.settings(pscad_options)
# Open PSCAD workspace
pscad.load(workspacePath)
# Get valid Fortran comilers for this machine
available_fortrans = pscad.setting_range('fortran_version')
valid_fortrans = [f for f in available_fortrans if 'GFortran' not in f]
if fortranVersion == '' or 'GFortran' in fortranVersion or fortranVersion not in valid_fortrans:
print('A valid Fortran compiler other than GFortran has to be specified when running from the command line')
print('Valid options for this machine:')
for valid_fortran in valid_fortrans:
print(f' * {valid_fortran}')
print()
exitPSCAD(pscad)
sys.exit(1)
else:
print(f'Fortran version set to {fortranVersion}')
pscad.settings({'fortran_version': fortranVersion})
return pscad
else:
print('PSCAD could not be started')
return
def exitPSCAD(pscad):
print('Releasing All Certificate...')
pscad.release_all_certificates()
print('Quiting PSCAD...')
pscad.quit()
print('Done.')
def outToCsv(srcPath : str, dstPath : str):
"""
Converts PSCAD .out file into .csv file
"""
with open(srcPath) as out, \
open(dstPath, 'w') as csv:
csv.writelines(','.join(line.split()) +'\n' for line in out)
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 + suffix))
def taskIdToRank(psoutFolder : str, projectName : str, emtCases : List[cs.Case], rank: int):
'''
Changes task ID to rank of the .psout files in psoutFolder.
'''
for file in os.listdir(psoutFolder):
_, fileName = os.path.split(file)
root, typ = os.path.splitext(fileName)
if rank is None:
if typ == '.psout_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[0] = str(emtCases[taskId - 1].rank)
newName = projectName + '_' + '_'.join(parts) + typ.replace('_taskid', '')
print(f'Renaming {fileName} to {newName}')
os.rename(os.path.join(psoutFolder, fileName), os.path.join(psoutFolder, 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.')
else:
if typ == '.psout_taskid':
newName = f'{projectName}_{rank}.psout'
else:
print(f'WARNING: {fileName} is of unknown type. Ignoring file.')
continue
print(f'Renaming {fileName} to {newName}')
os.rename(os.path.join(psoutFolder, fileName), os.path.join(psoutFolder, newName))
def cleanUpPsoutFiles(buildPath : str, exportPath : str, projectName : str) -> str:
'''
Cleans up the build folder by moving .psout files to an time-stamped results folder in the export path.
Return path to .psout folder.
'''
# Create the exportPath if requied
if not os.path.exists(exportPath):
os.mkdir(exportPath)
else:
for dir in os.listdir(exportPath):
_dir = os.path.join(exportPath, dir)
if os.path.isdir(_dir) and dir.startswith('MTB_'):
if os.listdir(_dir) == []:
shutil.rmtree(_dir)
#Creating a datetime stamped results subfolder
resultsFolder = f'MTB_{datetime.now().strftime(r"%d%m%Y%H%M%S")}'
#Move .psout files away from build folder into results subfolder in the export folder
psoutFolder = os.path.join(exportPath, resultsFolder)
os.mkdir(psoutFolder)
moveFiles(buildPath, psoutFolder, ['.psout'], '_taskid')
return psoutFolder
def cleanBuildfolder(buildPath : str):
'''
"Cleans" the build folder by trying to delete it.
'''
try:
shutil.rmtree(buildPath)
except FileNotFoundError:
pass
def findMTB(pscad : mhi.pscad.PSCAD) -> mhi.pscad.UserCmp:
'''
Finds the MTB block in the project.
'''
projectLst = pscad.projects()
MTBcand : Optional[mhi.pscad.UserCmp] = None
for prjDic in projectLst:
if prjDic['type'].lower() == 'case':
project = pscad.project(prjDic['name'])
MTBs : List[mhi.pscad.UserCmp]= project.find_all(Name_='$MTB_9124$') #type: ignore
if len(MTBs) > 0:
if MTBcand or len(MTBs) > 1:
exit('Multiple MTB blocks found in workspace.')
else:
MTBcand = MTBs[0]
if not MTBcand:
exit('No MTB block found in workspace.')
return MTBcand
def addInterfaceFile(project : mhi.pscad.Project):
'''
Adds the interface file to the project.
'''
resList = project.resources()
for res in resList:
if res.path == r'.\interface.f' or res.name == 'interface.f':
return
print('Adding interface.f to project')
project.create_resource(r'.\interface.f')
def writeCaseRankTaskIdCSV(emtCases):
data = []
for idx, case in enumerate(emtCases, start=1):
data.append({'Case Rank': case.rank, 'Task ID': idx, 'Case Name': case.Name})
df = pd.DataFrame(data)
df.to_csv('caseRankTaskID.csv', index=False)
def main():
print()
print('execute_pscad.py started at:', datetime.now().strftime('%Y-%m-%d %H:%M:%S'), '\n')
pscad = connectPSCAD()
# If the script is not executed from within PSCAD, run PSCAD as an external client
if pscad is None:
runningAsEternalClient = True
pscad = startPSCAD()
else:
runningAsEternalClient = False
#Update PGB names for all unit measurement components
updateUMs(pscad)
#Disable all unused PGBs not specified in the plotter's figureSetup.csv if required
if disableAllUnusedPGBs:
figureSetup = r'.\plotter\figureSetup.csv'
keep_signals = getSignalsFromFigureSetup(figureSetup)
missing_total = validateFigureSetupAgainstWorkspace(pscad, keep_signals)
if not missing_total:
# 3. Only if the CSV is 100% correct, proceed to modify projects
case_names = [p['name'] for p in pscad.projects() if p['type'] == 'Case']
for case_name in case_names:
proj = pscad.project(case_name)
# This function (from previous step) now handles one project at a time
synchronizePGBsInProject(proj, keep_signals, sync=True, verbose=True)
else:
print("\nAborting: Please fix the signal names in figureSetup.csv before proceeding.")
os.exit(1)
plantSettings, channels, _, _, emtCases = cs.setup(sheetPath, pscad = True, pfEncapsulation = None)
#Print plant settings from casesheet
print('Plant settings:')
for setting in plantSettings.__dict__:
print(f'{setting} : {plantSettings.__dict__[setting]}')
print()
#Prepare MTB based on execution mode
MTB = findMTB(pscad)
project = pscad.project(MTB.project_name)
caseList = []
for case in emtCases:
caseList.append(case.rank)
if MTB.parameters()['par_mode'] == 'VOLLEY':
#Output ranks in relation to task
print('---------EXECUTING VOLLEY MODE---------')
print('Rank / Task ID / Casename:')
for case in emtCases:
print(f'{case.rank} / {emtCases.index(case) + 1} / {case.Name}')
singleRank = None
elif MTB.parameters()['par_mode'] == 'MANUAL' and MTB.parameters()['par_manualrank'] in caseList:
#Output rank in relation to task id
singleRank = MTB.parameters()['par_manualrank']
singleName = emtCases[caseList.index(MTB.parameters()['par_manualrank'])].Name
print(f'Executing only Rank {singleRank}: {singleName}')
print(f'Excecuting only Rank {singleRank}: {singleName}')
else:
raise ValueError('Invalid rank selected for par_manualrank in MTB block.')
writeCaseRankTaskIdCSV(emtCases) # Save "Case Rank", "TaskID", "Case Name" in a .csv file for PSCAD OOM Recovery
print()
si.renderFortran('interface.f', channels)
#Set executed flag
MTB.parameters(executed = 1) #type: ignore
#Add interface file to project
addInterfaceFile(project)
buildFolder : str = project.temp_folder #type: ignore
cleanBuildfolder(buildFolder) #type: ignore
project.parameters(time_duration = 999,
time_step = plantSettings.PSCAD_Timestep,
sample_step = '1000',
PlotType = 'PSOUT',
output_filename = f'{plantSettings.Projectname}.psout',
SnapType = 0) #type: ignore
pscad.remove_all_simulation_sets()
pmr = pscad.create_simulation_set('MTB')
pmr.add_tasks(MTB.project_name)
project_pmr = pmr.task(MTB.project_name)
project_pmr.parameters(ammunition = len(emtCases) if MTB.parameters()['par_mode'] == 'VOLLEY' else 1,
volley = volley,
affinity_type = 2 if traceAffinity else 0) # Or "Tracing". Valid options are 0, 1 ('SINGLE') or 2 ('ALL)
project_pmr.overrides(state_animation = stateAnimation,
only_in_use_channels = onlyInUseChannels)
pscad.run_simulation_sets('MTB') #type: ignore ??? By sideeffect changes current working directory ???
os.chdir(executeFolder)
psoutFolder = cleanUpPsoutFiles(buildFolder, exportPath, plantSettings.Projectname)
print()
taskIdToRank(psoutFolder, plantSettings.Projectname, emtCases, singleRank)
print('execute_pscad.py finished at: ', datetime.now().strftime('%m-%d %H:%M:%S'))
if runningAsEternalClient:
exitPSCAD(pscad)
if __name__ == '__main__':
main()
if LOG_FILE:
LOG_FILE.close()