Пристеночная функция Spalding, многоуровневый домен, группы J и K
Решатель: - --wall-function spalding поверх bouzidi/grad/hrr: моментные схемы берут модельную скорость узла и u_τ²/ν в тензор давлений, Bouzidi — скольжение стенки, согласованное по напряжению (в вязком подслое тождественно ноль); - --levels: цепочка вложенных уровней с рекурсивным шагом на CPU и GPU, одиночный патч — её частный случай, прежние постановки побитово те же; - --sponge-side, β по узлам; в сводке y⁺, cl_mean, rho_probe_rms. - Исправлено: на GPU при --refine >= 3 сила лишних подшагов терялась за краем буфера, Cd занижался в r/2 раза. Кампания: группы J (65, схемы стенки с функцией) и K (25, внешний домен), 205 прогонов на 5 долей по ≈27 ч; bench/compare.py сводит J и K против эталонов; parity.py сверяет Spalding и три уровня. Образ 1.4.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
# Валидационная кампания
|
||||
|
||||
115 прогонов, ≈90 машинных часов: 108 на GPU (≈87 часов на RTX 4070 Ti) и 7 на CPU (≈3 часа —
|
||||
исследования сходимости группы A, где нужен f64). Проверяет решатель по трём независимым линиям:
|
||||
205 прогонов в одиннадцати группах, ≈101 машинный час: 198 на GPU (≈98 часов при 1200 MLUPS)
|
||||
и 7 на CPU (≈3 часа — исследования сходимости группы A, где нужен f64). Проверяет решатель по трём независимым линиям:
|
||||
эталонам из статей авторов метода, литературе по обтеканию тел и внутренним инвариантам самой
|
||||
схемы. Каждый прогон кладёт в собственную папку `cmd.txt`, `log.txt`, `report.txt`, `series.csv`
|
||||
и `summary.json`; гифку пишут **59 прогонов из 115** — остальным `--gif` не передаётся.
|
||||
и `summary.json`; гифку пишут **59 прогонов из 205** — остальным `--gif` не передаётся.
|
||||
|
||||
## Пять долей: чужие ПК под Windows / WSL2
|
||||
|
||||
@@ -44,11 +44,11 @@ Desktop с бэкендом WSL2 (так по умолчанию) либо Docke
|
||||
|
||||
| доля | прогонов | ≈ часов |
|
||||
|---|---|---|
|
||||
| 1 | 22 | 24.0 |
|
||||
| 2 | 23 | 24.0 |
|
||||
| 3 | 23 | 24.0 |
|
||||
| 4 | 23 | 24.0 |
|
||||
| 5 | 24 | 24.0 |
|
||||
| 1 | 40 | 26.7 |
|
||||
| 2 | 40 | 26.7 |
|
||||
| 3 | 41 | 26.7 |
|
||||
| 4 | 42 | 26.7 |
|
||||
| 5 | 42 | 26.7 |
|
||||
|
||||
Часы — оценка при 1200 MLUPS на крупной сетке **с поправкой на размер сетки**: на dzn мелкие
|
||||
сетки считаются медленнее на узел, а сверхмелкие из группы I идут через раздельные привязки
|
||||
@@ -66,7 +66,7 @@ Desktop с бэкендом WSL2 (так по умолчанию) либо Docke
|
||||
|
||||
## Быстрый старт на сервере
|
||||
|
||||
Образ опубликован, собирать ничего не нужно: **`notbigghost/kbc2d:1.3.0`**. Исходники на
|
||||
Образ опубликован, собирать ничего не нужно: **`notbigghost/kbc2d:1.4.0`**. Исходники на
|
||||
сервере тоже не нужны — переносится один файл `docker-compose.server.yml`.
|
||||
|
||||
```sh
|
||||
@@ -75,7 +75,7 @@ mkdir -p ~/kbc2d && cd ~/kbc2d # сюда же ляжет ./out с ре
|
||||
|
||||
C=docker-compose.server.yml
|
||||
docker compose -f $C --profile check run --rm vulkan # 1. карта видна из контейнера?
|
||||
docker compose -f $C --profile check run --rm preflight # 2. все 115 сценариев стартуют?
|
||||
docker compose -f $C --profile check run --rm preflight # 2. все 205 сценариев стартуют?
|
||||
docker compose -f $C --profile check run --rm calibrate # 3. сколько MLUPS на этой машине?
|
||||
docker compose -f $C --profile check run --rm plan # 4. смета в часах по замеренному
|
||||
docker compose -f $C up -d # 5. кампания
|
||||
@@ -277,6 +277,38 @@ out/<id>/xt_*.csv x–t диаграммы (группа G)
|
||||
| **G** | 14 | старт, акустика, время жизни: x–t диаграммы, губки, предел по Re, прогон на 10⁷ шагов |
|
||||
| **H** | 9 | инварианты: симметрия, зеркальность, зависимость от числа Маха, расхождение f32 против f64 |
|
||||
| **I** | 9 | сверхмелкие сетки 4096×2048 по всем формам и композиции из трёх тел |
|
||||
| **J** | 65 | схемы стенки с пристеночной функцией: Bouzidi без неё против Bouzidi, Grad и HRR со Spalding — цилиндр Re=150 / 2000 (D=8…64) и 5000 (D=32, 64), NACA 0012 α=4° Re=2000 (хорда 24…96) и 10⁴ (48, 96); эталон каждой серии — Bouzidi при удвоенном разрешении |
|
||||
| **K** | 25 | внешний домен: грубые буферы до тела, после и по бокам, переход разрешения скачком ×4 / ×8 против ступеней ×2, длина тонкого следа, ширина промежуточного уровня, боковые губки — против однородной тонкой сетки на домене 30D+60D, ±30D |
|
||||
|
||||
### Группы J и K: как читать
|
||||
|
||||
Обе группы отвечают на практические вопросы, и обе меряются против эталона внутри группы, а не
|
||||
против литературы — для турбулентных Re и для домена её просто нет.
|
||||
|
||||
**J** — что точнее: Bouzidi (лучшая геометрия) или моментные схемы с пристеночной функцией
|
||||
Spalding. Bouzidi + Spalding добавлен, чтобы отделить вклад функции от вклада схемы. Сетка
|
||||
однородная, разрешение задано числом клеток на тело. Re выбраны с оглядкой на устойчивость:
|
||||
предел здесь задаёт не модель стенки, а τ − ½ = 3uD/Re — при τ − ½ < 6·10⁻⁴ счёт
|
||||
разваливается у всех схем одинаково (замерено: цилиндр Re = 10⁴ при D = 16 и 32), и такой
|
||||
прогон сравнивал бы не схемы, а предел устойчивости. Отсюда Re = 5000 вместо 10⁴ у цилиндра и
|
||||
хорды от 48 у профиля при Re = 10⁴. В каждой сводке — y⁺ первого узла: при y⁺ < 5 функция
|
||||
обязана почти совпасть с прилипанием, разница возможна только там, где первая клетка дальше.
|
||||
|
||||
**K** — как строить внешний домен (ключ `--levels` решателя). Тело везде одно: цилиндр с
|
||||
D = 32 на самом тонком уровне. База — L0 в 4 раза грубее тела, домен 20D до тела, 40D после,
|
||||
±20D, две ступени ×2. Серии меняют по одному параметру. Кроме St, ⟨Cd⟩ и rms Cl сравнивается
|
||||
`rho_probe_rms` — пульсация плотности в зонде, мера паразитной акустики от отражений на границах
|
||||
и стыках уровней, — и стоимость: конфигурации K обходятся в 1–2 % стоимости эталона.
|
||||
|
||||
Сводка обеих групп:
|
||||
|
||||
```sh
|
||||
python compare.py # Markdown-таблицы против эталонов, читает out/
|
||||
python compare.py --out ../all_out --csv jk.csv # по сведённым результатам пяти долей
|
||||
```
|
||||
|
||||
Не посчитанный прогон помечается «нет», развалившийся — «развал»; под каждой серией J — какая
|
||||
схема ближе всех к эталону на каждом разрешении (по |ΔCd| у цилиндра, по |ΔCl| у профиля).
|
||||
|
||||
## Стоимость и время
|
||||
|
||||
@@ -298,6 +330,12 @@ python gen_scenarios.py --scale 3 # все прогоны в полтора
|
||||
группы A идут на CPU — это указано в самих сценариях.
|
||||
- **Развалы ожидаемы** в группе G (предел по Re) и у прогона `A09_shear_n512_lbgk`: LBGK при
|
||||
Re=3·10⁴ обязан развалиться там, где KBC доживает — это и есть проверяемое утверждение.
|
||||
Длинные прогоны канала показали предел около τ − ½ ≈ 6·10⁻⁴ (τ − ½ = 3uD/Re), а не
|
||||
Re ≈ 5·10⁴ при D = 16, как оценивалось раньше по коротким прогонам: при этом пределе
|
||||
`G11_limit_re10000` (τ − ½ = 7.2·10⁻⁴) на грани, а `G12` и `G13` обязаны развалиться.
|
||||
- **`F09_refine3` до версии 1.4.0 считался на GPU с заниженным в 1.5 раза Cd** — сила третьего
|
||||
подшага терялась за краем буфера. Исправлено; результаты этого прогона из образов до 1.4.0
|
||||
не использовать.
|
||||
- **Гифки** пишутся на всю длительность прогона, в реальном времени, 10 кадр/с. Число кадров
|
||||
этим задано жёстко (у самого длинного прогона их 16 666), поэтому единственный рычаг —
|
||||
размер кадра. Замерено: 0.103 байта на пиксель после LZW; отсюда бюджет
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Сводные таблицы групп J и K против их эталонов.
|
||||
|
||||
python compare.py # Markdown в консоль, читает out/
|
||||
python compare.py --out ../out_all # другой каталог результатов (например, сведённый
|
||||
# из пяти долей)
|
||||
python compare.py --csv compare.csv # те же строки в CSV
|
||||
|
||||
Группа J — схемы стенки. Каждая серия (тело, Re) имеет эталон: Bouzidi без пристеночной
|
||||
функции при удвоенном разрешении. Для каждого прогона печатается отклонение St, ⟨Cd⟩,
|
||||
rms Cl (у профиля — ещё ⟨Cl⟩) от эталона и y⁺ первого узла; в конце серии — какая схема
|
||||
ближе к эталону на каждом разрешении.
|
||||
|
||||
Группа K — внешний домен. Эталон каждого Re — однородная тонкая сетка на самом большом
|
||||
домене (K…_ref). Для каждой конфигурации — отклонения тех же величин, пульсация плотности в
|
||||
зонде (паразитная акустика) и стоимость (обновлений узлов и фактические секунды).
|
||||
|
||||
Прогон без summary.json (не посчитан) или развалившийся показывается строкой с пометкой —
|
||||
дыры в таблице видны сразу, а не теряются.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def load(out_dir, scen_path):
|
||||
runs = json.load(open(scen_path, encoding="utf-8"))["runs"]
|
||||
res = {}
|
||||
for r in runs:
|
||||
p = os.path.join(out_dir, r["id"], "summary.json")
|
||||
if os.path.exists(p):
|
||||
try:
|
||||
res[r["id"]] = json.load(open(p, encoding="utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
res[r["id"]] = None
|
||||
return runs, res
|
||||
|
||||
|
||||
def dev(v, ref):
|
||||
"""Относительное отклонение в процентах; None, если сравнивать нечего."""
|
||||
if v is None or ref is None or ref == 0:
|
||||
return None
|
||||
return 100.0 * (v - ref) / abs(ref)
|
||||
|
||||
|
||||
def fmt(v, spec="{:+.2f}"):
|
||||
return "—" if v is None else spec.format(v)
|
||||
|
||||
|
||||
def status(s):
|
||||
if s is None:
|
||||
return "нет"
|
||||
return "развал" if s.get("blew_up") else "ok"
|
||||
|
||||
|
||||
# ── J ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
J_RE = re.compile(r"^J\d+_(cyl|naca)_re(\d+)_(?:(?:d|c)(\d+)_(\w+)|ref_(?:d|c)(\d+))$")
|
||||
|
||||
|
||||
def table_j(runs, res):
|
||||
series = {}
|
||||
for r in runs:
|
||||
m = J_RE.match(r["id"])
|
||||
if not m:
|
||||
continue
|
||||
body, re_, n, scheme, nref = m.groups()
|
||||
key = (body, int(re_))
|
||||
s = series.setdefault(key, {"ref": None, "rows": []})
|
||||
if nref:
|
||||
s["ref"] = r["id"]
|
||||
else:
|
||||
s["rows"].append((int(n), scheme, r["id"]))
|
||||
|
||||
rows, lines = [], []
|
||||
for (body, re_), s in sorted(series.items()):
|
||||
ref = res.get(s["ref"]) if s["ref"] else None
|
||||
name = "цилиндр" if body == "cyl" else "NACA 0012, α=4°"
|
||||
unit = "D" if body == "cyl" else "хорда"
|
||||
lines.append(f"\n### J: {name}, Re = {re_}\n")
|
||||
lines.append(f"Эталон `{s['ref']}`: " + (
|
||||
"не посчитан" if ref is None else
|
||||
f"St {fmt(ref.get('strouhal'), '{:.4f}')}, Cd {fmt(ref.get('cd'), '{:.4f}')}, "
|
||||
f"Cl {fmt(ref.get('cl_mean'), '{:.4f}')}, rms Cl {fmt(ref.get('cl_rms'), '{:.4f}')}"))
|
||||
lines.append("")
|
||||
lines.append(f"| {unit} | схема | статус | ΔSt % | ΔCd % | ΔCl % | Δrms Cl % | y⁺ ср | y⁺ макс |")
|
||||
lines.append("|---|---|---|---|---|---|---|---|---|")
|
||||
best = {}
|
||||
for n, scheme, rid in sorted(s["rows"]):
|
||||
v = res.get(rid)
|
||||
ok = v is not None and not v.get("blew_up")
|
||||
g = (lambda k: v.get(k)) if ok else (lambda k: None)
|
||||
rf = (lambda k: ref.get(k)) if ref else (lambda k: None)
|
||||
d_st, d_cd = dev(g("strouhal"), rf("strouhal")), dev(g("cd"), rf("cd"))
|
||||
d_cl, d_clr = dev(g("cl_mean"), rf("cl_mean")), dev(g("cl_rms"), rf("cl_rms"))
|
||||
lines.append(
|
||||
f"| {n} | {scheme} | {status(v)} | {fmt(d_st)} | {fmt(d_cd)} | "
|
||||
f"{fmt(d_cl if body == 'naca' else None)} | {fmt(d_clr)} | "
|
||||
f"{fmt(g('yplus_mean'), '{:.2f}')} | {fmt(g('yplus_max'), '{:.1f}')} |")
|
||||
rows.append({"group": "J", "body": body, "re": re_, "n": n, "scheme": scheme,
|
||||
"id": rid, "status": status(v), "d_st": d_st, "d_cd": d_cd,
|
||||
"d_cl": d_cl, "d_cl_rms": d_clr, "yplus_mean": g("yplus_mean"),
|
||||
"yplus_max": g("yplus_max")})
|
||||
# для профиля главное — подъёмная сила, для цилиндра — сопротивление
|
||||
score = d_cl if body == "naca" else d_cd
|
||||
if score is not None:
|
||||
b = best.get(n)
|
||||
if b is None or abs(score) < abs(b[1]):
|
||||
best[n] = (scheme, score)
|
||||
if best:
|
||||
what = "|ΔCl|" if body == "naca" else "|ΔCd|"
|
||||
lines.append("")
|
||||
lines.append(f"Ближе всех к эталону по {what}: " + "; ".join(
|
||||
f"{unit} {n} — **{sc}** ({sv:+.2f} %)" for n, (sc, sv) in sorted(best.items())))
|
||||
return rows, lines
|
||||
|
||||
|
||||
# ── K ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
K_RE = re.compile(r"^K\d+_re(\d+)_(\w+)$")
|
||||
|
||||
|
||||
def table_k(runs, res):
|
||||
by_re = {}
|
||||
meta = {r["id"]: r for r in runs}
|
||||
for r in runs:
|
||||
m = K_RE.match(r["id"])
|
||||
if m:
|
||||
by_re.setdefault(int(m.group(1)), []).append((m.group(2), r["id"]))
|
||||
|
||||
rows, lines = [], []
|
||||
for re_, items in sorted(by_re.items()):
|
||||
ref_id = next((rid for tag, rid in items if tag == "ref"), None)
|
||||
ref = res.get(ref_id) if ref_id else None
|
||||
lines.append(f"\n### K: внешний домен, Re = {re_}\n")
|
||||
lines.append(f"Эталон `{ref_id}`: " + (
|
||||
"не посчитан" if ref is None else
|
||||
f"St {fmt(ref.get('strouhal'), '{:.4f}')}, Cd {fmt(ref.get('cd'), '{:.4f}')}, "
|
||||
f"rms Cl {fmt(ref.get('cl_rms'), '{:.4f}')}, ρ′ {fmt(ref.get('rho_probe_rms'), '{:.2e}')}"))
|
||||
lines.append("")
|
||||
lines.append("| конфигурация | статус | ΔSt % | ΔCd % | Δrms Cl % | ρ′ зонда | "
|
||||
"стоимость, отн. эталона | время, с |")
|
||||
lines.append("|---|---|---|---|---|---|---|---|")
|
||||
ref_cost = meta[ref_id]["cost"] if ref_id else None
|
||||
for tag, rid in items:
|
||||
if tag == "ref":
|
||||
continue
|
||||
v = res.get(rid)
|
||||
ok = v is not None and not v.get("blew_up")
|
||||
g = (lambda k: v.get(k)) if ok else (lambda k: None)
|
||||
rf = (lambda k: ref.get(k)) if ref else (lambda k: None)
|
||||
d_st, d_cd = dev(g("strouhal"), rf("strouhal")), dev(g("cd"), rf("cd"))
|
||||
d_clr = dev(g("cl_rms"), rf("cl_rms"))
|
||||
cost = meta[rid]["cost"] / ref_cost if ref_cost else None
|
||||
lines.append(
|
||||
f"| {tag} | {status(v)} | {fmt(d_st)} | {fmt(d_cd)} | {fmt(d_clr)} | "
|
||||
f"{fmt(g('rho_probe_rms'), '{:.2e}')} | {fmt(cost, '{:.3f}')} | "
|
||||
f"{fmt(g('wall_time_s'), '{:.0f}')} |")
|
||||
rows.append({"group": "K", "re": re_, "config": tag, "id": rid,
|
||||
"status": status(v), "d_st": d_st, "d_cd": d_cd, "d_cl_rms": d_clr,
|
||||
"rho_probe_rms": g("rho_probe_rms"), "cost_rel": cost,
|
||||
"wall_time_s": g("wall_time_s")})
|
||||
return rows, lines
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="Сводка групп J и K против эталонов")
|
||||
ap.add_argument("--out", default=os.path.join(HERE, "out"), help="каталог результатов")
|
||||
ap.add_argument("--scenarios", default=os.path.join(HERE, "scenarios.json"))
|
||||
ap.add_argument("--csv", help="записать строки таблиц в CSV")
|
||||
args = ap.parse_args()
|
||||
|
||||
runs, res = load(args.out, args.scenarios)
|
||||
rj, lj = table_j(runs, res)
|
||||
rk, lk = table_k(runs, res)
|
||||
print("\n".join(["# Сравнение схем стенки (J) и конфигураций домена (K)"] + lj + lk))
|
||||
if args.csv:
|
||||
keys = sorted({k for row in rj + rk for k in row})
|
||||
with open(args.csv, "w", newline="", encoding="utf-8") as f:
|
||||
w = csv.DictWriter(f, fieldnames=keys)
|
||||
w.writeheader()
|
||||
for row in rj + rk:
|
||||
w.writerow(row)
|
||||
print(f"\nCSV: {args.csv}", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -485,6 +485,207 @@ def group_i(scale):
|
||||
nx * ny, st, gif="fine_multi.gif", nx=nx)
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# J. Схемы стенки с пристеночной функцией
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# Главный практический вопрос: что точнее — Bouzidi (лучшая геометрия, без пристеночной
|
||||
# функции) или моментные схемы с пристеночной функцией Spalding, и как это меняется с Re и
|
||||
# разрешением. Bouzidi + Spalding добавлен, чтобы отделить вклад функции от вклада схемы.
|
||||
# Сетка однородная (--refine 1): разрешение задаётся прямо числом клеток на тело, без
|
||||
# вклада связки уровней. Эталон каждой серии — Bouzidi без функции при удвоенном разрешении.
|
||||
J_SCHEMES = [
|
||||
("bouzidi", "none"),
|
||||
("bouzidi", "spalding"),
|
||||
("grad", "spalding"),
|
||||
("hrr", "spalding"),
|
||||
]
|
||||
|
||||
|
||||
def jid():
|
||||
return f"J{len([r for r in RUNS if r['group'] == 'J']) + 1:02d}"
|
||||
|
||||
|
||||
def wf_tag(wall, wf):
|
||||
return wall if wf == "none" else f"{wall}_wf"
|
||||
|
||||
|
||||
# Разрешения по Re. Предел устойчивости здесь задаёт не модель стенки, а τ − ½ = 3·u·D/Re на
|
||||
# грубейшем уровне: замерено на цилиндре 20D×12D (обычный Bouzidi, длинные прогоны), что при
|
||||
# τ − ½ = 2.4·10⁻⁴ и 4.8·10⁻⁴ (Re = 10⁴, D = 16 и 32) счёт разваливается — на 13 и 27 тысячах
|
||||
# шагов, у HRR со Spalding так же, — а при 6·10⁻⁴ и выше (Re = 2000 при D = 8, Re = 5000 при
|
||||
# D = 32, Re = 10⁴ при D = 64) держится. Поэтому большой Re цилиндра — 5000, а разрешения
|
||||
# выбраны так, чтобы τ − ½ ≥ 6·10⁻⁴. Прогоны ниже предела схемы бы не сравнивали, а лишь
|
||||
# повторяли предел устойчивости, который меряет группа G.
|
||||
J_CYL_D = {150: (8, 16, 32, 64), 2000: (8, 16, 32, 64), 5000: (32, 64)}
|
||||
J_FOIL_C = {2000: (24, 48, 96), 10000: (48, 96)}
|
||||
|
||||
|
||||
def group_j(scale):
|
||||
# цилиндр: домен 20D × 12D, как сходимость по D в группе B
|
||||
for re in (150, 2000, 5000):
|
||||
for d in J_CYL_D[re]:
|
||||
nx, ny = 20 * d, 12 * d
|
||||
st = int(conv_steps(d, 300) * scale)
|
||||
for wall, wf in J_SCHEMES:
|
||||
add(f"{jid()}_cyl_re{re}_d{d}_{wf_tag(wall, wf)}", "J",
|
||||
f"Цилиндр Re={re}, D={d}, стенка {wall}, пристеночная функция {wf}",
|
||||
"отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке",
|
||||
cyl_args(d, re, nx, ny, wall=wall, wall_function=wf), nx * ny, st)
|
||||
d = 128
|
||||
nx, ny = 20 * d, 12 * d
|
||||
add(f"{jid()}_cyl_re{re}_ref_d{d}", "J",
|
||||
f"Цилиндр Re={re}, ЭТАЛОН серии: Bouzidi без функции, D={d}",
|
||||
"сеточно сошедшееся значение, к которому меряются схемы при D=8…64",
|
||||
cyl_args(d, re, nx, ny, wall="bouzidi", wall_function="none"), nx * ny,
|
||||
int(conv_steps(d, 300) * scale))
|
||||
|
||||
# профиль: NACA 0012 под углом 4°, домен 12c × 6c
|
||||
for re in (2000, 10000):
|
||||
for c in J_FOIL_C[re]:
|
||||
nx, ny = 12 * c, 6 * c
|
||||
st = int(conv_steps(c, 200) * scale)
|
||||
for wall, wf in J_SCHEMES:
|
||||
add(f"{jid()}_naca_re{re}_c{c}_{wf_tag(wall, wf)}", "J",
|
||||
f"NACA 0012, α=4°, Re={re}, хорда {c}, стенка {wall}, функция {wf}",
|
||||
"отклонение Cl, Cd от эталона серии (Bouzidi при хорде 192); y⁺ в сводке",
|
||||
foil_args(c, 4, re, nx, ny, wall=wall, wall_function=wf), nx * ny, st)
|
||||
c = 192
|
||||
nx, ny = 12 * c, 6 * c
|
||||
add(f"{jid()}_naca_re{re}_ref_c{c}", "J",
|
||||
f"NACA 0012, α=4°, Re={re}, ЭТАЛОН серии: Bouzidi без функции, хорда {c}",
|
||||
"сеточно сошедшееся значение для серии по хорде",
|
||||
foil_args(c, 4, re, nx, ny, wall="bouzidi", wall_function="none"), nx * ny,
|
||||
int(conv_steps(c, 200) * scale))
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# K. Внешний домен: грубые буферы, переходы разрешения, губки
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# Тело одно и то же во всех прогонах — цилиндр с D = 32 клетки на САМОМ ТОНКОМ уровне.
|
||||
# Меняется только то, что вокруг: размеры домена до тела, после и по бокам; как сетка
|
||||
# грубеет к краю (одним скачком или ступенями ×2); сколько тонкой сетки остаётся в следе;
|
||||
# есть ли губки на боковых кромках. Эталон — однородная тонкая сетка на самом большом
|
||||
# домене. Метрики: St, ⟨Cd⟩, rms Cl против эталона, пульсация плотности в зонде
|
||||
# (rho_probe_rms — паразитная акустика) и стоимость.
|
||||
K_D = 32 # клеток на тело на самом тонком уровне
|
||||
K_CONV = 300 # конвективных времён D/U до умножения на scale
|
||||
|
||||
|
||||
def _rround(x):
|
||||
"""Округление как у Rust f64::round — половина от нуля, а не к чётному."""
|
||||
return math.floor(x + 0.5) if x >= 0 else -math.floor(-x + 0.5)
|
||||
|
||||
|
||||
def level_chain(levels, cx, cy, d, nx, ny):
|
||||
"""Тот же расчёт границ уровней, что `parse_levels` в main.rs. Возвращает обновлений
|
||||
узлов за шаг L0 по всем уровням и строку для --levels."""
|
||||
pnx, pny, scale, ox, oy = nx, ny, 1.0, 0.0, 0.0
|
||||
total, sub = nx * ny, 1
|
||||
for r, up, down, side in levels:
|
||||
def to(x, o):
|
||||
return _rround((x - o) * scale)
|
||||
ax = int(max(to(cx - up * d, ox), 2.0))
|
||||
bx = int(max(min(to(cx + down * d, ox), pnx - 2.0), 0.0))
|
||||
ay = int(max(to(cy - side * d, oy), 1.0))
|
||||
by = int(max(min(to(cy + side * d, oy), pny - 2.0), 0.0))
|
||||
assert ax + 1 < bx and ay + 1 < by, (levels, ax, bx, ay, by)
|
||||
ox += ax / scale
|
||||
oy += ay / scale
|
||||
scale *= r
|
||||
pnx, pny = r * (bx - ax) + 1, r * (by - ay) + 1
|
||||
sub *= r
|
||||
total += sub * pnx * pny
|
||||
spec = "; ".join(f"{r}:{up:g},{down:g},{side:g}" for r, up, down, side in levels)
|
||||
return total, spec
|
||||
|
||||
|
||||
def kid():
|
||||
return f"K{len([r for r in RUNS if r['group'] == 'K']) + 1:02d}"
|
||||
|
||||
|
||||
def k_run(tag, title, expect, re, scale, up, down, side, levels, sponge_side_d=0.0):
|
||||
"""Прогон группы K. `up`, `down`, `side` — размеры домена L0 в диаметрах; `levels` —
|
||||
цепочка [(r, up, down, side), …] от внешнего уровня к внутреннему, отступы в D.
|
||||
Пустая цепочка — однородная тонкая сетка."""
|
||||
s = 1
|
||||
for lv in levels:
|
||||
s *= lv[0]
|
||||
d0 = K_D / s # тело в клетках L0
|
||||
nx = int(round((up + down) * d0))
|
||||
ny = int(round(2 * side * d0))
|
||||
cx, cy = up * d0, ny / 2
|
||||
args = {"--shape": "cylinder", "--size": f"{d0:g}", "--nx": nx, "--ny": ny,
|
||||
"--body-x": f"{cx:g}", "--re": re, "--wall": "bouzidi"}
|
||||
if levels:
|
||||
nodes, spec = level_chain(levels, cx, cy, d0, nx, ny)
|
||||
args["--levels"] = spec
|
||||
else:
|
||||
nodes = nx * ny
|
||||
args["--refine"] = 1
|
||||
if sponge_side_d > 0:
|
||||
args["--sponge-side"] = int(round(sponge_side_d * d0))
|
||||
# шагов L0 на одно и то же физическое время: тело на L0 в s раз мельче
|
||||
st = int(conv_steps(d0, K_CONV) * scale)
|
||||
add(f"{kid()}_re{re}_{tag}", "K", title, expect, flatten(args), nodes, st)
|
||||
|
||||
|
||||
# Базовая конфигурация: L0 в 4 раза грубее тела, к телу две ступени ×2. Промежуточный
|
||||
# уровень — 4D вверх, 15D вниз, 4D в стороны; тонкий — 1.5D вверх, 10D следа, 2D в стороны.
|
||||
K_MID = (2, 4, 15, 4)
|
||||
|
||||
|
||||
def k_fine(wake=10):
|
||||
return (2, 1.5, wake, 2)
|
||||
|
||||
|
||||
def group_k(scale):
|
||||
for re in (150, 2000):
|
||||
k_run("ref", f"Re={re}, ЭТАЛОН: однородная тонкая сетка 30D+60D, ±30D",
|
||||
"значения, к которым меряются все конфигурации буферов", re, scale,
|
||||
30, 60, 30, [])
|
||||
k_run("base", f"Re={re}, база: домен 20D+40D, ±20D, ступени ×2×2",
|
||||
"отклонение от эталона при умеренных буферах", re, scale,
|
||||
20, 40, 20, [K_MID, k_fine()])
|
||||
# переход: одно и то же суммарное огрубление — скачком или ступенями
|
||||
k_run("jump4", f"Re={re}, огрубление ×4 одним скачком",
|
||||
"скачок разрешения против ступеней ×2×2 (база)", re, scale,
|
||||
20, 40, 20, [(4, 1.5, 10, 2)])
|
||||
k_run("jump8", f"Re={re}, огрубление ×8 одним скачком",
|
||||
"скачок ×8 против цепочки ×2×2×2", re, scale,
|
||||
20, 40, 20, [(8, 1.5, 10, 2)])
|
||||
k_run("chain8", f"Re={re}, огрубление ×8 цепочкой ×2×2×2",
|
||||
"плавный переход при сильном огрублении", re, scale,
|
||||
20, 40, 20, [(2, 8, 25, 8), K_MID, k_fine()])
|
||||
k_run("sponge5", f"Re={re}, боковые губки 5D",
|
||||
"гасят ли губки отражения от боковых стенок", re, scale,
|
||||
20, 40, 20, [K_MID, k_fine()], sponge_side_d=5)
|
||||
if re != 150:
|
||||
continue
|
||||
# остальные серии — только Re=150: там есть литература и чистая дорожка
|
||||
for side in (5, 10, 30):
|
||||
k_run(f"side{side}", f"Re={re}, боковой буфер ±{side}D",
|
||||
"с какого размера бока перестают влиять (блокировка + отражения)", re, scale,
|
||||
20, 40, side, [K_MID, k_fine()])
|
||||
for up in (5, 10, 30):
|
||||
k_run(f"up{up}", f"Re={re}, буфер до тела {up}D",
|
||||
"с какого отступа вход перестаёт влиять", re, scale,
|
||||
up, 40, 20, [(2, min(4, up - 1), 15, 4), k_fine()])
|
||||
for down in (15, 30, 60):
|
||||
k_run(f"down{down}", f"Re={re}, буфер после тела {down}D",
|
||||
"с какой длины выход перестаёт влиять", re, scale,
|
||||
20, down, 20, [(2, 4, min(15, down - 2), 4), k_fine(min(10, down - 4))])
|
||||
for wake in (5, 20):
|
||||
k_run(f"wake{wake}", f"Re={re}, тонкий след {wake}D за телом",
|
||||
"сколько тонкой сетки нужно в следе до первого огрубления", re, scale,
|
||||
20, 40, 20, [(2, 4, max(15, wake + 5), 4), k_fine(wake)])
|
||||
k_run("midwide", f"Re={re}, широкий промежуточный уровень (8D вверх, 25D вниз, 8D вбок)",
|
||||
"влияет ли ширина ступени перехода", re, scale,
|
||||
20, 40, 20, [(2, 8, 25, 8), k_fine()])
|
||||
k_run("sponge2", f"Re={re}, боковые губки 2D",
|
||||
"ширина губки: 2D против 5D", re, scale,
|
||||
20, 40, 20, [K_MID, k_fine()], sponge_side_d=2)
|
||||
|
||||
|
||||
def est_hours(r):
|
||||
"""Оценка времени прогона в часах для раскладки по долям (см. DZN_MLUPS)."""
|
||||
if r["backend"] == "cpu":
|
||||
@@ -526,7 +727,8 @@ def main():
|
||||
ap.add_argument("--out", default=os.path.join(os.path.dirname(__file__), "scenarios.json"))
|
||||
args = ap.parse_args()
|
||||
|
||||
for g in (group_a, group_b, group_c, group_d, group_e, group_f, group_g, group_h, group_i):
|
||||
for g in (group_a, group_b, group_c, group_d, group_e, group_f, group_g, group_h, group_i,
|
||||
group_j, group_k):
|
||||
g(args.scale)
|
||||
shard_hours = assign_shards(RUNS, args.shards)
|
||||
|
||||
|
||||
@@ -79,6 +79,26 @@ CASES = [
|
||||
"degenerate_frac": 5.0},
|
||||
"report": ["strouhal"],
|
||||
},
|
||||
# Пристеночная функция переписана в WGSL отдельно от math.rs (закон Spalding, Ньютон в
|
||||
# f32), поэтому сверяется отдельно. При Re=20 y⁺ < 1 и функция почти не меняет
|
||||
# результат — проверяется именно совпадение двух реализаций, а не физика.
|
||||
{
|
||||
"name": "цилиндр Re=20, Град + Spalding, 8000 шагов",
|
||||
"args": ["--shape", "cylinder", "--size", "16", "--nx", "320", "--ny", "192",
|
||||
"--re", "20", "--refine", "1", "--steps", "8000",
|
||||
"--wall", "grad", "--wall-function", "spalding"],
|
||||
"fields": {"cd": 1.0, "rho_mean": 0.05, "yplus_mean": 1.0},
|
||||
"report": ["strouhal"],
|
||||
},
|
||||
# Три уровня: рекурсивная связка уровней и сквозная нумерация подшагов силы на GPU.
|
||||
{
|
||||
"name": "цилиндр Re=20, три уровня ×2×2, 3000 шагов",
|
||||
"args": ["--shape", "cylinder", "--size", "8", "--nx", "240", "--ny", "160",
|
||||
"--re", "20", "--levels", "2:4,10,4; 2:1.5,4,1.5", "--wall", "bouzidi",
|
||||
"--steps", "3000"],
|
||||
"fields": {"cd": 1.0, "rho_mean": 0.05},
|
||||
"report": ["strouhal"],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user