RANS CFD on 2D CRM Airfoil#

This notebook shows how to set up and run a 2D CRM airfoil simulation using Flow360 python API.

Note: the cost of running the case shown in this notebook is less than 1 FlexCredit.

Geometry for this tutorial was created using Engineering Sketch Pad (ESP) and is already provided.

We will walk through all the steps such as importing modules, creating a project and assigning simulation parameters.

Additionally, we will use the function fl.operating_condition_from_mach_reynolds(), which is often utilized when running workshop cases and allows us to define operating conditions using mach and reynolds number.

In order to get a closer look at how we describe each parameter, we will first split the simulation parameters into smaller parts, which will be assembled later on.

As a first step, we will import all dependencies.

[1]:
# flow360 imports
import flow360 as fl
from flow360.examples import Tutorial2DCRM
from flow360.log import log, set_logging_level

log.log_to_file = False
set_logging_level("INFO")

Project creation#

We then begin by creating a project variable, which will be used to do operations on the geometry as well as running the case, which includes creating surface and volume meshes.

[2]:
# Download files associated with the tutorial
Tutorial2DCRM.get_files()

# Create project variable
project = fl.Project.from_geometry(Tutorial2DCRM.geometry, name="Tutorial 2D CRM from Python")

# Create geometry variable
geometry = project.geometry

# Show face and edge groupings for the geometry
geometry.show_available_groupings(verbose_mode=True)
geometry.group_faces_by_tag("faceName")
geometry.group_edges_by_tag("edgeName")
[13:41:45] INFO: Geometry successfully submitted:
                   type   = Geometry
                   name   = Tutorial 2D CRM from Python
                   id     = geo-b7b5a67b-dfbc-4c9b-b6ec-8a88903506e4
                   status = uploaded
           
           INFO: Waiting for geometry to be processed.
[13:42:10] WARNING: Could not find face grouping info in the draft's simulation settings. Please remember to group 
           them if relevant features are used.
           WARNING: Could not find edge grouping info in the draft's simulation settings. Please remember to group 
           them if relevant features are used.
[13:42:12] INFO:  >> Available attribute tags for grouping **faces**:
           INFO:     >> Tag '0': faceName. Grouping with this tag results in:
           INFO:         >> Boundary 0: wingTrailing
           INFO:            IDs: ['body0001_face0001']
           INFO:         >> Boundary 1: wing
           INFO:            IDs: ['body0001_face0002', 'body0001_face0003', 'body0001_face0004',
           'body0001_face0005', 'body0001_face0006']
           INFO:         >> Boundary 2: flapTrailing
           INFO:            IDs: ['body0002_face0001']
           INFO:         >> Boundary 3: flap
           INFO:            IDs: ['body0002_face0002', 'body0002_face0003', 'body0002_face0004',
           'body0002_face0005']
           INFO:         >> Boundary 4: slatTrailing
           INFO:            IDs: ['body0003_face0001']
           INFO:         >> Boundary 5: slat
           INFO:            IDs: ['body0003_face0002', 'body0003_face0003', 'body0003_face0004',
           'body0003_face0005', 'body0003_face0006', 'body0003_face0007']
           INFO:     >> Tag '1': groupName. Grouping with this tag results in:
           INFO:         >> Boundary 0: wing
           INFO:            IDs: ['body0001_face0001', 'body0001_face0002', 'body0001_face0003',
           'body0001_face0004', 'body0001_face0005', 'body0001_face0006']
           INFO:         >> Boundary 1: flap
           INFO:            IDs: ['body0002_face0001', 'body0002_face0002', 'body0002_face0003',
           'body0002_face0004', 'body0002_face0005']
           INFO:         >> Boundary 2: slat
           INFO:            IDs: ['body0003_face0001', 'body0003_face0002', 'body0003_face0003',
           'body0003_face0004', 'body0003_face0005', 'body0003_face0006', 'body0003_face0007']
           INFO:  >> Available attribute tags for grouping **edges**:
           INFO:     >> Tag '0': edgeName. Grouping with this tag results in:
           INFO:         >> Boundary 0: wingtrailingEdge
           INFO:            IDs: ['body0001_edge0001', 'body0001_edge0002', 'body0001_edge0008']
           INFO:         >> Boundary 1: symmetry
           INFO:            IDs: ['body0001_edge0003', 'body0001_edge0004', 'body0001_edge0006',
           'body0001_edge0007', 'body0001_edge0009', 'body0001_edge0010', 'body0001_edge0011', 'body0001_edge0012',
           'body0002_edge0003', 'body0002_edge0004', 'body0002_edge0006', 'body0002_edge0007', 'body0002_edge0008',
           'body0002_edge0009', 'body0003_edge0003', 'body0003_edge0004', 'body0003_edge0006', 'body0003_edge0007',
           'body0003_edge0009', 'body0003_edge0010', 'body0003_edge0012', 'body0003_edge0013', 'body0003_edge0014',
           'body0003_edge0015']
           INFO:         >> Boundary 2: wingleadingEdge
           INFO:            IDs: ['body0001_edge0005']
           INFO:         >> Boundary 3: flaptrailingEdge
           INFO:            IDs: ['body0002_edge0001', 'body0002_edge0002']
           INFO:         >> Boundary 4: flapleadingEdge
           INFO:            IDs: ['body0002_edge0005']
           INFO:         >> Boundary 5: slattrailingEdge
           INFO:            IDs: ['body0003_edge0001', 'body0003_edge0002', 'body0003_edge0005',
           'body0003_edge0008']
           INFO:         >> Boundary 6: slatFrontLEadingEdge
           INFO:            IDs: ['body0003_edge0011']

