MACH-Aero Documentation

unknown

Sep 21, 2021

OVERVIEW

1 Overview of MACH-Aero 3

2 Cite Us 5

3 Using Docker 7

4 Installing MACH-Aero From Scratch9

5 Third Party Packages 13

6 Introduction 23

7 Aerodynamic Analysis 25

8 Aerodynamic Optimization 61

9 Airfoil Optimization 87

10 Overset Mesh 105

11 Frequently Asked Questions 165

Bibliography 167

i ii MACH-Aero Documentation

MACH-Aero is a framework for performing gradient-based aerodynamic shape optimization. It consists of the follow- ing core modules: • baseClasses defines Python classes used by the other packages • pySpline is a B-spline implementation used by the other packages • pyGeo is a module for geometry manipulation and constraint formulation • IDWarp performs mesh warping using an inverse distance method • ADflow is a 2nd-order finite volume CFD solver with an efficient adjoint implementation • pyOptSparse is an optimization framework which provides a unified interface to several popular optimizers And the following optional modules: • pyHyp is a hyperbolic mesh generation tool used as a preprocessing step • multiPoint facilitates distributed multipoint optimization and handles the parallel communication using MPI • cgnsUtilities is a command-line tool that allows carrying out several simple mesh manipulation operations on CGNS grids • DAFoam provides efficient adjoint implementations for OpenFOAM to be used for CFD instead ofADflow More detail for the framework can be found in Overview of MACH-Aero. If you use any of our codes, please cite us.

OVERVIEW 1 MACH-Aero Documentation

2 OVERVIEW CHAPTER ONE

OVERVIEW OF MACH-AERO

This page provides an overview of the aerodynamic shape optimization capability within MACH (framework for MDO of aircraft configurations with high fidelity). MACH-Aero consists of six major modules: • Pre-processing (pyHyp, ANSYS ICEM-CFD) • Geometry parameterization (pyGeo) • Volume mesh deformation (IDWarp) • Flow simulation (ADflow, DAFoam) • Adjoint computation (ADflow, DAFoam) • Optimization (pyOptSparse)

Generally, MACH-Aero starts with a baseline design and uses the gradient to find the most promising direction inthe design space for improvement. This process is repeated until the optimality and feasibility conditions are satisfied. More specifically, the process is as follows, using the above figure as a reference. Here we use the extendeddesign structure matrix (XDSM) representation developed by Lambe and Martins (2012). The diagonal nodes represent the modules and the off-diagonal nodes represent the data. The black lines represent the process flow for the adjoint solver, whereas the thick gray lines represent the data flow. The number in each node represents the execution order: • First, we generate a volume mesh for the baseline geometry (pre-processing in process 1). Several mesh gen- eration tools are available including pyHyp and ICEM. Refer to Surface Meshing, Volume Meshing, and Mesh Manipulation for more details. The generated mesh will be used later in process 4. In the pre-processing step, we

3 MACH-Aero Documentation

also generate free-form deformation (FFD) points (process 3) that will be used later to morph the design surface. Refer to Geometric Parameterization. • Then, we give a set of baseline design variables to the optimizer (process 2). We usually use SNOPT as the optimizer, which uses the SQP algorithm. We use pyOptSparse to facilitate the optimization problem setup. The optimizer will update the design variables and give them to the geometry parameterization module (pyGeo; process 3). pyGeo receives the updated design variables and the FFD points generated in the pre-processing step, performs the deformation for the design surface, and outputs the deformed design surface to the mesh deformation module (IDWarp) in process 4. pyGeo also computes the values of geometric constraints and their derivatives with respect to the design variables (process 7). • Next, IDWarp deforms the volume mesh based on the updated design surface and outputs the updated volume mesh to the flow simulation module in process 5. • The flow simulation module receives the updated volume mesh and uses high-fidelity CFD tools(i.e., ADflow or DAFoam) to compute the state variables (process 6) or physical fields (pressure, density, velocity, etc.). The flow simulation module also computes the objective and constraint functions (e.g., drag and lift; seeprocess7) and outputs the state variables to the adjoint computation module. • Then, the adjoint computation module (process 6) computes the total derivatives of the objective and constraint functions with respect to the design variables (process 7) and gives them back to the optimizer in process 7. The benefit of using the adjoint method to compute derivatives is that its computational cost is independent ofthe number of design variables, which makes it attractive for handling large-scale, complex design problems such as aircraft design. There are two available adjoint solvers: ADflow, DAFoam. • Finally, the optimizer receives the values and derivatives of the objective and constraint functions in process 7, performs the SQP computation, and outputs a set of updated design variables to pyGeo. The above process is repeated until the optimization converges. Refer to the MACH-Aero tutorials for tutorials.

4 Chapter 1. Overview of MACH-Aero CHAPTER TWO

CITE US

See the following papers for technical background of the above modules. If you use these modules for publications, please cite the corresponding papers. • pyOptSparse [1] • pyGeo [2] • ADflow [3,4,5] • DAFoam [6]

5 MACH-Aero Documentation

6 Chapter 2. Cite Us CHAPTER THREE

USING DOCKER

The MACH framework is packaged within Docker images that can be used to run the software without installing it natively on your machine. If you are using MACH for the first time, we encourage you to try Docker to avoid installation issues or other inconveniences caused by natively installing the tools. This guide assumes you are have Docker installed and running on your machine. If you need to install Docker, you can follow the Docker guide for Installing the Docker Engine. The commands used in this guide may differ depending on your operating system, so refer to the Docker documentation for more details for specific use cases.

3.1 Pull MDO Lab Docker Image

Pull one of the MDO Lab Docker images. The available images are listed in the table below:

Tag Operating System c7-intel-impi-latest CentOS 7 u20-gcc-ompi-latest Ubuntu 20.04 u20-gcc-ompi-stable Ubuntu 20.04 tacc-u18-gcc-impi-stable Ubuntu 18.04

To pull an image, use the docker pull command: $ docker pull mdolab/public: Check that the Docker image is pulled successfully by running: $ docker image ls You should see the image you just pulled.

3.2 Initialize Docker Container

Navigate to the directory containing the case you would like to run and initialize the Docker image you downloaded into a container, running interactively: $ docker run -it --name --mount "type=bind,src=,target=" /bin/bash Replace with the name you would like to give the container, set to the absolute path to the current directory, and set to /home/mdolabuser/mount/. Then provide the image tag as , matching the one downloaded previously: mdolab/public:. When you run this command, you will enter the container and have access to the MACH framework in your specified case directory.

7 MACH-Aero Documentation

Note: If you are running the MACH tutorials included in this guide using Docker, set the parameter to the path to the directory where you have cloned the tutorial files.

3.3 Exiting and Restarting the Container

At any point you can exit the container with the command: $ exit You can restart the container by running start: $ docker start Run exec to enter the container: $ docker exec -it /bin/bash

8 Chapter 3. Using Docker CHAPTER FOUR

INSTALLING MACH-AERO FROM SCRATCH

This tutorial is intended to be a step-by-step guide on how to set up the software needed to run MACH-Aero. The focus here is on installing common dependencies shared across the various packages. A general description for installing packages within MACH-Aero is also provided, but please refer to the documentation site for each package for specific instructions. This tutorial assumes that you have a working Linux distribution such as Ubuntu 18.04. The following list what steps are needed. The instructions are divided into three parts • Third party packages • MDO Lab packages • Standard MDO Lab Build Procedure Since MDO Lab packages depend heavily on third party tools and packages, it is generally good to start by compiling and testing them. Finally, an example .bashrc file is shown. • Example .bashrc

4.1 Third party packages

To install, follow the instructions on this page.

4.2 MDO Lab packages

To install the MDO Lab packages clone each repository from GitHub and follow the installation instructions found in the documentation of each package. Below, we give an overview of the general process, which consists of two parts. The building step is required for /-based codes, and not needed if the package is purely written in Python. After this optional step, all packages must be installed as a Python package. The packages needed are: 1. baseClasses 2. pySpline 3. pyGeo 4. IDWarp 5. ADflow 6. pyOptSparse Optional packages are:

9 MACH-Aero Documentation

1. pyHyp 2. multipoint 3. cgnsUtilities 4. DAFoam

4.3 Standard MDO Lab Build Procedure

The following general instructions apply to all the packages and repos maintained by the MDO Lab. Note that the Compilation step is not required if the package is entirely written in Python.

4.3.1 Compilation

To start, find a configuration file close to your current setupin config/defaults

and copy it to config/config.mk. For example $ cp config/defaults/config.LINUX_GFORTRAN.mk config/config.mk If you are a beginner user installing the packages on a Linux desktop, you should use the config.LINUX_GFORTRAN. mk versions of the configuration files. The config.LINUX_INTEL.mk versions are usually used on HPC systems, in conjunction with Intel compilers. Our codes can be successfully compiled on Linux with either ifort or gfortran.

Note: For Intel builds, the config.mk files are potentially out of date. With new intel compilers, the actual mpi- wrapped compilers changed names. Check out the compilers, and modify the FF90 and CC options in config.mk files as needed.

Once you have copied the config file, compile the module by running $ make in the package’s root directory. If everything was successful, the following lines will be printed to the screen (near the end): Testing if module can be imported... Module was successfully imported.

If you don’t see this, it will be necessary to configure the build manually. To configure manually, open config/ config.mk and modify options as necessary. Remember to type make clean to remove outdated build files, before building again.

10 Chapter 4. Installing MACH-Aero From Scratch MACH-Aero Documentation

4.3.2 Installation

To install the Python package, type $ pip install . If you are not using a virtual environment, you may need the --user flag to perform a user install. If you plan to modify the source code, we recommend using the -e option, e.g. pip install -e . so that you do not need to install each time the code is modified.

4.4 Example .bashrc

After installing the above software you should have something similar to the following somewhere in your ~/.bashrc file # -- PETSc export PETSC_DIR=$HOME/packages/petsc- export PETSC_ARCH=real-debug

# -- OpenMPI Installation export MPI_INSTALL_DIR=$HOME/packages/openmpi-/opt-gfortran export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$MPI_INSTALL_DIR/lib export PATH=$MPI_INSTALL_DIR/bin:$PATH

# -- CGNS export CGNS_HOME=$HOME/packages/CGNS-/opt-gfortran export PATH=$PATH:$CGNS_HOME/bin export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$CGNS_HOME/lib

4.4. Example .bashrc 11 MACH-Aero Documentation

12 Chapter 4. Installing MACH-Aero From Scratch CHAPTER FIVE

THIRD PARTY PACKAGES

Note: Before trying to compile everything yourself, including all dependencies, consider using the mdolab/public Docker image available on Docker Hub.

5.1 Supported dependency versions

This section lists out the dependency versions that have been verified to work with the latest MDO Lab tools available on GitHub.

Important: Although the code may work with other dependency versions (for example NumPy and SciPy require- ments are not strict), we only test code against the dependency versions listed below. Therefore, if you choose to use a different dependency version, then you are essentially on your own. If you are doing a clean install, it’s probably best to use the versions listed under the latest column. On the other hand, cluster installs may benefit from the stable versions.

Versions stable latest OpenMPI 3.1.* 4.0.* mpi4py 3.0.3 3.0.3 PETSc 3.12.* 3.15.* petsc4py 3.12.* 3.15.* CGNS 4.1.2 4.2.0 Python 3.7.* 3.9.* NumPy 1.17.* 1.19.* SciPy 1.2.* 1.5.*

The supported operating systems are Ubuntu 18.04 and 20.04, together with GNU compiler versions 7 to 9. For the rest of the instructions, we use angled brackets such as as placeholders, where you should enter values specific to your installation such as package versions.

13 MACH-Aero Documentation

5.2 Common Prerequisites

If they’re not available already, common prerequisites can be installed via apt under Ubuntu: $ sudo apt-get install python3-dev gfortran valgrind cmake libblas-dev liblapack-dev build-essential swig These packages are required by many of the packages installed later. On a cluster, check the output of module avail to see what has already been installed. They can also be installed locally, but they are common enough that they are typically pre-installed.

5.3 C and Fortran Based Packages

These packages have minimal dependencies and should be installed first, in the order listed here. These source code for these packages are often downloaded and installed to $HOME/packages/, which will be adopted as convention for the instructions here. The environment is adapted for each package by modifying $HOME/.bashrc or equivalent.

5.3.1 OpenMPI

Important: OpenMPI depends only on a C/Fortran compiler, such as gcc/gfortran or icc/ifort. On a cluster, the system administrator will have already compiled various versions of MPI on the system already. Do not build/install OpenMPI in this case, and simply load the correct MPI module.

Download the desired version from the OpenMPI website and place the tarball in your packages directory, $HOME/ packages: $ wget Then, unpack the source code: $ tar -xvaf openmpi-.tar.gz Add the following lines to $HOME/.bashrc: # -- OpenMPI Installation export MPI_INSTALL_DIR=$HOME/packages/openmpi-/opt-gfortran export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$MPI_INSTALL_DIR/lib export PATH=$MPI_INSTALL_DIR/bin:$PATH

After saving the file, source $HOME/.bashrc: $ source ~/.bashrc Go to the OpenMPI directory: $ cd $HOME/packages/openmpi- ONLY IF using an Intel compiler, run: $ export CC=icc CXX=icpc F77=ifort FC=ifort Finally, configure and build the package: $ ./configure --prefix=$MPI_INSTALL_DIR

14 Chapter 5. Third Party Packages MACH-Aero Documentation

$ make all install To verify that paths are as expected run $ which mpicc and $ echo $MPI_INSTALL_DIR/bin/mpicc The above should print out the same path for both.

5.3.2 PETSc

Important: PETSc depends on OpenMPI, a C/Fortran compiler, and it requires cmake to build.

PETSc, the Portable Extensible Toolkit for Scientific Computation is a comprehensive library for helping solve large scale PDE problems. Download the desired version from the PETSc website and place the tarball in your packages directory, $HOME/ packages: $ wget Unpack the source directory in your packages directory: $ tar -xvaf petsc-.tar.gz Next, configure your environment for PETSc by adding the following lines toyour $HOME/.bashrc: # -- PETSc Installation export PETSC_ARCH=real-debug export PETSC_DIR=$HOME/packages/petsc-/

After saving the file, source $HOME/.bashrc: $ source ~/.bashrc Go to the PETSc directory: $ cd $HOME/packages/petsc- The PETSC_ARCH variable is any user-specified string. It should be set to something representative of the actual archi- tecture. The next step is to configure PETSc. There are a huge number and variety of options. To get a list of all available options run: $ ./configure --help To facilitate installation of PETSc for use with MDO Lab tools, here are some common preset configurations. • Standard debug build (PETSC_ARCH=real-debug): $ ./configure --PETSC_ARCH=$PETSC_ARCH --with-scalar-type=real --with-debugging=1 --with-mpi-dir=$MPI_INSTALL_DIR --download-metis=yes --download-parmetis=yes --download-superlu_dist=yes --with-shared-libraries=yes --with-fortran-bindings=1 --with-cxx-dialect=C++11 • Debug complex build (PETSC_ARCH=complex-debug): $ ./configure --PETSC_ARCH=$PETSC_ARCH --with-scalar-type=complex --with-debugging=1 --with-mpi-dir=$MPI_INSTALL_DIR --download-metis=yes --download-parmetis=yes --download-superlu_dist=yes --with-shared-libraries=yes --with-fortran-bindings=1 --with-cxx-dialect=C++11 • Optimized real build on a cluster with existing MPI (PETSC_ARCH=real-opt): $ ./configure --with-shared-libraries --download-superlu_dist --download-parmetis=yes --download-metis=yes --with-fortran-bindings=1 --with-debugging=0 --with-scalar-type=real --PETSC_ARCH=$PETSC_ARCH --with-cxx-dialect=C++11

5.3. C and Fortran Based Packages 15 MACH-Aero Documentation

Note: If you are compiling PETSc on Great Lakes, check the cluster-specific setup page for the correct configurations.

Here is a short overview of some of the options used above. • Debugging: To compile without debugging use the switch: --with-debugging=0

If you are doing any code development which uses PETSc, it is highly recommended to use debugging. However, if you are doing production runs on an HPC, then you should turn this off to improve code performance. To further specify compiler optimization flags, use: --COPTFLAGS=-O3 --CXXOPTFLAGS=-O3 --FOPTFLAGS=-O3

• METIS and ParMETIS: partitioning packages If you do not have METIS and ParMETIS installed, include the following line: --download-metis=yes --download-parmetis=yes

If they are already installed, you can simply supply the installation directories: --with-metis --with-metis-dir= --with-parmetis --with-parmetis-

˓→dir=

• Complex build: partitioning packages A complex build is configured via: --with-scalar-type=complex

• Other: Various options are also required: --with-shared-libraries --download-superlu_dist=yes --with-fortran-

˓→bindings=1 --with-cxx-dialect=C++11

After the configuration step, PETSc must be built. This is accomplished with the command provided at the endof configure script. It will look something like below (the PETSc version should be consistent with the versionbeing installed.): $ make PETSC_DIR=$HOME/packages/petsc- PETSC_ARCH=$PETSC_ARCH all After build, follow the command provided at the end of the print out to test the functionality. It will look something like below: $ make PETSC_DIR=$HOME/packages/petsc- PETSC_ARCH=$PETSC_ARCH test

Note: If your PETSc is not able to find MPI, try: 1. Add --with-mpi-dir=$MPI_INSTALL_DIR when you configure PETSc 2. Check your LD_LIBRARY_PATH order. If you have PyTecplot, try moving the entry for PyTecplot in the LD_LIBRARY_PATH to the end, by modifying your .bashrc.

16 Chapter 5. Third Party Packages MACH-Aero Documentation

5.3.3 CGNS Library

Important: CGNS depends on a C/Fortran compiler. It can be built using either CMake or GNU make. The instruc- tions here use CMake.

CGNS is a general file format for storing CFD data, and is used by ADflow, IDWarp, pyHyp, and cgnsUtilities. The CGNS Library provides Fortran bindings to read/write files in that format.

Note: CGNS now supports two output types: HDF5 and the Advanced Data Format (ADF) format. While HDF5 is the officially supported format, its compatibility with other tools is sparse. Therefore, for using MDO Lab codes,the ADF format is recommended. The rest of the instructions use ADF and not HDF5.

Download the desired version from the CGNS website and place the tarball in your packages directory, $HOME/ packages: $ wget Unpack the source directory in your packages directory: $ tar -xvaf v.tar.gz Next, configure your environment for CGNS by adding the following lines toyour $HOME/.bashrc: # -- CGNS export CGNS_HOME=$HOME/packages/CGNS-/opt-gfortran export PATH=$PATH:$CGNS_HOME/bin export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$CGNS_HOME/lib

After saving the file, source $HOME/.bashrc: $ source ~/.bashrc Go to the CGNS directory: $ cd $HOME/packages/CGNS- To configure the package, run: $ cmake -D CGNS_ENABLE_FORTRAN=ON -D CMAKE_INSTALL_PREFIX=$CGNS_HOME -D CGNS_ENABLE_64BIT=OFF -D CGNS_BUILD_CGNSTOOLS=OFF . If your compilers are not located at /usr/bin/gcc, either because you are on an HPC system or using Intel compilers, you must adjust the configure command. This is done by passing additional variables to cmake: $ cmake -D CMAKE_C_COMPILER=/path/to/ccompiler -D CMAKE_Fortran_COMPILER=/path/to/fcompiler . where CMAKE_C_COMPILER sets the path to the C compiler, and CMAKE_Fortran_COMPILER sets the path to the For- tran compiler. If your compilers are on the $PATH (likely if you are using the module system on a cluster), you can use CMAKE_C_COMPILER=$(which icc) and CMAKE_Fortran_COMPILER=$(which ifort) for Intel compilers, or correspondingly CMAKE_C_COMPILER=$(which gcc) and CMAKE_Fortran_COMPILER=$(which gfortran) for GNU compilers. Finally, build and install: $ make install

5.3. C and Fortran Based Packages 17 MACH-Aero Documentation

Installing CGNS Tools (Optional)

The CGNS Library comes with a set of tools to view and edit CGNS files manually. To install these tools, use the flag -D CGNS_BUILD_CGNSTOOLS=ON during the configure step. Note that these tools should be installed on alocal computer and not on a cluster. To enable this option you may need to install the following packages: $ sudo apt-get install libxmu-dev libxi-dev CGNS library sometimes complains about missing includes and libraries. Most of the time this is either Tk/TCL or OpenGL. This can be solved by installing the following packages. Note that the version of these libraries might be different on your machine: $ sudo apt-get install freeglut3 $ sudo apt-get install tk8.6-dev If needed, install the following package as well: $ sudo apt-get install freeglut3-dev If you compiled with -D CGNS_BUILD_CGNSTOOLS=ON, you either need to add the binary path to your PATH envi- ronmental variable or you can install the binaries system wide. By specifying the installation prefix as shown in the example configure commands above, the binary path is in your PATH environmental variables; without specifying the prefix, the default is a system path, which requires sudo.

5.4 Python Packages

In this guide, python packages are installed using pip. Other methods, such as from source or using conda, will also work. Local installations (with --user) are also recommended but not required. When installing the same package multiple times with different dependencies, for example petsc4py with different petsc builds, the pip cache can become incorrect. Therefore, we recommend the --no-cache flag when installing python packages with pip.

5.4.1 NumPy

Important: Version 1.13.3 and 1.15.4 of numpy or f2py do NOT work. See Supported dependency versions for numpy versions that are tested.

NumPy is required for all MDO Lab packages. It is installed with: $ pip install numpy== --user --no-cache On a conda-based system, it is recommended to use conda to install numpy and scipy: $ conda install numpy=

18 Chapter 5. Third Party Packages MACH-Aero Documentation

5.4.2 SciPy

SciPy is required for several packages including pyOptSparse, pyGeo and certain functionality in pySpline. It is installed with: $ pip install scipy== --user --no-cache On a conda-based system, it is recommended to use conda to install numpy and scipy: $ conda install scipy=

Note: On a cluster, most likely numpy and scipy will already be installed. Unless the version is invalid, use the system-provided installation.

5.4.3 mpi4py

Important: mpi4py depends on OpenMPI. Since mpi4py generally lags in version, it is recommended to use a version that matches as closely as possible to the installed OpenMPI version.

mpi4py is the Python wrapper for MPI. This is required for all parallel MDO Lab codes.

Simple install with pip

It is installed with: $ pip install mpi4py== --user --no-cache

Note: Some function usages have changed in newer versions of mpi4py. Check the release to see the modifications that might be requried in the code.

Advanced install

Alternatively, installing from source is also possible. First, download the source code from releases, and extract it into the packages directory. Then, either run pip install . or python setup.py install in the root directory. Installing from source has the advantage of having access to the tests, which can be used to verify both the OpenMPI and mpi4py installations. To run the tests, go to the test directory, and type: $ python runtests.py

5.4. Python Packages 19 MACH-Aero Documentation

5.4.4 petsc4py

