%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 
\chapter{Computational Fluid Dynamics Simulations}\label{chapter:cfd}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\section{Parallel Unstructured Maritime Aerodynamics (PUMA)}\label{sec:puma}

Parallel Unstructured Maritime Aerodynamics (PUMA), is a computer
program for the analysis of internal and external non-reacting
compressible flows over arbitrarily complex 3D geometries. It was
originally written by Dr. Christopher W.S. Bruner as a part of his doctoral
thesis~\cite{puma2}. The program is written entirely in ANSI C using MPI
(Message Passing Interface) libraries for message passing. The resulting
code is portable and exhibits good performance. The discretization of
the fluid dynamics equations is based on the Finite Volume Method (FVM)
that solves the full three-dimensional Navier-Stokes
equations. The code supports mixed topology unstructured grids
composed of tetrahedra, wedges, pyramids and hexahedra (bricks). A
cell-centered finite-volume approach is used with the flow variables
represented by values at the center of every cell.

PUMA may be run in a mode which preserves time accuracy or as a
pseudo-unsteady formulation which enhances the convergence to a
steady-state solution. The code uses dynamic memory allocation; thus,
problem size is limited only by the amount of memory available on the
machine. The finite-volume formulation requires approximately $582$
bytes/cell and $634$ bytes/face using double precision variables (not
including message passing overhead). The development of PUMA was
motivated by the need of work to compare the performance of several
different algorithms on the same problem. Hence, PUMA implements a range
of time-integration schemes like {\em Runge-Kutta}, {\em Jacobi} and
various {\em Successive Over-relaxation Schemes (SOR)}, as well as both
{\em Roe} and {\em Van Leer} numerical flux schemes. It also implements
various monotone limiters used in second-order computations ({\em
Venkatakrishnan, Barth, Van Albada, Superbee}). PUMA requires anywhere
between $25000-30000$ floating point operations/iteration/cell, depending
very much on the nature of the grid and cell connectivity, for a second
order accurate computation running in double precision using SSOR
time-integration and Roe fluxes. Several modifications and extensions
have been incorporated into PUMA during the past several years~\cite{mythesis,savaryms,nilayms,anupamms,fredphd}.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 

\subsection{Finite Volume Formulation}

PUMA solves the Navier-Stokes equations for an ideal gas, expressing the
conservation of mass, momentum and energy for a compressible Newtonian
fluid in absence of external forces. In a FVM formulation, these
equations are written in integral form for a bounded domain $\Omega$
within the surface boundary $S$, as shown in the equation
below
\begin{equation}\label{eq:fvm}
\frac{\partial}{\partial t} {\int \!\!\! \int \!\!\! \int}_{\Omega} \vec{Q} \, dV 
+ {\int \!\!\! \int}_{S} \left( \vec{F} \cdot \hat{n} \right)\, dS
= 0,
\end{equation}
where the state vector $\vec{Q}$ is
$$
\vec{Q} = \{\rho \ \ \rho u \ \ \rho v \ \ \rho w \ \ \rho e_0 \},
$$
and the normal component of the flux vector $\vec{F} \cdot \hat{n}$ is
$$
\vec{F} \cdot \hat{n} = \{\rho V_n \ \ \rho u V_n \ \ \rho v  V_n \ \ \rho w V_n \ \ \rho h_0 V_n \}.
$$
Here, $\hat{n}$ denotes the unit surface normal, $\rho$ denotes the air-density, $u$, $v$ and $w$ denote the speed
of the flow in $x$, $y$ and $z$ directions, respectively, $e_0$ denotes
the stagnation energy per unit mass, $h_0$ denotes the stagnation
enthalpy per unit mass, and $V_n$ (=$\vec{V}\cdot \hat{n}$) denotes the
magnitude of the normal component of the flow velocity.
In the FVM formulation, the physical domain is broken up into many small
control volumes of simple shape referred as {\em cells}. These cells are
usually tetrahedra (4 faces) or bricks (6 faces). Once discretized
for a constant volume, equation~\ref{eq:fvm} becomes: 
\begin{eqnarray}\label{eq:fvm2}
\Omega_i \frac{d}{dt} \overline{Q_i} + \sum_{j=1}^{{\textrm{Nfaces}_i}}
\left[ (F\cdot \hat{n}S)_j \right]_i = 0 & & i = 1,...,\textrm{Ncells}
\end{eqnarray}
where $\overline{Q_i}$ is the volume average of $\vec{Q}$ over the cell $i$
and $\left[ (F\cdot \hat{n}S)_j \right]_i$ is the average flux through
face $j$ of cell $i$.
As one can see from equation~\ref{eq:fvm2}, the time rate of change
of state vector $\vec{Q}$ within the domain $\Omega$ of a cell is balanced
by the flux vector $\vec{F}$ across the cell boundary surface $S$.
Between two consecutive time-steps, new flux
 values are calculated from updated state variables, forming a new residue
