Note

Since version 0.1.4, the normals in the airfrans.Simulation class are inward-pointing instead of outward-pointing as previously. This for consistency with the normals given in the AirfRANS dataset.

Simulation

Each simulation is defined via boundary conditions (inlet velocity and angle of attack) and a geometry (NACA 4 or 5 digits). We recall that in an incompressible setup and when neglecting gravity, the only parameter of Navier-Stokes equations (and also of RANS equations) is the Reynolds number. If we fix the kinematic viscosity (i.e. if we fix the temperature of air one for all), the Reynolds number is completely defined by the inlet velocity (as the characteristic length of airfoil is chosen to be 1 meter). In this dataset, we chose to arbitrarily fix the temperature to 298.15K and to work with properties of air at that temperature, at a pressure of 1013.25 hPa.

You can run new simulation with the help of OpenFOAM v2112 and the NACA simulation GitHub.

Note

During the generation of the dataset, we fixed the kinematic viscosity at a numerical value of 1.56e-5. After the addition of the temperature parameters in the simulation, the computed kinematic viscosity at 298.15K is of roughly 1.55e-5 leading to slight discrepancies at a fix inlet velocity between newly generated simulations and simulations from the dataset. Those discrepencies do not exist if we only take as parameter the Reynolds number.

In the following, we present attributes and methods of the airfrans.Simulation class.

To load a simulation (for example simulation 'airFoil2D_SST_43.597_5.932_3.551_3.1_1.0_18.252'), simply run:

import airfrans as af

simulation = af.Simulation(root = PATH_TO_DATASET, name = 'airFoil2D_SST_43.597_5.932_3.551_3.1_1.0_18.252', T = 298.15)

Note

The name of a simulation gives all the information about the boundary conditions used to generate it. For example, in 'airFoil2D_SST_43.597_5.932_3.551_3.1_1.0_18.252', we read that the inlet velocity magnitude is \(43.597 m\cdot s^{-1}\), and the angle of attack \(5.932^{\circ}\). The last numbers are for the parameters of the NACA arifoil, if we have 3 parameters it means that we are dealing with an airfoil from the 4-digits series, and if we have 4 parameters then this airfoil comes from the 5-digits series (this is due to the fact that the last two digits gives the thickness of the airfoil which only defines a single parameter). To keep with our example, we here deal with an airfoil of the 5-digits series with parameters given by (3.551, 3.1, 1.0, 18.252) (which are real numbers and not integers). Another example, the NACA 0012 will lead to parameters (0, 0, 12) and a name ending by 0_0_12.

Attributes

One part of the attributes of the class is the properties of air at the given temperature:

  • airfrans.Simulation.MOL is the molar weigth of air in \(kg\cdot mol^{-1}\)

  • airfrans.Simulation.P_ref is the reference pressure in \(Pa\)

  • airfrans.Simulation.RHO is the specific mass in \(kg\cdot m^{-3}\)

  • airfrans.Simulation.NU is the knimeatic viscosity in \(m^2\cdot s^{-2}\)

  • airfrans.Simulation.C is the sound velocity in \(m\cdot s^{-1}\).

A second part is the boundary conditions:

  • airfrans.Simulation.inlet_velocity is the inlet velocity in \(m\cdot s^{-1}\)

  • airfrans.Simulation.angle_of_attack is the angle of attack in \(rad\).

A third part is the PyVista object of the reference simulation:

  • airfrans.Simulation.internal is the PyVista object of the internal patch

  • airfrans.Simulation.airfoil is the PyVista object of the aerofoil patch

Finally, the last part is the fields associated with the simulation under the form of NumPy ndarray. Those fields are either defined on the mesh nodes, are the airfoil patch nodes directly:

  • airfrans.Simulation.input_velocity is the inlet velocity copied on each nodes of the internal mesh in \(m\cdot s^{-1}\)

  • airfrans.Simulation.sdf is the distance function on the internal mesh in \(m\)

  • airfrans.Simulation.surface is a boolean on the internal mesh, it is True if the node lie on the airfoil

  • airfrans.Simulation.position is the position of the nodes of the internal mesh in \(m\)

  • airfrans.Simulation.airfoil_position is the position of the nodes of the airfoil mesh in \(m\)

  • airfrans.Simulation.normals is the inward-pointing normals of the surface on the internal mesh, it is set to 0 for points not lying on the airfoil

  • airfrans.Simulation.airfoil_normals is the inward-pointing normais of the surface on the airfoil mesh

