Alpha Sweep#

This notebook demonstrates how to perform an angle of attack (alpha) sweep using the Flow360 Python API. The process involves creating a project, defining simulation parameters, launching a series of cases with varying alpha values, and finally generating a comprehensive report summarizing the results.

1. Setup and Imports#

The following cell imports the necessary libraries and modules. flow360 is the main package for interacting with the Flow360 platform. Specific modules for defining simulation parameters, creating reports, and handling units are also imported.

[1]:
import flow360 as fl
from flow360.examples import EVTOL

2. Project Creation#

A Flow360 Project is a container for simulations and their associated assets, such as geometries and meshes. A project can be initiated either from an existing volume mesh or from a CAD geometry file. When starting from a geometry, meshing parameters must be defined to instruct the platform on how to generate the volume mesh.

[2]:
EVTOL.get_files()

project = fl.Project.from_geometry(EVTOL.geometry, name="sweep_evtol_from_geometry")

geometry = project.geometry
geometry.group_faces_by_tag("faceName")
[12:04:13] INFO: Geometry successfully submitted:
                   type   = Geometry
                   name   = sweep_evtol_from_geometry
                   id     = geo-b66e23df-1030-4070-89e7-3d2e9b86e46d
                   status = uploaded
           
           INFO: Waiting for geometry to be processed.
[12:06:07] INFO: Regrouping face entities under `faceName` tag (previous `_color`).

3. Meshing parameters#

We define some simple meshing parameters to achieve appropriate resolution for this example. The use of AngleBasedRefinement will allow us to better model the flow at the leading edge.

[3]:
with fl.SI_unit_system:
    meshing_params = fl.MeshingParams(
        defaults=fl.MeshingDefaults(
            boundary_layer_first_layer_thickness=1e-5, surface_max_edge_length=1
        ),
        volume_zones=[fl.AutomatedFarfield()],
        refinements=[
            fl.SurfaceEdgeRefinement(
                name="leading_edges",
                edges=[geometry["leadingEdge"]],
                method=fl.AngleBasedRefinement(value=2 * fl.u.deg),
            )
        ],
    )
           INFO: using: SI unit system for unit inference.

4. Boundary Conditions#

Boundary conditions define the physical behavior at the boundaries of the computational domain. We need to apply appropriate Wall and Freestream conditions to different surfaces of the geometry.

Surface selection can be done using exact names or with wildcards, for example:

  • geometry[“*”] will select all geometry boundaries

  • geometry[”*pylon”] will select all geometry boundaries that end with pylon

[4]:
models = [
    fl.Wall(surfaces=[geometry["*"]]),
    fl.Freestream(surfaces=fl.AutomatedFarfield().farfield),
]

5. Simulation Parameters#

The SimulationParams object encapsulates all settings for a simulation run. This includes meshing parameters, reference geometry, operating conditions, time-stepping scheme, physical models, and output specifications.

[5]:
with fl.SI_unit_system:
    params = fl.SimulationParams(
        meshing=meshing_params,
        reference_geometry=fl.ReferenceGeometry(
            moment_center=(0, 0, 0), moment_length=1, area=1
        ),
        operating_condition=fl.AerospaceCondition(
            velocity_magnitude=100, alpha=0 * fl.u.deg
        ),
        time_stepping=fl.Steady(max_steps=5000, CFL=fl.AdaptiveCFL()),
        models=[
            *models,
            fl.Fluid(
                navier_stokes_solver=fl.NavierStokesSolver(),
                turbulence_model_solver=fl.SpalartAllmaras(),
            ),
        ],
        outputs=[
            fl.VolumeOutput(
                output_format="tecplot",
                output_fields=[
                    "Mach",
                    "Cp",
                    "mut",
                    "mutRatio",
                    "primitiveVars",
                    "qcriterion",
                ],
            ),
            fl.SurfaceOutput(
                surfaces=[
                    geometry["fuselage"],
                    geometry["*pylon"],
                    geometry["*wing"],
                    geometry["*tail"],
                ],
                output_fields=[
                    "Cp",
                    "yPlus",
                    "Cf",
                    "CfVec",
                    "primitiveVars",
                    "wallDistance",
                ],
                output_format="tecplot",
            ),
        ],
    )
           INFO: using: SI unit system for unit inference.

