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 |
|---|---|---|
|
|
One row per probe point, indexed by |
|
|
One row per time step; remaining columns hold |
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_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
with \(z_{0,II} = 0.05\) m. Turbulence intensity is
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¶
Time Normalization Using the Integral Length Scale - extend the same probe time series to non-dimensional time analysis.
ABL guided case post-processing - end-to-end validation of an ABL setup.
Matching Custom Inlet - if the target profile comes from a wind tunnel rather than an EN 1991-1-4 category.