Решатель: - --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>
195 lines
9.3 KiB
Python
195 lines
9.3 KiB
Python
#!/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()
|