Reading a mesh from a CGNS file is pretty straightforward, but it helps to have some tools at hand to inspect CGNS files before we even start opening them, just to get a feeling for what type of information is stored and under which nodes. In this way, we know what mid-level library functions to call later to get all of that information out of the file. In this article, we look at a CGNS tool called cgnslist
, which allows us to display the entire tree within a CGNS file. We will use this tool to inspect the structured and unstructured grids and see how certain nodes can be stored in different manners within CGNS files.
By the end of this article, you should feel comfortable inspecting your own CGNS files to get an idea of what data is written to them. You will be able to identify nodes that you have to read, such as the grid coordinate, connectivity, interface and boundary nodes, and with that knowledge we will be in a good position to start developing our mesh reading library.
Download Resources
All developed code and resources in this article are available for download. If you are encountering issues running any of the scripts, please refer to the instructions for running scripts downloaded from this website.
- Download: cgnsMeshReaderLib-Part-1.zip
In this series
- Part 1: What is the CGNS format and how to get started
- Part 2: How to inspect structured and unstructured grids using CGNS
- Part 3: How to set up a simple CGNS-based mesh reading library
- Part 4: How to read a multi-block structured mesh from a CGNS file
- Part 5: How to read a multi-block unstructured mesh from a CGNS file
- Part 6: How to test our CGNS-based mesh reading library
In this article
Introduction
In our last article, we had a fairly high-level introduction to the CGNS file format and looked at how it stores data, why this is a good idea but also some of the criticism that frequently comes up (and by frequently I mean every time I try to implement it again for some solver development). The format is constantly adopting and improving so reading and writing a grid may require some additional reading of the Standard Interface Data Structure (SIDS) to see if things have changed.
In this article, I want to start inspecting some really mesh files. We won’t write any code just yet, we are trying to put all of the theoretical development of the last article into some practical context. We will inspect the CGNS files that you can download at the beginning of this article. For that, we will use one of the CGNS tools that is compiled as part of the library compilation step.
This article aims to provide you with a sufficient understanding of how data is written and represented in a CGNS file so that subsequent articles will become easier to follow. We need to have an intuitive understanding of how CGNS files are structured so that we need what sort of routines we have to call to get the information we want. There is a certain structure we need to follow and inspecting CGNS files will reveal that structure.
Example grid files
For the rest of this series, we will be working with 4 different mesh files: 2 structured and 2 unstructured grids. In terms of their nodes, vertices, interfaces, boundary conditions, etc. both structured and both unstructured grids are exactly the same. The way they differ is in the way we store the boundary conditions, and I hinted at this complication in my previous article during my criticism of the file format. We support both types of boundary reading and by inspecting the files using the provided CGNS tools, we can spot how boundary conditions are written.
If you download the above-provided zip archive, then you will find a folder called mesh/
, which contains the following 4 grid files:
structured2D.cgns
: A 2D, 2-block-structured CGNS file generated with Pointwise. Boundary conditions are written under a family node and not the boundary condition node.structured2DNoFamily.cgns
: A 2D, 2-block-structured CGNS file generated with Pointwise. Boundary conditions are directly under the boundary condition node.unstructured2D.cgns
: A 2D, single-block unstructured CGNS file generated with Pointwise. Boundary conditions are written under a family node and not the boundary condition node.unstructured2D
: A 2D, single-block unstructured CGNS file generated with Pointwise. Boundary conditions are directly under the boundary condition node.NoFamily
.cgns
As I mentioned above, both the structured2D.cgns
and structured2DNoFamily.cgns
files are identical except for the storage location of the boundary conditions within the CGNS file, and the same is true for the unstructured2D.cgns
and unstructured2D
file, respectively. Both the structured and unstructured grid are visualised below using ParaView (yes, I know I said ParaView has issues reading CGNS files in the last article, if you make use only of a small subset of non-controversial features, ParaView can read your mesh files just fine!):NoFamily
.cgns
Let’s analyse these two images and make some initial assumptions about how we would expect these grids to be represented in a CGNS file.
- structured mesh (left): This mesh contains two zones, both connected with a single interface in the middle. The different zones are here highlighted in black and red colour. Not shown in this figure are the boundary conditions which are of type wall for the south boundary, symmetry for the north boundary, inlet for the west and outlet for the east boundary. Both the wall and symmetry boundary conditions are thus split across the two separate zones. It is a 2D grid, so we would expect to read the x and y coordinates, and for each of them, we would expect a size of 5 by 5 (there are 5 vertices both in x and y in both zones).
- unstructured grid (right): This mesh also contains two zones, separated by a single interface in the middle. We use again red and black to separate the two zones. The left zone contains both triangle and quad elements and we expect to read both types from the mesh. We expect a total of 13 vertices for the left zone and 9 for the right, with 3 vertices in the interface. The boundary conditions are the same as for the structured grid described above.
In the case of the unstructured grid, we see that we have two zones. This is not strictly speaking necessary and during mesh generation, you could just throw all elements into the same zone (such is the beauty and appeal of unstructured grids). In this case, there wouldn’t be an interface and you could significantly reduce the amount of code you have to write, as you do not need to handle interfaces.
Since we want to learn the CGNS format, though, I didn’t want to make any simplifications here and specifically show how to read an unstructured mesh with more than two zones (grids). This may be useful when you want to mix both structured and unstructured zones for a truly hybrid solver, that solves portions of the flow using a structured grid and another portion with an unstructured solver.
So what’s a CGNS family node then?
We have already spoken a lot about the two different types of boundary condition writing and I wanted to address it here before we go on so that we have a clear idea why we have these different approaches and how we will deal with it. In general, we can write boundary conditions under the boundary or family node. While a boundary node is probably self-explanatory, the family node requires some explanation, and in general, the SIDS are probably the best place to learn about the CGNS format, albeit not always written in a very user-friendly way. Here is my definition of a family:
When we generate a CAD file, we typically associate a name to a group of surfaces to put them into logical meshing regions. A logical meshing region is typically a collection of surfaces on which we want to apply a certain type of boundary condition. During mesh generation, we first aim to create a body-fitted mesh that will align with the surfaces we have identified in our CAD model. Then, we want to apply the boundary conditions to our mesh that is associated with the group of surfaces. This puts us at a disadvantage, however; if we apply the boundary conditions directly to the mesh and then later decide to change it, we may lose the assigned boundary condition with it.
Thus, the CGNS steering committee has taken a more abstract approach to boundary condition assignment: All boundary conditions can be stored under an intermediary family node. Any surface mesh is then pointing to a particular family node and it will get its boundary condition from this family node. At the same time, the family node can point to a group of surfaces in a CAD file so should we change the mesh, the boundary information stay intact.
We don’t have to do this, though! CGNS offers us the possibility to store boundary conditions directly on the mesh. In this case, all boundary conditions will be stored in the same zone in which the coordinates of that grid are stored. If you are using Pointwise as a mesh generator, it allows you to define where you want to store the boundary information, other mesh generators may not be as liberal and you need to check with cgnscheck
, for example (which we will use below), where your boundary information have been stored.
What this means for us, is that we have to check during the mesh reading if boundary conditions are stored directly under the boundary node or the intermediary family node and then read the boundary conditions accordingly. As a side note, the recommended practice is to store boundary conditions under the family node, even if no corresponding CAD surfaces are stored alongside. We will implement both to see how they differ.
Just for fun, I calculated the Gunning-Fog Index for the family node description given in the SIDS which is about 16.1, which I find rather high. If you have never heard of the Gunning-Fog index, it tries to estimate how many years of formal education you need to understand a given text passage. An index of 16.1 suggest that you need 12 years of school + 4.1 years of university studies to understand the text. My description above scores 16.03, so apparently I am no better. I just wasted everyone’s time, but then again, you are still here …
Working with the CGNS tools
We already looked at in detail how we can compile the CGNS library on either Windows or UNIX (Linux, macOS). If you need a refresher, refer to my article on integrating external libraries into your code, which also contains two scripts for download that will fully automatically download, compile, and install the CGNS library with all of its dependencies for you. If you leave the default install directory (C:\libraries
on Windows and /home<username>/libs
on UNIX) then you can use all build scripts in this series without modifications.
If you follow the above-linked article on how to compile the CGNS library, you have three options; either compile the library manually using Autotools (please don’t), manually using CMake, or automatically using the build script. Whichever approach you have chosen, you will have access to the CGNS tools. If you compile the library manually, make sure to specify an install prefix and run the installation target. If you are using the script, this is done for you automatically.
The tools will be located in cgns-install-prefix/bin
for both Windows and UNIX. For example, if the install prefix on Windows is set to C:\libraries
, the CGNS tools would be available under C:\libraries\bin
. Similarly, on UNIX with an install prefix of /home/<usernam>/libs
, we will have access to the tools in /home/<username>/libs/bin
.
We will use the cgnslist
tool on the above described mesh files and inspect their content. Download the above-linked cgnsMeshReaderLib-Part-1.zip file which contains the 4 different types of grids within the mesh/
folder. Open a PowerShell (Windows) or a terminal (Linux, macOS) and navigate into the folder which contains the mesh/
folder. Then, you should be able to list the CGNS file with the following command:
- Windows:
C:\libraries\bin\cgnslist.exe .\mesh\structured2d.cgns
- UNIX:
~/libs/bin/cgnslist ./mesh/structured2D.cgns
Change here the name of the CGNS file to inspect any of the 4 different mesh files. You can also use the -a
flag to show some additional information about the type of each node and how much data is stored for each (e.g. C:\libraries\bin\cgnslist.exe -a .\mesh\structured2d.cgns
or ~/libs/bin/cgnslist -a ./mesh/structured2D.cgns
). This is useful later when we need to navigate to a specific node with a specific type within the CGNS file (which we will have to do at some point). For the moment, we don’t use the -a
flag as this will give us a clutter-free overview.
Inspecting structured grids
Let’s have a look at the output for the structured grid first. At this point, we are not trying to understand each individual node but rather get an overview of which nodes are available within the CGNS file. This will then provide us with some clues as to which mid-level library routines we have to use to retrieve the information later. We’ll go through them in detail in the next articles, for now, we are simply interested in the structure.
Below, I have provided the output for both the structured2D.cgns
file (left) and the structured2DNoFamily.cgns
file (right) as given by cgnslist
:
structured2D.cgns
HDF5 MotherNode
+-CGNSLibraryVersion
+-Base
+-Information
+-dom-1
| +-ZoneType
| +-FamilyName
| +-GridCoordinates
| | +-CoordinateX
| | | +-DimensionalExponents
| | | +-AdditionalExponents
| | +-CoordinateY
| | | +-DimensionalExponents
| | | +-AdditionalExponents
| | +-DataClass
| | +-DimensionalUnits
| +-ZoneBC
| | +-con-1
| | | +-PointRange
| | | +-GridLocation
| | | +-FamilyName
| | +-con-3
| | | +-PointRange
| | | +-GridLocation
| | | +-FamilyName
| | +-con-4
| | +-PointRange
| | +-GridLocation
| | +-FamilyName
| +-ZoneGridConnectivity
| +-1to1ConnectionA1
| +-Transform
| +-PointRange
| +-PointRangeDonor
+-Unspecified
| +-FamVC_TypeId
| +-FamVC_TypeName
| +-FamVC_UserId
| +-FamVC_UserName
+-wall
| +-FamBC
| +-Fam_Descr_Name
| +-FamBC_TypeId
| +-FamBC_TypeName
| +-FamBC_UserId
| +-FamBC_UserName
+-symmetry
| +-FamBC
| +-Fam_Descr_Name
| +-FamBC_TypeId
| +-FamBC_TypeName
| +-FamBC_UserId
| +-FamBC_UserName
+-inlet
| +-FamBC
| +-Fam_Descr_Name
| +-FamBC_TypeId
| +-FamBC_TypeName
| +-FamBC_UserId
| +-FamBC_UserName
+-dom-2
| +-ZoneType
| +-FamilyName
| +-GridCoordinates
| | +-CoordinateX
| | | +-DimensionalExponents
| | | +-AdditionalExponents
| | +-CoordinateY
| | | +-DimensionalExponents
| | | +-AdditionalExponents
| | +-DataClass
| | +-DimensionalUnits
| +-ZoneBC
| | +-con-5
| | | +-PointRange
| | | +-GridLocation
| | | +-FamilyName
| | +-con-6
| | | +-PointRange
| | | +-GridLocation
| | | +-FamilyName
| | +-con-7
| | +-PointRange
| | +-GridLocation
| | +-FamilyName
| +-ZoneGridConnectivity
| +-1to1ConnectionB1
| +-Transform
| +-PointRange
| +-PointRangeDonor
+-outlet
+-FamBC
+-Fam_Descr_Name
+-FamBC_TypeId
+-FamBC_TypeName
+-FamBC_UserId
+-FamBC_UserName
structured2DNoFamily.cgns
HDF5 MotherNode
+-CGNSLibraryVersion
+-Base
+-Information
+-dom-1
| +-ZoneType
| +-VC_TypeId
| +-VC_TypeName
| +-VC_UserId
| +-VC_UserName
| +-GridCoordinates
| | +-CoordinateX
| | | +-DimensionalExponents
| | | +-AdditionalExponents
| | +-CoordinateY
| | | +-DimensionalExponents
| | | +-AdditionalExponents
| | +-DataClass
| | +-DimensionalUnits
| +-ZoneBC
| | +-con-1
| | | +-PointRange
| | | +-GridLocation
| | | +-BC_TypeId
| | | +-BC_TypeName
| | | +-BC_UserId
| | | +-BC_UserName
| | +-con-3
| | | +-PointRange
| | | +-GridLocation
| | | +-BC_TypeId
| | | +-BC_TypeName
| | | +-BC_UserId
| | | +-BC_UserName
| | +-con-4
| | +-PointRange
| | +-GridLocation
| | +-BC_TypeId
| | +-BC_TypeName
| | +-BC_UserId
| | +-BC_UserName
| +-ZoneGridConnectivity
| +-1to1ConnectionA1
| +-Transform
| +-PointRange
| +-PointRangeDonor
+-dom-2
+-ZoneType
+-VC_TypeId
+-VC_TypeName
+-VC_UserId
+-VC_UserName
+-GridCoordinates
| +-CoordinateX
| | +-DimensionalExponents
| | +-AdditionalExponents
| +-CoordinateY
| | +-DimensionalExponents
| | +-AdditionalExponents
| +-DataClass
| +-DimensionalUnits
+-ZoneBC
| +-con-5
| | +-PointRange
| | +-GridLocation
| | +-BC_TypeId
| | +-BC_TypeName
| | +-BC_UserId
| | +-BC_UserName
| +-con-6
| | +-PointRange
| | +-GridLocation
| | +-BC_TypeId
| | +-BC_TypeName
| | +-BC_UserId
| | +-BC_UserName
| +-con-7
| +-PointRange
| +-GridLocation
| +-BC_TypeId
| +-BC_TypeName
| +-BC_UserId
| +-BC_UserName
+-ZoneGridConnectivity
+-1to1ConnectionB1
+-Transform
+-PointRange
+-PointRangeDonor
Analysing the structured2D.cgns file
Let’s inspect the left file first (structured2D.cgns
). The root node is the HDF5 MotherNode and all other nodes are children of this node. If a name is indented by spaces, this means it is a child node. For example, the Base node is indented with respect the HDF5 MotherNode, so the Base node is a child of the root. Similarily, dom-1 (domain 1, i.e. one of the two zones/grids) is a child of Base and indented with respect to the Base node.
There is just a single Base node, so we expect to read a single 2D grid that does not change over time. Under the Base, we have 7 children. Two of them are the two zones which contain our grid, which are called dom-1 and dom-2, respectively. We then have a child node called Unspecified which contains information about the volume conditions (note that its child nodes contain the string FamVC, i.e. family volume condition).
A volume condition is required for cases where we want to treat parts of the domain (volume) separately from the rest of the domain. Typical examples are the application of source terms (e.g. some volumetric heating). Other conditions could be to define some volume that should be treated as a porous medium, or we may want to apply some rotation for a specific volume which contains geometry like wheels or propellers. In our case, there is no volume conditions to be concerned with, hence Pointwise applies a default name of Unspecified here.
The remaining 4 zones are labelled wall, symmetry, inlet, and outlet and are our boundary conditions. Notice that these are not written directly to a specific grid node but rather are independent entities under the Base node. We also see under each boundary condition that the descriptors start with FamBC, i.e. Family Boundary Conditions. This tells us that we need to read the boundary conditions from the family node later, rather than reading it directly from the grid.
Let’s inspect the grid, then. This is stored under dom-1 and dom-2. For both of them we find 3 child nodes: GridCoordinates, ZoneBC, and ZoneGridConnectivity. The GridCoordinates are pretty self-explanatory, this is where we store the x and y coordinates of our mesh. Similarly, the ZoneGridConnectivity stores the start and end location of our interface between to adjacent zones. There is actually a pretty nifty transformation that we can apply to the indices on one side of the interface to calculate the indices on the other side of the interface. We will look at that in the next article.
Despite writing the boundary conditions to the family node, we still get a ZoneBC node? Why? God knows why. We have this possibility of writing boundary conditions either directly under the grid (i.e. into the ZoneBC node) or separately (i.e. under the Base node) but then we still get ZoneBC data. We will also later see that, even if we use the family nodes to store boundary conditions, we still need to retrieve some information from the ZoneBC node, and this makes boundary reading very messy. Honestly, in my view, this needs to be fixed, as writing code that can read family-based boundary conditions becomes messy, as we will see.
Analysing the structured2DNoFamily.cgns file
The structured2DNoFamily.cgns
file is shown above on the right. Let’s have a look at this as well. We already established that grid-wise we would expect the same structure, i.e. the same number of zones, the same boundary coordinates, the same interface and even the same boundary conditions, but these are just stored differently.
We see that the only difference, really, is just under the ZoneBC node within each grid (zone). If we inspect the children of the ZoneBC nodes for dom-1 and dom-2, we see that we now get information such as the BC_TypeName and BC_UserName, i.e. information about the name of the boundary condition and its type. In this case, the boundary condition is associated with the grid directly.
We don’t have any family nodes and even the volume condition which was labelled Unspecified before has now been moved to the grid zones directly. Thus, there are only 2 zones under the single Base. Otherwise, the file will contain the same information in terms of grid coordinates and interface information.
There are a few additional nodes which I have glossed over here; they are not important to understand the gist of a CGNS file, but rather, they will provide us with additional information should we need it. We’ll look into some of them later when we go through the CGNS file to read the actual mesh, interface, and boundary conditions.
Inspecting unstructured grids
As for the structured mesh, we can also look at the unstructured mesh as well which you will find in the same mesh/
folder. Again, we want to get an overview of what is stored inside the unstructured mesh, rather than understanding every node within the CGNS file right now. Most of that will become clearer once we look into the code to read the unstructured mesh.
The output from cgnslist
on both unstructured grids is shown below:
unstructured2D.cgns
HDF5 MotherNode
+-CGNSLibraryVersion
+-Base
+-Information
+-dom-1
| +-ZoneType
| +-FamilyName
| +-GridCoordinates
| | +-CoordinateX
| | | +-DimensionalExponents
| | | +-AdditionalExponents
| | +-CoordinateY
| | | +-DimensionalExponents
| | | +-AdditionalExponents
| | +-DataClass
| | +-DimensionalUnits
| +-TriElements
| | +-ElementRange
| | +-ElementConnectivity
| +-QuadElements
| | +-ElementRange
| | +-ElementConnectivity
| +-inlet
| | +-ElementRange
| | +-ElementConnectivity
| +-ZoneBC
| | +-inlet
| | | +-PointRange
| | | +-GridLocation
| | | +-FamilyName
| | +-symmetry
| | | +-PointRange
| | | +-GridLocation
| | | +-FamilyName
| | +-wall
| | +-PointRange
| | +-GridLocation
| | +-FamilyName
| +-symmetry
| | +-ElementRange
| | +-ElementConnectivity
| +-wall
| | +-ElementRange
| | +-ElementConnectivity
| +-con-2
| | +-ElementRange
| | +-ElementConnectivity
| +-ZoneGridConnectivity
| +-1to1Connection:con-2
| +-GridConnectivityType
| +-GridLocation
| +-PointList
| +-PointListDonor
+-Unspecified
| +-FamVC_TypeId
| +-FamVC_TypeName
| +-FamVC_UserId
| +-FamVC_UserName
+-dom-2
| +-ZoneType
| +-FamilyName
| +-GridCoordinates
| | +-CoordinateX
| | | +-DimensionalExponents
| | | +-AdditionalExponents
| | +-CoordinateY
| | | +-DimensionalExponents
| | | +-AdditionalExponents
| | +-DataClass
| | +-DimensionalUnits
| +-QuadElements
| | +-ElementRange
| | +-ElementConnectivity
| +-outlet
| | +-ElementRange
| | +-ElementConnectivity
| +-ZoneBC
| | +-outlet
| | | +-PointRange
| | | +-GridLocation
| | | +-FamilyName
| | +-symmetry
| | | +-PointRange
| | | +-GridLocation
| | | +-FamilyName
| | +-wall
| | +-PointRange
| | +-GridLocation
| | +-FamilyName
| +-symmetry
| | +-ElementRange
| | +-ElementConnectivity
| +-wall
| | +-ElementRange
| | +-ElementConnectivity
| +-con-2
| | +-ElementRange
| | +-ElementConnectivity
| +-ZoneGridConnectivity
| +-1to1Connection:con-2
| +-GridConnectivityType
| +-GridLocation
| +-PointList
| +-PointListDonor
+-inlet
| +-FamBC
| +-Fam_Descr_Name
| +-FamBC_TypeId
| +-FamBC_TypeName
| +-FamBC_UserId
| +-FamBC_UserName
+-symmetry
| +-FamBC
| +-Fam_Descr_Name
| +-FamBC_TypeId
| +-FamBC_TypeName
| +-FamBC_UserId
| +-FamBC_UserName
+-wall
| +-FamBC
| +-Fam_Descr_Name
| +-FamBC_TypeId
| +-FamBC_TypeName
| +-FamBC_UserId
| +-FamBC_UserName
+-outlet
+-FamBC
+-Fam_Descr_Name
+-FamBC_TypeId
+-FamBC_TypeName
+-FamBC_UserId
+-FamBC_UserName
unstructured2DNoFamily.cgns
HDF5 MotherNode
+-CGNSLibraryVersion
+-Base
+-Information
+-dom-1
| +-ZoneType
| +-VC_TypeId
| +-VC_TypeName
| +-VC_UserId
| +-VC_UserName
| +-GridCoordinates
| | +-CoordinateX
| | | +-DimensionalExponents
| | | +-AdditionalExponents
| | +-CoordinateY
| | | +-DimensionalExponents
| | | +-AdditionalExponents
| | +-DataClass
| | +-DimensionalUnits
| +-TriElements
| | +-ElementRange
| | +-ElementConnectivity
| +-QuadElements
| | +-ElementRange
| | +-ElementConnectivity
| +-inlet
| | +-ElementRange
| | +-ElementConnectivity
| +-ZoneBC
| | +-inlet
| | | +-PointRange
| | | +-GridLocation
| | | +-BC_TypeId
| | | +-BC_TypeName
| | | +-BC_UserId
| | | +-BC_UserName
| | +-symmetry
| | | +-PointRange
| | | +-GridLocation
| | | +-BC_TypeId
| | | +-BC_TypeName
| | | +-BC_UserId
| | | +-BC_UserName
| | +-wall
| | +-PointRange
| | +-GridLocation
| | +-BC_TypeId
| | +-BC_TypeName
| | +-BC_UserId
| | +-BC_UserName
| +-symmetry
| | +-ElementRange
| | +-ElementConnectivity
| +-wall
| | +-ElementRange
| | +-ElementConnectivity
| +-con-2
| | +-ElementRange
| | +-ElementConnectivity
| +-ZoneGridConnectivity
| +-1to1Connection:con-2
| +-GridConnectivityType
| +-GridLocation
| +-PointList
| +-PointListDonor
+-dom-2
+-ZoneType
+-VC_TypeId
+-VC_TypeName
+-VC_UserId
+-VC_UserName
+-GridCoordinates
| +-CoordinateX
| | +-DimensionalExponents
| | +-AdditionalExponents
| +-CoordinateY
| | +-DimensionalExponents
| | +-AdditionalExponents
| +-DataClass
| +-DimensionalUnits
+-QuadElements
| +-ElementRange
| +-ElementConnectivity
+-outlet
| +-ElementRange
| +-ElementConnectivity
+-ZoneBC
| +-outlet
| | +-PointRange
| | +-GridLocation
| | +-BC_TypeId
| | +-BC_TypeName
| | +-BC_UserId
| | +-BC_UserName
| +-symmetry
| | +-PointRange
| | +-GridLocation
| | +-BC_TypeId
| | +-BC_TypeName
| | +-BC_UserId
| | +-BC_UserName
| +-wall
| +-PointRange
| +-GridLocation
| +-BC_TypeId
| +-BC_TypeName
| +-BC_UserId
| +-BC_UserName
+-symmetry
| +-ElementRange
| +-ElementConnectivity
+-wall
| +-ElementRange
| +-ElementConnectivity
+-con-2
| +-ElementRange
| +-ElementConnectivity
+-ZoneGridConnectivity
+-1to1Connection:con-2
+-GridConnectivityType
+-GridLocation
+-PointList
+-PointListDonor
Analysing the unstructured2D.cgns file
If we have a closer look at the unstructured2D.cgns
file and compare it against the structured2D.cgns
file, we can see one major difference. The first zone (dom-1) has two additional children, named TriElements and QuadElements, while the second zone has only one additional node called QuadElements.
These nodes store the element connectivity that tells us which vertices make up a cell. For example, the TriElement array that we will later read may look something like this:
std::vector<std::vector<unsigned>> TriElements{{0, 1, 3}, {1, 4, 3}, {1, 2, 4}};
In this example, we are saying that there are 3 triangles in total in this zone, where the first triangle is composed of vertices 0, 1, and 3. The second triangle is composed of the vertices 1, 4, and 3, and so on. If we now read the coordinates, which may look something like this
std::vector<double> CoordinateX{0, 1, 2, 0, 1, 2, 0, 1, 2};
std::vector<double> CoordinateY{0, 0, 0, 1, 1, 1, 2, 2, 2};
then we can reconstruct the location of the first triangle in space as {0, 0}
, {1, 0}
, and {0, 1}
. From there, we can calculate things like the area/volume of the cell (required during the finite volume integration), the normal angle between cells, and so on.
In the CGNS file, the connectivity information for each cell type is stored separately per zone. Since we have both triangle and quad elements for the first zone (dom-1, see image above), we have both TriElements and QuadElements nodes. For the second zone, though, we only have one entry for QuadElements as these are the only elements that we are storing here.
This was a fairly quick and dirty overview of how unstructured grids are stored and what type of information they need. We’ll return to this topic in the future and see how to construct a sensible data structure for unstructured grids. In the meantime, if you can’t wait for this, have a look at the book of Rainald Löhner – Applied Computational Fluid Dynamics Techniques: An Introduction Based on Finite Element Methods, 2nd Edition. Chapter 2 is what you are looking for. It is good old Fortran with horrible naming conventions but probably the only (?) (and thus best) resource to learn about handling of unstructured grids.
Apart from the element connectivity, we see that the rest of the CGNS file is actually pretty much the same compared to our structured counterpart. Some of the information we receive back will be slightly different in structure, but otherwise, we see a fair bit of similarities. Boundary conditions are still written under intermediate family nodes and information from the family nodes will need to be supplemented with that of the boundary nodes under the ZoneBC node to get the full picture of the boundary conditions.
Analysing the unstructured2DNoFamily.cgns file
This should now feel very familiar. Again the only difference to the structured grid is the necessity to store element connectivity data. Regardless of how boundary conditions are written, the connectivity data always sits next to the grid. This makes sense, since if the mesh is changing, the element connectivity information needs to change with it, thus there is no point in storing it outside of a grid node. With family boundary conditions disabled, all information is now written next to the grid node again. Otherwise, the unstructured2DNoFamily.cgns
and unstructured2D.cgns
file are pretty much the same in terms of their content.
Summary
In my previous article, we looked at the basic structure of a CGNS file from a high-level point of view. In this article, we have supplemented these observations with inspections of actual CGNS files. We looked at structured and unstructured grids and saw that the main difference is the need to store element connectivity data for unstructured grids. We will see later that the data we receive back from the nodes will be slightly different stored for structured and unstructured grids, but we are getting ahead of ourselves.
We saw the difference in file structure for CGNS files where we utilise family nodes to store boundary conditions compared to storing that information directly next to the grid. We discussed what the advantages were but also pointed out that, while in theory, this may be a good idea, in practice this just leads to a messy implementation, which we will see in our next article.
I have said it before but it is worth reiterating; the CGNS file is not perfect but it gets a lot of things right. There are some areas where it still needs to improve, but what we have is already pretty solid. It is worth the pain to understand how to use it as it will allow us later to interact with mesh generators and post-processors alike. In the next article, we’ll start to write some CGNS mesh reading code.
Tom-Robin Teschner is a senior lecturer in computational fluid dynamics and course director for the MSc in computational fluid dynamics and the MSc in aerospace computational engineering at Cranfield University.