Пристеночная функция 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:
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user