Состояние на момент заведения репозитория. 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>
105 lines
8.4 KiB
Python
105 lines
8.4 KiB
Python
# Граничные условия: (1) тело/стенки — SDF + интерполированный отскок Bouzidi; (2) вход/выход канала.
|
||
#
|
||
# Bouzidi (линейный): для линка из жидкого узла x_f в твёрдый по направлению i вводится
|
||
# доля пересечения q = |x_f→стенка| / |x_f→x_solid| ∈ (0,1):
|
||
# q < 1/2 (есть «дальний» жидкий сосед x_f − c_i): f_ī = 2q·f_i^post + (1−2q)·f_i^post(дальний)
|
||
# q ≥ 1/2: f_ī = (1/2q)·f_i^post + (1−1/2q)·f_ī^post
|
||
# Здесь f_i^post — после столкновения, f_ī — то, что вернётся в x_f после переноса.
|
||
import backend as B
|
||
import lattice as L
|
||
|
||
cp = B.cp; xp = B.xp; DTYPE = B.DTYPE
|
||
Q = L.Q; Cx = L.Cx; Cy = L.Cy; OPP = L.OPP
|
||
|
||
def build_bc(solid, phi, body):
|
||
"""Сборка структуры ГУ (один раз). Для каждого направления i (1..8):
|
||
mask — жидкий узел, чей сосед по +c_i твёрдый;
|
||
q — доля пересечения по SDF (клип [0.02,0.98]);
|
||
xff — есть ли «дальний» жидкий сосед (для линейной формулы при q<1/2);
|
||
cl — подмаска линков именно ТЕЛА body (для расчёта силы)."""
|
||
fluid = ~solid; bc = []
|
||
for i in range(1, Q):
|
||
nb_solid = cp.roll(solid, (-Cy[i], -Cx[i]), (0, 1)); mask = fluid & nb_solid
|
||
cl = mask & cp.roll(body, (-Cy[i], -Cx[i]), (0, 1))
|
||
phinb = cp.roll(phi, (-Cy[i], -Cx[i]), (0, 1))
|
||
qq = phi / (phi - phinb) # деление; nan/inf маскируются ниже
|
||
q = xp.where(mask, cp.clip(qq, 0.02, 0.98), cp.asarray(0.5, DTYPE))
|
||
xff = mask & (~cp.roll(solid, (Cy[i], Cx[i]), (0, 1)))
|
||
bc.append((i, OPP[i], mask, q.astype(DTYPE), xff, cl))
|
||
return bc
|
||
|
||
def apply_bc(f, fpost, bc):
|
||
"""Применить Bouzidi ко всем линкам (полностью через where, GPU-friendly).
|
||
f — поле ПОСЛЕ стриминга (его и правим); fpost — поле ПОСЛЕ столкновения (до стриминга)."""
|
||
for (i, ib, mask, q, xff, cl) in bc:
|
||
fi = fpost[i]; fib = fpost[ib]
|
||
fiback = cp.roll(fpost[i], (Cy[i], Cx[i]), (0, 1)) # популяция «дальнего» соседа
|
||
near = mask & (q < 0.5) & xff
|
||
bad = mask & (q < 0.5) & (~xff) # нет дальнего соседа → простой отскок
|
||
far = mask & (q >= 0.5)
|
||
new = f[ib]
|
||
new = xp.where(near, 2*q*fi + (1 - 2*q)*fiback, new)
|
||
new = xp.where(far, (1/(2*q))*fi + (1 - 1/(2*q))*fib, new)
|
||
new = xp.where(bad, fi, new)
|
||
f[ib] = new
|
||
return f
|
||
|
||
def channel_bc(f, uin, rho_out=1.0, outlet_uy="zero"):
|
||
"""Корректные ГУ канала (Zou & He 1997), нумерация D2Q9: 1=E,2=N,3=W,4=S,5=NE,6=NW,7=SW,8=SE.
|
||
ВХОД (запад, x=0): скоростной Zou-He — задаём u=(ux,uy) из uin, плотность ρ ПЛАВАЕТ; достраиваем
|
||
приходящие извне популяции 1,5,8 из известных 0,2,3,4,6,7.
|
||
ВЫХОД (восток, x=-1): давление Zou-He — задаём ρ=rho_out, скорость ux плавает; достраиваем 3,6,7.
|
||
Поперечная скорость на выходе — по outlet_uy:
|
||
"zero" — классический Zou-He: uy=0 (жёстко). Вихри дорожки приходят на выход с
|
||
uy~±0.3U → принудительное зануление отражает их назад к телу (завышает
|
||
rms Cl и модулирует Cd — см. факторное исследование в ноутбуке);
|
||
"extrapolate" — uy берётся из соседнего столбца x=-2 (нуль-градиент поперечной скорости);
|
||
ρ-якорь (масса) сохраняется, кинематическое отражение снимается.
|
||
Зачем давление-выход вообще: грубый zero-gradient по f не выпускал поток → масса/противодавление
|
||
копились, поток глох (⟨ρ⟩↑, Cd/rmsCl→0). Давление-выход якорит массу, скоростной вход её не
|
||
пересоздаёт → дрейфа нет. Всё на срезах (graph-safe, без host→device).
|
||
|
||
ПОРЯДОК ВЫЗОВА: строго ПОСЛЕ free_slip_walls и покрывая ВЕСЬ столбец (включая ряды 0 и Ny−1).
|
||
Причина: stream() сделан через roll и потому периодичен по ОБЕИМ осям. Заворот по y снимают
|
||
стенки, заворот по x — этот ГУ. Если оставить углы (ряды 0 и Ny−1) без Zou-He, в них популяции
|
||
с cx=+1 на входе приходят прямо из столбца выхода (замерено: f[1,0,0] == post[1,0,-1], величина
|
||
~0.12) — вход и выход оказываются физически связаны в 4 узлах. Известные популяции, по которым
|
||
здесь считаются ρ (вход) и ux (выход), x-заворота не содержат ни в одном ряду, а те, что
|
||
заворот содержат, — это ровно неизвестные, которые ГУ и перезаписывает; поэтому такой порядок
|
||
закрывает углы полностью."""
|
||
# --- вход (запад): скоростной Zou-He ---
|
||
ux = uin[0, :, 0]; uy = uin[1, :, 0]
|
||
f0 = f[0, :, 0]; f2 = f[2, :, 0]; f3 = f[3, :, 0]
|
||
f4 = f[4, :, 0]; f6 = f[6, :, 0]; f7 = f[7, :, 0]
|
||
rho = (f0 + f2 + f4 + 2.0*(f3 + f6 + f7)) / (1.0 - ux)
|
||
f[1, :, 0] = f3 + (2.0/3.0)*rho*ux
|
||
f[5, :, 0] = f7 - 0.5*(f2 - f4) + (1.0/6.0)*rho*ux + 0.5*rho*uy
|
||
f[8, :, 0] = f6 + 0.5*(f2 - f4) + (1.0/6.0)*rho*ux - 0.5*rho*uy
|
||
# --- выход (восток): давление Zou-He ---
|
||
g0 = f[0, :, -1]; g1 = f[1, :, -1]; g2 = f[2, :, -1]
|
||
g4 = f[4, :, -1]; g5 = f[5, :, -1]; g8 = f[8, :, -1]
|
||
uxo = (g0 + g2 + g4 + 2.0*(g1 + g5 + g8)) / rho_out - 1.0
|
||
if outlet_uy == "extrapolate":
|
||
# нуль-градиент поперечной скорости: uy с предвыходного столбца (он полностью известен)
|
||
col = f[:, :, -2]
|
||
uyo = ((col[2] + col[5] + col[6]) - (col[4] + col[7] + col[8])) / col.sum(0)
|
||
else:
|
||
uyo = 0.0
|
||
f[3, :, -1] = g1 - (2.0/3.0)*rho_out*uxo
|
||
f[6, :, -1] = g8 - 0.5*(g2 - g4) - (1.0/6.0)*rho_out*uxo + 0.5*rho_out*uyo
|
||
f[7, :, -1] = g5 + 0.5*(g2 - g4) - (1.0/6.0)*rho_out*uxo - 0.5*rho_out*uyo
|
||
return f
|
||
|
||
def free_slip_walls(f):
|
||
"""FREE-SLIP (скользящие) стенки канала: верх/низ — зеркальное (specular) отражение.
|
||
Сохраняет касательный (x) импульс (нет трения, нет пограничного слоя) и нормальный заворачивает;
|
||
масса сохраняется (копирование популяций) → дрейфа не вносит. Применять ПОСЛЕ стриминга.
|
||
Нумерация: 2=(0,+1) 4=(0,-1) 5=(+1,+1) 6=(-1,+1) 7=(-1,-1) 8=(+1,-1).
|
||
Нижняя стенка (row 0): достраиваем вверх-идущие из вниз-идущих (зеркало по y, x сохранён).
|
||
Верхняя стенка (row -1): достраиваем вниз-идущие из вверх-идущих.
|
||
ВНИМАНИЕ: это только искусственные стенки канала; цилиндр остаётся no-slip (Bouzidi).
|
||
ПОРЯДОК: вызывать ПЕРЕД channel_bc (см. его docstring — так закрываются углы домена)."""
|
||
f[2, 0, :] = f[4, 0, :]; f[5, 0, :] = f[8, 0, :]; f[6, 0, :] = f[7, 0, :]
|
||
f[4, -1, :] = f[2, -1, :]; f[7, -1, :] = f[6, -1, :]; f[8, -1, :] = f[5, -1, :]
|
||
return f
|