Fem: Rework Elmer solver - fixes #21479
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -50,7 +50,6 @@ protected:
|
||||
|
||||
private:
|
||||
std::unique_ptr<Ui_DlgSettingsFemElmerImp> ui;
|
||||
int processor_count;
|
||||
};
|
||||
|
||||
} // namespace FemGui
|
||||
|
||||
@@ -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>
|
||||
|
||||
147
src/Mod/Fem/Gui/Resources/ui/SolverElmer.ui
Normal file
147
src/Mod/Fem/Gui/Resources/ui/SolverElmer.ui
Normal 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>
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
163
src/Mod/Fem/femobjects/solver_elmer.py
Normal file
163
src/Mod/Fem/femobjects/solver_elmer.py
Normal 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)
|
||||
175
src/Mod/Fem/femsolver/elmer/elmertools.py
Normal file
175
src/Mod/Fem/femsolver/elmer/elmertools.py
Normal 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
|
||||
@@ -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"
|
||||
|
||||
|
||||
## @}
|
||||
@@ -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
|
||||
|
||||
|
||||
## @}
|
||||
@@ -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
|
||||
|
||||
157
src/Mod/Fem/femtaskpanels/task_solver_elmer.py
Normal file
157
src/Mod/Fem/femtaskpanels/task_solver_elmer.py
Normal 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)
|
||||
51
src/Mod/Fem/femviewprovider/view_solver_elmer.py
Normal file
51
src/Mod/Fem/femviewprovider/view_solver_elmer.py
Normal 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
|
||||
Reference in New Issue
Block a user