v2026.11.16174
Tutorials / 7 · Rotor in hover

Rotor in hover

What is in this tutorial?

This tutorial simulates a helicopter-style rotor hovering in still air, and compares the pressure on its blades with wind-tunnel measurements. Along the way it shows three things you will need for your own cases: converting a mesh made by another program, referring to boundaries by name, and two ways of cutting the cost of a rotor run. The first is a rotating frame, which lets a spinning rotor be solved as a flow that does not change with time. The second is a periodic boundary, which lets you model only one of the identical blades.

The rotor is the one Caradonna and Tung tested at NASA in 19811. It has two straight blades, each the same NACA 0012 aerofoil shape from root to tip, six times as long as they are wide, set at a collective pitch of 8°: every blade meets the air at the same 8° angle. The tips move at 0.877 times the speed of sound (Mach 0.877), fast enough for a shock wave, a sudden jump in pressure, to form near the tip. The experiment measured the pressure on the blade surface at five distances from the shaft, and those measurements are the reference here.

What you need?

Work through Simple aerofoil first: this tutorial relies on it for running zCFD, reading the log and monitoring a run in zMon. The mesh has 447,468 cells, and the run takes about nine and a half minutes on an NVIDIA RTX 3060. The Python step in Step 5 also uses ParaView's Python modules, which come with zCFD.

Step 1. Download the files

The tutorial bundle can be downloaded here.

cd ~
wget https://zcfd.zenotech.com/v2026.11.16174/tutorial_7_rotor_in_hover.tgz
tar xzf tutorial_7_rotor_in_hover.tgz
cd tutorial_7_rotor_in_hover
FileContents
caradonna_tung.msh.h5the mesh as Fluent wrote it
caradonna_tung.h5the same mesh, converted for zCFD
caradonna_tung_zone.pythe zone helper written by the conversion
caradonna_tung.pythe control file
data/cp-0p50.txt …the measured surface pressure at r/R = 0.50, 0.68, 0.80, 0.89 and 0.96

Converting the mesh

zCFD reads its own mesh format. A mesh from another program is converted once, before the first run:

convert_mesh caradonna_tung.msh.h5 caradonna_tung.h5

The converter recognises the Fluent CFF format from the file name, and writes two files: the zCFD mesh, caradonna_tung.h5, and a zone helper, caradonna_tung_zone.py. The bundle already holds both, so this step can be skipped; running it replaces them with identical copies. Preparing a mesh lists the converters for the other formats.

A mesh's boundary faces are grouped into zones, each with a number. The zone helper maps the names the boundaries were given in the mesh generator to those numbers:

zone_name_to_id={
    "farfield": 48,
    "periodic2": 50,
    "core": 51,
    "lower_surface": 52,
    "upper_surface": 53,
    "tip": 54,
    "periodic1": 732,
    "interior--enclosure-enclosure": 1909,
}

Its to_id function turns a list of names into a list of numbers. The control file imports it, so every boundary condition below names its boundaries as the mesh generator did. See Zone identifiers.

Step 2. Walk through the input

The control file, caradonna_tung.py, is a short Python script. All it does is build one Python dictionary, called parameters, that tells zCFD everything about the run: the physics, the numerical method, what to write out, and the mesh with its boundaries. This step walks through that dictionary from top to bottom. Each heading below is the name of a key, written exactly as it appears in the file, and the headings nest the same way the dictionary does: numerical scheme, for example, sits inside solver. Under each heading you'll find what that part controls, what each setting means, and why it has the value it has here. Each section ends with a link to that part's reference page, which lists every setting it accepts and its default; you don't need it to follow the tutorial. On a wide screen the whole control file sits on the right, with the part being described highlighted as you scroll.

Two parts are new since Simple aerofoil: the rotating frame, under the model's transforms, and the pair of periodic boundaries among its boundary conditions. The file also starts with two Python import lines, which get their own heading.

import math

from caradonna_tung_zone import to_id