Important: The MAJOR.MINOR version of petsc4py MUST match the MAJOR.MINOR version of petsc, for ex- ample PETSc 3.12.X will only work with petsc4py 3.12.Y. In practice, this means you must request a specific version of petsc4py. petsc4py depends on PETSc and its dependencies. petsc4py is the Python wrapper for PETSc. If you want to make developments or multiple PETSc architectures are needed, you should install petsc4py manually, which described in Advanced install. Manually installing provide you useful run tests. If you know you will only need real PETSc architecture, you can use pip.

Simple install with pip

It is installed with: $ pip install petsc4py== --user --no-cache

Build from source (Required for multiple PETSc architectures)

Warning: You must compile a unique petsc4py install for each PETSc architecture.

If using PETSc < 3.14, Download the source code and extract the correct version matching your PETSc version: $ tar -xzf petsc4py-.tar.gz $ cd petsc4py- From 3.14 onwards, petsc4py is included in the PETSc source code, in which case you can skip the above step and simply go straight to the petsc4py source directory: $ cd $PETSC_DIR/src/binding/petsc4py Then install: $ pip install .

Warning: If there is an existing build directory it must be forcibly removed (rm -fr build) before doing another architecture install. To install with multiple architectures change the PETSC_ARCH variable to contain all the architecture you want to install petsc4py for: export PETSC_ARCH=:::...

Then install the package: $ pip install . Don’t forget to switch the PETSC_ARCH variable back to a single value after installing

Installing from source has the advantage of having access to the tests, which can be used to verify both the PETSc and petsc4py installations. To run the tests, go to the test directory, and type:

20 Chapter 5. Third Party Packages MACH-Aero Documentation

$ python runtests.py

5.5 Other Methods and Notes

The build examples described here are all installed locally (e.g. $HOME/...) rather than system-wide (e.g. /usr/ local/...). Local installations are generally preferred. Installing packages system-wide requires root access, which is an increased security risk when downloading packages from the internet. Also, it is typically easier to uninstall packages or otherwise revert changes made at a local level. Finally, local installations are required when running on a cluster environment. The build and installation paradigm demonstrated here puts source code, build files, and installed packages all in $HOME/packages. Another common convention is to use $HOME/src for source code and building, and $HOME/ opt for installed packages. This separation adds a level of complexity but is more extensible if multiple package versions/installations are going to be used. When configuring your environment, the examples shown here set environment variables, $PATH, and $LD_LIBRARY_PATH in .bashrc. If multiple versions and dependencies are being used simultaneously, for exam- ple on a cluster, the paradigm of environment modules is often used (e.g. module use petsc). A module file is simply a text file containing lines such as: append-path PATH $HOME/opt/petsc/3.7.7/OpenMPI-1.10.7/GCC-7.3.0/bin

MDO Lab tools can be used by configuring your environment with either .bashrc or environment modules, or some combination of the two.

5.5. Other Methods and Notes 21 MACH-Aero Documentation

22 Chapter 5. Third Party Packages CHAPTER SIX

INTRODUCTION

The MDO Lab at the University of Michigan has developed the MDO of aircraft configurations with high fidelity (MACH) framework. This tutorial was written to help new users become familiar with the tools and workflow of MACH for aerodynamic shape optimization and a few common practices of more experienced users. Most of the tools in the MACH framework are written with Python, although many of the tools incorporate Fortran code to handle operations that require speed. The user does not need to know Fortran to complete this tutorial, but if Python is not your strong suit, we recommend a refresher with one of the many online Python tutorials.

Warning: Please view this tutorial as a bare-minimum tutorial and not as a comprehensive tutorial. To gain proficiency and flexibility with the MACH tools, it is necessary to explore the dedicated documentation, source code, docstrings, and code comments of each tool.

This tutorial starts from scratch and leads the user through the steps necessary to conduct aerodynamic shape optimiza- tion of a B717 wing. The tutorial files are located on GitHub. The scripts referenced in the tutorial can be found in the tutorial directory, organized according to section. Although these scripts should be executable without any modi- fications, we highly recommend that you create a separate directory and type out the lines of code by yourself. As you do this, ask yourself, “Do I understand why the code is written this way?” This will result in a much deeper understanding of how to use the tools and eventually will help you develop code in a consistent manner. To make this easier for you, we provide a basic script that will create the directory structure in your desired location so that all you have to do is create the files themselves. To run this script, go to the MACH-Aero root folder and run the following: $ python make_tutorial_directory.py my_tutorial where my_tutorial is the name of the folder in which you will build your scripts. The directory structures for each section of the tutorial, including all files, are displayed at the beginning of each section. Throughout the tutorial, we will refer to the location of your developing tutorial as my_tutorial, so if you chose a different name make sure to adjust your commands accordingly. Before continuing with the tutorial, make sure that the MDOLab framework is already installed on your machine. If you set up your machine using an MDOLab iso, then the required packages should already be installed. If not, follow the instructions for installing the MDOLab framework from scratch. This tutorial requires the following software. Made in the MDO Lab • pyHyp • cgnsUtilities • baseclasses • pySpline • pyGeo

23 MACH-Aero Documentation

• IDWarp • ADflow • pyOptSparse • multipoint

Note: These links take you to the GitHub repositories. To see their documentation, follow the link in the README for each GitHub repository.

External Software • ICEM CFD (for surface mesh generation) • Tecplot (for flow visualization)

6.1 Documentation strategy

The tutorial resides on GitHub, but it is a living tutorial, which means that it is constantly updated with corrections and improvements. We invite you, especially as a new user, to take notes of the parts that you find confusing and bring them to the attention of an admin to the tutorial repository so that changes can be made. The rst files in the doc directory contain direct links to the python scripts in the tutorial directory to avoidcodedu- plication. This is done using the start-after and end-before options of Sphinx’s native literalinclude di- rective. We adopt the convention of using #rst

as the marker for the start and end of each literalinclude section, like so: #rst Simple addition (begin) a=2 b=3 c=a+b #rst Simple addition (end)

Please adopt this same convention for any future developments to the tutorial.

24 Chapter 6. Introduction CHAPTER SEVEN

AERODYNAMIC ANALYSIS

High-fidelity aerodynamic analysis in the MDOlab is done using ADflow. ADflow is a finite-volume CFD solver for cell-centered multiblock and overset meshes. ADflow solves the compressible Euler, laminar Navier–Stokes, and RANS equations with a second-order accurate spatial discretization. More information on ADflow can be found some- where (needs link). In order to analyze a geometry with ADflow, we need to take the following steps: • Obtain a CAD representation of the geometry In practice, we want an IGES file that contains the geometry description. This can be done with any commercial CAD package, but it can also be done by lofting airfoil sections using pyGeo. • Generate a valid multiblock or overset mesh ADflow uses the CGNS mesh format. In practice, we createa surface mesh using ICEM and then extrude a volume mesh using pyHyp. However, the volume mesh could be created in ICEM or any other meshing software. Component volume meshes can be combined using cgnsUtilities. • Analyze the flow with ADflow Since ADflow is a script-based software, it is important to understand the ele- ments of an ADflow runscript. Additionally, there are many settings that can be adjusted to make ADflow perform better for a given case.

7.1 Geometry Generation

7.1.1 Introduction

The geometry definition is a necessary precursor to generate a mesh, so it comes first in this tutorial. Thenative geometry file type for ICEM (our meshing software of choice) is a “.tin” file, but there are many ways togetit.You can use any CAD software to generate an “.igs” file and then convert the “.igs” file to a “.tin” file with ICEM orpyGeo. However, this tutorial will teach you how to generate a wing surface from airfoil data using pyGeo. pyGeo is set up to loft a surface between a set of airfoils distributed along the span of the wing. For more details on the options in pyGeo see the docs.

25 MACH-Aero Documentation

7.1.2 Files

Navigate to the directory aero/geometry in your tutorial folder. Copy the following file from the tutorial directory: $ cp ../../../tutorial/aero/geometry/rae2822.dat . Create the following empty runscript in the current directory: • generate_wing.py

7.1.3 Dissecting the pyGeo runscript

Open the file generate_wing.py in your favorite text editor. Then copy the code from each of the following sections into this file.

Import libraries

Numpy is used for scientific computing such as matrix/vector operations. We need to import the pyGeo library inorder to use its functions. The pyGeo library, along with all other MACH libraries, should be importable because its code is located in a directory which is exposed to the system environment variable PYTHON_PATH. import numpy as np from pygeo import pyGeo

Wing Definition

airfoil_list=["rae2822.dat"]*2 naf= len(airfoil_list) # number of airfoils

The wing is lofted from a set of airfoils distributed along the span of the wing. The airfoil data files can be downloaded from online or generated from a variety of software. The airfoil should be scaled to a unit chord with the leading edge centered at the origin. The number of sections needed for a geometry generation is dependent on the intention of the geometry. In general, any wing will require at least 2 sections to produce a valid geometry. Adding more sections allows more precise wing definition, which is generally necessary for matching previously defined wing geometries. Note that the number of sections must match the length of all the wing geometry data lists discussed below. Also note that the number of control points should vary with the number of sections. For cases with few sections, using a small number of control points yields the best results. If there are a large number of sections, the number of control points needs to be increased to prevent the size of the output files from becoming unreasonably large. For this tutorial, we use the RAE 2822 transonic airfoil as the cross-section of our wing. A visualization of the airfoil geometry is given on Figure 1. The coordinates of the airfoil are contained in the file rae2822.dat. The position, orientation, and scaling of the airfoil sections are stipulated in lists corresponding with the original airfoil data list. The leading edge of each airfoil is positioned in space based on the values in the x, y, and z lists. An offset in the x-y plane can be added to this position with the offset array before chord scaling is applied. In this case, we don’t want to apply any rotation (the rotation options shown below would be for rotation about the airfoil leading edge points and the units are degrees) or offset. # Airfoil leading edge positions x=[0.0, 7.5] y=[0.0, 0.0] (continues on next page)

26 Chapter 7. Aerodynamic Analysis MACH-Aero Documentation

Fig. 1: Figure 1: RAE 2822 Transonic Airfoil

(continued from previous page) z=[0.0, 14.0] offset= np.zeros((naf,2)) # x-y offset applied to airfoil position before scaling

# Airfoil rotations rot_x=[0.0, 0.0] rot_y=[0.0, 0.0] rot_z=[0.0, 0.0]

# Airfoil scaling chord=[5.0, 1.5] # chord lengths

Call pyGeo wing= pyGeo( "liftingSurface", xsections=airfoil_list, scale=chord, offset=offset, x=x, y=y, z=z, rotX=rot_x, rotY=rot_y, rotZ=rot_z, tip="rounded", bluntTe=True, squareTeTip=True, teHeight=0.25* 0.0254, ) (continues on next page)

7.1. Geometry Generation 27 MACH-Aero Documentation

(continued from previous page)

A detailed explanation of each argument is available in the pyGeo docs. The final four options stipulate a rounded wingtip and a blunt trailing edge with a square tip (i.e., a square face at the trailing edge instead of a rounded face) and a thickness of 0.25 inches.

Write output files

There are three options for writing the geometry surface definition to file. 1. Write a dat file to view wing in Tecplot. 2. Write an IGES file. This can be converted to a TIN file inICEM. 3. Write a TIN file directly from pyGeo. wing.writeTecplot("wing.dat") wing.writeIGES("wing.igs") wing.writeTin("wing.tin")

7.1.4 Run it yourself!

After copying rae2822.dat to your aero/geometry folder and making the generate_wing.py script, you can now run the python file with the command $ python generate_wing.py You can open wing.dat in Tecplot to view the wing surface.

7.2 Surface Meshing

7.2.1 Introduction

The objective of this section is to familiarize the user with the ICEM CFD software and to create a surface mesh. ICEM CFD is a meshing software with advanced CAD/geometry readers and repairs tools. It allows the user to produce volume or surface meshes. At times ICEM may test your patience, however, it offers a lot of functionality and is quite handy once you get to know its quirks. A full ICEM CFD tutorial can be found here. For more theoretical background of mesh generation, refer to “Thompson, et.al. Handbook of grid generation. CRC press, 1998.”

Warning: Make sure you save your work often when using ICEM. It is known to crash at the worst possible moments. We also recommend saving instances of a single project in different locations just in case you need togo back to a previous state.

28 Chapter 7. Aerodynamic Analysis MACH-Aero Documentation

7.2.2 Files

Navigate to the directory aero/meshing/surface in your tutorial folder. Copy the following file from the the geometry directory: $ cp ../../geometry/wing.tin .

7.2.3 Basic ICEM Usage

This section contains some general usage information that will helpful in becoming familiar with ICEM. The actual tutorial starts with Creating a surface mesh.

Opening ICEM

First, determine where the ICEM executable is located. $ which icemcfd The output should look like the following, where is the version you installed. /usr/ansys_inc//icemcfd/linux64_amd/bin/icemcfd

Then run the executable with superuser privileges, replacing the path with the results of the previous command. $ sudo /usr/ansys_inc//icemcfd/linux64_amd/bin/icemcfd

File Types

ICEM uses several native file types with the following extensions: .prj Project file. Contains references to the geometry and blocking files of the samename. .tin Geometry file. Contains a geometry definition made up of points, lines, and surfaces. .blk Blocking file. Contains the definition of the geometry and parameters used to generate themesh.

Navigating in ICEM

To adjust your view of the geometry in ICEM the following functions are possible with the mouse: • Hold down left button while dragging mouse: – Rotate the view in 3D space • Hold down middle button while dragging mouse: – Translate view in viewing plane • Scroll middle button: – Slow zoom in/out • Hold down right button – Drag mouse up/down: Fast zoom – Drag mouse left/right: Rotate view in viewing plane • x on the keyboard: fit the complete geometry to the window

7.2. Surface Meshing 29 MACH-Aero Documentation

• z on the keyboard: allows creating a zoom-in box on the viewing pane with the left mouse button

Changing the appearance of the geometry

The two buttons outlined in red can be used to view the geometry as a wire frame (left button) or a collection of opaque surfaces (right button).

7.2.4 Creating a surface mesh

Load the geometry

In ICEM, select File → Geometry → Open Geometry. Navigate to the surface meshing folder and open wing.tin. ICEM will prompt you to create a project called wing.prj. Select Yes.

Rename Parts

You will see in the model tree that there are 5 different parts with arbitrary names. We want to redefine a single part that contains all wing geometry and call it WING.

Right-click on Parts in the model tree and select Create Part. The options for creating a new part will appear in the lower left-hand pane as shown below. Change the name from “PART.1” to “WING”. We want to create the “WING” part by selecting objects in the viewing pane. To do this, select the arrow to the right of the Entities box (outlined in red) and then drag a box (with the left mouse button) over all the wing surfaces in the viewing pane. All of the selected geometry should become highlighted. Now click the center mouse button to verify the operation. All of the selected components should become the same color, and a new part called “WING” should appear in the model tree under Parts. To refresh the model tree, deselect and then reselect the checkbox next to the “WING” part. This should make all of the other parts go away.

30 Chapter 7. Aerodynamic Analysis MACH-Aero Documentation

Auxiliary Geometry

Before actually creating the mesh, it is helpful to create some additional geometric features to use as references for the mesh. All geometry creation and manipulation is done under the Geometry tab, outlined in red in the image below.

1. Create curves and points from surfaces

You will notice that the geometry section of the model tree contains only Subsets and Surfaces. We want to see the curves and points that define the boundaries of these surfaces. This can be done by clickingon the Repair Geometry button in the Geometry tab. The Repair Geometry section will open up in the lower left pane. The default operation in this section is Build Diagnostic Topology. This will create the curves and points that define the surface intersec- tions, if they are missing. Click Apply at the bottom of the pane (the default options should be sufficient). You will see red and yellow curves appear on the geometry. The red curves denote an intersection between two surfaces and the yellow curves denote unattached surface edges. Additionally, points appear at the corners of the surfaces. If you look at the model tree now, you should see Subsets, Points, Curves, and Surfaces under the Geometry branch and a single part named “WING” in the Parts branch.

There are some curves and points missing still. If you look closely at the trailing edge of the wing, you will see that only one curve was made when we repaired the geometry (uncheck the Surfaces branch in the model tree under Geometry to make it easier to see). This is because the lower surface of the wing is continuous with the trailing edge surface, so there is no intersection. We need to make a curve to define the lower edge of the trailing edge. First we need to create some points. To do this, let’s go to the Create Point button of the Geometry tab and then select Curve Ends in the lower left pane.

7.2. Surface Meshing 31 MACH-Aero Documentation

Select “both” in the How drop-down menu and then click the arrow to the right of the Curve(s) box. Now select the curve on the upper edge of the trailing edge and the lower surface curves at the root and tip of the wing. Now let’s go to the Create/Modify Curve button of the Geometry tab.

Select the first option in the lower left pane (From Points). This will create a straight line between two points or a spline between multiple points. Select the arrow to the right of the Points box and then choose the points at either end of the lower edge of the trailing edge. For good measure, you can close off the trailing edge by creating curves between the upper and lower surfaces at the root and tip of the trailing edge. In the end, your trailing edge should look like this (only Curves and Points are turned on in the Geometry tree).

32 Chapter 7. Aerodynamic Analysis MACH-Aero Documentation

2. Create auxiliary curves

Now let’s create some curves to help define the leading edge section of the surface mesh. First weneedto create some points, so go back to the Create Point section. This time select the Parameter along a Curve operation.

Put 0.01 in the Parameters box and then click the arrow to the right of the Curve box. Now select the upper and lower airfoil curves at the wingtip. You should see two points appear near the leading edge. For the root airfoil section, the curves are flipped, so we need to enter 0.99 inthe Parameters box and then select the upper and lower curves just like we did for the wingtip. Now we need to connect these points with curves. Go back to the Create/Modify Curve button under the Geometry tab and select the From Points operation. Connect the points on the upper surface with one line and the points on the lower surface with another line. Now the leading edge of your wing should look like this:

7.2. Surface Meshing 33 MACH-Aero Documentation

Blocking

The blocking is the underlying structure that defines the mesh. In the blocking we can define how many cells wewant and how we want them to be arranged. For this case, we will define properties for the edges of the blocks which will then be project by ICEM onto the geometry to create a surface mesh.

1. Create 3D blocking with bounding box

The best way to create the blocking is to first create a 3-D bounding box and to then convert that blocking from 3-D to 2-D. This approach is preferred as it helps ICEM understand the topology, often preventing future issues. To do this, under the Blocking tab, select the first icon, Create Block shown here:

This opens a menu in the lower left corner of the window. With the default options, click the button next to the input box for the entities (if it was not automatically selected). This button allows you to select the entities you want to create a blocking for from the CAD model. Directions for selecting entities are found in red text at the bottom of the CAD window. To create a bounding box around the entire wing, select all of the wing entities by clicking and dragging with the left mouse button.

34 Chapter 7. Aerodynamic Analysis MACH-Aero Documentation

2. Convert 3D blocking to 2D blocking

Now the 3-D bounding box needs to be converted to a 2-D blocking (as we only want a surface mesh output from ICEM). To do this, select the fifth icon in the Create Block menu (shown below).

After selecting the fifth icon, select OK or Apply at the bottom of the Create Block menu. If the conversion was successful, in the dialog box there will be a message reading “...Blocking successfully converted to 2D...” Look back at the model tree and you should see something like this (expand the Blocking tab).

7.2. Surface Meshing 35 MACH-Aero Documentation

If you check the box next to Blocks, you will see green surfaces appear surrounding the wing. Since the wing root is on the symmetry plane, we want to remove the block along the symmetry plane. This can be done with the Delete Block button in the Blocking tab. Check the box for “Delete permanently” and then select the green surface parallel with the root airfoil. It should become highlighted like in the image below.

To complete the operation, click the middle mouse button.

36 Chapter 7. Aerodynamic Analysis MACH-Aero Documentation

3. Associate blocking to geometry

In order to control the shape of the surface mesh, we can associate the block edges to curves on the geom- etry. We can do this with the Associate button in the Blocking tab. First, let’s associate the vertices of the blocks to points on the geometry.

The first operation in the Associate pane allows us to associate vertices to points, curves, or surfaces. We want to associate the 8 vertices to the corresponding 8 points at the corners of our wing. Click the arrow to the right of the Vertex box. The first selection in the view pane will choose the vertex and the second selection will choose the point to which it will be associated. The association will happen immediately and the vertex should move to the same location as the point. You can continue selecting vertex and point pairs until you are done. After associating the vertices at the wing root, the blocking should look like this.

Now do the same thing at the wing tip.

7.2. Surface Meshing 37 MACH-Aero Documentation

The next step is to associate the block edges to the geometry. Go to the second button in the Associate pane: Associate Edge to Curve. Now select the upper edge at the symmetry plane and then select the upper curve of the root airfoil. You must confirm each selection by clicking the middle mouse button. After the edge is associated, it should turn green. Do the same thing with the lower edge and the lower curve of the root airfoil. For the vertical edge at the leading edge of the root, we need to associate to both the upper and lower root airfoil curves. First select the edge and confirm, and then select both airfoil curves and confirm. Do the same for the wingtip. Let’s check out the state of the mesh at this point. We can view the mesh by checking the box next to Pre-Mesh in the Geometry branch of the model tree. If you are in wire mesh view, switch to a solid surface view (see Changing the appearance of the geometry). You will see that the mesh is collapsed in on itself (don’t worry, we’ll fix it in the next step).

4. Split and adjust edges

To remedy the collapsed mesh, we need to create some control points along the edges.

Go to the Edit Edge button in the Blocking tab. Under the Split Edge operation, choose the “Linear” method. Then click the arrow to the right of the Edge box and select the upper horizontal edge at the symmetry plane. Immediately, a point will snap to the associated curve (make sure you drag it up to the upper curve of the airfoil before you let go). Once you let go of the mouse button, a dialog box will pop up with the following message:

Select “Yes” and you will see the mesh snap up to the upper surface. Do the same for the wingtip. At this point we will have a very coarse discretization of the wing surface that looks like the following at the wingtip.

38 Chapter 7. Aerodynamic Analysis MACH-Aero Documentation

Un-check Pre-Mesh in the model tree to avoid the recompute mesh dialog box popping up at each step.

5. Define edge properties

To further refine the mesh, we need to modify some edge parameters. Inthe Blocking tab, click on the Pre-Mesh Params button (a cube with a grid). In the menu in the lower left corner, click on the Edge Params button under Meshing Parameters. For the Edge, select the vertical edge at the leading edge of the wingtip. Then type in 17 (in general this should be an odd number, 4n+1 where n is an integer so that multi-grid options can be used) for Nodes, select Uniform for the Mesh law, select Copy Parameters (with the default To All Parallel Edges under Copy), and click OK.

Note: The most commonly useful mesh-spacing laws are BiGeometric, Poisson, and Hyperbolic. When specifying edge spacings, it is important to keep in mind that there should not be large jumps in

7.2. Surface Meshing 39 MACH-Aero Documentation

cell sizes across edge boundaries. Large changes in cell size can result in pyHyp errors and poor quality results.

