Files
CFDManager/docs/theory/2d_solver/bench/preflight.py
T
NotBigGhostandClaude Opus 5 8d17bc8014 Валидационная кампания на 115 прогонов и Docker-образ для сервера
bench/ — список сценариев (порождается gen_scenarios.py, а не правится
руками), драйверы под Linux и Windows, предполётная проверка и описание.
Девять групп: эталоны первоисточников, цилиндр против литературы, модели
стенки и субсеточность, профили крыла, сложная и множественная геометрия,
границы домена, старт и время жизни, внутренние инварианты, сверхмелкие
сетки до 4096×2048. Стоимость планируется в обновлениях узлов —
единственной переносимой между машинами мере; в часы драйвер переводит её
по фактическим MLUPS, которые замеряет на месте (--calibrate). Итого
3.76e14 обновлений, ≈90 часов на 4070 Ti.

Гифки пишутся на всю длительность прогона в реальном времени, 10 кадр/с.
Число кадров этим задано жёстко, поэтому размер регулируется только кадром:
замерено 0.103 байта на пиксель после LZW, отсюда бюджет кадры×пиксели с
нижней границей по ширине. Итог 4.4 ГБ, самый тяжёлый файл 226 МБ.

preflight.py гоняет каждый сценарий на два шага. Окупился сразу: поймал,
что вся группа сверхмелких сеток падала на пределе GPU, а девять прогонов
группы F передавали --body-x дважды. Оба отказа проявились бы только на
сервере, часов через двадцать после старта кампании.

Docker: двухстадийная сборка, в образе libvulkan1 и vulkan-tools.
NVIDIA Container Toolkit подкладывает Vulkan-ICD только при graphics в
NVIDIA_DRIVER_CAPABILITIES — без него wgpu не увидит карту, поэтому
capabilities прописаны в образе, а vulkaninfo лежит внутрь для проверки.
ENTRYPOINT — драйвер кампании, CMD по умолчанию --dry-run, чтобы случайный
docker run не запустил сточасовую задачу. Проверено локально: образ
собирается, смоук проходит с монтированием результатов на хост, физика
совпадает с хостовой до последней цифры, --backend gpu без карты отказывает
явно. GPU-путь в контейнере проверяется только на машине с картой.

В README поправлено разделение вкладов в продольную акустику: измерением
2×2 показано, что 74.9% → 1.1% даёт ОДНОРОДНЫЙ СТАРТ, причём при полностью
выключенной губке, а губка снимает лишь остаток (1.12% → 0.83% при
утроении длины). При старте из покоя губка не помогает совсем — у стоячей
четвертьволновой моды там узел давления, где вязкость её не трогает.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 17:36:40 +03:00

80 lines
3.4 KiB
Python

#!/usr/bin/env python3
# Предполётная проверка кампании: каждый сценарий запускается на два шага.
#
# python preflight.py # проверить все
# python preflight.py --group I # только группу
#
# Ловит опечатки в ключах, несовместимые сочетания и пределы железа ДО того, как кампания
# уйдёт считать на девяносто часов. Не бесплатная привычка, а окупившаяся: именно так
# выяснилось, что вся группа сверхмелких сеток падала на пределе GPU в 65535 рабочих групп
# на измерение, а девять прогонов группы F передавали --body-x дважды.
#
# Физику проверка не трогает вовсе: два шага — это ровно «запустилось и не упало».
import argparse
import json
import os
import subprocess
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
BIN = os.environ.get("KBC2D_BIN") or os.path.join(
HERE, "..", "target", "release", "kbc2d" + (".exe" if os.name == "nt" else ""))
# Всё, что пишет файлы, выкидываем: проверяем запуск, а не вывод. Ключ идёт со значением,
# поэтому следующий за ним аргумент тоже пропускается.
DROP = {"--gif", "--xt", "--case-csv", "--gif-field", "--gif-downsample", "--gif-scale",
"--gif-every", "--gif-fps", "--series-every"}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--group", help="только эта группа, например I")
ap.add_argument("--scenarios", default=os.path.join(HERE, "scenarios.json"))
args = ap.parse_args()
runs = json.load(open(args.scenarios, encoding="utf-8"))["runs"]
if args.group:
runs = [r for r in runs if r["group"].upper() == args.group.upper()]
if not runs:
print("под выборку не попал ни один прогон")
return 0
if not os.path.exists(BIN):
print(f"не найден бинарь решателя: {BIN}")
return 1
bad = []
for i, r in enumerate(runs, 1):
argv, skip = [], False
for a in r["args"]:
if skip:
skip = False
continue
if a in DROP:
skip = True
continue
argv.append(a)
if "--steps" in argv:
argv[argv.index("--steps") + 1] = "2"
else:
argv += ["--steps", "2"]
p = subprocess.run([BIN] + argv + ["--verbose", "quiet", "--report-every", "0"],
capture_output=True, cwd=HERE)
ok = p.returncode == 0
print("." if ok else "X", end="", flush=True)
if not ok:
tail = (p.stdout + p.stderr).decode("utf-8", "replace").strip().splitlines()
bad.append((r["id"], tail[-1] if tail else "(без вывода)"))
if i % 40 == 0:
print(f" {i}/{len(runs)}", flush=True)
print(f"\n\nпроверено сценариев: {len(runs)}, не запустились: {len(bad)}")
for id_, msg in bad:
print(f" {id_:<28} {msg}")
return 1 if bad else 0
if __name__ == "__main__":
sys.exit(main())