and for the targets:

  • airfrans.Simulation.velocity is the air velocity on the internal mesh in \(m\cdot s^{-1}\)

  • airfrans.Simulation.pressure is the air pressure on the internal mesh (divided by the specific mass in the incompressible case)

  • airfrans.Simulation.nu_t is the kinematic turbulent viscosity on the internal mesh in \(m^2\cdot s^{-2}\)

import matplotlib.pyplot as plt

fig, ax = plt.subplots(3, 2, figsize = (36, 12))
ax[0, 0].scatter(simulation.position[:, 0], simulation.position[:, 1], c = simulation.velocity[:, 0], s = 0.75)
ax[0, 1].scatter(simulation.position[:, 0], simulation.position[:, 1], c = simulation.pressure[:, 0], s = 0.75)
ax[0, 2].scatter(simulation.position[:, 0], simulation.position[:, 1], c = simulation.sdf[:, 0], s = 0.75)
ax[1, 0].scatter(simulation.position[:, 0], simulation.position[:, 1], c = simulation.nu_t[:, 0], s = 0.75)
ax[1, 1].scatter(simulation.airfoil_position[:, 0], simulation.airfoil_position[:, 1], c = simulation.airfoil_normals[:, 0], s = 0.75)
ax[1, 2].scatter(simulation.airfoil_position[:, 0], simulation.airfoil_position[:, 1], c = simulation.airfoil_normals[:, 1], s = 0.75)
...
../_images/fields.png

Note

Be careful that the ordering of points over the airfoil in the internal mesh or in the airfoil mesh is not the same. The function airfrans.reorganize is built to reordered the points as we want.

internal_normals = simulation.normals[simulation.surface]
print((internal_normals == simulation.airfoil_normals).all())
>> False

reordered_normals = af.reorganize(simulation.position[simulation.surface], simulation.airfoil_position, internal_normals)
print((reordered_normals == simulation.airfoil_normals).all())
>> True

Methods

Sampling methods are available allowing to potentially free the constrainte of the mesh structure:

seed = 0

sampling_volume_uniform = simulation.sampling_volume(seed, 50000, density = 'uniform')
sampling_volume_mesh = simulation.sampling_volume(seed, 50000, density = 'mesh_density')

sampling_surface_uniform = simulation.sampling_surface(seed, 500, density = 'uniform')
sampling_surface_mesh = simulation.sampling_surface(seed, 500, density = 'mesh_density')

sampling_mesh = simulation.sampling_mesh(seed, 50000)
sampling_mesh_surface = sampling_mesh[sampling_mesh[:, 2].astype('bool')]

fig, ax = plt.subplots(2, 3, figsize = (36, 12))
ax[0, 0].scatter(sampling_volume_uniform[:, 0], sampling_volume_uniform[:, 1], c = sampling_volume_uniform[:, 3], s = 0.75)
ax[0, 1].scatter(sampling_volume_mesh[:, 0], sampling_volume_mesh[:, 1], c = sampling_volume_mesh[:, 3], s = 0.75)
ax[0, 2].scatter(sampling_mesh[:, 0], sampling_mesh[:, 1], c = sampling_mesh[:, 8], s = 0.75)
ax[1, 0].scatter(sampling_surface_uniform[:, 0], sampling_surface_uniform[:, 1], s = 0.75)
ax[1, 1].scatter(sampling_surface_mesh[:, 0], sampling_surface_mesh[:, 1], s = 0.75)
ax[1, 2].scatter(sampling_mesh_surface[:, 0], sampling_mesh_surface[:, 1], s = 0.75)
...
../_images/sampling.png

You can also directly compute the wall shear stress and the force coefficient with the class attributes or the reference simulation:

simulation.velocity = np.zeros_like(simulation.velocity)
simulation.pressure = np.zeros_like(simulation.pressure)

print(simulation.force())
>> (array([0., 0.]), array([-0., -0.]), array([0., 0.]))

print(simulation.force(reference = True))
>> (array([-79.15, 907.93]), array([-87.92, 906.80]), array([8.78, 1.14]))

print(simulation.force_coefficient())
>> ((0.0, 0.0, 0.0), (0.0, 0.0, 0.0))

print(simulation.force_coefficient(reference = True))
>> ((0.0134, 0.0056, 0.0079), (0.8099, 0.8097, 0.0002))

Some classical metrics between the attributes fields/forces and the reference fields/forces, for example the mean squared error:

print(simulation.mean_squared_error())
>> array([1100.53, 228.03, 227577.73, 0.])

simulation.reset()
print(simulation.mean_squared_error())
>> array([0., 0., 0., 0.])

Finally, you can save new .vtu and .vtp files with the fields given in attributes of the class:

simulation.save(root = SAVING_PATH)