Now we will specify parameters for the edges associated with the upper and lower airfoil curves at the wingtip. Select the upper edge at the wingtip for Edge in the Pre-Mesh Params menu. Specify 161 for Nodes and select Hyperbolic for the Mesh law. Next, to avoid large discontinuities in element size, we will select some edges to link to this edge. This is done by specifying edges to link to Sp1 and Sp2. The edge will have an arrow displayed on it. This arrow points from the vertex corresponding to Sp1 to the vertex corresponding to Sp2. Click on the box to the left of Sp1 and then click on Select and select the vertical edge at the leading side of the wingtip (or the trailing edge if Sp1 corresponds to the trailing edge). Then do the same for Sp2 with the vertical edge at the trailing side of the wingtip. Click the box for Copy Parameters if it isn’t selected by default (this will copy these settings for the three other edges at the wingtip and the root) and click OK. Next, we will set the edge parameters for the edges running spanwise along the leading and trailing edges of the wing. Select the upper edge at the leading edge of the wing for Edge in the Pre-Mesh Params menu. Specify 161 for Nodes and select Uniform for the Mesh law. The click on the box to the left of Copy Parameters and select To All Parallel Edges under Copy (if not already selected by default). At this point the pre-mesh should look like the following at the wingtip.

40 Chapter 7. Aerodynamic Analysis MACH-Aero Documentation

7. Check mesh quality

We can see that the above mesh is far from ideal (for example, due to the large changes in element size at the wingtip). We can also use the Pre-Mesh Quality Histogram tool to check the mesh quality. In the Blocking tab, click on the Pre-Mesh Quality Histogram button (a red Q around a cube with a grid) and then click on OK with the default settings. The following histogram should appear on the bottom right of the window.

This shows that we have a few poor quality elements (less than 0.5). To see the elements corresponding to a particular bar of the histogram, click on the bar. Hiding the pre-mesh and then pressing x on the keyboard should show the elements. Showing the pre-mesh again should help see where they lie with respect to the wing. These happen to be at the leading edge of the wingtip. Also, we need to improve the quality of the mesh as the elements transition from the upper and lower surfaces of the wing to the wingtip surface. For surface meshes that will be used in pyHyp, the minimum quality of any cell in the mesh should be about 0.7. The mesh needs to be adjusted if there are low quality cells. Oftentimes, adjusting node spacing or some associations can fix low mesh quality issues. However, adjusting the mesh to assure high quality can often be a bit tricky, particularly for inexperienced users. Taking a break at this point and reviewing the steps so far is recommended.

8. Improve mesh

To improve the mesh, we will first split the block to gain a little more flexibility with the mesh.Inthe Blocking tab, click on the Split Block button (an axe with a cube). In the menu at the bottom left, also select the Split Block option (an axe with a cube). Click on the arrow to the right of the Edge box, then click on the upper leading edge near the wing tip, as shown below to split the block.

7.2. Surface Meshing 41 MACH-Aero Documentation

After this, we will first change the edge parameters of the new horizontal edge at the leading sideofthe wing. Go to the Edge Params menu under Pre-mesh Params as shown earlier. Select the edge, enter 17 for Nodes, select Geometric2 for the Mesh law, link Sp2 to the vertical edge at the wingtip, click the box for Copy Parameters if it is not already selected by default, and accept the options.

Note: For reference, in the menu shown above, the numbers in the gray boxes next to some items (e.g.,

42 Chapter 7. Aerodynamic Analysis MACH-Aero Documentation

Spacing 1 and Spacing 2) show the smallest values that can actually be achieved. Also, the linked numbers shown when linking edges (e.g., linked 22 26) correspond to the numbers of the vertices of the edges. These numbers can be displayed by checking Vertices in the model tree and then right-clicking it and clicking on Numbers. These numbers can be used to verify that the correct edges are selected while linking.

Similarly, we will now set the Edge Params for the longer horizontal leading and trailing edges. Select the top edge at the leading side, enter 161 for Nodes, select Hyperbolic for the Mesh law, set Spacing 1 to 0.1, link Sp2 to the horizontal edge closer to the wingtip that we set parameters for right before this, click the box for Copy Parameters if it is not already selected by default, and accept the options. At this point the mesh should look something like the following at the wingtip.

Next, we will disassociate the edges at the wingtip from the curves we had selected in Step 3. In the Blocking tab, click on the Associate button, and click the Disassociate from Geometry (a finger with an X) button in the bottom left menu. For Edges, select both halves of the top edge at the wingtip and the bottom edge at the wingtip, and accept.

7.2. Surface Meshing 43 MACH-Aero Documentation

Next, click the Associate edge to Surface button under Edit Associations then select both halves of the top edge at the wingtip and the bottom edge at the wingtip, and accept. Now we will split these edges into a lot more pieces (Edit Edge in the Blocking tab, then Split Edge as described in Step 4). Split the top edge at the wingtip into about 6 segments and split the bottom edge at the wingtip into about 12 segments. For the upper chordwise edge inboard of the wingtip edge, split the edges into 2 segments. Splitting edges provides greater flexibility and more can be created if required. The following is what the edges should look like at this point.

44 Chapter 7. Aerodynamic Analysis MACH-Aero Documentation

The next phase will be more challenging because these edges are now associated with surfaces and moving the vertices can be tricky. In the Blocking tab, click on the Move Vertex button (an arrow with two vertices). With Move Vertex selected in the bottom left menu, click on the button to the right of Vertex and adjust the vertices of the upper and lower wingtip edges to look like the following image. Making Surfaces visible (as a wireframe) from the model tree should also help. This process will require some patience. Rotating the view should show if the vertices actually moved to the desired location. Also, the Fix X/Y/Z options in the menu can be useful to prevent the vertices from moving in unwanted directions while dragging them.

The pre-mesh should look something like the following at this point.

7.2. Surface Meshing 45 MACH-Aero Documentation

If you see an overlapping or collapsed mesh, check the associations of the edges. Right click on Edges in the model tree and click on Show Association. If an edge associated with a surface does not have an arrow pointing toward the surface, splitting and dragging should fix the problem as shown earlier inStep 4.

9. Check mesh quality again

Using the mesh quality check, we see that we have a better quality mesh at this point (although it can certainly still be improved with more fine tuning and splitting of edges).

10. Ensure correct block orientation

We now have a pre-mesh defined over the surface of the wing. Before proceeding, we need to check the orientation of the blocking. For pyHyp to correctly extrude the mesh and for the boundary conditions to be applied properly, it is essential that the blocking is correctly oriented. The orientation of the blocking can be checked in the Edit Block (fourth button under the Blocking tab) menu. Within that menu, select the button with ijk and kji in the icon. That will open the Change Block IJK sub menu, as shown below.

46 Chapter 7. Aerodynamic Analysis MACH-Aero Documentation

If the blocking faces are not shown, turn them on in the hierarchy tree. When they are enabled and the Change Block IJK button is selected, the faces of the blocking should be red and green. If the blocking is properly oriented, all of the green sides of the faces will be outward facing. If any of the faces have red facing outward, select the icon in the Change Block IJK and select the face to flip.

Convert to MultiBlock Mesh

Converting the pre-mesh to a multiblock mesh is relatively straightforward. Right-click on Pre-Mesh in the model tree. In the menu that opens, select Convert to MultiBlock Mesh.A Mesh branch should then be added to the hierarchy tree.

7.2. Surface Meshing 47 MACH-Aero Documentation

Export the mesh

Exporting the mesh is done from the Output tab. The first step is to select the first button, with the red toolbox. This opens a menu where you can select the solver to export to. For our purposes, select CGNS. At that point, the fourth and final button under the Output tab can be selected. At the prompt, click Open to use your multiblock mesh. Then select All domains of the mesh. After that a window should come up with saving options. All of the default options should work. The window is shown below.

The surface mesh is now ready for use in pyHyp. To proceed to the next tutorial (volume meshing with pyHyp), reduce the number of nodes specified for the edges so far (e.g., 17 to 5, and 161 to 41), and convert and export themesh again. Or use the wing.cgns file provided in MACH-Aero/tutorial/aero/meshing/volume. This should reduce computational time and the probability of pyHyp failing with the default options provided in the following tutorial and the mesh generated so far.

7.3 Volume Meshing

7.3.1 Introduction

The objective of this section is to create a volume mesh using pyHyp. The surface mesh serves as the seed for hyper- bolically marching the mesh to the farfield. Generating the volume mesh in this way is fast, repeatable, and results in a high-quality mesh. More details on pyHyp can be found in the pyHyp documentation or in the code itself.

7.3.2 Files

Navigate to the directory aero/meshing/volume in your tutorial folder. Copy the following file from the surface meshing directory: $ cp ../surface/wing.cgns . Create the following empty runscript in the current directory: • run_pyhyp.py

48 Chapter 7. Aerodynamic Analysis MACH-Aero Documentation

7.3.3 Dissecting the pyHyp runscript

Open the file run_pyhyp.py with your favorite text editor. Then copy the code from each of the following sections into this file.

Import libraries from pyhyp import pyHyp

This is the standard way of importing the pyHyp library.

Options

