Merge pull request #18608 from marioalexis84/fem-netgen_log_verbosity

Fem: Add preference entry to set Netgen (new implementation) log verbosity and number of threads for meshing
This commit is contained in:
Chris Hennes
2024-12-23 12:02:32 -05:00
committed by GitHub
13 changed files with 326 additions and 135 deletions

View File

@@ -646,6 +646,11 @@ SET(FemGuiViewProvider_SRCS
femviewprovider/view_solver_ccxtools.py
)
SET(FemGuiPreferencePages_SRCS
fempreferencepages/__init__.py
fempreferencepages/dlg_settings_netgen.py
)
SET(FemAllGuiScripts
${FemGuiBaseModules_SRCS}
${FemGuiObjects_SRCS}
@@ -653,6 +658,7 @@ SET(FemAllGuiScripts
${FemGuiTests_SRCS}
${FemGuiUtils_SRCS}
${FemGuiViewProvider_SRCS}
${FemGuiPreferencePages_SRCS}
)
if(BUILD_GUI)
@@ -669,4 +675,5 @@ if(BUILD_GUI)
INSTALL(FILES ${FemGuiTests_SRCS} DESTINATION Mod/Fem/femtest/gui/)
INSTALL(FILES ${FemGuiUtils_SRCS} DESTINATION Mod/Fem/femguiutils/)
INSTALL(FILES ${FemGuiViewProvider_SRCS} DESTINATION Mod/Fem/femviewprovider/)
INSTALL(FILES ${FemGuiPreferencePages_SRCS} DESTINATION Mod/Fem/fempreferencepages/)
endif(BUILD_GUI)

View File

@@ -301,10 +301,10 @@
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="toolTip">
<string>Set to zero to automatically use maximum number of available cores</string>
<string>Number of threads used for analysis</string>
</property>
<property name="minimum">
<number>0</number>
<number>1</number>
</property>
<property name="prefEntry" stdset="0">
<cstring>AnalysisNumCPUs</cstring>

View File

@@ -45,9 +45,6 @@ DlgSettingsFemCcxImp::DlgSettingsFemCcxImp(QWidget* parent)
// set ranges
ui->dsb_ccx_analysis_time->setMaximum(FLOAT_MAX);
ui->dsb_ccx_initial_time_step->setMaximum(FLOAT_MAX);
// determine number of CPU cores
int processor_count = QThread::idealThreadCount();
ui->sb_ccx_numcpu->setMaximum(processor_count);
connect(ui->fc_ccx_binary_path,
&Gui::PrefFileChooser::fileNameChanged,
@@ -117,6 +114,11 @@ void DlgSettingsFemCcxImp::loadSettings()
ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/Mod/Fem/Ccx");
// determine number of CPU threads
int processor_count = hGrp->GetInt("AnalysisNumCPUs", QThread::idealThreadCount());
ui->sb_ccx_numcpu->setValue(processor_count);
int index = hGrp->GetInt("Solver", 0);
if (index > -1) {
ui->cmb_solver->setCurrentIndex(index);

View File

@@ -151,6 +151,32 @@
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="lbl_threads">
<property name="text">
<string>Number of threads</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="Gui::PrefSpinBox" name="sb_threads">
<property name="alignment">
<set>Qt::AlignLeft|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="toolTip">
<string>Number of threads used for meshing</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="prefEntry" stdset="0">
<cstring>NumOfThreads</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Fem/Gmsh</cstring>
</property>
</widget>
</item>
</layout>
</item>
</layout>
@@ -193,6 +219,11 @@
<extends>QComboBox</extends>
<header>Gui/PrefWidgets.h</header>
</customwidget>
<customwidget>
<class>Gui::PrefSpinBox</class>
<extends>QSpinBox</extends>
<header>Gui/PrefWidgets.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="Resources/Fem.qrc"/>

View File

@@ -25,6 +25,7 @@
#include "PreCompiled.h"
#ifndef _PreComp_
#include <QMessageBox>
#include <QThread>
#endif
#include <App/Application.h>
@@ -53,6 +54,7 @@ void DlgSettingsFemGmshImp::saveSettings()
ui->cb_gmsh_binary_std->onSave();
ui->fc_gmsh_binary_path->onSave();
ui->cb_log_verbosity->onSave();
ui->sb_threads->onSave();
}
void DlgSettingsFemGmshImp::loadSettings()
@@ -60,6 +62,11 @@ void DlgSettingsFemGmshImp::loadSettings()
ui->cb_gmsh_binary_std->onRestore();
ui->fc_gmsh_binary_path->onRestore();
ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/Mod/Fem/Gmsh");
// determine number of CPU threads
ui->sb_threads->setValue(hGrp->GetInt("NumOfThreads", QThread::idealThreadCount()));
populateLogVerbosity();
ui->cb_log_verbosity->onRestore();
}

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Gui::Dialog::DlgSettingsNetgen</class>
<widget class="QWidget" name="Gui::Dialog::DlgSettingsNetgen">
<class>FemGui::DlgSettingsNetgen</class>
<widget class="QWidget" name="FemGui::DlgSettingsNetgen">
<property name="geometry">
<rect>
<x>0</x>
@@ -45,6 +45,71 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="gb_options">
<property name="title">
<string>Options</string>
</property>
<layout class="QHBoxLayout" name="hbl_otions">
<item>
<layout class="QGridLayout" name="gl_options">
<item row="0" column="0">
<widget class="QLabel" name="lbl_log_level">
<property name="text">
<string>Log verbosity</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="Gui::PrefComboBox" name="cb_log_verbosity">
<property name="toolTip">
<string>Level of verbosity printed on the task panel</string>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToContents</enum>
</property>
<property name="prefEntry" stdset="0">
<cstring>LogVerbosity</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Fem/Netgen</cstring>
</property>
<property name="prefType" stdset="0">
<number></number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="lbl_threads">
<property name="text">
<string>Number of threads</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="Gui::PrefSpinBox" name="sb_threads">
<property name="alignment">
<set>Qt::AlignLeft|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="toolTip">
<string>Number of threads used for meshing</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="prefEntry" stdset="0">
<cstring>NumOfThreads</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Fem/Netgen</cstring>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_1">
<property name="orientation">
@@ -60,6 +125,11 @@
<extends>QCheckBox</extends>
<header>Gui/PrefWidgets.h</header>
</customwidget>
<customwidget>
<class>Gui::PrefSpinBox</class>
<extends>QSpinBox</extends>
<header>Gui/PrefWidgets.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>

View File

@@ -71,8 +71,9 @@ class FemWorkbench(Workbench):
import Fem
import FemGui
import femcommands.commands
import fempreferencepages
FreeCADGui.addPreferencePage(":/ui/DlgSettingsNetgen.ui", "FEM")
FreeCADGui.addPreferencePage(fempreferencepages.DlgSettingsNetgen, "FEM")
# dummy usage to get flake8 and lgtm quiet
False if Fem.__name__ else True

View File

@@ -30,7 +30,7 @@ __url__ = "https://www.freecad.org"
import os
import re
import subprocess
from PySide.QtCore import QProcess
from PySide.QtCore import QProcess, QThread
import FreeCAD
from FreeCAD import Console
@@ -737,11 +737,12 @@ class GmshTools:
geo.write("// geo file for meshing with Gmsh meshing software created by FreeCAD\n")
geo.write("\n")
cpu_count = os.cpu_count()
if cpu_count is not None and cpu_count > 1:
geo.write("// enable multi-core processing\n")
geo.write(f"General.NumThreads = {cpu_count};\n")
geo.write("\n")
cpu_count = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Fem/Gmsh").GetInt(
"NumOfThreads", QThread.idealThreadCount()
)
geo.write("// enable multi-core processing\n")
geo.write(f"General.NumThreads = {cpu_count};\n")
geo.write("\n")
geo.write("// open brep geometry\n")
# explicit use double quotes in geo file

