diff --git a/.github/workflows/regression.yml b/.github/workflows/regression.yml index 992a0d6e30ad..3257e62c424b 100644 --- a/.github/workflows/regression.yml +++ b/.github/workflows/regression.yml @@ -235,7 +235,7 @@ jobs: uses: docker://ghcr.io/su2code/su2/test-su2:261011-1107 with: # -t -c - args: -b ${{github.ref}} -t develop -c develop -s ${{matrix.testscript}} + args: -b ${{github.ref}} -t develop -c cgnsReaderMarkers -s ${{matrix.testscript}} - name: Cleanup uses: docker://ghcr.io/su2code/su2/test-su2:261011-1107 with: @@ -284,7 +284,7 @@ jobs: uses: docker://ghcr.io/su2code/su2/test-su2:261011-1107 with: # -t -c - args: -b ${{github.ref}} -t develop -c develop -s ${{matrix.testscript}} -a "--tapetests" + args: -b ${{github.ref}} -t develop -c cgnsReaderMarkers -s ${{matrix.testscript}} -a "--tapetests" - name: Cleanup uses: docker://ghcr.io/su2code/su2/test-su2:261011-1107 with: @@ -334,7 +334,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 cgnsReaderMarkers -s ${{matrix.testscript}} -a "--tsan" - name: Cleanup uses: docker://ghcr.io/su2code/su2/test-su2-tsan:261011-1107 with: @@ -379,7 +379,7 @@ jobs: uses: docker://ghcr.io/su2code/su2/test-su2-asan:261011-1107 with: # -t -c - args: -b ${{github.ref}} -t develop -c develop -s ${{matrix.testscript}} -a "--asan" + args: -b ${{github.ref}} -t develop -c cgnsReaderMarkers -s ${{matrix.testscript}} -a "--asan" - name: Cleanup uses: docker://ghcr.io/su2code/su2/test-su2-asan:261011-1107 with: diff --git a/Common/include/geometry/meshreader/CCGNSMeshReaderBase.hpp b/Common/include/geometry/meshreader/CCGNSMeshReaderBase.hpp index a49c3493f78f..2a71af78ab90 100644 --- a/Common/include/geometry/meshreader/CCGNSMeshReaderBase.hpp +++ b/Common/include/geometry/meshreader/CCGNSMeshReaderBase.hpp @@ -58,6 +58,10 @@ class CCGNSMeshReaderBase : public CMeshReaderBase { CGNS section. First index is the section, second contains the connectivity in format [globalID VTK n1 n2 n3 n4 n5 n6 n7 n8] for each element. */ vector > sectionNames; /*!< \brief Vector for storing the names of each boundary section (marker). */ + vector sectionStart; /*!< \brief First element index of each CGNS section. */ + vector sectionEnd; /*!< \brief Last element index of each CGNS section. */ + vector sectionMarkerNames; /*!< \brief Marker name of each boundary section, several sections can share + one name, e.g. one with triangles and one with quadrilaterals. */ /*! * \brief Open the CGNS file and checks for errors. @@ -86,6 +90,14 @@ class CCGNSMeshReaderBase : public CMeshReaderBase { */ void ReadCGNSSectionMetadata(); + /*! + * \brief Assign a marker name to each boundary section. The name of a section covered by a boundary condition of + * the zone (ZoneBC) is the family name of that boundary condition, or its name if it has no family. Sections that + * are not covered by any boundary condition keep the name of the section, which is the only option for files + * written without a ZoneBC node. + */ + void ReadCGNSBoundaryMarkerNames(); + /*! * \brief Get the VTK type and string name for a CGNS element type. * \param[in] val_elem_type - CGNS element type. diff --git a/Common/src/geometry/meshreader/CCGNSMeshReaderBase.cpp b/Common/src/geometry/meshreader/CCGNSMeshReaderBase.cpp index ee56074898f5..7207d1e11261 100644 --- a/Common/src/geometry/meshreader/CCGNSMeshReaderBase.cpp +++ b/Common/src/geometry/meshreader/CCGNSMeshReaderBase.cpp @@ -262,6 +262,8 @@ void CCGNSMeshReaderBase::ReadCGNSSectionMetadata() { elemOffset[0] = 0; connElems.resize(nSections); sectionNames.resize(nSections, vector(CGNS_STRING_SIZE)); + sectionStart.resize(nSections, 0); + sectionEnd.resize(nSections, 0); numberOfGlobalElements = 0; for (int s = 0; s < nSections; s++) { @@ -277,6 +279,8 @@ void CCGNSMeshReaderBase::ReadCGNSSectionMetadata() { /*--- Compute the total element count in this section (global). ---*/ unsigned long element_count = (endE - startE + 1); + sectionStart[s] = startE; + sectionEnd[s] = endE; /* Get the details for the CGNS element type in this section. */ @@ -373,6 +377,101 @@ void CCGNSMeshReaderBase::ReadCGNSSectionMetadata() { } } +void CCGNSMeshReaderBase::ReadCGNSBoundaryMarkerNames() { + /*--- Without a matching boundary condition a section is a marker of its own, named after the section. ---*/ + + sectionMarkerNames.resize(nSections); + for (int s = 0; s < nSections; s++) sectionMarkerNames[s] = string(sectionNames[s].data()); + + /*--- Name given by the boundary conditions to each section, and number of its elements they cover. ---*/ + + vector bocoNames(nSections); + vector nCovered(nSections, 0); + vector severalNames(nSections, false); + + int nBocos = 0; + if (cg_nbocos(cgnsFileID, cgnsBase, cgnsZone, &nBocos)) cg_error_exit(); + + for (int iBoco = 1; iBoco <= nBocos; iBoco++) { + char bocoName[CGNS_STRING_SIZE]; + BCType_t bocoType; + PointSetType_t pointSetType; + cgsize_t nPoints, normalListSize; + int normalIndex, nDataset; + DataType_t normalDataType; + + if (cg_boco_info(cgnsFileID, cgnsBase, cgnsZone, iBoco, bocoName, &bocoType, &pointSetType, &nPoints, &normalIndex, + &normalListSize, &normalDataType, &nDataset)) + cg_error_exit(); + + /*--- Only a boundary condition given by elements can be matched to a section, one given by points cannot. + Older files give the elements with ElementRange or ElementList, without a GridLocation. ---*/ + + GridLocation_t location = Vertex; + cg_boco_gridlocation_read(cgnsFileID, cgnsBase, cgnsZone, iBoco, &location); + const bool byRange = (pointSetType == PointRange || pointSetType == ElementRange); + const bool byElements = (pointSetType == ElementRange || pointSetType == ElementList || location == FaceCenter || + location == EdgeCenter || location == CellCenter); + if (!byElements || (byRange && nPoints != 2)) continue; + + /*--- The marker takes the name of the family of the boundary condition, if it has one. The family name can be + a path in the family tree of the file (CGNS 4), in which case only its last part is kept. ---*/ + + string markerName = string(bocoName); + if (cg_goto(cgnsFileID, cgnsBase, "Zone_t", cgnsZone, "ZoneBC_t", 1, "BC_t", iBoco, "end") == CG_OK) { + char familyName[CG_MAX_GOTO_DEPTH * (CGNS_STRING_SIZE + 1) + 1] = {}; + if (cg_famname_read(familyName) == CG_OK) { + markerName = string(familyName); + const auto slash = markerName.find_last_of('/'); + if (slash != string::npos) markerName = markerName.substr(slash + 1); + } + } + + /*--- Count the elements of each boundary section that belong to this boundary condition. ---*/ + + vector points(nPoints); + if (cg_boco_read(cgnsFileID, cgnsBase, cgnsZone, iBoco, points.data(), nullptr)) cg_error_exit(); + + vector nInSection(nSections, 0); + if (byRange) { + for (int s = 0; s < nSections; s++) + if (!isInterior[s]) + nInSection[s] = max(0, min(points[1], sectionEnd[s]) - max(points[0], sectionStart[s]) + 1); + } else { + for (const auto point : points) { + for (int s = 0; s < nSections; s++) { + if (!isInterior[s] && point >= sectionStart[s] && point <= sectionEnd[s]) { + nInSection[s]++; + break; + } + } + } + } + + for (int s = 0; s < nSections; s++) { + if (nInSection[s] == 0) continue; + if (!bocoNames[s].empty() && bocoNames[s] != markerName) severalNames[s] = true; + bocoNames[s] = markerName; + nCovered[s] += nInSection[s]; + } + } + + /*--- A section takes the name of its boundary condition only if all its elements belong to boundary conditions + with that name. Otherwise it is kept as one marker named after the section, since a section cannot be split. ---*/ + + for (int s = 0; s < nSections; s++) { + if (isInterior[s] || bocoNames[s].empty()) continue; + + if (severalNames[s] || nCovered[s] < sectionEnd[s] - sectionStart[s] + 1) { + if (rank == MASTER_NODE) + cout << "WARNING: the boundary conditions of the file do not cover all elements of the section " + << sectionMarkerNames[s] << " with one name, the section is a marker named after the section." << endl; + continue; + } + sectionMarkerNames[s] = bocoNames[s]; + } +} + string CCGNSMeshReaderBase::GetCGNSElementType(ElementType_t val_elem_type, int& val_vtk_type) { /* Check the CGNS element type and return the string name for the element and the associated VTK type index. */ diff --git a/Common/src/geometry/meshreader/CCGNSMeshReaderFVM.cpp b/Common/src/geometry/meshreader/CCGNSMeshReaderFVM.cpp index 501e188e40cd..b67e41d319a1 100644 --- a/Common/src/geometry/meshreader/CCGNSMeshReaderFVM.cpp +++ b/Common/src/geometry/meshreader/CCGNSMeshReaderFVM.cpp @@ -48,12 +48,11 @@ CCGNSMeshReaderFVM::CCGNSMeshReaderFVM(const CConfig* val_config, unsigned short section together), the master node reads the boundary section. Otherwise, all ranks read and communicate the interior sections. ---*/ ReadCGNSSectionMetadata(); - numberOfMarkers = 0; + ReadCGNSBoundaryMarkerNames(); for (int s = 0; s < nSections; s++) { if (isInterior[s]) { ReadCGNSVolumeSection(s); } else { - numberOfMarkers++; ReadCGNSSurfaceSection(s); } } @@ -619,39 +618,57 @@ void CCGNSMeshReaderFVM::ReformatCGNSVolumeConnectivity() { } void CCGNSMeshReaderFVM::ReformatCGNSSurfaceConnectivity() { - /*--- Prepare the class data for the marker names and connectivity. ---*/ + /*--- Group the boundary sections by marker name, a marker can be split over several sections, for example one + with triangles and one with quadrilaterals. Remove any whitespaces from the names to avoid any issues. ---*/ - markerNames.resize(numberOfMarkers); - surfaceElementConnectivity.resize(numberOfMarkers); + markerNames.clear(); + vector markerOfSection(nSections, -1); - int markerCount = 0; - int elementCount = 0; for (int s = 0; s < nSections; s++) { - if (!isInterior[s]) { - /*--- Store the tag for this marker. Remove any whitespaces from - the marker names found in the CGNS file to avoid any issues. ---*/ - - string Marker_Tag = string(sectionNames[s].data()); - Marker_Tag.erase(remove(Marker_Tag.begin(), Marker_Tag.end(), ' '), Marker_Tag.end()); - markerNames[markerCount] = Marker_Tag; - - /*--- The master node alone stores the connectivity. ---*/ - - if (rank == MASTER_NODE) { - surfaceElementConnectivity[markerCount].resize(nElems[s] * SU2_CONN_SIZE); - elementCount = 0; - for (unsigned long iElem = 0; iElem < nElems[s]; iElem++) { - for (unsigned long iNode = 0; iNode < SU2_CONN_SIZE; iNode++) { - unsigned long nn = iElem * SU2_CONN_SIZE + iNode; - surfaceElementConnectivity[markerCount][elementCount] = static_cast(connElems[s][nn]); - elementCount++; - } - } - vector().swap(connElems[s]); + if (isInterior[s]) continue; + + string Marker_Tag = sectionMarkerNames[s]; + Marker_Tag.erase(remove(Marker_Tag.begin(), Marker_Tag.end(), ' '), Marker_Tag.end()); + + const auto it = find(markerNames.begin(), markerNames.end(), Marker_Tag); + markerOfSection[s] = static_cast(distance(markerNames.begin(), it)); + if (it == markerNames.end()) markerNames.push_back(Marker_Tag); + } + + numberOfMarkers = markerNames.size(); + + /*--- Tell the user which sections were merged into one marker. ---*/ + + if (rank == MASTER_NODE) { + for (int iMarker = 0; iMarker < static_cast(markerNames.size()); iMarker++) { + vector sections; + for (int s = 0; s < nSections; s++) + if (markerOfSection[s] == iMarker) sections.push_back(string(sectionNames[s].data())); + + if (sections.size() > 1) { + cout << "Marker " << markerNames[iMarker] << " groups the sections"; + for (const auto& section : sections) cout << " " << section; + cout << "." << endl; } - markerCount++; } } + + /*--- Prepare the class data for the connectivity. The master node alone stores it. ---*/ + + surfaceElementConnectivity.clear(); + surfaceElementConnectivity.resize(numberOfMarkers); + + if (rank != MASTER_NODE) return; + + for (int s = 0; s < nSections; s++) { + if (isInterior[s]) continue; + + auto& markerConnectivity = surfaceElementConnectivity[markerOfSection[s]]; + for (unsigned long iNode = 0; iNode < nElems[s] * SU2_CONN_SIZE; iNode++) + markerConnectivity.push_back(static_cast(connElems[s][iNode])); + + vector().swap(connElems[s]); + } } #endif diff --git a/TestCases/cgns_reader/channel_sections.cfg b/TestCases/cgns_reader/channel_sections.cfg new file mode 100644 index 000000000000..f67194284c45 --- /dev/null +++ b/TestCases/cgns_reader/channel_sections.cfg @@ -0,0 +1,61 @@ +% Test of the CGNS mesh reader with boundaries split in several sections, as meshers write them. +% The mesh is a small 3D channel, with hexahedra in the first half and prisms in the second half. +% The CGNS file has 9 boundary sections and a ZoneBC with 5 boundary conditions (BC): +% inlet : no BC covers it, the marker is named after the section ("inlet") +% upper_side : BC "upper" without family, the marker is named after the BC ("upper") +% lower_1, lower_2 : BC "lower_wall" of family "lower", one marker "lower" +% quad_back, tri_back : BC "back" of family "back", one marker "back" with quadrilaterals and triangles +% quad_front, tri_front : BC "front" of family "front", one marker "front" +% outlet : BC "outlet" of family "outlet", marker "outlet" +% The markers of this config file are the names above, so the case only runs if the reader +% groups the sections into markers through the ZoneBC. +% +% ------------- DIRECT, ADJOINT, AND LINEARIZED PROBLEM DEFINITION ------------% +SOLVER= EULER +MATH_PROBLEM= DIRECT +RESTART_SOL= NO +% +% ----------- COMPRESSIBLE AND INCOMPRESSIBLE FREE-STREAM DEFINITION ----------% +MACH_NUMBER= 0.3 +AOA= 0.0 +SIDESLIP_ANGLE= 0.0 +FREESTREAM_PRESSURE= 101325.0 +FREESTREAM_TEMPERATURE= 288.15 +% +% ---------------------- REFERENCE VALUE DEFINITION ---------------------------% +REF_ORIGIN_MOMENT_X= 0.0 +REF_ORIGIN_MOMENT_Y= 0.0 +REF_ORIGIN_MOMENT_Z= 0.0 +REF_LENGTH= 1.0 +REF_AREA= 1.0 +% +% -------------------- BOUNDARY CONDITION DEFINITION --------------------------% +MARKER_EULER= ( lower, upper ) +MARKER_SYM= ( back, front ) +MARKER_INLET= ( inlet, 293.336, 107822.0, 1.0, 0.0, 0.0 ) +MARKER_OUTLET= ( outlet, 101325.0 ) +% +% ------------- COMMON PARAMETERS DEFINING THE NUMERICAL METHOD ---------------% +NUM_METHOD_GRAD= GREEN_GAUSS +CFL_NUMBER= 10.0 +CFL_ADAPT= NO +ITER= 10 +% +% ------------------------ 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 +% +% ------------------------- INPUT/OUTPUT INFORMATION --------------------------% +MESH_FILENAME= mesh_channel_sections.cgns +MESH_FORMAT= CGNS +SCREEN_OUTPUT= INNER_ITER RMS_DENSITY RMS_MOMENTUM-X RMS_MOMENTUM-Y RMS_ENERGY +HISTORY_OUTPUT= ITER RMS_RES +OUTPUT_FILES= NONE diff --git a/TestCases/parallel_regression.py b/TestCases/parallel_regression.py index 89bdeb0fe118..fc580e7aa67a 100755 --- a/TestCases/parallel_regression.py +++ b/TestCases/parallel_regression.py @@ -2026,6 +2026,14 @@ def main(): cgns_mesh_readback_bend.test_vals = [-3.256728, -2.769844, -3.007840, -3.567315, 4.318459] test_list.append(cgns_mesh_readback_bend) + # CGNS mesh whose boundaries are split in several sections, grouped into markers by the ZoneBC. + cgns_reader_sections = TestCase('cgns_reader_sections') + cgns_reader_sections.cfg_dir = "cgns_reader" + cgns_reader_sections.cfg_file = "channel_sections.cfg" + cgns_reader_sections.test_iter = 9 + cgns_reader_sections.test_vals = [-4.344863, -1.284117, -3.081432, 0.924734] + test_list.append(cgns_reader_sections) + ###################################### ### RUN CHT TEST WITH FILEDIFF ### ###################################### diff --git a/TestCases/serial_regression.py b/TestCases/serial_regression.py index 1edd0378ab2a..19b5013953f0 100755 --- a/TestCases/serial_regression.py +++ b/TestCases/serial_regression.py @@ -1331,6 +1331,14 @@ def main(): cgns_mesh_readback_bend.test_vals = [-3.254168, -2.756564, -3.006605, -3.572686, 4.308287] test_list.append(cgns_mesh_readback_bend) + # CGNS mesh whose boundaries are split in several sections, grouped into markers by the ZoneBC. + cgns_reader_sections = TestCase('cgns_reader_sections') + cgns_reader_sections.cfg_dir = "cgns_reader" + cgns_reader_sections.cfg_file = "channel_sections.cfg" + cgns_reader_sections.test_iter = 9 + cgns_reader_sections.test_vals = [-4.344864, -1.284117, -3.081432, 0.924734] + test_list.append(cgns_reader_sections) + ###################################### ### RUN TESTS ### ######################################