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.

Files changed (46) hide show
  1. patme/__init__.py +52 -0
  2. patme/buildtools/__init__.py +7 -0
  3. patme/buildtools/rce_releasecreator.py +336 -0
  4. patme/buildtools/release.py +26 -0
  5. patme/femtools/__init__.py +5 -0
  6. patme/femtools/abqmsgfilechecker.py +137 -0
  7. patme/femtools/fecall.py +1092 -0
  8. patme/geometry/__init__.py +0 -0
  9. patme/geometry/area.py +124 -0
  10. patme/geometry/coordinatesystem.py +635 -0
  11. patme/geometry/intersect.py +284 -0
  12. patme/geometry/line.py +183 -0
  13. patme/geometry/misc.py +420 -0
  14. patme/geometry/plane.py +464 -0
  15. patme/geometry/rotate.py +244 -0
  16. patme/geometry/scale.py +152 -0
  17. patme/geometry/shape2d.py +50 -0
  18. patme/geometry/transformations.py +1831 -0
  19. patme/geometry/translate.py +139 -0
  20. patme/mechanics/__init__.py +4 -0
  21. patme/mechanics/loads.py +435 -0
  22. patme/mechanics/material.py +1260 -0
  23. patme/service/__init__.py +7 -0
  24. patme/service/decorators.py +85 -0
  25. patme/service/duration.py +96 -0
  26. patme/service/exceptionhook.py +104 -0
  27. patme/service/exceptions.py +36 -0
  28. patme/service/io/__init__.py +3 -0
  29. patme/service/io/basewriter.py +122 -0
  30. patme/service/logger.py +375 -0
  31. patme/service/mathutils.py +108 -0
  32. patme/service/misc.py +71 -0
  33. patme/service/moveimports.py +217 -0
  34. patme/service/stringutils.py +419 -0
  35. patme/service/systemutils.py +290 -0
  36. patme/sshtools/__init__.py +3 -0
  37. patme/sshtools/cara.py +435 -0
  38. patme/sshtools/clustercaller.py +420 -0
  39. patme/sshtools/facluster.py +350 -0
  40. patme/sshtools/sshcall.py +168 -0
  41. patme-0.4.4.dist-info/LICENSE +21 -0
  42. patme-0.4.4.dist-info/LICENSES/MIT.txt +9 -0
  43. patme-0.4.4.dist-info/METADATA +168 -0
  44. patme-0.4.4.dist-info/RECORD +46 -0
  45. patme-0.4.4.dist-info/WHEEL +4 -0
  46. patme-0.4.4.dist-info/entry_points.txt +3 -0
@@ -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