View File

@@ -29,10 +29,11 @@ import numpy as np
import shutil
import sys
import tempfile
from PySide.QtCore import QProcess
from PySide.QtCore import QProcess, QThread
import FreeCAD
import Fem
from freecad import utils
try:
from netgen import occ, meshing, config as ng_config
@@ -83,6 +84,7 @@ class NetgenTools:
self.tmpdir = ""
self.process = QProcess()
self.mesh_params = {}
self.param_grp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Fem/Netgen")
def write_geom(self):
if not self.tmpdir:
@@ -95,136 +97,152 @@ class NetgenTools:
geom_trans.Placement = global_pla
self.brep_file = self.tmpdir + "/shape.brep"
self.result_file = self.tmpdir + "/result.npy"
self.script_file = self.tmpdir + "/code.py"
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,
"threads": self.param_grp.GetInt("NumOfThreads", QThread.idealThreadCount()),
"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(),
"verbosity": self.param_grp.GetInt("LogVerbosity", 2),
}
with open(self.script_file, "w") as file:
file.write(
self.code.format(
kwds=self.mesh_params,
order_face=NetgenTools.order_face,
order_volume=NetgenTools.order_volume,
)
)
def compute(self):
code_str = self.code.format(params=self.mesh_params)
self.process.start(sys.executable, ["-c", code_str])
self.process.start(utils.get_python_exe(), [self.script_file])
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)
code = """
from netgen import occ, meshing
import pyngcore as ngcore
import numpy as np
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
order_face = {order_face}
order_volume = {order_volume}
with ngcore.TaskManager():
geom = occ.OCCGeometry(shape)
if heal:
geom.Heal()
mesh = geom.GenerateMesh(mp=meshing.MeshingParameters(**params))
def run_netgen(
brep_file,
threads,
heal,
params,
second_order,
second_order_linear,
result_file,
mesh_region,
verbosity,
):
geom = occ.OCCGeometry(brep_file)
ngcore.SetNumThreads(threads)
result = {
"coords": [],
"Edges": [[], []],
"Faces": [[], []],
"Volumes": [[], []],
}
groups = {"Edges": [], "Faces": [], "Solids": []}
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
# save empty data if last step is geometry analysis
if params["perfstepsend"] == NetgenTools.meshing_step["AnalyzeGeometry"]:
np.save(result_file, [result, groups])
return None
with ngcore.TaskManager():
meshing.SetMessageImportance(verbosity)
geom = occ.OCCGeometry(shape)
if heal:
geom.Heal()
mesh = geom.GenerateMesh(mp=meshing.MeshingParameters(**params))
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])
result = {{
"coords": [],
"Edges": [[], []],
"Faces": [[], []],
"Volumes": [[], []],
}}
groups = {{"Edges": [], "Faces": [], "Solids": []}}
# save empty data if last step is geometry analysis
if params["perfstepsend"] == 1:
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][order_face[np_faces[i]]]
for i in range(volumes.size):
nod_volumes[i] = nod_volumes[i][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])
run_netgen(**{kwds})
"""
def fem_mesh_from_result(self):
fem_mesh = Fem.FemMesh()
@@ -301,7 +319,7 @@ NetgenTools.run_netgen(**{params})
"inverttrigs": self.obj.InvertTrigs,
"autozrefine": self.obj.AutoZRefine,
"parallel_meshing": self.obj.ParallelMeshing,
"nthreads": self.obj.Threads,
"nthreads": self.param_grp.GetInt("NumOfThreads", QThread.idealThreadCount()),
"closeedgefac": self.obj.CloseEdgeFactor,
}