which stands on the right hand side of equation~\ref{eq:fvm3} below:
\begin{equation}\label{eq:fvm3}
Q^{n+1}_i = Q^{n}_i - \frac{\Delta t}{\Omega_i} \
\sum_{j=1}^{{\textrm{Nfaces}_i}}
\left[ (F\cdot \hat{n}S)_j \right]_i.
\end{equation}
For a steady-state calculation, the solver attempts to bring the
residue to zero, so that the solution converges to a constant set of
values. During an unsteady (time-accurate) calculation, the residue
continuously changes over time.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 
\subsection{Domain Decomposition}

PUMA uses the {\it Single Program Multiple Data} (SPMD) parallelism--that is,
the same code is replicated to each process. One process acts as
the master and collects the results from all other processes, thus doing
slightly more work than the other processes.

Domain decomposition is generally done with the aim to minimize
communications cost (i.e., time), by distributing the cells in the domain
in some optimal way.
$$
\textrm {communication time} = \textrm {latency} + \frac{\textrm {message size}}{\textrm {communications bandwidth}}
$$

Following are the two commonly used approaches for domain decomposition:
\begin{enumerate}
\item {\bf Minimization of inter-domain boundaries}: This approach is
ideal for slow networks (lower communications bandwidth) or whenever
many small messages are passed. It neglects the latency in the above
expression. In this approach, the total length of inter-domain
boundaries is minimized (which is length of all shared data). In other
words, it minimizes the shared part of perimeter/surface area of the
domain. It requires a-priori knowledge of the
number of domains (compute nodes) and hence has to be done every time
the problem is run on a different number of nodes. Popular techniques
to implement this approach need the eigenvalues of the connectivity matrix,
and thus can be expensive for large problems. However, this approach
produces the best grids for large problems.

\item {\bf Minimization of communications}: This approach is ideal for
fast networks or whenever few big messages are passed. It neglects
the second term in the above expression as communication bandwidth is
assumed large. This approach minimizes the total number of messages
exchanged. Since the above minimization
corresponds to minimization of bandwidth of the cell connectivity
matrix, this approach is independent of the number of domains (compute
nodes). A large knowledge base exists in this area and is readily
accessible so implementation is sound and easy. \end{enumerate}

\myfigureBW{figs/FredMesh_coarse.ps}{figs/FredMeshPartition_coarse.ps}{mesh}{gps}{10.8cm}
        {Section depicting unstructured grid for aircraft landing gear geometry (Courtesy Souliez~\cite{fredphd})}
        {$16$-way partitioning of the landing gear grid (figure~\ref{fig:mesh}) from the Gibbs-Poole-Stockmeyer reordering (Courtesy Souliez~\cite{fredphd})}

These domain decomposition techniques are absolutely essential since we
are dealing with unstructured grids in which the ordering of grid cells
is generally arbitrary (especially, if say, Advancing Front Method was
used to create the 2D/3D grids or Delaunay Triangulation for the 2D
grids). Either approach can be used with PUMA, as domain decomposition
is done by a separate program at the pre-processing stage. However,
the latter approach is preferred as it is aimed at machines with high
communications bandwidth, and also since the resulting decomposition
is independent of the number
of processors. Currently, the Gibbs-Poole-Stockmeyer (GPS)~\cite{gps}
algorithm is used to achieve this. For the first approach, a publicly
available graph partitioning software package called METIS~\cite{metis},
which is being actively developed by the Department of Computer Science
at University of Minnesota, can be used. A $16$-way partitioning
generated by the GPS approach as applied to a landing gear GRID
(Fig.~\ref{fig:mesh}) is shown in Fig.~\ref{fig:gps}.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\subsection{Parallelization in PUMA}

