From 2d96e810527e7725a2dd6acadbbc65fb5b1f2650 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Fri, 18 Sep 2026 11:47:26 +0200 Subject: [PATCH 01/35] Fix int overflow of MPI counts in CGNS output gather The CGNS writer gathered field and connectivity data on the master rank with a single MPI_CHAR message per rank, whose byte count was cast to int. With 64-bit cgsize_t, a rank holding more than ~33.5M hexahedra overflowed the count, which crashed the run or silently wrote corrupted connectivity. Send/receive the buffers in chunks of at most 1 GiB, so every MPI count fits in an int. MPI_CHAR is kept since typed transfers are not portable across AD builds and platforms where long is 32-bit. Also accumulate the element counts in CParallelDataSorter with an unsigned long initial value, to avoid int overflow of the sum. Co-Authored-By: Claude Opus 5 (1M context) --- .../output/filewriter/CCGNSFileWriter.hpp | 21 ++++++++++++ .../src/output/filewriter/CCGNSFileWriter.cpp | 34 +++++++++++++------ .../output/filewriter/CParallelDataSorter.cpp | 4 +-- 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp b/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp index 29dede06f626..811ce3d3f515 100644 --- a/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp +++ b/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp @@ -63,6 +63,9 @@ class CCGNSFileWriter final : public CFileWriter { vector sendBufferField; /*!< \brief Receive buffer for field data. */ cgsize_t cumulative; /*!< \brief Cumulative number of elements written. */ + + /*--- Max bytes per MPI message, kept below INT_MAX so the int count of MPI never overflows. ---*/ + static constexpr size_t maxChunkBytes = size_t(1) << 30; #endif public: /*! @@ -109,6 +112,24 @@ class CCGNSFileWriter final : public CFileWriter { */ void InitializeFields(); + /*! + * \brief Send a buffer of any size in chunks of at most maxChunkBytes (MPI counts are int). + * \param[in] buf - Data to send. + * \param[in] nBytes - Size of the data in bytes. + * \param[in] dest - Destination rank. + * \param[in] tag - Message tag. + */ + static void SendChunked(const void* buf, size_t nBytes, int dest, int tag); + + /*! + * \brief Receive a buffer sent with SendChunked. + * \param[out] buf - Receive buffer, must hold nBytes. + * \param[in] nBytes - Size of the data in bytes. + * \param[in] source - Source rank. + * \param[in] tag - Message tag. + */ + static void RecvChunked(void* buf, size_t nBytes, int source, int tag); + /*! * \brief Call a generic CGNS function. * \param[in] ier - error value. diff --git a/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp b/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp index 749abc74d201..7dd935c47810 100644 --- a/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp @@ -123,8 +123,7 @@ void CCGNSFileWriter::WriteField(int iField, const string& FieldName) { } if (rank != MASTER_NODE) { - SU2_MPI::Send(sendBufferField.data(), nLocalPoints * sizeof(dataPrecision), MPI_CHAR, MASTER_NODE, 0, - SU2_MPI::GetComm()); + SendChunked(sendBufferField.data(), nLocalPoints * sizeof(dataPrecision), MASTER_NODE, 0); return; } @@ -149,12 +148,11 @@ void CCGNSFileWriter::WriteField(int iField, const string& FieldName) { nodeBegin = static_cast(dataSorter->GetnPointCumulative(i) + 1); nodeEnd = static_cast(dataSorter->GetnPointCumulative(i + 1)); - const auto recvSize = static_cast(nodeEnd - nodeBegin + 1); + const auto recvSize = static_cast(nodeEnd - nodeBegin + 1); recvBufferField.resize(recvSize); - SU2_MPI::Recv(recvBufferField.data(), recvSize * sizeof(dataPrecision), MPI_CHAR, i, 0, SU2_MPI::GetComm(), - MPI_STATUS_IGNORE); - if (recvSize <= 0) continue; + RecvChunked(recvBufferField.data(), recvSize * sizeof(dataPrecision), i, 0); + if (recvSize == 0) continue; if (isCoord) { int CoordinateNumber; CallCGNS(cg_coord_partial_write(cgnsFileID, cgnsBase, cgnsZone, dataType, FieldName.c_str(), &nodeBegin, &nodeEnd, @@ -203,9 +201,8 @@ void CCGNSFileWriter::WriteConnectivity(GEO_TYPE type, const string& SectionName } } - const auto bufferSize = static_cast(nLocalElem * nPointsElem * sizeof(cgsize_t)); if (rank != MASTER_NODE) { - SU2_MPI::Send(sendBufferConnectivity.data(), bufferSize, MPI_CHAR, MASTER_NODE, 1, SU2_MPI::GetComm()); + SendChunked(sendBufferConnectivity.data(), sendBufferConnectivity.size() * sizeof(cgsize_t), MASTER_NODE, 1); return; } @@ -219,11 +216,10 @@ void CCGNSFileWriter::WriteConnectivity(GEO_TYPE type, const string& SectionName /*--- In CGNS numbering starts form 1 and ranges are inclusive ---*/ firstElem = endElem + 1; endElem += static_cast(distElem[i]); - const auto recvSize = static_cast((endElem - firstElem + 1) * nPointsElem); + const auto recvSize = static_cast(endElem - firstElem + 1) * nPointsElem; recvBufferConnectivity.resize(recvSize); - const auto recvByte = static_cast(recvBufferConnectivity.size() * sizeof(cgsize_t)); - SU2_MPI::Recv(recvBufferConnectivity.data(), recvByte, MPI_CHAR, i, 1, SU2_MPI::GetComm(), MPI_STATUS_IGNORE); + RecvChunked(recvBufferConnectivity.data(), recvBufferConnectivity.size() * sizeof(cgsize_t), i, 1); if (!recvBufferConnectivity.empty()) CallCGNS(cg_elements_partial_write(cgnsFileID, cgnsBase, cgnsZone, cgnsSection, firstElem, endElem, @@ -232,6 +228,22 @@ void CCGNSFileWriter::WriteConnectivity(GEO_TYPE type, const string& SectionName cumulative += static_cast(nTotElem); } +void CCGNSFileWriter::SendChunked(const void* buf, size_t nBytes, int dest, int tag) { + const auto* bytes = static_cast(buf); + for (size_t offset = 0; offset < nBytes; offset += maxChunkBytes) { + const auto count = static_cast(std::min(maxChunkBytes, nBytes - offset)); + SU2_MPI::Send(bytes + offset, count, MPI_CHAR, dest, tag, SU2_MPI::GetComm()); + } +} + +void CCGNSFileWriter::RecvChunked(void* buf, size_t nBytes, int source, int tag) { + auto* bytes = static_cast(buf); + for (size_t offset = 0; offset < nBytes; offset += maxChunkBytes) { + const auto count = static_cast(std::min(maxChunkBytes, nBytes - offset)); + SU2_MPI::Recv(bytes + offset, count, MPI_CHAR, source, tag, SU2_MPI::GetComm(), MPI_STATUS_IGNORE); + } +} + void CCGNSFileWriter::InitializeFields() { /*--- Create "Fields" node to store solution. ---*/ if (rank == MASTER_NODE) CallCGNS(cg_sol_write(cgnsFileID, cgnsBase, cgnsZone, "Fields", Vertex, &cgnsFields)); diff --git a/SU2_CFD/src/output/filewriter/CParallelDataSorter.cpp b/SU2_CFD/src/output/filewriter/CParallelDataSorter.cpp index 915e33d5c33f..87c15ef2fd9c 100644 --- a/SU2_CFD/src/output/filewriter/CParallelDataSorter.cpp +++ b/SU2_CFD/src/output/filewriter/CParallelDataSorter.cpp @@ -370,8 +370,8 @@ void CParallelDataSorter::SetTotalElements(){ SU2_MPI::Allreduce(nElemPerType.data(), nElemPerTypeGlobal.data(), N_ELEM_TYPES, MPI_UNSIGNED_LONG, MPI_SUM, SU2_MPI::GetComm()); - nElemGlobal = std::accumulate(nElemPerTypeGlobal.begin(), nElemPerTypeGlobal.end(), 0); - nElem = std::accumulate(nElemPerType.begin(), nElemPerType.end(), 0); + nElemGlobal = std::accumulate(nElemPerTypeGlobal.begin(), nElemPerTypeGlobal.end(), 0ul); + nElem = std::accumulate(nElemPerType.begin(), nElemPerType.end(), 0ul); nConn = 0; nConnGlobal = 0; From 7ac93b9ab2a12db7edae62aa6bfaa883ccd31cd9 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Fri, 18 Sep 2026 16:49:53 +0200 Subject: [PATCH 02/35] Fix int overflow of the per-rank size in collective MPI-IO writes WriteMPIBinaryDataAll cast the local size in bytes to int for MPI_Type_contiguous and MPI_File_write_all. When one rank wrote more than INT_MAX bytes in one call (e.g. a binary restart with 20 fields and more than ~13M points per rank, typical of hybrid MPI+OpenMP runs) the write failed or silently wrote a truncated file. Describe the local data as 1 MiB blocks plus a remainder in a single datatype and write it with count 1, so no int count can overflow and the call stays collective. Output files are unchanged. Affects the SU2 binary restart and the PARAVIEW / PARAVIEW_LEGACY writers. Co-Authored-By: Claude Opus 5 (1M context) --- .../output/filewriter/CParallelFileWriter.cpp | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/SU2_CFD/src/output/filewriter/CParallelFileWriter.cpp b/SU2_CFD/src/output/filewriter/CParallelFileWriter.cpp index bd115607b629..3794d50dd112 100644 --- a/SU2_CFD/src/output/filewriter/CParallelFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CParallelFileWriter.cpp @@ -61,25 +61,33 @@ bool CFileWriter::WriteMPIBinaryDataAll(const void *data, unsigned long sizeInBy startTime = SU2_MPI::Wtime(); - MPI_Datatype filetype; + /*--- Prepare to write the actual data. MPI counts are int, so the local data + (which can exceed INT_MAX bytes) is described as a number of fixed-size blocks + plus a remainder, combined in a single datatype that is written with count 1. ---*/ - /*--- Prepare to write the actual data ---*/ + constexpr unsigned long blockBytes = 1ul << 20; + int blockLengths[2] = {static_cast(sizeInBytes / blockBytes), static_cast(sizeInBytes % blockBytes)}; + MPI_Aint displacements[2] = {0, static_cast(sizeInBytes - sizeInBytes % blockBytes)}; - MPI_Type_contiguous(int(sizeInBytes), MPI_BYTE, &filetype); - MPI_Type_commit(&filetype); + MPI_Datatype blocktype, datatype; + MPI_Type_contiguous(static_cast(blockBytes), MPI_BYTE, &blocktype); + MPI_Datatype types[2] = {blocktype, MPI_BYTE}; + MPI_Type_create_struct(2, blockLengths, displacements, types, &datatype); + MPI_Type_commit(&datatype); /*--- Set the view for the MPI file write, i.e., describe the location in the file that this rank "sees" for writing its - piece of the file. ---*/ + piece of the file. The data is contiguous so a byte view is enough. ---*/ - MPI_File_set_view(fhw, disp + offsetInBytes, MPI_BYTE, filetype, + MPI_File_set_view(fhw, disp + offsetInBytes, MPI_BYTE, MPI_BYTE, (char*)"native", MPI_INFO_NULL); /*--- Collective call for all ranks to write simultaneously. ---*/ - int ierr = MPI_File_write_all(fhw, data, int(sizeInBytes), MPI_BYTE, MPI_STATUS_IGNORE); + int ierr = MPI_File_write_all(fhw, data, 1, datatype, MPI_STATUS_IGNORE); - MPI_Type_free(&filetype); + MPI_Type_free(&datatype); + MPI_Type_free(&blocktype); disp += totalSizeInBytes; fileSize += sizeInBytes; From 704ef0d1b2e648ee24c6853958892abce79c6da1 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Fri, 18 Sep 2026 16:49:53 +0200 Subject: [PATCH 03/35] Use Int64 connectivity in .vtu output for meshes above the Int32 limit The PARAVIEW (.vtu) writer stored connectivity and offsets as Int32, and the cumulative connectivity counter of CParallelDataSorter was an int. Meshes with more than 2^31-1 connectivity entries (e.g. more than ~268M hexahedra or ~358M prisms) got overflowed offsets and unreadable files. - Write connectivity and offsets as Int64 when the global connectivity size does not fit in Int32; smaller meshes keep Int32 (files unchanged). - Store nElemConn_Send/nElemConn_Cum as unsigned long. - Write NumberOfPoints/NumberOfCells without casting to int. Co-Authored-By: Claude Opus 5 (1M context) --- .../output/filewriter/CParallelDataSorter.hpp | 4 +- .../filewriter/CParaviewXMLFileWriter.hpp | 7 +++- .../output/filewriter/CParallelDataSorter.cpp | 10 ++--- .../filewriter/CParaviewXMLFileWriter.cpp | 41 ++++++++++++------- 4 files changed, 40 insertions(+), 22 deletions(-) diff --git a/SU2_CFD/include/output/filewriter/CParallelDataSorter.hpp b/SU2_CFD/include/output/filewriter/CParallelDataSorter.hpp index 9f2b0e52031f..78c716f06b2c 100644 --- a/SU2_CFD/include/output/filewriter/CParallelDataSorter.hpp +++ b/SU2_CFD/include/output/filewriter/CParallelDataSorter.hpp @@ -99,8 +99,8 @@ class CParallelDataSorter{ int *nPoint_Recv; //!< Number of points this processor receives from other processors int *nElem_Send; //!< Number of elements this processor has to send to other processors int *nElem_Cum; //!< Cumulative number of elements - int *nElemConn_Send; //!< Number of element connectivity this processor has to send to other processors - int *nElemConn_Cum; //!< Cumulative number of element connectivity entries + unsigned long *nElemConn_Send; //!< Number of element connectivity this processor has to send to other processors + unsigned long *nElemConn_Cum; //!< Cumulative number of element connectivity entries unsigned long *Index; //!< Index each point has in the send buffer passivedouble *connSend; //!< Send buffer holding the data that will be send to other processors passivedouble *dataBuffer; //!< Buffer holding the sorted, partitioned data as passivedouble types diff --git a/SU2_CFD/include/output/filewriter/CParaviewXMLFileWriter.hpp b/SU2_CFD/include/output/filewriter/CParaviewXMLFileWriter.hpp index 7862c26f89cc..d696f348212a 100644 --- a/SU2_CFD/include/output/filewriter/CParaviewXMLFileWriter.hpp +++ b/SU2_CFD/include/output/filewriter/CParaviewXMLFileWriter.hpp @@ -39,6 +39,7 @@ class CParaviewXMLFileWriter final: public CFileWriter{ enum class VTKDatatype { FLOAT32, INT32, + INT64, UINT8 }; @@ -118,7 +119,11 @@ class CParaviewXMLFileWriter final: public CFileWriter{ break; case VTKDatatype::INT32: typeStr = "\"Int32\""; - typeSize = sizeof(int); + typeSize = sizeof(int32_t); + break; + case VTKDatatype::INT64: + typeStr = "\"Int64\""; + typeSize = sizeof(int64_t); break; case VTKDatatype::UINT8: typeStr = "\"UInt8\""; diff --git a/SU2_CFD/src/output/filewriter/CParallelDataSorter.cpp b/SU2_CFD/src/output/filewriter/CParallelDataSorter.cpp index 87c15ef2fd9c..5eaff369a316 100644 --- a/SU2_CFD/src/output/filewriter/CParallelDataSorter.cpp +++ b/SU2_CFD/src/output/filewriter/CParallelDataSorter.cpp @@ -59,8 +59,8 @@ CParallelDataSorter::CParallelDataSorter(CConfig *config, const vector & nPoint_Recv = new int[size+1](); nElem_Send = new int[size+1](); nElem_Cum = new int[size+1](); - nElemConn_Send = new int[size+1](); - nElemConn_Cum = new int[size+1](); + nElemConn_Send = new unsigned long[size+1](); + nElemConn_Cum = new unsigned long[size+1](); nElemPerType.fill(0); nElemPerTypeGlobal.fill(0); @@ -397,7 +397,7 @@ void CParallelDataSorter::SetTotalElements(){ nElem_Cum[0] = 0; nElemConn_Cum[0] = 0; for (int ii=1; ii <= size; ii++) { nElem_Send[ii] = int(nElem); - nElemConn_Send[ii] = int(nConn); + nElemConn_Send[ii] = nConn; nElem_Cum[ii] = 0; nElemConn_Cum[ii] = 0; } @@ -407,8 +407,8 @@ void CParallelDataSorter::SetTotalElements(){ SU2_MPI::Alltoall(&(nElem_Send[1]), 1, MPI_INT, &(nElem_Cum[1]), 1, MPI_INT, SU2_MPI::GetComm()); - SU2_MPI::Alltoall(&(nElemConn_Send[1]), 1, MPI_INT, - &(nElemConn_Cum[1]), 1, MPI_INT, SU2_MPI::GetComm()); + SU2_MPI::Alltoall(&(nElemConn_Send[1]), 1, MPI_UNSIGNED_LONG, + &(nElemConn_Cum[1]), 1, MPI_UNSIGNED_LONG, SU2_MPI::GetComm()); /*--- Put the counters into cumulative storage format. ---*/ diff --git a/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp b/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp index f8bc5b0448d4..1ab60a49fbcc 100644 --- a/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp @@ -27,6 +27,8 @@ #include "../../../include/output/filewriter/CParaviewXMLFileWriter.hpp" #include "../../../../Common/include/toolboxes/printing_toolbox.hpp" +#include +#include const string CParaviewXMLFileWriter::fileExt = ".vtu"; @@ -64,7 +66,6 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ unsigned long iPoint, iElem; - char str_buf[255]; OpenMPIFile(val_filename); @@ -98,6 +99,12 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ GlobalElem = dataSorter->GetnElemGlobal(); GlobalElemStorage = dataSorter->GetnConnGlobal(); + /*--- The offsets into the connectivity array go up to GlobalElemStorage, use 64-bit + integers for connectivity and offsets only when that does not fit in Int32. ---*/ + + const bool connInt64 = GlobalElemStorage > static_cast(std::numeric_limits::max()); + const auto connType = connInt64 ? VTKDatatype::INT64 : VTKDatatype::INT32; + /* Write the ASCII XML header. Note that we use the appended format for the data, * which means that all data is appended at the end of the file in one binary blob. */ @@ -110,16 +117,14 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ WriteMPIString("\n", MASTER_NODE); - SPRINTF(str_buf, "\n", - SU2_TYPE::Int(GlobalPoint), SU2_TYPE::Int(GlobalElem)); - - WriteMPIString(std::string(str_buf), MASTER_NODE); + WriteMPIString("\n", MASTER_NODE); WriteMPIString("\n", MASTER_NODE); AddDataArray(VTKDatatype::FLOAT32, "", NCOORDS, myPoint*NCOORDS, GlobalPoint*NCOORDS); WriteMPIString("\n", MASTER_NODE); WriteMPIString("\n", MASTER_NODE); - AddDataArray(VTKDatatype::INT32, "connectivity", 1, myElemStorage, GlobalElemStorage); - AddDataArray(VTKDatatype::INT32, "offsets", 1, myElem, GlobalElem); + AddDataArray(connType, "connectivity", 1, myElemStorage, GlobalElemStorage); + AddDataArray(connType, "offsets", 1, myElem, GlobalElem); AddDataArray(VTKDatatype::UINT8, "types", 1, myElem, GlobalElem); WriteMPIString("\n", MASTER_NODE); @@ -205,18 +210,18 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ /*--- Load/write 1D buffers for the connectivity of each element type. ---*/ - vector connBuf(myElemStorage); - vector offsetBuf(myElem); + vector connBuf(myElemStorage); + vector offsetBuf(myElem); unsigned long iStorage = 0, iElemID = 0; unsigned short iNode = 0; auto copyToBuffer = [&](GEO_TYPE type, unsigned long nElem, unsigned short nPoints){ for (iElem = 0; iElem < nElem; iElem++) { for (iNode = 0; iNode < nPoints; iNode++){ - connBuf[iStorage+iNode] = int(dataSorter->GetElemConnectivity(type, iElem, iNode)-1); + connBuf[iStorage+iNode] = static_cast(dataSorter->GetElemConnectivity(type, iElem, iNode)) - 1; } iStorage += nPoints; - offsetBuf[iElemID++] = int(iStorage + dataSorter->GetnElemConnCumulative(rank)); + offsetBuf[iElemID++] = static_cast(iStorage + dataSorter->GetnElemConnCumulative(rank)); } }; @@ -228,9 +233,17 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ copyToBuffer(PRISM, nParallel_Pris, N_POINTS_PRISM); copyToBuffer(PYRAMID, nParallel_Pyra, N_POINTS_PYRAMID); - WriteDataArray(connBuf.data(), VTKDatatype::INT32, myElemStorage, GlobalElemStorage, - dataSorter->GetnElemConnCumulative(rank)); - WriteDataArray(offsetBuf.data(), VTKDatatype::INT32, myElem, GlobalElem, dataSorter->GetnElemCumulative(rank)); + if (connInt64) { + WriteDataArray(connBuf.data(), connType, myElemStorage, GlobalElemStorage, + dataSorter->GetnElemConnCumulative(rank)); + WriteDataArray(offsetBuf.data(), connType, myElem, GlobalElem, dataSorter->GetnElemCumulative(rank)); + } else { + vector connBuf32(connBuf.begin(), connBuf.end()); + vector offsetBuf32(offsetBuf.begin(), offsetBuf.end()); + WriteDataArray(connBuf32.data(), connType, myElemStorage, GlobalElemStorage, + dataSorter->GetnElemConnCumulative(rank)); + WriteDataArray(offsetBuf32.data(), connType, myElem, GlobalElem, dataSorter->GetnElemCumulative(rank)); + } /*--- Load/write the cell type for all elements in the file. ---*/ From 0327e497f6ac16413cd7e847541146f233ed5625 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Fri, 18 Sep 2026 16:49:53 +0200 Subject: [PATCH 04/35] Split CGNS element sections above 2^31-1 connectivity entries The VTK/ParaView CGNS reader stores the connectivity size of each section in a 32-bit int, so it crashes on sections with more than 2^31-1 entries (e.g. more than ~358M prisms), although the file is valid. Write an element type that exceeds the limit as several sections with consecutive element ranges (_1, _2, ...). Blocks received from each rank are split at section boundaries. Smaller meshes still get one section per element type, so their files are unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- .../output/filewriter/CCGNSFileWriter.hpp | 6 +++ .../src/output/filewriter/CCGNSFileWriter.cpp | 49 +++++++++++++------ 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp b/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp index 811ce3d3f515..268e4d58c87e 100644 --- a/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp +++ b/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp @@ -34,6 +34,9 @@ #include "cgnslib.h" #endif +#include +#include + #include "CFileWriter.hpp" class CCGNSFileWriter final : public CFileWriter { @@ -66,6 +69,9 @@ class CCGNSFileWriter final : public CFileWriter { /*--- Max bytes per MPI message, kept below INT_MAX so the int count of MPI never overflows. ---*/ static constexpr size_t maxChunkBytes = size_t(1) << 30; + + /*--- Max connectivity entries per section, so that readers using 32-bit sizes can read it. ---*/ + static constexpr cgsize_t maxSectionEntries = std::numeric_limits::max(); #endif public: /*! diff --git a/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp b/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp index 7dd935c47810..56225d1a2682 100644 --- a/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp @@ -169,16 +169,38 @@ void CCGNSFileWriter::WriteConnectivity(GEO_TYPE type, const string& SectionName const auto nTotElem = dataSorter->GetnElemGlobal(type); if (nTotElem == 0) return; - /*--- Create a new CGNS node to store connectivity. ---*/ + /*--- Create new CGNS nodes to store connectivity. Some readers (e.g. the VTK/ParaView CGNS reader) + store the connectivity size of a section in a 32-bit int, so an element type with more than + maxSectionEntries connectivity entries is split into several sections with consecutive ranges. ---*/ const auto elementType = GetCGNSType(type); + const auto nPointsElem = nPointsOfElementType(type); + const auto nTotElemCG = static_cast(nTotElem); + const auto maxElemSection = static_cast(maxSectionEntries / nPointsElem); + const auto nSections = (nTotElemCG + maxElemSection - 1) / maxElemSection; - cgsize_t firstElem = cumulative + 1; - cgsize_t endElem = cumulative + static_cast(nTotElem); + /*--- First and last element (CGNS numbering starts from 1 and ranges are inclusive) of a section. ---*/ + auto sectionBegin = [&](cgsize_t iSec) { return cumulative + 1 + iSec * maxElemSection; }; + auto sectionEnd = [&](cgsize_t iSec) { return cumulative + std::min(nTotElemCG, (iSec + 1) * maxElemSection); }; - int cgnsSection; - if (rank == MASTER_NODE) - CallCGNS(cg_section_partial_write(cgnsFileID, cgnsBase, cgnsZone, SectionName.c_str(), elementType, firstElem, - endElem, 0, &cgnsSection)); + vector cgnsSections(nSections); + if (rank == MASTER_NODE) { + for (cgsize_t iSec = 0; iSec < nSections; ++iSec) { + const string name = nSections == 1 ? SectionName : SectionName + "_" + std::to_string(iSec + 1); + CallCGNS(cg_section_partial_write(cgnsFileID, cgnsBase, cgnsZone, name.c_str(), elementType, + sectionBegin(iSec), sectionEnd(iSec), 0, &cgnsSections[iSec])); + } + } + + /*--- Write the connectivity of the elements [first, last], which may span more than one section. ---*/ + auto writeBlock = [&](cgsize_t first, cgsize_t last, const cgsize_t* conn) { + for (cgsize_t iSec = 0; iSec < nSections; ++iSec) { + const auto lo = std::max(first, sectionBegin(iSec)); + const auto hi = std::min(last, sectionEnd(iSec)); + if (lo > hi) continue; + CallCGNS(cg_elements_partial_write(cgnsFileID, cgnsBase, cgnsZone, cgnsSections[iSec], lo, hi, + conn + (lo - first) * nPointsElem)); + } + }; /*--- Retrieve element distribution among processes. ---*/ const auto nLocalElem = dataSorter->GetnElem(type); @@ -187,11 +209,10 @@ void CCGNSFileWriter::WriteConnectivity(GEO_TYPE type, const string& SectionName SU2_MPI::Allgather(&nLocalElem, 1, MPI_UNSIGNED_LONG, distElem.data(), 1, MPI_UNSIGNED_LONG, SU2_MPI::GetComm()); - firstElem = cumulative + 1; - endElem = cumulative + static_cast(distElem[rank]); + cgsize_t firstElem = cumulative + 1; + cgsize_t endElem = cumulative + static_cast(distElem[rank]); /*--- Connectivity is stored in send buffer. ---*/ - const auto nPointsElem = nPointsOfElementType(type); sendBufferConnectivity.resize(nLocalElem * nPointsElem); for (unsigned long iElem = 0; iElem < nLocalElem; iElem++) { @@ -207,9 +228,7 @@ void CCGNSFileWriter::WriteConnectivity(GEO_TYPE type, const string& SectionName } /*--- Connectivity vector is written in blocks, one for each process. ---*/ - if (nLocalElem > 0) - CallCGNS(cg_elements_partial_write(cgnsFileID, cgnsBase, cgnsZone, cgnsSection, firstElem, endElem, - sendBufferConnectivity.data())); + if (nLocalElem > 0) writeBlock(firstElem, endElem, sendBufferConnectivity.data()); for (int i = 0; i < size; ++i) { if (i == MASTER_NODE) continue; @@ -221,9 +240,7 @@ void CCGNSFileWriter::WriteConnectivity(GEO_TYPE type, const string& SectionName RecvChunked(recvBufferConnectivity.data(), recvBufferConnectivity.size() * sizeof(cgsize_t), i, 1); - if (!recvBufferConnectivity.empty()) - CallCGNS(cg_elements_partial_write(cgnsFileID, cgnsBase, cgnsZone, cgnsSection, firstElem, endElem, - recvBufferConnectivity.data())); + if (!recvBufferConnectivity.empty()) writeBlock(firstElem, endElem, recvBufferConnectivity.data()); } cumulative += static_cast(nTotElem); } From 9bf36fb1ff0ba888ab7aec25624fe26635908cd3 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Fri, 18 Sep 2026 16:58:55 +0200 Subject: [PATCH 05/35] Use the VTK 5.1 Int64 cell layout in legacy .vtk output for large meshes The PARAVIEW_LEGACY writer stored all cells in one Int32 array (the number of nodes followed by the node ids of each cell) and printed the header counts through int. Meshes with more than 2^31-1 entries in that array (e.g. more than ~238M hexahedra or ~307M prisms) got broken files. When the cell array does not fit in Int32, write the legacy 5.1 layout (readable by VTK >= 9.0 / ParaView >= 5.8) with separate Int64 OFFSETS and CONNECTIVITY arrays. Smaller meshes keep the 3.0 layout, so their files are unchanged. Header counts are no longer cast to int. Co-Authored-By: Claude Opus 5 (1M context) --- .../filewriter/CParaviewBinaryFileWriter.cpp | 113 +++++++++++------- 1 file changed, 73 insertions(+), 40 deletions(-) diff --git a/SU2_CFD/src/output/filewriter/CParaviewBinaryFileWriter.cpp b/SU2_CFD/src/output/filewriter/CParaviewBinaryFileWriter.cpp index bde27d26bc65..e065a421ae24 100644 --- a/SU2_CFD/src/output/filewriter/CParaviewBinaryFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CParaviewBinaryFileWriter.cpp @@ -27,6 +27,8 @@ #include "../../../include/output/filewriter/CParaviewBinaryFileWriter.hpp" #include "../../../../Common/include/toolboxes/SwapBytes.hpp" +#include +#include const string CParaviewBinaryFileWriter::fileExt = ".vtk"; @@ -56,7 +58,7 @@ void CParaviewBinaryFileWriter::WriteData(string val_filename){ unsigned short iDim = 0, nDim = dataSorter->GetnDim(); - unsigned long iPoint, iElem; + unsigned long iPoint; const int MAX_STRING_LENGTH = 255; char str_buf[MAX_STRING_LENGTH]; @@ -65,7 +67,13 @@ void CParaviewBinaryFileWriter::WriteData(string val_filename){ OpenMPIFile(val_filename); - string header = "# vtk DataFile Version 3.0\n" + /*--- The classic (3.0) cell layout stores the cell sizes and node ids of all cells in one Int32 array. When that + array does not fit in Int32, use the 5.1 layout (VTK >= 9.0) with separate Int64 offsets and connectivity. ---*/ + + const unsigned long GlobalCellStorage = dataSorter->GetnConnGlobal() + dataSorter->GetnElemGlobal(); + const bool cellsInt64 = GlobalCellStorage > static_cast(std::numeric_limits::max()); + + string header = string("# vtk DataFile Version ") + (cellsInt64 ? "5.1" : "3.0") + "\n" "vtk output\n" "BINARY\n" "DATASET UNSTRUCTURED_GRID\n"; @@ -82,9 +90,7 @@ void CParaviewBinaryFileWriter::WriteData(string val_filename){ GlobalPoint = dataSorter->GetnPointsGlobal(); myPoint = dataSorter->GetnPoints(); - SPRINTF(str_buf, "POINTS %i float\n", SU2_TYPE::Int(GlobalPoint)); - - WriteMPIString(string(str_buf), MASTER_NODE); + WriteMPIString("POINTS " + std::to_string(GlobalPoint) + " float\n", MASTER_NODE); /*--- Load/write the 1D buffer of point coordinates. Note that we always have 3 coordinate dimensions, even for 2D problems. ---*/ @@ -130,48 +136,76 @@ void CParaviewBinaryFileWriter::WriteData(string val_filename){ GlobalElem = dataSorter->GetnElemGlobal(); GlobalElemStorage = dataSorter->GetnConnGlobal(); - SPRINTF(str_buf, "\nCELLS %i %i\n", SU2_TYPE::Int(GlobalElem), - SU2_TYPE::Int(GlobalElemStorage+GlobalElem)); - WriteMPIString(str_buf, MASTER_NODE); - - /*--- Load/write 1D buffers for the connectivity of each element type. ---*/ - - vector connBuf(myElemStorage + myElem); - unsigned long iStorage = 0; - unsigned short iNode = 0; + /*--- Loop over the local elements of each type, calling f(type, iElem, nPoints). ---*/ - auto copyToBuffer = [&](GEO_TYPE type, unsigned long nElem, unsigned short nPoints){ - for (iElem = 0; iElem < nElem; iElem++) { - connBuf[iStorage+0] = nPoints; - for (iNode = 0; iNode < nPoints; iNode++){ - connBuf[iStorage+iNode+1] = int(dataSorter->GetElemConnectivity(type, iElem, iNode)-1); - } - iStorage += nPoints + 1; + auto forEachElem = [&](auto f) { + for (auto type : {LINE, TRIANGLE, QUADRILATERAL, TETRAHEDRON, HEXAHEDRON, PRISM, PYRAMID}) { + const auto nPoints = nPointsOfElementType(type); + for (unsigned long iElem = 0; iElem < dataSorter->GetnElem(type); iElem++) f(type, iElem, nPoints); } }; + unsigned long iStorage = 0; - copyToBuffer(LINE, nParallel_Line, N_POINTS_LINE); - copyToBuffer(TRIANGLE, nParallel_Tria, N_POINTS_TRIANGLE); - copyToBuffer(QUADRILATERAL, nParallel_Quad, N_POINTS_QUADRILATERAL); - copyToBuffer(TETRAHEDRON, nParallel_Tetr, N_POINTS_TETRAHEDRON); - copyToBuffer(HEXAHEDRON, nParallel_Hexa, N_POINTS_HEXAHEDRON); - copyToBuffer(PRISM, nParallel_Pris, N_POINTS_PRISM); - copyToBuffer(PYRAMID, nParallel_Pyra, N_POINTS_PYRAMID); + if (!cellsInt64) { - if (!bigEndian) SwapBytes((char *)connBuf.data(), sizeof(int), myElemStorage+myElem); + WriteMPIString("\nCELLS " + std::to_string(GlobalElem) + " " + std::to_string(GlobalCellStorage) + "\n", + MASTER_NODE); - /*--- Compute various data sizes --- */ + /*--- Load/write the 1D buffer of the number of nodes followed by the node ids of each cell. ---*/ - sizeInBytesPerPoint = sizeof(int); - sizeInBytesLocal = sizeInBytesPerPoint*(myElemStorage + myElem); - sizeInBytesGlobal = sizeInBytesPerPoint*(GlobalElemStorage + GlobalElem); - offsetInBytes = sizeInBytesPerPoint* - (dataSorter->GetnElemConnCumulative(rank) + dataSorter->GetnElemCumulative(rank)); + vector connBuf(myElemStorage + myElem); + + forEachElem([&](GEO_TYPE type, unsigned long iElem, unsigned short nPoints) { + connBuf[iStorage++] = nPoints; + for (unsigned short iNode = 0; iNode < nPoints; iNode++) + connBuf[iStorage++] = static_cast(dataSorter->GetElemConnectivity(type, iElem, iNode) - 1); + }); + + if (!bigEndian) SwapBytes((char *)connBuf.data(), sizeof(int32_t), myElemStorage+myElem); - WriteMPIBinaryDataAll(connBuf.data(), sizeInBytesLocal, sizeInBytesGlobal, offsetInBytes); + sizeInBytesPerPoint = sizeof(int32_t); + sizeInBytesLocal = sizeInBytesPerPoint*(myElemStorage + myElem); + sizeInBytesGlobal = sizeInBytesPerPoint*GlobalCellStorage; + offsetInBytes = sizeInBytesPerPoint* + (dataSorter->GetnElemConnCumulative(rank) + dataSorter->GetnElemCumulative(rank)); + + WriteMPIBinaryDataAll(connBuf.data(), sizeInBytesLocal, sizeInBytesGlobal, offsetInBytes); + + } else { + + WriteMPIString("\nCELLS " + std::to_string(GlobalElem + 1) + " " + std::to_string(GlobalElemStorage) + "\n", + MASTER_NODE); + + /*--- Load the offsets (where each cell ends in the connectivity) and the connectivity. ---*/ + + vector offsetBuf(myElem), connBuf(myElemStorage); + unsigned long iCell = 0; + + forEachElem([&](GEO_TYPE type, unsigned long iElem, unsigned short nPoints) { + for (unsigned short iNode = 0; iNode < nPoints; iNode++) + connBuf[iStorage++] = static_cast(dataSorter->GetElemConnectivity(type, iElem, iNode)) - 1; + offsetBuf[iCell++] = static_cast(iStorage + dataSorter->GetnElemConnCumulative(rank)); + }); + + if (!bigEndian) { + SwapBytes((char *)offsetBuf.data(), sizeof(int64_t), myElem); + SwapBytes((char *)connBuf.data(), sizeof(int64_t), myElemStorage); + } + + /*--- The offsets start with a 0, written by the master node. ---*/ + + WriteMPIString("OFFSETS vtktypeint64\n", MASTER_NODE); + const int64_t firstOffset = 0; + WriteMPIBinaryData(&firstOffset, sizeof(int64_t), MASTER_NODE); + WriteMPIBinaryDataAll(offsetBuf.data(), sizeof(int64_t)*myElem, sizeof(int64_t)*GlobalElem, + sizeof(int64_t)*dataSorter->GetnElemCumulative(rank)); + + WriteMPIString("\nCONNECTIVITY vtktypeint64\n", MASTER_NODE); + WriteMPIBinaryDataAll(connBuf.data(), sizeof(int64_t)*myElemStorage, sizeof(int64_t)*GlobalElemStorage, + sizeof(int64_t)*dataSorter->GetnElemConnCumulative(rank)); + } - SPRINTF (str_buf, "\nCELL_TYPES %i\n", SU2_TYPE::Int(GlobalElem)); - WriteMPIString(str_buf, MASTER_NODE); + WriteMPIString("\nCELL_TYPES " + std::to_string(GlobalElem) + "\n", MASTER_NODE); /*--- Load/write the cell type for all elements in the file. ---*/ @@ -197,8 +231,7 @@ void CParaviewBinaryFileWriter::WriteData(string val_filename){ WriteMPIBinaryDataAll(typeBuf.data(), sizeInBytesLocal, sizeInBytesGlobal, offsetInBytes); - SPRINTF (str_buf, "\nPOINT_DATA %i\n", SU2_TYPE::Int(GlobalPoint)); - WriteMPIString(str_buf, MASTER_NODE); + WriteMPIString("\nPOINT_DATA " + std::to_string(GlobalPoint) + "\n", MASTER_NODE); /*--- Adjust container start location to avoid point coords. ---*/ From cb8430d09b88f5c742c17d1fd5960e6813482ad1 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Fri, 18 Sep 2026 17:00:10 +0200 Subject: [PATCH 06/35] Fix int overflow when sorting output data with many values per rank SortOutputData and PrepareSendBuffers computed buffer sizes, offsets and MPI counts as (number of output fields) x (number of points) in int. When one rank sent or received more than INT_MAX values (e.g. 100 output fields and more than ~21M points per rank, as in hybrid MPI+OpenMP runs) the counts overflowed and the output was broken. Send the data of each point as one element of a contiguous MPI datatype, so the MPI counts are numbers of points, and use size_t for the buffer sizes and offsets. Output files are unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- .../output/filewriter/CParallelDataSorter.cpp | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/SU2_CFD/src/output/filewriter/CParallelDataSorter.cpp b/SU2_CFD/src/output/filewriter/CParallelDataSorter.cpp index 5eaff369a316..3aaa35ff4635 100644 --- a/SU2_CFD/src/output/filewriter/CParallelDataSorter.cpp +++ b/SU2_CFD/src/output/filewriter/CParallelDataSorter.cpp @@ -95,7 +95,7 @@ CParallelDataSorter::~CParallelDataSorter(){ void CParallelDataSorter::SortOutputData() { - const int VARS_PER_POINT = GlobalField_Counter; + const size_t VARS_PER_POINT = GlobalField_Counter; /*--- Allocate the memory that we need for receiving the conn values and then cue up the non-blocking receives. Note that @@ -109,6 +109,13 @@ void CParallelDataSorter::SortOutputData() { * because it communicates passivedoubles and not AD types. This avoids some * creative C++ to communicate AD types and then convert to passive. ---*/ + /*--- The data of each point is sent as one element of a contiguous datatype, so that the MPI counts are numbers + of points and do not overflow when a rank sends or receives more than INT_MAX values. ---*/ + + MPI_Datatype pointType; + MPI_Type_contiguous(GlobalField_Counter, MPI_DOUBLE, &pointType); + MPI_Type_commit(&pointType); + /*--- We need double the number of messages to send both the conn. and the global IDs. ---*/ auto send_req = new MPI_Request[2*nSends]; @@ -117,12 +124,11 @@ void CParallelDataSorter::SortOutputData() { unsigned long iMessage = 0; for (int ii=0; ii nPoint_Recv[ii])) { - int ll = VARS_PER_POINT*nPoint_Recv[ii]; - int kk = nPoint_Recv[ii+1] - nPoint_Recv[ii]; - int count = VARS_PER_POINT*kk; + size_t ll = VARS_PER_POINT*nPoint_Recv[ii]; + int count = nPoint_Recv[ii+1] - nPoint_Recv[ii]; int source = ii; int tag = ii + 1; - MPI_Irecv(&(dataBuffer[ll]), count, MPI_DOUBLE, source, tag, + MPI_Irecv(&(dataBuffer[ll]), count, pointType, source, tag, SU2_MPI::GetComm(), &(recv_req[iMessage])); iMessage++; } @@ -133,12 +139,11 @@ void CParallelDataSorter::SortOutputData() { iMessage = 0; for (int ii=0; ii nPoint_Send[ii])) { - int ll = VARS_PER_POINT*nPoint_Send[ii]; - int kk = nPoint_Send[ii+1] - nPoint_Send[ii]; - int count = VARS_PER_POINT*kk; + size_t ll = VARS_PER_POINT*nPoint_Send[ii]; + int count = nPoint_Send[ii+1] - nPoint_Send[ii]; int dest = ii; int tag = rank + 1; - MPI_Isend(&(connSend[ll]), count, MPI_DOUBLE, dest, tag, + MPI_Isend(&(connSend[ll]), count, pointType, dest, tag, SU2_MPI::GetComm(), &(send_req[iMessage])); iMessage++; } @@ -179,17 +184,17 @@ void CParallelDataSorter::SortOutputData() { /*--- Copy my own rank's data into the recv buffer directly. ---*/ - int mm = VARS_PER_POINT*nPoint_Recv[rank]; - int ll = VARS_PER_POINT*nPoint_Send[rank]; - int kk = VARS_PER_POINT*nPoint_Send[rank+1]; + size_t mm = VARS_PER_POINT*nPoint_Recv[rank]; + size_t ll = VARS_PER_POINT*nPoint_Send[rank]; + size_t kk = VARS_PER_POINT*nPoint_Send[rank+1]; - for (int nn=ll; nn tmpBuffer(nPoint_Recv[size]); - for (int jj = 0; jj < VARS_PER_POINT; jj++){ - for (int ii = 0; ii < nPoint_Recv[size]; ii++){ + for (size_t jj = 0; jj < VARS_PER_POINT; jj++){ + for (size_t ii = 0; ii < static_cast(nPoint_Recv[size]); ii++){ tmpBuffer[idRecv[ii]] = dataBuffer[ii*VARS_PER_POINT+jj]; } - for (int ii = 0; ii < nPoint_Recv[size]; ii++){ + for (size_t ii = 0; ii < static_cast(nPoint_Recv[size]); ii++){ dataBuffer[ii*VARS_PER_POINT+jj] = tmpBuffer[ii]; } } @@ -238,7 +244,7 @@ void CParallelDataSorter::PrepareSendBuffers(std::vector& globalI unsigned long iPoint; unsigned short iProcessor; - int VARS_PER_POINT = GlobalField_Counter; + const size_t VARS_PER_POINT = GlobalField_Counter; /*--- We start with the grid nodes distributed across all procs with no particular ordering assumed. We need to loop through our local partition From 918781d2a28b797bd52ee6b4be636953362e0f2c Mon Sep 17 00:00:00 2001 From: rois1995 Date: Fri, 18 Sep 2026 17:08:18 +0200 Subject: [PATCH 07/35] Write named boundaries in CGNS volume and surface output The CGNS output had no boundary information: the volume file only held the interior cells, and the surface file merged all plotted markers into one unnamed zone. Volume file (CGNS): - After the interior sections, write one boundary section per marker (except send-receive), named as the marker. A marker with a single element type gets a TRI_3/QUAD_4/BAR_2 section, otherwise MIXED. Faces use the volume point numbering, so they share the coordinates and fields of the zone. - Each boundary element is kept on exactly one rank (no halo node and at least one owned node) and gathered on the master node. - Add a ZoneBC with one BC_t per marker (FamilySpecified, PointRange of the section, FaceCenter/EdgeCenter) and a Family_t per marker that carries the physical BC type (wall, farfield, inflow, ...). The ParaView/VTK CGNS reader shows each marker as a named patch, and SU2 can read the file back as a mesh with the same markers. Surface file (SURFACE_CGNS): - Write one zone per plotted marker, named as the marker; the surface data is sorted again for each marker. Co-Authored-By: Claude Opus 5 (1M context) --- .../output/filewriter/CCGNSFileWriter.hpp | 60 ++++- SU2_CFD/src/output/COutput.cpp | 7 + .../src/output/filewriter/CCGNSFileWriter.cpp | 212 ++++++++++++++++-- 3 files changed, 262 insertions(+), 17 deletions(-) diff --git a/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp b/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp index 268e4d58c87e..0a7664458cac 100644 --- a/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp +++ b/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp @@ -39,10 +39,28 @@ #include "CFileWriter.hpp" +class CConfig; +class CGeometry; +class CFVMDataSorter; + class CCGNSFileWriter final : public CFileWriter { private: const bool isSurface; /*!< \brief True if surface file. */ + /*! + * \brief Boundary elements of one marker owned by this rank, written as a boundary section of a volume file. + */ + struct BoundaryMarker { + string name; /*!< \brief Marker tag. */ + unsigned short kindBC; /*!< \brief SU2 boundary condition kind. */ + vector conn; /*!< \brief VTK type followed by the (1-based) output ids of the nodes, per element. */ + }; + vector boundaryMarkers; /*!< \brief Markers written as boundaries of a volume file. */ + + vector surfaceMarkers; /*!< \brief Markers written as one zone each in a surface file. */ + CConfig* config = nullptr; /*!< \brief Config, to sort the surface data of each marker. */ + CGeometry* geometry = nullptr; /*!< \brief Geometry, to sort the surface data of each marker. */ + #ifdef HAVE_CGNS int cgnsFileID; /*!< \brief CGNS file identifier. */ int cgnsBase; /*!< \brief CGNS database index. */ @@ -92,13 +110,47 @@ class CCGNSFileWriter final : public CFileWriter { */ void WriteData(string val_filename) override ; + /*! + * \brief Add the boundaries to a volume file: one boundary section, BC and family per marker, named as the marker. + * \param[in] valConfig - Definition of the problem. + * \param[in] valGeometry - Geometrical definition of the problem. + * \param[in] volumeSorter - The volume data sorter, to find the boundary elements owned by this rank. + */ + void SetBoundaryMarkers(CConfig* valConfig, CGeometry* valGeometry, const CFVMDataSorter* volumeSorter); + + /*! + * \brief Write a surface file with one zone per plotted marker, named as the marker. The data of the surface + * sorter is sorted again for each marker when the file is written. + * \param[in] valConfig - Definition of the problem. + * \param[in] valGeometry - Geometrical definition of the problem. + */ + void SetSurfaceMarkers(CConfig* valConfig, CGeometry* valGeometry); + private: #ifdef HAVE_CGNS /*! - * \brief Initialize CGNS mesh file. + * \brief Create the CGNS file and its base. + * \param[in] val_filename - The name of the file. */ void InitializeMeshFile(const string& val_filename); + /*! + * \brief Write a zone with the data currently held by the data sorter. + * \param[in] zoneName - Name of the zone. + */ + void WriteZone(const string& zoneName); + + /*! + * \brief Create a zone for the data currently held by the data sorter. + * \param[in] zoneName - Name of the zone. + */ + void InitializeZone(const string& zoneName); + + /*! + * \brief Write the boundary sections, BCs and families of the markers set with SetBoundaryMarkers. + */ + void WriteBoundaries(); + /*! * \brief Write i-th coordinate to file in CGNS file format. * \param[in] iField - the output field ID. @@ -144,6 +196,12 @@ class CCGNSFileWriter final : public CFileWriter { if (ier) cg_error_exit(); } + /*! + * \brief Return the CGNS boundary condition type of an SU2 boundary condition kind. + * \param[in] kindBC - SU2 boundary condition kind. + */ + static BCType_t GetCGNSBCType(unsigned short kindBC); + /*! * \brief Return the CGNS element type (ElementType_t). * \param[in] elementType - GEO_TYPE. diff --git a/SU2_CFD/src/output/COutput.cpp b/SU2_CFD/src/output/COutput.cpp index a376483cee59..d2c21f24fe9e 100644 --- a/SU2_CFD/src/output/COutput.cpp +++ b/SU2_CFD/src/output/COutput.cpp @@ -762,6 +762,10 @@ void COutput::WriteToFile(CConfig *config, CGeometry *geometry, OUTPUT_TYPE form LogOutputFiles("CGNS"); fileWriter = new CCGNSFileWriter(volumeDataSorter); + /*--- Add the boundaries, named as the markers (the finite volume sorter knows which elements are halos). ---*/ + if (const auto* fvmSorter = dynamic_cast(volumeDataSorter)) + static_cast(fileWriter)->SetBoundaryMarkers(config, geometry, fvmSorter); + break; case OUTPUT_TYPE::SURFACE_CGNS: @@ -781,6 +785,9 @@ void COutput::WriteToFile(CConfig *config, CGeometry *geometry, OUTPUT_TYPE form LogOutputFiles("CGNS surface"); fileWriter = new CCGNSFileWriter(surfaceDataSorter, true); + /*--- One zone per plotted marker, named as the marker. ---*/ + static_cast(fileWriter)->SetSurfaceMarkers(config, geometry); + break; default: diff --git a/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp b/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp index 56225d1a2682..7f7b1d3c1c08 100644 --- a/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp @@ -26,6 +26,11 @@ */ #include "../../../include/output/filewriter/CCGNSFileWriter.hpp" +#include "../../../include/output/filewriter/CFVMDataSorter.hpp" +#include "../../../../Common/include/CConfig.hpp" +#include "../../../../Common/include/geometry/CGeometry.hpp" + +#include const string CCGNSFileWriter::fileExt = ".cgns"; @@ -41,6 +46,83 @@ void CCGNSFileWriter::WriteData(string val_filename) { /*--- Open the CGNS file for writing. ---*/ InitializeMeshFile(val_filename); + if (surfaceMarkers.empty()) { + WriteZone("Zone"); + } else { + /*--- One zone per marker, the surface data is sorted again for each of them. ---*/ + for (const auto& marker : surfaceMarkers) { + dataSorter->SortConnectivity(config, geometry, vector{marker}); + dataSorter->SortOutputData(); + WriteZone(marker); + } + } + + /*--- Close the CGNS file. ---*/ + if (rank == MASTER_NODE) CallCGNS(cg_close(cgnsFileID)); + +#endif +} + +void CCGNSFileWriter::SetBoundaryMarkers(CConfig* valConfig, CGeometry* valGeometry, + const CFVMDataSorter* volumeSorter) { + boundaryMarkers.clear(); + + for (unsigned short iMarkerCfg = 0; iMarkerCfg < valConfig->GetnMarker_CfgFile(); iMarkerCfg++) { + const string tag = valConfig->GetMarker_CfgFile_TagBound(iMarkerCfg); + const auto kindBC = valConfig->GetMarker_CfgFile_KindBC(tag); + if (kindBC == SEND_RECEIVE) continue; + + BoundaryMarker marker{tag, kindBC, {}}; + + for (unsigned short iMarker = 0; iMarker < valConfig->GetnMarker_All(); iMarker++) { + if (valConfig->GetMarker_All_TagBound(iMarker) != tag) continue; + + for (unsigned long iElem = 0; iElem < valGeometry->GetnElem_Bound(iMarker); iElem++) { + const auto* elem = valGeometry->bound[iMarker][iElem]; + + /*--- Same rule as for the volume elements: keep the element on the rank where none of its nodes is a halo. + Also require a node owned by this rank, so that an element whose nodes are all owned by lower ranks is not + kept again by a higher rank that holds it as a halo element. ---*/ + bool halo = false, owned = false; + for (unsigned short iNode = 0; iNode < elem->GetnNodes(); iNode++) { + halo |= volumeSorter->GetHalo(elem->GetNode(iNode)); + owned |= valGeometry->nodes->GetDomain(elem->GetNode(iNode)); + } + if (halo || !owned) continue; + + marker.conn.push_back(elem->GetVTK_Type()); + for (unsigned short iNode = 0; iNode < elem->GetnNodes(); iNode++) + marker.conn.push_back(valGeometry->nodes->GetGlobalIndex(elem->GetNode(iNode)) + 1); + } + } + boundaryMarkers.push_back(std::move(marker)); + } +} + +void CCGNSFileWriter::SetSurfaceMarkers(CConfig* valConfig, CGeometry* valGeometry) { + config = valConfig; + geometry = valGeometry; + surfaceMarkers.clear(); + + for (unsigned short iMarkerCfg = 0; iMarkerCfg < valConfig->GetnMarker_CfgFile(); iMarkerCfg++) { + const string tag = valConfig->GetMarker_CfgFile_TagBound(iMarkerCfg); + if (valConfig->GetMarker_CfgFile_Plotting(tag) != YES) continue; + + /*--- Only keep the markers present on at least one rank. ---*/ + int localFound = 0, globalFound = 0; + for (unsigned short iMarker = 0; iMarker < valConfig->GetnMarker_All(); iMarker++) { + if (valConfig->GetMarker_All_TagBound(iMarker) == tag && valConfig->GetMarker_All_KindBC(iMarker) != SEND_RECEIVE) + localFound = 1; + } + SU2_MPI::Allreduce(&localFound, &globalFound, 1, MPI_INT, MPI_SUM, SU2_MPI::GetComm()); + if (globalFound > 0) surfaceMarkers.push_back(tag); + } +} + +#ifdef HAVE_CGNS +void CCGNSFileWriter::WriteZone(const string& zoneName) { + InitializeZone(zoneName); + /*--- Write point coordinates. ---*/ WriteField(0, "CoordinateX"); WriteField(1, "CoordinateY"); @@ -61,6 +143,9 @@ void CCGNSFileWriter::WriteData(string val_filename) { WriteConnectivity(HEXAHEDRON, "Hexahedra"); } + /*--- Write the boundaries of a volume file. ---*/ + if (!isSurface) WriteBoundaries(); + /*--- Initialize and write fields. ---*/ InitializeFields(); @@ -68,24 +153,10 @@ void CCGNSFileWriter::WriteData(string val_filename) { for (unsigned long i = nDim; i < fieldNames.size(); ++i) { WriteField(i, fieldNames[i]); } - - /*--- Close the CGNS file. ---*/ - if (rank == MASTER_NODE) CallCGNS(cg_close(cgnsFileID)); - -#endif } -#ifdef HAVE_CGNS void CCGNSFileWriter::InitializeMeshFile(const string& val_filename) { - if (!dataSorter->GetConnectivitySorted()) { - SU2_MPI::Error("Connectivity must be sorted.", CURRENT_FUNCTION); - } - - nLocalPoints = dataSorter->GetnPoints(); nDim = dataSorter->GetnDim(); - GlobalElem = static_cast(dataSorter->GetnElemGlobal()); - GlobalPoint = static_cast(dataSorter->GetnPointsGlobal()); - cumulative = 0; /*--- If surface file cell dimension is decreased. ---*/ const auto nCell = static_cast(nDim - isSurface); @@ -99,15 +170,124 @@ void CCGNSFileWriter::InitializeMeshFile(const string& val_filename) { /*--- Create Base. ---*/ CallCGNS(cg_base_write(cgnsFileID, "Base", nCell, nDim, &cgnsBase)); + } +} - /*--- Create Zone. ---*/ +void CCGNSFileWriter::InitializeZone(const string& zoneName) { + if (!dataSorter->GetConnectivitySorted()) { + SU2_MPI::Error("Connectivity must be sorted.", CURRENT_FUNCTION); + } + + nLocalPoints = dataSorter->GetnPoints(); + GlobalElem = static_cast(dataSorter->GetnElemGlobal()); + GlobalPoint = static_cast(dataSorter->GetnPointsGlobal()); + cumulative = 0; + + if (rank == MASTER_NODE) { + /*--- Create Zone. The number of cells does not include the boundary elements. ---*/ array zoneData; zoneData[0] = GlobalPoint; zoneData[1] = GlobalElem; zoneData[2] = 0; - CallCGNS(cg_zone_write(cgnsFileID, cgnsBase, "Zone", zoneData.data(), Unstructured, &cgnsZone)); + CallCGNS(cg_zone_write(cgnsFileID, cgnsBase, zoneName.substr(0, 32).c_str(), zoneData.data(), Unstructured, + &cgnsZone)); + } +} + +void CCGNSFileWriter::WriteBoundaries() { + for (const auto& marker : boundaryMarkers) { + /*--- Gather the boundary elements of this marker on the master node, in rank order. ---*/ + const unsigned long localSize = marker.conn.size(); + vector sizes(size); + SU2_MPI::Allgather(&localSize, 1, MPI_UNSIGNED_LONG, sizes.data(), 1, MPI_UNSIGNED_LONG, SU2_MPI::GetComm()); + + const auto totalSize = std::accumulate(sizes.begin(), sizes.end(), 0ul); + if (totalSize == 0) continue; + + if (rank != MASTER_NODE) { + SendChunked(marker.conn.data(), localSize * sizeof(unsigned long), MASTER_NODE, 2); + continue; + } + + vector conn(totalSize); + std::copy(marker.conn.begin(), marker.conn.end(), conn.begin()); + auto offset = localSize; + for (int i = 0; i < size; ++i) { + if (i == MASTER_NODE) continue; + RecvChunked(conn.data() + offset, sizes[i] * sizeof(unsigned long), i, 2); + offset += sizes[i]; + } + + /*--- Convert to the CGNS numbering: with a single element type the node ids of the elements, otherwise a MIXED + section, i.e. the CGNS element type followed by the node ids of each element, plus the start offsets. ---*/ + vector elems, mixed, startOffsets{0}; + bool singleType = true; + for (size_t pos = 0; pos < conn.size();) { + const auto type = static_cast(conn[pos]); + const auto nNodes = nPointsOfElementType(type); + singleType &= (type == conn[0]); + mixed.push_back(GetCGNSType(type)); + for (unsigned short iNode = 1; iNode <= nNodes; ++iNode) { + elems.push_back(static_cast(conn[pos + iNode])); + mixed.push_back(static_cast(conn[pos + iNode])); + } + startOffsets.push_back(static_cast(mixed.size())); + pos += nNodes + 1; + } + const auto nElem = static_cast(startOffsets.size() - 1); + + const string name = marker.name.substr(0, 32); + cgsize_t range[2] = {cumulative + 1, cumulative + nElem}; + int section; + if (singleType) { + CallCGNS(cg_section_write(cgnsFileID, cgnsBase, cgnsZone, name.c_str(), GetCGNSType(conn[0]), range[0], + range[1], 0, elems.data(), §ion)); + } else { + CallCGNS(cg_poly_section_write(cgnsFileID, cgnsBase, cgnsZone, name.c_str(), MIXED, range[0], range[1], 0, + mixed.data(), startOffsets.data(), §ion)); + } + cumulative += nElem; + + /*--- The BC points to the boundary elements and takes its type from a family with the name of the marker. ---*/ + int bc, family, familyBC; + CallCGNS(cg_boco_write(cgnsFileID, cgnsBase, cgnsZone, name.c_str(), FamilySpecified, PointRange, 2, range, &bc)); + CallCGNS(cg_boco_gridlocation_write(cgnsFileID, cgnsBase, cgnsZone, bc, nDim == 3 ? FaceCenter : EdgeCenter)); + CallCGNS(cg_goto(cgnsFileID, cgnsBase, "Zone_t", cgnsZone, "ZoneBC_t", 1, "BC_t", bc, "end")); + CallCGNS(cg_famname_write(name.c_str())); + + CallCGNS(cg_family_write(cgnsFileID, cgnsBase, name.c_str(), &family)); + CallCGNS(cg_fambc_write(cgnsFileID, cgnsBase, family, "FamBC", GetCGNSBCType(marker.kindBC), &familyBC)); + } +} + +BCType_t CCGNSFileWriter::GetCGNSBCType(unsigned short kindBC) { + switch (kindBC) { + case EULER_WALL: + return BCWallInviscid; + case HEAT_FLUX: + return BCWallViscousHeatFlux; + case ISOTHERMAL: + return BCWallViscousIsothermal; + case HEAT_TRANSFER: + case CHT_WALL_INTERFACE: + case SMOLUCHOWSKI_MAXWELL: + return BCWallViscous; + case FAR_FIELD: + return BCFarfield; + case SYMMETRY_PLANE: + return BCSymmetryPlane; + case INLET_FLOW: + return BCInflow; + case OUTLET_FLOW: + return BCOutflow; + case SUPERSONIC_INLET: + return BCInflowSupersonic; + case SUPERSONIC_OUTLET: + return BCOutflowSupersonic; + default: + return BCTypeUserDefined; } } From bcec2eb3e4165d762648fa5ab6fb93de9c76a4b7 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Sun, 20 Sep 2026 22:33:45 +0200 Subject: [PATCH 08/35] Add WRT_OUTPUT_DOUBLE_PRECISION and always write CGNS coordinates in double The CGNS and Paraview XML writers stored coordinates and fields in single precision. Coordinates in single precision move the points by up to ~1e-7 of the size of the domain, which can exceed the size of the smallest cells, so a CGNS output file could not be reused as a mesh without losing accuracy (in a round trip of a periodic case the solution changed by up to 5e-5). - Write the CGNS coordinates always in double precision. - Add WRT_OUTPUT_DOUBLE_PRECISION (default NO) to write the fields of the volume and surface files in double precision, for CGNS, PARAVIEW and PARAVIEW_MULTIBLOCK. Single precision remains the default, so files and their sizes are unchanged for visualization use. With double coordinates the same round trip reproduces the coordinates exactly and the solution to ~1e-14. Co-Authored-By: Claude Opus 5 (1M context) --- Common/include/CConfig.hpp | 7 +++ Common/src/CConfig.cpp | 4 ++ .../output/filewriter/CCGNSFileWriter.hpp | 22 +++++--- .../filewriter/CParaviewVTMFileWriter.hpp | 3 +- .../filewriter/CParaviewXMLFileWriter.hpp | 12 ++++- SU2_CFD/src/output/COutput.cpp | 8 +-- .../src/output/filewriter/CCGNSFileWriter.cpp | 27 +++++++--- .../filewriter/CParaviewVTMFileWriter.cpp | 9 ++-- .../filewriter/CParaviewXMLFileWriter.cpp | 51 +++++++++++-------- 9 files changed, 98 insertions(+), 45 deletions(-) diff --git a/Common/include/CConfig.hpp b/Common/include/CConfig.hpp index 36e802e99f67..b5db5a89b3fa 100644 --- a/Common/include/CConfig.hpp +++ b/Common/include/CConfig.hpp @@ -746,6 +746,7 @@ class CConfig { su2double Cauchy_Eps; /*!< \brief Epsilon used for the convergence. */ bool Restart, /*!< \brief Restart solution (for direct, adjoint, and linearized problems).*/ Wrt_Restart_Compact, /*!< \brief Write compact restart files with minimum nr. of variables. */ + Wrt_Output_Double_Precision, /*!< \brief Write the fields of the volume and surface files in double. */ Read_Binary_Restart, /*!< \brief Read binary SU2 native restart files.*/ Wrt_Restart_Overwrite, /*!< \brief Overwrite restart files or append iteration number.*/ Wrt_Surface_Overwrite, /*!< \brief Overwrite surface output files or append iteration number.*/ @@ -5674,6 +5675,12 @@ class CConfig { */ bool GetWrt_Restart_Compact(void) const { return Wrt_Restart_Compact; } + /*! + * \brief Flag for whether the fields of the volume and surface files are written in double precision. + * \return TRUE means that double precision is used. + */ + bool GetWrt_Output_Double_Precision(void) const { return Wrt_Output_Double_Precision; } + /*! * \brief Flag for whether restart solution files are overwritten. * \return Flag for overwriting. If Flag=false, iteration nr is appended to filename diff --git a/Common/src/CConfig.cpp b/Common/src/CConfig.cpp index d5c403d9ab11..4bdc0e0399dd 100644 --- a/Common/src/CConfig.cpp +++ b/Common/src/CConfig.cpp @@ -1263,6 +1263,10 @@ void CConfig::SetConfig_Options() { addBoolOption("RESTART_SOL", Restart, false); /*!\brief WRT_RESTART_COMPACT \n DESCRIPTION: Minimize the size of restart files \n Options: NO, YES \ingroup Config */ addBoolOption("WRT_RESTART_COMPACT", Wrt_Restart_Compact, true); + /*!\brief WRT_OUTPUT_DOUBLE_PRECISION \n DESCRIPTION: Write the fields of the volume and surface files (CGNS and + Paraview XML) in double precision instead of single, the coordinates are always written in double precision by the + CGNS writer. \n Options: NO, YES \ingroup Config */ + addBoolOption("WRT_OUTPUT_DOUBLE_PRECISION", Wrt_Output_Double_Precision, false); /*!\brief BINARY_RESTART \n DESCRIPTION: Read binary SU2 native restart files. \n Options: YES, NO \ingroup Config */ addBoolOption("READ_BINARY_RESTART", Read_Binary_Restart, true); /*!\brief WRT_RESTART_OVERWRITE \n DESCRIPTION: overwrite restart files or append iteration number. \n Options: YES, NO \ingroup Config */ diff --git a/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp b/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp index 0a7664458cac..ff5ad92cec48 100644 --- a/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp +++ b/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp @@ -45,7 +45,8 @@ class CFVMDataSorter; class CCGNSFileWriter final : public CFileWriter { private: - const bool isSurface; /*!< \brief True if surface file. */ + const bool isSurface; /*!< \brief True if surface file. */ + const bool doublePrecisionFields; /*!< \brief True to write the fields in double precision instead of single. */ /*! * \brief Boundary elements of one marker owned by this rank, written as a boundary section of a volume file. @@ -75,13 +76,8 @@ class CCGNSFileWriter final : public CFileWriter { cgsize_t GlobalPoint; /*!< \brief Total number of points. */ cgsize_t GlobalElem; /*!< \brief Total number of elements. */ - typedef float dataPrecision; /*!< \brief Define data precision of output (float or double). */ - const DataType_t dataType = RealSingle; /*!< \brief Datatype of fields can be RealSingle or RealDouble. */ - vector sendBufferConnectivity; /*!< \brief Send buffer for connectivity data. */ vector recvBufferConnectivity; /*!< \brief Receive buffer for connectivity data. */ - vector recvBufferField; /*!< \brief Send buffer for field data. */ - vector sendBufferField; /*!< \brief Receive buffer for field data. */ cgsize_t cumulative; /*!< \brief Cumulative number of elements written. */ @@ -102,7 +98,7 @@ class CCGNSFileWriter final : public CFileWriter { * \param[in] valDataSorter - The parallel sorted data to write. * \param[in] isSurf - True if it is a surface file. */ - CCGNSFileWriter(CParallelDataSorter* valDataSorter, bool isSurf = false); + CCGNSFileWriter(CParallelDataSorter* valDataSorter, bool isSurf = false, bool doublePrecision = false); /*! * \brief Write sorted data to file in CGNS file format. @@ -152,12 +148,22 @@ class CCGNSFileWriter final : public CFileWriter { void WriteBoundaries(); /*! - * \brief Write i-th coordinate to file in CGNS file format. + * \brief Write i-th coordinate to file in CGNS file format. Coordinates are always written in double precision, + * the fields follow the precision requested by the user. * \param[in] iField - the output field ID. * \param[in] FieldName - Field name in the CGNS. */ void WriteField(int iField, const string& FieldName); + /*! + * \brief Write i-th coordinate or field to file with the given data type. + * \param[in] iField - the output field ID. + * \param[in] FieldName - Field name in the CGNS. + * \param[in] dataType - CGNS data type matching T, i.e. RealSingle for float and RealDouble for double. + */ + template + void WriteFieldOfType(int iField, const string& FieldName, DataType_t dataType); + /*! * \brief Write connectivity to file for GEO_TYPE in CGNS file format. * \param[in] type - GEO_TYPE. diff --git a/SU2_CFD/include/output/filewriter/CParaviewVTMFileWriter.hpp b/SU2_CFD/include/output/filewriter/CParaviewVTMFileWriter.hpp index ee3b7c601a9d..851c7b1fdeee 100644 --- a/SU2_CFD/include/output/filewriter/CParaviewVTMFileWriter.hpp +++ b/SU2_CFD/include/output/filewriter/CParaviewVTMFileWriter.hpp @@ -124,7 +124,8 @@ class CParaviewVTMFileWriter final: public CFileWriter{ * \param[in] dataSorter - Datasorter object containing the actual data. Note, data must be sorted. */ //void AddDataset(string name, string file, CParallelDataSorter* dataSorter); - void AddDataset(const string& foldername, string name, const string& file, CParallelDataSorter* dataSorter); + void AddDataset(const string& foldername, string name, const string& file, CParallelDataSorter* dataSorter, + bool doublePrecision); /*! * \brief Start a new block diff --git a/SU2_CFD/include/output/filewriter/CParaviewXMLFileWriter.hpp b/SU2_CFD/include/output/filewriter/CParaviewXMLFileWriter.hpp index d696f348212a..4528ec76f776 100644 --- a/SU2_CFD/include/output/filewriter/CParaviewXMLFileWriter.hpp +++ b/SU2_CFD/include/output/filewriter/CParaviewXMLFileWriter.hpp @@ -38,6 +38,7 @@ class CParaviewXMLFileWriter final: public CFileWriter{ */ enum class VTKDatatype { FLOAT32, + FLOAT64, INT32, INT64, UINT8 @@ -48,6 +49,11 @@ class CParaviewXMLFileWriter final: public CFileWriter{ */ bool bigEndian; + /*! + * \brief True to write the coordinates and fields in double precision instead of single. + */ + bool doublePrecision = false; + /*! * \brief The current data offset that is used to find data in the binary blob at the end of the file */ @@ -71,7 +77,7 @@ class CParaviewXMLFileWriter final: public CFileWriter{ * \brief Construct a file writer using field names and the data sorter. * \param[in] valDataSorter - The parallel sorted data to write */ - CParaviewXMLFileWriter(CParallelDataSorter* valDataSorter); + CParaviewXMLFileWriter(CParallelDataSorter* valDataSorter, bool valDoublePrecision = false); /*! * \brief Destructor @@ -113,6 +119,10 @@ class CParaviewXMLFileWriter final: public CFileWriter{ */ inline void GetTypeInfo(const VTKDatatype type, string &typeStr, unsigned long &typeSize) const { switch (type) { + case VTKDatatype::FLOAT64: + typeStr = "\"Float64\""; + typeSize = sizeof(double); + break; case VTKDatatype::FLOAT32: typeStr = "\"Float32\""; typeSize = sizeof(float); diff --git a/SU2_CFD/src/output/COutput.cpp b/SU2_CFD/src/output/COutput.cpp index d2c21f24fe9e..5081889b7373 100644 --- a/SU2_CFD/src/output/COutput.cpp +++ b/SU2_CFD/src/output/COutput.cpp @@ -556,7 +556,7 @@ void COutput::WriteToFile(CConfig *config, CGeometry *geometry, OUTPUT_TYPE form volumeDataSorter->SortConnectivity(config, geometry, true); LogOutputFiles("Paraview"); - fileWriter = new CParaviewXMLFileWriter(volumeDataSorter); + fileWriter = new CParaviewXMLFileWriter(volumeDataSorter, config->GetWrt_Output_Double_Precision()); break; @@ -683,7 +683,7 @@ void COutput::WriteToFile(CConfig *config, CGeometry *geometry, OUTPUT_TYPE form surfaceDataSorter->SortOutputData(); LogOutputFiles("Paraview surface"); - fileWriter = new CParaviewXMLFileWriter(surfaceDataSorter); + fileWriter = new CParaviewXMLFileWriter(surfaceDataSorter, config->GetWrt_Output_Double_Precision()); break; @@ -760,7 +760,7 @@ void COutput::WriteToFile(CConfig *config, CGeometry *geometry, OUTPUT_TYPE form volumeDataSorter->SortConnectivity(config, geometry, true); LogOutputFiles("CGNS"); - fileWriter = new CCGNSFileWriter(volumeDataSorter); + fileWriter = new CCGNSFileWriter(volumeDataSorter, false, config->GetWrt_Output_Double_Precision()); /*--- Add the boundaries, named as the markers (the finite volume sorter knows which elements are halos). ---*/ if (const auto* fvmSorter = dynamic_cast(volumeDataSorter)) @@ -783,7 +783,7 @@ void COutput::WriteToFile(CConfig *config, CGeometry *geometry, OUTPUT_TYPE form surfaceDataSorter->SortOutputData(); LogOutputFiles("CGNS surface"); - fileWriter = new CCGNSFileWriter(surfaceDataSorter, true); + fileWriter = new CCGNSFileWriter(surfaceDataSorter, true, config->GetWrt_Output_Double_Precision()); /*--- One zone per plotted marker, named as the marker. ---*/ static_cast(fileWriter)->SetSurfaceMarkers(config, geometry); diff --git a/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp b/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp index 7f7b1d3c1c08..a8b1b72b99c2 100644 --- a/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp @@ -34,8 +34,8 @@ const string CCGNSFileWriter::fileExt = ".cgns"; -CCGNSFileWriter::CCGNSFileWriter(CParallelDataSorter* valDataSorter, bool isSurf) - : CFileWriter(valDataSorter, fileExt), isSurface(isSurf) {} +CCGNSFileWriter::CCGNSFileWriter(CParallelDataSorter* valDataSorter, bool isSurf, bool doublePrecision) + : CFileWriter(valDataSorter, fileExt), isSurface(isSurf), doublePrecisionFields(doublePrecision) {} void CCGNSFileWriter::WriteData(string val_filename) { @@ -292,21 +292,36 @@ BCType_t CCGNSFileWriter::GetCGNSBCType(unsigned short kindBC) { } void CCGNSFileWriter::WriteField(int iField, const string& FieldName) { + /*--- The coordinates define the mesh, so they are always written in double precision. Single precision would + move the points by up to ~1e-7 of the size of the domain, which can be larger than the smallest cells. ---*/ + + const bool isCoord = iField < nDim; + + if (isCoord || doublePrecisionFields) + WriteFieldOfType(iField, FieldName, RealDouble); + else + WriteFieldOfType(iField, FieldName, RealSingle); +} + +template +void CCGNSFileWriter::WriteFieldOfType(int iField, const string& FieldName, DataType_t dataType) { /*--- Check if field is coordinate. ---*/ const bool isCoord = iField < nDim; /*--- Create send buffer. ---*/ - sendBufferField.resize(nLocalPoints); + vector sendBufferField(nLocalPoints); for (unsigned long iPoint = 0; iPoint < nLocalPoints; iPoint++) { - sendBufferField[iPoint] = static_cast(dataSorter->GetData(iField, iPoint)); + sendBufferField[iPoint] = static_cast(dataSorter->GetData(iField, iPoint)); } if (rank != MASTER_NODE) { - SendChunked(sendBufferField.data(), nLocalPoints * sizeof(dataPrecision), MASTER_NODE, 0); + SendChunked(sendBufferField.data(), nLocalPoints * sizeof(T), MASTER_NODE, 0); return; } + vector recvBufferField; + /*--- Coordinate vector is written in blocks, one for each process. ---*/ cgsize_t nodeBegin = 1; auto nodeEnd = static_cast(nLocalPoints); @@ -331,7 +346,7 @@ void CCGNSFileWriter::WriteField(int iField, const string& FieldName) { const auto recvSize = static_cast(nodeEnd - nodeBegin + 1); recvBufferField.resize(recvSize); - RecvChunked(recvBufferField.data(), recvSize * sizeof(dataPrecision), i, 0); + RecvChunked(recvBufferField.data(), recvSize * sizeof(T), i, 0); if (recvSize == 0) continue; if (isCoord) { int CoordinateNumber; diff --git a/SU2_CFD/src/output/filewriter/CParaviewVTMFileWriter.cpp b/SU2_CFD/src/output/filewriter/CParaviewVTMFileWriter.cpp index da812ee4d9d3..48d400df48e9 100644 --- a/SU2_CFD/src/output/filewriter/CParaviewVTMFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CParaviewVTMFileWriter.cpp @@ -87,7 +87,8 @@ void CParaviewVTMFileWriter::WriteData(string val_filename){ } -void CParaviewVTMFileWriter::AddDataset(const string& foldername, string name, const string& file, CParallelDataSorter* dataSorter){ +void CParaviewVTMFileWriter::AddDataset(const string& foldername, string name, const string& file, + CParallelDataSorter* dataSorter, bool doublePrecision){ /*--- Construct the full file name incl. folder ---*/ /*--- Note that the folder name is simply the filename ---*/ @@ -96,7 +97,7 @@ void CParaviewVTMFileWriter::AddDataset(const string& foldername, string name, c /*--- Create an XML writer and dump data into file ---*/ - CParaviewXMLFileWriter XMLWriter(dataSorter); + CParaviewXMLFileWriter XMLWriter(dataSorter, doublePrecision); XMLWriter.WriteData(fullFilename); /*--- Add the dataset to the vtm file ---*/ @@ -136,7 +137,7 @@ void CParaviewVTMFileWriter::WriteFolderData(const string& foldername, CConfig * StartBlock(std::move(multiZoneHeaderString)); StartBlock("Internal"); - AddDataset(foldername,"Internal", "Internal", volumeDataSorter); + AddDataset(foldername, "Internal", "Internal", volumeDataSorter, config->GetWrt_Output_Double_Precision()); EndBlock(); /*--- Open a block for the boundary ---*/ @@ -180,7 +181,7 @@ void CParaviewVTMFileWriter::WriteFolderData(const string& foldername, CConfig * /*--- Add the dataset ---*/ - AddDataset(foldername, markerTag, markerTag, surfaceDataSorter); + AddDataset(foldername, markerTag, markerTag, surfaceDataSorter, config->GetWrt_Output_Double_Precision()); } } diff --git a/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp b/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp index 1ab60a49fbcc..fedce3b4848f 100644 --- a/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp @@ -32,8 +32,8 @@ const string CParaviewXMLFileWriter::fileExt = ".vtu"; -CParaviewXMLFileWriter::CParaviewXMLFileWriter(CParallelDataSorter *valDataSorter) : - CFileWriter(valDataSorter, fileExt){ +CParaviewXMLFileWriter::CParaviewXMLFileWriter(CParallelDataSorter *valDataSorter, bool valDoublePrecision) : + CFileWriter(valDataSorter, fileExt), doublePrecision(valDoublePrecision){ /* Check for big endian. We have to swap bytes otherwise. * Since size of character is 1 byte when the character pointer @@ -105,6 +105,10 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ const bool connInt64 = GlobalElemStorage > static_cast(std::numeric_limits::max()); const auto connType = connInt64 ? VTKDatatype::INT64 : VTKDatatype::INT32; + /*--- Precision of the coordinates and fields. ---*/ + + const auto realType = doublePrecision ? VTKDatatype::FLOAT64 : VTKDatatype::FLOAT32; + /* Write the ASCII XML header. Note that we use the appended format for the data, * which means that all data is appended at the end of the file in one binary blob. */ @@ -120,7 +124,7 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ WriteMPIString("\n", MASTER_NODE); WriteMPIString("\n", MASTER_NODE); - AddDataArray(VTKDatatype::FLOAT32, "", NCOORDS, myPoint*NCOORDS, GlobalPoint*NCOORDS); + AddDataArray(realType, "", NCOORDS, myPoint*NCOORDS, GlobalPoint*NCOORDS); WriteMPIString("\n", MASTER_NODE); WriteMPIString("\n", MASTER_NODE); AddDataArray(connType, "connectivity", 1, myElemStorage, GlobalElemStorage); @@ -173,11 +177,11 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ fieldname.erase(fieldname.end()-2,fieldname.end()); - AddDataArray(VTKDatatype::FLOAT32, fieldname, NCOORDS, myPoint*NCOORDS, GlobalPoint*NCOORDS); + AddDataArray(realType, fieldname, NCOORDS, myPoint*NCOORDS, GlobalPoint*NCOORDS); } else if (output_variable) { - AddDataArray(VTKDatatype::FLOAT32, fieldname, 1, myPoint, GlobalPoint); + AddDataArray(realType, fieldname, 1, myPoint, GlobalPoint); } @@ -193,20 +197,30 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ /*--- Load/write the 1D buffer of point coordinates. Note that we always have 3 coordinate dimensions, even for 2D problems. ---*/ - vector dataBufferFloat(myPoint*NCOORDS); + vector dataBuffer(myPoint*NCOORDS); + + /*--- Write the staged point data with the precision requested by the user. ---*/ + + auto writeRealArray = [&](unsigned long size, unsigned long globalSize, unsigned long offset) { + if (realType == VTKDatatype::FLOAT64) { + WriteDataArray(dataBuffer.data(), realType, size, globalSize, offset); + } else { + vector buffer(dataBuffer.begin(), dataBuffer.begin() + size); + WriteDataArray(buffer.data(), realType, size, globalSize, offset); + } + }; + for (iPoint = 0; iPoint < myPoint; iPoint++) { for (iDim = 0; iDim < NCOORDS; iDim++) { if (nDim == 2 && iDim == 2) { - dataBufferFloat[iPoint*NCOORDS + iDim] = 0.0; + dataBuffer[iPoint*NCOORDS + iDim] = 0.0; } else { - auto val = (float)dataSorter->GetData(iDim, iPoint); - dataBufferFloat[iPoint*NCOORDS + iDim] = val; + dataBuffer[iPoint*NCOORDS + iDim] = SU2_TYPE::GetValue(dataSorter->GetData(iDim, iPoint)); } } } - WriteDataArray(dataBufferFloat.data(), VTKDatatype::FLOAT32, NCOORDS*myPoint, GlobalPoint*NCOORDS, - dataSorter->GetnPointCumulative(rank)*NCOORDS); + writeRealArray(NCOORDS*myPoint, GlobalPoint*NCOORDS, dataSorter->GetnPointCumulative(rank)*NCOORDS); /*--- Load/write 1D buffers for the connectivity of each element type. ---*/ @@ -292,20 +306,17 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ /*--- Load up the buffer for writing this rank's vector data. ---*/ - float val = 0.0; for (iPoint = 0; iPoint < myPoint; iPoint++) { for (iDim = 0; iDim < NCOORDS; iDim++) { if (nDim == 2 && iDim == 2) { - dataBufferFloat[iPoint*NCOORDS + iDim] = 0.0; + dataBuffer[iPoint*NCOORDS + iDim] = 0.0; } else { - val = (float)dataSorter->GetData(VarCounter+iDim,iPoint); - dataBufferFloat[iPoint*NCOORDS + iDim] = val; + dataBuffer[iPoint*NCOORDS + iDim] = SU2_TYPE::GetValue(dataSorter->GetData(VarCounter+iDim,iPoint)); } } } - WriteDataArray(dataBufferFloat.data(), VTKDatatype::FLOAT32, myPoint*NCOORDS, GlobalPoint*NCOORDS, - dataSorter->GetnPointCumulative(rank)*NCOORDS); + writeRealArray(myPoint*NCOORDS, GlobalPoint*NCOORDS, dataSorter->GetnPointCumulative(rank)*NCOORDS); VarCounter++; @@ -316,12 +327,10 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ This will be replaced with a derived data type most likely. ---*/ for (iPoint = 0; iPoint < myPoint; iPoint++) { - auto val = (float)dataSorter->GetData(VarCounter,iPoint); - dataBufferFloat[iPoint] = val; + dataBuffer[iPoint] = SU2_TYPE::GetValue(dataSorter->GetData(VarCounter,iPoint)); } - WriteDataArray(dataBufferFloat.data(), VTKDatatype::FLOAT32, myPoint, GlobalPoint, - dataSorter->GetnPointCumulative(rank)); + writeRealArray(myPoint, GlobalPoint, dataSorter->GetnPointCumulative(rank)); VarCounter++; } From d63f3d4e1a632628ccdac4d583ec3fb76ff2566f Mon Sep 17 00:00:00 2001 From: rois1995 Date: Sun, 20 Sep 2026 22:47:37 +0200 Subject: [PATCH 09/35] Write the CGNS output in parallel with the cgp_* API The CGNS writer gathered all data on the master node, which wrote the whole file by itself: the master was an I/O and memory bottleneck, and a large file could take long enough for the job to end before the file was closed. Open the file with cgp_open on all ranks and write each rank's own range of points, elements and fields with cgp_coord_write_data, cgp_elements_write_data and cgp_field_write_data. The nodes of the file (base, zone, sections, solution, fields, boundary conditions, families) are metadata and are created by all ranks with the same arguments. Ranks without data take part in the collective calls and write nothing. Boundary sections are written the same way, so the gather of the boundary faces on the master node is also gone; markers with more than one element type use the parallel MIXED API (cgp_poly_*). Without MPI the serial CGNS API is used, as before. The chunked MPI transfers of the old gather path are no longer needed and were removed. Co-Authored-By: Claude Opus 5 (1M context) --- .../include/parallelization/mpi_structure.hpp | 1 + .../output/filewriter/CCGNSFileWriter.hpp | 36 +- .../src/output/filewriter/CCGNSFileWriter.cpp | 321 +++++++++--------- 3 files changed, 183 insertions(+), 175 deletions(-) diff --git a/Common/include/parallelization/mpi_structure.hpp b/Common/include/parallelization/mpi_structure.hpp index 242aa00d7b0f..8d10cdc29d59 100644 --- a/Common/include/parallelization/mpi_structure.hpp +++ b/Common/include/parallelization/mpi_structure.hpp @@ -488,6 +488,7 @@ class CMediMPIWrapper : public CBaseMPIWrapper { #define MPI_MAX 10 #define MPI_INT 11 #define MPI_PROD 12 +#define MPI_BOR 13 #define MPI_STATUS_IGNORE nullptr /*! diff --git a/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp b/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp index ff5ad92cec48..60693a3abb10 100644 --- a/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp +++ b/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp @@ -32,6 +32,9 @@ #include #endif #include "cgnslib.h" +#ifdef HAVE_MPI +#include "pcgnslib.h" +#endif #endif #include @@ -76,14 +79,8 @@ class CCGNSFileWriter final : public CFileWriter { cgsize_t GlobalPoint; /*!< \brief Total number of points. */ cgsize_t GlobalElem; /*!< \brief Total number of elements. */ - vector sendBufferConnectivity; /*!< \brief Send buffer for connectivity data. */ - vector recvBufferConnectivity; /*!< \brief Receive buffer for connectivity data. */ - cgsize_t cumulative; /*!< \brief Cumulative number of elements written. */ - /*--- Max bytes per MPI message, kept below INT_MAX so the int count of MPI never overflows. ---*/ - static constexpr size_t maxChunkBytes = size_t(1) << 30; - /*--- Max connectivity entries per section, so that readers using 32-bit sizes can read it. ---*/ static constexpr cgsize_t maxSectionEntries = std::numeric_limits::max(); #endif @@ -177,22 +174,25 @@ class CCGNSFileWriter final : public CFileWriter { void InitializeFields(); /*! - * \brief Send a buffer of any size in chunks of at most maxChunkBytes (MPI counts are int). - * \param[in] buf - Data to send. - * \param[in] nBytes - Size of the data in bytes. - * \param[in] dest - Destination rank. - * \param[in] tag - Message tag. + * \brief Create an element section, collectively when built with MPI. + * \param[in] name - Name of the section. + * \param[in] type - CGNS element type of the section. + * \param[in] start - First element of the section. + * \param[in] end - Last element of the section. + * \param[out] section - Index of the section. + * \returns CGNS error code. */ - static void SendChunked(const void* buf, size_t nBytes, int dest, int tag); + int SectionWrite(const string& name, ElementType_t type, cgsize_t start, cgsize_t end, int* section); /*! - * \brief Receive a buffer sent with SendChunked. - * \param[out] buf - Receive buffer, must hold nBytes. - * \param[in] nBytes - Size of the data in bytes. - * \param[in] source - Source rank. - * \param[in] tag - Message tag. + * \brief Write the connectivity of the elements [start, end] of a section, the elements of this rank. + * \param[in] section - Index of the section. + * \param[in] start - First element written by this rank. + * \param[in] end - Last element written by this rank. + * \param[in] elements - Connectivity, nullptr if this rank writes no element of the section. + * \returns CGNS error code. */ - static void RecvChunked(void* buf, size_t nBytes, int source, int tag); + int ElementsWriteData(int section, cgsize_t start, cgsize_t end, const cgsize_t* elements); /*! * \brief Call a generic CGNS function. diff --git a/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp b/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp index a8b1b72b99c2..73730f3db419 100644 --- a/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp @@ -58,7 +58,11 @@ void CCGNSFileWriter::WriteData(string val_filename) { } /*--- Close the CGNS file. ---*/ - if (rank == MASTER_NODE) CallCGNS(cg_close(cgnsFileID)); +#ifdef HAVE_MPI + CallCGNS(cgp_close(cgnsFileID)); +#else + CallCGNS(cg_close(cgnsFileID)); +#endif #endif } @@ -161,16 +165,24 @@ void CCGNSFileWriter::InitializeMeshFile(const string& val_filename) { /*--- If surface file cell dimension is decreased. ---*/ const auto nCell = static_cast(nDim - isSurface); - if (rank == MASTER_NODE) { - /*--- Remove the previous file if present. ---*/ - remove(val_filename.c_str()); + /*--- Remove the previous file if present, before any rank opens it. ---*/ + if (rank == MASTER_NODE) remove(val_filename.c_str()); - /*--- Create CGNS file and open in write mode. ---*/ - CallCGNS(cg_open(val_filename.c_str(), CG_MODE_WRITE, &cgnsFileID)); +#ifdef HAVE_MPI + /*--- All ranks open the file and write their own part of the data with the parallel CGNS API. The nodes of the + file (base, zone, sections, solution, fields, boundary conditions) are metadata and must be created by all + ranks with the same arguments, only the data itself is written per rank. ---*/ - /*--- Create Base. ---*/ - CallCGNS(cg_base_write(cgnsFileID, "Base", nCell, nDim, &cgnsBase)); - } + SU2_MPI::Barrier(SU2_MPI::GetComm()); + CallCGNS(cgp_mpi_comm(SU2_MPI::GetComm())); + CallCGNS(cgp_pio_mode(CGP_COLLECTIVE)); + CallCGNS(cgp_open(val_filename.c_str(), CG_MODE_WRITE, &cgnsFileID)); +#else + CallCGNS(cg_open(val_filename.c_str(), CG_MODE_WRITE, &cgnsFileID)); +#endif + + /*--- Create Base. ---*/ + CallCGNS(cg_base_write(cgnsFileID, "Base", nCell, nDim, &cgnsBase)); } void CCGNSFileWriter::InitializeZone(const string& zoneName) { @@ -183,74 +195,109 @@ void CCGNSFileWriter::InitializeZone(const string& zoneName) { GlobalPoint = static_cast(dataSorter->GetnPointsGlobal()); cumulative = 0; - if (rank == MASTER_NODE) { - /*--- Create Zone. The number of cells does not include the boundary elements. ---*/ - array zoneData; + /*--- Create Zone. The number of cells does not include the boundary elements. ---*/ + array zoneData; - zoneData[0] = GlobalPoint; - zoneData[1] = GlobalElem; - zoneData[2] = 0; + zoneData[0] = GlobalPoint; + zoneData[1] = GlobalElem; + zoneData[2] = 0; - CallCGNS(cg_zone_write(cgnsFileID, cgnsBase, zoneName.substr(0, 32).c_str(), zoneData.data(), Unstructured, - &cgnsZone)); - } + CallCGNS( + cg_zone_write(cgnsFileID, cgnsBase, zoneName.substr(0, 32).c_str(), zoneData.data(), Unstructured, &cgnsZone)); } void CCGNSFileWriter::WriteBoundaries() { for (const auto& marker : boundaryMarkers) { - /*--- Gather the boundary elements of this marker on the master node, in rank order. ---*/ - const unsigned long localSize = marker.conn.size(); - vector sizes(size); - SU2_MPI::Allgather(&localSize, 1, MPI_UNSIGNED_LONG, sizes.data(), 1, MPI_UNSIGNED_LONG, SU2_MPI::GetComm()); + /*--- Count the elements of this rank and collect the element types it holds. The local connectivity holds the + VTK type of each element followed by the ids of its nodes. ---*/ + + unsigned long nLocalElem = 0, typesMask = 0; + for (size_t pos = 0; pos < marker.conn.size();) { + const auto type = static_cast(marker.conn[pos]); + typesMask |= 1ul << type; + nLocalElem++; + pos += nPointsOfElementType(type) + 1; + } + const unsigned long nLocalEntries = marker.conn.size() - nLocalElem; - const auto totalSize = std::accumulate(sizes.begin(), sizes.end(), 0ul); - if (totalSize == 0) continue; + /*--- Sizes and offsets of the elements of each rank, which are written as a contiguous range. ---*/ - if (rank != MASTER_NODE) { - SendChunked(marker.conn.data(), localSize * sizeof(unsigned long), MASTER_NODE, 2); - continue; - } + vector elemPerRank(size), entriesPerRank(size); + SU2_MPI::Allgather(&nLocalElem, 1, MPI_UNSIGNED_LONG, elemPerRank.data(), 1, MPI_UNSIGNED_LONG, SU2_MPI::GetComm()); + SU2_MPI::Allgather(&nLocalEntries, 1, MPI_UNSIGNED_LONG, entriesPerRank.data(), 1, MPI_UNSIGNED_LONG, + SU2_MPI::GetComm()); - vector conn(totalSize); - std::copy(marker.conn.begin(), marker.conn.end(), conn.begin()); - auto offset = localSize; - for (int i = 0; i < size; ++i) { - if (i == MASTER_NODE) continue; - RecvChunked(conn.data() + offset, sizes[i] * sizeof(unsigned long), i, 2); - offset += sizes[i]; - } + const auto nTotElem = std::accumulate(elemPerRank.begin(), elemPerRank.end(), 0ul); + if (nTotElem == 0) continue; - /*--- Convert to the CGNS numbering: with a single element type the node ids of the elements, otherwise a MIXED - section, i.e. the CGNS element type followed by the node ids of each element, plus the start offsets. ---*/ - vector elems, mixed, startOffsets{0}; - bool singleType = true; - for (size_t pos = 0; pos < conn.size();) { - const auto type = static_cast(conn[pos]); - const auto nNodes = nPointsOfElementType(type); - singleType &= (type == conn[0]); - mixed.push_back(GetCGNSType(type)); - for (unsigned short iNode = 1; iNode <= nNodes; ++iNode) { - elems.push_back(static_cast(conn[pos + iNode])); - mixed.push_back(static_cast(conn[pos + iNode])); - } - startOffsets.push_back(static_cast(mixed.size())); - pos += nNodes + 1; - } - const auto nElem = static_cast(startOffsets.size() - 1); + auto elemOffset = std::accumulate(elemPerRank.begin(), elemPerRank.begin() + rank, 0ul); + auto entryOffset = std::accumulate(entriesPerRank.begin(), entriesPerRank.begin() + rank, 0ul); + + /*--- A marker with a single element type is written as a section of that type, one with several types + (e.g. triangles and quadrilaterals) as a MIXED section. ---*/ + + unsigned long globalTypesMask = 0; + SU2_MPI::Allreduce(&typesMask, &globalTypesMask, 1, MPI_UNSIGNED_LONG, MPI_BOR, SU2_MPI::GetComm()); + const bool singleType = (globalTypesMask & (globalTypesMask - 1)) == 0; const string name = marker.name.substr(0, 32); - cgsize_t range[2] = {cumulative + 1, cumulative + nElem}; + const cgsize_t range[2] = {cumulative + 1, cumulative + static_cast(nTotElem)}; + const cgsize_t first = range[0] + static_cast(elemOffset); + const cgsize_t last = first + static_cast(nLocalElem) - 1; int section; + if (singleType) { - CallCGNS(cg_section_write(cgnsFileID, cgnsBase, cgnsZone, name.c_str(), GetCGNSType(conn[0]), range[0], - range[1], 0, elems.data(), §ion)); + /*--- Only the ids of the nodes are stored, the type is that of the section. ---*/ + + vector elems; + elems.reserve(nLocalEntries); + for (size_t pos = 0; pos < marker.conn.size();) { + const auto nNodes = nPointsOfElementType(static_cast(marker.conn[pos])); + for (unsigned short iNode = 1; iNode <= nNodes; ++iNode) + elems.push_back(static_cast(marker.conn[pos + iNode])); + pos += nNodes + 1; + } + + unsigned short type = 0; + while ((globalTypesMask >> type) != 1) type++; + + CallCGNS(SectionWrite(name, GetCGNSType(type), range[0], range[1], §ion)); + CallCGNS(ElementsWriteData(section, first, last, nLocalElem > 0 ? elems.data() : nullptr)); + } else { + /*--- The CGNS element type of each element is stored before the ids of its nodes, and the start offset of + each element in the connectivity array is stored in a second array. ---*/ + + const auto nTotEntries = std::accumulate(entriesPerRank.begin(), entriesPerRank.end(), 0ul) + nTotElem; + + vector elems, offsets{static_cast(entryOffset + elemOffset)}; + elems.reserve(marker.conn.size()); + for (size_t pos = 0; pos < marker.conn.size();) { + const auto type = static_cast(marker.conn[pos]); + const auto nNodes = nPointsOfElementType(type); + elems.push_back(GetCGNSType(type)); + for (unsigned short iNode = 1; iNode <= nNodes; ++iNode) + elems.push_back(static_cast(marker.conn[pos + iNode])); + offsets.push_back(offsets.back() + nNodes + 1); + pos += nNodes + 1; + } + +#ifdef HAVE_MPI + CallCGNS(cgp_poly_section_write(cgnsFileID, cgnsBase, cgnsZone, name.c_str(), MIXED, range[0], range[1], + static_cast(nTotEntries), 0, §ion)); + CallCGNS(cgp_poly_elements_write_data(cgnsFileID, cgnsBase, cgnsZone, section, first, last, + nLocalElem > 0 ? elems.data() : nullptr, + nLocalElem > 0 ? offsets.data() : nullptr)); +#else CallCGNS(cg_poly_section_write(cgnsFileID, cgnsBase, cgnsZone, name.c_str(), MIXED, range[0], range[1], 0, - mixed.data(), startOffsets.data(), §ion)); + elems.data(), offsets.data(), §ion)); +#endif } - cumulative += nElem; + cumulative += static_cast(nTotElem); + + /*--- The BC points to the boundary elements and takes its type from a family with the name of the marker. + These are metadata nodes, written by all ranks. ---*/ - /*--- The BC points to the boundary elements and takes its type from a family with the name of the marker. ---*/ int bc, family, familyBC; CallCGNS(cg_boco_write(cgnsFileID, cgnsBase, cgnsZone, name.c_str(), FamilySpecified, PointRange, 2, range, &bc)); CallCGNS(cg_boco_gridlocation_write(cgnsFileID, cgnsBase, cgnsZone, bc, nDim == 3 ? FaceCenter : EdgeCenter)); @@ -308,55 +355,38 @@ void CCGNSFileWriter::WriteFieldOfType(int iField, const string& FieldName, Data /*--- Check if field is coordinate. ---*/ const bool isCoord = iField < nDim; - /*--- Create send buffer. ---*/ - vector sendBufferField(nLocalPoints); + /*--- Each rank writes the points it holds, which are a contiguous range of the points of the file. ---*/ + vector buffer(nLocalPoints); for (unsigned long iPoint = 0; iPoint < nLocalPoints; iPoint++) { - sendBufferField[iPoint] = static_cast(dataSorter->GetData(iField, iPoint)); + buffer[iPoint] = static_cast(dataSorter->GetData(iField, iPoint)); } - if (rank != MASTER_NODE) { - SendChunked(sendBufferField.data(), nLocalPoints * sizeof(T), MASTER_NODE, 0); - return; - } + cgsize_t nodeBegin = static_cast(dataSorter->GetnPointCumulative(rank) + 1); + cgsize_t nodeEnd = static_cast(dataSorter->GetnPointCumulative(rank + 1)); - vector recvBufferField; + /*--- A rank without points takes part in the collective call but writes nothing. ---*/ + const T* data = nLocalPoints > 0 ? buffer.data() : nullptr; - /*--- Coordinate vector is written in blocks, one for each process. ---*/ - cgsize_t nodeBegin = 1; - auto nodeEnd = static_cast(nLocalPoints); - if (nLocalPoints > 0) { - if (isCoord) { - int CoordinateNumber; - CallCGNS(cg_coord_partial_write(cgnsFileID, cgnsBase, cgnsZone, dataType, FieldName.c_str(), &nodeBegin, &nodeEnd, - sendBufferField.data(), &CoordinateNumber)); - } else { - int fieldNumber; - CallCGNS(cg_field_partial_write(cgnsFileID, cgnsBase, cgnsZone, cgnsFields, dataType, FieldName.c_str(), &nodeBegin, - &nodeEnd, sendBufferField.data(), &fieldNumber)); - } - } - - for (int i = 0; i < size; ++i) { - if (i == MASTER_NODE) continue; - /*--- In CGNS numbering starts form 1 and ranges are inclusive ---*/ - nodeBegin = static_cast(dataSorter->GetnPointCumulative(i) + 1); - nodeEnd = static_cast(dataSorter->GetnPointCumulative(i + 1)); - - const auto recvSize = static_cast(nodeEnd - nodeBegin + 1); - recvBufferField.resize(recvSize); - - RecvChunked(recvBufferField.data(), recvSize * sizeof(T), i, 0); - if (recvSize == 0) continue; - if (isCoord) { - int CoordinateNumber; - CallCGNS(cg_coord_partial_write(cgnsFileID, cgnsBase, cgnsZone, dataType, FieldName.c_str(), &nodeBegin, &nodeEnd, - recvBufferField.data(), &CoordinateNumber)); - } else { - int fieldNumber; - CallCGNS(cg_field_partial_write(cgnsFileID, cgnsBase, cgnsZone, cgnsFields, dataType, FieldName.c_str(), - &nodeBegin, &nodeEnd, recvBufferField.data(), &fieldNumber)); - } + if (isCoord) { + int coordinateNumber; +#ifdef HAVE_MPI + CallCGNS(cgp_coord_write(cgnsFileID, cgnsBase, cgnsZone, dataType, FieldName.c_str(), &coordinateNumber)); + CallCGNS(cgp_coord_write_data(cgnsFileID, cgnsBase, cgnsZone, coordinateNumber, &nodeBegin, &nodeEnd, data)); +#else + CallCGNS(cg_coord_partial_write(cgnsFileID, cgnsBase, cgnsZone, dataType, FieldName.c_str(), &nodeBegin, &nodeEnd, + data, &coordinateNumber)); +#endif + } else { + int fieldNumber; +#ifdef HAVE_MPI + CallCGNS(cgp_field_write(cgnsFileID, cgnsBase, cgnsZone, cgnsFields, dataType, FieldName.c_str(), &fieldNumber)); + CallCGNS( + cgp_field_write_data(cgnsFileID, cgnsBase, cgnsZone, cgnsFields, fieldNumber, &nodeBegin, &nodeEnd, data)); +#else + CallCGNS(cg_field_partial_write(cgnsFileID, cgnsBase, cgnsZone, cgnsFields, dataType, FieldName.c_str(), + &nodeBegin, &nodeEnd, data, &fieldNumber)); +#endif } } @@ -377,87 +407,64 @@ void CCGNSFileWriter::WriteConnectivity(GEO_TYPE type, const string& SectionName auto sectionBegin = [&](cgsize_t iSec) { return cumulative + 1 + iSec * maxElemSection; }; auto sectionEnd = [&](cgsize_t iSec) { return cumulative + std::min(nTotElemCG, (iSec + 1) * maxElemSection); }; + /*--- The sections are metadata, all ranks create them with the same arguments. ---*/ vector cgnsSections(nSections); - if (rank == MASTER_NODE) { - for (cgsize_t iSec = 0; iSec < nSections; ++iSec) { - const string name = nSections == 1 ? SectionName : SectionName + "_" + std::to_string(iSec + 1); - CallCGNS(cg_section_partial_write(cgnsFileID, cgnsBase, cgnsZone, name.c_str(), elementType, - sectionBegin(iSec), sectionEnd(iSec), 0, &cgnsSections[iSec])); - } + for (cgsize_t iSec = 0; iSec < nSections; ++iSec) { + const string name = nSections == 1 ? SectionName : SectionName + "_" + std::to_string(iSec + 1); + CallCGNS(SectionWrite(name, elementType, sectionBegin(iSec), sectionEnd(iSec), &cgnsSections[iSec])); } - /*--- Write the connectivity of the elements [first, last], which may span more than one section. ---*/ - auto writeBlock = [&](cgsize_t first, cgsize_t last, const cgsize_t* conn) { - for (cgsize_t iSec = 0; iSec < nSections; ++iSec) { - const auto lo = std::max(first, sectionBegin(iSec)); - const auto hi = std::min(last, sectionEnd(iSec)); - if (lo > hi) continue; - CallCGNS(cg_elements_partial_write(cgnsFileID, cgnsBase, cgnsZone, cgnsSections[iSec], lo, hi, - conn + (lo - first) * nPointsElem)); - } - }; - - /*--- Retrieve element distribution among processes. ---*/ + /*--- Retrieve element distribution among processes, the elements of a rank are a contiguous range. ---*/ const auto nLocalElem = dataSorter->GetnElem(type); vector distElem(size); - SU2_MPI::Allgather(&nLocalElem, 1, MPI_UNSIGNED_LONG, distElem.data(), 1, MPI_UNSIGNED_LONG, SU2_MPI::GetComm()); cgsize_t firstElem = cumulative + 1; - cgsize_t endElem = cumulative + static_cast(distElem[rank]); + for (int i = 0; i < rank; ++i) firstElem += static_cast(distElem[i]); + const cgsize_t endElem = firstElem + static_cast(nLocalElem) - 1; - /*--- Connectivity is stored in send buffer. ---*/ - sendBufferConnectivity.resize(nLocalElem * nPointsElem); + /*--- Store the connectivity of this rank. ---*/ + vector connectivity(nLocalElem * nPointsElem); for (unsigned long iElem = 0; iElem < nLocalElem; iElem++) { for (unsigned long iPoint = 0; iPoint < nPointsElem; iPoint++) { - sendBufferConnectivity[iPoint + nPointsElem * iElem] = + connectivity[iPoint + nPointsElem * iElem] = static_cast(dataSorter->GetElemConnectivity(type, iElem, iPoint)); } } - if (rank != MASTER_NODE) { - SendChunked(sendBufferConnectivity.data(), sendBufferConnectivity.size() * sizeof(cgsize_t), MASTER_NODE, 1); - return; + /*--- Write the elements of this rank, which may span more than one section. A rank without elements takes + part in the collective calls but writes nothing. ---*/ + for (cgsize_t iSec = 0; iSec < nSections; ++iSec) { + const auto lo = std::max(firstElem, sectionBegin(iSec)); + const auto hi = std::min(endElem, sectionEnd(iSec)); + const bool empty = (nLocalElem == 0) || (lo > hi); + CallCGNS(ElementsWriteData(cgnsSections[iSec], lo, hi, empty ? nullptr : &connectivity[(lo - firstElem) * nPointsElem])); } - /*--- Connectivity vector is written in blocks, one for each process. ---*/ - if (nLocalElem > 0) writeBlock(firstElem, endElem, sendBufferConnectivity.data()); - - for (int i = 0; i < size; ++i) { - if (i == MASTER_NODE) continue; - /*--- In CGNS numbering starts form 1 and ranges are inclusive ---*/ - firstElem = endElem + 1; - endElem += static_cast(distElem[i]); - const auto recvSize = static_cast(endElem - firstElem + 1) * nPointsElem; - recvBufferConnectivity.resize(recvSize); - - RecvChunked(recvBufferConnectivity.data(), recvBufferConnectivity.size() * sizeof(cgsize_t), i, 1); - - if (!recvBufferConnectivity.empty()) writeBlock(firstElem, endElem, recvBufferConnectivity.data()); - } - cumulative += static_cast(nTotElem); + cumulative += nTotElemCG; } -void CCGNSFileWriter::SendChunked(const void* buf, size_t nBytes, int dest, int tag) { - const auto* bytes = static_cast(buf); - for (size_t offset = 0; offset < nBytes; offset += maxChunkBytes) { - const auto count = static_cast(std::min(maxChunkBytes, nBytes - offset)); - SU2_MPI::Send(bytes + offset, count, MPI_CHAR, dest, tag, SU2_MPI::GetComm()); - } +int CCGNSFileWriter::SectionWrite(const string& name, ElementType_t type, cgsize_t start, cgsize_t end, int* section) { +#ifdef HAVE_MPI + return cgp_section_write(cgnsFileID, cgnsBase, cgnsZone, name.c_str(), type, start, end, 0, section); +#else + return cg_section_partial_write(cgnsFileID, cgnsBase, cgnsZone, name.c_str(), type, start, end, 0, section); +#endif } -void CCGNSFileWriter::RecvChunked(void* buf, size_t nBytes, int source, int tag) { - auto* bytes = static_cast(buf); - for (size_t offset = 0; offset < nBytes; offset += maxChunkBytes) { - const auto count = static_cast(std::min(maxChunkBytes, nBytes - offset)); - SU2_MPI::Recv(bytes + offset, count, MPI_CHAR, source, tag, SU2_MPI::GetComm(), MPI_STATUS_IGNORE); - } +int CCGNSFileWriter::ElementsWriteData(int section, cgsize_t start, cgsize_t end, const cgsize_t* elements) { +#ifdef HAVE_MPI + return cgp_elements_write_data(cgnsFileID, cgnsBase, cgnsZone, section, start, end, elements); +#else + if (elements == nullptr) return CG_OK; + return cg_elements_partial_write(cgnsFileID, cgnsBase, cgnsZone, section, start, end, elements); +#endif } void CCGNSFileWriter::InitializeFields() { /*--- Create "Fields" node to store solution. ---*/ - if (rank == MASTER_NODE) CallCGNS(cg_sol_write(cgnsFileID, cgnsBase, cgnsZone, "Fields", Vertex, &cgnsFields)); + CallCGNS(cg_sol_write(cgnsFileID, cgnsBase, cgnsZone, "Fields", Vertex, &cgnsFields)); } #endif // HAVE_CGNS From 1e397c1703fe3f50d6abb6ef861eb0b443055ca0 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Mon, 21 Sep 2026 09:03:45 +0200 Subject: [PATCH 10/35] Write the ASCII output files in parallel PARAVIEW_ASCII, RESTART_ASCII and TECPLOT_ASCII wrote their files by letting the ranks take turns: each rank opened the file, appended its part and closed it, with a barrier after every rank and for every block of data (points, connectivity, cell types, each field). Only one rank wrote at any moment, and the number of barriers grows with the number of ranks, which makes these formats very slow at scale. Each rank now formats its own data into a string, and all ranks write their strings at the same time with the collective MPI-IO call of CFileWriter, at the offset that follows the text of the ranks before it (new helper CFileWriter::WriteMPIStringAll). The headers are still written by the master node only. The files are byte-identical to those of the previous implementation. Co-Authored-By: Claude Opus 5 (1M context) --- .../include/output/filewriter/CFileWriter.hpp | 7 + .../output/filewriter/CParallelFileWriter.cpp | 17 + .../output/filewriter/CParaviewFileWriter.cpp | 316 ++++-------------- .../src/output/filewriter/CSU2FileWriter.cpp | 65 ++-- .../output/filewriter/CTecplotFileWriter.cpp | 177 ++++------ 5 files changed, 170 insertions(+), 412 deletions(-) diff --git a/SU2_CFD/include/output/filewriter/CFileWriter.hpp b/SU2_CFD/include/output/filewriter/CFileWriter.hpp index 655ad787ffe3..b2cf55197571 100644 --- a/SU2_CFD/include/output/filewriter/CFileWriter.hpp +++ b/SU2_CFD/include/output/filewriter/CFileWriter.hpp @@ -168,6 +168,13 @@ class CFileWriter{ */ bool WriteMPIString(const std::string& str, unsigned short processor); + /*! + * \brief Write a string of each rank to the file, one after the other in rank order. + * \param[in] str - The string of this rank. + * \return TRUE if the writing was successful. + */ + bool WriteMPIStringAll(const std::string& str); + /*! * \brief Open a file to write using MPI I/O. Already existing file is deleted. * \param[in] val_filename - The name of the file diff --git a/SU2_CFD/src/output/filewriter/CParallelFileWriter.cpp b/SU2_CFD/src/output/filewriter/CParallelFileWriter.cpp index 3794d50dd112..88a7f09acf6b 100644 --- a/SU2_CFD/src/output/filewriter/CParallelFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CParallelFileWriter.cpp @@ -25,7 +25,9 @@ * License along with SU2. If not, see . */ +#include #include +#include #include "../../../include/output/filewriter/CFileWriter.hpp" @@ -254,6 +256,21 @@ bool CFileWriter::OpenMPIFile(string val_filename){ return true; } +bool CFileWriter::WriteMPIStringAll(const string &str){ + + /*--- Each rank writes its own text at the position that follows the text of the ranks before it. ---*/ + + const unsigned long sizeInBytes = str.size(); + vector sizes(size, sizeInBytes); + + SU2_MPI::Allgather(&sizeInBytes, 1, MPI_UNSIGNED_LONG, sizes.data(), 1, MPI_UNSIGNED_LONG, SU2_MPI::GetComm()); + + const auto offsetInBytes = std::accumulate(sizes.begin(), sizes.begin() + rank, 0ul); + const auto totalSizeInBytes = std::accumulate(sizes.begin(), sizes.end(), 0ul); + + return WriteMPIBinaryDataAll(str.data(), sizeInBytes, totalSizeInBytes, offsetInBytes); +} + bool CFileWriter::CloseMPIFile(){ #ifdef HAVE_MPI diff --git a/SU2_CFD/src/output/filewriter/CParaviewFileWriter.cpp b/SU2_CFD/src/output/filewriter/CParaviewFileWriter.cpp index 48f67b631637..64c3ab455541 100644 --- a/SU2_CFD/src/output/filewriter/CParaviewFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CParaviewFileWriter.cpp @@ -37,211 +37,73 @@ CParaviewFileWriter::~CParaviewFileWriter()= default; void CParaviewFileWriter::WriteData(string val_filename){ - /*--- We append the pre-defined suffix (extension) to the filename (prefix) ---*/ - val_filename.append(fileExt); - if (!dataSorter->GetConnectivitySorted()){ SU2_MPI::Error("Connectivity must be sorted.", CURRENT_FUNCTION); } - unsigned short iDim = 0, nDim = dataSorter->GetnDim(); - - unsigned long iPoint, iElem; - - unsigned long nGlobal_Elem_Storage; - - ofstream Paraview_File; - - int iProcessor; - + const unsigned short nDim = dataSorter->GetnDim(); const vector fieldNames = dataSorter->GetFieldNames(); - /*--- Set a timer for the file writing. ---*/ - - startTime = SU2_MPI::Wtime(); - - /*--- Open Paraview ASCII file and write the header. ---*/ - - if (rank == MASTER_NODE) { + /*--- Each rank formats the data of its own points and elements into a string, and all ranks then write + their strings to the file at the same time, one after the other in rank order. ---*/ - Paraview_File.open(val_filename.c_str(), ios::out); - Paraview_File.precision(6); - Paraview_File << "# vtk DataFile Version 3.0\n"; - Paraview_File << "vtk output\n"; - Paraview_File << "ASCII\n"; - Paraview_File << "DATASET UNSTRUCTURED_GRID\n"; + ostringstream data; + data << scientific; - /*--- Write the header ---*/ - Paraview_File << "POINTS "<< dataSorter->GetnPointsGlobal() <<" double\n"; + auto resetData = [&]() { + data.str(""); + data.clear(); + data << scientific; + }; - } - - Paraview_File.close(); - -#ifdef HAVE_MPI - SU2_MPI::Barrier(SU2_MPI::GetComm()); -#endif + OpenMPIFile(val_filename); - /*--- Each processor opens the file. ---*/ + /*--- Write the header. ---*/ - Paraview_File.open(val_filename.c_str(), ios::out | ios::app); + WriteMPIString("# vtk DataFile Version 3.0\nvtk output\nASCII\nDATASET UNSTRUCTURED_GRID\n", MASTER_NODE); + WriteMPIString("POINTS " + to_string(dataSorter->GetnPointsGlobal()) + " double\n", MASTER_NODE); /*--- Write surface and volumetric point coordinates. ---*/ - for (iProcessor = 0; iProcessor < size; iProcessor++) { - if (rank == iProcessor) { - - /*--- Write the node data from this proc ---*/ - - - for (iPoint = 0; iPoint < dataSorter->GetnPoints(); iPoint++) { - for (iDim = 0; iDim < nDim; iDim++) - Paraview_File << scientific << dataSorter->GetData(iDim, iPoint) << "\t"; - if (nDim == 2) Paraview_File << scientific << "0.0" << "\t"; - } - } - - Paraview_File.flush(); -#ifdef HAVE_MPI - SU2_MPI::Barrier(SU2_MPI::GetComm()); -#endif + for (unsigned long iPoint = 0; iPoint < dataSorter->GetnPoints(); iPoint++) { + for (unsigned short iDim = 0; iDim < nDim; iDim++) data << dataSorter->GetData(iDim, iPoint) << "\t"; + if (nDim == 2) data << "0.0" << "\t"; } + WriteMPIStringAll(data.str()); - /*--- Reduce the total number of each element. ---*/ - - unsigned long nParallel_Line = dataSorter->GetnElem(LINE), - nParallel_Tria = dataSorter->GetnElem(TRIANGLE), - nParallel_Quad = dataSorter->GetnElem(QUADRILATERAL), - nParallel_Tetr = dataSorter->GetnElem(TETRAHEDRON), - nParallel_Hexa = dataSorter->GetnElem(HEXAHEDRON), - nParallel_Pris = dataSorter->GetnElem(PRISM), - nParallel_Pyra = dataSorter->GetnElem(PYRAMID); - - if (rank == MASTER_NODE) { - - /*--- Write the header ---*/ - nGlobal_Elem_Storage = dataSorter->GetnElemGlobal() + dataSorter->GetnConnGlobal(); - - Paraview_File << "\nCELLS " << dataSorter->GetnElemGlobal() << "\t" << nGlobal_Elem_Storage << "\n"; - - } - - Paraview_File.flush(); -#ifdef HAVE_MPI - SU2_MPI::Barrier(SU2_MPI::GetComm()); -#endif - - /*--- Write connectivity data. ---*/ - - for (iProcessor = 0; iProcessor < size; iProcessor++) { - if (rank == iProcessor) { - - - for (iElem = 0; iElem < nParallel_Line; iElem++) { - Paraview_File << N_POINTS_LINE << "\t"; - Paraview_File << dataSorter->GetElemConnectivity(LINE, iElem, 0)-1 << "\t"; - Paraview_File << dataSorter->GetElemConnectivity(LINE, iElem, 1)-1 << "\t"; - } - - for (iElem = 0; iElem < nParallel_Tria; iElem++) { - Paraview_File << N_POINTS_TRIANGLE << "\t"; - Paraview_File << dataSorter->GetElemConnectivity(TRIANGLE, iElem, 0)-1 << "\t"; - Paraview_File << dataSorter->GetElemConnectivity(TRIANGLE, iElem, 1)-1 << "\t"; - Paraview_File << dataSorter->GetElemConnectivity(TRIANGLE, iElem, 2)-1 << "\t"; - } - - for (iElem = 0; iElem < nParallel_Quad; iElem++) { - Paraview_File << N_POINTS_QUADRILATERAL << "\t"; - Paraview_File << dataSorter->GetElemConnectivity(QUADRILATERAL, iElem, 0)-1 << "\t"; - Paraview_File << dataSorter->GetElemConnectivity(QUADRILATERAL, iElem, 1)-1 << "\t"; - Paraview_File << dataSorter->GetElemConnectivity(QUADRILATERAL, iElem, 2)-1 << "\t"; - Paraview_File << dataSorter->GetElemConnectivity(QUADRILATERAL, iElem, 3)-1 << "\t"; - } - + /*--- Write the connectivity, the number of nodes of each element is written before its nodes. ---*/ - for (iElem = 0; iElem < nParallel_Tetr; iElem++) { - Paraview_File << N_POINTS_TETRAHEDRON << "\t"; - Paraview_File << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 0)-1 << "\t" - << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 1)-1 << "\t"; - Paraview_File << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 2)-1 << "\t" - << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 3)-1 << "\t"; - } + const unsigned long nGlobal_Elem_Storage = dataSorter->GetnElemGlobal() + dataSorter->GetnConnGlobal(); - for (iElem = 0; iElem < nParallel_Hexa; iElem++) { - Paraview_File << N_POINTS_HEXAHEDRON << "\t"; - Paraview_File << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 0)-1 << "\t" - << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 1)-1 << "\t"; - Paraview_File << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 2)-1 << "\t" - << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 3)-1 << "\t"; - Paraview_File << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 4)-1 << "\t" - << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 5)-1 << "\t"; - Paraview_File << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 6)-1 << "\t" - << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 7)-1 << "\t"; - } + WriteMPIString("\nCELLS " + to_string(dataSorter->GetnElemGlobal()) + "\t" + to_string(nGlobal_Elem_Storage) + "\n", + MASTER_NODE); - for (iElem = 0; iElem < nParallel_Pris; iElem++) { - Paraview_File << N_POINTS_PRISM << "\t"; - Paraview_File << dataSorter->GetElemConnectivity(PRISM, iElem, 0)-1 << "\t" - << dataSorter->GetElemConnectivity(PRISM, iElem, 1)-1 << "\t"; - Paraview_File << dataSorter->GetElemConnectivity(PRISM, iElem, 2)-1 << "\t" - << dataSorter->GetElemConnectivity(PRISM, iElem, 3)-1 << "\t"; - Paraview_File << dataSorter->GetElemConnectivity(PRISM, iElem, 4)-1 << "\t" - << dataSorter->GetElemConnectivity(PRISM, iElem, 5)-1 << "\t"; - } - - for (iElem = 0; iElem < nParallel_Pyra; iElem++) { - Paraview_File << N_POINTS_PYRAMID << "\t"; - Paraview_File << dataSorter->GetElemConnectivity(PYRAMID, iElem, 0)-1 << "\t" - << dataSorter->GetElemConnectivity(PYRAMID, iElem, 1)-1 << "\t"; - Paraview_File << dataSorter->GetElemConnectivity(PYRAMID, iElem, 2)-1 << "\t" - << dataSorter->GetElemConnectivity(PYRAMID, iElem, 3)-1 << "\t"; - Paraview_File << dataSorter->GetElemConnectivity(PYRAMID, iElem, 4)-1 << "\t"; - } + resetData(); - } Paraview_File.flush(); -#ifdef HAVE_MPI - SU2_MPI::Barrier(SU2_MPI::GetComm()); -#endif + for (auto type : {LINE, TRIANGLE, QUADRILATERAL, TETRAHEDRON, HEXAHEDRON, PRISM, PYRAMID}) { + const auto nPoints = nPointsOfElementType(type); + for (unsigned long iElem = 0; iElem < dataSorter->GetnElem(type); iElem++) { + data << nPoints << "\t"; + for (unsigned short iNode = 0; iNode < nPoints; iNode++) + data << dataSorter->GetElemConnectivity(type, iElem, iNode) - 1 << "\t"; + } } + WriteMPIStringAll(data.str()); - if (rank == MASTER_NODE) { + /*--- Write the type of each element. ---*/ - /*--- Write the header ---*/ - Paraview_File << "\nCELL_TYPES " << dataSorter->GetnElemGlobal() << "\n"; + WriteMPIString("\nCELL_TYPES " + to_string(dataSorter->GetnElemGlobal()) + "\n", MASTER_NODE); - } + resetData(); - Paraview_File.flush(); -#ifdef HAVE_MPI - SU2_MPI::Barrier(SU2_MPI::GetComm()); -#endif - - for (iProcessor = 0; iProcessor < size; iProcessor++) { - if (rank == iProcessor) { - for (iElem = 0; iElem < nParallel_Line; iElem++) Paraview_File << "3\t"; - for (iElem = 0; iElem < nParallel_Tria; iElem++) Paraview_File << "5\t"; - for (iElem = 0; iElem < nParallel_Quad; iElem++) Paraview_File << "9\t"; - for (iElem = 0; iElem < nParallel_Tetr; iElem++) Paraview_File << "10\t"; - for (iElem = 0; iElem < nParallel_Hexa; iElem++) Paraview_File << "12\t"; - for (iElem = 0; iElem < nParallel_Pris; iElem++) Paraview_File << "13\t"; - for (iElem = 0; iElem < nParallel_Pyra; iElem++) Paraview_File << "14\t"; - } - Paraview_File.flush(); -#ifdef HAVE_MPI - SU2_MPI::Barrier(SU2_MPI::GetComm()); -#endif + for (auto type : {LINE, TRIANGLE, QUADRILATERAL, TETRAHEDRON, HEXAHEDRON, PRISM, PYRAMID}) { + for (unsigned long iElem = 0; iElem < dataSorter->GetnElem(type); iElem++) data << type << "\t"; } + WriteMPIStringAll(data.str()); - if (rank == MASTER_NODE) { - /*--- Write the header ---*/ - Paraview_File << "\nPOINT_DATA "<< dataSorter->GetnPointsGlobal() <<"\n"; - - } + /*--- Write the fields. ---*/ - Paraview_File.flush(); -#ifdef HAVE_MPI - SU2_MPI::Barrier(SU2_MPI::GetComm()); -#endif + WriteMPIString("\nPOINT_DATA " + to_string(dataSorter->GetnPointsGlobal()) + "\n", MASTER_NODE); unsigned short varStart = 2; if (nDim == 3) varStart++; @@ -263,109 +125,45 @@ void CParaviewFileWriter::WriteData(string val_filename){ } found = fieldNames[iField].find("_y"); if (found!=string::npos) { + /*--- We have found a vector, so skip the Y component. ---*/ output_variable = false; - //skip - Paraview_File.flush(); -#ifdef HAVE_MPI - SU2_MPI::Barrier(SU2_MPI::GetComm()); -#endif VarCounter++; } found = fieldNames[iField].find("_z"); if (found!=string::npos) { + /*--- We have found a vector, so skip the Z component. ---*/ output_variable = false; - //skip - Paraview_File.flush(); -#ifdef HAVE_MPI - SU2_MPI::Barrier(SU2_MPI::GetComm()); -#endif VarCounter++; } - if (output_variable && isVector) { - - fieldname.erase(fieldname.end()-2,fieldname.end()); - - if (rank == MASTER_NODE) { - Paraview_File << "\nVECTORS " << fieldname << " double\n"; - } + if (!output_variable) continue; - Paraview_File.flush(); -#ifdef HAVE_MPI - SU2_MPI::Barrier(SU2_MPI::GetComm()); -#endif + resetData(); - /*--- Write surface and volumetric point coordinates. ---*/ + if (isVector) { - for (iProcessor = 0; iProcessor < size; iProcessor++) { - if (rank == iProcessor) { - - /*--- Write the node data from this proc ---*/ - - for (iPoint = 0; iPoint < dataSorter->GetnPoints(); iPoint++) { - Paraview_File << scientific << dataSorter->GetData(VarCounter+0, iPoint) << "\t" << dataSorter->GetData(VarCounter+1, iPoint) << "\t"; - if (nDim == 3) Paraview_File << scientific << dataSorter->GetData(VarCounter+2, iPoint) << "\t"; - if (nDim == 2) Paraview_File << scientific << "0.0" << "\t"; - } - } - - Paraview_File.flush(); -#ifdef HAVE_MPI - SU2_MPI::Barrier(SU2_MPI::GetComm()); -#endif - } - - VarCounter++; - - } else if (output_variable) { + fieldname.erase(fieldname.end()-2,fieldname.end()); - if (rank == MASTER_NODE) { + WriteMPIString("\nVECTORS " + fieldname + " double\n", MASTER_NODE); - Paraview_File << "\nSCALARS " << fieldname << " double 1\n"; - Paraview_File << "LOOKUP_TABLE default\n"; + for (unsigned long iPoint = 0; iPoint < dataSorter->GetnPoints(); iPoint++) { + data << dataSorter->GetData(VarCounter+0, iPoint) << "\t" << dataSorter->GetData(VarCounter+1, iPoint) << "\t"; + if (nDim == 3) data << dataSorter->GetData(VarCounter+2, iPoint) << "\t"; + if (nDim == 2) data << "0.0" << "\t"; } - Paraview_File.flush(); -#ifdef HAVE_MPI - SU2_MPI::Barrier(SU2_MPI::GetComm()); -#endif - - /*--- Write surface and volumetric point coordinates. ---*/ + } else { - for (iProcessor = 0; iProcessor < size; iProcessor++) { - if (rank == iProcessor) { + WriteMPIString("\nSCALARS " + fieldname + " double 1\nLOOKUP_TABLE default\n", MASTER_NODE); - /*--- Write the node data from this proc ---*/ - - for (iPoint = 0; iPoint < dataSorter->GetnPoints(); iPoint++) { - Paraview_File << scientific << dataSorter->GetData(VarCounter, iPoint) << "\t"; - } - - } - Paraview_File.flush(); -#ifdef HAVE_MPI - SU2_MPI::Barrier(SU2_MPI::GetComm()); -#endif + for (unsigned long iPoint = 0; iPoint < dataSorter->GetnPoints(); iPoint++) { + data << dataSorter->GetData(VarCounter, iPoint) << "\t"; } - - VarCounter++; } + WriteMPIStringAll(data.str()); + VarCounter++; } - Paraview_File.close(); - - - /*--- Compute and store the write time. ---*/ - - stopTime = SU2_MPI::Wtime(); - - usedTime = stopTime-startTime; - - fileSize = DetermineFilesize(val_filename); - - /*--- Compute and store the bandwidth ---*/ - - bandwidth = fileSize/(1.0e6)/usedTime; + CloseMPIFile(); } - diff --git a/SU2_CFD/src/output/filewriter/CSU2FileWriter.cpp b/SU2_CFD/src/output/filewriter/CSU2FileWriter.cpp index e0e940b83284..035220db9368 100644 --- a/SU2_CFD/src/output/filewriter/CSU2FileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CSU2FileWriter.cpp @@ -34,65 +34,38 @@ CSU2FileWriter::CSU2FileWriter(CParallelDataSorter *valDataSorter) : void CSU2FileWriter::WriteData(string val_filename){ - ofstream restart_file; const vector fieldNames = dataSorter->GetRequiredFieldNames(); - /*--- We append the pre-defined suffix (extension) to the filename (prefix) ---*/ - val_filename.append(fileExt); + OpenMPIFile(val_filename); - /*--- Set a timer for the file writing. ---*/ + /*--- Write the header. ---*/ - startTime = SU2_MPI::Wtime(); + string header = "\"PointID\""; + for (auto& field : fieldNames) header += ",\"" + field + "\""; + header += "\n"; - /*--- Only the FIRST node writes the header (it does not matter if that is the master). ---*/ + WriteMPIString(header, MASTER_NODE); - if (rank == 0) { - restart_file.open(val_filename); - restart_file << "\"PointID\""; - for (auto& field : fieldNames) restart_file << ",\"" << field << "\""; - restart_file << "\n"; - restart_file.close(); - } - - /*--- Serialize the writes to the restart file. ---*/ - - for (int iProcessor = 0; iProcessor < size; iProcessor++) { - if (rank == iProcessor) { - restart_file.open(val_filename, ios::app); - restart_file.precision(15); - - for (auto iPoint = 0ul; iPoint < dataSorter->GetnPoints(); iPoint++) { - - /*--- Write global index of the current point. ---*/ + /*--- Each rank formats the data of its own points into a string, and all ranks then write their strings + to the file at the same time, one after the other in rank order. ---*/ - restart_file << dataSorter->GetGlobalIndex(iPoint); + ostringstream data; + data.precision(15); - /*--- Loop over the variables and write the values to file. ---*/ + for (auto iPoint = 0ul; iPoint < dataSorter->GetnPoints(); iPoint++) { - for (size_t iVar = 0; iVar < fieldNames.size(); iVar++) - restart_file << ", " << scientific << dataSorter->GetData(iVar, iPoint); - restart_file << "\n"; - } + /*--- Write global index of the current point. ---*/ - restart_file.close(); - } + data << dataSorter->GetGlobalIndex(iPoint); - /*--- Wait for iProcessor to finish and close the file. ---*/ + /*--- Loop over the variables and write the values to file. ---*/ - SU2_MPI::Barrier(SU2_MPI::GetComm()); + for (size_t iVar = 0; iVar < fieldNames.size(); iVar++) + data << ", " << scientific << dataSorter->GetData(iVar, iPoint); + data << "\n"; } - /*--- Compute and store the write time. ---*/ - - stopTime = SU2_MPI::Wtime(); - - usedTime = stopTime-startTime; - - /*--- Determine the file size ---*/ - - fileSize = DetermineFilesize(val_filename); - - /*--- Compute and store the bandwidth ---*/ + WriteMPIStringAll(data.str()); - bandwidth = fileSize/(1.0e6)/usedTime; + CloseMPIFile(); } diff --git a/SU2_CFD/src/output/filewriter/CTecplotFileWriter.cpp b/SU2_CFD/src/output/filewriter/CTecplotFileWriter.cpp index 4cfe9c8f1e41..1b4b4d48656d 100644 --- a/SU2_CFD/src/output/filewriter/CTecplotFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CTecplotFileWriter.cpp @@ -37,9 +37,6 @@ CTecplotFileWriter::~CTecplotFileWriter()= default; void CTecplotFileWriter::WriteData(string val_filename){ - /*--- We append the pre-defined suffix (extension) to the filename (prefix) ---*/ - val_filename.append(fileExt); - if (!dataSorter->GetConnectivitySorted()){ SU2_MPI::Error("Connectivity must be sorted.", CURRENT_FUNCTION); } @@ -50,16 +47,6 @@ void CTecplotFileWriter::WriteData(string val_filename){ unsigned long iPoint, iElem; - int iProcessor; - - ofstream Tecplot_File; - - fileSize = 0.0; - - /*--- Set a timer for the file writing. ---*/ - - startTime = SU2_MPI::Wtime(); - /*--- Reduce the total number of each element. ---*/ unsigned long nParallel_Line = dataSorter->GetnElem(LINE), @@ -80,150 +67,126 @@ void CTecplotFileWriter::WriteData(string val_filename){ /*--- Open Tecplot ASCII file and write the header. ---*/ - if (rank == MASTER_NODE) { - Tecplot_File.open(val_filename.c_str(), ios::out); - Tecplot_File.precision(6); - Tecplot_File << "TITLE = \"Visualization of the solution\"" << endl; + OpenMPIFile(val_filename); - Tecplot_File << "VARIABLES = "; - for (iVar = 0; iVar < fieldNames.size()-1; iVar++) { - Tecplot_File << "\"" << fieldNames[iVar] << "\","; - } - Tecplot_File << "\"" << fieldNames[fieldNames.size()-1] << "\"" << endl; + ostringstream header; + header.precision(6); + header << "TITLE = \"Visualization of the solution\"" << endl; - /*--- Write the header ---*/ + header << "VARIABLES = "; + for (iVar = 0; iVar < fieldNames.size()-1; iVar++) { + header << "\"" << fieldNames[iVar] << "\","; + } + header << "\"" << fieldNames[fieldNames.size()-1] << "\"" << endl; - Tecplot_File << "ZONE "; + header << "ZONE "; - if (timeStep > 0.0){ - Tecplot_File << "STRANDID="< 0.0){ + header << "STRANDID="<GetnDim() == 3){ + if ((nTot_Quad > 0 || nTot_Tria > 0) && (nTot_Hexa + nTot_Pris + nTot_Pyra + nTot_Tetr == 0)){ + header << ", DATAPACKING=POINT, ZONETYPE=FEQUADRILATERAL" << endl; } else { - if (nTot_Line > 0 && (nTot_Tria + nTot_Quad == 0)){ - Tecplot_File << ", DATAPACKING=POINT, ZONETYPE=FELINESEG"<< endl; - } - else{ - Tecplot_File << ", DATAPACKING=POINT, ZONETYPE=FEQUADRILATERAL"<< endl; - } + header <<", DATAPACKING=POINT, ZONETYPE=FEBRICK"<< endl; + } + } + else { + if (nTot_Line > 0 && (nTot_Tria + nTot_Quad == 0)){ + header << ", DATAPACKING=POINT, ZONETYPE=FELINESEG"<< endl; + } + else{ + header << ", DATAPACKING=POINT, ZONETYPE=FEQUADRILATERAL"<< endl; } - Tecplot_File.close(); } -#ifdef HAVE_MPI - SU2_MPI::Barrier(SU2_MPI::GetComm()); -#endif + WriteMPIString(header.str(), MASTER_NODE); - /*--- Each processor opens the file. ---*/ + /*--- Each rank formats the data of its own points and elements into a string, and all ranks then write + their strings to the file at the same time, one after the other in rank order. ---*/ - Tecplot_File.open(val_filename.c_str(), ios::out | ios::app); + ostringstream data; + data.precision(6); + data << scientific; /*--- Write surface and volumetric solution data. ---*/ - for (iProcessor = 0; iProcessor < size; iProcessor++) { - if (rank == iProcessor) { - - /*--- Write the node data from this proc ---*/ - + for (iPoint = 0; iPoint < dataSorter->GetnPoints(); iPoint++) { + for (iVar = 0; iVar < fieldNames.size(); iVar++) + data << dataSorter->GetData(iVar, iPoint) << "\t"; + data << endl; + } - for (iPoint = 0; iPoint < dataSorter->GetnPoints(); iPoint++) { - for (iVar = 0; iVar < fieldNames.size(); iVar++) - Tecplot_File << scientific << dataSorter->GetData(iVar, iPoint) << "\t"; - Tecplot_File << endl; - } - } + WriteMPIStringAll(data.str()); - Tecplot_File.flush(); -#ifdef HAVE_MPI - SU2_MPI::Barrier(SU2_MPI::GetComm()); -#endif - } + data.str(""); + data.clear(); /*--- Write connectivity data. ---*/ - for (iProcessor = 0; iProcessor < size; iProcessor++) { - if (rank == iProcessor) { + { + { for (iElem = 0; iElem < nParallel_Line; iElem++) { - Tecplot_File << dataSorter->GetElemConnectivity(LINE, iElem, 0) << "\t"; - Tecplot_File << dataSorter->GetElemConnectivity(LINE, iElem, 1)<< "\n"; + data << dataSorter->GetElemConnectivity(LINE, iElem, 0) << "\t"; + data << dataSorter->GetElemConnectivity(LINE, iElem, 1)<< "\n"; } for (iElem = 0; iElem < nParallel_Tria; iElem++) { - Tecplot_File << dataSorter->GetElemConnectivity(TRIANGLE, iElem, 0) << "\t"; - Tecplot_File << dataSorter->GetElemConnectivity(TRIANGLE, iElem, 1) << "\t"; - Tecplot_File << dataSorter->GetElemConnectivity(TRIANGLE, iElem, 2) << "\t"; - Tecplot_File << dataSorter->GetElemConnectivity(TRIANGLE, iElem, 2) << "\n"; + data << dataSorter->GetElemConnectivity(TRIANGLE, iElem, 0) << "\t"; + data << dataSorter->GetElemConnectivity(TRIANGLE, iElem, 1) << "\t"; + data << dataSorter->GetElemConnectivity(TRIANGLE, iElem, 2) << "\t"; + data << dataSorter->GetElemConnectivity(TRIANGLE, iElem, 2) << "\n"; } for (iElem = 0; iElem < nParallel_Quad; iElem++) { - Tecplot_File << dataSorter->GetElemConnectivity(QUADRILATERAL, iElem, 0) << "\t"; - Tecplot_File << dataSorter->GetElemConnectivity(QUADRILATERAL, iElem, 1) << "\t"; - Tecplot_File << dataSorter->GetElemConnectivity(QUADRILATERAL, iElem, 2) << "\t"; - Tecplot_File << dataSorter->GetElemConnectivity(QUADRILATERAL, iElem, 3) << "\n"; + data << dataSorter->GetElemConnectivity(QUADRILATERAL, iElem, 0) << "\t"; + data << dataSorter->GetElemConnectivity(QUADRILATERAL, iElem, 1) << "\t"; + data << dataSorter->GetElemConnectivity(QUADRILATERAL, iElem, 2) << "\t"; + data << dataSorter->GetElemConnectivity(QUADRILATERAL, iElem, 3) << "\n"; } for (iElem = 0; iElem < nParallel_Tetr; iElem++) { - Tecplot_File << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 0) << "\t" << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 1) << "\t"; - Tecplot_File << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 2) << "\t" << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 2) << "\t"; - Tecplot_File << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 3) << "\t" << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 3) << "\t"; - Tecplot_File << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 3) << "\t" << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 3) << "\n"; + data << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 0) << "\t" << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 1) << "\t"; + data << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 2) << "\t" << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 2) << "\t"; + data << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 3) << "\t" << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 3) << "\t"; + data << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 3) << "\t" << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 3) << "\n"; } for (iElem = 0; iElem < nParallel_Hexa; iElem++) { - Tecplot_File << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 0) << "\t" << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 1) << "\t"; - Tecplot_File << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 2) << "\t" << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 3) << "\t"; - Tecplot_File << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 4) << "\t" << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 5) << "\t"; - Tecplot_File << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 6) << "\t" << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 7) << "\n"; + data << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 0) << "\t" << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 1) << "\t"; + data << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 2) << "\t" << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 3) << "\t"; + data << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 4) << "\t" << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 5) << "\t"; + data << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 6) << "\t" << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 7) << "\n"; } for (iElem = 0; iElem < nParallel_Pris; iElem++) { - Tecplot_File << dataSorter->GetElemConnectivity(PRISM, iElem, 0) << "\t" << dataSorter->GetElemConnectivity(PRISM, iElem, 1) << "\t"; - Tecplot_File << dataSorter->GetElemConnectivity(PRISM, iElem, 1) << "\t" << dataSorter->GetElemConnectivity(PRISM, iElem, 2) << "\t"; - Tecplot_File << dataSorter->GetElemConnectivity(PRISM, iElem, 3) << "\t" << dataSorter->GetElemConnectivity(PRISM, iElem, 4) << "\t"; - Tecplot_File << dataSorter->GetElemConnectivity(PRISM, iElem, 4) << "\t" << dataSorter->GetElemConnectivity(PRISM, iElem, 5) << "\n"; + data << dataSorter->GetElemConnectivity(PRISM, iElem, 0) << "\t" << dataSorter->GetElemConnectivity(PRISM, iElem, 1) << "\t"; + data << dataSorter->GetElemConnectivity(PRISM, iElem, 1) << "\t" << dataSorter->GetElemConnectivity(PRISM, iElem, 2) << "\t"; + data << dataSorter->GetElemConnectivity(PRISM, iElem, 3) << "\t" << dataSorter->GetElemConnectivity(PRISM, iElem, 4) << "\t"; + data << dataSorter->GetElemConnectivity(PRISM, iElem, 4) << "\t" << dataSorter->GetElemConnectivity(PRISM, iElem, 5) << "\n"; } for (iElem = 0; iElem < nParallel_Pyra; iElem++) { - Tecplot_File << dataSorter->GetElemConnectivity(PYRAMID, iElem, 0) << "\t" << dataSorter->GetElemConnectivity(PYRAMID, iElem, 1) << "\t"; - Tecplot_File << dataSorter->GetElemConnectivity(PYRAMID, iElem, 2) << "\t" << dataSorter->GetElemConnectivity(PYRAMID, iElem, 3) << "\t"; - Tecplot_File << dataSorter->GetElemConnectivity(PYRAMID, iElem, 4) << "\t" << dataSorter->GetElemConnectivity(PYRAMID, iElem, 4) << "\t"; - Tecplot_File << dataSorter->GetElemConnectivity(PYRAMID, iElem, 4) << "\t" << dataSorter->GetElemConnectivity(PYRAMID, iElem, 4) << "\n"; + data << dataSorter->GetElemConnectivity(PYRAMID, iElem, 0) << "\t" << dataSorter->GetElemConnectivity(PYRAMID, iElem, 1) << "\t"; + data << dataSorter->GetElemConnectivity(PYRAMID, iElem, 2) << "\t" << dataSorter->GetElemConnectivity(PYRAMID, iElem, 3) << "\t"; + data << dataSorter->GetElemConnectivity(PYRAMID, iElem, 4) << "\t" << dataSorter->GetElemConnectivity(PYRAMID, iElem, 4) << "\t"; + data << dataSorter->GetElemConnectivity(PYRAMID, iElem, 4) << "\t" << dataSorter->GetElemConnectivity(PYRAMID, iElem, 4) << "\n"; } } - Tecplot_File.flush(); -#ifdef HAVE_MPI - SU2_MPI::Barrier(SU2_MPI::GetComm()); -#endif } - Tecplot_File.close(); - - /*--- Compute and store the write time. ---*/ - - stopTime = SU2_MPI::Wtime(); - - usedTime = stopTime-startTime; - - fileSize = DetermineFilesize(val_filename); - - /*--- Compute and store the bandwidth ---*/ + WriteMPIStringAll(data.str()); - bandwidth = fileSize/(1.0e6)/usedTime; + CloseMPIFile(); } From b7090e933263097ea9740dda98d108b457a5e72a Mon Sep 17 00:00:00 2001 From: rois1995 Date: Mon, 21 Sep 2026 09:44:55 +0200 Subject: [PATCH 11/35] Write the SU2 mesh output files in parallel CSU2MeshFileWriter and CSU2MeshBinaryFileWriter also let the ranks take turns: each rank opened the file, appended its elements or points and closed it, with a collective call after every rank. Each rank now builds its own part of the file in memory and all ranks write it at the same time with the collective MPI-IO call of CFileWriter, as the other output files do. The global element and point indices come from the number of elements and points of the ranks before, gathered once instead of once per rank. Multizone meshes are written into a single file, so CFileWriter:: OpenMPIFile takes an append flag that continues at the end of an existing file (and creates it when it does not exist, e.g. for the first zone of a file of its own). The files are byte-identical to those of the previous implementation, also for multizone cases. Co-Authored-By: Claude Opus 5 (1M context) --- .../include/output/filewriter/CFileWriter.hpp | 2 +- .../output/filewriter/CParallelFileWriter.cpp | 25 +- .../filewriter/CSU2MeshBinaryFileWriter.cpp | 230 ++++++++---------- .../output/filewriter/CSU2MeshFileWriter.cpp | 171 +++++-------- 4 files changed, 196 insertions(+), 232 deletions(-) diff --git a/SU2_CFD/include/output/filewriter/CFileWriter.hpp b/SU2_CFD/include/output/filewriter/CFileWriter.hpp index b2cf55197571..7974386b5103 100644 --- a/SU2_CFD/include/output/filewriter/CFileWriter.hpp +++ b/SU2_CFD/include/output/filewriter/CFileWriter.hpp @@ -180,7 +180,7 @@ class CFileWriter{ * \param[in] val_filename - The name of the file * \return Boolean indicating whether the opening was successful. */ - bool OpenMPIFile(string val_filename); + bool OpenMPIFile(string val_filename, bool append = false); /*! * \brief Close a file using MPI I/O. diff --git a/SU2_CFD/src/output/filewriter/CParallelFileWriter.cpp b/SU2_CFD/src/output/filewriter/CParallelFileWriter.cpp index 88a7f09acf6b..15feb546df92 100644 --- a/SU2_CFD/src/output/filewriter/CParallelFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CParallelFileWriter.cpp @@ -208,7 +208,7 @@ bool CFileWriter::WriteMPIString(const string &str, unsigned short processor){ } -bool CFileWriter::OpenMPIFile(string val_filename){ +bool CFileWriter::OpenMPIFile(string val_filename, bool append){ /*--- We append the pre-defined suffix (extension) to the filename (prefix) ---*/ val_filename.append(fileExt); @@ -217,6 +217,27 @@ bool CFileWriter::OpenMPIFile(string val_filename){ int ierr; disp = 0.0; + /*--- Continue writing at the end of a file that exists, e.g. to add a zone to a multizone mesh file. ---*/ + + if (append) { + ierr = MPI_File_open(SU2_MPI::GetComm(), val_filename.c_str(), MPI_MODE_WRONLY, MPI_INFO_NULL, &fhw); + + /*--- If the file does not exist yet, for example the first zone written to a file of its own, it is + created below like any other new file. ---*/ + + if (ierr == MPI_SUCCESS) { + MPI_Offset fileEnd; + MPI_File_get_size(fhw, &fileEnd); + disp = fileEnd; + + fileSize = 0.0; + usedTime = 0; + + return true; + } + MPI_File_close(&fhw); + } + /*--- All ranks open the file using MPI. Here, we try to open the file with exclusive so that an error is generated if the file exists. We always want to write a fresh output file, so we delete any existing files and create @@ -241,7 +262,7 @@ bool CFileWriter::OpenMPIFile(string val_filename){ val_filename, CURRENT_FUNCTION); } #else - fhw = fopen(val_filename.c_str(), "wb"); + fhw = fopen(val_filename.c_str(), append ? "ab" : "wb"); /*--- Error check for opening the file. ---*/ if (!fhw) { diff --git a/SU2_CFD/src/output/filewriter/CSU2MeshBinaryFileWriter.cpp b/SU2_CFD/src/output/filewriter/CSU2MeshBinaryFileWriter.cpp index 525eb1e23c9e..a44185db50fb 100644 --- a/SU2_CFD/src/output/filewriter/CSU2MeshBinaryFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CSU2MeshBinaryFileWriter.cpp @@ -25,6 +25,9 @@ * License along with SU2. If not, see . */ #include "../../../include/output/filewriter/CSU2MeshBinaryFileWriter.hpp" + +#include +#include #include "../../../../Common/include/toolboxes/printing_toolbox.hpp" #include @@ -65,144 +68,117 @@ void RestrictPermissions(const string& filename) { #endif } -FILE* OpenAppend(const string& filename) { - FILE* f = fopen(filename.c_str(), "ab"); - if (!f) SU2_MPI::Error(string("Unable to open file ") + filename, CURRENT_FUNCTION); - RestrictPermissions(filename); - return f; +/*--- Append the bytes of a value to a buffer, the buffers of all ranks are written to the file together. ---*/ +template +void AppendBytes(string& buffer, const T& value) { + buffer.append(reinterpret_cast(&value), sizeof(value)); } } // namespace void CSU2MeshBinaryFileWriter::WriteData(string val_filename) { - val_filename.append(fileExt); + /*--- For multizone cases all zones are written into one file, the zones after the first are appended. ---*/ - /*--- Write the file-level header (only once, before the first zone) followed - by the per-zone header (zone_id, n_dim, n_elem). Only rank 0 touches the - file for this; the implicit synchronization at the first Allreduce below - (also present in CSU2MeshFileWriter) keeps the other ranks from writing - before this is done. ---*/ + OpenMPIFile(val_filename, iZone != 0); - if (rank == 0) { - FILE* f = fopen(val_filename.c_str(), (iZone == 0) ? "wb" : "ab"); - if (!f) SU2_MPI::Error(string("Unable to open file ") + val_filename, CURRENT_FUNCTION); - RestrictPermissions(val_filename); + if (rank == MASTER_NODE) RestrictPermissions(val_filename + fileExt); - if (iZone == 0) { - int32_t size_conn_type = SU2B_CONN_TYPE_SIZE; - int32_t n_zone = nZone; - fwrite(&size_conn_type, sizeof(size_conn_type), 1, f); - fwrite(&n_zone, sizeof(n_zone), 1, f); - } + /*--- Write the file-level header (only once, before the first zone) followed by the per-zone header + (zone_id, n_dim, n_elem). Only the master node writes it. ---*/ - /*--- Zone IDs are 1-based, matching the "IZONE=" convention of the ASCII - format (CSU2MeshFileWriter writes iZone+1 as well). ---*/ - int32_t zone_id = iZone + 1; - int32_t n_dim = dataSorter->GetnDim(); - conn_t n_elem = dataSorter->GetnElemGlobal(); - fwrite(&zone_id, sizeof(zone_id), 1, f); - fwrite(&n_dim, sizeof(n_dim), 1, f); - fwrite(&n_elem, sizeof(n_elem), 1, f); + string buffer; - fclose(f); + if (iZone == 0) { + AppendBytes(buffer, SU2B_CONN_TYPE_SIZE); + AppendBytes(buffer, static_cast(nZone)); } - /*--- Section 1: element offsets. Every rank streams, in turn, the starting - connectivity-array position of each of its local elements (visited in - the fixed type order above), starting from the cumulative total left - behind by the previous ranks. Each rank's accumulator holds only its - own local contribution (like CSU2MeshFileWriter's nElem/myPoint), so - summing it across ranks via Allreduce yields the new cumulative total. - Once all ranks are done, the final total is appended once more as the - closing sentinel offset[n_elem]. ---*/ - - unsigned long connOffset = 0, localConnOffset = 0; - - for (int iProcessor = 0; iProcessor < size; iProcessor++) { - if (rank == iProcessor) { - FILE* f = OpenAppend(val_filename); - unsigned long running = connOffset; - for (auto type : ElemTypes) { - const conn_t nPointsElem = nPointsOfElementType(type); - for (auto iElem = 0ul; iElem < dataSorter->GetnElem(type); iElem++) { - conn_t value = running; - fwrite(&value, sizeof(value), 1, f); - running += nPointsElem + 2; - } - } - fclose(f); - localConnOffset = running - connOffset; - } - SU2_MPI::Allreduce(&localConnOffset, &connOffset, 1, MPI_UNSIGNED_LONG, MPI_SUM, SU2_MPI::GetComm()); - } + /*--- Zone IDs are 1-based, matching the "IZONE=" convention of the ASCII format. ---*/ + AppendBytes(buffer, static_cast(iZone + 1)); + AppendBytes(buffer, static_cast(dataSorter->GetnDim())); + AppendBytes(buffer, static_cast(dataSorter->GetnElemGlobal())); - if (rank == 0) { - FILE* f = OpenAppend(val_filename); - conn_t sentinel = connOffset; - fwrite(&sentinel, sizeof(sentinel), 1, f); - fclose(f); + WriteMPIString(buffer, MASTER_NODE); + + /*--- Each rank writes the data of its own elements and points, at the position that follows the data of the + ranks before it. The global offsets and indices of a rank are those of the ranks before it. ---*/ + + auto offsetOfRank = [&](unsigned long localCount) { + vector counts(size, localCount); + SU2_MPI::Allgather(&localCount, 1, MPI_UNSIGNED_LONG, counts.data(), 1, MPI_UNSIGNED_LONG, SU2_MPI::GetComm()); + return std::make_pair(std::accumulate(counts.begin(), counts.begin() + rank, 0ul), + std::accumulate(counts.begin(), counts.end(), 0ul)); + }; + + /*--- Section 1: element offsets, the starting connectivity-array position of each element, closed by the + total size as sentinel offset[n_elem]. ---*/ + + unsigned long localConnSize = 0, localElemCount = 0; + for (auto type : ElemTypes) { + localConnSize += dataSorter->GetnElem(type) * (nPointsOfElementType(type) + 2); + localElemCount += dataSorter->GetnElem(type); } - /*--- Section 2: element connectivity, [VTK_Type, node_0..node_n-1, GlobalIndex] - per element, in the same fixed type order and the same rank-by-rank - streaming pattern as CSU2MeshFileWriter uses for the ASCII format - (including the same "-1" to convert 1-based dataSorter indices to the - 0-based indices used throughout the SU2 mesh formats). ---*/ - - unsigned long elemIndexOffset = 0, localElemCount = 0; - - for (int iProcessor = 0; iProcessor < size; iProcessor++) { - if (rank == iProcessor) { - FILE* f = OpenAppend(val_filename); - conn_t globalIndex = elemIndexOffset; - for (auto type : ElemTypes) { - const auto nPointsElem = nPointsOfElementType(type); - for (auto iElem = 0ul; iElem < dataSorter->GetnElem(type); iElem++) { - conn_t vtkType = type; - fwrite(&vtkType, sizeof(vtkType), 1, f); - for (auto iNode = 0u; iNode < nPointsElem; iNode++) { - conn_t node = dataSorter->GetElemConnectivity(type, iElem, iNode) - 1; - fwrite(&node, sizeof(node), 1, f); - } - fwrite(&globalIndex, sizeof(globalIndex), 1, f); - globalIndex++; - } - } - fclose(f); - localElemCount = static_cast(globalIndex - elemIndexOffset); + unsigned long connOffset, totalConnSize; + std::tie(connOffset, totalConnSize) = offsetOfRank(localConnSize); + + unsigned long elemOffset, nElemGlobal; + std::tie(elemOffset, nElemGlobal) = offsetOfRank(localElemCount); + + buffer.clear(); + buffer.reserve(localElemCount * sizeof(conn_t)); + + unsigned long running = connOffset; + for (auto type : ElemTypes) { + const conn_t nPointsElem = nPointsOfElementType(type); + for (auto iElem = 0ul; iElem < dataSorter->GetnElem(type); iElem++) { + AppendBytes(buffer, static_cast(running)); + running += nPointsElem + 2; } - SU2_MPI::Allreduce(&localElemCount, &elemIndexOffset, 1, MPI_UNSIGNED_LONG, MPI_SUM, SU2_MPI::GetComm()); } + WriteMPIStringAll(buffer); + + buffer.clear(); + AppendBytes(buffer, static_cast(totalConnSize)); + WriteMPIString(buffer, MASTER_NODE); + + /*--- Section 2: element connectivity, [VTK_Type, node_0..node_n-1, GlobalIndex] per element. ---*/ + + buffer.clear(); + buffer.reserve(localConnSize * sizeof(conn_t)); + + conn_t globalIndex = elemOffset; + for (auto type : ElemTypes) { + const auto nPointsElem = nPointsOfElementType(type); + for (auto iElem = 0ul; iElem < dataSorter->GetnElem(type); iElem++) { + AppendBytes(buffer, static_cast(type)); + for (auto iNode = 0u; iNode < nPointsElem; iNode++) + AppendBytes(buffer, static_cast(dataSorter->GetElemConnectivity(type, iElem, iNode) - 1)); + AppendBytes(buffer, globalIndex); + globalIndex++; + } + } + WriteMPIStringAll(buffer); - /*--- Section 3: point coordinates and IDs, interleaved. Same rank-by-rank - streaming pattern as CSU2MeshFileWriter's point section. ---*/ + /*--- Section 3: point coordinates and IDs, interleaved. ---*/ - if (rank == 0) { - FILE* f = OpenAppend(val_filename); - conn_t nPointsGlobal = dataSorter->GetnPointsGlobal(); - fwrite(&nPointsGlobal, sizeof(nPointsGlobal), 1, f); - fclose(f); - } + buffer.clear(); + AppendBytes(buffer, static_cast(dataSorter->GetnPointsGlobal())); + WriteMPIString(buffer, MASTER_NODE); - unsigned long myPoint = 0, pointOffset = 0; + unsigned long pointOffset, nPointsTotal; + std::tie(pointOffset, nPointsTotal) = offsetOfRank(dataSorter->GetnPoints()); - for (int iProcessor = 0; iProcessor < size; iProcessor++) { - if (rank == iProcessor) { - FILE* f = OpenAppend(val_filename); - for (auto iPoint = 0ul; iPoint < dataSorter->GetnPoints(); iPoint++) { - for (auto iDim = 0u; iDim < dataSorter->GetnDim(); iDim++) { - double coord = dataSorter->GetData(iDim, iPoint); - fwrite(&coord, sizeof(coord), 1, f); - } - conn_t pointID = iPoint + pointOffset; - fwrite(&pointID, sizeof(pointID), 1, f); - } - fclose(f); - myPoint = dataSorter->GetnPoints(); - } - SU2_MPI::Allreduce(&myPoint, &pointOffset, 1, MPI_UNSIGNED_LONG, MPI_SUM, SU2_MPI::GetComm()); + buffer.clear(); + buffer.reserve(dataSorter->GetnPoints() * (dataSorter->GetnDim() * sizeof(double) + sizeof(conn_t))); + + for (auto iPoint = 0ul; iPoint < dataSorter->GetnPoints(); iPoint++) { + for (auto iDim = 0u; iDim < dataSorter->GetnDim(); iDim++) + AppendBytes(buffer, static_cast(dataSorter->GetData(iDim, iPoint))); + AppendBytes(buffer, static_cast(iPoint + pointOffset)); } + WriteMPIStringAll(buffer); /*--- Section 4: markers. Mirrors CSU2MeshFileWriter: the marker connectivity is not available from the data sorter, so it is read back from the @@ -210,8 +186,9 @@ void CSU2MeshBinaryFileWriter::WriteData(string val_filename) { SU2_COMPONENT::SU2_DEF) right after reading the original mesh. Only the master rank does this work, exactly as for the ASCII format. ---*/ + buffer.clear(); + if (rank == MASTER_NODE) { - FILE* f = OpenAppend(val_filename); string str = "boundary"; if (nZone > 1) str += "_" + PrintingToolbox::to_string(iZone); @@ -228,7 +205,7 @@ void CSU2MeshBinaryFileWriter::WriteData(string val_filename) { text_line.erase(0, 6); const int32_t nMarker_ = atoi(text_line.c_str()); - fwrite(&nMarker_, sizeof(nMarker_), 1, f); + AppendBytes(buffer, nMarker_); for (int iMarker = 0; iMarker < nMarker_; iMarker++) { @@ -250,7 +227,7 @@ void CSU2MeshBinaryFileWriter::WriteData(string val_filename) { are independent and must not silently drift apart. ---*/ char name_buf[SU2_BINARY_STRING_SIZE] = {}; strncpy(name_buf, Marker_Tag.c_str(), SU2_BINARY_STRING_SIZE - 1); - fwrite(name_buf, sizeof(char), SU2_BINARY_STRING_SIZE, f); + buffer.append(name_buf, SU2_BINARY_STRING_SIZE); getline(input_file, text_line); text_line.erase(0, 13); @@ -277,32 +254,35 @@ void CSU2MeshBinaryFileWriter::WriteData(string val_filename) { } auto nElemBoundConn = static_cast(nElem_Bound_); - fwrite(&nElemBoundConn, sizeof(nElemBoundConn), 1, f); + AppendBytes(buffer, nElemBoundConn); unsigned long running = 0; for (unsigned long iElem_Bound = 0; iElem_Bound < nElem_Bound_; iElem_Bound++) { conn_t value = running; - fwrite(&value, sizeof(value), 1, f); + AppendBytes(buffer, value); running += nPointsOfElementType(vtkTypes[iElem_Bound]) + 1; } conn_t sentinel = running; - fwrite(&sentinel, sizeof(sentinel), 1, f); + AppendBytes(buffer, sentinel); for (unsigned long iElem_Bound = 0; iElem_Bound < nElem_Bound_; iElem_Bound++) { conn_t vtkType = vtkTypes[iElem_Bound]; - fwrite(&vtkType, sizeof(vtkType), 1, f); + AppendBytes(buffer, vtkType); const auto nPointsElem = nPointsOfElementType(vtkTypes[iElem_Bound]); for (unsigned short iNode = 0; iNode < nPointsElem; iNode++) { conn_t node = nodes[iElem_Bound][iNode]; - fwrite(&node, sizeof(node), 1, f); + AppendBytes(buffer, node); } } } } input_file.close(); - fclose(f); } - SU2_MPI::Barrier(SU2_MPI::GetComm()); + /*--- Only the master node has the markers, the other ranks write nothing. ---*/ + + WriteMPIString(buffer, MASTER_NODE); + + CloseMPIFile(); } diff --git a/SU2_CFD/src/output/filewriter/CSU2MeshFileWriter.cpp b/SU2_CFD/src/output/filewriter/CSU2MeshFileWriter.cpp index c471110cc9ce..baae36b48303 100644 --- a/SU2_CFD/src/output/filewriter/CSU2MeshFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CSU2MeshFileWriter.cpp @@ -26,6 +26,8 @@ */ #include "../../../include/output/filewriter/CSU2MeshFileWriter.hpp" + +#include #include "../../../../Common/include/toolboxes/printing_toolbox.hpp" const string CSU2MeshFileWriter::fileExt = ".su2"; @@ -36,129 +38,87 @@ CSU2MeshFileWriter::CSU2MeshFileWriter(CParallelDataSorter *valDataSorter, void CSU2MeshFileWriter::WriteData(string val_filename) { - ofstream output_file; + /*--- For multizone cases all zones are written into one file, the zones after the first are appended. ---*/ - /*--- We append the pre-defined suffix (extension) to the filename (prefix) ---*/ - val_filename.append(fileExt); + OpenMPIFile(val_filename, iZone != 0); - /*--- Only the FIRST node writes the header (it does not matter if that is the master). ---*/ + /*--- Write the header. ---*/ - if (rank == 0) { - /*--- For multizone-cases this only works if the all zonal meshes are in one file. - If the meshes are separate for each zone another solution has to be found. ---*/ - if (iZone==0) { - output_file.open(val_filename); - } else { - output_file.open(val_filename, ios::app); - } + ostringstream header; - if (iZone==0 && nZone>1) { - output_file << "NZONE= " << nZone << endl; - } + if (iZone == 0 && nZone > 1) header << "NZONE= " << nZone << endl; + if (nZone > 1) header << "IZONE= " << iZone + 1 << endl; - if (nZone > 1){ - output_file << "IZONE= " << iZone+1 << endl; - } + header << "NDIME= " << dataSorter->GetnDim() << endl; + header << "NELEM= " << dataSorter->GetnElemGlobal() << endl; - /*--- Write dimensions data. ---*/ + WriteMPIString(header.str(), MASTER_NODE); - output_file << "NDIME= " << dataSorter->GetnDim() << endl; + /*--- Each rank formats the data of its own elements and points into a string, and all ranks then write their + strings to the file at the same time, one after the other in rank order. The global index of an element or + point is its local index plus the number of elements or points of the ranks before this one. ---*/ - output_file << "NELEM= " << dataSorter->GetnElemGlobal() << endl; + auto offsetOfRank = [&](unsigned long localCount) { + vector counts(size, localCount); + SU2_MPI::Allgather(&localCount, 1, MPI_UNSIGNED_LONG, counts.data(), 1, MPI_UNSIGNED_LONG, SU2_MPI::GetComm()); + return std::accumulate(counts.begin(), counts.begin() + rank, 0ul); + }; - output_file.close(); - } + ostringstream data; - unsigned long nElem = 0, offset = 0; + /*--- Write the connectivity, the type of each element is written before its nodes. ---*/ - for (int iProcessor = 0; iProcessor < size; iProcessor++) { - if (rank == iProcessor) { - output_file.open(val_filename, ios::app); + unsigned long nElem = 0; + for (auto type : {TRIANGLE, QUADRILATERAL, TETRAHEDRON, HEXAHEDRON, PRISM, PYRAMID}) + nElem += dataSorter->GetnElem(type); - for (auto iElem = 0ul; iElem < dataSorter->GetnElem(TRIANGLE); iElem++) { - output_file << "5\t"; - for (auto iNode = 0u; iNode < N_POINTS_TRIANGLE; ++iNode) - output_file << dataSorter->GetElemConnectivity(TRIANGLE, iElem, iNode) - 1 << "\t"; - output_file << nElem + offset << "\n"; nElem++; - } - for (auto iElem = 0ul; iElem < dataSorter->GetnElem(QUADRILATERAL); iElem++) { - output_file << "9\t"; - for (auto iNode = 0u; iNode < N_POINTS_QUADRILATERAL; ++iNode) - output_file << dataSorter->GetElemConnectivity(QUADRILATERAL, iElem, iNode) - 1 << "\t"; - output_file << nElem + offset << "\n"; nElem++; - } - for (auto iElem = 0ul; iElem < dataSorter->GetnElem(TETRAHEDRON); iElem++) { - output_file << "10\t"; - for (auto iNode = 0u; iNode < N_POINTS_TETRAHEDRON; ++iNode) - output_file << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, iNode) - 1 << "\t"; - output_file << nElem + offset << "\n"; nElem++; - } - for (auto iElem = 0ul; iElem < dataSorter->GetnElem(HEXAHEDRON); iElem++) { - output_file << "12\t"; - for (auto iNode = 0u; iNode < N_POINTS_HEXAHEDRON; ++iNode) - output_file << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, iNode) - 1 << "\t"; - output_file << nElem + offset << "\n"; nElem++; - } - for (auto iElem = 0ul; iElem < dataSorter->GetnElem(PRISM); iElem++) { - output_file << "13\t"; - for (auto iNode = 0u; iNode < N_POINTS_PRISM; ++iNode) - output_file << dataSorter->GetElemConnectivity(PRISM, iElem, iNode) - 1 << "\t"; - output_file << nElem + offset << "\n"; nElem++; - } + unsigned long offset = offsetOfRank(nElem); - for (auto iElem = 0ul; iElem < dataSorter->GetnElem(PYRAMID); iElem++) { - output_file << "14\t"; - for (auto iNode = 0u; iNode < N_POINTS_PYRAMID; ++iNode) - output_file << dataSorter->GetElemConnectivity(PYRAMID, iElem, iNode) - 1 << "\t"; - output_file << nElem + offset << "\n"; nElem++; - } - - output_file.close(); + nElem = 0; + for (auto type : {TRIANGLE, QUADRILATERAL, TETRAHEDRON, HEXAHEDRON, PRISM, PYRAMID}) { + const auto nPoints = nPointsOfElementType(type); + for (auto iElem = 0ul; iElem < dataSorter->GetnElem(type); iElem++) { + data << type << "\t"; + for (auto iNode = 0u; iNode < nPoints; ++iNode) + data << dataSorter->GetElemConnectivity(type, iElem, iNode) - 1 << "\t"; + data << nElem + offset << "\n"; nElem++; } - - /*--- Communicate offset, implies a barrier. ---*/ - SU2_MPI::Allreduce(&nElem, &offset, 1, MPI_UNSIGNED_LONG, MPI_SUM, SU2_MPI::GetComm()); } + WriteMPIStringAll(data.str()); + /*--- Write the node coordinates. ---*/ - if (rank == 0) { - output_file.open(val_filename, ios::app); - output_file << "NPOIN= " << dataSorter->GetnPointsGlobal() << "\n"; - output_file.close(); - } + WriteMPIString("NPOIN= " + to_string(dataSorter->GetnPointsGlobal()) + "\n", MASTER_NODE); - unsigned long myPoint = 0; offset = 0; + offset = offsetOfRank(dataSorter->GetnPoints()); - for (int iProcessor = 0; iProcessor < size; iProcessor++) { - if (rank == iProcessor) { - output_file.open(val_filename, ios::app); - output_file.precision(15); + data.str(""); + data.clear(); + data.precision(15); + data << scientific; - for (auto iPoint = 0ul; iPoint < dataSorter->GetnPoints(); iPoint++) { + for (auto iPoint = 0ul; iPoint < dataSorter->GetnPoints(); iPoint++) { - /*--- Loop over the coordinates and write the values to file. ---*/ + /*--- Loop over the coordinates and write the values to file. ---*/ - for (auto iDim = 0u; iDim < dataSorter->GetnDim(); iDim++) { - output_file << scientific << dataSorter->GetData(iDim, iPoint) << "\t"; - } + for (auto iDim = 0u; iDim < dataSorter->GetnDim(); iDim++) { + data << dataSorter->GetData(iDim, iPoint) << "\t"; + } - /*--- Write global index. ---*/ + /*--- Write global index. ---*/ - output_file << iPoint + offset << "\n"; - myPoint++; - } + data << iPoint + offset << "\n"; + } - output_file.close(); - } + WriteMPIStringAll(data.str()); - /*--- Communicate offset, implies a barrier. ---*/ - SU2_MPI::Allreduce(&myPoint, &offset, 1, MPI_UNSIGNED_LONG, MPI_SUM, SU2_MPI::GetComm()); - } + /*--- The boundaries are copied from the file written by the mesh deformation, only the master node has them. + This is the last thing written to the file. ---*/ - if (rank == MASTER_NODE) { + ostringstream boundaries; - output_file.open(val_filename, ios::app); + if (rank == MASTER_NODE) { /*--- Read the boundary information ---*/ @@ -186,7 +146,7 @@ void CSU2MeshFileWriter::WriteData(string val_filename) { text_line.erase(0,6); const auto nMarker_ = atoi(text_line.c_str()); - output_file << "NMARK= " << nMarker_ << endl; + boundaries << "NMARK= " << nMarker_ << endl; for (auto iMarker = 0; iMarker < nMarker_; iMarker++) { @@ -208,15 +168,15 @@ void CSU2MeshFileWriter::WriteData(string val_filename) { text_line.erase(0,13); const auto nElem_Bound_ = atoi(text_line.c_str()); - output_file << "MARKER_TAG= " << Marker_Tag << endl; - output_file << "MARKER_ELEMS= " << nElem_Bound_<< endl; + boundaries << "MARKER_TAG= " << Marker_Tag << endl; + boundaries << "MARKER_ELEMS= " << nElem_Bound_<< endl; getline (input_file, text_line); text_line.erase(0,8); const auto SendTo = atoi(text_line.c_str()); if (Marker_Tag == "SEND_RECEIVE") { - output_file << "SEND_TO= " << SendTo << endl; + boundaries << "SEND_TO= " << SendTo << endl; } for (auto iElem_Bound = 0; iElem_Bound < nElem_Bound_; iElem_Bound++) { @@ -226,30 +186,33 @@ void CSU2MeshFileWriter::WriteData(string val_filename) { unsigned short VTK_Type; bound_line >> VTK_Type; - output_file << VTK_Type; + boundaries << VTK_Type; unsigned long vnodes[4] = {0}; switch (VTK_Type) { case LINE: case VERTEX: bound_line >> vnodes[0]; bound_line >> vnodes[1]; - output_file << "\t" << vnodes[0] << "\t" << vnodes[1] << "\n"; + boundaries << "\t" << vnodes[0] << "\t" << vnodes[1] << "\n"; break; case TRIANGLE: bound_line >> vnodes[0]; bound_line >> vnodes[1]; bound_line >> vnodes[2]; - output_file << "\t" << vnodes[0] << "\t" << vnodes[1] << "\t" << vnodes[2] << "\n"; + boundaries << "\t" << vnodes[0] << "\t" << vnodes[1] << "\t" << vnodes[2] << "\n"; break; case QUADRILATERAL: bound_line >> vnodes[0]; bound_line >> vnodes[1]; bound_line >> vnodes[2]; bound_line >> vnodes[3]; - output_file << "\t" << vnodes[0] << "\t" << vnodes[1] << "\t" << vnodes[2] << "\t" << vnodes[3] << "\n"; + boundaries << "\t" << vnodes[0] << "\t" << vnodes[1] << "\t" << vnodes[2] << "\t" << vnodes[3] << "\n"; break; } } } } - output_file.close(); } - SU2_MPI::Barrier(SU2_MPI::GetComm()); + /*--- Only the master node has this text, the other ranks write nothing. ---*/ + + WriteMPIString(boundaries.str(), MASTER_NODE); + + CloseMPIFile(); } From 834fd29cb76e2079be7e62be48a352407df60607 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Wed, 23 Sep 2026 11:46:30 +0200 Subject: [PATCH 12/35] Report the write bandwidth of the CGNS output The CGNS writer never set the bandwidth, so the file writing summary always printed 0 MB/s for CGNS and SURFACE_CGNS. Time the writing and read the file size after closing it, as the Tecplot binary writer does. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LEL91DW5WPbPwgFtCvHga6 --- SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp b/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp index 73730f3db419..8ab490b59583 100644 --- a/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp @@ -43,6 +43,10 @@ void CCGNSFileWriter::WriteData(string val_filename) { /*--- We append the pre-defined suffix (extension) to the filename (prefix) ---*/ val_filename.append(fileExt); + + /*--- Set a timer for the file writing. ---*/ + startTime = SU2_MPI::Wtime(); + /*--- Open the CGNS file for writing. ---*/ InitializeMeshFile(val_filename); @@ -64,6 +68,12 @@ void CCGNSFileWriter::WriteData(string val_filename) { CallCGNS(cg_close(cgnsFileID)); #endif + /*--- Compute and store the write time and the bandwidth. ---*/ + stopTime = SU2_MPI::Wtime(); + usedTime = stopTime - startTime; + fileSize = DetermineFilesize(val_filename); + bandwidth = fileSize / (1.0e6) / usedTime; + #endif } From 5509da66ea289527487a8e88c8449169408e4c6b Mon Sep 17 00:00:00 2001 From: rois1995 Date: Wed, 23 Sep 2026 11:50:41 +0200 Subject: [PATCH 13/35] Add WRT_OUTPUT_DOUBLE_PRECISION to 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, 4 insertions(+) diff --git a/config_template.cfg b/config_template.cfg index f2aa561f9619..498b79a78d9f 100644 --- a/config_template.cfg +++ b/config_template.cfg @@ -2643,6 +2643,10 @@ TABULAR_FORMAT= CSV % Set .precision(value) to specified value for SU2_DOT and HISTORY output. Useful for exact gradient validation. OUTPUT_PRECISION= 10 % +% Write the fields of the volume and surface files (CGNS and Paraview XML) in double +% precision instead of single. The CGNS coordinates are always in double precision. +WRT_OUTPUT_DOUBLE_PRECISION= NO +% % For multizone problems, extend solution and restart filenames automatically by zone number MULTIZONE_ADAPT_FILENAME= YES % From ea33583f895172bc45ab8b73ee56370bce918a80 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Wed, 23 Sep 2026 12:45:08 +0200 Subject: [PATCH 14/35] Add regression tests for the parallel ASCII output writers The ASCII writers (RESTART_ASCII, PARAVIEW_ASCII, TECPLOT_ASCII and the SU2 mesh file) now write in parallel. Write the solution of the vandv/rans/flatplate case on 2 ranks without iterating, with SU2_CFD, SU2_SOL and SU2_DEF, and compare the files with reference files written by the previous writers. The reference files are in su2code/TestCases, folder output_writers. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LEL91DW5WPbPwgFtCvHga6 --- TestCases/output_writers/ascii_output.cfg | 80 +++++++++++++++++++++++ TestCases/parallel_regression.py | 43 ++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 TestCases/output_writers/ascii_output.cfg diff --git a/TestCases/output_writers/ascii_output.cfg b/TestCases/output_writers/ascii_output.cfg new file mode 100644 index 000000000000..ac3685f4163f --- /dev/null +++ b/TestCases/output_writers/ascii_output.cfg @@ -0,0 +1,80 @@ +% Test of the ASCII output writers, which write in parallel with MPI-IO. +% The solution of vandv/rans/flatplate is written without iterating (ITER= 0): +% - SU2_CFD writes RESTART_ASCII, +% - SU2_SOL writes PARAVIEW_ASCII and TECPLOT_ASCII (volume and surface), +% - SU2_DEF (no deformation) writes the SU2 mesh file. +% The files are compared with reference files written on 2 ranks by the previous writers, +% in which the ranks wrote one after the other. +% +% ------------- 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 +SIDESLIP_ANGLE= 0.0 +FREESTREAM_TEMPERATURE= 300.0 +REYNOLDS_NUMBER= 5000000.0 +REYNOLDS_LENGTH= 1.0 +FREESTREAM_TURBULENCEINTENSITY= 0.00038729 +FREESTREAM_TURB2LAMVISCRATIO= 0.009 +% ---------------------- REFERENCE VALUE DEFINITION ---------------------------% +REF_ORIGIN_MOMENT_X= 0.25 +REF_ORIGIN_MOMENT_Y= 0.00 +REF_ORIGIN_MOMENT_Z= 0.00 +REF_LENGTH= 1.0 +REF_AREA= 2.0 +% -------------------- 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 ) +MARKER_MONITORING= ( wall ) +% ------------- COMMON PARAMETERS DEFINING THE NUMERICAL METHOD ---------------% +NUM_METHOD_GRAD= WEIGHTED_LEAST_SQUARES +CFL_NUMBER= 400.0 +CFL_ADAPT= NO +ITER= 0 +% ------------------------ LINEAR SOLVER DEFINITION ---------------------------% +LINEAR_SOLVER= FGMRES +LINEAR_SOLVER_PREC= ILU +LINEAR_SOLVER_ILU_FILL_IN= 0 +LINEAR_SOLVER_ERROR= 1E-15 +LINEAR_SOLVER_ITER= 25 +% ----------------------- SLOPE LIMITER DEFINITION ----------------------------% +VENKAT_LIMITER_COEFF= 0.1 +ADJ_SHARP_LIMITER_COEFF= 3.0 +REF_SHARP_EDGES= 3.0 +SENS_REMOVE_SHARP= NO +% -------------------- FLOW NUMERICAL METHOD DEFINITION -----------------------% +CONV_NUM_METHOD_FLOW= ROE +MUSCL_FLOW= YES +MUSCL_KAPPA_FLOW= 0.5 +SLOPE_LIMITER_FLOW= NONE +TIME_DISCRE_FLOW= EULER_IMPLICIT +% -------------------- TURBULENT NUMERICAL METHOD DEFINITION ------------------% +CONV_NUM_METHOD_TURB= SCALAR_UPWIND +MUSCL_TURB= NO +MUSCL_KAPPA_TURB= 0.0 +TIME_DISCRE_TURB= EULER_IMPLICIT +% --------------------------- CONVERGENCE PARAMETERS --------------------------% +CONV_FIELD= RMS_DENSITY +CONV_RESIDUAL_MINVAL= -13 +% ------------------------- INPUT/OUTPUT INFORMATION --------------------------% +MESH_FILENAME= ../vandv/rans/flatplate/mesh_flatplate_turb_035x025.su2 +MESH_FORMAT= SU2 +MESH_OUT_FILENAME= mesh_out +SOLUTION_FILENAME= ../vandv/rans/flatplate/solution_flow +TABULAR_FORMAT= CSV +CONV_FILENAME= history +RESTART_FILENAME= restart_flow +VOLUME_FILENAME= flow +SURFACE_FILENAME= surface_flow +SCREEN_OUTPUT= INNER_ITER WALL_TIME RMS_RES DRAG +OUTPUT_FILES= (RESTART_ASCII, PARAVIEW_ASCII, TECPLOT_ASCII, SURFACE_PARAVIEW_ASCII, SURFACE_TECPLOT_ASCII) +OUTPUT_WRT_FREQ = 10000 +HISTORY_OUTPUT= ITER RMS_RES AERO_COEFF diff --git a/TestCases/parallel_regression.py b/TestCases/parallel_regression.py index 87edd6666a17..b6c7ce341c9e 100755 --- a/TestCases/parallel_regression.py +++ b/TestCases/parallel_regression.py @@ -1933,6 +1933,49 @@ def main(): pass_list.append(stl_writer_test.run_filediff()) test_list.append(stl_writer_test) + ###################################### + ### RUN ASCII OUTPUT WRITER TESTS ### + ###################################### + + # The ASCII writers write in parallel, compare their files with those of the previous writers. + # ITER= 0 (test_iter = -1): the restart solution is written without iterating. + ascii_restart_writer = TestCase('ascii_restart_writer') + ascii_restart_writer.cfg_dir = "output_writers" + ascii_restart_writer.cfg_file = "ascii_output.cfg" + ascii_restart_writer.test_iter = -1 + ascii_restart_writer.command = TestCase.Command("mpirun -n 2", "SU2_CFD") + ascii_restart_writer.timeout = 1600 + ascii_restart_writer.reference_file = "restart_flow.csv.ref" + ascii_restart_writer.test_file = "restart_flow.csv" + pass_list.append(ascii_restart_writer.run_filediff()) + test_list.append(ascii_restart_writer) + + for tag, test_file in [("ascii_paraview_writer", "flow.vtk"), ("ascii_tecplot_writer", "flow.dat"), + ("ascii_surface_paraview_writer", "surface_flow.vtk"), + ("ascii_surface_tecplot_writer", "surface_flow.dat")]: + ascii_writer = TestCase(tag) + ascii_writer.cfg_dir = "output_writers" + ascii_writer.cfg_file = "ascii_output.cfg" + ascii_writer.test_iter = -1 + ascii_writer.command = TestCase.Command("mpirun -n 2", "SU2_SOL") + ascii_writer.timeout = 1600 + ascii_writer.reference_file = test_file + ".ref" + ascii_writer.test_file = test_file + pass_list.append(ascii_writer.run_filediff()) + test_list.append(ascii_writer) + + # SU2 mesh file written by SU2_DEF without deformation + ascii_mesh_writer = TestCase('ascii_mesh_writer') + ascii_mesh_writer.cfg_dir = "output_writers" + ascii_mesh_writer.cfg_file = "ascii_output.cfg" + ascii_mesh_writer.test_iter = -1 + ascii_mesh_writer.command = TestCase.Command("mpirun -n 2", "SU2_DEF") + ascii_mesh_writer.timeout = 1600 + ascii_mesh_writer.reference_file = "mesh_out.su2.ref" + ascii_mesh_writer.test_file = "mesh_out.su2" + pass_list.append(ascii_mesh_writer.run_filediff()) + test_list.append(ascii_mesh_writer) + ###################################### ### RUN SU2_DEF TESTS ### ###################################### From c3d3f0ecea88c1a7c4ce656e9b935fdcd83966c8 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Wed, 23 Sep 2026 12:47:09 +0200 Subject: [PATCH 15/35] Run the regression tests with the fixCGNSOutput branch of TestCases Temporary, for the reference files of su2code/TestCases#205. To be reverted to develop before merging. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LEL91DW5WPbPwgFtCvHga6 --- .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 76eb60ebc8f8..577defe20413 100644 --- a/.github/workflows/regression.yml +++ b/.github/workflows/regression.yml @@ -233,7 +233,7 @@ jobs: uses: docker://ghcr.io/su2code/su2/test-su2:260405-0054 with: # -t -c - args: -b ${{github.ref}} -t develop -c develop -s ${{matrix.testscript}} + args: -b ${{github.ref}} -t develop -c fixCGNSOutput -s ${{matrix.testscript}} - name: Cleanup uses: docker://ghcr.io/su2code/su2/test-su2:260405-0054 with: @@ -282,7 +282,7 @@ jobs: uses: docker://ghcr.io/su2code/su2/test-su2:260405-0054 with: # -t -c - args: -b ${{github.ref}} -t develop -c develop -s ${{matrix.testscript}} -a "--tapetests" + args: -b ${{github.ref}} -t develop -c fixCGNSOutput -s ${{matrix.testscript}} -a "--tapetests" - name: Cleanup uses: docker://ghcr.io/su2code/su2/test-su2:260405-0054 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 fixCGNSOutput -s ${{matrix.testscript}} -a "--tsan" - name: Cleanup uses: docker://ghcr.io/su2code/su2/test-su2-tsan:260405-0054 with: @@ -375,7 +375,7 @@ jobs: uses: docker://ghcr.io/su2code/su2/test-su2-asan:260405-0054 with: # -t -c - args: -b ${{github.ref}} -t develop -c develop -s ${{matrix.testscript}} -a "--asan" + args: -b ${{github.ref}} -t develop -c fixCGNSOutput -s ${{matrix.testscript}} -a "--asan" - name: Cleanup uses: docker://ghcr.io/su2code/su2/test-su2-asan:260405-0054 with: From 955bca11a39542650ca462f5cbd5715a4ba4cf83 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Wed, 23 Sep 2026 14:58:49 +0200 Subject: [PATCH 16/35] Fix CodeQL alerts in the Tecplot ASCII and SU2 binary mesh writers Use unsigned long for the field counter of the Tecplot ASCII writer, which is compared with the size of the field names, and rename the offset of the boundary elements of the SU2 binary mesh writer, which hid the one of the volume elements. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LEL91DW5WPbPwgFtCvHga6 --- .../src/output/filewriter/CSU2MeshBinaryFileWriter.cpp | 8 ++++---- SU2_CFD/src/output/filewriter/CTecplotFileWriter.cpp | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/SU2_CFD/src/output/filewriter/CSU2MeshBinaryFileWriter.cpp b/SU2_CFD/src/output/filewriter/CSU2MeshBinaryFileWriter.cpp index a44185db50fb..5b53385be621 100644 --- a/SU2_CFD/src/output/filewriter/CSU2MeshBinaryFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CSU2MeshBinaryFileWriter.cpp @@ -256,13 +256,13 @@ void CSU2MeshBinaryFileWriter::WriteData(string val_filename) { auto nElemBoundConn = static_cast(nElem_Bound_); AppendBytes(buffer, nElemBoundConn); - unsigned long running = 0; + unsigned long boundOffset = 0; for (unsigned long iElem_Bound = 0; iElem_Bound < nElem_Bound_; iElem_Bound++) { - conn_t value = running; + conn_t value = boundOffset; AppendBytes(buffer, value); - running += nPointsOfElementType(vtkTypes[iElem_Bound]) + 1; + boundOffset += nPointsOfElementType(vtkTypes[iElem_Bound]) + 1; } - conn_t sentinel = running; + conn_t sentinel = boundOffset; AppendBytes(buffer, sentinel); for (unsigned long iElem_Bound = 0; iElem_Bound < nElem_Bound_; iElem_Bound++) { diff --git a/SU2_CFD/src/output/filewriter/CTecplotFileWriter.cpp b/SU2_CFD/src/output/filewriter/CTecplotFileWriter.cpp index 1b4b4d48656d..9077b049134f 100644 --- a/SU2_CFD/src/output/filewriter/CTecplotFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CTecplotFileWriter.cpp @@ -43,7 +43,7 @@ void CTecplotFileWriter::WriteData(string val_filename){ const vector fieldNames = dataSorter->GetFieldNames(); - unsigned short iVar; + unsigned long iVar; unsigned long iPoint, iElem; From 58803f5e02ca4153fd1f6c21a8d7955f82ca4f7b Mon Sep 17 00:00:00 2001 From: rois1995 Date: Wed, 23 Sep 2026 15:00:06 +0200 Subject: [PATCH 17/35] Remove the extra spaces in the ASCII output writer tests Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LEL91DW5WPbPwgFtCvHga6 --- TestCases/parallel_regression.py | 46 +++++++++++++++----------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/TestCases/parallel_regression.py b/TestCases/parallel_regression.py index b6c7ce341c9e..cd9a172f07fb 100755 --- a/TestCases/parallel_regression.py +++ b/TestCases/parallel_regression.py @@ -1933,46 +1933,42 @@ def main(): pass_list.append(stl_writer_test.run_filediff()) test_list.append(stl_writer_test) - ###################################### - ### RUN ASCII OUTPUT WRITER TESTS ### - ###################################### - # The ASCII writers write in parallel, compare their files with those of the previous writers. # ITER= 0 (test_iter = -1): the restart solution is written without iterating. - ascii_restart_writer = TestCase('ascii_restart_writer') - ascii_restart_writer.cfg_dir = "output_writers" - ascii_restart_writer.cfg_file = "ascii_output.cfg" - ascii_restart_writer.test_iter = -1 - ascii_restart_writer.command = TestCase.Command("mpirun -n 2", "SU2_CFD") - ascii_restart_writer.timeout = 1600 + ascii_restart_writer = TestCase('ascii_restart_writer') + ascii_restart_writer.cfg_dir = "output_writers" + ascii_restart_writer.cfg_file = "ascii_output.cfg" + ascii_restart_writer.test_iter = -1 + ascii_restart_writer.command = TestCase.Command("mpirun -n 2", "SU2_CFD") + ascii_restart_writer.timeout = 1600 ascii_restart_writer.reference_file = "restart_flow.csv.ref" - ascii_restart_writer.test_file = "restart_flow.csv" + ascii_restart_writer.test_file = "restart_flow.csv" pass_list.append(ascii_restart_writer.run_filediff()) test_list.append(ascii_restart_writer) for tag, test_file in [("ascii_paraview_writer", "flow.vtk"), ("ascii_tecplot_writer", "flow.dat"), ("ascii_surface_paraview_writer", "surface_flow.vtk"), ("ascii_surface_tecplot_writer", "surface_flow.dat")]: - ascii_writer = TestCase(tag) - ascii_writer.cfg_dir = "output_writers" - ascii_writer.cfg_file = "ascii_output.cfg" - ascii_writer.test_iter = -1 - ascii_writer.command = TestCase.Command("mpirun -n 2", "SU2_SOL") - ascii_writer.timeout = 1600 + ascii_writer = TestCase(tag) + ascii_writer.cfg_dir = "output_writers" + ascii_writer.cfg_file = "ascii_output.cfg" + ascii_writer.test_iter = -1 + ascii_writer.command = TestCase.Command("mpirun -n 2", "SU2_SOL") + ascii_writer.timeout = 1600 ascii_writer.reference_file = test_file + ".ref" - ascii_writer.test_file = test_file + ascii_writer.test_file = test_file pass_list.append(ascii_writer.run_filediff()) test_list.append(ascii_writer) # SU2 mesh file written by SU2_DEF without deformation - ascii_mesh_writer = TestCase('ascii_mesh_writer') - ascii_mesh_writer.cfg_dir = "output_writers" - ascii_mesh_writer.cfg_file = "ascii_output.cfg" - ascii_mesh_writer.test_iter = -1 - ascii_mesh_writer.command = TestCase.Command("mpirun -n 2", "SU2_DEF") - ascii_mesh_writer.timeout = 1600 + ascii_mesh_writer = TestCase('ascii_mesh_writer') + ascii_mesh_writer.cfg_dir = "output_writers" + ascii_mesh_writer.cfg_file = "ascii_output.cfg" + ascii_mesh_writer.test_iter = -1 + ascii_mesh_writer.command = TestCase.Command("mpirun -n 2", "SU2_DEF") + ascii_mesh_writer.timeout = 1600 ascii_mesh_writer.reference_file = "mesh_out.su2.ref" - ascii_mesh_writer.test_file = "mesh_out.su2" + ascii_mesh_writer.test_file = "mesh_out.su2" pass_list.append(ascii_mesh_writer.run_filediff()) test_list.append(ascii_mesh_writer) From 4fe9b3418fd4c1e4bac64d811f0636ed89b3c02e Mon Sep 17 00:00:00 2001 From: rois1995 Date: Mon, 28 Sep 2026 15:49:50 +0200 Subject: [PATCH 18/35] Open appended MPI files with MPI_MODE_CREATE, make truncated CGNS marker names unique - Appending (zones after the first of a multizone SU2 mesh file) opened the file without MPI_MODE_CREATE and closed the handle of a failed open. The file is now opened with MPI_MODE_CREATE|MPI_MODE_WRONLY, an open error is reported, and no invalid handle is closed after a failed exclusive open. - CGNS names have at most 32 characters. Two marker tags with the same first 32 characters gave duplicate section, BC and family names. A number is now appended to the truncated name to make it unique, and the renaming is reported on screen. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LEL91DW5WPbPwgFtCvHga6 --- .../src/output/filewriter/CCGNSFileWriter.cpp | 21 +++++++++++++- .../output/filewriter/CParallelFileWriter.cpp | 29 +++++++++---------- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp b/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp index 8ab490b59583..8f5702272dc0 100644 --- a/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp @@ -30,6 +30,7 @@ #include "../../../../Common/include/CConfig.hpp" #include "../../../../Common/include/geometry/CGeometry.hpp" +#include #include const string CCGNSFileWriter::fileExt = ".cgns"; @@ -217,7 +218,25 @@ void CCGNSFileWriter::InitializeZone(const string& zoneName) { } void CCGNSFileWriter::WriteBoundaries() { + /*--- CGNS names have at most 32 characters, longer tags are truncated. If two truncated tags are equal, a number is + appended to make the names of the sections, BCs and families unique. ---*/ + + vector names; for (const auto& marker : boundaryMarkers) { + string name = marker.name.substr(0, 32); + for (unsigned long n = 1; std::find(names.begin(), names.end(), name) != names.end(); n++) { + const string suffix = "_" + to_string(n); + name = marker.name.substr(0, 32 - suffix.size()) + suffix; + } + if (rank == MASTER_NODE && name != marker.name.substr(0, 32)) { + cout << "CGNS output: the marker " << marker.name << " is written as " << name + << " (names have at most 32 characters)." << endl; + } + names.push_back(name); + } + + for (size_t iMarker = 0; iMarker < boundaryMarkers.size(); ++iMarker) { + const auto& marker = boundaryMarkers[iMarker]; /*--- Count the elements of this rank and collect the element types it holds. The local connectivity holds the VTK type of each element followed by the ids of its nodes. ---*/ @@ -250,7 +269,7 @@ void CCGNSFileWriter::WriteBoundaries() { SU2_MPI::Allreduce(&typesMask, &globalTypesMask, 1, MPI_UNSIGNED_LONG, MPI_BOR, SU2_MPI::GetComm()); const bool singleType = (globalTypesMask & (globalTypesMask - 1)) == 0; - const string name = marker.name.substr(0, 32); + const string& name = names[iMarker]; const cgsize_t range[2] = {cumulative + 1, cumulative + static_cast(nTotElem)}; const cgsize_t first = range[0] + static_cast(elemOffset); const cgsize_t last = first + static_cast(nLocalElem) - 1; diff --git a/SU2_CFD/src/output/filewriter/CParallelFileWriter.cpp b/SU2_CFD/src/output/filewriter/CParallelFileWriter.cpp index 15feb546df92..d0d3839b1c9e 100644 --- a/SU2_CFD/src/output/filewriter/CParallelFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CParallelFileWriter.cpp @@ -220,22 +220,21 @@ bool CFileWriter::OpenMPIFile(string val_filename, bool append){ /*--- Continue writing at the end of a file that exists, e.g. to add a zone to a multizone mesh file. ---*/ if (append) { - ierr = MPI_File_open(SU2_MPI::GetComm(), val_filename.c_str(), MPI_MODE_WRONLY, MPI_INFO_NULL, &fhw); - - /*--- If the file does not exist yet, for example the first zone written to a file of its own, it is - created below like any other new file. ---*/ - - if (ierr == MPI_SUCCESS) { - MPI_Offset fileEnd; - MPI_File_get_size(fhw, &fileEnd); - disp = fileEnd; + /*--- The file is created if it does not exist yet, for example for the first zone written to a file of its + own, and the writing starts at its end. ---*/ + ierr = MPI_File_open(SU2_MPI::GetComm(), val_filename.c_str(), MPI_MODE_CREATE|MPI_MODE_WRONLY, MPI_INFO_NULL, + &fhw); + if (ierr != MPI_SUCCESS) { + SU2_MPI::Error(string("Unable to open file ") + val_filename, CURRENT_FUNCTION); + } + MPI_Offset fileEnd; + MPI_File_get_size(fhw, &fileEnd); + disp = fileEnd; - fileSize = 0.0; - usedTime = 0; + fileSize = 0.0; + usedTime = 0; - return true; - } - MPI_File_close(&fhw); + return true; } /*--- All ranks open the file using MPI. Here, we try to open the file with @@ -247,7 +246,7 @@ bool CFileWriter::OpenMPIFile(string val_filename, bool append){ MPI_MODE_CREATE|MPI_MODE_EXCL|MPI_MODE_WRONLY, MPI_INFO_NULL, &fhw); if (ierr != MPI_SUCCESS) { - MPI_File_close(&fhw); + /*--- The file exists: the failed open gives no valid handle to close, the file is deleted and created again. ---*/ if (rank == 0) MPI_File_delete(val_filename.c_str(), MPI_INFO_NULL); ierr = MPI_File_open(SU2_MPI::GetComm(), val_filename.c_str(), From 4f6729ddf7a0517055f8a08b244627512fe6bfc0 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Wed, 30 Sep 2026 07:55:17 +0200 Subject: [PATCH 19/35] Run the regression tests with the develop branch of TestCases Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LEL91DW5WPbPwgFtCvHga6 --- .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 577defe20413..76eb60ebc8f8 100644 --- a/.github/workflows/regression.yml +++ b/.github/workflows/regression.yml @@ -233,7 +233,7 @@ jobs: uses: docker://ghcr.io/su2code/su2/test-su2:260405-0054 with: # -t -c - args: -b ${{github.ref}} -t develop -c fixCGNSOutput -s ${{matrix.testscript}} + args: -b ${{github.ref}} -t develop -c develop -s ${{matrix.testscript}} - name: Cleanup uses: docker://ghcr.io/su2code/su2/test-su2:260405-0054 with: @@ -282,7 +282,7 @@ jobs: uses: docker://ghcr.io/su2code/su2/test-su2:260405-0054 with: # -t -c - args: -b ${{github.ref}} -t develop -c fixCGNSOutput -s ${{matrix.testscript}} -a "--tapetests" + args: -b ${{github.ref}} -t develop -c develop -s ${{matrix.testscript}} -a "--tapetests" - name: Cleanup uses: docker://ghcr.io/su2code/su2/test-su2:260405-0054 with: @@ -330,7 +330,7 @@ jobs: PMIX_MCA_gds: hash with: # -t -c - args: -b ${{github.ref}} -t develop -c fixCGNSOutput -s ${{matrix.testscript}} -a "--tsan" + args: -b ${{github.ref}} -t develop -c develop -s ${{matrix.testscript}} -a "--tsan" - name: Cleanup uses: docker://ghcr.io/su2code/su2/test-su2-tsan:260405-0054 with: @@ -375,7 +375,7 @@ jobs: uses: docker://ghcr.io/su2code/su2/test-su2-asan:260405-0054 with: # -t -c - args: -b ${{github.ref}} -t develop -c fixCGNSOutput -s ${{matrix.testscript}} -a "--asan" + args: -b ${{github.ref}} -t develop -c develop -s ${{matrix.testscript}} -a "--asan" - name: Cleanup uses: docker://ghcr.io/su2code/su2/test-su2-asan:260405-0054 with: From 8a55fe999926785eb1cbb93f051a7da24a4f4fd6 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Fri, 2 Oct 2026 07:24:39 +0200 Subject: [PATCH 20/35] Make the CGNS zone names unique The names of the boundary sections, BCs and families of the CGNS output were made unique when two marker tags have the same first 32 characters, but the zone names were not: in SURFACE_CGNS each plotted marker is a zone, so two such markers gave two zones with the same name. The unique-name logic is moved to GetUniqueNames and used for the zone names too. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013UkNcoCEH8nFNrHWzhJCar --- .../output/filewriter/CCGNSFileWriter.hpp | 13 ++++++- .../src/output/filewriter/CCGNSFileWriter.cpp | 38 ++++++++++++------- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp b/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp index 60693a3abb10..345d269b4f17 100644 --- a/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp +++ b/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp @@ -83,6 +83,9 @@ class CCGNSFileWriter final : public CFileWriter { /*--- Max connectivity entries per section, so that readers using 32-bit sizes can read it. ---*/ static constexpr cgsize_t maxSectionEntries = std::numeric_limits::max(); + + /*--- Max length of the names of the CGNS nodes (zones, sections, BCs, families). ---*/ + static constexpr size_t maxNameLength = 32; #endif public: /*! @@ -135,10 +138,18 @@ class CCGNSFileWriter final : public CFileWriter { /*! * \brief Create a zone for the data currently held by the data sorter. - * \param[in] zoneName - Name of the zone. + * \param[in] zoneName - Name of the zone, at most maxNameLength characters. */ void InitializeZone(const string& zoneName); + /*! + * \brief Get the names of CGNS nodes named as the markers. Tags longer than maxNameLength characters are truncated, + * and a number is appended to a truncated tag equal to a previous name, so that the names are unique. + * \param[in] tags - Marker tags. + * \return The names, in the order of the tags. + */ + vector GetUniqueNames(const vector& tags) const; + /*! * \brief Write the boundary sections, BCs and families of the markers set with SetBoundaryMarkers. */ diff --git a/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp b/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp index 8f5702272dc0..d3846edb7a55 100644 --- a/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp @@ -54,11 +54,13 @@ void CCGNSFileWriter::WriteData(string val_filename) { if (surfaceMarkers.empty()) { WriteZone("Zone"); } else { - /*--- One zone per marker, the surface data is sorted again for each of them. ---*/ - for (const auto& marker : surfaceMarkers) { - dataSorter->SortConnectivity(config, geometry, vector{marker}); + /*--- One zone per marker, the surface data is sorted again for each of them. The zones are named as the + markers, with names made unique within the 32 characters of CGNS. ---*/ + const auto zoneNames = GetUniqueNames(surfaceMarkers); + for (size_t iMarker = 0; iMarker < surfaceMarkers.size(); ++iMarker) { + dataSorter->SortConnectivity(config, geometry, vector{surfaceMarkers[iMarker]}); dataSorter->SortOutputData(); - WriteZone(marker); + WriteZone(zoneNames[iMarker]); } } @@ -213,27 +215,35 @@ void CCGNSFileWriter::InitializeZone(const string& zoneName) { zoneData[1] = GlobalElem; zoneData[2] = 0; - CallCGNS( - cg_zone_write(cgnsFileID, cgnsBase, zoneName.substr(0, 32).c_str(), zoneData.data(), Unstructured, &cgnsZone)); + CallCGNS(cg_zone_write(cgnsFileID, cgnsBase, zoneName.c_str(), zoneData.data(), Unstructured, &cgnsZone)); } -void CCGNSFileWriter::WriteBoundaries() { +vector CCGNSFileWriter::GetUniqueNames(const vector& tags) const { /*--- CGNS names have at most 32 characters, longer tags are truncated. If two truncated tags are equal, a number is - appended to make the names of the sections, BCs and families unique. ---*/ + appended to make the names unique. ---*/ vector names; - for (const auto& marker : boundaryMarkers) { - string name = marker.name.substr(0, 32); + for (const auto& tag : tags) { + string name = tag.substr(0, maxNameLength); for (unsigned long n = 1; std::find(names.begin(), names.end(), name) != names.end(); n++) { const string suffix = "_" + to_string(n); - name = marker.name.substr(0, 32 - suffix.size()) + suffix; + name = tag.substr(0, maxNameLength - suffix.size()) + suffix; } - if (rank == MASTER_NODE && name != marker.name.substr(0, 32)) { - cout << "CGNS output: the marker " << marker.name << " is written as " << name - << " (names have at most 32 characters)." << endl; + if (rank == MASTER_NODE && name != tag.substr(0, maxNameLength)) { + cout << "CGNS output: the marker " << tag << " is written as " << name << " (names have at most " << maxNameLength + << " characters)." << endl; } names.push_back(name); } + return names; +} + +void CCGNSFileWriter::WriteBoundaries() { + /*--- The sections, BCs and families are named as the markers. ---*/ + + vector tags; + for (const auto& marker : boundaryMarkers) tags.push_back(marker.name); + const auto names = GetUniqueNames(tags); for (size_t iMarker = 0; iMarker < boundaryMarkers.size(); ++iMarker) { const auto& marker = boundaryMarkers[iMarker]; From b1df8ccb81a03dd0164737caa8ecb488c5e22a9e Mon Sep 17 00:00:00 2001 From: rois1995 Date: Fri, 2 Oct 2026 07:25:10 +0200 Subject: [PATCH 21/35] Add a helper for the offset of the data of a rank in the file writers The SU2 mesh writers (ASCII and binary), the CGNS writer and WriteMPIStringAll each gathered the local sizes of all ranks to compute the offset of the data of a rank and the total size. This is now done by CFileWriter::GetRankOffset, which returns both. The offsets and the file content are unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013UkNcoCEH8nFNrHWzhJCar --- .../include/output/filewriter/CFileWriter.hpp | 9 ++++++++ .../src/output/filewriter/CCGNSFileWriter.cpp | 23 ++++++------------- .../output/filewriter/CParallelFileWriter.cpp | 18 +++++++++------ .../filewriter/CSU2MeshBinaryFileWriter.cpp | 14 +++-------- .../output/filewriter/CSU2MeshFileWriter.cpp | 11 ++------- 5 files changed, 32 insertions(+), 43 deletions(-) diff --git a/SU2_CFD/include/output/filewriter/CFileWriter.hpp b/SU2_CFD/include/output/filewriter/CFileWriter.hpp index 7974386b5103..68daec23ffa5 100644 --- a/SU2_CFD/include/output/filewriter/CFileWriter.hpp +++ b/SU2_CFD/include/output/filewriter/CFileWriter.hpp @@ -33,6 +33,7 @@ #include #include #include +#include #include "../../output/filewriter/CParallelDataSorter.hpp" @@ -168,6 +169,14 @@ class CFileWriter{ */ bool WriteMPIString(const std::string& str, unsigned short processor); + /*! + * \brief Get the position of the data of this rank in an array that holds the data of all ranks in rank order. + * \note Collective call, all ranks must call it. + * \param[in] localCount - The size of the data of this rank. + * \return The offset of this rank (the size of the data of the ranks before it) and the total size over all ranks. + */ + std::pair GetRankOffset(unsigned long localCount) const; + /*! * \brief Write a string of each rank to the file, one after the other in rank order. * \param[in] str - The string of this rank. diff --git a/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp b/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp index d3846edb7a55..82ccf5f5281b 100644 --- a/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp @@ -31,7 +31,7 @@ #include "../../../../Common/include/geometry/CGeometry.hpp" #include -#include +#include const string CCGNSFileWriter::fileExt = ".cgns"; @@ -259,19 +259,14 @@ void CCGNSFileWriter::WriteBoundaries() { } const unsigned long nLocalEntries = marker.conn.size() - nLocalElem; - /*--- Sizes and offsets of the elements of each rank, which are written as a contiguous range. ---*/ + /*--- Offsets and totals of the elements and node ids of each rank, which are written as a contiguous range. ---*/ - vector elemPerRank(size), entriesPerRank(size); - SU2_MPI::Allgather(&nLocalElem, 1, MPI_UNSIGNED_LONG, elemPerRank.data(), 1, MPI_UNSIGNED_LONG, SU2_MPI::GetComm()); - SU2_MPI::Allgather(&nLocalEntries, 1, MPI_UNSIGNED_LONG, entriesPerRank.data(), 1, MPI_UNSIGNED_LONG, - SU2_MPI::GetComm()); + unsigned long elemOffset, nTotElem, entryOffset, nTotNodeEntries; + std::tie(elemOffset, nTotElem) = GetRankOffset(nLocalElem); + std::tie(entryOffset, nTotNodeEntries) = GetRankOffset(nLocalEntries); - const auto nTotElem = std::accumulate(elemPerRank.begin(), elemPerRank.end(), 0ul); if (nTotElem == 0) continue; - auto elemOffset = std::accumulate(elemPerRank.begin(), elemPerRank.begin() + rank, 0ul); - auto entryOffset = std::accumulate(entriesPerRank.begin(), entriesPerRank.begin() + rank, 0ul); - /*--- A marker with a single element type is written as a section of that type, one with several types (e.g. triangles and quadrilaterals) as a MIXED section. ---*/ @@ -307,7 +302,7 @@ void CCGNSFileWriter::WriteBoundaries() { /*--- The CGNS element type of each element is stored before the ids of its nodes, and the start offset of each element in the connectivity array is stored in a second array. ---*/ - const auto nTotEntries = std::accumulate(entriesPerRank.begin(), entriesPerRank.end(), 0ul) + nTotElem; + const auto nTotEntries = nTotNodeEntries + nTotElem; vector elems, offsets{static_cast(entryOffset + elemOffset)}; elems.reserve(marker.conn.size()); @@ -456,11 +451,7 @@ void CCGNSFileWriter::WriteConnectivity(GEO_TYPE type, const string& SectionName /*--- Retrieve element distribution among processes, the elements of a rank are a contiguous range. ---*/ const auto nLocalElem = dataSorter->GetnElem(type); - vector distElem(size); - SU2_MPI::Allgather(&nLocalElem, 1, MPI_UNSIGNED_LONG, distElem.data(), 1, MPI_UNSIGNED_LONG, SU2_MPI::GetComm()); - - cgsize_t firstElem = cumulative + 1; - for (int i = 0; i < rank; ++i) firstElem += static_cast(distElem[i]); + const cgsize_t firstElem = cumulative + 1 + static_cast(GetRankOffset(nLocalElem).first); const cgsize_t endElem = firstElem + static_cast(nLocalElem) - 1; /*--- Store the connectivity of this rank. ---*/ diff --git a/SU2_CFD/src/output/filewriter/CParallelFileWriter.cpp b/SU2_CFD/src/output/filewriter/CParallelFileWriter.cpp index d0d3839b1c9e..b3730941ee33 100644 --- a/SU2_CFD/src/output/filewriter/CParallelFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CParallelFileWriter.cpp @@ -276,19 +276,23 @@ bool CFileWriter::OpenMPIFile(string val_filename, bool append){ return true; } +std::pair CFileWriter::GetRankOffset(unsigned long localCount) const { + vector counts(size, localCount); + + SU2_MPI::Allgather(&localCount, 1, MPI_UNSIGNED_LONG, counts.data(), 1, MPI_UNSIGNED_LONG, SU2_MPI::GetComm()); + + return std::make_pair(std::accumulate(counts.begin(), counts.begin() + rank, 0ul), + std::accumulate(counts.begin(), counts.end(), 0ul)); +} + bool CFileWriter::WriteMPIStringAll(const string &str){ /*--- Each rank writes its own text at the position that follows the text of the ranks before it. ---*/ const unsigned long sizeInBytes = str.size(); - vector sizes(size, sizeInBytes); - - SU2_MPI::Allgather(&sizeInBytes, 1, MPI_UNSIGNED_LONG, sizes.data(), 1, MPI_UNSIGNED_LONG, SU2_MPI::GetComm()); - - const auto offsetInBytes = std::accumulate(sizes.begin(), sizes.begin() + rank, 0ul); - const auto totalSizeInBytes = std::accumulate(sizes.begin(), sizes.end(), 0ul); + const auto offsetAndTotal = GetRankOffset(sizeInBytes); - return WriteMPIBinaryDataAll(str.data(), sizeInBytes, totalSizeInBytes, offsetInBytes); + return WriteMPIBinaryDataAll(str.data(), sizeInBytes, offsetAndTotal.second, offsetAndTotal.first); } bool CFileWriter::CloseMPIFile(){ diff --git a/SU2_CFD/src/output/filewriter/CSU2MeshBinaryFileWriter.cpp b/SU2_CFD/src/output/filewriter/CSU2MeshBinaryFileWriter.cpp index 5b53385be621..c826eb09d238 100644 --- a/SU2_CFD/src/output/filewriter/CSU2MeshBinaryFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CSU2MeshBinaryFileWriter.cpp @@ -26,7 +26,6 @@ */ #include "../../../include/output/filewriter/CSU2MeshBinaryFileWriter.hpp" -#include #include #include "../../../../Common/include/toolboxes/printing_toolbox.hpp" @@ -104,13 +103,6 @@ void CSU2MeshBinaryFileWriter::WriteData(string val_filename) { /*--- Each rank writes the data of its own elements and points, at the position that follows the data of the ranks before it. The global offsets and indices of a rank are those of the ranks before it. ---*/ - auto offsetOfRank = [&](unsigned long localCount) { - vector counts(size, localCount); - SU2_MPI::Allgather(&localCount, 1, MPI_UNSIGNED_LONG, counts.data(), 1, MPI_UNSIGNED_LONG, SU2_MPI::GetComm()); - return std::make_pair(std::accumulate(counts.begin(), counts.begin() + rank, 0ul), - std::accumulate(counts.begin(), counts.end(), 0ul)); - }; - /*--- Section 1: element offsets, the starting connectivity-array position of each element, closed by the total size as sentinel offset[n_elem]. ---*/ @@ -121,10 +113,10 @@ void CSU2MeshBinaryFileWriter::WriteData(string val_filename) { } unsigned long connOffset, totalConnSize; - std::tie(connOffset, totalConnSize) = offsetOfRank(localConnSize); + std::tie(connOffset, totalConnSize) = GetRankOffset(localConnSize); unsigned long elemOffset, nElemGlobal; - std::tie(elemOffset, nElemGlobal) = offsetOfRank(localElemCount); + std::tie(elemOffset, nElemGlobal) = GetRankOffset(localElemCount); buffer.clear(); buffer.reserve(localElemCount * sizeof(conn_t)); @@ -168,7 +160,7 @@ void CSU2MeshBinaryFileWriter::WriteData(string val_filename) { WriteMPIString(buffer, MASTER_NODE); unsigned long pointOffset, nPointsTotal; - std::tie(pointOffset, nPointsTotal) = offsetOfRank(dataSorter->GetnPoints()); + std::tie(pointOffset, nPointsTotal) = GetRankOffset(dataSorter->GetnPoints()); buffer.clear(); buffer.reserve(dataSorter->GetnPoints() * (dataSorter->GetnDim() * sizeof(double) + sizeof(conn_t))); diff --git a/SU2_CFD/src/output/filewriter/CSU2MeshFileWriter.cpp b/SU2_CFD/src/output/filewriter/CSU2MeshFileWriter.cpp index baae36b48303..d33dc89d7355 100644 --- a/SU2_CFD/src/output/filewriter/CSU2MeshFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CSU2MeshFileWriter.cpp @@ -27,7 +27,6 @@ #include "../../../include/output/filewriter/CSU2MeshFileWriter.hpp" -#include #include "../../../../Common/include/toolboxes/printing_toolbox.hpp" const string CSU2MeshFileWriter::fileExt = ".su2"; @@ -58,12 +57,6 @@ void CSU2MeshFileWriter::WriteData(string val_filename) { strings to the file at the same time, one after the other in rank order. The global index of an element or point is its local index plus the number of elements or points of the ranks before this one. ---*/ - auto offsetOfRank = [&](unsigned long localCount) { - vector counts(size, localCount); - SU2_MPI::Allgather(&localCount, 1, MPI_UNSIGNED_LONG, counts.data(), 1, MPI_UNSIGNED_LONG, SU2_MPI::GetComm()); - return std::accumulate(counts.begin(), counts.begin() + rank, 0ul); - }; - ostringstream data; /*--- Write the connectivity, the type of each element is written before its nodes. ---*/ @@ -72,7 +65,7 @@ void CSU2MeshFileWriter::WriteData(string val_filename) { for (auto type : {TRIANGLE, QUADRILATERAL, TETRAHEDRON, HEXAHEDRON, PRISM, PYRAMID}) nElem += dataSorter->GetnElem(type); - unsigned long offset = offsetOfRank(nElem); + unsigned long offset = GetRankOffset(nElem).first; nElem = 0; for (auto type : {TRIANGLE, QUADRILATERAL, TETRAHEDRON, HEXAHEDRON, PRISM, PYRAMID}) { @@ -91,7 +84,7 @@ void CSU2MeshFileWriter::WriteData(string val_filename) { WriteMPIString("NPOIN= " + to_string(dataSorter->GetnPointsGlobal()) + "\n", MASTER_NODE); - offset = offsetOfRank(dataSorter->GetnPoints()); + offset = GetRankOffset(dataSorter->GetnPoints()).first; data.str(""); data.clear(); From cdc2f065761a7642bf0159b612e501018b054f7e Mon Sep 17 00:00:00 2001 From: rois1995 Date: Fri, 2 Oct 2026 07:25:27 +0200 Subject: [PATCH 22/35] Keep typed pointers to the CGNS and VTM file writers in COutput The CGNS (volume and surface) and Paraview multiblock writers were created as CFileWriter* and cast back to their type to set the markers or write the folder data. The writer is now created with its own type, used, and then assigned to fileWriter. No change of behaviour. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013UkNcoCEH8nFNrHWzhJCar --- SU2_CFD/src/output/COutput.cpp | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/SU2_CFD/src/output/COutput.cpp b/SU2_CFD/src/output/COutput.cpp index 5081889b7373..f503128a6727 100644 --- a/SU2_CFD/src/output/COutput.cpp +++ b/SU2_CFD/src/output/COutput.cpp @@ -594,10 +594,9 @@ void COutput::WriteToFile(CConfig *config, CGeometry *geometry, OUTPUT_TYPE form volumeDataSorter->SortConnectivity(config, geometry, true); LogOutputFiles("Paraview Multiblock"); - fileWriter = new CParaviewVTMFileWriter(GetHistoryFieldValue("CUR_TIME"), config->GetiZone(), config->GetnZone()); - - /*--- We cast the pointer to its true type, to avoid virtual functions ---*/ - auto* vtmWriter = dynamic_cast(fileWriter); + auto* vtmWriter = + new CParaviewVTMFileWriter(GetHistoryFieldValue("CUR_TIME"), config->GetiZone(), config->GetnZone()); + fileWriter = vtmWriter; /*--- then we write the data into the folder---*/ vtmWriter->WriteFolderData(fileName, config, multiZoneHeaderString, volumeDataSorter, surfaceDataSorter, geometry); @@ -760,12 +759,15 @@ void COutput::WriteToFile(CConfig *config, CGeometry *geometry, OUTPUT_TYPE form volumeDataSorter->SortConnectivity(config, geometry, true); LogOutputFiles("CGNS"); - fileWriter = new CCGNSFileWriter(volumeDataSorter, false, config->GetWrt_Output_Double_Precision()); + { + auto* cgnsWriter = new CCGNSFileWriter(volumeDataSorter, false, config->GetWrt_Output_Double_Precision()); - /*--- Add the boundaries, named as the markers (the finite volume sorter knows which elements are halos). ---*/ - if (const auto* fvmSorter = dynamic_cast(volumeDataSorter)) - static_cast(fileWriter)->SetBoundaryMarkers(config, geometry, fvmSorter); + /*--- Add the boundaries, named as the markers (the finite volume sorter knows which elements are halos). ---*/ + if (const auto* fvmSorter = dynamic_cast(volumeDataSorter)) + cgnsWriter->SetBoundaryMarkers(config, geometry, fvmSorter); + fileWriter = cgnsWriter; + } break; case OUTPUT_TYPE::SURFACE_CGNS: @@ -783,11 +785,14 @@ void COutput::WriteToFile(CConfig *config, CGeometry *geometry, OUTPUT_TYPE form surfaceDataSorter->SortOutputData(); LogOutputFiles("CGNS surface"); - fileWriter = new CCGNSFileWriter(surfaceDataSorter, true, config->GetWrt_Output_Double_Precision()); + { + auto* cgnsWriter = new CCGNSFileWriter(surfaceDataSorter, true, config->GetWrt_Output_Double_Precision()); - /*--- One zone per plotted marker, named as the marker. ---*/ - static_cast(fileWriter)->SetSurfaceMarkers(config, geometry); + /*--- One zone per plotted marker, named as the marker. ---*/ + cgnsWriter->SetSurfaceMarkers(config, geometry); + fileWriter = cgnsWriter; + } break; default: From 1018badcad5c482b882a0d0f88f2372bfe9f2bde Mon Sep 17 00:00:00 2001 From: rois1995 Date: Fri, 2 Oct 2026 07:25:56 +0200 Subject: [PATCH 23/35] Take const config and geometry in CCGNSFileWriter::SetBoundaryMarkers SetBoundaryMarkers only reads the markers and the boundary elements, so it takes const pointers. SetSurfaceMarkers keeps non-const pointers: it stores them for the sorting of the surface data of each marker when the file is written, and SortConnectivity takes non-const pointers in all data sorters. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013UkNcoCEH8nFNrHWzhJCar --- SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp | 2 +- SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp b/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp index 345d269b4f17..c9baa088285c 100644 --- a/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp +++ b/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp @@ -112,7 +112,7 @@ class CCGNSFileWriter final : public CFileWriter { * \param[in] valGeometry - Geometrical definition of the problem. * \param[in] volumeSorter - The volume data sorter, to find the boundary elements owned by this rank. */ - void SetBoundaryMarkers(CConfig* valConfig, CGeometry* valGeometry, const CFVMDataSorter* volumeSorter); + void SetBoundaryMarkers(const CConfig* valConfig, const CGeometry* valGeometry, const CFVMDataSorter* volumeSorter); /*! * \brief Write a surface file with one zone per plotted marker, named as the marker. The data of the surface diff --git a/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp b/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp index 82ccf5f5281b..f77f932d471e 100644 --- a/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp @@ -80,7 +80,7 @@ void CCGNSFileWriter::WriteData(string val_filename) { #endif } -void CCGNSFileWriter::SetBoundaryMarkers(CConfig* valConfig, CGeometry* valGeometry, +void CCGNSFileWriter::SetBoundaryMarkers(const CConfig* valConfig, const CGeometry* valGeometry, const CFVMDataSorter* volumeSorter) { boundaryMarkers.clear(); From ffe588f0ec9c2ec0a52f6fd9ea395e9a9cc2fa90 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Fri, 2 Oct 2026 07:28:26 +0200 Subject: [PATCH 24/35] Template the type conversions of the Paraview XML writer The coordinates and fields are staged as double and written as float in single precision, the connectivity and offsets are staged as int64_t and written as int32_t below the Int32 limit. Each case copied the buffer by hand. WriteDataArrayOfType now writes a staged buffer as T, converting it only when its type differs. The written files are unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013UkNcoCEH8nFNrHWzhJCar --- .../filewriter/CParaviewXMLFileWriter.hpp | 16 ++++++++- .../filewriter/CParaviewXMLFileWriter.cpp | 33 ++++++++++++------- 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/SU2_CFD/include/output/filewriter/CParaviewXMLFileWriter.hpp b/SU2_CFD/include/output/filewriter/CParaviewXMLFileWriter.hpp index 4528ec76f776..f486857439ba 100644 --- a/SU2_CFD/include/output/filewriter/CParaviewXMLFileWriter.hpp +++ b/SU2_CFD/include/output/filewriter/CParaviewXMLFileWriter.hpp @@ -109,7 +109,21 @@ class CParaviewXMLFileWriter final: public CFileWriter{ * \param[in] globalSize - The global size of the array over all processors * \param[in] offset - The displacement in the file view for the current processor */ - void WriteDataArray(void *data, VTKDatatype type, unsigned long size, unsigned long globalSize, unsigned long offset); + void WriteDataArray(const void* data, VTKDatatype type, unsigned long size, unsigned long globalSize, + unsigned long offset); + + /*! + * \brief Write the first values of a buffer as an array of type T with ::WriteDataArray, the values are converted + * if the buffer holds another type (e.g. double written as float, int64_t written as int32_t). + * \param[in] buffer - The data of this processor. + * \param[in] type - The vtk datatype, matching T. + * \param[in] size - The number of values of this processor to write. + * \param[in] globalSize - The global size of the array over all processors + * \param[in] offset - The displacement in the file view for the current processor + */ + template + void WriteDataArrayOfType(const vector& buffer, VTKDatatype type, unsigned long size, unsigned long globalSize, + unsigned long offset); /*! * \brief Get the type string and size of a VTK datatype diff --git a/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp b/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp index fedce3b4848f..3d521acd1ffd 100644 --- a/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp @@ -29,6 +29,7 @@ #include "../../../../Common/include/toolboxes/printing_toolbox.hpp" #include #include +#include const string CParaviewXMLFileWriter::fileExt = ".vtu"; @@ -48,6 +49,17 @@ CParaviewXMLFileWriter::CParaviewXMLFileWriter(CParallelDataSorter *valDataSorte CParaviewXMLFileWriter::~CParaviewXMLFileWriter()= default; +template +void CParaviewXMLFileWriter::WriteDataArrayOfType(const vector& buffer, VTKDatatype type, unsigned long size, + unsigned long globalSize, unsigned long offset) { + if constexpr (std::is_same::value) { + WriteDataArray(buffer.data(), type, size, globalSize, offset); + } else { + const vector converted(buffer.begin(), buffer.begin() + size); + WriteDataArray(converted.data(), type, size, globalSize, offset); + } +} + void CParaviewXMLFileWriter::WriteData(string val_filename){ if (!dataSorter->GetConnectivitySorted()){ @@ -203,10 +215,9 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ auto writeRealArray = [&](unsigned long size, unsigned long globalSize, unsigned long offset) { if (realType == VTKDatatype::FLOAT64) { - WriteDataArray(dataBuffer.data(), realType, size, globalSize, offset); + WriteDataArrayOfType(dataBuffer, realType, size, globalSize, offset); } else { - vector buffer(dataBuffer.begin(), dataBuffer.begin() + size); - WriteDataArray(buffer.data(), realType, size, globalSize, offset); + WriteDataArrayOfType(dataBuffer, realType, size, globalSize, offset); } }; @@ -248,15 +259,13 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ copyToBuffer(PYRAMID, nParallel_Pyra, N_POINTS_PYRAMID); if (connInt64) { - WriteDataArray(connBuf.data(), connType, myElemStorage, GlobalElemStorage, - dataSorter->GetnElemConnCumulative(rank)); - WriteDataArray(offsetBuf.data(), connType, myElem, GlobalElem, dataSorter->GetnElemCumulative(rank)); + WriteDataArrayOfType(connBuf, connType, myElemStorage, GlobalElemStorage, + dataSorter->GetnElemConnCumulative(rank)); + WriteDataArrayOfType(offsetBuf, connType, myElem, GlobalElem, dataSorter->GetnElemCumulative(rank)); } else { - vector connBuf32(connBuf.begin(), connBuf.end()); - vector offsetBuf32(offsetBuf.begin(), offsetBuf.end()); - WriteDataArray(connBuf32.data(), connType, myElemStorage, GlobalElemStorage, - dataSorter->GetnElemConnCumulative(rank)); - WriteDataArray(offsetBuf32.data(), connType, myElem, GlobalElem, dataSorter->GetnElemCumulative(rank)); + WriteDataArrayOfType(connBuf, connType, myElemStorage, GlobalElemStorage, + dataSorter->GetnElemConnCumulative(rank)); + WriteDataArrayOfType(offsetBuf, connType, myElem, GlobalElem, dataSorter->GetnElemCumulative(rank)); } /*--- Load/write the cell type for all elements in the file. ---*/ @@ -344,7 +353,7 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ } -void CParaviewXMLFileWriter::WriteDataArray(void* data, VTKDatatype type, unsigned long arraySize, +void CParaviewXMLFileWriter::WriteDataArray(const void* data, VTKDatatype type, unsigned long arraySize, unsigned long globalSize, unsigned long offset){ std::string typeStr; From 393f9dead1e2656a9a353c78bebefdc9588b3ae7 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Fri, 2 Oct 2026 07:28:59 +0200 Subject: [PATCH 25/35] Remove the options not needed by the ASCII output writer tests The ASCII writer tests write the solution of the flat plate without iterating (ITER= 0), so the linear solver, limiter, MUSCL, time discretization, convergence, CFL, reference value and history options are not used. Only the physics, the markers, the restart, the gradient method and the convective schemes (required by the numerics preprocessing) are kept. The six written files are unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013UkNcoCEH8nFNrHWzhJCar --- TestCases/output_writers/ascii_output.cfg | 40 +---------------------- 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/TestCases/output_writers/ascii_output.cfg b/TestCases/output_writers/ascii_output.cfg index ac3685f4163f..d14519b58f55 100644 --- a/TestCases/output_writers/ascii_output.cfg +++ b/TestCases/output_writers/ascii_output.cfg @@ -14,18 +14,11 @@ RESTART_SOL= YES % ----------- COMPRESSIBLE AND INCOMPRESSIBLE FREE-STREAM DEFINITION ----------% MACH_NUMBER= 0.2 AOA= 0.0 -SIDESLIP_ANGLE= 0.0 FREESTREAM_TEMPERATURE= 300.0 REYNOLDS_NUMBER= 5000000.0 REYNOLDS_LENGTH= 1.0 FREESTREAM_TURBULENCEINTENSITY= 0.00038729 FREESTREAM_TURB2LAMVISCRATIO= 0.009 -% ---------------------- REFERENCE VALUE DEFINITION ---------------------------% -REF_ORIGIN_MOMENT_X= 0.25 -REF_ORIGIN_MOMENT_Y= 0.00 -REF_ORIGIN_MOMENT_Z= 0.00 -REF_LENGTH= 1.0 -REF_AREA= 2.0 % -------------------- BOUNDARY CONDITION DEFINITION --------------------------% MARKER_HEATFLUX= ( wall, 0.0 ) MARKER_FAR= ( farfield ) @@ -33,48 +26,17 @@ MARKER_INLET= ( inlet, 302.4, 117691.7874, 1.0, 0.0, 0.0 ) MARKER_OUTLET= ( outlet, 114455.0 ) MARKER_SYM= ( symmetry ) MARKER_PLOTTING= ( wall ) -MARKER_MONITORING= ( wall ) % ------------- COMMON PARAMETERS DEFINING THE NUMERICAL METHOD ---------------% NUM_METHOD_GRAD= WEIGHTED_LEAST_SQUARES -CFL_NUMBER= 400.0 -CFL_ADAPT= NO -ITER= 0 -% ------------------------ LINEAR SOLVER DEFINITION ---------------------------% -LINEAR_SOLVER= FGMRES -LINEAR_SOLVER_PREC= ILU -LINEAR_SOLVER_ILU_FILL_IN= 0 -LINEAR_SOLVER_ERROR= 1E-15 -LINEAR_SOLVER_ITER= 25 -% ----------------------- SLOPE LIMITER DEFINITION ----------------------------% -VENKAT_LIMITER_COEFF= 0.1 -ADJ_SHARP_LIMITER_COEFF= 3.0 -REF_SHARP_EDGES= 3.0 -SENS_REMOVE_SHARP= NO -% -------------------- FLOW NUMERICAL METHOD DEFINITION -----------------------% CONV_NUM_METHOD_FLOW= ROE -MUSCL_FLOW= YES -MUSCL_KAPPA_FLOW= 0.5 -SLOPE_LIMITER_FLOW= NONE -TIME_DISCRE_FLOW= EULER_IMPLICIT -% -------------------- TURBULENT NUMERICAL METHOD DEFINITION ------------------% CONV_NUM_METHOD_TURB= SCALAR_UPWIND -MUSCL_TURB= NO -MUSCL_KAPPA_TURB= 0.0 -TIME_DISCRE_TURB= EULER_IMPLICIT -% --------------------------- CONVERGENCE PARAMETERS --------------------------% -CONV_FIELD= RMS_DENSITY -CONV_RESIDUAL_MINVAL= -13 +ITER= 0 % ------------------------- INPUT/OUTPUT INFORMATION --------------------------% MESH_FILENAME= ../vandv/rans/flatplate/mesh_flatplate_turb_035x025.su2 MESH_FORMAT= SU2 MESH_OUT_FILENAME= mesh_out SOLUTION_FILENAME= ../vandv/rans/flatplate/solution_flow -TABULAR_FORMAT= CSV -CONV_FILENAME= history RESTART_FILENAME= restart_flow VOLUME_FILENAME= flow SURFACE_FILENAME= surface_flow -SCREEN_OUTPUT= INNER_ITER WALL_TIME RMS_RES DRAG OUTPUT_FILES= (RESTART_ASCII, PARAVIEW_ASCII, TECPLOT_ASCII, SURFACE_PARAVIEW_ASCII, SURFACE_TECPLOT_ASCII) -OUTPUT_WRT_FREQ = 10000 -HISTORY_OUTPUT= ITER RMS_RES AERO_COEFF From feed110e0c7c0fc902e69e98a396e3f2cf6da6a4 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Fri, 2 Oct 2026 07:32:23 +0200 Subject: [PATCH 26/35] Add regression tests that read the CGNS output back as mesh The CGNS output was only tested by cgns_writer, which checks the residuals of the run but not the content of the written file. Two pairs of tests are added: cgns_output writes the volume and surface CGNS files and the restart of the 2D flat plate, then cgns_mesh_readback reads the volume file as mesh (MESH_FORMAT= CGNS) and restarts from that restart. cgns_output_bend and cgns_mesh_readback_bend do the same on the 3D bend mesh (tetrahedra, pyramids, prisms, hexahedra), whose boundaries are written as MIXED sections. The read-back residuals are the same as those of the restart on the original mesh, so the coordinates, the connectivity, the order of the points and the named boundary markers of the file are checked. The tests are in both the parallel (cgp_* API) and the serial (cg_* API) regressions. The meshes are those already in the TestCases repository, no new data is needed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013UkNcoCEH8nFNrHWzhJCar --- .../output_writers/cgns_mesh_readback.cfg | 53 +++++++++++++++++ .../cgns_mesh_readback_bend.cfg | 50 ++++++++++++++++ TestCases/output_writers/cgns_output.cfg | 58 +++++++++++++++++++ TestCases/output_writers/cgns_output_bend.cfg | 56 ++++++++++++++++++ TestCases/parallel_regression.py | 31 ++++++++++ TestCases/serial_regression.py | 35 +++++++++++ 6 files changed, 283 insertions(+) create mode 100644 TestCases/output_writers/cgns_mesh_readback.cfg create mode 100644 TestCases/output_writers/cgns_mesh_readback_bend.cfg create mode 100644 TestCases/output_writers/cgns_output.cfg create mode 100644 TestCases/output_writers/cgns_output_bend.cfg diff --git a/TestCases/output_writers/cgns_mesh_readback.cfg b/TestCases/output_writers/cgns_mesh_readback.cfg new file mode 100644 index 000000000000..9b3cf5d0af30 --- /dev/null +++ b/TestCases/output_writers/cgns_mesh_readback.cfg @@ -0,0 +1,53 @@ +% Read back the volume CGNS file written by cgns_output.cfg (run that test first) as mesh, and restart +% from the restart file written by the same run. The boundaries of the file are named as the markers, +% the points are in the order of the restart file. The residuals are those of the same restart on +% the original SU2 mesh. +% +% ------------- 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 ) +MARKER_MONITORING= ( wall ) +% ------------- COMMON PARAMETERS DEFINING THE NUMERICAL METHOD ---------------% +NUM_METHOD_GRAD= WEIGHTED_LEAST_SQUARES +CFL_NUMBER= 10.0 +CFL_ADAPT= NO +ITER= 3 +% ------------------------ LINEAR SOLVER DEFINITION ---------------------------% +LINEAR_SOLVER= FGMRES +LINEAR_SOLVER_PREC= ILU +LINEAR_SOLVER_ERROR= 1E-6 +LINEAR_SOLVER_ITER= 10 +% -------------------- FLOW NUMERICAL METHOD DEFINITION -----------------------% +CONV_NUM_METHOD_FLOW= ROE +MUSCL_FLOW= YES +SLOPE_LIMITER_FLOW= NONE +TIME_DISCRE_FLOW= EULER_IMPLICIT +% -------------------- TURBULENT NUMERICAL METHOD DEFINITION ------------------% +CONV_NUM_METHOD_TURB= SCALAR_UPWIND +MUSCL_TURB= NO +TIME_DISCRE_TURB= EULER_IMPLICIT +% ------------------------- INPUT/OUTPUT INFORMATION --------------------------% +MESH_FILENAME= cgns_flow.cgns +MESH_FORMAT= CGNS +SOLUTION_FILENAME= cgns_restart_flow +CONV_FILENAME= cgns_readback_history +SCREEN_OUTPUT= INNER_ITER WALL_TIME RMS_DENSITY RMS_MOMENTUM-X RMS_ENERGY RMS_TKE RMS_DISSIPATION DRAG +HISTORY_OUTPUT= ITER RMS_RES AERO_COEFF +OUTPUT_FILES= NONE diff --git a/TestCases/output_writers/cgns_mesh_readback_bend.cfg b/TestCases/output_writers/cgns_mesh_readback_bend.cfg new file mode 100644 index 000000000000..c9ebaeb49b5f --- /dev/null +++ b/TestCases/output_writers/cgns_mesh_readback_bend.cfg @@ -0,0 +1,50 @@ +% Read back the volume CGNS file written by cgns_output_bend.cfg (run that test first) as mesh, and +% restart from the restart file written by the same run. The boundaries of the file are named as the +% markers, the points are in the order of the restart file. The residuals are those of the same +% restart on the original mesh. +% +% ------------- DIRECT, ADJOINT, AND LINEARIZED PROBLEM DEFINITION ------------% +SOLVER= INC_NAVIER_STOKES +KIND_TURB_MODEL= NONE +RESTART_SOL= YES +% ---------------- INCOMPRESSIBLE FLOW CONDITION DEFINITION -------------------% +INC_DENSITY_MODEL= CONSTANT +INC_ENERGY_EQUATION= NO +INC_DENSITY_INIT= 1.2886 +INC_VELOCITY_INIT= ( 0.1, 0.0, 0.0 ) +INC_TEMPERATURE_INIT= 288.15 +INC_NONDIM= INITIAL_VALUES +INC_INLET_TYPE= VELOCITY_INLET +INC_OUTLET_TYPE= PRESSURE_OUTLET +% --------------------------- VISCOSITY MODEL ---------------------------------% +VISCOSITY_MODEL= CONSTANT_VISCOSITY +MU_CONSTANT= 1.716E-5 +% -------------------- BOUNDARY CONDITION DEFINITION --------------------------% +MARKER_HEATFLUX= ( WALL1, 0.0, WALL2, 0.0 ) +MARKER_INLET= ( INLET, 288.15, 0.1, 1.0, 0.0, 0.0 ) +MARKER_OUTLET= ( OUTLET, 0.0 ) +MARKER_SYM= ( SYMMETRY ) +MARKER_PLOTTING= ( WALL1 ) +MARKER_MONITORING= ( WALL1, WALL2 ) +% ------------- COMMON PARAMETERS DEFINING THE NUMERICAL METHOD ---------------% +NUM_METHOD_GRAD= GREEN_GAUSS +CFL_NUMBER= 100.0 +ITER= 3 +% ------------------------ LINEAR SOLVER DEFINITION ---------------------------% +LINEAR_SOLVER= FGMRES +LINEAR_SOLVER_PREC= ILU +LINEAR_SOLVER_ERROR= 1E-6 +LINEAR_SOLVER_ITER= 10 +% -------------------- FLOW NUMERICAL METHOD DEFINITION -----------------------% +CONV_NUM_METHOD_FLOW= FDS +MUSCL_FLOW= YES +SLOPE_LIMITER_FLOW= NONE +TIME_DISCRE_FLOW= EULER_IMPLICIT +% ------------------------- INPUT/OUTPUT INFORMATION --------------------------% +MESH_FILENAME= cgns_flow_bend.cgns +MESH_FORMAT= CGNS +SOLUTION_FILENAME= cgns_restart_flow_bend +CONV_FILENAME= cgns_readback_history_bend +SCREEN_OUTPUT= INNER_ITER WALL_TIME RMS_PRESSURE RMS_VELOCITY-X RMS_VELOCITY-Y RMS_VELOCITY-Z DRAG +HISTORY_OUTPUT= ITER RMS_RES AERO_COEFF +OUTPUT_FILES= NONE diff --git a/TestCases/output_writers/cgns_output.cfg b/TestCases/output_writers/cgns_output.cfg new file mode 100644 index 000000000000..d57b9552d278 --- /dev/null +++ b/TestCases/output_writers/cgns_output.cfg @@ -0,0 +1,58 @@ +% Test of the CGNS output writer, which writes in parallel with the cgp_* API. +% The flat plate of vandv/rans/flatplate is run for a few iterations from the free stream. At the end, +% SU2_CFD writes the volume CGNS file (boundaries named as the markers), the surface CGNS file +% (one zone per plotted marker) and the restart file. +% The test cgns_mesh_readback.cfg then reads the volume CGNS file as mesh and restarts from this +% restart file, which checks the coordinates, the connectivity, the order of the points and the +% boundary markers written in the CGNS file. +% +% ------------- DIRECT, ADJOINT, AND LINEARIZED PROBLEM DEFINITION ------------% +SOLVER= RANS +KIND_TURB_MODEL= SST +SST_OPTIONS= V1994m +RESTART_SOL= NO +% ----------- 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, symmetry ) +MARKER_MONITORING= ( wall ) +% ------------- COMMON PARAMETERS DEFINING THE NUMERICAL METHOD ---------------% +NUM_METHOD_GRAD= WEIGHTED_LEAST_SQUARES +CFL_NUMBER= 10.0 +CFL_ADAPT= NO +ITER= 3 +% ------------------------ LINEAR SOLVER DEFINITION ---------------------------% +LINEAR_SOLVER= FGMRES +LINEAR_SOLVER_PREC= ILU +LINEAR_SOLVER_ERROR= 1E-6 +LINEAR_SOLVER_ITER= 10 +% -------------------- FLOW NUMERICAL METHOD DEFINITION -----------------------% +CONV_NUM_METHOD_FLOW= ROE +MUSCL_FLOW= YES +SLOPE_LIMITER_FLOW= NONE +TIME_DISCRE_FLOW= EULER_IMPLICIT +% -------------------- TURBULENT NUMERICAL METHOD DEFINITION ------------------% +CONV_NUM_METHOD_TURB= SCALAR_UPWIND +MUSCL_TURB= NO +TIME_DISCRE_TURB= EULER_IMPLICIT +% ------------------------- INPUT/OUTPUT INFORMATION --------------------------% +MESH_FILENAME= ../vandv/rans/flatplate/mesh_flatplate_turb_035x025.su2 +MESH_FORMAT= SU2 +RESTART_FILENAME= cgns_restart_flow +VOLUME_FILENAME= cgns_flow +SURFACE_FILENAME= cgns_surface_flow +CONV_FILENAME= cgns_history +SCREEN_OUTPUT= INNER_ITER WALL_TIME RMS_DENSITY RMS_MOMENTUM-X RMS_ENERGY RMS_TKE RMS_DISSIPATION DRAG +HISTORY_OUTPUT= ITER RMS_RES AERO_COEFF +OUTPUT_FILES= (RESTART, CGNS, SURFACE_CGNS) diff --git a/TestCases/output_writers/cgns_output_bend.cfg b/TestCases/output_writers/cgns_output_bend.cfg new file mode 100644 index 000000000000..d3d021661bb1 --- /dev/null +++ b/TestCases/output_writers/cgns_output_bend.cfg @@ -0,0 +1,56 @@ +% Test of the CGNS output writer on a 3D mesh with mixed elements (tetrahedra, pyramids, prisms and +% hexahedra; some markers have both triangles and quadrilaterals, which are written as MIXED sections). +% The laminar bend of incomp_navierstokes/bend is run for a few iterations. At the end, SU2_CFD writes +% the volume CGNS file (boundaries named as the markers), the surface CGNS file (one zone per plotted +% marker) and the restart file. +% The test cgns_mesh_readback_bend.cfg then reads the volume CGNS file as mesh and restarts from this +% restart file, which checks the coordinates, the connectivity, the order of the points and the +% boundary markers written in the CGNS file. +% +% ------------- DIRECT, ADJOINT, AND LINEARIZED PROBLEM DEFINITION ------------% +SOLVER= INC_NAVIER_STOKES +KIND_TURB_MODEL= NONE +RESTART_SOL= NO +% ---------------- INCOMPRESSIBLE FLOW CONDITION DEFINITION -------------------% +INC_DENSITY_MODEL= CONSTANT +INC_ENERGY_EQUATION= NO +INC_DENSITY_INIT= 1.2886 +INC_VELOCITY_INIT= ( 0.1, 0.0, 0.0 ) +INC_TEMPERATURE_INIT= 288.15 +INC_NONDIM= INITIAL_VALUES +INC_INLET_TYPE= VELOCITY_INLET +INC_OUTLET_TYPE= PRESSURE_OUTLET +% --------------------------- VISCOSITY MODEL ---------------------------------% +VISCOSITY_MODEL= CONSTANT_VISCOSITY +MU_CONSTANT= 1.716E-5 +% -------------------- BOUNDARY CONDITION DEFINITION --------------------------% +MARKER_HEATFLUX= ( WALL1, 0.0, WALL2, 0.0 ) +MARKER_INLET= ( INLET, 288.15, 0.1, 1.0, 0.0, 0.0 ) +MARKER_OUTLET= ( OUTLET, 0.0 ) +MARKER_SYM= ( SYMMETRY ) +MARKER_PLOTTING= ( WALL1, WALL2, SYMMETRY ) +MARKER_MONITORING= ( WALL1, WALL2 ) +% ------------- COMMON PARAMETERS DEFINING THE NUMERICAL METHOD ---------------% +NUM_METHOD_GRAD= GREEN_GAUSS +CFL_NUMBER= 100.0 +ITER= 3 +% ------------------------ LINEAR SOLVER DEFINITION ---------------------------% +LINEAR_SOLVER= FGMRES +LINEAR_SOLVER_PREC= ILU +LINEAR_SOLVER_ERROR= 1E-6 +LINEAR_SOLVER_ITER= 10 +% -------------------- FLOW NUMERICAL METHOD DEFINITION -----------------------% +CONV_NUM_METHOD_FLOW= FDS +MUSCL_FLOW= YES +SLOPE_LIMITER_FLOW= NONE +TIME_DISCRE_FLOW= EULER_IMPLICIT +% ------------------------- INPUT/OUTPUT INFORMATION --------------------------% +MESH_FILENAME= ../incomp_navierstokes/bend/mesh_bend_coarse.cgns +MESH_FORMAT= CGNS +RESTART_FILENAME= cgns_restart_flow_bend +VOLUME_FILENAME= cgns_flow_bend +SURFACE_FILENAME= cgns_surface_flow_bend +CONV_FILENAME= cgns_history_bend +SCREEN_OUTPUT= INNER_ITER WALL_TIME RMS_PRESSURE RMS_VELOCITY-X RMS_VELOCITY-Y RMS_VELOCITY-Z DRAG +HISTORY_OUTPUT= ITER RMS_RES AERO_COEFF +OUTPUT_FILES= (RESTART, CGNS, SURFACE_CGNS) diff --git a/TestCases/parallel_regression.py b/TestCases/parallel_regression.py index 6b9c4a854e76..b2244e0f433c 100755 --- a/TestCases/parallel_regression.py +++ b/TestCases/parallel_regression.py @@ -1885,6 +1885,37 @@ def main(): cgns_writer.new_output = True test_list.append(cgns_writer) + # The CGNS output of a 2D mesh is read back as mesh, with the restart written by the same run. + # The read-back test must run after the test that writes the files. + cgns_output = TestCase('cgns_output') + cgns_output.cfg_dir = "output_writers" + cgns_output.cfg_file = "cgns_output.cfg" + cgns_output.test_iter = 2 + cgns_output.test_vals = [-1.684071, 0.454658, 3.802834, -2.624573, 6.956844, 0.091480] + test_list.append(cgns_output) + + cgns_mesh_readback = TestCase('cgns_mesh_readback') + cgns_mesh_readback.cfg_dir = "output_writers" + cgns_mesh_readback.cfg_file = "cgns_mesh_readback.cfg" + cgns_mesh_readback.test_iter = 2 + cgns_mesh_readback.test_vals = [-2.480775, 0.352445, 2.998807, -3.103293, 6.892515, 0.085107] + test_list.append(cgns_mesh_readback) + + # Same for a 3D mesh with mixed elements, whose boundaries are written as MIXED sections. + cgns_output_bend = TestCase('cgns_output_bend') + cgns_output_bend.cfg_dir = "output_writers" + cgns_output_bend.cfg_file = "cgns_output_bend.cfg" + cgns_output_bend.test_iter = 2 + cgns_output_bend.test_vals = [-2.413023, -2.210141, -2.545448, -3.057828, 2.475696] + test_list.append(cgns_output_bend) + + cgns_mesh_readback_bend = TestCase('cgns_mesh_readback_bend') + cgns_mesh_readback_bend.cfg_dir = "output_writers" + cgns_mesh_readback_bend.cfg_file = "cgns_mesh_readback_bend.cfg" + cgns_mesh_readback_bend.test_iter = 2 + cgns_mesh_readback_bend.test_vals = [-3.256728, -2.769844, -3.007840, -3.567315, 4.318459] + test_list.append(cgns_mesh_readback_bend) + ###################################### ### RUN CHT TEST WITH FILEDIFF ### ###################################### diff --git a/TestCases/serial_regression.py b/TestCases/serial_regression.py index c34fe6f47225..cd7473d0c30e 100755 --- a/TestCases/serial_regression.py +++ b/TestCases/serial_regression.py @@ -1288,6 +1288,41 @@ def main(): mms_dg_ns_3d.tol = 0.0001 test_list.append(mms_dg_ns_3d) + ###################################### + ### CGNS writer ### + ###################################### + + # The CGNS output of a 2D mesh is read back as mesh, with the restart written by the same run. + # The read-back test must run after the test that writes the files. + cgns_output = TestCase('cgns_output') + cgns_output.cfg_dir = "output_writers" + cgns_output.cfg_file = "cgns_output.cfg" + cgns_output.test_iter = 2 + cgns_output.test_vals = [-1.684071, 0.454658, 3.802835, -2.624573, 6.956844, 0.091480] + test_list.append(cgns_output) + + cgns_mesh_readback = TestCase('cgns_mesh_readback') + cgns_mesh_readback.cfg_dir = "output_writers" + cgns_mesh_readback.cfg_file = "cgns_mesh_readback.cfg" + cgns_mesh_readback.test_iter = 2 + cgns_mesh_readback.test_vals = [-2.480775, 0.352445, 2.998807, -3.103293, 6.892515, 0.085107] + test_list.append(cgns_mesh_readback) + + # Same for a 3D mesh with mixed elements, whose boundaries are written as MIXED sections. + cgns_output_bend = TestCase('cgns_output_bend') + cgns_output_bend.cfg_dir = "output_writers" + cgns_output_bend.cfg_file = "cgns_output_bend.cfg" + cgns_output_bend.test_iter = 2 + cgns_output_bend.test_vals = [-2.420818, -2.225454, -2.551501, -3.067128, 2.673612] + test_list.append(cgns_output_bend) + + cgns_mesh_readback_bend = TestCase('cgns_mesh_readback_bend') + cgns_mesh_readback_bend.cfg_dir = "output_writers" + cgns_mesh_readback_bend.cfg_file = "cgns_mesh_readback_bend.cfg" + cgns_mesh_readback_bend.test_iter = 2 + cgns_mesh_readback_bend.test_vals = [-3.254168, -2.756564, -3.006605, -3.572686, 4.308287] + test_list.append(cgns_mesh_readback_bend) + ###################################### ### RUN TESTS ### ###################################### From e42adb19ed2d44098a9d52572b1a3bb266293c6d Mon Sep 17 00:00:00 2001 From: rois1995 Date: Fri, 2 Oct 2026 12:01:02 +0200 Subject: [PATCH 27/35] Support MPI_BOR in the MPI wrapper of the AD builds The CGNS writer combines the element types of all ranks with MPI_BOR, but CMediMPIWrapper::convertOp only converted MPI_SUM, MPI_PROD, MPI_MIN and MPI_MAX, so SU2_CFD_AD stopped with "Conversion not implemented" when it wrote a CGNS file. MeDiPack provides AMPI_BOR. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LEL91DW5WPbPwgFtCvHga6 --- Common/include/parallelization/mpi_structure.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Common/include/parallelization/mpi_structure.hpp b/Common/include/parallelization/mpi_structure.hpp index 8d10cdc29d59..5768eab9a1a8 100644 --- a/Common/include/parallelization/mpi_structure.hpp +++ b/Common/include/parallelization/mpi_structure.hpp @@ -347,6 +347,8 @@ class CMediMPIWrapper : public CBaseMPIWrapper { return medi::AMPI_MIN; } else if (MPI_MAX == op) { return medi::AMPI_MAX; + } else if (MPI_BOR == op) { + return medi::AMPI_BOR; } else { Error("Conversion not implemented", CURRENT_FUNCTION); return medi::AMPI_SUM; From 591c18018b5beceb036b92540b49f71b61a7e0c9 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Fri, 2 Oct 2026 12:03:25 +0200 Subject: [PATCH 28/35] Write a CGNS file in the discrete adjoint cylinder test No AD regression test wrote a CGNS file, so the missing MPI_BOR in the MPI wrapper of the AD builds was not caught. The reference values do not change. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LEL91DW5WPbPwgFtCvHga6 --- TestCases/disc_adj_euler/cylinder3D/inv_cylinder3D.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/TestCases/disc_adj_euler/cylinder3D/inv_cylinder3D.cfg b/TestCases/disc_adj_euler/cylinder3D/inv_cylinder3D.cfg index f575e1a10f18..ef0f8ab93d86 100644 --- a/TestCases/disc_adj_euler/cylinder3D/inv_cylinder3D.cfg +++ b/TestCases/disc_adj_euler/cylinder3D/inv_cylinder3D.cfg @@ -99,6 +99,9 @@ CONV_CAUCHY_EPS= 1E-10 % MESH_FILENAME= cylinder3D.cgns MESH_FORMAT= CGNS +% +% Also write a CGNS file, to test the CGNS writer of the AD builds +OUTPUT_FILES= (RESTART, CGNS) MESH_OUT_FILENAME= mesh_out SOLUTION_FILENAME= solution_flow SOLUTION_ADJ_FILENAME= solution_adj From 375864c8b2eb687e423424c0e326577451ae8fba Mon Sep 17 00:00:00 2001 From: rois1995 Date: Sat, 3 Oct 2026 07:32:58 +0200 Subject: [PATCH 29/35] Avoid name collisions between the CGNS marker sections and the internal nodes The boundary sections, BCs and families of a CGNS volume file are named after the markers, but these names were only made unique among themselves. A marker named as a volume section (Triangles, Quadrilaterals, ...), as the solution (Fields) or as the zone (Zone), or as a node with a standard name (GridCoordinates, ZoneBC, ZoneType), gave two children with the same name, and the CGNS library stopped the run with "Duplicate child name found". All the names are now decided before the file is opened. The marker names are kept, because the SU2 CGNS reader takes them as marker tags: - an internal node with the name of a marker gets the prefix "SU2 " (e.g. "SU2 Triangles"); - a marker named GridCoordinates, ZoneBC or ZoneType, nodes that cannot be renamed, is written with a space ("Grid Coordinates", "Zone BC", "Zone Type"); the reader removes the spaces from the section names, so it reads the original tag back and a restart with the same configuration works. Files without such markers are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LEL91DW5WPbPwgFtCvHga6 --- .../output/filewriter/CCGNSFileWriter.hpp | 27 +++++- .../src/output/filewriter/CCGNSFileWriter.cpp | 87 ++++++++++++++----- 2 files changed, 93 insertions(+), 21 deletions(-) diff --git a/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp b/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp index c9baa088285c..f0f6f27cef6c 100644 --- a/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp +++ b/SU2_CFD/include/output/filewriter/CCGNSFileWriter.hpp @@ -39,6 +39,7 @@ #include #include +#include #include "CFileWriter.hpp" @@ -62,6 +63,13 @@ class CCGNSFileWriter final : public CFileWriter { vector boundaryMarkers; /*!< \brief Markers written as boundaries of a volume file. */ vector surfaceMarkers; /*!< \brief Markers written as one zone each in a surface file. */ + + /*--- Names of the CGNS nodes, decided by PrepareNames before the file is opened. ---*/ + vector boundaryNames; /*!< \brief Boundary sections, BCs and families, per marker. */ + string volumeZoneName = "Zone"; /*!< \brief Zone of a volume file. */ + string solutionName = "Fields"; /*!< \brief Flow solution node of each zone. */ + std::map sectionNames; /*!< \brief Element sections of the zones, per element type. */ + CConfig* config = nullptr; /*!< \brief Config, to sort the surface data of each marker. */ CGeometry* geometry = nullptr; /*!< \brief Geometry, to sort the surface data of each marker. */ @@ -146,9 +154,26 @@ class CCGNSFileWriter final : public CFileWriter { * \brief Get the names of CGNS nodes named as the markers. Tags longer than maxNameLength characters are truncated, * and a number is appended to a truncated tag equal to a previous name, so that the names are unique. * \param[in] tags - Marker tags. + * \param[in] taken - Names that are already used. * \return The names, in the order of the tags. */ - vector GetUniqueNames(const vector& tags) const; + vector GetUniqueNames(const vector& tags, vector taken = {}) const; + + /*! + * \brief Decide the names of the CGNS nodes before the file is opened. The children of a node need unique names: + * in a zone the element sections (volume and boundary), GridCoordinates, ZoneBC, ZoneType and the solution, + * in the base the zone and the families. The boundary sections, BCs and families keep the marker names, + * which the SU2 CGNS reader takes as marker tags after removing the spaces: a marker named GridCoordinates, + * ZoneBC or ZoneType is written with a space ("Zone BC") and read back unchanged. An internal node (a volume + * section, the solution "Fields" or the zone "Zone") whose name equals a marker name gets the prefix "SU2 ", + * with a space, which marker names cannot contain. + */ + void PrepareNames(); + + /*! + * \brief Number of sections the elements of a type are split into (see WriteConnectivity). + */ + cgsize_t SectionCount(GEO_TYPE type) const; /*! * \brief Write the boundary sections, BCs and families of the markers set with SetBoundaryMarkers. diff --git a/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp b/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp index f77f932d471e..162f12813d35 100644 --- a/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CCGNSFileWriter.cpp @@ -48,11 +48,14 @@ void CCGNSFileWriter::WriteData(string val_filename) { /*--- Set a timer for the file writing. ---*/ startTime = SU2_MPI::Wtime(); + /*--- Names of all the nodes, before the file is opened: a duplicate name would stop the run with the file open. ---*/ + PrepareNames(); + /*--- Open the CGNS file for writing. ---*/ InitializeMeshFile(val_filename); if (surfaceMarkers.empty()) { - WriteZone("Zone"); + WriteZone(volumeZoneName); } else { /*--- One zone per marker, the surface data is sorted again for each of them. The zones are named as the markers, with names made unique within the 32 characters of CGNS. ---*/ @@ -147,17 +150,17 @@ void CCGNSFileWriter::WriteZone(const string& zoneName) { /*--- Write mesh connectivity. ---*/ if (nDim == 2) { - WriteConnectivity(LINE, "Lines"); - WriteConnectivity(TRIANGLE, "Triangles"); - WriteConnectivity(QUADRILATERAL, "Quadrilaterals"); + WriteConnectivity(LINE, sectionNames.at(LINE)); + WriteConnectivity(TRIANGLE, sectionNames.at(TRIANGLE)); + WriteConnectivity(QUADRILATERAL, sectionNames.at(QUADRILATERAL)); } if (nDim == 3) { - WriteConnectivity(TRIANGLE, "Triangles"); - WriteConnectivity(QUADRILATERAL, "Quadrilaterals"); - WriteConnectivity(TETRAHEDRON, "Tetrahedra"); - WriteConnectivity(PYRAMID, "Pyramids"); - WriteConnectivity(PRISM, "Prisms"); - WriteConnectivity(HEXAHEDRON, "Hexahedra"); + WriteConnectivity(TRIANGLE, sectionNames.at(TRIANGLE)); + WriteConnectivity(QUADRILATERAL, sectionNames.at(QUADRILATERAL)); + WriteConnectivity(TETRAHEDRON, sectionNames.at(TETRAHEDRON)); + WriteConnectivity(PYRAMID, sectionNames.at(PYRAMID)); + WriteConnectivity(PRISM, sectionNames.at(PRISM)); + WriteConnectivity(HEXAHEDRON, sectionNames.at(HEXAHEDRON)); } /*--- Write the boundaries of a volume file. ---*/ @@ -218,32 +221,76 @@ void CCGNSFileWriter::InitializeZone(const string& zoneName) { CallCGNS(cg_zone_write(cgnsFileID, cgnsBase, zoneName.c_str(), zoneData.data(), Unstructured, &cgnsZone)); } -vector CCGNSFileWriter::GetUniqueNames(const vector& tags) const { - /*--- CGNS names have at most 32 characters, longer tags are truncated. If two truncated tags are equal, a number is - appended to make the names unique. ---*/ +vector CCGNSFileWriter::GetUniqueNames(const vector& tags, vector taken) const { + /*--- CGNS names have at most 32 characters, longer tags are truncated. If a truncated tag equals a name already + used, a number is appended to make the names unique. ---*/ vector names; for (const auto& tag : tags) { string name = tag.substr(0, maxNameLength); - for (unsigned long n = 1; std::find(names.begin(), names.end(), name) != names.end(); n++) { + for (unsigned long n = 1; std::find(taken.begin(), taken.end(), name) != taken.end(); n++) { const string suffix = "_" + to_string(n); name = tag.substr(0, maxNameLength - suffix.size()) + suffix; } if (rank == MASTER_NODE && name != tag.substr(0, maxNameLength)) { cout << "CGNS output: the marker " << tag << " is written as " << name << " (names have at most " << maxNameLength - << " characters)." << endl; + << " characters and must differ from the other names of the file)." << endl; } + taken.push_back(name); names.push_back(name); } return names; } +void CCGNSFileWriter::PrepareNames() { + /*--- Boundary sections, BCs and families: the marker names, unique. A marker named as a node of the zone with a + standard name, which cannot be renamed, gets a space instead ("Zone BC"): the SU2 CGNS reader removes the spaces + from the section names, so it reads the original marker tag back. ---*/ + boundaryNames.clear(); + if (!isSurface) { + vector tags; + for (const auto& marker : boundaryMarkers) tags.push_back(marker.name); + boundaryNames = GetUniqueNames(tags); + + const std::map standardNames = { + {"GridCoordinates", "Grid Coordinates"}, {"ZoneBC", "Zone BC"}, {"ZoneType", "Zone Type"}}; + for (auto& name : boundaryNames) { + const auto it = standardNames.find(name); + if (it != standardNames.end()) name = it->second; + } + } + + /*--- Internal names: "SU2 " in front if a marker has the same name. ---*/ + auto isMarkerName = [&](const string& name) { + return std::find(boundaryNames.begin(), boundaryNames.end(), name) != boundaryNames.end(); + }; + volumeZoneName = isMarkerName("Zone") ? "SU2 Zone" : "Zone"; + solutionName = isMarkerName("Fields") ? "SU2 Fields" : "Fields"; + + const std::map baseNames = { + {LINE, "Lines"}, {TRIANGLE, "Triangles"}, {QUADRILATERAL, "Quadrilaterals"}, {TETRAHEDRON, "Tetrahedra"}, + {PYRAMID, "Pyramids"}, {PRISM, "Prisms"}, {HEXAHEDRON, "Hexahedra"}}; + sectionNames.clear(); + for (const auto& entry : baseNames) { + /*--- A type with many elements is split into the sections "_1", "_2", ... ---*/ + const auto nSec = isSurface ? 1 : SectionCount(static_cast(entry.first)); + bool collides = false; + for (cgsize_t iSec = 0; iSec < nSec; ++iSec) + collides |= isMarkerName(nSec == 1 ? entry.second : entry.second + "_" + std::to_string(iSec + 1)); + sectionNames[entry.first] = (collides ? "SU2 " : "") + entry.second; + } +} + +cgsize_t CCGNSFileWriter::SectionCount(GEO_TYPE type) const { + const auto nTotElem = static_cast(dataSorter->GetnElemGlobal(type)); + const auto maxElemSection = static_cast(maxSectionEntries / nPointsOfElementType(type)); + return (nTotElem + maxElemSection - 1) / maxElemSection; +} + void CCGNSFileWriter::WriteBoundaries() { /*--- The sections, BCs and families are named as the markers. ---*/ - vector tags; - for (const auto& marker : boundaryMarkers) tags.push_back(marker.name); - const auto names = GetUniqueNames(tags); + const auto& names = boundaryNames; for (size_t iMarker = 0; iMarker < boundaryMarkers.size(); ++iMarker) { const auto& marker = boundaryMarkers[iMarker]; @@ -435,7 +482,7 @@ void CCGNSFileWriter::WriteConnectivity(GEO_TYPE type, const string& SectionName const auto nPointsElem = nPointsOfElementType(type); const auto nTotElemCG = static_cast(nTotElem); const auto maxElemSection = static_cast(maxSectionEntries / nPointsElem); - const auto nSections = (nTotElemCG + maxElemSection - 1) / maxElemSection; + const auto nSections = SectionCount(type); /*--- First and last element (CGNS numbering starts from 1 and ranges are inclusive) of a section. ---*/ auto sectionBegin = [&](cgsize_t iSec) { return cumulative + 1 + iSec * maxElemSection; }; @@ -495,6 +542,6 @@ int CCGNSFileWriter::ElementsWriteData(int section, cgsize_t start, cgsize_t end void CCGNSFileWriter::InitializeFields() { /*--- Create "Fields" node to store solution. ---*/ - CallCGNS(cg_sol_write(cgnsFileID, cgnsBase, cgnsZone, "Fields", Vertex, &cgnsFields)); + CallCGNS(cg_sol_write(cgnsFileID, cgnsBase, cgnsZone, solutionName.c_str(), Vertex, &cgnsFields)); } #endif // HAVE_CGNS From 2861214ef94e484d7e2be9025b50ada624107255 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Sat, 3 Oct 2026 10:12:28 +0200 Subject: [PATCH 30/35] Explain when to use WRT_OUTPUT_DOUBLE_PRECISION Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LEL91DW5WPbPwgFtCvHga6 --- config_template.cfg | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/config_template.cfg b/config_template.cfg index c89470ac5994..4e8057787a87 100644 --- a/config_template.cfg +++ b/config_template.cfg @@ -2659,7 +2659,10 @@ TABULAR_FORMAT= CSV OUTPUT_PRECISION= 10 % % Write the fields of the volume and surface files (CGNS and Paraview XML) in double -% precision instead of single. The CGNS coordinates are always in double precision. +% precision instead of single. The CGNS coordinates are always in double precision, +% the Paraview XML coordinates follow this option. Single precision (about 7 digits) +% is enough to view the results; use double precision when the values are processed +% further, e.g. to compare two solutions or to match the points of periodic surfaces. WRT_OUTPUT_DOUBLE_PRECISION= NO % % For multizone problems, extend solution and restart filenames automatically by zone number From 74b12801852d12c8efd814e9fbca307cc05b73e9 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Sat, 3 Oct 2026 10:12:28 +0200 Subject: [PATCH 31/35] Write the CGNS boundaries unless the output is from the FEM solver Only the finite volume data sorter knows which boundary elements are halo copies. Say so with a check of femOutput instead of a dynamic_cast. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LEL91DW5WPbPwgFtCvHga6 --- SU2_CFD/src/output/COutput.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/SU2_CFD/src/output/COutput.cpp b/SU2_CFD/src/output/COutput.cpp index f503128a6727..9d863aac517c 100644 --- a/SU2_CFD/src/output/COutput.cpp +++ b/SU2_CFD/src/output/COutput.cpp @@ -762,9 +762,10 @@ void COutput::WriteToFile(CConfig *config, CGeometry *geometry, OUTPUT_TYPE form { auto* cgnsWriter = new CCGNSFileWriter(volumeDataSorter, false, config->GetWrt_Output_Double_Precision()); - /*--- Add the boundaries, named as the markers (the finite volume sorter knows which elements are halos). ---*/ - if (const auto* fvmSorter = dynamic_cast(volumeDataSorter)) - cgnsWriter->SetBoundaryMarkers(config, geometry, fvmSorter); + /*--- Write the boundaries, named as the markers. This needs to know which boundary elements are halo copies, + which only the finite volume data sorter knows: the files of the FEM solver have no boundaries. ---*/ + if (!femOutput) + cgnsWriter->SetBoundaryMarkers(config, geometry, static_cast(volumeDataSorter)); fileWriter = cgnsWriter; } From d39e4f2c22c4fb1fd30172781343070717106c17 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Sat, 3 Oct 2026 10:12:28 +0200 Subject: [PATCH 32/35] Declare the loop indices of the Tecplot and Paraview XML writers in their loops Also remove the empty blocks left in the Tecplot writer by the removed loop over the ranks. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LEL91DW5WPbPwgFtCvHga6 --- .../filewriter/CParaviewXMLFileWriter.cpp | 25 ++--- .../output/filewriter/CTecplotFileWriter.cpp | 106 ++++++++---------- 2 files changed, 57 insertions(+), 74 deletions(-) diff --git a/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp b/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp index 3d521acd1ffd..7f9cf66ad34e 100644 --- a/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp @@ -70,15 +70,11 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ const int NCOORDS = 3; const unsigned short nDim = dataSorter->GetnDim(); - unsigned short iDim = 0; /*--- Array containing the field names we want to output ---*/ const vector& fieldNames = dataSorter->GetFieldNames(); - unsigned long iPoint, iElem; - - OpenMPIFile(val_filename); dataOffset = 0; @@ -153,8 +149,8 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ /*--- Loop over all variables that have been registered in the output. ---*/ - unsigned short iField, VarCounter = varStart; - for (iField = varStart; iField < fieldNames.size(); iField++) { + unsigned short VarCounter = varStart; + for (unsigned short iField = varStart; iField < fieldNames.size(); iField++) { string fieldname = fieldNames[iField]; fieldname.erase(remove(fieldname.begin(), fieldname.end(), '"'), @@ -221,8 +217,8 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ } }; - for (iPoint = 0; iPoint < myPoint; iPoint++) { - for (iDim = 0; iDim < NCOORDS; iDim++) { + for (auto iPoint = 0ul; iPoint < myPoint; iPoint++) { + for (unsigned short iDim = 0; iDim < NCOORDS; iDim++) { if (nDim == 2 && iDim == 2) { dataBuffer[iPoint*NCOORDS + iDim] = 0.0; } else { @@ -238,11 +234,10 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ vector connBuf(myElemStorage); vector offsetBuf(myElem); unsigned long iStorage = 0, iElemID = 0; - unsigned short iNode = 0; auto copyToBuffer = [&](GEO_TYPE type, unsigned long nElem, unsigned short nPoints){ - for (iElem = 0; iElem < nElem; iElem++) { - for (iNode = 0; iNode < nPoints; iNode++){ + for (auto iElem = 0ul; iElem < nElem; iElem++) { + for (unsigned short iNode = 0; iNode < nPoints; iNode++){ connBuf[iStorage+iNode] = static_cast(dataSorter->GetElemConnectivity(type, iElem, iNode)) - 1; } iStorage += nPoints; @@ -286,7 +281,7 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ /*--- Loop over all variables that have been registered in the output. ---*/ VarCounter = varStart; - for (iField = varStart; iField < fieldNames.size(); iField++) { + for (unsigned short iField = varStart; iField < fieldNames.size(); iField++) { /*--- Check whether this field is a vector or scalar. ---*/ @@ -315,8 +310,8 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ /*--- Load up the buffer for writing this rank's vector data. ---*/ - for (iPoint = 0; iPoint < myPoint; iPoint++) { - for (iDim = 0; iDim < NCOORDS; iDim++) { + for (auto iPoint = 0ul; iPoint < myPoint; iPoint++) { + for (unsigned short iDim = 0; iDim < NCOORDS; iDim++) { if (nDim == 2 && iDim == 2) { dataBuffer[iPoint*NCOORDS + iDim] = 0.0; } else { @@ -335,7 +330,7 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ /*--- For now, create a temp 1D buffer to load up the data for writing. This will be replaced with a derived data type most likely. ---*/ - for (iPoint = 0; iPoint < myPoint; iPoint++) { + for (auto iPoint = 0ul; iPoint < myPoint; iPoint++) { dataBuffer[iPoint] = SU2_TYPE::GetValue(dataSorter->GetData(VarCounter,iPoint)); } diff --git a/SU2_CFD/src/output/filewriter/CTecplotFileWriter.cpp b/SU2_CFD/src/output/filewriter/CTecplotFileWriter.cpp index 9077b049134f..cf949696fd0d 100644 --- a/SU2_CFD/src/output/filewriter/CTecplotFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CTecplotFileWriter.cpp @@ -43,10 +43,6 @@ void CTecplotFileWriter::WriteData(string val_filename){ const vector fieldNames = dataSorter->GetFieldNames(); - unsigned long iVar; - - unsigned long iPoint, iElem; - /*--- Reduce the total number of each element. ---*/ unsigned long nParallel_Line = dataSorter->GetnElem(LINE), @@ -74,7 +70,7 @@ void CTecplotFileWriter::WriteData(string val_filename){ header << "TITLE = \"Visualization of the solution\"" << endl; header << "VARIABLES = "; - for (iVar = 0; iVar < fieldNames.size()-1; iVar++) { + for (auto iVar = 0ul; iVar < fieldNames.size()-1; iVar++) { header << "\"" << fieldNames[iVar] << "\","; } header << "\"" << fieldNames[fieldNames.size()-1] << "\"" << endl; @@ -115,8 +111,8 @@ void CTecplotFileWriter::WriteData(string val_filename){ /*--- Write surface and volumetric solution data. ---*/ - for (iPoint = 0; iPoint < dataSorter->GetnPoints(); iPoint++) { - for (iVar = 0; iVar < fieldNames.size(); iVar++) + for (auto iPoint = 0ul; iPoint < dataSorter->GetnPoints(); iPoint++) { + for (auto iVar = 0ul; iVar < fieldNames.size(); iVar++) data << dataSorter->GetData(iVar, iPoint) << "\t"; data << endl; } @@ -126,62 +122,54 @@ void CTecplotFileWriter::WriteData(string val_filename){ data.str(""); data.clear(); - /*--- Write connectivity data. ---*/ - { - { - - for (iElem = 0; iElem < nParallel_Line; iElem++) { - data << dataSorter->GetElemConnectivity(LINE, iElem, 0) << "\t"; - data << dataSorter->GetElemConnectivity(LINE, iElem, 1)<< "\n"; - } - - - for (iElem = 0; iElem < nParallel_Tria; iElem++) { - data << dataSorter->GetElemConnectivity(TRIANGLE, iElem, 0) << "\t"; - data << dataSorter->GetElemConnectivity(TRIANGLE, iElem, 1) << "\t"; - data << dataSorter->GetElemConnectivity(TRIANGLE, iElem, 2) << "\t"; - data << dataSorter->GetElemConnectivity(TRIANGLE, iElem, 2) << "\n"; - } - - for (iElem = 0; iElem < nParallel_Quad; iElem++) { - data << dataSorter->GetElemConnectivity(QUADRILATERAL, iElem, 0) << "\t"; - data << dataSorter->GetElemConnectivity(QUADRILATERAL, iElem, 1) << "\t"; - data << dataSorter->GetElemConnectivity(QUADRILATERAL, iElem, 2) << "\t"; - data << dataSorter->GetElemConnectivity(QUADRILATERAL, iElem, 3) << "\n"; - } - - for (iElem = 0; iElem < nParallel_Tetr; iElem++) { - data << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 0) << "\t" << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 1) << "\t"; - data << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 2) << "\t" << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 2) << "\t"; - data << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 3) << "\t" << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 3) << "\t"; - data << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 3) << "\t" << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 3) << "\n"; - } - - for (iElem = 0; iElem < nParallel_Hexa; iElem++) { - data << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 0) << "\t" << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 1) << "\t"; - data << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 2) << "\t" << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 3) << "\t"; - data << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 4) << "\t" << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 5) << "\t"; - data << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 6) << "\t" << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 7) << "\n"; - } - - for (iElem = 0; iElem < nParallel_Pris; iElem++) { - data << dataSorter->GetElemConnectivity(PRISM, iElem, 0) << "\t" << dataSorter->GetElemConnectivity(PRISM, iElem, 1) << "\t"; - data << dataSorter->GetElemConnectivity(PRISM, iElem, 1) << "\t" << dataSorter->GetElemConnectivity(PRISM, iElem, 2) << "\t"; - data << dataSorter->GetElemConnectivity(PRISM, iElem, 3) << "\t" << dataSorter->GetElemConnectivity(PRISM, iElem, 4) << "\t"; - data << dataSorter->GetElemConnectivity(PRISM, iElem, 4) << "\t" << dataSorter->GetElemConnectivity(PRISM, iElem, 5) << "\n"; - } - - for (iElem = 0; iElem < nParallel_Pyra; iElem++) { - data << dataSorter->GetElemConnectivity(PYRAMID, iElem, 0) << "\t" << dataSorter->GetElemConnectivity(PYRAMID, iElem, 1) << "\t"; - data << dataSorter->GetElemConnectivity(PYRAMID, iElem, 2) << "\t" << dataSorter->GetElemConnectivity(PYRAMID, iElem, 3) << "\t"; - data << dataSorter->GetElemConnectivity(PYRAMID, iElem, 4) << "\t" << dataSorter->GetElemConnectivity(PYRAMID, iElem, 4) << "\t"; - data << dataSorter->GetElemConnectivity(PYRAMID, iElem, 4) << "\t" << dataSorter->GetElemConnectivity(PYRAMID, iElem, 4) << "\n"; - } + for (auto iElem = 0ul; iElem < nParallel_Line; iElem++) { + data << dataSorter->GetElemConnectivity(LINE, iElem, 0) << "\t"; + data << dataSorter->GetElemConnectivity(LINE, iElem, 1)<< "\n"; + } + + for (auto iElem = 0ul; iElem < nParallel_Tria; iElem++) { + data << dataSorter->GetElemConnectivity(TRIANGLE, iElem, 0) << "\t"; + data << dataSorter->GetElemConnectivity(TRIANGLE, iElem, 1) << "\t"; + data << dataSorter->GetElemConnectivity(TRIANGLE, iElem, 2) << "\t"; + data << dataSorter->GetElemConnectivity(TRIANGLE, iElem, 2) << "\n"; + } - } + for (auto iElem = 0ul; iElem < nParallel_Quad; iElem++) { + data << dataSorter->GetElemConnectivity(QUADRILATERAL, iElem, 0) << "\t"; + data << dataSorter->GetElemConnectivity(QUADRILATERAL, iElem, 1) << "\t"; + data << dataSorter->GetElemConnectivity(QUADRILATERAL, iElem, 2) << "\t"; + data << dataSorter->GetElemConnectivity(QUADRILATERAL, iElem, 3) << "\n"; + } + + for (auto iElem = 0ul; iElem < nParallel_Tetr; iElem++) { + data << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 0) << "\t" << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 1) << "\t"; + data << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 2) << "\t" << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 2) << "\t"; + data << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 3) << "\t" << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 3) << "\t"; + data << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 3) << "\t" << dataSorter->GetElemConnectivity(TETRAHEDRON, iElem, 3) << "\n"; + } + + for (auto iElem = 0ul; iElem < nParallel_Hexa; iElem++) { + data << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 0) << "\t" << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 1) << "\t"; + data << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 2) << "\t" << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 3) << "\t"; + data << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 4) << "\t" << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 5) << "\t"; + data << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 6) << "\t" << dataSorter->GetElemConnectivity(HEXAHEDRON, iElem, 7) << "\n"; + } + + for (auto iElem = 0ul; iElem < nParallel_Pris; iElem++) { + data << dataSorter->GetElemConnectivity(PRISM, iElem, 0) << "\t" << dataSorter->GetElemConnectivity(PRISM, iElem, 1) << "\t"; + data << dataSorter->GetElemConnectivity(PRISM, iElem, 1) << "\t" << dataSorter->GetElemConnectivity(PRISM, iElem, 2) << "\t"; + data << dataSorter->GetElemConnectivity(PRISM, iElem, 3) << "\t" << dataSorter->GetElemConnectivity(PRISM, iElem, 4) << "\t"; + data << dataSorter->GetElemConnectivity(PRISM, iElem, 4) << "\t" << dataSorter->GetElemConnectivity(PRISM, iElem, 5) << "\n"; + } + + for (auto iElem = 0ul; iElem < nParallel_Pyra; iElem++) { + data << dataSorter->GetElemConnectivity(PYRAMID, iElem, 0) << "\t" << dataSorter->GetElemConnectivity(PYRAMID, iElem, 1) << "\t"; + data << dataSorter->GetElemConnectivity(PYRAMID, iElem, 2) << "\t" << dataSorter->GetElemConnectivity(PYRAMID, iElem, 3) << "\t"; + data << dataSorter->GetElemConnectivity(PYRAMID, iElem, 4) << "\t" << dataSorter->GetElemConnectivity(PYRAMID, iElem, 4) << "\t"; + data << dataSorter->GetElemConnectivity(PYRAMID, iElem, 4) << "\t" << dataSorter->GetElemConnectivity(PYRAMID, iElem, 4) << "\n"; } WriteMPIStringAll(data.str()); From 523f29799d6bd9cc85b749b18aa88f8536ae6075 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Sat, 3 Oct 2026 10:12:28 +0200 Subject: [PATCH 33/35] Split WriteData of the binary Paraview writer into smaller functions Points, cells (classic and VTK 9 layouts), cell types and point data are written by separate private functions. The written files are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LEL91DW5WPbPwgFtCvHga6 --- .../filewriter/CParaviewBinaryFileWriter.hpp | 49 +++ .../filewriter/CParaviewBinaryFileWriter.cpp | 327 +++++++----------- 2 files changed, 175 insertions(+), 201 deletions(-) diff --git a/SU2_CFD/include/output/filewriter/CParaviewBinaryFileWriter.hpp b/SU2_CFD/include/output/filewriter/CParaviewBinaryFileWriter.hpp index 507741a0e58f..e6793a9f817a 100644 --- a/SU2_CFD/include/output/filewriter/CParaviewBinaryFileWriter.hpp +++ b/SU2_CFD/include/output/filewriter/CParaviewBinaryFileWriter.hpp @@ -27,6 +27,8 @@ #pragma once +#include + #include "CFileWriter.hpp" class CParaviewBinaryFileWriter final: public CFileWriter{ @@ -37,6 +39,53 @@ class CParaviewBinaryFileWriter final: public CFileWriter{ */ bool bigEndian; + static constexpr unsigned short NCOORDS = 3; /*!< \brief Points and vectors always have 3 components. */ + + /*! + * \brief Element types, in the order in which the cells are written. + */ + static constexpr std::array elemTypes = {LINE, TRIANGLE, QUADRILATERAL, TETRAHEDRON, + HEXAHEDRON, PRISM, PYRAMID}; + + /*! + * \brief Write the point coordinates. + */ + void WritePoints(); + + /*! + * \brief Write the cells in the classic layout: the number of nodes followed by the node ids of each cell, Int32. + * \param[in] GlobalCellStorage - Total size of that array. + */ + void WriteCellsInt32(unsigned long GlobalCellStorage); + + /*! + * \brief Write the cells in the layout of VTK >= 9.0: Int64 offsets and connectivity. + */ + void WriteCellsInt64(); + + /*! + * \brief Write the type of each cell. + */ + void WriteCellTypes(); + + /*! + * \brief Write the fields, as scalars or 3-component vectors. + */ + void WritePointData(); + + /*! + * \brief Write 3 fields starting at firstVar as a vector (the third component is 0 in 2D). + * \param[in] firstVar - Index of the first field in the data sorter. + */ + void WriteVectorArray(unsigned short firstVar); + + /*! + * \brief Write the point values of this rank (nComponents per point) at their place in the file. + * \param[in,out] buffer - Values, byte-swapped in place to big endian. + * \param[in] nComponents - Values per point. + */ + void WritePointArray(vector& buffer, unsigned short nComponents); + public: /*! diff --git a/SU2_CFD/src/output/filewriter/CParaviewBinaryFileWriter.cpp b/SU2_CFD/src/output/filewriter/CParaviewBinaryFileWriter.cpp index e065a421ae24..53e717cae1b3 100644 --- a/SU2_CFD/src/output/filewriter/CParaviewBinaryFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CParaviewBinaryFileWriter.cpp @@ -54,17 +54,6 @@ void CParaviewBinaryFileWriter::WriteData(string val_filename){ SU2_MPI::Error("Connectivity must be sorted.", CURRENT_FUNCTION); } - const vector& fieldNames = dataSorter->GetFieldNames(); - - unsigned short iDim = 0, nDim = dataSorter->GetnDim(); - - unsigned long iPoint; - - const int MAX_STRING_LENGTH = 255; - char str_buf[MAX_STRING_LENGTH]; - - const int NCOORDS = 3; - OpenMPIFile(val_filename); /*--- The classic (3.0) cell layout stores the cell sizes and node ids of all cells in one Int32 array. When that @@ -80,263 +69,199 @@ void CParaviewBinaryFileWriter::WriteData(string val_filename){ WriteMPIString(header, MASTER_NODE); - /*--- Communicate the number of total points that will be - written by each rank. After this communication, each proc knows how - many poinnts will be written before its location in the file and the - offsets can be correctly set. ---*/ + WritePoints(); + + if (cellsInt64) { + WriteCellsInt64(); + } else { + WriteCellsInt32(GlobalCellStorage); + } + + WriteCellTypes(); + + WritePointData(); + + CloseMPIFile(); +} + +void CParaviewBinaryFileWriter::WritePointArray(vector& buffer, unsigned short nComponents) { + + const unsigned long myPoint = dataSorter->GetnPoints(); + + if (!bigEndian) SwapBytes((char *)buffer.data(), sizeof(float), myPoint*nComponents); - unsigned long myPoint, GlobalPoint; + const unsigned long sizeInBytesPerPoint = sizeof(float)*nComponents; + WriteMPIBinaryDataAll(buffer.data(), sizeInBytesPerPoint*myPoint, sizeInBytesPerPoint*dataSorter->GetnPointsGlobal(), + sizeInBytesPerPoint*dataSorter->GetnPointCumulative(rank)); +} - GlobalPoint = dataSorter->GetnPointsGlobal(); - myPoint = dataSorter->GetnPoints(); +void CParaviewBinaryFileWriter::WriteVectorArray(unsigned short firstVar) { - WriteMPIString("POINTS " + std::to_string(GlobalPoint) + " float\n", MASTER_NODE); + /*--- There are always 3 components, the third one is 0 in 2D. ---*/ - /*--- Load/write the 1D buffer of point coordinates. Note that we - always have 3 coordinate dimensions, even for 2D problems. ---*/ + const unsigned short nDim = dataSorter->GetnDim(); + const unsigned long myPoint = dataSorter->GetnPoints(); - vector dataBufferFloat(myPoint*NCOORDS); - for (iPoint = 0; iPoint < myPoint; iPoint++) { - for (iDim = 0; iDim < NCOORDS; iDim++) { + vector buffer(myPoint*NCOORDS); + for (auto iPoint = 0ul; iPoint < myPoint; iPoint++) { + for (unsigned short iDim = 0; iDim < NCOORDS; iDim++) { if (nDim == 2 && iDim == 2) { - dataBufferFloat[iPoint*NCOORDS + iDim] = 0.0; + buffer[iPoint*NCOORDS + iDim] = 0.0; } else { - auto val = (float)dataSorter->GetData(iDim, iPoint); - dataBufferFloat[iPoint*NCOORDS + iDim] = val; + buffer[iPoint*NCOORDS + iDim] = (float)dataSorter->GetData(firstVar+iDim, iPoint); } } } + WritePointArray(buffer, NCOORDS); +} - if (!bigEndian) SwapBytes((char *)dataBufferFloat.data(), sizeof(float), myPoint*NCOORDS); - - /*--- Compute various data sizes --- */ +void CParaviewBinaryFileWriter::WritePoints() { - unsigned long sizeInBytesPerPoint = sizeof(float)*NCOORDS; - unsigned long sizeInBytesLocal = sizeInBytesPerPoint*myPoint; - unsigned long sizeInBytesGlobal = sizeInBytesPerPoint*GlobalPoint; - unsigned long offsetInBytes = sizeInBytesPerPoint*dataSorter->GetnPointCumulative(rank); + WriteMPIString("POINTS " + std::to_string(dataSorter->GetnPointsGlobal()) + " float\n", MASTER_NODE); - WriteMPIBinaryDataAll(dataBufferFloat.data(), sizeInBytesLocal, sizeInBytesGlobal, offsetInBytes); + /*--- The coordinates are the first fields of the data sorter. ---*/ - /*--- Compute our local number of elements, the required storage, - and reduce the total number of elements and storage globally. ---*/ + WriteVectorArray(0); +} - unsigned long myElem, myElemStorage, GlobalElem, GlobalElemStorage; +void CParaviewBinaryFileWriter::WriteCellsInt32(unsigned long GlobalCellStorage) { - unsigned long nParallel_Line = dataSorter->GetnElem(LINE), - nParallel_Tria = dataSorter->GetnElem(TRIANGLE), - nParallel_Quad = dataSorter->GetnElem(QUADRILATERAL), - nParallel_Tetr = dataSorter->GetnElem(TETRAHEDRON), - nParallel_Hexa = dataSorter->GetnElem(HEXAHEDRON), - nParallel_Pris = dataSorter->GetnElem(PRISM), - nParallel_Pyra = dataSorter->GetnElem(PYRAMID); + const unsigned long myElem = dataSorter->GetnElem(); + const unsigned long myElemStorage = dataSorter->GetnConn(); - myElem = dataSorter->GetnElem(); - myElemStorage = dataSorter->GetnConn(); - GlobalElem = dataSorter->GetnElemGlobal(); - GlobalElemStorage = dataSorter->GetnConnGlobal(); + WriteMPIString("\nCELLS " + std::to_string(dataSorter->GetnElemGlobal()) + " " + std::to_string(GlobalCellStorage) + + "\n", MASTER_NODE); - /*--- Loop over the local elements of each type, calling f(type, iElem, nPoints). ---*/ + /*--- Load/write the 1D buffer of the number of nodes followed by the node ids of each cell. ---*/ - auto forEachElem = [&](auto f) { - for (auto type : {LINE, TRIANGLE, QUADRILATERAL, TETRAHEDRON, HEXAHEDRON, PRISM, PYRAMID}) { - const auto nPoints = nPointsOfElementType(type); - for (unsigned long iElem = 0; iElem < dataSorter->GetnElem(type); iElem++) f(type, iElem, nPoints); - } - }; + vector connBuf(myElemStorage + myElem); unsigned long iStorage = 0; - if (!cellsInt64) { - - WriteMPIString("\nCELLS " + std::to_string(GlobalElem) + " " + std::to_string(GlobalCellStorage) + "\n", - MASTER_NODE); - - /*--- Load/write the 1D buffer of the number of nodes followed by the node ids of each cell. ---*/ - - vector connBuf(myElemStorage + myElem); - - forEachElem([&](GEO_TYPE type, unsigned long iElem, unsigned short nPoints) { + for (auto type : elemTypes) { + const auto nPoints = nPointsOfElementType(type); + for (auto iElem = 0ul; iElem < dataSorter->GetnElem(type); iElem++) { connBuf[iStorage++] = nPoints; for (unsigned short iNode = 0; iNode < nPoints; iNode++) connBuf[iStorage++] = static_cast(dataSorter->GetElemConnectivity(type, iElem, iNode) - 1); - }); + } + } - if (!bigEndian) SwapBytes((char *)connBuf.data(), sizeof(int32_t), myElemStorage+myElem); + if (!bigEndian) SwapBytes((char *)connBuf.data(), sizeof(int32_t), myElemStorage+myElem); - sizeInBytesPerPoint = sizeof(int32_t); - sizeInBytesLocal = sizeInBytesPerPoint*(myElemStorage + myElem); - sizeInBytesGlobal = sizeInBytesPerPoint*GlobalCellStorage; - offsetInBytes = sizeInBytesPerPoint* - (dataSorter->GetnElemConnCumulative(rank) + dataSorter->GetnElemCumulative(rank)); + WriteMPIBinaryDataAll(connBuf.data(), sizeof(int32_t)*(myElemStorage + myElem), sizeof(int32_t)*GlobalCellStorage, + sizeof(int32_t)*(dataSorter->GetnElemConnCumulative(rank) + + dataSorter->GetnElemCumulative(rank))); +} - WriteMPIBinaryDataAll(connBuf.data(), sizeInBytesLocal, sizeInBytesGlobal, offsetInBytes); +void CParaviewBinaryFileWriter::WriteCellsInt64() { - } else { + const unsigned long myElem = dataSorter->GetnElem(); + const unsigned long myElemStorage = dataSorter->GetnConn(); + const unsigned long GlobalElem = dataSorter->GetnElemGlobal(); + const unsigned long GlobalElemStorage = dataSorter->GetnConnGlobal(); - WriteMPIString("\nCELLS " + std::to_string(GlobalElem + 1) + " " + std::to_string(GlobalElemStorage) + "\n", - MASTER_NODE); + WriteMPIString("\nCELLS " + std::to_string(GlobalElem + 1) + " " + std::to_string(GlobalElemStorage) + "\n", + MASTER_NODE); - /*--- Load the offsets (where each cell ends in the connectivity) and the connectivity. ---*/ + /*--- Load the offsets (where each cell ends in the connectivity) and the connectivity. ---*/ - vector offsetBuf(myElem), connBuf(myElemStorage); - unsigned long iCell = 0; + vector offsetBuf(myElem), connBuf(myElemStorage); + unsigned long iStorage = 0, iCell = 0; - forEachElem([&](GEO_TYPE type, unsigned long iElem, unsigned short nPoints) { + for (auto type : elemTypes) { + const auto nPoints = nPointsOfElementType(type); + for (auto iElem = 0ul; iElem < dataSorter->GetnElem(type); iElem++) { for (unsigned short iNode = 0; iNode < nPoints; iNode++) connBuf[iStorage++] = static_cast(dataSorter->GetElemConnectivity(type, iElem, iNode)) - 1; offsetBuf[iCell++] = static_cast(iStorage + dataSorter->GetnElemConnCumulative(rank)); - }); - - if (!bigEndian) { - SwapBytes((char *)offsetBuf.data(), sizeof(int64_t), myElem); - SwapBytes((char *)connBuf.data(), sizeof(int64_t), myElemStorage); } + } - /*--- The offsets start with a 0, written by the master node. ---*/ + if (!bigEndian) { + SwapBytes((char *)offsetBuf.data(), sizeof(int64_t), myElem); + SwapBytes((char *)connBuf.data(), sizeof(int64_t), myElemStorage); + } - WriteMPIString("OFFSETS vtktypeint64\n", MASTER_NODE); - const int64_t firstOffset = 0; - WriteMPIBinaryData(&firstOffset, sizeof(int64_t), MASTER_NODE); - WriteMPIBinaryDataAll(offsetBuf.data(), sizeof(int64_t)*myElem, sizeof(int64_t)*GlobalElem, - sizeof(int64_t)*dataSorter->GetnElemCumulative(rank)); + /*--- The offsets start with a 0, written by the master node. ---*/ - WriteMPIString("\nCONNECTIVITY vtktypeint64\n", MASTER_NODE); - WriteMPIBinaryDataAll(connBuf.data(), sizeof(int64_t)*myElemStorage, sizeof(int64_t)*GlobalElemStorage, - sizeof(int64_t)*dataSorter->GetnElemConnCumulative(rank)); - } + WriteMPIString("OFFSETS vtktypeint64\n", MASTER_NODE); + const int64_t firstOffset = 0; + WriteMPIBinaryData(&firstOffset, sizeof(int64_t), MASTER_NODE); + WriteMPIBinaryDataAll(offsetBuf.data(), sizeof(int64_t)*myElem, sizeof(int64_t)*GlobalElem, + sizeof(int64_t)*dataSorter->GetnElemCumulative(rank)); + + WriteMPIString("\nCONNECTIVITY vtktypeint64\n", MASTER_NODE); + WriteMPIBinaryDataAll(connBuf.data(), sizeof(int64_t)*myElemStorage, sizeof(int64_t)*GlobalElemStorage, + sizeof(int64_t)*dataSorter->GetnElemConnCumulative(rank)); +} + +void CParaviewBinaryFileWriter::WriteCellTypes() { + + const unsigned long myElem = dataSorter->GetnElem(); + const unsigned long GlobalElem = dataSorter->GetnElemGlobal(); WriteMPIString("\nCELL_TYPES " + std::to_string(GlobalElem) + "\n", MASTER_NODE); - /*--- Load/write the cell type for all elements in the file. ---*/ + /*--- Load/write the cell type for all elements in the file, in the same order as the cells. ---*/ vector typeBuf(myElem); auto typeIter = typeBuf.begin(); - - std::fill(typeIter, typeIter+nParallel_Line, LINE); typeIter += nParallel_Line; - std::fill(typeIter, typeIter+nParallel_Tria, TRIANGLE); typeIter += nParallel_Tria; - std::fill(typeIter, typeIter+nParallel_Quad, QUADRILATERAL); typeIter += nParallel_Quad; - std::fill(typeIter, typeIter+nParallel_Tetr, TETRAHEDRON); typeIter += nParallel_Tetr; - std::fill(typeIter, typeIter+nParallel_Hexa, HEXAHEDRON); typeIter += nParallel_Hexa; - std::fill(typeIter, typeIter+nParallel_Pris, PRISM); typeIter += nParallel_Pris; - std::fill(typeIter, typeIter+nParallel_Pyra, PYRAMID); typeIter += nParallel_Pyra; + for (auto type : elemTypes) { + const auto nElem = dataSorter->GetnElem(type); + std::fill(typeIter, typeIter+nElem, type); + typeIter += nElem; + } if (!bigEndian) SwapBytes((char *)typeBuf.data(), sizeof(int), myElem); - /*--- Compute various data sizes --- */ + WriteMPIBinaryDataAll(typeBuf.data(), sizeof(int)*myElem, sizeof(int)*GlobalElem, + sizeof(int)*dataSorter->GetnElemCumulative(rank)); +} - sizeInBytesPerPoint = sizeof(int); - sizeInBytesLocal = sizeInBytesPerPoint*myElem; - sizeInBytesGlobal = sizeInBytesPerPoint*GlobalElem; - offsetInBytes = sizeInBytesPerPoint*dataSorter->GetnElemCumulative(rank); +void CParaviewBinaryFileWriter::WritePointData() { - WriteMPIBinaryDataAll(typeBuf.data(), sizeInBytesLocal, sizeInBytesGlobal, offsetInBytes); + const vector& fieldNames = dataSorter->GetFieldNames(); + const unsigned long myPoint = dataSorter->GetnPoints(); - WriteMPIString("\nPOINT_DATA " + std::to_string(GlobalPoint) + "\n", MASTER_NODE); + WriteMPIString("\nPOINT_DATA " + std::to_string(dataSorter->GetnPointsGlobal()) + "\n", MASTER_NODE); - /*--- Adjust container start location to avoid point coords. ---*/ + /*--- Skip the coordinates, which are the first fields. ---*/ - unsigned short varStart = 2; - if (nDim == 3) varStart++; + const unsigned short varStart = (dataSorter->GetnDim() == 3) ? 3 : 2; - /*--- Loop over all variables that have been registered in the output. ---*/ + /*--- Loop over all variables that have been registered in the output. A field ending in "_x" starts a + vector, written with its "_y" (and "_z") components, which are then skipped. ---*/ - unsigned short iField, VarCounter = varStart; - for (iField = varStart; iField < fieldNames.size(); iField++) { + unsigned short VarCounter = varStart; + for (unsigned short iField = varStart; iField < fieldNames.size(); iField++) { string fieldname = fieldNames[iField]; - fieldname.erase(remove(fieldname.begin(), fieldname.end(), '"'), - fieldname.end()); - - /*--- Check whether this field is a vector or scalar. ---*/ - - bool output_variable = true, isVector = false; - size_t found = fieldNames[iField].find("_x"); - if (found!=string::npos) { - output_variable = true; - isVector = true; - } - found = fieldNames[iField].find("_y"); - if (found!=string::npos) { - /*--- We have found a vector, so skip the Y component. ---*/ - output_variable = false; - VarCounter++; - } - found = fieldNames[iField].find("_z"); - if (found!=string::npos) { - /*--- We have found a vector, so skip the Z component. ---*/ - output_variable = false; - VarCounter++; - } - - /*--- Write the point data as an vector or a scalar. ---*/ - - if (output_variable && isVector) { + fieldname.erase(remove(fieldname.begin(), fieldname.end(), '"'), fieldname.end()); - /*--- Adjust the string name to remove the leading "X-" ---*/ + const bool isVector = fieldNames[iField].find("_x") != string::npos; + const bool isY = fieldNames[iField].find("_y") != string::npos; + const bool isZ = fieldNames[iField].find("_z") != string::npos; - fieldname.erase(fieldname.end()-2,fieldname.end()); + if (isY || isZ) { + VarCounter += isY + isZ; + } else if (isVector) { - SPRINTF (str_buf, "\nVECTORS %s float\n", fieldname.c_str()); - WriteMPIString(str_buf, MASTER_NODE); - - /*--- Load up the buffer for writing this rank's vector data. ---*/ - - float val = 0.0; - for (iPoint = 0; iPoint < myPoint; iPoint++) { - for (iDim = 0; iDim < NCOORDS; iDim++) { - if (nDim == 2 && iDim == 2) { - dataBufferFloat[iPoint*NCOORDS + iDim] = 0.0; - } else { - val = (float)dataSorter->GetData(VarCounter+iDim,iPoint); - dataBufferFloat[iPoint*NCOORDS + iDim] = val; - } - } - } - if (!bigEndian) - SwapBytes((char *)dataBufferFloat.data(), sizeof(float), myPoint*NCOORDS); - - /*--- Compute various data sizes --- */ - - sizeInBytesPerPoint = sizeof(float)*NCOORDS; - sizeInBytesLocal = sizeInBytesPerPoint*myPoint; - sizeInBytesGlobal = sizeInBytesPerPoint*GlobalPoint; - offsetInBytes = sizeInBytesPerPoint*dataSorter->GetnPointCumulative(rank); - - WriteMPIBinaryDataAll(dataBufferFloat.data(), sizeInBytesLocal, sizeInBytesGlobal, offsetInBytes); + /*--- Remove the "_x" from the name. ---*/ + fieldname.erase(fieldname.end()-2, fieldname.end()); + WriteMPIString("\nVECTORS " + fieldname + " float\n", MASTER_NODE); + WriteVectorArray(VarCounter); VarCounter++; - } else if (output_variable) { + } else { - SPRINTF (str_buf, "\nSCALARS %s float 1\n", fieldname.c_str()); - WriteMPIString(str_buf, MASTER_NODE); + WriteMPIString("\nSCALARS " + fieldname + " float 1\n", MASTER_NODE); WriteMPIString("LOOKUP_TABLE default\n", MASTER_NODE); - /*--- For now, create a temp 1D buffer to load up the data for writing. - This will be replaced with a derived data type most likely. ---*/ - - for (iPoint = 0; iPoint < myPoint; iPoint++) { - auto val = (float)dataSorter->GetData(VarCounter,iPoint); - dataBufferFloat[iPoint] = val; - } - - if (!bigEndian) - SwapBytes((char *)dataBufferFloat.data(), sizeof(float), myPoint); - - /*--- Compute various data sizes --- */ - - sizeInBytesPerPoint = sizeof(float); - sizeInBytesLocal = sizeInBytesPerPoint*myPoint; - sizeInBytesGlobal = sizeInBytesPerPoint*GlobalPoint; - offsetInBytes = sizeInBytesPerPoint*dataSorter->GetnPointCumulative(rank); - - WriteMPIBinaryDataAll(dataBufferFloat.data(), sizeInBytesLocal, sizeInBytesGlobal, offsetInBytes); - + vector buffer(myPoint); + for (auto iPoint = 0ul; iPoint < myPoint; iPoint++) buffer[iPoint] = (float)dataSorter->GetData(VarCounter, iPoint); + WritePointArray(buffer, 1); VarCounter++; } - } - - CloseMPIFile(); - } From 40f1bdb2bf47053456433c60a77f7a1fdc48a905 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Sat, 3 Oct 2026 10:47:43 +0200 Subject: [PATCH 34/35] Use the type of the number of fields for the field index of the Paraview writers Fixes the CodeQL warnings about comparing an unsigned short with a size_t. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LEL91DW5WPbPwgFtCvHga6 --- SU2_CFD/src/output/filewriter/CParaviewBinaryFileWriter.cpp | 2 +- SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/SU2_CFD/src/output/filewriter/CParaviewBinaryFileWriter.cpp b/SU2_CFD/src/output/filewriter/CParaviewBinaryFileWriter.cpp index 53e717cae1b3..0135fe049f15 100644 --- a/SU2_CFD/src/output/filewriter/CParaviewBinaryFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CParaviewBinaryFileWriter.cpp @@ -233,7 +233,7 @@ void CParaviewBinaryFileWriter::WritePointData() { vector, written with its "_y" (and "_z") components, which are then skipped. ---*/ unsigned short VarCounter = varStart; - for (unsigned short iField = varStart; iField < fieldNames.size(); iField++) { + for (unsigned long iField = varStart; iField < fieldNames.size(); iField++) { string fieldname = fieldNames[iField]; fieldname.erase(remove(fieldname.begin(), fieldname.end(), '"'), fieldname.end()); diff --git a/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp b/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp index 7f9cf66ad34e..e1b2436c3ab9 100644 --- a/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp @@ -150,7 +150,7 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ /*--- Loop over all variables that have been registered in the output. ---*/ unsigned short VarCounter = varStart; - for (unsigned short iField = varStart; iField < fieldNames.size(); iField++) { + for (unsigned long iField = varStart; iField < fieldNames.size(); iField++) { string fieldname = fieldNames[iField]; fieldname.erase(remove(fieldname.begin(), fieldname.end(), '"'), @@ -281,7 +281,7 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ /*--- Loop over all variables that have been registered in the output. ---*/ VarCounter = varStart; - for (unsigned short iField = varStart; iField < fieldNames.size(); iField++) { + for (unsigned long iField = varStart; iField < fieldNames.size(); iField++) { /*--- Check whether this field is a vector or scalar. ---*/ From 1930fa0a73502162345092b137df8720e067cbca Mon Sep 17 00:00:00 2001 From: rois1995 Date: Mon, 5 Oct 2026 15:50:43 +0200 Subject: [PATCH 35/35] Rename WRT_OUTPUT_DOUBLE_PRECISION to VOLUME_OUTPUT_PRECISION= SINGLE | DOUBLE It is easier to tell apart from OUTPUT_PRECISION, whose description now says that it only sets the digits of the text outputs. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LEL91DW5WPbPwgFtCvHga6 --- Common/include/CConfig.hpp | 9 ++++++--- Common/include/option_structure.hpp | 12 ++++++++++++ Common/src/CConfig.cpp | 13 ++++++++----- SU2_CFD/src/output/COutput.cpp | 8 ++++---- .../output/filewriter/CParaviewVTMFileWriter.cpp | 4 ++-- config_template.cfg | 15 ++++++++------- 6 files changed, 40 insertions(+), 21 deletions(-) diff --git a/Common/include/CConfig.hpp b/Common/include/CConfig.hpp index afbcbad0876f..9d4920c871b0 100644 --- a/Common/include/CConfig.hpp +++ b/Common/include/CConfig.hpp @@ -747,7 +747,6 @@ class CConfig { su2double Cauchy_Eps; /*!< \brief Epsilon used for the convergence. */ bool Restart, /*!< \brief Restart solution (for direct, adjoint, and linearized problems).*/ Wrt_Restart_Compact, /*!< \brief Write compact restart files with minimum nr. of variables. */ - Wrt_Output_Double_Precision, /*!< \brief Write the fields of the volume and surface files in double. */ Read_Binary_Restart, /*!< \brief Read binary SU2 native restart files.*/ Wrt_Restart_Overwrite, /*!< \brief Overwrite restart files or append iteration number.*/ Wrt_Surface_Overwrite, /*!< \brief Overwrite surface output files or append iteration number.*/ @@ -836,6 +835,7 @@ class CConfig { unsigned short Mesh_FileFormat; /*!< \brief Mesh input format. */ unsigned short Mesh_Out_FileFormat; /*!< \brief Mesh output format. */ TAB_OUTPUT Tab_FileFormat; /*!< \brief Format of the output files. */ + VOLUME_OUTPUT_PRECISION Volume_Output_Precision; /*!< \brief Floating-point type of the volume and surface fields. */ unsigned short output_precision; /*!< \brief .precision(value) for SU2_DOT and HISTORY output */ unsigned short ActDisk_Jump; /*!< \brief Format of the output files. */ unsigned long StartWindowIteration; /*!< \brief Starting Iteration for long time Windowing apporach . */ @@ -5684,10 +5684,13 @@ class CConfig { bool GetWrt_Restart_Compact(void) const { return Wrt_Restart_Compact; } /*! - * \brief Flag for whether the fields of the volume and surface files are written in double precision. + * \brief Flag for whether the fields of the volume and surface files (CGNS and Paraview XML) are written in + * double precision. * \return TRUE means that double precision is used. */ - bool GetWrt_Output_Double_Precision(void) const { return Wrt_Output_Double_Precision; } + bool GetVolume_Output_Double_Precision(void) const { + return Volume_Output_Precision == VOLUME_OUTPUT_PRECISION::DOUBLE; + } /*! * \brief Flag for whether restart solution files are overwritten. diff --git a/Common/include/option_structure.hpp b/Common/include/option_structure.hpp index bcd2e9c06328..351e534671ae 100644 --- a/Common/include/option_structure.hpp +++ b/Common/include/option_structure.hpp @@ -2429,6 +2429,18 @@ static const MapType TabOutput_Map = { MakePair("TECPLOT", TAB_OUTPUT::TAB_TECPLOT) }; +/*! + * \brief Floating-point type of the fields of the volume and surface files (CGNS and Paraview XML). + */ +enum class VOLUME_OUTPUT_PRECISION { + SINGLE, /*!< \brief Single precision (float). */ + DOUBLE /*!< \brief Double precision (double). */ +}; +static const MapType VolumeOutputPrecision_Map = { + MakePair("SINGLE", VOLUME_OUTPUT_PRECISION::SINGLE) + MakePair("DOUBLE", VOLUME_OUTPUT_PRECISION::DOUBLE) +}; + /*! * \brief Type of volume sensitivity file formats (inout to SU2_DOT) */ diff --git a/Common/src/CConfig.cpp b/Common/src/CConfig.cpp index 8608b15b995a..1ab7ced2d354 100644 --- a/Common/src/CConfig.cpp +++ b/Common/src/CConfig.cpp @@ -1263,10 +1263,11 @@ void CConfig::SetConfig_Options() { addBoolOption("RESTART_SOL", Restart, false); /*!\brief WRT_RESTART_COMPACT \n DESCRIPTION: Minimize the size of restart files \n Options: NO, YES \ingroup Config */ addBoolOption("WRT_RESTART_COMPACT", Wrt_Restart_Compact, true); - /*!\brief WRT_OUTPUT_DOUBLE_PRECISION \n DESCRIPTION: Write the fields of the volume and surface files (CGNS and - Paraview XML) in double precision instead of single, the coordinates are always written in double precision by the - CGNS writer. \n Options: NO, YES \ingroup Config */ - addBoolOption("WRT_OUTPUT_DOUBLE_PRECISION", Wrt_Output_Double_Precision, false); + /*!\brief VOLUME_OUTPUT_PRECISION \n DESCRIPTION: Floating-point type of the fields of the volume and surface files + (CGNS and Paraview XML), the coordinates are always written in double precision by the CGNS writer. + \n Options: SINGLE, DOUBLE \ingroup Config */ + addEnumOption("VOLUME_OUTPUT_PRECISION", Volume_Output_Precision, VolumeOutputPrecision_Map, + VOLUME_OUTPUT_PRECISION::SINGLE); /*!\brief BINARY_RESTART \n DESCRIPTION: Read binary SU2 native restart files. \n Options: YES, NO \ingroup Config */ addBoolOption("READ_BINARY_RESTART", Read_Binary_Restart, true); /*!\brief WRT_RESTART_OVERWRITE \n DESCRIPTION: overwrite restart files or append iteration number. \n Options: YES, NO \ingroup Config */ @@ -2341,7 +2342,9 @@ void CConfig::SetConfig_Options() { /*!\brief OUTPUT_FORMAT \n DESCRIPTION: I/O format for output plots. \n OPTIONS: see \link TabOutput_Map \endlink \n DEFAULT: TECPLOT \ingroup Config */ addEnumOption("TABULAR_FORMAT", Tab_FileFormat, TabOutput_Map, TAB_OUTPUT::TAB_CSV); - /*!\brief OUTPUT_PRECISION \n DESCRIPTION: Set .precision(value) to specified value for SU2_DOT and HISTORY output. Useful for exact gradient validation. \n DEFAULT: 6 \ingroup Config */ + /*!\brief OUTPUT_PRECISION \n DESCRIPTION: Number of digits of the text outputs (SU2_DOT, HISTORY and screen). + Useful for exact gradient validation. It does not change the volume and surface files, see VOLUME_OUTPUT_PRECISION. + \n DEFAULT: 10 \ingroup Config */ addUnsignedShortOption("OUTPUT_PRECISION", output_precision, 10); /*!\brief ACTDISK_JUMP \n DESCRIPTION: The jump is given by the difference in values or a ratio */ addEnumOption("ACTDISK_JUMP", ActDisk_Jump, Jump_Map, DIFFERENCE); diff --git a/SU2_CFD/src/output/COutput.cpp b/SU2_CFD/src/output/COutput.cpp index 9d863aac517c..7b21d6499758 100644 --- a/SU2_CFD/src/output/COutput.cpp +++ b/SU2_CFD/src/output/COutput.cpp @@ -556,7 +556,7 @@ void COutput::WriteToFile(CConfig *config, CGeometry *geometry, OUTPUT_TYPE form volumeDataSorter->SortConnectivity(config, geometry, true); LogOutputFiles("Paraview"); - fileWriter = new CParaviewXMLFileWriter(volumeDataSorter, config->GetWrt_Output_Double_Precision()); + fileWriter = new CParaviewXMLFileWriter(volumeDataSorter, config->GetVolume_Output_Double_Precision()); break; @@ -682,7 +682,7 @@ void COutput::WriteToFile(CConfig *config, CGeometry *geometry, OUTPUT_TYPE form surfaceDataSorter->SortOutputData(); LogOutputFiles("Paraview surface"); - fileWriter = new CParaviewXMLFileWriter(surfaceDataSorter, config->GetWrt_Output_Double_Precision()); + fileWriter = new CParaviewXMLFileWriter(surfaceDataSorter, config->GetVolume_Output_Double_Precision()); break; @@ -760,7 +760,7 @@ void COutput::WriteToFile(CConfig *config, CGeometry *geometry, OUTPUT_TYPE form LogOutputFiles("CGNS"); { - auto* cgnsWriter = new CCGNSFileWriter(volumeDataSorter, false, config->GetWrt_Output_Double_Precision()); + auto* cgnsWriter = new CCGNSFileWriter(volumeDataSorter, false, config->GetVolume_Output_Double_Precision()); /*--- Write the boundaries, named as the markers. This needs to know which boundary elements are halo copies, which only the finite volume data sorter knows: the files of the FEM solver have no boundaries. ---*/ @@ -787,7 +787,7 @@ void COutput::WriteToFile(CConfig *config, CGeometry *geometry, OUTPUT_TYPE form LogOutputFiles("CGNS surface"); { - auto* cgnsWriter = new CCGNSFileWriter(surfaceDataSorter, true, config->GetWrt_Output_Double_Precision()); + auto* cgnsWriter = new CCGNSFileWriter(surfaceDataSorter, true, config->GetVolume_Output_Double_Precision()); /*--- One zone per plotted marker, named as the marker. ---*/ cgnsWriter->SetSurfaceMarkers(config, geometry); diff --git a/SU2_CFD/src/output/filewriter/CParaviewVTMFileWriter.cpp b/SU2_CFD/src/output/filewriter/CParaviewVTMFileWriter.cpp index 48d400df48e9..03fd4562a24c 100644 --- a/SU2_CFD/src/output/filewriter/CParaviewVTMFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CParaviewVTMFileWriter.cpp @@ -137,7 +137,7 @@ void CParaviewVTMFileWriter::WriteFolderData(const string& foldername, CConfig * StartBlock(std::move(multiZoneHeaderString)); StartBlock("Internal"); - AddDataset(foldername, "Internal", "Internal", volumeDataSorter, config->GetWrt_Output_Double_Precision()); + AddDataset(foldername, "Internal", "Internal", volumeDataSorter, config->GetVolume_Output_Double_Precision()); EndBlock(); /*--- Open a block for the boundary ---*/ @@ -181,7 +181,7 @@ void CParaviewVTMFileWriter::WriteFolderData(const string& foldername, CConfig * /*--- Add the dataset ---*/ - AddDataset(foldername, markerTag, markerTag, surfaceDataSorter, config->GetWrt_Output_Double_Precision()); + AddDataset(foldername, markerTag, markerTag, surfaceDataSorter, config->GetVolume_Output_Double_Precision()); } } diff --git a/config_template.cfg b/config_template.cfg index 4e8057787a87..5e611f22ed48 100644 --- a/config_template.cfg +++ b/config_template.cfg @@ -2655,15 +2655,16 @@ SOLUTION_ADJ_FILENAME= solution_adj % Output tabular file format (TECPLOT, CSV) TABULAR_FORMAT= CSV % -% Set .precision(value) to specified value for SU2_DOT and HISTORY output. Useful for exact gradient validation. +% Number of digits of the text outputs (SU2_DOT, history and screen). Useful for exact +% gradient validation. It does not change the volume and surface files (see VOLUME_OUTPUT_PRECISION). OUTPUT_PRECISION= 10 % -% Write the fields of the volume and surface files (CGNS and Paraview XML) in double -% precision instead of single. The CGNS coordinates are always in double precision, -% the Paraview XML coordinates follow this option. Single precision (about 7 digits) -% is enough to view the results; use double precision when the values are processed -% further, e.g. to compare two solutions or to match the points of periodic surfaces. -WRT_OUTPUT_DOUBLE_PRECISION= NO +% Floating-point type of the fields of the volume and surface files (CGNS and Paraview XML): +% SINGLE or DOUBLE. The CGNS coordinates are always in double precision, the Paraview XML +% coordinates follow this option. SINGLE (about 7 digits) is enough to view the results; use +% DOUBLE when the values are processed further, e.g. to compare two solutions or to match the +% points of periodic surfaces. +VOLUME_OUTPUT_PRECISION= SINGLE % % For multizone problems, extend solution and restart filenames automatically by zone number MULTIZONE_ADAPT_FILENAME= YES