For each module in MACH, we generally pass in options using a dictionary. A complete list of definitions of the pyHyp options can be found in the pyHyp documentation. Here we will point a few of the more basic options. For pyHyp, the options can be organized like so: options={ # ------# General options # ------"inputFile":"wing.cgns", "fileType":"CGNS", "unattachedEdgesAreSymmetry": True, "outerFaceBC":"farfield", "autoConnect": True, "BC": {}, "families":"wall",

General options: inputFile Name of the surface mesh file. fileType Filetype of the surface mesh file. Either cgns or plot3d. unattachedEdgesAreSymmetry Tells pyHyp to automatically apply symmetry boundary conditions to any unattached edges (those that do not interface with another block). outerFaceBC Tells pyHyp to which boundary condition to apply to the outermost face of the extruded mesh. Either farfield or overset. families Name given to wall surfaces. If a dictionary is submitted, each wall patch can have a different name. This can help the user to apply certain operations to specific wall patches in ADflow. # ------# Grid Parameters # ------"N": 49, "s0": 1e-5, "marchDist": 23.2* 14,

Grid parameters: N Number of nodes in off-wall direction. If multigrid will be used this number shouldm-1 be2 (n+1), where m is the number of multigrid levels and n is the number of layers on the coarsest mesh.

7.3. Volume Meshing 49 MACH-Aero Documentation

s0 Thickness of first off-wall cell layer. marchDist Distance of the far-field. The following options are related to the algorithms that are used to generate the mesh and usually these default values do not need to be modified. More information can be found in the pyHyp documentation. For example, epsE may be of interest when dealing with concave corners. # ------# Pseudo Grid Parameters # ------"ps0":-1.0, "pGridRatio":-1.0, "cMax": 1.0,

# ------# Smoothing parameters # ------"epsE": 1.0, "epsI": 2.0, "theta": 3.0, "volCoef": 0.2, "volBlend": 0.0005, "volSmoothIter": 20,

# ------# Solution Parameters # ------"kspRelTol": 1e-10, "kspMaxIts": 1500, "kspSubspaceSize": 50, }

Running pyHyp is quite simple, as shown below. After the mesh extrusion is done, we can write the volume mesh with the writeCGNS function. hyp= pyHyp(options=options) hyp.run() hyp.writeCGNS("wing_vol.cgns")

7.3.4 Run it yourself!

You can now run the python file with the command: $ python run_pyhyp.py For larger meshes, you will want to run pyHyp as a parallel process. This can be done with the command: $ mpirun -np 4 python run_pyhyp.py where the number of processors is given after -np. You can open wing_vol.cgns in Tecplot to view the volume mesh.

50 Chapter 7. Aerodynamic Analysis MACH-Aero Documentation

7.4 Mesh Manipulation Tools

7.4.1 Introduction

There are several simple meshing operations that can be done easily enough from the command line. For example, creating a 3D cartesian block of a given size with a specified uniform cell size only requires 4 parameters. We havede- veloped a suite of functions called cgnsUtilities that can be called from the command line to perform simple, repeatable tasks like this.

7.4.2 Files

Navigate to the directory aero/meshing/volume in your tutorial folder. We will perform operations on the file wing_vol.cgns.

7.4.3 cgnsUtilities Operations

To get a list of all of the operations available with cgnsUtilities, run the command $ cgns_utils -h For any cgns_utils operation, you can add the argument -h to get information about the required and optional parameters (e.g. cgns_utils symmzero -h).

Coarsening a volume mesh

The command cgns_utils coarsen takes a volume mesh and coarsens it by removing every other node in all three dimensions. Although our volume mesh is already very coarse, we can coarsen it even further. This function is more useful in instances where we start with a very fine mesh. To coarsen the mesh run the following from the terminal: $ cgns_utils coarsen wing_vol.cgns wing_vol_coarsened.cgns

7.4.4 CGNS Viewer

Bundled with cgnsUtilities is a very barebones mesh viewing software that can be useful for debugging problems with the mesh such as incorrect boundary conditions or missing face-to-face connections. CGNS Viewer can be run from the terminal with the command $ cgnsview More details to come later.

7.5 Analysis with ADflow

7.5.1 Introduction

There is no graphical user interface for ADflow. The cases are prepared with python scripts and run from the command line. In this section of the tutorial, we will explain the nuts and bolts of a basic ADflow runscript. You will find a complete introduction to ADflow in the docs.

7.4. Mesh Manipulation Tools 51 MACH-Aero Documentation

7.5.2 Files

Navigate to the directory aero/analysis in your tutorial folder. Copy the following file from the volume meshing directory: $ cp ../meshing/volume/wing_vol.cgns . Create the following empty runscript in the current directory: • aero_run.py

7.5.3 Dissecting the ADflow runscript

Open the file aero_run.py with your favorite text editor. Then copy the code from each of the following sections into this file.

Import libraries

import numpy as np import argparse import os from adflow import ADFLOW from baseclasses import AeroProblem from mpi4py import MPI

parser= argparse.ArgumentParser() parser.add_argument("--output", type=str, default="output") parser.add_argument("--gridFile", type=str, default="wing_vol.cgns") args= parser.parse_args()

comm= MPI.COMM_WORLD if not os.path.exists(args.output): if comm.rank ==0: os.mkdir(args.output)

First we import ADflow. We also need to import baseclasses, which is a library of problem and solver classes used to encourage a common API within the MACH suite. In this case we will be using the AeroProblem, which is a container for the flow conditions that we want to analyze. Finally, it is convenient to import the mpi4py library to prevent printing multiple times if we are running on multiple processors. Importing mpi4py is not entirely necessary in the runscript because ADflow does it internally if necessary.

ADflow options

aeroOptions={ # I/O Parameters "gridFile": args.gridFile, "outputDirectory": args.output, "monitorvariables":["resrho","cl","cd"], "writeTecplotSurfaceSolution": True, # Physics Parameters "equationType":"RANS", (continues on next page)

52 Chapter 7. Aerodynamic Analysis MACH-Aero Documentation

(continued from previous page) # Solver Parameters "smoother":"DADI", "MGCycle":"sg", # ANK Solver Parameters "useANKSolver": True, # NK Solver Parameters "useNKSolver": True, "nkswitchtol": 1e-4, # Termination Criteria "L2Convergence": 1e-6, "L2ConvergenceCoarse": 1e-2, "nCycles": 1000, }

An exhaustive list of the ADflow options and their descriptions can be found in the docs. For our purposes here, Iwillgo over them briefly. The I/O Parameters include the mesh file, the output directory, and the variables that will be printed as the solver runs. Under Solver Parameters, you can choose a basic solver (DADI or Runge Kutta) and set the CFL and multigrid parameters. Additionally, the Approximate Newton-Krylov (ANK) and Newton-Krylov (NK) solvers can be used to speed up convergence of the solver. Finally, we can terminate the solver based on relative convergence of the norm of the residuals or maximum number of iterations. We strongly recommend going over the descriptions and tips on solvers and solver options in the ADflow solvers docs.

Create solver

# Create solver CFDSolver= ADFLOW(options=aeroOptions)

# Add features CFDSolver.addLiftDistribution(150,"z") CFDSolver.addSlices("z", np.linspace(0.1, 14, 10))

When ADflow is instantiated, it reads in the mesh and then waits for the user to dictate further operations. Before running the case, we can choose to set up some additional output options. First, ADflow can write a file containing distribution data over the extent of the wing (e.g. lift, drag, twist) using addLiftDistribution. Also, ADflow can write airfoil data for a given set of slices along the wing using addSlices.

Set flow conditions

ap= AeroProblem(name="wing", mach=0.8, altitude=10000, alpha=1.5, areaRef=45.5,␣

˓→chordRef=3.25, evalFuncs=["cl","cd"])

The flow conditions and any auxiliary information about the geometry (such as reference dimensions) are providedto ADflow by way of an AeroProblem. The AeroProblem automatically generates complete flow state information from the Mach number and altitude based on the 1976 U.S. Standard Atmosphere. The alpha parameter is used to rotate the flow in the far-field to simulate angle-of-attack. The evalFuncs parameter stipulates which functions the user would like to compute from the converged flow solution. Some available functions include 'cl', 'cd', 'cmz', 'lift', and 'drag'.

7.5. Analysis with ADflow 53 MACH-Aero Documentation

Run solver

# Solve CFDSolver(ap)

Running the solver is very simple. It only requires an AeroProblem to run.

Evaluate functions funcs={} CFDSolver.evalFunctions(ap, funcs) # Print the evaluated functions if comm.rank ==0: print(funcs)

The function evaluation is done separately from the solution. We pass a dictionary to ADflow and it will populate it with the prescribed functions. We can request additional functions with the evalFuncs parameter. Finally we print out the requested functions on the root proc.

7.5.4 Run it yourself!

First make the output directory and then run the script (you may have to change your outputDirectory in aeroOptions) $ mkdir output $ mpirun -np 4 python aero_run.py

7.6 Drag Polar Analysis with ADflow

7.6.1 Introduction

This example is an extension of the previous example “Analysis with ADflow”. Much of the code used for the two cases is the same, however, we include the full script for completeness. We extend the basic single point run script to run a simple variable sweep, in this case angle of attack, to demonstrate the power of scripting the analysis. As we go through the code, we will highlight the differences between the this example and the previous case.

7.6.2 Files

Navigate to the directory aero/analysis in your tutorial folder. If you haven’t already done so, copy the following file from the volume meshing directory: $ cp ../meshing/volume/wing_vol.cgns . Create the following empty runscript in the current directory: • aero_run_drag_polar.py

54 Chapter 7. Aerodynamic Analysis MACH-Aero Documentation

7.6.3 Dissecting the ADflow runscript

Open the file aero_run_drag_polar.py with your favorite text editor. Then copy the code from each of the following sections into this file.

Import libraries import numpy as np import argparse import os from adflow import ADFLOW from baseclasses import AeroProblem from mpi4py import MPI parser= argparse.ArgumentParser() parser.add_argument("--output", type=str, default="output_drag_polar") parser.add_argument("--gridFile", type=str, default="wing_vol.cgns") args= parser.parse_args() comm= MPI.COMM_WORLD if not os.path.exists(args.output): if comm.rank ==0: os.mkdir(args.output)

As in the previous example, we first import ADflow. We also need to import baseclasses, which is a library of problem and solver classes used to encourage a common API within the MACH suite. We will again be using the AeroProblem, which is a container for the flow conditions that we want to analyze. However, in this case we will sequentially update the alpha parameter to produce the desired drag polar. Again, it is convenient to import the mpi4py library to prevent printing multiple times if we are running on multiple processors. Importing mpi4py is not entirely necessary in the runscript because ADflow does it internally if necessary.

ADflow options aeroOptions={ # I/O Parameters "gridFile": args.gridFile, "outputDirectory": args.output, "monitorvariables":["resrho","cl","cd"], "writeTecplotSurfaceSolution": True, # Physics Parameters "equationType":"RANS", "infchangecorrection": True, # Solver Parameters "smoother":"DADI", "MGCycle":"sg", "MGStartLevel":-1, "nCyclesCoarse": 250, # ANK Solver Parameters "useANKSolver": True, # NK Solver Parameters (continues on next page)

7.6. Drag Polar Analysis with ADflow 55 MACH-Aero Documentation

(continued from previous page) "useNKSolver": True, "nkswitchtol": 1e-4, # Termination Criteria "L2Convergence": 1e-6, "L2ConvergenceCoarse": 1e-2, "nCycles": 1000, }

An exhaustive list of the ADflow options and their descriptions can be found in ADflow options. We will not go over every option here, as they are outlined in the previous example. However, we will highlight the option that has been changed for this case. This option is the “infchangecorrection” option. This option causes the change in the freestream velocity to be added to every cell in the mesh whenever alpha is updated. This produces a significant improvement in the convergence of the implicit solver algorithms in the code, in particular the full Newton-Krylov algorithm.

Create solver

# Create solver CFDSolver= ADFLOW(options=aeroOptions)

# Add features CFDSolver.addLiftDistribution(150,"z") CFDSolver.addSlices("z", np.linspace(0.1, 14, 10))

When ADflow is instantiated, it reads in the mesh and then waits for the user to dictate further operations. Asinthe previous example, we tell ADflow to create lift distribution and section slice files for the test case. ADflowappendsa numeric counter to the file name, which is incremented for each solution call, so that the files will not beoverwritten in scripted cases like this drag polar calculation.

Create AeroProblem Object ap= AeroProblem(name="wing", mach=0.8, altitude=10000, alpha=1.5, areaRef=45.5,␣

˓→chordRef=3.25, evalFuncs=["cl","cd"])

As in the previous case, we create a single AeroProblem instance which describes the flight condition and reference quantities for the flow simulation. At this stage, the setup of the AeroProblem is the same as the single point analysis. However, as we will show later, we will update this AeroProblem with different values of alpha to produce the desired drag polar.

Create Drag Polar Arrays

# Create an array of alpha values. # In this case we create 6 evenly spaced values from 0 - 5. alphaList= np.linspace(0,5,6)

# Create storage for the evaluated lift and drag coefficients (continues on next page)

56 Chapter 7. Aerodynamic Analysis MACH-Aero Documentation

(continued from previous page) CL=[] CD=[]

This is where the major differences between a single point run and the drag polar become evident. We start bycreating an array of the angle of attack values that we wish to simulate. In this case we use the numpy.linspace function to create a uniformly space array with six whole number entries from 0 - 5. We also create the empty lists for storing the lift and drag coefficients. The lift and drag data will be appended to these lists as the flow solutions arecompleted.

Loop over the Angle of Attack Input Arrays

# Loop over the alpha values and evaluate the polar for alpha in alphaList:

Having created the input array and data storage lists, we can now loop over the desired angles of attack to evaluate the polar. We accomplish this by using the builtin “for” loop structure in python.

Update the AeroProblem

# Update the name in the AeroProblem. This allows us to modify the # output file names with the current alpha. ap.name="wing_ %4.2f "% alpha

# Update the alpha in aero problem and print it to the screen. ap.alpha= alpha if comm.rank ==0: print("current alpha: %f "% ap.alpha)

Now for each angle of attack, we update two attributes of the aero problem. We update the name to include the current angle of attack. This allow the filenames of the lift distribution, slices, volume solution and surface solution tobe updated with the current angle of attack, making it easier to keep track of the output files. We also update the alpha parameter, which is the attribute of the AeroProblem that represents the angle of attack.

Run solver and Accumulate Drag Polar

# Solve the flow CFDSolver(ap)

# Evaluate functions funcs={} CFDSolver.evalFunctions(ap, funcs)

# Store the function values in the output list CL.append(funcs["%s_cl"% ap.name]) CD.append(funcs["%s_cd"% ap.name])

Running the solver is identical to the simple single point example. We simply call the CFDSolver instance with the current AeroProblem. This causes the CFD solver to be updated with the values of that AeroProblem prior to solving

7.6. Drag Polar Analysis with ADflow 57 MACH-Aero Documentation the flow. We then use the same EvalFunctions call to integrate the surface forces to get the lift and dragcoefficients. The difference is that here, we append the coefficients from “funcs” into the “CL” and “CD” list, sothattheycanbe used later.

Print Drag Polar

# Print the evaluated functions if comm.rank ==0: print("Alpha:", alphaList) print("CL:", CL) print("CD:", CD)

Once we complete the loop and evaluate all of the desired flow conditions, we can print the completed data set tothe screen.

7.6.4 Run it yourself!

First make the output directory and then run the script $ mkdir output $ mpirun -np 4 python aero_run_drag_polar.py

7.7 Grid Refinement Study

7.7.1 Theory

Computational physics models generally use discretized physical domains (grids) to transform physical laws into sys- tems of equations that can be solved by numerical methods. The discretization of a physical domain introduces dis- cretization error, which can be reduced in two ways: • h-refinement: Increasing the resolution of the grid. • p-refinement: Increasing the order of the numerical approximation at eachcell. In a grid refinement study, we demonstrate that iterative h-refinement converges to the exact solution. Foragiven grid/mesh, the grid spacing is

ℎ = 푁 −1/푑 where 푁 is the number of cells and 푑 is the dimension of the domain. For two grids, the grid refinement ratio is ℎ 푟 = 푐표푎푟푠푒 ℎ푓푖푛푒

Generally, we use a grid refinement ratio of 2. The convention is to name the grids withan L (for level) followed by an integer beginning at 0 for the finest grid (i.e. L0 for the finest, L1 for the next, etc). If the grids are sufficiently fine, the error is roughly proportional to ℎ푝, where 푝 is the order of the solution method. Grids that follow this relationship are in what is termed the “region of asymptotic convergence”. Although for a second- order finite volume solver, the theoretical order of convergence is 푝 = 2, the achieved rate of convergence (푝ˆ) may vary. The achieved rate of convergence for a given h-refinement study can be computed as (︂푓 − 푓 )︂ 푝ˆ = 푙푛 퐿2 퐿1 /푙푛(푟) 푓퐿1 − 푓퐿0

58 Chapter 7. Aerodynamic Analysis MACH-Aero Documentation using the finest three grids. When plotting the results of the grid convergence study, we generally plot the results ofthe grid convergence using ℎ푝 on the x-axis. The Richardson extrapolation is an estimation of the solution for a grid with a grid spacing on the order of zero. It is computed using the solutions from the L0 and L1 grids with the equation 푓 − 푓 푓 = 푓 + 퐿0 퐿1 ℎ=0 퐿0 푟푝^ − 1

7.7.2 Grid refinement study with ADflow

1. Generate fine grid (L0) with 푁 = 2푛푚 + 1 nodes along each edge. 2. Coarsen the L0 grid 푛 − 1 times using cgns_utils coarsen.

3. Use solveCL or solveTrimCL in ADflow to obtain 퐶퐷 for a given 퐶퐿. 4. Compute the Richardson extrapolation using the L0 and L1 grids. 푝 5. Plot ℎ vs 퐶퐷. For ADflow, use 푝 = 2 to indicate a second-order method. An example of grid convergence plot for a family of RAE 2822 Airfoil meshes is illustrated below:

Fig. 2: Figure 1: Grid convergence plot for RAE 2822 Transonic Airfoil.

7.7. Grid Refinement Study 59 MACH-Aero Documentation

7.7.3 External Links

• https://www.grc.nasa.gov/www/wind/valid/tutorial/spatconv.html • https://turbmodels.larc.nasa.gov/uncertainty_summary.pdf

60 Chapter 7. Aerodynamic Analysis CHAPTER EIGHT

AERODYNAMIC OPTIMIZATION

In this part of the tutorial, we will demonstrate how to optimize the design of an aircraft with gradient-based optimiza- tion algorithms. One of the singular attributes of the AeroOpt framework is that it was specifically designed for the purpose of conducting gradient-based optimization studies. (For a simple demonstration of why we use gradient-based optimization, check out this optimization game.) Each module was developed from the beginning with gradient-based optimization in mind to ensure that accurate gradients could be obtained efficiently. The naive approach to gradient- based optimization is generally to use finite difference approximations for derivatives of the functions of interest with respect to the design variables. There are many problems with this approach, including prohibitive computational ex- pense and rampant inaccuracy, so as a rule, we don’t touch finite difference with a ten foot pole. Instead, weusea combination of analytic gradients, automatic differentiation, and the complex-step method to compute accurate gra- dients for our optimizations. We have spent a great deal of effort to make optimization a fairly straightforward and seamless process for the end user, so we hope you enjoy learning to use our tools! Here are a few of the items we will cover in the following pages: • Set up an optimization script using pyOptSparse • Parametrize a 3D geometry using the Free-form Deformation method • Run single-point and multi-point aerodynamic shape optimizations

8.1 pyOptSparse

8.1.1 Introduction

Although we specialize in optimization, we don’t write our own optimization algorithms. We like to work a little closer to the applications side, and we figure that the mathematicians have already done an exceptional job developing the algorithms. In fact, there are many great optimization algorithms out there, each with different advantages. From the user’s perspective most of these algorithms have similar inputs and outputs, so we decided to develop a library for optimization algorithms with a common interface: pyOptSparse. pyOptSparse allows the user to switch between several algorithms by changing a single argument. This facilitates comparison between different techniques and allows the user to focus on the other aspects of building an optimization problem. In this section, we will go over the basic pyOptSparse optimization script and try our hand at the familiar Rosenbrock problem.

61 MACH-Aero Documentation

8.1.2 Files

Navigate to the directory opt/pyoptsparse in your tutorial folder. Create the following empty runscript in the current directory: • rosenbrock.py

8.1.3 Optimization Problem Definition

We will be solving a constrained Rosenbrock problem defined in the following manner:

minimize 2 2 2 100(푥1 − 푥0) + (1 − 푥0) with respect to

푥0, 푥1 subject to 3 0.1 − (푥0 − 1) − (푥1 − 1) ≤ 0

8.1.4 Dissecting the pyOptSparse runscript

Open the file rosenbrock.py in your favorite text editor. Then copy the code from each of the following sections into this file.

Import libraries

from pyoptsparse import OPT, Optimization import argparse

First we import everything from pyOptSparse. Additionally we import argparse to enable the use of command line arguments.

Command line arguments

parser= argparse.ArgumentParser() parser.add_argument("--opt", type=str, default="slsqp") args= parser.parse_args()

We often set up scripts with the command line arguments to allow us to make subtle changes to the optimization without having to modify the script. In this case we set up a single command line argument to choose the optimizer that we will use to solve the optimization problem.

62 Chapter 8. Aerodynamic Optimization MACH-Aero Documentation

Define the callback function

def userfunc(xdict): x= xdict["xvars"] # Extract array funcs={} funcs["obj"]= 100* (x[1]- x[0]**2)**2+(1- x[0])**2 funcs["con"]= 0.1- (x[0]-1)**3- (x[1]-1) return funcs

For any optimization problem, pyOptSparse needs a way to query the functions of interest at a given point in the design space. This is done with callback functions. The callback function receives a dictionary with the design variables and returns a dictionary with the computed functions of interest. The names of the design variables (xvars) and functions of interest (obj and con) are user-defined, as you will see in the next few steps.

Define the sensitivity function

def userfuncsens(xdict, funcs): x= xdict["xvars"] # Extract array funcsSens={} funcsSens["obj"]={ "xvars":[2* 100* (x[1]- x[0]**2)*(-2* x[0])-2*(1- x[0]),2* 100*␣

˓→(x[1]- x[0]**2)] } funcsSens["con"]={"xvars":[-3* (x[0]-1)**2,-1]} return funcsSens

The user-defined sensitivity function allows the user to provide efficiently computed derivatives to the optimizer.In the absence of user-provided derivatives, pyOptSparse can also compute finite difference derivatives, but we generally avoid that option. The sensitivity function receives both the dictionary of design variables (xdict) and the previously computed dictionary of functions of interest (funcs). The function returns a dictionary with derivatives of each of the functions in funcs with respect to each of the design variables in xdict. For a vector variable (like xvars), the sensitivity is provided as a Jacobian.

Instantiate the optimization problem

optProb= Optimization("Rosenbrock function", userfunc)

The Optimization class holds all of the information about the optimization problem. We create an instance of this class by providing the name of the problem and the callback function.

8.1. pyOptSparse 63 MACH-Aero Documentation

Indicate the objective function optProb.addObj("obj")

Although we have already set the callback function for the optimization problem, the optimizer does not know which function of interest it should be minimizing. Here we tell the optimizer the name of the objective function. This should correspond with one of the keys in the funcs dictionary that is returned by the callback function.

Add design variables optProb.addVarGroup(name="xvars", nVars=2, type="c", value=[3,-3], lower=-5.12, upper=5.

˓→12, scale=1.0)

Now we need to add the design variables to the problem. The addVarGroup function requires the following parameters: name Name of the design variable group. nVars Number of variables in the group. type 'c' for continuous, 'i' for integer, 'd' for discrete value Starting value for design variables. If it is a a scalar, the same value is applied to all nVars variables. Otherwise, it must be iterable object with length equal to nVars. lower Lower bound of variables. Scalar/array usage is the same as value keyword. upper Upper bound of variables. Scalar/array usage is the same as value keyword. scale Scaling factor for the design variables. The optimizer sees the unscaled value multiplied by this scaling factor. Scalar/array usage is the same as value keyword. Once all design variables have been added, a call to finalizeDesignVariables tells pyOptSparse to do any final processing of the design variable information.

Add constraints optProb.addCon("con", upper=0, scale=1.0)

We complete the optimization problem set-up by adding the constraints. The following basic options are available: name Constraint name. All names given to constraints must be unique nCon The number of constraints in this group lower The lower bound(s) for the constraint. If it is a scalar, it is applied to all nCon constraints. If it is an array, the array must be the same length as nCon. upper The upper bound(s) for the constraint. Scalar/array usage is the same as lower keyword. scale A scaling factor for the constraint. It is generally advisable to have most optimization constraint around the same order of magnitude.

64 Chapter 8. Aerodynamic Optimization MACH-Aero Documentation

Set up the optimizer

We now have a fully defined optimization problem, but we haven’t said anything about choosing an optimizer. The optimizer is set up with the following lines of code. The options dictionary can be modified to fine-tune the optimizer, but if it is left empty, default values will be used. optOptions={} opt= OPT(args.opt, options=optOptions)

Solve the problem

Finally, we can solve the problem. We give the optimizer the optimization problem, the sensitivity function, and optionally, a location to save an optimization history file. The sens keyword also accepts 'FD' to indicate that the user wants to use finite difference for derivative computations. sol= opt(optProb, sens=userfuncsens, storeHistory="opt.hst") print(sol)

8.1.5 Run it yourself!

Try running the optimization. $ python rosenbrock.py

8.1.6 Visualization with OptView pyOptSparse comes with a simple GUI to view the optimization history called OptView. You can run it with the command $ python /postprocessing/OptView.py opt.hst or if you’ve properly installed PyOptSparse $ optview opt.hst

8.2 Geometric Parametrization

8.2.1 Introduction

In order to optimize the shape of a geometry such as an airfoil or a wing, we need some way to translate design variables into actual changes in the shape. We use the Free-form deformation (FFD) technique, popularized by Tom Sederberg in the world of computer-aided graphic design, as our parametrization method. The FFD is a mapping of a region in 2D or 3D that is bounded by a set of B-splines. Every point with the region is mapped to a new location based on the deformation of the bounding B-spline curves. The B-splines themselves are defined by control points, so by adjusting the positions of these control points, we can have a great deal of control over any points embedded in the FFD volume (as shown in the figure below). Since both our CFD meshes and finite element models are point-based, wecanembed them in the FFD and give the optimizer control over their shape.

8.2. Geometric Parametrization 65 MACH-Aero Documentation

The actual implementation of the FFD method is housed in the pyGeo repository, which we were already introduced to in the very beginning of the tutorial. The specific class to look for is DVGeometry. Before diving into the parametrization, however, we need to generate an FFD, which is basically a 3D grid in the plot3d format.

8.2.2 Files

Navigate to the directory opt/ffd in your tutorial folder. Copy the following files from the tutorial directory: $ cp ../../../tutorial/opt/ffd/simple_ffd.py . $ cp ../../../tutorial/aero/analysis/wing_vol.cgns . Create the following empty runscript in the current directory: • parametrize.py

8.2.3 Creating an FFD volume

As mentioned above, the actual definition of an FFD volume is simply a 3D grid of points. We can create thisbyhand in a meshing software like ICEM, or for very simple cases, we can generate it with a script. There is also a function in pyGeo, write_wing_FFD_file, that can be used to generate a simple FFD by specifying slices and point distributions. For this tutorial, we are dealing with a relatively simple wing geometry - straight edges, no kink, no dihedral - so we will just use the script approach. This script is not very generalizable though, so it is not part of the MACH library. I will explain briefly how it works, but I won’t give it the same attention as the other scripts we use in thistutorial. Open the script simple_ffd.py in your favorite text editor.

Specify bounds of FFD volume

We need to define the dimensions of the grid at the root and the tip, and the script will interpolate betweenthemto obtain the full 3D grid. We also need to specify the number of control points we want along each dimension of the FFD grid. # Bounding box for root airfoil x_root_range=[-0.1, 5.1] y_root_range=[-0.5, 0.5] z_root=-0.01

# Bounding box for tip airfoil x_tip_range=[7.4, 9.2] y_tip_range=[-0.25, 0.25] (continues on next page)

66 Chapter 8. Aerodynamic Optimization MACH-Aero Documentation

(continued from previous page) z_tip= 14.25

# Number of FFD control points per dimension nX=6 # streamwise nY=2 # perpendicular to wing planform nZ=8 # spanwise

Compute FFD nodes

This next section just computes the nodes of the FFD grid based on the information we have provided. The vector span_dist gives the spanwise distribution of the FFD sections, which can be varied based by the user. Here we use a distribution that varies from wider spacing at the root to narrower spacing at the tip. # Compute grid points span_dist= np.linspace(0,1, nZ)** 0.8 z_sections= span_dist* (z_tip- z_root)+ z_root x_te= span_dist* (x_tip_range[0]- x_root_range[0])+ x_root_range[0] x_le= span_dist* (x_tip_range[1]- x_root_range[1])+ x_root_range[1] y_coords= np.vstack( ( span_dist* (y_tip_range[0]- y_root_range[0])+ y_root_range[0], span_dist* (y_tip_range[1]- y_root_range[1])+ y_root_range[1], ) )

X= np.zeros((nY* nZ, nX)) Y= np.zeros((nY* nZ, nX)) Z= np.zeros((nY* nZ, nX)) row=0 for k in range(nZ): for j in range(nY): X[row, :]= np.linspace(x_te[k], x_le[k], nX) Y[row, :]= np.ones(nX)* y_coords[j, k] Z[row, :]= np.ones(nX)* z_sections[k] row+=1

Write to file

Finally we write to file using the plot3d format. # Write FFD to file filename="ffd.xyz" f= open(filename,"w") f.write("\t\t1\n") f.write("\t\t%d\t\t%d\t\t%d\n"% (nX, nY, nZ)) for i in [X, Y, Z]: for row in i: vals= tuple(row) (continues on next page)

8.2. Geometric Parametrization 67 MACH-Aero Documentation

(continued from previous page) f.write("\t%3.8f \t%3.8f \t%3.8f \t%3.8f \t%3.8f \t%3.8f \n"% vals) f.close()

Generate the FFD

Run the script with the command $ python simple_ffd.py

Visualizing the FFD

The wing surface mesh should fit inside the FFD volume. We can check this by viewing the mesh and the FFDvolume in Tecplot.

68 Chapter 8. Aerodynamic Optimization MACH-Aero Documentation

8.2.4 Setting up a geometric parametrization with DVGeometry

Open the file parametrize.py in your favorite text editor. Then copy the code from each of the following sections into this file.

Import libraries import numpy as np from pygeo import DVGeometry from idwarp import USMesh

As mentioned in the introduction, the geometric parametrization code is housed in the DVGeometry class which resides in the pyGeo module. So to begin with, we import DVGeometry from pyGeo. We also import IDWarp so that we can use one of its functions to obtain surface coordinates from our volume mesh.

Instantiate DVGeometry

All that is needed to create an instance of the DVGeometry class is an FFD file in the plot3d format. Usually wecall the DVGeometry instance DVGeo. FFDFile="ffd.xyz" DVGeo= DVGeometry(FFDFile)

Define the variables

As explained in the introduction, the shape embedded inside the FFD is controlled by the movement of the control points which define the FFD volume. Although the most general parametrization would give the optimizer complete control over the x, y, and z coordinates of the FFD control points, in practice this is currently impractical. There are an infinite number of shapes that could be created by adjusting the positions of all the control points, but themajority of these shapes are nonphysical or bad designs. We can reduce the design space to eliminate the undesirable shapes by parametrizing the geometry with more intuitive design variables. For instance, in wing design, we commonly compartmentalize the parametrization into a local and a global definition. The local definition refers to the shapeof the airfoil cross-section at a given spanwise position of the wing. The local airfoil shape can be controlled by the independent adjustment of FFD control points perpendicular to the local cross-sectional plane. On the other hand, the global definition of the wing would be in terms of parameters such as span, sweep, dihedral, and twist. These parameters can be controlled by moving groups of FFD control points simultaneously in a certain fashion. DVGeometry gives us the flexibility to maintain both global and local control over the design shape. First we will explain the globalvariables and then the local variables.

Reference Axis

The first step in defining the global design variables is to set up a reference axis. The reference axis isaB-spline embedded in the FFD volume. For a wing, we can generate this reference axis simply by stating the fraction of the chord length at which it should be placed and the index of the FFD volume along which it should extend. nRefAxPts= DVGeo.addRefAxis("wing", xFraction=0.25, alignIndex="k")

8.2. Geometric Parametrization 69 MACH-Aero Documentation

In this example, the reference axis is placed at the quarter-chord along the spanwise direction with control points defined at each section of FFD control points. The name of the reference axis is “wing”. The call to addRefAxis returns the number of control points in the reference axis B-spline. The reference axis can also be defined explicitly by giving a pySpline curve object to DVGeometry.

What role does the reference axis play in the definition of global variables? In the initialization step, each FFD control point is projected onto the reference axis and given a parametric position based on that projection. The reference axis is a B-spline defined by control points, and DVGeometry is set up to automatically apply any change in the position of the reference axis control points to the FFD control points based on their parametric projected position along the reference axis. Beyond controlling the position of the reference axis control points, there are also built-in functions to handle the rotation and scaling of the FFD volume along the reference axis. The user can tailor the global design variables to the design problem at hand through the use of callback functions. We will go through a few examples.

Dihedral def dihedral(val, geo): C= geo.extractCoef("wing") for i in range(1, nRefAxPts): C[i,1]+= val[i-1] geo.restoreCoef(C,"wing")

For every callback function, the required inputs are val, which contains the design variable array, and geo, which will contain the DVGeo object. In this example, we first extract the coordinates of the reference axis control points withthe function extractCoef. Then we loop through the control points, starting with the 2nd station, and add a displacement in the y-direction (which in this case creates dihedral). We start at the 2nd control point because the position of the root of the wing should remain fixed. Finally, we restore the new control point coordinates to DVGeo with thecall restoreCoef.

70 Chapter 8. Aerodynamic Optimization MACH-Aero Documentation

Twist def twist(val, geo): for i in range(1, nRefAxPts): geo.rot_z["wing"].coef[i]= val[i-1]

The twist function makes use of the built-in rotation functions in DVGeometry. In the callback function, we loop through a dictionary containing the values of rotation about the z-axis for the “wing” reference axis. We set a rotation value (in degrees) for all of the reference control points except the first. We exclude the first control point because we already provide the optimizer with an angle-of-attack variable to control the global incidence of the wing. Twisting about the x and y axes is provided through the rot_x and rot_y dictionaries.

Taper def taper(val, geo): s= geo.extractS("wing") slope= (val[1]- val[0])/ (s[-1]- s[0]) for i in range(nRefAxPts): geo.scale_x["wing"].coef[i]= slope* (s[i]- s[0])+ val[0]

Here we define a taper variable, which controls the chord length of the root and tip airfoils and does a linear interpolation for the chords in between. First, we extract the normalized position of the reference axis control points with the call extractS. This gives a vector from 0 to 1 with the relative positions of the control points. Then we compute the slope of the chord function between the root and tip airfoils. Finally, we loop through all of the control points and set a scaling factor in the scale_x dictionary progressing linearly from val[0] at the root to val[1] at the tip. The scale_x dictionary is used in another built-in function which will scale the FFD volume in the x-direction. Additional scaling dictionaries include scale_y, scale_z, and scale, of which the latter scales uniformly in all directions.

Note: Be aware that scale_x, scale_y, scale_z, and scale are sectional attributes and only work on scaling planes perpendicular to the reference axis, i.e. do not have any effect along the spanwise axis of your FFD grid. Inthis example, if you use scale you will see that the wing gets inflated along the x and y axis, but the wing span remains identical. Planform variables such as span, and sweep should only ever be done by moving the ref axis control points. This can be done using the .extractCoef() and .restoreCoef() (as done here for dihedral angle) and possibly normalizing the section / control points displacement w.r.t. the baseline FFD grid.

8.2. Geometric Parametrization 71 MACH-Aero Documentation

Adding global variables

We have now defined the callback functions for the global design variables, but we have yet to add the variables themselves. This is done with the call addGlobalDV. nTwist= nRefAxPts-1 DVGeo.addGlobalDV(dvName="dihedral", value=[0]* nTwist, func=dihedral, lower=-10,␣

˓→upper=10, scale=1) DVGeo.addGlobalDV(dvName="twist", value=[0]* nTwist, func=twist, lower=-10, upper=10,␣

˓→scale=1) DVGeo.addGlobalDV(dvName="taper", value=[1]*2, func=taper, lower=0.5, upper=1.5,␣

˓→scale=1)

We must provide a variable name, initial value, callback function, bounds, and scaling factor. The value input must be the size of the design variable vector, but the bounds and scaling factor can be scalars if they are to be applied uniformly to the entire design variable group.

Adding local variables

Local variables are not nearly as complicated because they are simply displacements of the FFD control points in a given direction. There are two options for defining local design variables. Only one of these should be used atone time. # Comment out one or the other DVGeo.addLocalDV("local", lower=-0.5, upper=0.5, axis="y", scale=1) DVGeo.addLocalSectionDV("slocal", secIndex="k", axis=1, lower=-0.5, upper=0.5, scale=1)

The first, addLocalDV, allows displacement along one of the global coordinate axes (x, y, or z). The other options, addLocalSectionDV, defines the displacement direction based on the plane of the FFD section to which thegiven control point belongs. On a wing with a winglet, the latter option is more useful because it allow control of the airfoil sections along the axis of the winglet, which has a different orientation than the main wing. This function requires the input secIndex which gives the FFD index along which the section planes should be computed (in this case, that is the same as the direction of the reference axis). The user can also choose the axis of the section’s local coordinate system along which the points will be translated (axis=1 chooses the direction perpendicular to the wing surface).

72 Chapter 8. Aerodynamic Optimization MACH-Aero Documentation

Test the design variables

In a normal optimization script, the foregoing code is sufficient to set up the geometric parametrization. However, in this script we want to test the behavior of the variables we have defined. The following snippets of code allow usto manually change the design variables and view the results.

Embed points

First we have to embed at least one point set in the FFD. Normally, ADflow automatically embeds the surface mesh nodes in the FFD, but here we will embed surface coordinates obtained using IDWarp’s getSurfaceCoordinates function (just for this example without ADflow). gridFile="wing_vol.cgns" meshOptions={"gridFile": gridFile} mesh= USMesh(options=meshOptions) coords= mesh.getSurfaceCoordinates()

DVGeo.addPointSet(coords,"coords")

Change the design variables

We can retrieve a dictionary with the current state of all of the variables with the call getValues(). To adjust the variables, simply change the values in each variable array. Once this is done, you can return the new values with the call setDesignVars. dvDict= DVGeo.getValues() dvDict["twist"]= np.linspace(0, 50, nRefAxPts)[1:] dvDict["dihedral"]= np.linspace(0,3, nRefAxPts)[1:] dvDict["taper"]= np.array([1.2, 0.5]) dvDict["slocal"][::5]= 0.5 DVGeo.setDesignVars(dvDict)

Write deformed FFD to file

The update function actually computes the new shape of the FFD and the new locations of the embedded points. We can view the current shape of the FFD by calling writePlot3d. We can also view the updated surface coordinates by calling writePointSet. DVGeo.update("coords") DVGeo.writePlot3d("ffd_deformed.xyz") DVGeo.writePointSet("coords","surf")

8.2. Geometric Parametrization 73 MACH-Aero Documentation

8.2.5 Run it yourself!

Once you have generated the FFD with simple_ffd.py you can try out different design variables by running $ python parametrize.py You can view the results in Tecplot. The following shows the deformed (darker points) and undeformed FFD grids (visualized by selecting Scatter and Mesh under Show zone layers in tecplot, then changing the Symbol shape to Sphere under Zone Style...).

The following shows the deformed (darker points) and undeformed surface coordinates.

74 Chapter 8. Aerodynamic Optimization MACH-Aero Documentation

Experiment with creating new global design variables like span or sweep.

8.3 Aerodynamic Optimization

8.3.1 Introduction

We will now demonstrate how to optimize the aerodynamic shape of a wing. We will be combining aspects of all of the following sections: Analysis with ADflow, pyOptSparse, and Geometric Parametrization. The optimization problem is defined as minimize

퐶퐷 with respect to 7 twist variables 96 shape variables subject to

퐶퐿 = 0.5

8.3. Aerodynamic Optimization 75 MACH-Aero Documentation

8.3.2 Files

Navigate to the directory opt/aero in your tutorial folder. Copy the following files to this directory: $ cp ../ffd/ffd.xyz . $ cp ../../aero/meshing/volume/wing_vol.cgns . Create the following empty runscript in the current directory: • aero_opt.py

8.3.3 Dissecting the aerodynamic optimization script

Open the file aero_opt.py in your favorite text editor. Then copy the code from each of the following sections into this file.

Import libraries import os import argparse import ast from mpi4py import MPI from baseclasses import AeroProblem from adflow import ADFLOW from pygeo import DVGeometry, DVConstraints from pyoptsparse import Optimization, OPT from idwarp import USMesh from multipoint import multiPointSparse

The multipoint library is the only new library to include in this script.

Adding command line arguments

This is a convenience feature that allows the user to pass in command line arguments to the script. Two options are provided: • specifying the output directory • specifying the optimizer to be used

# Use Python's built-in Argument parser to get commandline options parser= argparse.ArgumentParser() parser.add_argument("--output", type=str, default="output") parser.add_argument("--opt", type=str, default="SLSQP", choices=["IPOPT","SLSQP","SNOPT

˓→"]) parser.add_argument("--gridFile", type=str, default="wing_vol.cgns") parser.add_argument("--optOptions", type=ast.literal_eval, default={}, help="additional␣

˓→optimizer options to be added") args= parser.parse_args()

76 Chapter 8. Aerodynamic Optimization MACH-Aero Documentation

Creating processor sets

The multiPointSparse class allows us to allocate sets of processors for different analyses. This can be helpful if we want to consider multiple design points, each with a different set of flow conditions. In this case, we create a processor set for cruise cases, but we only add one point. MP= multiPointSparse(MPI.COMM_WORLD) MP.addProcessorSet("cruise", nMembers=1, memberSizes=MPI.COMM_WORLD.size) comm, setComm, setFlags, groupFlags, ptID=MP.createCommunicators() if not os.path.exists(args.output): if comm.rank ==0: os.mkdir(args.output)

If we want to add more points, we can increase the quantity nMembers. We can choose the number of processors per point with the argument memberSizes. We can add another processor set with another call to addProcessorSet. The call createCommunicators returns information about the current processor’s set.

ADflow set-up

The set-up for adflow should look the same as for the aerodynamic analysis script. We add a single lift distribution with 200 sampling points. aeroOptions={ # I/O Parameters "gridFile": args.gridFile, "outputDirectory": args.output, "monitorvariables":["resrho","cl","cd"], "writeTecplotSurfaceSolution": True, # Physics Parameters "equationType":"RANS", # Solver Parameters "smoother":"DADI", "MGCycle":"sg", "infchangecorrection": True, # ANK Solver Parameters "useANKSolver": True, # NK Solver Parameters "useNKSolver": True, "nkswitchtol": 1e-6, # Termination Criteria "L2Convergence": 1e-10, "L2ConvergenceCoarse": 1e-2, "nCycles": 10000, # Adjoint Parameters "adjointL2Convergence": 1e-10, }

# Create solver CFDSolver= ADFLOW(options=aeroOptions, comm=comm) CFDSolver.addLiftDistribution(200,"z")

8.3. Aerodynamic Optimization 77 MACH-Aero Documentation

Set the AeroProblem ap= AeroProblem(name="fc", alpha=1.5, mach=0.8, altitude=10000, areaRef=45.5,␣

˓→chordRef=3.25, evalFuncs=["cl","cd"])

# Add angle of attack variable ap.addDV("alpha", value=1.5, lower=0, upper=10.0, scale=0.1)

The only difference in setting up the AeroProblem is that now we add angle-of-attack as a design variable. Anyofthe quantities included in the initialization of the AeroProblem can be added as design variables.

Geometric parametrization

The set-up for DVGeometry should look very familiar (if not, see Geometric Parametrization). We include twist and local variables in the optimization. After setting up the DVGeometry instance we have to provide it to ADflow with the call setDVGeo. # Create DVGeometry object FFDFile="ffd.xyz" DVGeo= DVGeometry(FFDFile)

# Create reference axis nRefAxPts= DVGeo.addRefAxis("wing", xFraction=0.25, alignIndex="k") nTwist= nRefAxPts-1

# Set up global design variables def twist(val, geo): for i in range(1, nRefAxPts): geo.rot_z["wing"].coef[i]= val[i-1]

DVGeo.addGlobalDV(dvName="twist", value=[0]* nTwist, func=twist, lower=-10, upper=10,␣

˓→scale=0.01)

# Set up local design variables DVGeo.addLocalDV("local", lower=-0.5, upper=0.5, axis="y", scale=1)

# Add DVGeo object to CFD solver CFDSolver.setDVGeo(DVGeo)

Geometric constraints

We can set up constraints on the geometry with the DVConstraints class, also found in the pyGeo module. There are several built-in constraint functions within the DVConstraints class, including thickness, surface area, volume, location, and general linear constraints. The majority of the constraints are defined based on a triangulated-surface representation of the wing obtained from ADflow.

Note: The triangulated surface is created by ADflow (or DAfoam) using the wall surfaces defined in the CFD volume mesh. The resolution is similar to the CFD surface mesh, and users do not need to provide this triangulated mesh themselves. Optionally, this can also be defined with an external file, see the docstrings for setSurface(). This is

78 Chapter 8. Aerodynamic Optimization MACH-Aero Documentation

useful if users want to have a different resolution on the triangulated surface (finer or coarser) compared totheCFD mesh, or if DVConstraints is being used without ADflow (or DAfoam).

The volume and thickness constraints are set up by creating a uniformly spaced 2D grid of points, which is then projected onto the upper and lower surface of a triangulated-surface representation of the wing. The grid is defined by providing four corner points (using leList and teList) and by specifying the number of spanwise and chordwise points (using nSpan and nChord).

Note: These grid points are projected onto the triangulated surface along the normals of the ruled surface formed by these grid points. Typically, leList and teList are given such that the two curves lie in a plane. This ensures that the projection vectors are always exactly normal to this plane. If the surface formed by leList and teList is not planar, issues can arise near the end of an open surface (i.e., the root of a wing) which can result in failing intersections.

By default, scaled=True for addVolumeConstraint() and addThicknessConstraints2D(), which means that the volume and thicknesses calculated will be scaled by the initial values (i.e., they will be normalized). Therefore, lower=1.0 in this example means that the lower limits for these constraints are the initial values (i.e., if lower=0.5 then the lower limits would be half the initial volume and thicknesses).

Warning: The leList and teList points must lie completely inside the wing.

DVCon= DVConstraints() DVCon.setDVGeo(DVGeo)

# Only ADflow has the getTriangulatedSurface Function DVCon.setSurface(CFDSolver.getTriangulatedMeshSurface())

# Volume constraints leList=[[0.01,0, 0.001], [7.51,0, 13.99]] teList=[[4.99,0, 0.001], [8.99,0, 13.99]] DVCon.addVolumeConstraint(leList, teList, nSpan=20, nChord=20, lower=1.0, scaled=True)

# Thickness constraints DVCon.addThicknessConstraints2D(leList, teList, nSpan=10, nChord=10, lower=1.0,␣

˓→scaled=True)

For the volume constraint, the volume is computed by adding up the volumes of the prisms that make up the projected grid as illustrated in the following image (only showing a section for clarity). For the thickness constraints, the distances between the upper and lower projected points are used, as illustrated in the following image. During optimization, these projected points are also moved by the FFD, just like the wing surface, and are used again to calculate the thicknesses and volume for the new designs. More information on the options can be found in the pyGeo docs or by looking at the pyGeo source code.

8.3. Aerodynamic Optimization 79 MACH-Aero Documentation

The LeTe constraints (short for Leading edge/Trailing edge constraints) are linear constraints based on the FFD control points. When we have both twist and local shape variables, we want to prevent the local shape variables from creating a shearing twist. This is done by constraining the upper and lower FFD control points on the leading and trailing edges to move in opposite directions. Note that the LeTe constraint is not related to the leList and teList points discussed above. # Le/Te constraints DVCon.addLeTeConstraints(0,"iLow") DVCon.addLeTeConstraints(0,"iHigh")

if comm.rank ==0: # Only make one processor do this DVCon.writeTecplot(os.path.join(args.output,"constraints.dat"))

In this script DVCon.writeTecplot will save a file named constraints.dat which can be opened with Tecplot to visualize and check these constraints. Since this is added here, before the commands that run the optimization, the file will correspond to the initial geometry. The following image shows the constraints visualized with the wing surface superimposed. This command can also be added at the end of the script to visualize the final constraints.

80 Chapter 8. Aerodynamic Optimization MACH-Aero Documentation

Mesh warping set-up meshOptions={"gridFile": args.gridFile} mesh= USMesh(options=meshOptions, comm=comm) CFDSolver.setMesh(mesh)

Optimization callback functions

First we must set up a callback function and a sensitivity function for each processor set. In this case cruiseFuncs and cruiseFuncsSens belong to the cruise processor set. Then we need to set up an objCon function, which is used to create abstract functions of other functions. def cruiseFuncs(x): if comm.rank ==0: print(x) # Set design vars DVGeo.setDesignVars(x) ap.setDesignVars(x) # Run CFD CFDSolver(ap) # Evaluate functions funcs={} DVCon.evalFunctions(funcs) (continues on next page)

8.3. Aerodynamic Optimization 81 MACH-Aero Documentation

(continued from previous page) CFDSolver.evalFunctions(ap, funcs) CFDSolver.checkSolutionFailure(ap, funcs) if comm.rank ==0: print(funcs) return funcs def cruiseFuncsSens(x, funcs): funcsSens={} DVCon.evalFunctionsSens(funcsSens) CFDSolver.evalFunctionsSens(ap, funcsSens) CFDSolver.checkAdjointFailure(ap, funcsSens) return funcsSens def objCon(funcs, printOK): # Assemble the objective and any additional constraints: funcs["obj"]= funcs[ap["cd"]] funcs["cl_con_"+ ap.name]= funcs[ap["cl"]]- 0.5 if printOK: print("funcs in obj:", funcs) return funcs

Now we will explain each of these callback functions. cruiseFuncs

The input to cruiseFuncs is the dictionary of design variables. First, we pass this dictionary to DVGeometry and AeroProblem to set their respective design variables. Then we solve the flow solution given by the AeroProblem with ADflow. Finally, we fill the funcs dictionary with the function values computed by DVConstraints and ADflow. The call checkSolutionFailure checks ADflow to see if there was a failure in the solution (could be due tonegative volumes or something more sinister). If there was a failure it changes the fail flag in funcs to True. The funcs dictionary is the required return.

82 Chapter 8. Aerodynamic Optimization MACH-Aero Documentation

cruiseFuncsSens

The inputs to cruiseFuncsSens are the design variable and function dictionaries. Inside cruiseFuncsSens we populate the funcsSens dictionary with the derivatives of each of the functions in cruiseFuncs with respect to all of its dependence variables.

objCon

The main input to the objCon callback function is the dictionary of functions (which is a compilation of all the funcs dictionaries from each of the design points). Inside objCon, the user can define functionals (or functions of other functions). For instance, to maximize L/D, you could define the objective function as: funcs['obj']= funcs[ 'cl']/ funcs[ 'cd']

The objCon function is processed within the multipoint module and the partial derivatives of any functionals with respect to the input functions are automatically computed with the complex-step method. This means that the user doesn’t have to worry about computing analytic derivatives for the simple functionals defined in objCon. The printOK input is a boolean that is False when the complex-step is in process.

Optimization problem

Setting up the optimization problem follows the same format as before, only now we incorporate multiPointSparse. When creating the instance of the Optimization problem, MP.obj is given as the objective function. multiPointSparse will take care of calling both cruiseFuncs and objCon to provide the full funcs dictionary to pyOptSparse. Both AeroProblem and DVGeometry have built-in functions to add all of their respective design variables to the opti- mization problem. DVConstraints also has a built-in function to add all constraints to the optimization problem. The user must manually add any constraints that were defined in objCon. Finally, we need to tell multiPointSparse which callback functions belong to which processor set. We also need to provide it with the objCon and the optProb. The call optProb.printSparsity() prints out the constraint Jacobian at the beginning of the optimization. # Create optimization problem optProb= Optimization("opt", MP.obj, comm=comm)

# Add objective optProb.addObj("obj", scale=1e2)

# Add variables from the AeroProblem ap.addVariablesPyOpt(optProb)

# Add DVGeo variables DVGeo.addVariablesPyOpt(optProb)

# Add constraints DVCon.addConstraintsPyOpt(optProb) optProb.addCon("cl_con_"+ ap.name, lower=0.0, scale=10.0)

# The MP object needs the 'obj' and 'sens' function for each proc set, # the optimization problem and what the objcon function is: MP.setProcSetObjFunc("cruise", cruiseFuncs) MP.setProcSetSensFunc("cruise", cruiseFuncsSens) (continues on next page)

8.3. Aerodynamic Optimization 83 MACH-Aero Documentation

(continued from previous page) MP.setObjCon(objCon) MP.setOptProb(optProb) optProb.printSparsity()

Run optimization

To finish up, we choose the optimizer and then run the optimization. # Set up optimizer if args.opt =="SLSQP": optOptions={"IFILE": os.path.join(args.output,"SLSQP.out")} elif args.opt =="SNOPT": optOptions={ "Major feasibility tolerance": 1e-4, "Major optimality tolerance": 1e-4, "Difference interval": 1e-3, "Hessian full memory": None, "Function precision": 1e-8, "Print file": os.path.join(args.output,"SNOPT_print.out"), "Summary file": os.path.join(args.output,"SNOPT_summary.out"), "Major iterations limit": 1000, } elif args.opt =="IPOPT": optOptions={ "limited_memory_max_history": 1000, "print_level":5, "tol": 1e-6, "acceptable_tol": 1e-5, "max_iter": 300, "start_with_resto":"yes", } optOptions.update(args.optOptions) opt= OPT(args.opt, options=optOptions)

# Run Optimization sol= opt(optProb, MP.sens, storeHistory=os.path.join(args.output,"opt.hst")) if comm.rank ==0: print(sol)

Note: The complete set of options for SNOPT can be found in the pyOptSparse documentation. It is useful to remember that you can include major iterations information in the history file by providing the proper options. It has been observed that the _print and _summary files occasionally fail to be updated, possibly due to unknown hardware issueson GreatLakes. The problem is not common, but if you want to avoid losing this information, you might back it up in the history file. This would allow you monitor the optimization even ifthe _print and _summary files are not being updated. Note that the size of the history file will increase due to this additional data.

84 Chapter 8. Aerodynamic Optimization MACH-Aero Documentation

8.3.4 Run it yourself!

To run the script, use the mpirun and place the total number of processors after the -np argument $ mpirun -np 4 python aero_opt.py You can follow the progress of the optimization using OptView, as explained in pyOptSparse.

8.3. Aerodynamic Optimization 85 MACH-Aero Documentation

86 Chapter 8. Aerodynamic Optimization CHAPTER NINE

AIRFOIL OPTIMIZATION

In this part of the tutorial, we will go through an example of an airfoil optimization. The process is similar to that of the previous sections, but less complicated. Here are a few of the items we will cover in the following pages: • Generate an airfoil mesh to be used for optimization • Parametrize the 2D airfoil using the Free-form Deformation method • Run a single-point aerodynamic shape optimization • Run a multi-point aerodynamic shape optimization

9.1 Mesh Generation

In this tutorial, we will use pyHyp to generate a 3D mesh in CGNS format. The coordinates for the NACA0012 airfoil are in the file n0012.dat. Coordinates for most airfoils can be obtained from the UIUC Data site. Navigate to the directory airfoilopt/mesh in your tutorial folder. Copy the airfoil data from the tutorial directory: $ cp ../../../tutorial/airfoilopt/mesh/n0012.dat . Create the following empty runscript in the current directory. • genMesh.py

9.1.1 pyHyp runscript

Import the pyHyp libraries and numpy. import numpy as np from pyhyp import pyHyp

87 MACH-Aero Documentation

9.1.2 Surface Mesh Generation

data= np.loadtxt("n0012.dat") x= data[:,0].copy() newY= data[:,1].copy() ndim=x.shape[0]

airfoil3d= np.zeros((ndim,2,3)) for j in range(2): airfoil3d[:, j,0]= x[:] airfoil3d[:, j,1]= newY[:] airfoil3d[:,0,2]= 0.0 airfoil3d[:,1,2]= 1.0 # write out plot3d fsam= open("new.xyz","w") fsam.write(str(1)+" \n") fsam.write(str(ndim)+""+ str(2)+""+ str(1)+" \n") for ell in range(3): for _k in range(1): for j in range(2): for i in range(ndim): fsam.write("%.15f \n"% (airfoil3d[i, j, ell])) fsam.close()

pyHyp requires a surface mesh input before it can create a 3D mesh. A 2D surface mesh can be created using the code above, which produces a PLOT3D file with extension .xyz. This meshes only the airfoil surface, and is used as the input file for pyHyp, which marches the existing mesh to the farfield. By performing this intermediate step, thevolume mesh generation is faster and higher-quality.

9.1.3 Options

options={ # ------# Input Parameters # ------"inputFile":"new.xyz", "unattachedEdgesAreSymmetry": False, "outerFaceBC":"farfield", "autoConnect": True, "BC":{1:{"jLow":"zSymm","jHigh":"zSymm"}}, "families":"wall",

88 Chapter 9. Airfoil Optimization MACH-Aero Documentation

General Options

inputFile Name of the surface mesh file. unattachedEdgesAreSymmetry Tells pyHyp to automatically apply symmetry boundary conditions to any unattached edges (those that do not interface with another block). outerFaceBC Tells pyHyp which boundary condition to apply to the outermost face of the extruded mesh. Note that we do not set the inlet or outlet boundaries seperately because they are automatically handled in ADflow consistently with the free stream flow direction. BC Tells pyHyp that, since it is a 2D problem, both sides of the domain jLow and jHigh are set to be symmetry boundary conditions. The input surface is automatically assigned to be a wall boundary. families Name given to wall surfaces. If a dictionary is submitted, each wall patch can have a different name. This can help the user to apply certain operations to specific wall patches in ADflow. # ------# Grid Parameters # ------"N": 129, "s0": 3e-6, "marchDist": 100.0, }

Grid Parameters

N Number of nodes in off-wall direction. If multigrid will be used this number shouldm-1 be2 (n+1), where m is the number of multigrid levels and n is the number of layers on the coarsest mesh. s0 Thickness of first off-wall cell layer. marchDist Distance of the far-field.

9.1.4 Running pyHyp and Writing to File

The following three lines of code extrude the surface mesh and write the resulting volume mesh to a .cgns file. hyp= pyHyp(options=options) hyp.run() hyp.writeCGNS("n0012.cgns")

9.1.5 Run it yourself!

You can now run the python file with the command: $ python genMesh.py

9.1. Mesh Generation 89 MACH-Aero Documentation

9.2 Geometric Parametrization

Next, the FFD (Free-Form Deformation) file has to be generated in PLOT3D format. This file contains the coordinates of the FFD points around the airfoil. These are control points that are fitted to the airfoil using B-splines, which are used to deform the airfoil. The coordinates for the NACA0012 airfoil are in the file n0012.dat. Navigate to the directory airfoilopt/ffd in your tutorial folder. Copy the airfoil data from airfoilopt/mesh: $ cp ../mesh/n0012.dat . Create the following empty runscript in the current directory. • genFFD.py

9.2.1 Import Packages

import numpy as np

9.2.2 Load Airfoil

airfoil= np.loadtxt("n0012.dat") npts= airfoil.shape[0] nmid= (npts+1)//2

The following two functions are used to get the upper and lower points of the airfoil. def getupper(xtemp): myairfoil= np.ones(npts) for i in range(nmid): myairfoil[i]= abs(airfoil[i,0]- xtemp) myi= np.argmin(myairfoil) return airfoil[myi,1]

def getlower(xtemp): myairfoil= np.ones(npts) for i in range(nmid, npts): myairfoil[i]= abs(airfoil[i,0]- xtemp) myi= np.argmin(myairfoil) return airfoil[myi,1]

90 Chapter 9. Airfoil Optimization MACH-Aero Documentation

9.2.3 FFD Box Creation

The FFD box can now be set up. nffd= 10

FFDbox= np.zeros((nffd,2,2,3))

xslice= np.zeros(nffd) yupper= np.zeros(nffd) ylower= np.zeros(nffd)

xmargin= 0.001 ymargin1= 0.02 ymargin2= 0.005

for i in range(nffd): xtemp=i* 1.0/ (nffd- 1.0) xslice[i]=-1.0* xmargin+(1+ 2.0* xmargin)* xtemp ymargin= ymargin1+ (ymargin2- ymargin1)* xslice[i] yupper[i]= getupper(xslice[i])+ ymargin ylower[i]= getlower(xslice[i])- ymargin

nffd signifies the number of chordwise points. An empty FFD box is created. xmargin and ymargin specify the closest distance from the airfoil to place the FFD box. xslice, yupper, and ylower store the x- and y- coordinates of the control points for each slice along the chord, taking into account the margins from the airfoil. #X FFDbox[:,0,0,0]= xslice[:].copy() FFDbox[:,1,0,0]= xslice[:].copy() #Y # lower FFDbox[:,0,0,1]= ylower[:].copy() # upper FFDbox[:,1,0,1]= yupper[:].copy() # copy FFDbox[:, :,1, :]= FFDbox[:, :,0, :].copy() #Z FFDbox[:, :,0,2]= 0.0 #Z FFDbox[:, :,1,2]= 1.0

The x- and y- coordinates are transferred to the FFDbox variable. Since the airfoil slices are the same along the z- direction, the x- and y- coordinates are copied over. The z-coordinates are updated to 0 and 1.

9.2. Geometric Parametrization 91 MACH-Aero Documentation

9.2.4 Writing to File f= open("ffd.xyz","w") f.write("1\n") f.write(str(nffd)+"22 \n") for ell in range(3): for k in range(2): for j in range(2): for i in range(nffd): f.write("%.15f "% (FFDbox[i, j, k, ell])) f.write("\n") f.close()

9.2.5 Run it yourself!

You can now run the python file with the command: $ python genFFD.py The above script writes the FFD coordinates to a PLOT3D .xyz file, which will be used for optimization.

9.3 Single Point Optimization

9.3.1 Introduction

We will now proceed to optimizing a NACA0012 airfoil for a given set of flow conditions. It is very similar to awing optimization. The optimization problem is defined as: minimize

퐶퐷 with respect to 10 shape variables subject to

퐶퐿 = 0.5

The shape variables are controlled by the FFD points specified in the FFD file.

9.3.2 Files

Navigate to the directory airfoilopt/singlepoint in your tutorial folder. Copy the FFD file, ffd.xyz, and the CGNS mesh file, n0012.cgns, generated previously, into the directory: $ cp ../mesh/n0012.cgns . $ cp ../ffd/ffd.xyz . Create the following empty runscript in the current directory: • airfoil_opt.py

92 Chapter 9. Airfoil Optimization MACH-Aero Documentation

9.3.3 Dissecting the aerodynamic optimization script

Open the file airfoil_opt.py in your favorite text editor. Then copy the code from each of the following sections into this file.

Import libraries import os import numpy as np import argparse import ast from mpi4py import MPI from baseclasses import AeroProblem from adflow import ADFLOW from pygeo import DVGeometry, DVConstraints from pyoptsparse import Optimization, OPT from idwarp import USMesh from multipoint import multiPointSparse

Adding command line arguments

This is a convenience feature that allows the user to pass in command line arguments to the script. Four options are provided: • Output directory • Optimizer to be used • Grid file to be used • Optimizer options

# Use Python's built-in Argument parser to get commandline options parser= argparse.ArgumentParser() parser.add_argument("--output", type=str, default="output") parser.add_argument("--opt", type=str, default="SLSQP", choices=["SLSQP","SNOPT"]) parser.add_argument("--gridFile", type=str, default="n0012.cgns") parser.add_argument("--optOptions", type=ast.literal_eval, default={}, help="additional␣

˓→optimizer options to be added") args= parser.parse_args()

Specifying parameters for the optimization

Several conditions for the optimization are specified at the beginning of the script. These include the coefficient oflift constraint value, Mach number, and altitude to indicate flow conditions. # cL constraint mycl= 0.5 # angle of attack alpha= 1.5 # mach number (continues on next page)

9.3. Single Point Optimization 93 MACH-Aero Documentation

(continued from previous page) mach= 0.75 # cruising altitude alt= 10000

The angle of attack serves as the initial value for the optimization and should not affect the optimized result.

Creating processor sets

Allocating sets of processors for different analyses can be helpful for multiple design points, but this is a single-point optimization, so only one point is added. MP= multiPointSparse(MPI.COMM_WORLD) MP.addProcessorSet("cruise", nMembers=1, memberSizes=MPI.COMM_WORLD.size) comm, setComm, setFlags, groupFlags, ptID=MP.createCommunicators() if not os.path.exists(args.output): if comm.rank ==0: os.mkdir(args.output)

ADflow set-up

The ADflow set-up looks similar to the aerodynamic analysis script. aeroOptions={ # Common Parameters "gridFile": args.gridFile, "outputDirectory": args.output, # Physics Parameters "equationType":"RANS", "smoother":"DADI", "MGCycle":"sg", "nCycles": 20000, "monitorvariables":["resrho","cl","cd","cmz","yplus"], "useNKSolver": True, "useanksolver": True, "nsubiterturb": 10, "liftIndex":2, "infchangecorrection": True, # Convergence Parameters "L2Convergence": 1e-15, "L2ConvergenceCoarse": 1e-4, # Adjoint Parameters "adjointSolver":"GMRES", "adjointL2Convergence": 1e-12, "ADPC": True, "adjointMaxIter": 5000, "adjointSubspaceSize": 400, "ILUFill":3, "ASMOverlap":3, "outerPreconIts":3, (continues on next page)

94 Chapter 9. Airfoil Optimization MACH-Aero Documentation

(continued from previous page) "NKSubSpaceSize": 400, "NKASMOverlap":4, "NKPCILUFill":4, "NKJacobianLag":5, "nkswitchtol": 1e-6, "nkouterpreconits":3, "NKInnerPreConIts":3, "writeSurfaceSolution": False, "writeVolumeSolution": False, "frozenTurbulence": False, "restartADjoint": False, }

# Create solver CFDSolver= ADFLOW(options=aeroOptions, comm=comm) CFDSolver.addLiftDistribution(200,"z")

As it is, the options specified above allow for a good convergence of NACA0012 airfoil analysis, but may not converge for other airfoils. Some useful options to adjust are: nCycles If the analysis doesn’t converge, this can be increased. nkswitchtol If the analysis stops converging during NK (Newton-Krylov), this might mean that it is still outside of the radius of convergence of the NK method. The parameter should then be lowered. NKSubSpaceSize Decreasing this parameter will decrease memory usage when in the NK range. Only change this if there are memory issues when dealing with larger meshes. writeSurfaceSolution and writeVolumeSolution If you want to view the surface or volume solutions at the end of each analysis, these parameters can be set to True. We also add a single lift distribution with 200 sampling points using addLiftDistribution, meaning that these values are written to a text file after every iteration. Similarly, addSlices writes the airfoil coordinates of the specified slice to a text file.

Set the AeroProblem

We add angle of attack as a design variable and set up the AeroProblem using given flow conditions. ap= AeroProblem(name="fc", alpha=alpha, mach=mach, altitude=alt, areaRef=1.0,␣

˓→chordRef=1.0, evalFuncs=["cl","cd"]) # Add angle of attack variable ap.addDV("alpha", value=alpha, lower=0, upper=10.0, scale=1.0)

9.3. Single Point Optimization 95 MACH-Aero Documentation

Geometric parametrization

The set-up for DVGeometry is simpler for an airfoil since it doesn’t involve span-wise variables such as twist, dihedral, or taper. # Create DVGeometry object FFDFile="ffd.xyz"

DVGeo= DVGeometry(FFDFile) DVGeo.addLocalDV("shape", lower=-0.05, upper=0.05, axis="y", scale=1.0) span= 1.0 pos= np.array([0.5])* span CFDSolver.addSlices("z", pos, sliceType="absolute")

# Add DVGeo object to CFD solver CFDSolver.setDVGeo(DVGeo)

The local design variable shape is added.

Geometric constraints

Note: This section is also the same as the corresponding section in aircraft optimization. We can set up constraints on the geometry with the DVConstraints class, also found in the pyGeo module. There are several built-in constraint functions within the DVConstraints class, including thickness, surface area, volume, location, and general linear constraints. The majority of the constraints are defined based on a triangulated surface representation of the wing obtained from ADflow. The volume and thickness constraints are set up by creating a 2D grid of points which is projected through the planform of the wing. For the volume constraint, the 2D grid is transformed into a 3D grid bounded by the surface of the wing. The volume is computed by adding up the volumes of the cells that make up the 3D grid. For the thickness constraints, the nodes of the 2D grid are projected to the upper and lower surface of the wing. The thickness for a given node is the difference between its upper and lower projections. The LeTe constraints (short for Leading edge/Trailing edge) are linear constraints based on the FFD control points. When we have both twist and local shape variables, we want to prevent the local shape variables from creating a shearing twist. This is done by constraining that the upper and lower nodes on the leading and trailing edges must move in opposite directions.

DVCon= DVConstraints() DVCon.setDVGeo(DVGeo)

# Only ADflow has the getTriangulatedSurface Function DVCon.setSurface(CFDSolver.getTriangulatedMeshSurface())

# Le/Te constraints lIndex= DVGeo.getLocalIndex(0) indSetA=[] indSetB=[] for k in range(0,1): indSetA.append(lIndex[0,0, k]) # all DV for upper and lower should be same but␣

˓→different sign (continues on next page)

96 Chapter 9. Airfoil Optimization MACH-Aero Documentation

(continued from previous page) indSetB.append(lIndex[0,1, k]) for k in range(0,1): indSetA.append(lIndex[-1,0, k]) indSetB.append(lIndex[-1,1, k]) DVCon.addLeTeConstraints(0, indSetA=indSetA, indSetB=indSetB)

# DV should be same along spanwise lIndex= DVGeo.getLocalIndex(0) indSetA=[] indSetB=[] for i in range(lIndex.shape[0]): indSetA.append(lIndex[i,0,0]) indSetB.append(lIndex[i,0,1]) for i in range(lIndex.shape[0]): indSetA.append(lIndex[i,1,0]) indSetB.append(lIndex[i,1,1]) DVCon.addLinearConstraintsShape(indSetA, indSetB, factorA=1.0, factorB=-1.0, lower=0,␣

˓→upper=0) le= 0.0001 leList= [[le,0, le], [le,0, 1.0- le]] teList=[[1.0- le,0, le], [1.0- le,0, 1.0- le]]

DVCon.addVolumeConstraint(leList, teList,2, 100, lower=0.064837137176294343, upper=0.07,

˓→ scaled=False) DVCon.addThicknessConstraints2D(leList, teList,2, 100, lower=0.1, upper=3.0) if comm.rank ==0: fileName= os.path.join(args.output,"constraints.dat") DVCon.writeTecplot(fileName)

The parameters lower and upper are thickness or volume bounds relative to the original airfoil. For example, the lower bound of 0.1 in the thickness constraint means that the optimized airfoil can be no thinner than 10% of the original airfoil.

Mesh warping set-up meshOptions={"gridFile": args.gridFile} mesh= USMesh(options=meshOptions, comm=comm) CFDSolver.setMesh(mesh)

9.3. Single Point Optimization 97 MACH-Aero Documentation

Optimization callback functions

Note: This section is also the same as the corresponding section in aircraft optimization. First we must set up a callback function and a sensitivity function for each processor set. In this case cruiseFuncs and cruiseFuncsSens belong to the cruise processor set. Then we need to set up an objCon function, which is used to create abstract functions of other functions. This should be similar for all single-point optimizations. def cruiseFuncs(x): if MPI.COMM_WORLD.rank ==0: print(x) # Set design vars DVGeo.setDesignVars(x) ap.setDesignVars(x) # Run CFD CFDSolver(ap) # Evaluate functions funcs={} DVCon.evalFunctions(funcs) CFDSolver.evalFunctions(ap, funcs) CFDSolver.checkSolutionFailure(ap, funcs) if MPI.COMM_WORLD.rank ==0: print(funcs) return funcs def cruiseFuncsSens(x, funcs): funcsSens={} DVCon.evalFunctionsSens(funcsSens) CFDSolver.evalFunctionsSens(ap, funcsSens) CFDSolver.checkAdjointFailure(ap, funcsSens) if MPI.COMM_WORLD.rank ==0: print(funcsSens) return funcsSens def objCon(funcs, printOK): # Assemble the objective and any additional constraints: funcs["obj"]= funcs[ap["cd"]] funcs["cl_con_"+ ap.name]= funcs[ap["cl"]]- mycl if printOK: print("funcs in obj:", funcs) return funcs

98 Chapter 9. Airfoil Optimization MACH-Aero Documentation

cruiseFuncs

The input to cruiseFuncs is the dictionary of design variables. First, we pass this dictionary to DVGeometry and AeroProblem to set their respective design variables. Then we solve the flow solution given by the AeroProblem with ADflow. Finally, we fill the funcs dictionary with the function values computed by DVConstraints and ADflow. The call checkSolutionFailure checks ADflow to see if there was a failure in the solution (could be due tonegative volumes or something more sinister). If there was a failure it changes the fail flag in funcs to True. The funcs dictionary is the required return.

cruiseFuncsSens

The inputs to cruiseFuncsSens are the design variable and function dictionaries. Inside cruiseFuncsSens we populate the funcsSens dictionary with the derivatives of each of the functions in cruiseFuncs with respect to all of its dependence variables.

objCon

The main input to the objCon callback function is the dictionary of functions (which is a compilation of all the funcs dictionaries from each of the design points). Inside objCon, the user can define functionals (or functions of other functions).

Optimization problem

Note: This section is also the same as the corresponding section in aircraft optimization. To set up the optimization problem, we incorporate multiPointSparse. When creating the instance of the Optimization problem, MP.obj is given as the objective function. multiPointSparse will take care of calling both cruiseFuncs and objCon to provide the full funcs dictionary to pyOptSparse. Both AeroProblem and DVGeometry have built-in functions to add all of their respective design variables to the opti- mization problem. DVConstraints also has a built-in function to add all constraints to the optimization problem. The user must manually add any constraints that were defined in objCon. Finally, we need to tell multiPointSparse which callback functions belong to which processor set. We also need to provide it with the objCon and the optProb. The call optProb.printSparsity() prints out the constraint Jacobian at the beginning of the optimization. # Create optimization problem optProb= Optimization("opt", MP.obj, comm=MPI.COMM_WORLD)

# Add objective optProb.addObj("obj", scale=1e4)

# Add variables from the AeroProblem ap.addVariablesPyOpt(optProb)

# Add DVGeo variables DVGeo.addVariablesPyOpt(optProb)

# Add constraints DVCon.addConstraintsPyOpt(optProb) optProb.addCon("cl_con_"+ ap.name, lower=0.0, upper=0.0, scale=1.0) (continues on next page)

9.3. Single Point Optimization 99 MACH-Aero Documentation

(continued from previous page)

# The MP object needs the 'obj' and 'sens' function for each proc set, # the optimization problem and what the objcon function is: MP.setProcSetObjFunc("cruise", cruiseFuncs) MP.setProcSetSensFunc("cruise", cruiseFuncsSens) MP.setObjCon(objCon) MP.setOptProb(optProb) optProb.printSparsity()

Run optimization

# Set up optimizer if args.opt =="SLSQP": optOptions={"IFILE": os.path.join(args.output,"SLSQP.out")} elif args.opt =="SNOPT": optOptions={ "Major feasibility tolerance": 1e-4, "Major optimality tolerance": 1e-4, "Difference interval": 1e-3, "Hessian full memory": None, "Function precision": 1e-8, "Print file": os.path.join(args.output,"SNOPT_print.out"), "Summary file": os.path.join(args.output,"SNOPT_summary.out"), } optOptions.update(args.optOptions) opt= OPT(args.opt, options=optOptions)

# Run Optimization sol= opt(optProb, MP.sens, storeHistory=os.path.join(args.output,"opt.hst")) if MPI.COMM_WORLD.rank ==0: print(sol)

9.3.4 Run it yourself!

To run the script, use the mpirun and place the total number of processors after the -np argument $ mkdir output $ mpirun -np 4 python airfoil_opt.py | tee output.txt The command tee saves the text outputs of the optimization to the specified text file. You can follow the progress of the optimization using OptView, as explained in pyOptSparse.

100 Chapter 9. Airfoil Optimization MACH-Aero Documentation

9.4 Multipoint Optimization

9.4.1 Introduction

Optimization does not have to be limited to a single flight condition. This section goes through the same optimization as the single point case, except with one more flight condition. Instead of rewriting the code from scratch, the differences in code will be pointed out.

9.4.2 Files

Navigate to the directory airfoilopt/multipoint in your tutorial folder. Copy the FFD file, ffd.xyz, and the CGNS mesh file, n0012.cgns, generated previously, into the directory: $ cp ../mesh/n0012.cgns . $ cp ../ffd/ffd.xyz . Copy the singlepoint script from the previous section to a new file in this directory: $ cp ../singlepoint/airfoil_opt.py airfoil_multiopt.py

9.4.3 Highlighting the changes required in the multipoint optimization script

Open the file airfoil_multiopt.py in your favorite text editor. Change the following sections for multipoint opti- mization.

Specifying parameters for the optimization

For multipoint optimization, the parameters have to be specified in lists of the same size. # specify flight conditions and constraints mach=[0.4, 0.5] alt=[10000, 10000] alpha=[1,1] mycl=[0.5, 0.5] # number of points in multipoint optimization nFlowCases= len(mach) # assign number of processors nGroup=1 nProcPerGroup= MPI.COMM_WORLD.size

Set the AeroProblem

For more than one AeroProblem, a list needs to be created. Each AeroProblem is created with the respective optimiza- tion point and appended to the list. aeroProblems=[] for i in range(nFlowCases): ap= AeroProblem( name="fc%d"% i, alpha=alpha[i], (continues on next page)

9.4. Multipoint Optimization 101 MACH-Aero Documentation

(continued from previous page) mach=mach[i], altitude=alt[i], areaRef=1.0, chordRef=1.0, evalFuncs=["cl","cd"], ) # Add angle of attack variable ap.addDV("alpha", value=alpha[i], lower=0, upper=10.0, scale=1.0) aeroProblems.append(ap)

Optimization callback functions

The same for-loop needs to be added to the callback functions. The lines that require a call to the an AeroProblem is now put into a for-loop to iterate through all of them. def cruiseFuncs(x): if MPI.COMM_WORLD.rank ==0: print(x) # Set design vars DVGeo.setDesignVars(x) # Evaluate functions funcs={} DVCon.evalFunctions(funcs)

for i in range(nFlowCases): if i% nGroup == ptID: aeroProblems[i].setDesignVars(x) CFDSolver(aeroProblems[i]) CFDSolver.evalFunctions(aeroProblems[i], funcs) CFDSolver.checkSolutionFailure(aeroProblems[i], funcs) if MPI.COMM_WORLD.rank ==0: print(funcs) return funcs def cruiseFuncsSens(x, funcs): funcsSens={} DVCon.evalFunctionsSens(funcsSens) for i in range(nFlowCases): if i% nGroup == ptID: CFDSolver.evalFunctionsSens(aeroProblems[i], funcsSens) CFDSolver.checkAdjointFailure(aeroProblems[i], funcsSens) if MPI.COMM_WORLD.rank ==0: print(funcsSens) return funcsSens def objCon(funcs, printOK): # Assemble the objective and any additional constraints: funcs["obj"]= 0.0 for i in range(nFlowCases): (continues on next page)

102 Chapter 9. Airfoil Optimization MACH-Aero Documentation

(continued from previous page) ap= aeroProblems[i] funcs["obj"]+= funcs[ap["cd"]]/ nFlowCases funcs["cl_con_"+ ap.name]= funcs[ap["cl"]]- mycl[i] if printOK: print("funcs in obj:", funcs) return funcs

In the objCon function, the 푐퐿 constraint is also placed into the for-loop.

Optimization problem

Adding the constraints to the optimization problem requires adding them to each AeroProblem. # Create optimization problem optProb= Optimization("opt", MP.obj, comm=MPI.COMM_WORLD)

# Add objective optProb.addObj("obj", scale=1e4)

# Add variables from the AeroProblem ap.addVariablesPyOpt(optProb)

# Add DVGeo variables DVGeo.addVariablesPyOpt(optProb)

# Add constraints DVCon.addConstraintsPyOpt(optProb) for i in range(nFlowCases): ap= aeroProblems[i] optProb.addCon("cl_con_"+ ap.name, lower=0.0, upper=0.0, scale=1.0)

# The MP object needs the 'obj' and 'sens' function for each proc set, # the optimization problem and what the objcon function is: MP.setProcSetObjFunc("cruise", cruiseFuncs) MP.setProcSetSensFunc("cruise", cruiseFuncsSens) MP.setObjCon(objCon) MP.setOptProb(optProb) optProb.printSparsity()

9.4.4 Run it yourself!

The script can be run in the same way $ mkdir output $ mpirun -np 4 python airfoil_multiopt.py | tee output.txt

9.4. Multipoint Optimization 103 MACH-Aero Documentation

104 Chapter 9. Airfoil Optimization CHAPTER TEN

OVERSET MESH

In this part of the tutorial we will mainly generate an overset mesh for ADflow. Here is a list of commonly used steps to create an overset mesh with the usual workflow in the MDO Lab: 1. Create the geometry using pyGeo, if necessary. 2. Generate surface meshes for the main components in ICEM. 3. Extrude surface meshes into volume meshes using pyHyp. 4. Generate a background mesh using cgnsUtilities. 5. Merge blocks in a single file using cgnsUtilities. 6. Check connectivities using ADflow. We will take a slightly different approach in this tutorial by using OpenVSP to create the geometry and Pointwise to generate the surface meshes. The Aerodynamic Analysis tutorial covers how to use pyGeo and ICEM. We will do this process on the ONERA M6 Wing, which is a common example to validate flow solvers in the transonic regime. Note that an overset mesh might not be needed for a simple geometry like this; however, we will use this geometry as an example to demonstrate the overset mesh surface overlap. Here are a few of the items we will cover in the following pages: • Some theory on overset meshes • General tips and troubleshooting for overset meshes • Create a wing geometry using OpenVSP • Surface mesh generation with Pointwise • Volume mesh extrusion with pyHyp • ADflow analysis for overset meshes

10.1 Overset Theory

10.1.1 Overset Mesh

ADflow uses structured meshes. For simple geometries, a valid structured mesh can be obtained by the multiblock structured mesh approach. However, it can be difficult or even impossible to generate a high quality multiblock mesh for complex geometries. To mitigate this problem, the overset approach (also called chimera-patch) was implemented in ADflow. Overset meshes can be viewed as an unstructured network of overlapping structured meshes. Instead of having one big structured mesh,

105 MACH-Aero Documentation the fluid domain is split up into separate, overlapping meshes. Information is interpolated among overlapping meshes at every solver iteration.

Fig. 1: A simple 2-D overset mesh. The nearfield mesh of the airfoil is red, and the background mesh isblack.

The boundary conditions for this example is set as follows: Cells assume different tasks in an overset mesh: • Compute cells: Active cells that are relevant to the solution as they represent the volume. The PDEs are enforced on these cells. • Blanked cells: Inactive cells that are inside bodies or overlapped by better quality cells. • Interpolated cells (Receivers): Cells that inherit state variables from donor cells belonging to other overlapping meshes. The compute cells in an overset mesh for a more complicated configuration look like this: More about the overset implementation in ADflow can be found in this paper.

Note: Because the solver has to interpolate in the overlapping region, the numerical solution will locally not be as accurate. It is recommended to avoid such overlapping near critical regions of the flowfield, like the wing tip.

106 Chapter 10. Overset Mesh MACH-Aero Documentation

Fig. 2: The boundary-condition information for the simple overset example.

10.1. Overset Theory 107 MACH-Aero Documentation

Fig. 3: Example of a farfield mesh embedding multiple nearfield meshes for the CFD mesh of NASA’s STARC-ABL concept.

10.1.2 Implicit Hole Cutting (IHC)

ADflow uses implicit hole cutting (IHC) to automatically assign overset connectivities. IHC is based on the assumption that cells are finer near walls. IHC preserves smaller cells and blanks or interpolates larger ones. The general theory behind IHC can be found in this paper. In this section, we focus on the IHC implementation in ADflow. The iBlank array indicates the function of each cell. ADflow saves this array in the volume or surface CGNS filesif you add blank to the surfaceVariables or volumeVariables respectively. The complete list of iBlank values in ADflow is: • 1: Compute • 0: Blanked • -1: Interpolated • -2: Flooded • -3: Flood seed • -4: Explicitly blanked (using cutCallBack) • -5: Orphan (flagged for debugging purposes only) In the figure above, the red cells represent the compute cells in each mesh. The green cells are the interpolated cells, which bring in information from the overlapping compute cells. The yellow cells represent the blanked cells. These have no function in the flow solution but play an important role in the flooding process.

108 Chapter 10. Overset Mesh MACH-Aero Documentation

Fig. 4: The original mesh (left) and only the compute cells after IHC (right).

Fig. 5: The resulting iBlank values after the IHC process for the background and nearfield meshes.

10.1. Overset Theory 109 MACH-Aero Documentation

Flooding

Flooding is the process used to determine which side of a wall should not be included in the flow solution. This is usually the interior of a body such as a wing or aircraft. Flooding starts at the flood seeds, which are the dark blue cells in the figure above. A cell must satisfy two requirements to be designated as a flood seed. First, thecellmust intersect a wall on an overlapping mesh. Second, the cell must be farther than nearWallDist from any wall in its own mesh. In the example, the flood seeds are cells in the background mesh that overlap with the walls of the circleinthe nearfield mesh. The light blue cells are the flooded cells. Compute cells that are next to a flood seedorafloodedcell are converted to flooded cells until the flooding is stopped by at least two layers of blanked or interpolated cells.Inthe example, the flooded region is limited to the inside of the circle. If we did not have enough resolution intheblanked and interpolated cells, the flood seeds would flood the rest of the mesh and the IHC would fail.

Orphan cells

In the figure below, the center cell is marked with red, and all of the other cubes represent an exploded viewofthe computational stencil used in ADflow for RANS simulations. The stencil for all compute cells (excluding cellsat physical boundaries) should include only other compute or interpolated cells. If this is not satisfied, the center cell is tagged as an orphan cell. A valid mesh has no orphan cells.

Fig. 6: The computational stencil used in ADflow for a second-order accurate finite-volume formulation for RANS equations. The center compute cell (marked with red) needs to access the state in all of the cells included in the figure to compute the residuals.

10.1.3 Zipper Mesh

As seen in the STARC-ABL figure on this page, there can be multiple nearfield meshes that overlap onasurface. This makes it difficult to correctly integrate the forces and moments acting there. For that reason, ADflow useszipper meshes to provide a watertight surface. More about zipper meshes can be found this paper.

110 Chapter 10. Overset Mesh MACH-Aero Documentation

Fig. 7: Overlapped meshes (left), Removed overlaps (mid), Triangulated gaps (right)

10.1.4 Collar Mesh

Collar meshes outline the intersection between two component meshes. The collar mesh should be finer than the overlapping component meshes. This ensures that the collar cells are selected during IHC and there are no gaps at the intersection.

Fig. 8: The collar mesh at the wing-strut intersection of a strut-braced wing configuration.

We can also use a half-collar to reduce the number of overset blocks. In the following example, the half-collar on the fuselage belongs to the tail mesh. The half-collar and the rest of the tail mesh share the intersection line.

10.1. Overset Theory 111 MACH-Aero Documentation

Fig. 9: An example of a half-collar at the fuselage-horizontal tail region.

10.2 Tips and Troubleshooting

10.2.1 Tips for Getting a Valid Overset Mesh

Tip #1

Make sure there is sufficient overlap between meshes.

Tip #2

Match cell sizes of the overlapped meshes, especially near boundaries.

Tip #3

Match the growth ratios of the mesh extrusion. • Use similar values of initial cell height for all meshes (s0 option in pyHyp) • Make sure that all meshes have similar growth ratios during the pyHyp extrusion. Variations within 0.05 are acceptable. • If you want to prioritize one mesh, such as a collar mesh, use a slightly smaller value for s0 and aim for a slightly smaller growth ratio.

112 Chapter 10. Overset Mesh MACH-Aero Documentation

Fig. 10: Overlapping is needed between meshes.

Fig. 11: Left: Not recommended. May give a valid hole cutting with additional effort. Right: Better transition. Easier to find interpolation stencils.

10.2. Tips and Troubleshooting 113 MACH-Aero Documentation

Fig. 12: Output from pyHypMulti showing how to find the grid ratio value.

10.2.2 Troubleshooting an Overset Mesh

The ADflow output might help you to troubleshoot an overset mesh. Here is what the output from a validIHCprocess looks like: The following points indicate a problem: • Several flooding iterations • Small number of compute cells • Orphan cells are present

Flood troubleshooting

If the mesh is flooding (too many flooding iterations, a high number of flooded cells), we need to first preventthisto get a valid hole cutting. For this, we need to check leaks in the flooding process: 1. Set the ADflow option: "nRefine": 1. This stops the IHC algorithm after one iteration. You will get a warning, but this is fine. We just want to get an intermediate output for visualization. You can also modifythe nFloodIter option to control how many flooding iterations are performed. For example, if ADflow segfaults in the first overset iteration because the whole mesh floods, then you can stop the flooding iterations early bysetting nFloodIter to 1 or 2. A value of 1 will just determine the flood seeds, a value of 2 will do a first pass ofthe flooding process. 2. Set ADflow option: "useZipperMesh": False. This skips the zipper mesh generation, which may crash if the hole cutting does not work. 3. Run the overset check file: ihc_check.py. (This can be found under the tutorial/overset/mesh directory in this repo.) 4. Open the output volume file in Tecplot. 5. Use the blanking option to show only iBlank = -3 (flood seeds) and iBlank = -2 (flooded cells).

114 Chapter 10. Overset Mesh MACH-Aero Documentation

Fig. 13: ADflow output with a successful hole cutting. This results in a valid overset mesh.

10.2. Tips and Troubleshooting 115 MACH-Aero Documentation

Fig. 14: Bad IHC output.

116 Chapter 10. Overset Mesh MACH-Aero Documentation

6. Check which CGNS blocks are fully flooded. Identify the flood cells that connect cells inside the body tocells outside the body. This is where the leak occurs. The following points might help to fix your flooding issue. Check them first. Check if the meshes have similar growth ratios in the pyHyp extrusion. Flooding is usually caused by cells that grow too fast off a wall. A mesh with a high growth ratio may cause the flooding of the other overlapped meshes because the other meshes will not create a layer of interpolated cells to contain the flood. Increase the nearWallDist option in ADflow. This option controls how compute cells are preserved near walls. We usually use 0.01 for a full-scale aircraft mesh defined in meters. Increasing nearWallDist will reduce the number of flood seeds. Once you have a valid hole cutting, decrease nearwalldist to the minimum possible value. Check for sufficient overlap on the surface and in the volume. The overlap should have at least 5 cells from each mesh. Either extend the nearfield meshes or refine the background mesh until you have a 5 cell overlap inthe off-wall direction.

Warning: Even if the IHC is valid, the flooding may not behave as expected. Thin geometries at component intersections can cause problems with flooding and result in flow through solid surfaces. Increase the meshdensity at the intersection to avoid this.

Troubleshooting orphan cells

ADflow outputs the CGNS block ID and the i, j, k position of the orphan cells. The k values (4th column)represent the position in the off-wall direction and may point to the issue.

Fig. 15: Output from a mesh with orphan cells.

Orphans with high k There is a lack of volume overlap and some interpolated cells cannot find donors. Possible solutions are increasing the mesh extrusion distance (marchDist option in pyHyp) or adding more layers to the mesh extrusion process (N option in pyHyp). You can also refine the background mesh. Orphans with small k nearWallDist is too large and there are compute cells on the wrong side of the surface defined by overlapping meshes. Try reducing nearWallDist.

10.3 Geometry Generation

10.3.1 Introduction

OpenVSP is a parametric aircraft geometry tool. It allows the user to quickly generate aircraft geometries. We will use it to generate the ONERA M6 wing. This is not a full blown tutorial, more a walk through. If you want to learn more about it, you can go to their tutorial videos

10.3. Geometry Generation 117 MACH-Aero Documentation

10.3.2 Files

Navigate to the directory overset/geo in your tutorial folder. Copy the file profile_m6.dat from the tutorial directory: $ cp ../../../tutorial/overset/geo/profile_m6.dat .

10.3.3 Geometry Overview

Before we start, we should familiarize ourself with the geometry we are going to create. The real ONERA M6 experi- ment looked like this:

Fig. 16: ONERA M6 experiment [O1].

As we are not the first to simulate this wing, we can grab the geometry data and the airfoil from[O2]. Here is a short summary:

Airfoil profile_M6 (this can also be found in the tutorial folder) Root Chord 0.8105 m Tip chord 0.4559 m Semispan 1.1963 m Leading Edge Sweep 30°

10.3.4 Geometry Generation

OpenVSP Window

If you open OpenVSP the first time, it should look like that: Lets start the geometry generation:

118 Chapter 10. Overset Mesh MACH-Aero Documentation

Fig. 17: OpenVSP Overview.

Add a new Wing

1. Choose WING 2. Click Add 3. A new Window Wing: WingGeom pops up

Fig. 18: Add a new wing to OpenVSP.

We notice a new geometry in the Main View. This is the wing we just added. It also shows up as WingGeom in the Geom Browser. To control the view, use the following key- and mouse combinations: zoom Press the middle mouse button and move your mouse up and down. rotate Press the left mouse button and move your mouse. move Press the right mouse button and move you mouse.

10.3. Geometry Generation 119 MACH-Aero Documentation

Manipulate the wing geometry

Move your view, so you can take a look the whole wing. The first thing we notice, it is a ‘full’ wing, but we needonly half of it. To change this, do the following: 1. Click on XForm 2. Uncheck XZ in the Symmetry area

Fig. 19: Disable XZ Symmetry.

Now we change the wing geometry. OpenVSP has no units, but we want to create the mesh in meters and thus choose our unit size to be one meter. 1. Click on Sect 2. Change the values to the values listed in the table above

Fig. 20: Adjust the wing geometry.

To make the meshing process easier, we will round the tip: 1. Click on Plan 2. Choose Round for the Tip Cap Type

120 Chapter 10. Overset Mesh MACH-Aero Documentation

Fig. 21: Adjust the wing geometry.

Change the airfoil

Now, the wing geometry is as it should be, but we still have to change the airfoil: 1. Click on Airfoil 2. Select AF_FILE for Choose Type 3. Open the file profile_m6.dat in the tutorial\overset\geo folder 4. Click on the right single arrow to select the Tip airfoil 5. repeat the process from 2 to 3

Fig. 22: Change the airfoil.

10.3. Geometry Generation 121 MACH-Aero Documentation

Export the geometry

Now we can export the geometry to read it into our meshing software. It might also be a good idea to save it first. 1. Click on File -> Export 2. Click Untrimmed IGES (.igs) 3. Choose the proper unit. In this case it should be meter 4. Click OK and save it

Fig. 23: Export the geometry.

10.4 Surface Mesh

10.4.1 Introduction

Now that we have a geometry, we can start meshing it. We are using Pointwise to generate the surface mesh. This is not a full blown tutorial, more a walk through. If you want to learn more about it, their Youtube channel is highly recommended. You do not have to use Pointwise to generate an overset mesh. ICEM or an other meshing software would work as well.

10.4.2 Files

Navigate to the directory overset/mesh in your tutorial folder. Either use the previously generated .igs file or copy it from the tutorial folder. $ cp ../../../tutorial/overset/geo/onera_m6.igs . It is possible to script Pointwise. In order to use it, we have to download the script first. You can either download it here or copy it from the tutorial folder. $ cp ../../../tutorial/overset/mesh/Semicircle.glf .

122 Chapter 10. Overset Mesh MACH-Aero Documentation

10.4.3 Meshing strategy

Before we start meshing, we have to know how many meshes we create and where they overlap. For this tutorial, 3 different meshes are proposed: near_wing, near_tip and far. The following picture should give an overview: Now we should estimate the cell count of the mesh. For the purpose of a grid convergence study (GCS) and debugging it makes sense to have differently refined meshes. To limit the amount of work, we will create the finest meshand coarsen it multiple times. Usually, the finest mesh is called L0 (level 0) and should have approx 60M cells for this geometry. If every 2 cells are combined in each direction, we get a coarser mesh called L1. This usually goes to L2 for production and L3 for debugging purposes. Additionally, there could be an intermediate level starting at L0.5. It requires a different surface mesh that is sqrt(2) coarser than L0. In this tutorial, we will start at L1 (~8M cells) and end at L3 (~0.125M cells).

10.4.4 Mesh Generation

Pointwise overview

If you start Pointwise, it should look something like in the next picture. 1. Object, Layer and Default control 2. Solver information 3. Selection control 4. View control 5. Fast meshing controls You can control the main view with the following key- and mouse combinations: zoom Rotate your mouse wheel. The zoom centers around your mouse pointer. rotate Press ctrl and your right mouse button while moving your mouse. move Press shift and your right mouse button while moving your mouse.

Setup Pointwise

Before we actually begin meshing, we have to set some standard values and import our geometry. At first, we set some tolerances for Pointwise 1. Click on File -> Properties 2. Set Model Size to 1. (It is enough, if the order of magnitude is similar) 3. Set Node to 1e-6. The value of Connector should automatically jump to 1e-6 as well 4. OK Now we have to choose the proper solver. In my case it is CGNS with adf support. If you have compiled the MACH- Framework with hdf5 support, you can skip the last step. 1. Click CAE -> Select Solver 2. Make Sure CGNS is selected. 3. Click OK. 4. Click CAE -> Set Dimension -> 2D (That’s how surface meshes are called here) 5. Click CAE -> Set Solver Attributes (If you have hdf5 support, you can stop here)

10.4. Surface Mesh 123 MACH-Aero Documentation

Fig. 24: The three overset meshes.

124 Chapter 10. Overset Mesh MACH-Aero Documentation

Fig. 25: Pointwise Overview.

10.4. Surface Mesh 125 MACH-Aero Documentation

6. Select adf for CGNS File Type 7. Click Close Now we can import the .iges file we created in the previous tutorial. 1. Click File -> Import -> Database 2. Select your .iges File -> open 3. Make sure nothing but Units and From File is selected 4. Click OK 5. You will receive a warning that some entities could not be converted. Just ignore it and click YES After those steps, the window should look like this (you should probably save at this point):

Fig. 26: Pointwise after setup.

Few important Pointwise labels: Block This is a 3 dimensional Mesh Domain This is a 2 dimensional Mesh Connector A line constraining the extend of a Block or Domain

126 Chapter 10. Overset Mesh MACH-Aero Documentation

Database An imported geometry Spacing Constraint This controls how the nodes lay on a Connector. Further down the line, the Connector controls how the nodes lay in a Domain or Block

Prepare the Database

To make our live a bit easier in the coming mesh work, we first prepare the database a bit (take a look at the next picture to help guide you). 1. Select the whole database. Just draw a rectangle around it while your left mouse button is pressed 2. Click Wireframe -> Shaded 3. Click on Layers 4. Double click on Description and enter Geo

Fig. 27: Prepare the database #1.

Because we have two overlapping meshes (near_wing and near_tip), we have to cut the database at an appropriate place. This will indicate where the near_tip mesh will start. The near_wing mesh will go right to the tip of the wing. But because ADflow uses an Implicit Hole Cutting Scheme we only have to make sure, that the near_tip mesh is slightly smaller than the near_wing mesh. This will ensure, that the overlapping region is approximately where we

10.4. Surface Mesh 127 MACH-Aero Documentation cut the database. In this way we are certain, the solver does not have to interpolate in a critical region (like the wing tip). 1. Click on Create -> Planes 2. Choose Constant X, Y or Z 3. Select Y and enter a value of 0.9 4. Click OK (Your view should now look like detail A in the following picture) 5. Select only the upper, lower and trailing edge surface by drawing a rectangle with your left mouse button 6. Click Edit -> Trim by Surfaces 7. Select your freshly created plane (detail A) 8. Make sure Tolerance and Advanced is unselected 9. Click Imprint (Your geometry should now have a different color towards the tip) 10. Click OK

Fig. 28: Cut the database.

Now we are doing some cleaning up and delete some unneeded surfaces.

128 Chapter 10. Overset Mesh MACH-Aero Documentation

1. Rotate your view with pressing ctrl and your right mouse button while moving your mouse until you have a good view on the root surfaces. 2. Select the first root surface 3. Press ctrl while selecting the second root surface 4. Press del on your keyboard to delete them

Fig. 29: Delete the root surfaces.

Create the near_wing surface mesh

We create the mesh near_wing in a new layer to keep everything orderly. 1. Click Layers 2. Select Show Empty Layers 3. Click with your right mouse button on layer 10 -> Set Current 4. Double click with your left mouse button on the Description of layer 10 and enter near_wing 5. Unselect Show Empty Layers

10.4. Surface Mesh 129 MACH-Aero Documentation

Fig. 30: Create a new layer for near_wing.

130 Chapter 10. Overset Mesh MACH-Aero Documentation

Because we want to coarsen our mesh multiple times, it is important to think about how many nodes we should have on a connector (Apart from that, it is always good to be multi-grid-friendly). To calculate the number of nodes (푁) per connector, we use this formula:

푁 = 2푛푚 + 1

Where 푛 is the number of refinements + 1 and 푚 is an integer. For our chord-wise direction, we will use ‘’145” Nodes. To save some work, we will set it as default. 1. Click Defaults 2. Make sure Connector is checked 3. Select Dimension and enter 145 4. Select the upper and lower surface of the wing 5. Click Connectors on Database Entities 6. Click on Layers and uncheck the Geo layer 7. Select the two connectors in the middle of the wing (Detail A) and delete them. They showed up because we split the database 8. Select the 6 spanwise connectors (Detail B) 9. Click Edit -> Join When creating the connectors, we left out the TE. We did this because there were 2 surfaces from OpenVSP. It is less work for us, if we manually create two connectors. 1. Click Defaults 2. Select Dimension and enter 17 3. Click 2 Point Curves 4. Close the root trailing edge (make sure your pointer becomes a cross-hair before you click. This way you are sure the new connector lies on the closest point) 5. Close the tip trailing edge 6. Press OK Now we initialize the surface mesh. 1. Select everything 2. Click Assemble Domains 3. Select everything 4. Click on the small arrow pointing down next to Wireframe 5. Click on Hidden Line Now we size the LE (Leading Edge) and TE (Trailing Edge) connectors. 1. Click on All Masks On/Off 2. Click on `Connectors 3. Select the LE and TE Connectors by drawing a rectangle like it is shown 4. Click on the input field next to Dimension, enter 73 and hit enter

10.4. Surface Mesh 131 MACH-Aero Documentation

Fig. 31: Create the connectors for the near_wing mesh.

132 Chapter 10. Overset Mesh MACH-Aero Documentation

Fig. 32: Close the trailing edge.

10.4. Surface Mesh 133 MACH-Aero Documentation

Fig. 33: Initialize the near_wing mesh.

134 Chapter 10. Overset Mesh MACH-Aero Documentation

Fig. 34: Dimension the LE & TE connectors.

10.4. Surface Mesh 135 MACH-Aero Documentation

The surface mesh is now almost complete. We only have to distribute the nodes on it properly by changing the spacing. Usually all Points are distributed according to Tanh. But because we split up the database in the previous steps, we have to remove so called break point at that location.

Note: Break Points give you even more control to distribute your nodes on a connector.

1. Select the LE and TE connectors again. 2. Click on Grid -> Distribute 3. Click on Break Points 4. Click on Delete all Break Points 5. Click on OK

Fig. 35: Delete unneeded Break Points.

1. Click on All Masks On/Off 2. Click on Spacing Constraints 3. Select the 2 spacing constraints at the LE of the root (A) 4. Click the field next to Spacing and enter 0.0003. Then hit enter

136 Chapter 10. Overset Mesh MACH-Aero Documentation

5. Select the 2 spacing constraints at the TE root (B) 6. Apply 7.15e-5 for spacing 7. Select the 2 spacing constraints at the LE tip (C) 8. Apply 0.00016 for spacing 9. Select the 2 spacing constraints at the TE tip (D) 10. Apply 4e-5 for spacing 11. Select the 3 spacing constraints at the tip (E) 12. Apply 0.0025 for spacing 13. Select the 3 spacing constraints at the root (F) 14. Apply 0.04 as spacing

Fig. 36: Apply the proper spacing.

The mesh near_wing is now complete. We will export it later.

10.4. Surface Mesh 137 MACH-Aero Documentation

Create the near_tip surface mesh

Now we will create the near_tip mesh. Let’s start with creating a new layer and hide everything unnecessary. 1. Click on Layers 2. Check Show Empty Layers 3. Right click on Layer 20 -> Set Current 4. Double click the Description Field and enter near_tip 5. Uncheck Show Empty Layers 6. Check Layer 0 to make the database visible 7. Hide the mesh near_wing by un-checking layer 10 Now we will create the connectors. 1. Click on Defaults -> enter 201 for Dimension 2. Select everything from the tip to the cut we made earlier 3. Click Connectors on Database Entities 4. Click on Layers -> uncheck layer 0. Now, you should only see the connectors we created Let’s clean up the generated connectors at the tip TE. 1. Zoom into the tip TE 2. Select the 5 shown connectors (A) 3. Delete them 4. Select and delete the remaining pole (the point with a circle around) (B) 5. Select the 2 connectors that define the outer tip (C) 6. Click Edit -> Join 7. Select the newly joined connector (C) 8. Enter 65 For Dimension and hit enter 9. Click on Defaults and enter 65 for Dimension 10. Click on 2 Point Curves 11. Close the TE again (D) Next we clean up the root TE. 1. Select the 2 connectors that define the TE (A) 2. Delete them 3. Click on 2 Point Curves 4. Close the Tip again (B) The last thing to clean up is the tip LE. 1. Select the 3 shown connectors (A) 2. Click on the arrow pointing down next to show 3. Click Hide 4. Select and delete the remaining pole (B)

138 Chapter 10. Overset Mesh MACH-Aero Documentation

Fig. 37: Clean up the tip TE.

10.4. Surface Mesh 139 MACH-Aero Documentation

Fig. 38: Clean up the root TE.

140 Chapter 10. Overset Mesh MACH-Aero Documentation

5. Click on View -> Show Hidden 6. Select the 3 connectors (A) 7. Click on the arrow pointing down next to Hide 8. Click on Show

Fig. 39: Clean up the tip LE.

Now we will dimension the remaining connectors and space the nodes properly. 1. Select the 3 shown connectors (A) 2. Enter 97 for Dimension and hit enter 3. Click All Masks On/Off 4. Click Spacing Constraints 5. Select the 2 spacing constraints at the root LE (B) 6. Apply 0.0008 for spacing 7. Select the 2 spacing constraints at the tip LE (C) 8. Apply 0.0008 for spacing

10.4. Surface Mesh 141 MACH-Aero Documentation

9. Select the 2 spacing constraints at the root TE (D) 10. Apply 1.3e-5 as spacing 11. Select the 2 spacing constraints at the tip TE (E) 12. Apply 1.3e-5 as spacing 13. Select the 3 spacing constraints at the root (F) 14. Apply 0.01 as spacing 15. Select the 1 spacing constraint at the tip LE (G) 16. Apply 0.0005 as spacing 17. Select the 2 spacing constraints at the tip TE (H) 18. Apply 1.56e-5 as spacing

Fig. 40: Apply spacing constraints for the near_tip mesh.

Next, we split the connectors at the tip to allow a topology where we can achieve a decent quality mesh. 1. Select the tip top connector (A) 2. Click Edit -> Split

142 Chapter 10. Overset Mesh MACH-Aero Documentation

3. Make sure Advanced is checked 4. Enter 17 for IJK and hit enter 5. Click OK 6. Select the tip bottom connector (B) 7. Click Edit -> Split` 8. Enter 185 for IJK and hit enter 9. Click OK 10. Click on 2 Point Curves 11. Connect the 2 new points (A) to (B)

Fig. 41: Split the tip connectors.

Since our tip is rounded, we have to project the newly created connector on to our database. 1. Select the newly created connector (A) 2. Click on Edit -> Project 3. Click on Layers

10.4. Surface Mesh 143 MACH-Aero Documentation

4. Check layer 0 (Geo) 5. Click on Project 6. Make sure Target Database Selection is checked 7. Click Begin 8. Select the upper and lower tip surface (hold down ctrl) (B) 9. Click End 10. Click Project 11. Click OK

Fig. 42: Project the connector on to the database.

Now we actually start meshing. 1. Click on Layers 2. Uncheck layer 0 (Geo) 3. Select the newly created connector (A) 4. Click on the arrow pointing down next to Tanh Distribution

144 Chapter 10. Overset Mesh MACH-Aero Documentation

5. Click on Equal 6. Click Edit -> Split 7. Enter 17 for IJK and hit enter 8. Enter 49 for IJK and hit enter 9. Click OK 10. Click on Create -> Assemble Special -> Domain 11. Select 1 connector (B) 12. Click Next Edge 13. Select 2 connectors (C) 14. Click Next Edge 15. Click OK

Fig. 43: Assemble the mesh at the LE tip.

Next, we mesh the rest. 1. Select the 2 connectors that form the semi-circle (A)

10.4. Surface Mesh 145 MACH-Aero Documentation

2. Click Script -> Execute 3. Look for the script you just downloaded and open it. 4. Select all connectors 5. Click Assemble Domains

Fig. 44: Mesh the semi-circle at the TE.

The last step is to make sure, that the skewed elements at the tip are smoothed. As Assemble Domains didn’t work for the most outer mesh, we will delete this domain first, and create it manually again. 1. Select all domains 2. Click Hidden Line 3. Select the outer most domain and delete it (A) 4. Select all 9 connectors, that define the last remaining domain 5. Click Assemble Domain 6. Select the newly created domain and click Hidden Line 7. Select the 2 domains that define the tip (A & B) 8. Click Grid -> Solve

146 Chapter 10. Overset Mesh MACH-Aero Documentation

9. Click on Edge Attributes 10. Make sure Boundary Conditions is checked and set the Type to Floating 11. Click on Attributes 12. Make sure Surface Shape is checked and set Shape to Database 13. Click on Begin and make sure, the tip is selected (it should be) 14. Click on End 15. Make sure Solution Algorithm is checked and set Solver Engine to Successive Over Relaxation 16. Set Relaxation Factor to Nominal 17. Click on Solve 18. Enter 50 for Iterations and hit Run 19. Click OK

Fig. 45: Finish the near_tip mesh.

Lets check the quality of the created mesh. The most important metrics are Area Ratio and Equiangle Skewness. 1. Select all domains

10.4. Surface Mesh 147 MACH-Aero Documentation

2. Click Examine -> Area Ratio 3. Click on the Magnification Glass next to max 4. You see, the biggest Area Ratio is ~2.24 5. Click on Advanced 6. Make sure Histogram and Show Histogram are checked 7. As you see, the vast majority of cells has an Area Ratio of less than 1.25. This should be fine 8. Click on Examine 9. Choose Skewness Equiangle for Type 10. As you can see, the most skewed cell has a Skewness Equiangle of ~0.4. This is also fine 11. Click Close

Note: The lower max Area Ratio is, the easier it is to extrude a mesh with pyHyp. If it is more than 2, it can get tricky. Skewness Equiangle describes how skewed a cell is. It should be below 0.8

Fig. 46: Check the mesh quality.

148 Chapter 10. Overset Mesh MACH-Aero Documentation

10.4.5 Export all meshes for use in pyhyp

The last step is to export the mesh. For pyHyp it is important, that the normals look in the outwards direction. We will set the boundaries manually in pyHyp.

Note: As there has not been found an easy way to figure out which domain in Pointwise corresponds to which domain in pyHyp, it is recommended to orient them all the same way. Then apply the BC for all domains and run the pyHyp script. If an error pops up for one domain, the corresponding BC can be removed. This gets repeated until there are no errors left (This information is repeated on the next page where it probably makes more sense).

Lets start with orienting the near_tip mesh first. 1. Make sure only the layer near_tip is visible 2. Select all domains 3. Click Edit -> Orient 4. Select one domain (It does not matter which one) 5. Click I-J a few times until you are sure, the orange arrow is pointing outwards 6. Click Set Master 7. Select all domains 8. Click Align 9. Click OK Now we can export it. 1. Select all domains 2. Click File -> Export -> CAE 3. Set near_tip as Filename and save it somewhere 4. Make sure Data Precision and double is checked 5. You can uncheck the rest (It doesn’t really matter. But the files will be bigger if you leave it on) 6. Press OK Now lets do the same for the near_wing mesh. As we have a symmetry boundary condition, the orientation procedure is slightly more complicated. 1. Make sure only the layer near_wing is visible 2. Select all domains 3. Click Edit -> Orient 4. Select one domain (It doesn’t matter which one) 5. Click I-J until the orange arrow is pointing outwards 6. If the red arrow is not pointing towards the tip, click I and I-J until both conditions are satisfied 7. Click Set Master 8. Select all domains 9. Click Align 10. Make sure all red arrows point towards the tip (if this is not the case, select this domain and repeat step 6)

10.4. Surface Mesh 149 MACH-Aero Documentation

Fig. 47: Orient the near_tip mesh so all normals point outwards.

150 Chapter 10. Overset Mesh MACH-Aero Documentation

Fig. 48: Export the near_tip mesh.

10.4. Surface Mesh 151 MACH-Aero Documentation

11. Click OK Now you can export the mesh near_wing like you did in the previous step. Congratulations, you managed to create the surface mesh. On the next page, we will extrude it into a volume mesh.

10.5 Volume Mesh

10.5.1 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

10.5.2 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 .

10.5.3 pyHyp Script

Setup

First we have to import some stuff: from collections import OrderedDict from mpi4py import MPI from pyhyp import pyHypMulti 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()

152 Chapter 10. Overset Mesh MACH-Aero Documentation

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, wefirstdefine some common options: commonOptions={ # ------# Input Parameters # ------"unattachedEdgesAreSymmetry": False, "outerFaceBC":"overset", "autoConnect": True, "fileType":"CGNS", # ------# Grid Parameters # ------"N": nNearfield, "s0": s0/ fact, (continues on next page)

10.5. Volume Mesh 153 MACH-Aero Documentation

(continued from previous page) "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 weareonly 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

154 Chapter 10. Overset Mesh MACH-Aero Documentation

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 encloseall 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")

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

10.5. Volume Mesh 155 MACH-Aero Documentation

Generate the farfield

Now we can generate the farfield: farfield=" %s/far_%s.cgns"% (args.output_dir, args.level) combinedGrid.simpleOCart(dhStar, 40.0, nFarfield,"y",1, farfield)

These are the arguments for a simpleOCart: dhStar The spacing in the cartesian part 40. The extend of the farfield. Measured in the longest diagonal lenght in the cartesian part nFarfield How many layers the Ogrid part should have ‘y’ On which axis the symmetry lies 1 How many MultiGrid-Cicles are enforced during the generation farfield The outputfile name for the farfield

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))

