Volume Mesh

Introduction

In this part, we will extrude the previously generated surface mesh into a volume mesh using pyHyp. As this is an overset mesh, it consists of multiple sub-meshes (near_wing, near_tip and far). After extruding all of them, we will combine them into one single grid, that ADflow can read.

As we said in the previous tutorial, we want differently sized meshes. To accomplish this, we generated the finest and will use this script to coarsen it multiple times. We will implement a basic command line parsing to tell the script wich grid to generate. For example, a L1 mesh would be generated like this:

python run_pyhyp.py --level L1

Files

Navigate to the directory overset/mesh in your tutorial folder and create an empty file called run_pyhyp.py. You will also need to copy the surface meshes from the tutorial folder if you did not generate it in the previous part:

cp ../../../tutorial/overset/mesh/near_tip.cgns .
cp ../../../tutorial/overset/mesh/near_wing.cgns .

pyHyp Script

Setup

First we have to import some stuff:

from collections import OrderedDict
from mpi4py import MPI
from pyhyp import pyHypMulti
from pyhyp.utils import simpleOCart
from cgnsutilities.cgnsutilities import readGrid, combineGrids
import argparse

Then we need to setup up some libraries:

rank = MPI.COMM_WORLD.rank

parser = argparse.ArgumentParser()
parser.add_argument("--input_dir", default=".")
parser.add_argument("--output_dir", default=".")
parser.add_argument("--level", default="L1")
args = parser.parse_args()

The first line makes the processor number, on which this script is running, availabe. (Only used if it is parallelized via MPI).

After that, we setup up the command line parsing with three arguments (--input_dir, --output_dir and --level)

Level Dependent Options

Next, we define some basic mesh parameters that depend on the level used:

# Near-Field
# reference first off wall spacing for L2 level meshes
s0 = 1.4e-7

# number of Levels in the near-Field
nNearfield = {"L3": 31, "L2": 61, "L1": 121}[args.level]


# Farfield
# background mesh spacing
dhStar = {"L3": 0.178, "L2": 0.09, "L1": 0.045}[args.level]

nFarfield = {"L3": 13, "L2": 25, "L1": 49}[args.level]


# General
# factor for spacings
fact = {"L3": 1.0, "L2": 2.0, "L1": 4.0}[args.level]

# levels of coarsening for the surface meshes
coarsen = {"L1": 1, "L2": 2, "L3": 3}[args.level]

As you can see, for most options, we generate a dict with the three levels we want to create. Right after the dict, an indexing happens ([args.level]). This way, we dont actually save the dict in the variables. We actually load the value, that corresponds to the current level, to that variable.

Common pyHyp options

We extrude multiple nearfield meshes with pyHyp. As there are a lot of options used for all meshes, we first define some common options:

commonOptions = {
    # ---------------------------
    #        Input Parameters
    # ---------------------------
    "unattachedEdgesAreSymmetry": False,
    "outerFaceBC": "overset",
    "autoConnect": True,
    "fileType": "CGNS",
    # ---------------------------
    #        Grid Parameters
    # ---------------------------
    "N": nNearfield,
    "s0": s0 / fact,
    "marchDist": 2.5 * 0.8,
    "coarsen": coarsen,
    "nConstantEnd": 2,
    # ---------------------------
    #   Pseudo Grid Parameters
    # ---------------------------
    "ps0": -1.0,
    "pGridRatio": -1.0,
    "cMax": 1.0,
    # ---------------------------
    #   Smoothing parameters
    # ---------------------------
    "epsE": 1.0,
    "epsI": 2.0,
    "theta": 1.0,
    "volCoef": 0.5,
    "volBlend": 0.00001,
    "volSmoothIter": int(100 * fact),
}

This options are quite basic and you should recognize most of them. Some overset specific ones are pointed out:

outerFaceBC

This has to be set to overset. This way ADflow knows it has to interpolate the outer faces and doesn’t apply any boundary conditions.

marchDist

Usually, the farfield should be located about 100 root chords away from the wing. Since we are only generating the nearfield, we use 2.5 root chords.

Individual pyHyp options

Lets define some individual options:

# wing options
wing_dict = {
    "inputFile": "%s/near_wing.cgns" % (args.input_dir),
    "outputFile": "%s/near_wing_vol_%s.cgns" % (args.output_dir, args.level),
    "BC": {1: {"iLow": "ySymm"}, 2: {"iLow": "ySymm"}, 3: {"iLow": "ySymm"}},
    "families": "near_wing",
}

# tip options
tip_dict = {
    "inputFile": "%s/near_tip.cgns" % (args.input_dir),
    "outputFile": "%s/near_tip_vol_%s.cgns" % (args.output_dir, args.level),
    "families": "near_tip",
    "splay": 0.0,
}

The options in the wing_dict dictionary are applied to the near_wing mesh. The tip_dict is used for the near_tip mesh. This individual options overwrite the common options if the same key exists in both of them.

inputFile

Since we have different surface meshes, we have to supply the inputfile name individually

outputFile

