Решатель: - --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>
777 lines
51 KiB
Python
777 lines
51 KiB
Python
#!/usr/bin/env python3
|
||
# Генератор списка прогонов кампании -> scenarios.json
|
||
#
|
||
# Список порождается кодом, а не правится руками: серии по Re, по разрешению и по углу атаки
|
||
# получаются циклами, а стоимость каждого прогона считается тут же и суммируется. Запуск:
|
||
# python gen_scenarios.py # перезаписать scenarios.json и напечатать смету
|
||
# python gen_scenarios.py --scale 2 # растянуть все длительности вдвое
|
||
# python gen_scenarios.py --shards 5 # на сколько равных по времени долей делить кампанию
|
||
#
|
||
# Стоимость меряется в ОБНОВЛЕНИЯХ УЗЛОВ (nodes_per_step * steps) — единственная переносимая
|
||
# между машинами мера. Часы драйвер получает из неё, поделив на фактические MLUPS, которые
|
||
# замеряет на месте (--calibrate).
|
||
|
||
import argparse
|
||
import json
|
||
import math
|
||
import os
|
||
|
||
RUNS = []
|
||
U_LAT = 0.05 # решёточная скорость по умолчанию
|
||
DX = 0.1 # размер клетки, м (умолчание решателя)
|
||
U_PHYS = 30.0 # скорость потока, м/с (умолчание решателя)
|
||
GIF_MAX_W = 1280 # предельная ширина кадра после прореживания
|
||
GIF_MIN_W = 480 # ниже не опускаемся: нечитаемая гифка бесполезнее большой
|
||
GIF_PX_BUDGET = 1.5e9 # кадры×пиксели на гифку; при 0.103 байта на пиксель это ~150 МБ
|
||
|
||
# Доли кампании: прогоны раскладываются по N контейнерам так, чтобы те считали примерно
|
||
# одинаковое ВРЕМЯ, а не одинаковое число узлов. Время на узел зависит от размера сетки:
|
||
# на мелкой накладные расходы на отправку в очередь дороже самого счёта. Кривая — замер
|
||
# dzn в контейнере (Intel Iris Xe, bench/README.md, «Чего трансляция стоит по скорости»),
|
||
# потому что доли рассчитаны на чужие Windows/WSL-машины, где путь к карте — именно dzn.
|
||
# Важна только ФОРМА кривой: абсолют нормируется к ASSUMED_GPU_MLUPS.
|
||
ASSUMED_GPU_MLUPS = 1200
|
||
ASSUMED_CPU_MLUPS = 22
|
||
DZN_MLUPS = [(61_440, 44.5), (460_800, 86.0), (1_843_200, 98.7)]
|
||
SPLIT_NODES = 3_730_000 # выше — раздельные привязки популяций, у них своя скорость
|
||
SPLIT_MLUPS = 67.4 # замер на 2048×2048
|
||
|
||
|
||
def conv_steps(d_cells, n_conv, u_lat=U_LAT):
|
||
"""Сколько шагов нужно на n_conv конвективных времён D/U."""
|
||
return int(round(n_conv * d_cells / u_lat))
|
||
|
||
|
||
def add(rid, group, title, expect, args, nodes, steps, backend="gpu",
|
||
gif=None, nx=None, extra_out=()):
|
||
"""Записать прогон. `nodes` — обновлений узлов за шаг, `steps` — сколько шагов."""
|
||
a = list(args) + ["--steps", str(steps), "--backend", backend]
|
||
# ряды прореживаем так, чтобы их осталось порядка 200 тысяч записей
|
||
if steps > 400_000:
|
||
a += ["--series-every", str(max(1, steps // 200_000))]
|
||
if "case_csv" in extra_out:
|
||
a += ["--case-csv", f"{rid}_case.csv"]
|
||
if gif:
|
||
# Гифка идёт на всю длительность прогона в реальном времени, 10 кадр/с — число кадров
|
||
# задано физикой прогона и не обсуждается. Единственный рычаг — пиксели в кадре, и он
|
||
# нужен: замерено 0.103 байта на пиксель после LZW, так что прогон на 16 тысяч кадров
|
||
# при 960×576 весит 900 МБ, а такой файл уже нечем открыть. Поэтому кроме ограничения
|
||
# по ширине действует бюджет ПРОИЗВЕДЕНИЯ кадры×пиксели, но ширина не опускается ниже
|
||
# GIF_MIN_W: нечитаемая гифка бесполезнее большой.
|
||
ny = max(1, int(nodes) // max(1, nx or 1))
|
||
u_lat = float(a[a.index("--u-lat") + 1]) if "--u-lat" in a else U_LAT
|
||
frames = max(1, int(steps * (u_lat * DX / U_PHYS) * 10))
|
||
down = max(1, math.ceil((nx or 0) / GIF_MAX_W))
|
||
while nx and frames * (nx // down) * (ny // down) > GIF_PX_BUDGET \
|
||
and nx // (down + 1) >= GIF_MIN_W:
|
||
down += 1
|
||
a += ["--gif", gif, "--gif-field", "vorticity", "--gif-every", "auto",
|
||
"--gif-fps", "10", "--gif-speed", "1",
|
||
"--gif-downsample", str(down), "--gif-scale", "1"]
|
||
RUNS.append({
|
||
"id": rid, "group": group, "title": title, "expect": expect,
|
||
"backend": backend, "args": a,
|
||
"nodes_per_step": int(nodes), "steps": int(steps),
|
||
"cost": int(nodes) * int(steps),
|
||
"outputs": list(extra_out),
|
||
})
|
||
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════════
|
||
# A. Эталоны первоисточников: периодические течения, тела и ГУ нет вовсе
|
||
# ══════════════════════════════════════════════════════════════════════════════
|
||
def group_a(scale):
|
||
NU = 0.0192
|
||
for n in (64, 128, 256, 512):
|
||
# диффузионное измельчение: nu фиксирована, u0 ~ 1/N, поэтому Re сохраняется, а
|
||
# число Маха падает вместе с сеткой — только так виден второй порядок целиком
|
||
u0 = 0.03 * 64 / n
|
||
tc = int(round(math.log(2.0) / (NU * (2 * math.pi / n) ** 2 * 17)))
|
||
add(f"A{len(RUNS)+1:02d}_tg_diff_n{n}", "A",
|
||
f"Тейлор–Грин, диффузионное измельчение, N={n} (CPU/f64)",
|
||
"порядок сходимости 2 по ряду N=64..512; ошибка на N=512 ниже 2e-4",
|
||
["--case", "taylor-green", "--nx", str(n), "--ny", str(n), "--refine", "1",
|
||
"--u-lat", f"{u0:.6f}", "--re", "100", "--case-every", str(max(1, tc // 20))],
|
||
n * n, tc, backend="cpu", extra_out=("case_csv",))
|
||
|
||
for n in (128, 256):
|
||
# при ФИКСИРОВАННОМ u0 ошибка упирается в полку O(Ma^2) и от сетки не зависит
|
||
tc = int(round(math.log(2.0) / (NU * (2 * math.pi / n) ** 2 * 17)))
|
||
add(f"A{len(RUNS)+1:02d}_tg_fixed_n{n}", "A",
|
||
f"Тейлор–Грин при фиксированном u₀, N={n} — демонстрация полки O(Ma²)",
|
||
"ошибка перестаёт падать с ростом N: это свойство слабо-сжимаемого метода",
|
||
["--case", "taylor-green", "--nx", str(n), "--ny", str(n), "--refine", "1",
|
||
"--u-lat", "0.03", "--re", f"{0.03 * n / NU:.1f}",
|
||
"--case-every", str(max(1, tc // 20))],
|
||
n * n, tc, backend="cpu", extra_out=("case_csv",))
|
||
|
||
for n in (512,):
|
||
for op, km, tag in (("kbc", "n1", "kbc_n1"), ("kbc", "n2", "kbc_n2"), ("bgk", "n1", "lbgk")):
|
||
steps = int(4 * n / 0.04 * scale)
|
||
add(f"A{len(RUNS)+1:02d}_shear_n{n}_{tag}", "A",
|
||
f"Сдвиговый слой Re=3·10⁴, N={n}, {tag}",
|
||
"KBC доживает до конца, LBGK обязан развалиться (разд. VII статьи)",
|
||
["--case", "shear-layer", "--nx", str(n), "--ny", str(n), "--refine", "1",
|
||
"--u-lat", "0.04", "--re", "30000", "--collision", op, "--kbc-model", km,
|
||
"--case-every", str(max(1, steps // 200))],
|
||
n * n, steps, gif=f"shear_{tag}.gif", nx=n, extra_out=("case_csv",))
|
||
|
||
for n, tag, ops in ((2048, "n2048", ("kbc", "bgk")), (4096, "n4096", ("kbc",))):
|
||
for op in ops:
|
||
steps = int(200_000 * scale * (2048 / n))
|
||
add(f"A{len(RUNS)+1:02d}_turb_{tag}_{op}", "A",
|
||
f"Затухающая турбулентность N={n}, {op}",
|
||
"энстрофия и палинстрофия падают монотонно; KBC держится там, где LBGK нет",
|
||
["--case", "decaying-turbulence", "--nx", str(n), "--ny", str(n), "--refine", "1",
|
||
"--u-lat", "0.02", "--re", f"{0.02 * n / 8.16e-5:.0f}", "--collision", op,
|
||
"--case-every", str(max(1, steps // 300))],
|
||
n * n, steps, gif=f"turb_{tag}_{op}.gif", nx=n, extra_out=("case_csv",))
|
||
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════════
|
||
# B. Цилиндр против литературы
|
||
# ══════════════════════════════════════════════════════════════════════════════
|
||
def flatten(d):
|
||
"""Словарь ключей в плоский argv. Именно словарь, а не список: clap отвергает
|
||
повторённый ключ, а сценарии сплошь и рядом переопределяют умолчания сборщика."""
|
||
a = []
|
||
for k, v in d.items():
|
||
a += [k, str(v)]
|
||
return a
|
||
|
||
|
||
def cyl_args(d, re, nx, ny, **kw):
|
||
a = {"--shape": "cylinder", "--size": d, "--nx": nx, "--ny": ny,
|
||
"--body-x": nx // 5, "--re": re, "--refine": 1}
|
||
a.update({"--" + k.replace("_", "-"): v for k, v in kw.items()})
|
||
return flatten(a)
|
||
|
||
|
||
def group_b(scale):
|
||
# стационарные режимы: дорожки нет, Cd — одно число
|
||
for re in (20, 40):
|
||
d, nx, ny = 48, 960, 576
|
||
st = int(conv_steps(d, 60) * scale)
|
||
add(f"B{len(RUNS)-len(RUNS)+len([r for r in RUNS if r['group']=='B'])+1:02d}_cyl_re{re}",
|
||
"B", f"Цилиндр Re={re}, стационар, D=48",
|
||
"Cd выходит на постоянную; сравнение с литературой по стационарному обтеканию",
|
||
cyl_args(d, re, nx, ny, pert_amp=0), nx * ny, st,
|
||
gif=f"cyl_re{re}.gif", nx=nx)
|
||
|
||
# дорожка Кармана: длинное осреднение, это главный заход на литературу
|
||
for re in (100, 150, 200, 300):
|
||
d, nx, ny = 64, 1280, 768
|
||
st = int(conv_steps(d, 1500) * scale)
|
||
add(f"B{len([r for r in RUNS if r['group']=='B'])+1:02d}_cyl_re{re}", "B",
|
||
f"Цилиндр Re={re}, дорожка Кармана, D=64, 1500 конв. времён",
|
||
"St≈0.183 и ⟨Cd⟩≈1.33 при Re=150 после поправки на блокировку",
|
||
cyl_args(d, re, nx, ny), nx * ny, st, gif=f"cyl_re{re}.gif", nx=nx)
|
||
|
||
# разрешение тела: сходимость Cd
|
||
for d in (16, 32, 64, 128):
|
||
nx, ny = 20 * d, 12 * d
|
||
st = int(conv_steps(d, 800) * scale)
|
||
add(f"B{len([r for r in RUNS if r['group']=='B'])+1:02d}_cyl_d{d}", "B",
|
||
f"Цилиндр Re=150, разрешение D={d}",
|
||
"Cd сходится по D; экстраполяция даёт сеточно-независимое значение",
|
||
cyl_args(d, 150, nx, ny), nx * ny, st, gif=f"cyl_d{d}.gif", nx=nx)
|
||
|
||
# блокировка канала: экстраполяция к бесконечной среде — прямой заход на нерешённые 12%
|
||
for k in (6, 8, 12, 16, 24, 32):
|
||
d, nx, ny = 48, 960, k * 48
|
||
st = int(conv_steps(d, 1000) * scale)
|
||
add(f"B{len([r for r in RUNS if r['group']=='B'])+1:02d}_cyl_block{k}", "B",
|
||
f"Цилиндр Re=150, блокировка Ny/D={k}",
|
||
"экстраполяция Cd и St к нулевой блокировке; ожидание — сходимость к 1.33 и 0.183",
|
||
cyl_args(d, 150, nx, ny), nx * ny, st, gif=None, nx=nx)
|
||
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════════
|
||
# C. Модели стенки и субсеточность
|
||
# ══════════════════════════════════════════════════════════════════════════════
|
||
def group_c(scale):
|
||
for wall in ("hrr", "grad", "bouzidi", "staircase"):
|
||
for re in (150, 2000):
|
||
d, nx, ny = 32, 640, 384
|
||
st = int(conv_steps(d, 400 if re > 20 else 60) * scale)
|
||
add(f"C{len([r for r in RUNS if r['group']=='C'])+1:02d}_wall_{wall}_re{re}", "C",
|
||
f"Модель стенки {wall}, Re={re}",
|
||
"все субсеточные модели обязаны сойтись к одному пределу; staircase — база",
|
||
cyl_args(d, re, nx, ny, wall=wall), nx * ny, st,
|
||
gif=f"wall_{wall}_re{re}.gif" if re == 150 else None, nx=nx)
|
||
|
||
# Чувствительность к положению тела ВНУТРИ клетки — прямая проверка субсеточности, и
|
||
# единственное измерение, которое вообще разделяет модели стенки. Двух положений для этого
|
||
# мало: по двум точкам не отличить систематический разброс от совпадения, поэтому берём
|
||
# четверти, и все четыре модели. Прогоны короткие (стационар при Re=20), вся серия — минуты.
|
||
for wall in ("hrr", "grad", "bouzidi", "staircase"):
|
||
for off in (0.0, 0.25, 0.5):
|
||
d, nx, ny = 16, 320, 192
|
||
st = int(conv_steps(d, 60) * scale)
|
||
add(f"C{len([r for r in RUNS if r['group']=='C'])+1:02d}_sub_{wall}_{int(off*100):02d}",
|
||
"C", f"Субклеточный сдвиг тела {off} клетки, стенка {wall}",
|
||
"разброс Cd по сдвигам: чем субсеточнее модель, тем он меньше",
|
||
cyl_args(d, 20, nx, ny, body_y=96 + off, wall=wall, pert_amp=0),
|
||
nx * ny, st)
|
||
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════════
|
||
# D. Профили крыла
|
||
# ══════════════════════════════════════════════════════════════════════════════
|
||
def foil_args(chord, alpha, re, nx, ny, naca="0012", **kw):
|
||
a = {"--shape": "naca", "--naca": naca, "--size": chord,
|
||
"--body-angle": alpha, "--nx": nx, "--ny": ny,
|
||
"--body-x": nx // 4, "--re": re, "--refine": 1}
|
||
a.update({"--" + k.replace("_", "-"): v for k, v in kw.items()})
|
||
return flatten(a)
|
||
|
||
|
||
def group_d(scale):
|
||
# поляра: наклон Cl(alpha) и положение сваливания
|
||
for al in (0, 4, 8, 12, 16):
|
||
c, nx, ny = 128, 2048, 1024
|
||
st = int(conv_steps(c, 400) * scale)
|
||
add(f"D{len([r for r in RUNS if r['group']=='D'])+1:02d}_naca0012_a{al:02d}", "D",
|
||
f"NACA 0012, α={al}°, Re=1000, хорда 128",
|
||
"линейный участок Cl(α) и срыв; сравнение с низкорейнольдсовой литературой",
|
||
foil_args(c, al, 1000, nx, ny), nx * ny, st,
|
||
gif=f"naca0012_a{al:02d}.gif", nx=nx)
|
||
|
||
for re in (500, 2000, 10000):
|
||
c, nx, ny = 128, 2048, 1024
|
||
st = int(conv_steps(c, 400) * scale)
|
||
add(f"D{len([r for r in RUNS if r['group']=='D'])+1:02d}_naca0012_re{re}", "D",
|
||
f"NACA 0012, α=8°, Re={re}",
|
||
"перестройка следа с ростом Re; проверка устойчивости на профиле",
|
||
foil_args(c, 8, re, nx, ny), nx * ny, st,
|
||
gif=f"naca0012_re{re}.gif", nx=nx)
|
||
|
||
for al in (0, 6, 12):
|
||
c, nx, ny = 128, 2048, 1024
|
||
st = int(conv_steps(c, 400) * scale)
|
||
add(f"D{len([r for r in RUNS if r['group']=='D'])+1:02d}_naca4412_a{al:02d}", "D",
|
||
f"NACA 4412 (с изгибом), α={al}°, Re=1000",
|
||
"ненулевой Cl при α=0 — прямая проверка средней линии профиля",
|
||
foil_args(c, al, 1000, nx, ny, naca="4412"), nx * ny, st,
|
||
gif=f"naca4412_a{al:02d}.gif", nx=nx)
|
||
|
||
for c in (48, 96, 192):
|
||
nx, ny = 16 * c, 8 * c
|
||
st = int(conv_steps(c, 300) * scale)
|
||
add(f"D{len([r for r in RUNS if r['group']=='D'])+1:02d}_naca_chord{c}", "D",
|
||
f"NACA 0012, α=8°, сходимость по хорде {c}",
|
||
"Cl и Cd сходятся по разрешению хорды",
|
||
foil_args(c, 8, 1000, nx, ny), nx * ny, st, nx=nx)
|
||
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════════
|
||
# E. Сложная и множественная геометрия
|
||
# ══════════════════════════════════════════════════════════════════════════════
|
||
def group_e(scale):
|
||
# тандем: литература даёт переключение режимов около L/D 3.5–4
|
||
for ld in (1.5, 3.0, 5.0):
|
||
d, nx, ny = 48, 1440, 720
|
||
gap = int(ld * d)
|
||
bodies = f"cylinder:d={d},x=360,y=360; cylinder:d={d},x={360 + gap},y=360"
|
||
st = int(conv_steps(d, 800) * scale)
|
||
add(f"E{len([r for r in RUNS if r['group']=='E'])+1:02d}_tandem_ld{int(ld*10)}", "E",
|
||
f"Тандем цилиндров, зазор L/D={ld}",
|
||
"при малом зазоре у заднего тела ОТРИЦАТЕЛЬНОЕ сопротивление; переход около 3.5–4",
|
||
["--bodies", bodies, "--nx", str(nx), "--ny", str(ny), "--re", "150", "--refine", "1"],
|
||
nx * ny, st, gif=f"tandem_ld{int(ld*10)}.gif", nx=nx)
|
||
|
||
# решётка как пористая среда
|
||
for step_d, tag in ((3, "sparse"), (2, "dense")):
|
||
d, nx, ny = 24, 1440, 720
|
||
bodies = "; ".join(
|
||
f"cylinder:d={d},x={360 + i * step_d * d},y={360 + (j - 1.5) * step_d * d}"
|
||
for i in range(4) for j in range(4))
|
||
st = int(conv_steps(d, 600) * scale)
|
||
add(f"E{len([r for r in RUNS if r['group']=='E'])+1:02d}_array_{tag}", "E",
|
||
f"Решётка 4×4 цилиндров, шаг {step_d}D — пористая среда",
|
||
"суммарное сопротивление и структура течения сквозь набор тел",
|
||
["--bodies", bodies, "--nx", str(nx), "--ny", str(ny), "--re", "150", "--refine", "1"],
|
||
nx * ny, st, gif=f"array_{tag}.gif", nx=nx)
|
||
|
||
# перфорированная пластина: набор коротких пластин со щелями
|
||
for open_frac, tag in ((0.2, "open20"), (0.5, "open50")):
|
||
nx, ny = 1200, 600
|
||
seg, gap = 40, int(40 * open_frac / (1 - open_frac))
|
||
bodies = "; ".join(
|
||
f"plate:d={seg},t=0.12,a=90,x=300,y={60 + k * (seg + gap)}"
|
||
for k in range(max(1, (ny - 120) // (seg + gap))))
|
||
st = int(conv_steps(seg, 500) * scale)
|
||
add(f"E{len([r for r in RUNS if r['group']=='E'])+1:02d}_perf_{tag}", "E",
|
||
f"Перфорированная пластина, скважность {open_frac}",
|
||
"струи в щелях и общее сопротивление; проверка множества тонких тел",
|
||
["--bodies", bodies, "--nx", str(nx), "--ny", str(ny), "--re", "500", "--refine", "1"],
|
||
nx * ny, st, gif=f"perf_{tag}.gif", nx=nx)
|
||
|
||
# многоэлементный профиль: основной + предкрылок
|
||
nx, ny = 2048, 1024
|
||
bodies = ("naca:naca=4412,d=200,a=6,x=560,y=512; "
|
||
"naca:naca=2412,d=70,a=18,x=430,y=470")
|
||
st = int(conv_steps(200, 300) * scale)
|
||
add(f"E{len([r for r in RUNS if r['group']=='E'])+1:02d}_multielem", "E",
|
||
"Многоэлементный профиль: основной 4412 плюс предкрылок",
|
||
"щель между элементами и раздельные Cl/Cd по телам",
|
||
["--bodies", bodies, "--nx", str(nx), "--ny", str(ny), "--re", "2000", "--refine", "1"],
|
||
nx * ny, st, gif="multielem.gif", nx=nx)
|
||
|
||
# сверхтонкая пластина: предел субсеточности, тело тоньше клетки
|
||
for t, tag in ((0.02, "t05"), (0.01, "t025")):
|
||
nx, ny = 1200, 600
|
||
st = int(conv_steps(50, 500) * scale)
|
||
add(f"E{len([r for r in RUNS if r['group']=='E'])+1:02d}_thin_{tag}", "E",
|
||
f"Сверхтонкая пластина, толщина {t*50:.2f} клетки",
|
||
"предел субсеточной границы: тело тоньше клетки; ждём откаты на простой отскок",
|
||
["--shape", "plate", "--size", "50", "--thickness", str(t), "--body-angle", "20",
|
||
"--nx", str(nx), "--ny", str(ny), "--re", "500", "--refine", "1"],
|
||
nx * ny, st, gif=f"thin_{tag}.gif", nx=nx)
|
||
|
||
# клин и зазубренная кромка — субклеточная деталь на многоугольнике
|
||
nx, ny = 1200, 600
|
||
st = int(conv_steps(60, 500) * scale)
|
||
add(f"E{len([r for r in RUNS if r['group']=='E'])+1:02d}_wedge", "E",
|
||
"Клин остриём против потока", "острая кромка: особая точка геометрии",
|
||
["--shape", "polygon", "--poly=-30,-18;36,0;-30,18", "--size", "60",
|
||
"--nx", str(nx), "--ny", str(ny), "--re", "500", "--refine", "1"],
|
||
nx * ny, st, gif="wedge.gif", nx=nx)
|
||
saw = ";".join(f"{-30 + 12 * k},{(-1) ** k * 6}" for k in range(6)) + ";30,-18;-30,-18"
|
||
add(f"E{len([r for r in RUNS if r['group']=='E'])+1:02d}_serrated", "E",
|
||
"Зазубренная задняя кромка", "субклеточные зубцы: как их видит граница",
|
||
["--shape", "polygon", f"--poly={saw}", "--size", "60",
|
||
"--nx", str(nx), "--ny", str(ny), "--re", "500", "--refine", "1"],
|
||
nx * ny, st, gif="serrated.gif", nx=nx)
|
||
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════════
|
||
# F. Поле влияния и границы домена
|
||
# ══════════════════════════════════════════════════════════════════════════════
|
||
def group_f(scale):
|
||
d = 48
|
||
for up in (2, 6, 16):
|
||
nx, ny = int((up + 20) * d), 12 * d
|
||
st = int(conv_steps(d, 600) * scale)
|
||
add(f"F{len([r for r in RUNS if r['group']=='F'])+1:02d}_inlet{up}d", "F",
|
||
f"Отступ до входа {up}D",
|
||
"с какого отступа результат перестаёт зависеть от положения входа",
|
||
cyl_args(d, 150, nx, ny, body_x=up * d), nx * ny, st)
|
||
for down in (5, 15, 40):
|
||
nx, ny = int((6 + down) * d), 12 * d
|
||
st = int(conv_steps(d, 600) * scale)
|
||
add(f"F{len([r for r in RUNS if r['group']=='F'])+1:02d}_outlet{down}d", "F",
|
||
f"Отступ до выхода {down}D",
|
||
"с какой длины следа выход перестаёт влиять на силы",
|
||
cyl_args(d, 150, nx, ny, body_x=6 * d), nx * ny, st)
|
||
for r in (1, 2, 3):
|
||
d, nx, ny = 32, 640, 384
|
||
st = int(conv_steps(d, 600) * scale)
|
||
nodes = nx * ny
|
||
if r > 1:
|
||
ax, bx = int(nx // 5 - 1.5 * d), int(nx // 5 + 6.25 * d)
|
||
ay, by = int(ny / 2 - 2 * d), int(ny / 2 + 2 * d)
|
||
nodes += r * (r * (bx - ax) + 1) * (r * (by - ay) + 1)
|
||
add(f"F{len([r_ for r_ in RUNS if r_['group']=='F'])+1:02d}_refine{r}", "F",
|
||
f"Вложенный патч ×{r}",
|
||
"вносит ли связка уровней систематику: Cd и St обязаны совпасть",
|
||
cyl_args(d, 150, nx, ny, refine=r), nodes, st)
|
||
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════════
|
||
# G. Старт, акустика, время жизни
|
||
# ══════════════════════════════════════════════════════════════════════════════
|
||
def group_g(scale):
|
||
d, nx, ny = 48, 960, 576
|
||
for re in (150, 2000, 20000):
|
||
for init in ("uniform", "rest"):
|
||
st = int(conv_steps(d, 300) * scale)
|
||
add(f"G{len([r for r in RUNS if r['group']=='G'])+1:02d}_xt_re{re}_{init}", "G",
|
||
f"x–t диаграмма, Re={re}, старт {init}",
|
||
"откуда идёт стартовое возмущение и раскачивается ли продольная мода",
|
||
cyl_args(d, re, nx, ny, init=init, sponge_len=0,
|
||
xt=f"xt_re{re}_{init}.csv", xt_every=200),
|
||
nx * ny, st, gif=f"xt_re{re}_{init}.gif", nx=nx, extra_out=("xt",))
|
||
for sp in (0, 40):
|
||
st = int(conv_steps(d, 400) * scale)
|
||
add(f"G{len([r for r in RUNS if r['group']=='G'])+1:02d}_sponge{sp}", "G",
|
||
f"Губка выхода {sp} столбцов при Re=5000",
|
||
"губка — единственное, что реально ест продольную моду",
|
||
cyl_args(d, 5000, nx, ny, sponge_len=sp), nx * ny, st)
|
||
for sp in (0, 40):
|
||
st = int(conv_steps(d, 400) * scale)
|
||
add(f"G{len([r for r in RUNS if r['group']=='G'])+1:02d}_spongein{sp}", "G",
|
||
f"Губка входа {sp} столбцов при Re=5000",
|
||
"помогает ли гасить волну до отражения от входа",
|
||
cyl_args(d, 5000, nx, ny, sponge_in=sp), nx * ny, st)
|
||
for re in (10000, 50000, 200000):
|
||
st = int(conv_steps(d, 200) * scale)
|
||
add(f"G{len([r for r in RUNS if r['group']=='G'])+1:02d}_limit_re{re}", "G",
|
||
f"Предел устойчивости, Re={re}",
|
||
"где именно гибнет счёт; часть этих прогонов обязана развалиться",
|
||
cyl_args(d, re, nx, ny), nx * ny, st)
|
||
# тест на время жизни: один очень длинный прогон
|
||
st = int(5_000_000 * scale)
|
||
add(f"G{len([r for r in RUNS if r['group']=='G'])+1:02d}_lifetime", "G",
|
||
"Тест на время жизни: 5·10⁶ шагов",
|
||
"не уплывает ли ⟨ρ⟩ и не деградирует ли решение на очень длинной дистанции",
|
||
cyl_args(d, 150, nx, ny), nx * ny, st, gif="lifetime.gif", nx=nx)
|
||
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════════
|
||
# H. Инварианты и точность
|
||
# ══════════════════════════════════════════════════════════════════════════════
|
||
def group_h(scale):
|
||
d, nx, ny = 32, 640, 384
|
||
st = int(conv_steps(d, 400) * scale)
|
||
add(f"H{len([r for r in RUNS if r['group']=='H'])+1:02d}_sym_cyl", "H",
|
||
"Симметрия: цилиндр при α=0 без возмущения",
|
||
"Cl и Cm обязаны остаться ≈0 — это внутреннее свойство схемы, не литература",
|
||
cyl_args(d, 40, nx, ny, pert_amp=0), nx * ny, st)
|
||
add(f"H{len([r for r in RUNS if r['group']=='H'])+1:02d}_sym_foil", "H",
|
||
"Симметрия: NACA 0012 при α=0 без возмущения",
|
||
"Cl ≈ 0 у симметричного профиля под нулевым углом",
|
||
foil_args(64, 0, 500, 1024, 512, pert_amp=0), 1024 * 512,
|
||
int(conv_steps(64, 300) * scale))
|
||
for al, tag in ((8, "plus"), (-8, "minus")):
|
||
add(f"H{len([r for r in RUNS if r['group']=='H'])+1:02d}_mirror_{tag}", "H",
|
||
f"Зеркальность: NACA 0012 при α={al}°",
|
||
"ряды обязаны зеркалиться: Cl меняет знак, Cd совпадает",
|
||
foil_args(64, al, 500, 1024, 512), 1024 * 512,
|
||
int(conv_steps(64, 300) * scale))
|
||
for ul in (0.02, 0.05, 0.1):
|
||
st2 = int(conv_steps(d, 400, ul) * scale)
|
||
add(f"H{len([r for r in RUNS if r['group']=='H'])+1:02d}_mach{int(ul*100):02d}", "H",
|
||
f"Зависимость от числа Маха, u_lat={ul}",
|
||
"результат обязан сходиться при Ma→0; расхождение и есть сжимаемостная ошибка",
|
||
cyl_args(d, 150, nx, ny, u_lat=ul), nx * ny, st2)
|
||
# прямой замер расхождения f32 и f64 от длины прогона
|
||
for bk in ("cpu", "gpu"):
|
||
add(f"H{len([r for r in RUNS if r['group']=='H'])+1:02d}_prec_{bk}", "H",
|
||
f"Точность: одинаковая постановка на {bk}",
|
||
"расхождение f32 против f64 в зависимости от длины прогона",
|
||
cyl_args(d, 150, nx, ny), nx * ny, int(conv_steps(d, 800) * scale), backend=bk)
|
||
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════════
|
||
# I. Сверхмелкие сетки по всем формам
|
||
# ══════════════════════════════════════════════════════════════════════════════
|
||
def group_i(scale):
|
||
shapes = [
|
||
("cylinder", ["--shape", "cylinder"]),
|
||
("square", ["--shape", "square"]),
|
||
("diamond", ["--shape", "diamond"]),
|
||
("ellipse", ["--shape", "ellipse", "--thickness", "0.35"]),
|
||
("naca", ["--shape", "naca", "--naca", "0012", "--body-angle", "8"]),
|
||
("triangle", ["--shape", "triangle"]),
|
||
("plate", ["--shape", "plate", "--thickness", "0.08", "--body-angle", "25"]),
|
||
("wedge", ["--shape", "polygon", "--poly=-128,-72;154,0;-128,72"]),
|
||
]
|
||
d, nx, ny = 256, 4096, 2048
|
||
st = int(conv_steps(d, 300) * scale)
|
||
for tag, extra in shapes:
|
||
add(f"I{len([r for r in RUNS if r['group']=='I'])+1:02d}_fine_{tag}", "I",
|
||
f"Сверхмелкая сетка 4096×2048, {tag}, D=256",
|
||
"держится ли субсеточная граница на предельном разрешении для этой формы",
|
||
extra + ["--size", str(d), "--nx", str(nx), "--ny", str(ny),
|
||
"--body-x", "820", "--re", "1000", "--refine", "1"],
|
||
nx * ny, st, gif=f"fine_{tag}.gif", nx=nx)
|
||
bodies = ("cylinder:d=200,x=800,y=1024; cylinder:d=200,x=1400,y=1024; "
|
||
"naca:naca=4412,d=300,a=10,x=2200,y=1024")
|
||
add(f"I{len([r for r in RUNS if r['group']=='I'])+1:02d}_fine_multi", "I",
|
||
"Сверхмелкая сетка, композиция из трёх тел",
|
||
"взаимодействие следов на предельном разрешении",
|
||
["--bodies", bodies, "--nx", str(nx), "--ny", str(ny), "--re", "1000", "--refine", "1"],
|
||
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":
|
||
return r["cost"] / (ASSUMED_CPU_MLUPS * 1e6) / 3600
|
||
n = r["nodes_per_step"]
|
||
if n > SPLIT_NODES:
|
||
m = SPLIT_MLUPS
|
||
elif n <= DZN_MLUPS[0][0]:
|
||
m = DZN_MLUPS[0][1]
|
||
elif n >= DZN_MLUPS[-1][0]:
|
||
m = DZN_MLUPS[-1][1]
|
||
else:
|
||
for (n0, m0), (n1, m1) in zip(DZN_MLUPS, DZN_MLUPS[1:]):
|
||
if n <= n1:
|
||
t = math.log(n / n0) / math.log(n1 / n0)
|
||
m = m0 + t * (m1 - m0)
|
||
break
|
||
rel = m / DZN_MLUPS[-1][1]
|
||
return r["cost"] / (ASSUMED_GPU_MLUPS * 1e6 * rel) / 3600
|
||
|
||
|
||
def assign_shards(runs, n):
|
||
"""Разложить прогоны по n долям жадно (LPT): самый долгий — в самую лёгкую долю.
|
||
Порядок прогонов внутри доли остаётся порядком групп. Возвращает часы по долям."""
|
||
load = [0.0] * n
|
||
for r in sorted(runs, key=lambda r: (-est_hours(r), r["id"])):
|
||
k = min(range(n), key=lambda i: load[i])
|
||
r["shard"] = k + 1
|
||
load[k] += est_hours(r)
|
||
return load
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--scale", type=float, default=2.0,
|
||
help="общий множитель длительности всех прогонов")
|
||
ap.add_argument("--shards", type=int, default=5,
|
||
help="на сколько равных по времени долей делить кампанию")
|
||
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,
|
||
group_j, group_k):
|
||
g(args.scale)
|
||
shard_hours = assign_shards(RUNS, args.shards)
|
||
|
||
doc = {
|
||
"meta": {
|
||
"note": "Стоимость в обновлениях узлов — единственная переносимая между машинами "
|
||
"мера. Часы драйвер получает, поделив её на фактические MLUPS, которые "
|
||
"замеряет на месте (--calibrate).",
|
||
"gif_policy": "вся длительность прогона, реальная скорость, 10 кадр/с; кадр "
|
||
"прореживается до ширины не больше 1280 пикселей",
|
||
"scale": args.scale,
|
||
"assumed_gpu_mlups": ASSUMED_GPU_MLUPS,
|
||
"assumed_cpu_mlups": ASSUMED_CPU_MLUPS,
|
||
"shards": args.shards,
|
||
"shard_hours": [round(h, 2) for h in shard_hours],
|
||
},
|
||
"runs": RUNS,
|
||
}
|
||
with open(args.out, "w", encoding="utf-8", newline="\n") as f:
|
||
json.dump(doc, f, ensure_ascii=False, indent=1)
|
||
|
||
print(f"прогонов: {len(RUNS)} файл: {args.out}\n")
|
||
print(f"{'гр':>3}{'прогонов':>10}{'обновлений узлов':>20}{'часов GPU':>12}{'часов CPU':>12}")
|
||
tot_gpu = tot_cpu = 0.0
|
||
for g in sorted({r["group"] for r in RUNS}):
|
||
rs = [r for r in RUNS if r["group"] == g]
|
||
cost = sum(r["cost"] for r in rs)
|
||
hg = sum(r["cost"] / 1.2e9 / 3600 for r in rs if r["backend"] == "gpu")
|
||
hc = sum(r["cost"] / 2.2e7 / 3600 for r in rs if r["backend"] == "cpu")
|
||
tot_gpu += hg
|
||
tot_cpu += hc
|
||
print(f"{g:>3}{len(rs):>10}{cost:>20.3e}{hg:>12.1f}{hc:>12.1f}")
|
||
print(f"{'—':>3}{len(RUNS):>10}{sum(r['cost'] for r in RUNS):>20.3e}"
|
||
f"{tot_gpu:>12.1f}{tot_cpu:>12.1f}")
|
||
print(f"\nвсего ≈ {tot_gpu + tot_cpu:.1f} ч при 1200 MLUPS на GPU и 22 на CPU")
|
||
|
||
print("\nдоли (часы с поправкой на размер сетки, см. DZN_MLUPS):")
|
||
for k, h in enumerate(shard_hours, 1):
|
||
n = sum(1 for r in RUNS if r["shard"] == k)
|
||
print(f" доля {k}: {n:>3} прогонов ≈ {h:5.1f} ч")
|
||
print(f" разброс: {(max(shard_hours) / min(shard_hours) - 1) * 100:.1f} %")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|