10.5.4 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.

156 Chapter 10. Overset Mesh MACH-Aero Documentation

10.5.5 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, (continues on next page)

10.5. Volume Mesh 157 MACH-Aero Documentation

(continued from previous page) }

# 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)

10.6 CFD Analysis

10.6.1 Introduction

This part will help guide you through the analysis part. We will setup the run script and let ADflow compute the solution. From [O1] and [O3] we know the flow conditions:

AoA 3.06° Mach 0.8395 Reynolds Number 11.71e6 Reynolds Ref Length 0.646m Temperature 300° K

There is a convenience package for ADflow called adflow_util. It allows to plot the ADflow state variables live in the console and handles some annoying stuff like creating the output folder for ADflow automatically. It also makes it easy to sweep a variable, for example alpha. This utility will be used here, but the regular python API, that is detailed in other tutorials, would work as well. For simplicity, only the calculation on the L3 mesh is covered. The other meshes might require slightly different ADflow options.

10.6.2 Files

Navigate to the directory overset/analysis in your tutorial folder and create an empty file called run_adflow_L3. py. If you did not create the volume mesh on the previous page, you will also have to copy the mesh file: $ cp ../../../tutorial/overset/analysis/ONERA_M6_L3.cgns . If you want to use adflow_util download and install it: $ pip install git+https://github.com/DavidAnderegg/adflow_util.git