6. Executing an Alpha Sweep#

A sweep is performed by iterating through a list of alpha values. In each iteration, the operating_condition in SimulationParams is updated with a new alpha, and new Case is run using project.run_case(). The returned Case objects are collected in a list for later analysis and report generation. The cases will be running in parallel on the cloud. We then wait for all cases to complete before proceeding.

To not recreate the entire SimulationParams every time, we can modify existing one’s alpha angle by doing params.operating_condition.alpha = alpha_angle.

[6]:
case_list = []

alphas = [-10, -5, 0, 5, 10, 12, 14] * fl.u.deg

for alpha_angle in alphas:
    params.operating_condition.alpha = alpha_angle

    case = project.run_case(params=params, name=f"alpha_{alpha_angle.value}_case")

    print(f"The case ID is: {case.id} with {alpha_angle=} ")
    case_list.append(case)

print("Waiting for cases to complete...")
for case in case_list:
    case.wait()
print("All cases completed.")
[12:26:25] INFO: using: SI unit system for unit inference.
[12:26:29] INFO: Successfully submitted:
                   type   = Case
                   name   = alpha_-10_case
                   id     = case-420f149b-69b1-41f1-a8b5-20ee88f153d2
                   status = pending
           
The case ID is: case-420f149b-69b1-41f1-a8b5-20ee88f153d2 with alpha_angle=unyt_quantity(-10, 'degree')
           INFO: using: SI unit system for unit inference.
[12:26:31] INFO: Successfully submitted:
                   type   = Case
                   name   = alpha_-5_case
                   id     = case-d9f7acb8-7c1a-44b1-8672-b0bbef43859a
                   status = pending
           
The case ID is: case-d9f7acb8-7c1a-44b1-8672-b0bbef43859a with alpha_angle=unyt_quantity(-5, 'degree')
[12:26:32] INFO: using: SI unit system for unit inference.
[12:26:34] INFO: Successfully submitted:
                   type   = Case
                   name   = alpha_0_case
                   id     = case-3cc6f5d3-d7fe-4b9e-a639-0560111ca309
                   status = pending
           
The case ID is: case-3cc6f5d3-d7fe-4b9e-a639-0560111ca309 with alpha_angle=unyt_quantity(0, 'degree')
[12:26:35] INFO: using: SI unit system for unit inference.
[12:26:37] INFO: Successfully submitted:
                   type   = Case
                   name   = alpha_5_case
                   id     = case-aac5be66-37ae-452d-ac30-0f7f69d301f5
                   status = pending
           
The case ID is: case-aac5be66-37ae-452d-ac30-0f7f69d301f5 with alpha_angle=unyt_quantity(5, 'degree')
[12:26:38] INFO: using: SI unit system for unit inference.
[12:26:40] INFO: Successfully submitted:
                   type   = Case
                   name   = alpha_10_case
                   id     = case-dca4c09b-fce6-4e7e-a653-106efec6512d
                   status = pending
           
The case ID is: case-dca4c09b-fce6-4e7e-a653-106efec6512d with alpha_angle=unyt_quantity(10, 'degree')
[12:26:41] INFO: using: SI unit system for unit inference.
[12:26:43] INFO: Successfully submitted:
                   type   = Case
                   name   = alpha_12_case
                   id     = case-e55b6e47-3a45-42f8-854c-0b0d629c250a
                   status = pending
           
The case ID is: case-e55b6e47-3a45-42f8-854c-0b0d629c250a with alpha_angle=unyt_quantity(12, 'degree')
           INFO: using: SI unit system for unit inference.
