# 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, QProcessEnvironment 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: 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 = QProcessEnvironment.systemEnvironment() num_cpu = self.fem_param.GetGroup("Ccx").GetInt( "AnalysisNumCPUs", QThread.idealThreadCount() ) env.insert("OMP_NUM_THREADS", str(num_cpu)) pastix_prec = "1" if self.obj.PastixMixedPrecision else "0" env.insert("PASTIX_MIXED_PRECISION", pastix_prec) 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 self._load_ccxfrd_results() self._load_ccxdat_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) elif ext == ".dat": # remove .dat file os.remove(path) def _load_ccxdat_results(self): # search dat output dat = None for res in self.obj.Results: if res.isDerivedFrom("App::TextDocument"): dat = res if not dat: # create dat output dat = self.obj.Document.addObject("App::TextDocument", self.obj.Name + "Output") self.analysis.addObject(dat) tmp = self.obj.Results tmp.append(dat) self.obj.Results = tmp files = os.listdir(self.obj.WorkingDirectory) for f in files: if f.endswith(".dat"): dat_file = os.path.join(self.obj.WorkingDirectory, f) with open(dat_file, "r") as file: dat.Text = file.read() break def _load_ccxfrd_results(self): # search current pipeline pipeline = None create = False for res in self.obj.Results: if res.isDerivedFrom("Fem::FemPostPipeline"): pipeline = res if not pipeline: # create pipeline pipeline = self.obj.Document.addObject("Fem::FemPostPipeline", self.obj.Name + "Result") self.analysis.addObject(pipeline) tmp = self.obj.Results tmp.append(pipeline) self.obj.Results = tmp create = True frd_result_prefix = os.path.join(self.obj.WorkingDirectory, self.input_deck) 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"): vtm_file = os.path.join(self.obj.WorkingDirectory, f) pipeline.read(vtm_file) pipeline.renameArrays(self.frd_var_conversion(self.obj.AnalysisType)) break if create: # default display mode pipeline.ViewObject.DisplayMode = "Surface" pipeline.ViewObject.SelectionStyle = "BoundBox" pipeline.ViewObject.Field = self.get_default_field(self.obj.AnalysisType) def frd_var_conversion(self, analysis_type): common = { "CONTACT": "Contact Displacement", "PE": "Plastic Strain", "CELS": "Contact Energy", "ECD": "Current Density", "EMFB": "Magnetic Field", "EMFE": "Electric Field", "ENER": "Internal Energy Density", "DISP": "Displacement", "TOSTRAIN": "Strain", "STRESS": "Stress", "STR(%)": "Error", } thermo = {"FLUX": "Heat Flux", "T": "Temperature"} electrostatic = {"T": "Potential", "FLUX": "Electric Flux Density"} match analysis_type: case "thermomech": common.update(thermo) case "electromagnetic": common.update(electrostatic) return common def get_default_field(self, analysis_type): match analysis_type: case "static" | "frequency" | "buckling": return "Displacement" case "thermomech": return "Temperature" case "electromagnetic": return "Potential" 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