Files
CFDManager/docs/theory/2d_solver/bench/compare.py
T
NotBigGhostandClaude Opus 5.5 86e9c7139d Пристеночная функция 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>
2026-10-02 12:25:43 +03:00

195 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()