parameters = {
    "config version": 2,
    "solver": {
        "solver settings": {
            "type": "compressible",
        },
        "equations": {
            "type": "rans",
            "turbulence": {"model": "sst"},
            "precondition": False,
        },
        "fluid properties": {
            "material": "air",
            "gamma": 1.4,
            "gas constant": 287.0,
            "sutherlands const": 110.4,
            "prandtl no": 0.72,
            "turbulent prandtl no": 0.9,
        },
        "reference conditions": {
            "IC_1": {
                "temperature": 289.75,
                "pressure": 103037.0,
                "v": {"vector": [0.0, 0.0, -1.0], "mach": 0.877},
                "reynolds no": 3.93e6,
                "reference length": 1.0,
                "turbulence intensity": 0.01,
                "eddy viscosity ratio": 0.1,
            },
            "IC_2": {
                "temperature": 289.75,
                "pressure": 103037.0,
                "v": {"vector": [0.0, 0.0, -1.0], "mach": 0.00001},
                "reynolds no": 3.93e6 * 0.00001 / 0.87,
                "reference length": 1.0,
                "turbulence intensity": 0.01,
                "eddy viscosity ratio": 0.1,
            },
        },
        "initialisation": {"initial conditions": "IC_2", "reference": "IC_1"},
        "numerical scheme": {
            "order": "second",
            "limiter": "van albada",
        },
        "time settings": {"type": "steady"},
        "convergence control": {
            "scheme": {"name": "lu-sgs"},
            "cfl": {
                "cfl": 20.0,
                "cfl ramp": [{"type": "exponential", "factor": 1.05, "initial": 0.1}],
            },
            "cycles": 1500,
        },
        "output settings": {
            "report": {
                "frequency": 10,
                "forces": {
                    "FR_1": {
                        "name": "blade",
                        "zones": to_id(["lower_surface", "upper_surface", "tip"]),
                    },
                },
            },
            "solution": {
                "format": "vtk",
                "surface variables": ["V", "p", "T", "rho", "mach", "yplus"],
                "volume variables": ["V", "p", "T", "rho", "mach", "cell_velocity"],
            },
        },
    },
    "model": {
        "rotor": {
            "mesh": "caradonna_tung.h5",
            "transforms": {
                "motion": {
                    "type": "rotating",
                    "omega": 49.873,
                    "axis": [0.0, 0.0, 1.0],
                    "origin": [0.0, 0.0, 0.0],
                    "moving mesh": False,
                }
            },
            "boundary conditions": {
                "BC_1": {
                    "zones": to_id(["lower_surface", "upper_surface", "tip"]),
                    "type": "wall",
                    "kind": "wall function",
                },
                "BC_2": {
                    "zones": to_id(["farfield"]),
                    "type": "farfield",
                    "kind": "riemann",
                    "condition": "IC_2",
                },
                "BC_3": {"zones": to_id(["core"]), "type": "symmetry"},
                "BC_4": {
                    "zones": to_id(["periodic2"]),
                    "type": "interface",
                    "kind": "periodic",
                    "connection": {"boundary": "BC_5"},
                    "transform": {
                        "type": "rotated",
                        "theta": math.radians(180.0),
                        "axis": [0.0, 0.0, 1.0],
                        "origin": [0.0, 0.0, 0.0],
                    },
                },
                "BC_5": {
                    "zones": to_id(["periodic1"]),
                    "type": "interface",
                    "kind": "periodic",
                    "connection": {"boundary": "BC_4"},
                    "transform": {
                        "type": "rotated",
                        "theta": math.radians(-180.0),
                        "axis": [0.0, 0.0, 1.0],
                        "origin": [0.0, 0.0, 0.0],
                    },
                },
            },
        }
    },
}

from caradonna_tung_zone import to_id

The control file is Python, so it can import helpers before it defines parameters.

  • import math supplies math.radians, which turns the periodic angles below from degrees to radians.
  • from caradonna_tung_zone import to_id imports the zone helper written by the mesh conversion in Step 1. to_id(["farfield"]) returns the zone numbers of the boundaries the mesh generator named farfield, so the boundary conditions name their boundaries rather than number them.

See Zone identifiers.

config version

Says which layout of control file this is, so zCFD knows how to read the rest.

  • config version: 2, the current layout, with solver and model at the top level.

solver

The settings shared by the whole simulation: which solver runs, what it solves, the fluid, the flow states, and how the run marches and what it writes.

solver settings

