Tutorial 2: ONERA OAT15A at high-speed stall conditions
Outline
Introduction into the test case
Loading STL files and extracting a 2D airfoil from it (optional)
Preparing the input data for \(S^3\)
Executing \(S^3\) and exporting the results
1. Introduction into the test case
This tutorial shows the application of \(S^3\) for an ONERA OAT15A airfoil at high-speed flow conditions. We only analyze a 2D slice of the scale-resolving simulation, which has already been saved to HDF5. The ONERA OAT15A airfoil is provided as an STL file.
The data for the ONERA OAT15A was kindly provided by research partners of the TU Stuttgart within the research group unit FOR2895, the numerical setup can be found in:
Kleinert, Johannes and Ehrle, Maximilian and Waldmann, Andreas and Lutz, Thorsten. Wake tail plane interactions for a tandem wing configuration in high-speed stall conditions, DOI: https://link.springer.com/article/10.1007/s13272-023-00670-1 (June, 2023).
Our first goal is to load the data from the flow simulation. In contrast to the first tutorial, we can’t use the load_foam_data utility since we have stored our data in HDF5 binary format. Since we only have a 3D STL file representing the geometry but our simulation is only 2D, we then want to extract 2D coordinates of the airfoil. This step is only necessary to demontrate how to use 2D coordinates as geometry object within \(S^3\). How to deal with STL files as geometry objects for 3D simulations will be explained in a later tutorial.
Also we will learn how to use the n_cells_max as stopping criterion instead of approximation of the metric to limit the number of cells in the grid created by \(S^3\).
Note: You need to have the ONERA OAT15A geometry and flow data. It can’t be provided since it is proprietary. If you don’ have access to these data, you may find this repository helpful to generate some example data for this tutorial. You can therefore just execute the execute_validation.py using only a single angle of attack and the following CST parameters:
\(f_{max} = 0.0338, \quad t_{max} = 0.089, \quad x_f = 0.653, \quad KR = 0.8, \quad N_1 = 0.4, \quad N_2 = 1.1\)
In this case, you also have to change the metric for \(S^3\) to , e.g., \(\sigma(|U|)\) since the simulation is incompressible.
2. Loading STL files and extracting a 2D airfoil from it (optional)
Now we are ready to start with the tutorial. We first load and visualize the data as we have already done it in the first tutorial.
[5]:
import sys
import numpy as np
import torch as pt
from stl import mesh
from os import environ
from os.path import join
environ["sparseSpatialSampling"] = join("..", "..", "..")
sys.path.insert(0, environ["sparseSpatialSampling"])
from sparseSpatialSampling.export import ExportData
from sparseSpatialSampling.sparse_spatial_sampling import SparseSpatialSampling
from sparseSpatialSampling.geometry import CubeGeometry, GeometryCoordinates2D
[6]:
def load_airfoil_from_stl_file(_load_path: str, _name: str = "oat15.stl", sf: float = 1.0, dimensions: str = "xy",
x_offset: float = 0.0, y_offset: float = 0.0, z_offset: float = 0.0):
"""
Example function for loading airfoil geometries stored as STL file and extract an enclosed 2D-area from it.
Important Note:
the structure of the coordinates within the stl files depends on the order the blocks are exported from
Paraview; the goal is to form an enclosed area, through which we can draw a polygon. Therefore, the way of
loading and sorting the data depends on the stl file. For an airfoil, the data can be sorted as:
TE -> via suction side -> LE -> via pressure side -> TE
It is helpful to export the airfoil geometry without a training edge and close it manually by connecting the
last points from pressure to suction side
:param _load_path: path to the STL file
:param _name: name of the STL file
:param sf: scaling factor, in case the airfoil needs to be scaled
:param dimensions: which plane (orientation) to extract from the STL file
:param x_offset: offset for x-direction, in case the airfoil should be shifted in x-direction
:param y_offset: offset for y-direction, in case the airfoil should be shifted in y-direction
:param z_offset: offset for z-direction, in case the airfoil should be shifted in z-direction
:return: coordinates representing a 2D-airfoil as enclosed area
"""
# mapping for the coordinate directions
dim_mapping = {"x": 0, "y": 1, "z": 2}
dimensions = [dim_mapping[d] for d in dimensions.lower()]
# load stl file
stl_file = mesh.Mesh.from_file(_load_path)
# scale the airfoil to the original size used in CFD and shift if specified
stl_file.x = stl_file.x * sf + x_offset
stl_file.y = stl_file.y * sf + y_offset
stl_file.z = stl_file.z * sf + z_offset
# stack the coordinates (zeros column, because values are the same in all columns)
coord_af = np.stack([stl_file.x[:, 0], stl_file.y[:, 0], stl_file.z[:, 0]], -1)
# remove duplicates without altering the order -> required, otherwise the number of points is very large
coord_af = coord_af[:, dimensions]
_, idx = np.unique(coord_af, axis=0, return_index=True)
coord_af = coord_af[np.sort(idx)]
return coord_af
The original mesh of the simulation and the Mach number field at an arbitrary time step:
3. Preparing the input data for \(S^3\)
[7]:
# path to the CFD data and settings, assuming they are in the top-level of the repository
load_path = join("..", "..", "..", "flowTorch_Workshop_2025")
save_path = join("..", "..", "..", "run", "tutorials", "tutorial_2")
# here we want to use the n_cells_max stopping criterion to generate max. 25 000 cells
n_cells_max = 25000
save_name = f"OAT15_{n_cells_max}_cells"
[8]:
# load the coordinates of the original grid used in CFD
xz = pt.load(join(load_path, "OAT_example_data.pt"), weights_only=False)["xy"]
# load the Mach number field of the original CFD data
field = pt.load(join(load_path, "OAT_example_data.pt"), weights_only=False)["Ma"]
# compute the metric, we want to resolve the buffet, so it make sense to use the std(Ma)
metric = pt.std(field, dim=1)
The metric field then looks like this:
The metric is the highest at the shock near the airoil. Since \(S^3\) generates cells based on the gradient of the metric, we expect cells being created in the vicinity of the airfoil, the shock and the wake of the airfoil.
[9]:
# load the ONERA OAT15A geometry of the leading airfoil from an STL file
oat15 = load_airfoil_from_stl_file(join(load_path, "oat15_airfoil_no_TE.stl"), dimensions="xz")
# load the rear NACA airfoil from an STL file
naca = load_airfoil_from_stl_file(join(load_path, "naca_airfoil_no_TE.stl"), dimensions="xz")
# define the boundaries for the domain
bounds = [[pt.min(xz[:, 0]).item(), pt.min(xz[:, 1]).item()], [pt.max(xz[:, 0]).item(), pt.max(xz[:, 1]).item()]]
# assemble the geometry objects
geometry = [CubeGeometry("domain", True, bounds[0], bounds[1]),
GeometryCoordinates2D("OAT15", False, oat15, refine=True),
GeometryCoordinates2D("NACA", False, naca, refine=True)]
[10]:
# load the corresponding write times for export
times = pt.load(join(load_path, "OAT_example_data.pt"), weights_only=False)["t"]
4. Executing \(S^3\) and exporting the results
[11]:
# instantiate an S^3 object
s_cube = SparseSpatialSampling(xz, metric, geometry, save_path, save_name, "OAT15", n_jobs=4, n_cells_max=n_cells_max)
# execute S^3
s_cube.execute_grid_generation()
[2026-02-19 15:37:37] WARNING Detected stopping criterion 'n_cells_max'. Passing this stopping criterion deactivates the 'min_metric' stopping criterion. To use 'min_metric' as stopping criterion, remove 'n_cells_max' or set 'n_cells_max = None'.
[2026-02-19 15:37:37] INFO Selecting max. number of cells as stopping criterion.
[2026-02-19 15:37:37] INFO
Selected settings:
pre_select : False
n_jobs : 4
max_delta_level : False
geometry : ['domain', 'OAT15', 'NACA']
n_cells_max : 25000
min_level : 5
cells_per_iter_start : 245
cells_per_iter_end : 245
cells_per_iter : 245
cells_per_iter_last : 1000000000.0
reach_at_least : 0.75
n_dimensions : 2
n_cells_orig : 245568
relTol : 0.001
[2026-02-19 15:37:37] INFO Starting grid generation.
[2026-02-19 15:37:37] INFO Starting uniform refinement.
Starting iteration no. 0, N_cells = 1
Warning: TecplotDataloader can't be loaded. Most likely, the 'paraview' module is missing.
Refer to the installation instructions at https://github.com/FlowModelingControl/flowtorch
If you are not using the TecplotDataloader, ignore this warning.
Warning: TecplotDataloader can't be loaded. Most likely, the 'paraview' module is missing.
Refer to the installation instructions at https://github.com/FlowModelingControl/flowtorch
If you are not using the TecplotDataloader, ignore this warning.Warning: TecplotDataloader can't be loaded. Most likely, the 'paraview' module is missing.
Refer to the installation instructions at https://github.com/FlowModelingControl/flowtorch
If you are not using the TecplotDataloader, ignore this warning.
Warning: TecplotDataloader can't be loaded. Most likely, the 'paraview' module is missing.
Refer to the installation instructions at https://github.com/FlowModelingControl/flowtorch
If you are not using the TecplotDataloader, ignore this warning.
Starting iteration no. 1, N_cells = 4
Starting iteration no. 2, N_cells = 8
Starting iteration no. 3, N_cells = 32
Starting iteration no. 4, N_cells = 96
[2026-02-19 15:37:42] INFO Finished uniform refinement.
[2026-02-19 15:37:42] INFO Starting metric-based refinement.
Starting iteration no. 0, N_cells = 320
Starting iteration no. 1, N_cells = 1055
Starting iteration no. 2, N_cells = 1775
Starting iteration no. 3, N_cells = 2492
Starting iteration no. 4, N_cells = 3184
Starting iteration no. 5, N_cells = 3915
Starting iteration no. 6, N_cells = 4650
Starting iteration no. 7, N_cells = 5375
Starting iteration no. 8, N_cells = 6095
Starting iteration no. 9, N_cells = 6819
Starting iteration no. 10, N_cells = 7537
Starting iteration no. 11, N_cells = 8260
Starting iteration no. 12, N_cells = 8981
Starting iteration no. 13, N_cells = 9701
Starting iteration no. 14, N_cells = 10428
Starting iteration no. 15, N_cells = 11159
Starting iteration no. 16, N_cells = 11888
Starting iteration no. 17, N_cells = 12621
Starting iteration no. 18, N_cells = 13348
Starting iteration no. 19, N_cells = 14071
Starting iteration no. 20, N_cells = 14797
Starting iteration no. 21, N_cells = 15528
Starting iteration no. 22, N_cells = 16254
Starting iteration no. 23, N_cells = 16983
Starting iteration no. 24, N_cells = 17711
Starting iteration no. 25, N_cells = 18443
Starting iteration no. 26, N_cells = 19176
Starting iteration no. 27, N_cells = 19906
Starting iteration no. 28, N_cells = 20636
Starting iteration no. 29, N_cells = 21353
Starting iteration no. 30, N_cells = 22084
Starting iteration no. 31, N_cells = 22812
Starting iteration no. 32, N_cells = 23537
Starting iteration no. 33, N_cells = 24262
Starting iteration no. 34, N_cells = 24990
[2026-02-19 15:37:54] INFO Finished metric-based refinement.
[2026-02-19 15:37:54] INFO Starting geometry refinement.
[2026-02-19 15:37:54] INFO Starting refining geometry OAT15.
[2026-02-19 15:37:56] INFO Found a minimum cell level of 6. Target level is 12.
Refining level 7 / 12.
Refining level 8 / 12.
Refining level 9 / 12.
Refining level 10 / 12.
Refining level 11 / 12.
Refining level 12 / 12.
[2026-02-19 15:37:58] INFO Starting refining geometry NACA.
[2026-02-19 15:38:01] INFO Found a minimum cell level of 7. Target level is 11.
Refining level 8 / 11.
Refining level 9 / 11.
Refining level 10 / 11.
Refining level 11 / 11.
[2026-02-19 15:38:01] INFO Finished geometry refinement.
[2026-02-19 15:38:01] INFO Starting renumbering final mesh.
[2026-02-19 15:38:04] INFO Finished refinement in 26.9316 s
(35 iterations).
Time for uniform refinement: 5.2065 s
Time for metric-based refinement: 11.6737 s
Time for geometry refinement: 6.8912 s
Time for renumbering the final mesh: 3.1570 s
Number of cells: 28919
Minimum ref. level: 6
Maximum ref. level: 12
Captured metric of original grid: 56.29 %
As shown in the last log file, we generated \(28919\) cells. This is larger than our specified value of \(25000\) cells, since we employed a subsequent geometry refinement, which is not accounted for. However, looking the end of the first part of the log file, we can confirm that the stopping criterion was activated correctly:
\(\cdots\)
Starting iteration no. 34, N_cells = 24990
\(\cdots\)
[12]:
# create export instance, export all fields into the same HFD5 file and create single XDMF from it
# Note: HDF5 may throws an error when running multiple notebooks in parallel. In that case close the other notebooks and restart the kernel
export = ExportData(s_cube, write_times=times.tolist())
export.export(xz, field.unsqueeze(1), "Ma")
[2026-02-19 15:38:04] INFO Initializing KNN and computing interpolation weights.
[2026-02-19 15:38:04] INFO Starting interpolation and export of field Ma.
[2026-02-19 15:38:05] INFO Writing HDF5 file for field Ma.
[2026-02-19 15:38:06] INFO Writing XDMF file for file OAT15_25000_cells.h5
[2026-02-19 15:38:06] INFO Finished export of field Ma in 1.546s.
This completes the second tutorial. We can now visualize the final results in Paraview:
As you may noticed, there exists a single large cell near the trailing edge of the (rear) NACA airfoil even though we set the refine parameter within the geometry objects to True. This is because the metric almost doesn’t change in this part of the flow field leading to relatively large cells. Since this cell’s vertices are not in the direct vicinity of the airfoil surface, it doesn’t get refined.
We will learn how to deal with these kind of problems in the next tutorial.
[12]: