Fem: Rework Elmer solver - fixes #21479

This commit is contained in:
marioalexis
2025-10-29 01:44:26 -03:00
parent 275ece0bb2
commit 243b42f4cb
16 changed files with 764 additions and 753 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -144,14 +144,14 @@
<item>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="l_num_processes">
<widget class="QLabel" name="l_num_tasks">
<property name="text">
<string>Number of processes</string>
<string>Number of tasks</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="Gui::PrefSpinBox" name="sb_num_processes">
<widget class="Gui::PrefSpinBox" name="sb_num_tasks">
<property name="alignment">
<set>Qt::AlignLeft|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
@@ -162,7 +162,33 @@
<number>1</number>
</property>
<property name="prefEntry" stdset="0">
<cstring>UseNumberOfCores</cstring>
<cstring>NumberOfTasks</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Fem/Elmer</cstring>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="l_threads_per_task">
<property name="text">
<string>Threads per task</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="Gui::PrefSpinBox" name="sb_threads_per_task">
<property name="alignment">
<set>Qt::AlignLeft|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="toolTip">
<string>Number of threads per task. Take effect if Elmer uses OpenMP.</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="prefEntry" stdset="0">
<cstring>ThreadsPerTask</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Fem/Elmer</cstring>

View File

@@ -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();

View File

@@ -50,7 +50,6 @@ protected:
private:
std::unique_ptr<Ui_DlgSettingsFemElmerImp> ui;
int processor_count;
};
} // namespace FemGui

View File

@@ -156,6 +156,7 @@
<file>ui/ResultShow.ui</file>
<file>ui/SolverCalculiX.ui</file>
<file>ui/SolverCcxTools.ui</file>
<file>ui/SolverElmer.ui</file>
<file>ui/TaskPostGlyph.ui</file>
</qresource>
</RCC>

View File

@@ -0,0 +1,147 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SolverElmer</class>
<widget class="QWidget" name="SolverElmer">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>475</height>
</rect>
</property>
<property name="windowTitle">
<string>Solver Elmer Control</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QCheckBox" name="ckb_working_directory">
<property name="text">
<string>Working directory</string>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="gpb_working_directory">
<property name="title">
<string></string>
</property>
<layout class="QVBoxLayout" name="vbl_working_directory">
<item>
<layout class="QHBoxLayout" name="hbl_working_directory">
<item>
<widget class="QPushButton" name="pb_write_input">
<property name="text">
<string>Write</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pb_edit_input">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Edit</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="hbl_working_directory">
<item>
<widget class="Gui::FileChooser" name="fc_working_directory">
<property name="toolTip">
<string>Path to working directory</string>
</property>
<property name="readOnly">
<bool>false</bool>
</property>
<property name="mode">
<enum>Gui::FileChooser::Mode::Directory</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="gpb_solver_params">
<property name="title">
<string>Solver Parameters</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_9">
<item>
<layout class="QFormLayout" name="formLayout_1">
<item row="0" column="0">
<widget class="QLabel" name="lbl_simulation_type">
<property name="text">
<string>Simulation type</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="cb_simulation_type">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="gb_solver_log">
<layout class="QVBoxLayout" name="verticalLayout_8">
<item>
<widget class="QTextEdit" name="te_output">
<property name="lineWrapMode">
<enum>QTextEdit::NoWrap</enum>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="l_time">
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Time</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pb_solver_version">
<property name="text">
<string>Solver Version</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>Gui::FileChooser</class>
<extends>QWidget</extends>
<header>Gui/FileDialog.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -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

View File

@@ -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,

View File

@@ -0,0 +1,163 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * Copyright (c) 2017 Markus Hovorka <m.hovorka@live.de> *
# * Copyright (c) 2025 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__ = "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)

View File

@@ -0,0 +1,175 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * Copyright (c) 2025 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__ = "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<version>.*)$", re.M)
m = reg_exp.search(info)
ver = "Version: {}".format(m.group("version") if m else "")
return ver

View File

@@ -1,242 +0,0 @@
# ***************************************************************************
# * Copyright (c) 2017 Markus Hovorka <m.hovorka@live.de> *
# * *
# * 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"
## @}

View File

@@ -1,339 +0,0 @@
# ***************************************************************************
# * Copyright (c) 2017 Markus Hovorka <m.hovorka@live.de> *
# * *
# * 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:
# <DataSet timestep=" 5.000E-02" group="" part="0" file="FreeCAD_t0001.vtu"/>
# 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
## @}

View File

@@ -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

View File

@@ -0,0 +1,157 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * Copyright (c) 2025 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__ = "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)

View File

@@ -0,0 +1,51 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * Copyright (c) 2025 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__ = "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