From ae90dd0522af1db02a2c441fb25a59c6aca21c13 Mon Sep 17 00:00:00 2001 From: Bernd Hahnebach Date: Tue, 22 Nov 2016 19:48:02 +0100 Subject: [PATCH] FEM: GMSH mesh tool, the GMSH tool class and a task panel --- src/Mod/Fem/App/CMakeLists.txt | 3 + src/Mod/Fem/CMakeLists.txt | 4 + src/Mod/Fem/FemGmshTools.py | 258 ++++++++++++++++++++++++ src/Mod/Fem/TaskPanelFemMeshGmsh.ui | 203 +++++++++++++++++++ src/Mod/Fem/_TaskPanelFemMeshGmsh.py | 170 ++++++++++++++++ src/Mod/Fem/_ViewProviderFemMeshGmsh.py | 9 +- 6 files changed, 641 insertions(+), 6 deletions(-) create mode 100644 src/Mod/Fem/FemGmshTools.py create mode 100644 src/Mod/Fem/TaskPanelFemMeshGmsh.ui create mode 100644 src/Mod/Fem/_TaskPanelFemMeshGmsh.py diff --git a/src/Mod/Fem/App/CMakeLists.txt b/src/Mod/Fem/App/CMakeLists.txt index 448a96221..f84b4a996 100755 --- a/src/Mod/Fem/App/CMakeLists.txt +++ b/src/Mod/Fem/App/CMakeLists.txt @@ -87,6 +87,7 @@ SET(FemScripts_SRCS _FemSolverZ88.py _MechanicalMaterial.py _TaskPanelFemBeamSection.py + _TaskPanelFemMeshGmsh.py _TaskPanelFemShellThickness.py _TaskPanelFemSolverCalculix.py _TaskPanelMechanicalMaterial.py @@ -110,6 +111,7 @@ SET(FemScripts_SRCS FemBeamSection.py FemCommands.py FemConstraintSelfWeight.py + FemGmshTools.py FemInputWriter.py FemInputWriterCcx.py FemInputWriterZ88.py @@ -127,6 +129,7 @@ SET(FemScripts_SRCS TestFem.py z88DispReader.py TaskPanelFemBeamSection.ui + TaskPanelFemMeshGmsh.ui TaskPanelFemShellThickness.ui TaskPanelFemSolverCalculix.ui TaskPanelMechanicalMaterial.ui diff --git a/src/Mod/Fem/CMakeLists.txt b/src/Mod/Fem/CMakeLists.txt index b6e5c26c5..1d5636a81 100755 --- a/src/Mod/Fem/CMakeLists.txt +++ b/src/Mod/Fem/CMakeLists.txt @@ -40,6 +40,10 @@ INSTALL( _FemMeshGmsh.py _ViewProviderFemMeshGmsh.py _CommandMeshGmshFromShape.py + _TaskPanelFemMeshGmsh.py + TaskPanelFemMeshGmsh.ui + + FemGmshTools.py FemBeamSection.py _FemBeamSection.py diff --git a/src/Mod/Fem/FemGmshTools.py b/src/Mod/Fem/FemGmshTools.py new file mode 100644 index 000000000..f55999a05 --- /dev/null +++ b/src/Mod/Fem/FemGmshTools.py @@ -0,0 +1,258 @@ +# *************************************************************************** +# * * +# * Copyright (c) 2016 - Bernd Hahnebach * +# * * +# * 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__ = "Tools for the work with GMSH mesher" +__author__ = "Bernd Hahnebach" +__url__ = "http://www.freecadweb.org" + +## \addtogroup FEM +# @{ + +import FreeCAD +import Fem +import subprocess +import tempfile +from platform import system + + +# CONFIGURATION - EDIT THE FOLLOWING LINE TO MATCH YOUR GMSH BINARY +# gmsh_bin_linux = "/usr/bin/gmsh" +gmsh_bin_linux = "/usr/local/bin/gmsh" +gmsh_bin_windwos = "C:\\Daten\\gmsh-2.13.2-Windows\\gmsh.exe" +gmsh_bin_other = "/usr/bin/gmsh" +# END CONFIGURATION + + +class FemGmshTools(): + def __init__(self, gmsh_mesh_obj, analysis=None): + self.mesh_obj = gmsh_mesh_obj + if analysis: + self.analysis = analysis + # group meshing turned on + else: + self.analysis = None + # group meshing turned off + + # part to mesh + self.part_obj = self.mesh_obj.Part + + # clmax, ElementSizeMax: float, 0.0 = 1e+22 + self.clmax = self.mesh_obj.ElementSizeMax + if self.clmax == 0.0: + self.clmax = 1e+22 + + # clmin, ElementSizeMin: float + self.clmin = self.mesh_obj.ElementSizeMin + + # order, ElementOrder: ['Auto', '1st', '2nd'] + self.order = self.mesh_obj.ElementOrder + if self.order == '1st': + self.order = '1' + elif self.order == 'Auto' or self.order == '2nd': + self.order = '2' + else: + print('Error in order') + + # dimension, ElementDimension: ['Auto', '1D', '2D', '3D'] + self.dimension = self.mesh_obj.ElementDimension + + def create_mesh(self): + print("\nWe gone start GMSH FEM mesh run!") + print(' Part to mesh: Name --> ' + self.part_obj.Name + ', Label --> ' + self.part_obj.Label + ', ShapeType --> ' + self.part_obj.Shape.ShapeType) + print(' ElementSizeMax: ' + str(self.clmax)) + print(' ElementSizeMin: ' + str(self.clmin)) + print(' ElementOrder: ' + self.order) + self.get_dimension() + self.get_tmp_file_paths() + self.get_gmsh_command() + self.get_group_data() + self.write_part_file() + self.write_geo() + error = self.run_gmsh_with_geo() + self.read_and_set_new_mesh() + return error + + def get_dimension(self): + # Dimension + # GMSH uses the hightest availabe. + # A use case for not auto would be a surface (2D) mesh of a solid or other 3d shape + if self.dimension == 'Auto': + shty = self.part_obj.Shape.ShapeType + if shty == 'Solid' or shty == 'CompSolid': + # print('Found: ' + shty) + self.dimension = '3' + elif shty == 'Face' or shty == 'Shell': + # print('Found: ' + shty) + self.dimension = '2' + elif shty == 'Edge' or shty == 'Wire': + # print('Found: ' + shty) + self.dimension = '1' + elif shty == 'Vertex': + # print('Found: ' + shty) + FreeCAD.Console.PrintError("You can not mesh a Vertex.\n") + self.dimension = '0' + elif shty == 'Compound': + print('Found: ' + shty) + print('I do not know what is inside your Compound. Dimension was set to 3 anyway.') + # TODO check contents of Compound + # use dimension 3 on any shape works for 2D and 1d meshes as well ! + # but not in combination with sewfaces or connectfaces + self.dimension = '3' + else: + self.dimension = '0' + FreeCAD.Console.PrintError('Could not retrive Dimension from shape type. Please choose dimension.') + elif self.dimension == '3D': + self.dimension = '3' + elif self.dimension == '2D': + self.dimension = '2' + elif self.dimension == '1D': + self.dimension = '1' + else: + print('Error in dimension') + print(' ElementDimension: ' + self.dimension) + + def get_tmp_file_paths(self): + if system() == "Linux": + path_sep = "/" + elif system() == "Windows": + path_sep = "\\" + else: + path_sep = "/" + tmpdir = tempfile.gettempdir() + # geometry file + self.temp_file_geometry = tmpdir + path_sep + self.part_obj.Name + '_Geometry.brep' + print(' ' + self.temp_file_geometry) + # mesh file + self.mesh_name = self.part_obj.Name + '_Mesh_TmpGmsh' + self.temp_file_mesh = tmpdir + path_sep + self.mesh_name + '.unv' + print(' ' + self.temp_file_mesh) + # GMSH input file + self.temp_file_geo = tmpdir + path_sep + 'shape2mesh.geo' + print(' ' + self.temp_file_geo) + + def get_gmsh_command(self): + if system() == "Linux": + self.gmsh_bin = gmsh_bin_linux + elif system() == "Windows": + self.gmsh_bin = gmsh_bin_windwos + else: + self.gmsh_bin = gmsh_bin_other + self.gmsh_command = self.gmsh_bin + ' - ' + self.temp_file_geo # gmsh - /tmp/shape2mesh.geo + print(' ' + self.gmsh_command) + + def get_group_data(self): + if self.analysis: + print(' Group meshing.') + import FemMeshTools + self.group_elements = FemMeshTools.get_analysis_group_elements(self.analysis, self.part_obj) + print(self.group_elements) + else: + print(' NO group meshing.') + + def write_part_file(self): + self.part_obj.Shape.exportBrep(self.temp_file_geometry) + + def write_geo(self): + geo = open(self.temp_file_geo, "w") + geo.write('Merge "' + self.temp_file_geometry + '";\n') + geo.write("\n") + if self.analysis and self.group_elements: + print(' We gone have found elements to make mesh groups for!') + geo.write("// group data\n") + # we use the element name of FreeCAD which starts with 1 (example: 'Face1'), same as GMSH + for group in self.group_elements: + gdata = self.group_elements[group] + # print(gdata) + # geo.write("// " + group + "\n") + ele_nr = '' + if gdata[0].startswith('Solid'): + physical_type = 'Volume' + for ele in gdata: + ele_nr += (ele.lstrip('Solid') + ', ') + elif gdata[0].startswith('Face'): + physical_type = 'Surface' + for ele in gdata: + ele_nr += (ele.lstrip('Face') + ', ') + elif gdata[0].startswith('Edge') or gdata[0].startswith('Vertex'): + geo.write("// " + group + " group data not written. Edges or Vertexes group data not supported.\n") + print(' Groups for Edges or Vertexes reference shapes not handeled yet.') + if ele_nr: + ele_nr = ele_nr.rstrip(', ') + # print(ele_nr) + geo.write('Physical ' + physical_type + '("' + group + '") = {' + ele_nr + '};\n') + geo.write("\n") + geo.write("Mesh.CharacteristicLengthMax = " + str(self.clmax) + ";\n") + geo.write("Mesh.CharacteristicLengthMin = " + str(self.clmin) + ";\n") + geo.write("Mesh.ElementOrder = " + self.order + ";\n") + geo.write("//Mesh.HighOrderOptimize = 1;\n") # but does not really work, in GUI it does + geo.write("Mesh.Algorithm3D = 1;\n") + geo.write("Mesh.Algorithm = 2;\n") + geo.write("Mesh " + self.dimension + ";\n") + geo.write("Mesh.Format = 2;\n") # unv + if self.analysis and self.group_elements: + geo.write("// For each group save not only the elements but the nodes too.;\n") + geo.write("Mesh.SaveGroupsOfNodes = 1;\n") + geo.write("// Needed for Group meshing too, because for one material there is no group defined;\n") # belongs to Mesh.SaveAll but anly needed if there are groups + geo.write("// Ignore Physical definitions and save all elements;\n") + geo.write("Mesh.SaveAll = 1;\n") + geo.write("\n") + geo.write('Save "' + self.temp_file_mesh + '";\n') + geo.write("\n\n") + geo.write("//////////////////////////////////////////////////////////////////////\n") + geo.write("// GMSH documentation:\n") + geo.write("// http://gmsh.info/doc/texinfo/gmsh.html#Mesh\n") + geo.write("//\n") + geo.write("// We do not check if something went wrong, like negative jacobians etc. You can run GMSH manually yourself: \n") + geo.write("//\n") + geo.write("// to see full GMSH log, run in bash:\n") + geo.write("// " + self.gmsh_bin + " - " + self.temp_file_geo + "\n") + geo.write("//\n") + geo.write("// to run GMSH and keep file in GMSH GUI (with log), run in bash:\n") + geo.write("// " + self.gmsh_bin + " " + self.temp_file_geo + "\n") + geo.close + + def run_gmsh_with_geo(self): + self.error = False + comandlist = [self.gmsh_bin, '-', self.temp_file_geo] + # print(comandlist) + try: + p = subprocess.Popen(comandlist, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + output, error = p.communicate() + # print(output) # stdout is still cut at some point but the warnings are in stderr and thus printed :-) + # print(error) + except: + error = 'Error executing: {}\n'.format(self.gmsh_command) + FreeCAD.Console.PrintError(error) + self.error = True + return error + + def read_and_set_new_mesh(self): + if not self.error: + fem_mesh = Fem.read(self.temp_file_mesh) + self.mesh_obj.FemMesh = fem_mesh + print(' The Part should have a pretty new FEM mesh!') + else: + print('No mesh was created.') + del self.temp_file_geometry + del self.temp_file_mesh + +# @} diff --git a/src/Mod/Fem/TaskPanelFemMeshGmsh.ui b/src/Mod/Fem/TaskPanelFemMeshGmsh.ui new file mode 100644 index 000000000..48f738a8a --- /dev/null +++ b/src/Mod/Fem/TaskPanelFemMeshGmsh.ui @@ -0,0 +1,203 @@ + + + GmshMesh + + + + 0 + 0 + 400 + 413 + + + + FEM Mesh by GMSH + + + + + + + 16777215 + 1677215 + + + + FEM Mesh Parameter + + + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + Mesh element dimension: + + + + + + + + + + Mesh element order: + + + + + + + + + + Max element size (0.0 = Auto): + + + + + + + + 0 + 0 + + + + + 80 + 20 + + + + 0.0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 1.000000000000000 + + + 1000000000.000000000000000 + + + mm + + + 2 + + + 0.000000000000000 + + + + + + + Min element size (0.0 = Auto): + + + + + + + + 0 + 0 + + + + + 80 + 20 + + + + 0.0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 1.000000000000000 + + + 1000000000.000000000000000 + + + mm + + + 2 + + + 0.000000000000000 + + + + + + + + + + + + + 16777215 + 1677215 + + + + GMSH + + + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + + + + 12 + + + + Time: + + + + + + + QTextEdit::NoWrap + + + + + + + + + + + + + + + Gui::InputField + QLineEdit +
Gui/InputField.h
+
+
+ + +
diff --git a/src/Mod/Fem/_TaskPanelFemMeshGmsh.py b/src/Mod/Fem/_TaskPanelFemMeshGmsh.py new file mode 100644 index 000000000..8abd45fe4 --- /dev/null +++ b/src/Mod/Fem/_TaskPanelFemMeshGmsh.py @@ -0,0 +1,170 @@ +# *************************************************************************** +# * * +# * Copyright (c) 2016 - Bernd Hahnebach * +# * * +# * 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__ = "_TaskPanelFemMeshGmsh" +__author__ = "Bernd Hahnebach" +__url__ = "http://www.freecadweb.org" + +## @package TaskPanelFemMeshGmsh +# \ingroup FEM + +import FreeCAD +import time +import _FemMeshGmsh +import FreeCADGui +from PySide import QtGui +from PySide import QtCore +from PySide.QtCore import Qt +from PySide.QtGui import QApplication + + +class _TaskPanelFemMeshGmsh: + '''The TaskPanel for editing References property of FemMeshGmsh objects and creation of new FEM mesh''' + def __init__(self, obj): + self.mesh_obj = obj + self.form = FreeCADGui.PySideUic.loadUi(FreeCAD.getHomePath() + "Mod/Fem/TaskPanelFemMeshGmsh.ui") + + self.Timer = QtCore.QTimer() + self.Timer.start(100) # 100 milli seconds + self.gmsh_runs = False + self.console_message_gmsh = '' + + QtCore.QObject.connect(self.form.if_max, QtCore.SIGNAL("valueChanged(double)"), self.max_changed) + QtCore.QObject.connect(self.form.if_min, QtCore.SIGNAL("valueChanged(double)"), self.min_changed) + QtCore.QObject.connect(self.form.cb_dimension, QtCore.SIGNAL("activated(int)"), self.choose_dimension) + QtCore.QObject.connect(self.form.cb_order, QtCore.SIGNAL("activated(int)"), self.choose_order) + QtCore.QObject.connect(self.Timer, QtCore.SIGNAL("timeout()"), self.update_timer_text) + + self.form.cb_dimension.addItems(_FemMeshGmsh._FemMeshGmsh.known_element_dimensions) + self.form.cb_order.addItems(_FemMeshGmsh._FemMeshGmsh.known_element_orders) + + self.get_mesh_params() + self.get_active_analysis() + self.update() + + def getStandardButtons(self): + return int(QtGui.QDialogButtonBox.Apply | QtGui.QDialogButtonBox.Close) + # show a apply and a close button + # def reject() is called on close button + # def clicked(self, button) is needed, to access the apply button + # def accept() in no longer needed, since there is no OK button + + def reject(self): + FreeCADGui.ActiveDocument.resetEdit() + FreeCAD.ActiveDocument.recompute() + return True + + def clicked(self, button): + if button == QtGui.QDialogButtonBox.Apply: + self.run_gmsh() + + def get_mesh_params(self): + self.clmax = self.mesh_obj.ElementSizeMax + self.clmin = self.mesh_obj.ElementSizeMin + self.order = self.mesh_obj.ElementOrder + self.dimension = self.mesh_obj.ElementDimension + + def set_mesh_params(self): + self.mesh_obj.ElementSizeMax = self.clmax + self.mesh_obj.ElementSizeMin = self.clmin + self.mesh_obj.ElementOrder = self.order + self.mesh_obj.ElementDimension = self.dimension + + def update(self): + 'fills the widgets' + self.form.if_max.setText("{} mm".format(self.clmax)) + self.form.if_min.setText("{} mm".format(self.clmin)) + index_dimension = self.form.cb_dimension.findText(self.dimension) + self.form.cb_dimension.setCurrentIndex(index_dimension) + index_order = self.form.cb_order.findText(self.order) + self.form.cb_order.setCurrentIndex(index_order) + + def console_log(self, message="", color="#000000"): + self.console_message_gmsh = self.console_message_gmsh + '{0:4.1f}: {2}
'.\ + format(time.time() - self.Start, color, message.encode('utf-8', 'replace')) + self.form.te_output.setText(self.console_message_gmsh) + self.form.te_output.moveCursor(QtGui.QTextCursor.End) + + def update_timer_text(self): + # print('timer1') + if self.gmsh_runs: + print('timer2') + # print('Time: {0:4.1f}: '.format(time.time() - self.Start)) + self.form.l_time.setText('Time: {0:4.1f}: '.format(time.time() - self.Start)) + + def max_changed(self, value): + self.clmax = float(value) + + def min_changed(self, value): + self.clmin = float(value) + + def choose_dimension(self, index): + if index < 0: + return + self.form.cb_dimension.setCurrentIndex(index) + self.dimension = str(self.form.cb_dimension.itemText(index)) # form returns unicode + + def choose_order(self, index): + if index < 0: + return + self.form.cb_order.setCurrentIndex(index) + self.order = str(self.form.cb_order.itemText(index)) # form returns unicode + + def run_gmsh(self): + QApplication.setOverrideCursor(Qt.WaitCursor) + self.Start = time.time() + self.form.l_time.setText('Time: {0:4.1f}: '.format(time.time() - self.Start)) + self.console_message_gmsh = '' + self.gmsh_runs = True + self.console_log("We gone start ...") + self.get_active_analysis() + self.set_mesh_params() + import FemGmshTools + gmsh_mesh = FemGmshTools.FemGmshTools(self.obj, self.analysis) + self.console_log("Start GMSH ...") + error = gmsh_mesh.create_mesh() + if error: + print(error) + self.console_log('GMSH had warnings ...') + self.console_log(error, '#FF0000') + else: + self.console_log('Clean run of GMSH') + self.console_log("GMSH done!") + self.form.l_time.setText('Time: {0:4.1f}: '.format(time.time() - self.Start)) + self.Timer.stop() + self.update() + QApplication.restoreOverrideCursor() + + def get_active_analysis(self): + import FemGui + self.analysis = FemGui.getActiveAnalysis() + if self.analysis: + for m in FemGui.getActiveAnalysis().Member: + if m.Name == self.mesh_obj.Name: + print(self.analysis.Name) + return + else: + # print('Mesh is not member of active analysis, means no group meshing') + self.analysis = None # no group meshing + else: + # print('No active analyis, means no group meshing') + self.analysis = None # no group meshing diff --git a/src/Mod/Fem/_ViewProviderFemMeshGmsh.py b/src/Mod/Fem/_ViewProviderFemMeshGmsh.py index bcae49398..981bfeed6 100644 --- a/src/Mod/Fem/_ViewProviderFemMeshGmsh.py +++ b/src/Mod/Fem/_ViewProviderFemMeshGmsh.py @@ -72,8 +72,7 @@ class _ViewProviderFemMeshGmsh: if FemGui.getActiveAnalysis().Document is FreeCAD.ActiveDocument: if self.Object in FemGui.getActiveAnalysis().Member: if not gui_doc.getInEdit(): - FreeCAD.Console.PrintError('TaskPanel test not yet implemented\n') - # gui_doc.setEdit(vobj.Object.Name) + gui_doc.setEdit(vobj.Object.Name) else: FreeCAD.Console.PrintError('Activate the analysis this mesh belongs to!\n') else: @@ -84,8 +83,7 @@ class _ViewProviderFemMeshGmsh: if m == self.Object: FemGui.setActiveAnalysis(o) print('Analysis the Mesh belongs too was activated.') - FreeCAD.Console.PrintError('TaskPanel test not yet implemented\n') - # gui_doc.setEdit(vobj.Object.Name) + gui_doc.setEdit(vobj.Object.Name) break else: FreeCAD.Console.PrintError('Active Analysis is not in active Document!\n') @@ -97,8 +95,7 @@ class _ViewProviderFemMeshGmsh: if m == self.Object: FemGui.setActiveAnalysis(o) print('Analysis the Mesh belongs too was activated.') - FreeCAD.Console.PrintError('TaskPanel test not yet implemented\n') - # gui_doc.setEdit(vobj.Object.Name) + gui_doc.setEdit(vobj.Object.Name) break else: print('Mesh GMSH object does not belong to an analysis. Group meshing will is deactivated.')