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