Состояние на момент заведения репозитория. 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>
41 lines
3.0 KiB
Python
41 lines
3.0 KiB
Python
# Бэкенд: ТОЛЬКО GPU/CuPy. CPU-фолбэка нет (по требованию). Если CuPy/GPU недоступны — жёсткая ошибка.
|
|
# Здесь же — общие device-константы (DTYPE, скаляры 0/1/2) и помощник переноса на host для вывода.
|
|
import os
|
|
import cupy as cp
|
|
|
|
try:
|
|
_probe = (cp.zeros(2) + 1).sum(); cp.cuda.Device().synchronize() # реальная операция на device
|
|
except Exception as e:
|
|
raise RuntimeError("solver_2x_sdf требует рабочую GPU + CuPy (CPU-фолбэк удалён намеренно).") from e
|
|
|
|
xp = cp # единый алиас вычислительного бэкенда
|
|
DTYPE = cp.float64 if os.environ.get("AMR_FP64") else cp.float32
|
|
# Порог вырожденности знаменателя γ. ОТНОСИТЕЛЬНЫЙ (доля от ‖Δ‖² = ⟨Δ|Δ⟩), а НЕ абсолютный.
|
|
# Почему: den = ⟨Δh|Δh⟩ — квадратичная по неравновесию величина, у развитого следа она
|
|
# ~1e-7…1e-9 при вполне разрешённой физике. Прежний абсолютный порог 1e-6 (fp32) срабатывал
|
|
# на 77–99% узлов и молча подменял γ на 2, т.е. гнал ЧИСТЫЙ LBGK вместо KBC (замерено:
|
|
# энстрофия сдвигового слоя Re=3e4 уходила на +9%). den — сумма НЕотрицательных слагаемых,
|
|
# её относительная точность ~eps типа, поэтому дробный порог одинаков для fp32/fp64.
|
|
# 1e-8 на шесть порядков ниже наблюдаемого минимума den/‖Δ‖² (~5e-4) — срабатывает только на
|
|
# истинном вырождении (Δh ≡ 0), где γ всё равно ни на что не влияет, т.к. умножается на Δh.
|
|
GREL = 1e-8
|
|
BACKEND = f"CuPy/GPU dtype={'float64' if DTYPE == cp.float64 else 'float32'}"
|
|
|
|
# device-скаляры — не создаём host→device на горячем пути (важно для CUDA-graph capture)
|
|
ZERO = cp.zeros((), DTYPE)
|
|
ONE = cp.asarray(1.0, DTYPE)
|
|
TWO = cp.asarray(2.0, DTYPE)
|
|
|
|
def to_cpu(a):
|
|
"""Перенос на host (numpy) — только для диагностики/визуализации, не в солвере."""
|
|
return cp.asnumpy(a)
|
|
|
|
def pack(comps):
|
|
"""Собрать кортеж одинаковых по форме массивов в один (len,*shape) БЕЗ cupy.stack.
|
|
cupy.stack/concatenate грузит на device host-массив указателей (H2D) → запрещено при CUDA-graph
|
|
capture. Здесь: преаллокация + присваивание срезов — только device→device."""
|
|
out = xp.empty((len(comps),) + comps[0].shape, dtype=comps[0].dtype)
|
|
for i, c in enumerate(comps):
|
|
out[i] = c
|
|
return out
|