View File

@@ -485,15 +485,6 @@ class MeshNetgen(base_fempythonobject.BaseFemPythonObject):
value=True,
)
)
prop.append(
_PropHelper(
type="App::PropertyInteger",
name="Threads",
group="Mesh Parameters",
doc="Number of threads for parallel meshing",
value=4,
)
)
prop.append(
_PropHelper(
type="App::PropertyBool",

View File

@@ -0,0 +1 @@
from .dlg_settings_netgen import DlgSettingsNetgen

View File

@@ -0,0 +1,65 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * Copyright (c) 2024 Mario Passaglia <mpassaglia[at]cbc.uba.ar> *
# * *
# * 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 *
# * <https://www.gnu.org/licenses/>. *
# * *
# ***************************************************************************
__title__ = "Netgen preference page class"
__author__ = "Mario Passaglia"
__url__ = "https://www.freecad.org"
from PySide.QtCore import QThread
import FreeCAD
import FreeCADGui
class DlgSettingsNetgen:
def __init__(self):
self.form = FreeCADGui.PySideUic.loadUi(":ui/DlgSettingsNetgen.ui")
self.grp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Fem/Netgen")
def loadSettings(self):
self.form.ckb_legacy.setChecked(self.grp.GetBool("UseLegacyNetgen", True))
self.form.sb_threads.setValue(self.grp.GetInt("NumOfThreads", QThread.idealThreadCount()))
self.populate_log_verbosity()
def saveSettings(self):
self.grp.SetBool("UseLegacyNetgen", self.form.ckb_legacy.isChecked())
self.grp.SetInt("LogVerbosity", self.form.cb_log_verbosity.currentData())
self.grp.SetInt("NumOfThreads", self.form.sb_threads.value())
def populate_log_verbosity(self):
values = {
"None": 0,
"Least": 1,
"Little": 2,
"Moderate": 3,
"Much": 4,
"Most": 5,
}
for v in values:
self.form.cb_log_verbosity.addItem(v, values[v])
current = self.grp.GetInt("LogVerbosity", 2)
index = self.form.cb_log_verbosity.findData(current)
self.form.cb_log_verbosity.setCurrentIndex(index)

View File

@@ -381,11 +381,8 @@ class _TaskPanel:
# Set up for multi-threading. Note: same functionality as ccx_tools.py/start_ccx()
ccx_prefs = FreeCAD.ParamGet(self.PREFS_PATH)
env = QtCore.QProcessEnvironment.systemEnvironment()
num_cpu_pref = ccx_prefs.GetInt("AnalysisNumCPUs", 0)
if num_cpu_pref >= 1:
env.insert("OMP_NUM_THREADS", str(num_cpu_pref))
else:
env.insert("OMP_NUM_THREADS", str(QtCore.QThread.idealThreadCount()))
num_cpu_pref = ccx_prefs.GetInt("AnalysisNumCPUs", QtCore.QThread.idealThreadCount())
env.insert("OMP_NUM_THREADS", str(num_cpu_pref))
self.Calculix.setProcessEnvironment(env)
self.cwd = QtCore.QDir.currentPath()