Defining simulation parameters#

Once the geometry is processed, we can create separate variables for:

A helpful statement we will use is with fl.SI_unit_system:, which allows us to automatically assign SI units to parameters.

We still need to manually define the unit for angle as there is no default angle unit when using with fl.SI_unit_system:.

Entities and volume zones#

Cylinder entites are used for creating mesh refinements.

Farfield volume zone is used for defining volume zones and models.

[3]:
# Choosing a unit system
with fl.SI_unit_system:

    # Cylinder entities used for mesh refinements
    cylinders = [
        fl.Cylinder(
            name=f"cylinder{i+1}",
            axis=[0, 1, 0],
            center=[0.7, 0.5, 0],
            outer_radius=outer_radius,
            height=1.0,
        )
        for i, outer_radius in enumerate([1.1, 2.2, 3.3, 4.5])
    ]
    cylinder5 = fl.Cylinder(
        name="cylinder5", axis=[-1, 0, 0], center=[6.5, 0.5, 0], outer_radius=6.5, height=10
    )

    # Farfield used for creating a volume zone and assigning appropriate models
    farfield = fl.AutomatedFarfield(name="farfield", method="quasi-3d")
           INFO: using: SI unit system for unit inference.

Meshing parameters#

In this section we will assign settings so that our mesh will have sufficient level of refinement for this case.

Default settings describe how the the mesh will be created globally, while refinements allow for much more precise control of the mesh in regions of interest.

Here we will utilize the previously created cylinder entities, farfield, and we will also refer to geometry’s faces and edges, which were grouped in the Project creation step.

[4]:
with fl.SI_unit_system:
    meshing_params = fl.SimulationParams(
        meshing=fl.MeshingParams(
            # Default settings for meshing
            defaults=fl.MeshingDefaults(
                surface_edge_growth_rate=1.17,
                surface_max_edge_length=1.1,
                curvature_resolution_angle=12 * fl.u.deg,
                boundary_layer_growth_rate=1.17,
                boundary_layer_first_layer_thickness=1.8487111e-06,
            ),
            # Adjust all spacings in refinement regions
            refinement_factor=1.35,
            # Adjust narrow gap treatment strength for two surfaces in close proximity
            gap_treatment_strength=0.5,
            # Crete new volume zones
            volume_zones=[farfield],
            # Additional fine-tunning for refinements on top of defaults
            refinements=[
                # Uniform spacing refinement inside specified region of mesh
                fl.UniformRefinement(name="refinement1", spacing=0.1, entities=[cylinders[0]]),
                fl.UniformRefinement(name="refinement2", spacing=0.15, entities=[cylinders[1]]),
                fl.UniformRefinement(name="refinement3", spacing=0.225, entities=[cylinders[2]]),
                fl.UniformRefinement(name="refinement4", spacing=0.275, entities=[cylinders[3]]),
                fl.UniformRefinement(name="refinement5", spacing=0.325, entities=[cylinder5]),
                # Refine surface elements for given faces
                fl.SurfaceRefinement(name="wing", max_edge_length=0.74, faces=[geometry["wing"]]),
                fl.SurfaceRefinement(
                    name="flap-slat",
                    max_edge_length=0.55,
                    faces=[geometry["flap"], geometry["slat"]],
                ),
                fl.SurfaceRefinement(
                    name="trailing",
                    max_edge_length=0.36,
                    faces=[
                        # '*' allows us to select all faces faces ending with 'Trailing'
                        geometry["*Trailing"],
                    ],
                ),
                # Grows anisotropic layers orthogonal to the specified edges
                fl.SurfaceEdgeRefinement(
                    name="edges",
                    method=fl.HeightBasedRefinement(value=0.0007),
                    edges=[
                        # Similarly as above, we can select multiple edges using '*' expression
                        geometry["*trailingEdge"],
                        geometry["*leadingEdge"],
                    ],
                ),
                fl.SurfaceEdgeRefinement(
                    name="symmetry", method=fl.ProjectAnisoSpacing(), edges=[geometry["symmetry"]]
                ),
            ],
        ),
    )
           INFO: using: SI unit system for unit inference.