PUMA implements explicit and implicit time integration schemes on distributed
memory computers. The solution procedure for
the parallel implementation can be divided
into six major steps.
\begin{enumerate}
\item Each compute node reads its own portion of the grid file
at startup.

\item Cells are divided among the active compute nodes at runtime
based on cell IDs, and only faces associated with
local cells are read.

\item Faces on the interface surface between adjacent computational
domains are duplicated in both domains. Fluxes through these faces
are computed in both domains.

\item Solution variables are communicated between domains at every
timestep which ensures that the computed solution is independent
of the number of compute nodes.

\item Communication of the solution across domains is all that is
required for first-order spatial accuracy, since $Q_L$ and $Q_R$ are
simply cell averages to the first order. ($Q$ being the conservative set
of flow variables).

\item If the left and right states are computed to higher-order, then
$Q_L$ and $Q_R$ are shared explicitly with all adjacent domains. The
fluxes through each face are then computed in each domain to obtain
the residual for each local cell.
\end{enumerate}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\subsection{Boundary Condition Implementation}

Implicit boundary conditions are used in PUMA when any of the implicit 
time-integration schemes are used (i.e., the boundary condition is evaluated at
the next timestep as opposed to the current timestep).
The boundary condition Jacobian ($\frac{\partial Q_{BC}}
{\partial Q}$) is obtained numerically using central differences, which
is completely general and does much to improve the maintainability
of the code.
There are several types of BCs implemented in PUMA, and more
can be added without much difficulty as and when needed. The existing
list of boundary conditions is given in 
table~\ref{table:puma_bc}~\cite{pumaman}. A sample input file for PUMA
is present in Appendix~\ref{ch:pumainput}.
\begin{table}[h!]
\centerline{
\begin{tabular}{|c|l|l|} \hline
{\bf Type}&{\bf Description}&{\bf Variable values specified} \\ \hline
0 & Do nothing & \\ \hline
1 & Fixed at freestream & \\ \hline
2 & Fixed at given values & $\rho$,$u$,$v$,$w$,$p$ \\ \hline
3 & First order extrapolation from the interior & \\ \hline
4 & Second order extrapolation from the interior & \\ \hline
5 & Inflow/outflow (Riemann) & \\ \hline
6 & Specified $p_0$, $T_0$ & $p_0$, $T_0$ \\ \hline
7 & Fixed back pressure pressure & $p$ \\ \hline
8 & Tangency & \\ \hline
9 & No slip adiabatic wall & \\ \hline
10 & No slip fixed­temperature wall $T_{wall}$ & $T_{wall}$ \\ \hline
11 & Zero $u$ velocity & \\ \hline
12 & Zero $v$ velocity & \\ \hline
13 & Zero $w$ velocity & \\ \hline
\end{tabular}}
\caption{PUMA boundary conditions}
\label{table:puma_bc}
\end{table}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\section{Real-Time Visualization}

\subsection{Modifications to PUMA}
To achieve interactivity, several modifications were made to the
PUMA code. The POSSE server component, DataServerMPI, was added to
the {\tt main()} function of PUMA. This modification was done by
registering the cell-centered flow vector $q$ and various important flow
parameters in the code (refer to Appendix~\ref{ch:puma.cc}). Several
new global variables were added to receive iso-surface requests and
store resulting iso-surfaces. An iso-surface extraction routine also had
to be added to PUMA. Since we are dealing with unstructured mesh data
consisting of tetrahedra, a variation of the classic ``marching cubes''
algorithm~\cite{marchingcubes} is used for iso-surface extraction. The
implementation is closely related to the marching cube algorithm except
that the fundamental sampling structure here is a tetrahedron instead of
a cube~\cite{marchingtets2}. Since this implementation expects the flow
data at the nodes of every tetrahedron, a subroutine to interpolate the
flow data from cell-centers to the nodes also had to be added to PUMA.