[12:26:46] INFO: Successfully submitted:
                   type   = Case
                   name   = alpha_14_case
                   id     = case-fa4f47df-9363-4e34-ac66-178c41a0c30b
                   status = pending
           
The case ID is: case-fa4f47df-9363-4e34-ac66-178c41a0c30b with alpha_angle=unyt_quantity(14, 'degree')
Waiting for cases to complete...
All cases completed.

7. Report Generation#

Flow360 provides a reporting feature to automatically generate documents summarizing simulation results. A report is defined by a ReportTemplate which is populated with various items like tables, charts, and 3D scenes. The following cells will walk through the process of setting up the components of the report.

For convenience sake, we will assign fl.report namespace to rep and mark farfield as a surface we want to exclude from our report as it would skew the results.

[7]:
from flow360.plugins.report.report_items import Summary, Inputs

rep = fl.report

exclude_surfaces = ["fluid/farfield"]

Camera and View Setup#

For 3D visualizations within the report, such as surface contours or isosurfaces, specific camera views must be defined. The following code sets up a series of standard camera positions (top, bottom, front, etc.) that will be used to generate consistent imagery for each case in the sweep.

[8]:
top_camera = rep.TopCamera(
    pan_target=(3.5, 0, -0.5), dimension=15, dimension_dir="height"
)
bottom_camera = rep.BottomCamera(
    pan_target=(3.5, 0, -0.5), dimension=15, dimension_dir="height"
)
front_camera = rep.FrontCamera(
    pan_target=(3.5, 0, -0.5), dimension=15, dimension_dir="width"
)
rear_camera = rep.RearCamera(
    pan_target=(3.5, 0, -0.5), dimension=15, dimension_dir="width"
)
left_camera = rep.LeftCamera(
    pan_target=(3.5, 0, -0.5), dimension=10, dimension_dir="width"
)
right_camera = rep.Camera(
    pan_target=(3.5, 0, -0.5),
    position=(0.0, -1.0, 0.0),
    look_at=(0.0, 0.0, 0.0),
    up=(0.0, 0.0, 1.0),
    dimension=10,
    dimension_dir="width",
)
front_left_top_camera = rep.FrontLeftTopCamera(
    pan_target=(3.5, 0, -0.5), dimension=15, dimension_dir="width"
)
rear_right_bottom_camera = rep.RearRightBottomCamera(
    pan_target=(3.5, 0, -0.5), dimension=15, dimension_dir="width"
)

geo_cameras = [
    top_camera,
    bottom_camera,
    front_camera,
    rear_camera,
    left_camera,
    right_camera,
    front_left_top_camera,
    rear_right_bottom_camera,
]

geo_camera_names = [
    "top_camera",
    "bottom_camera",
    "front_camera",
    "rear_camera",
    "left_camera",
    "right_camera",
    "front_left_top_camera",
    "rear_right_bottom_camera",
]

Data Items and Averaging#

DataItem objects are used to specify which results to extract for tables and charts. They can be combined with operations, such as Average, to perform post-processing. Here, we define DataItems for aerodynamic coefficients and apply a time-averaging operation over the last 10% of the simulation steps.

[9]:
avg = rep.Average(fraction=0.1)

force_list = [
    "CD",
    "CL",
    "CFx",
    "CFy",
    "CFz",
    "CMx",
    "CMy",
    "CMz",
]

