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
| File | Contents |
|---|---|
caradonna_tung.msh.h5 | the mesh as Fluent wrote it |
caradonna_tung.h5 | the same mesh, converted for zCFD |
caradonna_tung_zone.py | the zone helper written by the conversion |
caradonna_tung.py | the 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 mathsuppliesmath.radians, which turns the periodic angles below from degrees to radians.from caradonna_tung_zone import to_idimports the zone helper written by the mesh conversion in Step 1.to_id(["farfield"])returns the zone numbers of the boundaries the mesh generator namedfarfield, 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, withsolverandmodelat 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.gammaandgas constant:1.4and287.0J/kg/K, the standard values for air.sutherlands const:110.4K, for Sutherland's law, which sets how the viscosity changes with temperature.prandtl noandturbulent prandtl no:0.72and0.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.
temperatureandpressure:289.75K and103037.0Pa, 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.0m, the blade chord.turbulence intensityandeddy viscosity ratio:0.01and0.1, the turbulence in the incoming air: very little here.
IC_2
The still air the rotor turns in.
temperatureandpressure: asIC_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 intensityandeddy viscosity ratio: asIC_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, whichtransformssets 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_Fyandblade_Fzfor the force,blade_Mxtoblade_Mzfor 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 throughto_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_Tqandblade_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: velocityV, pressurep, temperatureT, densityrho, Mach numbermachandyplus, which measures how close the first cell is to the wall. The pressure coefficientcpis 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 frompinstead.volume variables: the same flow variables, andcell_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.873rad/s, the rotor speed: 476 revolutions a minute, which at a radius of 6 m gives the tip Mach number ofIC_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.typeandkind:"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.typeandkind:"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.typeandkind:"interface","periodic".connection:{"boundary": "BC_5"}, its partner.transform: the rotation that maps this face onto its partner:type"rotated",theta180° in radians, aboutaxisz throughorigin.
See Periodic.
BC_5
The other flat face.
zones:periodic1.typeandkind:"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
- Activate your environment:
source ./<ZCFD_INSTALL_PATH>/bin/activate
- Check the control file for errors:
cd ~/tutorial_7_rotor_in_hover/
validate_input caradonna_tung.py
- 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
omegais 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
| File | Contents |
|---|---|
caradonna_tung.log | the solver log |
caradonna_tung_status.yaml | the run status |
caradonna_tung_OUTPUT/rotor/rotor_report.csv | residuals and forces every 10 cycles |
caradonna_tung_OUTPUT/rotor/rotor.vtkhdf | the volume solution |
caradonna_tung_OUTPUT/rotor/rotor_wall.vtkhdf | the 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

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.

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, , where 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:

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, : the force pushing the section up, relative to its dynamic pressure. The table compares from zCFD with worked out the same way from the measured pressures:
| r/R | zCFD | Experiment |
|---|---|---|
| 0.50 | 0.21 | 0.20 |
| 0.68 | 0.27 | 0.28 |
| 0.80 | 0.33 | 0.30 |
| 0.89 | 0.31 | 0.32 |
| 0.96 | 0.23 | 0.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.

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