The planar facet approximation to the iso-surface is calculated for each
tetrahedron independently. The facet vertices are determined by linearly
interpolating where iso-surface cuts the edges of the tetrahedron.
This gives rise to 8 different cases, 7 of which are illustrated in
Fig.~\ref{fig:marchingtets}. The hollow and filled circles at the
vertices of the tetrahedron indicate that the vertices are on different
sides of the iso-surface. The case not illustrated is where all the
vertices are either above of below the iso-surface, and no facets are
generated in this situation.

\myfigureW{figs/marching_tets.ps}{marchingtets}{10cm}
 {Different cases when iso-surface cuts the face of the tetrahedron (Courtesy Bourke~\cite{marchingtets2})}

In PUMA, when the request for an iso-surface reaches the master
processor (in the form of an update to a {\em read-write} variable
specifying the flow parameter of interest), it is broadcast to all the
processors, which act on their local tetrahedral elements to compute
their portion of the iso-surface in parallel before sending it back
to the master for consolidation. Once the data is consolidated by
the master, it is send to the client which is waiting to receive
it. Alternatively, the client can also choose to collect the data
individually from each processor in the computation.

\subsection{Graphical User Interface}
A graphical user interface (GUI) client is used to connect to the
computational steering server. Figures~\ref{fig:gui}
and~\ref{fig:vtk_apache} show a screenshot from the POSSE GUI client
application. 
A PUMA specific client is used to extract and visualize
iso-surfaces. The client is written in C++ with the cross-platform
FLTK~\cite{fltk} API for the GUI and VTK~\cite{vtk} for the 3D
visualization. Since VTK is written on top of OpenGL, the resulting
application can benefit from the OpenGL hardware acceleration
available on most modern graphics chipsets. Figure~\ref{fig:fltksrc}
illustrates a simple ``Hello World'' FLTK application written in C++
to draw a cylinder, and Fig.~\ref{fig:fltkapp} shows its output.
Figure~\ref{fig:vtksrc} illustrates a simple VTK application written in
C++ to draw a cylinder, and Fig.~\ref{fig:vtkapp} shows its output. 
Figure~\ref{fig:uml_gui} shows the UML diagram for the GUI design.

The GUI provides information on the application data registered by the
POSSE {\tt DataServer} application. For every registered data present in
a given row, the following information is displayed by the columns:
\begin{enumerate}
\item {\em keyword}: the string handle identifying the registered data.
\item {\em description}: the data type stating whether the registered
data is a variable,
a dynamic array, static array or a custom structure.
\item {\em variable type and size}: whether the data is of a standard
system type (char, int, float, double, etc.) or a custom user-defined
type.
\item {\em number of elements}: the number of elements if the data
is an array.
\item {\em value}: the current value of the data if it is a variable
or a string (character array).
\end{enumerate}
A blue-colored value field indicates read-only data which cannot
be modified by the client, whereas the black-colored value field indicates
read-write data which can be modified by clicking and 
changing the value in the text-box. The ``Refresh Interval'' field at 
the top-right corner is used to specify the client refresh interval
which determines the rate at which a new copy of the data is obtained
from the server.

\myfigureWp{figs/gui.ps}{gui}{14cm} {A POSSE GUI to connect to the flow solver}
\myfigureWp{figs/vtk_apache.ps}{vtk_apache}{14cm} {A POSSE client application depicting iso-surfaces for a flow solution over the Apache helicopter}

\begin{figure}[!p]
\begin{footnotesize}
\begin{verbatim}
#include <iostream>
#include <FL/Fl.H>
#include <FL/Fl_Window.H>
#include <FL/Fl_Button.H>

void clickAction(Fl_Button *o, void *v)
{
  cout << "Hello, World!" << endl;
}

