diff --git a/src/Mod/Fem/App/FemMesh.cpp b/src/Mod/Fem/App/FemMesh.cpp index 6dd70524cd..1995291c55 100644 --- a/src/Mod/Fem/App/FemMesh.cpp +++ b/src/Mod/Fem/App/FemMesh.cpp @@ -1879,7 +1879,7 @@ void FemMesh::read(const char *FileName) readNastran(File.filePath()); } #ifdef FC_USE_VTK - else if (File.hasExtension("vtk") || File.hasExtension("vtu")) { + else if (File.hasExtension("vtk") || File.hasExtension("vtu") || File.hasExtension("pvtu")) { // read *.vtk legacy format or *.vtu XML unstructure Mesh FemVTKTools::readVTKMesh(File.filePath().c_str(), this); } diff --git a/src/Mod/Fem/App/FemPostPipeline.cpp b/src/Mod/Fem/App/FemPostPipeline.cpp index c50deffed9..5fc5755c5e 100644 --- a/src/Mod/Fem/App/FemPostPipeline.cpp +++ b/src/Mod/Fem/App/FemPostPipeline.cpp @@ -33,6 +33,7 @@ # include # include # include +# include # include # include # include @@ -121,7 +122,8 @@ bool FemPostPipeline::canRead(Base::FileInfo File) { File.hasExtension("vts") || File.hasExtension("vtr") || File.hasExtension("vti") || - File.hasExtension("vtu")) + File.hasExtension("vtu") || + File.hasExtension("pvtu")) return true; return false; @@ -135,6 +137,8 @@ void FemPostPipeline::read(Base::FileInfo File) { if (File.hasExtension("vtu")) readXMLFile(File.filePath()); + else if (File.hasExtension("pvtu")) + readXMLFile(File.filePath()); else if (File.hasExtension("vtp")) readXMLFile(File.filePath()); else if (File.hasExtension("vts")) diff --git a/src/Mod/Fem/App/FemVTKTools.cpp b/src/Mod/Fem/App/FemVTKTools.cpp index 8d4630bde1..abde902d02 100644 --- a/src/Mod/Fem/App/FemVTKTools.cpp +++ b/src/Mod/Fem/App/FemVTKTools.cpp @@ -53,6 +53,7 @@ # include # include # include +# include # include # include # include @@ -215,6 +216,14 @@ FemMesh* FemVTKTools::readVTKMesh(const char* filename, FemMesh* mesh) } importVTKMesh(dataset, mesh); } + else if (f.hasExtension("pvtu")) { + vtkSmartPointer dataset = readVTKFile(filename); + if (!dataset.Get()) { + Base::Console().Error("Failed to load file %s\n", filename); + return nullptr; + } + importVTKMesh(dataset, mesh); + } else if(f.hasExtension("vtk")) { vtkSmartPointer dataset = readVTKFile(filename); diff --git a/src/Mod/Fem/App/PreCompiled.h b/src/Mod/Fem/App/PreCompiled.h index 6c97c9c1f8..8cd5f7a0b9 100644 --- a/src/Mod/Fem/App/PreCompiled.h +++ b/src/Mod/Fem/App/PreCompiled.h @@ -202,6 +202,7 @@ #include #include #include +#include #include #include diff --git a/src/Mod/Fem/Gui/DlgSettingsFemElmer.ui b/src/Mod/Fem/Gui/DlgSettingsFemElmer.ui index 2325fabbd1..de8da1844e 100644 --- a/src/Mod/Fem/Gui/DlgSettingsFemElmer.ui +++ b/src/Mod/Fem/Gui/DlgSettingsFemElmer.ui @@ -7,205 +7,247 @@ 0 0 400 - 199 + 203 Elmer - - 6 - - - - - - - 0 - 0 - - - - Qt::LeftToRight - - - Elmer binaries - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - - QLayout::SetNoConstraint + + + + 0 + 0 + + + + Qt::LeftToRight + + + Elmer binaries + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + ElmerGrid: + + + + + + Search in known binary directories + + + true + + + UseStandardGridLocation + + + Mod/Fem/Elmer + + + + + + + false + + + + 100 + 0 + + + + ElmerGrid binary path + + + + + + + false + + + + 0 + 0 + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 0 + 0 + + + + Leave blank to use default ElmerGrid binary file + + + gridBinaryPath + + + Mod/Fem/Elmer + + + + + + + ElmerSolver: + + + + + + + Search in known binary directories + + + true + + + UseStandardElmerLocation + + + Mod/Fem/Elmer + + + + + + + false + + + + 100 + 0 + + + + ElmerSolver binary path + + + + + + + false + + + + 0 + 0 + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 0 + 0 + + + + <html><head/><body><p>Leave blank to use default Elmer elmer binary file</p><p><span style=" font-weight:600;">Note:</span> To use multithreading you must specify here<br> the executable variant with the suffix &quot;_mpi&quot;.</p></body></html> + + + elmerBinaryPath + + + Mod/Fem/Elmer + + + + + + + Multithreading: + + + + + - - - - - ElmerGrid: - - - - - - - Search in known binary directories - - - true - - - UseStandardGridLocation - - - Mod/Fem/Elmer - - - - - - - false - - - - 100 - 0 - - - - ElmerGrid binary path - - - - - - - false - - - - 0 - 0 - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 0 - 0 - - - - Leave blank to use default ElmerGrid binary file - - - gridBinaryPath - - - Mod/Fem/Elmer - - - - - - - ElmerSolver: - - - - - - - Search in known binary directories - - - true - - - UseStandardElmerLocation - - - Mod/Fem/Elmer - - - - - - - false - - - - 100 - 0 - - - - ElmerSolver binary path - - - - - - - false - - - - 0 - 0 - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 0 - 0 - - - - Leave blank to use default Elmer elmer binary file - - - elmerBinaryPath - - - Mod/Fem/Elmer - - - - + + + true + + + CPU cores to be used: + + + + + + + true + + + <html><head/><body><p><span style=" font-weight:600;">Note:</span> It is recommended to use an even number of cores to benefit from mesh symmetries. (Using 8 cores can be faster than 9 cores.)<br/><span style=" font-weight:600;">Note too:</span> In extreme cases ElmerSolver might not converge if the core number is too high.</p></body></html> + + + 1 + + + 32 + + + UseNumberOfCores + + + Mod/Fem/Elmer + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + - - - + + + @@ -218,7 +260,7 @@ 20 - 40 + 20 @@ -237,6 +279,11 @@ Gui::FileChooser
Gui/PrefWidgets.h
+ + Gui::PrefSpinBox + QSpinBox +
Gui/PrefWidgets.h
+
Gui::PrefCheckBox QCheckBox @@ -279,22 +326,6 @@ - - cb_elmer_binary_std - toggled(bool) - l_elmer_binary_path - setDisabled(bool) - - - 247 - 91 - - - 71 - 114 - - - cb_elmer_binary_std toggled(bool) @@ -311,5 +342,21 @@ + + cb_elmer_binary_std + toggled(bool) + l_elmer_binary_path + setDisabled(bool) + + + 247 + 91 + + + 71 + 114 + + + diff --git a/src/Mod/Fem/Gui/DlgSettingsFemElmerImp.cpp b/src/Mod/Fem/Gui/DlgSettingsFemElmerImp.cpp index cdb4005d61..cc6a40dc59 100644 --- a/src/Mod/Fem/Gui/DlgSettingsFemElmerImp.cpp +++ b/src/Mod/Fem/Gui/DlgSettingsFemElmerImp.cpp @@ -24,6 +24,7 @@ #include "PreCompiled.h" #ifndef _PreComp_ +# include # include #endif @@ -39,10 +40,20 @@ DlgSettingsFemElmerImp::DlgSettingsFemElmerImp(QWidget* parent) { ui->setupUi(this); + // determine number of CPU cores + processor_count = std::thread::hardware_concurrency(); + // hardware check might fail and then returns 0 + if (processor_count > 0) + ui->sb_elmer_num_cores->setMaximum(processor_count); + connect(ui->fc_grid_binary_path, &Gui::PrefFileChooser::fileNameChanged, + this, &DlgSettingsFemElmerImp::onfileNameChanged); + connect(ui->fc_elmer_binary_path, &Gui::PrefFileChooser::fileNameChanged, this, &DlgSettingsFemElmerImp::onfileNameChanged); connect(ui->fc_elmer_binary_path, &Gui::PrefFileChooser::fileNameChanged, - this, &DlgSettingsFemElmerImp::onfileNameChanged); + this, &DlgSettingsFemElmerImp::onfileNameChangedMT); + connect(ui->sb_elmer_num_cores, qOverload(&Gui::PrefSpinBox::valueChanged), + this, &DlgSettingsFemElmerImp::onCoresValueChanged); } DlgSettingsFemElmerImp::~DlgSettingsFemElmerImp() @@ -57,6 +68,8 @@ void DlgSettingsFemElmerImp::saveSettings() ui->cb_grid_binary_std->onSave(); ui->fc_grid_binary_path->onSave(); + + ui->sb_elmer_num_cores->onSave(); } void DlgSettingsFemElmerImp::loadSettings() @@ -66,6 +79,8 @@ void DlgSettingsFemElmerImp::loadSettings() ui->cb_grid_binary_std->onRestore(); ui->fc_grid_binary_path->onRestore(); + + ui->sb_elmer_num_cores->onRestore(); } /** @@ -91,4 +106,44 @@ void DlgSettingsFemElmerImp::onfileNameChanged(QString FileName) } } +void DlgSettingsFemElmerImp::onfileNameChangedMT(QString FileName) +{ + // reset in case it was prevoisly set to 1 + ui->sb_elmer_num_cores->setMaximum(processor_count); + + if (ui->sb_elmer_num_cores->value() == 1) + return; + + auto strName = FileName.toStdString(); +#if defined(FC_OS_WIN32) + // name ends with "_mpi.exe" + if (strName.substr(strName.length() - 8) != "_mpi.exe") { + QMessageBox::warning(this, tr("Not suitable for mulithreading"), + tr("You use more than one CPU core.\n" + "Therefore an executable with the suffix '_mpi.exe' is required.")); + ui->sb_elmer_num_cores->setValue(1); + ui->sb_elmer_num_cores->setMaximum(1); + return; + } +#elif defined(FC_OS_LINUX) || defined(FC_OS_CYGWIN) || defined(FC_OS_MACOSX) || defined(FC_OS_BSD) + // name ends with "_mpi" + if (strName.substr(strName.length() - 4) != "_mpi") { + QMessageBox::warning(this, tr("Not suitable for mulithreading"), + tr("You use more than one CPU core.\n" + "Therefore an executable with the suffix '_mpi' is required.")); + ui->sb_elmer_num_cores->setValue(1); + ui->sb_elmer_num_cores->setMaximum(1); + return; + } +#endif +} + +void DlgSettingsFemElmerImp::onCoresValueChanged(int cores) +{ + if (cores > 1) { + // check if the right executable is loaded + onfileNameChangedMT(ui->fc_elmer_binary_path->fileName()); + } +} + #include "moc_DlgSettingsFemElmerImp.cpp" diff --git a/src/Mod/Fem/Gui/DlgSettingsFemElmerImp.h b/src/Mod/Fem/Gui/DlgSettingsFemElmerImp.h index 88b93b6a82..68ec6a043a 100644 --- a/src/Mod/Fem/Gui/DlgSettingsFemElmerImp.h +++ b/src/Mod/Fem/Gui/DlgSettingsFemElmerImp.h @@ -41,6 +41,8 @@ public: protected Q_SLOTS: void onfileNameChanged(QString FileName); + void onfileNameChangedMT(QString FileName); + void onCoresValueChanged(int cores); protected: void saveSettings(); @@ -49,6 +51,7 @@ protected: private: std::unique_ptr ui; + unsigned int processor_count; }; } // namespace FemGui diff --git a/src/Mod/Fem/Init.py b/src/Mod/Fem/Init.py index d1b5fbe46f..7c755e823f 100644 --- a/src/Mod/Fem/Init.py +++ b/src/Mod/Fem/Init.py @@ -63,7 +63,7 @@ FreeCAD.addExportType("FEM mesh Python (*.meshpy)", "feminout.importPyMesh") FreeCAD.addExportType("FEM mesh TetGen (*.poly)", "feminout.convert2TetGen") # see FemMesh::read() and FemMesh::write() methods in src/Mod/Fem/App/FemMesh.cpp -FreeCAD.addImportType("FEM mesh formats (*.bdf *.dat *.inp *.med *.unv *.vtk *.vtu *.z88)", "Fem") +FreeCAD.addImportType("FEM mesh formats (*.bdf *.dat *.inp *.med *.unv *.vtk *.vtu *.pvtu *.z88)", "Fem") FreeCAD.addExportType("FEM mesh formats (*.dat *.inp *.med *.stl *.unv *.vtk *.vtu *.z88)", "Fem") FreeCAD.addExportType("FEM mesh Nastran (*.bdf)", "feminout.exportNastranMesh") @@ -86,5 +86,5 @@ FreeCAD.addExportType("FEM mesh Z88 (*i1.txt)", "feminout.importZ88Mesh") FreeCAD.addImportType("FEM result Z88 displacements (*o2.txt)", "feminout.importZ88O2Results") if "BUILD_FEM_VTK" in FreeCAD.__cmake__: - FreeCAD.addImportType("FEM result VTK (*.vtk *.vtu)", "feminout.importVTKResults") + FreeCAD.addImportType("FEM result VTK (*.vtk *.vtu *.pvtu)", "feminout.importVTKResults") FreeCAD.addExportType("FEM result VTK (*.vtk *.vtu)", "feminout.importVTKResults") diff --git a/src/Mod/Fem/femsolver/elmer/tasks.py b/src/Mod/Fem/femsolver/elmer/tasks.py index ab3a1f0c3d..788224b900 100644 --- a/src/Mod/Fem/femsolver/elmer/tasks.py +++ b/src/Mod/Fem/femsolver/elmer/tasks.py @@ -121,10 +121,19 @@ class Solve(run.Solve): if os.path.isdir(solvpath): os.environ["ELMER_HOME"] = solvpath os.environ["LD_LIBRARY_PATH"] = "$LD_LIBRARY_PATH:{}/modules".format(solvpath) - # hide the popups on Windows + # different call depending if with multithreading or not + num_cores = settings.get_cores("ElmerSolver") + args = [] + if int(num_cores) > 1: + if system() != "Windows": + args.extend(["mpirun"]) + else: + args.extend(["mpiexec"]) + args.extend(["-np", num_cores]) + args.extend([binary]) if system() == "Windows": self._process = subprocess.Popen( - [binary], + args, cwd=self.directory, stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -132,7 +141,7 @@ class Solve(run.Solve): ) else: self._process = subprocess.Popen( - [binary], + args, cwd=self.directory, stdout=subprocess.PIPE, stderr=subprocess.PIPE @@ -190,12 +199,16 @@ class Results(run.Results): # elmer post file path changed with version x.x # see https://forum.freecadweb.org/viewtopic.php?f=18&t=42732 # workaround - possible_post_file_0 = os.path.join(self.directory, "case0001.vtu") - possible_post_file_t = os.path.join(self.directory, "case_t0001.vtu") - if os.path.isfile(possible_post_file_0): - postPath = possible_post_file_0 - elif os.path.isfile(possible_post_file_t): - postPath = possible_post_file_t + possible_post_file_old = os.path.join(self.directory, "case0001.vtu") + possible_post_file_single = os.path.join(self.directory, "case_t0001.vtu") + possible_post_file_multi = os.path.join(self.directory, "case_t0001.pvtu") + # first try the multi-thread result, then single then old name + if os.path.isfile(possible_post_file_multi): + postPath = possible_post_file_multi + elif os.path.isfile(possible_post_file_single): + postPath = possible_post_file_single + elif os.path.isfile(possible_post_file_old): + postPath = possible_post_file_old else: self.report.error("Result file not found.") self.fail() diff --git a/src/Mod/Fem/femsolver/elmer/writer.py b/src/Mod/Fem/femsolver/elmer/writer.py index 429f3ea58d..3bb37d0a81 100644 --- a/src/Mod/Fem/femsolver/elmer/writer.py +++ b/src/Mod/Fem/femsolver/elmer/writer.py @@ -216,23 +216,38 @@ class Writer(object): ) else: binary = settings.get_binary("ElmerGrid") + num_cores = settings.get_cores("ElmerGrid") if binary is None: raise WriteError("Could not find ElmerGrid binary.") - args = [binary, - _ELMERGRID_IFORMAT, - _ELMERGRID_OFORMAT, - unvPath, - "-scale", "0.001", "0.001", "0.001", - "-out", self.directory] - # hide the popups on Windows + # for multithreading we first need a normal mesh creation run + # then a second to split the mesh into the number of used cores + argsBasic = [binary, + _ELMERGRID_IFORMAT, + _ELMERGRID_OFORMAT, + unvPath, + "-scale", "0.001", "0.001", "0.001"] + args = argsBasic + args.extend(["-out", self.directory]) if system() == "Windows": subprocess.call( args, - stdout=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, startupinfo=femutils.startProgramInfo("hide") ) else: subprocess.call(args, stdout=subprocess.DEVNULL) + if int(num_cores) > 1: + args = argsBasic + args.extend(["-partdual", "-metiskway", num_cores, + "-out", self.directory]) + if system() == "Windows": + subprocess.call( + args, + stdout=subprocess.DEVNULL, + startupinfo=femutils.startProgramInfo("hide") + ) + else: + subprocess.call(args, stdout=subprocess.DEVNULL) def _writeStartinfo(self): path = os.path.join(self.directory, _STARTINFO_NAME) diff --git a/src/Mod/Fem/femsolver/settings.py b/src/Mod/Fem/femsolver/settings.py index 36e43ec149..52b96fb4ac 100644 --- a/src/Mod/Fem/femsolver/settings.py +++ b/src/Mod/Fem/femsolver/settings.py @@ -94,10 +94,7 @@ def get_binary(name): """ if name in _SOLVER_PARAM: binary = _SOLVER_PARAM[name].get_binary() - FreeCAD.Console.PrintMessage( - 'Solver binary path (returned from binary getter): {} \n' - .format(binary) - ) + FreeCAD.Console.PrintMessage('Solver binary path: {} \n'.format(binary)) return binary else: FreeCAD.Console.PrintError( @@ -107,6 +104,28 @@ def get_binary(name): ) return None +def get_cores(name): + """ Read number of CPU cores for solver *name* honoring user settings. + + Returns number of CPU cores to be used for the solvier run + + :param name: solver id as a ``str`` (see :mod:`femsolver.settings`) + """ + if name in _SOLVER_PARAM: + cores = _SOLVER_PARAM[name].get_cores() + FreeCAD.Console.PrintMessage( + 'Number of CPU cores to be used for the solvier run: {} \n' + .format(cores) + ) + return cores + else: + FreeCAD.Console.PrintError( + "Settings solver name: {} not found in " + "solver settings modules _SOLVER_PARAM dirctionary.\n" + .format(name) + ) + return None + def get_write_comments(name): """ Check whether "write_comments" is set for solver. @@ -220,6 +239,10 @@ class _SolverDlg(object): FreeCAD.Console.PrintLog("Solver binary found path: {}\n".format(the_found_binary)) return the_found_binary + def get_cores(self): + cores = str(self.param_group.GetInt("UseNumberOfCores")) + return cores + def get_write_comments(self): return self.param_group.GetBool(self.WRITE_COMMENTS_PARAM, True)