From 243b42f4cbedfbd659cb28a6300720ed94a378d6 Mon Sep 17 00:00:00 2001 From: marioalexis Date: Wed, 29 Oct 2025 01:44:26 -0300 Subject: [PATCH] Fem: Rework Elmer solver - fixes #21479 --- src/Mod/Fem/CMakeLists.txt | 6 +- src/Mod/Fem/Gui/CMakeLists.txt | 1 + src/Mod/Fem/Gui/DlgSettingsFemElmer.ui | 34 +- src/Mod/Fem/Gui/DlgSettingsFemElmerImp.cpp | 6 +- src/Mod/Fem/Gui/DlgSettingsFemElmerImp.h | 1 - src/Mod/Fem/Gui/Resources/Fem.qrc | 1 + src/Mod/Fem/Gui/Resources/ui/SolverElmer.ui | 147 ++++++++ src/Mod/Fem/ObjectsFem.py | 10 +- src/Mod/Fem/femcommands/commands.py | 22 +- src/Mod/Fem/femobjects/solver_elmer.py | 163 +++++++++ src/Mod/Fem/femsolver/elmer/elmertools.py | 175 +++++++++ src/Mod/Fem/femsolver/elmer/solver.py | 242 ------------- src/Mod/Fem/femsolver/elmer/tasks.py | 339 ------------------ src/Mod/Fem/femsolver/elmer/writer.py | 162 +-------- .../Fem/femtaskpanels/task_solver_elmer.py | 157 ++++++++ .../Fem/femviewprovider/view_solver_elmer.py | 51 +++ 16 files changed, 764 insertions(+), 753 deletions(-) create mode 100644 src/Mod/Fem/Gui/Resources/ui/SolverElmer.ui create mode 100644 src/Mod/Fem/femobjects/solver_elmer.py create mode 100644 src/Mod/Fem/femsolver/elmer/elmertools.py delete mode 100644 src/Mod/Fem/femsolver/elmer/solver.py delete mode 100644 src/Mod/Fem/femsolver/elmer/tasks.py create mode 100644 src/Mod/Fem/femtaskpanels/task_solver_elmer.py create mode 100644 src/Mod/Fem/femviewprovider/view_solver_elmer.py diff --git a/src/Mod/Fem/CMakeLists.txt b/src/Mod/Fem/CMakeLists.txt index 6a00241619..8f634afb87 100755 --- a/src/Mod/Fem/CMakeLists.txt +++ b/src/Mod/Fem/CMakeLists.txt @@ -211,6 +211,7 @@ SET(FemObjects_SRCS femobjects/result_mechanical.py femobjects/solver_calculix.py femobjects/solver_ccxtools.py + femobjects/solver_elmer.py ) if(BUILD_FEM_VTK_PYTHON) @@ -285,9 +286,8 @@ SET(FemSolverCalculix_SRCS SET(FemSolverElmer_SRCS femsolver/elmer/__init__.py femsolver/elmer/sifio.py - femsolver/elmer/solver.py - femsolver/elmer/tasks.py femsolver/elmer/writer.py + femsolver/elmer/elmertools.py ) SET(FemSolverElmerEquations_SRCS @@ -631,6 +631,7 @@ SET(FemGuiTaskPanels_SRCS femtaskpanels/task_result_mechanical.py femtaskpanels/task_solver_calculix.py femtaskpanels/task_solver_ccxtools.py + femtaskpanels/task_solver_elmer.py ) if(BUILD_FEM_VTK_PYTHON) @@ -701,6 +702,7 @@ SET(FemGuiViewProvider_SRCS femviewprovider/view_result_mechanical.py femviewprovider/view_solver_calculix.py femviewprovider/view_solver_ccxtools.py + femviewprovider/view_solver_elmer.py ) if(BUILD_FEM_VTK_PYTHON) diff --git a/src/Mod/Fem/Gui/CMakeLists.txt b/src/Mod/Fem/Gui/CMakeLists.txt index 74c7a339a4..8b06dc096e 100755 --- a/src/Mod/Fem/Gui/CMakeLists.txt +++ b/src/Mod/Fem/Gui/CMakeLists.txt @@ -449,6 +449,7 @@ SET(FemGuiPythonUI_SRCS Resources/ui/ResultShow.ui Resources/ui/SolverCalculiX.ui Resources/ui/SolverCcxTools.ui + Resources/ui/SolverElmer.ui Resources/ui/TaskPostGlyph.ui Resources/ui/TaskPostExtraction.ui Resources/ui/TaskPostHistogram.ui diff --git a/src/Mod/Fem/Gui/DlgSettingsFemElmer.ui b/src/Mod/Fem/Gui/DlgSettingsFemElmer.ui index 9efc747758..05531a9d1c 100644 --- a/src/Mod/Fem/Gui/DlgSettingsFemElmer.ui +++ b/src/Mod/Fem/Gui/DlgSettingsFemElmer.ui @@ -144,14 +144,14 @@ - + - Number of processes + Number of tasks - + Qt::AlignLeft|Qt::AlignTrailing|Qt::AlignVCenter @@ -162,7 +162,33 @@ 1 - UseNumberOfCores + NumberOfTasks + + + Mod/Fem/Elmer + + + + + + + Threads per task + + + + + + + Qt::AlignLeft|Qt::AlignTrailing|Qt::AlignVCenter + + + Number of threads per task. Take effect if Elmer uses OpenMP. + + + 1 + + + ThreadsPerTask Mod/Fem/Elmer diff --git a/src/Mod/Fem/Gui/DlgSettingsFemElmerImp.cpp b/src/Mod/Fem/Gui/DlgSettingsFemElmerImp.cpp index 5430b8723b..0edae3b094 100644 --- a/src/Mod/Fem/Gui/DlgSettingsFemElmerImp.cpp +++ b/src/Mod/Fem/Gui/DlgSettingsFemElmerImp.cpp @@ -54,7 +54,8 @@ void DlgSettingsFemElmerImp::saveSettings() ui->fc_elmer_binary_path->onSave(); ui->fc_grid_binary_path->onSave(); - ui->sb_num_processes->onSave(); + ui->sb_num_tasks->onSave(); + ui->sb_threads_per_task->onSave(); ui->ckb_binary_format->onSave(); ui->ckb_geom_id->onSave(); @@ -65,7 +66,8 @@ void DlgSettingsFemElmerImp::loadSettings() ui->fc_elmer_binary_path->onRestore(); ui->fc_grid_binary_path->onRestore(); - ui->sb_num_processes->onRestore(); + ui->sb_num_tasks->onRestore(); + ui->sb_threads_per_task->onRestore(); ui->ckb_binary_format->onRestore(); ui->ckb_geom_id->onRestore(); diff --git a/src/Mod/Fem/Gui/DlgSettingsFemElmerImp.h b/src/Mod/Fem/Gui/DlgSettingsFemElmerImp.h index 827dcfe136..8521089136 100644 --- a/src/Mod/Fem/Gui/DlgSettingsFemElmerImp.h +++ b/src/Mod/Fem/Gui/DlgSettingsFemElmerImp.h @@ -50,7 +50,6 @@ protected: private: std::unique_ptr ui; - int processor_count; }; } // namespace FemGui diff --git a/src/Mod/Fem/Gui/Resources/Fem.qrc b/src/Mod/Fem/Gui/Resources/Fem.qrc index 351dad3e48..f08d0b2e4c 100755 --- a/src/Mod/Fem/Gui/Resources/Fem.qrc +++ b/src/Mod/Fem/Gui/Resources/Fem.qrc @@ -156,6 +156,7 @@ ui/ResultShow.ui ui/SolverCalculiX.ui ui/SolverCcxTools.ui + ui/SolverElmer.ui ui/TaskPostGlyph.ui diff --git a/src/Mod/Fem/Gui/Resources/ui/SolverElmer.ui b/src/Mod/Fem/Gui/Resources/ui/SolverElmer.ui new file mode 100644 index 0000000000..c9d1635fad --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/SolverElmer.ui @@ -0,0 +1,147 @@ + + + SolverElmer + + + + 0 + 0 + 400 + 475 + + + + Solver Elmer Control + + + + + + Working directory + + + + + + + + + + + + + + + Write + + + + + + + false + + + Edit + + + + + + + + + + + Path to working directory + + + false + + + Gui::FileChooser::Mode::Directory + + + + + + + + + + + + Solver Parameters + + + + + + + + Simulation type + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + QTextEdit::NoWrap + + + true + + + + + + + + 12 + + + + Time + + + + + + + Solver Version + + + + + + + + + + + Gui::FileChooser + QWidget +
Gui/FileDialog.h
+
+
+ + +
diff --git a/src/Mod/Fem/ObjectsFem.py b/src/Mod/Fem/ObjectsFem.py index eeddb44748..32a8acf790 100644 --- a/src/Mod/Fem/ObjectsFem.py +++ b/src/Mod/Fem/ObjectsFem.py @@ -941,9 +941,15 @@ def makeSolverCalculiX(doc, name="SolverCalculiX"): def makeSolverElmer(doc, name="SolverElmer"): """makeSolverElmer(document, [name]): makes a Elmer solver object""" - import femsolver.elmer.solver + obj = doc.addObject("Fem::FemSolverObjectPython", name) + from femobjects import solver_elmer - obj = femsolver.elmer.solver.create(doc, name) + solver_elmer.SolverElmer(obj) + obj.SimulationType = "Steady State" + if FreeCAD.GuiUp: + from femviewprovider import view_solver_elmer + + view_solver_elmer.VPSolverElmer(obj.ViewObject) return obj diff --git a/src/Mod/Fem/femcommands/commands.py b/src/Mod/Fem/femcommands/commands.py index fc773ae7d7..bf0fb7c1f1 100644 --- a/src/Mod/Fem/femcommands/commands.py +++ b/src/Mod/Fem/femcommands/commands.py @@ -1174,18 +1174,17 @@ class _SolverRun(CommandManager): self.tool = None def Activated(self): - if self.selobj.Proxy.Type == "Fem::SolverCalculiX": - QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) + if self.selobj.Proxy.Type in ["Fem::SolverCalculiX", "Fem::SolverElmer"]: try: - from femsolver.calculix.calculixtools import CalculiXTools - - self.tool = CalculiXTools(self.selobj) + 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() - raise + FreeCAD.Console.PrintError(e) + return else: from femsolver.run import run_fem_solver @@ -1194,6 +1193,17 @@ class _SolverRun(CommandManager): 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, diff --git a/src/Mod/Fem/femobjects/solver_elmer.py b/src/Mod/Fem/femobjects/solver_elmer.py new file mode 100644 index 0000000000..c855c5083f --- /dev/null +++ b/src/Mod/Fem/femobjects/solver_elmer.py @@ -0,0 +1,163 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# *************************************************************************** +# * Copyright (c) 2017 Markus Hovorka * +# * Copyright (c) 2025 Mario Passaglia * +# * * +# * 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 * +# * . * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM solver Elmer document object" +__author__ = "Markus Hovorka, Mario Passaglia" +__url__ = "https://www.freecad.org" + +## @package solver_elmer +# \ingroup FEM +# \brief solver Elmer object + +from FreeCAD import Base +from . import base_fempythonobject + +_PropHelper = base_fempythonobject._PropHelper + + +class SolverElmer(base_fempythonobject.BaseFemPythonObject): + + Type = "Fem::SolverElmer" + + def __init__(self, obj): + super().__init__(obj) + obj.addExtension("App::GroupExtensionPython") + + for prop in self._get_properties(): + prop.add_to_object(obj) + + def _get_properties(self): + prop = [] + + prop.append( + _PropHelper( + type="App::PropertyEnumeration", + name="CoordinateSystem", + group="Solver", + doc="Type of the analysis", + value=[ + "Cartesian", + "Cartesian 1D", + "Cartesian 2D", + "Cartesian 3D", + "Polar 2D", + "Polar 3D", + "Cylindric", + "Cylindric Symmetric", + "Axi Symmetric", + ], + ) + ) + prop.append( + _PropHelper( + type="App::PropertyIntegerConstraint", + name="BDFOrder", + group="Timestepping", + doc="Order of time stepping method 'BDF'", + value={"value": 2, "min": 1, "max": 5}, + ) + ) + prop.append( + _PropHelper( + type="App::PropertyIntegerList", + name="OutputIntervals", + group="Timestepping", + doc="After how many time steps a result file is output", + value=[1], + ) + ) + prop.append( + _PropHelper( + type="App::PropertyIntegerList", + name="TimestepIntervals", + group="Timestepping", + doc="List of times if Simulation Type\n" + "is either `Scanning` or `Transient`", + value=[100], + ) + ) + prop.append( + _PropHelper( + type="App::PropertyFloatList", + name="TimestepSizes", + group="Timestepping", + doc="List of times steps if Simulation Type\n" + + "is either `Scanning` or `Transient`", + value=[0.1], + ) + ) + prop.append( + _PropHelper( + type="App::PropertyEnumeration", + name="SimulationType", + group="Solver", + doc="Simulation type", + value=["Scanning", "Steady State", "Transient"], + ) + ) + prop.append( + _PropHelper( + type="App::PropertyInteger", + name="SteadyStateMaxIterations", + group="Solver", + doc="Maximal steady state iterations", + value=1, + ) + ) + prop.append( + _PropHelper( + type="App::PropertyInteger", + name="SteadyStateMinIterations", + group="Solver", + doc="Minimal steady state iterations", + value=0, + ) + ) + prop.append( + _PropHelper( + type="App::PropertyBool", + name="BinaryOutput", + group="Solver", + doc="Save result in binary format", + value=False, + ) + ) + prop.append( + _PropHelper( + type="App::PropertyBool", + name="SaveGeometryIndex", + group="Solver", + doc="Save geometry IDs", + value=False, + ) + ) + + return prop + + def onDocumentRestored(self, obj): + # update old project with new properties + for prop in self._get_properties(): + try: + obj.getPropertyByName(prop.name) + except Base.PropertyError: + prop.add_to_object(obj) diff --git a/src/Mod/Fem/femsolver/elmer/elmertools.py b/src/Mod/Fem/femsolver/elmer/elmertools.py new file mode 100644 index 0000000000..2587a9d687 --- /dev/null +++ b/src/Mod/Fem/femsolver/elmer/elmertools.py @@ -0,0 +1,175 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# *************************************************************************** +# * Copyright (c) 2025 Mario Passaglia * +# * * +# * 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 * +# * . * +# * * +# *************************************************************************** + +__title__ = "Tools for the work with Elmer solver" +__author__ = "Mario Passaglia" +__url__ = "https://www.freecad.org" + + +from PySide.QtCore import QProcess, QProcessEnvironment +import tempfile +import os +import re +import shutil + +import FreeCAD + +from . import writer +from .. import settings + +from femtools import membertools + + +class ElmerTools: + + name = "Elmer" + + def __init__(self, obj): + self.obj = obj + self.process = QProcess() + 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() + + mesh = w.getSingleMember("Fem::FemMeshObject") + if not mesh.FemMesh.Groups: + raise ValueError(f"Mesh object '{mesh.Label}' has no groups, please remesh\n") + + mesh_file = os.path.join(self.obj.WorkingDirectory, "mesh.unv") + mesh.FemMesh.write(mesh_file) + + grid_bin = settings.get_binary("ElmerGrid") + env = QProcessEnvironment.systemEnvironment() + p = QProcess() + p.setProcessEnvironment(env) + p.setWorkingDirectory(self.obj.WorkingDirectory) + grid_args = ["8", "2", mesh_file, "-out", self.obj.WorkingDirectory] + p.start(grid_bin, grid_args) + p.waitForFinished() + num_proc = self.fem_param.GetGroup("Elmer").GetInt("NumberOfTasks", 1) + if num_proc > 1: + # MPI parallel computing version + grid_args.extend(["-partdual", "-metiskway", str(num_proc)]) + p.start(grid_bin, grid_args) + p.waitForFinished() + + self.model_file = os.path.join(self.obj.WorkingDirectory, writer._SIF_NAME) + handled = w.getHandledConstraints() + allConstraints = membertools.get_member(self.analysis, "Fem::Constraint") + for obj in set(allConstraints) - handled: + FreeCAD.Console.PrintWarning(f"Ignored constraint {obj.Label}") + + def compute(self): + self._clear_results() + elmer_bin = settings.get_binary("ElmerSolver") + num_proc = self.fem_param.GetGroup("Elmer").GetInt("NumberOfTasks", 1) + num_thr = self.fem_param.GetGroup("Elmer").GetInt("ThreadsPerTask", 1) + env = QProcessEnvironment.systemEnvironment() + env.insert("OMP_NUM_THREADS", str(num_thr)) + self.process.setProcessEnvironment(env) + self.process.setWorkingDirectory(self.obj.WorkingDirectory) + + if num_proc > 1: + # MPI parallel computing version + mpi = shutil.which("mpiexec") + self._result_format = ".pvtu" + command_list = ["-n", str(num_proc), elmer_bin] + self.process.start(mpi, command_list) + else: + self._result_format = ".vtu" + command_list = [] + self.process.start(elmer_bin, command_list) + + if self.obj.SimulationType == "Transient": + self._result_format = ".pvd" + + return self.process + + def update_properties(self): + keep_result = self.fem_param.GetGroup("General").GetBool("KeepResultsOnReRun", False) + if not self.obj.Results or keep_result: + pipeline = self.obj.Document.addObject("Fem::FemPostPipeline", self.obj.Name + "Result") + self.analysis.addObject(pipeline) + temp_res = self.obj.Results + temp_res.append(pipeline) + self.obj.Results = temp_res + self._load_results() + # default display mode + pipeline.ViewObject.DisplayMode = "Surface" + pipeline.ViewObject.SelectionStyle = "BoundBox" + else: + self._load_results() + + def _clear_results(self): + dir_content = os.listdir(self.obj.WorkingDirectory) + for f in dir_content: + path = os.path.join(self.obj.WorkingDirectory, f) + base, ext = os.path.splitext(path) + if ext in [".vtu", ".vtp", ".pvtu", ".pvd"]: + os.remove(path) + + def _load_results(self): + files = os.listdir(self.obj.WorkingDirectory) + for f in files: + base, ext = os.path.splitext(f) + if ext == self._result_format: + res = os.path.join(self.obj.WorkingDirectory, f) + self.obj.Results[-1].read(res) + break + + def version(self): + p = QProcess() + elmer_bin = settings.get_binary("ElmerSolver") + p.start(elmer_bin, ["-v"]) + p.waitForFinished() + info = p.readAll().data().decode() + reg_exp = re.compile(r"Version:\s*(?P.*)$", re.M) + m = reg_exp.search(info) + ver = "Version: {}".format(m.group("version") if m else "") + return ver diff --git a/src/Mod/Fem/femsolver/elmer/solver.py b/src/Mod/Fem/femsolver/elmer/solver.py deleted file mode 100644 index 077ddfd490..0000000000 --- a/src/Mod/Fem/femsolver/elmer/solver.py +++ /dev/null @@ -1,242 +0,0 @@ -# *************************************************************************** -# * Copyright (c) 2017 Markus Hovorka * -# * * -# * 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 * -# * * -# *************************************************************************** - -__title__ = "FreeCAD FEM solver object Elmer" -__author__ = "Markus Hovorka" -__url__ = "https://www.freecad.org" - -## \addtogroup FEM -# @{ - -import glob -import os - -import FreeCAD - -from . import tasks -from .equations import deformation -from .equations import elasticity -from .equations import electricforce -from .equations import electrostatic -from .equations import flow -from .equations import flux -from .equations import heat -from .equations import magnetodynamic -from .equations import magnetodynamic2D -from .. import run -from .. import solverbase -from femtools import femutils - -if FreeCAD.GuiUp: - import FemGui - -COORDINATE_SYSTEM = [ - "Cartesian", - "Cartesian 1D", - "Cartesian 2D", - "Cartesian 3D", - "Polar 2D", - "Polar 3D", - "Cylindric", - "Cylindric Symmetric", - "Axi Symmetric", -] -SIMULATION_TYPE = ["Scanning", "Steady State", "Transient"] - - -def create(doc, name="ElmerSolver"): - return femutils.createObject(doc, name, Proxy, ViewProxy) - - -class Proxy(solverbase.Proxy): - """Proxy for FemSolverElmers Document Object.""" - - Type = "Fem::SolverElmer" - - _EQUATIONS = { - "Deformation": deformation, - "Elasticity": elasticity, - "Electrostatic": electrostatic, - "Flux": flux, - "Electricforce": electricforce, - "Flow": flow, - "Heat": heat, - "Magnetodynamic": magnetodynamic, - "Magnetodynamic2D": magnetodynamic2D, - } - - def __init__(self, obj): - super().__init__(obj) - - obj.addProperty( - "App::PropertyEnumeration", "CoordinateSystem", "Coordinate System", "", locked=True - ) - obj.CoordinateSystem = COORDINATE_SYSTEM - obj.CoordinateSystem = "Cartesian" - - obj.addProperty( - "App::PropertyIntegerConstraint", - "BDFOrder", - "Timestepping", - "Order of time stepping method 'BDF'", - locked=True, - ) - # according to the Elmer manual recommended is order 2 - # possible range is 1 - 5 - obj.BDFOrder = (2, 1, 5, 1) - - obj.addProperty( - "App::PropertyIntegerList", - "OutputIntervals", - "Timestepping", - "After how many time steps a result file is output", - locked=True, - ) - obj.OutputIntervals = [1] - - obj.addProperty( - "App::PropertyIntegerList", - "TimestepIntervals", - "Timestepping", - ("List of times if 'Simulation Type'\nis either 'Scanning' or 'Transient'"), - locked=True, - ) - obj.addProperty( - "App::PropertyFloatList", - "TimestepSizes", - "Timestepping", - ( - "List of time steps sizes if 'Simulation Type'\n" - "is either 'Scanning' or 'Transient'" - ), - locked=True, - ) - # there is no universal default, it all depends on the analysis, however - # we have to set something and set 10 seconds in steps of 0.1s - obj.TimestepIntervals = [100] - obj.TimestepSizes = [0.1] - - obj.addProperty("App::PropertyEnumeration", "SimulationType", "Type", "") - obj.SimulationType = SIMULATION_TYPE - obj.SimulationType = "Steady State" - - obj.addProperty( - "App::PropertyInteger", - "SteadyStateMaxIterations", - "Type", - "Maximal steady state iterations", - locked=True, - ) - obj.SteadyStateMaxIterations = 1 - - obj.addProperty( - "App::PropertyInteger", - "SteadyStateMinIterations", - "Type", - "Minimal steady state iterations", - locked=True, - ) - obj.SteadyStateMinIterations = 0 - - obj.addProperty("App::PropertyLink", "ElmerResult", "Base", "", 4 | 8, locked=True) - - obj.addProperty("App::PropertyLink", "ElmerOutput", "Base", "", 4 | 8, locked=True) - - obj.addProperty( - "App::PropertyBool", - "BinaryOutput", - "Result File", - "Save result in binary format", - locked=True, - ) - obj.BinaryOutput = False - - obj.addProperty( - "App::PropertyBool", - "SaveGeometryIndex", - "Result File", - "Save geometry IDs", - locked=True, - ) - obj.SaveGeometryIndex = False - - def onDocumentRestored(self, obj): - # update old project with new properties - try: - obj.getPropertyByName("BinaryOutput") - except FreeCAD.Base.PropertyError: - obj.addProperty( - "App::PropertyBool", - "BinaryOutput", - "Result File", - "Save result in binary format", - locked=True, - ) - obj.BinaryOutput = False - try: - obj.getPropertyByName("SaveGeometryIndex") - except FreeCAD.Base.PropertyError: - obj.addProperty( - "App::PropertyBool", - "SaveGeometryIndex", - "Result File", - "Save geometry IDs", - locked=True, - ) - obj.SaveGeometryIndex = False - - def createMachine(self, obj, directory, testmode=False): - return run.Machine( - solver=obj, - directory=directory, - check=tasks.Check(), - prepare=tasks.Prepare(), - solve=tasks.Solve(), - results=tasks.Results(), - testmode=testmode, - ) - - def createEquation(self, doc, eqId): - return self._EQUATIONS[eqId].create(doc) - - def isSupported(self, eqId): - return eqId in self._EQUATIONS - - def editSupported(self): - return True - - def edit(self, directory): - pattern = os.path.join(directory, "case.sif") - FreeCAD.Console.PrintMessage(f"{pattern}\n") - f = glob.glob(pattern)[0] - FemGui.open(f) - - -class ViewProxy(solverbase.ViewProxy): - """Proxy for FemSolverElmers View Provider.""" - - def getIcon(self): - return ":/icons/FEM_SolverElmer.svg" - - -## @} diff --git a/src/Mod/Fem/femsolver/elmer/tasks.py b/src/Mod/Fem/femsolver/elmer/tasks.py deleted file mode 100644 index 0f947a6ab2..0000000000 --- a/src/Mod/Fem/femsolver/elmer/tasks.py +++ /dev/null @@ -1,339 +0,0 @@ -# *************************************************************************** -# * Copyright (c) 2017 Markus Hovorka * -# * * -# * 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 * -# * * -# *************************************************************************** - -__title__ = "FreeCAD FEM solver Elmer tasks" -__author__ = "Markus Hovorka" -__url__ = "https://www.freecad.org" - -## \addtogroup FEM -# @{ - -import cmath -import os -import os.path -import subprocess -from platform import system - -import FreeCAD - -from . import writer -from .. import run -from .. import settings -from femtools import femutils -from femtools import membertools - - -class Check(run.Check): - - def run(self): - self.pushStatus("Checking analysis...\n") - if self.check_mesh_exists(): - self.checkMeshType() - self.check_material_exists() - self.checkEquations() - - def checkMeshType(self): - mesh = membertools.get_single_member(self.analysis, "Fem::FemMeshObject") - if not femutils.is_of_type(mesh, "Fem::FemMeshGmsh"): - self.report.error("Unsupported type of mesh. Mesh must be created with gmsh.") - self.fail() - return False - return True - - def checkEquations(self): - equations = self.solver.Group - if not equations: - self.report.error("Solver has no equations. Add at least one equation.") - self.fail() - - -class Prepare(run.Prepare): - - def run(self): - # TODO print working dir to report console - self.pushStatus("Preparing input files...\n") - num_cores = settings.get_cores("ElmerGrid") - self.pushStatus(f"Number of CPU cores to be used for the solver run: {num_cores}\n") - if self.testmode: - # test mode: neither gmsh, nor elmergrid nor elmersolver binaries needed - FreeCAD.Console.PrintMessage(f"Machine testmode: {self.testmode}\n") - w = writer.Writer(self.solver, self.directory, True) - else: - FreeCAD.Console.PrintLog(f"Machine testmode: {self.testmode}\n") - w = writer.Writer(self.solver, self.directory) - try: - w.write_solver_input() - self.checkHandled(w) - self.pushStatus("Writing solver input completed.") - except writer.WriteError as e: - self.report.error(str(e)) - self.fail() - except OSError: - self.report.error("Can't access working directory.") - self.fail() - - def checkHandled(self, w): - handled = w.getHandledConstraints() - allConstraints = membertools.get_member(self.analysis, "Fem::Constraint") - for obj in set(allConstraints) - handled: - self.report.warning("Ignored constraint %s." % obj.Label) - - -class Solve(run.Solve): - - def run(self): - # on rerun the result file will not deleted before starting the solver - # if the solver fails, the existing result from a former run file will be loaded - # TODO: delete result file (may be delete all files which will be recreated) - self.pushStatus("Executing solver...\n") - binary = settings.get_binary("ElmerSolver") - if binary is not None: - # if ELMER_HOME is not set, set it. - # Needed if elmer is compiled but not installed on Linux - # http://www.elmerfem.org/forum/viewtopic.php?f=2&t=7119 - # https://stackoverflow.com/questions/1506010/how-to-use-export-with-python-on-linux - # TODO move retrieving the param to solver settings module - elparams = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Fem/Elmer") - elmer_env = elparams.GetBool("SetElmerEnvVariables", False) - if elmer_env is True and system() == "Linux" and "ELMER_HOME" not in os.environ: - solvpath = os.path.split(binary)[0] - if os.path.isdir(solvpath): - os.environ["ELMER_HOME"] = solvpath - os.environ["LD_LIBRARY_PATH"] = f"$LD_LIBRARY_PATH:{solvpath}/modules" - # different call depending if with multithreading or not - num_cores = settings.get_cores("ElmerSolver") - self.pushStatus(f"Number of CPU cores to be used for the solver run: {num_cores}\n") - args = [] - if num_cores > 1: - if system() != "Windows": - args.extend(["mpirun"]) - else: - args.extend(["mpiexec"]) - args.extend(["-np", str(num_cores)]) - args.extend([binary]) - self._process = subprocess.Popen( - args, - cwd=self.directory, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - startupinfo=femutils.startProgramInfo("hide"), - ) - self.signalAbort.add(self._process.terminate) - output = self._observeSolver(self._process) - self._process.communicate() - self.signalAbort.remove(self._process.terminate) - if not self.aborted: - self._updateOutput(output) - else: - self.report.error("ElmerSolver binary not found.") - self.pushStatus("Error: ElmerSolver binary has not been found!") - self.fail() - - def _updateOutput(self, output): - if self.solver.ElmerOutput is None: - self._createOutput() - # check if eigenmodes were calculated and if so append them to output - output = self._calculateEigenfrequencies(output) - self.solver.ElmerOutput.Text = output - - def _createOutput(self): - self.solver.ElmerOutput = self.analysis.Document.addObject( - "App::TextDocument", self.solver.Name + "Output" - ) - self.solver.ElmerOutput.Label = self.solver.Label + "Output" - # App::TextDocument has no Attribute ReadOnly - # TODO check if the attribute has been removed from App::TextDocument - # self.solver.ElmerOutput.ReadOnly = True - self.analysis.addObject(self.solver.ElmerOutput) - self.solver.Document.recompute() - - def _calculateEigenfrequencies(self, output): - # takes the EigenSolve results and performs the calculation - # sqrt(aResult) / 2*PI but with aResult as complex number - - # first search the output file for the results - OutputList = output.split("\n") - modeNumber = 0 - modeCount = 0 - real = 0 - imaginary = 0 - haveImaginary = False - FrequencyList = [] - for line in OutputList: - LineList = line.split(" ") - if len(LineList) > 1 and LineList[0] == "EigenSolve:" and LineList[1] == "Computed": - # we found a result and take now the next LineList[2] lines - modeCount = int(LineList[2]) - modeNumber = modeCount - continue - if modeCount > 0: - for LineString in reversed(LineList): - # the output of Elmer may vary, we only know the last float - # is the imaginary and second to last float the real part - if self._isNumber(LineString): - if not haveImaginary: - imaginary = float(LineString) - haveImaginary = True - else: - real = float(LineString) - break - eigenFreq = complex(real, imaginary) - haveImaginary = False - # now we can perform the calculation - eigenFreq = cmath.sqrt(eigenFreq) / (2 * cmath.pi) - # create an output line - FrequencyList.append(f"Mode {modeNumber - modeCount + 1}: {eigenFreq.real} Hz") - modeCount = modeCount - 1 - if modeNumber > 0: - # push the results and append to output - self.pushStatus("\n\nEigenfrequency results:") - output = output + "\n\nEigenfrequency results:" - for i in range(0, modeNumber): - output = output + "\n" + FrequencyList[i] - self.pushStatus("\n" + FrequencyList[i]) - self.pushStatus("\n") - return output - - def _isNumber(self, string): - try: - float(string) - return True - except ValueError: - return False - - -class Results(run.Results): - - def run(self): - if self.solver.SimulationType == "Steady State": - self._handleStedyStateResult() - else: - self._handleTransientResults() - - def _handleStedyStateResult(self): - if self.solver.ElmerResult is None: - self._createResults() - postPath = self._getResultFile() - if postPath is None: - self.pushStatus("\nNo result file was created.\n") - self.fail() - return - self.solver.ElmerResult.read(postPath) - # at the moment we scale the mesh back using Elmer - # this might be changed in future, this commented code is left as info - # self.solver.ElmerResult.scale(1000) - - # for eigen analyses the resulting values are by a factor 1000 to high - # therefore scale all *EigenMode results - self.solver.ElmerResult.ViewObject.transformField("displacement EigenMode1", 0.001) - - self.solver.ElmerResult.recomputeChildren() - self.solver.Document.recompute() - # recompute() updated the result mesh data - # but not the shape and bar coloring - self.solver.ElmerResult.ViewObject.updateColorBars() - - def _createResults(self): - self.solver.ElmerResult = self.analysis.Document.addObject( - "Fem::FemPostPipeline", self.solver.Name + "Result" - ) - self.solver.ElmerResult.Label = self.solver.ElmerResult.Name - self.solver.ElmerResult.ViewObject.SelectionStyle = "BoundBox" - self.analysis.addObject(self.solver.ElmerResult) - # to assure the user sees something, set the default to Surface - self.solver.ElmerResult.ViewObject.DisplayMode = "Surface" - - def _handleTransientResults(self): - # for transient results we must create a result pipeline for every time - # the connection between result files and and their time is in the FreeCAD.pvd file - # therefore first open FreeCAD.pvd - pvdFilePath = os.path.join(self.directory, "FreeCAD.pvd") - if not os.path.exists(pvdFilePath): - self.pushStatus("\nNo result file was created.\n") - self.fail() - return - pvdFile = open(pvdFilePath) - # read all lines - pvdContent = pvdFile.readlines() - # skip header and footer line and evaluate all lines - # a line has the form like this: - # - # so .split("\"") gives as 2nd the time and as 7th the filename - files = [] - values = [] - for i in range(0, len(pvdContent) - 2): - # get time - lineArray = pvdContent[i + 1].split('"') - time = float(lineArray[1]) - filename = os.path.join(self.directory, lineArray[7]) - if os.path.isfile(filename): - values.append(time) - files.append(filename) - else: - self.pushStatus(f"\nResult file for time {time} is missing.\n") - self.fail() - return - - if self.solver.ElmerResult is None: - self._createResults() - - self.solver.ElmerResult.read(files, values, FreeCAD.Units.TimeSpan, "Time") - - # for eigen analyses the resulting values are by a factor 1000 to high - # therefore scale all *EigenMode results - self.solver.ElmerResult.ViewObject.transformField("displacement EigenMode1", 0.001) - - self.solver.ElmerResult.recomputeChildren() - self.solver.Document.recompute() - # recompute() updated the result mesh data - # but not the shape and bar coloring - self.solver.ElmerResult.ViewObject.updateColorBars() - - def _getResultFile(self): - postPath = None - # elmer post file path changed with version x.x - # see https://forum.freecad.org/viewtopic.php?f=18&t=42732 - # workaround - possible_post_file_old = os.path.join(self.directory, "case0001.vtu") - possible_post_file_single = os.path.join(self.directory, "FreeCAD_t0001.vtu") - possible_post_file_multi = os.path.join(self.directory, "FreeCAD_t0001.pvtu") - # depending on the currently set number of cores we try to load either - # the multi-thread result or the single result - if settings.get_cores("ElmerSolver") > 1: - if os.path.isfile(possible_post_file_multi): - postPath = possible_post_file_multi - else: - self.report.error("Result file not found.") - self.fail() - else: - if os.path.isfile(possible_post_file_single): - postPath = possible_post_file_single - elif os.path.isfile(possible_post_file_old): - postPath = possible_post_file_old - else: - self.report.error("Result file not found.") - self.fail() - return postPath - - -## @} diff --git a/src/Mod/Fem/femsolver/elmer/writer.py b/src/Mod/Fem/femsolver/elmer/writer.py index 974729667f..b311a09c33 100644 --- a/src/Mod/Fem/femsolver/elmer/writer.py +++ b/src/Mod/Fem/femsolver/elmer/writer.py @@ -42,9 +42,7 @@ from FreeCAD import ParamGet import Fem from . import sifio -from . import solver as solverClass from .. import settings -from femmesh import gmshtools from femtools import constants from femtools import femutils from femtools import membertools @@ -98,7 +96,7 @@ class Writer: def getHandledConstraints(self): return self._handledObjects - def write_solver_input(self): + def _writeBlocks(self): self._handleRedifinedConstants() self._handleSimulation() self._handleDeformation() @@ -113,8 +111,9 @@ class Writer: self._handleStaticCurrent() self._addOutputSolver() + def write_solver_input(self): + self._writeBlocks() self._writeSif() - self._writeMesh() self._writeStartinfo() def _handleUnits(self): @@ -212,90 +211,6 @@ class Writer: "BoltzmannConstant": constants.boltzmann_constant(), } - def _writeMesh(self): - mesh = self.getSingleMember("Fem::FemMeshObject") - unvPath = os.path.join(self.directory, "mesh.unv") - groups = [] - groups.extend(self._builder.getBodyNames()) - groups.extend(self._builder.getBoundaryNames()) - self._exportToUnv(groups, mesh, unvPath) - if self.testmode: - Console.PrintMessage( - "Solver Elmer testmode, ElmerGrid will not be used. It might not be installed.\n" - ) - else: - binary = settings.get_binary("ElmerGrid") - num_cores = settings.get_cores("ElmerGrid") - if binary is None: - raise WriteError("Could not find ElmerGrid binary.") - # for multithreading we first need a normal mesh creation run - # then a second to split the mesh into the number of used cores - argsBasic = [binary, _ELMERGRID_IFORMAT, _ELMERGRID_OFORMAT, unvPath] - args = argsBasic - args.extend(["-out", self.directory]) - if system() == "Windows": - subprocess.call( - args, stdout=subprocess.DEVNULL, startupinfo=femutils.startProgramInfo("hide") - ) - else: - subprocess.call(args, stdout=subprocess.DEVNULL) - if num_cores > 1: - args = argsBasic - args.extend(["-partdual", "-metiskway", str(num_cores), "-out", self.directory]) - if system() == "Windows": - subprocess.call( - args, - stdout=subprocess.DEVNULL, - startupinfo=femutils.startProgramInfo("hide"), - ) - else: - subprocess.call(args, stdout=subprocess.DEVNULL) - - def _writeStartinfo(self): - path = os.path.join(self.directory, _STARTINFO_NAME) - with open(path, "w") as f: - f.write(f"{_SIF_NAME}\n") - - def _exportToUnv(self, groups, mesh, meshPath): - unvGmshFd, unvGmshPath = tempfile.mkstemp(suffix=".unv") - brepFd, brepPath = tempfile.mkstemp(suffix=".brep") - geoFd, geoPath = tempfile.mkstemp(suffix=".geo") - os.close(brepFd) - os.close(geoFd) - os.close(unvGmshFd) - - tools = gmshtools.GmshTools(mesh) - tools.group_elements = {g: [g] for g in groups} - tools.group_nodes_export = False - tools.ele_length_map = {} - tools.temp_file_geometry = brepPath - tools.temp_file_geo = geoPath - tools.temp_file_mesh = unvGmshPath - - tools.get_dimension() - tools.get_region_data() - tools.get_boundary_layer_data() - tools.write_part_file() - tools.write_geo() - if self.testmode: - Console.PrintMessage( - "Solver Elmer testmode, Gmsh will not be used. It might not be installed.\n" - ) - import shutil - - shutil.copyfile(geoPath, os.path.join(self.directory, "group_mesh.geo")) - else: - tools.get_gmsh_command() - tools.run_gmsh_with_geo() - - ioMesh = Fem.FemMesh() - ioMesh.read(unvGmshPath) - ioMesh.write(meshPath) - - os.remove(brepPath) - os.remove(geoPath) - os.remove(unvGmshPath) - def _handleRedifinedConstants(self): """ redefine constants in self.constsdef according constant redefine objects @@ -315,17 +230,6 @@ class Writer: ) def _handleSimulation(self): - # check if we need to update the equation - self._updateSimulation(self.solver) - # output the equation parameters - # first check what equations we have - - # hasHeat ist not used, thus commented ATM - # hasHeat = False - # for equation in self.solver.Group: - # if femutils.is_of_type(equation, "Fem::EquationElmerHeat"): - # hasHeat = True - self._simulation("Coordinate System", self.solver.CoordinateSystem) self._simulation("Coordinate Mapping", (1, 2, 3)) # Elmer uses SI base units, but our mesh is in mm, therefore we must tell @@ -343,62 +247,10 @@ class Writer: self._simulation("Timestepping Method", "BDF") self._simulation("Use Mesh Names", True) - def _updateSimulation(self, solver): - # updates older simulations - if not hasattr(self.solver, "CoordinateSystem"): - solver.addProperty( - "App::PropertyEnumeration", "CoordinateSystem", "Coordinate System", "", locked=True - ) - solver.CoordinateSystem = solverClass.COORDINATE_SYSTEM - solver.CoordinateSystem = "Cartesian" - if not hasattr(self.solver, "BDFOrder"): - solver.addProperty( - "App::PropertyIntegerConstraint", - "BDFOrder", - "Timestepping", - "Order of time stepping method 'BDF'", - locked=True, - ) - solver.BDFOrder = (2, 1, 5, 1) - if not hasattr(self.solver, "OutputIntervals"): - solver.addProperty( - "App::PropertyIntegerList", - "OutputIntervals", - "Timestepping", - "After how many time steps a result file is output", - locked=True, - ) - solver.OutputIntervals = [1] - if not hasattr(self.solver, "SimulationType"): - solver.addProperty( - "App::PropertyEnumeration", "SimulationType", "Type", "", locked=True - ) - solver.SimulationType = solverClass.SIMULATION_TYPE - solver.SimulationType = "Steady State" - if not hasattr(self.solver, "TimestepIntervals"): - solver.addProperty( - "App::PropertyIntegerList", - "TimestepIntervals", - "Timestepping", - ( - "List of maximum optimization rounds if 'Simulation Type'\n" - "is either 'Scanning' or 'Transient'" - ), - locked=True, - ) - solver.TimestepIntervals = [100] - if not hasattr(self.solver, "TimestepSizes"): - solver.addProperty( - "App::PropertyFloatList", - "TimestepSizes", - "Timestepping", - ( - "List of time steps of optimization if 'Simulation Type'\n" - "is either 'Scanning' or 'Transient'" - ), - locked=True, - ) - solver.TimestepSizes = [0.1] + def _writeStartinfo(self): + path = os.path.join(self.directory, _STARTINFO_NAME) + with open(path, "w") as f: + f.write(f"{_SIF_NAME}\n") # ------------------------------------------------------------------------------------------- # Deformation diff --git a/src/Mod/Fem/femtaskpanels/task_solver_elmer.py b/src/Mod/Fem/femtaskpanels/task_solver_elmer.py new file mode 100644 index 0000000000..ff451f4705 --- /dev/null +++ b/src/Mod/Fem/femtaskpanels/task_solver_elmer.py @@ -0,0 +1,157 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# *************************************************************************** +# * Copyright (c) 2025 Mario Passaglia * +# * * +# * 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 * +# * . * +# * * +# *************************************************************************** + +__title__ = "FreeCAD task panel for Elmer solver" +__author__ = "Mario Passaglia" +__url__ = "https://www.freecad.org" + +## @package task_solver_elmer +# \ingroup FEM +# \brief task panel for Elmer solver + +from PySide import QtCore +from PySide import QtGui + +import FreeCAD +import FreeCADGui + +import FemGui + +from femsolver.elmer import elmertools + +from . import base_femlogtaskpanel + + +class _TaskPanel(base_femlogtaskpanel._BaseLogTaskPanel): + """ + The TaskPanel for run Elmer solver + """ + + def __init__(self, obj): + super().__init__(obj, elmertools.ElmerTools(obj)) + + self.form = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/SolverElmer.ui" + ) + + self.text_log = self.form.te_output + self.text_time = self.form.l_time + self.prepared = False + self.run_complete = False + + self.setup_connections() + + def setup_connections(self): + super().setup_connections() + + QtCore.QObject.connect( + self.form.ckb_working_directory, + QtCore.SIGNAL("toggled(bool)"), + self.working_directory_toggled, + ) + QtCore.QObject.connect( + self.form.cb_simulation_type, + QtCore.SIGNAL("currentIndexChanged(int)"), + self.simulation_type_changed, + ) + QtCore.QObject.connect( + self.form.pb_write_input, QtCore.SIGNAL("clicked()"), self.write_input_clicked + ) + QtCore.QObject.connect( + self.form.pb_edit_input, QtCore.SIGNAL("clicked()"), self.edit_input_clicked + ) + QtCore.QObject.connect( + self.form.fc_working_directory, + QtCore.SIGNAL("fileNameSelected(QString)"), + self.working_directory_selected, + ) + QtCore.QObject.connect( + self.form.pb_solver_version, QtCore.SIGNAL("clicked()"), self.get_version + ) + + self.get_object_params() + self.set_widgets() + + def preparation_finished(self): + # override base class method to not auto compute + self.prepared = True + if not self.run_complete: + self.timer.stop() + self.form.pb_edit_input.setEnabled(True) + else: + super().preparation_finished() + + def apply(self): + self.text_log.clear() + self.elapsed.restart() + if self.prepared: + self.timer.start(100) + self.tool.compute() + else: + # run complete process if 'Apply' is pressed without + # previously write the input files + self.run_complete = True + super().apply() + + def get_object_params(self): + self.simulation_type = self.obj.SimulationType + + def set_object_params(self): + self.obj.SimulationType = self.simulation_type + + def set_widgets(self): + "fill the widgets" + self.simulation_type_enum = self.obj.getEnumerationsOfProperty("SimulationType") + index = self.simulation_type_enum.index(self.simulation_type) + self.form.cb_simulation_type.addItems(self.simulation_type_enum) + self.form.cb_simulation_type.setCurrentIndex(index) + + self.form.fc_working_directory.setProperty("fileName", self.obj.WorkingDirectory) + self.form.ckb_working_directory.setChecked(True) + self.form.gpb_working_directory.setVisible(True) + + def simulation_type_changed(self, index): + self.simulation_type = self.simulation_type_enum[index] + self.obj.SimulationType = self.simulation_type + + def working_directory_selected(self): + self.obj.WorkingDirectory = self.form.fc_working_directory.property("fileName") + + def write_input_clicked(self): + self.prepared = False + self.run_complete = False + self.run_process() + + def edit_input_clicked(self): + gen_param = self.tool.fem_param.GetGroup("General") + internal = gen_param.GetBool("UseInternalEditor", True) + ext_editor_path = gen_param.GetString("ExternalEditorPath", "") + if internal or not ext_editor_path: + FemGui.open(self.tool.model_file) + else: + ext_editor_process = QtCore.QProcess() + ext_editor_process.start(ext_editor_path, [self.tool.model_file]) + ext_editor_process.waitForFinished() + + def working_directory_toggled(self, bool_value): + self.form.gpb_working_directory.setVisible(bool_value) diff --git a/src/Mod/Fem/femviewprovider/view_solver_elmer.py b/src/Mod/Fem/femviewprovider/view_solver_elmer.py new file mode 100644 index 0000000000..55459e8f7b --- /dev/null +++ b/src/Mod/Fem/femviewprovider/view_solver_elmer.py @@ -0,0 +1,51 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# *************************************************************************** +# * Copyright (c) 2025 Mario Passaglia * +# * * +# * 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 * +# * . * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM solver Elmer view provider" +__author__ = "Mario Passaglia" +__url__ = "https://www.freecad.org" + +## @package view_elmer +# \ingroup FEM +# \brief solver Elmer view provider + +import FreeCADGui + +from femtaskpanels import task_solver_elmer +from femviewprovider import view_base_femobject + + +class VPSolverElmer(view_base_femobject.VPBaseFemObject): + + def __init__(self, vobj): + super().__init__(vobj) + vobj.addExtension("Gui::ViewProviderGroupExtensionPython") + + def getIcon(self): + return ":/icons/FEM_SolverElmer.svg" + + def setEdit(self, vobj, mode=0): + task = task_solver_elmer._TaskPanel(vobj.Object) + FreeCADGui.Control.showDialog(task) + + return True