From d2ddc8e56acf4d7a1c00083d4395dcd2c0a1678e Mon Sep 17 00:00:00 2001 From: marioalexis Date: Wed, 2 Apr 2025 10:50:45 -0300 Subject: [PATCH 1/8] Fem: Rename base_femmeshtaskpanel to base_femlogtaskpanel --- src/Mod/Fem/CMakeLists.txt | 2 +- ...shtaskpanel.py => base_femlogtaskpanel.py} | 56 +++++++++++++------ src/Mod/Fem/femtaskpanels/task_mesh_gmsh.py | 10 ++-- src/Mod/Fem/femtaskpanels/task_mesh_netgen.py | 10 ++-- 4 files changed, 49 insertions(+), 29 deletions(-) rename src/Mod/Fem/femtaskpanels/{base_femmeshtaskpanel.py => base_femlogtaskpanel.py} (81%) diff --git a/src/Mod/Fem/CMakeLists.txt b/src/Mod/Fem/CMakeLists.txt index aabc377464..622c3a3d46 100755 --- a/src/Mod/Fem/CMakeLists.txt +++ b/src/Mod/Fem/CMakeLists.txt @@ -579,7 +579,7 @@ SET(FemGuiObjects_SRCS SET(FemGuiTaskPanels_SRCS femtaskpanels/__init__.py femtaskpanels/base_femtaskpanel.py - femtaskpanels/base_femmeshtaskpanel.py + femtaskpanels/base_femlogtaskpanel.py femtaskpanels/task_constraint_bodyheatsource.py femtaskpanels/task_constraint_centrif.py femtaskpanels/task_constraint_currentdensity.py diff --git a/src/Mod/Fem/femtaskpanels/base_femmeshtaskpanel.py b/src/Mod/Fem/femtaskpanels/base_femlogtaskpanel.py similarity index 81% rename from src/Mod/Fem/femtaskpanels/base_femmeshtaskpanel.py rename to src/Mod/Fem/femtaskpanels/base_femlogtaskpanel.py index 5bce46cc90..5c37d7ceb2 100644 --- a/src/Mod/Fem/femtaskpanels/base_femmeshtaskpanel.py +++ b/src/Mod/Fem/femtaskpanels/base_femlogtaskpanel.py @@ -21,13 +21,13 @@ # * * # *************************************************************************** -__title__ = "FreeCAD FEM mesh base task panel for mesh object object" +__title__ = "FreeCAD FEM base task panel with logging" __author__ = "Mario Passaglia" __url__ = "https://www.freecad.org" -## @package base_femmeshtaskpanel +## @package base_femlogtaskpanel # \ingroup FEM -# \brief base task panel for mesh object +# \brief base task panel for logging from abc import ABC, abstractmethod @@ -44,27 +44,34 @@ from . import base_femtaskpanel class _Thread(QtCore.QThread): """ Class for thread and subprocess manipulation - 'tool' argument must be an object with 'compute' and 'prepare' methods + 'tool' argument must be an object with 'compute', 'prepare', 'update_properties' methods and a 'process' attribute of type QProcess object """ def __init__(self, tool): super().__init__() self.tool = tool + self.prepare_ok = False def run(self): - self.tool.prepare() + try: + self.tool.prepare() + self.prepare_ok = True + except Exception as e: + self.prepare_ok = False + FreeCAD.Console.PrintError("{}\n".format(e)) -class _BaseMeshTaskPanel(base_femtaskpanel._BaseTaskPanel, ABC): +class _BaseLogTaskPanel(base_femtaskpanel._BaseTaskPanel, ABC): """ - Abstract base class for FemMesh object TaskPanel + Abstract base class for TaskPanel with logging """ def __init__(self, obj, tool): super().__init__(obj) self.tool = tool self.timer = QtCore.QTimer() + self.elapsed = QtCore.QElapsedTimer() self._thread = _Thread(self.tool) self.text_log = None self.text_time = None @@ -97,17 +104,27 @@ class _BaseMeshTaskPanel(base_femtaskpanel._BaseTaskPanel, ABC): def thread_started(self): self.text_log.clear() - self.write_log("Prepare meshing...\n", QtGui.QColor(getOutputWinColor("Text"))) + self.write_log("Prepare process...\n", QtGui.QColor(getOutputWinColor("Text"))) QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) def thread_finished(self): + QtGui.QApplication.restoreOverrideCursor() + if self._thread.prepare_ok: + self.write_log("Preparation finished\n", QtGui.QColor(getOutputWinColor("Text"))) + self.preparation_finished() + else: + self.timer.stop() + self.write_log("Preparation failed.\n", QtGui.QColor(getOutputWinColor("Error"))) + return None + + def preparation_finished(self): self.tool.compute() def process_finished(self, code, status): if status == QtCore.QProcess.ExitStatus.NormalExit: if code != 0: self.write_log( - "Process finished with errors. Mesh not updated\n", + "Process finished with errors. Result not updated\n", QtGui.QColor(getOutputWinColor("Error")), ) return @@ -117,7 +134,8 @@ class _BaseMeshTaskPanel(base_femtaskpanel._BaseTaskPanel, ABC): self.write_log("Process crashed\n", QtGui.QColor(getOutputWinColor("Error"))) def process_started(self): - self.write_log("Start meshing...\n", QtGui.QColor(getOutputWinColor("Text"))) + QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) + self.write_log("Start process...\n", QtGui.QColor(getOutputWinColor("Text"))) def write_output(self): self.write_log( @@ -143,11 +161,11 @@ class _BaseMeshTaskPanel(base_femtaskpanel._BaseTaskPanel, ABC): self.text_log.ensureCursorVisible() @abstractmethod - def set_mesh_params(self): + def set_object_params(self): pass @abstractmethod - def get_mesh_params(self): + def get_object_params(self): pass def getStandardButtons(self): @@ -166,7 +184,7 @@ class _BaseMeshTaskPanel(base_femtaskpanel._BaseTaskPanel, ABC): self.timer.stop() QtGui.QApplication.restoreOverrideCursor() - self.set_mesh_params() + self.set_object_params() return super().accept() def reject(self): @@ -190,8 +208,10 @@ class _BaseMeshTaskPanel(base_femtaskpanel._BaseTaskPanel, ABC): FreeCAD.Console.PrintWarning("Process already running\n") return None - self.set_mesh_params() - self.run_mesher() + self.apply() + + def apply(self): + self.run_process() def update_timer_text(self): self.text_time.setText(f"Time: {self.elapsed.elapsed()/1000:4.1f} s") @@ -200,8 +220,8 @@ class _BaseMeshTaskPanel(base_femtaskpanel._BaseTaskPanel, ABC): self.timer.stop() QtGui.QApplication.restoreOverrideCursor() - def run_mesher(self): - self.elapsed = QtCore.QElapsedTimer() + def run_process(self): + self.set_object_params() self.elapsed.start() self.update_timer_text() self.timer.start(100) @@ -210,4 +230,4 @@ class _BaseMeshTaskPanel(base_femtaskpanel._BaseTaskPanel, ABC): def get_version(self): full_message = self.tool.version() - QtGui.QMessageBox.information(None, "{} - Information".format(self.tool.name), full_message) + QtGui.QMessageBox.information(None, "{} - Info".format(self.tool.name), full_message) diff --git a/src/Mod/Fem/femtaskpanels/task_mesh_gmsh.py b/src/Mod/Fem/femtaskpanels/task_mesh_gmsh.py index bbdf906d2c..0ebe8f7534 100644 --- a/src/Mod/Fem/femtaskpanels/task_mesh_gmsh.py +++ b/src/Mod/Fem/femtaskpanels/task_mesh_gmsh.py @@ -36,10 +36,10 @@ import FreeCADGui from femmesh import gmshtools -from . import base_femmeshtaskpanel +from . import base_femlogtaskpanel -class _TaskPanel(base_femmeshtaskpanel._BaseMeshTaskPanel): +class _TaskPanel(base_femlogtaskpanel._BaseLogTaskPanel): """ The TaskPanel for editing References property of MeshGmsh objects and creation of new FEM mesh @@ -78,16 +78,16 @@ class _TaskPanel(base_femmeshtaskpanel._BaseMeshTaskPanel): self.form.pb_get_gmsh_version, QtCore.SIGNAL("clicked()"), self.get_version ) - self.get_mesh_params() + self.get_object_params() self.set_widgets() - def get_mesh_params(self): + def get_object_params(self): self.clmax = self.obj.CharacteristicLengthMax self.clmin = self.obj.CharacteristicLengthMin self.dimension = self.obj.ElementDimension self.order = self.obj.ElementOrder - def set_mesh_params(self): + def set_object_params(self): self.obj.CharacteristicLengthMax = self.clmax self.obj.CharacteristicLengthMin = self.clmin self.obj.ElementDimension = self.dimension diff --git a/src/Mod/Fem/femtaskpanels/task_mesh_netgen.py b/src/Mod/Fem/femtaskpanels/task_mesh_netgen.py index 8d8081170c..e5d5e5a5c2 100644 --- a/src/Mod/Fem/femtaskpanels/task_mesh_netgen.py +++ b/src/Mod/Fem/femtaskpanels/task_mesh_netgen.py @@ -36,10 +36,10 @@ import FreeCADGui from femmesh import netgentools -from . import base_femmeshtaskpanel +from . import base_femlogtaskpanel -class _TaskPanel(base_femmeshtaskpanel._BaseMeshTaskPanel): +class _TaskPanel(base_femlogtaskpanel._BaseLogTaskPanel): """ The TaskPanel for editing References property of MeshNetgen objects and creation of new FEM mesh @@ -97,10 +97,10 @@ class _TaskPanel(base_femmeshtaskpanel._BaseMeshTaskPanel): self.form.pb_get_netgen_version, QtCore.SIGNAL("clicked()"), self.get_version ) - self.get_mesh_params() + self.get_object_params() self.set_widgets() - def get_mesh_params(self): + def get_object_params(self): self.min_size = self.obj.MinSize self.max_size = self.obj.MaxSize self.fineness = self.obj.Fineness @@ -110,7 +110,7 @@ class _TaskPanel(base_femmeshtaskpanel._BaseMeshTaskPanel): self.second_order = self.obj.SecondOrder self.user_p = self.get_user_fineness_params(self.obj) - def set_mesh_params(self): + def set_object_params(self): self.obj.MinSize = self.min_size self.obj.MaxSize = self.max_size self.obj.Fineness = self.fineness From 47bdf1d01d5f4e0b7336c60900cef4c3249ee5eb Mon Sep 17 00:00:00 2001 From: marioalexis Date: Thu, 3 Apr 2025 00:28:01 -0300 Subject: [PATCH 2/8] Fem: Add properties to FemSolverObject --- src/Mod/Fem/App/FemSolverObject.cpp | 14 +++++++++++++- src/Mod/Fem/App/FemSolverObject.h | 3 +++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Mod/Fem/App/FemSolverObject.cpp b/src/Mod/Fem/App/FemSolverObject.cpp index 8809c216aa..eb8876c6b6 100644 --- a/src/Mod/Fem/App/FemSolverObject.cpp +++ b/src/Mod/Fem/App/FemSolverObject.cpp @@ -35,7 +35,19 @@ using namespace App; PROPERTY_SOURCE(Fem::FemSolverObject, App::DocumentObject) -FemSolverObject::FemSolverObject() = default; +FemSolverObject::FemSolverObject() +{ + ADD_PROPERTY_TYPE(Results, + (nullptr), + "Solver", + App::PropertyType(App::Prop_ReadOnly | App::Prop_Output), + "Solver results list"); + ADD_PROPERTY_TYPE(WorkingDirectory, + (""), + "Solver", + App::PropertyType(App::Prop_Transient | App::Prop_Hidden | App::Prop_Output), + "Solver working directory"); +} FemSolverObject::~FemSolverObject() = default; diff --git a/src/Mod/Fem/App/FemSolverObject.h b/src/Mod/Fem/App/FemSolverObject.h index 629f7cbc7d..191a47a1df 100644 --- a/src/Mod/Fem/App/FemSolverObject.h +++ b/src/Mod/Fem/App/FemSolverObject.h @@ -26,6 +26,7 @@ #define Fem_FemSolverObject_H #include +#include #include namespace Fem @@ -40,6 +41,8 @@ public: FemSolverObject(); ~FemSolverObject() override; + App::PropertyLinkList Results; + App::PropertyPath WorkingDirectory; // Attributes are implemented in the FemSolverObjectPython /// returns the type name of the ViewProvider From 4f7a835e22d7b6bbc7069d9511ba0ef87ff195c8 Mon Sep 17 00:00:00 2001 From: marioalexis Date: Wed, 2 Apr 2025 11:01:11 -0300 Subject: [PATCH 3/8] Fem: SolverCalculiX object refactor --- src/Mod/Fem/CMakeLists.txt | 4 + src/Mod/Fem/Gui/CMakeLists.txt | 1 + .../Fem/Gui/Resources/ui/SolverCalculiX.ui | 144 +++++++++++++ src/Mod/Fem/ObjectsFem.py | 13 +- src/Mod/Fem/femcommands/commands.py | 18 +- src/Mod/Fem/femmesh/meshsetsgetter.py | 2 +- src/Mod/Fem/femobjects/solver_calculix.py | 46 +++++ .../Fem/femsolver/calculix/calculixtools.py | 190 ++++++++++++++++++ .../Fem/femtaskpanels/task_solver_calculix.py | 168 ++++++++++++++++ .../femviewprovider/view_solver_calculix.py | 50 +++++ 10 files changed, 625 insertions(+), 11 deletions(-) create mode 100644 src/Mod/Fem/Gui/Resources/ui/SolverCalculiX.ui create mode 100644 src/Mod/Fem/femobjects/solver_calculix.py create mode 100644 src/Mod/Fem/femsolver/calculix/calculixtools.py create mode 100644 src/Mod/Fem/femtaskpanels/task_solver_calculix.py create mode 100644 src/Mod/Fem/femviewprovider/view_solver_calculix.py diff --git a/src/Mod/Fem/CMakeLists.txt b/src/Mod/Fem/CMakeLists.txt index 622c3a3d46..1e30655868 100755 --- a/src/Mod/Fem/CMakeLists.txt +++ b/src/Mod/Fem/CMakeLists.txt @@ -203,6 +203,7 @@ SET(FemObjects_SRCS femobjects/mesh_region.py femobjects/mesh_result.py femobjects/result_mechanical.py + femobjects/solver_calculix.py femobjects/solver_ccxtools.py ) @@ -227,6 +228,7 @@ SET(FemSolver_SRCS SET(FemSolverCalculix_SRCS femsolver/calculix/__init__.py + femsolver/calculix/calculixtools.py femsolver/calculix/solver.py femsolver/calculix/tasks.py femsolver/calculix/write_constraint_bodyheatsource.py @@ -603,6 +605,7 @@ SET(FemGuiTaskPanels_SRCS femtaskpanels/task_mesh_region.py femtaskpanels/task_mesh_netgen.py femtaskpanels/task_result_mechanical.py + femtaskpanels/task_solver_calculix.py femtaskpanels/task_solver_ccxtools.py ) @@ -652,6 +655,7 @@ SET(FemGuiViewProvider_SRCS femviewprovider/view_mesh_region.py femviewprovider/view_mesh_result.py femviewprovider/view_result_mechanical.py + femviewprovider/view_solver_calculix.py femviewprovider/view_solver_ccxtools.py ) diff --git a/src/Mod/Fem/Gui/CMakeLists.txt b/src/Mod/Fem/Gui/CMakeLists.txt index d7fdd2c4b5..17c7ca28ce 100755 --- a/src/Mod/Fem/Gui/CMakeLists.txt +++ b/src/Mod/Fem/Gui/CMakeLists.txt @@ -429,6 +429,7 @@ SET(FemGuiPythonUI_SRCS Resources/ui/ResultHints.ui Resources/ui/ResultShow.ui Resources/ui/SolverCalculix.ui + Resources/ui/SolverCalculiX.ui ) ADD_CUSTOM_TARGET(FemPythonUi ALL diff --git a/src/Mod/Fem/Gui/Resources/ui/SolverCalculiX.ui b/src/Mod/Fem/Gui/Resources/ui/SolverCalculiX.ui new file mode 100644 index 0000000000..890991abe1 --- /dev/null +++ b/src/Mod/Fem/Gui/Resources/ui/SolverCalculiX.ui @@ -0,0 +1,144 @@ + + + SolverCalculiX + + + + 0 + 0 + 400 + 475 + + + + Solver CalculiX Control + + + + + + Working Directory + + + + + + + + + + + + + + + Write + + + + + + + false + + + Edit + + + + + + + + + + + Path to working directory + + + true + + + + + + + ... + + + + + + + + + + + + Solver Parameters + + + + + + + + Analysis Type: + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + QTextEdit::NoWrap + + + true + + + + + + + + 12 + + + + Time: + + + + + + + Solver Version + + + + + + + + + + + diff --git a/src/Mod/Fem/ObjectsFem.py b/src/Mod/Fem/ObjectsFem.py index 17a8b436f9..72c407b3bc 100644 --- a/src/Mod/Fem/ObjectsFem.py +++ b/src/Mod/Fem/ObjectsFem.py @@ -795,12 +795,17 @@ def makeSolverCalculiXCcxTools(doc, name="SolverCcxTools"): return obj -def makeSolverCalculix(doc, name="SolverCalculix"): - """makeSolverCalculix(document, [name]): +def makeSolverCalculiX(doc, name="SolverCalculiX"): + """makeSolverCalculiX(document, [name]): makes a Calculix solver object""" - import femsolver.calculix.solver + obj = doc.addObject("Fem::FemSolverObjectPython", name) + from femobjects import solver_calculix - obj = femsolver.calculix.solver.create(doc, name) + solver_calculix.SolverCalculiX(obj) + if FreeCAD.GuiUp: + from femviewprovider import view_solver_calculix + + view_solver_calculix.VPSolverCalculiX(obj.ViewObject) return obj diff --git a/src/Mod/Fem/femcommands/commands.py b/src/Mod/Fem/femcommands/commands.py index 89dad73b83..751adbb7bf 100644 --- a/src/Mod/Fem/femcommands/commands.py +++ b/src/Mod/Fem/femcommands/commands.py @@ -630,7 +630,7 @@ class _MaterialMechanicalNonlinear(CommandManager): # CalculiX solver or new frame work CalculiX solver if solver_object and ( is_of_type(solver_object, "Fem::SolverCcxTools") - or is_of_type(solver_object, "Fem::SolverCalculix") + or is_of_type(solver_object, "Fem::SolverCalculiX") ): FreeCAD.Console.PrintMessage( f"Set MaterialNonlinearity to nonlinear for {solver_object.Label}\n" @@ -938,7 +938,7 @@ class _SolverCalculixContextManager: def __enter__(self): ccx_prefs = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Fem/Ccx") - FreeCAD.ActiveDocument.openTransaction("Create SolverCalculix") + FreeCAD.ActiveDocument.openTransaction("Create SolverCalculiX") FreeCADGui.addModule("ObjectsFem") FreeCADGui.addModule("FemGui") FreeCADGui.doCommand( @@ -1051,8 +1051,8 @@ class _SolverCcxTools(CommandManager): FreeCADGui.doCommand(f"{cm.cli_name}.MaterialNonlinearity = 'nonlinear'") -class _SolverCalculix(CommandManager): - "The FEM_SolverCalculix command definition" +class _SolverCalculiX(CommandManager): + "The FEM_SolverCalculiX command definition" def __init__(self): super().__init__() @@ -1068,7 +1068,13 @@ class _SolverCalculix(CommandManager): self.is_active = "with_analysis" def Activated(self): - with _SolverCalculixContextManager("makeSolverCalculix", "solver") as cm: + ccx_prefs = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Fem/Ccx") + if ccx_prefs.GetBool("ResultAsPipeline", False): + make_solver = "makeSolverCalculiX" + else: + make_solver = "makeSolverCalculiXCcxTools" + + with _SolverCalculixContextManager(make_solver, "solver") as cm: has_nonlinear_material_obj = False for m in self.active_analysis.Group: if is_of_type(m, "Fem::MaterialMechanicalNonlinear"): @@ -1227,7 +1233,7 @@ FreeCADGui.addCommand("FEM_MeshRegion", _MeshRegion()) FreeCADGui.addCommand("FEM_ResultShow", _ResultShow()) FreeCADGui.addCommand("FEM_ResultsPurge", _ResultsPurge()) FreeCADGui.addCommand("FEM_SolverCalculiXCcxTools", _SolverCcxTools()) -FreeCADGui.addCommand("FEM_SolverCalculiX", _SolverCalculix()) +FreeCADGui.addCommand("FEM_SolverCalculiX", _SolverCalculiX()) FreeCADGui.addCommand("FEM_SolverControl", _SolverControl()) FreeCADGui.addCommand("FEM_SolverElmer", _SolverElmer()) FreeCADGui.addCommand("FEM_SolverMystran", _SolverMystran()) diff --git a/src/Mod/Fem/femmesh/meshsetsgetter.py b/src/Mod/Fem/femmesh/meshsetsgetter.py index 27ccf458b5..0f9b62fa9e 100644 --- a/src/Mod/Fem/femmesh/meshsetsgetter.py +++ b/src/Mod/Fem/femmesh/meshsetsgetter.py @@ -74,7 +74,7 @@ class MeshSetsGetter: # TODO somehow this is not smart, pur mesh objects might be used often if self.member.geos_beamsection and ( type_of_obj(self.solver_obj) == "Fem::SolverCcxTools" - or type_of_obj(self.solver_obj) == "Fem::SolverCalculix" + or type_of_obj(self.solver_obj) == "Fem::SolverCalculiX" ): FreeCAD.Console.PrintError( "The mesh does not know the geometry it is made from. " diff --git a/src/Mod/Fem/femobjects/solver_calculix.py b/src/Mod/Fem/femobjects/solver_calculix.py new file mode 100644 index 0000000000..d7b74f4b7f --- /dev/null +++ b/src/Mod/Fem/femobjects/solver_calculix.py @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# *************************************************************************** +# * Copyright (c) 2017 Bernd Hahnebach * +# * Copyright (c) 2025 Mario Passaglia * +# * * +# * This file is part of FreeCAD. * +# * * +# * FreeCAD is free software: you can redistribute it and/or modify it * +# * under the terms of the GNU Lesser General Public License as * +# * published by the Free Software Foundation, either version 2.1 of the * +# * License, or (at your option) any later version. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, but * +# * WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * +# * Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Lesser General Public * +# * License along with FreeCAD. If not, see * +# * . * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM solver CalculiX document object" +__author__ = "Bernd Hahnebach, Mario Passaglia" +__url__ = "https://www.freecad.org" + +## @package solver_calculix +# \ingroup FEM +# \brief solver CalculiX object + +from . import base_fempythonobject +from femsolver.calculix.solver import _BaseSolverCalculix + + +class SolverCalculiX(base_fempythonobject.BaseFemPythonObject, _BaseSolverCalculix): + + Type = "Fem::SolverCalculiX" + + def __init__(self, obj): + super().__init__(obj) + self.add_attributes(obj) + + def onDocumentRestored(self, obj): + self.on_restore_of_document(obj) diff --git a/src/Mod/Fem/femsolver/calculix/calculixtools.py b/src/Mod/Fem/femsolver/calculix/calculixtools.py new file mode 100644 index 0000000000..88c91397c6 --- /dev/null +++ b/src/Mod/Fem/femsolver/calculix/calculixtools.py @@ -0,0 +1,190 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# *************************************************************************** +# * Copyright (c) 2025 Mario Passaglia * +# * * +# * This file is part of FreeCAD. * +# * * +# * FreeCAD is free software: you can redistribute it and/or modify it * +# * under the terms of the GNU Lesser General Public License as * +# * published by the Free Software Foundation, either version 2.1 of the * +# * License, or (at your option) any later version. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, but * +# * WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * +# * Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Lesser General Public * +# * License along with FreeCAD. If not, see * +# * . * +# * * +# *************************************************************************** + +__title__ = "Tools for the work with CalculiX solver" +__author__ = "Mario Passaglia" +__url__ = "https://www.freecad.org" + + +from PySide.QtCore import QProcess, QThread +import tempfile +import os +import shutil + +import FreeCAD +import Fem + +from . import writer +from .. import settings + +# from feminout import importCcxDatResults +from femmesh import meshsetsgetter +from femtools import membertools + + +class CalculiXTools: + + frd_var_conversion = { + "CONTACT": "Contact Displacement", + "PE": "Plastic Strain", + "CELS": "Contact Energy", + "ECD": "Current Density", + "EMFB": "Magnetic Field", + "EMFE": "Electric Field", + "ENER": "Internal Energy Density", + "FLUX": "Heat Flux", + "DISP": "Displacement", + "T": "Temperature", + "TOSTRAIN": "Strain", + "STRESS": "Stress", + "STR(%)": "Error", + } + + name = "CalculiX" + + 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) + + 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): + from femtools.checksanalysis import check_member_for_solver_calculix + + self._clear_results() + + message = check_member_for_solver_calculix( + self.analysis, + self.obj, + membertools.get_mesh_to_solve(self.analysis)[0], + membertools.AnalysisMember(self.analysis), + ) + + mesh_obj = membertools.get_mesh_to_solve(self.analysis)[0] + meshdatagetter = meshsetsgetter.MeshSetsGetter( + self.analysis, + self.obj, + mesh_obj, + membertools.AnalysisMember(self.analysis), + ) + meshdatagetter.get_mesh_sets() + + # write solver input + w = writer.FemInputWriterCcx( + self.analysis, + self.obj, + mesh_obj, + meshdatagetter.member, + self.obj.WorkingDirectory, + meshdatagetter.mat_geo_sets, + ) + self.model_file = w.write_solver_input() + # report to user if task succeeded + self.input_deck = os.path.splitext(os.path.basename(self.model_file))[0] + + def compute(self): + self._clear_results() + ccx_bin = settings.get_binary("Calculix") + env = self.process.processEnvironment() + num_cpu = self.fem_param.GetGroup("Ccx").GetInt( + "AnalysisNumCPUs", QThread.idealThreadCount() + ) + env.insert("OMP_NUM_THREADS", str(num_cpu)) + self.process.setProcessEnvironment(env) + self.process.setWorkingDirectory(self.obj.WorkingDirectory) + + command_list = ["-i", os.path.join(self.obj.WorkingDirectory, self.input_deck)] + self.process.start(ccx_bin, command_list) + + return self.process + + def update_properties(self): + # TODO at the moment, only one .vtm file is assumed + if not self.obj.Results: + pipeline = self.obj.Document.addObject("Fem::FemPostPipeline", self.obj.Name + "Result") + self.analysis.addObject(pipeline) + self.obj.Results = [pipeline] + self._load_ccxfrd_results() + # default display mode + pipeline.ViewObject.DisplayMode = "Surface" + pipeline.ViewObject.SelectionStyle = "BoundBox" + if self.obj.AnalysisType in ["static", "frequency", "buckling"]: + pipeline.ViewObject.Field = "Displacement" + elif self.obj.AnalysisType in ["thermomech"]: + pipeline.ViewObject.Field = "Temperature" + else: + self._load_ccxfrd_results() + + def _clear_results(self): + # result is a 'Result.vtm' file and a 'Result' directory + # with the .vtu files + 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 == ".vtm": + # remove .vtm file + os.remove(path) + # remove dir with .vtu files + shutil.rmtree(base) + + def _load_ccxfrd_results(self): + frd_result_prefix = os.path.join(self.obj.WorkingDirectory, self.input_deck) + Fem.frdToVTK(frd_result_prefix + ".frd") + files = os.listdir(self.obj.WorkingDirectory) + for f in files: + if f.endswith(".vtm"): + res = os.path.join(self.obj.WorkingDirectory, f) + self.obj.Results[0].read(res) + self.obj.Results[0].renameArrays(self.frd_var_conversion) + break + + def version(self): + p = QProcess() + ccx_bin = settings.get_binary("Calculix") + p.start(ccx_bin, ["-v"]) + p.waitForFinished() + info = p.readAll().data().decode() + return info diff --git a/src/Mod/Fem/femtaskpanels/task_solver_calculix.py b/src/Mod/Fem/femtaskpanels/task_solver_calculix.py new file mode 100644 index 0000000000..583286706e --- /dev/null +++ b/src/Mod/Fem/femtaskpanels/task_solver_calculix.py @@ -0,0 +1,168 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# *************************************************************************** +# * Copyright (c) 2025 Mario Passaglia * +# * * +# * This file is part of FreeCAD. * +# * * +# * FreeCAD is free software: you can redistribute it and/or modify it * +# * under the terms of the GNU Lesser General Public License as * +# * published by the Free Software Foundation, either version 2.1 of the * +# * License, or (at your option) any later version. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, but * +# * WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * +# * Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Lesser General Public * +# * License along with FreeCAD. If not, see * +# * . * +# * * +# *************************************************************************** + +__title__ = "FreeCAD task panel for CalculiX solver" +__author__ = "Mario Passaglia" +__url__ = "https://www.freecad.org" + +## @package task_solver_calculix +# \ingroup FEM +# \brief task panel for CalculiX solver + +from PySide import QtCore +from PySide import QtGui + +import FreeCAD +import FreeCADGui + +import FemGui + +from femsolver.calculix import calculixtools + +from . import base_femlogtaskpanel + + +class _TaskPanel(base_femlogtaskpanel._BaseLogTaskPanel): + """ + The TaskPanel for run CalculiX solver + """ + + def __init__(self, obj): + super().__init__(obj, calculixtools.CalculiXTools(obj)) + + self.form = FreeCADGui.PySideUic.loadUi( + FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/SolverCalculiX.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_analysis_type, + QtCore.SIGNAL("currentIndexChanged(int)"), + self.analysis_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.pb_working_directory, + QtCore.SIGNAL("clicked()"), + self.working_directory_clicked, + ) + QtCore.QObject.connect( + self.form.pb_solver_version, QtCore.SIGNAL("clicked()"), self.get_version + ) + QtCore.QObject.connect( + self.form.let_working_directory, + QtCore.SIGNAL("editingFinished()"), + self.working_directory_edited, + ) + + 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.analysis_type = self.obj.AnalysisType + + def set_object_params(self): + self.obj.AnalysisType = self.analysis_type + + def set_widgets(self): + "fills the widgets" + self.analysis_type_enum = self.obj.getEnumerationsOfProperty("AnalysisType") + index = self.analysis_type_enum.index(self.analysis_type) + self.form.cb_analysis_type.addItems(self.analysis_type_enum) + self.form.cb_analysis_type.setCurrentIndex(index) + + self.form.let_working_directory.setText(self.obj.WorkingDirectory) + self.form.ckb_working_directory.setChecked(False) + self.form.gpb_working_directory.setVisible(False) + + def analysis_type_changed(self, index): + self.analysis_type = self.analysis_type_enum[index] + self.obj.AnalysisType = self.analysis_type + + def working_directory_clicked(self): + directory = QtGui.QFileDialog.getExistingDirectory(dir=self.obj.WorkingDirectory) + if directory: + self.form.let_working_directory.setText(directory) + self.form.let_working_directory.editingFinished.emit() + + def working_directory_edited(self): + self.obj.WorkingDirectory = self.form.let_working_directory.text() + + def write_input_clicked(self): + self.prepared = False + self.run_complete = False + self.run_process() + + def edit_input_clicked(self): + ccx_param = self.tool.fem_param.GetGroup("Ccx") + internal = ccx_param.GetBool("UseInternalEditor", True) + ext_editor_path = ccx_param.GetString("ExternalEditorPath", "") + if internal or not ext_editor_path: + FemGui.open(self.tool.model_file) + else: + ext_editor_process = QtCore.QProcess() + ext_editor_process.start(ext_editor_path, [self.tool.model_file]) + ext_editor_process.waitForFinished() + + def working_directory_toggled(self, bool_value): + self.form.gpb_working_directory.setVisible(bool_value) diff --git a/src/Mod/Fem/femviewprovider/view_solver_calculix.py b/src/Mod/Fem/femviewprovider/view_solver_calculix.py new file mode 100644 index 0000000000..499967dbc8 --- /dev/null +++ b/src/Mod/Fem/femviewprovider/view_solver_calculix.py @@ -0,0 +1,50 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# *************************************************************************** +# * Copyright (c) 2025 Mario Passaglia * +# * * +# * This file is part of FreeCAD. * +# * * +# * FreeCAD is free software: you can redistribute it and/or modify it * +# * under the terms of the GNU Lesser General Public License as * +# * published by the Free Software Foundation, either version 2.1 of the * +# * License, or (at your option) any later version. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, but * +# * WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * +# * Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Lesser General Public * +# * License along with FreeCAD. If not, see * +# * . * +# * * +# *************************************************************************** + +__title__ = "FreeCAD FEM solver CalculiX view provider" +__author__ = "Mario Passaglia" +__url__ = "https://www.freecad.org" + +## @package view_calculix +# \ingroup FEM +# \brief solver CalculiX view provider + +import FreeCADGui + +from femtaskpanels import task_solver_calculix +from femviewprovider import view_base_femobject + + +class VPSolverCalculiX(view_base_femobject.VPBaseFemObject): + + def __init__(self, vobj): + super().__init__(vobj) + + def getIcon(self): + return ":/icons/FEM_SolverStandard.svg" + + def setEdit(self, vobj, mode=0): + task = task_solver_calculix._TaskPanel(vobj.Object) + FreeCADGui.Control.showDialog(task) + + return True From eaee52900292b64876afe19b13add2364db18d1f Mon Sep 17 00:00:00 2001 From: marioalexis Date: Thu, 3 Apr 2025 03:20:19 -0300 Subject: [PATCH 4/8] Fem: Update test --- src/Mod/Fem/femtest/app/test_object.py | 14 +++++++------- src/Mod/Fem/femtest/app/test_open.py | 4 ---- src/Mod/Fem/femtest/gui/test_open.py | 4 ---- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/Mod/Fem/femtest/app/test_object.py b/src/Mod/Fem/femtest/app/test_object.py index cbd67e9c84..cf9ed123ac 100644 --- a/src/Mod/Fem/femtest/app/test_object.py +++ b/src/Mod/Fem/femtest/app/test_object.py @@ -254,7 +254,7 @@ class TestObjectType(unittest.TestCase): self.assertEqual( "Fem::SolverCcxTools", type_of_obj(ObjectsFem.makeSolverCalculiXCcxTools(doc)) ) - self.assertEqual("Fem::SolverCalculix", type_of_obj(ObjectsFem.makeSolverCalculix(doc))) + self.assertEqual("Fem::SolverCalculiX", type_of_obj(ObjectsFem.makeSolverCalculiX(doc))) self.assertEqual("Fem::SolverElmer", type_of_obj(solverelmer)) self.assertEqual("Fem::SolverMystran", type_of_obj(ObjectsFem.makeSolverMystran(doc))) self.assertEqual("Fem::SolverZ88", type_of_obj(ObjectsFem.makeSolverZ88(doc))) @@ -434,7 +434,7 @@ class TestObjectType(unittest.TestCase): self.assertTrue( is_of_type(ObjectsFem.makeSolverCalculiXCcxTools(doc), "Fem::SolverCcxTools") ) - self.assertTrue(is_of_type(ObjectsFem.makeSolverCalculix(doc), "Fem::SolverCalculix")) + self.assertTrue(is_of_type(ObjectsFem.makeSolverCalculiX(doc), "Fem::SolverCalculiX")) self.assertTrue(is_of_type(solverelmer, "Fem::SolverElmer")) self.assertTrue(is_of_type(ObjectsFem.makeSolverMystran(doc), "Fem::SolverMystran")) self.assertTrue(is_of_type(ObjectsFem.makeSolverZ88(doc), "Fem::SolverZ88")) @@ -805,12 +805,12 @@ class TestObjectType(unittest.TestCase): self.assertTrue(is_derived_from(solver_ccxtools, "Fem::FemSolverObjectPython")) self.assertTrue(is_derived_from(solver_ccxtools, "Fem::SolverCcxTools")) - # SolverCalculix - solver_calculix = ObjectsFem.makeSolverCalculix(doc) + # SolverCalculiX + solver_calculix = ObjectsFem.makeSolverCalculiX(doc) self.assertTrue(is_derived_from(solver_calculix, "App::DocumentObject")) self.assertTrue(is_derived_from(solver_calculix, "Fem::FemSolverObject")) self.assertTrue(is_derived_from(solver_calculix, "Fem::FemSolverObjectPython")) - self.assertTrue(is_derived_from(solver_calculix, "Fem::SolverCalculix")) + self.assertTrue(is_derived_from(solver_calculix, "Fem::SolverCalculiX")) # SolverElmer solver_elmer = ObjectsFem.makeSolverElmer(doc) @@ -1036,7 +1036,7 @@ class TestObjectType(unittest.TestCase): ObjectsFem.makeSolverCalculiXCcxTools(doc).isDerivedFrom("Fem::FemSolverObjectPython") ) self.assertTrue( - ObjectsFem.makeSolverCalculix(doc).isDerivedFrom("Fem::FemSolverObjectPython") + ObjectsFem.makeSolverCalculiX(doc).isDerivedFrom("Fem::FemSolverObjectPython") ) self.assertTrue(solverelmer.isDerivedFrom("Fem::FemSolverObjectPython")) self.assertTrue( @@ -1156,7 +1156,7 @@ def create_all_fem_objects_doc(doc): ObjectsFem.makePostVtkFilterContours(doc, vres) analysis.addObject(ObjectsFem.makeSolverCalculiXCcxTools(doc)) - analysis.addObject(ObjectsFem.makeSolverCalculix(doc)) + analysis.addObject(ObjectsFem.makeSolverCalculiX(doc)) sol = analysis.addObject(ObjectsFem.makeSolverElmer(doc))[0] analysis.addObject(ObjectsFem.makeSolverMystran(doc)) analysis.addObject(ObjectsFem.makeSolverZ88(doc)) diff --git a/src/Mod/Fem/femtest/app/test_open.py b/src/Mod/Fem/femtest/app/test_open.py index 28928b01dc..3c6a84b6fb 100644 --- a/src/Mod/Fem/femtest/app/test_open.py +++ b/src/Mod/Fem/femtest/app/test_open.py @@ -259,10 +259,6 @@ class TestObjectOpen(unittest.TestCase): self.assertEqual(SolverCcxTools, doc.SolverCcxTools.Proxy.__class__) - from femsolver.calculix.solver import Proxy - - self.assertEqual(Proxy, doc.SolverCalculix.Proxy.__class__) - from femsolver.elmer.solver import Proxy self.assertEqual(Proxy, doc.SolverElmer.Proxy.__class__) diff --git a/src/Mod/Fem/femtest/gui/test_open.py b/src/Mod/Fem/femtest/gui/test_open.py index 8fea41e71b..d84f716645 100644 --- a/src/Mod/Fem/femtest/gui/test_open.py +++ b/src/Mod/Fem/femtest/gui/test_open.py @@ -243,10 +243,6 @@ class TestObjectOpen(unittest.TestCase): self.assertEqual(VPSolverCcxTools, doc.SolverCcxTools.ViewObject.Proxy.__class__) - from femsolver.calculix.solver import ViewProxy - - self.assertEqual(ViewProxy, doc.SolverCalculix.ViewObject.Proxy.__class__) - from femsolver.elmer.solver import ViewProxy self.assertEqual(ViewProxy, doc.SolverElmer.ViewObject.Proxy.__class__) From 69fbde7058ec75c0dc5e39bb4118582e6d9c5ab7 Mon Sep 17 00:00:00 2001 From: marioalexis Date: Thu, 3 Apr 2025 00:26:24 -0300 Subject: [PATCH 5/8] Fem: Rename VectorMode view property to Component --- src/Mod/Fem/Gui/TaskPostBoxes.cpp | 26 ++++----- src/Mod/Fem/Gui/TaskPostDisplay.ui | 22 +------- src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp | 56 ++++++++++++++----- src/Mod/Fem/Gui/ViewProviderFemPostObject.h | 6 +- 4 files changed, 61 insertions(+), 49 deletions(-) diff --git a/src/Mod/Fem/Gui/TaskPostBoxes.cpp b/src/Mod/Fem/Gui/TaskPostBoxes.cpp index 3989857ce5..37047f9883 100644 --- a/src/Mod/Fem/Gui/TaskPostBoxes.cpp +++ b/src/Mod/Fem/Gui/TaskPostBoxes.cpp @@ -392,7 +392,7 @@ TaskPostDisplay::TaskPostDisplay(ViewProviderFemPostObject* view, QWidget* paren updateEnumerationList(getTypedView()->DisplayMode, ui->Representation); updateEnumerationList(getTypedView()->Field, ui->Field); - updateEnumerationList(getTypedView()->VectorMode, ui->VectorMode); + updateEnumerationList(getTypedView()->Component, ui->VectorMode); // get Transparency from ViewProvider int trans = getTypedView()->Transparency.getValue(); @@ -432,18 +432,18 @@ void TaskPostDisplay::onRepresentationActivated(int i) { getTypedView()->DisplayMode.setValue(i); updateEnumerationList(getTypedView()->Field, ui->Field); - updateEnumerationList(getTypedView()->VectorMode, ui->VectorMode); + updateEnumerationList(getTypedView()->Component, ui->VectorMode); } void TaskPostDisplay::onFieldActivated(int i) { getTypedView()->Field.setValue(i); - updateEnumerationList(getTypedView()->VectorMode, ui->VectorMode); + updateEnumerationList(getTypedView()->Component, ui->VectorMode); } void TaskPostDisplay::onVectorModeActivated(int i) { - getTypedView()->VectorMode.setValue(i); + getTypedView()->Component.setValue(i); } void TaskPostDisplay::onTransparencyValueChanged(int i) @@ -660,7 +660,7 @@ TaskPostDataAlongLine::TaskPostDataAlongLine(ViewProviderFemPostDataAlongLine* v updateEnumerationList(getTypedView()->DisplayMode, ui->Representation); updateEnumerationList(getTypedView()->Field, ui->Field); - updateEnumerationList(getTypedView()->VectorMode, ui->VectorMode); + updateEnumerationList(getTypedView()->Component, ui->VectorMode); } TaskPostDataAlongLine::~TaskPostDataAlongLine() @@ -963,7 +963,7 @@ void TaskPostDataAlongLine::onRepresentationActivated(int i) { getTypedView()->DisplayMode.setValue(i); updateEnumerationList(getTypedView()->Field, ui->Field); - updateEnumerationList(getTypedView()->VectorMode, ui->VectorMode); + updateEnumerationList(getTypedView()->Component, ui->VectorMode); } void TaskPostDataAlongLine::onFieldActivated(int i) @@ -971,15 +971,15 @@ void TaskPostDataAlongLine::onFieldActivated(int i) getTypedView()->Field.setValue(i); std::string FieldName = ui->Field->currentText().toStdString(); getObject()->PlotData.setValue(FieldName); - updateEnumerationList(getTypedView()->VectorMode, ui->VectorMode); + updateEnumerationList(getTypedView()->Component, ui->VectorMode); - auto vecMode = static_cast(getView())->VectorMode.getEnum(); + auto vecMode = static_cast(getView())->Component.getEnum(); getObject()->PlotDataComponent.setValue(vecMode); } void TaskPostDataAlongLine::onVectorModeActivated(int i) { - getTypedView()->VectorMode.setValue(i); + getTypedView()->Component.setValue(i); int comp = ui->VectorMode->currentIndex(); getObject()->PlotDataComponent.setValue(comp); } @@ -1628,7 +1628,7 @@ void TaskPostContours::onFieldsChanged(int idx) // we must also update the VectorMode if (!getObject()->NoColor.getValue()) { auto newMode = getTypedObject()->VectorMode.getValue(); - getTypedView()->VectorMode.setValue(newMode); + getTypedView()->Component.setValue(newMode); } } @@ -1644,7 +1644,7 @@ void TaskPostContours::onVectorModeChanged(int idx) updateFields(); // now we can set the VectorMode if (!getObject()->NoColor.getValue()) { - getTypedView()->VectorMode.setValue(idx); + getTypedView()->Component.setValue(idx); } } } @@ -1668,9 +1668,9 @@ void TaskPostContours::onNoColorChanged(bool state) // the ViewProvider field starts with an additional entry "None", // therefore the desired new setting is idx + 1 getTypedView()->Field.setValue(currentField + 1); - // set the VectorMode too + // set the Component too auto currentMode = getTypedObject()->VectorMode.getValue(); - getTypedView()->VectorMode.setValue(currentMode); + getTypedView()->Component.setValue(currentMode); } recompute(); } diff --git a/src/Mod/Fem/Gui/TaskPostDisplay.ui b/src/Mod/Fem/Gui/TaskPostDisplay.ui index 0447dfcbdb..a123e7ade3 100644 --- a/src/Mod/Fem/Gui/TaskPostDisplay.ui +++ b/src/Mod/Fem/Gui/TaskPostDisplay.ui @@ -113,7 +113,7 @@ - Vector + Component @@ -125,26 +125,6 @@ 0 - - - Magnitute - - - - - X - - - - - Y - - - - - Z - - diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp index 86b13b39c1..0eaeb5047b 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp +++ b/src/Mod/Fem/Gui/ViewProviderFemPostObject.cpp @@ -160,11 +160,11 @@ ViewProviderFemPostObject::ViewProviderFemPostObject() "Coloring", App::Prop_None, "Select the field used for calculating the color"); - ADD_PROPERTY_TYPE(VectorMode, + ADD_PROPERTY_TYPE(Component, ((long)0), "Coloring", App::Prop_None, - "Select what to show for a vector field"); + "Select component to display"); ADD_PROPERTY_TYPE(Transparency, (0), "Object Style", @@ -453,8 +453,8 @@ void ViewProviderFemPostObject::updateProperties() Field.purgeTouched(); // Vector mode - if (VectorMode.hasEnums() && VectorMode.getValue() >= 0) { - val = VectorMode.getValueAsString(); + if (Component.hasEnums() && Component.getValue() >= 0) { + val = Component.getValueAsString(); } colorArrays.clear(); @@ -469,27 +469,41 @@ void ViewProviderFemPostObject::updateProperties() } if (data->GetNumberOfComponents() == 1) { + // scalar colorArrays.emplace_back("Not a vector"); } else { colorArrays.emplace_back("Magnitude"); - if (data->GetNumberOfComponents() >= 2) { + if (data->GetNumberOfComponents() == 2) { + // 2D vector colorArrays.emplace_back("X"); colorArrays.emplace_back("Y"); } - if (data->GetNumberOfComponents() >= 3) { + else if (data->GetNumberOfComponents() == 3) { + // 3D vector + colorArrays.emplace_back("X"); + colorArrays.emplace_back("Y"); colorArrays.emplace_back("Z"); } + else if (data->GetNumberOfComponents() == 6) { + // symmetric tensor + colorArrays.emplace_back("XX"); + colorArrays.emplace_back("YY"); + colorArrays.emplace_back("ZZ"); + colorArrays.emplace_back("XY"); + colorArrays.emplace_back("YZ"); + colorArrays.emplace_back("ZX"); + } } } - VectorMode.setValue(empty); + Component.setValue(empty); m_vectorEnum.setEnums(colorArrays); - VectorMode.setValue(m_vectorEnum); + Component.setValue(m_vectorEnum); it = std::ranges::find(colorArrays, val); if (!val.empty() && it != colorArrays.end()) { - VectorMode.setValue(val.c_str()); + Component.setValue(val.c_str()); } m_blockPropertyChanges = false; @@ -693,10 +707,10 @@ void ViewProviderFemPostObject::WriteColorData(bool ResetColorBarRange) return; } - int component = VectorMode.getValue() - 1; // 0 is either "Not a vector" or magnitude, - // for -1 is correct for magnitude. - // x y and z are one number too high - if (strcmp(VectorMode.getValueAsString(), "Not a vector") == 0) { + int component = Component.getValue() - 1; // 0 is either "Not a vector" or magnitude, + // for -1 is correct for magnitude. + // x y and z are one number too high + if (strcmp(Component.getValueAsString(), "Not a vector") == 0) { component = 0; } @@ -920,7 +934,7 @@ void ViewProviderFemPostObject::onChanged(const App::Property* prop) updateProperties(); WriteColorData(ResetColorBarRange); } - else if (prop == &VectorMode && setupPipeline()) { + else if (prop == &Component && setupPipeline()) { WriteColorData(ResetColorBarRange); } else if (prop == &Transparency) { @@ -1108,3 +1122,17 @@ void ViewProviderFemPostObject::onSelectionChanged(const Gui::SelectionChanges& } } } + +void ViewProviderFemPostObject::handleChangedPropertyName(Base::XMLReader& reader, + const char* typeName, + const char* propName) +{ + if (strcmp(propName, "Field") == 0 && strcmp(typeName, "App::PropertyEnumeration") == 0) { + App::PropertyEnumeration field; + field.Restore(reader); + Component.setValue(field.getValue()); + } + else { + Gui::ViewProviderDocumentObject::handleChangedPropertyName(reader, typeName, propName); + } +} diff --git a/src/Mod/Fem/Gui/ViewProviderFemPostObject.h b/src/Mod/Fem/Gui/ViewProviderFemPostObject.h index 19aa86be95..0e2e74954e 100644 --- a/src/Mod/Fem/Gui/ViewProviderFemPostObject.h +++ b/src/Mod/Fem/Gui/ViewProviderFemPostObject.h @@ -81,7 +81,7 @@ public: ~ViewProviderFemPostObject() override; App::PropertyEnumeration Field; - App::PropertyEnumeration VectorMode; + App::PropertyEnumeration Component; App::PropertyPercent Transparency; App::PropertyBool PlainColorEdgeOnSurface; App::PropertyColor EdgeColor; @@ -130,6 +130,10 @@ public: // //@} protected: + void handleChangedPropertyName(Base::XMLReader& reader, + const char* typeName, + const char* propName) override; + virtual void setupTaskDialog(TaskDlgPost* dlg); bool setupPipeline(); void updateVtk(); From f9c4e6e23cb6f0496467a2e959d624cca850dc31 Mon Sep 17 00:00:00 2001 From: marioalexis Date: Wed, 2 Apr 2025 11:06:01 -0300 Subject: [PATCH 6/8] Fem: Add preference to create CalculiX result as pipeline - fixes #20541 --- src/Mod/Fem/Gui/DlgSettingsFemCcx.ui | 27 ++++++++++++++++++++++++ src/Mod/Fem/Gui/DlgSettingsFemCcxImp.cpp | 2 ++ src/Mod/Fem/Gui/Workbench.cpp | 4 ++-- src/Mod/Fem/femcommands/commands.py | 6 ++---- 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/Mod/Fem/Gui/DlgSettingsFemCcx.ui b/src/Mod/Fem/Gui/DlgSettingsFemCcx.ui index cd76b7af51..00444ce443 100644 --- a/src/Mod/Fem/Gui/DlgSettingsFemCcx.ui +++ b/src/Mod/Fem/Gui/DlgSettingsFemCcx.ui @@ -659,6 +659,33 @@ + + + + Result object + + + + + + + Pipeline only + + + Load results as pipeline instead of Result Object. +By uncheck this option, CalculiX command behave like SolverCalculiXCcxTools + + + false + + + ResultAsPipeline + + + Mod/Fem/Ccx + + + diff --git a/src/Mod/Fem/Gui/DlgSettingsFemCcxImp.cpp b/src/Mod/Fem/Gui/DlgSettingsFemCcxImp.cpp index cbded442f1..3aaa88e7a3 100644 --- a/src/Mod/Fem/Gui/DlgSettingsFemCcxImp.cpp +++ b/src/Mod/Fem/Gui/DlgSettingsFemCcxImp.cpp @@ -72,6 +72,7 @@ void DlgSettingsFemCcxImp::saveSettings() ui->dsb_ccx_analysis_time->onSave(); // Analysis time ui->dsb_ccx_minimum_time_step->onSave(); // Minimum time step ui->dsb_ccx_maximum_time_step->onSave(); // Maximum time step + ui->ckb_pipeline_result->onSave(); ui->cb_analysis_type->onSave(); ui->cb_BeamShellOutput->onSave(); // Beam shell output 3d or 2d @@ -99,6 +100,7 @@ void DlgSettingsFemCcxImp::loadSettings() ui->dsb_ccx_analysis_time->onRestore(); // Analysis time ui->dsb_ccx_minimum_time_step->onRestore(); // Minimum time step ui->dsb_ccx_maximum_time_step->onRestore(); // Maximum time step + ui->ckb_pipeline_result->onRestore(); ui->cb_analysis_type->onRestore(); ui->cb_BeamShellOutput->onRestore(); // Beam shell output 3d or 2d diff --git a/src/Mod/Fem/Gui/Workbench.cpp b/src/Mod/Fem/Gui/Workbench.cpp index cbfaea76c1..66d46af435 100644 --- a/src/Mod/Fem/Gui/Workbench.cpp +++ b/src/Mod/Fem/Gui/Workbench.cpp @@ -166,7 +166,7 @@ Gui::ToolBarItem* Workbench::setupToolBars() const Gui::ToolBarItem* solve = new Gui::ToolBarItem(root); solve->setCommand("Solve"); if (!Fem::Tools::checkIfBinaryExists("Ccx", "ccx", "ccx").empty()) { - *solve << "FEM_SolverCalculiXCcxTools"; + *solve << "FEM_SolverCalculiX"; } if (!Fem::Tools::checkIfBinaryExists("Elmer", "elmer", "ElmerSolver").empty()) { *solve << "FEM_SolverElmer"; @@ -326,7 +326,7 @@ Gui::MenuItem* Workbench::setupMenuBar() const Gui::MenuItem* solve = new Gui::MenuItem; root->insertItem(item, solve); solve->setCommand("&Solve"); - *solve << "FEM_SolverCalculiXCcxTools" + *solve << "FEM_SolverCalculiX" << "FEM_SolverElmer" << "FEM_SolverMystran" << "FEM_SolverZ88" diff --git a/src/Mod/Fem/femcommands/commands.py b/src/Mod/Fem/femcommands/commands.py index 751adbb7bf..fc34ada6d6 100644 --- a/src/Mod/Fem/femcommands/commands.py +++ b/src/Mod/Fem/femcommands/commands.py @@ -1057,13 +1057,11 @@ class _SolverCalculiX(CommandManager): def __init__(self): super().__init__() self.pixmap = "FEM_SolverStandard" - self.menutext = Qt.QT_TRANSLATE_NOOP( - "FEM_SolverCalculiX", "Solver CalculiX (new framework)" - ) + self.menutext = Qt.QT_TRANSLATE_NOOP("FEM_SolverCalculiX", "Solver CalculiX") self.accel = "S, C" self.tooltip = Qt.QT_TRANSLATE_NOOP( "FEM_SolverCalculiX", - "Creates a FEM solver CalculiX new framework (less result error handling)", + "Creates a FEM solver CalculiX", ) self.is_active = "with_analysis" From 1126723284e32fdc527ed05084ea3f1e16aeb784 Mon Sep 17 00:00:00 2001 From: marioalexis Date: Fri, 11 Apr 2025 13:31:20 -0300 Subject: [PATCH 7/8] Fem: Add option to set data mode for SolverCalculiX --- src/Mod/Fem/App/AppFemPy.cpp | 6 ++--- src/Mod/Fem/App/FemVTKTools.cpp | 4 ++- src/Mod/Fem/App/FemVTKTools.h | 2 +- src/Mod/Fem/Gui/DlgSettingsFemCcx.ui | 27 +++++++++++++++++++ src/Mod/Fem/Gui/DlgSettingsFemCcxImp.cpp | 2 ++ .../Fem/femsolver/calculix/calculixtools.py | 3 ++- 6 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/Mod/Fem/App/AppFemPy.cpp b/src/Mod/Fem/App/AppFemPy.cpp index bbbe4520d4..c87474fd08 100644 --- a/src/Mod/Fem/App/AppFemPy.cpp +++ b/src/Mod/Fem/App/AppFemPy.cpp @@ -252,14 +252,14 @@ private: Py::Object frdToVTK(const Py::Tuple& args) { char* filename = nullptr; - - if (!PyArg_ParseTuple(args.ptr(), "et", "utf-8", &filename)) { + PyObject* binary = Py_True; + if (!PyArg_ParseTuple(args.ptr(), "et|O!", "utf-8", &filename, &PyBool_Type, &binary)) { throw Py::Exception(); } std::string encodedName = std::string(filename); PyMem_Free(filename); - FemVTKTools::frdToVTK(encodedName.c_str()); + FemVTKTools::frdToVTK(encodedName.c_str(), Base::asBoolean(binary)); return Py::None(); } diff --git a/src/Mod/Fem/App/FemVTKTools.cpp b/src/Mod/Fem/App/FemVTKTools.cpp index 78958fff92..e8cb884d93 100644 --- a/src/Mod/Fem/App/FemVTKTools.cpp +++ b/src/Mod/Fem/App/FemVTKTools.cpp @@ -1711,7 +1711,7 @@ vtkSmartPointer readFRD(std::ifstream& ifstr) } // namespace FRDReader -void FemVTKTools::frdToVTK(const char* filename) +void FemVTKTools::frdToVTK(const char* filename, bool binary) { Base::FileInfo fi(filename); @@ -1733,6 +1733,8 @@ void FemVTKTools::frdToVTK(const char* filename) std::string type = info->GetValue(0).c_str(); auto writer = vtkSmartPointer::New(); + writer->SetDataMode(binary ? vtkXMLMultiBlockDataWriter::Binary + : vtkXMLMultiBlockDataWriter::Ascii); std::string blockFile = dir + "/" + fi.fileNamePure() + type + "." + writer->GetDefaultFileExtension(); diff --git a/src/Mod/Fem/App/FemVTKTools.h b/src/Mod/Fem/App/FemVTKTools.h index 00db4d0763..cf8db957b4 100644 --- a/src/Mod/Fem/App/FemVTKTools.h +++ b/src/Mod/Fem/App/FemVTKTools.h @@ -72,7 +72,7 @@ public: // write FemResult (activeObject if res= NULL) to vtkUnstructuredGrid dataset file static void writeResult(const char* filename, const App::DocumentObject* res = nullptr); - static void frdToVTK(const char* filename); + static void frdToVTK(const char* filename, bool binary = true); }; } // namespace Fem diff --git a/src/Mod/Fem/Gui/DlgSettingsFemCcx.ui b/src/Mod/Fem/Gui/DlgSettingsFemCcx.ui index 00444ce443..3a82a89a5e 100644 --- a/src/Mod/Fem/Gui/DlgSettingsFemCcx.ui +++ b/src/Mod/Fem/Gui/DlgSettingsFemCcx.ui @@ -686,6 +686,33 @@ By uncheck this option, CalculiX command behave like SolverCalculiXCcxTools + + + + Result format + + + + + + + Save result in binary format. +Only ta kes effect if 'Pipeline only' is enabled + + + Use binary format + + + false + + + BinaryOutput + + + Mod/Fem/Ccx + + + diff --git a/src/Mod/Fem/Gui/DlgSettingsFemCcxImp.cpp b/src/Mod/Fem/Gui/DlgSettingsFemCcxImp.cpp index 3aaa88e7a3..eca76761c8 100644 --- a/src/Mod/Fem/Gui/DlgSettingsFemCcxImp.cpp +++ b/src/Mod/Fem/Gui/DlgSettingsFemCcxImp.cpp @@ -73,6 +73,7 @@ void DlgSettingsFemCcxImp::saveSettings() ui->dsb_ccx_minimum_time_step->onSave(); // Minimum time step ui->dsb_ccx_maximum_time_step->onSave(); // Maximum time step ui->ckb_pipeline_result->onSave(); + ui->ckb_result_format->onSave(); ui->cb_analysis_type->onSave(); ui->cb_BeamShellOutput->onSave(); // Beam shell output 3d or 2d @@ -101,6 +102,7 @@ void DlgSettingsFemCcxImp::loadSettings() ui->dsb_ccx_minimum_time_step->onRestore(); // Minimum time step ui->dsb_ccx_maximum_time_step->onRestore(); // Maximum time step ui->ckb_pipeline_result->onRestore(); + ui->ckb_result_format->onRestore(); ui->cb_analysis_type->onRestore(); ui->cb_BeamShellOutput->onRestore(); // Beam shell output 3d or 2d diff --git a/src/Mod/Fem/femsolver/calculix/calculixtools.py b/src/Mod/Fem/femsolver/calculix/calculixtools.py index 88c91397c6..331de692b1 100644 --- a/src/Mod/Fem/femsolver/calculix/calculixtools.py +++ b/src/Mod/Fem/femsolver/calculix/calculixtools.py @@ -172,7 +172,8 @@ class CalculiXTools: def _load_ccxfrd_results(self): frd_result_prefix = os.path.join(self.obj.WorkingDirectory, self.input_deck) - Fem.frdToVTK(frd_result_prefix + ".frd") + binary_mode = self.fem_param.GetGroup("Ccx").GetBool("BinaryOutput", False) + Fem.frdToVTK(frd_result_prefix + ".frd", binary_mode) files = os.listdir(self.obj.WorkingDirectory) for f in files: if f.endswith(".vtm"): From 89eb6789c4865c34ceee0009b1fa1c136591b08d Mon Sep 17 00:00:00 2001 From: marioalexis Date: Fri, 11 Apr 2025 15:44:32 -0300 Subject: [PATCH 8/8] Fem: Update command FEM_SolverRun --- src/Mod/Fem/femcommands/commands.py | 42 ++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/src/Mod/Fem/femcommands/commands.py b/src/Mod/Fem/femcommands/commands.py index fc34ada6d6..2025fbaabd 100644 --- a/src/Mod/Fem/femcommands/commands.py +++ b/src/Mod/Fem/femcommands/commands.py @@ -29,6 +29,9 @@ __url__ = "https://www.freecad.org" # \ingroup FEM # \brief FreeCAD FEM command definitions +from PySide import QtCore +from PySide import QtGui + import FreeCAD import FreeCADGui from FreeCAD import Qt @@ -1162,13 +1165,44 @@ class _SolverRun(CommandManager): "FEM_SolverRun", "Runs the calculations for the selected solver" ) self.is_active = "with_solver" + self.tool = None def Activated(self): - from femsolver.run import run_fem_solver + if self.selobj.Proxy.Type == "Fem::SolverCalculiX": + QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) + try: + from femsolver.calculix.calculixtools import CalculiXTools - run_fem_solver(self.selobj) - FreeCADGui.Selection.clearSelection() - FreeCAD.ActiveDocument.recompute() + self.tool = CalculiXTools(self.selobj) + self._conn(self.tool) + self.tool.prepare() + self.tool.compute() + except Exception as e: + QtGui.QApplication.restoreOverrideCursor() + raise + + else: + from femsolver.run import run_fem_solver + + run_fem_solver(self.selobj) + FreeCADGui.Selection.clearSelection() + FreeCAD.ActiveDocument.recompute() + + def _conn(self, tool): + QtCore.QObject.connect( + tool.process, + QtCore.SIGNAL("finished(int, QProcess::ExitStatus)"), + self._process_finished, + ) + + def _process_finished(self, code, status): + if status == QtCore.QProcess.ExitStatus.NormalExit and code == 0: + self.tool.update_properties() + FreeCAD.ActiveDocument.recompute() + QtGui.QApplication.restoreOverrideCursor() + else: + QtGui.QApplication.restoreOverrideCursor() + FreeCAD.Console.PrintError("Process finished with errors. Result not updated\n") class _SolverZ88(CommandManager):