CD = rep.DataItem(
    data="surface_forces/totalCD", exclude=exclude_surfaces, title="CD", operations=avg
)
CL = rep.DataItem(
    data="surface_forces/totalCL", exclude=exclude_surfaces, title="CL", operations=avg
)
CFX = rep.DataItem(
    data="surface_forces/totalCFx",
    exclude=exclude_surfaces,
    title="CFx",
    operations=avg,
)
CFY = rep.DataItem(
    data="surface_forces/totalCFy",
    exclude=exclude_surfaces,
    title="CFy",
    operations=avg,
)
CFZ = rep.DataItem(
    data="surface_forces/totalCFz",
    exclude=exclude_surfaces,
    title="CFz",
    operations=avg,
)
CMX = rep.DataItem(
    data="surface_forces/totalCMx",
    exclude=exclude_surfaces,
    title="CMx",
    operations=avg,
)
CMY = rep.DataItem(
    data="surface_forces/totalCMy",
    exclude=exclude_surfaces,
    title="CMy",
    operations=avg,
)
CMZ = rep.DataItem(
    data="surface_forces/totalCMz",
    exclude=exclude_surfaces,
    title="CMz",
    operations=avg,
)

table_data = [
    CD,
    CL,
    CFX,
    CFY,
    CFZ,
    CMX,
    CMY,
    CMZ,
]

Assembling the Report Items#

With the cameras and data items defined, the individual components of the report can be assembled. We will create generate_report_items function that creates a list of report items based on the provided boolean flags. This modular approach allows for easy customization of the report content.