Chooses the solver.

  • type: "compressible": the solver that lets the air's density change. The tip moves at Mach 0.877, fast enough to squeeze the air noticeably.

See solver settings.

equations

Sets the equations being solved.

  • type: "rans", the Reynolds-averaged Navier–Stokes equations. These solve for the average flow and represent the effect of turbulence, the small chaotic eddies, with a turbulence model rather than resolving every eddy.
  • turbulence: {"model": "sst"}, Menter's SST model, a widely used general-purpose turbulence model.
  • precondition: False: low-speed preconditioning, a technique that helps the compressible solver with slow flows, is off. The flow the blades see is fast, up to Mach 0.877 at the tip.

See equations and Choosing a turbulence model.

fluid properties

Describes the gas.

  • material: "air", as an ideal gas.
  • gamma and gas constant: 1.4 and 287.0 J/kg/K, the standard values for air.
  • sutherlands const: 110.4 K, for Sutherland's law, which sets how the viscosity changes with temperature.
  • prandtl no and turbulent prandtl no: 0.72 and 0.9, which relate how fast heat spreads to how fast momentum does, in smooth and turbulent flow.

See fluid properties.

reference conditions

Defines named flow states, each a set of values such as temperature, pressure and velocity. Other parts of the file refer to them by name: to fill the domain at the start, to set conditions at boundaries, and to scale the equations.

IC_1

The conditions at the blade tip, used as the reference.

  • temperature and pressure: 289.75 K and 103037.0 Pa, the test conditions.
  • v: {"vector": [0.0, 0.0, -1.0], "mach": 0.877}: the tip Mach number, 299 m/s.
  • reynolds no: 3.93e6, the Reynolds number at the tip: the ratio of the air's inertia to its viscosity, based on the blade's width (its chord). The solver sets the viscosity to give this value.
  • reference length: 1.0 m, the blade chord.
  • turbulence intensity and eddy viscosity ratio: 0.01 and 0.1, the turbulence in the incoming air: very little here.
IC_2

The still air the rotor turns in.

  • temperature and pressure: as IC_1.
  • v: {"vector": [0.0, 0.0, -1.0], "mach": 0.00001}: a speed so small that the air is, in effect, still.
  • reynolds no: 3.93e6 * 0.00001 / 0.87: IC_1's Reynolds number scaled down with the speed, so that both states have very nearly the same viscosity.
  • reference length, turbulence intensity and eddy viscosity ratio: as IC_1.

See reference conditions.

initialisation

Chooses which states start the run and scale it.

  • initial conditions: "IC_2": the air starts still.
  • reference: "IC_1": internally the solver works with values divided by a reference velocity. Still air would give it almost none to divide by, so the tip conditions provide one that matches the flow the blades see.

See initialisation and Reference state.

numerical scheme

Sets how the equations are discretised in space: how the smooth flow is represented by one value in each cell of the mesh.

  • order: "second": the solver estimates how each variable, the turbulence variables included, varies across each cell, which is more accurate than treating it as constant.
  • limiter: "van albada": near a sharp change such as the shock on the outer blade, that estimate can overshoot and produce unphysical wiggles; the limiter damps them.

See numerical scheme and Choosing a limiter.

time settings

Says what kind of run this is.

  • type: "steady": the run looks for a flow that does not change with time. In hover that is true for an observer turning with the blades, which transforms sets up.

See time settings.

convergence control

Sets how the solver reaches the steady solution. It starts from a guess and improves it once per cycle, or iteration, until the flow stops changing; the run has then converged.

scheme
  • name: "lu-sgs": each cycle updates the solution with a cheap sweep through the mesh (lower–upper symmetric Gauss–Seidel). A cycle costs less than with the "implicit euler" scheme of Simple aerofoil, but it needs a smaller CFL number, so more cycles.
cfl
  • cfl: 20.0, the CFL number the run ramps up to. The CFL number sets how big a step each cycle takes towards the answer: too large and the run becomes unstable, too small and it needs many cycles.
  • cfl ramp: [{"type": "exponential", "factor": 1.05, "initial": 0.1}]: start at 0.1 and multiply by 1.05 each cycle, so the early cycles, far from the answer, take small steps.
cycles
  • cycles: 1500: the run stops after 1500 cycles. Step 4 shows the thrust, the upward force on the rotor, has settled by then.