We also want different output names. This way we can inspect the generated mesh separately

BC

Here we apply the boundary conditions (BC). The integer defines the Domain (starting at 1). The dict key defines which side of the domain the BC applies to. The dict value defines the type of the BC.

As it has been mentioned in the previous tutorial, there is not a reliable way to get this integer, which defines the domain, out of Pointwise. So it is recommended to rotate all domains in such a way, that the BC can be applied on the same side of all domains. Then they are deleted one by one until no more error messages pop up in pyHyp.

families

Here we give a unique name to a surface. This lets ADflow calculate the forces seperately and would allow you, for example, to get the lift and drag forces for your wing and tail individually

Extrude the nearfield

Now we extrude the nearfield:

# figure out what grids we will generate again
options = OrderedDict()
options["wing"] = wing_dict
options["tip"] = tip_dict

# Run pyHypMulti
hyp = pyHypMulti(options=options, commonOptions=commonOptions)
MPI.COMM_WORLD.barrier()

We start the extrusion by calling pyHypMulti. As arguments we give the previously defined common and individual options. After the extrusion, we wait for all procs to finish before we continue.

Combine the nearfield

The farfield consist of a cartesian part in the middle and a simple Ogrid around it. This cartesian part will enclose all the nearfields. Because of that, we have to combine all the nearfields first:

# read the grids
wing = "%s/near_wing_vol_%s.cgns" % (args.output_dir, args.level)
tip = "%s/near_tip_vol_%s.cgns" % (args.output_dir, args.level)

wingGrid = readGrid(wing)
tipGrid = readGrid(tip)

gridList = [wingGrid, tipGrid]

# combine grids
combinedGrid = combineGrids(gridList)

# move to y=0
combinedGrid.symmZero("y")

# Write nearfield mesh
nearfield = "%s/near_%s.cgns" % (args.output_dir, args.level)
if rank == 0:
    combinedGrid.writeToCGNS(nearfield)

MPI.COMM_WORLD.barrier()

First, the script reads the files, then it combines them. In the end everything gets moved to y=0.

Generate the farfield

Now we can generate the farfield:

farfield = "%s/far_%s.cgns" % (args.output_dir, args.level)
simpleOCart(nearfield, dhStar, 40.0, nFarfield, "y", 1, farfield)

The arguments are explained in the pyHyp docs for simpleOCart().

Combine everything

Here we combine all the meshes into one. We do this only on the root processor if we run it in parallel.

# we can do the stuff in one proc after this point
if rank == 0:
    # read the grids
    farfieldGrid = readGrid(farfield)
    gridList.append(farfieldGrid)
    finalGrid = combineGrids(gridList)

    # write the final file
    finalGrid.writeToCGNS("%s/ONERA_M6_%s.cgns" % (args.output_dir, args.level))

Run the Script

To run the script, simply type this in your console:

python run_pyhyp.py --level L1

If you have MPI installed and enough processors available, you can also run it in parallel:

mpirun -np 4 python run_pyhyp.py --level L1

Since we want 3 meshes of different size, you will have to run this script 3 times with the appropriate --level argument.

Check the Final Mesh

Finally, we can use the ihc_check.py script to check the result of the implicit hole cutting process in ADflow:

from baseclasses import AeroProblem
from adflow import ADFLOW
import argparse

# ======================================================================
#         Init stuff
# ======================================================================
# rst Init (beg)
parser = argparse.ArgumentParser()
parser.add_argument("--input_dir", default=".")
parser.add_argument("--output_dir", default=".")
parser.add_argument("--level", default="L1")
args = parser.parse_args()
# rst Init (end)

# ======================================================================
#         Input Information
# ======================================================================

# File name of the mesh
gridFile = "%s/ONERA_M6_%s.cgns" % (args.output_dir, args.level)

# Common aerodynamic problem description and design variables
ap = AeroProblem(name="ihc_check", mach=0.3, altitude=1000, areaRef=0.24 * 0.64 * 2, chordRef=0.24)

# dictionary with name of the zone as a key and a factor to multiply it with.
oversetpriority = {}

aeroOptions = {
    # Common Parameters
    "gridFile": gridFile,
    "outputDirectory": "./",
    "MGCycle": "sg",
    "volumeVariables": ["blank"],
    "surfaceVariables": ["blank"],
    # Physics Parameters
    "equationType": "RANS",
    # Debugging parameters
    "debugZipper": False,
    "useZipperMesh": False,
    # number of times to run IHC cycle
    "nRefine": 10,
    # number of flooding iterations per IHC cycle.
    # the default value of -1 just lets the algorithm run until flooded cells stop changing
    "nFloodIter": -1,
    "nearWallDist": 0.1,
    "oversetPriority": oversetpriority,
}

# Create solver
CFDSolver = ADFLOW(options=aeroOptions, debug=False)

# Uncoment this if just want to check flooding
CFDSolver.setAeroProblem(ap)

name = ".".join(gridFile.split(".")[0:-1])
CFDSolver.writeVolumeSolutionFile(name + "_IHC.cgns", writeGrid=True)