int main(int argc, char **argv)
{
  // Create new FLTK window of size 300x180
  Fl_Window *window = new Fl_Window(300, 180);

  // Create a button of size 260x100 with upper-left corner at point (20,40)
  Fl_Button *button = new Fl_Button(20, 40, 260, 100, "Click Here!");
  // Register a callback function to be called when button is clicked
  button->callback((Fl_Callback*) clickAction);

  // Set visual properties of the button
  button->box(FL_UP_BOX);
  button->labelfont(FL_HELVETICA_BOLD_ITALIC);
  button->labelsize(36);
  button->labeltype(FL_SHADOW_LABEL);

  // Add the button to the window
  window->add(button);

  // Finish setting the window
  window->end();
  // Display the window
  window->show(argc, argv);

  return Fl::run(); // Run the main FLTK interaction loop
}
\end{verbatim}
\end{footnotesize}
\caption{A simple FLTK application written in C++}
\label{fig:fltksrc}
\end{figure}

\begin{figure}[!p]  
\begin{footnotesize} 
\begin{verbatim}
#include "vtkCylinderSource.h"
#include "vtkPolyDataMapper.h"
#include "vtkActor.h"
#include "vtkRenderer.h"
#include "vtkRenderWindow.h"
#include "vtkRenderWindowInteractor.h"

int main(int argc, char *argv[])
{
  // This creates a polygonal cylinder model with 50 circumferential facets.
  vtkCylinderSource *cylinder = vtkCylinderSource::New();
  cylinder->SetResolution(50);

  // The mapper pushes the geometry into the graphics library
  vtkPolyDataMapper *cylinderMapper = vtkPolyDataMapper::New();
  cylinderMapper->SetInput(cylinder->GetOutput());

  // The actor is a grouping mechanism: besides the geometry (mapper), it
  // also has a property, transformation matrix, and/or texture map.
  vtkActor *cylinderActor = vtkActor::New();
  cylinderActor->SetMapper(cylinderMapper);
  cylinderActor->GetProperty()->SetColor(0.0, 0.0, 1.0); // Set blue color

  // Create the graphics structure. The renderer renders into the 
  // render window. The render window interactor captures mouse events.
  vtkRenderer *ren = vtkRenderer::New();
  vtkRenderWindow *renWin = vtkRenderWindow::New();
  renWin->AddRenderer(ren);        // Add the renderer to the window
  vtkRenderWindowInteractor *iren = vtkRenderWindowInteractor::New();
  iren->SetRenderWindow(renWin);   // Add the window to the interactor

  ren->AddActor(cylinderActor);    // Add the actor to the renderer
  ren->SetBackground(1.0,1.0,1.0); // Set background to white
  renWin->SetSize(600, 600);       // Set the background size
  renWin->Render();                // Render the display window
  iren->Start();                   // Start the event loop

  // Exiting by deleting the instances that have been created.
  cylinder->Delete();
  cylinderMapper->Delete();
  cylinderActor->Delete();
  ren->Delete();
  renWin->Delete();
  iren->Delete();
  return 0;
}
\end{verbatim}
\end{footnotesize}
\caption{A simple VTK application written in C++}
\label{fig:vtksrc}
\end{figure}

\myfigureWp{figs/fltkapp.ps}{fltkapp}{8cm}
{Output from the FLTK application shown in Fig.~\ref{fig:fltksrc}}
\myfigureWp{figs/vtkapp.ps}{vtkapp}{12cm}
{Output from the VTK application shown in Fig.~\ref{fig:vtksrc}}
\myfigureWp{figs/uml_gui.ps}{uml_gui}{16cm}
{UML Diagram for client GUI classes}

A drop-down menu is provided to choose the flow variable for which 
iso-surfaces are requested. After the numerical value for the
iso-surface has been selected, a request is sent to the flow solver
which responds by extracting the iso-surface for the given flow
parameters on each of the processors; then collecting it on the
master processor and sending the final iso-surface to the client. The
iso-surface is then displayed in a separate window in stereo along
with the surface of the object of interest (i.e., helicopter, ship,
etc.). There are two modes provided for querying iso-surfaces. In the
default mode, the user queries for iso-surfaces while the flow-solver
is computing the solution for the next iteration. This mode can be slow
if the user wants to query several iso-surfaces one after another, as
the flow solver cannot answer the client requests until the current
iteration is over. For greater responsiveness, the user can enable the
``Query'' mode which temporarily halts the PUMA computations, so that
the flow-solver can exclusively devote all its CPU cycles to answering
the client iso-surface requests without any lag. There is also an option
``Get Grid'', which will download the entire grid and the updated
solution file and construct a Tecplot~\cite{tecplot} volume grid file
for the user to browse locally using Tecplot. Several iso-surfaces can
be layered on top of each other to compare the differences between
two iterations. Also, as seen in the Fig.~\ref{fig:tecplot}, all the
retrieved data can be optionally filtered through a user-specified
Tecplot layout file to create a powerful and highly-customized
visualization display on a separate Tecplot window.

