diff --git a/src/Mod/Fem/App/CMakeLists.txt b/src/Mod/Fem/App/CMakeLists.txt index 7a36ecfd14..a76fcd227a 100644 --- a/src/Mod/Fem/App/CMakeLists.txt +++ b/src/Mod/Fem/App/CMakeLists.txt @@ -90,6 +90,7 @@ SET(FemScripts_SRCS FemInputWriterZ88.py FemMesh2Mesh.py FemMeshTools.py + FemResultTools.py FemSelectionObserver.py FemTools.py FemToolsCcx.py diff --git a/src/Mod/Fem/CMakeLists.txt b/src/Mod/Fem/CMakeLists.txt index 4faa4288de..25926922b1 100755 --- a/src/Mod/Fem/CMakeLists.txt +++ b/src/Mod/Fem/CMakeLists.txt @@ -38,6 +38,7 @@ INSTALL( FemInputWriterZ88.py FemMesh2Mesh.py FemMeshTools.py + FemResultTools.py FemSelectionObserver.py FemTools.py FemToolsCcx.py diff --git a/src/Mod/Fem/FemResultTools.py b/src/Mod/Fem/FemResultTools.py new file mode 100644 index 0000000000..566d630688 --- /dev/null +++ b/src/Mod/Fem/FemResultTools.py @@ -0,0 +1,156 @@ +# *************************************************************************** +# * * +# * Copyright (c) 2017 - 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__ = "Fem Tools for results" +__author__ = "Bernd Hahnebach" +__url__ = "http://www.freecadweb.org" + +## \addtogroup FEM +# @{ + +import FreeCAD + + +## Removes all result objects from an analysis group +# @param analysis +def purge_results(analysis): + for m in analysis.Member: + if (m.isDerivedFrom('Fem::FemResultObject')): + if m.Mesh and hasattr(m.Mesh, "Proxy") and m.Mesh.Proxy.Type == "FemMeshResult": + analysis.Document.removeObject(m.Mesh.Name) + analysis.Document.removeObject(m.Name) + FreeCAD.ActiveDocument.recompute() + + +## Resets result mesh deformation +# @param result object +def reset_mesh_deformation(resultobj): + if FreeCAD.GuiUp: + if resultobj.Mesh: + resultobj.Mesh.ViewObject.applyDisplacement(0.0) + + +## Resets reslut mesh color +# @param result object +def reset_mesh_color(resultobj): + if FreeCAD.GuiUp: + if resultobj.Mesh: + resultobj.Mesh.ViewObject.NodeColor = {} + resultobj.Mesh.ViewObject.ElementColor = {} + node_numbers = resultobj.Mesh.FemMesh.Nodes.keys() + zero_values = [0] * len(node_numbers) + resultobj.Mesh.ViewObject.setNodeColorByScalars(node_numbers, zero_values) + + +def show_displacement(resultobj, displacement_factor=0.0): + if FreeCAD.GuiUp: + if resultobj.Mesh.ViewObject.Visibility is False: + resultobj.Mesh.ViewObject.Visibility = True + resultobj.Mesh.ViewObject.setNodeDisplacementByVectors(resultobj.NodeNumbers, resultobj.DisplacementVectors) + resultobj.Mesh.ViewObject.applyDisplacement(displacement_factor) + + +## Sets mesh color using selected type of results (Sabs by default) +# @param self The python object self +# @param result_type Type of FEM result, allowed are: +# - U1, U2, U3 - deformation +# - Uabs - absolute deformation +# - Sabs - Von Mises stress +# @param limit cutoff value. All values over the limit are treated as equal to the limit. Useful for filtering out hot spots. +def show_result(resultobj, result_type="Sabs", limit=None): + if result_type == "None": + reset_mesh_color(resultobj.Mesh) + return + if resultobj: + if result_type == "Sabs": + values = resultobj.StressValues + elif result_type == "Uabs": + values = resultobj.DisplacementLengths + # TODO the result object does have more result types to show, implement them + else: + match = {"U1": 0, "U2": 1, "U3": 2} + d = zip(*resultobj.DisplacementVectors) + values = list(d[match[result_type]]) + show_color_by_scalar_with_cutoff(resultobj, values, limit) + else: + print('Error, No result object given.') + + +## Sets mesh color using list of values. Internally used by show_result function. +# @param self The python object self +# @param values list of values +# @param limit cutoff value. All values over the limit are treated as equel to the limit. Useful for filtering out hot spots. +def show_color_by_scalar_with_cutoff(resultobj, values, limit=None): + if limit: + filtered_values = [] + for v in values: + if v > limit: + filtered_values.append(limit) + else: + filtered_values.append(v) + else: + filtered_values = values + if FreeCAD.GuiUp: + if resultobj.Mesh.ViewObject.Visibility is False: + resultobj.Mesh.ViewObject.Visibility = True + resultobj.Mesh.ViewObject.setNodeColorByScalars(resultobj.NodeNumbers, filtered_values) + + +## Returns minimum, average and maximum value for provided result type +# @param result object +# @param result_type Type of FEM result, allowed are: +# - U1, U2, U3 - deformation +# - Uabs - absolute deformation +# - Sabs - Von Mises stress +# - Prin1 - Principal stress 1 +# - Prin2 - Principal stress 2 +# - Prin3 - Principal stress 3 +# - MaxSear - maximum shear stress +# - Peeq - peeq strain +# - Temp - Temperature +# - MFlow - MassFlowRate +# - NPress - NetworkPressure +# - None - always return (0.0, 0.0, 0.0) +def get_stats(resultobj, result_type): + m = resultobj + stats = (0.0, 0.0, 0.0) + match_table = { + "U1": (m.Stats[0], m.Stats[1], m.Stats[2]), + "U2": (m.Stats[3], m.Stats[4], m.Stats[5]), + "U3": (m.Stats[6], m.Stats[7], m.Stats[8]), + "Uabs": (m.Stats[9], m.Stats[10], m.Stats[11]), + "Sabs": (m.Stats[12], m.Stats[13], m.Stats[14]), + "MaxPrin": (m.Stats[15], m.Stats[16], m.Stats[17]), + "MidPrin": (m.Stats[18], m.Stats[19], m.Stats[20]), + "MinPrin": (m.Stats[21], m.Stats[22], m.Stats[23]), + "MaxShear": (m.Stats[24], m.Stats[25], m.Stats[26]), + "Peeq": (m.Stats[27], m.Stats[28], m.Stats[29]), + "Temp": (m.Stats[30], m.Stats[31], m.Stats[32]), + "MFlow": (m.Stats[33], m.Stats[34], m.Stats[35]), + "NPress": (m.Stats[36], m.Stats[37], m.Stats[38]), + "None": (0.0, 0.0, 0.0) + } + stats = match_table[result_type] + return stats + + +## @} diff --git a/src/Mod/Fem/FemTools.py b/src/Mod/Fem/FemTools.py index 3809a028fd..a874f41a2f 100644 --- a/src/Mod/Fem/FemTools.py +++ b/src/Mod/Fem/FemTools.py @@ -56,8 +56,6 @@ class FemTools(QtCore.QRunnable, QtCore.QObject): self.solver = None if self.analysis: self.update_objects() - self.results_present = False - self.result_object = None else: raise Exception('FEM: No active analysis found!') @@ -69,27 +67,8 @@ class FemTools(QtCore.QRunnable, QtCore.QObject): if m.Mesh and hasattr(m.Mesh, "Proxy") and m.Mesh.Proxy.Type == "FemMeshResult": self.analysis.Document.removeObject(m.Mesh.Name) self.analysis.Document.removeObject(m.Name) - self.results_present = False FreeCAD.ActiveDocument.recompute() - ## Resets mesh deformation - # @param self The python object self - def reset_mesh_deformation(self): - if FreeCAD.GuiUp: - if self.mesh: - self.mesh.ViewObject.applyDisplacement(0.0) - - ## Resets mesh color - # @param self The python object self - def reset_mesh_color(self): - if FreeCAD.GuiUp: - if self.mesh: - self.mesh.ViewObject.NodeColor = {} - self.mesh.ViewObject.ElementColor = {} - node_numbers = self.mesh.FemMesh.Nodes.keys() - zero_values = [0] * len(node_numbers) - self.mesh.ViewObject.setNodeColorByScalars(node_numbers, zero_values) - ## Resets mesh color, deformation and removes all result objects if preferences to keep them is not set # @param self The python object self def reset_mesh_purge_results_checked(self): @@ -97,65 +76,11 @@ class FemTools(QtCore.QRunnable, QtCore.QObject): keep_results_on_rerun = self.fem_prefs.GetBool("KeepResultsOnReRun", False) if not keep_results_on_rerun: self.purge_results() - if FreeCAD.GuiUp: - self.reset_mesh_color() - self.reset_mesh_deformation() ## Resets mesh color, deformation and removes all result objects # @param self The python object self def reset_all(self): self.purge_results() - if FreeCAD.GuiUp: - self.reset_mesh_color() - self.reset_mesh_deformation() - - ## Sets mesh color using selected type of results (Sabs by default) - # @param self The python object self - # @param result_type Type of FEM result, allowed are: - # - U1, U2, U3 - deformation - # - Uabs - absolute deformation - # - Sabs - Von Mises stress - # @param limit cutoff value. All values over the limit are treated as equal to the limit. Useful for filtering out hot spots. - def show_result(self, result_type="Sabs", limit=None): - self.update_objects() - if result_type == "None": - self.reset_mesh_color() - return - if self.result_object: - if FreeCAD.GuiUp: - if self.result_object.Mesh.ViewObject.Visibility is False: - self.result_object.Mesh.ViewObject.Visibility = True - if result_type == "Sabs": - values = self.result_object.StressValues - elif result_type == "Uabs": - values = self.result_object.DisplacementLengths - else: - match = {"U1": 0, "U2": 1, "U3": 2} - d = zip(*self.result_object.DisplacementVectors) - values = list(d[match[result_type]]) - self.show_color_by_scalar_with_cutoff(values, limit) - - ## Sets mesh color using list of values. Internally used by show_result function. - # @param self The python object self - # @param values list of values - # @param limit cutoff value. All values over the limit are treated as equel to the limit. Useful for filtering out hot spots. - def show_color_by_scalar_with_cutoff(self, values, limit=None): - if limit: - filtered_values = [] - for v in values: - if v > limit: - filtered_values.append(limit) - else: - filtered_values.append(v) - else: - filtered_values = values - self.mesh.ViewObject.setNodeColorByScalars(self.result_object.NodeNumbers, filtered_values) - - def show_displacement(self, displacement_factor=0.0): - if FreeCAD.GuiUp: - self.mesh.ViewObject.setNodeDisplacementByVectors(self.result_object.NodeNumbers, - self.result_object.DisplacementVectors) - self.mesh.ViewObject.applyDisplacement(displacement_factor) def update_objects(self): # [{'Object':materials_linear}, {}, ...] @@ -597,70 +522,6 @@ class FemTools(QtCore.QRunnable, QtCore.QObject): # Update inp file name self.set_inp_file_name() - ## Set the analysis result object - # if no result object is provided, check if the analysis has result objects - # if the analysis has exact one result object use this result object - # @param self The python object self - # @param result object name - def use_results(self, results_name=None): - self.result_object = None - if results_name is not None: - for m in self.analysis.Member: - if m.isDerivedFrom("Fem::FemResultObject") and m.Name == results_name: - self.result_object = m - break - if not self.result_object: - raise Exception("{} doesn't exist".format(results_name)) - else: - has_results = False - for m in self.analysis.Member: - if m.isDerivedFrom("Fem::FemResultObject"): - self.result_object = m - if has_results is True: - self.result_object = None - raise Exception("No result name was provided, but more than one result objects in the analysis.") - has_results = True - if not self.result_object: - raise Exception("No result object found in the analysis") - - ## Returns minimum, average and maximum value for provided result type - # @param self The python object self - # @param result_type Type of FEM result, allowed are: - # - U1, U2, U3 - deformation - # - Uabs - absolute deformation - # - Sabs - Von Mises stress - # - Prin1 - Principal stress 1 - # - Prin2 - Principal stress 2 - # - Prin3 - Principal stress 3 - # - MaxSear - maximum shear stress - # - Peeq - peeq strain - # - Temp - Temperature - # - MFlow - MassFlowRate - # - NPress - NetworkPressure - # - None - always return (0.0, 0.0, 0.0) - def get_stats(self, result_type, results_name=None): - self.use_results(results_name) - m = self.result_object - stats = (0.0, 0.0, 0.0) - match_table = { - "U1": (m.Stats[0], m.Stats[1], m.Stats[2]), - "U2": (m.Stats[3], m.Stats[4], m.Stats[5]), - "U3": (m.Stats[6], m.Stats[7], m.Stats[8]), - "Uabs": (m.Stats[9], m.Stats[10], m.Stats[11]), - "Sabs": (m.Stats[12], m.Stats[13], m.Stats[14]), - "MaxPrin": (m.Stats[15], m.Stats[16], m.Stats[17]), - "MidPrin": (m.Stats[18], m.Stats[19], m.Stats[20]), - "MinPrin": (m.Stats[21], m.Stats[22], m.Stats[23]), - "MaxShear": (m.Stats[24], m.Stats[25], m.Stats[26]), - "Peeq": (m.Stats[27], m.Stats[28], m.Stats[29]), - "Temp": (m.Stats[30], m.Stats[31], m.Stats[32]), - "MFlow": (m.Stats[33], m.Stats[34], m.Stats[35]), - "NPress": (m.Stats[36], m.Stats[37], m.Stats[38]), - "None": (0.0, 0.0, 0.0) - } - stats = match_table[result_type] - return stats - # helper def get_refshape_type(fem_doc_object): diff --git a/src/Mod/Fem/TestFem.py b/src/Mod/Fem/TestFem.py index 4021525754..c87dfe9405 100644 --- a/src/Mod/Fem/TestFem.py +++ b/src/Mod/Fem/TestFem.py @@ -26,6 +26,7 @@ import Fem import FemToolsCcx +import FemResultTools import FreeCAD import ObjectsFem import tempfile @@ -507,7 +508,7 @@ class FemCcxAnalysisTest(unittest.TestCase): self.assertTrue(fea.results_present, "Cannot read results from {}.frd frd file".format(fea.base_name)) fcc_print('Reading stats from result object for static analysis...') - ret = compare_stats(fea, static_expected_values) + ret = compare_stats(fea, static_expected_values, 'CalculiX_static_results') self.assertFalse(ret, "Invalid results read from .frd file") fcc_print('Save FreeCAD file for static analysis to {}...'.format(static_save_fc_file)) @@ -557,7 +558,7 @@ class FemCcxAnalysisTest(unittest.TestCase): self.assertTrue(fea.results_present, "Cannot read results from {}.frd frd file".format(fea.base_name)) fcc_print('Reading stats from result object for frequency analysis...') - ret = compare_stats(fea, frequency_expected_values) + ret = compare_stats(fea, frequency_expected_values, 'CalculiX_frequency_mode_1_results') self.assertFalse(ret, "Invalid results read from .frd file") fcc_print('Save FreeCAD file for frequency analysis to {}...'.format(frequency_save_fc_file)) @@ -685,7 +686,7 @@ class FemCcxAnalysisTest(unittest.TestCase): self.assertTrue(fea.results_present, "Cannot read results from {}.frd frd file".format(fea.base_name)) fcc_print('Reading stats from result object for thermomech analysis...') - ret = compare_stats(fea, thermomech_expected_values) + ret = compare_stats(fea, thermomech_expected_values, 'CalculiX_thermomech_results') self.assertFalse(ret, "Invalid results read from .frd file") fcc_print('Save FreeCAD file for thermomech analysis to {}...'.format(thermomech_save_fc_file)) @@ -1013,9 +1014,10 @@ def compare_stats(fea, stat_file=None, loc_stat_types=None, res_obj_name=None): stats = [] for s in loc_stat_types: if res_obj_name: - statval = fea.get_stats(s, res_obj_name) + statval = FemResultTools.get_stats(FreeCAD.ActiveDocument.getObject(res_obj_name), s) else: - statval = fea.get_stats(s) + print('No result object name given') + return False stats.append("{0}: ({1:.14g}, {2:.14g}, {3:.14g})\n".format(s, statval[0], statval[1], statval[2])) if sf_content != stats: fcc_print("Expected stats from {}".format(stat_file)) @@ -1048,7 +1050,7 @@ def runTestFem(): def create_test_results(): - # run FEM unit tests + print("run FEM unit tests") runTestFem() import shutil @@ -1059,20 +1061,19 @@ def create_test_results(): FemGui.setActiveAnalysis(FreeCAD.ActiveDocument.Analysis) fea = FemToolsCcx.FemToolsCcx() - # static + print("create static result files") fea.reset_all() fea.run() fea.load_results() - stats_static = [] # we only have one result object so we are fine + stats_static = [] for s in stat_types: - statval = fea.get_stats(s) + statval = FemResultTools.get_stats(FreeCAD.ActiveDocument.getObject('CalculiX_static_results'), s) stats_static.append("{0}: ({1:.14g}, {2:.14g}, {3:.14g})\n".format(s, statval[0], statval[1], statval[2])) static_expected_values_file = static_analysis_dir + 'cube_static_expected_values' f = open(static_expected_values_file, 'w') for s in stats_static: f.write(s) f.close() - # could be added in FemToolsCcx to the self object as an Attribut frd_result_file = os.path.splitext(fea.inp_file_name)[0] + '.frd' dat_result_file = os.path.splitext(fea.inp_file_name)[0] + '.dat' frd_static_test_result_file = static_analysis_dir + 'cube_static.frd' @@ -1080,15 +1081,15 @@ def create_test_results(): shutil.copyfile(frd_result_file, frd_static_test_result_file) shutil.copyfile(dat_result_file, dat_static_test_result_file) - # frequency + print("create frequency result files") fea.reset_all() fea.set_analysis_type('frequency') fea.solver.EigenmodesCount = 1 # we should only have one result object fea.run() fea.load_results() - stats_frequency = [] # since we set eigenmodeno. we only have one result object so we are fine + stats_frequency = [] for s in stat_types: - statval = fea.get_stats(s) + statval = FemResultTools.get_stats(FreeCAD.ActiveDocument.getObject('CalculiX_static_mode_1_results'), s) # FIXME for some reason result obj name has static stats_frequency.append("{0}: ({1:.14g}, {2:.14g}, {3:.14g})\n".format(s, statval[0], statval[1], statval[2])) frequency_expected_values_file = frequency_analysis_dir + 'cube_frequency_expected_values' f = open(frequency_expected_values_file, 'w') @@ -1100,23 +1101,22 @@ def create_test_results(): shutil.copyfile(frd_result_file, frd_frequency_test_result_file) shutil.copyfile(dat_result_file, dat_frequency_test_result_file) - # thermomech + print("create thermomech result files") FreeCAD.open(thermomech_save_fc_file) FemGui.setActiveAnalysis(FreeCAD.ActiveDocument.Analysis) fea = FemToolsCcx.FemToolsCcx() fea.reset_all() fea.run() fea.load_results() - stats_thermomech = [] # we only have one result object so we are fine + stats_thermomech = [] for s in stat_types: - statval = fea.get_stats(s) + statval = FemResultTools.get_stats(FreeCAD.ActiveDocument.getObject('CalculiX_thermomech_results'), s) stats_thermomech.append("{0}: ({1:.14g}, {2:.14g}, {3:.14g})\n".format(s, statval[0], statval[1], statval[2])) thermomech_expected_values_file = thermomech_analysis_dir + 'spine_thermomech_expected_values' f = open(thermomech_expected_values_file, 'w') for s in stats_thermomech: f.write(s) f.close() - # could be added in FemToolsCcx to the self object as an Attribut frd_result_file = os.path.splitext(fea.inp_file_name)[0] + '.frd' dat_result_file = os.path.splitext(fea.inp_file_name)[0] + '.dat' frd_thermomech_test_result_file = thermomech_analysis_dir + 'spine_thermomech.frd' @@ -1125,23 +1125,22 @@ def create_test_results(): shutil.copyfile(dat_result_file, dat_thermomech_test_result_file) print('Results copied to the appropriate FEM test dirs in: ' + temp_dir) - # Flow1D + print("create Flow1D result files") FreeCAD.open(Flow1D_thermomech_save_fc_file) FemGui.setActiveAnalysis(FreeCAD.ActiveDocument.Analysis) fea = FemToolsCcx.FemToolsCcx() fea.reset_all() fea.run() fea.load_results() - stats_flow1D = [] # be carefule if we have more than one result object ! + stats_flow1D = [] for s in stat_types: - statval = fea.get_stats(s, 'CalculiX_thermomech_time_1_0_results') + statval = FemResultTools.get_stats(FreeCAD.ActiveDocument.getObject('CalculiX_thermomech_time_1_0_results'), s) stats_flow1D.append("{0}: ({1:.14g}, {2:.14g}, {3:.14g})\n".format(s, statval[0], statval[1], statval[2])) Flow1D_thermomech_expected_values_file = Flow1D_thermomech_analysis_dir + 'Flow1D_thermomech_expected_values' f = open(Flow1D_thermomech_expected_values_file, 'w') for s in stats_flow1D: f.write(s) f.close() - # could be added in FemToolsCcx to the self object as an Attribut frd_result_file = os.path.splitext(fea.inp_file_name)[0] + '.frd' dat_result_file = os.path.splitext(fea.inp_file_name)[0] + '.dat' frd_Flow1D_thermomech_test_result_file = Flow1D_thermomech_analysis_dir + 'Flow1D_thermomech.frd' diff --git a/src/Mod/Fem/importToolsFem.py b/src/Mod/Fem/importToolsFem.py index 12c874fe4a..94574e34b5 100644 --- a/src/Mod/Fem/importToolsFem.py +++ b/src/Mod/Fem/importToolsFem.py @@ -395,7 +395,7 @@ def fill_femresult_mechanical(results, result_set, span): temp_min, temp_avg, temp_max, mflow_min, mflow_avg, mflow_max, npress_min, npress_avg, npress_max] - # do not forget to adapt the def get_stats in FemTools and _TaskPanelFemResultShow module as well as the TestFem module + # do not forget to adapt the def get_stats in FemResultTools and _TaskPanelFemResultShow module as well as the TestFem module # stat_types = ["U1", "U2", "U3", "Uabs", "Sabs", "MaxPrin", "MidPrin", "MinPrin", "MaxShear", "Peeq", "Temp", "MFlow", "NPress"] # TODO a dictionary would be far robust than a list, but needs adapten in VTK too because of VTK result import