Reference geometry#

Now we will define the reference_geometry which is used to report forces such as drag and lift.

[5]:
with fl.SI_unit_system:
    reference_geometry_params = fl.SimulationParams(
        # Create geometrical related reference values
        reference_geometry=fl.ReferenceGeometry(
            moment_center=[0.25, 0, 0], moment_length=[1, 1, 1], area=0.01
        )
    )
           INFO: using: SI unit system for unit inference.

Operating condition#

Next up is the definition of flow conditions with the help of fl.operating_condition_from_mach_reynolds() function, which does not require knowledge of air properties in desired conditions and instead allows the usage of mach number in conjunction with reynolds number. This is particularly useful for conducting workshop cases as we can easily simulate flow conditions for different values of reynolds number. Viscosity as well as density are going to be automatically calculated based on input parameters.

[6]:
with fl.SI_unit_system:
    operating_condition_params = fl.SimulationParams(
        # Create operating conditions using mach and reynolds number
        operating_condition=fl.operating_condition_from_mach_reynolds(
            mach=0.2,
            reynolds=5e6,
            temperature=272.1,
            alpha=16 * fl.u.deg,
            beta=0 * fl.u.deg,
            project_length_unit=1 * fl.u.m,
        ),
    )
           INFO: using: SI unit system for unit inference.
           INFO: Density and viscosity were calculated based on input data, ThermalState will be automatically
           created.

Time stepping#

Even though our simulation will be run as a Steady State case, the solver will use information about time stepping, so we define it accordingly.

max_steps is the maximum amount of psuedo time steps in one physical step, which can be interpreted as the maximum number of iterations.

CFL is the Courant–Friedrichs–Lewy condition and RampCFL specifies its initial and final value, as well as the amount of pseudo time steps before achieving the final value.

[7]:
with fl.SI_unit_system:
    time_stepping_params = fl.SimulationParams(
        # Time stepping settings for Steady State simulation
        time_stepping=fl.Steady(
            max_steps=3000, CFL=fl.RampCFL(initial=20, final=300, ramp_steps=500)
        ),
    )
           INFO: using: SI unit system for unit inference.

Models#

This parameter is used to define solver settings, numerical models and boundary conditions within our simulation.

[8]:
with fl.SI_unit_system:
    models_params = fl.SimulationParams(
        # Solver settings, numerical models and boundary conditions
        models=[
            # Create wall boundary condition
            fl.Wall(
                surfaces=[
                    # Here we use '*' to select all surfaces
                    geometry["*"],
                ],
                name="wall",
            ),
            # Create freestream boundary condition
            fl.Freestream(surfaces=farfield.farfield, name="Freestream"),
            # Create slip wall boundary condition
            fl.SlipWall(surfaces=farfield.symmetry_planes, name="slipwall"),
            # Create fluid volume model
            fl.Fluid(
                # Set up compressible Navier-Stokes solver
                navier_stokes_solver=fl.NavierStokesSolver(
                    absolute_tolerance=1e-11,
                    linear_solver=fl.LinearSolver(max_iterations=35),
                    kappa_MUSCL=0.33,
                ),
                # Set up turbulence solver based on the Spalart-Allmaras model
                turbulence_model_solver=fl.SpalartAllmaras(
                    absolute_tolerance=1e-10,
                    linear_solver=fl.LinearSolver(max_iterations=25),
                    equation_evaluation_frequency=1,
                ),
            ),
        ],
    )
           INFO: using: SI unit system for unit inference.

Outputs#

Here we will specify which outputs we want Flow360 to produce for us.

