patme 0.4.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of patme might be problematic. Click here for more details.
- patme/__init__.py +52 -0
- patme/buildtools/__init__.py +7 -0
- patme/buildtools/rce_releasecreator.py +336 -0
- patme/buildtools/release.py +26 -0
- patme/femtools/__init__.py +5 -0
- patme/femtools/abqmsgfilechecker.py +137 -0
- patme/femtools/fecall.py +1092 -0
- patme/geometry/__init__.py +0 -0
- patme/geometry/area.py +124 -0
- patme/geometry/coordinatesystem.py +635 -0
- patme/geometry/intersect.py +284 -0
- patme/geometry/line.py +183 -0
- patme/geometry/misc.py +420 -0
- patme/geometry/plane.py +464 -0
- patme/geometry/rotate.py +244 -0
- patme/geometry/scale.py +152 -0
- patme/geometry/shape2d.py +50 -0
- patme/geometry/transformations.py +1831 -0
- patme/geometry/translate.py +139 -0
- patme/mechanics/__init__.py +4 -0
- patme/mechanics/loads.py +435 -0
- patme/mechanics/material.py +1260 -0
- patme/service/__init__.py +7 -0
- patme/service/decorators.py +85 -0
- patme/service/duration.py +96 -0
- patme/service/exceptionhook.py +104 -0
- patme/service/exceptions.py +36 -0
- patme/service/io/__init__.py +3 -0
- patme/service/io/basewriter.py +122 -0
- patme/service/logger.py +375 -0
- patme/service/mathutils.py +108 -0
- patme/service/misc.py +71 -0
- patme/service/moveimports.py +217 -0
- patme/service/stringutils.py +419 -0
- patme/service/systemutils.py +290 -0
- patme/sshtools/__init__.py +3 -0
- patme/sshtools/cara.py +435 -0
- patme/sshtools/clustercaller.py +420 -0
- patme/sshtools/facluster.py +350 -0
- patme/sshtools/sshcall.py +168 -0
- patme-0.4.4.dist-info/LICENSE +21 -0
- patme-0.4.4.dist-info/LICENSES/MIT.txt +9 -0
- patme-0.4.4.dist-info/METADATA +168 -0
- patme-0.4.4.dist-info/RECORD +46 -0
- patme-0.4.4.dist-info/WHEEL +4 -0
- patme-0.4.4.dist-info/entry_points.txt +3 -0
patme/femtools/fecall.py
ADDED
|
@@ -0,0 +1,1092 @@
|
|
|
1
|
+
# Copyright (C) 2013 Deutsches Zentrum fuer Luft- und Raumfahrt(DLR, German Aerospace Center) <www.dlr.de>
|
|
2
|
+
# SPDX-FileCopyrightText: 2022 German Aerospace Center (DLR)
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Calling Analysis
|
|
8
|
+
|
|
9
|
+
Several finite element solvers can be used to solve different kinds of structural problems.
|
|
10
|
+
Currently, interfaces to Ansys, Nastran and Abaqus are provided. They all base on a general
|
|
11
|
+
python interface which provide convenient methods to establish a fe solver call whithout taking effort
|
|
12
|
+
e.g. in file I/O or error handling. This is all done by the general interface.
|
|
13
|
+
For example,running an linear static analysis in Ansys with a beforehand written file Model.mac
|
|
14
|
+
will be instantiated as follows::
|
|
15
|
+
|
|
16
|
+
from patme.femtools.fecall import AnsysCaller
|
|
17
|
+
ansCall = AnsysCaller(feFilename = "Model.mac")
|
|
18
|
+
ansCall.run(doRemoteCall = False, jobName = "testJobName")
|
|
19
|
+
|
|
20
|
+
If Abaqus or Nastran are used as external fe solver, the approach is similiar and the input file and caller object
|
|
21
|
+
need to be changed. Within a run() call, the user can also define if the calculation shall be done locally
|
|
22
|
+
or one the FA-Cluster in remote mode.
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
import argparse
|
|
26
|
+
import getpass
|
|
27
|
+
import glob
|
|
28
|
+
import os
|
|
29
|
+
import platform
|
|
30
|
+
import re
|
|
31
|
+
import shutil
|
|
32
|
+
import subprocess
|
|
33
|
+
import sys
|
|
34
|
+
import time
|
|
35
|
+
|
|
36
|
+
from numpy import array
|
|
37
|
+
|
|
38
|
+
from patme.service.duration import Duration
|
|
39
|
+
from patme.service.exceptions import DelisSshError, ImproperParameterError, InternalError
|
|
40
|
+
from patme.service.logger import log
|
|
41
|
+
from patme.service.systemutils import dos2unix, searchForWordsWithinFile
|
|
42
|
+
from patme.sshtools.clustercaller import ClusterCaller
|
|
43
|
+
from patme.sshtools.sshcall import callSSH
|
|
44
|
+
|
|
45
|
+
duration = Duration()
|
|
46
|
+
|
|
47
|
+
femUsedCores = 2
|
|
48
|
+
"""Number of the cores used for an fem calculation. Defaults to 2. If you change this for abaqus
|
|
49
|
+
runs, please also recognize the STM-rules for abaqus usage."""
|
|
50
|
+
ansysPath = "C:\\Program Files\\ANSYS Inc\\v192\\ansys\\bin\\winx64\\ANSYS192.EXE"
|
|
51
|
+
ansysLicense = "ansys"
|
|
52
|
+
"""Ansys license. Some possible values are (ansys, anshpc)"""
|
|
53
|
+
nastranPath = "C:\\MSC.Software\\MSC_Nastran\\20190\\bin\\nastran.exe"
|
|
54
|
+
abaqusPath = "C:\\SIMULIA\\Commands\\abaqus.bat"
|
|
55
|
+
"""path to abaqus.bat"""
|
|
56
|
+
|
|
57
|
+
RE_MULTILINE = re.RegexFlag.MULTILINE
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ResultLogFileChecker:
|
|
61
|
+
"""checks result log files for errors"""
|
|
62
|
+
|
|
63
|
+
def __init__(self):
|
|
64
|
+
self.maxErrors = 0
|
|
65
|
+
|
|
66
|
+
def getErrorFileAndErrorPattern(self, jobName):
|
|
67
|
+
"""must be implemented in sub class"""
|
|
68
|
+
raise NotImplementedError("This method must be implemented in a subclass")
|
|
69
|
+
|
|
70
|
+
def checkResultLogFile(self, jobName):
|
|
71
|
+
"""returns true if no specified errors are found"""
|
|
72
|
+
errorFileName, errorPattern = self.getErrorFileAndErrorPattern(jobName)
|
|
73
|
+
if not os.path.exists(errorFileName):
|
|
74
|
+
log.error(f"Did not find given error file {errorFileName}")
|
|
75
|
+
return None
|
|
76
|
+
else:
|
|
77
|
+
|
|
78
|
+
matches = searchForWordsWithinFile(errorFileName, errorPattern)
|
|
79
|
+
if len(matches) > self.maxErrors:
|
|
80
|
+
log.error(f"Call failed due to errors in {errorFileName}")
|
|
81
|
+
return False
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class FECaller(ResultLogFileChecker, ClusterCaller):
|
|
86
|
+
"""This class represents a general interface to call several FE-solvers.
|
|
87
|
+
It can also distinguish between local and remote(fa institute cluster) calculations.
|
|
88
|
+
|
|
89
|
+
The remote jobs are performed by copying the files of the local input directory to the
|
|
90
|
+
cluster on "\\\\cluster.fa.bs.dlr.de\\<username>\\delis\\<runDirName>".
|
|
91
|
+
Then the program creates an ssh connection to the cluster. More information about
|
|
92
|
+
the ssh connection can be found in service.utilities.callSSH.
|
|
93
|
+
After the completion of the job the result is copied back to the local runDir.
|
|
94
|
+
This feature is inherited from ClusterCaller.
|
|
95
|
+
|
|
96
|
+
:param feFilename: name of fe input file optionally with relative or absolute path
|
|
97
|
+
:param runDir: absolute or relative path to the folder where the fe run should be executed
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def __init__(self, **kwargs):
|
|
101
|
+
ResultLogFileChecker.__init__(self)
|
|
102
|
+
runDir = kwargs.pop("runDir", None)
|
|
103
|
+
ClusterCaller.__init__(self, runDir, **kwargs)
|
|
104
|
+
self.feFilename = kwargs.pop("feFilename", "")
|
|
105
|
+
if not self.runDir:
|
|
106
|
+
self.runDir = os.path.dirname(self.feFilename)
|
|
107
|
+
|
|
108
|
+
self.feFilename = os.path.join(self.runDir, os.path.basename(self.feFilename))
|
|
109
|
+
if not os.path.exists(self.feFilename):
|
|
110
|
+
raise InternalError(f"Given fem input file does not exist: {self.feFilename}")
|
|
111
|
+
|
|
112
|
+
self.localCmds = None
|
|
113
|
+
self.localSubProcessSettings = {"shell": False}
|
|
114
|
+
|
|
115
|
+
self.remoteCallFailed = False
|
|
116
|
+
"""This is set to true if a remote call failed. Calling methods can use this flag as information"""
|
|
117
|
+
self.activateLicenseCheck = True
|
|
118
|
+
|
|
119
|
+
self._useNumberOfCores = 2
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def isLicenseAvailable():
|
|
123
|
+
"""Returns True if the required number of license tokens is available, otherwise False"""
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
@duration.timeit
|
|
127
|
+
def run(self, doRemoteCall=False, copyFilesLocalToHost=True, jobName=None, **kwargs):
|
|
128
|
+
"""
|
|
129
|
+
This methods serves to execute a FE-input model within a fe solver (e.g. for linear static analysis)
|
|
130
|
+
:param doRemoteCall: flag if the calculation should be done on a remote computer
|
|
131
|
+
:param copyFilesLocalToHost: flag if all files in the directory runDir should be copied to the
|
|
132
|
+
remote machine
|
|
133
|
+
:param jobName: Name of job
|
|
134
|
+
:returns: True if there were no errors occured when calling the fe solver with the given input file
|
|
135
|
+
"""
|
|
136
|
+
if not jobName:
|
|
137
|
+
jobName = os.path.splitext(os.path.basename(self.feFilename))[0]
|
|
138
|
+
|
|
139
|
+
if doRemoteCall:
|
|
140
|
+
try:
|
|
141
|
+
if self.activateLicenseCheck:
|
|
142
|
+
self.returnWhenLicenseAvailable()
|
|
143
|
+
self.runRemote(copyFilesLocalToHost, jobName, **kwargs)
|
|
144
|
+
# check error file after remote files were copied to local machine
|
|
145
|
+
if not self.checkResultLogFile(jobName):
|
|
146
|
+
doRemoteCall = False
|
|
147
|
+
else:
|
|
148
|
+
retVal = 8
|
|
149
|
+
|
|
150
|
+
except DelisSshError as exception:
|
|
151
|
+
msg = "Remote call failed. Maybe the remote dir was not found, "
|
|
152
|
+
msg += "the remote server did not answer or the ssh authentication failed. "
|
|
153
|
+
msg += "Calculating locally."
|
|
154
|
+
log.error(msg)
|
|
155
|
+
|
|
156
|
+
log.info(f"Error message to the above warning: {exception}")
|
|
157
|
+
doRemoteCall = False
|
|
158
|
+
self.remoteCallFailed = True
|
|
159
|
+
|
|
160
|
+
if not doRemoteCall:
|
|
161
|
+
if self.activateLicenseCheck:
|
|
162
|
+
self.returnWhenLicenseAvailable()
|
|
163
|
+
retVal = self.runLocal(jobName, **kwargs)
|
|
164
|
+
|
|
165
|
+
if self.checkResultLogFile(jobName) is False:
|
|
166
|
+
log.info("Change return value to 1 due to erros in the above file.")
|
|
167
|
+
retVal = 1
|
|
168
|
+
|
|
169
|
+
retVal = self.checkReturnValueOfCall(retVal)
|
|
170
|
+
log.info(f"{self.solverName} run finished")
|
|
171
|
+
return retVal
|
|
172
|
+
|
|
173
|
+
def runLocal(self, jobName, **kwargs):
|
|
174
|
+
"""doc"""
|
|
175
|
+
self.localCmds = [jobName if elem == "<jobName>" else elem for elem in self.localCmds]
|
|
176
|
+
log.info(f"call {self.solverName} locally ")
|
|
177
|
+
infoStr = f"call {self.solverName} with the following command: "
|
|
178
|
+
log.debug(infoStr + " ".join(self.localCmds))
|
|
179
|
+
|
|
180
|
+
toolexe = self.localCmds[0]
|
|
181
|
+
if not os.path.exists(toolexe) and "win" in sys.platform:
|
|
182
|
+
msg = "The given executable does not exist. "
|
|
183
|
+
msg += "Please enter the correct path to settings.py. "
|
|
184
|
+
msg += f"Acutal path: {toolexe}"
|
|
185
|
+
raise InternalError(msg)
|
|
186
|
+
|
|
187
|
+
toStderr = kwargs.pop("toStderr", None)
|
|
188
|
+
if toStderr and isinstance(toStderr, str):
|
|
189
|
+
toStderr = open(toStderr, "w+")
|
|
190
|
+
elif not toStderr:
|
|
191
|
+
toStderr = os.path.join(self.runDir, f"{jobName}_err.log")
|
|
192
|
+
toStderr = open(toStderr, "w+")
|
|
193
|
+
|
|
194
|
+
toStdout = kwargs.pop("toStdout", None)
|
|
195
|
+
if toStdout and isinstance(toStdout, str):
|
|
196
|
+
toStdout = open(toStdout, "w+")
|
|
197
|
+
elif not toStdout:
|
|
198
|
+
toStdout = os.path.join(self.runDir, f"{jobName}_out.log")
|
|
199
|
+
toStdout = open(toStdout, "w+")
|
|
200
|
+
|
|
201
|
+
retVal = subprocess.call(
|
|
202
|
+
self.localCmds,
|
|
203
|
+
cwd=self.runDir,
|
|
204
|
+
stderr=toStderr,
|
|
205
|
+
stdout=toStdout,
|
|
206
|
+
shell=self.localSubProcessSettings.get("shell", False),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
log.info(f'return value of {self.solverName} call is "{retVal}"')
|
|
210
|
+
return retVal
|
|
211
|
+
|
|
212
|
+
def returnWhenLicenseAvailable(self, sleeptime=5):
|
|
213
|
+
"""doc"""
|
|
214
|
+
# wait one minute
|
|
215
|
+
numberOfTries = 12
|
|
216
|
+
while numberOfTries > 0:
|
|
217
|
+
|
|
218
|
+
if self.isLicenseAvailable():
|
|
219
|
+
return 0
|
|
220
|
+
|
|
221
|
+
msgs = [
|
|
222
|
+
f"No license for {self.solverName} available! ",
|
|
223
|
+
"Wait until a license is available.",
|
|
224
|
+
"check every 5s.",
|
|
225
|
+
]
|
|
226
|
+
log.info(" ".join(msgs))
|
|
227
|
+
log.debug(msgs[0])
|
|
228
|
+
time.sleep(sleeptime)
|
|
229
|
+
numberOfTries -= 1
|
|
230
|
+
|
|
231
|
+
def checkReturnValueOfCall(self, retVal):
|
|
232
|
+
"""Methods check normal subprocess call result. 1 means failure, 0 means success
|
|
233
|
+
If the fe solver returns special codes, please overwrite this method in derived subclass for the used fe solver
|
|
234
|
+
"""
|
|
235
|
+
if retVal == 1:
|
|
236
|
+
log.error(f'The {retVal} return code "{self.solverName}" indicates errors. Please check the logfile.')
|
|
237
|
+
return False
|
|
238
|
+
return True
|
|
239
|
+
|
|
240
|
+
def _getNumberOfCores(self):
|
|
241
|
+
return self._useNumberOfCores
|
|
242
|
+
|
|
243
|
+
def _setNumberOfCores(self, numCores):
|
|
244
|
+
self._useNumberOfCores = numCores
|
|
245
|
+
|
|
246
|
+
useNumberOfCores = property(fget=_getNumberOfCores, fset=_setNumberOfCores)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class AnsysCaller(FECaller):
|
|
250
|
+
|
|
251
|
+
solverName = "Ansys"
|
|
252
|
+
|
|
253
|
+
def __init__(self, **kwargs):
|
|
254
|
+
|
|
255
|
+
FECaller.__init__(self, **kwargs)
|
|
256
|
+
|
|
257
|
+
# running ansys
|
|
258
|
+
os.environ["ANS_CONSEC"] = "YES"
|
|
259
|
+
|
|
260
|
+
ansPath = os.path.normpath(ansysPath)
|
|
261
|
+
if not os.path.exists(ansPath):
|
|
262
|
+
ansPath = self.findAnsysExecutable()
|
|
263
|
+
|
|
264
|
+
self.localCmds = [
|
|
265
|
+
ansPath,
|
|
266
|
+
"-o",
|
|
267
|
+
"ansys.log",
|
|
268
|
+
"-i",
|
|
269
|
+
os.path.join(self.runDir, os.path.basename(self.feFilename)),
|
|
270
|
+
"-b",
|
|
271
|
+
"-np",
|
|
272
|
+
str(self.useNumberOfCores),
|
|
273
|
+
"-j",
|
|
274
|
+
"<jobName>",
|
|
275
|
+
"-m",
|
|
276
|
+
"1200",
|
|
277
|
+
"-db",
|
|
278
|
+
"64",
|
|
279
|
+
"-p",
|
|
280
|
+
ansysLicense,
|
|
281
|
+
]
|
|
282
|
+
|
|
283
|
+
def getErrorFileAndErrorPattern(self, jobName):
|
|
284
|
+
"""doc"""
|
|
285
|
+
basePath = os.path.dirname(self.feFilename)
|
|
286
|
+
baseName = jobName if jobName else os.path.splitext(os.path.basename(self.feFilename))[0]
|
|
287
|
+
allerrs = glob.glob(os.path.join(basePath, baseName + "[0-9].err"))
|
|
288
|
+
if not allerrs:
|
|
289
|
+
errorFile = os.path.join(basePath, baseName + ".err")
|
|
290
|
+
else:
|
|
291
|
+
errorFile = allerrs[0]
|
|
292
|
+
|
|
293
|
+
return errorFile, re.compile(r"\*{3} ERROR \*{3}", RE_MULTILINE)
|
|
294
|
+
|
|
295
|
+
def findAnsysExecutable(self):
|
|
296
|
+
"""doc"""
|
|
297
|
+
if "win" in sys.platform:
|
|
298
|
+
|
|
299
|
+
os_env_key, ansysPath = next(
|
|
300
|
+
((key, value) for key, value in os.environ.items() if re.match(r"ANSYS\d+_DIR", key)), (None, None)
|
|
301
|
+
)
|
|
302
|
+
if ansysPath is None:
|
|
303
|
+
return None
|
|
304
|
+
versNum = re.findall(r"\d+", os_env_key)[0]
|
|
305
|
+
return os.path.join(ansysPath, "bin", "winx64", "ANSYS%s.exe" % versNum)
|
|
306
|
+
|
|
307
|
+
elif "linux" in sys.platform:
|
|
308
|
+
anysdis = shutil.which("ansysdis")
|
|
309
|
+
ansysVersion = re.search(r"/v(\d+)/ansys/bin", anysdis).group(1)
|
|
310
|
+
return os.path.join(os.path.dirname(anysdis), "ansys%s" % ansysVersion)
|
|
311
|
+
else:
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
def checkReturnValueOfCall(self, retVal):
|
|
315
|
+
"""doc"""
|
|
316
|
+
if retVal == 7:
|
|
317
|
+
msg = 'Ansys return code is "7".This indicates license problems. '
|
|
318
|
+
msg += "Please see ansys error file for more information."
|
|
319
|
+
log.error(msg)
|
|
320
|
+
|
|
321
|
+
if retVal in [0, 1]:
|
|
322
|
+
msg = f'The ansys return code "{retVal}" indicates errors within ansys. '
|
|
323
|
+
msg += "Please check the logfile."
|
|
324
|
+
log.error(msg)
|
|
325
|
+
|
|
326
|
+
return retVal
|
|
327
|
+
|
|
328
|
+
@staticmethod
|
|
329
|
+
def isLicenseAvailable():
|
|
330
|
+
"""
|
|
331
|
+
This method returns if there is at least 1 ansys token available
|
|
332
|
+
"""
|
|
333
|
+
licenseServerString = "1055@ansyssz1.intra.dlr.de"
|
|
334
|
+
log.info("Check Ansys license availability.")
|
|
335
|
+
return 1 <= availableFlexlmLicenseTokens(licenseServerString, ansysLicense)
|
|
336
|
+
|
|
337
|
+
def generateSolverSpecificRemoteCmds(self):
|
|
338
|
+
"""doc"""
|
|
339
|
+
baseFileInDir = os.path.basename(self.feFilename)
|
|
340
|
+
base, _ = os.path.splitext(baseFileInDir)
|
|
341
|
+
ansysCall = [
|
|
342
|
+
"ansys241",
|
|
343
|
+
"-o",
|
|
344
|
+
"ansys.log",
|
|
345
|
+
"-i",
|
|
346
|
+
baseFileInDir,
|
|
347
|
+
"-b",
|
|
348
|
+
"-np",
|
|
349
|
+
str(self.useNumberOfCores),
|
|
350
|
+
"-j",
|
|
351
|
+
base,
|
|
352
|
+
"-m",
|
|
353
|
+
"1200",
|
|
354
|
+
"-db",
|
|
355
|
+
"64",
|
|
356
|
+
"-p",
|
|
357
|
+
ansysLicense,
|
|
358
|
+
]
|
|
359
|
+
runScriptCmds = []
|
|
360
|
+
if "cara" in self.clusterName:
|
|
361
|
+
runScriptCmds += [
|
|
362
|
+
"module load env/spack",
|
|
363
|
+
"module load rev/23.05_r8",
|
|
364
|
+
"module load ansys/2024r1",
|
|
365
|
+
]
|
|
366
|
+
|
|
367
|
+
runScriptCmds.append(" ".join(ansysCall))
|
|
368
|
+
|
|
369
|
+
return runScriptCmds
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class ParallelAnsysCaller(AnsysCaller):
|
|
373
|
+
def __init__(self, **kwargs):
|
|
374
|
+
"""Derived ansys caller for parallel use
|
|
375
|
+
Used for parallel calculation on CASE within Victoria but unless the
|
|
376
|
+
model complexity does not exceed the computational ressources it can be used
|
|
377
|
+
on any local machine (no remote call to cluster tested at the moment"""
|
|
378
|
+
AnsysCaller.__init__(self, **kwargs)
|
|
379
|
+
|
|
380
|
+
# running ansys
|
|
381
|
+
os.environ["ANS_CONSEC"] = "YES"
|
|
382
|
+
|
|
383
|
+
# command to enable multiple ansys instances to run on the same input file
|
|
384
|
+
self.preCommands = ["setenv ANSYS_LOCK OFF"]
|
|
385
|
+
|
|
386
|
+
# remove option because it is only useful on small memory systems
|
|
387
|
+
dbFlag = self.localCmds.index("-db")
|
|
388
|
+
self.localCmds = self.localCmds[:dbFlag] + self.localCmds[dbFlag + 2 :]
|
|
389
|
+
|
|
390
|
+
memoryFlag = self.localCmds.index("-m")
|
|
391
|
+
self.localCmds[memoryFlag + 1] = "2048"
|
|
392
|
+
|
|
393
|
+
def setAnsysLogFile(self):
|
|
394
|
+
"""doc"""
|
|
395
|
+
logFileIndxLocal = self.localCmds.index("-o")
|
|
396
|
+
|
|
397
|
+
if logFileIndxLocal != -1:
|
|
398
|
+
runIndex = self.feFilename.rsplit("_", 1)[-1].split(".", 1)[0]
|
|
399
|
+
self.localCmds[logFileIndxLocal + 1] = f"ansys_{runIndex}.log"
|
|
400
|
+
|
|
401
|
+
def getErrorFileAndErrorPattern(self, jobName):
|
|
402
|
+
"""doc"""
|
|
403
|
+
basePath = os.path.dirname(self.feFilename)
|
|
404
|
+
baseName = jobName if jobName else os.path.splitext(os.path.basename(self.feFilename))[0]
|
|
405
|
+
errorFile = os.path.join(basePath, baseName + ".err")
|
|
406
|
+
if not os.path.exists(errorFile):
|
|
407
|
+
errorFile = ""
|
|
408
|
+
|
|
409
|
+
return errorFile, re.compile("ERROR", RE_MULTILINE)
|
|
410
|
+
|
|
411
|
+
def checkReturnValueOfCall(self, retVal):
|
|
412
|
+
"""doc"""
|
|
413
|
+
jobName = os.path.splitext(os.path.basename(self.feFilename))[0]
|
|
414
|
+
numberOfTries = 240
|
|
415
|
+
|
|
416
|
+
while retVal == 7 and numberOfTries > 0:
|
|
417
|
+
msg = 'Ansys return code is "7".This indicates license problems. '
|
|
418
|
+
msg += "Please see ansys error file for more information."
|
|
419
|
+
log.warning(msg)
|
|
420
|
+
|
|
421
|
+
time.sleep(15)
|
|
422
|
+
numberOfTries -= 1
|
|
423
|
+
log.info("Start Ansys again!")
|
|
424
|
+
|
|
425
|
+
retVal = self.runLocal(jobName)
|
|
426
|
+
|
|
427
|
+
if retVal in [0, 1]:
|
|
428
|
+
msg = f'The ansys return code "{retVal}" indicates errors within ansys. '
|
|
429
|
+
msg += "Please check the logfile."
|
|
430
|
+
log.error(msg)
|
|
431
|
+
|
|
432
|
+
return retVal
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
class NastranCaller(FECaller):
|
|
436
|
+
|
|
437
|
+
solverName = "Nastran"
|
|
438
|
+
|
|
439
|
+
def __init__(self, **kwargs):
|
|
440
|
+
"""doc"""
|
|
441
|
+
FECaller.__init__(self, **kwargs)
|
|
442
|
+
|
|
443
|
+
nastran_exe = shutil.which("nastran")
|
|
444
|
+
if not nastran_exe:
|
|
445
|
+
nastran_exe = nastranPath
|
|
446
|
+
|
|
447
|
+
self.localCmds = [
|
|
448
|
+
nastran_exe,
|
|
449
|
+
os.path.basename(self.feFilename),
|
|
450
|
+
f"parallel={self.useNumberOfCores}",
|
|
451
|
+
"scratch=yes",
|
|
452
|
+
"old=no",
|
|
453
|
+
"system(363)=1",
|
|
454
|
+
f"sdirectory={self.runDir}",
|
|
455
|
+
]
|
|
456
|
+
|
|
457
|
+
if platform.system() == "Linux":
|
|
458
|
+
self.localCmds.insert(5, "batch=no")
|
|
459
|
+
|
|
460
|
+
def generateSolverSpecificRemoteCmds(self):
|
|
461
|
+
"""doc"""
|
|
462
|
+
baseFileInDir = os.path.basename(self.feFilename)
|
|
463
|
+
if self.clusterName == "cara":
|
|
464
|
+
# user defined environment variable
|
|
465
|
+
cara_partion = os.environ.get("CARA_PARTITION", "ppp")
|
|
466
|
+
useNumFECores = 1 if cara_partion == "ppp" else self.useNumberOfCores
|
|
467
|
+
else:
|
|
468
|
+
useNumFECores = self.useNumberOfCores
|
|
469
|
+
|
|
470
|
+
runCmds = []
|
|
471
|
+
if "cara" in self.clusterName:
|
|
472
|
+
runCmds += [
|
|
473
|
+
"module load env/spack",
|
|
474
|
+
"module load rev/23.05_r8",
|
|
475
|
+
"module load nastran/2023.2",
|
|
476
|
+
]
|
|
477
|
+
runCmds.append(
|
|
478
|
+
f"nast20232 {baseFileInDir} parallel={useNumFECores} scratch=yes old=no sdirectory=$(pwd)"
|
|
479
|
+
) # "system(363)=1")
|
|
480
|
+
return runCmds
|
|
481
|
+
|
|
482
|
+
def getErrorFileAndErrorPattern(self, jobName):
|
|
483
|
+
"""doc"""
|
|
484
|
+
outFlag = re.search(r"out=(.*?)(\s|\Z)", " ".join(self.localCmds))
|
|
485
|
+
dirname, base_file = os.path.split(self.feFilename)
|
|
486
|
+
if outFlag:
|
|
487
|
+
dirname = os.path.join(dirname, outFlag.group(1))
|
|
488
|
+
|
|
489
|
+
basename = os.path.join(dirname, os.path.splitext(base_file)[0] + ".f06")
|
|
490
|
+
|
|
491
|
+
return basename, re.compile(r"(?<!(IF THE FLAG IS ))FATAL", RE_MULTILINE)
|
|
492
|
+
|
|
493
|
+
@staticmethod
|
|
494
|
+
def isLicenseAvailable():
|
|
495
|
+
"""This method returns if there is at least 1 nastran token available"""
|
|
496
|
+
licenseServer = "1700@nastransz2.intra.dlr.de"
|
|
497
|
+
|
|
498
|
+
log.info("Check Nastran license availability.")
|
|
499
|
+
return 13 <= availableFlexlmLicenseTokens(licenseServer, "MSCONE")
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
class AbaqusCaller(FECaller):
|
|
503
|
+
"""This class realizes an interface to Abaqus"""
|
|
504
|
+
|
|
505
|
+
clusterAbaqusName = "abq2023"
|
|
506
|
+
|
|
507
|
+
def __init__(self, **kwargs):
|
|
508
|
+
FECaller.__init__(self, **kwargs)
|
|
509
|
+
self.solverName = "Abaqus"
|
|
510
|
+
# eclipse adds some env variabels that do not work with abaqus
|
|
511
|
+
os.environ.pop("PYTHONIOENCODING", None)
|
|
512
|
+
|
|
513
|
+
# scratch file dir set due to error with abaqus not able to create the temp dir in c:users ...
|
|
514
|
+
tmpScratchFilesPath = os.path.join(self.runDir, "abaqusScratch")
|
|
515
|
+
if os.path.exists(tmpScratchFilesPath):
|
|
516
|
+
shutil.rmtree(tmpScratchFilesPath)
|
|
517
|
+
|
|
518
|
+
os.makedirs(tmpScratchFilesPath, exist_ok=True)
|
|
519
|
+
|
|
520
|
+
self.localCmds = [
|
|
521
|
+
abaqusPath,
|
|
522
|
+
"job=%s" % os.path.basename(os.path.splitext(self.feFilename)[0]),
|
|
523
|
+
"interactive",
|
|
524
|
+
"-scratch",
|
|
525
|
+
"abaqusScratch",
|
|
526
|
+
"-cpus",
|
|
527
|
+
str(self.useNumberOfCores),
|
|
528
|
+
]
|
|
529
|
+
|
|
530
|
+
def checkReturnValueOfCall(self, retVal):
|
|
531
|
+
"""doc"""
|
|
532
|
+
if retVal == 1:
|
|
533
|
+
msg = f'The abaqus return code "{retVal}" indicates errors within abaqus. '
|
|
534
|
+
msg += "Please check the logfile."
|
|
535
|
+
log.error(msg)
|
|
536
|
+
return False
|
|
537
|
+
return True
|
|
538
|
+
|
|
539
|
+
def runLocal(self, jobName, **kwargs):
|
|
540
|
+
"""doc"""
|
|
541
|
+
if "-cpus" in self.localCmds:
|
|
542
|
+
cpuNumsParam = self.localCmds.index("-cpus")
|
|
543
|
+
self.localCmds[cpuNumsParam + 1] = str(self.useNumberOfCores)
|
|
544
|
+
return super().runLocal(jobName, **kwargs)
|
|
545
|
+
|
|
546
|
+
def getErrorFileAndErrorPattern(self, jobName):
|
|
547
|
+
"""doc"""
|
|
548
|
+
basePath = os.path.dirname(self.feFilename)
|
|
549
|
+
baseName = jobName if jobName else os.path.splitext(os.path.basename(self.feFilename))[0]
|
|
550
|
+
useMsgFile = True
|
|
551
|
+
if useMsgFile:
|
|
552
|
+
filename = os.path.join(basePath, baseName + ".msg")
|
|
553
|
+
errorPattern = re.compile(r"\*{3}ERROR", RE_MULTILINE)
|
|
554
|
+
else:
|
|
555
|
+
filename = os.path.join(basePath, baseName + ".dat")
|
|
556
|
+
errorPattern = re.compile(r"(error|Error)", RE_MULTILINE)
|
|
557
|
+
return filename, errorPattern
|
|
558
|
+
|
|
559
|
+
@staticmethod
|
|
560
|
+
def isLicenseAvailable(licenseType="abaqus", requiredTokens=5):
|
|
561
|
+
"""Abaqus uses it's own license queue. To utilize it, this method returns no license information"""
|
|
562
|
+
return True
|
|
563
|
+
|
|
564
|
+
# licenseServerString = '27018@abaqussd1.t-systems-sfr.com'
|
|
565
|
+
# licenseServerString = '27018@abaqussd1.intra.dlr.de'
|
|
566
|
+
# log.info('Check Abaqus license availability.')
|
|
567
|
+
# return requiredTokens <= availableFlexlmLicenseTokens(licenseServerString, licenseType)
|
|
568
|
+
|
|
569
|
+
@staticmethod
|
|
570
|
+
def getMaxNumberOfParallelExecutions(licenseType="abaqus", requiredTokens=5):
|
|
571
|
+
"""This method returns the actual number of parallel executions that are possible as int
|
|
572
|
+
|
|
573
|
+
For a parameter description, please refer to "availableFlexlmLicenseTokens".
|
|
574
|
+
"""
|
|
575
|
+
licenseServerString = "27018@dldeffmimp04lic"
|
|
576
|
+
log.debug("Check Abaqus license availability - Max number.")
|
|
577
|
+
numLic = availableFlexlmLicenseTokens(licenseServerString, licenseType) // requiredTokens
|
|
578
|
+
log.debug(f"Number of possible abaqus runs: {numLic}")
|
|
579
|
+
return numLic
|
|
580
|
+
|
|
581
|
+
def generateSolverSpecificRemoteCmds(self):
|
|
582
|
+
"""doc"""
|
|
583
|
+
baseFileInDir = os.path.basename(self.feFilename)
|
|
584
|
+
cmds = []
|
|
585
|
+
if "cara" in self.clusterName:
|
|
586
|
+
cmds += [
|
|
587
|
+
"module load env/easybuild",
|
|
588
|
+
"module load ABAQUS/2023",
|
|
589
|
+
]
|
|
590
|
+
cmds.append(
|
|
591
|
+
f"{self.clusterAbaqusName} job={baseFileInDir} interactive -scratch abaqusScratch -cpus {self.useNumberOfCores}"
|
|
592
|
+
)
|
|
593
|
+
return cmds
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
class AbaqusPythonCaller(AbaqusCaller):
|
|
597
|
+
"""This class realizes an interface to Abaqus to call a Python script in an specified directory with the specified
|
|
598
|
+
input filename.
|
|
599
|
+
|
|
600
|
+
.. note::
|
|
601
|
+
"from abaqus import *" is not possible with this call. Use AbaqusPythonCaeCaller instead!
|
|
602
|
+
|
|
603
|
+
:param pythonSkriptPath: name of Abaqus Python input file optionally with relative or absolute path
|
|
604
|
+
:param arguments: list of arguments passed on to the script
|
|
605
|
+
i.e. -odb odbFilename.odb -> ['-odb', 'odbFilename.odb']"""
|
|
606
|
+
|
|
607
|
+
def __init__(self, **kwargs):
|
|
608
|
+
AbaqusCaller.__init__(self, **kwargs)
|
|
609
|
+
self.solverName = "AbaqusPython"
|
|
610
|
+
# eclipse adds some env variabels that do not work with abaqus
|
|
611
|
+
os.environ.pop("PYTHONIOENCODING", None)
|
|
612
|
+
|
|
613
|
+
self.pythonScriptPath = kwargs.pop("pythonSkriptPath", None)
|
|
614
|
+
if not self.pythonScriptPath:
|
|
615
|
+
raise ImproperParameterError("python script was not given!")
|
|
616
|
+
|
|
617
|
+
pyArguments = kwargs.pop("arguments", [])
|
|
618
|
+
|
|
619
|
+
callParams = ["python", self.pythonScriptPath] + pyArguments
|
|
620
|
+
|
|
621
|
+
baseFile, _ = os.path.splitext(self.feFilename)
|
|
622
|
+
callParams += [">", f"{baseFile}.msg"]
|
|
623
|
+
|
|
624
|
+
self.localCmds = [abaqusPath] + callParams
|
|
625
|
+
|
|
626
|
+
def generateSolverSpecificRemoteCmds(self):
|
|
627
|
+
"""doc"""
|
|
628
|
+
baseFile, ext = os.path.splitext(os.path.basename(self.feFilename))
|
|
629
|
+
|
|
630
|
+
callParms = self.localCmds[1:]
|
|
631
|
+
pyScriptBaseDir = os.path.dirname(self.pythonScriptPath)
|
|
632
|
+
pyScriptBaseFile = os.path.basename(self.pythonScriptPath)
|
|
633
|
+
|
|
634
|
+
if pyScriptBaseDir != self.runDir:
|
|
635
|
+
shutil.copy(self.pythonScriptPath, self.runDir)
|
|
636
|
+
|
|
637
|
+
callParms[1] = pyScriptBaseFile
|
|
638
|
+
callParms[-1] = f"{baseFile}.msg"
|
|
639
|
+
|
|
640
|
+
runScriptCmds = []
|
|
641
|
+
if "cara" in self.clusterName:
|
|
642
|
+
runScriptCmds += [
|
|
643
|
+
"module load env/easybuild",
|
|
644
|
+
"module load ABAQUS/2023",
|
|
645
|
+
]
|
|
646
|
+
|
|
647
|
+
runScriptCmds.append(f"basefile={baseFile}")
|
|
648
|
+
|
|
649
|
+
if ext == ".odb":
|
|
650
|
+
runScriptCmds += [
|
|
651
|
+
"mv $basefile.odb $basefile_old.odb",
|
|
652
|
+
"abaqus -upgrade -job $basefile -odb $basefile_old.odb > upgrade.log",
|
|
653
|
+
'if grep -Fq "NO NEED TO UPGRADE" upgrade.log;',
|
|
654
|
+
" then",
|
|
655
|
+
" mv $basefile_old.odb $basefile.odb",
|
|
656
|
+
"fi",
|
|
657
|
+
]
|
|
658
|
+
|
|
659
|
+
runScriptCmds += ["abq2023 " + " ".join(callParms)]
|
|
660
|
+
|
|
661
|
+
return runScriptCmds
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
class AbaqusPythonCaeCaller(AbaqusCaller):
|
|
665
|
+
"""This class realizes an interface to AbaqusCAE to call a Python script in an specified directory with the specified
|
|
666
|
+
input filename.
|
|
667
|
+
:param pythonSkriptPath: name of Abaqus Python input file optionally with relative or absolute path
|
|
668
|
+
:param arguments: list of arguments passed on to the script
|
|
669
|
+
i.e. -odb odbFilename.odb -> ['-odb', 'odbFilename.odb']
|
|
670
|
+
|
|
671
|
+
"""
|
|
672
|
+
|
|
673
|
+
def __init__(self, **kwargs):
|
|
674
|
+
FECaller.__init__(self, **kwargs)
|
|
675
|
+
self.solverName = "AbaqusCaePython"
|
|
676
|
+
# eclipse adds some env variabels that do not work with abaqus
|
|
677
|
+
os.environ.pop("PYTHONIOENCODING", None)
|
|
678
|
+
|
|
679
|
+
pythonScriptPath = kwargs.pop("pythonSkriptPath", None)
|
|
680
|
+
if not pythonScriptPath:
|
|
681
|
+
raise ImproperParameterError("python script was not given!")
|
|
682
|
+
|
|
683
|
+
pyArguments = kwargs.pop("arguments", [])
|
|
684
|
+
# instead of "noGUI" use "script" if cae should be opened in gui mode (it does not close automatically with "script")
|
|
685
|
+
|
|
686
|
+
self.localCmds = [abaqusPath, "cae", "noGUI=" + pythonScriptPath] + pyArguments
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
class B2000ppCaller(FECaller):
|
|
690
|
+
|
|
691
|
+
solverName = "B2000++"
|
|
692
|
+
|
|
693
|
+
def __init__(self, **kwargs):
|
|
694
|
+
"""doc"""
|
|
695
|
+
FECaller.__init__(self, **kwargs)
|
|
696
|
+
|
|
697
|
+
b2000Run = shutil.which("b2000++")
|
|
698
|
+
|
|
699
|
+
self.localCmds = [b2000Run, self.feFilename]
|
|
700
|
+
|
|
701
|
+
self.subcaseNumber = kwargs.get("runSubcase", 1)
|
|
702
|
+
self.workingDir = kwargs.get("feWorkDir", self.runDir)
|
|
703
|
+
self.b2k_toolName_cara = kwargs.get("b2k_toolName_cara", "b2000++/4.6.3")
|
|
704
|
+
|
|
705
|
+
def runLocal(self, jobName, **kwargs):
|
|
706
|
+
"""doc"""
|
|
707
|
+
base, ext = os.path.splitext(self.feFilename)
|
|
708
|
+
flag_modified = False
|
|
709
|
+
if ".bdf" == ext:
|
|
710
|
+
converter = B2000FromBDFConverter(self.feFilename)
|
|
711
|
+
converter.runLocal(jobName)
|
|
712
|
+
flag_modified = True
|
|
713
|
+
self.feFilename = f"{base}.mdl"
|
|
714
|
+
|
|
715
|
+
if self.workingDir != self.runDir:
|
|
716
|
+
|
|
717
|
+
dbCreator = B2000ModelToDatabase(feWorkDir=self.workingDir)
|
|
718
|
+
dbCreator.runLocal(jobName)
|
|
719
|
+
basename = os.path.basename(self.feFilename)
|
|
720
|
+
base2, _ = os.path.splitext(basename)
|
|
721
|
+
self.feFilename = os.path.join(self.workingDir, f"{base2}.b2m")
|
|
722
|
+
flag_modified = True
|
|
723
|
+
|
|
724
|
+
if flag_modified:
|
|
725
|
+
|
|
726
|
+
self.localCmds = [self.localCmds[0], self.feFilename]
|
|
727
|
+
|
|
728
|
+
return FECaller.runLocal(self, jobName, **kwargs)
|
|
729
|
+
|
|
730
|
+
def generateSolverSpecificRemoteCmds(self):
|
|
731
|
+
"""doc"""
|
|
732
|
+
|
|
733
|
+
baseFileInDir = os.path.basename(self.feFilename)
|
|
734
|
+
baseFile, ext = os.path.splitext(baseFileInDir)
|
|
735
|
+
|
|
736
|
+
runScriptCmds = []
|
|
737
|
+
if "cara" in self.clusterName:
|
|
738
|
+
|
|
739
|
+
runScriptCmds += [
|
|
740
|
+
"module load env/spack",
|
|
741
|
+
"module load rev/23.05_r8",
|
|
742
|
+
"module use /sw/DLR/FA/BS/STM/modulefiles",
|
|
743
|
+
f"module load {self.b2k_toolName_cara}",
|
|
744
|
+
]
|
|
745
|
+
|
|
746
|
+
if ext == ".bdf":
|
|
747
|
+
runScriptCmds.append(f"b2convert_from_nas {baseFileInDir} {baseFile}.mdl")
|
|
748
|
+
|
|
749
|
+
runScriptCmds.append(f"b2000++ {baseFile}.mdl")
|
|
750
|
+
|
|
751
|
+
if "b2mconv.py" in os.listdir(self.runDir):
|
|
752
|
+
|
|
753
|
+
runScriptCmds.append(f"python b2mconv.py {baseFile}.b2m -o {baseFile}.pkl")
|
|
754
|
+
|
|
755
|
+
return runScriptCmds
|
|
756
|
+
|
|
757
|
+
def getErrorFileAndErrorPattern(self, jobName):
|
|
758
|
+
"""doc"""
|
|
759
|
+
resDir = self.feFilename.replace(".mdl", ".b2m")
|
|
760
|
+
errorFile = os.path.join(resDir, "log.txt")
|
|
761
|
+
if not os.path.exists(errorFile):
|
|
762
|
+
errorFile = ""
|
|
763
|
+
|
|
764
|
+
return errorFile, re.compile("CRITICAL", RE_MULTILINE)
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
class B2000ModelToDatabase(B2000ppCaller):
|
|
768
|
+
def __init__(self, **kwargs):
|
|
769
|
+
|
|
770
|
+
B2000ppCaller.__init__(self, **kwargs)
|
|
771
|
+
self.solverName = "b2ip++"
|
|
772
|
+
|
|
773
|
+
b2ipTool = shutil.which("b2ip++")
|
|
774
|
+
|
|
775
|
+
basename = os.path.basename(self.feFilename)
|
|
776
|
+
baseDir = os.path.dirname(self.feFilename)
|
|
777
|
+
base, _ = os.path.splitext(basename)
|
|
778
|
+
|
|
779
|
+
workingDir = kwargs.pop("feWorkDir", baseDir)
|
|
780
|
+
dbFile = os.path.join(workingDir, f"{base}.b2m")
|
|
781
|
+
|
|
782
|
+
self.localCmds = [b2ipTool, self.feFilename, dbFile]
|
|
783
|
+
|
|
784
|
+
def runLocal(self, jobName, **kwargs):
|
|
785
|
+
"""doc"""
|
|
786
|
+
return FECaller.runLocal(self, jobName, **kwargs)
|
|
787
|
+
|
|
788
|
+
def checkResultLogFile(self, jobName):
|
|
789
|
+
return True
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
class B2000FromBDFConverter(B2000ppCaller):
|
|
793
|
+
def __init__(self, **kwargs):
|
|
794
|
+
|
|
795
|
+
B2000ppCaller.__init__(self, **kwargs)
|
|
796
|
+
self.solverName = "b2convert_from_nas"
|
|
797
|
+
|
|
798
|
+
b2convTool = shutil.which("b2convert_from_nas")
|
|
799
|
+
|
|
800
|
+
toMdlFile = kwargs.pop("toMdlFile", None)
|
|
801
|
+
if toMdlFile is None:
|
|
802
|
+
base, _ = os.path.splitext(self.feFilename)
|
|
803
|
+
toMdlFile = f"{base}.mdl"
|
|
804
|
+
|
|
805
|
+
self.localCmds = [b2convTool, self.feFilename, toMdlFile]
|
|
806
|
+
|
|
807
|
+
def generateSolverSpecificRemoteCmds(self):
|
|
808
|
+
"""doc"""
|
|
809
|
+
baseFileInDir = os.path.basename(self.feFilename)
|
|
810
|
+
baseFile, _ = os.path.splitext(baseFileInDir)
|
|
811
|
+
|
|
812
|
+
runScriptCmds = [
|
|
813
|
+
"module load env/spack",
|
|
814
|
+
"module load rev/23.05_r8",
|
|
815
|
+
"module use /sw/DLR/FA/BS/STM/modulefiles",
|
|
816
|
+
"module load b2000++/4.6.3",
|
|
817
|
+
f"b2convert_from_nas {baseFileInDir} {baseFile}.mdl",
|
|
818
|
+
]
|
|
819
|
+
return runScriptCmds
|
|
820
|
+
|
|
821
|
+
def checkResultLogFile(self, jobName):
|
|
822
|
+
return True
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
class B2MToPickleConverterCARA(B2000ppCaller):
|
|
826
|
+
|
|
827
|
+
REMOTE_SCRIPT = "run_b2mToPickle.sh"
|
|
828
|
+
solverName = "b2mToPickle"
|
|
829
|
+
|
|
830
|
+
def __init__(self, **kwargs):
|
|
831
|
+
|
|
832
|
+
B2000ppCaller.__init__(self, **kwargs)
|
|
833
|
+
|
|
834
|
+
self.pythonScriptPath = kwargs.pop("pythonScript", None)
|
|
835
|
+
if not self.pythonScriptPath:
|
|
836
|
+
raise Exception("python script was not given!")
|
|
837
|
+
|
|
838
|
+
pyArguments = kwargs.pop("arguments", [])
|
|
839
|
+
|
|
840
|
+
callParams = ["python", self.pythonScriptPath] + pyArguments
|
|
841
|
+
|
|
842
|
+
baseFile, _ = os.path.splitext(self.feFilename)
|
|
843
|
+
callParams += [">", f"{baseFile}.msg"]
|
|
844
|
+
|
|
845
|
+
self.localCmds = callParams
|
|
846
|
+
|
|
847
|
+
def generateSolverSpecificRemoteCmds(self):
|
|
848
|
+
"""doc"""
|
|
849
|
+
baseFileInDir = os.path.basename(self.feFilename)
|
|
850
|
+
baseFile, _ = os.path.splitext(baseFileInDir)
|
|
851
|
+
|
|
852
|
+
pyScriptBaseDir = os.path.dirname(self.pythonScriptPath)
|
|
853
|
+
pyScriptBaseFile = os.path.basename(self.pythonScriptPath)
|
|
854
|
+
|
|
855
|
+
if pyScriptBaseDir != self.runDir:
|
|
856
|
+
shutil.copy(self.pythonScriptPath, self.runDir)
|
|
857
|
+
|
|
858
|
+
callParms = self.localCmds[:]
|
|
859
|
+
callParms[1] = pyScriptBaseFile
|
|
860
|
+
callParms[-1] = f"{baseFile}.msg"
|
|
861
|
+
|
|
862
|
+
runScriptCmds = []
|
|
863
|
+
if "cara" in self.clusterName:
|
|
864
|
+
runScriptCmds = [
|
|
865
|
+
"module load env/spack",
|
|
866
|
+
"module load rev/23.05_r8",
|
|
867
|
+
"module use /sw/DLR/FA/BS/STM/modulefiles",
|
|
868
|
+
"module load python-3.10",
|
|
869
|
+
"module load b2000++/4.6.3",
|
|
870
|
+
]
|
|
871
|
+
|
|
872
|
+
runScriptCmds += [" ".join(callParms)]
|
|
873
|
+
return runScriptCmds
|
|
874
|
+
|
|
875
|
+
def checkResultLogFile(self, jobName):
|
|
876
|
+
return True
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
class AsterCaller(FECaller):
|
|
880
|
+
def __init__(self, **kwargs):
|
|
881
|
+
FECaller.__init__(self, **kwargs)
|
|
882
|
+
self.solverName = "Code-Aster"
|
|
883
|
+
|
|
884
|
+
asterBat = kwargs.pop("as_run_script", shutil.which("as_run"))
|
|
885
|
+
if not asterBat:
|
|
886
|
+
raise Exception("Cannot find as_run script to run a code aster study")
|
|
887
|
+
|
|
888
|
+
self.localCmds = [f"{asterBat} --run {self.feFilename}"]
|
|
889
|
+
self.remoteCmds = []
|
|
890
|
+
|
|
891
|
+
def runLocal(self, jobName, **kwargs):
|
|
892
|
+
"""doc"""
|
|
893
|
+
self.localCmds = [jobName if elem == "<jobName>" else elem for elem in self.localCmds]
|
|
894
|
+
|
|
895
|
+
if "win" in sys.platform:
|
|
896
|
+
cmd = self.localCmds[0]
|
|
897
|
+
else:
|
|
898
|
+
cmd = ";".join(self.localCmds)
|
|
899
|
+
|
|
900
|
+
log.info(f"call {self.solverName} locally ")
|
|
901
|
+
log.debug(f"call {self.solverName} with the following command: {cmd}")
|
|
902
|
+
|
|
903
|
+
if "win" in sys.platform:
|
|
904
|
+
as_run_file = re.search("(.*) --run", cmd).group(1)
|
|
905
|
+
if not os.path.exists(as_run_file):
|
|
906
|
+
raise InternalError(
|
|
907
|
+
"The given executable does not exist. Please enter the correct path to settings.py. "
|
|
908
|
+
+ f"Acutal path: {as_run_file}"
|
|
909
|
+
)
|
|
910
|
+
|
|
911
|
+
p = subprocess.Popen(cmd, cwd=self.runDir, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
912
|
+
output, output_err = p.communicate()
|
|
913
|
+
p.wait()
|
|
914
|
+
|
|
915
|
+
retVal = p.returncode
|
|
916
|
+
if retVal != 0:
|
|
917
|
+
|
|
918
|
+
prefix = os.path.splitext(self.feFilename)[0]
|
|
919
|
+
logFileAster = os.path.join(self.runDir, f"{prefix}_aster_out.log")
|
|
920
|
+
logFileAsterErr = os.path.join(self.runDir, f"{prefix}_aster_err.log")
|
|
921
|
+
|
|
922
|
+
with open(logFileAster, "w") as f:
|
|
923
|
+
f.write(output.decode("utf-8", "ignore").strip())
|
|
924
|
+
|
|
925
|
+
if any("SLURM" in key for key in os.environ.keys()):
|
|
926
|
+
"""Write all outputs to one file to reduce File I/O on HPC"""
|
|
927
|
+
write_mode = "a"
|
|
928
|
+
logFileAsterErr = logFileAster
|
|
929
|
+
else:
|
|
930
|
+
write_mode = "w"
|
|
931
|
+
|
|
932
|
+
with open(logFileAsterErr, write_mode) as f:
|
|
933
|
+
f.write(output_err.decode("utf-8", "ignore").strip())
|
|
934
|
+
|
|
935
|
+
log.info(f'return value of {self.solverName} call is "{retVal}"')
|
|
936
|
+
return retVal
|
|
937
|
+
|
|
938
|
+
def checkResultLogFile(self, jobName):
|
|
939
|
+
return True
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
class VegaCaller(FECaller):
|
|
943
|
+
def __init__(self, **kwargs):
|
|
944
|
+
FECaller.__init__(self, **kwargs)
|
|
945
|
+
self.solverName = "Vega"
|
|
946
|
+
|
|
947
|
+
vega_executable = kwargs.pop("vega_executable", shutil.which("vegapp"))
|
|
948
|
+
if not vega_executable:
|
|
949
|
+
raise Exception("Cannot find vegapp to convert fe files to code-aster")
|
|
950
|
+
|
|
951
|
+
as_run_script = kwargs.pop("as_run_script", shutil.which("as_run"))
|
|
952
|
+
availAsterVers = self.getAvailableAsterVersions(availAsterVers=True, as_run_script=as_run_script)
|
|
953
|
+
|
|
954
|
+
asterVersion = next((vers for vers in availAsterVers if vers != "testing"), "testing")
|
|
955
|
+
self.localCmds = [
|
|
956
|
+
vega_executable,
|
|
957
|
+
"-o",
|
|
958
|
+
self.runDir,
|
|
959
|
+
"--solver-version",
|
|
960
|
+
asterVersion,
|
|
961
|
+
self.feFilename,
|
|
962
|
+
"nastran",
|
|
963
|
+
"aster",
|
|
964
|
+
]
|
|
965
|
+
self.remoteCmds = []
|
|
966
|
+
|
|
967
|
+
def getAvailableAsterVersions(self, raiseOnError=False, as_run_script=None):
|
|
968
|
+
"""doc"""
|
|
969
|
+
if as_run_script is None:
|
|
970
|
+
as_run_script = shutil.which("as_run")
|
|
971
|
+
|
|
972
|
+
if not as_run_script and raiseOnError:
|
|
973
|
+
raise Exception("Code-Aster not found in system path")
|
|
974
|
+
|
|
975
|
+
p = subprocess.run([as_run_script, "--info"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
976
|
+
|
|
977
|
+
describe = p.stderr if p.stdout == b"" else p.stdout
|
|
978
|
+
describe = describe.decode("utf-8").strip()
|
|
979
|
+
pattern = re.compile("@VERSIONS@(.*?)@FINVERSIONS@", re.RegexFlag.DOTALL)
|
|
980
|
+
res = pattern.search(describe).group(1)
|
|
981
|
+
|
|
982
|
+
versions = re.findall(r"(?<=vers : )([\w\._-]+)", res, re.RegexFlag.MULTILINE)
|
|
983
|
+
return versions
|
|
984
|
+
|
|
985
|
+
def checkResultLogFile(self, jobName):
|
|
986
|
+
return True
|
|
987
|
+
|
|
988
|
+
@staticmethod
|
|
989
|
+
def isLicenseAvailable():
|
|
990
|
+
"""
|
|
991
|
+
Code Aster does not need any license! :-)
|
|
992
|
+
"""
|
|
993
|
+
return True
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
def availableFlexlmLicenseTokens(licenseServerString, licenseType):
|
|
997
|
+
"""This method returns the number of available license tokens for a specific flexlm-based
|
|
998
|
+
licensing system.
|
|
999
|
+
|
|
1000
|
+
:param licenseServerString: string to the flexlm server including port
|
|
1001
|
+
:param licenseType: type of license. It is the name after "Users of"
|
|
1002
|
+
"""
|
|
1003
|
+
lmutilExe = _checkLmutils()
|
|
1004
|
+
lmutilStdout, lmutilStderr = _callLmUtils(licenseServerString, lmutilExe=lmutilExe)
|
|
1005
|
+
return _parseLmUtilsReturn(lmutilStdout, lmutilStderr, licenseType)
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
def _checkLmutils():
|
|
1009
|
+
"""This method checks if lmutils can be found on the system path.
|
|
1010
|
+
|
|
1011
|
+
:return: None
|
|
1012
|
+
:raise FileNotFoundError: if "lmutil" was not found on the system path or in the current dir
|
|
1013
|
+
"""
|
|
1014
|
+
lmutilPath = shutil.which("lmutil")
|
|
1015
|
+
if lmutilPath is None:
|
|
1016
|
+
msg = '"lmutil" was not found on the system path to perform a license check. '
|
|
1017
|
+
msg += "Please provide the path to lmutil in the system path."
|
|
1018
|
+
raise FileNotFoundError(msg)
|
|
1019
|
+
|
|
1020
|
+
return lmutilPath
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
def _callLmUtils(licenseServerString, lmutilExe="lmutil"):
|
|
1024
|
+
"""doc"""
|
|
1025
|
+
|
|
1026
|
+
lmTools = subprocess.Popen([lmutilExe, "lmstat", "-a", "-c", licenseServerString], stdout=subprocess.PIPE)
|
|
1027
|
+
lmutilStdout, lmutilStderr = lmTools.communicate()
|
|
1028
|
+
return lmutilStdout, lmutilStderr
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
def _parseLmUtilsReturn(lmutilStdout, lmutilStderr, licenseType):
|
|
1032
|
+
"""doc"""
|
|
1033
|
+
if lmutilStderr is None:
|
|
1034
|
+
lmOutString = str(lmutilStdout.decode("utf-8"))
|
|
1035
|
+
licenseLine = f"Users of {licenseType}"
|
|
1036
|
+
for line in lmOutString.split("\n"):
|
|
1037
|
+
if licenseLine in line:
|
|
1038
|
+
# Count number of licenses available and used
|
|
1039
|
+
numbers = array(re.findall(r"\d+", line), dtype=int)
|
|
1040
|
+
if len(numbers) < 2:
|
|
1041
|
+
log.warning(f'License state of type {licenseType} could not be obtained. Got this line : "{line}"')
|
|
1042
|
+
return 0
|
|
1043
|
+
return numbers[0] - numbers[1]
|
|
1044
|
+
|
|
1045
|
+
log.warning(f"could not find licenseType: {licenseType}")
|
|
1046
|
+
log.debug("Flexlm output: " + str(lmutilStdout))
|
|
1047
|
+
return 0
|
|
1048
|
+
else:
|
|
1049
|
+
# this is the stderr part - it should be empty
|
|
1050
|
+
raise BaseException("problems with return from license server")
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
def run_fe_cli():
|
|
1054
|
+
"""doc"""
|
|
1055
|
+
parser = argparse.ArgumentParser(description="Run FE Model")
|
|
1056
|
+
parser.add_argument("feFile")
|
|
1057
|
+
parser.add_argument("-r", "--calc_remote", action="store_true", dest="calc_remote")
|
|
1058
|
+
parser.add_argument("-u", "--user_remote", type=str, dest="user_remote")
|
|
1059
|
+
|
|
1060
|
+
options = parser.parse_args()
|
|
1061
|
+
feFile = os.path.abspath(options.feFile)
|
|
1062
|
+
_, ext = os.path.splitext(feFile)
|
|
1063
|
+
if ext == ".bdf":
|
|
1064
|
+
fecallClass = NastranCaller
|
|
1065
|
+
elif ext == ".mdl":
|
|
1066
|
+
fecallClass = B2000ppCaller
|
|
1067
|
+
elif ext == ".inp":
|
|
1068
|
+
fecallClass = AbaqusCaller
|
|
1069
|
+
elif ext in [".mac", ".ans"]:
|
|
1070
|
+
fecallClass = AnsysCaller
|
|
1071
|
+
else:
|
|
1072
|
+
msg = f"Unknown file extension: {ext}. The following extensions and tools are supported:\n"
|
|
1073
|
+
msg += "Abaqus: .inp\n"
|
|
1074
|
+
msg += "Nastran: .bdf\n"
|
|
1075
|
+
msg += "B2000++: .mdl\n"
|
|
1076
|
+
msg += "ANSYS: .mac|.ans\n"
|
|
1077
|
+
raise Exception(msg)
|
|
1078
|
+
|
|
1079
|
+
if options.calc_remote and (options.user_remote is None):
|
|
1080
|
+
msg = "FE model should be executed remote on CARA but no username was given as parameter. "
|
|
1081
|
+
msg += "Please specify the username using the option '-u <USERNAME>' ."
|
|
1082
|
+
raise Exception(msg)
|
|
1083
|
+
|
|
1084
|
+
fecall = fecallClass(feFilename=feFile)
|
|
1085
|
+
fecall.run(doRemoteCall=options.calc_remote)
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
if __name__ == "__main__":
|
|
1089
|
+
print(NastranCaller.isLicenseAvailable())
|
|
1090
|
+
# AbaqusCaller.isLicenseAvailable()
|
|
1091
|
+
# AnsysCaller.isLicenseAvailable()
|
|
1092
|
+
pass
|