Tutorial: GABLS1 LES Intercomparison Study for Stable Boundary Layers
Last updated: May 2026
This tutorial walks through a complete JAX-ALFA simulation from scratch using the GABLS1 LES intercomparison study as the working example. By the end you will have:
Created a run directory with a
Config.py, initial-condition files, and a surface boundary-condition file.Launched the solver and understood the log output.
Loaded the statistics output and produced the standard mean-profile plots.
References:
Beare, R. J., and Coauthors. (2006). An intercomparison of large-eddy simulations of the stable boundary layer. Boundary-Layer Meteorology, 118, 247–272.
Basu, S. and Porté-Agel, F. (2006). Large-eddy simulation of stably stratified atmospheric boundary layer turbulence: A scale-dependent dynamic modeling approach. Journal of the Atmospheric Sciences, 63, 2074–2091.
Dai, Y., Basu, S., Maronga, B., and de Roode, S. R. (2020). Addressing the grid-size sensitivity issue in large-eddy simulations of stable boundary layers. Boundary-Layer Meteorology, 178, 63–89.
SGS model used here: Locally Averaged Scale-Dependent Dynamic (LASDD) – Smagorinsky (optSgs = 1).
1. Case Overview
The GABLS1 case simulates a stable nocturnal boundary layer driven by a prescribed surface cooling rate. It is widely used as a reference benchmark because the forcing is simple, the quasi-steady state is reached within 9 h, and a large ensemble of LES results is available in the literature.
Parameter |
Value |
|---|---|
Domain |
400 m × 400 m × 400 m |
Grid |
128 × 128 × 128 |
Grid spacing |
≈ 3.15 m (isotropic) |
Geostrophic wind |
\(U_g = 8\) m s\(^{-1}\), \(V_g = 0\) |
Coriolis parameter |
\(f = 1.39 \times 10^{-4}\) s\(^{-1}\) (73° N) |
Roughness lengths |
\(z_{0m} = z_{0T} = 0.1\) m |
Initial surface temperature |
265 K |
Surface cooling rate |
0.25 K hr\(^{-1}\) |
Simulation duration |
9 h |
Time step |
0.1 s |
SGS model |
LASDD-SM ( |
Precision |
double ( |
Averaging window |
hours 8–9 (quasi-steady state) |
2. Prerequisites
Before running this tutorial, ensure JAX-ALFA and its dependencies are installed. See the Installation page for step-by-step instructions.
3. Directory Layout
A JAX-ALFA run lives in its own directory. The packaged GABLS1 reference run used in this tutorial has the structure below:
examples/SBL_GABLS1/runs/128x128x128_LASDD_SM_DP/
├── Config.py
├── CreateInputs_SBL_GABLS1.py
├── CreateSurfaceBC_GABLS1.py
├── input/
│ ├── vel.ini
│ ├── TH.ini
│ └── SurfaceBC.npz
└── output/
├── ALFA_Statistics_Iteration_*.npz
└── ALFA_3DFields_Iteration_*.npz
The notebook reads this packaged run for post-processing. When setting up your own case, copy the same directory pattern, edit Config.py, regenerate the input/ files, and point JAXALFA_RUNDIR to your new run directory.
[37]:
import os
import re
import glob
import numpy as np
import matplotlib.pyplot as plt
from pathlib import Path
def find_repo_root(start=None):
"""Locate the JAXALFA0.1 repository root from common notebook launch paths."""
path = Path(start or ('__file__' in globals() and __file__) or Path.cwd()).resolve()
for candidate in (path, *path.parents):
if (candidate / 'examples').is_dir() and (candidate / 'docs').is_dir():
return candidate
raise FileNotFoundError(
'Could not locate the JAXALFA0.1 root. Launch the notebook from the '
'repository, or set RunDir manually in the next cell.'
)
BaseDir = find_repo_root()
RunDir = BaseDir / 'examples' / 'SBL_GABLS1' / 'runs' / '128x128x128_LASDD_SM_DP'
print('Repository root:', BaseDir)
print('Run directory :', RunDir)
print('Run exists :', RunDir.exists())
Repository root: /Users/sukantabasu/Dropbox/Codes/LES/JAX-ALFA/JAXALFA0.1
Run directory : /Users/sukantabasu/Dropbox/Codes/LES/JAX-ALFA/JAXALFA0.1/examples/SBL_GABLS1/runs/128x128x128_LASDD_SM_DP
Run exists : True
4. Step 1 - Inspect Config.py
Config.py is the main file to edit when changing the simulation setup. It contains the domain sizes, time step, SGS options, surface boundary condition, and output intervals. The cell below prints the packaged GABLS1 namelist so the settings used for the reference output are visible in the tutorial.
[38]:
config_path = RunDir / 'Config.py'
if not config_path.exists():
raise FileNotFoundError(
f'Missing {config_path}. Check RunDir or copy the packaged GABLS1 run directory.'
)
config_text = config_path.read_text()
print(config_text)
# Copyright (C) 2025 Sukanta Basu
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
File: Config.py
===============
:Author: Sukanta Basu
:AI Assistance: Claude.AI (Anthropic) is used for documentation,
code restructuring, and performance optimization
:Date: 2026-05-20
:Description: Stable BL benchmark after Beare et al. (2006), GABLS1.
Domain 400x400x400 m; surface cooling 0.25 K/hr.
Grid: 128x128x128, SGS: LASDD-SM,
Precision: double.
"""
# ============================================================
# Imports
# ============================================================
import numpy as np
# ============================================================
# User Input
# ============================================================
# ------------------------------------------------------------
# Platform options
# ------------------------------------------------------------
use_double_precision = True
# 0: use CPU, 1: use GPU
optGPU = 1
GPU_ID = 0
# ------------------------------------------------------------
# Domain configuration
# ------------------------------------------------------------
# Domain size (m)
l_x = 400
l_y = 400
l_z = 400
# Number of grid points
nx = 128
ny = 128
nz = 128
# ------------------------------------------------------------
# Time integration configuration
# ------------------------------------------------------------
# Change this if it is a restart run
istep = 1
# Time stepping and simulation time
dt = 0.1 # unit: sec
SimTime = 9 * 3600 # unit: sec
# Galilean transformation (m/s)
Ugal = 5
# ------------------------------------------------------------
# Surface configuration
# ------------------------------------------------------------
# optSurfFlux: 0 = homogeneous, 1 = heterogeneous
optSurfFlux = 0
# optSurfBC: 0 = constant flux
# 1 = time-varying flux (from SurfaceBCFile)
# 2 = time-varying surface temperature (from SurfaceBCFile)
optSurfBC = 2
# Roughness lengths (m)
z0m = 0.1
z0T = 0.1
# Screen-level temperature reference height (m); 0 = use z0T
zTemperature = 0.0
# SensibleHeatFlux: not used when optSurfBC >= 1; set to 0 as placeholder
SensibleHeatFlux = 0.0 # K m/s
# Path to surface BC file (relative to run directory)
SurfaceBCFile = 'input/SurfaceBC.npz'
# ------------------------------------------------------------
# Forcing configuration
# ------------------------------------------------------------
# Geostrophic wind option:
# 0 = constant Ug2, Vg2 from Config
# 1 = time + height varying, loaded from GeoWindFile
optGeoWind = 0
# Constant geostrophic wind (m/s)
Ug2 = 8
Vg2 = 0
# Path to geostrophic wind file (relative to run directory)
GeoWindFile = 'input/GeoWind.npz'
# Coriolis parameter (1/s)
f_coriolis = 0.000139
# Potential temperature lapse rate above domain top (K/m)
inversion = 0.01
# Buoyancy calculation: 0 = use reference T_0, 1 = use local THv
optBuoyancy = 1
# Reference temperature (K)
T_0 = 265
# ------------------------------------------------------------
# Subgrid-scale configuration
# ------------------------------------------------------------
# SGS model: 0 = Static SM, 1 = LASDD-SM, 2 = LASDD-WL, 3 = LAD-SM, 4 = LAD-WL
optSgs = 1
# Dynamic SGS update frequency (every N steps)
dynamicSGS_call_time = 1
# Filter to grid ratio (FGR=1: implicit filtering; FGR>=2: explicit + dealiasing)
FGR = 2
# Initial SGS coefficients (used before first dynamic update)
Cs2 = 0.1 ** 2 # SM models: initial Cs^2
Cwl = 0.1 ** 2 # WL models: initial C_WL
Cs2PrRatio = Cs2 / 1.0
CwlPrRatio = Cwl / 1.0
# ------------------------------------------------------------
# Damping layer configuration
# ------------------------------------------------------------
optDamping = 1 # 1: activate Rayleigh damping
z_damping = 300 # unit: m
RelaxTime = 60 # unit: s
# ------------------------------------------------------------
# Statistics computation
# ------------------------------------------------------------
SampleInterval_sec = 10.0 # collect a sample every N s
OutputInterval_sec = 60.0 # output averaged stats every N s
Output3DInterval_sec = SimTime # output 3D fields only at the end of the run
# ------------------------------------------------------------
# Large-scale advection forcing
# ------------------------------------------------------------
# 0: none, 1: time/height-varying (from AdvectionFile)
optAdvection = 0
AdvectionFile = 'input/AdvForcing.npz'
# ------------------------------------------------------------
# Moisture configuration
# ------------------------------------------------------------
# 0: dry run, 1: prognostic specific humidity Q
optMoisture = 0
# Screen-level moisture reference height (m); 0 = use z0T
zMoisture = 0.0
# Surface moisture flux (kg/kg m/s); used when optMoistureSurfBC = 0
MoistureFlux = 0.0
# 0: constant flux, 1: time-varying flux, 2: time-varying surface Q
optMoistureSurfBC = 0
MoistureSurfaceBCFile = 'input/MoistureSurfaceBC.npz'
# Specific humidity lapse rate above domain top (kg/kg/m); 0 = zero gradient
q_inversion = 0.0
[39]:
# Extract grid and timing parameters from Config.py
_cfg = {}
exec(config_text, _cfg)
nz = int(_cfg['nz'])
l_z = float(_cfg['l_z'])
dt_cfg = float(_cfg['dt'])
SimTime_cfg = float(_cfg['SimTime'])
OutputInterval_sec = float(_cfg['OutputInterval_sec'])
dz = l_z / (nz - 1)
print(f'nz={nz}, l_z={l_z} m, dz={dz:.4f} m')
print(f'dt={dt_cfg} s, SimTime={SimTime_cfg} s ({SimTime_cfg/3600:.1f} h)')
print(f'OutputInterval_sec={OutputInterval_sec} s')
nz=128, l_z=400.0 m, dz=3.1496 m
dt=0.1 s, SimTime=32400.0 s (9.0 h)
OutputInterval_sec=60.0 s
Key parameters to understand:
Parameter |
Meaning |
|---|---|
|
|
|
|
|
Surface temperature prescribed from |
|
LASDD-SM: tuning-free, scale-dependent dynamic closure |
|
Test filter is twice the grid filter (dealiasing inactive) |
|
Rayleigh sponge activates above 300 m to suppress reflections |
5. Step 2 — Create Initial Conditions
Initial conditions follow the GABLS1 protocol (Beare et al. 2006):
\(u = U_g = 8\) m s\(^{-1}\), \(v = w = 0\) everywhere.
\(\theta = 265\) K for \(z \le 100\) m; \(\theta = 265 + 0.01(z-100)\) K above.
Small random perturbations below 50 m to trigger the turbulent cascade.
The run directory ships with a ready-to-use script ``CreateInputs_SBL_GABLS1.py`` that writes input/vel.ini (three-column ASCII: \(u\), \(v\), \(w\)) and input/TH.ini (one-column ASCII: potential temperature), both stored in Fortran column-major order.
Run it once before the first simulation:
cd /path/to/runs/128x128x128_LASDD_SM_DP
python CreateInputs_SBL_GABLS1.py
Expected output:
GABLS1 initial conditions written to .../input
Grid: nx=128, ny=128, nz=128, dz=3.1496 m
vel.ini shape: (2097152, 3)
TH.ini shape: (2097152,)
TH range: 264.990 - 265.010 K
The initial temperature profile is plotted below for reference.
The temperature profile can be visualised to confirm the initial stratification:
[40]:
# Reconstruct the deterministic part of the initial temperature profile
# (nz, l_z, dz loaded from Config.py in the cell above)
z_plot = np.array([(k + 0.5) * dz for k in range(nz)])
TH_profile = np.where(z_plot <= 100.0, 265.0, 265.0 + 0.01 * (z_plot - 100.0))
fig, ax = plt.subplots(figsize=(4, 5))
ax.plot(TH_profile, z_plot, 'k-', linewidth=1.8)
ax.axhline(100, color='gray', linestyle='--', linewidth=0.8, label='inversion base (100 m)')
ax.set_xlabel(r'$\theta$ (K)', fontsize=13)
ax.set_ylabel(r'$z$ (m)', fontsize=13)
ax.set_title('Initial potential-temperature profile', fontsize=12)
ax.legend(fontsize=10)
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()
6. Step 3 — Create the Surface Boundary-Condition File
Because optSurfBC = 2, the solver reads the surface temperature at each time step from input/SurfaceBC.npz. The GABLS1 protocol prescribes a linear cooling rate of 0.25 K hr\(^{-1}\):
Over 9 h the surface cools from 265 K to 262.75 K.
The run directory ships with ``CreateSurfaceBC_GABLS1.py``, which reads dt and SimTime directly from Config.py and writes the full time series to input/SurfaceBC.npz.
Run it once before the first simulation:
cd /path/to/runs/128x128x128_LASDD_SM_DP
python CreateSurfaceBC_GABLS1.py
Expected output:
Surface BC file written to .../input/SurfaceBC.npz
Mode: linear cooling
nsteps + 1 = 324001 (dt = 0.1 s, SimTime = 32400.0 s)
T_s(t=0) = 265.0000 K
T_s(t=SimTime)= 262.7500 K
T_s range : [262.7500, 265.0000] K
The prescribed cooling curve is shown below.
[41]:
# Prescribed surface temperature — GABLS1 linear cooling
# (dt_cfg, SimTime_cfg loaded from Config.py in the cell above)
nsteps = int(np.ceil(SimTime_cfg / dt_cfg))
t_series = np.arange(0, nsteps + 1) * dt_cfg
T_sfc = 265.0 - 0.25 * (t_series / 3600.0)
fig, ax = plt.subplots(figsize=(5, 3))
ax.plot(t_series / 3600, T_sfc, 'k-', linewidth=1.5)
ax.set_xlabel('Time (h)', fontsize=12)
ax.set_ylabel(r'$T_s$ (K)', fontsize=12)
ax.set_title('Prescribed surface temperature (GABLS1)', fontsize=11)
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()
7. Step 4 — Launch the Solver
With Config.py, vel.ini, TH.ini, and SurfaceBC.npz in place, launch JAX-ALFA from the terminal:
export JAXALFA_RUNDIR=/path/to/128x128x128_LASDD_SM_DP
python /path/to/JAXALFA0.1/src/Main.py
The solver writes statistics to output/ALFA_Statistics_Iteration_*.npz every OutputInterval_sec seconds (60 s → one file per minute → 540 files for a 9-hour run).
Progress monitoring: the log file run.log (or stdout) prints a one-line summary every OutputInterval_sec seconds showing the current simulation time, wall-clock time per step, and the instantaneous friction velocity \(u_*\).
8. Step 5 - Load and Inspect the Output
Each statistics file contains horizontally averaged profiles sampled every SampleInterval_sec seconds (10 s) and time-averaged over OutputInterval_sec (60 s). The arrays stored in each .npz file are:
Array |
Description |
Grid |
|---|---|---|
|
Mean velocity components and potential temperature |
half levels |
|
Resolved velocity variances |
half / full levels |
|
Resolved temperature variance |
half levels |
|
Resolved momentum fluxes |
half / full levels |
|
SGS stress components |
half / full levels |
|
Resolved heat flux |
full levels |
|
SGS heat flux |
full levels |
[42]:
OutputDir = RunDir / 'output'
def get_stat_files(output_dir):
"""Return statistics files sorted by iteration number."""
pattern = str(output_dir / 'ALFA_Statistics_Iteration_*.npz')
files = sorted(
glob.glob(pattern),
key=lambda x: int(re.search(r'Iteration_(\d+)', x).group(1))
)
return files
stat_files = get_stat_files(OutputDir)
print(f'{len(stat_files)} statistics files found in {OutputDir}')
if not stat_files:
raise FileNotFoundError(
'No ALFA_Statistics_Iteration_*.npz files were found. Run the solver first, '
'or point RunDir to a completed GABLS1 run.'
)
with np.load(stat_files[0]) as d:
print('Arrays in each file:', sorted(d.keys()))
print('Profile length (nz):', d['U'].shape)
540 statistics files found in /Users/sukantabasu/Dropbox/Codes/LES/JAX-ALFA/JAXALFA0.1/examples/SBL_GABLS1/runs/128x128x128_LASDD_SM_DP/output
Arrays in each file: ['Beta1', 'Beta2', 'Cs2PrRatio', 'Cs2_1', 'Cs2_2', 'M_sfc', 'Q', 'Q2', 'TH', 'TH2', 'U', 'Ugal', 'V', 'W', 'ZeRo1D', 'dQdz', 'dTHdz', 'dUdz', 'dVdz', 'qHz', 'qm_sfc', 'qz', 'qz_sfc', 'txy', 'txz', 'tyz', 'u2', 'uTH', 'ustar', 'uv', 'uw', 'v2', 'vTH', 'vw', 'w2', 'wQ', 'wTH']
Profile length (nz): (128,)
8.1 Quasi-steady state averaging window
The GABLS1 benchmark defines the quasi-steady state as hours 8–9. We average over all statistics files in that window.
[43]:
# (nz, l_z, OutputInterval_sec loaded from Config.py in the cell above)
T_start, T_end = 8 * 3600, 9 * 3600
i_start = int(T_start / OutputInterval_sec) - 1
i_end = int(T_end / OutputInterval_sec) - 1
# Vertical grids
z = np.array([(k + 0.5) * l_z / (nz - 1) for k in range(nz)]) # half levels
z_w = np.array([k * l_z / (nz - 1) for k in range(nz)]) # full levels
print(f'Averaging window: file indices {i_start}–{i_end} '
f'({i_end - i_start + 1} files)')
print(f'Half-level range: {z[0]:.2f}–{z[-1]:.2f} m')
Averaging window: file indices 479–539 (61 files)
Half-level range: 1.57–401.57 m
[44]:
def load_time_average(stat_files, i_start, i_end):
"""Load and time-average statistics over [i_start, i_end] inclusive."""
if len(stat_files) <= i_end:
raise ValueError(
f'Need at least {i_end + 1} statistics files for the 8-9 h average; '
f'found {len(stat_files)}.'
)
keys = ['U', 'V', 'TH', 'u2', 'v2', 'w2', 'TH2',
'uv', 'uw', 'vw', 'txy', 'txz', 'tyz', 'wTH', 'qz']
stacks = {k: [] for k in keys}
for f in stat_files:
with np.load(f) as d:
missing = [k for k in keys if k not in d]
if missing:
raise KeyError(f'{f} is missing arrays: {missing}')
for k in keys:
stacks[k].append(d[k])
sl = slice(i_start, i_end + 1)
return {k: np.mean(np.array(stacks[k])[sl], axis=0) for k in keys}
avg = load_time_average(stat_files, i_start, i_end)
# Convenience: total (resolved + SGS) fluxes
uw_tot = avg['uw'] + avg['txz']
vw_tot = avg['vw'] + avg['tyz']
wTH_tot = avg['wTH'] + avg['qz']
S_mag = np.sqrt(avg['U']**2 + avg['V']**2)
print('Time-averaged profiles loaded.')
print(f" U range : {avg['U'].min():.2f} - {avg['U'].max():.2f} m/s")
print(f" TH range : {avg['TH'].min():.2f} - {avg['TH'].max():.2f} K")
Time-averaged profiles loaded.
U range : 1.62 - 9.46 m/s
TH range : 263.19 - 268.02 K
9. Mean Wind and Temperature Profiles
The panels below show the streamwise velocity \(U\), cross-stream velocity \(V\), wind-speed magnitude \(S\), and mean potential temperature \(\theta\) averaged over hours 8–9.
[45]:
plt.rcParams.update({'font.size': 12, 'axes.labelsize': 13})
fig, axs = plt.subplots(1, 4, figsize=(14, 5), constrained_layout=True)
axs[0].plot(avg['U'], z, 'k-', linewidth=2)
axs[0].set_xlabel(r'$\langle U \rangle$ (m s$^{-1}$)')
axs[0].set_ylabel(r'$z$ (m)')
axs[0].set_title('Streamwise wind')
axs[1].plot(avg['V'], z, 'k-', linewidth=2)
axs[1].set_xlabel(r'$\langle V \rangle$ (m s$^{-1}$)')
axs[1].set_title('Cross-stream wind')
axs[2].plot(S_mag, z, 'k-', linewidth=2)
axs[2].set_xlabel(r'$S$ (m s$^{-1}$)')
axs[2].set_title('Wind speed')
axs[3].plot(avg['TH'], z, 'k-', linewidth=2)
axs[3].set_xlabel(r'$\langle \theta \rangle$ (K)')
axs[3].set_title('Potential temperature')
for ax in axs:
ax.set_ylim(0, 300)
ax.grid(alpha=0.3)
ax.set_ylabel(r'$z$ (m)')
fig.suptitle('GABLS1 — LASDD-SM — 8–9 h average ($128^3$ grid)', fontsize=13)
plt.show()
10. Momentum and Heat Fluxes
The total vertical flux is the sum of the resolved turbulent flux and the SGS contribution. In a well-resolved LES the SGS fraction should be small in the bulk and larger only near the surface.
[46]:
fig, axs = plt.subplots(1, 3, figsize=(12, 5), constrained_layout=True)
# Total uw stress
axs[0].plot(avg['uw'], z_w, color='steelblue', linewidth=2, label='Resolved')
axs[0].plot(avg['txz'], z_w, color='tomato', linewidth=2, label='SGS')
axs[0].plot(uw_tot, z_w, 'k--', linewidth=1.5, label='Total')
axs[0].set_xlabel(r"$\langle u'w' \rangle + \tau_{xz}$ (m$^2$ s$^{-2}$)")
axs[0].set_title(r'$xz$ momentum flux')
# Total vw stress
axs[1].plot(avg['vw'], z_w, color='steelblue', linewidth=2, label='Resolved')
axs[1].plot(avg['tyz'], z_w, color='tomato', linewidth=2, label='SGS')
axs[1].plot(vw_tot, z_w, 'k--', linewidth=1.5, label='Total')
axs[1].set_xlabel(r"$\langle v'w' \rangle + \tau_{yz}$ (m$^2$ s$^{-2}$)")
axs[1].set_title(r'$yz$ momentum flux')
# Total heat flux
axs[2].plot(avg['wTH'], z_w, color='steelblue', linewidth=2, label='Resolved')
axs[2].plot(avg['qz'], z_w, color='tomato', linewidth=2, label='SGS')
axs[2].plot(wTH_tot, z_w, 'k--', linewidth=1.5, label='Total')
axs[2].set_xlabel(r"$\langle w'\theta' \rangle + q_z$ (K m s$^{-1}$)")
axs[2].set_title('Heat flux')
for ax in axs:
ax.set_ylim(0, 300)
ax.set_ylabel(r'$z$ (m)')
ax.legend(fontsize=9, frameon=False)
ax.grid(alpha=0.3)
fig.suptitle('GABLS1 — LASDD-SM — 8–9 h average ($128^3$ grid)', fontsize=13)
plt.show()
11. Temporal Evolution of the Friction Velocity
The friction velocity \(u_* = \bigl(-\langle u'w' \rangle_s\bigr)^{1/2}\) at the surface provides a compact diagnostic of the boundary-layer spin-up and quasi-steady approach. We compute it from the total surface stress (uw_tot[0], vw_tot[0]) as a function of time.
[47]:
ustar_series = []
time_series = []
for idx, f in enumerate(stat_files):
with np.load(f) as d:
uw_surf = d['uw'][0] + d['txz'][0]
vw_surf = d['vw'][0] + d['tyz'][0]
tau_surf = np.sqrt(uw_surf**2 + vw_surf**2)
ustar_series.append(np.sqrt(tau_surf))
time_series.append((idx + 1) * OutputInterval_sec / 3600.0) # hours
ustar_series = np.array(ustar_series)
time_series = np.array(time_series)
fig, ax = plt.subplots(figsize=(6, 3.5))
ax.plot(time_series, ustar_series, 'k-', linewidth=1.5)
ax.axvspan(8, 9, alpha=0.12, color='steelblue', label='Averaging window')
ax.set_xlabel('Time (h)', fontsize=12)
ax.set_ylabel(r'$u_*$ (m s$^{-1}$)', fontsize=12)
ax.set_title('GABLS1 - LASDD-SM - friction velocity', fontsize=11)
ax.legend(fontsize=10, frameon=False)
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()
window = (time_series >= 8) & (time_series <= 9)
ustar_avg = np.mean(ustar_series[window])
print(f'u* averaged over 8-9 h: {ustar_avg:.4f} m/s')
u* averaged over 8-9 h: 0.2788 m/s
12. Velocity Variances
Resolved velocity variances characterise the turbulent intensity in each direction. Under stable stratification the vertical component \(\sigma_w^2\) is strongly suppressed compared to the horizontal components.
[48]:
fig, axs = plt.subplots(1, 3, figsize=(12, 5), constrained_layout=True)
axs[0].plot(avg['u2'], z, 'k-', linewidth=2)
axs[0].set_xlabel(r"$\langle u'^2 \rangle$ (m$^2$ s$^{-2}$)")
axs[0].set_title(r'$u$ variance')
axs[1].plot(avg['v2'], z, 'k-', linewidth=2)
axs[1].set_xlabel(r"$\langle v'^2 \rangle$ (m$^2$ s$^{-2}$)")
axs[1].set_title(r'$v$ variance')
axs[2].plot(avg['w2'], z_w, 'k-', linewidth=2)
axs[2].set_xlabel(r"$\langle w'^2 \rangle$ (m$^2$ s$^{-2}$)")
axs[2].set_title(r'$w$ variance')
for ax in axs:
ax.set_ylim(0, 300)
ax.set_ylabel(r'$z$ (m)')
ax.grid(alpha=0.3)
fig.suptitle('GABLS1 — LASDD-SM — velocity variances, 8–9 h average ($128^3$)', fontsize=13)
plt.show()
13. Next Steps
Now that you have a working GABLS1 run you can:
Change the SGS model — edit
optSgsinConfig.py(1 = LASDD-SM, 2 = LASDD-WL, 3 = LAD-SM, 4 = LAD-WL) and re-run.Change precision — set
use_double_precision = Falsefor float32 and compare with the double-precision reference.Change resolution — adjust
nx, ny, nz(anddtif needed for stability) to explore resolution sensitivity.Explore the Case Studies section for more complex examples (CBL, NBL, GABLS3, Wangara diurnal cycle).
See the API Reference section for a complete description of every module and configuration parameter.