#!/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()