[10]:
def generate_report_items(
    params,
    table_data,
    force_list,
    geo_cameras,
    geo_camera_names,
    front_left_top_camera,
    exclude_surfaces,
    include_geometry: bool,
    include_general_tables: bool,
    include_residuals: bool,
    include_cfl: bool,
    include_forces_moments_table: bool,
    include_forces_moments_charts: bool,
    include_forces_moments_alpha_charts: bool,
    include_forces_moments_beta_charts: bool,
    include_cf_vec: bool,
    include_cp: bool,
    include_yplus: bool,
    include_qcriterion: bool,
):
    items = []

    if params.time_stepping.type_name == "Unsteady":
        step_type = "physical_step"
    else:
        step_type = "pseudo_step"

    for model in params.models:
        if model.type == "Fluid":
            turbulence_solver = model.turbulence_model_solver.type_name

    if include_geometry:
        geometry_screenshots = [
            rep.Chart3D(
                section_title="Geometry",
                items_in_row=2,
                force_new_page=True,
                show="boundaries",
                camera=front_left_top_camera,
                exclude=exclude_surfaces,
                fig_name="Geometry_view",
            )
        ]
        items.extend(geometry_screenshots)

    if include_general_tables:
        items.append(Summary())
        items.append(Inputs())

    if include_forces_moments_table:
        table = rep.Table(
            data=table_data,
            section_title="Quantities of interest",
        )
        items.append(table)

    if include_residuals:
        residual_chart = rep.NonlinearResiduals(
            force_new_page=True,
            section_title="Nonlinear residuals",
            fig_name=f"nonlin-res_fig",
        )
        items.append(residual_chart)

    if include_cfl and params.time_stepping.CFL.type == "adaptive":
        cfl_chart = rep.Chart2D(
            x=f"cfl/{step_type}",
            y=["cfl/0_NavierStokes_cfl", f"cfl/1_{turbulence_solver}_cfl"],
            force_new_page=True,
            section_title="CFL",
            fig_name="cfl_fig",
            y_log=True,
        )
        items.append(cfl_chart)

    if include_forces_moments_charts:
        force_charts = [
            rep.Chart2D(
                x=f"surface_forces/{step_type}",
                y=f"surface_forces/total{force}",
                force_new_page=True,
                section_title="Forces/Moments",
                fig_name=f"{force}_fig",
                exclude=exclude_surfaces,
                ylim=rep.SubsetLimit(subset=(0.5, 1), offset=0.25),
            )
            for force in force_list
        ]
        items.extend(force_charts)

    if include_forces_moments_alpha_charts:
        force_alpha_charts = [
            rep.Chart2D(
                x=f"params/operating_condition/alpha",
                y=f"total_forces/averages/{force}",
                force_new_page=True,
                section_title="Averaged Forces/Moments against alpha",
                fig_name=f"{force}_alpha_fig",
            )
            for force in force_list
        ]
        items.extend(force_alpha_charts)

    if include_forces_moments_beta_charts:
        force_beta_charts = [
            rep.Chart2D(
                x=f"params/operating_condition/beta",
                y=f"total_forces/averages/{force}",
                force_new_page=True,
                section_title="Averaged Forces/Moments against beta",
                fig_name=f"{force}_beta_fig",
            )
            for force in force_list
        ]
        items.extend(force_beta_charts)

    if include_yplus:
        y_plus_screenshots = [
            rep.Chart3D(
                caption=rep.PatternCaption(pattern=f"y+_{camera_name}_[case.name]"),
                show="boundaries",
                field="yPlus",
                exclude=exclude_surfaces,
                limits=(0, 5),
                camera=camera,
                fig_name=f"yplus_{camera_name}_fig",
                fig_size=1,
            )
            for camera_name, camera in zip(geo_camera_names, geo_cameras)
        ]
        items.extend(y_plus_screenshots)

    if include_cp:
        cp_screenshots = [
            rep.Chart3D(
                caption=rep.PatternCaption(pattern=f"Cp_{camera_name}_[case.name]"),
                show="boundaries",
                field="Cp",
                exclude=exclude_surfaces,
                limits=(-1, 1),
                camera=camera,
                fig_name=f"cp_{camera_name}_fig",
                fig_size=1,
            )
            for camera_name, camera in zip(geo_camera_names, geo_cameras)
        ]
        items.extend(cp_screenshots)

    if include_cf_vec:
        cfvec_screenshots = [
            rep.Chart3D(
                caption=rep.PatternCaption(pattern=f"Cf_vec_{camera_name}_[case.name]"),
                show="boundaries",
                field="CfVec",
                mode="lic",
                exclude=exclude_surfaces,
                limits=(0, 0.025),
                camera=camera,
                fig_name=f"cfvec_{camera_name}_fig",
                fig_size=1,
            )
            for camera_name, camera in zip(geo_camera_names, geo_cameras)
        ]
        items.extend(cfvec_screenshots)

    if include_qcriterion:
        qcriterion_screenshots = [
            rep.Chart3D(
                caption=rep.PatternCaption(
                    pattern=f"Isosurface_q_criterion_{camera_name}_[case.name]"
                ),
                show="isosurface",
                iso_field="qcriterion",
                exclude=exclude_surfaces,
                limits=(0, 0.8),
                camera=camera,
                fig_name=f"qcriterion_{camera_name}_fig",
                fig_size=1,
            )
            for camera_name, camera in zip(geo_camera_names, geo_cameras)
        ]
        items.extend(qcriterion_screenshots)

    return items

Now we can easily choose which items to include in our report.

[11]:
items = generate_report_items(
    params,
    table_data,
    force_list,
    geo_cameras,
    geo_camera_names,
    front_left_top_camera,
    exclude_surfaces,
    include_geometry=True,
    include_general_tables=True,
    include_residuals=True,
    include_cfl=True,
    include_forces_moments_table=True,
    include_forces_moments_charts=True,
    include_forces_moments_alpha_charts=True,
    include_forces_moments_beta_charts=True,
    include_cf_vec=True,
    include_cp=True,
    include_yplus=True,
    include_qcriterion=True,
)

Creating and Downloading the Report#

Finally, the ReportTemplate is instantiated with the list of items. The create_in_cloud method initiates the report generation process on the Flow360 platform. The script then waits for the report to be completed and downloads it as a PDF file.

[12]:
report = rep.ReportTemplate(
    title="Sweep Template Report",
    items=items,
    settings=rep.Settings(dpi=150),
)

report = report.create_in_cloud(
    "sweep-script-report",
    case_list,
)

report.wait()
report.download("report.pdf")
print("Report downloaded as report.pdf")
[12:34:55] INFO: Saved to report.pdf
Report downloaded as report.pdf