Fem: Add base class for solver and mesher tools

(cherry picked from commit f8e5a92b98)
This commit is contained in:
marioalexis
2026-02-21 15:36:36 -06:00
committed by Chris Hennes
parent 860aa6a45b
commit 4e1c3933d7
9 changed files with 161 additions and 129 deletions
+1
View File
@@ -516,6 +516,7 @@ SET(FemTools_SRCS
femtools/geomtools.py
femtools/membertools.py
femtools/migrate_app.py
femtools/objecttools.py
femtools/tokrules.py
)
+4 -47
View File
@@ -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):
+8 -21
View File
@@ -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")
+6 -4
View File
@@ -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
+4 -28
View File
@@ -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
+44 -1
View File
@@ -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):
+91
View File
@@ -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()