\myfigureW{figs/tecplot.ps}{tecplot}{14cm}
{Screenshot of PUMA GUI with Tecplot filter}

\subsection{Scalability and Dimensional Reduction}
Traditionally, visualization of CFD data has been done by consolidating
the 3D data from a parallel simulation into a single file, transporting
it over the network to a single or dual-processor graphics workstation,
and post-processing it to extract the required data. This is both slow
and cumbersome.

A significant advantage arising from the use of POSSE within PUMA
is that of ``scalability''.
For an
evenly distributed grid, the number of grid faces on each processor of
a parallel computation is $N/P$ where $N$ is the total number of grid
faces and $P$ is the total number of processors. Here, the scalability
comes from the fact that the extraction of iso-surfaces is done on a
parallel machine rather than the traditional and non-scalable way of
consolidating the data into a file and post-processing it. Thus, the
computational time for extraction of an iso-surface is $O(N/P)$ as
compared to the sequential algorithm which takes $O(N)$ for the same
procedure.

Another significant advantage arising due to the nature of data
typically used for flow visualization is that of ``dimensional
reduction.''
The dimensional reduction comes from the fact that the data required for
the CFD simulation lives in a higher dimensional space (3D) than the
data that is required for visualization (which are in 2D and 1D space
for iso-surfaces and chord plots, respectively). The total number of
grid faces, $N$, are expected to be $O(n^3)$ where $n$ is the average
number of grid faces in each direction. The total number of triangles
in an iso-surface obtained from this grid are expected to be only
$O(n^2)$ or $O(N^{2/3})$. Thus, the average time required for the
communication of this iso-surface data between the client and the server
is $O(N^{2/3})$. Scalability and dimensional reduction combine to give
an expected $O(N^{2/3}/P)$ data coming from each processor during the
parallel computation of an iso-surface.

This approach is also scalable both in ``space'' and ``time.'' By monitoring
a time-dependent simulation, the entire time history can be accessed,
whereas it would be prohibitive to store the time history in files 
which could possibly take tens or even hundreds of gigabytes of disk
space even for a moderately complex case. Figures~\ref{fig:IsoX}, \ref{fig:IsoM} and \ref{fig:IsoCp} show
the relative size of the iso-surfaces with the total size of the grid
for varying X-coordinate, Mach number and $C_p$ values for the flow over
an Apache helicopter geometry. It can be clearly seen that the average
number of triangles in an iso-surface is less than $1\%$ of the total
number of grid faces for this case. Figure~\ref{fig:IsoSens} depicts
another case where two grids with vastly varying sizes are used for
extraction of iso-surfaces with varying values of X-coordinate. Here,
although the larger grid (with 2.3 million faces) is more than twice as
large as the smaller grid (with 1.1 million faces), the average number
of triangles in its iso-surfaces is only about $55\%$ more. The theoretical
average difference is expected to be $(2.3/1.1)^{2/3} = 1.6352$ or $63.52\%$
more, which is close to the result obtained above.

\myfigureW{figs/Iso_X.ps}{IsoX}{11cm} {Relative size of X-coordinate iso-surfaces for the Apache case}
\myfigureW{figs/Iso_Mach.ps}{IsoM}{11cm} {Relative size of Mach number iso-surfaces for the Apache case}
\myfigureW{figs/Iso_Cp.ps}{IsoCp}{11cm} {Relative size of $C_p$ iso-surfaces for the Apache case}
\myfigureW{figs/gridSensitivity.ps}{IsoSens}{11cm} {Grid sensitivity analysis for iso-surfaces}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