See convergence control and CFL control.

output settings

Sets what zCFD writes, and how often. See output settings.

report

The report is a table with one row per report cycle, written to caradonna_tung_OUTPUT/rotor/rotor_report.csv and plotted by zMon.

  • frequency: 10: a row every 10 cycles, holding the residuals and the forces.

  • forces: one block, FR_1, which sums the forces on a set of zones:

    • name: "blade", which prefixes its report columns: blade_Fx, blade_Fy and blade_Fz for the force, blade_Mx to blade_Mz for the moment, and their pressure (Fp, Mp) and friction (Ff, Mf) parts.
    • zones: the lower surface, upper surface and tip of the blade, by name through to_id.

    On a rotating model the block also reports the thrust (the force along the shaft), the torque (the turning resistance about it) and the power: blade_rotor_Thrust, blade_rotor_Tq and blade_rotor_Tqp, named after the block and the model.

solution

The flow solution, written for visualisation and post-processing.

  • format: "vtk": VTK HDF files, .vtkhdf, which ParaView opens.
  • surface variables: velocity V, pressure p, temperature T, density rho, Mach number mach and yplus, which measures how close the first cell is to the wall. The pressure coefficient cp is left out. It compares the local pressure with the pressure of the oncoming flow, and in hover there is no oncoming flow, so Step 5 works out the experiment's version from p instead.
  • volume variables: the same flow variables, and cell_velocity, the velocity of the turning frame at each cell.

No frequency is given, so the solution is written once, at the end of the run. Output variables lists the others.

model

One entry per mesh, each with its own placement, motion and boundary conditions. See model.

rotor

The name of the only model. It names the output folder, caradonna_tung_OUTPUT/rotor/, and the files in it, and appears in the thrust column.

mesh
  • mesh: "caradonna_tung.h5", the mesh converted in Step 1. It holds one blade in a half-cylinder.
transforms

Places and moves the mesh. Here it holds only a motion, which sets up a rotating frame.

In hover the air far from the rotor is still, and each blade always sees the same flow. To an observer standing still the flow keeps changing as the blades sweep past, but to an observer turning with the blades it does not change at all. Solving the flow as that turning observer sees it, in a rotating frame, turns the rotor into a steady problem, which is far cheaper than following the blades round in time.

  • motion:
    • type: "rotating".
    • omega: 49.873 rad/s, the rotor speed: 476 revolutions a minute, which at a radius of 6 m gives the tip Mach number of IC_1.
    • axis: [0.0, 0.0, 1.0], the rotor shaft, along z.
    • origin: [0.0, 0.0, 0.0], a point on the shaft.
    • moving mesh: False: the mesh stays where it is. Instead, the solver adds to every cell the extra forces a turning observer feels, the centrifugal and Coriolis forces.

The frame is valid while the flow is steady in it. A rotor in forward flight, or one interacting with a body that does not turn with it, sees a flow that changes as it goes round, and needs a moving mesh in a time-accurate run. See transforms and Placing and moving a mesh.

boundary conditions

Assigns a condition to each zone of boundary faces. See boundary conditions and Choosing boundary conditions.

BC_1

The blade.

  • zones: the lower surface, upper surface and tip.
  • type and kind: "wall", "wall function": the air sticks to the blade, and the thin layer next to the surface, where the speed drops to zero, is represented by a formula, a wall function, rather than resolved with very fine cells. See Choosing a wall model.
BC_2

The outer boundary.

  • zones: farfield.
  • type and kind: "farfield", "riemann": the outer edge of the domain, far from the rotor. Air can pass in and out, and pressure waves leave without bouncing back in.
  • condition: "IC_2", the still air.
BC_3

The hub.

  • zones: core.
  • type: "symmetry": no flow passes through it.
BC_4

One flat face of the half-cylinder.

The two blades are identical and half a turn apart, so the flow round one blade is a copy of the flow round the other, turned through 180°. The mesh therefore holds only half the rotor, one blade in a half-cylinder, and two periodic boundaries join its two flat faces: flow leaving through one comes back in through the other, turned through 180°. Modelling one blade halves the cost of the run.

  • zones: periodic2.
  • type and kind: "interface", "periodic".
  • connection: {"boundary": "BC_5"}, its partner.
  • transform: the rotation that maps this face onto its partner: type "rotated", theta 180° in radians, about axis z through origin.

