Download src/Mod/Fem/femsolver/run.py from AbdulElahGwaith/FreeCAD: direct link, hf CLI and curl.
- Browser
- Download file 21.4 kB
-
https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/Fem/femsolver/run.py
- Command line
-
hf download hf://AbdulElahGwaith/FreeCAD/src/Mod/Fem/femsolver/run.py
-
curl -L -o run.py https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/Fem/femsolver/run.py
21.4 kB
| # *************************************************************************** | |
| # * Copyright (c) 2017 Markus Hovorka <m.hovorka@live.de> * | |
| # * Copyright (c) 2017 Bernd Hahnebach <bernd@bimstatik.org> * | |
| # * * | |
| # * This file is part of the FreeCAD CAx development system. * | |
| # * * | |
| # * This program is free software; you can redistribute it and/or modify * | |
| # * it under the terms of the GNU Lesser General Public License (LGPL) * | |
| # * as published by the Free Software Foundation; either version 2 of * | |
| # * the License, or (at your option) any later version. * | |
| # * for detail see the LICENCE text file. * | |
| # * * | |
| # * This program is distributed in the hope that it will be useful, * | |
| # * but WITHOUT ANY WARRANTY; without even the implied warranty of * | |
| # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * | |
| # * GNU Library General Public License for more details. * | |
| # * * | |
| # * You should have received a copy of the GNU Library General Public * | |
| # * License along with this program; if not, write to the Free Software * | |
| # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * | |
| # * USA * | |
| # * * | |
| # *************************************************************************** | |
| """Execute Solver and obtain Reports and Results. | |
| Integral part of the Solver Framework which contains components responsible for | |
| executing the solver in the background. Also provides an asynchronous | |
| communication system with the solver running in the background. The purpose of | |
| this module is to be as generic as possible. It can execute every solver | |
| supported by the fem workbench. The threading and communication support is | |
| mainly implemented by the :mod:`femsolver.task` and :mod:`femsolver.signal` | |
| modules. | |
| """ | |
| __title__ = "FreeCAD FEM solver run" | |
| __author__ = "Markus Hovorka, Bernd Hahnebach" | |
| __url__ = "https://www.freecad.org" | |
| import os | |
| import os.path | |
| import shutil | |
| import tempfile | |
| # import threading # not used ATM | |
| import FreeCAD as App | |
| from . import settings | |
| from . import signal | |
| from . import task | |
| from femtools import femutils | |
| from femtools import membertools | |
| from femtools.errors import DirectoryDoesNotExistError | |
| from femtools.errors import MustSaveError | |
| if App.GuiUp: | |
| from PySide import QtGui | |
| import FreeCADGui | |
| CHECK = 0 | |
| PREPARE = 1 | |
| SOLVE = 2 | |
| RESULTS = 3 | |
| DONE = 4 | |
| _machines = {} | |
| _dirTypes = {} | |
| def run_fem_solver(solver, working_dir=None): | |
| """Execute *solver* of the solver framework. | |
| Uses :meth:`getMachine <femsolver.solverbase.Proxy.getMachine>` to obtain a | |
| :class:`Machine` instance of the solver. It than executes the Machine with | |
| using the ``RESULTS`` target (see :class:`Machine` for infos about | |
| different targets). This method is blocking, it waits for the solver to | |
| finished before returning. Be aware of :class:`Machine` caching when using | |
| the function. | |
| :param solver: | |
| A document object which must be a framework compliant solver. This means | |
| that it should be derived from the document object provided by | |
| :mod:`femsolver.solverbase` and implement all required methods | |
| correctly. Of particular importance is :meth:`getMachine | |
| <femsolver.solverbase.Proxy.getMachine>` as it is used by this method | |
| the get the :class:`Machine` used to execute the solver. | |
| :param working_dir: | |
| If specified it overwrites the automatic and user configurable working | |
| directory management of the Solver framework. Should always be a | |
| absolute path because the location of the binary is not consistent | |
| among platforms. If ``None`` the automatic working directory management | |
| is used. | |
| :note: | |
| There is some legacy code to execute the old Calculix solver | |
| (pre-framework) which behaives differently because it does not | |
| use a :class:`Machine`. | |
| """ | |
| if solver.Proxy.Type == "Fem::SolverCcxTools": | |
| from femtools.ccxtools import CcxTools as ccx | |
| App.Console.PrintMessage("Run of CalxuliX ccx tools solver started.\n") | |
| fea = ccx(solver) | |
| fea.reset_mesh_purge_results_checked() | |
| if working_dir is None: | |
| fea.run() # standard, no working dir is given in solver | |
| else: | |
| # not the standard way | |
| fea.update_objects() | |
| fea.setup_working_dir(working_dir) | |
| fea.setup_ccx() | |
| message = fea.check_prerequisites() | |
| if not message: | |
| fea.write_inp_file() | |
| fea.ccx_run() | |
| fea.load_results() | |
| else: | |
| App.Console.PrintError(f"Houston, we have a problem...!\n{message}\n") | |
| App.Console.PrintMessage("Run of CalxuliX ccx tools solver finished.\n") | |
| else: | |
| # App.Console.PrintMessage("Frame work solver!\n") | |
| try: | |
| if working_dir is not None: | |
| machine = getMachine(solver, working_dir) | |
| else: | |
| machine = getMachine(solver) | |
| except MustSaveError: | |
| error_message = ( | |
| "Please save the file before executing the solver. " | |
| "This must be done because the location of the working " | |
| 'directory is set to "Beside *.FCStd File".' | |
| ) | |
| App.Console.PrintError(error_message + "\n") | |
| if App.GuiUp: | |
| QtGui.QMessageBox.critical( | |
| FreeCADGui.getMainWindow(), "Can't start Solver", error_message | |
| ) | |
| return | |
| except DirectoryDoesNotExistError: | |
| error_message = "Selected working directory doesn't exist." | |
| App.Console.PrintError(error_message + "\n") | |
| if App.GuiUp: | |
| QtGui.QMessageBox.critical( | |
| FreeCADGui.getMainWindow(), "Can't start Solver", error_message | |
| ) | |
| return | |
| if not machine.running: | |
| machine.reset() | |
| machine.target = RESULTS | |
| machine.start() | |
| machine.join() # wait for the machine to finish. | |
| if machine.failed is True: | |
| App.Console.PrintError("Machine failed to run.\n") | |
| from .report import displayLog | |
| displayLog(machine.report) | |
| if App.GuiUp: | |
| error_message = ( | |
| "Failed to run. Please try again after all " | |
| "of the following errors are resolved." | |
| ) | |
| from .report import display | |
| display(machine.report, "Run Report", error_message) | |
| def getMachine(solver, path=None): | |
| """Get or create :class:`Machine` using caching mechanism. | |
| :param solver: | |
| A document object which must be a framework compliant solver. This means | |
| that it should be derived from the document object provided by | |
| :mod:`femsolver.solverbase` and implement all required methods | |
| correctly. Of particular importance is :meth:`getMachine | |
| <femsolver.solverbase.Proxy.getMachine>` as it is used by this method | |
| to create a new :class:`Machine` on cache miss. | |
| :param path: | |
| A valid filesystem path which shall be associetad with the machine. | |
| """ | |
| # print(path) | |
| _DocObserver.attach() | |
| m = _machines.get(solver) | |
| if m is None or not _isPathValid(m, path): | |
| m = _createMachine(solver, path, testmode=False) | |
| # print(m.__dir__()) # document these attributes somewhere | |
| # print(m.directory) | |
| return m | |
| def _isPathValid(m, path): | |
| t = _dirTypes.get(m.directory) # setting default None | |
| setting = settings.get_dir_setting() | |
| if path is not None: | |
| return t is None and m.directory == path | |
| if setting == settings.DirSetting.BESIDE: | |
| if t == settings.DirSetting.BESIDE: | |
| base = os.path.split(m.directory.rstrip("/"))[0] | |
| return base == _getBesideBase(m.solver) | |
| return False | |
| if setting == settings.DirSetting.TEMPORARY: | |
| return t == settings.DirSetting.TEMPORARY | |
| if setting == settings.DirSetting.CUSTOM: | |
| if t == settings.DirSetting.CUSTOM: | |
| firstBase = os.path.split(m.directory.rstrip("/"))[0] | |
| customBase = os.path.split(firstBase)[0] | |
| return customBase == _getCustomBase(m.solver) | |
| return False | |
| def _createMachine(solver, path, testmode): | |
| global _dirTypes | |
| setting = settings.get_dir_setting() | |
| if path is not None: | |
| _dirTypes[path] = None | |
| elif setting == settings.DirSetting.BESIDE: | |
| path = _getBesideDir(solver) | |
| _dirTypes[path] = settings.DirSetting.BESIDE | |
| elif setting == settings.DirSetting.TEMPORARY: | |
| path = _getTempDir(solver) | |
| _dirTypes[path] = settings.DirSetting.TEMPORARY | |
| elif setting == settings.DirSetting.CUSTOM: | |
| path = _getCustomDir(solver) | |
| _dirTypes[path] = settings.DirSetting.CUSTOM | |
| m = solver.Proxy.createMachine(solver, path, testmode) | |
| oldMachine = _machines.get(solver) | |
| if oldMachine is not None and _dirTypes.get(oldMachine.directory) is not None: | |
| del _dirTypes[oldMachine.directory] | |
| _machines[solver] = m | |
| return m | |
| def _getTempDir(solver): | |
| return tempfile.mkdtemp(prefix="fem") | |
| def _getBesideDir(solver): | |
| base = _getBesideBase(solver) | |
| specificPath = os.path.join(base, solver.Label) | |
| specificPath = _getUniquePath(specificPath) | |
| if not os.path.isdir(specificPath): | |
| os.makedirs(specificPath) | |
| return specificPath | |
| def _getBesideBase(solver): | |
| path = os.path.splitext(solver.Document.FileName)[0] | |
| # doc=App.newDocument() | |
| # doc.FileName | |
| # the above returns an empty string in FreeCAD 0.19 | |
| # https://forum.freecad.org/viewtopic.php?f=10&t=48842 | |
| if path == "": | |
| error_message = ( | |
| "Please save the file before executing the solver. " | |
| "This must be done because the location of the working " | |
| 'directory is set to "Beside *.FCStd File".' | |
| ) | |
| App.Console.PrintError(error_message + "\n") | |
| if App.GuiUp: | |
| QtGui.QMessageBox.critical( | |
| FreeCADGui.getMainWindow(), "Can't start Solver", error_message | |
| ) | |
| raise MustSaveError() | |
| # TODO may be do not abort but use a temporary directory | |
| return path | |
| def _getCustomDir(solver): | |
| base = _getCustomBase(solver) | |
| specificPath = os.path.join(base, solver.Document.Name, solver.Label) | |
| specificPath = _getUniquePath(specificPath) | |
| if not os.path.isdir(specificPath): | |
| os.makedirs(specificPath) | |
| return specificPath | |
| def _getCustomBase(solver): | |
| path = settings.get_custom_dir() | |
| if not os.path.isdir(path): | |
| error_message = "Selected working directory doesn't exist." | |
| App.Console.PrintError(error_message + "\n") | |
| if App.GuiUp: | |
| QtGui.QMessageBox.critical( | |
| FreeCADGui.getMainWindow(), "Can't start Solver", error_message | |
| ) | |
| raise DirectoryDoesNotExistError("Invalid path") | |
| return path | |
| def _getUniquePath(path): | |
| postfix = 1 | |
| if path in _dirTypes: | |
| path += "_%03d" % postfix | |
| while path in _dirTypes: | |
| postfix += 1 | |
| path = path[:-4] + "_%03d" % postfix | |
| return path | |
| class BaseTask(task.Thread): | |
| def __init__(self): | |
| super().__init__() | |
| self.solver = None | |
| self.directory = None | |
| self.testmode = None | |
| def analysis(self): | |
| return self.solver.getParentGroup() | |
| class Machine(BaseTask): | |
| def __init__(self, solver, directory, check, prepare, solve, results, testmode): | |
| super().__init__() | |
| self.solver = solver | |
| self.directory = directory | |
| self.signalState = set() | |
| self.check = check | |
| self.prepare = prepare | |
| self.solve = solve | |
| self.results = results | |
| self.target = RESULTS | |
| self._state = CHECK | |
| self._pendingState = None | |
| self._isReset = False | |
| self.testmode = testmode | |
| def state(self): | |
| return self._state | |
| def run(self): | |
| self._confTasks() | |
| self._isReset = False | |
| self._pendingState = self.state | |
| while not self.aborted and not self.failed and self._pendingState <= self.target: | |
| task = self._getTask(self._pendingState) | |
| self._runTask(task) | |
| self.report.extend(task.report) | |
| if task.failed: | |
| self.fail() | |
| elif task.aborted: | |
| self.abort() | |
| else: | |
| self._pendingState += 1 | |
| self._applyPending() | |
| def reset(self, newState=CHECK): | |
| state = self.state if self._pendingState is None else self._pendingState | |
| if newState < state: | |
| self._isReset = True | |
| self._state = newState | |
| signal.notify(self.signalState) | |
| def _confTasks(self): | |
| tasks = [self.check, self.prepare, self.solve, self.results] | |
| for t in tasks: | |
| t.solver = self.solver | |
| t.directory = self.directory | |
| t.testmode = self.testmode | |
| def _applyPending(self): | |
| if not self._isReset: | |
| self._state = self._pendingState | |
| signal.notify(self.signalState) | |
| self._isReset = False | |
| self._pendingState = None | |
| def _runTask(self, task): | |
| def statusProxy(line): | |
| self.pushStatus(line) | |
| def killer(): | |
| task.abort() | |
| self.signalAbort.add(killer) | |
| task.signalStatus.add(statusProxy) | |
| task.start() | |
| task.join() | |
| self.signalAbort.remove(killer) | |
| task.signalStatus.remove(statusProxy) | |
| def _getTask(self, state): | |
| if state == CHECK: | |
| return self.check | |
| elif state == PREPARE: | |
| return self.prepare | |
| elif state == SOLVE: | |
| return self.solve | |
| elif state == RESULTS: | |
| return self.results | |
| return None | |
| class Check(BaseTask): | |
| def get_several_member(self, t): | |
| return membertools.get_several_member(self.analysis, t) | |
| def check_mesh_exists(self): | |
| meshes = self.get_several_member("Fem::FemMeshObject") | |
| if len(meshes) == 0: | |
| self.report.error("Missing a mesh object.") | |
| self.fail() | |
| return False | |
| elif len(meshes) > 1: | |
| self.report.error("Too many meshes. More than one mesh is not supported.") | |
| self.fail() | |
| return False | |
| return True | |
| def check_material_exists(self): | |
| objs = self.get_several_member("App::MaterialObjectPython") | |
| if len(objs) == 0: | |
| self.report.error("Missing a material object. At least one material is required.") | |
| self.fail() | |
| return False | |
| return True | |
| def check_material_single(self): | |
| objs = self.get_several_member("App::MaterialObjectPython") | |
| if len(objs) > 1: | |
| self.report.error("Only one Material is supported for this solver.") | |
| self.fail() | |
| return False | |
| return True | |
| def check_geos_beamsection_no(self): | |
| objs = self.get_several_member("Fem::ElementGeometry1D") | |
| if len(objs) > 0: | |
| self.report.error("Beamsections are not supported for this solver.") | |
| self.fail() | |
| return False | |
| return True | |
| def check_geos_beamsection_single(self): | |
| objs = self.get_several_member("Fem::ElementGeometry1D") | |
| if len(objs) > 1: | |
| self.report.error("Only one beamsection is supported for this solver.") | |
| self.fail() | |
| return False | |
| return True | |
| def check_geos_shellthickness_no(self): | |
| objs = self.get_several_member("Fem::ElementGeometry2D") | |
| if len(objs) > 0: | |
| self.report.error("Shellsections are not supported for this solver.") | |
| self.fail() | |
| return False | |
| return True | |
| def check_geos_shellthickness_single(self): | |
| objs = self.get_several_member("Fem::ElementGeometry2D") | |
| if len(objs) > 1: | |
| self.report.error("Only one shellthickness is supported for this solver.") | |
| self.fail() | |
| return False | |
| return True | |
| def check_geos_beamsection_and_shellthickness(self): | |
| beamsec_obj = self.get_several_member("Fem::ElementGeometry1D") | |
| shellth_obj = self.get_several_member("Fem::ElementGeometry2D") | |
| if len(beamsec_obj) > 0 and len(shellth_obj) > 0: | |
| self.report.error( | |
| "Either beamsection or shellthickness objects are " | |
| "supported for this solver, but not both in one analysis." | |
| ) | |
| self.fail() | |
| return False | |
| return True | |
| def checkSupported(self, allSupported): | |
| for m in self.analysis.Group: | |
| if femutils.is_of_type(m, "Fem::Constraint"): | |
| supported = False | |
| for sc in allSupported: | |
| if femutils.is_of_type(m, *sc): | |
| supported = True | |
| if not supported: | |
| self.report.warning(f"Ignored unsupported constraint: {m.Label}") | |
| return True | |
| class Solve(BaseTask): | |
| def _observeSolver(self, process): | |
| output = "" | |
| line = femutils.pydecode(process.stdout.readline()) | |
| self.pushStatus(line) | |
| output += line | |
| line = femutils.pydecode(process.stdout.readline()) | |
| while line: | |
| line = "\n%s" % line.rstrip() | |
| self.pushStatus(line) | |
| output += line | |
| line = femutils.pydecode(process.stdout.readline()) | |
| return output | |
| class Prepare(BaseTask): | |
| pass | |
| class Results(BaseTask): | |
| pass | |
| class _DocObserver: | |
| _instance = None | |
| _WHITELIST = [ | |
| "Fem::Constraint", | |
| "App::MaterialObject", | |
| "Fem::FemMeshObject", | |
| ] | |
| _BLACKLIST_PROPS = ["Label", "ElmerOutput", "ElmerResult"] | |
| def __init__(self): | |
| self._saved = {} | |
| for doc in iter(App.listDocuments().values()): | |
| for obj in doc.Objects: | |
| if obj.isDerivedFrom("Fem::FemAnalysis"): | |
| self._saved[obj] = obj.Group | |
| def attach(cls): | |
| if cls._instance is None: | |
| cls._instance = cls() | |
| App.addDocumentObserver(cls._instance) | |
| def slotDeletedObject(self, obj): | |
| self._checkModel(obj) | |
| if obj in _machines: | |
| self._deleteMachine(obj) | |
| def slotChangedObject(self, obj, prop): | |
| if prop not in self._BLACKLIST_PROPS: | |
| self._checkAnalysis(obj) | |
| self._checkEquation(obj) | |
| self._checkSolver(obj) | |
| self._checkModel(obj) | |
| def slotDeletedDocument(self, doc): | |
| for obj in doc.Objects: | |
| if obj in _machines: | |
| self._deleteMachine(obj) | |
| def _deleteMachine(self, obj): | |
| m = _machines[obj] | |
| t = _dirTypes[m.directory] | |
| m.abort() | |
| if t == settings.DirSetting.TEMPORARY: | |
| shutil.rmtree(m.directory) | |
| del _machines[obj] | |
| del _dirTypes[m.directory] | |
| def _checkEquation(self, obj): | |
| for o in obj.Document.Objects: | |
| if ( | |
| femutils.is_derived_from(o, "Fem::FemSolverObject") | |
| and hasattr(o, "Group") | |
| and obj in o.Group | |
| ): | |
| if o in _machines: | |
| _machines[o].reset() | |
| def _checkSolver(self, obj): | |
| analysis = obj.getParentGroup() | |
| for m in iter(_machines.values()): | |
| if analysis == m.analysis and obj == m.solver: | |
| m.reset() | |
| def _checkAnalysis(self, obj): | |
| if femutils.is_derived_from(obj, "Fem::FemAnalysis"): | |
| deltaObjs = self._getAdded(obj) | |
| if deltaObjs: | |
| reset = False | |
| for o in deltaObjs: | |
| if self._partOfModel(o): | |
| reset = True | |
| if reset: | |
| self._resetAll(obj) | |
| def _checkModel(self, obj): | |
| if self._partOfModel(obj): | |
| analysis = obj.getParentGroup() | |
| if analysis is not None: | |
| self._resetAll(analysis) | |
| def _getAdded(self, analysis): | |
| if analysis not in self._saved: | |
| self._saved[analysis] = [] | |
| delta = set(analysis.Group) - set(self._saved[analysis]) | |
| self._saved[analysis] = analysis.Group | |
| return delta | |
| def _resetAll(self, analysis): | |
| for m in iter(_machines.values()): | |
| if analysis == m.analysis: | |
| m.reset() | |
| def _partOfModel(self, obj): | |
| for t in self._WHITELIST: | |
| if femutils.is_derived_from(obj, t): | |
| return True | |
| return False | |