Fem: Add base class for solver and mesher tools
(cherry picked from commit f8e5a92b98)
This commit is contained in:
@@ -516,6 +516,7 @@ SET(FemTools_SRCS
|
||||
femtools/geomtools.py
|
||||
femtools/membertools.py
|
||||
femtools/migrate_app.py
|
||||
femtools/objecttools.py
|
||||
femtools/tokrules.py
|
||||
)
|
||||
|
||||
|
||||
@@ -29,9 +29,6 @@ __url__ = "https://www.freecad.org"
|
||||
# \ingroup FEM
|
||||
# \brief FreeCAD FEM command definitions
|
||||
|
||||
from PySide import QtCore
|
||||
from PySide import QtGui
|
||||
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
from FreeCAD import Qt
|
||||
@@ -1174,51 +1171,11 @@ class _SolverRun(CommandManager):
|
||||
self.tool = None
|
||||
|
||||
def Activated(self):
|
||||
if self.selobj.Proxy.Type in ["Fem::SolverCalculiX", "Fem::SolverElmer"]:
|
||||
try:
|
||||
QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor)
|
||||
self._set_tool()
|
||||
self._conn(self.tool)
|
||||
self.tool.prepare()
|
||||
self.tool.compute()
|
||||
except Exception as e:
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
FreeCAD.Console.PrintError(e)
|
||||
return
|
||||
from femsolver.run import run_fem_solver
|
||||
|
||||
else:
|
||||
from femsolver.run import run_fem_solver
|
||||
|
||||
run_fem_solver(self.selobj)
|
||||
FreeCADGui.Selection.clearSelection()
|
||||
FreeCAD.ActiveDocument.recompute()
|
||||
|
||||
def _set_tool(self):
|
||||
match self.selobj.Proxy.Type:
|
||||
case "Fem::SolverCalculiX":
|
||||
from femsolver.calculix.calculixtools import CalculiXTools
|
||||
|
||||
self.tool = CalculiXTools(self.selobj)
|
||||
case "Fem::SolverElmer":
|
||||
from femsolver.elmer.elmertools import ElmerTools
|
||||
|
||||
self.tool = ElmerTools(self.selobj)
|
||||
|
||||
def _conn(self, tool):
|
||||
QtCore.QObject.connect(
|
||||
tool.process,
|
||||
QtCore.SIGNAL("finished(int, QProcess::ExitStatus)"),
|
||||
self._process_finished,
|
||||
)
|
||||
|
||||
def _process_finished(self, code, status):
|
||||
if status == QtCore.QProcess.ExitStatus.NormalExit and code == 0:
|
||||
self.tool.update_properties()
|
||||
FreeCAD.ActiveDocument.recompute()
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
else:
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
FreeCAD.Console.PrintError("Process finished with errors. Result not updated\n")
|
||||
run_fem_solver(self.selobj)
|
||||
FreeCADGui.Selection.clearSelection()
|
||||
FreeCAD.ActiveDocument.recompute()
|
||||
|
||||
|
||||
class _SolverZ88(CommandManager):
|
||||
|
||||
@@ -41,32 +41,21 @@ import Fem
|
||||
from . import meshtools
|
||||
from femtools import femutils
|
||||
from femtools import geomtools
|
||||
from femtools.objecttools import ObjectTools
|
||||
|
||||
|
||||
class GmshError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class GmshTools:
|
||||
class GmshTools(ObjectTools):
|
||||
|
||||
name = "Gmsh"
|
||||
|
||||
def __init__(self, gmsh_mesh_obj, analysis=None):
|
||||
|
||||
# mesh obj
|
||||
self.mesh_obj = gmsh_mesh_obj
|
||||
|
||||
self.process = QProcess()
|
||||
# analysis
|
||||
self.analysis = None
|
||||
if analysis:
|
||||
self.analysis = analysis
|
||||
else:
|
||||
for i in self.mesh_obj.InList:
|
||||
if i.isDerivedFrom("Fem::FemAnalysis"):
|
||||
self.analysis = i
|
||||
break
|
||||
|
||||
def __init__(self, obj):
|
||||
super().__init__(obj)
|
||||
self.mesh_obj = obj
|
||||
self.analysis = obj.getParentGroup()
|
||||
self.load_properties()
|
||||
self.error = False
|
||||
|
||||
@@ -232,10 +221,8 @@ class GmshTools:
|
||||
self.rename_groups()
|
||||
|
||||
def create_mesh(self):
|
||||
self.prepare()
|
||||
p = self.compute()
|
||||
p.waitForFinished()
|
||||
self.update_properties()
|
||||
# for backward compatibility only
|
||||
self.run(True)
|
||||
|
||||
def start_logs(self):
|
||||
Console.PrintLog("\nGmsh FEM mesh run is being started.\n")
|
||||
|
||||
@@ -34,9 +34,10 @@ from PySide.QtCore import QProcess, QThread, QProcessEnvironment
|
||||
import FreeCAD
|
||||
import Fem
|
||||
from freecad import utils
|
||||
from femtools.objecttools import ObjectTools
|
||||
|
||||
|
||||
class NetgenTools:
|
||||
class NetgenTools(ObjectTools):
|
||||
|
||||
# to change order of nodes from netgen to smesh
|
||||
order_edge = {
|
||||
@@ -73,11 +74,9 @@ class NetgenTools:
|
||||
__param_grp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Fem/Netgen")
|
||||
|
||||
def __init__(self, obj):
|
||||
self.obj = obj
|
||||
super().__init__(obj)
|
||||
self.fem_mesh = None
|
||||
self.process = None
|
||||
self.tmpdir = ""
|
||||
self.process = QProcess()
|
||||
self.mesh_params = {}
|
||||
|
||||
def write_geom(self):
|
||||
@@ -267,6 +266,9 @@ def run_netgen(
|
||||
|
||||
np.save(result_file, [result, groups])
|
||||
|
||||
# remove traceback
|
||||
sys.excepthook = lambda type, value, traceback: print(value)
|
||||
|
||||
run_netgen(**{kwds})
|
||||
"""
|
||||
|
||||
|
||||
@@ -37,42 +37,18 @@ import Fem
|
||||
from . import writer
|
||||
from .. import settings
|
||||
|
||||
# from feminout import importCcxDatResults
|
||||
from femmesh import meshsetsgetter
|
||||
from femtools import membertools
|
||||
from femtools.objecttools import ObjectTools
|
||||
|
||||
|
||||
class CalculiXTools:
|
||||
class CalculiXTools(ObjectTools):
|
||||
|
||||
name = "CalculiX"
|
||||
|
||||
def __init__(self, obj):
|
||||
self.obj = obj
|
||||
self.process = QProcess()
|
||||
super().__init__(obj)
|
||||
self.model_file = ""
|
||||
self.analysis = obj.getParentGroup()
|
||||
self.fem_param = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Fem")
|
||||
self._create_working_directory(obj)
|
||||
|
||||
def _create_working_directory(self, obj):
|
||||
"""
|
||||
Create working directory according to preferences
|
||||
"""
|
||||
if not os.path.isdir(obj.WorkingDirectory):
|
||||
gen_param = self.fem_param.GetGroup("General")
|
||||
if gen_param.GetBool("UseTempDirectory"):
|
||||
self.obj.WorkingDirectory = tempfile.mkdtemp(prefix="fem_")
|
||||
elif gen_param.GetBool("UseBesideDirectory"):
|
||||
root, ext = os.path.splitext(obj.Document.FileName)
|
||||
if root:
|
||||
self.obj.WorkingDirectory = os.path.join(root, obj.Label)
|
||||
os.makedirs(self.obj.WorkingDirectory, exist_ok=True)
|
||||
else:
|
||||
# file not saved, use temporary
|
||||
self.obj.WorkingDirectory = tempfile.mkdtemp(prefix="fem_")
|
||||
elif gen_param.GetBool("UseCustomDirectory"):
|
||||
self.obj.WorkingDirectory = gen_param.GetString("CustomDirectoryPath")
|
||||
os.makedirs(self.obj.WorkingDirectory, exist_ok=True)
|
||||
|
||||
def prepare(self):
|
||||
from femtools.checksanalysis import check_member_for_solver_calculix
|
||||
|
||||
@@ -27,7 +27,6 @@ __url__ = "https://www.freecad.org"
|
||||
|
||||
|
||||
from PySide.QtCore import QProcess, QProcessEnvironment
|
||||
import tempfile
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
@@ -38,41 +37,18 @@ from . import writer
|
||||
from .. import settings
|
||||
|
||||
from femtools import membertools
|
||||
from femtools.objecttools import ObjectTools
|
||||
|
||||
|
||||
class ElmerTools:
|
||||
class ElmerTools(ObjectTools):
|
||||
|
||||
name = "Elmer"
|
||||
|
||||
def __init__(self, obj):
|
||||
self.obj = obj
|
||||
self.process = QProcess()
|
||||
super().__init__(obj)
|
||||
self.model_file = ""
|
||||
self.analysis = obj.getParentGroup()
|
||||
self.fem_param = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Fem")
|
||||
self._create_working_directory(obj)
|
||||
self._result_format = ""
|
||||
|
||||
def _create_working_directory(self, obj):
|
||||
"""
|
||||
Create working directory according to preferences
|
||||
"""
|
||||
if not os.path.isdir(obj.WorkingDirectory):
|
||||
gen_param = self.fem_param.GetGroup("General")
|
||||
if gen_param.GetBool("UseTempDirectory"):
|
||||
self.obj.WorkingDirectory = tempfile.mkdtemp(prefix="fem_")
|
||||
elif gen_param.GetBool("UseBesideDirectory"):
|
||||
root, ext = os.path.splitext(obj.Document.FileName)
|
||||
if root:
|
||||
self.obj.WorkingDirectory = os.path.join(root, obj.Label)
|
||||
os.makedirs(self.obj.WorkingDirectory, exist_ok=True)
|
||||
else:
|
||||
# file not saved, use temporary
|
||||
self.obj.WorkingDirectory = tempfile.mkdtemp(prefix="fem_")
|
||||
elif gen_param.GetBool("UseCustomDirectory"):
|
||||
self.obj.WorkingDirectory = gen_param.GetString("CustomDirectoryPath")
|
||||
os.makedirs(self.obj.WorkingDirectory, exist_ok=True)
|
||||
|
||||
def prepare(self):
|
||||
w = writer.Writer(self.obj, self.obj.WorkingDirectory)
|
||||
w.write_solver_input()
|
||||
@@ -91,7 +67,7 @@ class ElmerTools:
|
||||
p.setWorkingDirectory(self.obj.WorkingDirectory)
|
||||
grid_args = ["8", "2", mesh_file, "-out", self.obj.WorkingDirectory]
|
||||
p.start(grid_bin, grid_args)
|
||||
p.waitForFinished()
|
||||
p.waitForFinished(-1)
|
||||
num_proc = self.fem_param.GetGroup("Elmer").GetInt("NumberOfTasks", 1)
|
||||
if num_proc > 1:
|
||||
# MPI parallel computing version
|
||||
|
||||
@@ -40,6 +40,7 @@ import os
|
||||
import os.path
|
||||
import shutil
|
||||
import tempfile
|
||||
from PySide import QtCore
|
||||
|
||||
# import threading # not used ATM
|
||||
|
||||
@@ -48,6 +49,8 @@ import FreeCAD as App
|
||||
from . import settings
|
||||
from . import signal
|
||||
from . import task
|
||||
from femsolver.elmer import elmertools
|
||||
from femsolver.calculix import calculixtools
|
||||
from femtools import femutils
|
||||
from femtools import membertools
|
||||
from femtools.errors import DirectoryDoesNotExistError
|
||||
@@ -69,7 +72,7 @@ _machines = {}
|
||||
_dirTypes = {}
|
||||
|
||||
|
||||
def run_fem_solver(solver, working_dir=None):
|
||||
def run_fem_solver(solver, working_dir=None, blocking=False):
|
||||
"""Execute *solver* of the solver framework.
|
||||
|
||||
Uses :meth:`getMachine <femsolver.solverbase.Proxy.getMachine>` to obtain a
|
||||
@@ -100,6 +103,35 @@ def run_fem_solver(solver, working_dir=None):
|
||||
use a :class:`Machine`.
|
||||
"""
|
||||
|
||||
tool = None
|
||||
if working_dir:
|
||||
solver.WorkingDirectory = working_dir
|
||||
|
||||
match solver.Proxy.Type:
|
||||
case "Fem::SolverElmer":
|
||||
tool = elmertools.ElmerTools(solver)
|
||||
case "Fem::SolverCalculiX":
|
||||
tool = calculixtools.CalculiXTools(solver)
|
||||
|
||||
if tool is not None:
|
||||
# Redirect process error to report view
|
||||
print_error = lambda: App.Console.PrintError(
|
||||
tool.process.readAllStandardError().data().decode("utf-8")
|
||||
)
|
||||
tool.process.readyReadStandardError.connect(print_error)
|
||||
tool.process.finished.connect(_solver_finish(solver))
|
||||
try:
|
||||
if App.GuiUp:
|
||||
QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor)
|
||||
tool.run(blocking)
|
||||
except Exception as e:
|
||||
if App.GuiUp:
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
raise e
|
||||
return
|
||||
|
||||
# code for old solver implementations
|
||||
|
||||
if solver.Proxy.Type == "Fem::SolverCcxTools":
|
||||
from femtools.ccxtools import CcxTools as ccx
|
||||
|
||||
@@ -192,6 +224,17 @@ def getMachine(solver, path=None):
|
||||
return m
|
||||
|
||||
|
||||
def _solver_finish(obj):
|
||||
def receiver(code, status):
|
||||
if status != QtCore.QProcess.ExitStatus.NormalExit or code != 0:
|
||||
App.Console.PrintError("Solver finished with errors. Result not updated\n")
|
||||
if App.GuiUp:
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
obj.Document.recompute()
|
||||
|
||||
return receiver
|
||||
|
||||
|
||||
def _isPathValid(m, path):
|
||||
t = _dirTypes.get(m.directory) # setting default None
|
||||
setting = settings.get_dir_setting()
|
||||
|
||||
@@ -138,7 +138,6 @@ class _BaseLogTaskPanel(base_femtaskpanel._BaseTaskPanel, ABC):
|
||||
QtGui.QColor(getOutputWinColor("Error")),
|
||||
)
|
||||
return
|
||||
self.tool.update_properties()
|
||||
self.write_log("Process finished\n", QtGui.QColor(getOutputWinColor("Text")))
|
||||
|
||||
def process_started(self):
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
# ***************************************************************************
|
||||
# * Copyright (c) 2026 Mario Passaglia <mpassaglia[at]cbc.uba.ar> *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD 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 *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
__title__ = "Abstract base class for the work with solvers and meshers"
|
||||
__author__ = "Mario Passaglia"
|
||||
__url__ = "https://www.freecad.org"
|
||||
|
||||
|
||||
from PySide.QtCore import QProcess
|
||||
from abc import ABC, abstractmethod
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import FreeCAD
|
||||
|
||||
|
||||
class ObjectTools(ABC):
|
||||
"""Abstract base class for the work with solvers and meshers"""
|
||||
|
||||
def __init__(self, obj):
|
||||
obj.Tool = self
|
||||
self.obj = obj
|
||||
self.process = QProcess()
|
||||
self.analysis = obj.getParentGroup()
|
||||
self.fem_param = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Fem")
|
||||
self._create_working_directory(obj)
|
||||
|
||||
self.process.finished.connect(self._process_finished)
|
||||
|
||||
def _create_working_directory(self, obj):
|
||||
"""
|
||||
Create working directory according to preferences
|
||||
"""
|
||||
if not os.path.isdir(obj.WorkingDirectory):
|
||||
gen_param = self.fem_param.GetGroup("General")
|
||||
if gen_param.GetBool("UseTempDirectory"):
|
||||
self.obj.WorkingDirectory = tempfile.mkdtemp(prefix="fem_")
|
||||
elif gen_param.GetBool("UseBesideDirectory"):
|
||||
root, ext = os.path.splitext(obj.Document.FileName)
|
||||
if root:
|
||||
self.obj.WorkingDirectory = os.path.join(root, obj.Label)
|
||||
os.makedirs(self.obj.WorkingDirectory, exist_ok=True)
|
||||
else:
|
||||
# file not saved, use temporary
|
||||
self.obj.WorkingDirectory = tempfile.mkdtemp(prefix="fem_")
|
||||
elif gen_param.GetBool("UseCustomDirectory"):
|
||||
self.obj.WorkingDirectory = gen_param.GetString("CustomDirectoryPath")
|
||||
os.makedirs(self.obj.WorkingDirectory, exist_ok=True)
|
||||
|
||||
@abstractmethod
|
||||
def prepare(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def compute(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update_properties(self):
|
||||
pass
|
||||
|
||||
def run(self, blocking=False):
|
||||
self.prepare()
|
||||
self.compute()
|
||||
if blocking:
|
||||
return self.process.waitForFinished(-1)
|
||||
return None
|
||||
|
||||
def _process_finished(self, code, status):
|
||||
if status == QProcess.ExitStatus.NormalExit and code == 0:
|
||||
self.update_properties()
|
||||
Reference in New Issue
Block a user