# 2D Flat-Plate Boundary Layer — RANS k-ε with Neural Network damping functions





## Overview
The NN is trained on DNS channel flow data (Re_τ = 10 000) using `NN_SR.py`
and then loaded into the CFD solver via `calceps_NN_SR.py`.

---

## Directory structure

```
2d-example-boundary-layer/
├── pyCALC-RANS.py               # main solver (do not edit)
├── global                       # global variable declarations (do not edit)
├── calc_earsm.py                # empty EARSM placeholder (not used here)
│
└── boundary-layer-RANS-keps-NN/
    ├── run-python               # bash script to assemble and run the solver
    ├── setup_case.py            # case settings (turbulence model, BCs, solvers)
    ├── modify_case.py           # hooks: modify_init, fix_eps, modify_eps, etc.
    ├── calceps_NN_SR.py         # NN-augmented calceps (replaces calceps in solver)
    ├── generate-bound-layer-grid.py  # grid generation script
    ├── plot_inlet_bound.py      # post-processing and plotting
    ├── create-inlet-rans-profiles.py # saves inlet profiles for restarts
    ├── vel_2540_dns.prof        # DNS reference data (Re_θ ≈ 2540)
    ├── x2d.dat, y2d.dat         # grid files (generated by grid script)
    ├── z.dat                    # spanwise extent
    └── nn/                      # trained NN model files
        ├── model-f_2-f_mu-Re10000.pth
        ├── scaler-input0-f_2-f_mu-Re10000.bin
        ├── scaler-input1-f_2-f_mu-Re10000.bin
        └── min-max-f_2-f_mu-Re10000.txt
```

---

## Changes from the original k-ω boundary layer case

### `setup_case.py`

| Parameter | Original (k-ω) | New (k-ε NN) |
|-----------|---------------|--------------|
| `kom` | `True` | `False` |
| `keps` | not set | `True` |
| `c_omega_1/2` | `5/9`, `3/40` | kept as dummies (required by solver init) |
| `prand_omega` | `2.0` | kept as dummy |
| `c_eps_1` | — | `1.5` |
| `c_eps_2` | — | `1.9` |
| `prand_eps` | — | `1.4` |
| `prand_k` | `2.0` | `1.4` |
| `urf_omega` | `0.5` | kept as dummy |
| `urf_eps` | — | `0.5` |
| `convergence_limit_om` | `-1e-6` | kept as dummy |
| `convergence_limit_eps` | — | `-1e-6` |
| `restart` | `True` | `False` (no eps restart file from k-ω run) |
| omega BCs | full block | replaced with eps BCs |

**Epsilon wall BC** (`eps_bc_south`) is set as Dirichlet but the value is
overridden every iteration by `fix_eps()` using ε = 2ν k/y².

### `modify_case.py`

- `modify_om` → replaced with `modify_eps` (same inlet injection logic)
- `fix_omega` → replaced with `fix_eps` (enforces ε = 2ν k/y² at south wall each iteration)
- `modify_init` → initialises `eps2d = Cμ k²/(100ν)` instead of `om2d`; **return order** is `u2d, v2d, k2d, om2d, eps2d, vis2d, dist`

### `calceps_NN_SR.py` (new file, injected by `run-python`)

Replaces `calceps` in `pyCALC-RANS.py`. Key differences from the standard solver:

- Loads `NuNet` model (tanh activations, dynamic layer list via `nn.ModuleList`)
- Predicts **both** f₂ and f_μ simultaneously (2-output network)
- Inputs: y⁺ (scaled with local u_τ from south wall) and y* (from ε)
- Outputs clipped to training min/max range
- Set `NN_bool = False` to fall back to standard analytic AKN expressions

### `run-python`

The original k-ω script used `cat` directly. The new script:

1. Renames `calceps` → `calceps_standard` in the solver (via `sed`)
2. Renames `fix_eps` → `fix_eps_standard` in the solver (via `sed`)  
3. Injects `calceps_NN_SR.py` with the NN version before the solver
4. Uses `echo ""` between files to prevent concatenation syntax errors

```bash
#!/bin/bash
sed '/setup_case()/d' setup_case.py > temp_file

sed 's/def calceps/def calceps_standard/' ../pyCALC-RANS.py | \
sed 's/def fix_eps/def fix_eps_standard/' > temp_file1

{
    cat ../global
    echo ""
    cat temp_file
    echo ""
    cat modify_case.py
    echo ""
    cat calceps_NN_SR.py
    echo ""
    cat temp_file1
} > exec-pyCALC-RANS.py

/chalmers/groups/lada_8/anaconda3/bin/python -u exec-pyCALC-RANS.py > out
```

---

## How to run

### Step 1 — Generate the grid (first time only)

```bash
cd boundary-layer-RANS-keps-NN/
python generate-bound-layer-grid.py
```

This produces `x2d.dat` and `y2d.dat`.

### Step 2 — Train the NN (first time only, or if re-training)

The NN files are already in `nn/`. If you need to retrain:

```bash
cd ../          # go to 2d-example-boundary-layer/
python NN_SR.py
```

This writes the model, scalers and min-max file into `nn/`.

### Step 3 — Link the nn/ folder into the case directory

```bash
cd boundary-layer-RANS-keps-NN/
ln -s ../nn nn   # only needed if nn/ symlink does not already exist
```

### Step 4 — Run the CFD solver

```bash
chmod +x run-python   # only needed once
./run-python
```

Output is written to `out`. Monitor convergence in a second terminal:

```bash
tail -f out | grep 'max res'
```

The run is converged when `max res` drops below `sormax = 5e-5` (set in `setup_case.py`).

### Step 5 — Plot results

```bash
python plot_inlet_bound.py
```

Produces the following figures:

| File | Contents |
|------|----------|
| `ustar-vs-x.png` | Friction velocity u_τ along the plate |
| `u_log_python.png` | U⁺ vs y⁺ (log), compared to DNS at Re_θ ≈ 2540 |
| `vis_python.png` | ν_t/ν vs y⁺ at three x stations |
| `vis_vs_y_python.png` | ν_t/ν vs y at three x stations |
| `eps_vs_y_python.png` | ε vs y at three x stations |
| `k_model_python.png` | k/u_τ² vs y⁺ at three x stations |
| `k_vs_y_python.png` | k/u_τ² vs y at three x stations |
| `cf_vs_re_mom.png` | Skin friction C_f vs Re_θ, compared to empirical correlation |

---

