Files
CFDManager/docs/theory/solver_2x_sdf/run_factors.py
NotBigGhostandClaude Opus 5 11ff7b79b4 Начальный коммит: Vulkan-редактор SimVulcan + исследование KBC-LBM
Состояние на момент заведения репозитория.

C++ приложение (src/, shaders/, tests/) — минимальный редактор 3D-моделей
на Vulkan 1.3: орбитальная камера, три опорные сетки через начало координат,
загрузка .obj с режимами отображения. Весь Vulkan изолирован в src/vk/.

Исследование (docs/) — оригинальные статьи по KBC (docs/origins) и
Python-решатель D2Q9 KBC-N1 с AMR 2x и SDF+Bouzidi (docs/theory).

В решателе перед коммитом исправлены дефекты, найденные сверкой с
первоисточниками: относительный порог знаменателя энтропийного стабилизатора
(абсолютный вырождал KBC в LBGK на 77-99% узлов), заворот вход/выход в углах
домена, диагностика средней плотности по фиктивным узлам тела, зашитый
refine=2. Подробности — docs/theory/solver_2x_sdf/README.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 16:37:38 +03:00

185 lines
12 KiB
Python
Raw Permalink 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.
# ФАКТОРНОЕ ИССЛЕДОВАНИЕ постановки: какие границы расчётной области отвечают за завышение
# Cd / rms Cl относительно безграничной литературы (Re=150: Cd≈1.33, rms Cl≈0.3, St≈0.183).
#
# Контекст (эксперимент №0 — серия по боковой блокировке, см. ноутбук, раздел 23): Cd(β)
# немонотонен (1.80 → 1.77 → 1.87 при β = 0.178 → 0.125 → 0.089) → боковое стеснение НЕ
# доминирующий фактор. Подозреваемые — продольные границы, которые серия держала константой:
# вход 2.5D (жёсткий равномерный профиль), выход 8.3D (Zou-He давление с жёстким uy=0,
# отражает вихри дорожки), и их связка.
#
# Здесь каждый фактор варьируется ОТДЕЛЬНО при фиксированной боковой блокировке (Ny=90):
# изоляция вклада входа, выхода и выходного ГУ. Все результаты накапливаются в out/factors.csv
# (одна строка на прогон, с полной конфигурацией) — реестр для документирования исследования.
#
# Запуск: python run_factors.py
# AMR_FACTORS=B0_baseline,F4_outlet_uy python run_factors.py # подмножество факторов
# AMR_STEPS=20000 python run_factors.py # смоук
import os, csv, time
import numpy as np
import config as C
import backend as B
import solver as S
import diagnostics as Diag
import visualization as Viz
HERE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.join(HERE, "out"); os.makedirs(OUT, exist_ok=True)
LIT = dict(Cd=1.33, Clrms=0.3, St=0.183)
# (имя, overrides конфига, что изолирует)
# Эксперимент №1 (B0–F5, прогнан 2026-06-10): итоги — мягкий uy-выход (F4) выигрывает по всем
# метрикам; ВСЕ длинные домены без демпфера уходят в смещённый режим ⟨ρ⟩≈1.66 (акустический
# резонатор «вход-скорость/выход-давление» + накачка массы входом) или взрываются (F5).
# Эксперимент №2 (F6–F8): губка перед выходом — лечит акустику, открывает длинные домены.
FACTORS = [
("B0_baseline", dict(),
"база 174x90: вход 2.5D, выход 8.3D, uy=0 (контроль; кросс-чек уже с конвективным членом)"),
("F1_outlet_far", dict(Nx=300),
"выход 8.3D -> 16.2D (вход 2.5D как в базе) — изолирует близость выхода"),
("F2_inlet_far", dict(Nx=230, cx=96),
"вход 2.5D -> 6.0D (выход 8.3D как в базе) — изолирует близость входа"),
("F3_inout_far", dict(Nx=390, cx=96),
"вход 6.0D + выход 18.3D — оба продольных фактора вместе"),
("F4_outlet_uy", dict(outlet_uy="extrapolate"),
"мягкий выход: uy нуль-градиент вместо uy=0 (геометрия базовая) — изолирует отражение вихрей"),
("F5_combo", dict(Nx=390, cx=96, outlet_uy="extrapolate"),
"вход 6.0D + выход 18.3D + мягкий uy-выход — всё вместе"),
# --- эксперимент №2: губка (absorbing layer) ---
("F6_sponge", dict(outlet_uy="extrapolate", sponge_len=32),
"база 174x90 + мягкий uy + ГУБКА 32 столбца (nu x30) — валидация губки против F4"),
("F7_long_sponge", dict(Nx=390, cx=96, outlet_uy="extrapolate", sponge_len=32),
"вход 6D + выход 18.3D + мягкий uy + губка — целевая «чистая» постановка"),
("F8_long_sp_uy0", dict(Nx=390, cx=96, sponge_len=32),
"длинный домен + губка, но ЖЁСТКОЕ uy=0 — изолирует вклад губки от uy-режима"),
]
CSV_FIELDS = ["when", "name", "status", "steps", "Nx", "Ny", "cx", "in_D", "out_D", "outlet_uy",
"sponge_len", "beta", "St", "St_corr", "Cd", "Clrms", "Cm", "Cd_st", "Clrms_st",
"dCd_pct", "dCl_pct", "rho_last", "cd_win_std", "cl_win_std", "wall_s", "desc"]
def window_stats(res, cfg, nwin=10):
"""Срединная изменчивость: std оконных средних Cd и оконных rms Cl (окна 2..nwin,
первое окно с разгоном пропускается). Метрика низкочастотной модуляции дорожки."""
Fx = B.to_cpu(res["Fx"]); Fy = B.to_cpu(res["Fy"])
Cd = 2.0*Fx/(cfg.U**2*res["DL"]); Cl = 2.0*Fy/(cfg.U**2*res["DL"])
n = len(Cd); w = n // nwin
cds = [float(Cd[k*w:(k+1)*w].mean()) for k in range(1, nwin)]
cls = [float(Cl[k*w:(k+1)*w].std()) for k in range(1, nwin)]
return float(np.std(cds)), float(np.std(cls))
def run_factor(name, overrides, desc):
cfg = C.from_env(frame_every=0, **overrides)
in_D = cfg.cx / cfg.D; out_D = (cfg.Nx - 1 - cfg.cx) / cfg.D
print(f"\n=== Фактор {name}: {desc}")
print(f" домен {cfg.Nx}x{cfg.Ny}; вход {in_D:.1f}D / выход {out_D:.1f}D; "
f"outlet_uy={cfg.outlet_uy}; губка={cfg.sponge_len or '—'}; "
f"патч x=[{cfg.ax1},{cfg.bx1}] y=[{cfg.ay1},{cfg.by1}]; steps={cfg.steps}")
t0 = time.perf_counter()
sim = S.Solver(cfg)
res = sim.run(Viz.make_progress(name[:8]))
wall = time.perf_counter() - t0
print(" время:", round(wall, 1), "s")
status = "ok" if len(res["Fx"]) == cfg.steps + 1 else f"BLEW_UP@{len(res['Fx'])}"
a = Diag.analyze(res, cfg)
Diag.print_table(a, cfg)
Diag.print_crosscheck(a)
Diag.convergence_report(res, cfg)
cd_ws, cl_ws = window_stats(res, cfg)
rho_last = float(B.to_cpu(res["rho"])[-1])
np.savez(os.path.join(OUT, f"factor_{name}.npz"),
St=a["St"], Cd=a["Cd"], Clrms=a["Clrms"], uyrms=a["uyrms"],
Cm=a.get("Cm", float("nan")),
Cd_st=a.get("Cd_st", float("nan")), Clrms_st=a.get("Clrms_st", float("nan")),
Nx=cfg.Nx, Ny=cfg.Ny, cx=cfg.cx, D=cfg.D, U=cfg.U, Re=cfg.Re, steps=cfg.steps,
outlet_uy=cfg.outlet_uy, beta=cfg.D/cfg.Ny,
Cl=a["Cl_s"], uy=a["uy"], rho=B.to_cpu(res["rho"]),
Fx=B.to_cpu(res["Fx"]), Fy=B.to_cpu(res["Fy"]), Tz=B.to_cpu(res["Tz"]))
beta = cfg.D / cfg.Ny
dcd = abs(a.get("Cd_st", float("nan")) - a["Cd"]) / a["Cd"] * 100.0
dcl = abs(a.get("Clrms_st", float("nan")) - a["Clrms"]) / max(a["Clrms"], 1e-12) * 100.0
row = dict(when=time.strftime("%Y-%m-%d %H:%M:%S"), name=name, status=status, steps=cfg.steps,
Nx=cfg.Nx, Ny=cfg.Ny, cx=cfg.cx, in_D=round(in_D, 2), out_D=round(out_D, 2),
outlet_uy=cfg.outlet_uy, sponge_len=cfg.sponge_len, beta=round(beta, 4),
St=round(a["St"], 4), St_corr=round(a["St"]*(1-beta), 4),
Cd=round(a["Cd"], 4), Clrms=round(a["Clrms"], 4),
Cm=round(a.get("Cm", float("nan")), 6),
Cd_st=round(a.get("Cd_st", float("nan")), 4),
Clrms_st=round(a.get("Clrms_st", float("nan")), 4),
dCd_pct=round(dcd, 2), dCl_pct=round(dcl, 2),
rho_last=round(rho_last, 5), cd_win_std=round(cd_ws, 4),
cl_win_std=round(cl_ws, 4), wall_s=round(wall, 1), desc=desc)
del sim, res, a
B.cp.get_default_memory_pool().free_all_blocks()
return row
def append_csv(rows):
path = os.path.join(OUT, "factors.csv")
if os.path.exists(path):
with open(path, encoding="utf-8") as fh:
header = fh.readline().strip()
if header != ",".join(CSV_FIELDS):
# схема реестра изменилась — старый файл сохраняем (история не теряется)
k = 1
while os.path.exists(os.path.join(OUT, f"factors_v{k}.csv")):
k += 1
os.rename(path, os.path.join(OUT, f"factors_v{k}.csv"))
print(f"[реестр] схема CSV изменилась — прежний файл сохранён как factors_v{k}.csv")
new = not os.path.exists(path)
with open(path, "a", newline="", encoding="utf-8") as fh:
wr = csv.DictWriter(fh, fieldnames=CSV_FIELDS)
if new:
wr.writeheader()
for r in rows:
wr.writerow(r)
print("CSV:", path, f"(+{len(rows)} строк)")
def save_summary_figure(rows, path):
import matplotlib; matplotlib.use("Agg")
import matplotlib.pyplot as plt
names = [r["name"] for r in rows]
xs = np.arange(len(rows))
fig, axs = plt.subplots(1, 3, figsize=(15, 4.6))
ax = axs[0]
ax.bar(xs, [r["Cd"] for r in rows], color="#1f6feb", label="⟨Cd⟩ GMEM")
ax.plot(xs, [r["Cd_st"] for r in rows], "o", color="#ff8f00", label="⟨Cd⟩ ∮[σ·n−ρu(u·n)]ds")
ax.axhline(LIT["Cd"], color="#d1495b", ls="--", label=f"лит. безгранич. {LIT['Cd']}")
ax.set_ylabel("⟨Cd⟩"); ax.set_title("Сопротивление по факторам")
ax = axs[1]
ax.bar(xs, [r["Clrms"] for r in rows], color="#2e7d32")
ax.axhline(LIT["Clrms"], color="#d1495b", ls="--", label=f"лит. ~{LIT['Clrms']}")
ax.set_ylabel("rms Cl"); ax.set_title("Амплитуда подъёмной силы")
ax = axs[2]
ax.bar(xs, [r["cd_win_std"] for r in rows], color="#7b1fa2")
ax.set_ylabel("std оконных ⟨Cd⟩"); ax.set_title("НЧ-модуляция (меньше — стабильнее)")
for ax in axs:
ax.set_xticks(xs)
ax.set_xticklabels([f"{r['name']}\nвх {r['in_D']}D / вых {r['out_D']}D\n"
f"uy:{r['outlet_uy']} губ:{r['sponge_len']}" for r in rows], fontsize=7)
ax.legend(fontsize=8)
fig.suptitle("Факторное исследование постановки (Ny=90, β=0.178 фикс., 100k шагов)", fontsize=12)
fig.tight_layout(rect=(0, 0, 1, 0.94))
fig.savefig(path, bbox_inches="tight"); plt.close(fig)
print("saved", os.path.basename(path))
def main():
sel = os.environ.get("AMR_FACTORS")
todo = FACTORS if not sel else [f for f in FACTORS if f[0] in sel.split(",")]
print(f"[факторы] backend={B.BACKEND}; прогонов: {len(todo)}")
rows = [run_factor(*f) for f in todo]
print("\n=== Сводка факторного исследования ===")
hdr = (f"{'фактор':15}{'вх,D':>6}{'вых,D':>7}{'uy':>12}{'губка':>6}{'St':>7}{'<Cd>':>8}"
f"{'rms Cl':>8}{'Cd σ·n':>8}{'dCd%':>6}{'<ρ>фин':>8}{'модул.':>8} статус")
print(hdr)
for r in rows:
print(f"{r['name']:15}{r['in_D']:>6}{r['out_D']:>7}{r['outlet_uy']:>12}{r['sponge_len']:>6}"
f"{r['St']:>7.3f}{r['Cd']:>8.3f}{r['Clrms']:>8.4f}{r['Cd_st']:>8.3f}{r['dCd_pct']:>6.1f}"
f"{r['rho_last']:>8.4f}{r['cd_win_std']:>8.4f} {r['status']}")
print(f"{'лит. безгранич.':15}{'':>6}{'':>7}{'':>12}{'':>6}{LIT['St']:>7.3f}{LIT['Cd']:>8.3f}{LIT['Clrms']:>8.4f}")
append_csv(rows)
if len(rows) > 1:
save_summary_figure(rows, os.path.join(OUT, "factors_summary.png"))
print("DONE (факторы)")
if __name__ == "__main__":
main()