ABL Velocity and Turbulence Intensity from a Velocity Profile

A common ABL post-processing task is to extract the mean streamwise velocity profile \(U(z)\) and the streamwise turbulence intensity \(I_u(z)\) from a line probe placed in the empty domain, and compare them against a target terrain category (e.g., EN 1991-1-4). This notebook walks through the calculation from a probe time series and shows how to overlay the simulated curves on the analytical reference.

Note. This notebook assumes a vertical line probe was configured in the AeroSim setup interface and exported as CSV after the simulation. See the ABL guided case setup for the probe configuration and Visualizing Results for the export workflow.

Probe Output Format

A vertical line probe exports two CSV files: a points file with the coordinates of each probe point, and a velocity file with the time series of ux, uy, uz at each point. For this workflow, only the streamwise component ux is needed.

File

Columns

Notes

line.points.csv

idx, x, y, z

One row per probe point, indexed by idx

line.ux.csv

time_step, 0, 1, 2, ...

One row per time step; remaining columns hold ux at each probe point (column header is the point idx)

The sampling rate fs is implied by the time step column: dt = time_step[1] - time_step[0] and fs = 1 / dt.

Step 1: Load the Probe Data

[ ]:
import numpy as np
import pandas as pd

points_path = "line.line_profile line.points.csv"
ux_path = "line.line_profile line.ux.csv"

df_pts = pd.read_csv(points_path, index_col="idx")
df_ux = pd.read_csv(ux_path)

time_steps = df_ux["time_step"].to_numpy(dtype=np.float64)
df_ux.drop(columns=["time_step"], inplace=True)
df_ux.columns = df_ux.columns.astype(int)

z = df_pts["z"].to_numpy()
dt = time_steps[1] - time_steps[0]
print(f"z range: {z.min():.1f} - {z.max():.1f} m  |  dt = {dt:.4f} s")

Step 2: Compute Mean Velocity and Turbulence Intensity

For each probe point, take the time mean and standard deviation of the ux time series. Turbulence intensity is the ratio \(\sigma_u / U\).

\[U(z) = \overline{u_x(z, t)}, \qquad \sigma_u(z) = \sqrt{\overline{u_x'(z, t)^2}}, \qquad I_u(z) = \frac{\sigma_u(z)}{U(z)}\]
[ ]:
u_mean = df_ux.mean(axis=0).to_numpy()
u_rms  = df_ux.std(axis=0).to_numpy()
Iu     = u_rms / u_mean

Tip. Discard the initial transient before computing statistics. A common rule of thumb is to drop the first \(T \approx L_x / U_{ref}\) of the time series, where \(L_x\) is the streamwise domain length, so that the field has been advected once across the domain before averaging starts.

Step 3: EN 1991-1-4 Reference Profiles

The Eurocode wind action standard defines five terrain categories. Each is parameterised by a roughness length \(z_0\) and a minimum height \(z_{min}\) below which the profile is held constant.

Category

\(z_0\) (m)

\(z_{min}\) (m)

Description

0

0.003

1.0

Sea, coastal areas exposed to open sea

I

0.01

1.0

Lakes or flat horizontal area with negligible vegetation

II

0.05

2.0

Low vegetation, isolated obstacles

III

0.30

5.0

Regular cover of vegetation, suburbs, forest

IV

1.00

10.0

Urban area, at least 15% covered with buildings

The mean velocity profile is

\[\frac{U(z)}{U_{ref}} = k_r \, \ln\!\left(\frac{z}{z_0}\right), \qquad k_r = 0.19 \left(\frac{z_0}{z_{0,II}}\right)^{0.07}\]

with \(z_{0,II} = 0.05\) m. Turbulence intensity is

\[I_u(z) = \frac{1}{\ln(z / z_0)} \quad \text{for } z \geq z_{min}\]

clamped to \(z_{min}\) below the minimum height.

[ ]:
EU_CATEGORIES = {
    "Cat 0":   {"z0": 0.003, "z_min": 1.0},
    "Cat I":   {"z0": 0.01,  "z_min": 1.0},
    "Cat II":  {"z0": 0.05,  "z_min": 2.0},
    "Cat III": {"z0": 0.30,  "z_min": 5.0},
    "Cat IV":  {"z0": 1.00,  "z_min": 10.0},
}
Z0_REF = 0.05  # z0_II


def eu_velocity_profile(z_arr, z0):
    kr = 0.19 * (z0 / Z0_REF) ** 0.07
    return kr * np.log(np.maximum(z_arr, 1e-9) / z0)


def eu_turbulence_intensity(z_arr, z0, z_min):
    z_eff = np.maximum(z_arr, z_min)
    return 1.0 / np.log(z_eff / z0)

Step 4: Overlay Simulation and Reference

Pick the target category, normalise the simulation by the reference velocity used in the setup interface, and plot both curves.

[ ]:
import matplotlib.pyplot as plt

category = "Cat II"
cat = EU_CATEGORIES[category]
u_ref = 20.0  # reference velocity used in the AeroSim setup [m/s]
z_eu = np.linspace(0.5, 200, 500)

fig, axes = plt.subplots(1, 2, figsize=(12, 6))

# Mean velocity
axes[0].plot(u_mean / u_ref, z, label="AeroSim LES", linewidth=2)
axes[0].plot(eu_velocity_profile(z_eu, cat["z0"]), z_eu,
             "k--", label=f"EN 1991-1-4 {category}", linewidth=2)
axes[0].set_xlabel("U / U_ref [-]")
axes[0].set_ylabel("z [m]")
axes[0].set_title("Mean longitudinal velocity")
axes[0].set_ylim(2, 200)
axes[0].grid(True)
axes[0].legend(loc="lower right")

# Turbulence intensity
axes[1].plot(Iu, z, label="AeroSim LES", linewidth=2)
axes[1].plot(eu_turbulence_intensity(z_eu, cat["z0"], cat["z_min"]), z_eu,
             "k--", label=f"EN 1991-1-4 {category}", linewidth=2)
axes[1].set_xlabel("Iu [-]")
axes[1].set_ylabel("z [m]")
axes[1].set_title("Turbulence intensity")
axes[1].set_ylim(2, 200)
axes[1].set_xlim(0, 0.6)
axes[1].grid(True)
axes[1].legend(loc="upper right")

fig.tight_layout()
plt.show()

Acceptance Criteria

For the empty-domain ABL case, the simulated profiles should match the target across the height range of engineering interest (\(z_{ref}\) to \(2 z_{ref}\), where \(z_{ref}\) is the reference height of the structure under study):

Quantity

Acceptable deviation

Common cause if exceeded

\(U(z) / U_{ref}\)

within \(\pm 5\%\) of the target log law

Inlet \(z_0\) and ground \(z_0\) not aligned; insufficient fetch

\(I_u(z)\)

within \(\pm 10\%\) of the target

Probe positioned in roughness fin wake; sampling window too short

If the deviation is larger, see the troubleshooting table in Matching Custom Inlet.

Next Steps