# SPDX-License-Identifier: LGPL-2.1-or-later # *************************************************************************** # * Copyright (c) 2024 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 Netgen mesher" __author__ = "Mario Passaglia" __url__ = "https://www.freecad.org" import numpy as np import shutil import sys import tempfile from PySide.QtCore import QProcess import FreeCAD import Fem try: from netgen import occ, meshing, config as ng_config import pyngcore as ngcore except ModuleNotFoundError: FreeCAD.Console.PrintError("To use FemMesh Netgen objects, install the Netgen Python bindings") class NetgenTools: # to change order of nodes from netgen to smesh order_edge = { 2: [0, 1, 2], # seg2 3: [0, 1, 2], # seg3 } order_face = { 3: [0, 1, 2, 3, 4, 5, 6, 7], # tria3 6: [0, 1, 2, 5, 3, 4, 6, 7], # tria6 4: [0, 1, 2, 3, 4, 5, 6, 7], # quad4 8: [0, 1, 2, 3, 4, 7, 5, 6], # quad8 } order_volume = { 4: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], # tetra4 10: [0, 1, 2, 3, 4, 7, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], # tetra10 8: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], # hexa8 20: [0, 1, 2, 3, 4, 5, 6, 7, 8, 11, 9, 10, 12, 15, 13, 14, 16, 17, 18, 19], # hexa20 5: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], # pyra5 13: [0, 1, 2, 3, 4, 5, 8, 6, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], # pyra13 6: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], # penta6 15: [0, 1, 2, 3, 4, 5, 6, 8, 7, 12, 14, 13, 9, 10, 11, 15, 16, 17, 18, 19], # penta15 } meshing_step = { "AnalyzeGeometry": 1, # MESHCONST_ANALYSE "MeshEdges": 2, # MESHCONST_MESHEDGES "MeshSurface": 3, # MESHCONST_MESHSURFACE "OptimizeSurface": 4, # MESHCONST_OPTSURFACE "MeshVolume": 5, # MESHCONST_MESHVOLUME "OptimizeVolume": 6, # MESHCONST_OPTVOLUME } name = "Netgen" def __init__(self, obj): self.obj = obj self.fem_mesh = None self.process = None self.tmpdir = "" self.process = QProcess() self.mesh_params = {} def write_geom(self): if not self.tmpdir: self.tmpdir = tempfile.mkdtemp(prefix="fem_") global_pla = self.obj.Shape.getGlobalPlacement() geom = self.obj.Shape.getPropertyOfGeometry() # get partner shape geom_trans = geom.transformed(FreeCAD.Placement().Matrix) geom_trans.Placement = global_pla self.brep_file = self.tmpdir + "/shape.brep" self.result_file = self.tmpdir + "/result.npy" geom_trans.exportBrep(self.brep_file) code = """ from femmesh.netgentools import NetgenTools NetgenTools.run_netgen(**{params}) """ def prepare(self): self.write_geom() self.mesh_params = { "brep_file": self.brep_file, "threads": self.obj.Threads, "heal": self.obj.HealShape, "params": self.get_meshing_parameters(), "second_order": self.obj.SecondOrder, "second_order_linear": self.obj.SecondOrderLinear, "result_file": self.result_file, "mesh_region": self.get_mesh_region(), } def compute(self): code_str = self.code.format(params=self.mesh_params) self.process.start(sys.executable, ["-c", code_str]) return self.process @staticmethod def run_netgen( brep_file, threads, heal, params, second_order, second_order_linear, result_file, mesh_region, ): geom = occ.OCCGeometry(brep_file) ngcore.SetNumThreads(threads) shape = geom.shape for items, l in mesh_region: for t, n in items: if t == "Vertex": shape.vertices.vertices[n - 1].maxh = l elif t == "Edge": shape.edges.edges[n - 1].maxh = l elif t == "Face": shape.faces.faces[n - 1].maxh = l elif t == "Solid": shape.solids.solids[n - 1].maxh = l with ngcore.TaskManager(): geom = occ.OCCGeometry(shape) if heal: geom.Heal() mesh = geom.GenerateMesh(mp=meshing.MeshingParameters(**params)) result = { "coords": [], "Edges": [[], []], "Faces": [[], []], "Volumes": [[], []], } groups = {"Edges": [], "Faces": [], "Solids": []} # save empty data if last step is geometry analysis if params["perfstepsend"] == NetgenTools.meshing_step["AnalyzeGeometry"]: np.save(result_file, [result, groups]) return None if second_order: if second_order_linear: mesh.SetGeometry(None) mesh.SecondOrder() coords = mesh.Coordinates() edges = mesh.Elements1D().NumPy() faces = mesh.Elements2D().NumPy() volumes = mesh.Elements3D().NumPy() nod_edges = edges["nodes"] nod_faces = faces["nodes"] nod_volumes = volumes["nodes"] np_edges = (nod_edges != 0).sum(axis=1).tolist() np_faces = faces["np"].tolist() np_volumes = volumes["np"].tolist() # set smesh node order for i in range(faces.size): nod_faces[i] = nod_faces[i][NetgenTools.order_face[np_faces[i]]] for i in range(volumes.size): nod_volumes[i] = nod_volumes[i][NetgenTools.order_volume[np_volumes[i]]] flat_edges = nod_edges[nod_edges != 0].tolist() flat_faces = nod_faces[nod_faces != 0].tolist() flat_volumes = nod_volumes[nod_volumes != 0].tolist() result = { "coords": coords, "Edges": [flat_edges, np_edges], "Faces": [flat_faces, np_faces], "Volumes": [flat_volumes, np_volumes], } # create groups nb_edges = edges.size nb_faces = faces.size nb_volumes = volumes.size idx_edges = edges["index"] idx_faces = faces["index"] idx_volumes = volumes["index"] for i in np.unique(idx_edges): edge_i = (np.nonzero(idx_edges == i)[0] + 1).tolist() groups["Edges"].append([i, edge_i]) for i in np.unique(idx_faces): face_i = (np.nonzero(idx_faces == i)[0] + nb_edges + 1).tolist() groups["Faces"].append([i, face_i]) for i in np.unique(idx_volumes): volume_i = (np.nonzero(idx_volumes == i)[0] + nb_edges + nb_faces + 1).tolist() groups["Solids"].append([i, volume_i]) np.save(result_file, [result, groups]) def fem_mesh_from_result(self): fem_mesh = Fem.FemMesh() # load Netgen result netgen_result, groups = np.load(self.result_file, allow_pickle=True) for node in netgen_result["coords"]: fem_mesh.addNode(*node) fem_mesh.addEdgeList(*netgen_result["Edges"]) fem_mesh.addFaceList(*netgen_result["Faces"]) fem_mesh.addVolumeList(*netgen_result["Volumes"]) for g in groups["Edges"]: grp_id = fem_mesh.addGroup("Edge" + str(g[0]), "Edge") fem_mesh.addGroupElements(grp_id, g[1]) for g in groups["Faces"]: grp_id = fem_mesh.addGroup("Face" + str(g[0]), "Face") fem_mesh.addGroupElements(grp_id, g[1]) for g in groups["Solids"]: grp_id = fem_mesh.addGroup("Solid" + str(g[0]), "Volume") fem_mesh.addGroupElements(grp_id, g[1]) return fem_mesh def update_properties(self): self.obj.FemMesh = self.fem_mesh_from_result() def get_meshing_parameters(self): params = { "optimize3d": self.obj.Optimize3d, "optimize2d": self.obj.Optimize2d, "optsteps3d": self.obj.OptimizationSteps3d, "optsteps2d": self.obj.OptimizationSteps2d, "opterrpow": self.obj.OptimizationErrorPower, "blockfill": self.obj.BlockFill, "filldist": self.obj.FillDistance.Value, "safety": self.obj.Safety, "relinnersafety": self.obj.RelinnerSafety, "uselocalh": self.obj.UseLocalH, "grading": self.obj.GrowthRate, "delaunay": self.obj.Delaunay, "delaunay2d": self.obj.Delaunay2d, "maxh": self.obj.MaxSize.Value, "minh": self.obj.MinSize.Value, "startinsurface": self.obj.StartInSurface, "checkoverlap": self.obj.CheckOverlap, "checkoverlappingboundary": self.obj.CheckOverlappingBoundary, "checkchartboundary": self.obj.CheckChartBoundary, "curvaturesafety": self.obj.CurvatureSafety, "segmentsperedge": self.obj.SegmentsPerEdge, "elsizeweight": self.obj.ElementSizeWeight, "parthread": self.obj.ParallelMeshing, "perfstepsstart": NetgenTools.meshing_step[self.obj.StartStep], "perfstepsend": NetgenTools.meshing_step[self.obj.EndStep], "giveuptol2d": self.obj.GiveUpTolerance2d, "giveuptol": self.obj.GiveUpTolerance, "giveuptolopenquads": self.obj.GiveUpToleranceOpenQuads, "maxoutersteps": self.obj.MaxOuterSteps, "starshapeclass": self.obj.StarShapeClass, "baseelnp": self.obj.BaseElementNp, "sloppy": self.obj.Sloppy, "badellimit": self.obj.BadElementLimit, "check_impossible": self.obj.CheckImpossible, "only3D_domain_nr": self.obj.Only3dDomainNr, "secondorder": self.obj.SecondOrder, "elementorder": self.obj.ElementOrder, "quad_dominated": self.obj.QuadDominated, "try_hexes": self.obj.TryHexes, "inverttets": self.obj.InvertTets, "inverttrigs": self.obj.InvertTrigs, "autozrefine": self.obj.AutoZRefine, "parallel_meshing": self.obj.ParallelMeshing, "nthreads": self.obj.Threads, "closeedgefac": self.obj.CloseEdgeFactor, } # set specific parameters by fineness if self.obj.Fineness == "VeryCoarse": params["curvaturesafety"] = 1 params["segmentsperedge"] = 0.3 params["grading"] = 0.7 params["closeedgefac"] = 0.5 params["optsteps3d"] = 5 elif self.obj.Fineness == "Coarse": params["curvaturesafety"] = 1.5 params["segmentsperedge"] = 0.5 params["grading"] = 0.5 params["closeedgefac"] = 1 params["optsteps3d"] = 5 elif self.obj.Fineness == "Moderate": params["curvaturesafety"] = 2 params["segmentsperedge"] = 1 params["grading"] = 0.3 params["closeedgefac"] = 2 params["optsteps3d"] = 5 elif self.obj.Fineness == "Fine": params["curvaturesafety"] = 3 params["segmentsperedge"] = 2 params["grading"] = 0.2 params["closeedgefac"] = 3.5 params["optsteps3d"] = 5 elif self.obj.Fineness == "VeryFine": params["curvaturesafety"] = 5 params["segmentsperedge"] = 3 params["grading"] = 0.1 params["closeedgefac"] = 5 params["optsteps3d"] = 5 return params def get_mesh_region(self): from Part import Shape as PartShape result = [] for reg in self.obj.MeshRegionList: for s, sub_list in reg.References: if s.isDerivedFrom("App::GeoFeature") and isinstance( s.getPropertyOfGeometry(), PartShape ): geom = s.getPropertyOfGeometry() sub_obj = [s.getSubObject(_) for _ in sub_list] sub_sh = geom.findSubShape(sub_obj) l = reg.CharacteristicLength.getValueAs("mm").Value result.append((sub_sh, l)) return result @staticmethod def version(): result = "{}: {}\n" + "{}: {}\n" + "{}: {}\n" + "{}: {}" return result.format( "Netgen", ng_config.version, "Python", ng_config.PYTHON_VERSION, "OpenCASCADE", occ.occ_version, "Use MPI", ng_config.USE_MPI, ) def __del__(self): if self.tmpdir: shutil.rmtree(self.tmpdir)