Turbulent Channel (Reynolds 180)¶
The simulation of a periodic turbulent channel is also used for validation of a multilevel domain. A friction Reynolds number of 180 is fixed for both cases. The pressure gradient is estabilished through a constant body force.
[1]:
from nassu.cfg.model import ConfigScheme
filename = "validation/turbulence/02_turbulent_channel_flow/02_turbulent_channel_flow.nassu.yaml"
sim_cfgs = ConfigScheme.sim_cfgs_from_file_dct(filename)
The multilevel setup tries to reproduce a DNS using a optimized amount of grid nodes.
[2]:
sim_cfg_mb = next(
sim_cfg
for (name, _), sim_cfg in sim_cfgs.items()
if sim_cfg.name == "periodicTurbulentChannelMultilevel"
)
sim_cfgs_use = {"mb": sim_cfg_mb}
Functions to use for turbulence channel processing
[3]:
import pathlib
import numpy as np
import pandas as pd
import nassu.viz as common
common.use_style()
def get_experimental_profiles(reynolds_tau: float) -> dict[str, pd.DataFrame]:
files_tau: dict[float, dict[str, str]] = {
142: {
"ux": "Re_tau_142_u_avg.csv",
"ux_rms": "Re_tau_142_u_rms.csv",
"uy_rms": "Re_tau_142_v_rms.csv",
"uz_rms": "Re_tau_142_w_rms.csv",
},
520: {
"ux": "Re_tau_520_u_avg.csv",
"ux_rms": "Re_tau_142_u_rms.csv",
},
}
files_get = files_tau[reynolds_tau]
vals_exp: dict[str, pd.DataFrame] = {}
for name, comp_file in files_get.items():
filename = (
pathlib.Path("validation/turbulence/02_turbulent_channel_flow/reference") / comp_file
)
df = pd.read_csv(filename, delimiter=",")
vals_exp[name] = df
return vals_exp
# One cached wall-normal (y) centre-line probe per simulation's statistics
# export, reused for every macroscopic instead of re-probing per call.
_line_probes: dict[str, common.LineProbe] = {}
def _line_probe(sim_cfg) -> common.LineProbe:
if sim_cfg.name not in _line_probes:
ds = sim_cfg.domain.domain_size
_line_probes[sim_cfg.name] = common.LineProbe.from_export(
sim_cfg.output.exports["default_stats"].volumes["default_stats"].stats,
sim_cfg.n_steps + 1,
(ds.x // 2, 0, ds.z // 2),
(ds.x // 2, ds.y - 1, ds.z // 2),
ds.y,
)
return _line_probes[sim_cfg.name]
def get_macr_compressed(sim_cfg, macr_name: str, is_2nd_order: bool) -> np.ndarray:
probe = _line_probe(sim_cfg)
ds = sim_cfg.domain.domain_size
# Normalized wall-normal position of each sample (data is cell centered in the vtm).
norm_pos = (probe.sample_points[:, 1] + 0.5) / (ds.y + 1)
name = macr_name if not is_2nd_order else f"{macr_name}_2nd"
return np.array([norm_pos, probe.sample(name)])
Results¶
The average velocity profile is shown for the case. It can be a good approximation from reference results for the multilevel case.
[4]:
import matplotlib.pyplot as plt
# Despite the reynolds value being 180, the experimental data for 142 and 180 is pretty much the same
reynolds_tau = 142
# Friction velocity implied by the body force and the channel half-height
# (delta = domain_size.y / 2); replaces the hand-typed u_ref literal.
u_ref = common.friction_velocity(
sim_cfg_mb, geometry="channel", length=sim_cfg_mb.domain.domain_size.y // 2
)
fig, ax = common.fig_single()
for i, sim_cfg in enumerate(sim_cfgs_use.values()):
height_scale = common.wall_units_scale(sim_cfg, u_ref)
analytical_values = get_experimental_profiles(reynolds_tau)
macr_compr = get_macr_compressed(sim_cfg, "ux", is_2nd_order=False)
y = macr_compr[0].copy()
ux_avg = macr_compr[1].copy()
ux_avg /= u_ref
y *= height_scale * sim_cfg.domain.domain_size.y
exp_y = analytical_values["ux"]["y+"]
exp_ux_avg = analytical_values["ux"]["u/u*"]
ax.plot(
y,
ux_avg,
**common.markers.sim_line(linestyle="--"),
label=f"AeroSim N={sim_cfg.domain.domain_size.x}",
)
ax.plot(exp_y, exp_ux_avg, **common.markers.exp(shape="o"), label="Reference")
ax.set_title("Multilevel")
ax.legend()
ax.set_xlim(1, 160)
ax.set_xscale("symlog")
ax.set_ylabel("$u/u*$")
ax.set_xlabel("$y^+$")
plt.tight_layout()
plt.show(fig)
The results of the \({\mathrm{u_{rms}}}\) velocity profiles shown below also indicate a good representation with the multilevel approach.
[5]:
fig, ax = plt.subplots(figsize=(5.7, 5.7))
vel_name_map = {"ux": "u", "uy": "v", "uz": "w"}
vel_colors = {"ux": common.colors.sim, "uy": common.colors.blue, "uz": common.colors.green}
for i, sim_cfg in enumerate(sim_cfgs_use.values()):
for macr_name in ("ux", "uy", "uz"):
macr_compr_rms = get_macr_compressed(sim_cfg, macr_name, is_2nd_order=True)
macr_compr_avg = get_macr_compressed(sim_cfg, macr_name, is_2nd_order=False)
y = macr_compr_rms[0].copy()
vel_2nd = macr_compr_rms[1].copy()
vel_avg = macr_compr_avg[1].copy()
vel_rms = (vel_2nd - vel_avg**2) ** 0.5
vel_rms /= u_ref
y *= height_scale * sim_cfg.domain.domain_size.y
# Remove wall value
vel_rms = vel_rms[1:]
y = y[1:]
c = vel_colors[macr_name]
name_vel = vel_name_map[macr_name]
df = analytical_values[f"{macr_name}_rms"]
exp_y = df["y+"]
exp_rms = df[f"{name_vel}'/u*"]
ax.plot(
y,
vel_rms,
marker="x",
linestyle="none",
color=c,
markeredgewidth=1.7,
label=f"AeroSim {name_vel.upper()}",
)
ax.plot(
exp_y,
exp_rms,
marker="o",
fillstyle="none",
linestyle="none",
color=c,
markeredgewidth=1.7,
label=f"Ref. {name_vel.upper()}",
)
ax.set_title("Multilevel")
ax.legend()
ax.set_xlim(0, 80)
ax.set_xlabel("$y^+$")
plt.tight_layout()
plt.show(fig)
Flow field¶
Instantaneous velocity magnitude on the channel planes (plane_series): the streamwise / wall-normal mid-plane and the wall-parallel plane inside the refined wall block (y+ ~ 15).
[6]:
from nassu import viz
viz.enable_offscreen()
PANEL = (1040, 260)
cfg = sim_cfgs["periodicTurbulentChannelMultilevel", 0]
domain = (384.0, 96.0, 96.0)
panels = [
viz.Panel(
"mid-channel",
viz.PlaneSource.from_cfg(cfg, series="plane_series", plane="mid_channel"),
viz.frame_domain(domain, "z", panel=PANEL, slice_coord=48.0),
),
viz.Panel(
"near-wall (y+ ~ 15)",
viz.PlaneSource.from_cfg(cfg, series="plane_series", plane="near_wall"),
viz.frame_domain(domain, "y", panel=PANEL, slice_coord=4.0),
),
]
steps = [panels[0].source.steps[-1]]
plotter = viz.render_grid(
panels,
steps=steps,
scalar="u_mag",
cmap="viridis",
clim=(0.0, 0.12),
bar_title="|u|",
panel_size=PANEL,
)
plotter.show()
2026-06-29 14:45:40.071 ( 1.311s) [ 7FF1B0D77740]vtkXOpenGLRenderWindow.:1460 WARN| bad X server connection. DISPLAY=
/tmp/ipykernel_1979898/3063530119.py:31: UserWarning: Using static image for notebook display.
Install trame for interactive backends: pip install "pyvista[jupyter]"
plotter.show()
Version¶
[7]:
sim_info = sim_cfg.output.read_info()
nassu_commit = sim_info["commit"]
nassu_version = sim_info["version"]
print("Version:", nassu_version)
print("Commit hash:", nassu_commit)
Version: 2.0.1a0
Commit hash: 50dc5112a00ba586bcdeecfde90794927e2789bf
Configuration¶
[8]:
from IPython.display import Code
Code(filename=filename)
[8]:
variables:
# Domain size
ds_1:
x: 512
y: 128
z: 128
ds_2:
x: 384
y: 96
z: 96
# Equation-based ("cold") start - self-contained, no external
# artefact. The streamwise field is an analytic Reichardt mean
# profile mirrored across both walls (wall distance in viscous
# units y+ = (u*/nu) * min(y, NY - y)), scaled by the friction
# velocity u*, seeded with wall-enveloped multi-mode sinusoidal
# perturbations (streamwise streaks plus cross-flow rolls carrying
# wall-normal velocity) at ~10% intensity to break the x/z symmetry
# and trip transition to turbulence. The sin(pi*y/NY) envelope
# vanishes at both no-slip walls. The scalar is initialised
# separately via its `initial_field` ramp.
#
# Per-domain friction velocity u* and inner-scale coefficient
# y+/lattice = u*/nu, with nu = cs^2 (tau - 0.5):
# ds_1: Re_tau 142, tau=0.5074 -> nu=2.467e-3, u*=0.0054962, u*/nu=2.228
# ds_2: Re_tau 180, tau=0.5042 -> nu=1.400e-3, u*=0.00525, u*/nu=3.75
us_1: 0.0054962
yp_1: 2.228
us_2: 0.00525
yp_2: 3.75
init:
ds_1:
ux: !sub "${us_1} * (2.5*log(1 + 0.41*${yp_1}*min(y, ${ds_1.y} - y)) + 7.8*(1 - exp(-${yp_1}*min(y, ${ds_1.y} - y)/11) - (${yp_1}*min(y, ${ds_1.y} - y)/11)*exp(-${yp_1}*min(y, ${ds_1.y} - y)/3))) + 0.010*sin(pi*y/${ds_1.y})*cos(2*pi*5*z/${ds_1.z}) + 0.006*sin(pi*y/${ds_1.y})*sin(2*pi*3*x/${ds_1.x})*cos(2*pi*4*z/${ds_1.z})"
uy: !sub "0.006*sin(pi*y/${ds_1.y})*sin(2*pi*3*x/${ds_1.x})*cos(2*pi*4*z/${ds_1.z}) + 0.004*sin(pi*y/${ds_1.y})*sin(2*pi*2*x/${ds_1.x})*sin(2*pi*3*z/${ds_1.z})"
uz: !sub "0.006*sin(pi*y/${ds_1.y})*sin(2*pi*3*x/${ds_1.x})*sin(2*pi*4*z/${ds_1.z}) - 0.004*sin(pi*y/${ds_1.y})*cos(2*pi*2*x/${ds_1.x})*cos(2*pi*3*z/${ds_1.z})"
ds_2:
ux: !sub "${us_2} * (2.5*log(1 + 0.41*${yp_2}*min(y, ${ds_2.y} - y)) + 7.8*(1 - exp(-${yp_2}*min(y, ${ds_2.y} - y)/11) - (${yp_2}*min(y, ${ds_2.y} - y)/11)*exp(-${yp_2}*min(y, ${ds_2.y} - y)/3))) + 0.010*sin(pi*y/${ds_2.y})*cos(2*pi*5*z/${ds_2.z}) + 0.006*sin(pi*y/${ds_2.y})*sin(2*pi*3*x/${ds_2.x})*cos(2*pi*4*z/${ds_2.z})"
uy: !sub "0.006*sin(pi*y/${ds_2.y})*sin(2*pi*3*x/${ds_2.x})*cos(2*pi*4*z/${ds_2.z}) + 0.004*sin(pi*y/${ds_2.y})*sin(2*pi*2*x/${ds_2.x})*sin(2*pi*3*z/${ds_2.z})"
uz: !sub "0.006*sin(pi*y/${ds_2.y})*sin(2*pi*3*x/${ds_2.x})*sin(2*pi*4*z/${ds_2.z}) - 0.004*sin(pi*y/${ds_2.y})*cos(2*pi*2*x/${ds_2.x})*cos(2*pi*3*z/${ds_2.z})"
simulations:
- name: periodicTurbulentChannel
save_path: ./validation/turbulence/02_turbulent_channel_flow/results/
n_steps: 600000
report:
frequency: 1000
domain:
# u* = 0.0054962 / l = 64 / ETT = l/u* = 11,644
# Re_tau = 142
# y+ = 2.228
domain_size:
x: !math ${ds_1.x}
y: !math ${ds_1.y}
z: !math ${ds_1.z}
block_size: 8
data:
exports:
default:
macrs: [rho, u, S]
interval:
frequency: 200000
lvl: 0
target:
volume: {}
outputs:
instantaneous: true
default_stats:
macrs:
- rho
- u
interval:
frequency: 100
start_step: 300000
lvl: 0
target:
volume: {}
outputs:
instantaneous: false
stats:
macrs_1st_order:
- rho
- u
macrs_2nd_order:
- u
plane_series:
macrs: [rho, u]
interval: {frequency: 50000, lvl: 0}
target:
planes:
# Streamwise / wall-normal mid-plane (walls are y-normal here).
mid_channel:
axis: z
axis_pos: 64
dist: 1
# Wall-parallel plane in the buffer layer (y+ ~ 16):
# near-wall streak visualization.
near_wall:
axis: y
axis_pos: 8
dist: 1
outputs:
instantaneous: true
models:
precision:
default: single
LBM:
tau: 0.5074
F:
x: 4.72e-7
y: 0
z: 0
vel_set: D3Q27
coll_oper: RRBGK
initialization:
equations:
rho: "1"
ux: !sub ${init.ds_1.ux}
uy: !sub ${init.ds_1.uy}
uz: !sub ${init.ds_1.uz}
engine:
name: CUDA
BC:
periodic_dims: [true, false, true]
BC_map:
- pos: N
BC: RegularizedHWBB
wall_normal: N
order: 1
- pos: S
BC: RegularizedHWBB
wall_normal: S
order: 1
- name: periodicTurbulentChannelMultilevel
save_path: ./validation/turbulence/02_turbulent_channel_flow/results/
n_steps: 200000
# u* = 0.00525 / l = 48 / ETT = l/u* = 9,150
# Re_tau = 180
# y+ = 3.75 (lvl 0)
report:
frequency: 1000
domain:
domain_size:
x: !math ${ds_2.x}
y: !math ${ds_2.y}
z: !math ${ds_2.z}
block_size: 8
refinement:
static:
default:
volumes_refine:
- start: [0, 0, 0]
end: [384, 8, 96]
lvl: 1
is_abs: true
- start: [0, 88, 0]
end: [384, 96, 96]
lvl: 1
is_abs: true
data:
exports:
default:
macrs: [rho, u, S]
interval:
frequency: 200000
lvl: 0
target:
volume: {}
outputs:
instantaneous: true
start_simul:
macrs: [rho, u, S]
interval:
end_step: 2000
frequency: 500
lvl: 0
target:
volume: {}
outputs:
instantaneous: true
default_stats:
macrs:
- rho
- u
interval:
frequency: 50
start_step: 50000
lvl: 0
target:
volume: {}
outputs:
instantaneous: false
stats:
macrs_1st_order:
- rho
- u
macrs_2nd_order:
- u
plane_series:
macrs: [rho, u]
interval: {frequency: 50000, lvl: 0}
target:
planes:
# Streamwise / wall-normal mid-plane (walls are y-normal here).
mid_channel:
axis: z
axis_pos: 48
dist: 1
# Wall-parallel plane in the buffer layer (y+ ~ 15, inside the
# refined wall block): near-wall streak visualization. One node
# apart is enough - the streak spacing is ~26 nodes.
near_wall:
axis: y
axis_pos: 4
dist: 1
outputs:
instantaneous: true
models:
precision:
default: single
LBM:
tau: 0.5042
F:
x: 5.75e-7
y: 0
z: 0
vel_set: D3Q27
coll_oper: RRBGK
initialization:
equations:
rho: "1"
ux: !sub ${init.ds_2.ux}
uy: !sub ${init.ds_2.uy}
uz: !sub ${init.ds_2.uz}
engine:
name: CUDA
BC:
periodic_dims: [true, false, true]
BC_map:
- pos: N
BC: RegularizedHWBB
wall_normal: N
order: 1
- pos: S
BC: RegularizedHWBB
wall_normal: S
order: 1