[9]:
with fl.SI_unit_system:
    outputs_params = fl.SimulationParams(
        outputs=[
            # Specify the list of volume output variables
            fl.VolumeOutput(
                name="fl.VolumeOutput",
                output_fields=[
                    "primitiveVars",
                    "vorticity",
                    "residualNavierStokes",
                    "residualTurbulence",
                    "Cp",
                    "Mach",
                    "qcriterion",
                    "mut",
                ],
            ),
            # Specify the list of surface output variables
            fl.SurfaceOutput(
                name="fl.SurfaceOutput",
                surfaces=geometry["*"],
                output_fields=[
                    "primitiveVars",
                    "Cp",
                    "Cf",
                    "CfVec",
                    "yPlus",
                ],
            ),
        ],
    )
           INFO: using: SI unit system for unit inference.

Combining parameters#

After we created multiple instances of SimulationParams class, we can create one final instace, which will contain all of the parameters defined by us previously. Flow360 allows us to refer to each parameter specified within SimulationParams, for example using meshing_params.meshing, we can access meshing parameters within that variable.

[10]:
with fl.SI_unit_system:
    params = fl.SimulationParams(
        meshing=meshing_params.meshing,
        reference_geometry=reference_geometry_params.reference_geometry,
        operating_condition=operating_condition_params.operating_condition,
        time_stepping=time_stepping_params.time_stepping,
        models=models_params.models,
        outputs=outputs_params.outputs,
    )
           INFO: using: SI unit system for unit inference.

Running the case#

With all of our parameters defined, we can pass them to our project and run the case.

[11]:
project.run_case(params=params, name="Case of tutorial 2D CRM from Python")
           INFO: using: SI unit system for unit inference.
[13:42:16] INFO: Successfully submitted:
                   type   = Case
                   name   = Case of tutorial 2D CRM from Python
                   id     = case-13380c76-571b-4401-94f1-0dfeb000af1d
                   status = pending
           
[11]:
CaseMeta(name='Case of tutorial 2D CRM from Python', user_id='AIDAXWCOWJGJJBXX543AQ', id='case-13380c76-571b-4401-94f1-0dfeb000af1d', parent_id=None, solver_version='release-25.2.6', status=<Flow360Status.PENDING: 'pending'>, tags=None, created_at=None, updated_at=datetime.datetime(2025, 4, 8, 11, 42, 14, 918000, tzinfo=datetime.timezone.utc), updated_by=None, deleted=False, cloud_path_prefix=None, case_mesh_id='vm-5284bdf5-f00b-47aa-ba22-c6a72884ea61', supportStorageSize=None, realFlexUnit=0.0, metadataProcessed=False, elapsedTimeInSeconds=None, oriRealFlexUnit=None, clonedFrom=None, exampleResource=False, caseFinishTime=None, unitSystem=None, userEmail='[email protected]', highPriority=False, restoreAt=None, storageSize=0, internal=False, parentFolders=[{'userId': 'AIDAXWCOWJGJJBXX543AQ', 'name': 'ROOT.FLOW360', 'tags': None, 'id': 'ROOT.FLOW360', 'parentFolderId': None, 'createdAt': '2023-09-01T09:14:15.293797Z', 'updatedAt': '2023-09-01T09:14:15.293797Z'}], refId='vm-5284bdf5-f00b-47aa-ba22-c6a72884ea61', running=False, nodesInfo=None, estFlexUnit=0.0, caseName='Case of tutorial 2D CRM from Python', estWorkUnit=0.0, combinedStatus='pending', success=False, userDataUploaded=None, storageStatus='STANDARD', retryCount=None, caseStatus='pending', storageClass='STANDARD', computeCost=0.0, casePriority=None, viewed=None, objectRefId='case-13380c76-571b-4401-94f1-0dfeb000af1d', parentFolderId='ROOT.FLOW360', caseTags=None, caseParentId=None, meshNodeSize=0, events=None, meshSize=0, nodeSize=0, currentProgress=None, errorType=None, caseStartTime=None, estimationDuration=0.0, standardAge=None, caseSubmitTime='2025-04-08T11:42:14.918Z', projectId='prj-ccde584b-5089-4daa-b743-18bd84135edd')

Post processing#

After we have successfully run the simulation, we can post process our results.

We will do so by first, downloading the results of interest from the server.

To do that we will create a variable containing information about the case. >Note: we will use the wait() method, which will wait with the execution of the rest of the code until the case has finished running.

[12]:
case = project.case
case.wait()

Next we can download the results. We will be looking at the post processing of these quantities:

  • total forces

  • nonlinear residuals

  • CFL

  • total CD along the X axis