See Periodic.

BC_5

The other flat face.

  • zones: periodic1.
  • type and kind: "interface", "periodic".
  • connection: {"boundary": "BC_4"}.
  • transform: the same rotation the other way, theta −180°: each side carries the rotation that maps it onto its partner, so the two angles are opposite.

Step 3. Run

  1. Activate your environment:
source ./<ZCFD_INSTALL_PATH>/bin/activate
  1. Check the control file for errors:
cd ~/tutorial_7_rotor_in_hover/
validate_input caradonna_tung.py
  1. Run zCFD in the case directory:
run_zcfd -c caradonna_tung.py

These are extracts from the log of a run of this tutorial, with ... marking lines left out:

Parameters validated successfully
...
 Reading file caradonna_tung.h5
 Global Mesh Stats: Cells: 447468 Faces: 2529579
...
 Setup periodic boundaries
...
 Dimensional Conditions
 ----------------------
  Pressure:     103037 Pa
  Temperature:  289.75 K
  Density:      1.23905 kg/m^3
  Viscosity:    9.43434e-05 Ns/m^2 @ 289.75 K
  Speed:        299.238 m/s
...
  Mach:         0.877
  Reynolds No:  3.93e+06 @ 289.75 K
...
LU-SGS: rotating-frame Coriolis diagonal enabled (omega = 0.172947, cells = 447468)
...
Starting solve loop
...
Cycle     10 | Step    0 t=0s | CFL   0.155 | MG 1 |   375.3 ms | Total     3.779s
+------------------------------------------------------------------------------------------+
| Report: Cycle 10  Wall time: 3.779s                                                      |
...
| Residuals:                                                                               |
|   rho       1.1218E+02  rhoV[0]   2.6131E+01  rhoV[1]   9.9169E-01  rhoV[2]   2.1170E+01 |
|   rhoE      4.1126E+02  rhok      5.6513E-01  rhoOmega  7.7031E+06                       |
+------------------------------------------------------------------------------------------+
...
 Solve Complete: 1500 cycles in 9m 24.5s (avg 376.4 ms/cycle)
...
zCFD finished: all requested cycles are complete (1500 in total).
  • The mesh is read and its periodic faces paired.
  • The dimensional conditions are those of the reference state, the blade tip: 299 m/s, Mach 0.877.
  • The LU-SGS line confirms the rotating frame is active; its omega is the control file's 49.873 rad/s in the solver's non-dimensional units.
  • The CFL number is still ramping up at cycle 10, from 0.1 towards 20.
  • The run takes about nine and a half minutes on an NVIDIA RTX 3060.

Output files

FileContents
caradonna_tung.logthe solver log
caradonna_tung_status.yamlthe run status
caradonna_tung_OUTPUT/rotor/rotor_report.csvresiduals and forces every 10 cycles
caradonna_tung_OUTPUT/rotor/rotor.vtkhdfthe volume solution
caradonna_tung_OUTPUT/rotor/rotor_wall.vtkhdfthe blade surface solution

Step 4. Monitor

Open the run in zMon to watch the residuals and forces, while it runs or after it has finished:

zmon caradonna_tung.py

Monitoring a run describes each panel. The plots below are the residual and thrust panels as zMon draws them.

Residuals

Residuals of the rotor run against cycle
Residuals of the rotor run against cycle

A residual measures how far the current solution is from satisfying one of the equations; as the run converges, the residuals fall. The mean-flow residuals fall by between three and five and a half orders of magnitude (factors of a thousand to a few hundred thousand) over the 1500 cycles, and the turbulence residual rhok by about four and a half. The rhoOmega residual jumps about throughout and is no guide to convergence here; the thrust is.

Thrust

The force block is named blade and the model rotor, so the thrust is in the column blade_rotor_Thrust.

Thrust on the blade against cycle
Thrust on the blade against cycle

The thrust overshoots as the flow starts up, then settles. It is within 1% of its final value by cycle 1300, and changes by less than 1% over the last 200 cycles.

Step 5. Post-process

Python

First activate your zCFD environment, which provides Python with zutil, h5py and ParaView:

