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)
../../../_images/validation_turbulence_02_turbulent_channel_flow_02.2_turbulent_channel_180_7_0.png

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)
../../../_images/validation_turbulence_02_turbulent_channel_flow_02.2_turbulent_channel_180_9_0.png

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()
../../../_images/validation_turbulence_02_turbulent_channel_flow_02.2_turbulent_channel_180_11_1.png

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