[13]:
total_forces = case.results.total_forces
total_forces = total_forces.as_dataframe()

nonlinear_residuals = case.results.nonlinear_residuals
nonlinear_residuals = nonlinear_residuals.as_dataframe()

cfl = case.results.cfl
cfl = cfl.as_dataframe()

x_slicing_force_distribution = case.results.x_slicing_force_distribution
x_slicing_force_distribution.wait()
x_slicing_force_distribution = x_slicing_force_distribution.as_dataframe()
[14:03:54] INFO: Saved to
           /var/folders/qk/mywsrvps5gl_f3yjx2k1v1xm0000gn/T/tmp5n0q1cf1/dd1d867e-e8e7-4d69-b869-7959e57aaaeb.csv
[14:03:56] INFO: Saved to
           /var/folders/qk/mywsrvps5gl_f3yjx2k1v1xm0000gn/T/tmp5n0q1cf1/4ed7c5e7-07a7-45a2-aa83-39fe124178d1.csv
[14:03:57] INFO: Saved to
           /var/folders/qk/mywsrvps5gl_f3yjx2k1v1xm0000gn/T/tmp5n0q1cf1/20b36f43-8eff-455f-b63a-9bec9404f16b.csv
[14:03:58] INFO: Saved to
           /var/folders/qk/mywsrvps5gl_f3yjx2k1v1xm0000gn/T/tmp5n0q1cf1/cfdf3e8f-9f41-40c5-afa2-fde5190bffc8.csv

In order to obtain the X and Y variable names for our plots, we will print out the dataframe and look at its column names.

[14]:
print(cfl)
     physical_step  pseudo_step  0_NavierStokes_cfl  1_SpalartAllmaras_cfl
0                0            0           20.000000              40.000000
1                0           10           21.113091              42.226182
2                0           20           22.288131              44.576261
3                0           30           23.528567              47.057133
4                0           40           24.838039              49.676077
..             ...          ...                 ...                    ...
296              0         2960          300.000000             600.000000
297              0         2970          300.000000             600.000000
298              0         2980          300.000000             600.000000
299              0         2990          300.000000             600.000000
300              0         2999          300.000000             600.000000

[301 rows x 4 columns]

First, we will look at how the residuals change as the pseudo step progresses.

[15]:
nonlinear_residuals.plot(
    x="pseudo_step",
    y=["0_cont", "1_momx", "2_momy", "3_momz", "4_energ", "5_nuHat"],
    logy=True,
    xlim=(10, 3000),
    xlabel="Pseudo Step",
    ylabel="residuals",
    figsize=(10, 5),
)
[15]:
<Axes: xlabel='Pseudo Step', ylabel='residuals'>
../../../_images/python_tutorials_notebooks_notebook_2d_crm_31_1.png

Next are the CFL values for both the Navier-Stokes solver, as well as the turbulence model solver with Spalart-Allmaras model selected.

[16]:
cfl.plot(
    x="pseudo_step",
    y=["0_NavierStokes_cfl", "1_SpalartAllmaras_cfl"],
    xlim=(0, 3000),
    figsize=(10, 5),
)
[16]:
<Axes: xlabel='pseudo_step'>
../../../_images/python_tutorials_notebooks_notebook_2d_crm_33_1.png

Another popular form of data visualisation indicating the convergence status of the solution for aerodynamic applications is monitoring how the CL and CD values change with pseudo steps (iterations).

[17]:
total_forces.plot(
    x="pseudo_step", y="CL", xlabel="Pseudo Step", ylabel="CL", figsize=(10, 5), style="orange"
)
total_forces.plot(
    x="pseudo_step", y="CD", xlabel="Pseudo Step", ylabel="CD", figsize=(10, 5), style="lightgreen"
)
[17]:
<Axes: xlabel='Pseudo Step', ylabel='CD'>
../../../_images/python_tutorials_notebooks_notebook_2d_crm_35_1.png
../../../_images/python_tutorials_notebooks_notebook_2d_crm_35_2.png

Finally, we will define a graph showcasing how CD varies alongside the X axis.

[18]:
x_slicing_force_distribution.plot(
    x="X", y="totalCumulative_CD_Curve", xlabel="X", ylabel="CD", figsize=(10, 5)
)
[18]:
<Axes: xlabel='X', ylabel='CD'>
../../../_images/python_tutorials_notebooks_notebook_2d_crm_37_1.png

More information can be found on the documentation page for this case, available here.