158 Chapter 10. Overset Mesh MACH-Aero Documentation

10.6.3 ADflow

Setup the Script

First we have to import adflow_util: from adflow_util import ADFLOW_UTIL

Then we define a variable for the level we want to use. This makes it easier to switch, if we wantto: level="L3" adflow_util takes 3 different dictionaries as input. One sets some adflow_util-specific options, one sets the bound- ary conditions, as AeroProblem would and the last one is the regular ADflow options dict. Lets set the adflow_util options first: options={"name":"ONERA_M6_ %s"% (level),"surfaceFamilyGroups":{"wall":["near_wing",

˓→ "near_tip"]}} name This sets the name for the analysis. It defines how the various output files are named. surfaceFamilyGroups This defines how the various surface families should be assembled. The key sets the family name and the array defines the various surfaces the family is made off. All adflow_util options can be found here. Now we define the AeroProblem options: aeroOptions={ "alpha": 3.06, "mach": 0.8395, "reynolds": 11.72e6, "reynoldsLength": 0.646, "T": 300, "xRef": 0.0, "areaRef": 0.75750, "chordRef": 0.646, "evalFuncs":["cl","cd","cmy","cdp","cdv"], }

Here we set the various flow parameters. It is exactly the same as you would setin baseclasses.AeroProblem. But we could, for example, set alpha as an array of variables. In that case, adflow_util would handle everything else for us. Now, let’s set the ADflow options: solverOptions={ # Common Parameters "gridFile":"ONERA_M6_ %s.cgns"% (level), "outputDirectory":"output", # Physics Parameters "equationType":"RANS", # RK "smoother":"Runge-Kutta", "rkreset": True, (continues on next page)

10.6. CFD Analysis 159 MACH-Aero Documentation

(continued from previous page) "nrkreset": 35, "CFL": 0.8, "MGCycle":"sg", "nsubiterturb":5, # ANK "useanksolver": True, "anklinresmax": 0.1, "anksecondordswitchtol": 1e-3, "ankcoupledswitchtol": 1e-5, # NK "useNKSolver": True, "nkswitchtol": 1e-6, # General "liftindex":3, "monitorvariables":["resrho","resturb","cl","cd","yplus"], "printIterations": True, "writeSurfaceSolution": True, "writeVolumeSolution": True, "outputsurfacefamily":"wall", "zippersurfacefamily":"wall", "surfacevariables":["cp","vx","vy","vz","blank"], "volumevariables":["resrho","rmach","blank"], "nCycles": 10000, "L2Convergence": 1e-12, }

Some things to note: outputsurfacefamily We choose wall which we defined earlier as consisting of near_wing and near_tip. This will write out only the wing as our surface solution. zippersurfacefamily This tells ADflow which surfaces it should use to construct the geometry on which the forces are integrated. surfacevariables & volumevariables Here it is very important to add blank. This way we know which cells we can hide in the postprocessor as the ‘blanked’ cells still show up in the solution.

Note: To only view computed cells, add a filter to your post-processor in a way, that only cells where blank is bigger than 0 are shown.

And lastly, we plug everything into adflow_util: au= ADFLOW_UTIL(aeroOptions, solverOptions, options) au.run()

160 Chapter 10. Overset Mesh MACH-Aero Documentation

Simply Run the Script

To run the script, proceed as usual: $ python run_adflow_L3.py If you want to run in parallel, start it with MPI: $ mpirun -np 4 python run_adflow_L3.py

Plot the Iterations in realtime

If you want to have a graphical representation of all the ADflow variables, adflow_util comes in handy as well. It has an additional package called adflow_plot. If you installed it using pip, you can simply start it this way: $ adflow_plot -i run_adflow_L3.py If you want to run in parallel: $ adflow_plot -i run_adflow_L3.py -np 4 This is simply an overlay, which starts the adflow script in the background and parses it’s stdout. At startup you will see the regular adflow-ouput. But as soon as the calculation starts, you’ll see aplotof resRho: At the bottom is a console where you can define which variables you want to see. As terminals usually havealow number of ‘pixels’, it is also possible to show only a limited number of iterations. Simply type help or h and hit Enter. You will get a list of all available commands. To quit, simply type q and confirm with y.

Output files

In addition to the expected volume and surface files from adflow, there will also be a file called ONERA_M6_L3.out. It is from adflow_util and looks like this: ONERA_M6_L3

Aero Options ------alpha 3.06 mach 0.8395 reynolds 11720000.0 reynoldsLength 0.646 T 300 xRef 0.0 areaRef 0.7575 chordRef 0.646 evalFuncs cl, cd, cmy, cdp, cdv ------

RESULTS cd cdp cdv cl cmy totalRes iterTot ------0.01879813 0.01326940 0.00552873 0.26064807-0.18639411 0.00011945 82

It gives us a nice summary of the input and output values. If we had a sweep variable defined, the multiple points would be listed here too.

10.6. CFD Analysis 161 MACH-Aero Documentation

Fig. 49: adflow_plot output.

162 Chapter 10. Overset Mesh MACH-Aero Documentation

10.6.4 Results

For validation purposes, ADflow was run with all meshes and the results were plotted against various different solvers from [O3]. As you can see, ADflow lies right in the middle:

Fig. 50: Grid Convergence of ADflow in comparison to various other solvers.

10.7 About this tutorial

This tutorial was written by David Anderegg and Anil Yildirim. David Anderegg is employed by ZHAW in Switzerland at the time this tutorial was published. Most of the ‘Overset Theory’ and ‘Tips and Troubleshooting’ sections is based on material from Ney Secco.

10.8 References

10.7. About this tutorial 163 MACH-Aero Documentation

164 Chapter 10. Overset Mesh CHAPTER ELEVEN

FREQUENTLY ASKED QUESTIONS

In this section, we have a list of frequently asked questions and answers.

11.1 How do I obtain the cell count in a mesh?

There are several ways to get the cell count information from a mesh file. The simplest way is to use the info command in CGNS Utilities, which will print this information to the terminal. $ cgns_utils info wing_vol.cgns The output should look like: Total Zones:9 Total Cells: 193536 Total Nodes: 217413 Wall Boundary Cells: 4032 Wall Boundary Nodes: 4437

Running ADflow with a mesh will also print the number of cells to the terminal. For example, after printing thefull options dictionary, ADflow will print: # # Grid level: 1, Total number of cells: 193536 #

Finally, users should note that for overset meshes, the number of “compute” cells will be different than number of total cells. In both methods above, the code will print the total number of cells present in the CGNS file. However, with overset grids, a portion of the cells will be “blanked”. During the overset hole cutting, ADflow will print some detailed information about this process. For example: Flood Iteration:1 Blanked 3357 Interior Cells. Flood Iteration:2 Blanked0 Interior Cells. +------+ | Compute Cells : 548237 | Fringe Cells : 61968 | Blanked Cells : 852 | Explicitly Blanked Cells:0 | Flooded Cells : 1650 | FloodSeed Cells : 5161 +------+ Total number of orphans: 8860 (continues on next page)

165 MACH-Aero Documentation

(continued from previous page) Flood Iteration:1 Blanked 7619 Interior Cells. Flood Iteration:2 Blanked0 Interior Cells. +------+ | Compute Cells : 548540 | Fringe Cells : 61665 | Blanked Cells : 44 | Explicitly Blanked Cells:0 | Flooded Cells : 2458 | FloodSeed Cells : 5161 +------+ Total number of orphans: 424 Flood Iteration:1 Blanked 7619 Interior Cells. Flood Iteration:2 Blanked0 Interior Cells. +------+ | Compute Cells : 548540 | Fringe Cells : 61709 | Blanked Cells :0 | Explicitly Blanked Cells:0 | Flooded Cells : 2458 | FloodSeed Cells : 5161 +------+ Total number of orphans:0 Flood Iteration:1 Blanked 7619 Interior Cells. Flood Iteration:2 Blanked0 Interior Cells. +------+ | Compute Cells : 548540 | Fringe Cells : 61709 | Blanked Cells :0 | Explicitly Blanked Cells:0 | Flooded Cells : 2458 | FloodSeed Cells : 5161 +------+ Total number of orphans:0 +------+ | Compute Cells : 548540 | Fringe Cells : 46574 | Blanked Cells : 15135 | Explicitly Blanked Cells:0 | Flooded Cells : 2458 | FloodSeed Cells : 5161 +------+ Total number of orphans:0

The last iteration of the hole cutting algorithm will print the number of Compute Cells. With simulations using overset meshes, the code will still loop over all of the cells during residual computations; however, the blanked cells do not add directly to the cost of linear solutions with implicit solvers or the adjoint solver. As a result, the residual evaluations’ cost will still be proportional to the total number of cells, while the cost of linear solutions will roughly be proportional to the number of compute cells. See the overset theory guide for more details.

166 Chapter 11. Frequently Asked Questions BIBLIOGRAPHY

[1] Ruben E. Perez, Peter W. Jansen, and Joaquim R. R. A. Martins. pyOpt: a Python-based object-oriented frame- work for nonlinear constrained optimization. Structural and Multidisciplinary Optimization, 45(1):101–118, Jan- uary 2012. doi:10.1007/s00158-011-0666-3. [2] Gaetan K.W. Kenway, Graeme. J. Kennedy, and Joaquim R. R. A. Martins. A CAD-free approach to high-fidelity aerostructural optimization. In Proceedings of the 13th AIAA/ISSMO Multidisciplinary Analysis Optimization Conference, number AIAA 2010-9231. Fort Worth, TX, September 2010. doi:10.2514/6.2010-9231. [3] Charles A. Mader, Gaetan K. W. Kenway, Anil Yildirim, and Joaquim R. R. A. Martins. ADflow—an open-source computational fluid dynamics solver for aerodynamic and multidisciplinary optimization. Journal of Aerospace Information Systems, 2020. doi:10.2514/1.I010796. [4] Gaetan K. W. Kenway, Charles A. Mader, Ping He, and Joaquim R. R. A. Martins. Effective adjoint ap- proaches for computational fluid dynamics. Progress in Aerospace Sciences, 110:100542, October 2019. doi:10.1016/j.paerosci.2019.05.002. [5] Anil Yildirim, Gaetan K. W. Kenway, Charles A. Mader, and Joaquim R. R. A. Martins. A Jacobian-free approx- imate Newton–Krylov startup strategy for RANS simulations. Journal of Computational Physics, 397:108741, November 2019. doi:10.1016/j.jcp.2019.06.018. [6] Ping He, Charles A. Mader, Joaquim R. R. A. Martins, and Kevin J. Maki. An aerodynamic design optimization framework using a discrete adjoint approach with OpenFOAM. Computers & Fluids, 168:285–303, May 2018. doi:10.1016/j.compfluid.2018.04.012. [O1] ONERA M6 Wing. URL: https://www.grc.nasa.gov/WWW/wind/valid/m6wing/m6wing.html. [O2] 3D ONERA M6 Wing validation for turbulence model numerical analysis. URL: https://turbmodels.larc.nasa. gov/onerawingnumerics_val.html. [O3] 3D ONERA M6 Wing validation for turbulence model numerical analysis - SA-neg model results. URL: https: //turbmodels.larc.nasa.gov/onerawingnumerics_val_sa.html (visited on 2020-12-11).

167