source <ZCFD_INSTALL_PATH>/bin/activate

The pressure coefficient, Cp, compares the pressure at a point on the blade with a reference pressure, divided by the dynamic pressure, ½ × density × speed², which measures how hard the air is pushing. The experiment refers each station at radius r to the speed of the blade section there, Ωr\Omega r, where Ω\Omega is the rotor speed. This script cuts the blade surface at r/R = 0.80, works out Cp round the section that way, and plots it against the measured values. It uses ParaView's Python modules to make the cut. Save it as plot_blade_cp.py beside the control file:

import matplotlib.pyplot as plt
import numpy as np
import paraview.simple as pvs

from zutil.post import clean_vtk
from zutil.post.post import slice_metric

# Values from the control file
tip_radius = 6.0  # m
omega = 49.873  # rad/s
p_ref = 103037.0  # Pa
rho_ref = p_ref / (287.0 * 289.75)  # kg/m^3, the still air

# The experiment refers Cp to the blade section's own dynamic pressure
radius = 0.80 * tip_radius
q_local = 0.5 * rho_ref * (omega * radius) ** 2

# Cut the blade surface at r/R = 0.80 and read the pressure round the section
wall = clean_vtk(pvs.VTKHDFReader(FileName="caradonna_tung_OUTPUT/rotor/rotor_wall.vtkhdf"))
profile = slice_metric(wall, [0.0, 1.0, 0.0], [0.0, radius, 0.0], "p")

# The measured file stores -Cp against x/c
measured = np.loadtxt("data/cp-0p80.txt", skiprows=1)

fig, ax = plt.subplots(figsize=(8, 4.5))
for block in profile.values():
    cp = (np.asarray(block["p"]) - p_ref) / q_local
    ax.plot(block["chord"], cp, ".", label="zCFD")
ax.plot(measured[:, 0], -measured[:, 1], "o", mfc="none", label="Experiment")
ax.invert_yaxis()
ax.set_xlabel("x/c")
ax.set_ylabel("Cp")
ax.legend()
fig.savefig("blade_cp.png", dpi=200, bbox_inches="tight")

and run it:

python plot_blade_cp.py

It writes blade_cp.png:

Pressure coefficient at r/R = 0.80 from zCFD against the experiment
Pressure coefficient at r/R = 0.80 from zCFD against the experiment

Cp is plotted with negative values upwards, as is usual for aerofoils, so the low-pressure upper surface is the top curve. zCFD follows the experiment closely on the lower surface and in the suction peak near the leading edge.

Adding up the pressure all round each section gives the normal-force coefficient, CnC_n: the force pushing the section up, relative to its dynamic pressure. The table compares CnC_n from zCFD with CnC_n worked out the same way from the measured pressures:

r/RzCFDExperiment
0.500.210.20
0.680.270.28
0.800.330.30
0.890.310.32
0.960.230.28

The loading is predicted to within 0.03 at the inner stations. The station nearest the tip, r/R = 0.96, is furthest from the experiment. Each blade trails a swirling tip vortex, and the vortex of the blade ahead passes close to this station. This mesh captures that vortex for only about a quarter turn behind the blade before its cells grow too coarse, so the tutorial makes no claim for the wake.

ParaView

Open caradonna_tung_OUTPUT/rotor/rotor_wall.vtkhdf in ParaView to see the blade. To colour it by the pressure coefficient of the experiment, apply a Calculator with the expression

(p-103037)/(0.5*1.2390*49.873^2*(coordsX^2+coordsY^2))

where 1.2390 kg/m³ is the density of the still air. To see both blades, apply a Transform that rotates the surface by 180° about z, and show it alongside the original. The volume solution, caradonna_tung_OUTPUT/rotor/rotor.vtkhdf, holds the velocity, from which a slice through the rotor axis shows the air driven down through the disc.

The Caradonna–Tung rotor in hover, coloured by local-section Cp, with the air driven down below it
The Caradonna–Tung rotor in hover, coloured by local-section Cp, with the air driven down below it

:::

Citations

Footnotes

  1. Caradonna, F. X. and Tung, C., Experimental and Analytical Studies of a Model Helicopter Rotor in Hover, NASA TM-81232, 1981. ↩