#!/usr/bin/env python3 # Генератор списка прогонов кампании -> scenarios.json # # Список порождается кодом, а не правится руками: серии по Re, по разрешению и по углу атаки # получаются циклами, а стоимость каждого прогона считается тут же и суммируется. Запуск: # python gen_scenarios.py # перезаписать scenarios.json и напечатать смету # python gen_scenarios.py --scale 2 # растянуть все длительности вдвое # # Стоимость меряется в ОБНОВЛЕНИЯХ УЗЛОВ (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 МБ 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) def main(): ap = argparse.ArgumentParser() ap.add_argument("--scale", type=float, default=2.0, 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): g(args.scale) doc = { "meta": { "note": "Стоимость в обновлениях узлов — единственная переносимая между машинами " "мера. Часы драйвер получает, поделив её на фактические MLUPS, которые " "замеряет на месте (--calibrate).", "gif_policy": "вся длительность прогона, реальная скорость, 10 кадр/с; кадр " "прореживается до ширины не больше 1280 пикселей", "scale": args.scale, "assumed_gpu_mlups": 1200, "assumed_cpu_mlups": 22, }, "runs": RUNS, } with open(args.out, "w", encoding="utf-8") 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") if __name__ == "__main__": main()