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

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

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


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

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