From d3203c8cdd33142e347aa04de3b65c68609b3cbf Mon Sep 17 00:00:00 2001 From: rois1995 Date: Mon, 21 Sep 2026 12:03:15 +0200 Subject: [PATCH 1/7] Add the Paraview parallel output format (.pvtu with one piece per rank) The PARAVIEW writer produces a single .vtu with one piece, which ParaView loads on one process: a large case does not fit in the memory of one process, and reading, filtering and rendering are serial. Users work around this by converting the output to .pvtu by hand. Add PARAVIEW_PARALLEL and SURFACE_PARAVIEW_PARALLEL, which write a .pvtu file listing one .vtu piece per rank, in a folder next to it. Every rank writes its own piece with no collective call, so the writing is also the fastest of the formats. A piece must be self-contained, so each rank collects the points used by its elements and asks the ranks that own them for their data (one all-to-all exchange). The connectivity is loaded without sorting, i.e. each rank writes the elements of its own partition, which are next to each other, so only the points on the partition boundary are duplicated: on a 1.9M element mesh the pieces hold 3% more points than the mesh on 4 ranks and 8% more on 16 ranks (the sorted partition would give +122% and +225%). The precision follows VOLUME_OUTPUT_PRECISION, and the connectivity uses Int64 when a piece needs it. Co-Authored-By: Claude Opus 5 (1M context) --- Common/include/option_structure.hpp | 6 + .../filewriter/CParaviewPVTUFileWriter.hpp | 103 +++++ SU2_CFD/src/meson.build | 1 + SU2_CFD/src/output/COutput.cpp | 41 ++ .../filewriter/CParaviewPVTUFileWriter.cpp | 410 ++++++++++++++++++ 5 files changed, 561 insertions(+) create mode 100644 SU2_CFD/include/output/filewriter/CParaviewPVTUFileWriter.hpp create mode 100644 SU2_CFD/src/output/filewriter/CParaviewPVTUFileWriter.cpp diff --git a/Common/include/option_structure.hpp b/Common/include/option_structure.hpp index 351e534671ae..8fd3ccbbff2d 100644 --- a/Common/include/option_structure.hpp +++ b/Common/include/option_structure.hpp @@ -2355,6 +2355,8 @@ enum class OUTPUT_TYPE { PARAVIEW_XML, /*!< \brief Paraview XML with binary data format */ SURFACE_PARAVIEW_XML, /*!< \brief Surface Paraview XML with binary data format */ PARAVIEW_MULTIBLOCK, /*!< \brief Paraview XML Multiblock */ + PARAVIEW_PARALLEL, /*!< \brief Paraview parallel XML (.pvtu), one piece per rank. */ + SURFACE_PARAVIEW_PARALLEL, /*!< \brief Surface Paraview parallel XML (.pvtu), one piece per rank. */ CGNS, /*!< \brief CGNS format. */ SURFACE_CGNS, /*!< \brief CGNS format. */ STL_ASCII, /*!< \brief STL ASCII format for surface solution output. */ @@ -2374,6 +2376,8 @@ static const MapType Output_Map = { MakePair("PARAVIEW", OUTPUT_TYPE::PARAVIEW_XML) MakePair("SURFACE_PARAVIEW", OUTPUT_TYPE::SURFACE_PARAVIEW_XML) MakePair("PARAVIEW_MULTIBLOCK", OUTPUT_TYPE::PARAVIEW_MULTIBLOCK) + MakePair("PARAVIEW_PARALLEL", OUTPUT_TYPE::PARAVIEW_PARALLEL) + MakePair("SURFACE_PARAVIEW_PARALLEL", OUTPUT_TYPE::SURFACE_PARAVIEW_PARALLEL) MakePair("MESH", OUTPUT_TYPE::MESH) MakePair("MESH_BINARY", OUTPUT_TYPE::MESH_BINARY) MakePair("RESTART_ASCII", OUTPUT_TYPE::RESTART_ASCII) @@ -2396,6 +2400,8 @@ inline bool isParaview(OUTPUT_TYPE format) { case OUTPUT_TYPE::PARAVIEW_XML: case OUTPUT_TYPE::SURFACE_PARAVIEW_XML: case OUTPUT_TYPE::PARAVIEW_MULTIBLOCK: + case OUTPUT_TYPE::PARAVIEW_PARALLEL: + case OUTPUT_TYPE::SURFACE_PARAVIEW_PARALLEL: return true; default: return false; diff --git a/SU2_CFD/include/output/filewriter/CParaviewPVTUFileWriter.hpp b/SU2_CFD/include/output/filewriter/CParaviewPVTUFileWriter.hpp new file mode 100644 index 000000000000..cac1e26c0b1f --- /dev/null +++ b/SU2_CFD/include/output/filewriter/CParaviewPVTUFileWriter.hpp @@ -0,0 +1,103 @@ +/*! + * \file CParaviewPVTUFileWriter.hpp + * \brief Headers fo the Paraview parallel XML file writer (.pvtu and one .vtu piece per rank). + * \author T. Albring + * \version 8.5.0 "Harrier" + * + * SU2 Project Website: https://su2code.github.io + * + * The SU2 Project is maintained by the SU2 Foundation + * (http://su2foundation.org) + * + * Copyright 2012-2026, SU2 Contributors (cf. AUTHORS.md) + * + * SU2 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. + * + * SU2 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 SU2. If not, see . + */ + +#pragma once + +#include + +#include "CFileWriter.hpp" + +/*! + * \class CParaviewPVTUFileWriter + * \brief Writes the data as a Paraview parallel unstructured grid: a .pvtu file that lists one .vtu + * piece per rank. Each rank writes its own piece, so no rank gathers data of other ranks and + * ParaView can read, filter and render the pieces in parallel (unlike a single .vtu file, + * which one process has to load as a whole). + */ +class CParaviewPVTUFileWriter final : public CFileWriter { + private: + /*! + * \brief Boolean storing whether we are on a big or little endian machine. + */ + bool bigEndian; + + /*! + * \brief True to write the coordinates and fields in double precision instead of single. + */ + bool doublePrecision; + + /*! + * \brief Points of the piece of this rank: those used by its elements, wherever they belong. + */ + vector pieceData; /*!< \brief Fields of the points of the piece, point by point. */ + vector usedPoints; /*!< \brief Global indices of the points of the piece, sorted. */ + + /*! + * \brief Collect the points used by the elements of this rank and get their data from the ranks that own + * them, so that the piece of this rank is self-contained. + * \returns The number of points of the piece. + */ + unsigned long CollectExternalPoints(); + + /*! + * \brief Write the piece of this rank. + * \param[in] val_filename - Name of the piece file. + * \param[in] nPointsPiece - Number of points of the piece. + */ + void WritePiece(const string& val_filename, unsigned long nPointsPiece); + + /*! + * \brief Write the .pvtu file that lists the pieces, only the master node writes it. + * \param[in] val_filename - Name of the .pvtu file. + * \param[in] pieceNames - Names of the pieces, relative to the .pvtu file. + */ + void WriteIndexFile(const string& val_filename, const vector& pieceNames); + + public: + /*! + * \brief File extension + */ + const static string fileExt; + + /*! + * \brief Construct a file writer using field names and the data sorter. + * \param[in] valDataSorter - The parallel sorted data to write. + * \param[in] valDoublePrecision - Write the coordinates and fields in double precision. + */ + CParaviewPVTUFileWriter(CParallelDataSorter* valDataSorter, bool valDoublePrecision = false); + + /*! + * \brief Destructor + */ + ~CParaviewPVTUFileWriter() override; + + /*! + * \brief Write sorted data to the .pvtu file and its pieces. + * \param[in] val_filename - The name of the file. + */ + void WriteData(string val_filename) override; +}; diff --git a/SU2_CFD/src/meson.build b/SU2_CFD/src/meson.build index 8798e29f1869..195217ed084e 100644 --- a/SU2_CFD/src/meson.build +++ b/SU2_CFD/src/meson.build @@ -50,6 +50,7 @@ su2_cfd_src += files(['output/COutputFactory.cpp', 'output/filewriter/CSU2BinaryFileWriter.cpp', 'output/filewriter/CParaviewXMLFileWriter.cpp', 'output/filewriter/CParaviewVTMFileWriter.cpp', + 'output/filewriter/CParaviewPVTUFileWriter.cpp', 'output/filewriter/CSU2MeshFileWriter.cpp', 'output/filewriter/CSU2MeshBinaryFileWriter.cpp', 'output/filewriter/CCGNSFileWriter.cpp', diff --git a/SU2_CFD/src/output/COutput.cpp b/SU2_CFD/src/output/COutput.cpp index c9ca947546cb..d458d6718559 100644 --- a/SU2_CFD/src/output/COutput.cpp +++ b/SU2_CFD/src/output/COutput.cpp @@ -42,6 +42,7 @@ #include "../../include/output/filewriter/CSTLFileWriter.hpp" #include "../../include/output/filewriter/CParaviewBinaryFileWriter.hpp" #include "../../include/output/filewriter/CParaviewXMLFileWriter.hpp" +#include "../../include/output/filewriter/CParaviewPVTUFileWriter.hpp" #include "../../include/output/filewriter/CParaviewVTMFileWriter.hpp" #include "../../include/output/filewriter/CTecplotFileWriter.hpp" #include "../../include/output/filewriter/CTecplotBinaryFileWriter.hpp" @@ -560,6 +561,46 @@ void COutput::WriteToFile(CConfig *config, CGeometry *geometry, OUTPUT_TYPE form break; + case OUTPUT_TYPE::PARAVIEW_PARALLEL: + + extension = CParaviewPVTUFileWriter::fileExt; + + if (fileName.empty()) + fileName = config->GetFilename(volumeFilename, "", curTimeIter); + + if (!config->GetWrt_Volume_Overwrite()) + filename_iter = config->GetFilename_Iter(fileName, curInnerIter, curOuterIter); + + /*--- Load the connectivity without sorting it: each rank then writes the elements of its own + partition, which are next to each other, so its piece needs few points of other ranks. ---*/ + + volumeDataSorter->SortConnectivity(config, geometry, false); + + LogOutputFiles("Paraview parallel"); + fileWriter = new CParaviewPVTUFileWriter(volumeDataSorter, config->GetVolume_Output_Double_Precision()); + + break; + + case OUTPUT_TYPE::SURFACE_PARAVIEW_PARALLEL: + + extension = CParaviewPVTUFileWriter::fileExt; + + if (fileName.empty()) + fileName = config->GetFilename(surfaceFilename, "", curTimeIter); + + if (!config->GetWrt_Surface_Overwrite()) + filename_iter = config->GetFilename_Iter(fileName, curInnerIter, curOuterIter); + + /*--- Load and sort the output data and connectivity. ---*/ + + surfaceDataSorter->SortConnectivity(config, geometry); + surfaceDataSorter->SortOutputData(); + + LogOutputFiles("Paraview parallel surface"); + fileWriter = new CParaviewPVTUFileWriter(surfaceDataSorter, config->GetVolume_Output_Double_Precision()); + + break; + case OUTPUT_TYPE::PARAVIEW_LEGACY_BINARY: extension = CParaviewBinaryFileWriter::fileExt; diff --git a/SU2_CFD/src/output/filewriter/CParaviewPVTUFileWriter.cpp b/SU2_CFD/src/output/filewriter/CParaviewPVTUFileWriter.cpp new file mode 100644 index 000000000000..447216911af0 --- /dev/null +++ b/SU2_CFD/src/output/filewriter/CParaviewPVTUFileWriter.cpp @@ -0,0 +1,410 @@ +/*! + * \file CParaviewPVTUFileWriter.cpp + * \brief Filewriter class for the Paraview parallel XML format (.pvtu and one .vtu piece per rank). + * \author T. Albring + * \version 8.5.0 "Harrier" + * + * SU2 Project Website: https://su2code.github.io + * + * The SU2 Project is maintained by the SU2 Foundation + * (http://su2foundation.org) + * + * Copyright 2012-2026, SU2 Contributors (cf. AUTHORS.md) + * + * SU2 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. + * + * SU2 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 SU2. If not, see . + */ + +#include "../../../include/output/filewriter/CParaviewPVTUFileWriter.hpp" + +#include +#include +#include + +#ifdef _MSC_VER +#include +#else +#include +#include +#endif + +namespace { + +constexpr unsigned short NCOORDS = 3; + +/*--- The element types in the order in which they are written. ---*/ +const std::array ElemTypes = {LINE, TRIANGLE, QUADRILATERAL, TETRAHEDRON, + HEXAHEDRON, PRISM, PYRAMID}; + +/*--- Append the bytes of a value to a buffer. ---*/ +template +void AppendBytes(string& buffer, const T& value) { + buffer.append(reinterpret_cast(&value), sizeof(value)); +} + +/*--- Append a data array to a buffer in the "appended raw" format: the size in bytes of the array + followed by its values, converted to the type the file declares. ---*/ +template +void AppendDataArray(string& buffer, const vector& values) { + AppendBytes(buffer, static_cast(values.size() * sizeof(T))); + for (const auto& value : values) AppendBytes(buffer, static_cast(value)); +} + +/*--- Size in bytes of a data array in the file, including the size written in front of it. ---*/ +size_t DataArraySize(size_t nValues, size_t typeSize) { return sizeof(uint64_t) + nValues * typeSize; } + +/*--- Name of a field without the component suffix of a vector, e.g. "Momentum_x" -> "Momentum". ---*/ +string FieldBaseName(string name) { + name.erase(remove(name.begin(), name.end(), '"'), name.end()); + return name.substr(0, name.size() - 2); +} + +} // namespace + +const string CParaviewPVTUFileWriter::fileExt = ".pvtu"; + +CParaviewPVTUFileWriter::CParaviewPVTUFileWriter(CParallelDataSorter* valDataSorter, bool valDoublePrecision) + : CFileWriter(valDataSorter, fileExt), doublePrecision(valDoublePrecision) { + /* Check for big endian. We have to swap bytes otherwise. */ + + bigEndian = false; + unsigned int i = 1; + char* c = (char*)&i; + bigEndian = *c == 0; +} + +CParaviewPVTUFileWriter::~CParaviewPVTUFileWriter() = default; + +void CParaviewPVTUFileWriter::WriteData(string val_filename) { + if (!dataSorter->GetConnectivitySorted()) { + SU2_MPI::Error("Connectivity must be sorted.", CURRENT_FUNCTION); + } + + if (bigEndian) { + SU2_MPI::Error("The Paraview parallel writer is only implemented for little endian machines.", CURRENT_FUNCTION); + } + + startTime = SU2_MPI::Wtime(); + + /*--- The pieces are written into a folder next to the .pvtu file, one file per rank. ---*/ + + const string folderName = val_filename; + + if (rank == MASTER_NODE) { +#ifdef _MSC_VER + _mkdir(folderName.c_str()); +#else + mkdir(folderName.c_str(), 0777); +#endif + } + SU2_MPI::Barrier(SU2_MPI::GetComm()); + + const auto lastSlash = val_filename.find_last_of("/\\"); + const string baseName = (lastSlash == string::npos) ? val_filename : val_filename.substr(lastSlash + 1); + + vector pieceNames(size); + for (int i = 0; i < size; i++) pieceNames[i] = baseName + "/" + baseName + "_" + to_string(i) + ".vtu"; + + /*--- Make the data of this rank self-contained and write its piece. ---*/ + + const auto nPointsPiece = CollectExternalPoints(); + + WritePiece(folderName + "/" + baseName + "_" + to_string(rank) + ".vtu", nPointsPiece); + + /*--- The master node writes the file that lists the pieces. ---*/ + + WriteIndexFile(val_filename + fileExt, pieceNames); + + stopTime = SU2_MPI::Wtime(); + usedTime = stopTime - startTime; + + su2double myFileSize = fileSize; + SU2_MPI::Allreduce(&myFileSize, &fileSize, 1, MPI_DOUBLE, MPI_SUM, SU2_MPI::GetComm()); + + bandwidth = fileSize / (1.0e6) / usedTime; +} + +unsigned long CParaviewPVTUFileWriter::CollectExternalPoints() { + const auto nFields = dataSorter->GetFieldNames().size(); + const auto firstPoint = dataSorter->GetnPointCumulative(rank); + + /*--- The piece holds exactly the points used by the elements of this rank, wherever they live. ---*/ + + usedPoints.clear(); + + for (auto type : ElemTypes) { + const auto nNodes = nPointsOfElementType(type); + for (unsigned long iElem = 0; iElem < dataSorter->GetnElem(type); iElem++) + for (unsigned short iNode = 0; iNode < nNodes; iNode++) + usedPoints.push_back(dataSorter->GetElemConnectivity(type, iElem, iNode) - 1); + } + + sort(usedPoints.begin(), usedPoints.end()); + usedPoints.erase(unique(usedPoints.begin(), usedPoints.end()), usedPoints.end()); + + /*--- Ask the rank that owns each point for its data. The points are sorted by their global index, and + the ranks own ranges of increasing global indices, so they are already grouped by rank. ---*/ + + vector nRequest(size, 0), nAnswer(size, 0), requestOffset(size + 1, 0), answerOffset(size + 1, 0); + + for (auto iPoint : usedPoints) nRequest[dataSorter->FindProcessor(iPoint)]++; + + SU2_MPI::Alltoall(nRequest.data(), 1, MPI_INT, nAnswer.data(), 1, MPI_INT, SU2_MPI::GetComm()); + + for (int i = 0; i < size; i++) { + requestOffset[i + 1] = requestOffset[i] + nRequest[i]; + answerOffset[i + 1] = answerOffset[i] + nAnswer[i]; + } + + vector answeredPoints(answerOffset[size]); + + SU2_MPI::Alltoallv(usedPoints.data(), nRequest.data(), requestOffset.data(), MPI_UNSIGNED_LONG, + answeredPoints.data(), nAnswer.data(), answerOffset.data(), MPI_UNSIGNED_LONG, + SU2_MPI::GetComm()); + + /*--- Answer with the data of the points this rank owns. ---*/ + + vector answerData(answeredPoints.size() * nFields); + + for (size_t i = 0; i < answeredPoints.size(); i++) { + const auto iPoint = answeredPoints[i] - firstPoint; + for (size_t iField = 0; iField < nFields; iField++) + answerData[i * nFields + iField] = dataSorter->GetData(iField, iPoint); + } + + vector nRequestData(size), nAnswerData(size), requestDataOffset(size + 1), answerDataOffset(size + 1); + for (int i = 0; i <= size; i++) { + if (i < size) { + nRequestData[i] = nRequest[i] * nFields; + nAnswerData[i] = nAnswer[i] * nFields; + } + requestDataOffset[i] = requestOffset[i] * nFields; + answerDataOffset[i] = answerOffset[i] * nFields; + } + + pieceData.resize(usedPoints.size() * nFields); + + SU2_MPI::Alltoallv(answerData.data(), nAnswerData.data(), answerDataOffset.data(), MPI_DOUBLE, pieceData.data(), + nRequestData.data(), requestDataOffset.data(), MPI_DOUBLE, SU2_MPI::GetComm()); + + return usedPoints.size(); +} + +void CParaviewPVTUFileWriter::WritePiece(const string& val_filename, unsigned long nPointsPiece) { + const auto& fieldNames = dataSorter->GetFieldNames(); + const auto nFields = fieldNames.size(); + const auto nDim = dataSorter->GetnDim(); + + /*--- Local index of a point of the piece. ---*/ + + auto localIndex = [&](unsigned long iPoint) { + return static_cast(lower_bound(usedPoints.begin(), usedPoints.end(), iPoint) - usedPoints.begin()); + }; + + /*--- Connectivity and cell types of the piece. ---*/ + + const auto nElem = dataSorter->GetnElem(); + const auto nConn = dataSorter->GetnConn(); + + vector connectivity, offsets; + vector types; + connectivity.reserve(nConn); + offsets.reserve(nElem); + types.reserve(nElem); + + for (auto type : ElemTypes) { + const auto nNodes = nPointsOfElementType(type); + for (unsigned long iElem = 0; iElem < dataSorter->GetnElem(type); iElem++) { + for (unsigned short iNode = 0; iNode < nNodes; iNode++) + connectivity.push_back(localIndex(dataSorter->GetElemConnectivity(type, iElem, iNode) - 1)); + offsets.push_back(connectivity.size()); + types.push_back(static_cast(type)); + } + } + + /*--- Sizes of the arrays in the file, needed for the offsets of the appended data. ---*/ + + const size_t realSize = doublePrecision ? sizeof(double) : sizeof(float); + const string realType = doublePrecision ? "Float64" : "Float32"; + + const bool connInt64 = connectivity.size() > static_cast(std::numeric_limits::max()); + const size_t connSize = connInt64 ? sizeof(int64_t) : sizeof(int32_t); + const string connType = connInt64 ? "Int64" : "Int32"; + + /*--- Header of the piece, the offset of each array in the appended data is written with it. ---*/ + + ostringstream header; + size_t offset = 0; + + header << "\n"; + header << "\n"; + header << "\n"; + + header << "\n\n\n"; + offset += DataArraySize(nPointsPiece * NCOORDS, realSize); + + header << "\n"; + header << "\n"; + offset += DataArraySize(connectivity.size(), connSize); + header << "\n"; + offset += DataArraySize(offsets.size(), connSize); + header << "\n"; + offset += DataArraySize(types.size(), sizeof(uint8_t)); + header << "\n"; + + /*--- The fields start after the coordinates, a field whose name ends in _x is written as a vector + together with the fields that follow it. ---*/ + + unsigned short varStart = (nDim == 3) ? 3 : 2; + + header << "\n"; + for (unsigned short iField = varStart; iField < nFields; iField++) { + if (fieldNames[iField].find("_y") != string::npos || fieldNames[iField].find("_z") != string::npos) continue; + + const bool isVector = fieldNames[iField].find("_x") != string::npos; + string name = fieldNames[iField]; + if (isVector) { + name = FieldBaseName(name); + } else { + name.erase(remove(name.begin(), name.end(), '"'), name.end()); + } + + header << "\n"; + offset += DataArraySize(nPointsPiece * (isVector ? NCOORDS : 1), realSize); + } + header << "\n\n\n\n_"; + + /*--- Data of the piece. ---*/ + + string data; + data.reserve(offset); + + vector buffer(nPointsPiece * NCOORDS); + + auto loadField = [&](unsigned short iField, unsigned short nComponents) { + buffer.resize(nPointsPiece * nComponents); + for (unsigned long iPoint = 0; iPoint < nPointsPiece; iPoint++) { + for (unsigned short iComp = 0; iComp < nComponents; iComp++) { + const bool empty = (nDim == 2) && (nComponents == NCOORDS) && (iComp == 2); + buffer[iPoint * nComponents + iComp] = empty ? 0.0 : pieceData[iPoint * nFields + iField + iComp]; + } + } + }; + + auto appendReal = [&](const vector& values) { + if (doublePrecision) + AppendDataArray(data, values); + else + AppendDataArray(data, values); + }; + + auto appendConn = [&](const vector& values) { + if (connInt64) + AppendDataArray(data, values); + else + AppendDataArray(data, values); + }; + + loadField(0, NCOORDS); + appendReal(buffer); + + appendConn(connectivity); + appendConn(offsets); + AppendDataArray(data, types); + + for (unsigned short iField = varStart; iField < nFields; iField++) { + if (fieldNames[iField].find("_y") != string::npos || fieldNames[iField].find("_z") != string::npos) continue; + + const bool isVector = fieldNames[iField].find("_x") != string::npos; + loadField(iField, isVector ? NCOORDS : 1); + appendReal(buffer); + } + + /*--- Each rank writes its own file, no rank waits for another one. ---*/ + + FILE* file = fopen(val_filename.c_str(), "wb"); + if (!file) SU2_MPI::Error(string("Unable to open file ") + val_filename, CURRENT_FUNCTION); + + const string tail = "\n\n\n"; + const auto headerString = header.str(); + + fwrite(headerString.data(), sizeof(char), headerString.size(), file); + fwrite(data.data(), sizeof(char), data.size(), file); + fwrite(tail.data(), sizeof(char), tail.size(), file); + fclose(file); + + fileSize = headerString.size() + data.size() + tail.size(); +} + +void CParaviewPVTUFileWriter::WriteIndexFile(const string& val_filename, const vector& pieceNames) { + /*--- All pieces must declare the same types, so the connectivity type is that of the largest piece. ---*/ + + unsigned long localConn = dataSorter->GetnConn(), maxConn = 0; + SU2_MPI::Allreduce(&localConn, &maxConn, 1, MPI_UNSIGNED_LONG, MPI_MAX, SU2_MPI::GetComm()); + + if (rank != MASTER_NODE) return; + + const auto& fieldNames = dataSorter->GetFieldNames(); + const auto nDim = dataSorter->GetnDim(); + + const string realType = doublePrecision ? "Float64" : "Float32"; + const string connType = maxConn > static_cast(std::numeric_limits::max()) ? "Int64" : "Int32"; + + ostringstream file; + + file << "\n"; + file << "\n"; + file << "\n\n\n"; + file << "\n"; + file << "\n"; + file << "\n"; + file << "\n"; + file << "\n"; + + unsigned short varStart = (nDim == 3) ? 3 : 2; + + file << "\n"; + for (unsigned short iField = varStart; iField < fieldNames.size(); iField++) { + if (fieldNames[iField].find("_y") != string::npos || fieldNames[iField].find("_z") != string::npos) continue; + + const bool isVector = fieldNames[iField].find("_x") != string::npos; + string name = fieldNames[iField]; + if (isVector) { + name = FieldBaseName(name); + } else { + name.erase(remove(name.begin(), name.end(), '"'), name.end()); + } + + file << "\n"; + } + file << "\n"; + + for (const auto& piece : pieceNames) file << "\n"; + + file << "\n\n"; + + const auto fileString = file.str(); + + FILE* indexFile = fopen(val_filename.c_str(), "w"); + if (!indexFile) SU2_MPI::Error(string("Unable to open file ") + val_filename, CURRENT_FUNCTION); + fwrite(fileString.data(), sizeof(char), fileString.size(), indexFile); + fclose(indexFile); + + fileSize += fileString.size(); +} From e84fb9486751d0c022608080bd92f492caedf72b Mon Sep 17 00:00:00 2001 From: rois1995 Date: Wed, 23 Sep 2026 11:50:41 +0200 Subject: [PATCH 2/7] Add PARAVIEW_PARALLEL to the output formats in config_template.cfg Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LEL91DW5WPbPwgFtCvHga6 --- config_template.cfg | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config_template.cfg b/config_template.cfg index 5e611f22ed48..cc55305204e6 100644 --- a/config_template.cfg +++ b/config_template.cfg @@ -2672,7 +2672,9 @@ MULTIZONE_ADAPT_FILENAME= YES % Files to output % Possible formats : (TECPLOT_ASCII, TECPLOT, SURFACE_TECPLOT_ASCII, % SURFACE_TECPLOT, CSV, SURFACE_CSV, PARAVIEW_ASCII, PARAVIEW_LEGACY, SURFACE_PARAVIEW_ASCII, -% SURFACE_PARAVIEW_LEGACY, PARAVIEW, SURFACE_PARAVIEW, RESTART_ASCII, RESTART, CGNS, SURFACE_CGNS, STL_ASCII, STL_BINARY) +% SURFACE_PARAVIEW_LEGACY, PARAVIEW, SURFACE_PARAVIEW, PARAVIEW_PARALLEL, SURFACE_PARAVIEW_PARALLEL, +% RESTART_ASCII, RESTART, CGNS, SURFACE_CGNS, STL_ASCII, STL_BINARY) +% PARAVIEW_PARALLEL writes a .pvtu file and a folder with one .vtu piece per MPI rank. % default : (RESTART, PARAVIEW, SURFACE_PARAVIEW) OUTPUT_FILES= (RESTART, PARAVIEW, SURFACE_PARAVIEW) % From c188b1e4795b6a83d037c0f8d1e3bb314fbff558 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Wed, 14 Oct 2026 07:47:24 +0200 Subject: [PATCH 3/7] Add a regression test for the Paraview parallel writer The flat plate solution is written with PARAVIEW_PARALLEL and SURFACE_PARAVIEW_PARALLEL on 2 ranks, and the .pvtu files are compared with reference files (su2code/TestCases, output_writers/). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L7oxzVir4zENrpy3K3W6Fm --- TestCases/output_writers/pvtu_output.cfg | 38 ++++++++++++++++++++++++ TestCases/parallel_regression.py | 13 ++++++++ 2 files changed, 51 insertions(+) create mode 100644 TestCases/output_writers/pvtu_output.cfg diff --git a/TestCases/output_writers/pvtu_output.cfg b/TestCases/output_writers/pvtu_output.cfg new file mode 100644 index 000000000000..24d977b77a84 --- /dev/null +++ b/TestCases/output_writers/pvtu_output.cfg @@ -0,0 +1,38 @@ +% Test of the Paraview parallel writer (PARAVIEW_PARALLEL and SURFACE_PARAVIEW_PARALLEL). +% The solution of vandv/rans/flatplate is written without iterating (ITER= 0) on 2 ranks. +% Each rank writes its own .vtu piece in the folders flow/ and surface_flow/, and rank 0 writes +% the .pvtu files that list the pieces and the fields. The .pvtu files are compared with +% reference files. The pieces are binary, so they are not compared here. +% +% ------------- DIRECT, ADJOINT, AND LINEARIZED PROBLEM DEFINITION ------------% +SOLVER= RANS +KIND_TURB_MODEL= SST +SST_OPTIONS= V1994m +RESTART_SOL= YES +% ----------- COMPRESSIBLE AND INCOMPRESSIBLE FREE-STREAM DEFINITION ----------% +MACH_NUMBER= 0.2 +AOA= 0.0 +FREESTREAM_TEMPERATURE= 300.0 +REYNOLDS_NUMBER= 5000000.0 +REYNOLDS_LENGTH= 1.0 +FREESTREAM_TURBULENCEINTENSITY= 0.00038729 +FREESTREAM_TURB2LAMVISCRATIO= 0.009 +% -------------------- BOUNDARY CONDITION DEFINITION --------------------------% +MARKER_HEATFLUX= ( wall, 0.0 ) +MARKER_FAR= ( farfield ) +MARKER_INLET= ( inlet, 302.4, 117691.7874, 1.0, 0.0, 0.0 ) +MARKER_OUTLET= ( outlet, 114455.0 ) +MARKER_SYM= ( symmetry ) +MARKER_PLOTTING= ( wall ) +% ------------- COMMON PARAMETERS DEFINING THE NUMERICAL METHOD ---------------% +NUM_METHOD_GRAD= WEIGHTED_LEAST_SQUARES +CONV_NUM_METHOD_FLOW= ROE +CONV_NUM_METHOD_TURB= SCALAR_UPWIND +ITER= 0 +% ------------------------- INPUT/OUTPUT INFORMATION --------------------------% +MESH_FILENAME= ../vandv/rans/flatplate/mesh_flatplate_turb_035x025.su2 +MESH_FORMAT= SU2 +SOLUTION_FILENAME= ../vandv/rans/flatplate/solution_flow +VOLUME_FILENAME= flow +SURFACE_FILENAME= surface_flow +OUTPUT_FILES= (PARAVIEW_PARALLEL, SURFACE_PARAVIEW_PARALLEL) diff --git a/TestCases/parallel_regression.py b/TestCases/parallel_regression.py index 54569d389d50..eaef2166b04c 100755 --- a/TestCases/parallel_regression.py +++ b/TestCases/parallel_regression.py @@ -2091,6 +2091,19 @@ def main(): pass_list.append(ascii_mesh_writer.run_filediff()) test_list.append(ascii_mesh_writer) + # Paraview parallel writer: each rank writes its .vtu piece, compare the .pvtu files that list them. + for tag, test_file in [("pvtu_writer", "flow.pvtu"), ("pvtu_surface_writer", "surface_flow.pvtu")]: + pvtu_writer = TestCase(tag) + pvtu_writer.cfg_dir = "output_writers" + pvtu_writer.cfg_file = "pvtu_output.cfg" + pvtu_writer.test_iter = -1 + pvtu_writer.command = TestCase.Command("mpirun -n 2", "SU2_CFD") + pvtu_writer.timeout = 1600 + pvtu_writer.reference_file = test_file + ".ref" + pvtu_writer.test_file = test_file + pass_list.append(pvtu_writer.run_filediff()) + test_list.append(pvtu_writer) + ###################################### ### RUN SU2_DEF TESTS ### ###################################### From 891813e43f1f2eb90239d93af5bc61bcfa998037 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Wed, 14 Oct 2026 07:47:24 +0200 Subject: [PATCH 4/7] Run the regression tests with the paraviewParallelOutput branch of TestCases Temporary, for the reference files of the TestCases PR. To be reverted to develop before merging. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L7oxzVir4zENrpy3K3W6Fm --- .github/workflows/regression.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/regression.yml b/.github/workflows/regression.yml index 5c2157eb2277..78fdea29cc80 100644 --- a/.github/workflows/regression.yml +++ b/.github/workflows/regression.yml @@ -233,7 +233,7 @@ jobs: uses: docker://ghcr.io/su2code/su2/test-su2:261011-1107 with: # -t -c - args: -b ${{github.ref}} -t develop -c develop -s ${{matrix.testscript}} + args: -b ${{github.ref}} -t develop -c paraviewParallelOutput -s ${{matrix.testscript}} - name: Cleanup uses: docker://ghcr.io/su2code/su2/test-su2:261011-1107 with: @@ -282,7 +282,7 @@ jobs: uses: docker://ghcr.io/su2code/su2/test-su2:261011-1107 with: # -t -c - args: -b ${{github.ref}} -t develop -c develop -s ${{matrix.testscript}} -a "--tapetests" + args: -b ${{github.ref}} -t develop -c paraviewParallelOutput -s ${{matrix.testscript}} -a "--tapetests" - name: Cleanup uses: docker://ghcr.io/su2code/su2/test-su2:261011-1107 with: @@ -330,7 +330,7 @@ jobs: PMIX_MCA_gds: hash with: # -t -c - args: -b ${{github.ref}} -t develop -c develop -s ${{matrix.testscript}} -a "--tsan" + args: -b ${{github.ref}} -t develop -c paraviewParallelOutput -s ${{matrix.testscript}} -a "--tsan" - name: Cleanup uses: docker://ghcr.io/su2code/su2/test-su2-tsan:261011-1107 with: @@ -375,7 +375,7 @@ jobs: uses: docker://ghcr.io/su2code/su2/test-su2-asan:261011-1107 with: # -t -c - args: -b ${{github.ref}} -t develop -c develop -s ${{matrix.testscript}} -a "--asan" + args: -b ${{github.ref}} -t develop -c paraviewParallelOutput -s ${{matrix.testscript}} -a "--asan" - name: Cleanup uses: docker://ghcr.io/su2code/su2/test-su2-asan:261011-1107 with: From 99f5cb5177192df8adf0cfa4532fbf5e38c7a5df Mon Sep 17 00:00:00 2001 From: rois1995 Date: Wed, 14 Oct 2026 08:21:01 +0200 Subject: [PATCH 5/7] Fix the Paraview parallel writer for AD builds, MinGW and large pieces - The point data is passive: exchange it with the passive MPI wrapper, the AD wrapper treated the buffers as su2double and overran them. - Create the folder of the pieces with the one-argument mkdir on MinGW, as CPhysicalGeometry does. - Exchange the data one field at a time, so that the MPI counts are numbers of points and cannot overflow an int with many fields. - Write each array to the file as soon as it is built, instead of building the whole piece in memory first. - Decide Int32 or Int64 for the connectivity once for all pieces, so that the pieces and the .pvtu file declare the same type. - Fix a typo and the author of the new files. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L7oxzVir4zENrpy3K3W6Fm --- .../filewriter/CParaviewPVTUFileWriter.hpp | 12 +- .../filewriter/CParaviewPVTUFileWriter.cpp | 168 ++++++++---------- 2 files changed, 85 insertions(+), 95 deletions(-) diff --git a/SU2_CFD/include/output/filewriter/CParaviewPVTUFileWriter.hpp b/SU2_CFD/include/output/filewriter/CParaviewPVTUFileWriter.hpp index cac1e26c0b1f..c7230eb1384f 100644 --- a/SU2_CFD/include/output/filewriter/CParaviewPVTUFileWriter.hpp +++ b/SU2_CFD/include/output/filewriter/CParaviewPVTUFileWriter.hpp @@ -1,7 +1,7 @@ /*! * \file CParaviewPVTUFileWriter.hpp - * \brief Headers fo the Paraview parallel XML file writer (.pvtu and one .vtu piece per rank). - * \author T. Albring + * \brief Headers of the Paraview parallel XML file writer (.pvtu and one .vtu piece per rank). + * \author A. Rausa * \version 8.5.0 "Harrier" * * SU2 Project Website: https://su2code.github.io @@ -53,7 +53,7 @@ class CParaviewPVTUFileWriter final : public CFileWriter { /*! * \brief Points of the piece of this rank: those used by its elements, wherever they belong. */ - vector pieceData; /*!< \brief Fields of the points of the piece, point by point. */ + vector pieceData; /*!< \brief Fields of the points of the piece, field by field. */ vector usedPoints; /*!< \brief Global indices of the points of the piece, sorted. */ /*! @@ -67,15 +67,17 @@ class CParaviewPVTUFileWriter final : public CFileWriter { * \brief Write the piece of this rank. * \param[in] val_filename - Name of the piece file. * \param[in] nPointsPiece - Number of points of the piece. + * \param[in] connInt64 - Write the connectivity and offsets as Int64 instead of Int32. */ - void WritePiece(const string& val_filename, unsigned long nPointsPiece); + void WritePiece(const string& val_filename, unsigned long nPointsPiece, bool connInt64); /*! * \brief Write the .pvtu file that lists the pieces, only the master node writes it. * \param[in] val_filename - Name of the .pvtu file. * \param[in] pieceNames - Names of the pieces, relative to the .pvtu file. + * \param[in] connInt64 - The connectivity and offsets of the pieces are Int64 instead of Int32. */ - void WriteIndexFile(const string& val_filename, const vector& pieceNames); + void WriteIndexFile(const string& val_filename, const vector& pieceNames, bool connInt64); public: /*! diff --git a/SU2_CFD/src/output/filewriter/CParaviewPVTUFileWriter.cpp b/SU2_CFD/src/output/filewriter/CParaviewPVTUFileWriter.cpp index 447216911af0..961ede7f0429 100644 --- a/SU2_CFD/src/output/filewriter/CParaviewPVTUFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CParaviewPVTUFileWriter.cpp @@ -1,7 +1,7 @@ /*! * \file CParaviewPVTUFileWriter.cpp * \brief Filewriter class for the Paraview parallel XML format (.pvtu and one .vtu piece per rank). - * \author T. Albring + * \author A. Rausa * \version 8.5.0 "Harrier" * * SU2 Project Website: https://su2code.github.io @@ -31,7 +31,7 @@ #include #include -#ifdef _MSC_VER +#if defined(_WIN32) #include #else #include @@ -46,18 +46,30 @@ constexpr unsigned short NCOORDS = 3; const std::array ElemTypes = {LINE, TRIANGLE, QUADRILATERAL, TETRAHEDRON, HEXAHEDRON, PRISM, PYRAMID}; -/*--- Append the bytes of a value to a buffer. ---*/ -template -void AppendBytes(string& buffer, const T& value) { - buffer.append(reinterpret_cast(&value), sizeof(value)); +/*--- Write a data array in the "appended raw" format: the size in bytes of the array followed by its + values, converted to the type the file declares. Returns the number of bytes written. ---*/ +template +size_t WriteDataArray(FILE* file, const vector& values) { + const auto nBytes = static_cast(values.size() * sizeof(T)); + fwrite(&nBytes, sizeof(nBytes), 1, file); + + vector converted(values.begin(), values.end()); + fwrite(converted.data(), sizeof(T), converted.size(), file); + + return sizeof(nBytes) + nBytes; } -/*--- Append a data array to a buffer in the "appended raw" format: the size in bytes of the array - followed by its values, converted to the type the file declares. ---*/ -template -void AppendDataArray(string& buffer, const vector& values) { - AppendBytes(buffer, static_cast(values.size() * sizeof(T))); - for (const auto& value : values) AppendBytes(buffer, static_cast(value)); +/*--- Create a folder, if it does not exist yet. ---*/ +void MakeFolder(const string& name) { +#if defined(_WIN32) +#ifdef __MINGW32__ + mkdir(name.c_str()); +#else + _mkdir(name.c_str()); +#endif +#else + mkdir(name.c_str(), 0777); +#endif } /*--- Size in bytes of a data array in the file, including the size written in front of it. ---*/ @@ -100,13 +112,7 @@ void CParaviewPVTUFileWriter::WriteData(string val_filename) { const string folderName = val_filename; - if (rank == MASTER_NODE) { -#ifdef _MSC_VER - _mkdir(folderName.c_str()); -#else - mkdir(folderName.c_str(), 0777); -#endif - } + if (rank == MASTER_NODE) MakeFolder(folderName); SU2_MPI::Barrier(SU2_MPI::GetComm()); const auto lastSlash = val_filename.find_last_of("/\\"); @@ -119,11 +125,17 @@ void CParaviewPVTUFileWriter::WriteData(string val_filename) { const auto nPointsPiece = CollectExternalPoints(); - WritePiece(folderName + "/" + baseName + "_" + to_string(rank) + ".vtu", nPointsPiece); + /*--- All pieces must declare the same types, so the connectivity type is that of the largest piece. ---*/ + + unsigned long localConn = dataSorter->GetnConn(), maxConn = 0; + SU2_MPI::Allreduce(&localConn, &maxConn, 1, MPI_UNSIGNED_LONG, MPI_MAX, SU2_MPI::GetComm()); + const bool connInt64 = maxConn > static_cast(std::numeric_limits::max()); + + WritePiece(folderName + "/" + baseName + "_" + to_string(rank) + ".vtu", nPointsPiece, connInt64); /*--- The master node writes the file that lists the pieces. ---*/ - WriteIndexFile(val_filename + fileExt, pieceNames); + WriteIndexFile(val_filename + fileExt, pieceNames, connInt64); stopTime = SU2_MPI::Wtime(); usedTime = stopTime - startTime; @@ -141,6 +153,7 @@ unsigned long CParaviewPVTUFileWriter::CollectExternalPoints() { /*--- The piece holds exactly the points used by the elements of this rank, wherever they live. ---*/ usedPoints.clear(); + usedPoints.reserve(dataSorter->GetnConn()); for (auto type : ElemTypes) { const auto nNodes = nPointsOfElementType(type); @@ -151,6 +164,10 @@ unsigned long CParaviewPVTUFileWriter::CollectExternalPoints() { sort(usedPoints.begin(), usedPoints.end()); usedPoints.erase(unique(usedPoints.begin(), usedPoints.end()), usedPoints.end()); + usedPoints.shrink_to_fit(); + + if (usedPoints.size() > static_cast(std::numeric_limits::max())) + SU2_MPI::Error("Too many points in one piece of the Paraview parallel output.", CURRENT_FUNCTION); /*--- Ask the rank that owns each point for its data. The points are sorted by their global index, and the ranks own ranges of increasing global indices, so they are already grouped by rank. ---*/ @@ -172,35 +189,27 @@ unsigned long CParaviewPVTUFileWriter::CollectExternalPoints() { answeredPoints.data(), nAnswer.data(), answerOffset.data(), MPI_UNSIGNED_LONG, SU2_MPI::GetComm()); - /*--- Answer with the data of the points this rank owns. ---*/ - - vector answerData(answeredPoints.size() * nFields); + /*--- Answer with the data of the points this rank owns, one field at a time so that the MPI counts are numbers + of points. The data is passive, so the passive MPI wrapper is used also in AD builds. ---*/ - for (size_t i = 0; i < answeredPoints.size(); i++) { - const auto iPoint = answeredPoints[i] - firstPoint; - for (size_t iField = 0; iField < nFields; iField++) - answerData[i * nFields + iField] = dataSorter->GetData(iField, iPoint); - } + const auto nPointsPiece = usedPoints.size(); + pieceData.resize(nPointsPiece * nFields); - vector nRequestData(size), nAnswerData(size), requestDataOffset(size + 1), answerDataOffset(size + 1); - for (int i = 0; i <= size; i++) { - if (i < size) { - nRequestData[i] = nRequest[i] * nFields; - nAnswerData[i] = nAnswer[i] * nFields; - } - requestDataOffset[i] = requestOffset[i] * nFields; - answerDataOffset[i] = answerOffset[i] * nFields; - } + vector answerData(answeredPoints.size()); - pieceData.resize(usedPoints.size() * nFields); + for (size_t iField = 0; iField < nFields; iField++) { + for (size_t i = 0; i < answeredPoints.size(); i++) + answerData[i] = dataSorter->GetData(iField, answeredPoints[i] - firstPoint); - SU2_MPI::Alltoallv(answerData.data(), nAnswerData.data(), answerDataOffset.data(), MPI_DOUBLE, pieceData.data(), - nRequestData.data(), requestDataOffset.data(), MPI_DOUBLE, SU2_MPI::GetComm()); + SelectMPIWrapper::W::Alltoallv(answerData.data(), nAnswer.data(), answerOffset.data(), MPI_DOUBLE, + &pieceData[iField * nPointsPiece], nRequest.data(), + requestOffset.data(), MPI_DOUBLE, SU2_MPI::GetComm()); + } return usedPoints.size(); } -void CParaviewPVTUFileWriter::WritePiece(const string& val_filename, unsigned long nPointsPiece) { +void CParaviewPVTUFileWriter::WritePiece(const string& val_filename, unsigned long nPointsPiece, bool connInt64) { const auto& fieldNames = dataSorter->GetFieldNames(); const auto nFields = fieldNames.size(); const auto nDim = dataSorter->GetnDim(); @@ -237,7 +246,6 @@ void CParaviewPVTUFileWriter::WritePiece(const string& val_filename, unsigned lo const size_t realSize = doublePrecision ? sizeof(double) : sizeof(float); const string realType = doublePrecision ? "Float64" : "Float32"; - const bool connInt64 = connectivity.size() > static_cast(std::numeric_limits::max()); const size_t connSize = connInt64 ? sizeof(int64_t) : sizeof(int32_t); const string connType = connInt64 ? "Int64" : "Int32"; @@ -289,81 +297,61 @@ void CParaviewPVTUFileWriter::WritePiece(const string& val_filename, unsigned lo } header << "\n\n\n\n_"; - /*--- Data of the piece. ---*/ + /*--- Each rank writes its own file, no rank waits for another one. The arrays are written one at a time. ---*/ - string data; - data.reserve(offset); + FILE* file = fopen(val_filename.c_str(), "wb"); + if (!file) SU2_MPI::Error(string("Unable to open file ") + val_filename, CURRENT_FUNCTION); - vector buffer(nPointsPiece * NCOORDS); + const auto headerString = header.str(); + fwrite(headerString.data(), sizeof(char), headerString.size(), file); + fileSize = headerString.size(); - auto loadField = [&](unsigned short iField, unsigned short nComponents) { - buffer.resize(nPointsPiece * nComponents); - for (unsigned long iPoint = 0; iPoint < nPointsPiece; iPoint++) { - for (unsigned short iComp = 0; iComp < nComponents; iComp++) { - const bool empty = (nDim == 2) && (nComponents == NCOORDS) && (iComp == 2); - buffer[iPoint * nComponents + iComp] = empty ? 0.0 : pieceData[iPoint * nFields + iField + iComp]; - } - } - }; + vector buffer; - auto appendReal = [&](const vector& values) { - if (doublePrecision) - AppendDataArray(data, values); - else - AppendDataArray(data, values); + auto writeField = [&](unsigned short iField, unsigned short nComponents) { + buffer.assign(nPointsPiece * nComponents, 0.0); + for (unsigned short iComp = 0; iComp < nComponents; iComp++) { + if ((nDim == 2) && (nComponents == NCOORDS) && (iComp == 2)) continue; + const auto* field = &pieceData[(iField + iComp) * nPointsPiece]; + for (unsigned long iPoint = 0; iPoint < nPointsPiece; iPoint++) + buffer[iPoint * nComponents + iComp] = field[iPoint]; + } + fileSize += doublePrecision ? WriteDataArray(file, buffer) : WriteDataArray(file, buffer); }; - auto appendConn = [&](const vector& values) { - if (connInt64) - AppendDataArray(data, values); - else - AppendDataArray(data, values); + auto writeConn = [&](const vector& values) { + fileSize += connInt64 ? WriteDataArray(file, values) : WriteDataArray(file, values); }; - loadField(0, NCOORDS); - appendReal(buffer); + writeField(0, NCOORDS); - appendConn(connectivity); - appendConn(offsets); - AppendDataArray(data, types); + writeConn(connectivity); + writeConn(offsets); + fileSize += WriteDataArray(file, types); for (unsigned short iField = varStart; iField < nFields; iField++) { if (fieldNames[iField].find("_y") != string::npos || fieldNames[iField].find("_z") != string::npos) continue; const bool isVector = fieldNames[iField].find("_x") != string::npos; - loadField(iField, isVector ? NCOORDS : 1); - appendReal(buffer); + writeField(iField, isVector ? NCOORDS : 1); } - /*--- Each rank writes its own file, no rank waits for another one. ---*/ - - FILE* file = fopen(val_filename.c_str(), "wb"); - if (!file) SU2_MPI::Error(string("Unable to open file ") + val_filename, CURRENT_FUNCTION); - const string tail = "\n\n\n"; - const auto headerString = header.str(); - - fwrite(headerString.data(), sizeof(char), headerString.size(), file); - fwrite(data.data(), sizeof(char), data.size(), file); fwrite(tail.data(), sizeof(char), tail.size(), file); fclose(file); - fileSize = headerString.size() + data.size() + tail.size(); + fileSize += tail.size(); } -void CParaviewPVTUFileWriter::WriteIndexFile(const string& val_filename, const vector& pieceNames) { - /*--- All pieces must declare the same types, so the connectivity type is that of the largest piece. ---*/ - - unsigned long localConn = dataSorter->GetnConn(), maxConn = 0; - SU2_MPI::Allreduce(&localConn, &maxConn, 1, MPI_UNSIGNED_LONG, MPI_MAX, SU2_MPI::GetComm()); - +void CParaviewPVTUFileWriter::WriteIndexFile(const string& val_filename, const vector& pieceNames, + bool connInt64) { if (rank != MASTER_NODE) return; const auto& fieldNames = dataSorter->GetFieldNames(); const auto nDim = dataSorter->GetnDim(); const string realType = doublePrecision ? "Float64" : "Float32"; - const string connType = maxConn > static_cast(std::numeric_limits::max()) ? "Int64" : "Int32"; + const string connType = connInt64 ? "Int64" : "Int32"; ostringstream file; From 04820d42b846a13e460b8e6b36f8d06b2513ed63 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Wed, 14 Oct 2026 09:40:57 +0200 Subject: [PATCH 6/7] Fix the CodeQL alerts of the Paraview parallel writer Create the files with owner read/write and group/others read permissions instead of fopen (0666 before the umask), and use size_t for the field counters, which are compared with the number of fields. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L7oxzVir4zENrpy3K3W6Fm --- .../filewriter/CParaviewPVTUFileWriter.cpp | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/SU2_CFD/src/output/filewriter/CParaviewPVTUFileWriter.cpp b/SU2_CFD/src/output/filewriter/CParaviewPVTUFileWriter.cpp index 961ede7f0429..aa7818df114f 100644 --- a/SU2_CFD/src/output/filewriter/CParaviewPVTUFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CParaviewPVTUFileWriter.cpp @@ -34,8 +34,10 @@ #if defined(_WIN32) #include #else +#include #include #include +#include #endif namespace { @@ -59,6 +61,20 @@ size_t WriteDataArray(FILE* file, const vector& values) { return sizeof(nBytes) + nBytes; } +/*--- Open a new file for writing. On POSIX systems it is created with the permissions of the other output files + (owner read/write, group and others read), fopen would create it with 0666 before the umask. ---*/ +FILE* OpenFile(const string& name, const char* mode) { +#if defined(_WIN32) + return fopen(name.c_str(), mode); +#else + const int fd = open(name.c_str(), O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); + if (fd < 0) return nullptr; + FILE* file = fdopen(fd, mode); + if (!file) close(fd); + return file; +#endif +} + /*--- Create a folder, if it does not exist yet. ---*/ void MakeFolder(const string& name) { #if defined(_WIN32) @@ -277,10 +293,10 @@ void CParaviewPVTUFileWriter::WritePiece(const string& val_filename, unsigned lo /*--- The fields start after the coordinates, a field whose name ends in _x is written as a vector together with the fields that follow it. ---*/ - unsigned short varStart = (nDim == 3) ? 3 : 2; + const size_t varStart = (nDim == 3) ? 3 : 2; header << "\n"; - for (unsigned short iField = varStart; iField < nFields; iField++) { + for (size_t iField = varStart; iField < nFields; iField++) { if (fieldNames[iField].find("_y") != string::npos || fieldNames[iField].find("_z") != string::npos) continue; const bool isVector = fieldNames[iField].find("_x") != string::npos; @@ -299,7 +315,7 @@ void CParaviewPVTUFileWriter::WritePiece(const string& val_filename, unsigned lo /*--- Each rank writes its own file, no rank waits for another one. The arrays are written one at a time. ---*/ - FILE* file = fopen(val_filename.c_str(), "wb"); + FILE* file = OpenFile(val_filename, "wb"); if (!file) SU2_MPI::Error(string("Unable to open file ") + val_filename, CURRENT_FUNCTION); const auto headerString = header.str(); @@ -308,7 +324,7 @@ void CParaviewPVTUFileWriter::WritePiece(const string& val_filename, unsigned lo vector buffer; - auto writeField = [&](unsigned short iField, unsigned short nComponents) { + auto writeField = [&](size_t iField, unsigned short nComponents) { buffer.assign(nPointsPiece * nComponents, 0.0); for (unsigned short iComp = 0; iComp < nComponents; iComp++) { if ((nDim == 2) && (nComponents == NCOORDS) && (iComp == 2)) continue; @@ -329,7 +345,7 @@ void CParaviewPVTUFileWriter::WritePiece(const string& val_filename, unsigned lo writeConn(offsets); fileSize += WriteDataArray(file, types); - for (unsigned short iField = varStart; iField < nFields; iField++) { + for (size_t iField = varStart; iField < nFields; iField++) { if (fieldNames[iField].find("_y") != string::npos || fieldNames[iField].find("_z") != string::npos) continue; const bool isVector = fieldNames[iField].find("_x") != string::npos; @@ -364,10 +380,10 @@ void CParaviewPVTUFileWriter::WriteIndexFile(const string& val_filename, const v file << "\n"; file << "\n"; - unsigned short varStart = (nDim == 3) ? 3 : 2; + const size_t varStart = (nDim == 3) ? 3 : 2; file << "\n"; - for (unsigned short iField = varStart; iField < fieldNames.size(); iField++) { + for (size_t iField = varStart; iField < fieldNames.size(); iField++) { if (fieldNames[iField].find("_y") != string::npos || fieldNames[iField].find("_z") != string::npos) continue; const bool isVector = fieldNames[iField].find("_x") != string::npos; @@ -389,7 +405,7 @@ void CParaviewPVTUFileWriter::WriteIndexFile(const string& val_filename, const v const auto fileString = file.str(); - FILE* indexFile = fopen(val_filename.c_str(), "w"); + FILE* indexFile = OpenFile(val_filename, "w"); if (!indexFile) SU2_MPI::Error(string("Unable to open file ") + val_filename, CURRENT_FUNCTION); fwrite(fileString.data(), sizeof(char), fileString.size(), indexFile); fclose(indexFile); From c934ca3439957e471b34b5f12fe93db943bf2090 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Wed, 28 Oct 2026 11:00:16 +0100 Subject: [PATCH 7/7] Fix lost surface elements when marker indices restart Reset duplicate-visit flags for each boundary marker in both counting and packing. Indices are local to a marker, so carrying flags across markers silently drops valid surface cells. Add an isolated solver regression covering marker subsets, line, triangle and quad surfaces, and optional MPI execution. --- .../filewriter/CSurfaceFVMDataSorter.cpp | 4 + .../check_surface_marker_connectivity.py | 207 ++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 UnitTests/SU2_CFD/output/check_surface_marker_connectivity.py diff --git a/SU2_CFD/src/output/filewriter/CSurfaceFVMDataSorter.cpp b/SU2_CFD/src/output/filewriter/CSurfaceFVMDataSorter.cpp index 55fb2296ad70..9689bffb2db2 100644 --- a/SU2_CFD/src/output/filewriter/CSurfaceFVMDataSorter.cpp +++ b/SU2_CFD/src/output/filewriter/CSurfaceFVMDataSorter.cpp @@ -1167,6 +1167,8 @@ void CSurfaceFVMDataSorter::SortSurfaceConnectivity(CConfig *config, CGeometry * auto it = std::find(markerList.begin(), markerList.end(), markerTag); if (it != markerList.end()) { + /*--- Element indices restart at zero for each marker. ---*/ + std::fill(nElem_Flag, nElem_Flag + size, -1); for (int ii = 0; ii < (int)geometry->GetnElem_Bound(iMarker); ii++) { @@ -1257,6 +1259,8 @@ void CSurfaceFVMDataSorter::SortSurfaceConnectivity(CConfig *config, CGeometry * auto it = std::find(markerList.begin(), markerList.end(), markerTag); if (it != markerList.end()) { + /*--- Element indices restart at zero for each marker. ---*/ + std::fill(nElem_Flag, nElem_Flag + size, -1); for (int ii = 0; ii < (int)geometry->GetnElem_Bound(iMarker); ii++) { diff --git a/UnitTests/SU2_CFD/output/check_surface_marker_connectivity.py b/UnitTests/SU2_CFD/output/check_surface_marker_connectivity.py new file mode 100644 index 000000000000..e52ad53035c2 --- /dev/null +++ b/UnitTests/SU2_CFD/output/check_surface_marker_connectivity.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +"""Check that surface sorting preserves cells across distinct marker-local indices. + +Run against SU2_CFD, optionally with --launcher mpirun -np 4. Each probe runs in +an isolated temporary directory. Select --case box9 for larger MPI rank counts. +""" +import argparse +from pathlib import Path +import subprocess +import tempfile + + +CUBE_MARKERS = ("x_minus", "x_plus", "y_minus", "y_plus", "z_minus", "z_plus") + + +def canonical(nodes): + return tuple(sorted(tuple(round(v, 12) for v in p) for p in nodes)) + + +def box(n, markers): + values = [i / (n - 1) for i in range(n)] + faces = {} + for axis in range(3): + other = [i for i in range(3) if i != axis] + for side in range(2): + name = CUBE_MARKERS[2 * axis + side] + faces[name] = [] + for i in range(n - 1): + for j in range(n - 1): + nodes = [] + for a, b in ((i, j), (i + 1, j), (i + 1, j + 1), (i, j + 1)): + p = [0.0] * 3 + p[axis] = side + p[other[0]] = values[a] + p[other[1]] = values[b] + nodes.append(p) + faces[name].append(canonical(nodes)) + return set(face for name in markers for face in faces[name]) + + +def mesh_fixture(kind): + if kind == "square": + points = [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)] + dim = 2 + volume = "9 0 1 2 3 0" + boundaries = [(3, 0), (1, 2), (0, 1), (2, 3)] + vtk = 3 + else: + points = [(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)] + dim = 3 + volume = "10 0 1 2 3 0" + boundaries = [(1, 2, 3), (0, 3, 2), (0, 1, 3), (0, 2, 1)] + vtk = 5 + markers = tuple("face" + str(i) for i in range(4)) + lines = ["NDIME= " + str(dim), "NELEM= 1", volume, "NPOIN= 4"] + lines += [" ".join(map(str, p[:dim])) + " " + str(i) for i, p in enumerate(points)] + lines += ["NMARK= 4"] + for name, nodes in zip(markers, boundaries): + lines += [ + "MARKER_TAG= " + name, + "MARKER_ELEMS= 1", + str(vtk) + " " + " ".join(map(str, nodes)), + ] + return ( + "\n".join(lines) + "\n", + markers, + {canonical([points[i] for i in face]) for face in boundaries}, + ) + + +def read_surface(path): + tokens = path.read_text().split() + start = tokens.index("POINTS") + n = int(tokens[start + 1]) + offset = start + 3 + points = [ + tuple(map(float, tokens[offset + 3 * i : offset + 3 * i + 3])) for i in range(n) + ] + start = tokens.index("CELLS") + count = int(tokens[start + 1]) + offset = start + 3 + faces = [] + for _ in range(count): + width = int(tokens[offset]) + offset += 1 + ids = list(map(int, tokens[offset : offset + width])) + offset += width + faces.append(canonical([points[i] for i in ids])) + return faces + + +def run(binary, launcher, name): + markers = CUBE_MARKERS + with tempfile.TemporaryDirectory(prefix="su2-surface-marker-") as temporary: + work = Path(temporary) + if name in ("square", "tetrahedron"): + mesh, markers, expected = mesh_fixture(name) + (work / "mesh.su2").write_text(mesh) + mesh_config = "MESH_FORMAT= SU2\nMESH_FILENAME= mesh.su2\n" + else: + n = { + "single_hex": 2, + "box3": 3, + "box9": 9, + "subset": 2, + "single_marker": 2, + }[name] + if name == "subset": + markers = ("x_minus", "z_plus") + elif name == "single_marker": + markers = ("x_minus",) + expected = box(n, markers) + mesh_config = ( + "MESH_FORMAT= BOX\nMESH_FILENAME= unused.su2\nMESH_BOX_SIZE= (%s,%s,%s)\nMESH_BOX_LENGTH= (1,1,1)\n" + % (n, n, n) + ) + far = ",".join( + CUBE_MARKERS if name not in ("square", "tetrahedron") else markers + ) + cfg = ( + """SOLVER= EULER +MACH_NUMBER= 0.5 +ITER= 0 +MGLEVEL= 0 +CONV_NUM_METHOD_FLOW= ROE +MUSCL_FLOW= NO +TIME_DISCRE_FLOW= RUNGE-KUTTA_EXPLICIT +OUTPUT_FILES= (SURFACE_PARAVIEW_ASCII) +SURFACE_FILENAME= surface +HISTORY_OUTPUT= (ITER) +SCREEN_OUTPUT= (INNER_ITER) +MARKER_FAR= (""" + + far + + """) +MARKER_PLOTTING= (""" + + ",".join(markers) + + ")\n" + + mesh_config + ) + (work / "case.cfg").write_text(cfg) + result = subprocess.run( + launcher + [str(binary), "case.cfg"], + cwd=work, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + timeout=120, + ) + if result.returncode: + raise RuntimeError(name + " solver failed:\n" + result.stdout[-4000:]) + faces = read_surface(work / "surface.vtk") + if ( + len(faces) != len(expected) + or len(set(faces)) != len(faces) + or set(faces) != expected + ): + raise AssertionError( + "%s: wrote %s cells, expected %s; missing %s, unexpected %s" + % ( + name, + len(faces), + len(expected), + len(expected - set(faces)), + len(set(faces) - expected), + ) + ) + print( + "PASS: " + + name + + " preserves all " + + str(len(expected)) + + " distinct surface cells", + flush=True, + ) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("binary", type=Path) + parser.add_argument( + "--case", + action="append", + choices=( + "single_hex", + "box3", + "box9", + "subset", + "single_marker", + "square", + "tetrahedron", + ), + ) + parser.add_argument("--launcher", nargs=argparse.REMAINDER, default=[]) + args = parser.parse_args() + for name in args.case or ( + "single_hex", + "box3", + "subset", + "single_marker", + "square", + "tetrahedron", + ): + run(args.binary.resolve(), args.launcher, name) + + +if __name__ == "__main__": + main()