Состояние на момент заведения репозитория. 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>
185 lines
12 KiB
Python
185 lines
12 KiB
Python
# ФАКТОРНОЕ ИССЛЕДОВАНИЕ постановки: какие границы расчётной области отвечают за завышение
|
||
# 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()
|