Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
137876193d | ||
|
|
6902b887d7 | ||
|
|
83eb87d81d |
@@ -1,24 +0,0 @@
|
|||||||
# Детерминированные переводы строк. У пользователя core.autocrlf=true (дефолт Git for Windows),
|
|
||||||
# из-за чего git ругался на LF→CRLF почти на каждом файле. Проект собирается ещё под Linux и
|
|
||||||
# macOS (см. CMakePresets.json), поэтому в репозитории храним LF, а в рабочей копии Windows
|
|
||||||
# git сам развернёт CRLF там, где это нужно.
|
|
||||||
* text=auto eol=lf
|
|
||||||
|
|
||||||
# .bat обязан быть CRLF — иначе cmd.exe спотыкается на многострочных конструкциях
|
|
||||||
*.bat text eol=crlf
|
|
||||||
|
|
||||||
# Бинарные: не диффать, не трогать переводы строк
|
|
||||||
*.png binary
|
|
||||||
*.gif binary
|
|
||||||
*.pdf binary
|
|
||||||
*.npz binary
|
|
||||||
*.spv binary
|
|
||||||
*.bin binary
|
|
||||||
*.exe binary
|
|
||||||
|
|
||||||
# Wavefront OBJ — текст (ASCII), но диффы бессмысленны
|
|
||||||
*.obj text diff=none
|
|
||||||
|
|
||||||
# Ноутбуки: слияние по строкам всегда даёт мусор из-за outputs/метаданных —
|
|
||||||
# конфликт решается вручную, а не авто-мержем
|
|
||||||
*.ipynb -merge
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Сборка
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Каталог пресетов CMake целиком: объектники, SPIR-V, скопированные assets,
|
|
||||||
# и _deps — клоны зависимостей через FetchContent (только они ~775 МБ).
|
|
||||||
/build/
|
|
||||||
/out/
|
|
||||||
CMakeUserPresets.json # локальные пресеты разработчика, не общие
|
|
||||||
compile_commands.json # генерится (CMAKE_EXPORT_COMPILE_COMMANDS), часто симлинк в корень
|
|
||||||
|
|
||||||
# Зависимости, если их когда-нибудь начнут класть вручную (сейчас каталог пуст,
|
|
||||||
# всё тянет FetchContent). Убрать строку, если вендорить их в репозиторий.
|
|
||||||
/extern/*
|
|
||||||
!/extern/.gitkeep
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Артефакты выполнения приложения
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# SimVulcan пишет их в текущий каталог при запуске (обычно внутри build/,
|
|
||||||
# но при запуске из другого места окажутся здесь).
|
|
||||||
imgui.ini
|
|
||||||
pipeline_cache.bin
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Python (docs/theory — исследовательский код)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
.ipynb_checkpoints/
|
|
||||||
.venv/
|
|
||||||
venv/
|
|
||||||
.pytest_cache/
|
|
||||||
|
|
||||||
# Числовые сводки демо-прогонов: генерятся скриптами demos_gpu/*.py на GPU-сервере
|
|
||||||
# (ноутбук цитирует их значения в markdown). Сами прогоны воспроизводимы.
|
|
||||||
docs/theory/demos_gpu/out/
|
|
||||||
docs/theory/solver_2x_sdf/out/
|
|
||||||
|
|
||||||
# ВАЖНО: docs/theory/figures/ и docs/theory/anim/ НЕ игнорируются намеренно —
|
|
||||||
# ноутбук kbc_lbm.ipynb их встраивает, и без них он нечитаем на свежем клоне
|
|
||||||
# (перегенерация требует GPU-сервера). См. заметку о них в README ветки research.
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Редакторы и ОС
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
.vs/
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
*.user
|
|
||||||
*.suo
|
|
||||||
*.swp
|
|
||||||
*~
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
desktop.ini
|
|
||||||
|
|
||||||
# Общие заметки Claude Code: один файл на все ветки, вне версионирования
|
|
||||||
/CLAUDE.md
|
|
||||||
@@ -0,0 +1,729 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Read this first: the file is shared across branches
|
||||||
|
|
||||||
|
`CLAUDE.md` is **deliberately untracked on every working branch** — `/CLAUDE.md`
|
||||||
|
sits in `.gitignore` on `main`, `dev`, `research` and `rust`. One physical file
|
||||||
|
therefore survives every `git checkout`, so these notes stay put while the tree
|
||||||
|
around them changes, and editing them never shows up in `git status`.
|
||||||
|
|
||||||
|
The master copy lives on the **orphan branch `notes`**, which holds this one file
|
||||||
|
and nothing else, one commit per revision — that branch is both the backup and the
|
||||||
|
change history. It shares no ancestor with any code branch, so it never merges into
|
||||||
|
anything and never appears in a diff.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git show notes:CLAUDE.md > CLAUDE.md # restore it in a fresh clone
|
||||||
|
git log --oneline notes # history of these notes
|
||||||
|
git diff notes~1 notes # what changed in the last revision
|
||||||
|
```
|
||||||
|
|
||||||
|
After editing this file, publish the new revision — this touches neither the
|
||||||
|
working tree nor the checked-out branch:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
blob=$(git hash-object -w CLAUDE.md)
|
||||||
|
tree=$(printf '100644 blob %s\tCLAUDE.md\n' "$blob" | git mktree)
|
||||||
|
git branch -f notes "$(git commit-tree "$tree" -p notes -m 'CLAUDE.md: what changed')"
|
||||||
|
git push CFDManager notes
|
||||||
|
```
|
||||||
|
|
||||||
|
`git clean -xfd` deletes the working copy, since it is untracked — restore it with
|
||||||
|
the first command above.
|
||||||
|
|
||||||
|
Drift is guarded automatically: a `PostToolUse` hook
|
||||||
|
(`.claude/hooks/notes_guard.py`, wired in `.claude/settings.local.json`) compares
|
||||||
|
this file against `notes` after every edit and demands a described commit when the
|
||||||
|
two diverge — necessary because an untracked file never shows up in `git status`.
|
||||||
|
Both live outside git, excluded through `.git/info/exclude` rather than
|
||||||
|
`.gitignore`, so no branch has to carry them.
|
||||||
|
|
||||||
|
The consequence to keep in mind: **this file describes the whole project, but no
|
||||||
|
single branch contains all of it.** Before assuming a directory exists, check
|
||||||
|
`git branch --show-current` against the map below — `rust/` and
|
||||||
|
`docs/theory/2d_solver/` each live on one branch only.
|
||||||
|
|
||||||
|
## Branch map
|
||||||
|
|
||||||
|
| branch | what it adds | trees present |
|
||||||
|
|---|---|---|
|
||||||
|
| `main`, `dev` | baseline; both at the same commit, one past the initial import — and that commit only untracks `CLAUDE.md` | `src/`, `shaders/`, `tests/`, `docs/theory/` (Python) |
|
||||||
|
| `research` | the CFD work — **`kbc2d`**, a Rust LBM solver, its validation campaign and Docker images | `+ docs/theory/2d_solver/`, `+ docs/origins/Grad's_aproximation.pdf` |
|
||||||
|
| `rust` | a **Rust port of the C++ editor** plus a measured C++/Rust comparison | `+ rust/`, `docs/rust_vs_cpp.md` |
|
||||||
|
|
||||||
|
All five branches (including `notes`) are fully pushed to the `CFDManager` remote;
|
||||||
|
verify with `git ls-remote CFDManager` rather than trusting a stale tracking ref.
|
||||||
|
|
||||||
|
Nothing is shared between the two Rust efforts: `docs/theory/2d_solver/` (a CFD
|
||||||
|
solver, branch `research`) and `rust/` (a port of the 3D editor, branch `rust`) are
|
||||||
|
unrelated projects that merely both happen to be Rust. Neither is part of the CMake
|
||||||
|
build — `CMakeLists.txt` never mentions either tree.
|
||||||
|
|
||||||
|
**A checkout away from `research` does not remove `docs/theory/2d_solver/`** — the
|
||||||
|
tracked sources vanish but the untracked artefacts stay, so on `rust` that directory
|
||||||
|
contains only `out/`, `target/`, `wake.gif` and `demo_wake.gif` and looks like a
|
||||||
|
mutilated copy of the solver. To read the solver from another branch, go through
|
||||||
|
git rather than the filesystem:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git show research:docs/theory/2d_solver/src/math.rs
|
||||||
|
git archive research docs/theory/2d_solver | tar -x -C /tmp/kbc2d # to grep it
|
||||||
|
```
|
||||||
|
|
||||||
|
Untracked leftovers you will see regardless of branch: `build/`, `rust/target/`,
|
||||||
|
`docs/theory/2d_solver/target/`, the `__pycache__` dirs under `docs/theory/`, and
|
||||||
|
`docs/theory/2d_solver/out/` plus the two ~9 MB GIFs beside it (campaign results —
|
||||||
|
**the user's data, do not clean**). `pipeline_cache.bin` and `imgui.ini` are written
|
||||||
|
next to whichever executable ran, i.e. in `build/<preset>/src/app/` and
|
||||||
|
`rust/pipeline_cache.bin` — not at the repo root.
|
||||||
|
|
||||||
|
**`build/` in this working tree is foreign and stale.** Its
|
||||||
|
`CMakeCache.txt` records `CMAKE_HOME_DIRECTORY=C:/Users/ivan/UAS/SimV4(Not_KBC)/SimVulcan`,
|
||||||
|
and `_deps/` carries a 262 MB `nlohmann_json` clone that this project never declares.
|
||||||
|
Do not measure anything from it without re-configuring first.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# SimVulcan — C++/Vulkan editor (all branches)
|
||||||
|
|
||||||
|
A minimal **Vulkan 1.3 / C++20** 3D model editor: three reference grid planes (XY,
|
||||||
|
XZ, YZ) through the origin plus coloured X/Y/Z axes, an orbit camera, and `.obj`
|
||||||
|
loading with selectable display modes (solid, wireframe, solid + wireframe). It
|
||||||
|
runs **no simulation** — no compute pipeline or `.comp` shader exists anywhere, and
|
||||||
|
`Renderer.cpp` disables async compute explicitly. It is a rendering skeleton with
|
||||||
|
two ImGui windows, titled **`Viewport`** and **`Mesh`** (the class behind the second
|
||||||
|
is `MeshLoadPanel`, which is why the docs keep calling it the "mesh load panel").
|
||||||
|
Unchanged since the initial commit on every branch — `git log --all -- src` returns
|
||||||
|
exactly one commit.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Prerequisites: **Vulkan SDK** (for `glslangValidator`), **CMake 3.26+**, **Ninja**,
|
||||||
|
a C++20 compiler. Only CMake 3.26 and C++20 are actually enforced
|
||||||
|
(`CMakeLists.txt:1`, `:9-11`). `cmake/VulkanSetup.cmake:3` is
|
||||||
|
`find_package(Vulkan REQUIRED COMPONENTS glslangValidator)` with **no version
|
||||||
|
argument**, so the "SDK 1.3.290+" in `README.md` is a comment, not a check; the
|
||||||
|
de-facto floor comes from the pinned volk `1.3.295` and vk-bootstrap `v1.3.295`.
|
||||||
|
The compiler minimums (MSVC 19.36+, gcc 11+, clang 14+) are likewise README prose,
|
||||||
|
enforced nowhere. On macOS the only hard constraint is
|
||||||
|
`CMAKE_OSX_ARCHITECTURES: arm64` in the preset; MoltenVK is mentioned in a
|
||||||
|
`message(STATUS)`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake --preset windows-msvc-release
|
||||||
|
cmake --build --preset windows-msvc-release
|
||||||
|
```
|
||||||
|
|
||||||
|
Presets: `windows-msvc-debug`, `windows-msvc-release`, `linux-gcc-release`,
|
||||||
|
`linux-clang-release`, `macos-arm64-release` (all Ninja, one dir per preset under
|
||||||
|
`build/<presetName>/`). None of them pins a compiler — they inherit whatever the
|
||||||
|
environment provides, so `README.md`'s "MSVC / clang-cl" is aspirational.
|
||||||
|
|
||||||
|
The first configure fetches nine repositories via FetchContent (GLFW, GLM, volk,
|
||||||
|
vk-bootstrap, VulkanMemoryAllocator, Dear ImGui, spdlog, tinyobjloader, Catch2) and
|
||||||
|
needs network access. There is no `GIT_SHALLOW`, so each is a clone with full
|
||||||
|
history, and **no shared cache** is configured (the only `FETCHCONTENT_*` variable
|
||||||
|
set is `FETCHCONTENT_QUIET OFF`) — every new build directory downloads all of it
|
||||||
|
again. Budget **~400 MB of sources / ~510 MB of `_deps` once built**, not the
|
||||||
|
775 MB quoted in `docs/rust_vs_cpp.md`: that figure was measured on the foreign
|
||||||
|
`build/` tree described above and includes 262 MB of `nlohmann_json` that nothing
|
||||||
|
here declares.
|
||||||
|
|
||||||
|
`VK_NO_PROTOTYPES` is **not** project-wide — there is no `add_compile_definitions`
|
||||||
|
at project scope. It is set `PUBLIC` on exactly two targets, `imgui`
|
||||||
|
(`CMakeLists.txt:114`) and `simv_vk` (`src/vk/CMakeLists.txt:29`). Because
|
||||||
|
`simv_core` links `simv_vk` **PRIVATE**, the definition never reaches `simv_mesh`,
|
||||||
|
`simv_editor` or `simv_tests`. Vulkan entry points load through **volk**.
|
||||||
|
|
||||||
|
Warnings come from `simv_set_warnings` (`cmake/CompilerWarnings.cmake`), called by
|
||||||
|
all six targets: `/W4 /permissive- /Zc:preprocessor /Zc:__cplusplus /wd4100` plus
|
||||||
|
the defines `_CRT_SECURE_NO_WARNINGS`, `NOMINMAX`, `WIN32_LEAN_AND_MEAN` on MSVC;
|
||||||
|
`-Wall -Wextra -Wpedantic -Wno-unused-parameter -Wshadow -Wnon-virtual-dtor
|
||||||
|
-Wold-style-cast -Wcast-align -Wunused -Woverloaded-virtual` elsewhere. They are
|
||||||
|
**not** errors — no `/WX` or `-Werror` anywhere. `cmake/Sanitizers.cmake` defines
|
||||||
|
`simv_enable_sanitizers` (Debug-only; ASan **only** on MSVC, ASan+UBSan elsewhere)
|
||||||
|
but no target calls it — wire it in manually when chasing memory bugs.
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
The executable resolves SPIR-V relative to the working directory (`FindSpvPath`,
|
||||||
|
`src/vk/Shader.cpp:24-33`, probes `spirv/<rel>` then `current_path()/spirv/<rel>` —
|
||||||
|
the second probe is a no-op, since the first is already CWD-relative). Two
|
||||||
|
`SimVulcan` POST_BUILD steps copy `assets/` and the compiled `spirv/` tree next to
|
||||||
|
the executable, so **run from the executable's own directory**
|
||||||
|
(`build/<preset>/src/app/`). `main.cpp:40-50` additionally probes four `../`
|
||||||
|
ancestors for `assets/meshes`. Meshes load at runtime through the `Mesh` panel.
|
||||||
|
|
||||||
|
Running writes two files into the CWD: `pipeline_cache.bin` (serialised
|
||||||
|
`VkPipelineCache`, reloaded on the next start) and ImGui's `imgui.ini` (no
|
||||||
|
`IniFilename` is ever set, so ImGui's default applies). Both are disposable —
|
||||||
|
delete them if pipeline creation or the panel layout misbehaves.
|
||||||
|
|
||||||
|
`ContextOptions::enableValidation` / `enableDebugUtils` default to **true**
|
||||||
|
(`src/vk/Context.h:15-16`) and nothing overrides them, so the Khronos validation
|
||||||
|
layer is requested even in Release. The severity mask is error + warning only
|
||||||
|
(`Context.cpp:56-58`), which makes the `INFO` branch of the callback dead code.
|
||||||
|
Those messages go to **raw `spdlog::`**, i.e. spdlog's default logger — not the
|
||||||
|
`"simv"` logger that `core::Logger` creates.
|
||||||
|
|
||||||
|
`run.bat` at the repo root is **stale**: it launches
|
||||||
|
`build/vs2022/src/app/Release/SimVulcan.exe`, a path the Ninja presets never
|
||||||
|
produce — while its own error message tells the user to run those presets. Do not
|
||||||
|
point users at it without fixing the path first.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
Catch2 unit tests, mesh bounds + welding. No GPU is touched at runtime, but the
|
||||||
|
binary is **not** free of Vulkan: static-lib propagation through
|
||||||
|
`simv_mesh → simv_core → (PRIVATE) simv_vk` makes `simv_tests.exe` link
|
||||||
|
`simv_vk`, `vk-bootstrap`, `imgui`, `volk` and `glfw3`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake --build --preset windows-msvc-debug --target simv_tests
|
||||||
|
ctest --preset windows-msvc-debug
|
||||||
|
```
|
||||||
|
|
||||||
|
`windows-msvc-debug` is the only preset with a `testPreset` — for the other
|
||||||
|
configs, invoke `ctest` in `build/<preset>/` directly.
|
||||||
|
|
||||||
|
There are exactly three `TEST_CASE`s, all in `tests/unit/test_mesh.cpp`:
|
||||||
|
`Mesh::RecalculateBounds finds AABB` `[mesh]`, `WeldVertices collapses
|
||||||
|
near-duplicate vertices` and `WeldVertices drops degenerate triangles`
|
||||||
|
(both `[mesh][decimator]`). Each is registered individually by
|
||||||
|
`catch_discover_tests`, so either filter works:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ctest --preset windows-msvc-debug -R "WeldVertices" -V # matches the last two
|
||||||
|
./build/windows-msvc-debug/tests/simv_tests.exe "[decimator]"
|
||||||
|
./build/windows-msvc-debug/tests/simv_tests.exe --list-tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Library / target map
|
||||||
|
|
||||||
|
Arrows below show the **project** edges; each target also links third-party
|
||||||
|
libraries, and the visibility keywords matter more than they look.
|
||||||
|
|
||||||
|
```
|
||||||
|
SimVulcan (exe) → simv_core, simv_vk, simv_mesh, simv_editor (all PRIVATE)
|
||||||
|
+ glm, spdlog; add_dependencies(… simv_shaders)
|
||||||
|
simv_core → PUBLIC spdlog · PRIVATE simv_vk, glfw (App owns Window + Renderer)
|
||||||
|
simv_editor → PUBLIC simv_core, simv_mesh, imgui, glm (no Vulkan)
|
||||||
|
simv_mesh → PUBLIC simv_core, glm, tinyobjloader (no Vulkan)
|
||||||
|
simv_vk → PUBLIC Vulkan::Headers, volk, vk-bootstrap, VMA, glfw, glm, spdlog
|
||||||
|
PRIVATE imgui
|
||||||
|
simv_shaders → glslangValidator (GLSL → SPIR-V), an ALL custom target
|
||||||
|
```
|
||||||
|
|
||||||
|
The `simv_core → simv_vk` edge being **PRIVATE** is what keeps `VK_NO_PROTOTYPES`
|
||||||
|
(and volk) from reaching the rest of the tree — see the Build section.
|
||||||
|
|
||||||
|
Namespaces follow directories: `simv::core`, `simv::vk`, `simv::mesh`,
|
||||||
|
`simv::editor`. Each library exports `src/` as its include root, so includes are
|
||||||
|
written module-qualified (`#include "mesh/Mesh.h"`, `#include "vk/Renderer.h"`).
|
||||||
|
|
||||||
|
### Frame loop
|
||||||
|
|
||||||
|
`main.cpp` creates `core::App`, which owns the `core::Window` and a
|
||||||
|
`vk::Renderer`, then runs the loop. Each frame `Renderer::DrawFrame` acquires a
|
||||||
|
swapchain image, calls the UI callback (between ImGui NewFrame/Render), *then*
|
||||||
|
begins the command buffer and records the scene: grid + mesh into one
|
||||||
|
dynamic-rendering pass with a depth attachment, followed by ImGui, then presents
|
||||||
|
(2 frames in flight, `vkQueueSubmit2` / `VkImageMemoryBarrier2`).
|
||||||
|
|
||||||
|
The acquire-before-callback ordering is the root of defect 2 below — keep it in
|
||||||
|
mind before moving work into the callback.
|
||||||
|
|
||||||
|
`main.cpp` wires the editor via the UI callback: it draws the panels, applies
|
||||||
|
mouse input to the `editor::Camera`, and pushes the resulting state into the
|
||||||
|
renderer (`SetRenderMode`, `SetGridVisible`, `SetViewProj`). Mesh loads go through
|
||||||
|
`MeshLoadPanel`'s callback → `Renderer::SetMeshCpu`.
|
||||||
|
|
||||||
|
### Mesh load path
|
||||||
|
|
||||||
|
`MeshLoadPanel` lists `*.obj` in the mesh directory (extension match is
|
||||||
|
**case-insensitive**, so `.OBJ` shows up too) and kicks off `mesh::LoadObjAsync`
|
||||||
|
(`std::async(std::launch::async, …)`, `std::future`). The future is **drained on
|
||||||
|
the main thread** at the top of `MeshLoadPanel::Draw`, before the first
|
||||||
|
`ImGui::Begin`, so the `OnLoaded` callback — and therefore the GPU upload — always
|
||||||
|
runs on the render thread. Loader exceptions surface as the panel's status string.
|
||||||
|
|
||||||
|
`main.cpp`'s `OnLoaded` welds the mesh (`WeldVertices`, tolerance `1e-4`), logs
|
||||||
|
the counts, uploads via `SetMeshCpu`, and reframes the camera to the bbox radius.
|
||||||
|
Despite the file name, `mesh/MeshDecimator.h` declares **one** function and
|
||||||
|
implements **only** spatial-hash vertex welding (round-to-nearest cell hash,
|
||||||
|
collapse near-duplicates, drop degenerate triangles, recompute bounds) — there is
|
||||||
|
no LOD/decimation.
|
||||||
|
|
||||||
|
### Non-obvious invariants (read before editing)
|
||||||
|
|
||||||
|
- **All Vulkan lives in `src/vk/`.** `core/`, `mesh/`, `editor/` and `app/` make no
|
||||||
|
Vulkan API calls — a grep for `volk|vulkan|Vk[A-Z]` across them returns zero hits.
|
||||||
|
`vk::Renderer`'s public header is deliberately Vulkan-free (pImpl + glm/`RenderMode`
|
||||||
|
only) so `App` can own it without pulling in volk. Keep it that way — do not leak
|
||||||
|
`Vk*` types into the public interfaces of those modules. Note this is **convention
|
||||||
|
only**: every library exports `src/` publicly, so nothing stops a
|
||||||
|
`#include <volk.h>` in `mesh/`; the compiler will not catch it.
|
||||||
|
- **Single render pass with depth.** Scene (grid + mesh) and ImGui draw into one
|
||||||
|
`vkCmdBeginRendering` pass that has both a colour and a `D32_SFLOAT` depth
|
||||||
|
attachment (depth `storeOp` is `DONT_CARE`). The ImGui backend is initialised with
|
||||||
|
`depthAttachmentFormat` set so its pipeline matches the pass.
|
||||||
|
- **Wireframe needs `fillModeNonSolid`.** `MeshRenderer` builds a fill pipeline and
|
||||||
|
a `VK_POLYGON_MODE_LINE` pipeline; the line pipeline uses a small *static* depth
|
||||||
|
bias (`constantFactor` and `slopeFactor` both `-1.0`) so the overlay sits on top
|
||||||
|
of the fill. The device feature is requested in `Context`. Culling is off
|
||||||
|
(`VK_CULL_MODE_NONE`) — loaded models may have mixed winding.
|
||||||
|
- **Camera is fixed on the origin.** `editor::Camera` orbits (yaw/pitch/distance)
|
||||||
|
the world origin; loaded models are recentred there via a translate-only model
|
||||||
|
matrix. Projection uses Vulkan clip space (`GLM_FORCE_DEPTH_ZERO_TO_ONE` + Y flip,
|
||||||
|
isolated to `Camera.cpp` — the macro appears nowhere else in the tree).
|
||||||
|
- **Buffers.** `vk::GpuMesh` owns the model's vertex/index buffers (staged upload on
|
||||||
|
the dedicated transfer queue). `GridRenderer` builds a static host-visible mapped
|
||||||
|
line buffer once in its constructor. Both scene renderers create pipeline layouts
|
||||||
|
with one push-constant range and **no** `pSetLayouts` — no descriptor sets at all.
|
||||||
|
- **Push-constant layout is a cross-file contract.** `MeshRenderer.cpp`'s anonymous
|
||||||
|
`MeshPC { mat4 mvp; vec4 color; }` (range `VERTEX|FRAGMENT`) must stay
|
||||||
|
byte-identical to the `push_constant` block in `mesh.vert`/`mesh.frag`; `color.a`
|
||||||
|
is a *flag*, not alpha — `mesh.frag` does `mix(base, base*shade, pc.color.a)`, so
|
||||||
|
1 = flat-shaded and 0 = constant colour for wireframe. `GridRenderer` pushes a
|
||||||
|
bare `mat4` (vertex stage only). Change either side and you must change both.
|
||||||
|
- **Swapchain recreation rebuilds depth *and* sync objects.** `RecreateSwapDependent`
|
||||||
|
runs `WaitIdle` → `swap->Recreate()` → **`CreateDepth`** → per-frame
|
||||||
|
`imageAvailable` semaphores (a failed acquire can leave one signalled) → per-
|
||||||
|
swapchain-image `renderFinished` semaphores (the image count may change) → re-ensure
|
||||||
|
both pipelines. Keep that ordering if you touch resize handling.
|
||||||
|
- **Mesh upload stalls the device.** `SetMeshCpu`/`ClearMesh` call `WaitIdle`
|
||||||
|
before touching `GpuMesh` — acceptable because loads are rare; do not copy that
|
||||||
|
pattern into per-frame paths. `SetMeshCpu` early-returns on an empty mesh *before*
|
||||||
|
the wait.
|
||||||
|
|
||||||
|
### Known defects (found by the Rust port, all still present here)
|
||||||
|
|
||||||
|
Porting the editor surfaced four real bugs that nobody was looking for. They are
|
||||||
|
unfixed in the C++ tree on every branch; full write-up in `docs/rust_vs_cpp.md`
|
||||||
|
(branch `rust`).
|
||||||
|
|
||||||
|
1. **A source-level dependency cycle that the CMake graph hides.** `core::App` owns
|
||||||
|
`vk::Renderer`, and `vk::Renderer` takes a `core::Window&` — so the *sources*
|
||||||
|
are mutually dependent. The **CMake graph is acyclic**, which is exactly why
|
||||||
|
nothing complains: `simv_vk` links no project target at all, seeing
|
||||||
|
`core/Window.h` through `target_include_directories(simv_vk PUBLIC ..)` and
|
||||||
|
getting `Window`'s symbols at executable link time. Cargo rejects the same shape
|
||||||
|
outright, which is what surfaced it.
|
||||||
|
2. **Mesh upload runs inside the frame.** `MeshLoadPanel::Draw` calls `OnLoaded`
|
||||||
|
from the UI callback, which `Renderer::DrawFrame` invokes *after*
|
||||||
|
`vkAcquireNextImageKHR`; the handler calls `SetMeshCpu` → `vkDeviceWaitIdle`.
|
||||||
|
Legal, and command recording has not started yet (`vkBeginCommandBuffer` comes
|
||||||
|
later) — but it waits for device idle while holding an acquired swapchain image.
|
||||||
|
3. **Two sources of truth for "is there a mesh".** `GpuMesh::IsValid()` and
|
||||||
|
`Renderer`'s separate `hasMesh` flag must be kept consistent by hand;
|
||||||
|
`MeshRenderer::Draw` then re-checks `IsValid()` a third time.
|
||||||
|
4. **`Mesh ready` is never printed.** `ObjLoader::LoadObj` logs to category
|
||||||
|
`MeshIO`, and `main.cpp`'s handler logs `Mesh ready: …` to the same category
|
||||||
|
immediately after; the category throttles at 0.5 s and both are `Info`, so the
|
||||||
|
second message is always dropped (only Warn/Error/Critical bypass the throttle).
|
||||||
|
Post-weld counts have therefore never appeared in any log.
|
||||||
|
|
||||||
|
Also dead weight: **`vk::Buffer` and `vk::Image` are used by nobody** — 362 lines
|
||||||
|
across four files, still compiled into `simv_vk`. `GridRenderer`, `GpuMesh` and the
|
||||||
|
depth attachment all call VMA directly. `Image::Desc` even defaults to
|
||||||
|
`VK_IMAGE_TYPE_3D` and an `R32G32B32A32_SFLOAT` storage image, a leftover from some
|
||||||
|
compute/CFD design. Note `README.md:53-54` still lists both as part of the working
|
||||||
|
Vulkan stack — that line is wrong.
|
||||||
|
|
||||||
|
### Shaders
|
||||||
|
|
||||||
|
GLSL under `shaders/editor/` (`mesh.{vert,frag}`, `grid.{vert,frag}`). `simv_shaders`
|
||||||
|
compiles each to `build/<preset>/spirv/editor/<name>.<stage>.spv` — the stage suffix
|
||||||
|
is **kept**, so the real artefacts are `mesh.vert.spv`, `mesh.frag.spv` and so on —
|
||||||
|
targeting `vulkan1.3`, with `shaders/` as the `-I` root (so `#include
|
||||||
|
"common/foo.glsl"` would resolve). The glob is `CONFIGURE_DEPENDS`, so a new shader
|
||||||
|
is picked up automatically, but it matches **only `editor/*.vert` and
|
||||||
|
`editor/*.frag`**; a `.comp` or `.geom` dropped there is silently ignored.
|
||||||
|
|
||||||
|
`mesh.frag` reconstructs a flat normal from screen-space derivatives, so the mesh
|
||||||
|
vertex stream carries only positions (tight `float3`, one binding, one attribute).
|
||||||
|
The *grid* pipeline is different — two attributes, position + colour.
|
||||||
|
**The Rust port on branch `rust` compiles this same directory** (`build.rs` reaches
|
||||||
|
`../../../shaders`) with the same `glslangValidator` flags — a shader change affects
|
||||||
|
both versions.
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
`simv::core::Logger` (spdlog-backed, singleton) with category-based throttling.
|
||||||
|
Log via `LogFmt(LogCategory, LogLevel, fmt, args...)`. Categories: `Core`,
|
||||||
|
`Vulkan`, `MeshIO`, `UI`, `Test` (printed as `core`, `vk`, `mesh`, `ui`, `test`).
|
||||||
|
|
||||||
|
Two things the API surface does not tell you:
|
||||||
|
|
||||||
|
- **Only `MeshIO` is ever used.** The entire tree contains three `LogFmt` call
|
||||||
|
sites — two in `ObjLoader.cpp`, one in `main.cpp`. `Core`, `Vulkan`, `UI` and
|
||||||
|
`Test` have zero.
|
||||||
|
- **The runtime knobs are never turned.** `SetMinLevel` / `SetThrottle` /
|
||||||
|
`SetEnabled` exist but have no callers, and no flag, env var or UI control is
|
||||||
|
wired to them. They are settable programmatically, not configurable.
|
||||||
|
|
||||||
|
And the whole of `vk/` logs through **raw `spdlog::`**, never through `LogFmt` —
|
||||||
|
as do `core/App.cpp` and `main.cpp`'s fatal handler. That bypasses not just the
|
||||||
|
category throttle but the `"simv"` logger entirely (different pattern, unaffected
|
||||||
|
by `Logger::Shutdown()`), which is the only reason the GPU name survives startup;
|
||||||
|
see defect 4 above.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# `rust/` — Rust port of the editor (branch `rust` only)
|
||||||
|
|
||||||
|
A port of SimVulcan from C++20 to Rust, existing **for comparison**: both versions
|
||||||
|
sit side by side and build independently. Same Vulkan 1.3, dynamic rendering,
|
||||||
|
synchronization2, and the same `shaders/` directory.
|
||||||
|
|
||||||
|
**The declared MSRV is wrong.** `rust/Cargo.toml:13` says `rust-version = "1.82"`
|
||||||
|
(and `rust/README.md` repeats it), but the locked `egui`/`egui-winit`/`epaint`
|
||||||
|
0.36.1 each declare `rust-version = "1.95"`, so Cargo refuses to resolve on 1.82.
|
||||||
|
**The real minimum is 1.95**; the tree is tested on 1.97.1. The Vulkan SDK is needed
|
||||||
|
for `glslangValidator` only.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd rust
|
||||||
|
cargo build --release
|
||||||
|
./target/release/SimVulcan # SimVulcan.exe on Windows; package is simv-app
|
||||||
|
cargo test # 13 tests + 1 #[ignore]d diagnostic
|
||||||
|
```
|
||||||
|
|
||||||
|
**Work on the code with plain `cargo build`, not `--release`.** The release profile
|
||||||
|
sets `lto = "thin"` with `codegen-units = 1`, which makes a one-line edit
|
||||||
|
re-optimise and relink the whole binary: 52 s versus ~5 s in debug.
|
||||||
|
|
||||||
|
The tests cover mesh bounds, welding (three cases), `Cube.obj` loading and its error
|
||||||
|
path, camera clip space and zoom/pitch clamping, and logger category mapping. They
|
||||||
|
need no GPU but are **not** pure math: the loader tests read real files from
|
||||||
|
`assets/meshes` via `$CARGO_MANIFEST_DIR`, so they require the repo checkout. The
|
||||||
|
ignored `weld_counts_for_bundled_assets` is the diagnostic that produced the loader
|
||||||
|
parity table below.
|
||||||
|
|
||||||
|
Five crates mirror the CMake target map, and the "all Vulkan in one place"
|
||||||
|
invariant is **compiler-enforced** here — `simv-mesh` and `simv-editor` have
|
||||||
|
neither `ash` nor `simv-vk` among their dependencies. Third-party deps in full
|
||||||
|
(each crate also depends on the workspace crates shown):
|
||||||
|
|
||||||
|
```
|
||||||
|
crates/simv-core/ logger, window → log, chrono, winit
|
||||||
|
crates/simv-mesh/ .obj, welding, bounds → glam, tobj, log, thiserror
|
||||||
|
(+ simv-core)
|
||||||
|
crates/simv-vk/ all Vulkan + build.rs → ash, ash-window, raw-window-handle,
|
||||||
|
(GLSL→SPIR-V) gpu-allocator, egui, egui-winit,
|
||||||
|
egui-ash-renderer, winit, glam,
|
||||||
|
bytemuck, log, thiserror
|
||||||
|
(+ simv-core, simv-mesh)
|
||||||
|
crates/simv-editor/ camera, input, panels → egui, glam, log
|
||||||
|
(+ simv-core, simv-mesh)
|
||||||
|
crates/simv-app/ App and entry point → the four crates above
|
||||||
|
+ egui, winit, glam, log, anyhow
|
||||||
|
```
|
||||||
|
|
||||||
|
Structural differences from C++, each with a reason (full list in
|
||||||
|
`docs/rust_vs_cpp.md`): `App` lives in the executable crate (Cargo rejects the
|
||||||
|
target-graph cycle); the UI closure **returns** a `FrameState` instead of calling
|
||||||
|
setters (it is invoked from a renderer method, so it cannot borrow the renderer) —
|
||||||
|
`Renderer`'s public API has no `Set*` methods at all; the loaded mesh is uploaded
|
||||||
|
*after* `draw_frame` returns, not from inside it; `Buffer` and `Image` are actually
|
||||||
|
used; no pImpl (private module fields give the same isolation); winit owns the
|
||||||
|
event loop, so `ShouldClose`/`PollEvents` are gone.
|
||||||
|
|
||||||
|
SPIR-V is embedded via `include_bytes!` from `build.rs`, so there is no runtime
|
||||||
|
shader lookup. Two runtime path dependencies remain, though, so "runs from any
|
||||||
|
directory" is not quite true: `assets/meshes` is searched from the executable's
|
||||||
|
ancestors **and then the CWD's**, and `pipeline_cache.bin` is written next to the
|
||||||
|
exe. Unlike the C++ build, nothing copies `assets/` — the port finds the repo-root
|
||||||
|
copy by walking up from `rust/target/<profile>/`.
|
||||||
|
|
||||||
|
The UI toolkit differs **by design**: egui instead of Dear ImGui, because the
|
||||||
|
`imgui` crate is bindings and would compile ~40k lines of C++, defeating the point
|
||||||
|
of comparing ecosystems.
|
||||||
|
|
||||||
|
## Comparison result (`docs/rust_vs_cpp.md`)
|
||||||
|
|
||||||
|
The document deliberately picks no winner. Machine: i5-1135G7 / Iris Xe /
|
||||||
|
Windows 11, both builds release.
|
||||||
|
|
||||||
|
| | C++ | Rust |
|
||||||
|
|---|---|---|
|
||||||
|
| lines of code | 2434 | 2976 (+22 %) |
|
||||||
|
| release from scratch | **114 s** | 194 s (thin LTO) · 182 s (no LTO) |
|
||||||
|
| release, one-file edit | **4.4 s** | 52.3 s (LTO) · **5.5 s** (no LTO) |
|
||||||
|
| debug from scratch / one-file edit | 112 s / 8.0 s | **83 s** / **5.2 s** |
|
||||||
|
| release exe | **1.18 MB** | 5.80 MB |
|
||||||
|
| dependency sources | ~400 MB **per build dir** | **80 MB** shared registry |
|
||||||
|
| startup to renderer ready (best of 3) | 1068 ms | **754 ms** |
|
||||||
|
| CPU at 60 Hz | **11.2 %** | 12.6 % |
|
||||||
|
|
||||||
|
Frame rate measures nothing — both are FIFO on a 60 Hz screen. The 1.4 pp CPU gap
|
||||||
|
is egui rebuilding its layout every frame, not the language. The one sharp build
|
||||||
|
cell (52 s) is the price of thin LTO, not of Rust — with LTO off it is 5.5 s
|
||||||
|
against 4.4 s. Note the document's summary claim that *no* measurement differs by
|
||||||
|
an order of magnitude is false on its own numbers: 52.3 / 4.4 = **11.9×**. Every
|
||||||
|
other cell stays under 10×.
|
||||||
|
|
||||||
|
The +22 % line count is **mostly, not almost entirely**, the missing vk-bootstrap
|
||||||
|
replacement: `Context` + `Swapchain` go from 301 to 606 lines, which is 305 of the
|
||||||
|
542-line overhang (56 %); the whole Vulkan layer accounts for 346 (64 %). The rest
|
||||||
|
is real — mesh +117, editor +104, app +90, core −64, and 176 in-module test lines
|
||||||
|
against C++'s 51.
|
||||||
|
|
||||||
|
Loader parity: `cow.obj` matches exactly (2451 verts / 4898 tris); `plane.obj`
|
||||||
|
differs by 0.13 %, entirely due to the reading libraries (tobj drops the 35
|
||||||
|
vertices no face references; fan triangulation of n-gons versus tinyobjloader's
|
||||||
|
earcut).
|
||||||
|
|
||||||
|
**Known errors in `docs/rust_vs_cpp.md`** (numbers below are the measured truth):
|
||||||
|
line-count total is 2976 / 494 comments, not 2962 / 492 — the `mesh` row is stale by
|
||||||
|
the 17 lines of the ignored diagnostic test; "775 MB per build dir" is the foreign
|
||||||
|
`build/` tree (see the Build section) and so is the matching "10 packages in the
|
||||||
|
C++ graph" — there are nine; the Rust package graph is 82, not 76; `plane.obj` has
|
||||||
|
119 faces with five or more vertices, not 121; "three Catch2 tests ported verbatim"
|
||||||
|
should be *all three ported, two more added*; and `Renderer::Impl`'s destructor is
|
||||||
|
22 lines, not thirty.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# `docs/theory/2d_solver/` — kbc2d, Rust LBM solver (branch `research` only)
|
||||||
|
|
||||||
|
The active research work. A **D2Q9 Lattice Boltzmann solver with the entropic KBC
|
||||||
|
collision operator**, written from scratch in Rust (not a port): flow past bodies in
|
||||||
|
a channel, two interchangeable backends, a ×2 nested AMR patch, sub-grid wall
|
||||||
|
models, and GIF output locked to *physical* flow time. `Cargo.toml` declares
|
||||||
|
**Rust 1.75+**; the Dockerfile pins `rust:1.97-bookworm`. Physics is anchored to the
|
||||||
|
method authors' papers in `docs/origins/`; formula references in the code follow the
|
||||||
|
2D paper (arXiv:1507.02509), the only arXiv id in the tree, with the wall models
|
||||||
|
citing Dorschner et al. *JFM* 801 (2016) for `grad` and Malaspinas 2015 /
|
||||||
|
Coreixas et al. *PRE* 96 for `hrr`.
|
||||||
|
|
||||||
|
`README.md` there is the authoritative status/validation log (609 lines: what is
|
||||||
|
verified, against which equation, with what measured numbers) and `bench/README.md`
|
||||||
|
(248 lines) covers the validation campaign. **Read them before changing physics or
|
||||||
|
defaults** — most defaults are the outcome of a documented measurement, not a guess.
|
||||||
|
|
||||||
|
## Build / test / run
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd docs/theory/2d_solver
|
||||||
|
cargo build --release # with the GPU backend (default feature `gpu`)
|
||||||
|
cargo build --release --no-default-features # CPU only, no wgpu
|
||||||
|
cargo test --release # 29 fast tests
|
||||||
|
cargo test --release -- --include-ignored # + 4 long CPU runs (~18 s)
|
||||||
|
cargo test --release -- --ignored taylor_green_kbc_vs_bgk --nocapture # one diagnostic
|
||||||
|
```
|
||||||
|
|
||||||
|
33 `#[test]` functions exist (18 in `math.rs`, 9 in `cpu.rs`, 6 in `gif.rs`); the
|
||||||
|
4 `#[ignore]`d ones are all in `cpu.rs` and are two reference benchmarks plus two
|
||||||
|
diagnostics, not four benchmarks.
|
||||||
|
|
||||||
|
**Never build `--no-default-features` last.** Both builds write the same
|
||||||
|
`target/release/kbc2d`, so a CPU-only build silently overwrites the wgpu one and
|
||||||
|
`--backend gpu` then refuses to run. Order: no-default-features first, normal
|
||||||
|
build second.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./target/release/kbc2d --shape cylinder --size 24 --re 150 \
|
||||||
|
--nx 480 --ny 240 --steps 40000 --sponge-len 32 \
|
||||||
|
--gif wake.gif --gif-field vorticity --verbose full
|
||||||
|
```
|
||||||
|
|
||||||
|
`--help` groups every key by role, under **Russian** headings —
|
||||||
|
`Физика, Сетка, Тело, Время, Схема, Анимация, Вывод`. Note `--time <seconds>` as an
|
||||||
|
alternative to `--steps` (the two conflict): a step is not a fixed slice of time
|
||||||
|
(`dt = u_lat * dx / u_phys`, literally `math.rs`'s `Units::new`), so refining the
|
||||||
|
cell silently shortens a fixed step count.
|
||||||
|
|
||||||
|
Defaults worth knowing beyond the three discussed below: `--backend` is **`cpu`**,
|
||||||
|
`--collision kbc`, `--outlet extrapolate`, `--case channel`, `--refine 2`.
|
||||||
|
|
||||||
|
## File roles — one concern each
|
||||||
|
|
||||||
|
`src/` holds exactly these five files — no `lib.rs`, no `benches/`, no integration
|
||||||
|
tests.
|
||||||
|
|
||||||
|
| file | owns |
|
||||||
|
|---|---|
|
||||||
|
| `src/math.rs` | **all solver mathematics**, nodewise and pure: D2Q9 lattice, product-form entropic equilibrium, shear projector, γ stabiliser, collision, Zou–He, body SDFs, wall-model closures, unit conversion, spectral diagnostics (`strouhal`). Tests against the papers' formulas live here. `pub type R = f64`. |
|
||||||
|
| `src/cpu.rs` | CPU backend: AoS layout, rayon, **and the shared topology** — `Geom::build` (masks), Bouzidi link assembly, `Patch` (AMR level coupling), `initial_field`. Also holds the four ignored reference runs. |
|
||||||
|
| `src/gpu.rs` | GPU backend: wgpu + WGSL (Vulkan/DX12/Metal), SoA layout, f32. Re-implements the physics line-for-line in WGSL but **imports topology from `cpu`** rather than duplicating it. |
|
||||||
|
| `src/main.rs` | CLI, problem assembly, step loop, live output, report, CSV. Owns the `Spec` / `StepRec` / `FieldKind` contract shared by both backends. |
|
||||||
|
| `src/gif.rs` | encoding, palettes, normalisation, the HUD, delay dithering, and the physical-time frame timing. |
|
||||||
|
|
||||||
|
## Invariants (read before editing)
|
||||||
|
|
||||||
|
- **Physics is written twice, topology once.** `gpu.rs` mirrors `math.rs` by hand
|
||||||
|
in WGSL; masks, Bouzidi links and the patch frame come from `cpu::Geom` /
|
||||||
|
`cpu::Patch` / `cpu::initial_field`. Keep it that way — a past regression had the
|
||||||
|
GPU silently running Bouzidi for `--wall staircase`, caught only because two
|
||||||
|
models produced bit-identical output where they had to differ. Backend parity is
|
||||||
|
a hard requirement and `bench/parity.py` checks it: CPU f64 vs GPU f32 agree to
|
||||||
|
4–5 significant digits, and for **each** wall model the two backends agree to
|
||||||
|
0.007 % on Cd. (That number is *backend* parity, not agreement between wall
|
||||||
|
models — those differ from one another by ~1 %.)
|
||||||
|
- **The backends have different step contracts.** `Backend` in `main.rs` is an
|
||||||
|
enum, not a trait: CPU `step()` returns one `StepRec`, GPU `advance(out)` /
|
||||||
|
`flush(out)` push a *batch* — results accumulate in a 128-slot ring (`HIST`,
|
||||||
|
declared identically in Rust and WGSL) and sync once per batch. Adding a per-step
|
||||||
|
GPU readback outside `StepRec` reintroduces a `map_async` + `poll(Wait)` per step,
|
||||||
|
which cost ~8× throughput before batching (measured 863 → 6715 steps/s).
|
||||||
|
- **`GREL = 1e-8` is a relative threshold.** The test is `den > GREL * nrm`, where
|
||||||
|
`den = ⟨Δh|Δh⟩` and `nrm = ⟨Δ|Δ⟩` — so GREL is a fraction of the **full**
|
||||||
|
non-equilibrium norm, not of ⟨Δh|Δh⟩ itself. ⟨Δh|Δh⟩ is quadratic in
|
||||||
|
non-equilibrium and physically tiny (~1e-7…1e-9), so an absolute threshold fires
|
||||||
|
almost everywhere and silently substitutes γ = 2, i.e. plain LBGK instead of KBC.
|
||||||
|
The report prints the degenerate-node fraction; on a healthy threshold it must be
|
||||||
|
~0 (except the very first step).
|
||||||
|
- **GPU is f32 and cannot be otherwise** — WGSL has no `f64` type at all, so no
|
||||||
|
hardware helps. Convergence studies therefore run on CPU, everything else on GPU;
|
||||||
|
below a true error of ~1e-3 f32 diverges from f64 by an order of magnitude.
|
||||||
|
The **statistics reductions** (ρ, ⟨γ⟩, node and degenerate counts) use compensated
|
||||||
|
Kahan–Neumaier summation via `kadd`. The **force/torque kernel does not** — it
|
||||||
|
accumulates in plain f32 and reduces with a naive tree. Both `README.md:366` and
|
||||||
|
earlier revisions of this file claim otherwise; the code is the authority.
|
||||||
|
- **GPU dispatch is 2-D with a linear index rebuilt in the shader** (`lin()` /
|
||||||
|
`wlin()`), lifting the 65535-workgroup limit that capped grids at ~2048². The
|
||||||
|
workgroup → node mapping stays exactly linear, which is what lets the reductions
|
||||||
|
keep working; verified to 4096×4096.
|
||||||
|
- **Population storage has two binding layouts.** Normally two bindings (`f` and
|
||||||
|
`post`). When the adapter's max binding size is small (dzn/WSL2 caps a binding at
|
||||||
|
128 MiB while allowing a 2047 MiB buffer), each array is split across nine
|
||||||
|
bindings — one per direction, **18 in total** — raising the ceiling from 3.73 M to
|
||||||
|
33.5 M nodes. The choice comes from adapter limits. The split layout is the one
|
||||||
|
built out of `switch` statements in `fget`/`fset`/`pget`/`pset`; the combined
|
||||||
|
layout has none, which is the whole reason it is kept. Both give identical numbers;
|
||||||
|
the split costs 5.7 % bandwidth. Force it with `KBC2D_SPLIT_POPULATIONS` set to
|
||||||
|
anything other than `0`.
|
||||||
|
- **`MAX_BODY_BUCKETS = 4`.** Geometry is assembled from any number of bodies, but
|
||||||
|
per-body force is bucketed — bodies beyond the fourth have their forces merged
|
||||||
|
(`min(body, MAXB-1)` in three places), and the CLI warns when that happens.
|
||||||
|
- **Defaults encode measurements, not taste**: `--init uniform` (a rest start pumps
|
||||||
|
a quarter-wave channel resonance to 75 % of U and wrecks St/Cd/Cl — and the sponge
|
||||||
|
cannot remove it, since a standing mode has its pressure node exactly where the
|
||||||
|
sponge sits); `--kbc-model n1` (equal accuracy to `n2`, but ~4× higher bulk
|
||||||
|
viscosity, which damps longitudinal acoustics); `--wall hrr` (**provisional** —
|
||||||
|
chosen on scheme structure, pending campaign group C; `bouzidi` resolves geometry
|
||||||
|
~4× better). Accepted values: `--init uniform|rest`, `--kbc-model n1|n2`,
|
||||||
|
`--wall hrr|bouzidi|grad|staircase`.
|
||||||
|
|
||||||
|
## Validation campaign (`bench/`)
|
||||||
|
|
||||||
|
115 runs in nine groups (A 12, B 16, C 20, D 14, E 12, F 9, G 14, H 9, I 9), ≈90
|
||||||
|
machine-hours — ≈87 GPU-hours over 108 runs plus ≈3 CPU-hours over the 7 CPU runs
|
||||||
|
of group A. Each run writes `cmd.txt`, `log.txt`, `report.txt`, `series.csv` and
|
||||||
|
`summary.json` into its own folder under `out/` (untracked — **those are results,
|
||||||
|
never clean them**). Only **59 of the 115** pass `--gif`; the rest produce no
|
||||||
|
animation at all.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd bench
|
||||||
|
python preflight.py # start every scenario for two steps — catches typos
|
||||||
|
./run_campaign.sh --calibrate # measure this machine's MLUPS (hour estimates need it)
|
||||||
|
./run_campaign.sh --dry-run # cost estimate
|
||||||
|
./run_campaign.sh --resume # run, skipping what is already done
|
||||||
|
```
|
||||||
|
|
||||||
|
`run_campaign.sh` also takes `--smoke`, `--group`, `--only` and `--budget-hours`;
|
||||||
|
`run_campaign.ps1` is the Windows twin. Run `preflight.py` for real — it caught
|
||||||
|
group I failing on a GPU limit and group F's nine runs passing `--body-x` twice,
|
||||||
|
both of which would otherwise have surfaced twenty hours into a server campaign.
|
||||||
|
|
||||||
|
Note the campaign has barely started: `out/` currently holds four finished runs
|
||||||
|
(`A01`, `A02`, `A05`, `C09`) plus `campaign.log` and `summary.csv`.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Published image **`notbigghost/kbc2d:1.2.0`**; the server needs only
|
||||||
|
`docker-compose.server.yml` (the only compose file with no `build:` section), not
|
||||||
|
the sources. `ENTRYPOINT` is the campaign driver and `CMD` defaults to `--dry-run`,
|
||||||
|
so a stray `docker run` prints an estimate instead of starting a 100-hour job. Check
|
||||||
|
the card first (`--profile check run --rm vulkan`) — a missing GPU is better
|
||||||
|
discovered in a minute than in an hour. The `check` profile also carries
|
||||||
|
`preflight`, `calibrate` and `plan` services (plus `parity` in the WSL file).
|
||||||
|
|
||||||
|
Two version caveats: the `1.2.0` tag lives only in the compose files and prose —
|
||||||
|
`Cargo.toml` still says `version = "0.1.0"` and the Dockerfile defaults
|
||||||
|
`ARG VERSION=dev`. And `linux/amd64` is asserted in `README.md` only; no compose
|
||||||
|
file sets `platform:` and the Dockerfile sets no `--platform`.
|
||||||
|
|
||||||
|
Two environment traps, both already handled in the image but overridable from
|
||||||
|
outside: NVIDIA Container Toolkit only injects the Vulkan ICD when
|
||||||
|
`NVIDIA_DRIVER_CAPABILITIES` contains `graphics` (with `compute` alone wgpu sees no
|
||||||
|
adapter), and **WSL2 has no NVIDIA Vulkan driver for Linux at all** — the card
|
||||||
|
arrives over `/dev/dxg`. That path instead uses **dzn** (Mesa's Vulkan→D3D12
|
||||||
|
translation, why the base image is `archlinux:base` — Debian/Ubuntu do not build
|
||||||
|
dzn), needs no NVIDIA runtime, and requires
|
||||||
|
`WGPU_ALLOW_UNDERLYING_NONCOMPLIANT_ADAPTER=1` because dzn reports
|
||||||
|
`conformanceVersion = 0.0.0.0` and wgpu hides such adapters by default (the variable
|
||||||
|
is read only because `gpu.rs` builds the instance with `.with_env()`). Use
|
||||||
|
`docker-compose.wsl.yml` there.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# `docs/theory/` — Python prototype (all branches)
|
||||||
|
|
||||||
|
The earlier **Python/CuPy** implementation of the same LBM physics — cylinder flow
|
||||||
|
with a nested AMR patch and SDF+Bouzidi boundaries. `kbc2d` is a deliberate rewrite
|
||||||
|
of this, not a port. Do not fold any of it into the CMake build, and do not treat it
|
||||||
|
as dead code — but do not treat it as one program either.
|
||||||
|
|
||||||
|
**This directory holds four generations of the same solver.** CLAUDE.md used to name
|
||||||
|
only the newest, which made the rest look like clutter. Oldest to newest:
|
||||||
|
|
||||||
|
| generation | files | status |
|
||||||
|
|---|---|---|
|
||||||
|
| the original write-up | `_legacy/kbc_lbm.ipynb` (64 cells, executed, outputs stored) + `_legacy/kbc_lbm.py` | superseded |
|
||||||
|
| CPU reference prototypes | `_amr_anims.py`, `_amr_anims_sdf.py` (pure NumPy) | superseded, but these are the "verified CPU prototypes" the GPU core is checked against |
|
||||||
|
| GPU core + runners | `amr_gpu_core.py` (**has a NumPy CPU fallback**) driven by `amr_cylinder_gpu.py` (no SDF) and `amr_cylinder_sdf_gpu.py` (SDF+Bouzidi); both self-titled "ОСНОВНОЙ", three modes: none / 2× / nested 2×+4× | superseded as a model, **still the producer of committed figures 11e–11g and four `*_cyl*.gif` animations** — deleting it orphans notebook images |
|
||||||
|
| optimised rewrite | `amr_opt/` (`*_opt.py`: no NumPy fallback, `cp.fuse`, precomputed link masks, CUDA Graphs) | dead intermediate — produced nothing that is committed |
|
||||||
|
| **the current model** | `solver_2x_sdf/` (17 modules + README), demos in `demos_gpu/` | live |
|
||||||
|
|
||||||
|
So "**GPU/CuPy only, no CPU fallback**" is true of `solver_2x_sdf/` (`backend.py`
|
||||||
|
raises outright), `demos_gpu/` and `amr_opt/` — but **not** of `amr_gpu_core.py`,
|
||||||
|
which silently falls back to NumPy, nor of `_amr_anims*.py` and `_legacy/`, which are
|
||||||
|
CPU-only.
|
||||||
|
|
||||||
|
Also committed and easy to mistake for junk: `figures/` (14 PNG) and `anim/`
|
||||||
|
(10 GIF, 55 MB) are tracked **deliberately** so the notebook renders on a fresh
|
||||||
|
clone; `_amr_probe_data*.npz` are probe scalars one runner writes and the other
|
||||||
|
reads; `factors.csv` is a hand-preserved snapshot of the Experiment-№1 registry
|
||||||
|
(nothing writes that path — `run_factors.py` writes `solver_2x_sdf/out/factors.csv`,
|
||||||
|
which is gitignored).
|
||||||
|
|
||||||
|
- `solver_2x_sdf/README.md` — maps every file to the physics it owns (17 rows, 17
|
||||||
|
modules, exact); entry points `run.py`, `run_blockage.py`, `run_factors.py`.
|
||||||
|
**Two defects in it:** it suggests switching `cfg.collision="bgk"` or registering
|
||||||
|
a TRT operator in a `get` registry — neither the config field nor the registry
|
||||||
|
exists, and the same README says two paragraphs later that the operator is fixed;
|
||||||
|
and it declares **all of its own quantitative results stale** pending a re-run
|
||||||
|
after the `GREL` fix, which also invalidates notebook §22–24.
|
||||||
|
- `demos_gpu/` — 8 scripts + README producing 17 of the notebook's artefacts. They
|
||||||
|
import the model's operators from `solver_2x_sdf` and add only what the model
|
||||||
|
deliberately lacks: `_common.py` reimplements a polynomial equilibrium, BGK, the
|
||||||
|
H-function and a γ-field for visualisation. `figures_static.py` is pure matplotlib
|
||||||
|
and imports no physics at all.
|
||||||
|
- `kbc_lbm.ipynb` — the write-up. It does not merely "execute nothing": it has **20
|
||||||
|
cells, all markdown, zero code cells**. Artefacts are relative-path links, not
|
||||||
|
embedded data, so it is only readable in place — and **4 of its 26 image links are
|
||||||
|
dead**, all pointing into the gitignored `solver_2x_sdf/out/`.
|
||||||
|
- `docs/origins/` — the six source PDFs behind both implementations (seven on
|
||||||
|
`research`, which adds Grad's approximation).
|
||||||
|
|
||||||
|
## How kbc2d differs from this prototype
|
||||||
|
|
||||||
|
Documented in one paragraph at `docs/theory/2d_solver/README.md:390` — **on branch
|
||||||
|
`research` only**; nothing under `docs/theory/` mentions the divergence. There are
|
||||||
|
**three** differences, not two:
|
||||||
|
|
||||||
|
1. kbc2d does not collide inside the body (those populations are fictitious; γ
|
||||||
|
statistics are then gathered strictly over fluid). The Python solver *does*
|
||||||
|
collide there — `kbc_collide` takes no fluid mask.
|
||||||
|
2. kbc2d's restriction additionally skips coarse nodes whose *fine source* node lies
|
||||||
|
inside the body. The Python restriction already gates on the **coarse
|
||||||
|
destination** mask.
|
||||||
|
3. kbc2d's CPU backend is f64 and its GPU backend f32; the Python side is fp32 by
|
||||||
|
default, f64 only via `AMR_FP64`.
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
cmake_minimum_required(VERSION 3.26)
|
|
||||||
|
|
||||||
project(SimVulcan
|
|
||||||
VERSION 0.1.0
|
|
||||||
DESCRIPTION "Vulkan 3D editor: orbit camera, reference grid, OBJ model viewer"
|
|
||||||
LANGUAGES CXX C
|
|
||||||
)
|
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
||||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
|
||||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
|
||||||
|
|
||||||
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
|
|
||||||
|
|
||||||
include(CompilerWarnings)
|
|
||||||
include(Sanitizers)
|
|
||||||
include(VulkanSetup)
|
|
||||||
include(ShaderCompile)
|
|
||||||
include(FetchContent)
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# External dependencies (FetchContent — pinned commits)
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
set(FETCHCONTENT_QUIET OFF)
|
|
||||||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
|
||||||
|
|
||||||
# GLFW
|
|
||||||
set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
|
|
||||||
set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
|
||||||
set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE)
|
|
||||||
FetchContent_Declare(glfw
|
|
||||||
GIT_REPOSITORY https://github.com/glfw/glfw.git
|
|
||||||
GIT_TAG 3.4
|
|
||||||
)
|
|
||||||
|
|
||||||
# GLM
|
|
||||||
FetchContent_Declare(glm
|
|
||||||
GIT_REPOSITORY https://github.com/g-truc/glm.git
|
|
||||||
GIT_TAG 1.0.1
|
|
||||||
)
|
|
||||||
|
|
||||||
# volk (lazy Vulkan loader)
|
|
||||||
FetchContent_Declare(volk
|
|
||||||
GIT_REPOSITORY https://github.com/zeux/volk.git
|
|
||||||
GIT_TAG 1.3.295
|
|
||||||
)
|
|
||||||
|
|
||||||
# vk-bootstrap
|
|
||||||
FetchContent_Declare(vk-bootstrap
|
|
||||||
GIT_REPOSITORY https://github.com/charles-lunarg/vk-bootstrap.git
|
|
||||||
GIT_TAG v1.3.295
|
|
||||||
)
|
|
||||||
|
|
||||||
# Vulkan Memory Allocator
|
|
||||||
FetchContent_Declare(VulkanMemoryAllocator
|
|
||||||
GIT_REPOSITORY https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git
|
|
||||||
GIT_TAG v3.1.0
|
|
||||||
)
|
|
||||||
|
|
||||||
# Dear ImGui (we wrap it in a small CMake target ourselves below).
|
|
||||||
FetchContent_Declare(imgui
|
|
||||||
GIT_REPOSITORY https://github.com/ocornut/imgui.git
|
|
||||||
GIT_TAG v1.91.5-docking
|
|
||||||
)
|
|
||||||
|
|
||||||
# spdlog
|
|
||||||
set(SPDLOG_BUILD_SHARED OFF CACHE BOOL "" FORCE)
|
|
||||||
set(SPDLOG_FMT_EXTERNAL OFF CACHE BOOL "" FORCE)
|
|
||||||
FetchContent_Declare(spdlog
|
|
||||||
GIT_REPOSITORY https://github.com/gabime/spdlog.git
|
|
||||||
GIT_TAG v1.14.1
|
|
||||||
)
|
|
||||||
|
|
||||||
# tinyobjloader (header-only)
|
|
||||||
FetchContent_Declare(tinyobjloader
|
|
||||||
GIT_REPOSITORY https://github.com/tinyobjloader/tinyobjloader.git
|
|
||||||
GIT_TAG v2.0.0rc13
|
|
||||||
)
|
|
||||||
|
|
||||||
# Catch2
|
|
||||||
FetchContent_Declare(Catch2
|
|
||||||
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
|
|
||||||
GIT_TAG v3.7.1
|
|
||||||
)
|
|
||||||
|
|
||||||
FetchContent_MakeAvailable(
|
|
||||||
glfw glm volk vk-bootstrap VulkanMemoryAllocator
|
|
||||||
imgui spdlog tinyobjloader Catch2
|
|
||||||
)
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# ImGui static library (needs explicit target — official repo has none)
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
add_library(imgui STATIC
|
|
||||||
${imgui_SOURCE_DIR}/imgui.cpp
|
|
||||||
${imgui_SOURCE_DIR}/imgui_demo.cpp
|
|
||||||
${imgui_SOURCE_DIR}/imgui_draw.cpp
|
|
||||||
${imgui_SOURCE_DIR}/imgui_tables.cpp
|
|
||||||
${imgui_SOURCE_DIR}/imgui_widgets.cpp
|
|
||||||
${imgui_SOURCE_DIR}/backends/imgui_impl_glfw.cpp
|
|
||||||
${imgui_SOURCE_DIR}/backends/imgui_impl_vulkan.cpp
|
|
||||||
)
|
|
||||||
target_include_directories(imgui PUBLIC
|
|
||||||
${imgui_SOURCE_DIR}
|
|
||||||
${imgui_SOURCE_DIR}/backends
|
|
||||||
)
|
|
||||||
target_link_libraries(imgui PUBLIC glfw Vulkan::Headers volk)
|
|
||||||
# Have ImGui's Vulkan backend pick up function pointers from volk directly,
|
|
||||||
# instead of asking us to load them via ImGui_ImplVulkan_LoadFunctions.
|
|
||||||
target_compile_definitions(imgui PUBLIC
|
|
||||||
IMGUI_IMPL_VULKAN_NO_PROTOTYPES
|
|
||||||
IMGUI_IMPL_VULKAN_USE_VOLK
|
|
||||||
VK_NO_PROTOTYPES
|
|
||||||
)
|
|
||||||
|
|
||||||
# Catch2 helpers (catch_discover_tests).
|
|
||||||
include(CTest)
|
|
||||||
include(${Catch2_SOURCE_DIR}/extras/Catch.cmake)
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Project targets
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
add_subdirectory(shaders) # produces simv_shaders custom target
|
|
||||||
add_subdirectory(src/core)
|
|
||||||
add_subdirectory(src/vk)
|
|
||||||
add_subdirectory(src/mesh)
|
|
||||||
add_subdirectory(src/editor)
|
|
||||||
add_subdirectory(src/app)
|
|
||||||
enable_testing()
|
|
||||||
add_subdirectory(tests)
|
|
||||||
|
|
||||||
message(STATUS "")
|
|
||||||
message(STATUS "SimVulcan ${PROJECT_VERSION} configured.")
|
|
||||||
message(STATUS " Build type: ${CMAKE_BUILD_TYPE}")
|
|
||||||
message(STATUS " C++ std: ${CMAKE_CXX_STANDARD}")
|
|
||||||
message(STATUS " Vulkan: ${Vulkan_VERSION}")
|
|
||||||
message(STATUS " Generator: ${CMAKE_GENERATOR}")
|
|
||||||
message(STATUS "")
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 6,
|
|
||||||
"cmakeMinimumRequired": { "major": 3, "minor": 26, "patch": 0 },
|
|
||||||
"configurePresets": [
|
|
||||||
{
|
|
||||||
"name": "base",
|
|
||||||
"hidden": true,
|
|
||||||
"generator": "Ninja",
|
|
||||||
"binaryDir": "${sourceDir}/build/${presetName}",
|
|
||||||
"cacheVariables": {
|
|
||||||
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "windows-msvc-debug",
|
|
||||||
"inherits": "base",
|
|
||||||
"cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" },
|
|
||||||
"condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "windows-msvc-release",
|
|
||||||
"inherits": "base",
|
|
||||||
"cacheVariables": { "CMAKE_BUILD_TYPE": "Release" },
|
|
||||||
"condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "linux-gcc-release",
|
|
||||||
"inherits": "base",
|
|
||||||
"cacheVariables": {
|
|
||||||
"CMAKE_BUILD_TYPE": "Release",
|
|
||||||
"CMAKE_C_COMPILER": "gcc",
|
|
||||||
"CMAKE_CXX_COMPILER": "g++"
|
|
||||||
},
|
|
||||||
"condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Linux" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "linux-clang-release",
|
|
||||||
"inherits": "base",
|
|
||||||
"cacheVariables": {
|
|
||||||
"CMAKE_BUILD_TYPE": "Release",
|
|
||||||
"CMAKE_C_COMPILER": "clang",
|
|
||||||
"CMAKE_CXX_COMPILER": "clang++"
|
|
||||||
},
|
|
||||||
"condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Linux" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "macos-arm64-release",
|
|
||||||
"inherits": "base",
|
|
||||||
"cacheVariables": {
|
|
||||||
"CMAKE_BUILD_TYPE": "Release",
|
|
||||||
"CMAKE_OSX_ARCHITECTURES": "arm64"
|
|
||||||
},
|
|
||||||
"condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Darwin" }
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"buildPresets": [
|
|
||||||
{ "name": "windows-msvc-debug", "configurePreset": "windows-msvc-debug" },
|
|
||||||
{ "name": "windows-msvc-release", "configurePreset": "windows-msvc-release" },
|
|
||||||
{ "name": "linux-gcc-release", "configurePreset": "linux-gcc-release" },
|
|
||||||
{ "name": "linux-clang-release", "configurePreset": "linux-clang-release" },
|
|
||||||
{ "name": "macos-arm64-release", "configurePreset": "macos-arm64-release" }
|
|
||||||
],
|
|
||||||
"testPresets": [
|
|
||||||
{ "name": "windows-msvc-debug", "configurePreset": "windows-msvc-debug",
|
|
||||||
"output": { "outputOnFailure": true } }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
# SimVulcan
|
|
||||||
|
|
||||||
A minimal Vulkan 1.3 / C++20 **3D model editor**: an orbit camera over three
|
|
||||||
reference grid planes through the origin, with `.obj` model loading and display.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
* Orbit camera — drag to rotate around the centre, wheel to zoom, and Front / Top
|
|
||||||
/ Side buttons to align the view to the principal planes.
|
|
||||||
* A 3D editor space: three grid planes (XY, XZ, YZ) and coloured X/Y/Z axes meeting
|
|
||||||
at the origin.
|
|
||||||
* `.obj` loading with three display modes — solid (flat-shaded), wireframe, and
|
|
||||||
solid + wireframe overlay.
|
|
||||||
* ImGui interface (Viewport controls + Mesh load panel).
|
|
||||||
|
|
||||||
All Vulkan code is isolated in `src/vk/`; the rest of the app is Vulkan-free.
|
|
||||||
|
|
||||||
## Targets
|
|
||||||
|
|
||||||
* Windows x64 (MSVC / clang-cl)
|
|
||||||
* Linux x64 (gcc / clang)
|
|
||||||
* macOS arm64 (Apple Silicon, via MoltenVK)
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
* Vulkan SDK 1.3.290+ (`https://vulkan.lunarg.com/`)
|
|
||||||
* CMake 3.26+
|
|
||||||
* Ninja
|
|
||||||
* C++20 compiler (MSVC 19.36+, gcc 11+, clang 14+)
|
|
||||||
|
|
||||||
## Build
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cmake --preset windows-msvc-release
|
|
||||||
cmake --build --preset windows-msvc-release
|
|
||||||
```
|
|
||||||
|
|
||||||
Run from the executable's own directory (`build/<preset>/src/app/`) so it finds
|
|
||||||
the `spirv/` and `assets/` trees copied next to it.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cmake --build --preset windows-msvc-debug --target simv_tests
|
|
||||||
ctest --preset windows-msvc-debug
|
|
||||||
```
|
|
||||||
|
|
||||||
## Layout
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
core/ Logger, Window, App (orchestration, Vulkan-free)
|
|
||||||
vk/ All Vulkan: Context, Swapchain, Buffer, Image, Shader, GpuMesh,
|
|
||||||
Renderer (frame loop + depth + ImGui), MeshRenderer, GridRenderer
|
|
||||||
mesh/ CPU mesh: tinyobjloader + welding (no Vulkan)
|
|
||||||
editor/ Camera, mouse input, EditorUI + MeshLoadPanel (ImGui, no Vulkan)
|
|
||||||
app/ main.cpp
|
|
||||||
shaders/
|
|
||||||
editor/ mesh + grid vertex/fragment shaders (GLSL → SPIR-V at build time)
|
|
||||||
tests/ Catch2 unit tests (mesh math)
|
|
||||||
```
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
mtllib C:\Users\ivan\Downloads\Cube.mtl
|
|
||||||
o Solid_1
|
|
||||||
v -0.50000000 0.50000000 1
|
|
||||||
v -0.50000000 -0.50000000 1
|
|
||||||
v 0.50000000 -0.50000000 1
|
|
||||||
v 0.50000000 0.50000000 1
|
|
||||||
v -0.50000000 -0.50000000 0
|
|
||||||
v 0.50000000 -0.50000000 0
|
|
||||||
v -0.50000000 0.50000000 0
|
|
||||||
v 0.50000000 0.50000000 0
|
|
||||||
vt 0 1
|
|
||||||
vt 0 0
|
|
||||||
vt 1 0
|
|
||||||
vt 1 1
|
|
||||||
vt 1 0
|
|
||||||
vt 1 1
|
|
||||||
vt 0 1
|
|
||||||
vt 0 0
|
|
||||||
vt 0 1
|
|
||||||
vt 0 0
|
|
||||||
vt 1 0
|
|
||||||
vt 1 1
|
|
||||||
vt 0 1
|
|
||||||
vt 0 0
|
|
||||||
vt 1 0
|
|
||||||
vt 1 1
|
|
||||||
vt 1 1
|
|
||||||
vt 0 1
|
|
||||||
vt 0 0
|
|
||||||
vt 1 0
|
|
||||||
vt 0 0
|
|
||||||
vt 1 0
|
|
||||||
vt 1 1
|
|
||||||
vt 0 1
|
|
||||||
vn 0 0 1
|
|
||||||
vn 0 0 1
|
|
||||||
vn 0 0 1
|
|
||||||
vn 0 0 1
|
|
||||||
vn 0 -1 0
|
|
||||||
vn 0 -1 0
|
|
||||||
vn 0 -1 0
|
|
||||||
vn 0 -1 0
|
|
||||||
vn -1 0 0
|
|
||||||
vn -1 0 0
|
|
||||||
vn -1 0 0
|
|
||||||
vn -1 0 0
|
|
||||||
vn 0 1 0
|
|
||||||
vn 0 1 0
|
|
||||||
vn 0 1 0
|
|
||||||
vn 0 1 0
|
|
||||||
vn 0 0 -1
|
|
||||||
vn 0 0 -1
|
|
||||||
vn 0 0 -1
|
|
||||||
vn 0 0 -1
|
|
||||||
vn 1 0 0
|
|
||||||
vn 1 0 0
|
|
||||||
vn 1 0 0
|
|
||||||
vn 1 0 0
|
|
||||||
f 1/1/1 2/2/2 3/3/3
|
|
||||||
f 3/3/3 4/4/4 1/1/1
|
|
||||||
f 5/5/5 6/6/6 3/7/7
|
|
||||||
f 3/7/7 2/8/8 5/5/5
|
|
||||||
f 7/9/9 5/10/10 2/11/11
|
|
||||||
f 2/11/11 1/12/12 7/9/9
|
|
||||||
f 8/13/13 7/14/14 1/15/15
|
|
||||||
f 1/15/15 4/16/16 8/13/13
|
|
||||||
f 7/17/17 8/18/18 6/19/19
|
|
||||||
f 6/19/19 5/20/20 7/17/17
|
|
||||||
f 3/21/21 6/22/22 8/23/23
|
|
||||||
f 8/23/23 4/24/24 3/21/21
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
mtllib C:\Users\ivan\Downloads\Cube.mtl
|
|
||||||
o Solid_1
|
|
||||||
v -0.50000000 0.50000000 1
|
|
||||||
v -0.50000000 -0.50000000 1
|
|
||||||
v 0.50000000 -0.50000000 1
|
|
||||||
v 0.50000000 0.50000000 1
|
|
||||||
v -0.50000000 -0.50000000 0
|
|
||||||
v 0.50000000 -0.50000000 0
|
|
||||||
v -0.50000000 0.50000000 0
|
|
||||||
v 0.50000000 0.50000000 0
|
|
||||||
vt 0 1
|
|
||||||
vt 0 0
|
|
||||||
vt 1 0
|
|
||||||
vt 1 1
|
|
||||||
vt 1 0
|
|
||||||
vt 1 1
|
|
||||||
vt 0 1
|
|
||||||
vt 0 0
|
|
||||||
vt 0 1
|
|
||||||
vt 0 0
|
|
||||||
vt 1 0
|
|
||||||
vt 1 1
|
|
||||||
vt 0 1
|
|
||||||
vt 0 0
|
|
||||||
vt 1 0
|
|
||||||
vt 1 1
|
|
||||||
vt 1 1
|
|
||||||
vt 0 1
|
|
||||||
vt 0 0
|
|
||||||
vt 1 0
|
|
||||||
vt 0 0
|
|
||||||
vt 1 0
|
|
||||||
vt 1 1
|
|
||||||
vt 0 1
|
|
||||||
vn 0 0 1
|
|
||||||
vn 0 0 1
|
|
||||||
vn 0 0 1
|
|
||||||
vn 0 0 1
|
|
||||||
vn 0 -1 0
|
|
||||||
vn 0 -1 0
|
|
||||||
vn 0 -1 0
|
|
||||||
vn 0 -1 0
|
|
||||||
vn -1 0 0
|
|
||||||
vn -1 0 0
|
|
||||||
vn -1 0 0
|
|
||||||
vn -1 0 0
|
|
||||||
vn 0 1 0
|
|
||||||
vn 0 1 0
|
|
||||||
vn 0 1 0
|
|
||||||
vn 0 1 0
|
|
||||||
vn 0 0 -1
|
|
||||||
vn 0 0 -1
|
|
||||||
vn 0 0 -1
|
|
||||||
vn 0 0 -1
|
|
||||||
vn 1 0 0
|
|
||||||
vn 1 0 0
|
|
||||||
vn 1 0 0
|
|
||||||
vn 1 0 0
|
|
||||||
f 1/1/1 2/2/2 3/3/3
|
|
||||||
f 3/3/3 4/4/4 1/1/1
|
|
||||||
f 5/5/5 6/6/6 3/7/7
|
|
||||||
f 3/7/7 2/8/8 5/5/5
|
|
||||||
f 7/9/9 5/10/10 2/11/11
|
|
||||||
f 2/11/11 1/12/12 7/9/9
|
|
||||||
f 8/13/13 7/14/14 1/15/15
|
|
||||||
f 1/15/15 4/16/16 8/13/13
|
|
||||||
f 7/17/17 8/18/18 6/19/19
|
|
||||||
f 6/19/19 5/20/20 7/17/17
|
|
||||||
f 3/21/21 6/22/22 8/23/23
|
|
||||||
f 8/23/23 4/24/24 3/21/21
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
function(simv_set_warnings TARGET)
|
|
||||||
if(MSVC)
|
|
||||||
target_compile_options(${TARGET} PRIVATE
|
|
||||||
/W4 /permissive- /Zc:preprocessor /Zc:__cplusplus
|
|
||||||
/wd4100 # unused parameter (interfaces have many)
|
|
||||||
)
|
|
||||||
target_compile_definitions(${TARGET} PRIVATE
|
|
||||||
_CRT_SECURE_NO_WARNINGS
|
|
||||||
NOMINMAX
|
|
||||||
WIN32_LEAN_AND_MEAN
|
|
||||||
)
|
|
||||||
else()
|
|
||||||
target_compile_options(${TARGET} PRIVATE
|
|
||||||
-Wall -Wextra -Wpedantic
|
|
||||||
-Wno-unused-parameter
|
|
||||||
-Wshadow -Wnon-virtual-dtor -Wold-style-cast
|
|
||||||
-Wcast-align -Wunused -Woverloaded-virtual
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
endfunction()
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
function(simv_enable_sanitizers TARGET)
|
|
||||||
if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
|
||||||
return()
|
|
||||||
endif()
|
|
||||||
if(MSVC)
|
|
||||||
target_compile_options(${TARGET} PRIVATE /fsanitize=address)
|
|
||||||
else()
|
|
||||||
target_compile_options(${TARGET} PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer)
|
|
||||||
target_link_options (${TARGET} PRIVATE -fsanitize=address,undefined)
|
|
||||||
endif()
|
|
||||||
endfunction()
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
# Compile GLSL (.comp/.vert/.frag) into SPIR-V at build time.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# simv_compile_shaders(simv_shaders
|
|
||||||
# shaders/editor/mesh.vert
|
|
||||||
# shaders/editor/mesh.frag
|
|
||||||
# INCLUDES shaders
|
|
||||||
# )
|
|
||||||
#
|
|
||||||
# Produces ${CMAKE_BINARY_DIR}/spirv/<relative_dir>/<name>.spv as a custom target.
|
|
||||||
|
|
||||||
function(simv_compile_shaders TARGET)
|
|
||||||
cmake_parse_arguments(SCS "" "" "INCLUDES" ${ARGN})
|
|
||||||
|
|
||||||
set(SOURCES ${SCS_UNPARSED_ARGUMENTS})
|
|
||||||
set(SPIRV_OUT_DIR "${CMAKE_BINARY_DIR}/spirv")
|
|
||||||
file(MAKE_DIRECTORY "${SPIRV_OUT_DIR}")
|
|
||||||
|
|
||||||
set(SPV_OUTPUTS "")
|
|
||||||
foreach(SRC IN LISTS SOURCES)
|
|
||||||
get_filename_component(SHADER_NAME "${SRC}" NAME)
|
|
||||||
file(RELATIVE_PATH REL_PATH "${CMAKE_CURRENT_SOURCE_DIR}" "${SRC}")
|
|
||||||
get_filename_component(REL_DIR "${REL_PATH}" DIRECTORY)
|
|
||||||
set(OUT_PATH "${SPIRV_OUT_DIR}/${REL_DIR}/${SHADER_NAME}.spv")
|
|
||||||
|
|
||||||
get_filename_component(OUT_DIR "${OUT_PATH}" DIRECTORY)
|
|
||||||
file(MAKE_DIRECTORY "${OUT_DIR}")
|
|
||||||
|
|
||||||
set(INCLUDE_FLAGS "")
|
|
||||||
foreach(INC IN LISTS SCS_INCLUDES)
|
|
||||||
# glslangValidator requires -I<dir> with NO whitespace.
|
|
||||||
list(APPEND INCLUDE_FLAGS "-I${INC}")
|
|
||||||
endforeach()
|
|
||||||
|
|
||||||
add_custom_command(
|
|
||||||
OUTPUT "${OUT_PATH}"
|
|
||||||
COMMAND "${Vulkan_GLSLANG_VALIDATOR_EXECUTABLE}" -V --target-env vulkan1.3
|
|
||||||
${INCLUDE_FLAGS} -o "${OUT_PATH}" "${SRC}"
|
|
||||||
DEPENDS "${SRC}"
|
|
||||||
COMMENT "GLSL → SPIR-V: ${REL_PATH}"
|
|
||||||
VERBATIM
|
|
||||||
)
|
|
||||||
list(APPEND SPV_OUTPUTS "${OUT_PATH}")
|
|
||||||
endforeach()
|
|
||||||
|
|
||||||
add_custom_target(${TARGET} ALL DEPENDS ${SPV_OUTPUTS})
|
|
||||||
set_target_properties(${TARGET} PROPERTIES
|
|
||||||
SIMV_SPIRV_DIR "${SPIRV_OUT_DIR}"
|
|
||||||
)
|
|
||||||
endfunction()
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# Locate Vulkan SDK + glslangValidator (needed for offline shader compilation).
|
|
||||||
# On macOS this expects MoltenVK to be present (provided by Vulkan SDK 1.3.290+).
|
|
||||||
find_package(Vulkan REQUIRED COMPONENTS glslangValidator)
|
|
||||||
|
|
||||||
if(NOT Vulkan_glslangValidator_FOUND)
|
|
||||||
message(FATAL_ERROR "glslangValidator not found. Install Vulkan SDK from https://vulkan.lunarg.com/")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
message(STATUS "Vulkan version: ${Vulkan_VERSION}")
|
|
||||||
message(STATUS "Vulkan include dir: ${Vulkan_INCLUDE_DIRS}")
|
|
||||||
message(STATUS "glslangValidator: ${Vulkan_GLSLANG_VALIDATOR_EXECUTABLE}")
|
|
||||||
|
|
||||||
if(APPLE)
|
|
||||||
# MoltenVK ships with the SDK; it's loaded as ICD at runtime, no link change needed,
|
|
||||||
# but we set MVK_CONFIG_USE_METAL_PRIVATE_API=0 in env when running.
|
|
||||||
message(STATUS "macOS detected — Vulkan via MoltenVK (Apple Silicon arm64 only)")
|
|
||||||
endif()
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
# AMR на цилиндре: полный домен 150x72, три версии (без AMR / 2x / вложенный 2x+4x).
|
|
||||||
# Встроенные зонды: сила на теле (обмен импульсом) -> Cd/Cl; u_y в следе -> Струхаль.
|
|
||||||
# Визуализация: inferno по |omega|, ТОЛЬКО рамки зон (без линий сетки), подписи осей.
|
|
||||||
# После прогона — анализ и сравнение трёх версий на реально собранных данных.
|
|
||||||
import sys, os, io as _io, numpy as np, matplotlib
|
|
||||||
matplotlib.use("Agg")
|
|
||||||
import matplotlib.pyplot as plt, matplotlib.patches as mpatches
|
|
||||||
from PIL import Image
|
|
||||||
try: sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
except Exception: pass
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ANIM = os.path.join(HERE, "anim"); os.makedirs(ANIM, exist_ok=True)
|
|
||||||
FIGS = os.path.join(HERE, "figures"); os.makedirs(FIGS, exist_ok=True)
|
|
||||||
|
|
||||||
C=np.array([[0,0],[1,0],[0,1],[-1,0],[0,-1],[1,1],[-1,1],[-1,-1],[1,-1]],float)
|
|
||||||
W=np.array([4/9,1/9,1/9,1/9,1/9,1/36,1/36,1/36,1/36]); CS2=1/3; Q=9
|
|
||||||
CX,CY=C[:,0],C[:,1]; OPP=np.array([0,3,4,1,2,7,8,5,6])
|
|
||||||
M=np.zeros((Q,Q)); M[0]=1;M[1]=CX;M[2]=CY;M[3]=3*(CX**2+CY**2)-2
|
|
||||||
M[4]=CX**2-CY**2;M[5]=CX*CY;M[6]=CX**2*CY;M[7]=CX*CY**2;M[8]=CX**2*CY**2
|
|
||||||
Dm=np.zeros((Q,Q)); Dm[4,4]=Dm[5,5]=1.0; Ps=np.linalg.inv(M)@Dm@M
|
|
||||||
def feq(rho,u):
|
|
||||||
ux=np.clip(u[0],-0.95,0.95); uy=np.clip(u[1],-0.95,0.95)
|
|
||||||
sx=np.sqrt(1+3*ux**2); sy=np.sqrt(1+3*uy**2); base=rho*(2-sx)*(2-sy)
|
|
||||||
Bx=((2*ux+sx)/(1-ux))[None]**CX[:,None,None]; By=((2*uy+sy)/(1-uy))[None]**CY[:,None,None]
|
|
||||||
return W[:,None,None]*base[None]*Bx*By
|
|
||||||
def macros(f):
|
|
||||||
r=f.sum(0); return r,np.stack([(CX[:,None,None]*f).sum(0)/r,(CY[:,None,None]*f).sum(0)/r])
|
|
||||||
def collide(f,fe,beta):
|
|
||||||
dfn=f-fe; ds=np.einsum("ij,jab->iab",Ps,dfn); dh=dfn-ds; inv=1.0/fe
|
|
||||||
num=(ds*dh*inv).sum(0); den=(dh*dh*inv).sum(0)
|
|
||||||
with np.errstate(divide="ignore",invalid="ignore"):
|
|
||||||
g=np.where(den>1e-14,1/beta-(2-1/beta)*num/den,2.0)
|
|
||||||
return f-beta*(2*ds+g[None]*dh)
|
|
||||||
def stream(f):
|
|
||||||
fs=np.empty_like(f)
|
|
||||||
for i in range(Q): fs[i]=np.roll(f[i],(int(CY[i]),int(CX[i])),(0,1))
|
|
||||||
return fs
|
|
||||||
def smoothstep(x): x=min(max(x,0.0),1.0); return x*x*(3-2*x)
|
|
||||||
def tauc(tp,r): return r*tp-(r-1)/2
|
|
||||||
def vort(u):
|
|
||||||
return (np.roll(u[1],-1,1)-np.roll(u[1],1,1))*0.5-(np.roll(u[0],-1,0)-np.roll(u[0],1,0))*0.5
|
|
||||||
def patch(pNx,pNy,ax,bx,ay,by,r):
|
|
||||||
Wx,Wy=bx-ax,by-ay; Nfx,Nfy=r*Wx+1,r*Wy+1
|
|
||||||
FX,FY=np.meshgrid(ax+np.arange(Nfx)/r, ay+np.arange(Nfy)/r)
|
|
||||||
x0=np.floor(FX).astype(int); y0=np.floor(FY).astype(int)
|
|
||||||
x1=np.minimum(x0+1,pNx-1); y1=np.minimum(y0+1,pNy-1)
|
|
||||||
return dict(Nfx=Nfx,Nfy=Nfy,ax=ax,bx=bx,ay=ay,by=by,r=r,x0=x0,y0=y0,x1=x1,y1=y1,
|
|
||||||
tx=FX-x0,ty=FY-y0,slx=slice(r,r*Wx,r),sly=slice(r,r*Wy,r))
|
|
||||||
def pint(fld,P):
|
|
||||||
return (fld[...,P["y0"],P["x0"]]*(1-P["tx"])*(1-P["ty"])+fld[...,P["y0"],P["x1"]]*P["tx"]*(1-P["ty"])
|
|
||||||
+fld[...,P["y1"],P["x0"]]*(1-P["tx"])*P["ty"]+fld[...,P["y1"],P["x1"]]*P["tx"]*P["ty"])
|
|
||||||
def ghost(pf,P,Rcf):
|
|
||||||
r,u=macros(pf); neq=pf-feq(r,u)
|
|
||||||
return feq(pint(r,P),np.stack([pint(u[0],P),pint(u[1],P)]))+Rcf*pint(neq,P)
|
|
||||||
def fill(cf,gh):
|
|
||||||
cf[:,0,:]=gh[:,0,:];cf[:,-1,:]=gh[:,-1,:];cf[:,:,0]=gh[:,:,0];cf[:,:,-1]=gh[:,:,-1]; return cf
|
|
||||||
def restrict(cf,pf,P,Rfc,fluid):
|
|
||||||
r,u=macros(cf); neq=cf-feq(r,u); slx,sly=P["slx"],P["sly"]
|
|
||||||
nv=feq(r[sly,slx],u[:,sly,slx])+Rfc*neq[:,sly,slx]
|
|
||||||
cur=pf[:,P["ay"]+1:P["by"],P["ax"]+1:P["bx"]]
|
|
||||||
pf[:,P["ay"]+1:P["by"],P["ax"]+1:P["bx"]]=np.where(fluid[None],nv,cur); return pf
|
|
||||||
def bb(pre,f,solid):
|
|
||||||
for i in range(Q): f[i,solid]=pre[OPP[i],solid]
|
|
||||||
return f
|
|
||||||
def cyl(NX,NY,ccx,ccy,rad,walls=True):
|
|
||||||
Y,X=np.mgrid[0:NY,0:NX]; m=(X-ccx)**2+(Y-ccy)**2<=rad*rad
|
|
||||||
if walls: m[0,:]=True; m[-1,:]=True
|
|
||||||
return m
|
|
||||||
def force_on(fpost,solid_body):
|
|
||||||
# обмен импульсом (Mei et al. 2002): по линкам жидкий-узел -> твёрдый сосед,
|
|
||||||
# F = sum c_i (f_i + f_ī)^post — корректная передача импульса на стенку.
|
|
||||||
fluid=~solid_body; Fx=0.0; Fy=0.0
|
|
||||||
for i in range(1,Q):
|
|
||||||
nb=np.roll(solid_body,(-int(CY[i]),-int(CX[i])),(0,1)) # nb[x]=solid[x+c_i]
|
|
||||||
link=fluid & nb; s=(fpost[i]+fpost[OPP[i]])[link].sum()
|
|
||||||
Fx+=CX[i]*s; Fy+=CY[i]*s
|
|
||||||
return Fx,Fy
|
|
||||||
|
|
||||||
# --- полный домен 150x72 (вся сетка в кадре, измельчение внутри) ---
|
|
||||||
Nx,Ny=150,72; D=16; cx,cy=40,36; U=0.07; Re=150.0; STEPS=8000; ramp=1000; FE=34
|
|
||||||
D0=D; nu=U*D/Re; tau0=nu/CS2+0.5; b0=1/(2*tau0)
|
|
||||||
px,py=cx+3*D,cy # зонд в следе
|
|
||||||
solid0=cyl(Nx,Ny,cx,cy,D/2) # коарс: цилиндр + стенки
|
|
||||||
solid0_cyl=cyl(Nx,Ny,cx,cy,D/2,walls=False)
|
|
||||||
# уровень1 (2×, зелёный): тело+след
|
|
||||||
ax1,bx1,ay1,by1=16,118,8,64; P1=patch(Nx,Ny,ax1,bx1,ay1,by1,2)
|
|
||||||
t1=tauc(tau0,2); b1=1/(2*t1); R01=t1/(2*tau0); Rf01=1/R01
|
|
||||||
solid1=cyl(P1["Nfx"],P1["Nfy"],(cx-ax1)*2,(cy-ay1)*2,D,walls=False) # D_L=32
|
|
||||||
fl1=~solid0[ay1+1:by1,ax1+1:bx1]
|
|
||||||
# уровень2 (4×, оранжевый): у тела (в коарс-координатах [26:64]x[18:54])
|
|
||||||
cx2a,cx2b,cy2a,cy2b=26,64,18,54
|
|
||||||
P2=patch(P1["Nfx"],P1["Nfy"],(cx2a-ax1)*2,(cx2b-ax1)*2,(cy2a-ay1)*2,(cy2b-ay1)*2,2)
|
|
||||||
t2=tauc(t1,2); b2=1/(2*t2); R12=t2/(2*t1); Rf12=1/R12
|
|
||||||
solid2=cyl(P2["Nfx"],P2["Nfy"],(cx-cx2a)*4,(cy-cy2a)*4,2*D,walls=False) # D_L=64
|
|
||||||
fl2=np.ones((P2["by"]-P2["ay"]-1,P2["bx"]-P2["ax"]-1),bool)
|
|
||||||
onecol=np.ones((Ny,1))
|
|
||||||
print(f"домен {Nx}x{Ny}; L1(2×) {P1['Nfx']}x{P1['Nfy']}; L2(4×) {P2['Nfx']}x{P2['Nfy']}; "
|
|
||||||
f"Re={Re} D0={D0} steps={STEPS}")
|
|
||||||
|
|
||||||
def run(mode): # "none" | "amr2x" | "nested"
|
|
||||||
use1=mode in ("amr2x","nested"); use2=(mode=="nested")
|
|
||||||
DL={"none":D0,"amr2x":2*D0,"nested":4*D0}[mode]
|
|
||||||
f0=feq(np.ones((Ny,Nx)),np.zeros((2,Ny,Nx)))
|
|
||||||
f1=feq(np.ones((P1["Nfy"],P1["Nfx"])),np.zeros((2,P1["Nfy"],P1["Nfx"]))) if use1 else None
|
|
||||||
f2=feq(np.ones((P2["Nfy"],P2["Nfx"])),np.zeros((2,P2["Nfy"],P2["Nfx"]))) if use2 else None
|
|
||||||
Fx=np.zeros(STEPS+1); Fy=np.zeros(STEPS+1); uy=np.zeros(STEPS+1); frames=[]
|
|
||||||
for t in range(STEPS+1):
|
|
||||||
rho,u0=macros(f0)
|
|
||||||
if not np.all(np.isfinite(u0)): print("BLEW UP",mode,t); Fx=Fx[:t];Fy=Fy[:t];uy=uy[:t]; break
|
|
||||||
pre=f0.copy(); post0=collide(f0,feq(rho,u0),b0)
|
|
||||||
if mode=="none": Fx[t],Fy[t]=force_on(post0,solid0_cyl)
|
|
||||||
f0=stream(bb(pre,post0,solid0))
|
|
||||||
uin=np.zeros((2,Ny,1)); uin[0,:,0]=U*smoothstep(t/ramp)
|
|
||||||
f0[:,1:-1,0]=feq(onecol,uin)[:,1:-1,0]; f0[:,1:-1,-1]=f0[:,1:-1,-2]
|
|
||||||
if use1:
|
|
||||||
g1o=ghost(pre,P1,R01); g1n=ghost(f0,P1,R01)
|
|
||||||
for s1 in range(2):
|
|
||||||
pre1=f1.copy(); r1,u1=macros(f1); post1=collide(f1,feq(r1,u1),b1)
|
|
||||||
if mode=="amr2x" and s1==1: Fx[t],Fy[t]=force_on(post1,solid1)
|
|
||||||
f1=stream(bb(pre1,post1,solid1)); f1=fill(f1,(1-(s1+1)/2)*g1o+((s1+1)/2)*g1n)
|
|
||||||
if use2:
|
|
||||||
g2o=ghost(pre1,P2,R12); g2n=ghost(f1,P2,R12)
|
|
||||||
for s2 in range(2):
|
|
||||||
pre2=f2.copy(); r2,u2=macros(f2); post2=collide(f2,feq(r2,u2),b2)
|
|
||||||
if s1==1 and s2==1: Fx[t],Fy[t]=force_on(post2,solid2)
|
|
||||||
f2=stream(bb(pre2,post2,solid2)); f2=fill(f2,(1-(s2+1)/2)*g2o+((s2+1)/2)*g2n)
|
|
||||||
f1=restrict(f2,f1,P2,Rf12,fl2)
|
|
||||||
f0=restrict(f1,f0,P1,Rf01,fl1)
|
|
||||||
uc=macros(f0)[1]; uy[t]=uc[1,py,px]
|
|
||||||
if use1 and t%FE==0:
|
|
||||||
frames.append((t,uc.copy(),macros(f1)[1].copy(),(macros(f2)[1].copy() if use2 else None)))
|
|
||||||
return dict(mode=mode,DL=DL,Fx=Fx,Fy=Fy,uy=uy,frames=frames)
|
|
||||||
|
|
||||||
def save_gif(res,name,fps=24):
|
|
||||||
nested=(res["mode"]=="nested"); frames=res["frames"]
|
|
||||||
mid=frames[len(frames)//2][1]
|
|
||||||
vm=np.nanpercentile(np.abs(np.where(solid0,np.nan,vort(mid))),99.0)
|
|
||||||
cm=plt.get_cmap("inferno").copy(); cm.set_bad("white")
|
|
||||||
pil=[]
|
|
||||||
for (t,u0,u1,u2) in frames:
|
|
||||||
fig=plt.figure(figsize=(11.2,5.7),dpi=96); ax=fig.add_subplot(111)
|
|
||||||
ax.imshow(np.abs(np.where(solid0,np.nan,vort(u0))),origin="lower",cmap=cm,vmin=0,vmax=vm,
|
|
||||||
extent=(0,Nx,0,Ny),interpolation="nearest")
|
|
||||||
o1=np.abs(np.where(solid1,np.nan,vort(u1)))[1:-1,1:-1]
|
|
||||||
ax.imshow(o1,origin="lower",cmap=cm,vmin=0,vmax=vm,
|
|
||||||
extent=(ax1+0.5,bx1-0.5,ay1+0.5,by1-0.5),interpolation="nearest")
|
|
||||||
if nested and u2 is not None:
|
|
||||||
o2=np.abs(np.where(solid2,np.nan,vort(u2)))[1:-1,1:-1]
|
|
||||||
ax.imshow(o2,origin="lower",cmap=cm,vmin=0,vmax=vm,
|
|
||||||
extent=(cx2a+0.25,cx2b-0.25,cy2a+0.25,cy2b-0.25),interpolation="nearest")
|
|
||||||
# ТОЛЬКО рамки зон (без линий сетки)
|
|
||||||
ax.add_patch(mpatches.Rectangle((0.2,0.2),Nx-0.4,Ny-0.4,fill=False,edgecolor="#4fc3f7",lw=1.6))
|
|
||||||
ax.text(1.5,Ny-4,"уровень 0: Δx (крупный)",color="#4fc3f7",fontsize=9,weight="bold")
|
|
||||||
ax.add_patch(mpatches.Rectangle((ax1,ay1),bx1-ax1,by1-ay1,fill=False,edgecolor="#aed581",lw=2.2))
|
|
||||||
ax.text(ax1+1,by1-3.5,"уровень 1: Δx/2 (тело+след)",color="#aed581",fontsize=9,weight="bold")
|
|
||||||
if nested:
|
|
||||||
ax.add_patch(mpatches.Rectangle((cx2a,cy2a),cx2b-cx2a,cy2b-cy2a,fill=False,edgecolor="#ff8a65",lw=2.2))
|
|
||||||
ax.text(cx2a+1,cy2b-3.5,"уровень 2: Δx/4 (у тела)",color="#ff8a65",fontsize=9,weight="bold")
|
|
||||||
ttl="Вложенный AMR на цилиндре: 2× (след) + 4× (у тела)" if nested else "AMR на цилиндре: одиночный блок 2×"
|
|
||||||
ax.set_title(f"{ttl} · поверх $|\\omega|$ · t={t}",fontsize=11)
|
|
||||||
ax.set_xlabel("x [lu]"); ax.set_ylabel("y [lu]")
|
|
||||||
ax.set_xlim(0,Nx); ax.set_ylim(0,Ny)
|
|
||||||
buf=_io.BytesIO(); fig.savefig(buf,format="png",bbox_inches="tight"); buf.seek(0); plt.close(fig)
|
|
||||||
pil.append(Image.open(buf).convert("RGB").convert("P",palette=Image.ADAPTIVE))
|
|
||||||
path=os.path.join(ANIM,name)
|
|
||||||
pil[0].save(path,save_all=True,append_images=pil[1:],duration=int(1000/fps),loop=0,optimize=True)
|
|
||||||
print("saved",name,len(frames),"кадров")
|
|
||||||
|
|
||||||
def analyze(res):
|
|
||||||
Fx,Fy,uy,DL=res["Fx"],res["Fy"],res["uy"],res["DL"]
|
|
||||||
n=len(uy); h=slice(n//2,n) # статистически установившаяся половина
|
|
||||||
Cd=2*Fx/(U**2*DL); Cl=2*Fy/(U**2*DL)
|
|
||||||
sig=uy[h]-uy[h].mean(); npad=8192
|
|
||||||
freqs=np.fft.rfftfreq(npad,1.0); amp=np.abs(np.fft.rfft(sig,n=npad))
|
|
||||||
fpk=freqs[1+int(np.argmax(amp[1:]))] if len(amp)>2 else 0.0
|
|
||||||
St=fpk*D0/U
|
|
||||||
return dict(Cd=float(Cd[h].mean()),Clrms=float(Cl[h].std()),St=float(St),
|
|
||||||
uyrms=float(uy[h].std()),Cd_s=Cd,Cl_s=Cl,uy=uy,freqs=freqs,amp=amp,h0=n//2)
|
|
||||||
|
|
||||||
# ===== прогон трёх версий =====
|
|
||||||
print("=== none ==="); R0=run("none")
|
|
||||||
print("=== amr2x ==="); R1=run("amr2x")
|
|
||||||
print("=== nested ==="); R2=run("nested")
|
|
||||||
save_gif(R1,"amr2x_cyl.gif"); save_gif(R2,"amr_nested_cyl.gif")
|
|
||||||
|
|
||||||
A0,A1,A2=analyze(R0),analyze(R1),analyze(R2)
|
|
||||||
LABELS=["без AMR (D=16)","AMR 2× (D=32)","AMR 2×+4× (D=64)"]
|
|
||||||
AS=[A0,A1,A2]
|
|
||||||
print("\n=== СРАВНЕНИЕ НА СОБРАННЫХ ДАННЫХ (установившийся режим) ===")
|
|
||||||
print(f"{'версия':22}{'D у тела':>9}{'St':>8}{'<Cd>':>9}{'rms Cl':>9}{'rms u_y':>9}")
|
|
||||||
for lab,DL,a in zip(LABELS,[16,32,64],AS):
|
|
||||||
print(f"{lab:22}{DL:>9}{a['St']:>8.3f}{a['Cd']:>9.3f}{a['Clrms']:>9.4f}{a['uyrms']:>9.4f}")
|
|
||||||
print(f"{'литература Re≈150':22}{'—':>9}{0.183:>8.3f}{1.33:>9.3f}{'~0.3':>9}")
|
|
||||||
print("(лит.: St≈0.18, Cd≈1.3 для цилиндра при Re≈150; Williamson 1996, Henderson 1995)")
|
|
||||||
|
|
||||||
# ===== фигура сравнения =====
|
|
||||||
np.savez(os.path.join(HERE,"_amr_probe_data.npz"),
|
|
||||||
St=np.array([a['St'] for a in AS]), Cd=np.array([a['Cd'] for a in AS]),
|
|
||||||
Clrms=np.array([a['Clrms'] for a in AS]), uyrms=np.array([a['uyrms'] for a in AS]),
|
|
||||||
Cd0=A0['Cd_s'],Cd1=A1['Cd_s'],Cd2=A2['Cd_s'],
|
|
||||||
Cl0=A0['Cl_s'],Cl1=A1['Cl_s'],Cl2=A2['Cl_s'],
|
|
||||||
uy0=R0['uy'],uy1=R1['uy'],uy2=R2['uy'])
|
|
||||||
cols=["#9e9e9e","#1f6feb","#d1495b"]
|
|
||||||
fig=plt.figure(figsize=(13.5,8.2))
|
|
||||||
gs=fig.add_gridspec(2,2,hspace=0.32,wspace=0.22)
|
|
||||||
# поле |omega| вложенного AMR
|
|
||||||
axA=fig.add_subplot(gs[0,0])
|
|
||||||
mid=R2["frames"][len(R2["frames"])//2]; vmf=np.nanpercentile(np.abs(np.where(solid0,np.nan,vort(mid[1]))),99)
|
|
||||||
cmf=plt.get_cmap("inferno").copy(); cmf.set_bad("white")
|
|
||||||
axA.imshow(np.abs(np.where(solid0,np.nan,vort(mid[1]))),origin="lower",cmap=cmf,vmin=0,vmax=vmf,
|
|
||||||
extent=(0,Nx,0,Ny),interpolation="nearest")
|
|
||||||
axA.add_patch(mpatches.Rectangle((ax1,ay1),bx1-ax1,by1-ay1,fill=False,edgecolor="#aed581",lw=1.8))
|
|
||||||
axA.add_patch(mpatches.Rectangle((cx2a,cy2a),cx2b-cx2a,cy2b-cy2a,fill=False,edgecolor="#ff8a65",lw=1.8))
|
|
||||||
axA.set_title("Вложенный AMR: $|\\omega|$ + рамки зон 2×/4×"); axA.set_xlabel("x [lu]"); axA.set_ylabel("y [lu]")
|
|
||||||
# Cl(t)
|
|
||||||
axB=fig.add_subplot(gs[0,1])
|
|
||||||
for a,lab,c in zip(AS,LABELS,cols):
|
|
||||||
axB.plot(a['Cl_s'][a['h0']:],lw=0.8,color=c,label=lab)
|
|
||||||
axB.set_title("Подъёмная сила $C_l(t)$ (зонд силы, обмен импульсом)")
|
|
||||||
axB.set_xlabel("шаг (установившийся режим)"); axB.set_ylabel("$C_l$"); axB.legend(fontsize=8); axB.grid(alpha=0.3)
|
|
||||||
# спектры u_y
|
|
||||||
axC=fig.add_subplot(gs[1,0])
|
|
||||||
for a,lab,c in zip(AS,LABELS,cols):
|
|
||||||
sp=a['amp']/a['amp'][1:].max(); axC.plot(a['freqs']*D0/U,sp,lw=1.0,color=c,label=lab)
|
|
||||||
axC.axvline(0.183,color="k",ls="--",alpha=0.6,label="лит. St≈0.18")
|
|
||||||
axC.set_xlim(0,0.6); axC.set_title("Спектр $u_y$ в следе → число Струхаля")
|
|
||||||
axC.set_xlabel("St = f·D/U"); axC.set_ylabel("нормир. амплитуда"); axC.legend(fontsize=8); axC.grid(alpha=0.3)
|
|
||||||
# столбики St и Cd
|
|
||||||
axD=fig.add_subplot(gs[1,1]); x=np.arange(3); w=0.35
|
|
||||||
axD.bar(x-w/2,[a['St'] for a in AS],w,color="#1f6feb",label="St")
|
|
||||||
axD.bar(x+w/2,[a['Cd'] for a in AS],w,color="#fb8c00",label="<Cd>")
|
|
||||||
axD.axhline(0.183,color="#1f6feb",ls="--",alpha=0.6); axD.axhline(1.33,color="#fb8c00",ls="--",alpha=0.6)
|
|
||||||
axD.set_xticks(x); axD.set_xticklabels(["без AMR","2×","2×+4×"],fontsize=9)
|
|
||||||
axD.set_title("St и <Cd> vs разрешение у тела (пунктир — лит.)"); axD.legend(fontsize=8); axD.grid(alpha=0.3,axis="y")
|
|
||||||
for i,a in enumerate(AS):
|
|
||||||
axD.text(i-w/2,a['St']+0.005,f"{a['St']:.3f}",ha="center",fontsize=7)
|
|
||||||
axD.text(i+w/2,a['Cd']+0.02,f"{a['Cd']:.2f}",ha="center",fontsize=7)
|
|
||||||
fig.suptitle("AMR на цилиндре: сравнение трёх версий на реально собранных данных зондов",fontweight="bold")
|
|
||||||
fig.savefig(os.path.join(FIGS,"11e_amr_probe_comparison.png"),dpi=110,bbox_inches="tight"); plt.close(fig)
|
|
||||||
print("saved figures/11e_amr_probe_comparison.png")
|
|
||||||
print("DONE")
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
# СЛЕДУЮЩАЯ ИТЕРАЦИЯ решателя: тот же AMR (без / 2× / вложенный 2×+4×), но граница тела
|
|
||||||
# задана ПОЛНОЦЕННЫМ SDF (знаковое поле расстояний) + интерполированное отражение Bouzidi
|
|
||||||
# (Bouzidi-Firdaouss-Lallemand 2001) — сглаживает ступенчатость криволинейной границы.
|
|
||||||
# Геометрия/Re/шаги ИДЕНТИЧНЫ _amr_anims.py — отличие ТОЛЬКО в граничном условии, поэтому
|
|
||||||
# результаты прямо сравнимы. В конце — сводное сравнение с версиями без SDF.
|
|
||||||
import sys, os, io as _io, numpy as np, matplotlib
|
|
||||||
matplotlib.use("Agg")
|
|
||||||
import matplotlib.pyplot as plt, matplotlib.patches as mpatches
|
|
||||||
from PIL import Image
|
|
||||||
try: sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
except Exception: pass
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ANIM = os.path.join(HERE, "anim"); os.makedirs(ANIM, exist_ok=True)
|
|
||||||
FIGS = os.path.join(HERE, "figures"); os.makedirs(FIGS, exist_ok=True)
|
|
||||||
|
|
||||||
# ===== проверенное ядро D2Q9 KBC (идентично _amr_anims.py) =====
|
|
||||||
C=np.array([[0,0],[1,0],[0,1],[-1,0],[0,-1],[1,1],[-1,1],[-1,-1],[1,-1]],float)
|
|
||||||
W=np.array([4/9,1/9,1/9,1/9,1/9,1/36,1/36,1/36,1/36]); CS2=1/3; Q=9
|
|
||||||
CX,CY=C[:,0],C[:,1]; OPP=np.array([0,3,4,1,2,7,8,5,6])
|
|
||||||
M=np.zeros((Q,Q)); M[0]=1;M[1]=CX;M[2]=CY;M[3]=3*(CX**2+CY**2)-2
|
|
||||||
M[4]=CX**2-CY**2;M[5]=CX*CY;M[6]=CX**2*CY;M[7]=CX*CY**2;M[8]=CX**2*CY**2
|
|
||||||
Dm=np.zeros((Q,Q)); Dm[4,4]=Dm[5,5]=1.0; Ps=np.linalg.inv(M)@Dm@M
|
|
||||||
def feq(rho,u):
|
|
||||||
ux=np.clip(u[0],-0.95,0.95); uy=np.clip(u[1],-0.95,0.95)
|
|
||||||
sx=np.sqrt(1+3*ux**2); sy=np.sqrt(1+3*uy**2); base=rho*(2-sx)*(2-sy)
|
|
||||||
Bx=((2*ux+sx)/(1-ux))[None]**CX[:,None,None]; By=((2*uy+sy)/(1-uy))[None]**CY[:,None,None]
|
|
||||||
return W[:,None,None]*base[None]*Bx*By
|
|
||||||
def macros(f):
|
|
||||||
r=f.sum(0); return r,np.stack([(CX[:,None,None]*f).sum(0)/r,(CY[:,None,None]*f).sum(0)/r])
|
|
||||||
def collide(f,fe,beta):
|
|
||||||
dfn=f-fe; ds=np.einsum("ij,jab->iab",Ps,dfn); dh=dfn-ds; inv=1.0/fe
|
|
||||||
num=(ds*dh*inv).sum(0); den=(dh*dh*inv).sum(0)
|
|
||||||
with np.errstate(divide="ignore",invalid="ignore"):
|
|
||||||
g=np.where(den>1e-14,1/beta-(2-1/beta)*num/den,2.0)
|
|
||||||
return f-beta*(2*ds+g[None]*dh)
|
|
||||||
def stream(f):
|
|
||||||
fs=np.empty_like(f)
|
|
||||||
for i in range(Q): fs[i]=np.roll(f[i],(int(CY[i]),int(CX[i])),(0,1))
|
|
||||||
return fs
|
|
||||||
def smoothstep(x): x=min(max(x,0.0),1.0); return x*x*(3-2*x)
|
|
||||||
def tauc(tp,r): return r*tp-(r-1)/2
|
|
||||||
def vort(u):
|
|
||||||
return (np.roll(u[1],-1,1)-np.roll(u[1],1,1))*0.5-(np.roll(u[0],-1,0)-np.roll(u[0],1,0))*0.5
|
|
||||||
def patch(pNx,pNy,ax,bx,ay,by,r):
|
|
||||||
Wx,Wy=bx-ax,by-ay; Nfx,Nfy=r*Wx+1,r*Wy+1
|
|
||||||
FX,FY=np.meshgrid(ax+np.arange(Nfx)/r, ay+np.arange(Nfy)/r)
|
|
||||||
x0=np.floor(FX).astype(int); y0=np.floor(FY).astype(int)
|
|
||||||
x1=np.minimum(x0+1,pNx-1); y1=np.minimum(y0+1,pNy-1)
|
|
||||||
return dict(Nfx=Nfx,Nfy=Nfy,ax=ax,bx=bx,ay=ay,by=by,r=r,x0=x0,y0=y0,x1=x1,y1=y1,
|
|
||||||
tx=FX-x0,ty=FY-y0,slx=slice(r,r*Wx,r),sly=slice(r,r*Wy,r))
|
|
||||||
def pint(fld,P):
|
|
||||||
return (fld[...,P["y0"],P["x0"]]*(1-P["tx"])*(1-P["ty"])+fld[...,P["y0"],P["x1"]]*P["tx"]*(1-P["ty"])
|
|
||||||
+fld[...,P["y1"],P["x0"]]*(1-P["tx"])*P["ty"]+fld[...,P["y1"],P["x1"]]*P["tx"]*P["ty"])
|
|
||||||
def ghost(pf,P,Rcf):
|
|
||||||
r,u=macros(pf); neq=pf-feq(r,u)
|
|
||||||
return feq(pint(r,P),np.stack([pint(u[0],P),pint(u[1],P)]))+Rcf*pint(neq,P)
|
|
||||||
def fill(cf,gh):
|
|
||||||
cf[:,0,:]=gh[:,0,:];cf[:,-1,:]=gh[:,-1,:];cf[:,:,0]=gh[:,:,0];cf[:,:,-1]=gh[:,:,-1]; return cf
|
|
||||||
def restrict(cf,pf,P,Rfc,fluid):
|
|
||||||
r,u=macros(cf); neq=cf-feq(r,u); slx,sly=P["slx"],P["sly"]
|
|
||||||
nv=feq(r[sly,slx],u[:,sly,slx])+Rfc*neq[:,sly,slx]
|
|
||||||
cur=pf[:,P["ay"]+1:P["by"],P["ax"]+1:P["bx"]]
|
|
||||||
pf[:,P["ay"]+1:P["by"],P["ax"]+1:P["bx"]]=np.where(fluid[None],nv,cur); return pf
|
|
||||||
|
|
||||||
# ===== ПОЛНОЦЕННЫЙ SDF + интерполированное отражение Bouzidi =====
|
|
||||||
def sdf_circle(NX,NY,ccx,ccy,R):
|
|
||||||
# знаковое расстояние до окружности: >0 жидкость, <0 внутри тела
|
|
||||||
Y,X=np.mgrid[0:NY,0:NX]; return np.sqrt((X-ccx)**2+(Y-ccy)**2)-R
|
|
||||||
def sdf_from_mask(mask):
|
|
||||||
# общий SDF из произвольной бинарной маски (для не-круглых тел), через EDT
|
|
||||||
from scipy import ndimage
|
|
||||||
din=ndimage.distance_transform_edt(mask); dout=ndimage.distance_transform_edt(~mask)
|
|
||||||
return dout-din-0.5 # >0 жидкость, <0 тело; -0.5 — поверхность между узлами
|
|
||||||
def add_walls(phi):
|
|
||||||
NY=phi.shape[0]; Y=np.arange(NY)[:,None]*np.ones((1,phi.shape[1]))
|
|
||||||
return np.minimum(phi, np.minimum(Y-0.5, (NY-1.5)-Y)) # стенки: поверхность на y=0.5 и Ny-1.5
|
|
||||||
def build_bc(solid, phi, cyl_mask):
|
|
||||||
# для каждого направления: линки жидкий->твёрдый, доля стенки q (по SDF),
|
|
||||||
# есть ли второй жидкий узел (для интерполяции), и принадлежит ли сосед телу (для силы)
|
|
||||||
fluid=~solid; bc=[]
|
|
||||||
for i in range(1,Q):
|
|
||||||
cy_,cx_=int(CY[i]),int(CX[i])
|
|
||||||
nb_solid=np.roll(solid,(-cy_,-cx_),(0,1)) # solid[x+c_i]
|
|
||||||
mask=fluid & nb_solid
|
|
||||||
cl=mask & np.roll(cyl_mask,(-cy_,-cx_),(0,1)) # сосед — тело (не стенка)
|
|
||||||
phinb=np.roll(phi,(-cy_,-cx_),(0,1))
|
|
||||||
with np.errstate(divide="ignore",invalid="ignore"):
|
|
||||||
qq=phi/(phi-phinb) # доля линка до стенки (phi=0)
|
|
||||||
q=np.where(mask, np.clip(qq,0.02,0.98), 0.5)
|
|
||||||
xff=mask & (~np.roll(solid,(cy_,cx_),(0,1))) # x_f - c_i — жидкий?
|
|
||||||
bc.append((i,int(OPP[i]),mask,q,xff,cl))
|
|
||||||
return bc
|
|
||||||
def apply_bc(f, fpost, bc):
|
|
||||||
# Bouzidi (линейный): пристеночный жидкий узел получает корректно отражённую популяцию
|
|
||||||
for (i,ib,mask,q,xff,cl) in bc:
|
|
||||||
cy_,cx_=int(CY[i]),int(CX[i])
|
|
||||||
fi=fpost[i]; fib=fpost[ib]; fiback=np.roll(fpost[i],(cy_,cx_),(0,1)) # f_i в x_f-c_i
|
|
||||||
near=mask&(q<0.5)&xff; bad=mask&(q<0.5)&(~xff); far=mask&(q>=0.5)
|
|
||||||
qn=q[near]; f[ib][near]=2*qn*fi[near]+(1-2*qn)*fiback[near]
|
|
||||||
f[ib][bad]=fi[bad] # запас: нет второго узла -> halfway
|
|
||||||
qf=q[far]; f[ib][far]=(1/(2*qf))*fi[far]+(1-1/(2*qf))*fib[far]
|
|
||||||
return f
|
|
||||||
def force_bc(fpost, f, bc):
|
|
||||||
# обмен импульсом (Mei et al. 2002) для интерполированной границы: F=sum c_i (f_i^post + f_ī^bc)
|
|
||||||
Fx=0.0; Fy=0.0
|
|
||||||
for (i,ib,mask,q,xff,cl) in bc:
|
|
||||||
s=(fpost[i]+f[ib])[cl].sum(); Fx+=CX[i]*s; Fy+=CY[i]*s
|
|
||||||
return Fx,Fy
|
|
||||||
|
|
||||||
# ===== геометрия (идентична версии без SDF) =====
|
|
||||||
Nx,Ny=150,72; D=16; cx,cy=40,36; U=0.07; Re=150.0; STEPS=8000; ramp=1000; FE=34
|
|
||||||
D0=D; nu=U*D/Re; tau0=nu/CS2+0.5; b0=1/(2*tau0); px,py=cx+3*D,cy
|
|
||||||
# коарс: цилиндр (SDF) + стенки
|
|
||||||
phi0=add_walls(sdf_circle(Nx,Ny,cx,cy,D/2)); solid0=phi0<0
|
|
||||||
cyl0=sdf_circle(Nx,Ny,cx,cy,D/2)<0
|
|
||||||
bc0=build_bc(solid0,phi0,cyl0)
|
|
||||||
# уровень1 (2×): тело+след
|
|
||||||
ax1,bx1,ay1,by1=16,118,8,64; P1=patch(Nx,Ny,ax1,bx1,ay1,by1,2)
|
|
||||||
t1=tauc(tau0,2); b1=1/(2*t1); R01=t1/(2*tau0); Rf01=1/R01
|
|
||||||
phi1=sdf_circle(P1["Nfx"],P1["Nfy"],(cx-ax1)*2,(cy-ay1)*2,D); solid1=phi1<0
|
|
||||||
bc1=build_bc(solid1,phi1,solid1); fl1=~solid0[ay1+1:by1,ax1+1:bx1]
|
|
||||||
# уровень2 (4×): у тела
|
|
||||||
cx2a,cx2b,cy2a,cy2b=26,64,18,54
|
|
||||||
P2=patch(P1["Nfx"],P1["Nfy"],(cx2a-ax1)*2,(cx2b-ax1)*2,(cy2a-ay1)*2,(cy2b-ay1)*2,2)
|
|
||||||
t2=tauc(t1,2); b2=1/(2*t2); R12=t2/(2*t1); Rf12=1/R12
|
|
||||||
phi2=sdf_circle(P2["Nfx"],P2["Nfy"],(cx-cx2a)*4,(cy-cy2a)*4,2*D); solid2=phi2<0
|
|
||||||
bc2=build_bc(solid2,phi2,solid2); fl2=np.ones((P2["by"]-P2["ay"]-1,P2["bx"]-P2["ax"]-1),bool)
|
|
||||||
onecol=np.ones((Ny,1))
|
|
||||||
print(f"[SDF] домен {Nx}x{Ny}; L1(2×) {P1['Nfx']}x{P1['Nfy']}; L2(4×) {P2['Nfx']}x{P2['Nfy']}; "
|
|
||||||
f"Re={Re} D0={D0} steps={STEPS}")
|
|
||||||
|
|
||||||
def run(mode): # "none" | "amr2x" | "nested"
|
|
||||||
use1=mode in ("amr2x","nested"); use2=(mode=="nested")
|
|
||||||
f0=feq(np.ones((Ny,Nx)),np.zeros((2,Ny,Nx)))
|
|
||||||
f1=feq(np.ones((P1["Nfy"],P1["Nfx"])),np.zeros((2,P1["Nfy"],P1["Nfx"]))) if use1 else None
|
|
||||||
f2=feq(np.ones((P2["Nfy"],P2["Nfx"])),np.zeros((2,P2["Nfy"],P2["Nfx"]))) if use2 else None
|
|
||||||
Fx=np.zeros(STEPS+1); Fy=np.zeros(STEPS+1); uy=np.zeros(STEPS+1); frames=[]
|
|
||||||
for t in range(STEPS+1):
|
|
||||||
rho,u0=macros(f0)
|
|
||||||
if not np.all(np.isfinite(u0)): print("BLEW UP",mode,t); Fx=Fx[:t];Fy=Fy[:t];uy=uy[:t]; break
|
|
||||||
pre=f0.copy(); post0=collide(f0,feq(rho,u0),b0); f0=stream(post0); f0=apply_bc(f0,post0,bc0)
|
|
||||||
if mode=="none": Fx[t],Fy[t]=force_bc(post0,f0,bc0)
|
|
||||||
uin=np.zeros((2,Ny,1)); uin[0,:,0]=U*smoothstep(t/ramp)
|
|
||||||
f0[:,1:-1,0]=feq(onecol,uin)[:,1:-1,0]; f0[:,1:-1,-1]=f0[:,1:-1,-2]
|
|
||||||
if use1:
|
|
||||||
g1o=ghost(pre,P1,R01); g1n=ghost(f0,P1,R01)
|
|
||||||
for s1 in range(2):
|
|
||||||
pre1=f1.copy(); r1,u1=macros(f1); post1=collide(f1,feq(r1,u1),b1)
|
|
||||||
f1=stream(post1); f1=apply_bc(f1,post1,bc1)
|
|
||||||
if mode=="amr2x" and s1==1: Fx[t],Fy[t]=force_bc(post1,f1,bc1)
|
|
||||||
f1=fill(f1,(1-(s1+1)/2)*g1o+((s1+1)/2)*g1n)
|
|
||||||
if use2:
|
|
||||||
g2o=ghost(pre1,P2,R12); g2n=ghost(f1,P2,R12)
|
|
||||||
for s2 in range(2):
|
|
||||||
pre2=f2.copy(); r2,u2=macros(f2); post2=collide(f2,feq(r2,u2),b2)
|
|
||||||
f2=stream(post2); f2=apply_bc(f2,post2,bc2)
|
|
||||||
if s1==1 and s2==1: Fx[t],Fy[t]=force_bc(post2,f2,bc2)
|
|
||||||
f2=fill(f2,(1-(s2+1)/2)*g2o+((s2+1)/2)*g2n)
|
|
||||||
f1=restrict(f2,f1,P2,Rf12,fl2)
|
|
||||||
f0=restrict(f1,f0,P1,Rf01,fl1)
|
|
||||||
uc=macros(f0)[1]; uy[t]=uc[1,py,px]
|
|
||||||
if use1 and t%FE==0:
|
|
||||||
frames.append((t,uc.copy(),macros(f1)[1].copy(),(macros(f2)[1].copy() if use2 else None)))
|
|
||||||
return dict(mode=mode,Fx=Fx,Fy=Fy,uy=uy,frames=frames,DL={"none":D0,"amr2x":2*D0,"nested":4*D0}[mode])
|
|
||||||
|
|
||||||
def save_gif(res,name,fps=24):
|
|
||||||
nested=(res["mode"]=="nested"); frames=res["frames"]
|
|
||||||
vm=np.nanpercentile(np.abs(np.where(solid0,np.nan,vort(frames[len(frames)//2][1]))),99.0)
|
|
||||||
cm=plt.get_cmap("inferno").copy(); cm.set_bad("white")
|
|
||||||
pil=[]
|
|
||||||
for (t,u0,u1,u2) in frames:
|
|
||||||
fig=plt.figure(figsize=(11.2,5.7),dpi=96); ax=fig.add_subplot(111)
|
|
||||||
ax.imshow(np.abs(np.where(solid0,np.nan,vort(u0))),origin="lower",cmap=cm,vmin=0,vmax=vm,
|
|
||||||
extent=(0,Nx,0,Ny),interpolation="nearest")
|
|
||||||
ax.imshow(np.abs(np.where(solid1,np.nan,vort(u1)))[1:-1,1:-1],origin="lower",cmap=cm,vmin=0,vmax=vm,
|
|
||||||
extent=(ax1+0.5,bx1-0.5,ay1+0.5,by1-0.5),interpolation="nearest")
|
|
||||||
if nested and u2 is not None:
|
|
||||||
ax.imshow(np.abs(np.where(solid2,np.nan,vort(u2)))[1:-1,1:-1],origin="lower",cmap=cm,vmin=0,vmax=vm,
|
|
||||||
extent=(cx2a+0.25,cx2b-0.25,cy2a+0.25,cy2b-0.25),interpolation="nearest")
|
|
||||||
ax.add_patch(mpatches.Rectangle((0.2,0.2),Nx-0.4,Ny-0.4,fill=False,edgecolor="#4fc3f7",lw=1.6))
|
|
||||||
ax.text(1.5,Ny-4,"уровень 0: Δx (крупный)",color="#4fc3f7",fontsize=9,weight="bold")
|
|
||||||
ax.add_patch(mpatches.Rectangle((ax1,ay1),bx1-ax1,by1-ay1,fill=False,edgecolor="#aed581",lw=2.2))
|
|
||||||
ax.text(ax1+1,by1-3.5,"уровень 1: Δx/2 (тело+след)",color="#aed581",fontsize=9,weight="bold")
|
|
||||||
if nested:
|
|
||||||
ax.add_patch(mpatches.Rectangle((cx2a,cy2a),cx2b-cx2a,cy2b-cy2a,fill=False,edgecolor="#ff8a65",lw=2.2))
|
|
||||||
ax.text(cx2a+1,cy2b-3.5,"уровень 2: Δx/4 (у тела)",color="#ff8a65",fontsize=9,weight="bold")
|
|
||||||
ttl="Вложенный AMR + SDF: 2×(след)+4×(тело)" if nested else "AMR + SDF: одиночный блок 2×"
|
|
||||||
ax.set_title(f"{ttl} · граница тела по SDF (Bouzidi) · поверх $|\\omega|$ · t={t}",fontsize=11)
|
|
||||||
ax.set_xlabel("x [lu]"); ax.set_ylabel("y [lu]"); ax.set_xlim(0,Nx); ax.set_ylim(0,Ny)
|
|
||||||
buf=_io.BytesIO(); fig.savefig(buf,format="png",bbox_inches="tight"); buf.seek(0); plt.close(fig)
|
|
||||||
pil.append(Image.open(buf).convert("RGB").convert("P",palette=Image.ADAPTIVE))
|
|
||||||
pil[0].save(os.path.join(ANIM,name),save_all=True,append_images=pil[1:],duration=int(1000/fps),loop=0,optimize=True)
|
|
||||||
print("saved",name,len(frames),"кадров")
|
|
||||||
|
|
||||||
def analyze(res):
|
|
||||||
Fx,Fy,uy,DL=res["Fx"],res["Fy"],res["uy"],res["DL"]
|
|
||||||
n=len(uy); h=slice(n//2,n); Cd=2*Fx/(U**2*DL); Cl=2*Fy/(U**2*DL)
|
|
||||||
sig=uy[h]-uy[h].mean(); npad=8192
|
|
||||||
freqs=np.fft.rfftfreq(npad,1.0); amp=np.abs(np.fft.rfft(sig,n=npad))
|
|
||||||
St=(freqs[1+int(np.argmax(amp[1:]))] if len(amp)>2 else 0.0)*D0/U
|
|
||||||
return dict(Cd=float(Cd[h].mean()),Clrms=float(Cl[h].std()),St=float(St),uyrms=float(uy[h].std()),
|
|
||||||
Cl_s=Cl,uy=uy,freqs=freqs,amp=amp,h0=n//2)
|
|
||||||
|
|
||||||
print("=== SDF none ==="); R0=run("none")
|
|
||||||
print("=== SDF amr2x ==="); R1=run("amr2x")
|
|
||||||
print("=== SDF nested ==="); R2=run("nested")
|
|
||||||
save_gif(R1,"amr2x_cyl_sdf.gif"); save_gif(R2,"amr_nested_cyl_sdf.gif")
|
|
||||||
AS=[analyze(R0),analyze(R1),analyze(R2)]; LAB=["без AMR (D=16)","AMR 2× (D=32)","AMR 2×+4× (D=64)"]
|
|
||||||
print("\n=== SDF: СРАВНЕНИЕ НА СОБРАННЫХ ДАННЫХ ===")
|
|
||||||
print(f"{'версия':22}{'D у тела':>9}{'St':>8}{'<Cd>':>9}{'rms Cl':>9}{'rms u_y':>9}")
|
|
||||||
for lab,DL,a in zip(LAB,[16,32,64],AS):
|
|
||||||
print(f"{lab:22}{DL:>9}{a['St']:>8.3f}{a['Cd']:>9.3f}{a['Clrms']:>9.4f}{a['uyrms']:>9.4f}")
|
|
||||||
np.savez(os.path.join(HERE,"_amr_probe_data_sdf.npz"),
|
|
||||||
St=np.array([a['St'] for a in AS]),Cd=np.array([a['Cd'] for a in AS]),
|
|
||||||
Clrms=np.array([a['Clrms'] for a in AS]),uyrms=np.array([a['uyrms'] for a in AS]))
|
|
||||||
|
|
||||||
# ===== сводное сравнение SDF vs без SDF (если есть данные без SDF) =====
|
|
||||||
nosdf_path=os.path.join(HERE,"_amr_probe_data.npz")
|
|
||||||
if os.path.exists(nosdf_path) and ("St" in np.load(nosdf_path).files):
|
|
||||||
N=np.load(nosdf_path); St_n,Cd_n,Cl_n=N["St"],N["Cd"],N["Clrms"]
|
|
||||||
St_s=np.array([a['St'] for a in AS]); Cd_s=np.array([a['Cd'] for a in AS]); Cl_s=np.array([a['Clrms'] for a in AS])
|
|
||||||
print("\n=== СВОДНО: SDF vs без SDF ===")
|
|
||||||
print(f"{'версия':14}{'St(noSDF)':>11}{'St(SDF)':>9}{'Cd(noSDF)':>11}{'Cd(SDF)':>9}")
|
|
||||||
for k,lab in enumerate(["без AMR","2×","2×+4×"]):
|
|
||||||
print(f"{lab:14}{St_n[k]:>11.3f}{St_s[k]:>9.3f}{Cd_n[k]:>11.3f}{Cd_s[k]:>9.3f}")
|
|
||||||
print(f"{'лит. Re≈150':14}{0.183:>11.3f}{0.183:>9.3f}{1.330:>11.3f}{1.330:>9.3f}")
|
|
||||||
x=np.arange(3); w=0.38
|
|
||||||
fig,axs=plt.subplots(1,2,figsize=(13.5,4.6))
|
|
||||||
axs[0].bar(x-w/2,Cd_n,w,color="#9e9e9e",label="без SDF (ступенч.)")
|
|
||||||
axs[0].bar(x+w/2,Cd_s,w,color="#1f6feb",label="с SDF (Bouzidi)")
|
|
||||||
axs[0].axhline(1.33,color="k",ls="--",alpha=0.6,label="лит. Cd≈1.33")
|
|
||||||
axs[0].set_xticks(x); axs[0].set_xticklabels(["без AMR","2×","2×+4×"]); axs[0].set_ylabel("<Cd>")
|
|
||||||
axs[0].set_title("Сопротивление: SDF убирает завышение от ступенчатой границы"); axs[0].legend(fontsize=8); axs[0].grid(alpha=0.3,axis="y")
|
|
||||||
axs[1].bar(x-w/2,St_n,w,color="#9e9e9e",label="без SDF")
|
|
||||||
axs[1].bar(x+w/2,St_s,w,color="#1f6feb",label="с SDF")
|
|
||||||
axs[1].axhline(0.183,color="k",ls="--",alpha=0.6,label="лит. St≈0.18")
|
|
||||||
axs[1].set_xticks(x); axs[1].set_xticklabels(["без AMR","2×","2×+4×"]); axs[1].set_ylabel("St")
|
|
||||||
axs[1].set_title("Число Струхаля"); axs[1].legend(fontsize=8); axs[1].grid(alpha=0.3,axis="y")
|
|
||||||
fig.suptitle("Цилиндр: SDF (Bouzidi) против ступенчатого bounce-back, на реально собранных данных",fontweight="bold")
|
|
||||||
fig.savefig(os.path.join(FIGS,"11f_sdf_vs_nosdf.png"),dpi=110,bbox_inches="tight"); plt.close(fig)
|
|
||||||
print("saved figures/11f_sdf_vs_nosdf.png")
|
|
||||||
else:
|
|
||||||
print("\n(нет свежего _amr_probe_data.npz со скалярами — сначала запусти _amr_anims.py)")
|
|
||||||
print("DONE")
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# ОСНОВНОЙ GPU-решатель (без SDF): цилиндр, три версии (без AMR / 2× / вложенный 2×+4×).
|
|
||||||
# Математика идентична CPU-прототипу _amr_anims.py; backend — CuPy (GPU) с откатом на numpy.
|
|
||||||
# Запуск: python amr_cylinder_gpu.py (fp32 по умолчанию)
|
|
||||||
# AMR_FP64=1 python amr_cylinder_gpu.py (двойная точность)
|
|
||||||
# AMR_STEPS=2000 python amr_cylinder_gpu.py (короткий прогон)
|
|
||||||
import os, time, numpy as np
|
|
||||||
import amr_gpu_core as core
|
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ANIM = os.path.join(HERE, "anim"); os.makedirs(ANIM, exist_ok=True)
|
|
||||||
FIGS = os.path.join(HERE, "figures"); os.makedirs(FIGS, exist_ok=True)
|
|
||||||
print(f"[ОСНОВНОЙ · без SDF] backend={core.BACKEND}; домен {core.Nx}x{core.Ny}; steps={core.STEPS}; "
|
|
||||||
f"L1(2×) {core.P1['Nfx']}x{core.P1['Nfy']}; L2(4×) {core.P2['Nfx']}x{core.P2['Nfy']}")
|
|
||||||
|
|
||||||
t0 = time.perf_counter()
|
|
||||||
R0 = core.run("none", False); print(" none :", round(time.perf_counter()-t0, 1), "s")
|
|
||||||
R1 = core.run("amr2x", False); print(" amr2x :", round(time.perf_counter()-t0, 1), "s")
|
|
||||||
R2 = core.run("nested", False); print(" nested :", round(time.perf_counter()-t0, 1), "s")
|
|
||||||
|
|
||||||
core.save_gif(R1, "amr2x_cyl.gif", ANIM, " (без SDF)")
|
|
||||||
core.save_gif(R2, "amr_nested_cyl.gif", ANIM, " (без SDF)")
|
|
||||||
|
|
||||||
AS = [core.analyze(R0), core.analyze(R1), core.analyze(R2)]
|
|
||||||
core.print_table(AS, "БЕЗ SDF — сравнение на реально собранных данных (установившийся режим)")
|
|
||||||
np.savez(os.path.join(HERE, "_amr_probe_data.npz"),
|
|
||||||
St=np.array([a["St"] for a in AS]), Cd=np.array([a["Cd"] for a in AS]),
|
|
||||||
Clrms=np.array([a["Clrms"] for a in AS]), uyrms=np.array([a["uyrms"] for a in AS]),
|
|
||||||
Cl0=AS[0]["Cl_s"], Cl1=AS[1]["Cl_s"], Cl2=AS[2]["Cl_s"],
|
|
||||||
uy0=AS[0]["uy"], uy1=AS[1]["uy"], uy2=AS[2]["uy"])
|
|
||||||
core.probe_figure(AS, R2, os.path.join(FIGS, "11e_amr_probe_comparison.png"),
|
|
||||||
"AMR на цилиндре (без SDF): сравнение трёх версий на данных зондов")
|
|
||||||
print("DONE (без SDF)")
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
# ОСНОВНОЙ GPU-решатель (с SDF + Bouzidi): цилиндр, три версии (без AMR / 2× / 2×+4×).
|
|
||||||
# Граница тела — по знаковому полю расстояний + интерполированное отражение (Bouzidi);
|
|
||||||
# стенки канала — тот же механизм с q=0.5 (обычный halfway). Математика идентична _amr_anims_sdf.py.
|
|
||||||
# backend — CuPy (GPU) с откатом на numpy. В конце — сводное сравнение с версией без SDF.
|
|
||||||
# Запуск: python amr_cylinder_sdf_gpu.py (после amr_cylinder_gpu.py — для сводного сравнения)
|
|
||||||
import os, time, numpy as np
|
|
||||||
import amr_gpu_core as core
|
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ANIM = os.path.join(HERE, "anim"); os.makedirs(ANIM, exist_ok=True)
|
|
||||||
FIGS = os.path.join(HERE, "figures"); os.makedirs(FIGS, exist_ok=True)
|
|
||||||
print(f"[ОСНОВНОЙ · SDF+Bouzidi] backend={core.BACKEND}; домен {core.Nx}x{core.Ny}; steps={core.STEPS}; "
|
|
||||||
f"L1(2×) {core.P1['Nfx']}x{core.P1['Nfy']}; L2(4×) {core.P2['Nfx']}x{core.P2['Nfy']}")
|
|
||||||
|
|
||||||
def make_progress(label, width=32, every=0.1):
|
|
||||||
"""Колбэк-индикатор: рисует ASCII-шкалу по выполненным шагам (t из STEPS), обновляется на месте."""
|
|
||||||
st = {"t0": time.perf_counter(), "last": 0.0}
|
|
||||||
def cb(t, total):
|
|
||||||
now = time.perf_counter(); done = (t >= total)
|
|
||||||
if not done and (now - st["last"] < every): # троттлинг, чтобы не спамить консоль
|
|
||||||
return
|
|
||||||
st["last"] = now; frac = (t + 1) / (total + 1)
|
|
||||||
filled = int(round(width * frac)); bar = "#" * filled + "-" * (width - filled)
|
|
||||||
el = now - st["t0"]; eta = (el / frac - el) if frac > 0 else 0.0
|
|
||||||
print(f"\r {label:7}[{bar}] {frac*100:5.1f}% ({t}/{total}) {el:5.1f}s ETA {eta:5.1f}s",
|
|
||||||
end=("\n" if done else ""), flush=True)
|
|
||||||
return cb
|
|
||||||
|
|
||||||
t0 = time.perf_counter()
|
|
||||||
R0 = core.run("none", True, make_progress("none")); print(" none :", round(time.perf_counter()-t0, 1), "s")
|
|
||||||
R1 = core.run("amr2x", True, make_progress("amr2x")); print(" amr2x :", round(time.perf_counter()-t0, 1), "s")
|
|
||||||
R2 = core.run("nested", True, make_progress("nested")); print(" nested :", round(time.perf_counter()-t0, 1), "s")
|
|
||||||
|
|
||||||
core.save_gif(R1, "amr2x_cyl_sdf.gif", ANIM, " + SDF (Bouzidi)")
|
|
||||||
core.save_gif(R2, "amr_nested_cyl_sdf.gif", ANIM, " + SDF (Bouzidi)")
|
|
||||||
|
|
||||||
AS = [core.analyze(R0), core.analyze(R1), core.analyze(R2)]
|
|
||||||
core.print_table(AS, "С SDF — сравнение на реально собранных данных (установившийся режим)")
|
|
||||||
np.savez(os.path.join(HERE, "_amr_probe_data_sdf.npz"),
|
|
||||||
St=np.array([a["St"] for a in AS]), Cd=np.array([a["Cd"] for a in AS]),
|
|
||||||
Clrms=np.array([a["Clrms"] for a in AS]), uyrms=np.array([a["uyrms"] for a in AS]),
|
|
||||||
Cl0=AS[0]["Cl_s"], Cl1=AS[1]["Cl_s"], Cl2=AS[2]["Cl_s"],
|
|
||||||
uy0=AS[0]["uy"], uy1=AS[1]["uy"], uy2=AS[2]["uy"])
|
|
||||||
core.probe_figure(AS, R2, os.path.join(FIGS, "11f_amr_sdf_probe_comparison.png"),
|
|
||||||
"AMR на цилиндре (SDF+Bouzidi): сравнение трёх версий на данных зондов")
|
|
||||||
|
|
||||||
# ---- сводное сравнение: SDF против без-SDF (если есть свежие данные без SDF) ----
|
|
||||||
nosdf = os.path.join(HERE, "_amr_probe_data.npz")
|
|
||||||
if os.path.exists(nosdf) and ("St" in np.load(nosdf).files):
|
|
||||||
import matplotlib; matplotlib.use("Agg"); import matplotlib.pyplot as plt
|
|
||||||
N = np.load(nosdf); Stn, Cdn = N["St"], N["Cd"]
|
|
||||||
Sts = np.array([a["St"] for a in AS]); Cds = np.array([a["Cd"] for a in AS])
|
|
||||||
print("\n=== СВОДНО: SDF vs без SDF (Cd, St) ===")
|
|
||||||
print(f"{'версия':14}{'St(noSDF)':>11}{'St(SDF)':>9}{'Cd(noSDF)':>11}{'Cd(SDF)':>9}")
|
|
||||||
for k, lab in enumerate(["без AMR", "2×", "2×+4×"]):
|
|
||||||
print(f"{lab:14}{Stn[k]:>11.3f}{Sts[k]:>9.3f}{Cdn[k]:>11.3f}{Cds[k]:>9.3f}")
|
|
||||||
print(f"{'лит. Re≈150':14}{0.183:>11.3f}{0.183:>9.3f}{1.330:>11.3f}{1.330:>9.3f}")
|
|
||||||
x = np.arange(3); w = 0.38
|
|
||||||
fig, axs = plt.subplots(1, 2, figsize=(13.5, 4.6))
|
|
||||||
axs[0].bar(x-w/2, Cdn, w, color="#9e9e9e", label="без SDF (ступенч.)")
|
|
||||||
axs[0].bar(x+w/2, Cds, w, color="#1f6feb", label="с SDF (Bouzidi)")
|
|
||||||
axs[0].axhline(1.33, color="k", ls="--", alpha=0.6, label="лит. Cd≈1.33")
|
|
||||||
axs[0].set_xticks(x); axs[0].set_xticklabels(["без AMR", "2×", "2×+4×"]); axs[0].set_ylabel("<Cd>")
|
|
||||||
axs[0].set_title("Сопротивление: SDF убирает завышение от ступенчатой границы")
|
|
||||||
axs[0].legend(fontsize=8); axs[0].grid(alpha=0.3, axis="y")
|
|
||||||
axs[1].bar(x-w/2, Stn, w, color="#9e9e9e", label="без SDF"); axs[1].bar(x+w/2, Sts, w, color="#1f6feb", label="с SDF")
|
|
||||||
axs[1].axhline(0.183, color="k", ls="--", alpha=0.6, label="лит. St≈0.18")
|
|
||||||
axs[1].set_xticks(x); axs[1].set_xticklabels(["без AMR", "2×", "2×+4×"]); axs[1].set_ylabel("St")
|
|
||||||
axs[1].set_title("Число Струхаля"); axs[1].legend(fontsize=8); axs[1].grid(alpha=0.3, axis="y")
|
|
||||||
fig.suptitle("Цилиндр: SDF (Bouzidi) против ступенчатого bounce-back — на реальных данных", fontweight="bold")
|
|
||||||
fig.savefig(os.path.join(FIGS, "11g_sdf_vs_nosdf.png"), dpi=110, bbox_inches="tight"); plt.close(fig)
|
|
||||||
print("saved 11g_sdf_vs_nosdf.png")
|
|
||||||
else:
|
|
||||||
print("\n(нет свежего _amr_probe_data.npz — сначала запусти amr_cylinder_gpu.py)")
|
|
||||||
print("DONE (SDF)")
|
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
# Общее GPU-ядро для основных AMR-решателей (D2Q9 KBC) на цилиндре.
|
|
||||||
# Математика ДОСЛОВНО та же, что в проверенных CPU-прототипах (_amr_anims*.py):
|
|
||||||
# - энтропийное равновесие (product-form), KBC-N1 столкновение f<-f-β(2Δs+γΔh),
|
|
||||||
# - AMR-связка τ_f=2τ_c-½, R_cf=τ_f/(2τ_c), m подшагов, временна́я интерполяция,
|
|
||||||
# - сила по Mei (обмен импульсом), вход/выход/стенки, опционально SDF+Bouzidi.
|
|
||||||
# Оптимизация под GPU (CuPy): всё векторно на device; проектор через matmul; ГУ и силы
|
|
||||||
# через where (без gather/scatter); БЕЗ пер-шаговых синхронизаций host↔device.
|
|
||||||
import os, io as _io, numpy as np
|
|
||||||
|
|
||||||
# ---- backend: CuPy (GPU) с откатом на numpy (CPU) ----
|
|
||||||
try:
|
|
||||||
import cupy as cp
|
|
||||||
_ = (cp.zeros(2) + 1).sum() # реальная операция на device — ловит "cupy есть, GPU нет"
|
|
||||||
xp = cp; GPU = True
|
|
||||||
except Exception:
|
|
||||||
import numpy as cp # noqa
|
|
||||||
xp = np; GPU = False
|
|
||||||
def to_cpu(a):
|
|
||||||
return cp.asnumpy(a) if GPU else np.asarray(a)
|
|
||||||
DTYPE = xp.float64 if os.environ.get("AMR_FP64") else xp.float32
|
|
||||||
GREL = 1e-8 # ОТНОСИТЕЛЬНЫЙ порог вырожденности знаменателя γ (доля от ‖Δ‖²); см. solver_2x_sdf/backend.py.
|
|
||||||
# Прежний абсолютный порог (1e-6 в fp32) срабатывал на большинстве узлов и подменял KBC на LBGK.
|
|
||||||
BACKEND = f"{'CuPy/GPU' if GPU else 'numpy/CPU'} dtype={'float64' if DTYPE == xp.float64 else 'float32'}"
|
|
||||||
|
|
||||||
# ---- решётка D2Q9 ----
|
|
||||||
Cx = [0, 1, 0, -1, 0, 1, -1, -1, 1] # python-инты для roll/индексации
|
|
||||||
Cy = [0, 0, 1, 0, -1, 1, 1, -1, -1]
|
|
||||||
OPP = [0, 3, 4, 1, 2, 7, 8, 5, 6]
|
|
||||||
Q = 9
|
|
||||||
_W = np.array([4/9, 1/9, 1/9, 1/9, 1/9, 1/36, 1/36, 1/36, 1/36])
|
|
||||||
_CXn = np.array(Cx, float); _CYn = np.array(Cy, float)
|
|
||||||
# моментный базис и проектор на сдвиг {cx²−cy², cxcy} (KBC-N1) — считаем в numpy, грузим на device
|
|
||||||
_M = np.zeros((Q, Q)); _M[0] = 1; _M[1] = _CXn; _M[2] = _CYn; _M[3] = 3*(_CXn**2+_CYn**2)-2
|
|
||||||
_M[4] = _CXn**2-_CYn**2; _M[5] = _CXn*_CYn; _M[6] = _CXn**2*_CYn; _M[7] = _CXn*_CYn**2; _M[8] = _CXn**2*_CYn**2
|
|
||||||
_Dm = np.zeros((Q, Q)); _Dm[4, 4] = _Dm[5, 5] = 1.0
|
|
||||||
_Psn = np.linalg.inv(_M) @ _Dm @ _M
|
|
||||||
# device-константы
|
|
||||||
Wa = xp.asarray(_W, dtype=DTYPE)
|
|
||||||
CXa = xp.asarray(_CXn, dtype=DTYPE); CYa = xp.asarray(_CYn, dtype=DTYPE)
|
|
||||||
Ps = xp.asarray(_Psn, dtype=DTYPE)
|
|
||||||
CS2 = 1.0/3.0
|
|
||||||
|
|
||||||
# ---- ядро LBM/KBC (идентичная математика) ----
|
|
||||||
def feq(rho, u):
|
|
||||||
ux = xp.clip(u[0], -0.95, 0.95); uy = xp.clip(u[1], -0.95, 0.95)
|
|
||||||
sx = xp.sqrt(1 + 3*ux*ux); sy = xp.sqrt(1 + 3*uy*uy); base = rho*(2-sx)*(2-sy)
|
|
||||||
Bx = ((2*ux+sx)/(1-ux))[None]**CXa[:, None, None]
|
|
||||||
By = ((2*uy+sy)/(1-uy))[None]**CYa[:, None, None]
|
|
||||||
return Wa[:, None, None]*base[None]*Bx*By
|
|
||||||
def macros(f):
|
|
||||||
r = f.sum(0)
|
|
||||||
return r, xp.stack([(CXa[:, None, None]*f).sum(0)/r, (CYa[:, None, None]*f).sum(0)/r])
|
|
||||||
def collide(f, fe, beta):
|
|
||||||
dfn = f - fe
|
|
||||||
ds = (Ps @ dfn.reshape(Q, -1)).reshape(dfn.shape) # проектор на сдвиг (matmul, GPU-friendly)
|
|
||||||
dh = dfn - ds; inv = 1.0/fe
|
|
||||||
num = (ds*dh*inv).sum(0); den = (dh*dh*inv).sum(0)
|
|
||||||
nrm = (dfn*dfn*inv).sum(0) # ‖Δ‖² — масштаб неравновесия узла
|
|
||||||
ok = den > GREL*nrm
|
|
||||||
den_safe = xp.where(ok, den, xp.asarray(1.0, DTYPE)) # избегаем 0/0 (обе ветки where считаются)
|
|
||||||
g = xp.where(ok, 1/beta - (2 - 1/beta)*num/den_safe, xp.asarray(2.0, DTYPE))
|
|
||||||
return f - beta*(2*ds + g[None]*dh)
|
|
||||||
def stream(f):
|
|
||||||
fs = xp.empty_like(f)
|
|
||||||
for i in range(Q):
|
|
||||||
fs[i] = xp.roll(f[i], (Cy[i], Cx[i]), (0, 1))
|
|
||||||
return fs
|
|
||||||
|
|
||||||
# ---- AMR-связка ----
|
|
||||||
def patch(pNx, pNy, ax, bx, ay, by, r):
|
|
||||||
Wx, Wy = bx-ax, by-ay; Nfx, Nfy = r*Wx+1, r*Wy+1
|
|
||||||
FX, FY = np.meshgrid(ax+np.arange(Nfx)/r, ay+np.arange(Nfy)/r)
|
|
||||||
x0 = np.floor(FX).astype(np.int64); y0 = np.floor(FY).astype(np.int64)
|
|
||||||
x1 = np.minimum(x0+1, pNx-1); y1 = np.minimum(y0+1, pNy-1)
|
|
||||||
return dict(Nfx=Nfx, Nfy=Nfy, ax=ax, bx=bx, ay=ay, by=by, r=r,
|
|
||||||
x0=xp.asarray(x0), y0=xp.asarray(y0), x1=xp.asarray(x1), y1=xp.asarray(y1),
|
|
||||||
tx=xp.asarray(FX-x0, DTYPE), ty=xp.asarray(FY-y0, DTYPE),
|
|
||||||
slx=slice(r, r*Wx, r), sly=slice(r, r*Wy, r))
|
|
||||||
def pint(fld, P):
|
|
||||||
return (fld[..., P["y0"], P["x0"]]*(1-P["tx"])*(1-P["ty"]) + fld[..., P["y0"], P["x1"]]*P["tx"]*(1-P["ty"])
|
|
||||||
+ fld[..., P["y1"], P["x0"]]*(1-P["tx"])*P["ty"] + fld[..., P["y1"], P["x1"]]*P["tx"]*P["ty"])
|
|
||||||
def ghost(pf, P, Rcf):
|
|
||||||
r, u = macros(pf); neq = pf - feq(r, u)
|
|
||||||
return feq(pint(r, P), xp.stack([pint(u[0], P), pint(u[1], P)])) + Rcf*pint(neq, P)
|
|
||||||
def fill(cf, gh):
|
|
||||||
cf[:, 0, :] = gh[:, 0, :]; cf[:, -1, :] = gh[:, -1, :]; cf[:, :, 0] = gh[:, :, 0]; cf[:, :, -1] = gh[:, :, -1]
|
|
||||||
return cf
|
|
||||||
def restrict(cf, pf, P, Rfc, fluid):
|
|
||||||
r, u = macros(cf); neq = cf - feq(r, u); slx, sly = P["slx"], P["sly"]
|
|
||||||
nv = feq(r[sly, slx], u[:, sly, slx]) + Rfc*neq[:, sly, slx]
|
|
||||||
cur = pf[:, P["ay"]+1:P["by"], P["ax"]+1:P["bx"]]
|
|
||||||
pf[:, P["ay"]+1:P["by"], P["ax"]+1:P["bx"]] = xp.where(fluid[None], nv, cur)
|
|
||||||
return pf
|
|
||||||
|
|
||||||
# ---- граница: (1) узловой bounce-back (без SDF); (2) SDF+Bouzidi ----
|
|
||||||
def bb_set(post, pre, solid): # узлы тела <- развёрнутые до-столкновения (как в CPU-версии)
|
|
||||||
for i in range(Q):
|
|
||||||
post[i] = xp.where(solid, pre[OPP[i]], post[i])
|
|
||||||
return post
|
|
||||||
def force_on(fpost, cyl, fluid): # Mei для узловой границы: F=Σ c_i(f_i+f_ī) по линкам жидк.->тело
|
|
||||||
Fx = 0.0; Fy = 0.0
|
|
||||||
for i in range(1, Q):
|
|
||||||
nb = xp.roll(cyl, (-Cy[i], -Cx[i]), (0, 1)); link = fluid & nb
|
|
||||||
s = xp.where(link, fpost[i] + fpost[OPP[i]], xp.asarray(0.0, DTYPE)).sum()
|
|
||||||
Fx = Fx + Cx[i]*s; Fy = Fy + Cy[i]*s
|
|
||||||
return Fx, Fy
|
|
||||||
def build_bc(solid_np, phi_np, cyl_np): # SDF-границу строим на host один раз, переносим на device
|
|
||||||
fluid = ~solid_np; bc = []
|
|
||||||
for i in range(1, Q):
|
|
||||||
nb_solid = np.roll(solid_np, (-Cy[i], -Cx[i]), (0, 1)); mask = fluid & nb_solid
|
|
||||||
cl = mask & np.roll(cyl_np, (-Cy[i], -Cx[i]), (0, 1))
|
|
||||||
phinb = np.roll(phi_np, (-Cy[i], -Cx[i]), (0, 1))
|
|
||||||
with np.errstate(divide="ignore", invalid="ignore"):
|
|
||||||
qq = phi_np/(phi_np - phinb)
|
|
||||||
q = np.where(mask, np.clip(qq, 0.02, 0.98), 0.5)
|
|
||||||
xff = mask & (~np.roll(solid_np, (Cy[i], Cx[i]), (0, 1)))
|
|
||||||
bc.append((i, OPP[i], xp.asarray(mask), xp.asarray(q, DTYPE), xp.asarray(xff), xp.asarray(cl)))
|
|
||||||
return bc
|
|
||||||
def apply_bc(f, fpost, bc): # Bouzidi (линейный), полностью через where (GPU)
|
|
||||||
for (i, ib, mask, q, xff, cl) in bc:
|
|
||||||
fi = fpost[i]; fib = fpost[ib]; fiback = xp.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 force_bc(fpost, f, bc): # Mei для интерполированной границы
|
|
||||||
Fx = 0.0; Fy = 0.0
|
|
||||||
for (i, ib, mask, q, xff, cl) in bc:
|
|
||||||
s = xp.where(cl, fpost[i] + f[ib], xp.asarray(0.0, DTYPE)).sum()
|
|
||||||
Fx = Fx + Cx[i]*s; Fy = Fy + Cy[i]*s
|
|
||||||
return Fx, Fy
|
|
||||||
|
|
||||||
# ---- геометрия (ИДЕНТИЧНА CPU-версиям) ----
|
|
||||||
Nx, Ny, D, cx, cy = 150, 72, 16, 40, 36
|
|
||||||
U, Re, STEPS, ramp, FE = 0.07, 150.0, int(os.environ.get("AMR_STEPS", 8000)), 1000, 34
|
|
||||||
D0 = D; nu = U*D/Re; tau0 = nu/CS2 + 0.5
|
|
||||||
ax1, bx1, ay1, by1 = 16, 118, 8, 64
|
|
||||||
cx2a, cx2b, cy2a, cy2b = 26, 64, 18, 54
|
|
||||||
px, py = cx + 3*D, cy
|
|
||||||
def _cmask(NX, NY, ccx, ccy, R):
|
|
||||||
Y, X = np.meshgrid(np.arange(NY), np.arange(NX), indexing="ij"); return (X-ccx)**2 + (Y-ccy)**2 <= R*R
|
|
||||||
def _csdf(NX, NY, ccx, ccy, R):
|
|
||||||
Y, X = np.meshgrid(np.arange(NY), np.arange(NX), indexing="ij"); return np.sqrt((X-ccx)**2.0 + (Y-ccy)**2.0) - R
|
|
||||||
# host-маски/SDF
|
|
||||||
_walls = np.zeros((Ny, Nx), bool); _walls[0, :] = True; _walls[-1, :] = True
|
|
||||||
cyl0_np = _cmask(Nx, Ny, cx, cy, D/2); solid0_np = cyl0_np | _walls
|
|
||||||
_Yg = np.arange(Ny)[:, None]*np.ones((1, Nx))
|
|
||||||
phi0_np = np.minimum(_csdf(Nx, Ny, cx, cy, D/2), np.minimum(_Yg-0.5, (Ny-1.5)-_Yg))
|
|
||||||
P1 = patch(Nx, Ny, ax1, bx1, ay1, by1, 2); P2 = patch(P1["Nfx"], P1["Nfy"], (cx2a-ax1)*2, (cx2b-ax1)*2, (cy2a-ay1)*2, (cy2b-ay1)*2, 2)
|
|
||||||
solid1_np = _cmask(P1["Nfx"], P1["Nfy"], (cx-ax1)*2, (cy-ay1)*2, D); phi1_np = _csdf(P1["Nfx"], P1["Nfy"], (cx-ax1)*2, (cy-ay1)*2, D)
|
|
||||||
solid2_np = _cmask(P2["Nfx"], P2["Nfy"], (cx-cx2a)*4, (cy-cy2a)*4, 2*D); phi2_np = _csdf(P2["Nfx"], P2["Nfy"], (cx-cx2a)*4, (cy-cy2a)*4, 2*D)
|
|
||||||
# device-маски (без SDF)
|
|
||||||
g_solid0 = xp.asarray(solid0_np); g_cyl0 = xp.asarray(cyl0_np); g_fl0 = ~g_solid0
|
|
||||||
g_solid1 = xp.asarray(solid1_np); g_fl1 = ~g_solid1
|
|
||||||
g_solid2 = xp.asarray(solid2_np); g_fl2 = ~g_solid2
|
|
||||||
# SDF-ГУ
|
|
||||||
BC0 = build_bc(solid0_np, phi0_np, cyl0_np); BC1 = build_bc(solid1_np, phi1_np, solid1_np); BC2 = build_bc(solid2_np, phi2_np, solid2_np)
|
|
||||||
# рестрикция: жидкие узлы
|
|
||||||
fl1 = xp.asarray(~solid0_np[ay1+1:by1, ax1+1:bx1])
|
|
||||||
fl2 = xp.ones((P2["by"]-P2["ay"]-1, P2["bx"]-P2["ax"]-1), bool); fl2 = xp.asarray(fl2)
|
|
||||||
# τ / масштабы
|
|
||||||
t1 = 2*tau0 - 0.5; t2 = 2*t1 - 0.5
|
|
||||||
b0 = 1/(2*tau0); b1 = 1/(2*t1); b2 = 1/(2*t2)
|
|
||||||
R01 = t1/(2*tau0); Rf01 = 1/R01; R12 = t2/(2*t1); Rf12 = 1/R12
|
|
||||||
|
|
||||||
def smoothstep(x):
|
|
||||||
x = min(max(x, 0.0), 1.0); return x*x*(3-2*x)
|
|
||||||
|
|
||||||
def run(mode, use_sdf, progress=None):
|
|
||||||
"""mode: none|amr2x|nested. Возвращает {Fx,Fy,uy (на host), frames (host), DL, mode}.
|
|
||||||
progress(t, STEPS) — необязательный колбэк прогресса (вызывается каждый шаг)."""
|
|
||||||
use1 = mode in ("amr2x", "nested"); use2 = (mode == "nested")
|
|
||||||
DL = {"none": D0, "amr2x": 2*D0, "nested": 4*D0}[mode]
|
|
||||||
f0 = feq(xp.ones((Ny, Nx), DTYPE), xp.zeros((2, Ny, Nx), DTYPE))
|
|
||||||
f1 = feq(xp.ones((P1["Nfy"], P1["Nfx"]), DTYPE), xp.zeros((2, P1["Nfy"], P1["Nfx"]), DTYPE)) if use1 else None
|
|
||||||
f2 = feq(xp.ones((P2["Nfy"], P2["Nfx"]), DTYPE), xp.zeros((2, P2["Nfy"], P2["Nfx"]), DTYPE)) if use2 else None
|
|
||||||
Fx = xp.zeros(STEPS+1, DTYPE); Fy = xp.zeros(STEPS+1, DTYPE); uy = xp.zeros(STEPS+1, DTYPE)
|
|
||||||
onecol = xp.ones((Ny, 1), DTYPE); frames = []
|
|
||||||
for t in range(STEPS+1):
|
|
||||||
rho, u0 = macros(f0)
|
|
||||||
if t % 500 == 0 and not bool(xp.isfinite(rho).all()):
|
|
||||||
print("BLEW UP", mode, t); Fx = Fx[:t]; Fy = Fy[:t]; uy = uy[:t]; break
|
|
||||||
pre = f0.copy(); post0 = collide(f0, feq(rho, u0), b0)
|
|
||||||
if not use_sdf:
|
|
||||||
post0b = bb_set(post0.copy(), pre, g_solid0); f0 = stream(post0b)
|
|
||||||
else:
|
|
||||||
f0 = stream(post0); f0 = apply_bc(f0, post0, BC0)
|
|
||||||
if mode == "none":
|
|
||||||
Fx[t], Fy[t] = (force_bc(post0, f0, BC0) if use_sdf else force_on(post0, g_cyl0, g_fl0))
|
|
||||||
uin = xp.zeros((2, Ny, 1), DTYPE); uin[0, :, 0] = U*smoothstep(t/ramp)
|
|
||||||
f0[:, 1:-1, 0] = feq(onecol, uin)[:, 1:-1, 0]; f0[:, 1:-1, -1] = f0[:, 1:-1, -2]
|
|
||||||
if use1:
|
|
||||||
g1o = ghost(pre, P1, R01); g1n = ghost(f0, P1, R01)
|
|
||||||
for s1 in range(2):
|
|
||||||
pre1 = f1.copy(); r1, u1 = macros(f1); post1 = collide(f1, feq(r1, u1), b1)
|
|
||||||
if not use_sdf:
|
|
||||||
f1 = stream(bb_set(post1.copy(), pre1, g_solid1))
|
|
||||||
else:
|
|
||||||
f1 = stream(post1); f1 = apply_bc(f1, post1, BC1)
|
|
||||||
if mode == "amr2x" and s1 == 1:
|
|
||||||
Fx[t], Fy[t] = (force_bc(post1, f1, BC1) if use_sdf else force_on(post1, g_solid1, g_fl1))
|
|
||||||
f1 = fill(f1, (1-(s1+1)/2)*g1o + ((s1+1)/2)*g1n)
|
|
||||||
if use2:
|
|
||||||
g2o = ghost(pre1, P2, R12); g2n = ghost(f1, P2, R12)
|
|
||||||
for s2 in range(2):
|
|
||||||
pre2 = f2.copy(); r2, u2 = macros(f2); post2 = collide(f2, feq(r2, u2), b2)
|
|
||||||
if not use_sdf:
|
|
||||||
f2 = stream(bb_set(post2.copy(), pre2, g_solid2))
|
|
||||||
else:
|
|
||||||
f2 = stream(post2); f2 = apply_bc(f2, post2, BC2)
|
|
||||||
if s1 == 1 and s2 == 1:
|
|
||||||
Fx[t], Fy[t] = (force_bc(post2, f2, BC2) if use_sdf else force_on(post2, g_solid2, g_fl2))
|
|
||||||
f2 = fill(f2, (1-(s2+1)/2)*g2o + ((s2+1)/2)*g2n)
|
|
||||||
f1 = restrict(f2, f1, P2, Rf12, fl2)
|
|
||||||
f0 = restrict(f1, f0, P1, Rf01, fl1)
|
|
||||||
col = f0[:, py, px]; uy[t] = (CYa*col).sum()/col.sum() # зонд следа (без full-macros)
|
|
||||||
if progress is not None: progress(t, STEPS)
|
|
||||||
if use1 and t % FE == 0:
|
|
||||||
frames.append((t, to_cpu(macros(f0)[1]), to_cpu(macros(f1)[1]),
|
|
||||||
(to_cpu(macros(f2)[1]) if use2 else None)))
|
|
||||||
return dict(mode=mode, DL=DL, Fx=to_cpu(Fx), Fy=to_cpu(Fy), uy=to_cpu(uy), frames=frames)
|
|
||||||
|
|
||||||
# ---- анализ (на host: FFT малых рядов) ----
|
|
||||||
def analyze(res):
|
|
||||||
Fx, Fy, uy, DL = res["Fx"], res["Fy"], res["uy"], res["DL"]
|
|
||||||
n = len(uy); h = slice(n//2, n); Cd = 2*Fx/(U**2*DL); Cl = 2*Fy/(U**2*DL)
|
|
||||||
sig = uy[h] - uy[h].mean(); npad = 8192
|
|
||||||
freqs = np.fft.rfftfreq(npad, 1.0); amp = np.abs(np.fft.rfft(sig, n=npad))
|
|
||||||
St = (freqs[1+int(np.argmax(amp[1:]))] if len(amp) > 2 else 0.0)*D0/U
|
|
||||||
return dict(Cd=float(Cd[h].mean()), Clrms=float(Cl[h].std()), St=float(St), uyrms=float(uy[h].std()),
|
|
||||||
Cl_s=Cl, uy=uy, freqs=freqs, amp=amp, h0=n//2)
|
|
||||||
|
|
||||||
# ---- визуализация (matplotlib на CPU; кадры уже host) ----
|
|
||||||
def _vort_np(u):
|
|
||||||
return (np.roll(u[1], -1, 1)-np.roll(u[1], 1, 1))*0.5 - (np.roll(u[0], -1, 0)-np.roll(u[0], 1, 0))*0.5
|
|
||||||
def save_gif(res, name, anim_dir, sdf_tag, fps=24):
|
|
||||||
import matplotlib; matplotlib.use("Agg")
|
|
||||||
import matplotlib.pyplot as plt, matplotlib.patches as mpatches
|
|
||||||
from PIL import Image
|
|
||||||
nested = (res["mode"] == "nested"); frames = res["frames"]
|
|
||||||
vm = np.nanpercentile(np.abs(np.where(solid0_np, np.nan, _vort_np(frames[len(frames)//2][1]))), 99.0)
|
|
||||||
cm = plt.get_cmap("inferno").copy(); cm.set_bad("white"); pil = []
|
|
||||||
for (t, u0, u1, u2) in frames:
|
|
||||||
fig = plt.figure(figsize=(11.2, 5.7), dpi=96); ax = fig.add_subplot(111)
|
|
||||||
ax.imshow(np.abs(np.where(solid0_np, np.nan, _vort_np(u0))), origin="lower", cmap=cm, vmin=0, vmax=vm,
|
|
||||||
extent=(0, Nx, 0, Ny), interpolation="nearest")
|
|
||||||
ax.imshow(np.abs(np.where(solid1_np, np.nan, _vort_np(u1)))[1:-1, 1:-1], origin="lower", cmap=cm, vmin=0, vmax=vm,
|
|
||||||
extent=(ax1+0.5, bx1-0.5, ay1+0.5, by1-0.5), interpolation="nearest")
|
|
||||||
if nested and u2 is not None:
|
|
||||||
ax.imshow(np.abs(np.where(solid2_np, np.nan, _vort_np(u2)))[1:-1, 1:-1], origin="lower", cmap=cm, vmin=0, vmax=vm,
|
|
||||||
extent=(cx2a+0.25, cx2b-0.25, cy2a+0.25, cy2b-0.25), interpolation="nearest")
|
|
||||||
ax.add_patch(mpatches.Rectangle((0.2, 0.2), Nx-0.4, Ny-0.4, fill=False, edgecolor="#4fc3f7", lw=1.6))
|
|
||||||
ax.text(1.5, Ny-4, "уровень 0: Δx (крупный)", color="#4fc3f7", fontsize=9, weight="bold")
|
|
||||||
ax.add_patch(mpatches.Rectangle((ax1, ay1), bx1-ax1, by1-ay1, fill=False, edgecolor="#aed581", lw=2.2))
|
|
||||||
ax.text(ax1+1, by1-3.5, "уровень 1: Δx/2 (тело+след)", color="#aed581", fontsize=9, weight="bold")
|
|
||||||
if nested:
|
|
||||||
ax.add_patch(mpatches.Rectangle((cx2a, cy2a), cx2b-cx2a, cy2b-cy2a, fill=False, edgecolor="#ff8a65", lw=2.2))
|
|
||||||
ax.text(cx2a+1, cy2b-3.5, "уровень 2: Δx/4 (у тела)", color="#ff8a65", fontsize=9, weight="bold")
|
|
||||||
base = "Вложенный AMR" if nested else "AMR (одиночный блок 2×)"
|
|
||||||
ax.set_title(f"{base}{sdf_tag} · поверх $|\\omega|$ · t={t}", fontsize=11)
|
|
||||||
ax.set_xlabel("x [lu]"); ax.set_ylabel("y [lu]"); ax.set_xlim(0, Nx); ax.set_ylim(0, Ny)
|
|
||||||
buf = _io.BytesIO(); fig.savefig(buf, format="png", bbox_inches="tight"); buf.seek(0); plt.close(fig)
|
|
||||||
pil.append(Image.open(buf).convert("RGB").convert("P", palette=Image.ADAPTIVE))
|
|
||||||
path = os.path.join(anim_dir, name)
|
|
||||||
pil[0].save(path, save_all=True, append_images=pil[1:], duration=int(1000/fps), loop=0, optimize=True)
|
|
||||||
print("saved", name, len(frames), "кадров")
|
|
||||||
|
|
||||||
LAB = ["без AMR (D=16)", "AMR 2× (D=32)", "AMR 2×+4× (D=64)"]
|
|
||||||
def print_table(AS, header):
|
|
||||||
print(f"\n=== {header} ===")
|
|
||||||
print(f"{'версия':22}{'D у тела':>9}{'St':>8}{'<Cd>':>9}{'rms Cl':>9}{'rms u_y':>9}")
|
|
||||||
for lab, DL, a in zip(LAB, [16, 32, 64], AS):
|
|
||||||
print(f"{lab:22}{DL:>9}{a['St']:>8.3f}{a['Cd']:>9.3f}{a['Clrms']:>9.4f}{a['uyrms']:>9.4f}")
|
|
||||||
print(f"{'литература Re≈150':22}{'—':>9}{0.183:>8.3f}{1.330:>9.3f}{'~0.3':>9}{'':>9}")
|
|
||||||
print("(лит.: St≈0.18, Cd≈1.3 для цилиндра при Re≈150; Williamson 1996, Henderson 1995)")
|
|
||||||
def probe_figure(AS, R2, path, suptitle):
|
|
||||||
import matplotlib; matplotlib.use("Agg")
|
|
||||||
import matplotlib.pyplot as plt, matplotlib.patches as mpatches
|
|
||||||
cols = ["#9e9e9e", "#1f6feb", "#d1495b"]
|
|
||||||
fig = plt.figure(figsize=(13.5, 8.2)); gs = fig.add_gridspec(2, 2, hspace=0.32, wspace=0.22)
|
|
||||||
axA = fig.add_subplot(gs[0, 0]); mid = R2["frames"][len(R2["frames"])//2]
|
|
||||||
vmf = np.nanpercentile(np.abs(np.where(solid0_np, np.nan, _vort_np(mid[1]))), 99)
|
|
||||||
cmf = plt.get_cmap("inferno").copy(); cmf.set_bad("white")
|
|
||||||
axA.imshow(np.abs(np.where(solid0_np, np.nan, _vort_np(mid[1]))), origin="lower", cmap=cmf, vmin=0, vmax=vmf,
|
|
||||||
extent=(0, Nx, 0, Ny), interpolation="nearest")
|
|
||||||
axA.add_patch(mpatches.Rectangle((ax1, ay1), bx1-ax1, by1-ay1, fill=False, edgecolor="#aed581", lw=1.8))
|
|
||||||
axA.add_patch(mpatches.Rectangle((cx2a, cy2a), cx2b-cx2a, cy2b-cy2a, fill=False, edgecolor="#ff8a65", lw=1.8))
|
|
||||||
axA.set_title("$|\\omega|$ + рамки зон 2×/4×"); axA.set_xlabel("x [lu]"); axA.set_ylabel("y [lu]")
|
|
||||||
axB = fig.add_subplot(gs[0, 1])
|
|
||||||
for a, lab, c in zip(AS, LAB, cols): axB.plot(a["Cl_s"][a["h0"]:], lw=0.8, color=c, label=lab)
|
|
||||||
axB.set_title("Подъёмная сила $C_l(t)$ (зонд силы)"); axB.set_xlabel("шаг"); axB.set_ylabel("$C_l$")
|
|
||||||
axB.legend(fontsize=8); axB.grid(alpha=0.3)
|
|
||||||
axC = fig.add_subplot(gs[1, 0])
|
|
||||||
for a, lab, c in zip(AS, LAB, cols):
|
|
||||||
sp = a["amp"]/a["amp"][1:].max(); axC.plot(a["freqs"]*D0/U, sp, lw=1.0, color=c, label=lab)
|
|
||||||
axC.axvline(0.183, color="k", ls="--", alpha=0.6, label="лит. St≈0.18"); axC.set_xlim(0, 0.6)
|
|
||||||
axC.set_title("Спектр $u_y$ в следе → St"); axC.set_xlabel("St = f·D/U"); axC.set_ylabel("норм. ампл.")
|
|
||||||
axC.legend(fontsize=8); axC.grid(alpha=0.3)
|
|
||||||
axD = fig.add_subplot(gs[1, 1]); x = np.arange(3); w = 0.35
|
|
||||||
axD.bar(x-w/2, [a["St"] for a in AS], w, color="#1f6feb", label="St")
|
|
||||||
axD.bar(x+w/2, [a["Cd"] for a in AS], w, color="#fb8c00", label="<Cd>")
|
|
||||||
axD.axhline(0.183, color="#1f6feb", ls="--", alpha=0.6); axD.axhline(1.33, color="#fb8c00", ls="--", alpha=0.6)
|
|
||||||
axD.set_xticks(x); axD.set_xticklabels(["без AMR", "2×", "2×+4×"]); axD.set_title("St и <Cd> (пунктир — лит.)")
|
|
||||||
axD.legend(fontsize=8); axD.grid(alpha=0.3, axis="y")
|
|
||||||
fig.suptitle(suptitle, fontweight="bold"); fig.savefig(path, dpi=110, bbox_inches="tight"); plt.close(fig)
|
|
||||||
print("saved", os.path.basename(path))
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# ОПТИМИЗИРОВАННЫЙ GPU-решатель (без SDF): цилиндр, три версии (без AMR / 2× / вложенный 2×+4×).
|
|
||||||
# То же, что amr_cylinder_gpu.py, но: GPU-only ядро _opt (фикс силового зонда + ускорение),
|
|
||||||
# прогресс-бар на каждый прогон, гифки для ВСЕХ трёх режимов (none/amr2x/nested).
|
|
||||||
# Запуск: python amr_cylinder_gpu_opt.py
|
|
||||||
# AMR_FP64=1 ... (двойная точность)
|
|
||||||
# AMR_STEPS=2000 ... (короткий прогон)
|
|
||||||
# AMR_CUDAGRAPH=0 ... (выключить CUDA Graphs; по умолчанию ВКЛ, с рантайм-самопроверкой)
|
|
||||||
import os, time, numpy as np
|
|
||||||
import amr_gpu_core_opt as core
|
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ANIM = os.path.join(HERE, "anim"); os.makedirs(ANIM, exist_ok=True)
|
|
||||||
FIGS = os.path.join(HERE, "figures"); os.makedirs(FIGS, exist_ok=True)
|
|
||||||
print(f"[ОПТ · без SDF] backend={core.BACKEND}; домен {core.Nx}x{core.Ny}; steps={core.STEPS}; "
|
|
||||||
f"graphs={'on' if os.environ.get('AMR_CUDAGRAPH','1') != '0' else 'off'}; "
|
|
||||||
f"L1(2×) {core.P1['Nfx']}x{core.P1['Nfy']}; L2(4×) {core.P2['Nfx']}x{core.P2['Nfy']}")
|
|
||||||
|
|
||||||
t0 = time.perf_counter()
|
|
||||||
R0 = core.run("none", False, core.make_progress("none")); print(" none :", round(time.perf_counter()-t0, 1), "s")
|
|
||||||
R1 = core.run("amr2x", False, core.make_progress("amr2x")); print(" amr2x :", round(time.perf_counter()-t0, 1), "s")
|
|
||||||
R2 = core.run("nested", False, core.make_progress("nested")); print(" nested :", round(time.perf_counter()-t0, 1), "s")
|
|
||||||
|
|
||||||
core.save_gif(R0, "none_cyl.gif", ANIM, " (без SDF)")
|
|
||||||
core.save_gif(R1, "amr2x_cyl.gif", ANIM, " (без SDF)")
|
|
||||||
core.save_gif(R2, "amr_nested_cyl.gif", ANIM, " (без SDF)")
|
|
||||||
|
|
||||||
AS = [core.analyze(R0), core.analyze(R1), core.analyze(R2)]
|
|
||||||
core.print_table(AS, "БЕЗ SDF — сравнение на реально собранных данных (установившийся режим)")
|
|
||||||
np.savez(os.path.join(HERE, "_amr_probe_data.npz"),
|
|
||||||
St=np.array([a["St"] for a in AS]), Cd=np.array([a["Cd"] for a in AS]),
|
|
||||||
Clrms=np.array([a["Clrms"] for a in AS]), uyrms=np.array([a["uyrms"] for a in AS]),
|
|
||||||
Cl0=AS[0]["Cl_s"], Cl1=AS[1]["Cl_s"], Cl2=AS[2]["Cl_s"],
|
|
||||||
uy0=AS[0]["uy"], uy1=AS[1]["uy"], uy2=AS[2]["uy"])
|
|
||||||
core.probe_figure(AS, R2, os.path.join(FIGS, "11e_amr_probe_comparison.png"),
|
|
||||||
"AMR на цилиндре (без SDF): сравнение трёх версий на данных зондов")
|
|
||||||
print("DONE (без SDF)")
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
# ОПТИМИЗИРОВАННЫЙ GPU-решатель (с SDF + Bouzidi): цилиндр, три версии (без AMR / 2× / 2×+4×).
|
|
||||||
# То же, что amr_cylinder_sdf_gpu.py, но: GPU-only ядро _opt (ускорение), прогресс-бар на
|
|
||||||
# каждый прогон, гифки для ВСЕХ трёх режимов (none/amr2x/nested). В конце — сводное сравнение с без-SDF.
|
|
||||||
# Запуск: python amr_cylinder_sdf_gpu_opt.py (после amr_cylinder_gpu_opt.py — для сводного сравнения)
|
|
||||||
# AMR_CUDAGRAPH=0 ... (выключить CUDA Graphs; по умолчанию ВКЛ, с рантайм-самопроверкой)
|
|
||||||
import os, time, numpy as np
|
|
||||||
import amr_gpu_core_opt as core
|
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
ANIM = os.path.join(HERE, "anim"); os.makedirs(ANIM, exist_ok=True)
|
|
||||||
FIGS = os.path.join(HERE, "figures"); os.makedirs(FIGS, exist_ok=True)
|
|
||||||
print(f"[ОПТ · SDF+Bouzidi] backend={core.BACKEND}; домен {core.Nx}x{core.Ny}; steps={core.STEPS}; "
|
|
||||||
f"graphs={'on' if os.environ.get('AMR_CUDAGRAPH','1') != '0' else 'off'}; "
|
|
||||||
f"L1(2×) {core.P1['Nfx']}x{core.P1['Nfy']}; L2(4×) {core.P2['Nfx']}x{core.P2['Nfy']}")
|
|
||||||
|
|
||||||
t0 = time.perf_counter()
|
|
||||||
R0 = core.run("none", True, core.make_progress("none")); print(" none :", round(time.perf_counter()-t0, 1), "s")
|
|
||||||
R1 = core.run("amr2x", True, core.make_progress("amr2x")); print(" amr2x :", round(time.perf_counter()-t0, 1), "s")
|
|
||||||
R2 = core.run("nested", True, core.make_progress("nested")); print(" nested :", round(time.perf_counter()-t0, 1), "s")
|
|
||||||
|
|
||||||
core.save_gif(R0, "none_cyl_sdf.gif", ANIM, " + SDF (Bouzidi)")
|
|
||||||
core.save_gif(R1, "amr2x_cyl_sdf.gif", ANIM, " + SDF (Bouzidi)")
|
|
||||||
core.save_gif(R2, "amr_nested_cyl_sdf.gif", ANIM, " + SDF (Bouzidi)")
|
|
||||||
|
|
||||||
AS = [core.analyze(R0), core.analyze(R1), core.analyze(R2)]
|
|
||||||
core.print_table(AS, "С SDF — сравнение на реально собранных данных (установившийся режим)")
|
|
||||||
np.savez(os.path.join(HERE, "_amr_probe_data_sdf.npz"),
|
|
||||||
St=np.array([a["St"] for a in AS]), Cd=np.array([a["Cd"] for a in AS]),
|
|
||||||
Clrms=np.array([a["Clrms"] for a in AS]), uyrms=np.array([a["uyrms"] for a in AS]),
|
|
||||||
Cl0=AS[0]["Cl_s"], Cl1=AS[1]["Cl_s"], Cl2=AS[2]["Cl_s"],
|
|
||||||
uy0=AS[0]["uy"], uy1=AS[1]["uy"], uy2=AS[2]["uy"])
|
|
||||||
core.probe_figure(AS, R2, os.path.join(FIGS, "11f_amr_sdf_probe_comparison.png"),
|
|
||||||
"AMR на цилиндре (SDF+Bouzidi): сравнение трёх версий на данных зондов")
|
|
||||||
|
|
||||||
# ---- сводное сравнение: SDF против без-SDF (если есть свежие данные без SDF) ----
|
|
||||||
nosdf = os.path.join(HERE, "_amr_probe_data.npz")
|
|
||||||
if os.path.exists(nosdf) and ("St" in np.load(nosdf).files):
|
|
||||||
import matplotlib; matplotlib.use("Agg"); import matplotlib.pyplot as plt
|
|
||||||
N = np.load(nosdf); Stn, Cdn = N["St"], N["Cd"]
|
|
||||||
Sts = np.array([a["St"] for a in AS]); Cds = np.array([a["Cd"] for a in AS])
|
|
||||||
print("\n=== СВОДНО: SDF vs без SDF (Cd, St) ===")
|
|
||||||
print(f"{'версия':14}{'St(noSDF)':>11}{'St(SDF)':>9}{'Cd(noSDF)':>11}{'Cd(SDF)':>9}")
|
|
||||||
for k, lab in enumerate(["без AMR", "2×", "2×+4×"]):
|
|
||||||
print(f"{lab:14}{Stn[k]:>11.3f}{Sts[k]:>9.3f}{Cdn[k]:>11.3f}{Cds[k]:>9.3f}")
|
|
||||||
print(f"{'лит. Re≈150':14}{0.183:>11.3f}{0.183:>9.3f}{1.330:>11.3f}{1.330:>9.3f}")
|
|
||||||
x = np.arange(3); w = 0.38
|
|
||||||
fig, axs = plt.subplots(1, 2, figsize=(13.5, 4.6))
|
|
||||||
axs[0].bar(x-w/2, Cdn, w, color="#9e9e9e", label="без SDF (ступенч.)")
|
|
||||||
axs[0].bar(x+w/2, Cds, w, color="#1f6feb", label="с SDF (Bouzidi)")
|
|
||||||
axs[0].axhline(1.33, color="k", ls="--", alpha=0.6, label="лит. Cd≈1.33")
|
|
||||||
axs[0].set_xticks(x); axs[0].set_xticklabels(["без AMR", "2×", "2×+4×"]); axs[0].set_ylabel("<Cd>")
|
|
||||||
axs[0].set_title("Сопротивление: SDF убирает завышение от ступенчатой границы")
|
|
||||||
axs[0].legend(fontsize=8); axs[0].grid(alpha=0.3, axis="y")
|
|
||||||
axs[1].bar(x-w/2, Stn, w, color="#9e9e9e", label="без SDF"); axs[1].bar(x+w/2, Sts, w, color="#1f6feb", label="с SDF")
|
|
||||||
axs[1].axhline(0.183, color="k", ls="--", alpha=0.6, label="лит. St≈0.18")
|
|
||||||
axs[1].set_xticks(x); axs[1].set_xticklabels(["без AMR", "2×", "2×+4×"]); axs[1].set_ylabel("St")
|
|
||||||
axs[1].set_title("Число Струхаля"); axs[1].legend(fontsize=8); axs[1].grid(alpha=0.3, axis="y")
|
|
||||||
fig.suptitle("Цилиндр: SDF (Bouzidi) против ступенчатого bounce-back — на реальных данных", fontweight="bold")
|
|
||||||
fig.savefig(os.path.join(FIGS, "11g_sdf_vs_nosdf.png"), dpi=110, bbox_inches="tight"); plt.close(fig)
|
|
||||||
print("saved 11g_sdf_vs_nosdf.png")
|
|
||||||
else:
|
|
||||||
print("\n(нет свежего _amr_probe_data.npz — сначала запусти amr_cylinder_gpu_opt.py)")
|
|
||||||
print("DONE (SDF)")
|
|
||||||
@@ -1,448 +0,0 @@
|
|||||||
# Оптимизированное GPU-ЯДРО (D2Q9 KBC) для AMR-решателей на цилиндре. GPU-only (CuPy).
|
|
||||||
# Отличия от amr_gpu_core.py:
|
|
||||||
# * УБРАН numpy-фолбэк: backend всегда CuPy/GPU (иначе жёсткая ошибка). numpy остаётся
|
|
||||||
# только для host-задач (маски/SDF/базис один раз, FFT в analyze, matplotlib/PIL).
|
|
||||||
# * ФИКС силового зонда force_on: второй член обмена импульсом берётся из поля ПОСЛЕ
|
|
||||||
# стриминга (как в корректном force_bc), а не из post-collision. Раньше это давало
|
|
||||||
# нефизичный отрицательный Cd и крошечный rms Cl.
|
|
||||||
# * feq без pow (показатели c∈{-1,0,1}) + cp.fuse; фьюзинг хвоста collide.
|
|
||||||
# * Предвычисленные маски линков для силы (без per-step roll статических масок).
|
|
||||||
# * run(): один шаг = замкнутая _step() над persistent-буферами; опционально CUDA Graphs
|
|
||||||
# (AMR_CUDAGRAPH=1, по умолчанию вкл) с автоматическим откатом на eager.
|
|
||||||
# * Кадры для всех трёх режимов (none/amr2x/nested); save_gif рисует и режим none.
|
|
||||||
# * make_progress — общий прогресс-бар в ядре.
|
|
||||||
# Запуск через раннеры: amr_cylinder_gpu_opt.py (без SDF) и amr_cylinder_sdf_gpu_opt.py (SDF).
|
|
||||||
import os, io as _io, numpy as np
|
|
||||||
|
|
||||||
# ---- backend: ТОЛЬКО CuPy/GPU (фолбэк удалён) ----
|
|
||||||
import cupy as cp
|
|
||||||
try:
|
|
||||||
_ = (cp.zeros(2) + 1).sum(); cp.cuda.Device().synchronize() # реальная операция на device
|
|
||||||
except Exception as e:
|
|
||||||
raise RuntimeError("Этому решателю нужна рабочая GPU + CuPy: numpy-фолбэк удалён.") from e
|
|
||||||
xp = cp; GPU = True
|
|
||||||
def to_cpu(a):
|
|
||||||
return cp.asnumpy(a)
|
|
||||||
DTYPE = cp.float64 if os.environ.get("AMR_FP64") else cp.float32
|
|
||||||
GREL = 1e-8 # ОТНОСИТЕЛЬНЫЙ порог вырожденности знаменателя γ (доля от ‖Δ‖²); см. solver_2x_sdf/backend.py.
|
|
||||||
# Прежний абсолютный порог (1e-6 в fp32) срабатывал на большинстве узлов и подменял KBC на LBGK.
|
|
||||||
BACKEND = f"CuPy/GPU dtype={'float64' if DTYPE == cp.float64 else 'float32'}"
|
|
||||||
|
|
||||||
# ---- решётка D2Q9 ----
|
|
||||||
Cx = [0, 1, 0, -1, 0, 1, -1, -1, 1] # python-инты для roll/индексации
|
|
||||||
Cy = [0, 0, 1, 0, -1, 1, 1, -1, -1]
|
|
||||||
OPP = [0, 3, 4, 1, 2, 7, 8, 5, 6]
|
|
||||||
Q = 9
|
|
||||||
_W = np.array([4/9, 1/9, 1/9, 1/9, 1/9, 1/36, 1/36, 1/36, 1/36])
|
|
||||||
_CXn = np.array(Cx, float); _CYn = np.array(Cy, float)
|
|
||||||
# моментный базис и проектор на сдвиг {cx²−cy², cxcy} (KBC-N1) — считаем в numpy, грузим на device
|
|
||||||
_M = np.zeros((Q, Q)); _M[0] = 1; _M[1] = _CXn; _M[2] = _CYn; _M[3] = 3*(_CXn**2+_CYn**2)-2
|
|
||||||
_M[4] = _CXn**2-_CYn**2; _M[5] = _CXn*_CYn; _M[6] = _CXn**2*_CYn; _M[7] = _CXn*_CYn**2; _M[8] = _CXn**2*_CYn**2
|
|
||||||
_Dm = np.zeros((Q, Q)); _Dm[4, 4] = _Dm[5, 5] = 1.0
|
|
||||||
_Psn = np.linalg.inv(_M) @ _Dm @ _M
|
|
||||||
# device-константы
|
|
||||||
Wa = xp.asarray(_W, dtype=DTYPE)
|
|
||||||
CXa = xp.asarray(_CXn, dtype=DTYPE); CYa = xp.asarray(_CYn, dtype=DTYPE)
|
|
||||||
Ps = xp.asarray(_Psn, dtype=DTYPE)
|
|
||||||
CS2 = 1.0/3.0
|
|
||||||
# device-скаляры (чтобы не делать host→device asarray на горячем пути и не ломать graph-capture)
|
|
||||||
_Z0 = xp.zeros((), DTYPE); _ONE = xp.asarray(1.0, DTYPE); _TWO = xp.asarray(2.0, DTYPE)
|
|
||||||
|
|
||||||
# ---- ядро LBM/KBC (математика идентична; feq без pow) ----
|
|
||||||
def _feq9_body(rho, ux, uy):
|
|
||||||
# равновесие product-form для D2Q9: показатели c∈{-1,0,1} → используем qx/iqx вместо pow.
|
|
||||||
# clip входит в ТЕЛО ядра через min/max c литералами (запекаются в кернел → нет H2D при capture).
|
|
||||||
ux = cp.minimum(cp.maximum(ux, -0.95), 0.95); uy = cp.minimum(cp.maximum(uy, -0.95), 0.95)
|
|
||||||
sx = cp.sqrt(1.0 + 3.0*ux*ux); sy = cp.sqrt(1.0 + 3.0*uy*uy)
|
|
||||||
base = rho*(2.0 - sx)*(2.0 - sy)
|
|
||||||
qx = (2.0*ux + sx)/(1.0 - ux); qy = (2.0*uy + sy)/(1.0 - uy)
|
|
||||||
ix = 1.0/qx; iy = 1.0/qy
|
|
||||||
return (base*(4.0/9.0), # ( 0, 0)
|
|
||||||
base*(1.0/9.0)*qx, base*(1.0/9.0)*qy, # (+1,0) (0,+1)
|
|
||||||
base*(1.0/9.0)*ix, base*(1.0/9.0)*iy, # (-1,0) (0,-1)
|
|
||||||
base*(1.0/36.0)*qx*qy, base*(1.0/36.0)*ix*qy, # (+1,+1) (-1,+1)
|
|
||||||
base*(1.0/36.0)*ix*iy, base*(1.0/36.0)*qx*iy) # (-1,-1) (+1,-1)
|
|
||||||
_feq9 = cp.fuse()(_feq9_body) # одно слитное ядро (если cp.fuse доступен)
|
|
||||||
_FUSE_OK = True
|
|
||||||
|
|
||||||
def feq(rho, u):
|
|
||||||
global _FUSE_OK
|
|
||||||
if _FUSE_OK:
|
|
||||||
try:
|
|
||||||
return xp.stack(_feq9(rho, u[0], u[1]), axis=0)
|
|
||||||
except Exception as e: # откат на не-fused (тоже без pow)
|
|
||||||
print(f"[feq] cp.fuse отключён ({type(e).__name__}: {e}); обычный путь без pow")
|
|
||||||
_FUSE_OK = False
|
|
||||||
return xp.stack(_feq9_body(rho, u[0], u[1]), axis=0)
|
|
||||||
|
|
||||||
def macros(f):
|
|
||||||
r = f.sum(0)
|
|
||||||
return r, xp.stack([(CXa[:, None, None]*f).sum(0)/r, (CYa[:, None, None]*f).sum(0)/r])
|
|
||||||
|
|
||||||
def collide(f, fe, beta):
|
|
||||||
dfn = f - fe
|
|
||||||
# проектор на сдвиг БЕЗ cuBLAS (gemm при capture тащит alpha/beta host→device → H2D):
|
|
||||||
dfn2d = dfn.reshape(Q, -1) # (Q, N)
|
|
||||||
ds = (Ps[:, :, None] * dfn2d[None]).sum(1).reshape(dfn.shape) # Σ_k Ps[i,k]·dfn[k] — чистые ядра
|
|
||||||
dh = dfn - ds; inv = 1.0/fe
|
|
||||||
num = (ds*dh*inv).sum(0); den = (dh*dh*inv).sum(0)
|
|
||||||
nrm = (dfn*dfn*inv).sum(0) # ‖Δ‖² — масштаб неравновесия узла
|
|
||||||
ok = den > GREL*nrm
|
|
||||||
den_safe = xp.where(ok, den, _ONE) # избегаем 0/0
|
|
||||||
binv = 1.0/beta
|
|
||||||
g = xp.where(ok, binv - (2.0 - binv)*num/den_safe, _TWO)
|
|
||||||
return f - beta*(2.0*ds + g[None]*dh)
|
|
||||||
|
|
||||||
def stream(f):
|
|
||||||
fs = xp.empty_like(f)
|
|
||||||
for i in range(Q):
|
|
||||||
fs[i] = xp.roll(f[i], (Cy[i], Cx[i]), (0, 1))
|
|
||||||
return fs
|
|
||||||
|
|
||||||
# ---- AMR-связка ----
|
|
||||||
def patch(pNx, pNy, ax, bx, ay, by, r):
|
|
||||||
Wx, Wy = bx-ax, by-ay; Nfx, Nfy = r*Wx+1, r*Wy+1
|
|
||||||
FX, FY = np.meshgrid(ax+np.arange(Nfx)/r, ay+np.arange(Nfy)/r)
|
|
||||||
x0 = np.floor(FX).astype(np.int64); y0 = np.floor(FY).astype(np.int64)
|
|
||||||
x1 = np.minimum(x0+1, pNx-1); y1 = np.minimum(y0+1, pNy-1)
|
|
||||||
return dict(Nfx=Nfx, Nfy=Nfy, ax=ax, bx=bx, ay=ay, by=by, r=r,
|
|
||||||
x0=xp.asarray(x0), y0=xp.asarray(y0), x1=xp.asarray(x1), y1=xp.asarray(y1),
|
|
||||||
tx=xp.asarray(FX-x0, DTYPE), ty=xp.asarray(FY-y0, DTYPE),
|
|
||||||
slx=slice(r, r*Wx, r), sly=slice(r, r*Wy, r))
|
|
||||||
def pint(fld, P):
|
|
||||||
return (fld[..., P["y0"], P["x0"]]*(1-P["tx"])*(1-P["ty"]) + fld[..., P["y0"], P["x1"]]*P["tx"]*(1-P["ty"])
|
|
||||||
+ fld[..., P["y1"], P["x0"]]*(1-P["tx"])*P["ty"] + fld[..., P["y1"], P["x1"]]*P["tx"]*P["ty"])
|
|
||||||
def ghost(pf, P, Rcf):
|
|
||||||
r, u = macros(pf); neq = pf - feq(r, u)
|
|
||||||
return feq(pint(r, P), xp.stack([pint(u[0], P), pint(u[1], P)])) + Rcf*pint(neq, P)
|
|
||||||
def fill(cf, gh):
|
|
||||||
cf[:, 0, :] = gh[:, 0, :]; cf[:, -1, :] = gh[:, -1, :]; cf[:, :, 0] = gh[:, :, 0]; cf[:, :, -1] = gh[:, :, -1]
|
|
||||||
return cf
|
|
||||||
def restrict(cf, pf, P, Rfc, fluid):
|
|
||||||
r, u = macros(cf); neq = cf - feq(r, u); slx, sly = P["slx"], P["sly"]
|
|
||||||
nv = feq(r[sly, slx], u[:, sly, slx]) + Rfc*neq[:, sly, slx]
|
|
||||||
cur = pf[:, P["ay"]+1:P["by"], P["ax"]+1:P["bx"]]
|
|
||||||
pf[:, P["ay"]+1:P["by"], P["ax"]+1:P["bx"]] = xp.where(fluid[None], nv, cur)
|
|
||||||
return pf
|
|
||||||
|
|
||||||
# ---- граница: (1) узловой bounce-back (без SDF); (2) SDF+Bouzidi ----
|
|
||||||
def bb_set(post, pre, solid): # узлы тела <- развёрнутые до-столкновения
|
|
||||||
for i in range(Q):
|
|
||||||
post[i] = xp.where(solid, pre[OPP[i]], post[i])
|
|
||||||
return post
|
|
||||||
def build_links(solid_np, body_np): # предвычисление масок линков жидкость->тело (статичны)
|
|
||||||
fluid = ~solid_np; links = []
|
|
||||||
for i in range(1, Q):
|
|
||||||
nb = np.roll(body_np, (-Cy[i], -Cx[i]), (0, 1)); mask = fluid & nb
|
|
||||||
links.append((i, OPP[i], Cx[i], Cy[i], xp.asarray(mask, DTYPE)))
|
|
||||||
return links
|
|
||||||
def force_on(fpost, fnew, links): # Mei (узловая граница): F=Σ c_i (f_i^после-столкн. + f_ī^после-стриминга)
|
|
||||||
Fx = _Z0; Fy = _Z0
|
|
||||||
for (i, ib, cx_i, cy_i, maskf) in links:
|
|
||||||
s = (maskf*(fpost[i] + fnew[ib])).sum()
|
|
||||||
Fx = Fx + cx_i*s; Fy = Fy + cy_i*s
|
|
||||||
return Fx, Fy
|
|
||||||
def build_bc(solid_np, phi_np, cyl_np): # SDF-границу строим на host один раз, переносим на device
|
|
||||||
fluid = ~solid_np; bc = []
|
|
||||||
for i in range(1, Q):
|
|
||||||
nb_solid = np.roll(solid_np, (-Cy[i], -Cx[i]), (0, 1)); mask = fluid & nb_solid
|
|
||||||
cl = mask & np.roll(cyl_np, (-Cy[i], -Cx[i]), (0, 1))
|
|
||||||
phinb = np.roll(phi_np, (-Cy[i], -Cx[i]), (0, 1))
|
|
||||||
with np.errstate(divide="ignore", invalid="ignore"):
|
|
||||||
qq = phi_np/(phi_np - phinb)
|
|
||||||
q = np.where(mask, np.clip(qq, 0.02, 0.98), 0.5)
|
|
||||||
xff = mask & (~np.roll(solid_np, (Cy[i], Cx[i]), (0, 1)))
|
|
||||||
bc.append((i, OPP[i], xp.asarray(mask), xp.asarray(q, DTYPE), xp.asarray(xff), xp.asarray(cl)))
|
|
||||||
return bc
|
|
||||||
def apply_bc(f, fpost, bc): # Bouzidi (линейный), полностью через where (GPU)
|
|
||||||
for (i, ib, mask, q, xff, cl) in bc:
|
|
||||||
fi = fpost[i]; fib = fpost[ib]; fiback = xp.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 force_bc(fpost, f, bc): # Mei для интерполированной границы (уже корректно: f[ib] — после стриминга)
|
|
||||||
Fx = _Z0; Fy = _Z0
|
|
||||||
for (i, ib, mask, q, xff, cl) in bc:
|
|
||||||
s = xp.where(cl, fpost[i] + f[ib], _Z0).sum()
|
|
||||||
Fx = Fx + Cx[i]*s; Fy = Fy + Cy[i]*s
|
|
||||||
return Fx, Fy
|
|
||||||
|
|
||||||
# ---- геометрия (ИДЕНТИЧНА базовым версиям) ----
|
|
||||||
Nx, Ny, D, cx, cy = 150, 72, 16, 40, 36
|
|
||||||
U, Re, STEPS, ramp, FE = 0.07, 150.0, int(os.environ.get("AMR_STEPS", 8000)), 1000, 34
|
|
||||||
D0 = D; nu = U*D/Re; tau0 = nu/CS2 + 0.5
|
|
||||||
ax1, bx1, ay1, by1 = 16, 118, 8, 64
|
|
||||||
cx2a, cx2b, cy2a, cy2b = 26, 64, 18, 54
|
|
||||||
px, py = cx + 3*D, cy
|
|
||||||
def _cmask(NX, NY, ccx, ccy, R):
|
|
||||||
Y, X = np.meshgrid(np.arange(NY), np.arange(NX), indexing="ij"); return (X-ccx)**2 + (Y-ccy)**2 <= R*R
|
|
||||||
def _csdf(NX, NY, ccx, ccy, R):
|
|
||||||
Y, X = np.meshgrid(np.arange(NY), np.arange(NX), indexing="ij"); return np.sqrt((X-ccx)**2.0 + (Y-ccy)**2.0) - R
|
|
||||||
# host-маски/SDF
|
|
||||||
_walls = np.zeros((Ny, Nx), bool); _walls[0, :] = True; _walls[-1, :] = True
|
|
||||||
cyl0_np = _cmask(Nx, Ny, cx, cy, D/2); solid0_np = cyl0_np | _walls
|
|
||||||
_Yg = np.arange(Ny)[:, None]*np.ones((1, Nx))
|
|
||||||
phi0_np = np.minimum(_csdf(Nx, Ny, cx, cy, D/2), np.minimum(_Yg-0.5, (Ny-1.5)-_Yg))
|
|
||||||
P1 = patch(Nx, Ny, ax1, bx1, ay1, by1, 2); P2 = patch(P1["Nfx"], P1["Nfy"], (cx2a-ax1)*2, (cx2b-ax1)*2, (cy2a-ay1)*2, (cy2b-ay1)*2, 2)
|
|
||||||
solid1_np = _cmask(P1["Nfx"], P1["Nfy"], (cx-ax1)*2, (cy-ay1)*2, D); phi1_np = _csdf(P1["Nfx"], P1["Nfy"], (cx-ax1)*2, (cy-ay1)*2, D)
|
|
||||||
solid2_np = _cmask(P2["Nfx"], P2["Nfy"], (cx-cx2a)*4, (cy-cy2a)*4, 2*D); phi2_np = _csdf(P2["Nfx"], P2["Nfy"], (cx-cx2a)*4, (cy-cy2a)*4, 2*D)
|
|
||||||
# device-маски (без SDF)
|
|
||||||
g_solid0 = xp.asarray(solid0_np); g_cyl0 = xp.asarray(cyl0_np); g_fl0 = ~g_solid0
|
|
||||||
g_solid1 = xp.asarray(solid1_np); g_fl1 = ~g_solid1
|
|
||||||
g_solid2 = xp.asarray(solid2_np); g_fl2 = ~g_solid2
|
|
||||||
# предвычисленные маски линков для силы (узловая граница)
|
|
||||||
L0_links = build_links(solid0_np, cyl0_np)
|
|
||||||
L1_links = build_links(solid1_np, solid1_np)
|
|
||||||
L2_links = build_links(solid2_np, solid2_np)
|
|
||||||
# SDF-ГУ
|
|
||||||
BC0 = build_bc(solid0_np, phi0_np, cyl0_np); BC1 = build_bc(solid1_np, phi1_np, solid1_np); BC2 = build_bc(solid2_np, phi2_np, solid2_np)
|
|
||||||
# рестрикция: жидкие узлы
|
|
||||||
fl1 = xp.asarray(~solid0_np[ay1+1:by1, ax1+1:bx1])
|
|
||||||
fl2 = xp.ones((P2["by"]-P2["ay"]-1, P2["bx"]-P2["ax"]-1), bool); fl2 = xp.asarray(fl2)
|
|
||||||
# τ / масштабы
|
|
||||||
t1 = 2*tau0 - 0.5; t2 = 2*t1 - 0.5
|
|
||||||
b0 = 1/(2*tau0); b1 = 1/(2*t1); b2 = 1/(2*t2)
|
|
||||||
R01 = t1/(2*tau0); Rf01 = 1/R01; R12 = t2/(2*t1); Rf12 = 1/R12
|
|
||||||
|
|
||||||
# ---- прогресс-бар (ASCII-шкала по выполненным шагам из заданных) ----
|
|
||||||
import time as _time
|
|
||||||
def make_progress(label, width=32, every=0.1):
|
|
||||||
st = {"t0": _time.perf_counter(), "last": 0.0}
|
|
||||||
def cb(t, total):
|
|
||||||
now = _time.perf_counter(); done = (t >= total)
|
|
||||||
if not done and (now - st["last"] < every): # троттлинг, чтобы не спамить консоль
|
|
||||||
return
|
|
||||||
st["last"] = now; frac = (t + 1) / (total + 1)
|
|
||||||
filled = int(round(width * frac)); bar = "#" * filled + "-" * (width - filled)
|
|
||||||
el = now - st["t0"]; eta = (el / frac - el) if frac > 0 else 0.0
|
|
||||||
print(f"\r {label:7}[{bar}] {frac*100:5.1f}% ({t}/{total}) {el:5.1f}s ETA {eta:5.1f}s",
|
|
||||||
end=("\n" if done else ""), flush=True)
|
|
||||||
return cb
|
|
||||||
|
|
||||||
# ---- захват CUDA-графа одного шага ----
|
|
||||||
def _capture(step_fn):
|
|
||||||
cp.cuda.Device().synchronize()
|
|
||||||
s = cp.cuda.Stream(non_blocking=True)
|
|
||||||
with s:
|
|
||||||
s.begin_capture()
|
|
||||||
try:
|
|
||||||
step_fn()
|
|
||||||
except Exception:
|
|
||||||
try: s.end_capture()
|
|
||||||
except Exception: pass
|
|
||||||
raise
|
|
||||||
g = s.end_capture()
|
|
||||||
return g
|
|
||||||
|
|
||||||
def run(mode, use_sdf, progress=None):
|
|
||||||
"""mode: none|amr2x|nested. Возвращает {Fx,Fy,uy (host), frames (host), DL, mode}.
|
|
||||||
progress(t, STEPS) — необязательный колбэк. CUDA Graphs: env AMR_CUDAGRAPH (по умолч. вкл)."""
|
|
||||||
use1 = mode in ("amr2x", "nested"); use2 = (mode == "nested")
|
|
||||||
DL = {"none": D0, "amr2x": 2*D0, "nested": 4*D0}[mode]
|
|
||||||
|
|
||||||
# ---- persistent-состояние (фиксированные буферы для replay графа) ----
|
|
||||||
F0 = feq(xp.ones((Ny, Nx), DTYPE), xp.zeros((2, Ny, Nx), DTYPE)).copy()
|
|
||||||
F1 = feq(xp.ones((P1["Nfy"], P1["Nfx"]), DTYPE), xp.zeros((2, P1["Nfy"], P1["Nfx"]), DTYPE)).copy() if use1 else None
|
|
||||||
F2 = feq(xp.ones((P2["Nfy"], P2["Nfx"]), DTYPE), xp.zeros((2, P2["Nfy"], P2["Nfx"]), DTYPE)).copy() if use2 else None
|
|
||||||
onecol = xp.ones((Ny, 1), DTYPE)
|
|
||||||
uin_dev = xp.zeros((2, Ny, 1), DTYPE) # вход; обновляется СНАРУЖИ захвата
|
|
||||||
Fx_s = xp.zeros((), DTYPE); Fy_s = xp.zeros((), DTYPE); uy_s = xp.zeros((), DTYPE)
|
|
||||||
# разгон входа на device: U*smoothstep(t/ramp)
|
|
||||||
_ts = np.minimum(np.arange(STEPS+1)/ramp, 1.0)
|
|
||||||
ramp_u = xp.asarray(U*(_ts*_ts*(3-2*_ts)), DTYPE)
|
|
||||||
Fx = xp.zeros(STEPS+1, DTYPE); Fy = xp.zeros(STEPS+1, DTYPE); uy = xp.zeros(STEPS+1, DTYPE)
|
|
||||||
frames = []
|
|
||||||
|
|
||||||
def _step():
|
|
||||||
# ----- уровень 0 -----
|
|
||||||
rho, u0 = macros(F0)
|
|
||||||
pre = F0.copy()
|
|
||||||
post0 = collide(F0, feq(rho, u0), b0)
|
|
||||||
if not use_sdf:
|
|
||||||
f0 = stream(bb_set(post0.copy(), pre, g_solid0))
|
|
||||||
else:
|
|
||||||
f0 = stream(post0); f0 = apply_bc(f0, post0, BC0)
|
|
||||||
if mode == "none":
|
|
||||||
fx, fy = (force_bc(post0, f0, BC0) if use_sdf else force_on(post0, f0, L0_links))
|
|
||||||
Fx_s[...] = fx; Fy_s[...] = fy
|
|
||||||
# вход/выход (разгон читаем из uin_dev)
|
|
||||||
f0[:, 1:-1, 0] = feq(onecol, uin_dev)[:, 1:-1, 0]; f0[:, 1:-1, -1] = f0[:, 1:-1, -2]
|
|
||||||
# ----- уровень 1 -----
|
|
||||||
if use1:
|
|
||||||
g1o = ghost(pre, P1, R01); g1n = ghost(f0, P1, R01)
|
|
||||||
f1 = F1
|
|
||||||
for s1 in range(2):
|
|
||||||
pre1 = f1.copy(); r1, u1 = macros(f1); post1 = collide(f1, feq(r1, u1), b1)
|
|
||||||
if not use_sdf:
|
|
||||||
f1 = stream(bb_set(post1.copy(), pre1, g_solid1))
|
|
||||||
else:
|
|
||||||
f1 = stream(post1); f1 = apply_bc(f1, post1, BC1)
|
|
||||||
if mode == "amr2x" and s1 == 1:
|
|
||||||
fx, fy = (force_bc(post1, f1, BC1) if use_sdf else force_on(post1, f1, L1_links))
|
|
||||||
Fx_s[...] = fx; Fy_s[...] = fy
|
|
||||||
f1 = fill(f1, (1-(s1+1)/2)*g1o + ((s1+1)/2)*g1n)
|
|
||||||
# ----- уровень 2 -----
|
|
||||||
if use2:
|
|
||||||
g2o = ghost(pre1, P2, R12); g2n = ghost(f1, P2, R12)
|
|
||||||
f2 = F2
|
|
||||||
for s2 in range(2):
|
|
||||||
pre2 = f2.copy(); r2, u2 = macros(f2); post2 = collide(f2, feq(r2, u2), b2)
|
|
||||||
if not use_sdf:
|
|
||||||
f2 = stream(bb_set(post2.copy(), pre2, g_solid2))
|
|
||||||
else:
|
|
||||||
f2 = stream(post2); f2 = apply_bc(f2, post2, BC2)
|
|
||||||
if s1 == 1 and s2 == 1:
|
|
||||||
fx, fy = (force_bc(post2, f2, BC2) if use_sdf else force_on(post2, f2, L2_links))
|
|
||||||
Fx_s[...] = fx; Fy_s[...] = fy
|
|
||||||
f2 = fill(f2, (1-(s2+1)/2)*g2o + ((s2+1)/2)*g2n)
|
|
||||||
f1 = restrict(f2, f1, P2, Rf12, fl2)
|
|
||||||
F2[...] = f2
|
|
||||||
f0 = restrict(f1, f0, P1, Rf01, fl1)
|
|
||||||
F1[...] = f1
|
|
||||||
# зонд скорости в следе + запись состояния
|
|
||||||
col = f0[:, py, px]; uy_s[...] = (CYa*col).sum()/col.sum()
|
|
||||||
F0[...] = f0
|
|
||||||
|
|
||||||
use_graph = (os.environ.get("AMR_CUDAGRAPH", "1") != "0") # CUDA Graphs по умолчанию ВКЛ (AMR_CUDAGRAPH=0 — выкл)
|
|
||||||
graph = None; graph_failed = False
|
|
||||||
def _snap():
|
|
||||||
return (F0.copy(), F1.copy() if use1 else None, F2.copy() if use2 else None)
|
|
||||||
def _restore(s):
|
|
||||||
F0[...] = s[0]
|
|
||||||
if use1: F1[...] = s[1]
|
|
||||||
if use2: F2[...] = s[2]
|
|
||||||
def _maxdiff(s):
|
|
||||||
d = float(xp.abs(s[0]-F0).max())
|
|
||||||
if use1: d = max(d, float(xp.abs(s[1]-F1).max()))
|
|
||||||
if use2: d = max(d, float(xp.abs(s[2]-F2).max()))
|
|
||||||
return d
|
|
||||||
for t in range(STEPS+1):
|
|
||||||
uin_dev[0, :, 0] = ramp_u[t]
|
|
||||||
did = False
|
|
||||||
if t == 1 and use_graph and not graph_failed:
|
|
||||||
# захват графа + РАНТАЙМ-САМОПРОВЕРКА: один шаг графом vs eager; при расхождении — откат
|
|
||||||
try:
|
|
||||||
graph = _capture(_step) # запись (без исполнения)
|
|
||||||
before = _snap() # состояние после t=0
|
|
||||||
graph.launch() # граф -> результат t=1
|
|
||||||
gres = _snap()
|
|
||||||
_restore(before); _step() # eager -> доверенный результат t=1
|
|
||||||
d = _maxdiff(gres)
|
|
||||||
if d > 1e-3:
|
|
||||||
print(f"\n[graph] self-check разошёлся (Δ={d:.2e}) — отключаю графы, работаю eager")
|
|
||||||
graph = None; graph_failed = True
|
|
||||||
else:
|
|
||||||
print(f"\n[graph] self-check OK (Δ={d:.2e}) — CUDA graphs активны")
|
|
||||||
did = True # шаг t=1 уже выполнен (eager-результат — доверенный)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"\n[graph] недоступны ({type(e).__name__}: {e}) — работаю eager")
|
|
||||||
import traceback as _tb; _tb.print_exc() # точная строка H2D/несовместимости
|
|
||||||
graph = None; graph_failed = True
|
|
||||||
try: cp.cuda.Device().synchronize()
|
|
||||||
except Exception: pass
|
|
||||||
if not did:
|
|
||||||
if graph is not None and t >= 1:
|
|
||||||
graph.launch()
|
|
||||||
else:
|
|
||||||
_step()
|
|
||||||
Fx[t] = Fx_s; Fy[t] = Fy_s; uy[t] = uy_s
|
|
||||||
if progress is not None:
|
|
||||||
progress(t, STEPS)
|
|
||||||
if t % 500 == 0 and not bool(xp.isfinite(F0).all()):
|
|
||||||
print("BLEW UP", mode, t); Fx = Fx[:t]; Fy = Fy[:t]; uy = uy[:t]; break
|
|
||||||
if t % FE == 0:
|
|
||||||
frames.append((t, to_cpu(macros(F0)[1]),
|
|
||||||
to_cpu(macros(F1)[1]) if use1 else None,
|
|
||||||
to_cpu(macros(F2)[1]) if use2 else None))
|
|
||||||
return dict(mode=mode, DL=DL, Fx=to_cpu(Fx), Fy=to_cpu(Fy), uy=to_cpu(uy), frames=frames)
|
|
||||||
|
|
||||||
# ---- анализ (на host: FFT малых рядов) ----
|
|
||||||
def analyze(res):
|
|
||||||
Fx, Fy, uy, DL = res["Fx"], res["Fy"], res["uy"], res["DL"]
|
|
||||||
n = len(uy); h = slice(n//2, n); Cd = 2*Fx/(U**2*DL); Cl = 2*Fy/(U**2*DL)
|
|
||||||
sig = uy[h] - uy[h].mean(); npad = 8192
|
|
||||||
freqs = np.fft.rfftfreq(npad, 1.0); amp = np.abs(np.fft.rfft(sig, n=npad))
|
|
||||||
St = (freqs[1+int(np.argmax(amp[1:]))] if len(amp) > 2 else 0.0)*D0/U
|
|
||||||
return dict(Cd=float(Cd[h].mean()), Clrms=float(Cl[h].std()), St=float(St), uyrms=float(uy[h].std()),
|
|
||||||
Cl_s=Cl, uy=uy, freqs=freqs, amp=amp, h0=n//2)
|
|
||||||
|
|
||||||
# ---- визуализация (matplotlib на CPU; кадры уже host) ----
|
|
||||||
def _vort_np(u):
|
|
||||||
return (np.roll(u[1], -1, 1)-np.roll(u[1], 1, 1))*0.5 - (np.roll(u[0], -1, 0)-np.roll(u[0], 1, 0))*0.5
|
|
||||||
def save_gif(res, name, anim_dir, sdf_tag, fps=24):
|
|
||||||
import matplotlib; matplotlib.use("Agg")
|
|
||||||
import matplotlib.pyplot as plt, matplotlib.patches as mpatches
|
|
||||||
from PIL import Image
|
|
||||||
nested = (res["mode"] == "nested"); has1 = res["mode"] in ("amr2x", "nested"); frames = res["frames"]
|
|
||||||
vm = np.nanpercentile(np.abs(np.where(solid0_np, np.nan, _vort_np(frames[len(frames)//2][1]))), 99.0)
|
|
||||||
cm = plt.get_cmap("inferno").copy(); cm.set_bad("white"); pil = []
|
|
||||||
for (t, u0, u1, u2) in frames:
|
|
||||||
fig = plt.figure(figsize=(11.2, 5.7), dpi=96); ax = fig.add_subplot(111)
|
|
||||||
ax.imshow(np.abs(np.where(solid0_np, np.nan, _vort_np(u0))), origin="lower", cmap=cm, vmin=0, vmax=vm,
|
|
||||||
extent=(0, Nx, 0, Ny), interpolation="nearest")
|
|
||||||
if has1 and u1 is not None:
|
|
||||||
ax.imshow(np.abs(np.where(solid1_np, np.nan, _vort_np(u1)))[1:-1, 1:-1], origin="lower", cmap=cm, vmin=0, vmax=vm,
|
|
||||||
extent=(ax1+0.5, bx1-0.5, ay1+0.5, by1-0.5), interpolation="nearest")
|
|
||||||
if nested and u2 is not None:
|
|
||||||
ax.imshow(np.abs(np.where(solid2_np, np.nan, _vort_np(u2)))[1:-1, 1:-1], origin="lower", cmap=cm, vmin=0, vmax=vm,
|
|
||||||
extent=(cx2a+0.25, cx2b-0.25, cy2a+0.25, cy2b-0.25), interpolation="nearest")
|
|
||||||
ax.add_patch(mpatches.Rectangle((0.2, 0.2), Nx-0.4, Ny-0.4, fill=False, edgecolor="#4fc3f7", lw=1.6))
|
|
||||||
ax.text(1.5, Ny-4, "уровень 0: Δx (крупный)", color="#4fc3f7", fontsize=9, weight="bold")
|
|
||||||
if has1:
|
|
||||||
ax.add_patch(mpatches.Rectangle((ax1, ay1), bx1-ax1, by1-ay1, fill=False, edgecolor="#aed581", lw=2.2))
|
|
||||||
ax.text(ax1+1, by1-3.5, "уровень 1: Δx/2 (тело+след)", color="#aed581", fontsize=9, weight="bold")
|
|
||||||
if nested:
|
|
||||||
ax.add_patch(mpatches.Rectangle((cx2a, cy2a), cx2b-cx2a, cy2b-cy2a, fill=False, edgecolor="#ff8a65", lw=2.2))
|
|
||||||
ax.text(cx2a+1, cy2b-3.5, "уровень 2: Δx/4 (у тела)", color="#ff8a65", fontsize=9, weight="bold")
|
|
||||||
base = ("Вложенный AMR" if nested else "AMR (одиночный блок 2×)") if has1 else "Без AMR (D=16)"
|
|
||||||
ax.set_title(f"{base}{sdf_tag} · поверх $|\\omega|$ · t={t}", fontsize=11)
|
|
||||||
ax.set_xlabel("x [lu]"); ax.set_ylabel("y [lu]"); ax.set_xlim(0, Nx); ax.set_ylim(0, Ny)
|
|
||||||
buf = _io.BytesIO(); fig.savefig(buf, format="png", bbox_inches="tight"); buf.seek(0); plt.close(fig)
|
|
||||||
pil.append(Image.open(buf).convert("RGB").convert("P", palette=Image.ADAPTIVE))
|
|
||||||
path = os.path.join(anim_dir, name)
|
|
||||||
pil[0].save(path, save_all=True, append_images=pil[1:], duration=int(1000/fps), loop=0, optimize=True)
|
|
||||||
print("saved", name, len(frames), "кадров")
|
|
||||||
|
|
||||||
LAB = ["без AMR (D=16)", "AMR 2× (D=32)", "AMR 2×+4× (D=64)"]
|
|
||||||
def print_table(AS, header):
|
|
||||||
print(f"\n=== {header} ===")
|
|
||||||
print(f"{'версия':22}{'D у тела':>9}{'St':>8}{'<Cd>':>9}{'rms Cl':>9}{'rms u_y':>9}")
|
|
||||||
for lab, DL, a in zip(LAB, [16, 32, 64], AS):
|
|
||||||
print(f"{lab:22}{DL:>9}{a['St']:>8.3f}{a['Cd']:>9.3f}{a['Clrms']:>9.4f}{a['uyrms']:>9.4f}")
|
|
||||||
print(f"{'литература Re≈150':22}{'—':>9}{0.183:>8.3f}{1.330:>9.3f}{'~0.3':>9}{'':>9}")
|
|
||||||
print("(лит.: St≈0.18, Cd≈1.3 для цилиндра при Re≈150; Williamson 1996, Henderson 1995)")
|
|
||||||
def probe_figure(AS, R2, path, suptitle):
|
|
||||||
import matplotlib; matplotlib.use("Agg")
|
|
||||||
import matplotlib.pyplot as plt, matplotlib.patches as mpatches
|
|
||||||
cols = ["#9e9e9e", "#1f6feb", "#d1495b"]
|
|
||||||
fig = plt.figure(figsize=(13.5, 8.2)); gs = fig.add_gridspec(2, 2, hspace=0.32, wspace=0.22)
|
|
||||||
axA = fig.add_subplot(gs[0, 0]); mid = R2["frames"][len(R2["frames"])//2]
|
|
||||||
vmf = np.nanpercentile(np.abs(np.where(solid0_np, np.nan, _vort_np(mid[1]))), 99)
|
|
||||||
cmf = plt.get_cmap("inferno").copy(); cmf.set_bad("white")
|
|
||||||
axA.imshow(np.abs(np.where(solid0_np, np.nan, _vort_np(mid[1]))), origin="lower", cmap=cmf, vmin=0, vmax=vmf,
|
|
||||||
extent=(0, Nx, 0, Ny), interpolation="nearest")
|
|
||||||
axA.add_patch(mpatches.Rectangle((ax1, ay1), bx1-ax1, by1-ay1, fill=False, edgecolor="#aed581", lw=1.8))
|
|
||||||
axA.add_patch(mpatches.Rectangle((cx2a, cy2a), cx2b-cx2a, cy2b-cy2a, fill=False, edgecolor="#ff8a65", lw=1.8))
|
|
||||||
axA.set_title("$|\\omega|$ + рамки зон 2×/4×"); axA.set_xlabel("x [lu]"); axA.set_ylabel("y [lu]")
|
|
||||||
axB = fig.add_subplot(gs[0, 1])
|
|
||||||
for a, lab, c in zip(AS, LAB, cols): axB.plot(a["Cl_s"][a["h0"]:], lw=0.8, color=c, label=lab)
|
|
||||||
axB.set_title("Подъёмная сила $C_l(t)$ (зонд силы)"); axB.set_xlabel("шаг"); axB.set_ylabel("$C_l$")
|
|
||||||
axB.legend(fontsize=8); axB.grid(alpha=0.3)
|
|
||||||
axC = fig.add_subplot(gs[1, 0])
|
|
||||||
for a, lab, c in zip(AS, LAB, cols):
|
|
||||||
sp = a["amp"]/a["amp"][1:].max(); axC.plot(a["freqs"]*D0/U, sp, lw=1.0, color=c, label=lab)
|
|
||||||
axC.axvline(0.183, color="k", ls="--", alpha=0.6, label="лит. St≈0.18"); axC.set_xlim(0, 0.6)
|
|
||||||
axC.set_title("Спектр $u_y$ в следе → St"); axC.set_xlabel("St = f·D/U"); axC.set_ylabel("норм. ампл.")
|
|
||||||
axC.legend(fontsize=8); axC.grid(alpha=0.3)
|
|
||||||
axD = fig.add_subplot(gs[1, 1]); x = np.arange(3); w = 0.35
|
|
||||||
axD.bar(x-w/2, [a["St"] for a in AS], w, color="#1f6feb", label="St")
|
|
||||||
axD.bar(x+w/2, [a["Cd"] for a in AS], w, color="#fb8c00", label="<Cd>")
|
|
||||||
axD.axhline(0.183, color="#1f6feb", ls="--", alpha=0.6); axD.axhline(1.33, color="#fb8c00", ls="--", alpha=0.6)
|
|
||||||
axD.set_xticks(x); axD.set_xticklabels(["без AMR", "2×", "2×+4×"]); axD.set_title("St и <Cd> (пунктир — лит.)")
|
|
||||||
axD.legend(fontsize=8); axD.grid(alpha=0.3, axis="y")
|
|
||||||
fig.suptitle(suptitle, fontweight="bold"); fig.savefig(path, dpi=110, bbox_inches="tight"); plt.close(fig)
|
|
||||||
print("saved", os.path.basename(path))
|
|
||||||
|
Before Width: | Height: | Size: 182 KiB |
|
Before Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 6.2 MiB |
|
Before Width: | Height: | Size: 19 MiB |
|
Before Width: | Height: | Size: 16 MiB |
|
Before Width: | Height: | Size: 8.5 MiB |
@@ -1,36 +0,0 @@
|
|||||||
# demos_gpu — GPU-прогоны демо для ноутбука kbc_lbm.ipynb
|
|
||||||
|
|
||||||
Ноутбук **не исполняет код** — он встраивает готовые артефакты (png/gif) и цитирует числа.
|
|
||||||
Все симуляции здесь, **только GPU/CuPy** (CPU-фолбэка нет). Физика не дублируется: решётка,
|
|
||||||
равновесие, KBC-столкновение, перенос и AMR-связка импортируются из `../solver_2x_sdf` —
|
|
||||||
демо тем самым гоняют **те же компоненты, что и финальная модель**.
|
|
||||||
|
|
||||||
## Запуск (на GPU-сервере, из этой папки)
|
|
||||||
|
|
||||||
```sh
|
|
||||||
python checks_gpu.py # самопроверки математики → out/checks.txt
|
|
||||||
python tgv_gpu.py # Тейлор–Грин: вязкость, H-теорема → figures/07*, anim/tgv_decay.gif
|
|
||||||
python shear_gpu.py # сдвиговый слой BGK vs KBC → figures/08*, anim/shear_bgk_vs_kbc.gif
|
|
||||||
python cavity_gpu.py # каверна Re=1000 vs Ghia → figures/09*, anim/cavity_transient.gif
|
|
||||||
python obstacle_gpu.py # 4 тела, дорожки Кармана → figures/10*, anim/obstacle_shapes.gif
|
|
||||||
python amr_demo_gpu.py # AMR: демо + бенчмарк + вложенный → figures/11d*, anim/amr_{cylinder,nested}.gif
|
|
||||||
python figures_static.py # стенсили/блок-схема/схема AMR → figures/01,06,11c,11_d3q27 (после obstacle!)
|
|
||||||
```
|
|
||||||
|
|
||||||
`figures_static.py` — единственный скрипт без GPU (чистый matplotlib, можно гонять локально);
|
|
||||||
схему `11c` он перерисовывает по фону из `out/obstacle_circle_field.npz`, который пишет
|
|
||||||
`obstacle_gpu.py` — поэтому его запускать **после** obstacle.
|
|
||||||
|
|
||||||
Env: `AMR_FP64=1` — двойная точность (жёстче пороги самопроверок в checks_gpu).
|
|
||||||
|
|
||||||
| Скрипт | Время (примерно, совр. GPU) |
|
|
||||||
|---|---|
|
|
||||||
| checks_gpu | секунды |
|
|
||||||
| tgv_gpu | ~1 мин (10k шагов, 96²) |
|
|
||||||
| shear_gpu | ~1–2 мин (2×8k, 128²) |
|
|
||||||
| cavity_gpu | ~1–2 мин (до 14k, 110²) |
|
|
||||||
| obstacle_gpu | ~3–5 мин (4×9.6k, 150×72) |
|
|
||||||
| amr_demo_gpu | ~5–10 мин (демо + 6 бенчмарков + анимация) |
|
|
||||||
|
|
||||||
Артефакты идут в общие каталоги `../figures` и `../anim` под **теми же именами**, что
|
|
||||||
исторически использовал ноутбук; числовые сводки — в `out/*.txt|npz`.
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
# Общая инфраструктура GPU-демо (ноутбук kbc_lbm.ipynb код не исполняет — только встраивает
|
|
||||||
# артефакты, которые генерят эти скрипты на GPU-сервере).
|
|
||||||
#
|
|
||||||
# Физика НЕ дублируется: решётка/равновесие/столкновение/перенос импортируются из
|
|
||||||
# ../solver_2x_sdf (те же модули, что и в финальной модели) — демо тем самым доказывают,
|
|
||||||
# что КОМПОНЕНТЫ финальной модели воспроизводят классические бенчмарки.
|
|
||||||
# Здесь только то, чего в модели нет намеренно: полиномиальное равновесие и BGK
|
|
||||||
# (для сравнений «BGK vs KBC»), H-функция, γ-поле для визуализации, гифки.
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import io as _io
|
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
THEORY = os.path.dirname(HERE)
|
|
||||||
sys.path.insert(0, os.path.join(THEORY, "solver_2x_sdf"))
|
|
||||||
|
|
||||||
import backend as B # GPU-only: жёсткая ошибка без CuPy (CPU-фолбэка нет намеренно)
|
|
||||||
import lattice as L
|
|
||||||
import equilibrium as E
|
|
||||||
import collision as Coll
|
|
||||||
import streaming as Strm
|
|
||||||
import visualization as Viz # make_progress (ASCII-бар)
|
|
||||||
|
|
||||||
cp = B.cp; xp = B.xp; DTYPE = B.DTYPE; to_cpu = B.to_cpu
|
|
||||||
Q = L.Q; CS2 = L.CS2; OPP = L.OPP; Cx = L.Cx; Cy = L.Cy
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
# пути артефактов: фигуры/гифки — общие каталоги theory, числа — demos_gpu/out
|
|
||||||
FIGDIR = os.path.join(THEORY, "figures"); os.makedirs(FIGDIR, exist_ok=True)
|
|
||||||
ANIMDIR = os.path.join(THEORY, "anim"); os.makedirs(ANIMDIR, exist_ok=True)
|
|
||||||
OUTDIR = os.path.join(HERE, "out"); os.makedirs(OUTDIR, exist_ok=True)
|
|
||||||
|
|
||||||
import matplotlib
|
|
||||||
matplotlib.use("Agg")
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
|
|
||||||
try:
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8") # кириллица/греческие в print при cp1251-консоли
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
plt.rcParams.update({
|
|
||||||
"figure.dpi": 110, "savefig.dpi": 120, "font.size": 11,
|
|
||||||
"axes.titlesize": 13, "axes.titleweight": "bold",
|
|
||||||
"axes.grid": True, "grid.alpha": 0.25, "axes.axisbelow": True,
|
|
||||||
"figure.facecolor": "white", "axes.facecolor": "#fbfbfd",
|
|
||||||
})
|
|
||||||
|
|
||||||
# пороги самопроверок под точность бэкенда (демо обычно гоняются в float32)
|
|
||||||
FP64 = (DTYPE == cp.float64)
|
|
||||||
TOL_EXACT = 1e-12 if FP64 else 2e-5 # «точные» тождества (моменты равновесия и т.п.)
|
|
||||||
|
|
||||||
def finish(fig, name):
|
|
||||||
"""Сохранить фигуру в figures/ под тем же именем, что использовал ноутбук."""
|
|
||||||
fig.savefig(os.path.join(FIGDIR, name), bbox_inches="tight")
|
|
||||||
plt.close(fig)
|
|
||||||
print("PNG:", name)
|
|
||||||
|
|
||||||
def write_out(name, lines):
|
|
||||||
"""Дописать результаты в demos_gpu/out/<name> (ноутбук цитирует эти числа в markdown)."""
|
|
||||||
with open(os.path.join(OUTDIR, name), "w", encoding="utf-8") as fh:
|
|
||||||
fh.write("\n".join(lines) + "\n")
|
|
||||||
print("OUT:", name)
|
|
||||||
|
|
||||||
# ----- дополнения к модели (нужны только демо) ------------------------------------------------
|
|
||||||
|
|
||||||
def feq_poly(rho, u):
|
|
||||||
"""Полиномиальное равновесие O(u²) — стандарт BGK (в модели НЕ используется; здесь —
|
|
||||||
для демонстрации «BGK+poly разрушается там, где KBC+entropic держит»)."""
|
|
||||||
CXa = L.CXa[:, None, None]; CYa = L.CYa[:, None, None]; Wa = L.Wa[:, None, None]
|
|
||||||
cu = CXa*u[0] + CYa*u[1]
|
|
||||||
usq = u[0]**2 + u[1]**2
|
|
||||||
return Wa * rho * (1.0 + cu/CS2 + cu**2/(2.0*CS2*CS2) - usq/(2.0*CS2))
|
|
||||||
|
|
||||||
def bgk_collide(f, fe, beta):
|
|
||||||
"""Классический BGK: f − ω(f−feq), ω=2β. Эквивалент KBC при γ=2."""
|
|
||||||
return f - 2.0*beta*(f - fe)
|
|
||||||
|
|
||||||
def kbc_gamma(f, fe, beta):
|
|
||||||
"""Поле энтропийного стабилизатора γ (для визуализации; сам оператор — collision.kbc_collide,
|
|
||||||
формула та же: γ = 1/β − (2−1/β)·⟨Δs|Δh⟩/⟨Δh|Δh⟩)."""
|
|
||||||
dfn = f - fe
|
|
||||||
ds = Coll._project_shift(dfn)
|
|
||||||
dh = dfn - ds; inv = 1.0/fe
|
|
||||||
num = (ds*dh*inv).sum(0); den = (dh*dh*inv).sum(0)
|
|
||||||
nrm = (dfn*dfn*inv).sum(0) # тот же относительный порог, что в collision.py
|
|
||||||
ok = den > B.GREL*nrm
|
|
||||||
return xp.where(ok, 1.0/beta - (2.0 - 1.0/beta)*num/xp.where(ok, den, B.ONE), B.TWO)
|
|
||||||
|
|
||||||
def nu_to_beta(nu):
|
|
||||||
return 1.0 / (2.0 * (nu / CS2 + 0.5))
|
|
||||||
|
|
||||||
def H_function(f):
|
|
||||||
"""Дискретная H-функция H = Σ f_i ln(f_i/w_i) (поле)."""
|
|
||||||
fp = xp.maximum(f, 1e-30)
|
|
||||||
return (fp * xp.log(fp / L.Wa[:, None, None])).sum(0)
|
|
||||||
|
|
||||||
def smoothstep(x):
|
|
||||||
x = min(max(x, 0.0), 1.0)
|
|
||||||
return x*x*(3.0 - 2.0*x)
|
|
||||||
|
|
||||||
# ----- host-постобработка (numpy: кадры уже перенесены на хост) --------------------------------
|
|
||||||
|
|
||||||
def vorticity(u):
|
|
||||||
"""ω = ∂x u_y − ∂y u_x центральными разностями (host, по кадру)."""
|
|
||||||
duy_dx = (np.roll(u[1], -1, 1) - np.roll(u[1], 1, 1)) * 0.5
|
|
||||||
dux_dy = (np.roll(u[0], -1, 0) - np.roll(u[0], 1, 0)) * 0.5
|
|
||||||
return duy_dx - dux_dy
|
|
||||||
|
|
||||||
def render_gif(n_frames, draw_fn, name, figsize, fps=18, dpi=80):
|
|
||||||
"""Отрисовать n_frames кадров (draw_fn(fig, i)) и сохранить зацикленный GIF в anim/."""
|
|
||||||
from PIL import Image
|
|
||||||
path = os.path.join(ANIMDIR, name)
|
|
||||||
fig = plt.figure(figsize=figsize, dpi=dpi)
|
|
||||||
pil = []
|
|
||||||
for i in range(n_frames):
|
|
||||||
fig.clf()
|
|
||||||
draw_fn(fig, i)
|
|
||||||
buf = _io.BytesIO()
|
|
||||||
fig.savefig(buf, format="png")
|
|
||||||
buf.seek(0)
|
|
||||||
pil.append(Image.open(buf).convert("RGB").convert("P", palette=Image.ADAPTIVE))
|
|
||||||
plt.close(fig)
|
|
||||||
pil[0].save(path, save_all=True, append_images=pil[1:],
|
|
||||||
duration=int(1000 / fps), loop=0, optimize=True)
|
|
||||||
print("GIF:", name, f"({n_frames} кадров)")
|
|
||||||
return path
|
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
# Демо-5: блочное измельчение (AMR) на компонентах solver_2x_sdf (amr.patch/ghost/fill/restrict —
|
|
||||||
# ТЕ ЖЕ функции, что в финальной модели). Три части:
|
|
||||||
# 1) минимальный 2-уровневый AMR на цилиндре (без временно́й интерполяции) → anim/amr_cylinder.gif
|
|
||||||
# 2) бенчмарк 5 вариантов против эталона uniform-fine 4× (тест-вихрь) → figures/11d_amr_comparison.png
|
|
||||||
# 3) анимация полного вложенного 2×+4× с временно́й интерполяцией → anim/amr_nested.gif
|
|
||||||
# Числа → out/amr_bench.txt|npz. Запуск: python amr_demo_gpu.py
|
|
||||||
import os
|
|
||||||
import time as _time
|
|
||||||
import numpy as np
|
|
||||||
import _common as cm
|
|
||||||
import amr as Amr # из solver_2x_sdf (sys.path настроен в _common)
|
|
||||||
|
|
||||||
cp = cm.cp; xp = cm.xp; E = cm.E; Coll = cm.Coll; Strm = cm.Strm
|
|
||||||
plt = cm.plt; vorticity = cm.vorticity; OPP = cm.OPP; Q = cm.Q; CS2 = cm.CS2
|
|
||||||
import matplotlib.patches as mpatches
|
|
||||||
|
|
||||||
def _sync():
|
|
||||||
cp.cuda.Device().synchronize()
|
|
||||||
|
|
||||||
# ===== часть 1: минимальный 2-уровневый AMR на цилиндре =========================================
|
|
||||||
def run_amr_cylinder(Nx=120, Ny=60, D=10, U=0.06, Re=120.0, steps=5400,
|
|
||||||
ramp=1000, frame_every=22):
|
|
||||||
cx, cy = Nx//4, Ny//2
|
|
||||||
nu = U*D/Re
|
|
||||||
tau_c = nu/CS2 + 0.5; beta_c = cm.nu_to_beta(nu)
|
|
||||||
tau_f = 2*tau_c - 0.5; beta_f = 1.0/(2*tau_f)
|
|
||||||
Rcf = tau_f/(2*tau_c); Rfc = 1.0/Rcf
|
|
||||||
bx0 = max(cx - 2*D, 2); bx1 = min(cx + 3*D, Nx - 2)
|
|
||||||
by0 = max(cy - 2*D, 2); by1 = min(cy + 2*D, Ny - 2)
|
|
||||||
P = Amr.patch(Nx, Ny, bx0, bx1, by0, by1, 2)
|
|
||||||
Nfx, Nfy = P["Nfx"], P["Nfy"]
|
|
||||||
# маски тела (host → device); стенки канала входят в solid_c
|
|
||||||
Yc_, Xc_ = np.mgrid[0:Ny, 0:Nx]
|
|
||||||
solid_c_np = (Xc_ - cx)**2 + (Yc_ - cy)**2 <= (D/2)**2
|
|
||||||
solid_c_np[0, :] = True; solid_c_np[-1, :] = True
|
|
||||||
fcx, fcy = 2*(cx - bx0), 2*(cy - by0)
|
|
||||||
Yf_, Xf_ = np.mgrid[0:Nfy, 0:Nfx]
|
|
||||||
solid_f_np = (Xf_ - fcx)**2 + (Yf_ - fcy)**2 <= float(D)**2
|
|
||||||
solid_c = cp.asarray(solid_c_np); solid_f = cp.asarray(solid_f_np)
|
|
||||||
block_fluid = cp.asarray(~solid_c_np[by0+1:by1, bx0+1:bx1])
|
|
||||||
onecol = cp.ones((Ny, 1), cm.DTYPE)
|
|
||||||
fc = E.feq(cp.ones((Ny, Nx), cm.DTYPE), cp.zeros((2, Ny, Nx), cm.DTYPE))
|
|
||||||
ff = E.feq(cp.ones((Nfy, Nfx), cm.DTYPE), cp.zeros((2, Nfy, Nfx), cm.DTYPE))
|
|
||||||
frames = []
|
|
||||||
prog = cm.Viz.make_progress("amr-cyl")
|
|
||||||
for t in range(steps + 1):
|
|
||||||
rho, u = E.macros(fc)
|
|
||||||
pre = fc.copy()
|
|
||||||
fc = Coll.kbc_collide(fc, E.feq(rho, u), beta_c)
|
|
||||||
for i in range(Q):
|
|
||||||
fc[i, solid_c] = pre[OPP[i], solid_c]
|
|
||||||
fc = Strm.stream(fc)
|
|
||||||
uin = cp.zeros((2, Ny, 1), cm.DTYPE); uin[0, :, 0] = U*cm.smoothstep(t/ramp)
|
|
||||||
fc[:, 1:-1, 0] = E.feq(onecol, uin)[:, 1:-1, 0]
|
|
||||||
fc[:, 1:-1, -1] = fc[:, 1:-1, -2]
|
|
||||||
# МИНИМАЛЬНЫЙ AMR: один ghost на оба подшага (без временно́й интерполяции)
|
|
||||||
gh = Amr.ghost(fc, P, Rcf)
|
|
||||||
for _ in range(2):
|
|
||||||
rf, uf = E.macros(ff); pref = ff.copy()
|
|
||||||
ff = Coll.kbc_collide(ff, E.feq(rf, uf), beta_f)
|
|
||||||
for i in range(Q):
|
|
||||||
ff[i, solid_f] = pref[OPP[i], solid_f]
|
|
||||||
ff = Strm.stream(ff)
|
|
||||||
ff = Amr.fill(ff, gh)
|
|
||||||
fc = Amr.restrict(ff, fc, P, Rfc, block_fluid)
|
|
||||||
if t % frame_every == 0:
|
|
||||||
frames.append((t, cm.to_cpu(E.macros(fc)[1]), cm.to_cpu(E.macros(ff)[1])))
|
|
||||||
prog(t, steps)
|
|
||||||
return dict(frames=frames, solid_c=solid_c_np, solid_f=solid_f_np, Nx=Nx, Ny=Ny,
|
|
||||||
bx0=bx0, bx1=bx1, by0=by0, by1=by1, tau_c=tau_c, tau_f=tau_f, Rcf=Rcf)
|
|
||||||
|
|
||||||
amr = run_amr_cylinder()
|
|
||||||
print(f"AMR-цилиндр: крупная {amr['Nx']}x{amr['Ny']} + мелкий блок 2x, "
|
|
||||||
f"tau_c={amr['tau_c']:.3f} tau_f={amr['tau_f']:.3f} Rcf={amr['Rcf']:.3f}, "
|
|
||||||
f"кадров={len(amr['frames'])} (устойчиво)")
|
|
||||||
|
|
||||||
_af = amr["frames"]
|
|
||||||
_vmaxA = np.nanpercentile(np.abs(np.where(amr["solid_c"], np.nan,
|
|
||||||
vorticity(_af[len(_af)//2][1]))), 99.0)
|
|
||||||
_cmapA = plt.get_cmap("RdBu_r").copy(); _cmapA.set_bad("#1b1b1b")
|
|
||||||
|
|
||||||
def _draw_amr(fig, i):
|
|
||||||
t, uc, uf = _af[i]
|
|
||||||
ax = fig.add_subplot(111)
|
|
||||||
ax.imshow(np.where(amr["solid_c"], np.nan, vorticity(uc)), origin="lower",
|
|
||||||
cmap=_cmapA, vmin=-_vmaxA, vmax=_vmaxA, extent=(0, amr["Nx"], 0, amr["Ny"]),
|
|
||||||
interpolation="nearest")
|
|
||||||
omf = np.where(amr["solid_f"], np.nan, vorticity(uf))[1:-1, 1:-1]
|
|
||||||
ax.imshow(omf, origin="lower", cmap=_cmapA, vmin=-_vmaxA, vmax=_vmaxA,
|
|
||||||
extent=(amr["bx0"]+0.5, amr["bx1"]-0.5, amr["by0"]+0.5, amr["by1"]-0.5),
|
|
||||||
interpolation="nearest")
|
|
||||||
ax.add_patch(mpatches.Rectangle((amr["bx0"], amr["by0"]),
|
|
||||||
amr["bx1"]-amr["bx0"], amr["by1"]-amr["by0"],
|
|
||||||
fill=False, edgecolor="#ffe600", lw=3.0))
|
|
||||||
ax.text(amr["bx0"]+1, amr["by1"]-4, "мелкий блок 2×", color="#ffe600", fontsize=9, weight="bold")
|
|
||||||
ax.set_title(f"AMR: крупная сетка + мелкий блок 2× (жёлтая рамка) · t={t}", fontsize=10)
|
|
||||||
ax.set_xticks([]); ax.set_yticks([])
|
|
||||||
fig.tight_layout()
|
|
||||||
|
|
||||||
cm.render_gif(len(_af), _draw_amr, "amr_cylinder.gif", (9.2, 4.7), fps=24)
|
|
||||||
|
|
||||||
# ===== часть 2: бенчмарк точность/работа (тест-вихрь, без тел) ==================================
|
|
||||||
_Nc = 48; _xc = _yc = 24.0; _sig = 1.4; _A = 0.115
|
|
||||||
_nu_c = 0.008; _tau_c = _nu_c/CS2 + 0.5; _BSTEPS = 400
|
|
||||||
|
|
||||||
def _vfield(X, Y):
|
|
||||||
rr = (X - _xc)**2 + (Y - _yc)**2; e = cp.exp(-rr/(2*_sig**2))
|
|
||||||
return cm.B.pack(((-_A*(Y - _yc)/_sig**2*e).astype(cm.DTYPE),
|
|
||||||
(_A*(X - _xc)/_sig**2*e).astype(cm.DTYPE)))
|
|
||||||
|
|
||||||
def _grid_v(N, h):
|
|
||||||
xs = cp.arange(N, dtype=cm.DTYPE)*h
|
|
||||||
Xg, Yg = cp.meshgrid(xs, xs)
|
|
||||||
return _vfield(Xg, Yg)
|
|
||||||
|
|
||||||
def _adv(f, beta):
|
|
||||||
r, u = E.macros(f)
|
|
||||||
return Strm.stream(Coll.kbc_collide(f, E.feq(r, u), beta))
|
|
||||||
|
|
||||||
def _sq_patch(pN, a, b, r):
|
|
||||||
return Amr.patch(pN, pN, a, b, a, b, r)
|
|
||||||
|
|
||||||
def _allfluid(P):
|
|
||||||
return cp.ones((P["by"]-P["ay"]-1, P["bx"]-P["ax"]-1), bool)
|
|
||||||
|
|
||||||
def _tauc(tp, r): return r*tp - (r - 1)/2
|
|
||||||
|
|
||||||
def _init_patch(P, a_phys, h_par):
|
|
||||||
pos = a_phys + cp.arange(P["Nfx"], dtype=cm.DTYPE)*(h_par/P["r"])
|
|
||||||
X, Y = cp.meshgrid(pos, pos)
|
|
||||||
return E.feq(cp.ones((P["Nfy"], P["Nfx"]), cm.DTYPE), _vfield(X, Y))
|
|
||||||
|
|
||||||
def _run_uniform(N, h, tau, nsteps):
|
|
||||||
beta = 1/(2*tau); f = E.feq(cp.ones((N, N), cm.DTYPE), _grid_v(N, h))
|
|
||||||
_sync(); t0 = _time.perf_counter()
|
|
||||||
for _ in range(nsteps):
|
|
||||||
f = _adv(f, beta)
|
|
||||||
_sync()
|
|
||||||
return E.macros(f)[1], _time.perf_counter() - t0, N*N*nsteps
|
|
||||||
|
|
||||||
def _run_single(r, a, b, temporal):
|
|
||||||
P = _sq_patch(_Nc, a, b, r); tau_f = _tauc(_tau_c, r)
|
|
||||||
b0, bf = 1/(2*_tau_c), 1/(2*tau_f); Rcf = tau_f/(r*_tau_c); Rfc = 1/Rcf
|
|
||||||
fl = _allfluid(P)
|
|
||||||
f0 = E.feq(cp.ones((_Nc, _Nc), cm.DTYPE), _grid_v(_Nc, 1.0)); ff = _init_patch(P, a, 1.0)
|
|
||||||
_sync(); t0 = _time.perf_counter()
|
|
||||||
for t in range(_BSTEPS):
|
|
||||||
f0o = f0.copy(); f0 = _adv(f0, b0)
|
|
||||||
gho = Amr.ghost(f0o, P, Rcf); ghn = Amr.ghost(f0, P, Rcf)
|
|
||||||
for s in range(r):
|
|
||||||
ff = _adv(ff, bf)
|
|
||||||
w = (s + 1)/r
|
|
||||||
ff = Amr.fill(ff, (1 - w)*gho + w*ghn if temporal else ghn)
|
|
||||||
f0 = Amr.restrict(ff, f0, P, Rfc, fl)
|
|
||||||
_sync()
|
|
||||||
return E.macros(f0)[1], _time.perf_counter() - t0, _Nc*_Nc*_BSTEPS + P["Nfx"]**2*r*_BSTEPS
|
|
||||||
|
|
||||||
def _run_nested(temporal, steps=_BSTEPS, frame_every=0):
|
|
||||||
P1 = _sq_patch(_Nc, 8, 40, 2); P2 = _sq_patch(P1["Nfx"], 16, 48, 2)
|
|
||||||
t1 = _tauc(_tau_c, 2); t2 = _tauc(t1, 2)
|
|
||||||
b0, b1, b2 = 1/(2*_tau_c), 1/(2*t1), 1/(2*t2)
|
|
||||||
R01 = t1/(2*_tau_c); Rf01 = 1/R01; R12 = t2/(2*t1); Rf12 = 1/R12
|
|
||||||
fl1 = _allfluid(P1); fl2 = _allfluid(P2)
|
|
||||||
f0 = E.feq(cp.ones((_Nc, _Nc), cm.DTYPE), _grid_v(_Nc, 1.0))
|
|
||||||
f1 = _init_patch(P1, 8, 1.0); f2 = _init_patch(P2, 16.0, 0.5)
|
|
||||||
frames = []
|
|
||||||
_sync(); t0 = _time.perf_counter()
|
|
||||||
for t in range(steps + (1 if frame_every else 0)):
|
|
||||||
f0o = f0.copy(); f0 = _adv(f0, b0)
|
|
||||||
g1o = Amr.ghost(f0o, P1, R01); g1n = Amr.ghost(f0, P1, R01)
|
|
||||||
for s1 in range(2):
|
|
||||||
f1o = f1.copy(); f1 = _adv(f1, b1)
|
|
||||||
w1 = (s1 + 1)/2
|
|
||||||
f1 = Amr.fill(f1, (1 - w1)*g1o + w1*g1n if temporal else g1n)
|
|
||||||
g2o = Amr.ghost(f1o, P2, R12); g2n = Amr.ghost(f1, P2, R12)
|
|
||||||
for s2 in range(2):
|
|
||||||
f2 = _adv(f2, b2)
|
|
||||||
w2 = (s2 + 1)/2
|
|
||||||
f2 = Amr.fill(f2, (1 - w2)*g2o + w2*g2n if temporal else g2n)
|
|
||||||
f1 = Amr.restrict(f2, f1, P2, Rf12, fl2)
|
|
||||||
f0 = Amr.restrict(f1, f0, P1, Rf01, fl1)
|
|
||||||
if frame_every and t % frame_every == 0:
|
|
||||||
frames.append((t, cm.to_cpu(E.macros(f0)[1]), cm.to_cpu(E.macros(f1)[1]),
|
|
||||||
cm.to_cpu(E.macros(f2)[1])))
|
|
||||||
_sync()
|
|
||||||
cells = _Nc*_Nc*steps + P1["Nfx"]**2*2*steps + P2["Nfx"]**2*4*steps
|
|
||||||
if frame_every:
|
|
||||||
return frames
|
|
||||||
return E.macros(f0)[1], _time.perf_counter() - t0, cells
|
|
||||||
|
|
||||||
print("\nБенчмарк AMR (эталон uniform-fine 4×, физ. время одинаково)...")
|
|
||||||
_uref, _tref, _cref = _run_uniform(_Nc*4, 0.25, _tauc(_tau_c, 4), 4*_BSTEPS)
|
|
||||||
_uref = _uref[:, ::4, ::4]
|
|
||||||
_rms = float(cp.sqrt((_uref[0]**2 + _uref[1]**2).mean()))
|
|
||||||
|
|
||||||
def _err(u):
|
|
||||||
return float(cp.sqrt(((u[0] - _uref[0])**2 + (u[1] - _uref[1])**2).mean()))/_rms
|
|
||||||
|
|
||||||
_bench = []
|
|
||||||
u, t, c = _run_uniform(_Nc, 1.0, _tau_c, _BSTEPS); _bench.append(("uniform-coarse", _err(u), t, c))
|
|
||||||
u, t, c = _run_single(2, 12, 36, False); _bench.append(("AMR 2x (мин., без вр.инт.)", _err(u), t, c))
|
|
||||||
u, t, c = _run_single(4, 12, 36, True); _bench.append(("AMR 4x single (вр.инт.)", _err(u), t, c))
|
|
||||||
u, t, c = _run_nested(False); _bench.append(("AMR 2x+4x nested (без вр.инт.)", _err(u), t, c))
|
|
||||||
u, t, c = _run_nested(True); _bench.append(("AMR 2x+4x nested ПОЛНЫЙ", _err(u), t, c))
|
|
||||||
|
|
||||||
out_lines = [f"reference uniform-fine 4x (192^2): time={_tref:.2f}s cell-updates={_cref:.3e}"]
|
|
||||||
print(f"Эталон uniform-fine 4× (192²): время={_tref:.1f}с, cell-updates={_cref:.2e}")
|
|
||||||
print(f"{'метод':32}{'ε,%':>8}{'cell-upd':>11}{'η=1/(εW)':>10}{'×fine(работа)':>14}")
|
|
||||||
for name, e, t, c in _bench:
|
|
||||||
eta = 1.0/(e*c/1e6)
|
|
||||||
print(f"{name:32}{e*100:>8.2f}{c:>11.2e}{eta:>10.2f}{_cref/c:>14.1f}")
|
|
||||||
out_lines.append(f"{name}: eps_pct={e*100:.3f} cells={c:.3e} eta={eta:.2f} work_vs_fine={_cref/c:.1f}")
|
|
||||||
|
|
||||||
np.savez(os.path.join(cm.OUTDIR, "amr_bench.npz"),
|
|
||||||
names=np.array([b[0] for b in _bench]),
|
|
||||||
eps=np.array([b[1] for b in _bench]),
|
|
||||||
wall=np.array([b[2] for b in _bench]),
|
|
||||||
cells=np.array([b[3] for b in _bench]),
|
|
||||||
cref=_cref, tref=_tref)
|
|
||||||
|
|
||||||
# фигура: ошибка по методам + фронт «точность–работа»
|
|
||||||
names = [b[0] for b in _bench]; errs = np.array([b[1]*100 for b in _bench])
|
|
||||||
cells = np.array([b[3] for b in _bench]); cols = ["#9e9e9e", "#1f6feb", "#2e7d32", "#fb8c00", "#d1495b"]
|
|
||||||
fig, axs = plt.subplots(1, 2, figsize=(13.5, 4.4))
|
|
||||||
axs[0].barh(range(len(names)), errs, color=cols)
|
|
||||||
axs[0].set_yticks(range(len(names))); axs[0].set_yticklabels(names, fontsize=8); axs[0].invert_yaxis()
|
|
||||||
axs[0].set_xlabel("отн. L2-ошибка к эталону, %"); axs[0].set_title("Точность (меньше — лучше)")
|
|
||||||
for i, e in enumerate(errs):
|
|
||||||
axs[0].text(e + 0.05, i, f"{e:.2f}", va="center", fontsize=8)
|
|
||||||
axs[1].scatter(cells, errs, c=cols, s=90, zorder=3, edgecolors="k")
|
|
||||||
for n, c2, e in zip(names, cells, errs):
|
|
||||||
axs[1].annotate(n, (c2, e), fontsize=7, xytext=(5, 4), textcoords="offset points")
|
|
||||||
axs[1].axvline(_cref, color="#d1495b", ls="--", alpha=0.6)
|
|
||||||
axs[1].text(_cref, errs.max()*0.9, " работа эталона", color="#d1495b", fontsize=8)
|
|
||||||
axs[1].set_xscale("log"); axs[1].set_xlabel("cell-updates (работа, лог)"); axs[1].set_ylabel("ошибка, %")
|
|
||||||
axs[1].set_title("Фронт «точность–работа»: AMR ближе к низ-лево (точно и дёшево)")
|
|
||||||
fig.suptitle("AMR: численное сравнение точности и производительности (KBC, тест-вихрь)", fontweight="bold")
|
|
||||||
cm.finish(fig, "11d_amr_comparison.png")
|
|
||||||
|
|
||||||
# ===== часть 3: анимация полного вложенного 2×+4× ===============================================
|
|
||||||
_naf = _run_nested(True, steps=750, frame_every=3)
|
|
||||||
_vmaxN = np.nanpercentile(np.abs(vorticity(_naf[0][1])), 99.0)
|
|
||||||
_cN = plt.get_cmap("RdBu_r").copy(); _cN.set_bad("#1b1b1b")
|
|
||||||
|
|
||||||
def _draw_nested(fig, i):
|
|
||||||
t, u0, u1, u2 = _naf[i]
|
|
||||||
ax = fig.add_subplot(111)
|
|
||||||
ax.imshow(vorticity(u0), origin="lower", cmap=_cN, vmin=-_vmaxN, vmax=_vmaxN,
|
|
||||||
extent=(0, _Nc, 0, _Nc), interpolation="nearest")
|
|
||||||
ax.imshow(vorticity(u1)[1:-1, 1:-1], origin="lower", cmap=_cN, vmin=-_vmaxN, vmax=_vmaxN,
|
|
||||||
extent=(8.5, 39.5, 8.5, 39.5), interpolation="nearest")
|
|
||||||
ax.imshow(vorticity(u2)[1:-1, 1:-1], origin="lower", cmap=_cN, vmin=-_vmaxN, vmax=_vmaxN,
|
|
||||||
extent=(16.25, 31.75, 16.25, 31.75), interpolation="nearest")
|
|
||||||
ax.add_patch(mpatches.Rectangle((8, 8), 32, 32, fill=False, edgecolor="#7CFC00", lw=2.5))
|
|
||||||
ax.add_patch(mpatches.Rectangle((16, 16), 16, 16, fill=False, edgecolor="#ff8a00", lw=2.5))
|
|
||||||
ax.text(8.3, 40.4, "2× блок", color="#7CFC00", fontsize=9, weight="bold")
|
|
||||||
ax.text(16.3, 32.4, "4× блок", color="#ff8a00", fontsize=9, weight="bold")
|
|
||||||
ax.set_title(f"Полный вложенный AMR: коарс + 2× + 4× (видны размеры ячеек) · t={t}", fontsize=10)
|
|
||||||
ax.set_xticks([]); ax.set_yticks([]); fig.tight_layout()
|
|
||||||
|
|
||||||
cm.render_gif(len(_naf), _draw_nested, "amr_nested.gif", (6.2, 6.2), fps=24)
|
|
||||||
|
|
||||||
out_lines.append("PASS AMR: вложенный 2x+4x точнее и дешевле; временная интерполяция снижает ошибку")
|
|
||||||
cm.write_out("amr_bench.txt", out_lines)
|
|
||||||
print("DONE (amr demo)")
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
# Демо-3: каверна с движущейся крышкой (Re=1000) против эталона Ghia, Ghia & Shin (1982).
|
|
||||||
# Стенки — bounce-back; крышка — bounce-back с поправкой импульса −2wᵢρ(cᵢ·u_w)/cs².
|
|
||||||
# Сетка намеренно грубая, BB первого порядка → совпадение качественное.
|
|
||||||
# Артефакты: figures/09_cavity_ghia.png, anim/cavity_transient.gif, out/cavity.txt|npz.
|
|
||||||
# Запуск: python cavity_gpu.py
|
|
||||||
import numpy as np
|
|
||||||
import _common as cm
|
|
||||||
|
|
||||||
cp = cm.cp; xp = cm.xp; E = cm.E; Coll = cm.Coll; Strm = cm.Strm
|
|
||||||
plt = cm.plt; OPP = cm.OPP; Q = cm.Q; CS2 = cm.CS2
|
|
||||||
|
|
||||||
UP = [2, 5, 6] # c_iy = +1
|
|
||||||
DOWN = [4, 7, 8] # c_iy = -1
|
|
||||||
RIGHT = [1, 5, 8] # c_ix = +1
|
|
||||||
LEFT = [3, 6, 7] # c_ix = -1
|
|
||||||
|
|
||||||
def run_cavity(N=110, Re=1000.0, U=0.05, steps=14000, tol=1e-7, check=500, frame_every=58):
|
|
||||||
nu = U*N/Re
|
|
||||||
beta = cm.nu_to_beta(nu)
|
|
||||||
rho = cp.ones((N, N), cm.DTYPE)
|
|
||||||
u = cp.zeros((2, N, N), cm.DTYPE)
|
|
||||||
f = E.feq(rho, u)
|
|
||||||
W = cm.to_cpu(cm.L.Wa).astype(float)
|
|
||||||
prev = None
|
|
||||||
frames = []
|
|
||||||
prog = cm.Viz.make_progress("cavity")
|
|
||||||
t = 0
|
|
||||||
for t in range(steps):
|
|
||||||
rho, u = E.macros(f)
|
|
||||||
if t % frame_every == 0:
|
|
||||||
frames.append((t, cm.to_cpu(u)))
|
|
||||||
fcol = Coll.kbc_collide(f, E.feq(rho, u), beta)
|
|
||||||
f = Strm.stream(fcol)
|
|
||||||
# bounce-back: на каждой стенке достраиваем входящие в область направления
|
|
||||||
for i in UP: # низ (y=0)
|
|
||||||
f[i, 0, :] = fcol[OPP[i], 0, :]
|
|
||||||
for i in RIGHT: # лево (x=0)
|
|
||||||
f[i, :, 0] = fcol[OPP[i], :, 0]
|
|
||||||
for i in LEFT: # право (x=N-1)
|
|
||||||
f[i, :, -1] = fcol[OPP[i], :, -1]
|
|
||||||
for i in DOWN: # верх (крышка) + поправка подвижной стенки
|
|
||||||
io = OPP[i]
|
|
||||||
corr = 2.0*W[io]*(cm.Cx[io]*U)/CS2 # u_w=(U,0): c_y-член равен нулю
|
|
||||||
f[i, -1, :] = fcol[io, -1, :] - corr
|
|
||||||
if t % check == 0 and t > 0:
|
|
||||||
cur = u.copy()
|
|
||||||
if prev is not None and float(xp.abs(cur - prev).max()) < tol:
|
|
||||||
print(f"\ncavity: сошлось по полю скорости на шаге {t}")
|
|
||||||
break
|
|
||||||
prev = cur
|
|
||||||
prog(t, steps - 1)
|
|
||||||
rho, u = E.macros(f)
|
|
||||||
return dict(N=N, Re=Re, U=U, u=cm.to_cpu(u), steps=t, frames=frames)
|
|
||||||
|
|
||||||
cav = run_cavity()
|
|
||||||
print(f"Каверна: Re={cav['Re']:.0f}, N={cav['N']}, остановлено на шаге {cav['steps']}")
|
|
||||||
|
|
||||||
# Эталон Ghia, Ghia & Shin (1982), Re=1000.
|
|
||||||
ghia_y = np.array([0.0000, 0.0547, 0.0625, 0.0703, 0.1016, 0.1719, 0.2813, 0.4531,
|
|
||||||
0.5000, 0.6172, 0.7344, 0.8516, 0.9531, 0.9609, 0.9688, 0.9766, 1.0000])
|
|
||||||
ghia_u = np.array([0.0000, -0.18109, -0.20196, -0.22220, -0.29730, -0.38289, -0.27805,
|
|
||||||
-0.10648, -0.06080, 0.05702, 0.18719, 0.33304, 0.46604, 0.51117,
|
|
||||||
0.57492, 0.65928, 1.0000])
|
|
||||||
ghia_x = np.array([0.0000, 0.0625, 0.0703, 0.0781, 0.0938, 0.1563, 0.2266, 0.2344,
|
|
||||||
0.5000, 0.8047, 0.8594, 0.9063, 0.9453, 0.9531, 0.9609, 0.9688, 1.0000])
|
|
||||||
ghia_v = np.array([0.0000, 0.27485, 0.29012, 0.30353, 0.32627, 0.37095, 0.33075, 0.32235,
|
|
||||||
0.02526, -0.31966, -0.42665, -0.51550, -0.39188, -0.33714, -0.27669,
|
|
||||||
-0.21388, 0.0000])
|
|
||||||
|
|
||||||
Ncav = cav["N"]
|
|
||||||
yc = (np.arange(Ncav) + 0.5)/Ncav
|
|
||||||
xc = (np.arange(Ncav) + 0.5)/Ncav
|
|
||||||
u_centerline = cav["u"][0, :, Ncav//2]/cav["U"]
|
|
||||||
v_centerline = cav["u"][1, Ncav//2, :]/cav["U"]
|
|
||||||
|
|
||||||
fig, axs = plt.subplots(1, 3, figsize=(14, 4.4))
|
|
||||||
spd = np.sqrt(cav["u"][0]**2 + cav["u"][1]**2)/cav["U"]
|
|
||||||
axs[0].imshow(spd, origin="lower", cmap="viridis", extent=[0, 1, 0, 1])
|
|
||||||
sx = np.linspace(0, 1, Ncav)
|
|
||||||
axs[0].streamplot(sx, sx, cav["u"][0], cav["u"][1], density=1.1, color="white", linewidth=0.6)
|
|
||||||
axs[0].set_title("Линии тока и $|\\mathbf{u}|/U$"); axs[0].set_xlabel("$x/L$"); axs[0].set_ylabel("$y/L$")
|
|
||||||
axs[1].plot(u_centerline, yc, "-", color="#1f6feb", label="KBC")
|
|
||||||
axs[1].plot(ghia_u, ghia_y, "o", ms=5, mfc="none", color="#d1495b", label="Ghia 1982")
|
|
||||||
axs[1].set_title("$u_x/U$ по вертикали $x=0.5$"); axs[1].set_xlabel("$u_x/U$")
|
|
||||||
axs[1].set_ylabel("$y/L$"); axs[1].legend()
|
|
||||||
axs[2].plot(xc, v_centerline, "-", color="#1f6feb", label="KBC")
|
|
||||||
axs[2].plot(ghia_x, ghia_v, "o", ms=5, mfc="none", color="#d1495b", label="Ghia 1982")
|
|
||||||
axs[2].set_title("$u_y/U$ по горизонтали $y=0.5$"); axs[2].set_xlabel("$x/L$")
|
|
||||||
axs[2].set_ylabel("$u_y/U$"); axs[2].legend()
|
|
||||||
fig.suptitle("Демо-3: каверна с движущейся крышкой, Re=1000 (KBC vs Ghia)",
|
|
||||||
y=1.04, fontweight="bold")
|
|
||||||
cm.finish(fig, "09_cavity_ghia.png")
|
|
||||||
|
|
||||||
u_interp = np.interp(ghia_y, yc, u_centerline)
|
|
||||||
rms = float(np.sqrt(np.mean((u_interp - ghia_u)**2)))
|
|
||||||
print(f"Каверна: RMS-отклонение u_x от Ghia ~ {rms:.3f} (грубое BB → допустимо)")
|
|
||||||
|
|
||||||
# --- гифка становления течения -----------------------------------------------------------------
|
|
||||||
_cf = cav["frames"]
|
|
||||||
_vmax = max(np.sqrt(u[0]**2 + u[1]**2).max() for _, u in _cf)/cav["U"]
|
|
||||||
|
|
||||||
def _draw(fig, i):
|
|
||||||
t, u = _cf[i]
|
|
||||||
ax = fig.add_subplot(111)
|
|
||||||
sp = np.sqrt(u[0]**2 + u[1]**2)/cav["U"]
|
|
||||||
ax.imshow(sp, origin="lower", cmap="viridis", vmin=0, vmax=_vmax, extent=[0, 1, 0, 1])
|
|
||||||
ax.set_title(f"Каверна Re=1000 · t={t} · $|\\mathbf{{u}}|/U$", fontsize=10)
|
|
||||||
ax.set_xlabel("$x/L$"); ax.set_ylabel("$y/L$")
|
|
||||||
fig.tight_layout()
|
|
||||||
|
|
||||||
cm.render_gif(len(_cf), _draw, "cavity_transient.gif", (4.9, 4.5), fps=24)
|
|
||||||
|
|
||||||
np.savez(__import__("os").path.join(cm.OUTDIR, "cavity.npz"),
|
|
||||||
yc=yc, xc=xc, u_centerline=u_centerline, v_centerline=v_centerline,
|
|
||||||
ghia_y=ghia_y, ghia_u=ghia_u, ghia_x=ghia_x, ghia_v=ghia_v, rms=rms,
|
|
||||||
Re=cav["Re"], N=cav["N"], steps=cav["steps"])
|
|
||||||
cm.write_out("cavity.txt", [
|
|
||||||
f"N={cav['N']} Re={cav['Re']:.0f} U={cav['U']} stopped_at={cav['steps']}",
|
|
||||||
f"rms_ux_vs_Ghia={rms:.4f}",
|
|
||||||
"PASS каверна: качественное совпадение с Ghia (BB первого порядка)",
|
|
||||||
])
|
|
||||||
print("DONE (cavity)")
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
# Самопроверки математики НА КОМПОНЕНТАХ ФИНАЛЬНОЙ МОДЕЛИ (solver_2x_sdf): решётка,
|
|
||||||
# энтропийное равновесие, KBC-проектор, предел γ=2 ≡ BGK. Результаты → out/checks.txt
|
|
||||||
# (ноутбук цитирует их в markdown). Запуск: python checks_gpu.py [AMR_FP64=1 — жёстче пороги].
|
|
||||||
import numpy as np
|
|
||||||
import _common as cm
|
|
||||||
|
|
||||||
cp = cm.cp; xp = cm.xp; L = cm.L; E = cm.E; Coll = cm.Coll
|
|
||||||
Q = cm.Q; CS2 = cm.CS2; TOL = cm.TOL_EXACT
|
|
||||||
|
|
||||||
lines = [f"backend={cm.B.BACKEND}; порог точных тождеств TOL={TOL:g}"]
|
|
||||||
def check(name, value, tol):
|
|
||||||
ok = value < tol
|
|
||||||
lines.append(f"{'PASS' if ok else 'FAIL'} {name}: {value:.3e} (tol {tol:g})")
|
|
||||||
print(lines[-1])
|
|
||||||
assert ok, name
|
|
||||||
return ok
|
|
||||||
|
|
||||||
def check_gt(name, value, floor):
|
|
||||||
"""Для величин, которые обязаны быть НЕ малы (иначе модель выродилась)."""
|
|
||||||
ok = value > floor
|
|
||||||
lines.append(f"{'PASS' if ok else 'FAIL'} {name}: {value:.3e} (должно быть > {floor:g})")
|
|
||||||
print(lines[-1])
|
|
||||||
assert ok, name
|
|
||||||
return ok
|
|
||||||
|
|
||||||
# --- 1. Решётка D2Q9: тождества весов и изотропии ---------------------------------------------
|
|
||||||
# Допуск по типу: Wa хранится в DTYPE, и в float32 уже само Σw отличается от 1 на ~7e-9,
|
|
||||||
# так что жёсткий 1e-12 здесь означал бы падение проверки в режиме по умолчанию.
|
|
||||||
TOL_W = 1e-12 if cm.FP64 else 1e-6
|
|
||||||
C = np.array(list(zip(L.Cx, L.Cy)), float)
|
|
||||||
W = cm.to_cpu(L.Wa).astype(float)
|
|
||||||
check("Σw − 1", abs(W.sum() - 1.0), TOL_W)
|
|
||||||
check("|Σ w·c|", np.abs((W[:, None]*C).sum(0)).max(), TOL_W)
|
|
||||||
check("|Σ w·c⊗c − cs²·I|",
|
|
||||||
np.abs((W[:, None, None]*C[:, :, None]*C[:, None, :]).sum(0) - CS2*np.eye(2)).max(), TOL_W)
|
|
||||||
assert np.all(C[list(L.OPP)] == -C), "OPP: c_opp != -c"
|
|
||||||
lines.append("PASS OPP: c_opp = −c (точно)")
|
|
||||||
|
|
||||||
# --- 2. Энтропийное равновесие (product-form) точно воспроизводит ρ и ρu -----------------------
|
|
||||||
rng = np.random.default_rng(0)
|
|
||||||
rho_t = xp.asarray(1.0 + 0.1*rng.standard_normal((16, 16)), cm.DTYPE)
|
|
||||||
u_t = xp.asarray(0.06*rng.standard_normal((2, 16, 16)), cm.DTYPE)
|
|
||||||
fe = E.feq(rho_t, u_t)
|
|
||||||
rho_m, u_m = E.macros(fe)
|
|
||||||
check("feq: |Σf − ρ|", float(xp.abs(rho_m - rho_t).max()), TOL)
|
|
||||||
check("feq: |u(f) − u|", float(xp.abs(u_m - u_t).max()), TOL)
|
|
||||||
Pxx = (L.CXa[:, None, None]**2 * fe).sum(0)
|
|
||||||
check("feq: |Σc_x²f − ρ(cs²+u_x²)| (мал при малом Ma)",
|
|
||||||
float(xp.abs(Pxx - rho_t*(CS2 + u_t[0]**2)).max()), 5e-3)
|
|
||||||
|
|
||||||
# --- 3. KBC-проектор Ps: идемпотентность и состав сдвиг-моментов --------------------------------
|
|
||||||
Ps = cm.to_cpu(L.Ps).astype(float)
|
|
||||||
check("Ps идемпотентен: |Ps·Ps − Ps|", np.abs(Ps @ Ps - Ps).max(), 1e-5 if not cm.FP64 else 1e-12)
|
|
||||||
# моментная матрица (как в lattice._build_projector): Δs несёт ТОЛЬКО {cx²−cy², cx·cy}
|
|
||||||
cx, cy = C[:, 0], C[:, 1]
|
|
||||||
M = np.zeros((Q, Q))
|
|
||||||
M[0] = 1.0; M[1] = cx; M[2] = cy; M[3] = 3*(cx**2 + cy**2) - 2
|
|
||||||
M[4] = cx**2 - cy**2; M[5] = cx*cy
|
|
||||||
M[6] = cx**2*cy; M[7] = cx*cy**2; M[8] = cx**2*cy**2
|
|
||||||
dfn = rng.standard_normal((Q, 5, 7))
|
|
||||||
ds = np.einsum("ij,jab->iab", Ps, dfn)
|
|
||||||
mds = np.einsum("ai,ijk->ajk", M, ds)
|
|
||||||
other = [0, 1, 2, 3, 6, 7, 8]
|
|
||||||
check("Ps: |моменты Δs вне {N, Πxy}|", np.abs(mds[other]).max(), 1e-4 if not cm.FP64 else 1e-10)
|
|
||||||
lines.append("PASS состав KBC-N1: s = {Πxx−Πyy, Πxy}, h = остальное (k отщеплён точно)")
|
|
||||||
|
|
||||||
# --- 4. Предел γ=2: KBC-форма с γ=2 совпадает с BGK (через РЕАЛЬНЫЙ проектор модели) ------------
|
|
||||||
f0 = fe + xp.asarray(0.01*rng.standard_normal((Q, 16, 16)), cm.DTYPE)
|
|
||||||
r0, u0 = E.macros(f0)
|
|
||||||
fe0 = E.feq(r0, u0)
|
|
||||||
beta = 0.7
|
|
||||||
dfn0 = f0 - fe0
|
|
||||||
ds0 = Coll._project_shift(dfn0)
|
|
||||||
fk2 = f0 - beta*(2.0*ds0 + 2.0*(dfn0 - ds0)) # KBC с γ=2
|
|
||||||
fbgk = cm.bgk_collide(f0, fe0, beta)
|
|
||||||
check("KBC(γ=2) ≡ BGK", float(xp.abs(fk2 - fbgk).max()), TOL)
|
|
||||||
|
|
||||||
# --- 5. kbc_collide сохраняет ρ и импульс -------------------------------------------------------
|
|
||||||
fpost = Coll.kbc_collide(f0, fe0, beta)
|
|
||||||
r1, u1 = E.macros(fpost)
|
|
||||||
check("KBC: |Δρ| за столкновение", float(xp.abs(r1 - r0).max()), TOL)
|
|
||||||
check("KBC: |Δ(ρu)| за столкновение", float(xp.abs(u1*r1[None] - u0*r0[None]).max()), TOL)
|
|
||||||
|
|
||||||
# --- 6. Стабилизатор γ ДЕЙСТВИТЕЛЬНО считается на физическом поле ------------------------------
|
|
||||||
# Регрессия на дефект от 2026-08-14: абсолютный порог знаменателя (1e-6 в fp32) отправлял в откат
|
|
||||||
# γ←2 77–99% узлов, т.е. модель молча вырождалась в LBGK. Проверяем на РАЗВИТОМ поле (сдвиговый
|
|
||||||
# слой после прогрева), что откат не срабатывает и γ не залипает на 2.
|
|
||||||
Strm = cm.Strm
|
|
||||||
Nsl = 64; u0sl = 0.04; beta_sl = cm.nu_to_beta(u0sl*Nsl/5000.0)
|
|
||||||
yy = xp.arange(Nsl, dtype=cm.DTYPE)[:, None] * xp.ones((1, Nsl), cm.DTYPE)
|
|
||||||
xx = xp.ones((Nsl, 1), cm.DTYPE) * xp.arange(Nsl, dtype=cm.DTYPE)[None, :]
|
|
||||||
uxsl = xp.where(yy <= Nsl/2, u0sl*xp.tanh(40.0*(yy/Nsl - 0.25)), u0sl*xp.tanh(40.0*(0.75 - yy/Nsl)))
|
|
||||||
uysl = 0.05*u0sl*xp.sin(2.0*np.pi*(xx/Nsl + 0.25))
|
|
||||||
fsl = E.feq(xp.ones((Nsl, Nsl), cm.DTYPE), cm.B.pack((uxsl, uysl)))
|
|
||||||
for _ in range(400):
|
|
||||||
rsl, usl = E.macros(fsl)
|
|
||||||
fsl = Strm.stream(Coll.kbc_collide(fsl, E.feq(rsl, usl), beta_sl))
|
|
||||||
rsl, usl = E.macros(fsl); fesl = E.feq(rsl, usl)
|
|
||||||
dfs = fsl - fesl; dss = Coll._project_shift(dfs); dhs = dfs - dss; invs = 1.0/fesl
|
|
||||||
den_s = (dhs*dhs*invs).sum(0); nrm_s = (dfs*dfs*invs).sum(0)
|
|
||||||
frac_fb = float((den_s <= cm.B.GREL*nrm_s).mean())
|
|
||||||
check("γ: доля узлов в откате γ←2 на развитом поле", frac_fb, 1e-3)
|
|
||||||
gam = cm.kbc_gamma(fsl, fesl, beta_sl)
|
|
||||||
# при полном вырождении в LBGK эта величина строго 0, при частичном (90% узлов) — ~0.01,
|
|
||||||
# наблюдаемое значение ~0.13, поэтому порог 0.05 ловит регресс с запасом и не флапает
|
|
||||||
check_gt("γ: ⟨|γ−2|⟩ — отличие от LBGK", float(xp.abs(gam - 2.0).mean()), 0.05)
|
|
||||||
lines.append(f"INFO γ на развитом поле: ⟨γ⟩={float(gam.mean()):.4f} "
|
|
||||||
f"[{float(gam.min()):.3f}, {float(gam.max()):.3f}], min den/‖Δ‖²="
|
|
||||||
f"{float((den_s/xp.maximum(nrm_s, 1e-30)).min()):.2e} (порог {cm.B.GREL:g})")
|
|
||||||
print(lines[-1])
|
|
||||||
|
|
||||||
# --- 7. Замкнутая формула γ ур.(17)/(39) против корня точного ур.(15) --------------------------
|
|
||||||
# Берём один узел развитого поля и решаем Σ Δh·ln(1 + ((1−βγ)Δh − (2β−1)Δs)/feq) = 0 бисекцией.
|
|
||||||
iy, ix = Nsl//3, Nsl//3
|
|
||||||
fe_n = cm.to_cpu(fesl[:, iy, ix]).astype(float)
|
|
||||||
ds_n = cm.to_cpu(dss[:, iy, ix]).astype(float); dh_n = cm.to_cpu(dhs[:, iy, ix]).astype(float)
|
|
||||||
g_formula = float(cm.to_cpu(gam[iy, ix]))
|
|
||||||
def _crit(g):
|
|
||||||
return float((dh_n*np.log(1.0 + ((1.0 - beta_sl*g)*dh_n - (2.0*beta_sl - 1.0)*ds_n)/fe_n)).sum())
|
|
||||||
lo, hi = g_formula - 2.0, g_formula + 2.0
|
|
||||||
while _crit(lo)*_crit(hi) > 0 and hi - lo < 40:
|
|
||||||
lo -= 1.0; hi += 1.0
|
|
||||||
for _ in range(200):
|
|
||||||
mid = 0.5*(lo + hi)
|
|
||||||
if _crit(lo)*_crit(mid) <= 0: hi = mid
|
|
||||||
else: lo = mid
|
|
||||||
check("γ: |формула − точный корень ур.(15)|", abs(g_formula - 0.5*(lo + hi)), 5e-2)
|
|
||||||
|
|
||||||
# --- 8. Гидродинамический предел: ν по затуханию сдвиговой волны --------------------------------
|
|
||||||
# Единственная проверка, которая ловит подмену оператора: LBGK и KBC дают одну ν, но неверная
|
|
||||||
# релаксация сдвиг-моментов сразу видна здесь.
|
|
||||||
Nw = 48; kw = 2.0*np.pi/Nw; NSW = 1500
|
|
||||||
sinw = xp.sin(kw*xp.arange(Nw, dtype=cm.DTYPE))
|
|
||||||
for tau_w in (0.6, 1.0):
|
|
||||||
beta_w = 1.0/(2.0*tau_w); nu_th = CS2*(tau_w - 0.5)
|
|
||||||
# амплитуда 1e-2 (Ma≈0.017, режим ещё линейный): в float32 при 1e-4 сигнал тонет в округлении
|
|
||||||
uxw = 0.01*sinw[:, None]*xp.ones((1, 4), cm.DTYPE)
|
|
||||||
fw = E.feq(xp.ones((Nw, 4), cm.DTYPE), cm.B.pack((uxw, xp.zeros_like(uxw))))
|
|
||||||
amps = []
|
|
||||||
for _ in range(NSW + 1):
|
|
||||||
rw, uw = E.macros(fw)
|
|
||||||
amps.append(abs(float((uw[0][:, 0]*sinw).sum()))) # проекция на моду, без abs поэлементно
|
|
||||||
fw = Strm.stream(Coll.kbc_collide(fw, E.feq(rw, uw), beta_w))
|
|
||||||
a = np.array(amps); i0 = NSW//3
|
|
||||||
rate = -np.polyfit(np.arange(i0, NSW + 1), np.log(a[i0:]), 1)[0]
|
|
||||||
# допуск 1e-2 покрывает решёточную дисперсию O(k²) (k=0.131 → ~3e-3)
|
|
||||||
check(f"ν(τ={tau_w}): отн. ошибка затухания сдвиговой волны", abs(rate/(kw*kw)/nu_th - 1.0), 1e-2)
|
|
||||||
|
|
||||||
# --- 9. ГУ канала: вход и выход не связаны заворотом roll (включая углы) ------------------------
|
|
||||||
# Регрессия на дефект от 2026-08-14: Zou-He стоял на срезе 1:-1, и в 4 углах stream() через roll
|
|
||||||
# сносил популяции прямо из столбца выхода в столбец входа.
|
|
||||||
import boundary as Bnd
|
|
||||||
Ny_b, Nx_b = 24, 32
|
|
||||||
fb = E.feq(xp.ones((Ny_b, Nx_b), cm.DTYPE), 0.02*xp.ones((2, Ny_b, Nx_b), cm.DTYPE))
|
|
||||||
fb = fb*(1.0 + xp.asarray(0.05*rng.standard_normal(fb.shape), cm.DTYPE))
|
|
||||||
post_b = fb.copy()
|
|
||||||
uin_b = xp.zeros((2, Ny_b, 1), cm.DTYPE); uin_b[0] = 0.05
|
|
||||||
fs_b = Bnd.channel_bc(Bnd.free_slip_walls(Strm.stream(post_b)), uin_b, outlet_uy="zero")
|
|
||||||
r_in = fs_b[:, :, 0].sum(0)
|
|
||||||
ux_in = (L.CXa[:, None]*fs_b[:, :, 0]).sum(0)/r_in
|
|
||||||
check("ГУ: |u_x(вход) − заданной| по ВСЕМ рядам, вкл. углы", float(xp.abs(ux_in - 0.05).max()), TOL)
|
|
||||||
check("ГУ: |ρ(выход) − 1| по ВСЕМ рядам, вкл. углы",
|
|
||||||
float(xp.abs(fs_b[:, :, -1].sum(0) - 1.0).max()), TOL)
|
|
||||||
leak = min(float(xp.abs(fs_b[i, row, 0] - post_b[i, row, -1]).max())
|
|
||||||
for i in (1, 5, 8) for row in (0, -1))
|
|
||||||
check_gt("ГУ: расхождение углового f с популяцией выхода (заворот подавлен)", leak, 1e-6)
|
|
||||||
|
|
||||||
lines.append("=== Все самопроверки пройдены ===")
|
|
||||||
print(lines[-1])
|
|
||||||
cm.write_out("checks.txt", lines)
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
# Статические иллюстрации (НЕ симуляции): стенсили D2Q9/D3Q27, блок-схема шага KBC,
|
|
||||||
# схема блочного измельчения. Чистый numpy+matplotlib — НЕ импортирует CuPy и потому
|
|
||||||
# исполним и локально (единственный скрипт demos_gpu без GPU).
|
|
||||||
# Схема измельчения использует фон |ω| из out/obstacle_circle_field.npz (его пишет
|
|
||||||
# obstacle_gpu.py на сервере); без него рисуется нейтральный фон.
|
|
||||||
# Артефакты: figures/01_stencil_d2q9.png, 06_algorithm_flow.png, 11c_refinement_schematic.png,
|
|
||||||
# 11_d3q27.png. Запуск: python figures_static.py
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import numpy as np
|
|
||||||
import matplotlib
|
|
||||||
matplotlib.use("Agg")
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import matplotlib.patches as mpatches
|
|
||||||
from matplotlib.lines import Line2D
|
|
||||||
|
|
||||||
try:
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
THEORY = os.path.dirname(HERE)
|
|
||||||
FIGDIR = os.path.join(THEORY, "figures"); os.makedirs(FIGDIR, exist_ok=True)
|
|
||||||
OUTDIR = os.path.join(HERE, "out")
|
|
||||||
|
|
||||||
plt.rcParams.update({
|
|
||||||
"figure.dpi": 110, "savefig.dpi": 120, "font.size": 11,
|
|
||||||
"axes.titlesize": 13, "axes.titleweight": "bold",
|
|
||||||
"axes.grid": True, "grid.alpha": 0.25, "axes.axisbelow": True,
|
|
||||||
"figure.facecolor": "white", "axes.facecolor": "#fbfbfd",
|
|
||||||
})
|
|
||||||
|
|
||||||
def finish(fig, name):
|
|
||||||
fig.savefig(os.path.join(FIGDIR, name), bbox_inches="tight")
|
|
||||||
plt.close(fig)
|
|
||||||
print("PNG:", name)
|
|
||||||
|
|
||||||
# D2Q9 (та же нумерация, что в solver_2x_sdf/lattice.py)
|
|
||||||
CX = np.array([0, 1, 0, -1, 0, 1, -1, -1, 1], float)
|
|
||||||
CY = np.array([0, 0, 1, 0, -1, 1, 1, -1, -1], float)
|
|
||||||
W = np.array([4/9, 1/9, 1/9, 1/9, 1/9, 1/36, 1/36, 1/36, 1/36])
|
|
||||||
CS2 = 1.0/3.0
|
|
||||||
|
|
||||||
# --- 1. Стенсиль D2Q9 ---------------------------------------------------------------------------
|
|
||||||
fig, ax = plt.subplots(figsize=(5.2, 5.2))
|
|
||||||
tier_color = {4/9: "#d1495b", 1/9: "#2e7d32", 1/36: "#1f6feb"}
|
|
||||||
for i in range(9):
|
|
||||||
col = tier_color[W[i]]
|
|
||||||
if i == 0:
|
|
||||||
ax.plot(0, 0, "o", ms=18, color=col, zorder=3)
|
|
||||||
ax.annotate(f"$w_0={W[i]:.3f}$", (0, 0), textcoords="offset points",
|
|
||||||
xytext=(8, 8), fontsize=9)
|
|
||||||
else:
|
|
||||||
ax.annotate("", xy=(CX[i], CY[i]), xytext=(0, 0),
|
|
||||||
arrowprops=dict(arrowstyle="-|>", lw=1.5 + 40*W[i], color=col))
|
|
||||||
ax.annotate(f"$c_{{{i}}}$", (CX[i], CY[i]), textcoords="offset points",
|
|
||||||
xytext=(6*np.sign(CX[i] + 0.1), 6*np.sign(CY[i] + 0.1)), fontsize=9)
|
|
||||||
ax.set_xlim(-1.6, 1.6); ax.set_ylim(-1.6, 1.6); ax.set_aspect("equal")
|
|
||||||
ax.set_title("Решётка D2Q9: дискретные скорости $\\mathbf{c}_i$ и веса $w_i$")
|
|
||||||
ax.set_xlabel("$c_x$ [lu/ts]"); ax.set_ylabel("$c_y$ [lu/ts]")
|
|
||||||
ax.legend(handles=[Line2D([0], [0], color=c, lw=3, label=f"$w={k:.4f}$")
|
|
||||||
for k, c in tier_color.items()], loc="upper right", fontsize=8)
|
|
||||||
finish(fig, "01_stencil_d2q9.png")
|
|
||||||
|
|
||||||
# --- 2. Блок-схема шага KBC ---------------------------------------------------------------------
|
|
||||||
fig, ax = plt.subplots(figsize=(4.3, 6.2))
|
|
||||||
ax.set_axis_off()
|
|
||||||
steps_txt = [
|
|
||||||
("Макро-моменты\n$\\rho=\\sum f_i,\\ \\rho\\mathbf{u}=\\sum \\mathbf{c}_i f_i$", "#e8f0fe"),
|
|
||||||
("Равновесие $f_i^{eq}$\n(энтропийное)", "#e6f4ea"),
|
|
||||||
("Неравновесие\n$\\Delta s=P_s(f-f^{eq}),\\ \\Delta h=(f-f^{eq})-\\Delta s$", "#fff4e5"),
|
|
||||||
("Стабилизатор $\\gamma$\n$=1/\\beta-(2-1/\\beta)\\frac{\\langle\\Delta s|\\Delta h\\rangle}{\\langle\\Delta h|\\Delta h\\rangle}$", "#fde7ef"),
|
|
||||||
("Столкновение\n$f\\leftarrow f-\\beta(2\\Delta s+\\gamma\\Delta h)$", "#f3e8fd"),
|
|
||||||
("Перенос\n$f_i(\\mathbf{x}+\\mathbf{c}_i)\\leftarrow f_i(\\mathbf{x})$", "#e8f0fe"),
|
|
||||||
("Граничные условия", "#eceff1"),
|
|
||||||
]
|
|
||||||
y = 0.95
|
|
||||||
for txt, col in steps_txt:
|
|
||||||
ax.add_patch(plt.Rectangle((0.05, y - 0.1), 0.9, 0.085, transform=ax.transAxes,
|
|
||||||
facecolor=col, edgecolor="#555", lw=1.2))
|
|
||||||
ax.text(0.5, y - 0.057, txt, transform=ax.transAxes, ha="center", va="center", fontsize=8.5)
|
|
||||||
if y > 0.2:
|
|
||||||
ax.annotate("", xy=(0.5, y - 0.118), xytext=(0.5, y - 0.1), xycoords="axes fraction",
|
|
||||||
arrowprops=dict(arrowstyle="-|>", color="#555"))
|
|
||||||
y -= 0.13
|
|
||||||
ax.set_title("Шаг KBC")
|
|
||||||
finish(fig, "06_algorithm_flow.png")
|
|
||||||
|
|
||||||
# --- 3. Схема блочного измельчения (фон — |ω| цилиндра, если есть артефакт) ----------------------
|
|
||||||
def _vorticity(u):
|
|
||||||
duy_dx = (np.roll(u[1], -1, 1) - np.roll(u[1], 1, 1))*0.5
|
|
||||||
dux_dy = (np.roll(u[0], -1, 0) - np.roll(u[0], 1, 0))*0.5
|
|
||||||
return duy_dx - dux_dy
|
|
||||||
|
|
||||||
field_path = os.path.join(OUTDIR, "obstacle_circle_field.npz")
|
|
||||||
schematic_path = os.path.join(FIGDIR, "11c_refinement_schematic.png")
|
|
||||||
if not os.path.exists(field_path) and os.path.exists(schematic_path):
|
|
||||||
# без серверного фона не затираем уже существующую схему с |ω|-подложкой
|
|
||||||
print("(нет out/obstacle_circle_field.npz — оставляю существующий 11c_refinement_schematic.png; "
|
|
||||||
"запустите obstacle_gpu.py на сервере и повторите)")
|
|
||||||
else:
|
|
||||||
fig, ax = plt.subplots(figsize=(11, 5.6))
|
|
||||||
if os.path.exists(field_path):
|
|
||||||
d = np.load(field_path)
|
|
||||||
u = d["u"]; solid = d["solid"]
|
|
||||||
Nx, Ny = int(d["Nx"]), int(d["Ny"]); cx, cy, D = int(d["cx"]), int(d["cy"]), int(d["D"])
|
|
||||||
vmag = np.where(solid, np.nan, np.abs(_vorticity(u)))
|
|
||||||
ax.imshow(vmag, origin="lower", cmap="inferno", vmax=np.nanpercentile(vmag, 99))
|
|
||||||
else:
|
|
||||||
Nx, Ny, cx, cy, D = 150, 72, 37, 36, 14
|
|
||||||
ax.add_patch(plt.Circle((cx, cy), D/2, color="#37474f"))
|
|
||||||
ax.set_facecolor("#10101a")
|
|
||||||
|
|
||||||
def _grid_block(x0, y0, w, h, step, color, label):
|
|
||||||
ax.add_patch(mpatches.Rectangle((x0, y0), w, h, fill=False, edgecolor=color, lw=2.2))
|
|
||||||
for xx in np.arange(x0, x0 + w + 0.1, step):
|
|
||||||
ax.plot([xx, xx], [y0, y0 + h], color=color, lw=0.4, alpha=0.45)
|
|
||||||
for yy in np.arange(y0, y0 + h + 0.1, step):
|
|
||||||
ax.plot([x0, x0 + w], [yy, yy], color=color, lw=0.4, alpha=0.45)
|
|
||||||
ax.text(x0 + 1, y0 + h - 4, label, color=color, fontsize=8, weight="bold")
|
|
||||||
|
|
||||||
_grid_block(0, 0, Nx - 1, Ny - 1, 16, "#4fc3f7", "уровень 0: Δx (крупный)")
|
|
||||||
_grid_block(cx - 1.5*D, cy - 1.9*D, 6.5*D, 3.8*D, 8, "#aed581", "уровень 1: Δx/2 (тело+след)")
|
|
||||||
_grid_block(cx - 1.1*D, cy - 1.3*D, 2.6*D, 2.6*D, 4, "#ff8a65", "уровень 2: Δx/4 (у тела)")
|
|
||||||
ax.set_xlim(0, Nx - 1); ax.set_ylim(0, Ny - 1)
|
|
||||||
ax.set_title("Схема блочного измельчения вокруг тела (поверх $|\\omega|$): "
|
|
||||||
"сетка мельче там, где градиенты резче")
|
|
||||||
ax.set_xlabel("x [lu]"); ax.set_ylabel("y [lu]")
|
|
||||||
finish(fig, "11c_refinement_schematic.png")
|
|
||||||
|
|
||||||
# --- 4. Стенсиль D3Q27 --------------------------------------------------------------------------
|
|
||||||
C3 = np.array([[x, y, z] for x in (-1, 0, 1) for y in (-1, 0, 1) for z in (-1, 0, 1)], dtype=float)
|
|
||||||
nz = (C3 != 0).sum(1)
|
|
||||||
W3 = np.select([nz == 0, nz == 1, nz == 2, nz == 3], [8/27, 2/27, 1/54, 1/216])
|
|
||||||
assert np.isclose(W3.sum(), 1.0)
|
|
||||||
assert np.allclose((W3[:, None, None]*C3[:, :, None]*C3[:, None, :]).sum(0), CS2*np.eye(3))
|
|
||||||
print("D3Q27: Σw=1 и Σw c⊗c = cs²·I — OK; направлений:", len(C3))
|
|
||||||
|
|
||||||
fig = plt.figure(figsize=(6.4, 5.6))
|
|
||||||
ax = fig.add_subplot(111, projection="3d")
|
|
||||||
tier3 = {8/27: ("#d1495b", "покой"), 2/27: ("#2e7d32", "оси (×6)"),
|
|
||||||
1/54: ("#1f6feb", "рёбра (×12)"), 1/216: ("#9c27b0", "углы (×8)")}
|
|
||||||
for wv, (col, lab) in tier3.items():
|
|
||||||
m = np.isclose(W3, wv)
|
|
||||||
ax.scatter(C3[m, 0], C3[m, 1], C3[m, 2], s=70 + 5000*wv, color=col,
|
|
||||||
label=f"{lab}, w={wv:.4f}", depthshade=True, edgecolors="k", linewidths=0.4)
|
|
||||||
ax.set_xlabel("$c_x$"); ax.set_ylabel("$c_y$"); ax.set_zlabel("$c_z$")
|
|
||||||
ax.set_title("Решётка D3Q27 (цель реализации)")
|
|
||||||
ax.legend(loc="upper left", fontsize=7, bbox_to_anchor=(0.0, 1.0))
|
|
||||||
finish(fig, "11_d3q27.png")
|
|
||||||
|
|
||||||
print("DONE (static figures)")
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
# Демо-4: обтекание тел разной формы (цилиндр/квадрат/треугольник/профиль) при Re≈150 —
|
|
||||||
# вихревые дорожки Кармана на грубой сетке (KBC держит устойчиво). Ступенчатый bounce-back
|
|
||||||
# (узловой) — именно его «лесенку» позже лечит SDF+Bouzidi в финальной модели.
|
|
||||||
# Артефакты: figures/10_obstacle_shapes.png, figures/10b_obstacle_probe.png,
|
|
||||||
# anim/obstacle_shapes.gif, out/obstacle.txt, out/obstacle_circle_field.npz (фон для схемы AMR).
|
|
||||||
# Запуск: python obstacle_gpu.py
|
|
||||||
import os
|
|
||||||
import numpy as np
|
|
||||||
import _common as cm
|
|
||||||
|
|
||||||
cp = cm.cp; xp = cm.xp; E = cm.E; Coll = cm.Coll; Strm = cm.Strm
|
|
||||||
plt = cm.plt; vorticity = cm.vorticity; OPP = cm.OPP; Q = cm.Q
|
|
||||||
|
|
||||||
def solid_mask(Nx, Ny, shape, cx, cy, D):
|
|
||||||
"""Маска тела + стенки канала (host numpy → device bool)."""
|
|
||||||
Y, X = np.mgrid[0:Ny, 0:Nx]
|
|
||||||
if shape == "circle":
|
|
||||||
m = (X - cx)**2 + (Y - cy)**2 <= (D/2)**2
|
|
||||||
elif shape == "square":
|
|
||||||
m = (np.abs(X - cx) <= D/2) & (np.abs(Y - cy) <= D/2)
|
|
||||||
elif shape == "triangle": # вершиной против потока
|
|
||||||
tt = (X - (cx - D/2))/D
|
|
||||||
m = (tt >= 0) & (tt <= 1) & (np.abs(Y - cy) <= tt*(D/2))
|
|
||||||
elif shape == "airfoil": # наклонный эллипс (угол атаки 18°)
|
|
||||||
aoa = np.deg2rad(18.0); a = D*0.95; b = D*0.26
|
|
||||||
xr = (X - cx)*np.cos(aoa) + (Y - cy)*np.sin(aoa)
|
|
||||||
yr = -(X - cx)*np.sin(aoa) + (Y - cy)*np.cos(aoa)
|
|
||||||
m = (xr/a)**2 + (yr/b)**2 <= 1
|
|
||||||
else:
|
|
||||||
m = np.zeros((Ny, Nx), bool)
|
|
||||||
m[0, :] = True; m[-1, :] = True # стенки канала (верх/низ)
|
|
||||||
return m
|
|
||||||
|
|
||||||
def run_obstacle(shape, Nx=150, Ny=72, D=14, U=0.06, Re=150.0, steps=9600,
|
|
||||||
ramp=1000, frame_every=40):
|
|
||||||
cx, cy = Nx//4, Ny//2
|
|
||||||
solid_np = solid_mask(Nx, Ny, shape, cx, cy, D)
|
|
||||||
solid = cp.asarray(solid_np)
|
|
||||||
beta = cm.nu_to_beta(U*D/Re)
|
|
||||||
f = E.feq(cp.ones((Ny, Nx), cm.DTYPE), cp.zeros((2, Ny, Nx), cm.DTYPE))
|
|
||||||
frames, probe = [], []
|
|
||||||
px, py = cx + 3*D, cy
|
|
||||||
ones_col = cp.ones((Ny, 1), cm.DTYPE)
|
|
||||||
prog = cm.Viz.make_progress(shape)
|
|
||||||
for t in range(steps + 1):
|
|
||||||
rho, u = E.macros(f)
|
|
||||||
fcol = Coll.kbc_collide(f, E.feq(rho, u), beta)
|
|
||||||
for i in range(Q):
|
|
||||||
fcol[i, solid] = f[OPP[i], solid] # узловой bounce-back (тело + стенки)
|
|
||||||
f = Strm.stream(fcol)
|
|
||||||
Uin = U*cm.smoothstep(t/ramp) # плавный разгон входа
|
|
||||||
uin = cp.zeros((2, Ny, 1), cm.DTYPE); uin[0, :, 0] = Uin
|
|
||||||
f[:, 1:-1, 0] = E.feq(ones_col, uin)[:, 1:-1, 0] # вток (Дирихле)
|
|
||||||
f[:, 1:-1, -1] = f[:, 1:-1, -2] # отток (нуль-градиент)
|
|
||||||
if t % frame_every == 0:
|
|
||||||
_, uu = E.macros(f)
|
|
||||||
frames.append((t, cm.to_cpu(uu)))
|
|
||||||
probe.append(float(uu[1, py, px]))
|
|
||||||
prog(t, steps)
|
|
||||||
return dict(shape=shape, solid=solid_np, frames=frames, probe=np.array(probe),
|
|
||||||
Nx=Nx, Ny=Ny, U=U, Re=Re, D=D, cx=cx, cy=cy)
|
|
||||||
|
|
||||||
SHAPES = [("circle", "цилиндр"), ("square", "квадрат"),
|
|
||||||
("triangle", "треугольник"), ("airfoil", "профиль")]
|
|
||||||
obs = {key: run_obstacle(key) for key, _ in SHAPES}
|
|
||||||
out_lines = ["Nx=150 Ny=72 D=14 U=0.06 Re=150 steps=9600"]
|
|
||||||
for key, name in SHAPES:
|
|
||||||
p = obs[key]["probe"]; half = p[len(p)//2:]
|
|
||||||
out_lines.append(f"{key}: max|u_y(probe)|={np.abs(half).max():.4f}")
|
|
||||||
print(f"{name:12}: max|u_y(зонд)|={np.abs(half).max():.4f} (колебания → срыв вихрей)")
|
|
||||||
|
|
||||||
# --- гифка-монтаж всех четырёх тел -------------------------------------------------------------
|
|
||||||
_keys = [k for k, _ in SHAPES]
|
|
||||||
_titles = {k: n for k, n in SHAPES}
|
|
||||||
_nf = min(len(obs[k]["frames"]) for k in _keys)
|
|
||||||
_vmax = max(np.percentile(np.abs(vorticity(obs[k]["frames"][_nf//2][1])), 99.0) for k in _keys)
|
|
||||||
_ocmap = plt.get_cmap("RdBu_r").copy(); _ocmap.set_bad("#1b1b1b")
|
|
||||||
|
|
||||||
def _draw(fig, i):
|
|
||||||
for j, k in enumerate(_keys):
|
|
||||||
ax = fig.add_subplot(2, 2, j + 1)
|
|
||||||
t, u = obs[k]["frames"][i]
|
|
||||||
vort = np.where(obs[k]["solid"], np.nan, vorticity(u))
|
|
||||||
ax.imshow(vort, origin="lower", cmap=_ocmap, vmin=-_vmax, vmax=_vmax)
|
|
||||||
ax.set_title(f"{_titles[k]} · t={t}", fontsize=9)
|
|
||||||
ax.set_xticks([]); ax.set_yticks([])
|
|
||||||
fig.suptitle("Обтекание тел (KBC, Re≈150): завихренность", fontsize=11)
|
|
||||||
fig.tight_layout(rect=(0, 0, 1, 0.96))
|
|
||||||
|
|
||||||
cm.render_gif(_nf, _draw, "obstacle_shapes.gif", (11, 6.2), fps=24)
|
|
||||||
|
|
||||||
# --- статические панели ------------------------------------------------------------------------
|
|
||||||
fig, axs = plt.subplots(2, 2, figsize=(12, 5.6), constrained_layout=True)
|
|
||||||
for ax, k in zip(axs.ravel(), _keys):
|
|
||||||
t, u = obs[k]["frames"][-1]
|
|
||||||
vort = np.where(obs[k]["solid"], np.nan, vorticity(u))
|
|
||||||
ax.imshow(vort, origin="lower", cmap=_ocmap, vmin=-_vmax, vmax=_vmax)
|
|
||||||
ax.set_title(f"{_titles[k]} (t={t})", fontsize=10)
|
|
||||||
ax.set_xticks([]); ax.set_yticks([])
|
|
||||||
fig.suptitle("Обтекание тел: поле завихренности в конце прогона", fontweight="bold")
|
|
||||||
cm.finish(fig, "10_obstacle_shapes.png")
|
|
||||||
|
|
||||||
fig, ax = plt.subplots(figsize=(9.5, 3.4))
|
|
||||||
for k in _keys:
|
|
||||||
ax.plot(np.arange(len(obs[k]["probe"])), obs[k]["probe"], label=_titles[k])
|
|
||||||
ax.set_title("Поперечная скорость в следе (зонд за телом): колебания = срыв вихрей")
|
|
||||||
ax.set_xlabel("кадр"); ax.set_ylabel("$u_y$ в зонде [lu/ts]"); ax.legend(fontsize=8, ncol=4)
|
|
||||||
cm.finish(fig, "10b_obstacle_probe.png")
|
|
||||||
|
|
||||||
# фон для схемы блочного измельчения (figures_static.py): |ω| цилиндра в конце прогона
|
|
||||||
rc = obs["circle"]
|
|
||||||
np.savez(os.path.join(cm.OUTDIR, "obstacle_circle_field.npz"),
|
|
||||||
u=rc["frames"][-1][1], solid=rc["solid"],
|
|
||||||
Nx=rc["Nx"], Ny=rc["Ny"], cx=rc["cx"], cy=rc["cy"], D=rc["D"])
|
|
||||||
|
|
||||||
out_lines.append("PASS дорожки Кармана на всех четырёх телах")
|
|
||||||
cm.write_out("obstacle.txt", out_lines)
|
|
||||||
print("DONE (obstacle)")
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
# Демо-2: двойной сдвиговый слой (Minion–Brown) на недоразрешённой сетке — устойчивость
|
|
||||||
# BGK (poly-равновесие) против KBC (entropic + γ): BGK разрушается, KBC держит (implicit LES).
|
|
||||||
# Артефакты: figures/08_shear_stability.png, anim/shear_bgk_vs_kbc.gif, out/shear.txt.
|
|
||||||
# Запуск: python shear_gpu.py
|
|
||||||
import math
|
|
||||||
import numpy as np
|
|
||||||
import _common as cm
|
|
||||||
|
|
||||||
cp = cm.cp; xp = cm.xp; E = cm.E; Coll = cm.Coll; Strm = cm.Strm
|
|
||||||
plt = cm.plt; vorticity = cm.vorticity
|
|
||||||
|
|
||||||
def init_shear(N, U0=0.04, delta=0.05, kappa=80.0):
|
|
||||||
xs = cp.arange(N, dtype=cm.DTYPE)/N
|
|
||||||
X, Yc = cp.meshgrid(xs, xs)
|
|
||||||
ux = cp.where(Yc <= 0.5, U0*cp.tanh(kappa*(Yc - 0.25)), U0*cp.tanh(kappa*(0.75 - Yc)))
|
|
||||||
uy = delta*U0*cp.sin(2.0*math.pi*(X + 0.25))
|
|
||||||
return cm.B.pack((ux.astype(cm.DTYPE), uy.astype(cm.DTYPE)))
|
|
||||||
|
|
||||||
def run_shear(scheme, N=128, U0=0.04, nu=1.7e-4, steps=8000, rec=100, frame_every=33):
|
|
||||||
u0 = init_shear(N, U0=U0)
|
|
||||||
rho = cp.ones((N, N), cm.DTYPE)
|
|
||||||
beta = cm.nu_to_beta(nu)
|
|
||||||
f = E.feq(rho, u0) if scheme == "kbc" else cm.feq_poly(rho, u0)
|
|
||||||
ts, umax, frames = [], [], []
|
|
||||||
blew_up_at = None
|
|
||||||
last_u = cm.to_cpu(u0)
|
|
||||||
prog = cm.Viz.make_progress(scheme)
|
|
||||||
for t in range(steps + 1):
|
|
||||||
rho, u = E.macros(f)
|
|
||||||
um = float(xp.abs(u).max())
|
|
||||||
if not math.isfinite(um) or um > 0.5:
|
|
||||||
blew_up_at = t
|
|
||||||
print(f"\n{scheme}: разрушился на шаге {t}")
|
|
||||||
break
|
|
||||||
if t % frame_every == 0:
|
|
||||||
frames.append((t, cm.to_cpu(u)))
|
|
||||||
if scheme == "kbc":
|
|
||||||
f = Coll.kbc_collide(f, E.feq(rho, u), beta)
|
|
||||||
else:
|
|
||||||
f = cm.bgk_collide(f, cm.feq_poly(rho, u), beta)
|
|
||||||
f = Strm.stream(f)
|
|
||||||
last_u = cm.to_cpu(u)
|
|
||||||
if t % rec == 0:
|
|
||||||
ts.append(t); umax.append(um)
|
|
||||||
prog(t, steps)
|
|
||||||
gamma = None
|
|
||||||
if scheme == "kbc": # поле γ на финальном состоянии — для панели
|
|
||||||
r, u = E.macros(f)
|
|
||||||
gamma = cm.to_cpu(cm.kbc_gamma(f, E.feq(r, u), beta))
|
|
||||||
return dict(scheme=scheme, ts=np.array(ts), umax=np.array(umax), frames=frames,
|
|
||||||
u=last_u, gamma=gamma, blew_up_at=blew_up_at, N=N, U0=U0, nu=nu)
|
|
||||||
|
|
||||||
Re_shear = 0.04*128/1.7e-4
|
|
||||||
print(f"Сдвиговый слой: Re ~ {Re_shear:.0f}, β ~ {cm.nu_to_beta(1.7e-4):.4f}")
|
|
||||||
res_bgk = run_shear("bgk")
|
|
||||||
res_kbc = run_shear("kbc")
|
|
||||||
print("BGK:", (f"разрушился на t={res_bgk['blew_up_at']}" if res_bgk["blew_up_at"] else "устойчив"))
|
|
||||||
print("KBC:", (f"разрушился на t={res_kbc['blew_up_at']}" if res_kbc["blew_up_at"] else "устойчив"))
|
|
||||||
assert res_kbc["blew_up_at"] is None, "KBC должен оставаться устойчивым"
|
|
||||||
|
|
||||||
# --- панель: ω(KBC), поле γ−2, max|u|(t) -------------------------------------------------------
|
|
||||||
fig, axs = plt.subplots(1, 3, figsize=(13.5, 4.0))
|
|
||||||
om_k = vorticity(res_kbc["u"])
|
|
||||||
im0 = axs[0].imshow(om_k, origin="lower", cmap="RdBu_r")
|
|
||||||
axs[0].set_title("KBC: завихренность (устойчиво)")
|
|
||||||
axs[0].set_xlabel("$x$ [lu]"); axs[0].set_ylabel("$y$ [lu]")
|
|
||||||
plt.colorbar(im0, ax=axs[0], fraction=0.046)
|
|
||||||
im1 = axs[1].imshow(res_kbc["gamma"] - 2.0, origin="lower", cmap="magma")
|
|
||||||
axs[1].set_title("Поле стабилизатора $\\gamma-2$")
|
|
||||||
axs[1].set_xlabel("$x$ [lu]"); axs[1].set_ylabel("$y$ [lu]")
|
|
||||||
plt.colorbar(im1, ax=axs[1], fraction=0.046)
|
|
||||||
axs[2].plot(res_kbc["ts"], res_kbc["umax"], color="#2e7d32", label="KBC")
|
|
||||||
axs[2].plot(res_bgk["ts"], res_bgk["umax"], color="#d1495b", label="BGK")
|
|
||||||
if res_bgk["blew_up_at"]:
|
|
||||||
axs[2].axvline(res_bgk["blew_up_at"], color="#d1495b", ls="--", alpha=0.6)
|
|
||||||
axs[2].set_title("$\\max|\\mathbf{u}|$ во времени")
|
|
||||||
axs[2].set_xlabel("$t$ [ts]"); axs[2].set_ylabel("$\\max|\\mathbf{u}|$ [lu/ts]"); axs[2].legend()
|
|
||||||
fig.suptitle(f"Демо-2: сдвиговый слой, Re~{Re_shear:.0f} (BGK разрушается, KBC держит)",
|
|
||||||
y=1.04, fontweight="bold")
|
|
||||||
cm.finish(fig, "08_shear_stability.png")
|
|
||||||
|
|
||||||
# --- гифка: BGK (слева, замирает с пометкой) vs KBC (справа) -----------------------------------
|
|
||||||
_kf, _bf = res_kbc["frames"], res_bgk["frames"]
|
|
||||||
_om0s = np.percentile(np.abs(vorticity(_kf[len(_kf)//2][1])), 99.5)
|
|
||||||
|
|
||||||
def _draw(fig, i):
|
|
||||||
ax1 = fig.add_subplot(1, 2, 1)
|
|
||||||
ax2 = fig.add_subplot(1, 2, 2)
|
|
||||||
dead = i >= len(_bf)
|
|
||||||
tb, ub = _bf[min(i, len(_bf) - 1)]
|
|
||||||
ax1.imshow(vorticity(ub), origin="lower", cmap="RdBu_r", vmin=-_om0s, vmax=_om0s)
|
|
||||||
ax1.set_title("BGK — разрушился" if dead else f"BGK · t={tb}",
|
|
||||||
color="red" if dead else "black", fontsize=10)
|
|
||||||
tk, uk = _kf[i]
|
|
||||||
ax2.imshow(vorticity(uk), origin="lower", cmap="RdBu_r", vmin=-_om0s, vmax=_om0s)
|
|
||||||
ax2.set_title(f"KBC · t={tk}", fontsize=10)
|
|
||||||
for a in (ax1, ax2):
|
|
||||||
a.set_xticks([]); a.set_yticks([])
|
|
||||||
fig.tight_layout()
|
|
||||||
|
|
||||||
cm.render_gif(len(_kf), _draw, "shear_bgk_vs_kbc.gif", (8.4, 4.3), fps=24)
|
|
||||||
|
|
||||||
cm.write_out("shear.txt", [
|
|
||||||
f"N=128 U0=0.04 nu=1.7e-4 Re~{Re_shear:.0f} steps=8000",
|
|
||||||
f"BGK_blew_up_at={res_bgk['blew_up_at']}",
|
|
||||||
f"KBC_stable={res_kbc['blew_up_at'] is None}",
|
|
||||||
"PASS KBC устойчив там, где BGK разрушается",
|
|
||||||
])
|
|
||||||
print("DONE (shear)")
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
# Демо-1: вихрь Тейлора–Грина (валидация вязкости и H-теоремы) на компонентах solver_2x_sdf.
|
|
||||||
# Аналитика: E(t) = E0·exp(−4νk²t). Меряем ν по наклону ln E(t) — должен совпасть с заданным.
|
|
||||||
# Артефакты: figures/07_taylor_green.png, figures/07b_tgv_decay_slices.png, anim/tgv_decay.gif,
|
|
||||||
# out/tgv.txt. Запуск: python tgv_gpu.py
|
|
||||||
import math
|
|
||||||
import numpy as np
|
|
||||||
import _common as cm
|
|
||||||
|
|
||||||
cp = cm.cp; xp = cm.xp; E = cm.E; Coll = cm.Coll; Strm = cm.Strm
|
|
||||||
plt = cm.plt; vorticity = cm.vorticity
|
|
||||||
|
|
||||||
def run_tgv(N=96, U0=0.04, nu=0.0125, steps=10000, rec=50, n_snap=10, frame_every=42):
|
|
||||||
k = 2.0*math.pi/N
|
|
||||||
xs = cp.arange(N, dtype=cm.DTYPE)
|
|
||||||
X, Yc = cp.meshgrid(xs, xs) # X — ось 1, Yc — ось 0
|
|
||||||
ux = -U0*cp.cos(k*X)*cp.sin(k*Yc)
|
|
||||||
uy = U0*cp.sin(k*X)*cp.cos(k*Yc)
|
|
||||||
rho = cp.ones((N, N), cm.DTYPE)
|
|
||||||
f = E.feq(rho, cm.B.pack((ux.astype(cm.DTYPE), uy.astype(cm.DTYPE))))
|
|
||||||
beta = cm.nu_to_beta(nu)
|
|
||||||
snap_at = sorted(set(int(round(v)) for v in np.linspace(0, steps, n_snap)))
|
|
||||||
snaps, frames, ts, KE, Hs = [], [], [], [], []
|
|
||||||
prog = cm.Viz.make_progress("tgv")
|
|
||||||
for t in range(steps + 1):
|
|
||||||
rho, u = E.macros(f)
|
|
||||||
f = Coll.kbc_collide(f, E.feq(rho, u), beta)
|
|
||||||
f = Strm.stream(f) # периодический перенос — ровно случай TGV
|
|
||||||
if (t in snap_at) or (t % rec == 0) or (t % frame_every == 0):
|
|
||||||
_, uu = E.macros(f)
|
|
||||||
if t in snap_at:
|
|
||||||
snaps.append((t, cm.to_cpu(uu)))
|
|
||||||
if t % frame_every == 0:
|
|
||||||
frames.append((t, cm.to_cpu(uu)))
|
|
||||||
if t % rec == 0:
|
|
||||||
ts.append(t)
|
|
||||||
KE.append(float((0.5*(uu[0]**2 + uu[1]**2)).mean()))
|
|
||||||
Hs.append(float(cm.H_function(f).mean()))
|
|
||||||
prog(t, steps)
|
|
||||||
return dict(N=N, k=k, nu=nu, U0=U0, ts=np.array(ts), KE=np.array(KE),
|
|
||||||
H=np.array(Hs), u=cm.to_cpu(E.macros(f)[1]), snaps=snaps, frames=frames)
|
|
||||||
|
|
||||||
tgv = run_tgv()
|
|
||||||
mask = tgv["KE"] > 0
|
|
||||||
slope = np.polyfit(tgv["ts"][mask], np.log(tgv["KE"][mask]), 1)[0]
|
|
||||||
nu_meas = -slope/(4.0*tgv["k"]**2)
|
|
||||||
rel_err = abs(nu_meas - tgv["nu"])/tgv["nu"]
|
|
||||||
print(f"TGV: ν задано={tgv['nu']:.5f}, ν измерено={nu_meas:.5f}, отн. ошибка={rel_err*100:.2f}%")
|
|
||||||
assert rel_err < 0.06, "TGV: измеренная вязкость отклонилась слишком сильно"
|
|
||||||
dH = np.diff(tgv["H"])
|
|
||||||
print(f"TGV: H-функция монотонно невозрастает: max(ΔH)={dH.max():.2e}")
|
|
||||||
|
|
||||||
# --- статическая панель: ω, затухание энергии, H(t) -------------------------------------------
|
|
||||||
fig, axs = plt.subplots(1, 3, figsize=(13.5, 4.0))
|
|
||||||
om = vorticity(tgv["u"])
|
|
||||||
im = axs[0].imshow(om, origin="lower", cmap="RdBu_r")
|
|
||||||
axs[0].set_title("Завихренность $\\omega$ (TGV, срез)")
|
|
||||||
axs[0].set_xlabel("$x$ [lu]"); axs[0].set_ylabel("$y$ [lu]")
|
|
||||||
plt.colorbar(im, ax=axs[0], fraction=0.046)
|
|
||||||
axs[1].semilogy(tgv["ts"], tgv["KE"], "o", ms=3, label="KBC (измерено)")
|
|
||||||
axs[1].semilogy(tgv["ts"], tgv["KE"][0]*np.exp(-4*tgv["nu"]*tgv["k"]**2*tgv["ts"]),
|
|
||||||
"-", color="#d1495b", label="$E_0 e^{-4\\nu k^2 t}$ (аналитика)")
|
|
||||||
axs[1].set_title("Затухание кин. энергии"); axs[1].set_xlabel("$t$ [ts]")
|
|
||||||
axs[1].set_ylabel("$E(t)$"); axs[1].legend()
|
|
||||||
axs[2].plot(tgv["ts"], tgv["H"], color="#2e7d32")
|
|
||||||
axs[2].set_title("H-функция (энтропия) во времени")
|
|
||||||
axs[2].set_xlabel("$t$ [ts]"); axs[2].set_ylabel("$\\langle H\\rangle$")
|
|
||||||
fig.suptitle("Демо-1: вихрь Тейлора–Грина (KBC, D2Q9)", y=1.04, fontweight="bold")
|
|
||||||
cm.finish(fig, "07_taylor_green.png")
|
|
||||||
|
|
||||||
# --- 10 срезов распада с единой шкалой цвета ---------------------------------------------------
|
|
||||||
snaps = tgv["snaps"]
|
|
||||||
om0 = np.abs(vorticity(snaps[0][1])).max()
|
|
||||||
KE0 = 0.5*np.mean(snaps[0][1][0]**2 + snaps[0][1][1]**2)
|
|
||||||
fig, axs = plt.subplots(2, 5, figsize=(15, 6.3), constrained_layout=True)
|
|
||||||
im = None
|
|
||||||
for ax, (tt, uu) in zip(axs.ravel(), snaps):
|
|
||||||
im = ax.imshow(vorticity(uu), origin="lower", cmap="RdBu_r", vmin=-om0, vmax=om0)
|
|
||||||
KEf = 0.5*np.mean(uu[0]**2 + uu[1]**2)/KE0
|
|
||||||
ax.set_title(f"t={tt} · $E/E_0$={KEf:.2f}", fontsize=10)
|
|
||||||
ax.set_xticks([]); ax.set_yticks([])
|
|
||||||
fig.colorbar(im, ax=axs, fraction=0.025, pad=0.01, label="завихренность $\\omega$ [1/ts]")
|
|
||||||
fig.suptitle("Распад вихря Тейлора–Грина: завихренность на равных интервалах времени "
|
|
||||||
"(единая шкала цвета)", fontweight="bold")
|
|
||||||
cm.finish(fig, "07b_tgv_decay_slices.png")
|
|
||||||
|
|
||||||
# --- гифка затухания ---------------------------------------------------------------------------
|
|
||||||
_fr = tgv["frames"]
|
|
||||||
_om0 = np.abs(vorticity(_fr[0][1])).max()
|
|
||||||
_KE0 = 0.5*np.mean(_fr[0][1][0]**2 + _fr[0][1][1]**2)
|
|
||||||
|
|
||||||
def _draw(fig, i):
|
|
||||||
t, u = _fr[i]
|
|
||||||
ax = fig.add_subplot(111)
|
|
||||||
ax.imshow(vorticity(u), origin="lower", cmap="RdBu_r", vmin=-_om0, vmax=_om0)
|
|
||||||
ke = 0.5*np.mean(u[0]**2 + u[1]**2)/_KE0
|
|
||||||
ax.set_title(f"Тейлор–Грин · t={t} · $E/E_0$={ke:.2f}", fontsize=10)
|
|
||||||
ax.set_xticks([]); ax.set_yticks([])
|
|
||||||
fig.tight_layout()
|
|
||||||
|
|
||||||
cm.render_gif(len(_fr), _draw, "tgv_decay.gif", (4.6, 4.4), fps=24)
|
|
||||||
|
|
||||||
cm.write_out("tgv.txt", [
|
|
||||||
f"N={tgv['N']} U0={tgv['U0']} steps=10000",
|
|
||||||
f"nu_given={tgv['nu']:.5f} nu_measured={nu_meas:.5f} rel_err_pct={rel_err*100:.2f}",
|
|
||||||
f"H_monotone_max_dH={dH.max():.3e}",
|
|
||||||
"PASS TGV: вязкость по затуханию энергии и H-теорема",
|
|
||||||
])
|
|
||||||
print("DONE (tgv)")
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
when,name,steps,Nx,Ny,cx,in_D,out_D,outlet_uy,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
|
|
||||||
2026-06-10 01:22:17,B0_baseline,100000,174,90,40,2.5,8.31,zero,0.1778,0.2207,0.1814,1.7987,0.7051,-2.9e-05,1.8165,0.732,0.99,3.8,0.99956,0.0244,0.0263,276.8,"база 174x90: вход 2.5D, выход 8.3D, uy=0 (контроль; кросс-чек уже с конвективным членом)"
|
|
||||||
2026-06-10 01:27:05,F1_outlet_far,100000,300,90,40,2.5,16.19,zero,0.1778,0.1977,0.1625,2.8379,6.7939,-0.000322,2.8839,7.4148,1.62,9.14,1.66349,0.2246,0.7482,287.7,выход 8.3D -> 16.2D (вход 2.5D как в базе) — изолирует близость выхода
|
|
||||||
2026-06-10 01:31:48,F2_inlet_far,100000,230,90,96,6.0,8.31,zero,0.1778,0.1676,0.1378,2.2287,6.1198,0.000547,2.0134,6.845,9.66,11.85,1.65477,0.3062,3.3794,282.9,вход 2.5D -> 6.0D (выход 8.3D как в базе) — изолирует близость входа
|
|
||||||
2026-06-10 01:36:32,F3_inout_far,100000,390,90,96,6.0,18.31,zero,0.1778,0.1944,0.1599,2.5772,5.3482,-0.000739,2.5247,5.6976,2.03,6.53,1.65972,0.2693,0.8575,284.0,вход 6.0D + выход 18.3D — оба продольных фактора вместе
|
|
||||||
2026-06-10 01:41:11,F4_outlet_uy,100000,174,90,40,2.5,8.31,extrapolate,0.1778,0.2223,0.1828,1.7539,0.5572,-3e-05,1.7642,0.5658,0.59,1.54,0.99985,0.0226,0.0429,279.3,мягкий выход: uy нуль-градиент вместо uy=0 (геометрия базовая) — изолирует отражение вихрей
|
|
||||||
2026-06-10 01:41:25,F5_combo,100000,390,90,96,6.0,18.31,extrapolate,0.1778,nan,nan,nan,nan,nan,nan,nan,nan,nan,nan,nan,nan,13.7,вход 6.0D + выход 18.3D + мягкий uy-выход — всё вместе
|
|
||||||
|
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 317 KiB |
|
Before Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 42 KiB |
@@ -1,886 +0,0 @@
|
|||||||
{
|
|
||||||
"cells": [
|
|
||||||
{
|
|
||||||
"cell_type": "markdown",
|
|
||||||
"id": "ea656dfb",
|
|
||||||
"metadata": {},
|
|
||||||
"source": [
|
|
||||||
"# Метод KBC (энтропийный Lattice Boltzmann): от формул к валидированной GPU-модели\n",
|
|
||||||
"\n",
|
|
||||||
"**Karlin–Bösch–Chikatamarla (KBC)** — энтропийная multi-relaxation-time формулировка\n",
|
|
||||||
"решёточного метода Больцмана (LBM), дающая **безусловную нелинейную устойчивость** при больших\n",
|
|
||||||
"числах Рейнольдса без явной подсеточной модели турбулентности (работает как *implicit LES*).\n",
|
|
||||||
"\n",
|
|
||||||
"Этот документ — рабочий разбор метода и **история эволюции 2D-макета**: от формул и\n",
|
|
||||||
"самопроверок через классические бенчмарки (Тейлора–Грина, сдвиговый слой, каверна),\n",
|
|
||||||
"блочное измельчение сетки (AMR) и границу SDF+Bouzidi — к **финальной валидированной\n",
|
|
||||||
"GPU-модели** обтекания цилиндра `solver_2x_sdf` (D2Q9, KBC-N1, L0+L1, CUDA-графы) с\n",
|
|
||||||
"двумя независимыми способами считывания силы.\n",
|
|
||||||
"\n",
|
|
||||||
"> **Как устроен документ.** Ноутбук **не исполняет код** — только теория и готовые\n",
|
|
||||||
"> артефакты (картинки, гифки, числа). Весь код живёт рядом и запускается на GPU-сервере:\n",
|
|
||||||
">\n",
|
|
||||||
"> | Где | Что |\n",
|
|
||||||
"> |---|---|\n",
|
|
||||||
"> | `demos_gpu/` | GPU-прогоны всех демо и самопроверок (см. `demos_gpu/README.md`) |\n",
|
|
||||||
"> | `solver_2x_sdf/` | финальная модель (см. `solver_2x_sdf/README.md`) |\n",
|
|
||||||
"> | `figures/`, `anim/`, `*/out/` | артефакты, встроенные ниже |\n",
|
|
||||||
">\n",
|
|
||||||
"> Демо гоняются **теми же модулями** (решётка, равновесие, столкновение, перенос,\n",
|
|
||||||
"> AMR-связка), что и финальная модель, — каждый бенчмарк ниже одновременно валидирует\n",
|
|
||||||
"> компонент финальной модели."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "markdown",
|
|
||||||
"id": "23513c20",
|
|
||||||
"metadata": {},
|
|
||||||
"source": [
|
|
||||||
"## Сводная таблица обозначений\n",
|
|
||||||
"\n",
|
|
||||||
"Везде **решёточные единицы**: шаг сетки $\\Delta x = 1$ (lu), шаг по времени $\\Delta t = 1$ (ts).\n",
|
|
||||||
"\n",
|
|
||||||
"| Символ | Название | Смысл / формула | Диапазон |\n",
|
|
||||||
"|---|---|---|---|\n",
|
|
||||||
"| $f_i$ | популяция | плотность частиц со скоростью $\\mathbf c_i$ | $f_i>0$ |\n",
|
|
||||||
"| $\\mathbf c_i$ | дискретная скорость | направление переноса $i$ | компоненты $\\in\\{-1,0,1\\}$ |\n",
|
|
||||||
"| $w_i$ | вес квадратуры | $\\sum_i w_i=1$ | $\\{4/9,1/9,1/36\\}$ (D2Q9) |\n",
|
|
||||||
"| $c_s$ | скорость звука решётки | $c_s^2=1/3$ | $c_s=1/\\sqrt3$ |\n",
|
|
||||||
"| $\\rho$, $\\mathbf u$ | плотность, скорость | $\\rho=\\sum_i f_i$, $\\rho\\mathbf u=\\sum_i \\mathbf c_i f_i$ | $\\rho\\approx1$, $|\\mathbf u|\\ll c_s$ |\n",
|
|
||||||
"| $\\tau$, $\\beta$ | релаксация | $\\nu=c_s^2(\\tau-\\tfrac12)$, $\\beta=1/(2\\tau)$ | $\\tau>1/2$ |\n",
|
|
||||||
"| $\\gamma$ | **энтропийный стабилизатор** | релаксация высших мод | $\\approx2$ |\n",
|
|
||||||
"| $f_i^{\\mathrm{eq}}$ | равновесие | максимум энтропии при $\\rho,\\mathbf u$ | $>0$ |\n",
|
|
||||||
"| $k_i,s_i,h_i$ | разложение | кинематич./сдвиг/высшие моды | $k+s+h=f$ |\n",
|
|
||||||
"| $\\Delta s_i,\\Delta h_i$ | неравновесие | $P_s(f{-}f^{eq})$, остальное | малы |\n",
|
|
||||||
"| $H$ | H-функция | $H=\\sum_i f_i\\ln(f_i/w_i)$ | не растёт |\n",
|
|
||||||
"| $\\langle X|Y\\rangle$ | энтропийное скал. произв. | $\\sum_i X_iY_i/f_i^{eq}$ | — |\n",
|
|
||||||
"| $T,N,\\Pi_{xy}$ | натуральные 2-е моменты | $\\Pi_{xx}{+}\\Pi_{yy}$, $\\Pi_{xx}{-}\\Pi_{yy}$, сдвиг | — |\n",
|
|
||||||
"| $\\Pi^{neq}_{\\alpha\\beta}$ | неравновесный тензор | $\\sum_i c_{i\\alpha}c_{i\\beta}(f_i-f_i^{eq})$ | — |\n",
|
|
||||||
"| $\\sigma_{\\alpha\\beta}$ | тензор напряжений | $-p'\\delta_{\\alpha\\beta}-(1-\\tfrac{1}{2\\tau})\\Pi^{neq,dev}_{\\alpha\\beta}$ | — |\n",
|
|
||||||
"| $q$ | доля стенки (Bouzidi) | положение стенки на линке из SDF | $(0,1)$ |\n",
|
|
||||||
"| $C_d, C_l, C_m$ | коэффициенты | $\\tfrac{2F_x}{U^2 D}$, $\\tfrac{2F_y}{U^2 D}$, $\\tfrac{2T_z}{U^2 D^2}$ | — |\n",
|
|
||||||
"| $\\beta_{бл}$ | блокировка канала | $D/N_y$ (стеснение стенками) | $\\to0$ — безграничный |\n",
|
|
||||||
"| $\\mathrm{Re}$, $\\mathrm{Ma}$, $\\mathrm{St}$ | подобие | $UL/\\nu$, $|\\mathbf u|/c_s$, $fD/U$ | — |"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "markdown",
|
|
||||||
"id": "02c48319",
|
|
||||||
"metadata": {},
|
|
||||||
"source": [
|
|
||||||
"---\n",
|
|
||||||
"# Часть I. Теория\n",
|
|
||||||
"\n",
|
|
||||||
"## 1. Основы LBM и решётка D2Q9\n",
|
|
||||||
"\n",
|
|
||||||
"LBM отслеживает **популяции** $f_i(\\mathbf x, t)$ — плотность фиктивных частиц, движущихся в\n",
|
|
||||||
"узле $\\mathbf x$ со скоростью $\\mathbf c_i$. Динамика — чередование **переноса** (streaming) и\n",
|
|
||||||
"**столкновения** (collision):\n",
|
|
||||||
"$$ f_i(\\mathbf x + \\mathbf c_i\\,\\Delta t,\\; t+\\Delta t) = f_i(\\mathbf x,t) + \\Omega_i. $$\n",
|
|
||||||
"\n",
|
|
||||||
"**Решётка D2Q9**: покоящаяся частица, 4 осевые и 4 диагональные скорости; $c_s^2=1/3$;\n",
|
|
||||||
"веса $w_0=4/9$, осевые $1/9$, диагональные $1/36$ дают изотропию 4-го порядка.\n",
|
|
||||||
"Макромоменты: $\\rho=\\sum_i f_i$, $\\rho\\mathbf u=\\sum_i \\mathbf c_i f_i$.\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"**Самопроверка** (`demos_gpu/checks_gpu.py`, исполняется на компонентах финальной модели —\n",
|
|
||||||
"`solver_2x_sdf/lattice.py`): $\\sum w_i=1$, $\\sum w_i\\mathbf c_i=0$,\n",
|
|
||||||
"$\\sum w_i c_{i\\alpha}c_{i\\beta}=c_s^2\\delta_{\\alpha\\beta}$, $\\mathbf c_{\\bar i}=-\\mathbf c_i$ —\n",
|
|
||||||
"все тождества проходят (см. `demos_gpu/out/checks.txt`)."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "markdown",
|
|
||||||
"id": "8a74a375",
|
|
||||||
"metadata": {},
|
|
||||||
"source": [
|
|
||||||
"## 2. Равновесие: полиномиальное и энтропийное\n",
|
|
||||||
"\n",
|
|
||||||
"**Полиномиальное** (разложение максвеллиана до $O(u^2)$, стандарт BGK):\n",
|
|
||||||
"$$ f_i^{\\mathrm{eq,poly}} = w_i\\,\\rho\\left[1 + \\frac{\\mathbf c_i\\cdot\\mathbf u}{c_s^2}\n",
|
|
||||||
" + \\frac{(\\mathbf c_i\\cdot\\mathbf u)^2}{2c_s^4} - \\frac{\\mathbf u^2}{2c_s^2}\\right]. $$\n",
|
|
||||||
"\n",
|
|
||||||
"**Энтропийное** (точный максимум дискретной H-функции при связях $\\rho$, $\\rho\\mathbf u$) для\n",
|
|
||||||
"D2Q9/D3Q27 имеет замкнутую **product-form**:\n",
|
|
||||||
"$$ f_i^{\\mathrm{eq}} = \\rho\\,w_i\\prod_{\\alpha\\in\\{x,y\\}}\n",
|
|
||||||
" A(u_\\alpha)\\,B(u_\\alpha)^{\\,c_{i\\alpha}},\\qquad\n",
|
|
||||||
" A(u) = 2-\\sqrt{1+3u^2},\\qquad B(u) = \\frac{2u+\\sqrt{1+3u^2}}{1-u}. $$\n",
|
|
||||||
"\n",
|
|
||||||
"✓ **Сверено с первоисточником**: форма и коэффициенты в точности совпадают с уравнениями\n",
|
|
||||||
"(7)–(11) Bösch, Chikatamarla, Karlin, *Phys. Rev. E* **92**, 043309 (2015)\n",
|
|
||||||
"(восходит к Ansumali, Karlin, Öttinger, *Europhys. Lett.* **63**, 798, 2003).\n",
|
|
||||||
"Реализация — `solver_2x_sdf/equilibrium.py` (показатели $c_{i\\alpha}\\in\\{-1,0,1\\}$, поэтому\n",
|
|
||||||
"вместо `pow` берётся $B$ или $1/B$ — быстрее и совместимо с CUDA-графами).\n",
|
|
||||||
"\n",
|
|
||||||
"**Самопроверка** (`checks_gpu.py`): энтропийное равновесие воспроизводит $\\rho$ и\n",
|
|
||||||
"$\\rho\\mathbf u$ **точно** (до машинного нуля выбранной точности), а второй момент при малом\n",
|
|
||||||
"$\\mathrm{Ma}$ близок к $\\rho(c_s^2\\delta_{\\alpha\\beta}+u_\\alpha u_\\beta)$ — тензору давления\n",
|
|
||||||
"Навье–Стокса. В отличие от полинома, product-form гарантирует положительность и H-теорему."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "markdown",
|
|
||||||
"id": "c6abf6ca",
|
|
||||||
"metadata": {},
|
|
||||||
"source": [
|
|
||||||
"## 3. Разложение популяций $f = k + s + h$\n",
|
|
||||||
"\n",
|
|
||||||
"Сердце KBC — разбиение 9-вектора популяций по **моментным подпространствам**. Натуральные\n",
|
|
||||||
"вторые моменты D2Q9: след $T=\\Pi_{xx}{+}\\Pi_{yy}$, разность нормальных напряжений\n",
|
|
||||||
"$N=\\Pi_{xx}{-}\\Pi_{yy}$, сдвиг $\\Pi_{xy}$; плюс третьи ($Q_{xxy},Q_{xyy}$) и четвёртый\n",
|
|
||||||
"($\\Pi_{xxyy}$) моменты:\n",
|
|
||||||
"\n",
|
|
||||||
"- $k_i$ — **кинематическая** часть: только сохраняющиеся моменты ($\\rho, j_x, j_y$);\n",
|
|
||||||
"- $s_i$ — **сдвиговая** часть: девиаторный тензор напряжений ($N$ и $\\Pi_{xy}$);\n",
|
|
||||||
"- $h_i$ — **высшие** («ghost») моды: $T$, третьи и четвёртый моменты.\n",
|
|
||||||
"\n",
|
|
||||||
"Строится моментная матрица $M_{ai}=\\phi^{(a)}(\\mathbf c_i)$ из 9 независимых мономов; проектор\n",
|
|
||||||
"на группу моментов $G$: $P_G = M^{-1} D_G M$. По построению $P_k+P_s+P_h=I$, разложение точное.\n",
|
|
||||||
"\n",
|
|
||||||
"✓ **Сверено с первоисточником**: назначение $s$ задаёт *вариант* KBC; вариант с\n",
|
|
||||||
"$s=\\{N,\\Pi_{xy}\\}$ (только девиатор) — канонический «KBC-d» / **N1**\n",
|
|
||||||
"(Karlin, Bösch, Chikatamarla, *PRE* **90**, 031302(R), 2014; Bösch et al., *PRE* **92**,\n",
|
|
||||||
"043309, 2015 — там же семейство $s\\in\\{d,\\,d{+}t,\\,d{+}q,\\,d{+}t{+}q\\}$). Именно он\n",
|
|
||||||
"реализован в `solver_2x_sdf/lattice.py` (проектор `Ps` на строки $\\{c_x^2{-}c_y^2,\\,c_xc_y\\}$).\n",
|
|
||||||
"\n",
|
|
||||||
"**Самопроверка** (`checks_gpu.py`): `Ps` идемпотентен, $\\Delta s$ несёт **только** моменты\n",
|
|
||||||
"$\\{N,\\Pi_{xy}\\}$ (остальные строки $M\\Delta s$ — нули)."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "markdown",
|
|
||||||
"id": "3fe4992c",
|
|
||||||
"metadata": {},
|
|
||||||
"source": [
|
|
||||||
"## 4. Оператор столкновений KBC и стабилизатор $\\gamma$\n",
|
|
||||||
"\n",
|
|
||||||
"Так как $f$ и $f^{eq}$ имеют одинаковые сохраняющиеся моменты, $\\Delta k=0$ и\n",
|
|
||||||
"$f-f^{eq}=\\Delta s+\\Delta h$, где $\\Delta s = P_s(f-f^{eq})$.\n",
|
|
||||||
"\n",
|
|
||||||
"**Столкновение KBC** (через «зеркальное состояние» $f^{mirr}$):\n",
|
|
||||||
"$$ f_i' = (1-\\beta)\\,f_i + \\beta\\,f_i^{mirr}\n",
|
|
||||||
" \\;\\;\\Longleftrightarrow\\;\\;\n",
|
|
||||||
" \\boxed{\\,f_i' = f_i - \\beta\\,\\bigl(2\\,\\Delta s_i + \\gamma\\,\\Delta h_i\\bigr)\\,},\\qquad\n",
|
|
||||||
" \\nu = c_s^2\\Bigl(\\frac{1}{2\\beta}-\\frac12\\Bigr)\\;\\Rightarrow\\;\\beta=\\frac{1}{2\\tau}. $$\n",
|
|
||||||
"Множитель **2** перед $\\Delta s$ фиксирует сдвиговую вязкость; высшие моды релаксируются\n",
|
|
||||||
"отдельным $\\gamma$.\n",
|
|
||||||
"\n",
|
|
||||||
"**Стабилизатор** $\\gamma$ выбирается локально (в каждом узле на каждом шаге) из условия\n",
|
|
||||||
"неувеличения дискретной энтропии; аналитическое приближение корня:\n",
|
|
||||||
"$$ \\gamma^\\* = \\frac{1}{\\beta} - \\Bigl(2-\\frac{1}{\\beta}\\Bigr)\n",
|
|
||||||
" \\frac{\\langle \\Delta s\\,|\\,\\Delta h\\rangle}{\\langle \\Delta h\\,|\\,\\Delta h\\rangle},\n",
|
|
||||||
" \\qquad \\langle X|Y\\rangle = \\sum_i \\frac{X_i Y_i}{f_i^{eq}}. $$\n",
|
|
||||||
"\n",
|
|
||||||
"✓ **Сверено с первоисточником**: обе формулы (включая зеркальную форму и связь\n",
|
|
||||||
"$\\nu(\\beta)$) дословно совпадают с Karlin, Bösch, Chikatamarla, *PRE* **90**, 031302(R)\n",
|
|
||||||
"(2014) и Bösch et al., *PRE* **92**, 043309 (2015), ур. (24)–(25). Реализация —\n",
|
|
||||||
"`solver_2x_sdf/collision.py` (с регуляризацией $\\gamma\\to2$ при\n",
|
|
||||||
"$\\langle\\Delta h|\\Delta h\\rangle\\to0$).\n",
|
|
||||||
"\n",
|
|
||||||
"При $\\gamma=2$: $2\\Delta s+2\\Delta h=2(f-f^{eq})$ — KBC **сводится к BGK** с $\\omega=2\\beta$.\n",
|
|
||||||
"**Самопроверка** (`checks_gpu.py`): KBC$(\\gamma{=}2)\\equiv$ BGK до машинного нуля; столкновение\n",
|
|
||||||
"сохраняет $\\rho$ и $\\rho\\mathbf u$.\n",
|
|
||||||
"\n",
|
|
||||||
"**Популярно.** Разложение $k+s+h$ — три «слоя» движения: перенос массы/импульса, вязкий сдвиг\n",
|
|
||||||
"(трение слоёв — задаёт вязкость и Re) и мелкая «рябь» высших мод, где на грубой сетке копится\n",
|
|
||||||
"неустойчивость. KBC гасит сдвиг фиксированным темпом $2\\beta$ (физика), а рябь — адаптивным\n",
|
|
||||||
"$\\gamma\\beta$: в спокойных зонах $\\gamma\\approx2$ (как BGK), у резких градиентов $\\gamma$\n",
|
|
||||||
"отклоняется и добавляет ровно столько диссипации, сколько разрешает энтропия. Это «локальный\n",
|
|
||||||
"термостат потока» — встроенная, без подгоночных констант, модель мелких масштабов\n",
|
|
||||||
"(*implicit LES*)."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "markdown",
|
|
||||||
"id": "9eba837e",
|
|
||||||
"metadata": {},
|
|
||||||
"source": [
|
|
||||||
"## 5. Перенос и связь вязкости с $\\beta$; H-функция\n",
|
|
||||||
"\n",
|
|
||||||
"Перенос — сдвиг каждой популяции на её $\\mathbf c_i$ (pull-схема, `solver_2x_sdf/streaming.py`).\n",
|
|
||||||
"Вязкость: $\\nu = c_s^2(\\tfrac{1}{2\\beta}-\\tfrac12)$, т.е. $\\tau=\\nu/c_s^2+\\tfrac12$.\n",
|
|
||||||
"Число Рейнольдса $\\mathrm{Re}=UL/\\nu$; Маха $\\mathrm{Ma}=U/c_s\\lesssim0.1$ (ошибка\n",
|
|
||||||
"сжимаемости $\\propto \\mathrm{Ma}^2$).\n",
|
|
||||||
"\n",
|
|
||||||
"Дискретная энтропия $H=\\sum_i f_i\\ln(f_i/w_i)$ — «физический компас» устойчивости: из условия\n",
|
|
||||||
"«$H$ не растёт» выведен $\\gamma$; в демо Тейлора–Грина ниже $\\langle H\\rangle(t)$ монотонно\n",
|
|
||||||
"не возрастает.\n",
|
|
||||||
"\n",
|
|
||||||
"## 6. Алгоритм одного шага KBC\n",
|
|
||||||
"\n",
|
|
||||||
"```\n",
|
|
||||||
"для каждого узла:\n",
|
|
||||||
" 1) макро-моменты: rho = sum f_i, rho*u = sum c_i f_i\n",
|
|
||||||
" 2) равновесие: f_i^eq (энтропийное product-form)\n",
|
|
||||||
" 3) неравновесие: Ds = P_s (f - f^eq), Dh = (f - f^eq) - Ds\n",
|
|
||||||
" 4) стабилизатор: gamma = 1/beta - (2 - 1/beta) <Ds|Dh> / <Dh|Dh>\n",
|
|
||||||
" 5) столкновение: f <- f - beta (2 Ds + gamma Dh)\n",
|
|
||||||
" 6) перенос: f_i(x + c_i) <- f_i(x)\n",
|
|
||||||
" 7) граничные условия (bounce-back / Bouzidi / Zou-He / free-slip)\n",
|
|
||||||
"```\n",
|
|
||||||
"\n",
|
|
||||||
""
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "markdown",
|
|
||||||
"id": "a27e6548",
|
|
||||||
"metadata": {},
|
|
||||||
"source": [
|
|
||||||
"---\n",
|
|
||||||
"# Часть II. Валидационные демо\n",
|
|
||||||
"\n",
|
|
||||||
"Четыре классических теста; каждый гоняется скриптом из `demos_gpu/` **на тех же модулях, что\n",
|
|
||||||
"финальная модель** (решётка/равновесие/KBC/перенос из `solver_2x_sdf`). Числа ниже цитируются\n",
|
|
||||||
"из `demos_gpu/out/*.txt` — перегенерируются при каждом прогоне на GPU-сервере.\n",
|
|
||||||
"\n",
|
|
||||||
"## 7. Демо-1: вихрь Тейлора–Грина — валидация вязкости и H-теоремы\n",
|
|
||||||
"\n",
|
|
||||||
"Точное решение несжимаемого Навье–Стокса в периодической области:\n",
|
|
||||||
"$u_x=-U_0\\cos(kx)\\sin(ky)$, $u_y=U_0\\sin(kx)\\cos(ky)$, $k=2\\pi/N$; кинетическая энергия\n",
|
|
||||||
"затухает как $E(t)=E_0\\,e^{-4\\nu k^2 t}$. Прогоняем KBC ($N=96$, $\\nu=0.0125$, 10 000 шагов)\n",
|
|
||||||
"и **измеряем** $\\nu$ по наклону $\\ln E(t)$ — критерий приёмки: отклонение $<6\\%$\n",
|
|
||||||
"(фактически $\\lesssim1\\%$, см. `demos_gpu/out/tgv.txt`). $\\langle H\\rangle(t)$ монотонно\n",
|
|
||||||
"не возрастает — H-теорема выполняется.\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"*Скрипт: `demos_gpu/tgv_gpu.py`.*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "markdown",
|
|
||||||
"id": "0f68444b",
|
|
||||||
"metadata": {},
|
|
||||||
"source": [
|
|
||||||
"## 8. Демо-2: устойчивость — двойной сдвиговый слой (BGK против KBC)\n",
|
|
||||||
"\n",
|
|
||||||
"Классический тест Minion–Brown: тонкие сдвиговые слои на **недоразрешённой** сетке\n",
|
|
||||||
"($N=128$, $\\nu=1.7\\cdot10^{-4}$, $\\mathrm{Re}\\sim3\\cdot10^4$). Стандартный BGK\n",
|
|
||||||
"(+полиномиальное равновесие) при малой вязкости разрушается (отрицательные популяции → NaN),\n",
|
|
||||||
"а KBC за счёт адаптивного $\\gamma$ остаётся устойчивым и гладко сворачивает вихри — это и есть\n",
|
|
||||||
"*implicit LES*. На средней панели — поле $\\gamma-2$: стабилизатор отклоняется от BGK-предела\n",
|
|
||||||
"именно там, где градиенты резкие.\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"*Скрипт: `demos_gpu/shear_gpu.py`; момент разрушения BGK — в `demos_gpu/out/shear.txt`.*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "markdown",
|
|
||||||
"id": "d8ebf6c3",
|
|
||||||
"metadata": {},
|
|
||||||
"source": [
|
|
||||||
"## 9. Демо-3: каверна с движущейся крышкой против эталона Ghia (Re=1000)\n",
|
|
||||||
"\n",
|
|
||||||
"Квадратная каверна, верхняя стенка движется со скоростью $U$. На неподвижных стенках —\n",
|
|
||||||
"**bounce-back**; на крышке — bounce-back с поправкой импульса подвижной стенки\n",
|
|
||||||
"$-2w_i\\rho\\,(\\mathbf c_i\\cdot\\mathbf u_w)/c_s^2$. Профили скорости по центральным линиям\n",
|
|
||||||
"сравниваются с эталоном **Ghia, Ghia & Shin (1982)**. Сетка намеренно грубая ($N=110$) и\n",
|
|
||||||
"bounce-back первого порядка, поэтому совпадение **качественное** (RMS-отклонение —\n",
|
|
||||||
"в `demos_gpu/out/cavity.txt`); оно улучшается измельчением и ГУ второго порядка.\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"*Скрипт: `demos_gpu/cavity_gpu.py`; профили сохраняются в `demos_gpu/out/cavity.npz`.*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "markdown",
|
|
||||||
"id": "761a536f",
|
|
||||||
"metadata": {},
|
|
||||||
"source": [
|
|
||||||
"## 10. Демо-4: обтекание тел разной формы — дорожки Кармана\n",
|
|
||||||
"\n",
|
|
||||||
"Канал с втоком слева (Дирихле + плавный разгон), нуль-градиентным оттоком и стенками.\n",
|
|
||||||
"Тела (цилиндр, квадрат, треугольник, профиль под углом атаки) — **узловой bounce-back** по\n",
|
|
||||||
"маске. При $\\mathrm{Re}\\approx150$ за телами формируется **вихревая дорожка Кармана**;\n",
|
|
||||||
"KBC держит грубую сетку устойчиво. Колебания $u_y$ в зонде за телом — мерило частоты срыва.\n",
|
|
||||||
"\n",
|
|
||||||
"Именно «лесенка» ступенчатой маски — мотивация перехода к **SDF + Bouzidi** в части IV.\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"*Скрипт: `demos_gpu/obstacle_gpu.py`.*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "markdown",
|
|
||||||
"id": "2056c378",
|
|
||||||
"metadata": {},
|
|
||||||
"source": [
|
|
||||||
"---\n",
|
|
||||||
"# Часть III. Измельчение сетки (AMR)\n",
|
|
||||||
"\n",
|
|
||||||
"## 11. Блочное измельчение: зачем и как\n",
|
|
||||||
"\n",
|
|
||||||
"Чистый LBM живёт на **равномерной** решётке: перенос требует соседа ровно на $\\mathbf c_i$.\n",
|
|
||||||
"Неоднородное разрешение делают **вложенными равномерными блоками** разного шага: крупный\n",
|
|
||||||
"вдали, мелкий у тела и в следе. На стыке блоков ($\\Delta x\\to\\Delta x/2$,\n",
|
|
||||||
"$\\Delta t\\to\\Delta t/2$ — акустическое масштабирование, $c_s$ сохраняется):\n",
|
|
||||||
"\n",
|
|
||||||
"- **время релаксации**: $\\beta_f = \\dfrac{1}{1+r\\,(1/\\beta_c-1)}$, что при $r=2$ эквивалентно\n",
|
|
||||||
" $\\boxed{\\tau_f = 2\\tau_c - \\tfrac12}$ (вязкость $\\nu$ непрерывна через стык);\n",
|
|
||||||
"- **неравновесная часть** масштабируется: $f^{neq}_f = \\dfrac{\\beta_c}{r\\,\\beta_f}\\,f^{neq}_c\n",
|
|
||||||
" = \\dfrac{\\tau_f}{2\\tau_c}\\,f^{neq}_c$ (обратно — множитель $\\tfrac{2\\tau_c}{\\tau_f}$);\n",
|
|
||||||
"- **ghost-рамка** тонкого блока заполняется равновесием по интерполированным $\\rho,\\mathbf u$\n",
|
|
||||||
" плюс масштабированным $f^{neq}$; между подшагами тонкого уровня граница **линейно\n",
|
|
||||||
" интерполируется по времени** между состояниями родителя в $t$ и $t+\\Delta t$;\n",
|
|
||||||
"- **рестрикция**: внутренние узлы родителя обновляются с тонкого блока (каждый $r$-й узел)\n",
|
|
||||||
" с обратным масштабом $f^{neq}$.\n",
|
|
||||||
"\n",
|
|
||||||
"✓ **Сверено с первоисточниками**: формулы $\\beta_f$ и множитель $\\beta_c/(r\\beta_f)$ —\n",
|
|
||||||
"Dorschner, Frapolli, Chikatamarla, Karlin, *PRE* **94**, 053311 (2016), ур. (34), (40)\n",
|
|
||||||
"(KBC полностью совместим с измельчением — $\\gamma$ поузловой на каждом уровне); временна́я\n",
|
|
||||||
"интерполяция границы между подшагами — Lagrava, Malaspinas, Latt, Chopard, *J. Comput. Phys.*\n",
|
|
||||||
"**231**, 4808 (2012). Реализация — `solver_2x_sdf/amr.py` (`patch/ghost/fill/restrict`),\n",
|
|
||||||
"`R01 = τ_f/(2τ_c)` в `config.py`.\n",
|
|
||||||
"\n",
|
|
||||||
""
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "markdown",
|
|
||||||
"id": "6645f68a",
|
|
||||||
"metadata": {},
|
|
||||||
"source": [
|
|
||||||
"## 12. Минимальный 2-уровневый AMR на цилиндре\n",
|
|
||||||
"\n",
|
|
||||||
"Рабочая связка «крупная сетка + мелкий блок 2× вокруг тела» (то же тело bounce-back на обоих\n",
|
|
||||||
"уровнях): на 1 крупный шаг — 2 мелких подшага, ghost из крупного уровня, рестрикция только по\n",
|
|
||||||
"жидким узлам. В этой *минимальной* версии граница блока держится постоянной в течение\n",
|
|
||||||
"подшагов (без временно́й интерполяции) — её эффект виден в бенчмарке ниже.\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"*Скрипт: `demos_gpu/amr_demo_gpu.py` (часть 1) — на `amr.patch/ghost/fill/restrict`\n",
|
|
||||||
"финальной модели.*\n",
|
|
||||||
"\n",
|
|
||||||
"## 13. Полный вложенный 2×+4× и численное сравнение\n",
|
|
||||||
"\n",
|
|
||||||
"Три уровня (коарс → 2× → 4×), рекурсивный sub-cycling 1:2:4, временна́я интерполяция границ.\n",
|
|
||||||
"Контролируемый тест — локализованный резкий вихрь; эталон — равномерная мелкая 4× сетка.\n",
|
|
||||||
"Работа меряется в **cell-updates** (аппаратно-независимо).\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"Выводы (точные числа — `demos_gpu/out/amr_bench.txt`):\n",
|
|
||||||
"- **вложенность эффективна**: полный 2×+4× точнее одиночного 4× при заметно меньшей работе;\n",
|
|
||||||
"- **временна́я интерполяция** снижает ошибку при той же работе;\n",
|
|
||||||
"- полный AMR даёт точность уровня мелкой сетки за **в разы меньшую работу**;\n",
|
|
||||||
"- крупная сетка дёшева, но её ошибка принципиально не лечится без измельчения.\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"## 14. AMR на большой области (исторические прогоны)\n",
|
|
||||||
"\n",
|
|
||||||
"Та же связка на большой области (150×72, длинный прогон) — видны грубая сетка и учащённые\n",
|
|
||||||
"зоны: **2× (тело+след)** и **4× (у тела)**:\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
""
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "markdown",
|
|
||||||
"id": "ed665c26",
|
|
||||||
"metadata": {},
|
|
||||||
"source": [
|
|
||||||
"---\n",
|
|
||||||
"# Часть IV. Граница SDF + Bouzidi\n",
|
|
||||||
"\n",
|
|
||||||
"## 15. Каталог граничных условий\n",
|
|
||||||
"\n",
|
|
||||||
"KBC задаёт только оператор столкновения; полному решателю нужны ГУ:\n",
|
|
||||||
"\n",
|
|
||||||
"- **Bounce-back** — no-slip стенка «на полпути между узлами»: $f_{\\bar i}=f_i$. Первый\n",
|
|
||||||
" порядок; для движущейся стенки — поправка $-2w_i\\rho(\\mathbf c_i\\cdot\\mathbf u_w)/c_s^2$.\n",
|
|
||||||
"- **Интерполированный bounce-back (Bouzidi)** — для **кривых** стенок: доля стенки на линке\n",
|
|
||||||
" $q\\in(0,1)$ из SDF; две ветви:\n",
|
|
||||||
" $$ q<\\tfrac12:\\;\\; f_{\\bar i}(\\mathbf x_f) = 2q\\,f_i^{post}(\\mathbf x_f) + (1-2q)\\,f_i^{post}(\\mathbf x_f-\\mathbf c_i);\\qquad\n",
|
|
||||||
" q\\ge\\tfrac12:\\;\\; f_{\\bar i}(\\mathbf x_f) = \\tfrac{1}{2q}\\,f_i^{post}(\\mathbf x_f) + \\bigl(1-\\tfrac{1}{2q}\\bigr)f_{\\bar i}^{post}(\\mathbf x_f). $$\n",
|
|
||||||
" ✓ Bouzidi, Firdaouss, Lallemand, *Phys. Fluids* **13**, 3452 (2001); реализация —\n",
|
|
||||||
" `solver_2x_sdf/boundary.py` (`build_bc`/`apply_bc`).\n",
|
|
||||||
"- **Zou–He** — заданные скорость/давление на входе/выходе (восстановление недостающих\n",
|
|
||||||
" популяций из $\\rho,\\mathbf u$ + bounce-back неравновесной части; второй порядок).\n",
|
|
||||||
" ✓ Zou & He, *Phys. Fluids* **9**, 1591 (1997); реализация — `channel_bc`. На давление-выходе\n",
|
|
||||||
" поперечная скорость задаётся режимом `outlet_uy`: классическое жёсткое $u_y=0$ либо\n",
|
|
||||||
" нуль-градиент (мягкий выход — не отражает вихри дорожки; см. факторное исследование,\n",
|
|
||||||
" раздел 24).\n",
|
|
||||||
"- **Free-slip (specular)** — скользящая стенка: касательный импульс сохраняется, нормальный\n",
|
|
||||||
" отражается; массу не дрейфует. Реализация — `free_slip_walls`.\n",
|
|
||||||
"- **Sponge / absorbing-слой** — плавный рост вязкости перед выходом; гасит вихри и акустику до\n",
|
|
||||||
" отражающей границы. В финальной модели **добавлен** по итогам факторного исследования\n",
|
|
||||||
" (раздел 25): без него длинные домены с парой «вход-скорость / выход-давление» работают как\n",
|
|
||||||
" недодемпфированный акустический резонатор. Использовался и в прежней 3D-реализации.\n",
|
|
||||||
"- **Grad / регуляризованные** — реконструкция $f^{neq}$ через $\\Pi^{neq}$; в 2D-модели\n",
|
|
||||||
" не понадобились.\n",
|
|
||||||
"\n",
|
|
||||||
"## 16. Зачем SDF: лесенка против гладкой границы\n",
|
|
||||||
"\n",
|
|
||||||
"Ступенчатая маска (часть II, демо-4) приближает окружность «лесенкой»: положение стенки\n",
|
|
||||||
"скачет на $O(\\Delta x)$, что шумит в силе и завышает $C_d$. **SDF** (signed distance field,\n",
|
|
||||||
"$\\varphi<0$ в теле) даёт точную долю пересечения каждого линка $q=\\varphi_f/(\\varphi_f-\\varphi_s)$,\n",
|
|
||||||
"и Bouzidi восстанавливает отражение **в правильной точке** — граница эффективно гладкая\n",
|
|
||||||
"при том же разрешении.\n",
|
|
||||||
"\n",
|
|
||||||
"Сравнение реализаций без SDF (bounce-back) и с SDF+Bouzidi на одинаковой сетке:\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
""
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "markdown",
|
|
||||||
"id": "9d235a12",
|
|
||||||
"metadata": {},
|
|
||||||
"source": [
|
|
||||||
"---\n",
|
|
||||||
"# Часть V. Эволюция GPU-макета\n",
|
|
||||||
"\n",
|
|
||||||
"## 17. Поколения\n",
|
|
||||||
"\n",
|
|
||||||
"| Поколение | Файлы | Что нового | Известные проблемы (чинились в следующем) |\n",
|
|
||||||
"|---|---|---|---|\n",
|
|
||||||
"| 0. CPU-прототипы | `_amr_anims.py`, `_amr_anims_sdf.py` | первая полная связка: KBC + AMR (none/2×/2×+4×) на цилиндре; ветка с SDF+Bouzidi | numpy-CPU медленный; сила в BB-ветке считалась из post-collision |\n",
|
|
||||||
"| 1. Базовый GPU | `amr_gpu_core.py` + `amr_cylinder_gpu.py`, `amr_cylinder_sdf_gpu.py` | перенос на CuPy (~100× быстрее), оба типа границы в одном ядре | **сила `force_on`: оба члена из post-collision → Cd отрицательный** |\n",
|
|
||||||
"| 2. Оптимизированный GPU | `amr_opt/*` | **фикс силы** (второй член из post-streaming); `feq` без `pow`; `cp.fuse`; **CUDA-графы** с рантайм-самопроверкой; GPU-only | монолитные скрипты, параметры захардкожены |\n",
|
|
||||||
"| 3. Финальная модель | `solver_2x_sdf/*` | модульная архитектура; **Zou-He вход + давление-выход**; **free-slip стенки**; GMEM-сила с торком; **кросс-чек ∮σ·n ds**; серия по блокировке | — |\n",
|
|
||||||
"\n",
|
|
||||||
"## 18. Хронология багов и фиксов (чему научил макет)\n",
|
|
||||||
"\n",
|
|
||||||
"1. **Сила: второй член — из поля ПОСЛЕ стриминга.** В обмене импульсом\n",
|
|
||||||
" $F=\\sum c_i(f_i^{post}+f_{\\bar i})$ популяция $f_{\\bar i}$ — та, что *вернулась* в жидкий\n",
|
|
||||||
" узел после отражения. Если взять её из post-collision поля (как в поколении 1), ведущий\n",
|
|
||||||
" симметричный член $\\sim2\\rho w_i$ сокращается: $C_d$ выходил **отрицательным** и\n",
|
|
||||||
" схлопывался к нулю при измельчении. Урок: считывание силы валидируется так же строго,\n",
|
|
||||||
" как сам решатель.\n",
|
|
||||||
"2. **Дрейф массы из-за ГУ выхода.** Грубый zero-gradient выход не выпускал поток: масса и\n",
|
|
||||||
" противодавление копились ($\\langle\\rho\\rangle\\uparrow$), поток глох, $C_d$ и rms $C_l$\n",
|
|
||||||
" уползали к нулю за 100k шагов. Вскрыто **диагностикой по окнам времени** (тренды\n",
|
|
||||||
" $\\langle\\rho\\rangle$, $C_d/\\rho$). Фикс: **Zou-He скоростной вход + давление-выход** —\n",
|
|
||||||
" масса заякорена ($\\langle\\rho\\rangle\\approx1.000$).\n",
|
|
||||||
"3. **Free-slip стенки канала** (specular) вместо no-slip: нет паразитного пограничного слоя\n",
|
|
||||||
" на искусственных стенках, блокировка уменьшается; цилиндр остаётся no-slip (Bouzidi).\n",
|
|
||||||
"4. **Стартовое возмущение** — короткий поперечный sin-импульс входа после разгона: вихревая\n",
|
|
||||||
" дорожка запускается за единицы периодов вместо тысяч шагов симметричного «застоя».\n",
|
|
||||||
"5. **Зонд скорости — на тонком уровне L1** (меньше численного размытия вихрей) и **оценка St\n",
|
|
||||||
" с параболической интерполяцией пика FFT** (иначе St «застревает» на шаге сетки частот).\n",
|
|
||||||
"6. **CUDA-графы**: захват шага требовал убрать все host→device на горячем пути —\n",
|
|
||||||
" `cupy.stack` заменён преаллокацией (`backend.pack`), проектор KBC переписан без cuBLAS\n",
|
|
||||||
" (gemm тащит alpha/beta с хоста), границы clip запечены литералами в ядро. На шаге $t=1$ —\n",
|
|
||||||
" **рантайм-самопроверка** граф vs eager с автооткатом.\n",
|
|
||||||
"7. **Осреднение силы по подшагам L1 и момент $C_m$** — финальная ревизия считывания\n",
|
|
||||||
" (часть VI): сила меряется на каждом подшаге тонкого уровня, торк — с точкой приложения\n",
|
|
||||||
" на пересечении линка со стенкой."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "markdown",
|
|
||||||
"id": "e7b57ba6",
|
|
||||||
"metadata": {},
|
|
||||||
"source": [
|
|
||||||
"---\n",
|
|
||||||
"# Часть VI. Финальная модель `solver_2x_sdf`\n",
|
|
||||||
"\n",
|
|
||||||
"## 19. Архитектура\n",
|
|
||||||
"\n",
|
|
||||||
"Обтекание цилиндра: грубый уровень **L0** (174×90, $D=16$, $\\mathrm{Re}=150$, $U=0.07$) +\n",
|
|
||||||
"вложенный патч **L1** (×2 — тело и ближний след). Граница цилиндра — SDF+Bouzidi (no-slip);\n",
|
|
||||||
"вход/выход — Zou-He (скорость/давление); верх/низ — free-slip. Только GPU/CuPy, шаг целиком\n",
|
|
||||||
"захватывается в CUDA-граф.\n",
|
|
||||||
"\n",
|
|
||||||
"| Модуль | Ответственность |\n",
|
|
||||||
"|---|---|\n",
|
|
||||||
"| `config.py` | параметры + производные ($\\tau$, $\\beta$, патч из $N_y$); env-переопределения |\n",
|
|
||||||
"| `lattice.py` | D2Q9 + KBC-проектор $P_s$ |\n",
|
|
||||||
"| `equilibrium.py` | product-form $f^{eq}$, макромоменты |\n",
|
|
||||||
"| `collision.py` | KBC-N1 (единственный оператор) |\n",
|
|
||||||
"| `streaming.py` | pull-перенос |\n",
|
|
||||||
"| `geometry.py` | маски/SDF для L0 и L1 |\n",
|
|
||||||
"| `boundary.py` | Bouzidi + Zou-He + free-slip |\n",
|
|
||||||
"| `forces.py` | GMEM-сила и момент (считывание №1) |\n",
|
|
||||||
"| `forces_stress.py` | ∮σ·n ds по контуру (считывание №2, кросс-чек) |\n",
|
|
||||||
"| `amr.py` | связка L0↔L1 (часть III) |\n",
|
|
||||||
"| `solver.py` | оркестратор: persistent-буферы, CUDA-граф с самопроверкой |\n",
|
|
||||||
"| `diagnostics.py` | St (параболический пик), таблицы, кросс-чек, фиты серии по $\\beta_{бл}$ |\n",
|
|
||||||
"| `run.py` / `run_blockage.py` / `run_factors.py` | одиночный прогон / серия по блокировке / факторное исследование (реестр `out/factors.csv`) |\n",
|
|
||||||
"\n",
|
|
||||||
"## 20. Считывание силы №1: галилей-инвариантный обмен импульсом (GMEM)\n",
|
|
||||||
"\n",
|
|
||||||
"Для каждого линка жидкость→тело (Bouzidi-маска):\n",
|
|
||||||
"$$ \\Delta\\mathbf F = (\\mathbf c_i - \\mathbf u_w)\\,f_i^{post}(\\mathbf x_f)\n",
|
|
||||||
" - (\\mathbf c_{\\bar i} - \\mathbf u_w)\\,f_{\\bar i}^{stream}(\\mathbf x_f), $$\n",
|
|
||||||
"где $f_i^{post}$ — популяция, уходящая в стенку (после столкновения), $f_{\\bar i}^{stream}$ —\n",
|
|
||||||
"вернувшаяся (после стриминга/Bouzidi), $\\mathbf u_w$ — скорость стенки (для неподвижного\n",
|
|
||||||
"цилиндра $0$, но члены с $\\mathbf u_w$ делают оценку **галилей-инвариантной** и готовой к\n",
|
|
||||||
"подвижным телам — цель SimV4). ✓ Wen, Zhang, Tu, Wang, Fang, *J. Comput. Phys.* **266**,\n",
|
|
||||||
"161 (2014).\n",
|
|
||||||
"\n",
|
|
||||||
"**Момент (торк)**: $T_z=\\sum_{links} (\\mathbf r_w\\times\\Delta\\mathbf F)_z$ с точкой приложения\n",
|
|
||||||
"на **пересечении линка со стенкой** $\\mathbf r_w=\\mathbf r_f+q\\,\\mathbf c_i$ ($q$ уже лежит в\n",
|
|
||||||
"структуре Bouzidi). $C_m = 2T_z/(U^2D_{L1}^2)$; для кругового цилиндра $\\langle C_m\\rangle\\approx0$ —\n",
|
|
||||||
"встроенный **контроль симметрии** считывания.\n",
|
|
||||||
"\n",
|
|
||||||
"Сила меряется на **каждом** подшаге L1 и усредняется (анти-алиасинг быстрых мод).\n",
|
|
||||||
"\n",
|
|
||||||
"## 21. Считывание силы №2: баланс импульса контрольного объёма (кросс-чек)\n",
|
|
||||||
"\n",
|
|
||||||
"Независимая оценка по замкнутому контуру (окружность $R+\\delta$, $\\delta=2$ тонкие ячейки —\n",
|
|
||||||
"вне зоны Bouzidi-реконструкции). Полный баланс импульса контрольного объёма:\n",
|
|
||||||
"$$ \\mathbf F_{тела} = \\oint \\bigl[\\sigma\\cdot\\mathbf n\n",
|
|
||||||
" - \\rho\\,\\mathbf u\\,(\\mathbf u\\cdot\\mathbf n)\\bigr]\\, ds\n",
|
|
||||||
" \\;-\\;\\underbrace{\\frac{d}{dt}\\!\\int_{кольцо}\\rho\\mathbf u\\,dV}_{\\to\\,0\\ в\\ среднем},\\qquad\n",
|
|
||||||
" \\sigma_{\\alpha\\beta} = -p'\\,\\delta_{\\alpha\\beta}\n",
|
|
||||||
" - \\Bigl(1-\\frac{1}{2\\tau_1}\\Bigr)\\,\\Pi^{neq,dev}_{\\alpha\\beta}. $$\n",
|
|
||||||
"Тонкости:\n",
|
|
||||||
"- давление без константы ($\\oint p_0\\mathbf n\\,ds\\equiv0$);\n",
|
|
||||||
"- строго **девиаторная** проекция $\\Pi^{neq}=\\sum_i \\mathbf c_i\\mathbf c_i(f_i-f_i^{eq})$ —\n",
|
|
||||||
" в KBC след релаксирует со стабилизатором $\\gamma\\beta$, а не $1/\\tau$, тогда как\n",
|
|
||||||
" сдвиг-моменты — с точным $2\\beta=1/\\tau$;\n",
|
|
||||||
"- **конвективный член** $-\\rho\\mathbf u(\\mathbf u\\cdot\\mathbf n)$ зануляется только при\n",
|
|
||||||
" $\\delta\\to0$ (no-slip); в первой версии кросс-чека он был опущен, что давало\n",
|
|
||||||
" **систематический сдвиг $+4\\%$ по $\\langle C_d\\rangle$ на всех сетках** (см. таблицы\n",
|
|
||||||
" разделов 22–23) — диагностическая ценность: смещение одинаково при любом $N_y$, т.е. это\n",
|
|
||||||
" свойство эстиматора, а не течения. Добавлен в `forces_stress.py`;\n",
|
|
||||||
"- сравнение — по средним ($\\langle C_d\\rangle$, rms $C_l$): мгновенные ряды сдвинуты по фазе\n",
|
|
||||||
" инерцией кольца $R..R{+}\\delta$ (член $d/dt$).\n",
|
|
||||||
"\n",
|
|
||||||
"**Согласие двух независимых методов = считывание силы корректно** — расхождение с\n",
|
|
||||||
"литературой тогда лежит в физике постановки, что и проверяют эксперименты разделов 23–24."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "markdown",
|
|
||||||
"id": "ccac6a82",
|
|
||||||
"metadata": {},
|
|
||||||
"source": [
|
|
||||||
"## 22. Результаты основного прогона (174×90, 100k шагов, 2026-06-09)\n",
|
|
||||||
"\n",
|
|
||||||
"Прогон на GPU-сервере: CuPy float32, CUDA-графы активны (self-check $\\Delta=0$), 271 с на\n",
|
|
||||||
"100k шагов L0 (вместе с L1-подшагами и гифкой).\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"**Валидация** ($\\beta_{бл}=D/N_y=0.178$, осреднение по второй половине ряда):\n",
|
|
||||||
"\n",
|
|
||||||
"| | raw | ×(1−β)$^k$ | лит. безгранич. (Re≈150) |\n",
|
|
||||||
"|---|---|---|---|\n",
|
|
||||||
"| St | 0.221 | **0.181** | **0.183** ✓ (1%) |\n",
|
|
||||||
"| $C_d$ | 1.799 | 1.216 | 1.330 |\n",
|
|
||||||
"| rms $C_l$ | 0.7051 | 0.4767 | ~0.3 |\n",
|
|
||||||
"| $\\langle C_m\\rangle$ | **−0.00003** | | 0 ✓ |\n",
|
|
||||||
"\n",
|
|
||||||
"**Кросс-чек считывания** (версия ещё без конвективного члена — см. раздел 21):\n",
|
|
||||||
"\n",
|
|
||||||
"| метод | $\\langle C_d\\rangle$ | rms $C_l$ | $\\langle C_m\\rangle$ |\n",
|
|
||||||
"|---|---|---|---|\n",
|
|
||||||
"| GMEM (обмен импульсом) | 1.799 | 0.7051 | −0.00003 |\n",
|
|
||||||
"| ∮σ·n ds (контур R+δ) | 1.872 | 0.7252 | −0.00003 |\n",
|
|
||||||
"| расхождение | **4.1%** | 2.8% | — |\n",
|
|
||||||
"\n",
|
|
||||||
"Диагностика сходимости: $\\langle\\rho\\rangle = 1.001$ стабильна (масса заякорена); оконные\n",
|
|
||||||
"средние $C_d$ в окнах 6–10: 1.797, 1.813, 1.795, 1.797, 1.791 — плато; rms $C_l$ насыщен.\n",
|
|
||||||
"\n",
|
|
||||||
"**Выводы по считыванию** (всё подтверждается):\n",
|
|
||||||
"1. Два независимых метода согласуются в пределах 4% (систематика +4% объяснена опущенным\n",
|
|
||||||
" конвективным членом и закрыта — раздел 21).\n",
|
|
||||||
"2. $\\langle C_m\\rangle\\approx-3\\cdot10^{-5}$ при масштабе $C_l\\sim0.7$ — симметрия в норме.\n",
|
|
||||||
"3. $C_d=1.799$ воспроизводит предыдущую итерацию бит-в-бит: осреднение силы по подшагам L1\n",
|
|
||||||
" не сместило среднее (регрессии нет, ряды стали глаже).\n",
|
|
||||||
"4. Следовательно $C_d\\approx1.8$ — **истинная сила в смоделированной постановке**; вопрос\n",
|
|
||||||
" к расхождению с литературой — вопрос к *постановке*, а не к считыванию.\n",
|
|
||||||
"\n",
|
|
||||||
"## 23. Эксперимент №0: серия по боковой блокировке — гипотеза ОПРОВЕРГНУТА\n",
|
|
||||||
"\n",
|
|
||||||
"**Гипотеза**: завышение $C_d$/rms $C_l$ объясняется боковым стеснением канала; тогда при\n",
|
|
||||||
"$D=16$ фикс. и $N_y\\uparrow$ величины должны монотонно стремиться к безграничной литературе,\n",
|
|
||||||
"а экстраполяция $\\beta\\to0$ — попасть в $C_d\\approx1.33$, rms $C_l\\approx0.3$.\n",
|
|
||||||
"\n",
|
|
||||||
"**Постановка**: $N_y\\in\\{90,128,180\\}$ → $\\beta\\in\\{0.178,0.125,0.089\\}$; центр и патч\n",
|
|
||||||
"выводятся из $N_y$; всё остальное фиксировано. 3×100k шагов (по ~282 с).\n",
|
|
||||||
"\n",
|
|
||||||
"**Результат** (`python run_blockage.py`, 2026-06-09):\n",
|
|
||||||
"\n",
|
|
||||||
"| $N_y$ | β | St | St(1−β) | $\\langle C_d\\rangle$ | rms $C_l$ | $\\langle C_m\\rangle$ | $C_d$ σ·n | dCd |\n",
|
|
||||||
"|---|---|---|---|---|---|---|---|---|\n",
|
|
||||||
"| 90 | 0.178 | 0.221 | 0.181 | 1.799 | 0.7051 | −0.00003 | 1.872 | 4.1% |\n",
|
|
||||||
"| 128 | 0.125 | 0.196 | 0.171 | 1.771 | 0.6734 | −0.00007 | 1.838 | 3.8% |\n",
|
|
||||||
"| 180 | 0.089 | 0.193 | 0.176 | **1.870** | **0.8354** | +0.00043 | 1.937 | 3.6% |\n",
|
|
||||||
"\n",
|
|
||||||
"Экстраполяция $\\beta\\to0$ (интерсепт линейного / квадратичного фита, спред = неопределённость):\n",
|
|
||||||
"\n",
|
|
||||||
"| | лин. | квадр. | спред | лит. |\n",
|
|
||||||
"|---|---|---|---|---|\n",
|
|
||||||
"| $C_d(0)$ | 1.905 | 1.855 | 0.050 | 1.33 |\n",
|
|
||||||
"| rms $C_l(0)$ | 0.910 | 0.818 | 0.091 | ~0.3 |\n",
|
|
||||||
"| St(0) | 0.161 | **0.181** | 0.020 | **0.183** |\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"**Анализ:**\n",
|
|
||||||
"1. $C_d(\\beta)$ **немонотонен** (1.80 → 1.77 → 1.87), rms $C_l$ при самом широком канале даже\n",
|
|
||||||
" вырос — экстраполяция «$C_d(0)\\approx1.9$» физически абсурдна и означает одно: **β — не\n",
|
|
||||||
" управляющий параметр**. Поправка $(1-\\beta)^2$ из предыдущей итерации была совпадением на\n",
|
|
||||||
" одной точке ($N_y=90$).\n",
|
|
||||||
"2. Это согласуется с классической теорией продувок: поправки Аллена–Винченти\n",
|
|
||||||
" (solid + wake blockage) при $\\beta=0.178$ дают завышение лишь ~15–18%, при $\\beta=0.089$ —\n",
|
|
||||||
" ~10%. Боковое стеснение объясняло максимум половину превышения даже на исходной сетке.\n",
|
|
||||||
"3. **St — единственная величина с правильным поведением**: raw 0.221→0.196→0.193 монотонно\n",
|
|
||||||
" стремится к 0.183 сверху; квадратичная экстраполяция даёт **0.181 ≈ лит. 0.183**.\n",
|
|
||||||
" Кинематика вихреобразования (частота, эффективное Re) верна; завышены именно силовые\n",
|
|
||||||
" (давленческие) амплитуды.\n",
|
|
||||||
"4. **Улика — поведение $N_y=180$**: низкочастотная модуляция с размахом оконных $C_d$\n",
|
|
||||||
" 1.68–1.98 и rms $C_l$ 0.58–0.96 (период ~20–30k шагов; на 50k установившегося режима —\n",
|
|
||||||
" всего 2–3 цикла, статистика среднего не набрана; $\\langle C_m\\rangle$ на порядок больше,\n",
|
|
||||||
" чем на узких каналах). Чем шире канал, тем свободнее меандрирует дорожка — и тем сильнее\n",
|
|
||||||
" она бьётся о **жёсткое условие $u_y=0$ на выходе**, порождая отражения и обратную связь.\n",
|
|
||||||
"5. **Диагноз**: доминируют **продольные ограничения**, которые серия держала константой:\n",
|
|
||||||
" вход 2.5D до тела (жёсткий равномерный профиль), выход 8.3D (Zou-He давление + $u_y=0$ —\n",
|
|
||||||
" отражает вихри), стык L1→L0 на 6.25D. Их изолирует эксперимент №1.\n",
|
|
||||||
"\n",
|
|
||||||
"## 24. Эксперимент №1: факторное исследование продольных границ\n",
|
|
||||||
"\n",
|
|
||||||
"**Метод**: каждый подозреваемый фактор варьируется **отдельно** при фиксированной боковой\n",
|
|
||||||
"блокировке ($N_y=90$, $\\beta=0.178$) и 100k шагах — изоляция вкладов входа, выхода и\n",
|
|
||||||
"выходного ГУ. Реализация: `config.py` выводит x-границы патча из `cx` (патч едет за телом),\n",
|
|
||||||
"`boundary.channel_bc` получил режим `outlet_uy`:\n",
|
|
||||||
"- `\"zero\"` — классический Zou-He: $u_y=0$ жёстко (как во всех прогонах выше);\n",
|
|
||||||
"- `\"extrapolate\"` — $u_y$ нуль-градиент (берётся из столбца $x{=}-2$), якорь $\\rho=1$\n",
|
|
||||||
" сохраняется; алгебраически: $f_6 \\mathrel{+}= \\tfrac12\\rho u_y$, $f_7 \\mathrel{-}= \\tfrac12\\rho u_y$\n",
|
|
||||||
" при тех же $f_3$ и $u_x$ (балансы массы и импульса выполняются точно).\n",
|
|
||||||
"\n",
|
|
||||||
"| Фактор | Nx | cx | вход | выход | $u_y$-выход | что изолирует |\n",
|
|
||||||
"|---|---|---|---|---|---|---|\n",
|
|
||||||
"| B0_baseline | 174 | 40 | 2.5D | 8.3D | zero | контроль (+ кросс-чек с конвективным членом) |\n",
|
|
||||||
"| F1_outlet_far | 300 | 40 | 2.5D | 16.2D | zero | близость выхода |\n",
|
|
||||||
"| F2_inlet_far | 230 | 96 | 6.0D | 8.3D | zero | близость входа |\n",
|
|
||||||
"| F3_inout_far | 390 | 96 | 6.0D | 18.3D | zero | оба продольных расстояния |\n",
|
|
||||||
"| F4_outlet_uy | 174 | 40 | 2.5D | 8.3D | extrapolate | кинематическое отражение вихрей ГУ |\n",
|
|
||||||
"| F5_combo | 390 | 96 | 6.0D | 18.3D | extrapolate | «чистая» постановка |\n",
|
|
||||||
"\n",
|
|
||||||
"Метрики каждого прогона: St, $\\langle C_d\\rangle$, rms $C_l$, $\\langle C_m\\rangle$, кросс-чек\n",
|
|
||||||
"dCd (уже с конвективным членом), **std оконных средних** (мера НЧ-модуляции), $\\langle\\rho\\rangle$.\n",
|
|
||||||
"Каждый прогон фиксируется строкой в накопительном реестре `solver_2x_sdf/out/factors.csv`\n",
|
|
||||||
"(полная конфигурация + результаты + время) — документация всех изменений постановки.\n",
|
|
||||||
"\n",
|
|
||||||
"**Результаты** (`python run_factors.py`, 2026-06-10, 6×100k шагов по ~280 с):\n",
|
|
||||||
"\n",
|
|
||||||
"| Фактор | вход | выход | $u_y$-выход | St(1−β) | $\\langle C_d\\rangle$ | rms $C_l$ | dCd кросс | $\\langle\\rho\\rangle_{фин}$ | модуляция | статус |\n",
|
|
||||||
"|---|---|---|---|---|---|---|---|---|---|---|\n",
|
|
||||||
"| B0_baseline | 2.5D | 8.3D | zero | 0.181 | 1.799 | 0.705 | **1.0%** | 1.000 | 0.024 | ok |\n",
|
|
||||||
"| F1_outlet_far | 2.5D | 16.2D | zero | 0.163 | 2.838 | 6.794 | 1.6% | **1.663** | 0.225 | режим смещён |\n",
|
|
||||||
"| F2_inlet_far | 6.0D | 8.3D | zero | 0.138 | 2.229 | 6.120 | 9.7% | **1.655** | 0.306 | режим смещён (срыв на ~75k) |\n",
|
|
||||||
"| F3_inout_far | 6.0D | 18.3D | zero | 0.160 | 2.577 | 5.348 | 2.0% | **1.660** | 0.269 | режим смещён (срыв на ~20k) |\n",
|
|
||||||
"| **F4_outlet_uy** | 2.5D | 8.3D | extrapolate | **0.183** | **1.754** | **0.557** | **0.6%** | **1.0000** | **0.023** | **ok ✓** |\n",
|
|
||||||
"| F5_combo | 6.0D | 18.3D | extrapolate | — | — | — | — | NaN | — | **взрыв на 4.5k** |\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"**Анализ:**\n",
|
|
||||||
"1. **Конвективный член подтверждён**: кросс-чек B0 сжался с 4.1% до **1.0%** (F4 — 0.6%) при\n",
|
|
||||||
" неизменной физике. Считывание силы теперь согласовано двумя методами на уровне ~1%.\n",
|
|
||||||
"2. **F4 (мягкий $u_y$-выход) — выигрыш по всем метрикам**: rms $C_l$ 0.705→0.557 (−21%),\n",
|
|
||||||
" $\\langle C_d\\rangle$ 1.799→1.754, $\\langle\\rho\\rangle=1.0000$ (точнее базы), нулевая\n",
|
|
||||||
" НЧ-модуляция (окна 6–10: $C_d$ 1.753–1.755, идеальное плато) и **St·(1−β)=0.1828 ≈ лит.\n",
|
|
||||||
" 0.183 точно**. Жёсткое $u_y=0$ действительно кинематически отражало вихри.\n",
|
|
||||||
"3. **Главное открытие — длинные домены без демпфера невалидны.** Все три (F1/F2/F3, $u_y=0$)\n",
|
|
||||||
" уходят на смещённую ветвь $\\langle\\rho\\rangle\\approx1.66$ (F1 и F3 — за ~20k шагов, F2 —\n",
|
|
||||||
" на ~75k), где $C_d$, $C_l$ бессмысленны для сравнения (у F2 разваливается и кросс-чек:\n",
|
|
||||||
" 9.7% — поле у контура нестационарно «звенит»). Механизм: канал «вход-скорость +\n",
|
|
||||||
" выход-давление» — **недодемпфированный акустический резонатор**; затухание продольной моды\n",
|
|
||||||
" $\\propto\\nu(\\pi/N_x)^2$ падает в ~5 раз при $N_x$ 174→390. Стартовый транзиент раскачивает\n",
|
|
||||||
" моду, вход с фиксированной скоростью качает массу $\\propto\\rho_{in}$ (положительная обратная\n",
|
|
||||||
" связь) — система садится на новую ветвь равновесия потоков массы. **F5** — чистая картина\n",
|
|
||||||
" роста: $\\langle\\rho\\rangle$ по окнам осциллирует 1.04→0.93→1.05 с периодом\n",
|
|
||||||
" $\\approx 2N_x/c_s\\approx1350$ шагов (точно акустический период туда-обратно) → NaN на 4.5k.\n",
|
|
||||||
"4. При этом rms $u_y$ зонда нормальный (0.033–0.046) во **всех** прогонах — гидродинамика\n",
|
|
||||||
" дорожки жива; ломаются акустика и массовый баланс. Это ретроспективно объясняет и «загадку»\n",
|
|
||||||
" эксперимента №0: $N_y=180$ был зачатком той же моды (НЧ-модуляция), но поперечное расширение\n",
|
|
||||||
" менее опасно продольного.\n",
|
|
||||||
"\n",
|
|
||||||
"**Урок**: продольное удлинение требует **поглощающего слоя** — стандартное лекарство из\n",
|
|
||||||
"каталога ГУ (раздел 15), упоминавшееся ещё в прежней 3D-реализации.\n",
|
|
||||||
"\n",
|
|
||||||
"## 25. Эксперимент №2: губка (absorbing layer) перед выходом\n",
|
|
||||||
"\n",
|
|
||||||
"Реализация: в последних `sponge_len` столбцах L0 вязкость плавно (smoothstep) растёт до\n",
|
|
||||||
"$\\nu\\cdot$`sponge_nu_mult` (по умолчанию ×30 — локальное Re падает до ~5, вихри и акустика\n",
|
|
||||||
"диссипируют до прихода на выход). Технически $\\beta$ на L0 становится **полем** $\\beta(x)$\n",
|
|
||||||
"(KBC это допускает: $\\gamma$ и так поузловой; CUDA-граф не страдает — поле предвычислено);\n",
|
|
||||||
"патч L1 губку не видит (гарантировано assert'ом). Ручки: `AMR_SPONGE_LEN`, `AMR_SPONGE_NU`.\n",
|
|
||||||
"\n",
|
|
||||||
"| Фактор | геометрия | $u_y$-выход | губка | что проверяет |\n",
|
|
||||||
"|---|---|---|---|---|\n",
|
|
||||||
"| F6_sponge | 174×90 (база) | extrapolate | 32 | валидация губки против F4 (не портит ли короткий домен) |\n",
|
|
||||||
"| F7_long_sponge | 390×90, вход 6D / выход 18.3D | extrapolate | 32 | **целевая «чистая» постановка** |\n",
|
|
||||||
"| F8_long_sp_uy0 | 390×90 | zero | 32 | изоляция: достаточно ли губки без мягкого $u_y$ |\n",
|
|
||||||
"\n",
|
|
||||||
"> ⏳ *Числа появятся после `python run_factors.py` с факторами F6–F8 (3×100k ≈ 15 мин).*\n",
|
|
||||||
"\n",
|
|
||||||
"**Критерии**: у всех $\\langle\\rho\\rangle\\approx1.000$ и нет НЧ-модуляции; F7 — ожидание\n",
|
|
||||||
"$\\langle C_d\\rangle\\approx1.5$–1.6 (остаётся боковая блокировка β=0.178: по Аллену–Винченти\n",
|
|
||||||
"+15–18% к безграничному 1.33) и rms $C_l<0.45$. Если критерии выполнены — **эксперимент №3**:\n",
|
|
||||||
"повторная серия по β ($N_y=90/128/180$) на конфигурации F7 → финальная экстраполяция к\n",
|
|
||||||
"безграничному пределу. Остаточное расхождение после неё — фактор разрешения (D-рефайнмент)."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "markdown",
|
|
||||||
"id": "0afa973f",
|
|
||||||
"metadata": {},
|
|
||||||
"source": [
|
|
||||||
"---\n",
|
|
||||||
"# Часть VII. Перенос на D3Q27 и связь с целевой реализацией\n",
|
|
||||||
"\n",
|
|
||||||
"Вся математика KBC переносится в 3D **без изменений по сути**: решётка D3Q27 (веса\n",
|
|
||||||
"$8/27, 2/27, 1/54, 1/216$ — изотропия 4-го порядка, предпочтительна для KBC/турбулентности),\n",
|
|
||||||
"моментный базис 27×27, сдвиговая часть $s$ — девиаторный тензор (5 независимых компонент).\n",
|
|
||||||
"Формула $\\gamma$ и оператор $f\\leftarrow f-\\beta(2\\Delta s+\\gamma\\Delta h)$ идентичны.\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"Чем настоящий KBC отличается от прежнего подхода в 3D-движке (poly eq + TRT + α-лимитер +\n",
|
|
||||||
"Smagorinsky):\n",
|
|
||||||
"\n",
|
|
||||||
"| Аспект | Настоящий KBC | Прежний подход |\n",
|
|
||||||
"|---|---|---|\n",
|
|
||||||
"| Равновесие | энтропийное product-form | полиномиальное $O(u^2)$ |\n",
|
|
||||||
"| Столкновение | энтропийный MRT, $k/s/h$ | TRT (симм./антисимм.) |\n",
|
|
||||||
"| Стабилизация | $\\gamma$ из энтропии | α-лимитер положительности |\n",
|
|
||||||
"| H-теорема | встроена в вывод $\\gamma$ | контролируется отдельно |\n",
|
|
||||||
"| Турбулентность | implicit LES (параметр-free) | явный Smagorinsky |\n",
|
|
||||||
"| Решётка | D3Q27 | D3Q27 (совпадает) |\n",
|
|
||||||
"\n",
|
|
||||||
"**Дорожная карта переноса** (всё отработано на 2D-макете): (1) product-form $f^{eq}$;\n",
|
|
||||||
"(2) проекторы $P_s$ для D3Q27 (предвычислить); (3) per-node $\\gamma$; (4) столкновение;\n",
|
|
||||||
"(5) SDF+Bouzidi и GMEM-сила с $\\mathbf u_w$ на линке (подвижные тела) + кросс-чек ∮σ·n dA по\n",
|
|
||||||
"SDF-изоповерхности; (6) AMR-связка $\\tau_f=2\\tau_c-\\tfrac12$ с временно́й интерполяцией."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "markdown",
|
|
||||||
"id": "d0691787",
|
|
||||||
"metadata": {},
|
|
||||||
"source": [
|
|
||||||
"---\n",
|
|
||||||
"# Часть VIII. Сверка формул с первоисточниками\n",
|
|
||||||
"\n",
|
|
||||||
"Все формулы, ранее помеченные «⚠ сверить с оригиналом», проверены по первоисточникам\n",
|
|
||||||
"(плюс численная самопроверка каждой в `demos_gpu/checks_gpu.py`):\n",
|
|
||||||
"\n",
|
|
||||||
"| Формула | Источник | Статус |\n",
|
|
||||||
"|---|---|---|\n",
|
|
||||||
"| Product-form $f^{eq}$: $A(u)=2-\\sqrt{1+3u^2}$, $B(u)=\\frac{2u+\\sqrt{1+3u^2}}{1-u}$ | Bösch/Chikatamarla/Karlin, PRE **92**, 043309 (2015), ур. (7)–(11); Ansumali/Karlin/Öttinger, EPL **63**, 798 (2003) | ✓ дословно |\n",
|
|
||||||
"| $f' = f-\\beta(2\\Delta s+\\gamma\\Delta h)$, $\\beta=1/(2\\tau)$, mirror-форма | Karlin/Bösch/Chikatamarla, PRE **90**, 031302(R) (2014) | ✓ дословно |\n",
|
|
||||||
"| $\\gamma^\\*=\\frac1\\beta-(2-\\frac1\\beta)\\frac{\\langle\\Delta s|\\Delta h\\rangle}{\\langle\\Delta h|\\Delta h\\rangle}$, $\\langle X|Y\\rangle=\\sum X_iY_i/f_i^{eq}$ | там же; PRE **92**, ур. (24)–(25) | ✓ дословно |\n",
|
|
||||||
"| Состав $s=\\{N,\\Pi_{xy}\\}$ (девиатор; «KBC-d»/N1) | PRE **92** (семейство $s\\in\\{d,d{+}t,d{+}q,d{+}t{+}q\\}$); Dorschner et al., JFM **801**, 623 (2016) | ✓ |\n",
|
|
||||||
"| AMR: $\\beta_f=\\frac{1}{1+r(1/\\beta_c-1)}$ ⇔ $\\tau_f=2\\tau_c-\\frac12$; $f^{neq}\\cdot\\frac{\\beta_c}{r\\beta_f}=\\frac{\\tau_f}{2\\tau_c}f^{neq}$ | Dorschner/Frapolli/Chikatamarla/Karlin, PRE **94**, 053311 (2016), ур. (34), (40) | ✓ дословно |\n",
|
|
||||||
"| Временна́я интерполяция ghost-границы | Lagrava/Malaspinas/Latt/Chopard, JCP **231**, 4808 (2012) | ✓ |\n",
|
|
||||||
"| Bouzidi: ветви $q<\\frac12$ / $q\\ge\\frac12$ | Bouzidi/Firdaouss/Lallemand, Phys. Fluids **13**, 3452 (2001) | ✓ |\n",
|
|
||||||
"| Zou-He: скоростной вход / давление-выход | Zou & He, Phys. Fluids **9**, 1591 (1997) | ✓ |\n",
|
|
||||||
"| GMEM: $(\\mathbf c_i-\\mathbf u_w)f_i^{post}-(\\mathbf c_{\\bar i}-\\mathbf u_w)f_{\\bar i}^{stream}$ | Wen/Zhang/Tu/Wang/Fang, JCP **266**, 161 (2014) | ✓ |\n",
|
|
||||||
"| $\\sigma^v=-(1-\\frac{1}{2\\tau})\\Pi^{neq}$ (Чепмен–Энског) | стандарт (Krüger et al., *The Lattice Boltzmann Method*, 2017) | ✓ |\n",
|
|
||||||
"\n",
|
|
||||||
"**Литература:**\n",
|
|
||||||
"- I. V. Karlin, F. Bösch, S. S. Chikatamarla, *Gibbs' principle for the lattice-kinetic theory\n",
|
|
||||||
" of fluid dynamics*, Phys. Rev. E **90**, 031302(R) (2014).\n",
|
|
||||||
"- F. Bösch, S. S. Chikatamarla, I. V. Karlin, *Entropic multirelaxation lattice Boltzmann\n",
|
|
||||||
" models for turbulent flows*, Phys. Rev. E **92**, 043309 (2015) (arXiv:1507.02518).\n",
|
|
||||||
"- S. Ansumali, I. V. Karlin, H. C. Öttinger, *Minimal entropic kinetic models for\n",
|
|
||||||
" hydrodynamics*, Europhys. Lett. **63**, 798 (2003).\n",
|
|
||||||
"- B. Dorschner, F. Bösch, S. S. Chikatamarla, K. Boulouchos, I. V. Karlin, *Entropic\n",
|
|
||||||
" multi-relaxation time lattice Boltzmann model for complex flows*, J. Fluid Mech. **801**,\n",
|
|
||||||
" 623–651 (2016).\n",
|
|
||||||
"- B. Dorschner, N. Frapolli, S. S. Chikatamarla, I. V. Karlin, *Grid refinement for entropic\n",
|
|
||||||
" lattice Boltzmann models*, Phys. Rev. E **94**, 053311 (2016) (arXiv:1608.06915).\n",
|
|
||||||
"- D. Lagrava, O. Malaspinas, J. Latt, B. Chopard, *Advances in multi-domain lattice Boltzmann\n",
|
|
||||||
" grid refinement*, J. Comput. Phys. **231**, 4808 (2012).\n",
|
|
||||||
"- M. Bouzidi, M. Firdaouss, P. Lallemand, *Momentum transfer of a Boltzmann-lattice fluid with\n",
|
|
||||||
" boundaries*, Phys. Fluids **13**, 3452 (2001).\n",
|
|
||||||
"- Q. Zou, X. He, *On pressure and velocity boundary conditions for the lattice Boltzmann BGK\n",
|
|
||||||
" model*, Phys. Fluids **9**, 1591 (1997).\n",
|
|
||||||
"- B. Wen, C. Zhang, Y. Tu, C. Wang, H. Fang, *Galilean invariant fluid–solid interfacial\n",
|
|
||||||
" dynamics in lattice Boltzmann simulations*, J. Comput. Phys. **266**, 161 (2014).\n",
|
|
||||||
"- Z. Guo, C. Zheng, B. Shi, *Discrete lattice effects on the forcing term in the lattice\n",
|
|
||||||
" Boltzmann method*, Phys. Rev. E **65**, 046308 (2002).\n",
|
|
||||||
"- U. Ghia, K. N. Ghia, C. T. Shin, *High-Re solutions for incompressible flow using the\n",
|
|
||||||
" Navier-Stokes equations and a multigrid method*, J. Comput. Phys. **48**, 387 (1982)."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "markdown",
|
|
||||||
"id": "a8cc8c0d",
|
|
||||||
"metadata": {},
|
|
||||||
"source": [
|
|
||||||
"---\n",
|
|
||||||
"# Часть IX. Итоги\n",
|
|
||||||
"\n",
|
|
||||||
"- **KBC** = энтропийный MRT-LBM: сохраняющиеся моды не трогаем, сдвиг релаксируем фиксированным\n",
|
|
||||||
" $2\\beta$ (вязкость), высшие моды — адаптивным $\\gamma$ из условия неувеличения энтропии.\n",
|
|
||||||
" При $\\gamma=2$ — в точности BGK. Все формулы сверены с первоисточниками и самопроверены\n",
|
|
||||||
" численно.\n",
|
|
||||||
"- **Бенчмарки** (на компонентах финальной модели): вязкость по TGV (<6%, факт ≲1%), H-теорема,\n",
|
|
||||||
" устойчивость на сдвиговом слое (BGK разрушается — KBC держит), каверна ≈ Ghia, дорожки\n",
|
|
||||||
" Кармана на четырёх телах.\n",
|
|
||||||
"- **Измельчение сетки**: связка $\\tau_f=2\\tau_c-\\tfrac12$ + масштабирование $f^{neq}$ +\n",
|
|
||||||
" временна́я интерполяция ghost; вложенный 2×+4× даёт точность мелкой сетки за в разы меньшую\n",
|
|
||||||
" работу.\n",
|
|
||||||
"- **Финальная модель** `solver_2x_sdf`: KBC-N1 + SDF/Bouzidi + Zou-He + free-slip + CUDA-графы.\n",
|
|
||||||
" Главные уроки эволюции: второй член силы — из post-streaming; дрейф массы лечится\n",
|
|
||||||
" давлением-выходом; зонд и сила — на тонком уровне; графы требуют чистоты горячего пути.\n",
|
|
||||||
"- **Считывание сил ДОКАЗАНО корректным** (разделы 22, 24): два независимых метода — GMEM с\n",
|
|
||||||
" торком и баланс импульса ∮[σ·n−ρu(u·n)]ds — после добавления конвективного члена согласуются\n",
|
|
||||||
" на уровне **0.6–1.0%** по $\\langle C_d\\rangle$; $\\langle C_m\\rangle\\approx0$; масса стабильна.\n",
|
|
||||||
"- **Исследование факторов постановки** (разделы 23–25):\n",
|
|
||||||
" - эксперимент №0 (серия по β) **опроверг** гипотезу бокового стеснения: $C_d(\\beta)$\n",
|
|
||||||
" немонотонен; при этом **St(β→0)=0.181 ≈ лит. 0.183** — кинематика верна;\n",
|
|
||||||
" - эксперимент №1 (6 факторов): **мягкий $u_y$-выход (F4) улучшил всё** (rms $C_l$ −21%,\n",
|
|
||||||
" St·(1−β)=0.183 точно, идеальное плато) — жёсткое $u_y=0$ отражало вихри. **Длинные домены\n",
|
|
||||||
" без демпфера невалидны**: пара «вход-скорость / выход-давление» — недодемпфированный\n",
|
|
||||||
" акустический резонатор (затухание ∝ν(π/Nx)²); масса накачивается до смещённой ветви\n",
|
|
||||||
" ⟨ρ⟩≈1.66 либо взрыв (F5, период осцилляций ⟨ρ⟩ ≈ 2Nx/c_s — прямая улика);\n",
|
|
||||||
" - эксперимент №2 (в работе): **губка перед выходом** (β(x)-поле на L0) — F6/F7/F8; затем\n",
|
|
||||||
" эксперимент №3 — серия по β на чистой постановке F7 → финальная экстраполяция.\n",
|
|
||||||
"- **Реестр всех прогонов** — `solver_2x_sdf/out/factors.csv`: полная конфигурация + метрики\n",
|
|
||||||
" каждого изменения постановки (готовый материал для отдельного исследования факторов).\n",
|
|
||||||
"- **Дальше**: эксперименты №2–3 → финальные числа; при остаточном превышении — фактор\n",
|
|
||||||
" разрешения (D-рефайнмент). Затем перенос отработанной схемы на D3Q27/GPU в основной движок\n",
|
|
||||||
" (часть VII)."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"metadata": {
|
|
||||||
"kernelspec": {
|
|
||||||
"display_name": "Python 3",
|
|
||||||
"language": "python",
|
|
||||||
"name": "python3"
|
|
||||||
},
|
|
||||||
"language_info": {
|
|
||||||
"name": "python",
|
|
||||||
"version": "3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nbformat": 4,
|
|
||||||
"nbformat_minor": 5
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
# solver_2x_sdf — модель цилиндра 2×+SDF, разложенная по компонентам
|
|
||||||
|
|
||||||
Самодостаточный LBM-решатель (D2Q9, KBC) для обтекания цилиндра: грубый уровень **L0**
|
|
||||||
плюс один вложенный патч **L1** (измельчение ×2). Граница цилиндра — **SDF + Bouzidi (no-slip)**;
|
|
||||||
вход/выход — **Zou-He** (скорость/давление); верх/низ канала — **free-slip (specular)**.
|
|
||||||
Только GPU/CuPy — **CPU-фолбэка нет**. Запуск — из этой папки: `python run.py`
|
|
||||||
(серия по блокировке: `python run_blockage.py`).
|
|
||||||
|
|
||||||
## Карта файлов (каждый компонент изолирован и легко меняется)
|
|
||||||
|
|
||||||
| Файл | Ответственность |
|
|
||||||
|---|---|
|
|
||||||
| `backend.py` | жёсткое требование CuPy/GPU, `DTYPE`, device-скаляры, `to_cpu` |
|
|
||||||
| `config.py` | все параметры модели + производные (τ, β, R, DL, cy/патч из Ny); `from_env()` |
|
|
||||||
| `lattice.py` | D2Q9 (c_i, w_i, OPP) + KBC-проектор `Ps` на сдвиг-моменты |
|
|
||||||
| `equilibrium.py` | `feq` (product-form, без `pow`), `macros` (ρ, u) |
|
|
||||||
| `collision.py` | **столкновение**: `kbc_collide` (энтропийный KBC-N1) — единственный оператор |
|
|
||||||
| `streaming.py` | **перенос**: `stream` (пул-схема через roll) |
|
|
||||||
| `geometry.py` | маски тела/стенок и SDF для L0 и L1 |
|
|
||||||
| `boundary.py` | `build_bc`/`apply_bc` (SDF+Bouzidi) + `channel_bc` (Zou-He) + `free_slip_walls` |
|
|
||||||
| `forces.py` | **силы**: галилей-инвариантный обмен импульсом `force_gmem` (Fx, Fy, **торк Tz**) + `coefficients` (Cd, Cl, **Cm**) |
|
|
||||||
| `forces_stress.py` | **кросс-чек силы**: ∮σ·n ds по контуру R+δ вокруг тела (независимый эстиматор) |
|
|
||||||
| `amr.py` | связка L0↔L1: `patch`/`ghost`/`fill`/`restrict` |
|
|
||||||
| `solver.py` | оркестратор: persistent-буферы, `step()`, `run()` (+ CUDA-граф) |
|
|
||||||
| `diagnostics.py` | `analyze`, St (параболическая интерполяция пика), кросс-чек, фиты серии по β |
|
|
||||||
| `visualization.py` | гифка |ω|, png-панель рядов (`series_2x_sdf.png`), прогресс-бар |
|
|
||||||
| `run.py` | точка входа (один прогон) |
|
|
||||||
| `run_blockage.py` | серия по блокировке Ny↑ → β↓, экстраполяция β→0, сводный npz+png |
|
|
||||||
| `run_factors.py` | факторное исследование постановки (вход/выход/uy-выход); реестр `out/factors.csv` |
|
|
||||||
|
|
||||||
## Где какая математика (для ревизии)
|
|
||||||
|
|
||||||
- **Столкновение** — `collision.py`. KBC-N1: `f ← f − β(2Δs + γΔh)`, где `Δs = Ps·(f−feq)` —
|
|
||||||
проекция на сдвиг-моменты, `γ` — энтропийный лимитер. Чтобы поэкспериментировать:
|
|
||||||
переключить `cfg.collision="bgk"`, либо добавить TRT новой функцией и зарегистрировать в `get`.
|
|
||||||
- **Перенос** — `streaming.py`. Чистая пул-схема; на физику влияет только корректность сдвигов.
|
|
||||||
- **Силы** — `forces.py`. Обмен импульсом по линкам тела:
|
|
||||||
`F = Σ_links c_i (f_i^{после столкн.} + f_ī^{после стриминга})`. Второй член — из поля ПОСЛЕ
|
|
||||||
стриминга (вернувшаяся Bouzidi-популяция); иначе ведущий симметричный член сокращается и Cd врёт.
|
|
||||||
Сила/момент меряются на **каждом** подшаге L1 и усредняются (анти-алиасинг; средний Cd не меняется).
|
|
||||||
**Торк** `Tz` — вокруг центра тела, точка приложения — пересечение линка со стенкой `r_f + q·c_i`
|
|
||||||
(Bouzidi-доля q уже лежит в BC); `Cm = 2Tz/(U²·DL²)`, для цилиндра ⟨Cm⟩≈0 — контроль симметрии.
|
|
||||||
- **Кросс-чек силы** — `forces_stress.py`. Независимый эстиматор — баланс импульса
|
|
||||||
контрольного объёма: `F = ∮[σ·n − ρu(u·n)] ds` по окружности R+δ (δ=2 тонкие ячейки);
|
|
||||||
`σ = −p′·I − (1−1/(2τ₁))·Π^neq_dev` (строго девиаторная проекция: след Π^neq в KBC
|
|
||||||
релаксирует с γβ, а не 1/τ). Конвективный член −ρu(u·n) обязателен при δ>0 — без него
|
|
||||||
систематика +4% по ⟨Cd⟩. Считается раз в `stress_every` шагов **вне CUDA-графа**; сравнение
|
|
||||||
по средним (⟨Cd⟩, rms Cl) — мгновенные ряды сдвинуты по фазе кольцом R..R+δ.
|
|
||||||
Согласие двух методов = считывание силы корректно.
|
|
||||||
- **Серия по блокировке** — `run_blockage.py`. D фиксирован, Ny ∈ {90,128,180} → β ∈ {0.178,
|
|
||||||
0.125, 0.089}; `cy` и y-границы патча выводятся из Ny автоматически (`config.__post_init__`).
|
|
||||||
Экстраполяция к β→0 двумя фитами (a+b·β и a+b·β²; спред интерсептов — неопределённость) —
|
|
||||||
прямое сравнение с безграничной литературой без ручной поправки (1−β)².
|
|
||||||
|
|
||||||
## ⚠ Сверка с первоисточниками (2026-08-14): найден дефект, все числа ниже требуют перепрогона
|
|
||||||
|
|
||||||
Сверка реализации со статьями в `docs/origins` (Karlin/Bösch/Chikatamarla, PRE **90**, 031302(R)
|
|
||||||
2014 — принцип; Bösch/Chikatamarla/Karlin, PRE **92**, 043309 2015 — алгоритм; arXiv:1507.02509 —
|
|
||||||
2D-реализация). **Ядро KBC-N1 подтверждено точным**: проектор `Ps` совпадает с аналитической
|
|
||||||
s-частью из ур.(10) 2D-статьи (ранг 2, идемпотентен, несёт ровно {N, Π_xy}); равновесие —
|
|
||||||
точный энтропийный максимизатор (сходится с численной максимизацией S при фикс. ρ, ρu);
|
|
||||||
γ по замкнутой формуле ур.(17)/(39) совпадает с корнем точного ур.(15); сдвиг-моменты
|
|
||||||
релаксируют ровно с 2β при любом γ; вязкость по затуханию сдвиговой волны воспроизводится
|
|
||||||
с ошибкой <0.1%.
|
|
||||||
|
|
||||||
**Дефект — порог `GEPS` в `kbc_collide`.** Он был АБСОЛЮТНЫМ (1e-6 в float32, режим по умолчанию),
|
|
||||||
тогда как `den = ⟨Δh|Δh⟩` квадратична по неравновесию и при разрешённой физике равна ~1e-7…1e-9.
|
|
||||||
Замерено на боевой конфигурации: откат `γ←2` срабатывал на **77–99% узлов**, то есть решатель
|
|
||||||
считал **чистый LBGK**, а не KBC. На эталоне 2D-статьи (двойной периодический сдвиговый слой,
|
|
||||||
Re=30000, N=128, разд. VII) это давало **+9.3%** по энстрофии на t=t_c (0.6599 против эталонных
|
|
||||||
0.6035); после исправления fp32 даёт 0.6038. Порог сделан относительным (`B.GREL`, доля от ‖Δ‖²);
|
|
||||||
на тех же полях он теперь не срабатывает нигде (min den/‖Δ‖² ≈ 1.5e-3 против порога 1e-8).
|
|
||||||
|
|
||||||
**Следствие: все количественные результаты ниже (Cd, rms Cl, St, серия по блокировке,
|
|
||||||
факторное исследование №0/№1) получены на дефектной ветке и подлежат перепрогону.**
|
|
||||||
Качественные выводы про ГУ (мода акустического резонатора, роль конвективного члена
|
|
||||||
в кросс-чеке) дефектом не затрагиваются.
|
|
||||||
|
|
||||||
Исправлено там же:
|
|
||||||
- **углы домена**: `stream()` на `roll` периодичен по обеим осям, а Zou-He стоял на срезе `1:-1` —
|
|
||||||
в 4 угловых узлах вход был напрямую связан с выходом (`f[1,0,0] == post[1,0,-1]`, величина ~0.12).
|
|
||||||
Порядок изменён на `free_slip_walls` → `channel_bc` по всему столбцу; заворот закрыт полностью.
|
|
||||||
- **диагностика ⟨ρ⟩** считалась по всему домену, включая фиктивные узлы внутри цилиндра
|
|
||||||
(смещение −5.4e-4 — ровно тот разряд, в котором ⟨ρ⟩ и цитируется). Теперь только по жидкости.
|
|
||||||
- **`refine` был зашит как 2** в `tau1`, `R01` и радиусе тела на L1/контуре σ·n. Обобщено
|
|
||||||
(при r=2 значения бит-в-бит прежние; при r=3 вязкость уровней теперь согласована).
|
|
||||||
|
|
||||||
**Отдельно — не дефект, а следствие выбора модели.** В KBC-N1 след тензора напряжений T отнесён
|
|
||||||
к h-части, поэтому объёмная вязкость плавает: ξ = c_s²(1/(γβ) − ½) (ур.36 статьи 2015, ур.57
|
|
||||||
2D-статьи). Замер на боевой конфигурации: γ ∈ [−4.1, 4.6], медиана ξ/ν ≈ 17, и на **2.1% узлов
|
|
||||||
ξ < 0** — акустическое АНТИдемпфирование. Это прямо релевантно диагнозу «недодемпфированный
|
|
||||||
акустический резонатор» ниже: KBC-N2 (T в s-части) даёт ξ = ν фиксированно и является
|
|
||||||
каноническим средством именно от этой проблемы. Модель не менялась — решение за автором.
|
|
||||||
|
|
||||||
## Статус: считывание силы валидировано; идёт факторное исследование постановки
|
|
||||||
|
|
||||||
Оператор столкновения зафиксирован — **KBC-N1 с энтропийным стабилизатором** (канонический
|
|
||||||
Karlin/Bösch/Chikatamarla 2014), без замен (никаких BGK/TRT).
|
|
||||||
**Полная документация исследования — в ноутбуке `../kbc_lbm.ipynb` (части VI, разделы 22–24).**
|
|
||||||
|
|
||||||
**Считывание силы доказано корректным** (прогон 100k, 174×90, 2026-06-09):
|
|
||||||
- кросс-чек GMEM vs ∮σ·n: dCd=4.1% (систематика объяснена конвективным членом −ρu(u·n),
|
|
||||||
добавлен в `forces_stress.py` — ожидаемое расхождение ≲1%);
|
|
||||||
- ⟨Cm⟩=−0.00003 (≈0 — симметрия), ⟨ρ⟩=1.001 стабильна, Cd=1.799 воспроизводит прежнюю
|
|
||||||
итерацию бит-в-бит (осреднение по подшагам не сместило среднее).
|
|
||||||
|
|
||||||
**Эксперимент №0 (серия по блокировке) — гипотеза бокового стеснения ОПРОВЕРГНУТА:**
|
|
||||||
Cd(β) немонотонен: 1.799 / 1.771 / **1.870** при Ny=90/128/180; rms Cl на широком канале
|
|
||||||
вырос (0.835), НЧ-модуляция (окна Cd 1.68–1.98). При этом **St(β→0)=0.181 ≈ лит. 0.183** —
|
|
||||||
кинематика верна, завышены давленческие амплитуды. Диагноз: продольные границы
|
|
||||||
(вход 2.5D с жёстким профилем; выход 8.3D Zou-He с жёстким uy=0 — отражает вихри).
|
|
||||||
|
|
||||||
**Эксперимент №1 (прогнан 2026-06-10) — факторное исследование** `run_factors.py`, 6 прогонов
|
|
||||||
при Ny=90 фикс. Итоги (полный разбор — ноутбук, раздел 24; реестр — `out/factors.csv`):
|
|
||||||
- конвективный член подтверждён: кросс-чек B0 4.1%→**1.0%**, F4 — **0.6%**;
|
|
||||||
- **F4 (мягкий uy-выход) — лучший по всем метрикам**: Cd 1.799→1.754, rms Cl 0.705→**0.557**,
|
|
||||||
St·(1−β)=**0.183** (точно лит.), ⟨ρ⟩=1.0000, нулевая НЧ-модуляция;
|
|
||||||
- **длинные домены без демпфера НЕвалидны** (F1/F2/F3 → смещённый режим ⟨ρ⟩≈1.66; F5 → NaN):
|
|
||||||
пара «вход-скорость / выход-давление» — недодемпфированный акустический резонатор
|
|
||||||
(затухание моды ∝ν(π/Nx)²; период осцилляций ⟨ρ⟩ у F5 ≈ 2Nx/c_s — прямая улика).
|
|
||||||
|
|
||||||
**Эксперимент №2 (в работе) — губка перед выходом**: плавный рост ν (×30, smoothstep) в
|
|
||||||
последних `sponge_len` столбцах L0; β(x) — поле (graph-safe), патч L1 губки не видит.
|
|
||||||
Факторы: F6_sponge (база+uy-extrap+губка 32 — валидация против F4), F7_long_sponge
|
|
||||||
(390×90, вход 6D/выход 18.3D — целевая постановка), F8_long_sp_uy0 (изоляция вклада губки).
|
|
||||||
Критерии: ⟨ρ⟩≈1.000 у всех; F7: Cd≈1.5–1.6, rms Cl<0.45. Затем эксперимент №3 — серия по β
|
|
||||||
на конфигурации F7.
|
|
||||||
|
|
||||||
| прогон | команда | артефакты в `out/` |
|
|
||||||
|---|---|---|
|
|
||||||
| смоук | `AMR_STEPS=2000 python run.py` | graph self-check OK, Cm≈0, кросс-чек ≲1% (с конв. членом) |
|
|
||||||
| основной | `python run.py` | `cyl_2x_sdf.gif`, `series_2x_sdf.png`, `probe_2x_sdf.npz` |
|
|
||||||
| серия по β | `python run_blockage.py` | `probe_blockage_Ny*.npz`, `blockage_summary.npz`, `blockage_extrapolation.png` |
|
|
||||||
| **факторы №2** | `AMR_FACTORS=F6_sponge,F7_long_sponge,F8_long_sp_uy0 python run_factors.py` | `factor_*.npz`, **`factors.csv`** (реестр; при смене схемы старый → `factors_vN.csv`), `factors_summary.png` |
|
|
||||||
|
|
||||||
Env-ручки: `AMR_NX`, `AMR_CX` (продольная геометрия; x-границы патча L1 выводятся из cx),
|
|
||||||
`AMR_OUTLET_UY=zero|extrapolate`, `AMR_SPONGE_LEN`/`AMR_SPONGE_NU` (губка),
|
|
||||||
`AMR_FACTORS=...` (подмножество факторов).
|
|
||||||
|
|
||||||
**Что привело сюда (хронология фиксов):**
|
|
||||||
1. Зонд скорости → L1; оценка St с параболической интерполяцией пика.
|
|
||||||
2. Сила → галилей-инвариантный `force_gmem` (Wen 2014), готов к подвижным телам.
|
|
||||||
3. Стартовое возмущение (поперечный sin-импульс входа) — запуск дорожки за единицы циклов.
|
|
||||||
4. Диагностика по окнам (`convergence_report`, трекинг ⟨ρ⟩) — вскрыла дрейф массы.
|
|
||||||
5. **Ключевой фикс:** грубое ГУ выхода (zero-gradient) копило массу → поток глох (Cd/rms→0 за 100k).
|
|
||||||
Заменено на **Zou-He скоростной вход + давление-выход** (`boundary.channel_bc`) — масса заякорена.
|
|
||||||
6. **Free-slip стенки** (`boundary.free_slip_walls`, specular) + умеренно расширенный домен (меньше
|
|
||||||
блокировка). Цилиндр остаётся no-slip.
|
|
||||||
7. Cm + осреднение по подшагам + кросс-чек ∮σ·n ds + серия по блокировке (текущая итерация).
|
|
||||||
|
|
||||||
**Прямой raw-матч на одной сетке непрактичен** (нужен `β<0.03 → Ny≈500+`); серия из трёх умеренных
|
|
||||||
Ny с экстраполяцией даёт безпоправочное сравнение за ~3 стоимости одного прогона (патч L1 —
|
|
||||||
основная работа — от Ny не зависит).
|
|
||||||
|
|
||||||
## CUDA Graphs
|
|
||||||
|
|
||||||
По умолчанию ВКЛ (`cfg.use_cuda_graph`, env `AMR_CUDAGRAPH=0` — выкл). **Захватываются успешно** (после
|
|
||||||
замены `cupy.stack`→`backend.pack`, отказа от cuBLAS в проекторе KBC и clip-литералов в ядре —
|
|
||||||
всё это убрало host→device при capture). На шаге t=1 — рантайм-самопроверка (граф vs eager); при
|
|
||||||
расхождении/сбое — откат на eager с печатью трейсбэка.
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
# AMR-связка L0 (грубый) ↔ L1 (тонкий, измельчение r).
|
|
||||||
#
|
|
||||||
# Идея: на тонком патче считаем r подшагов по времени на каждый шаг L0. Границы патча («ghost»)
|
|
||||||
# заполняем интерполяцией с грубого уровня (по пространству + по времени между «до» и «после» шага L0),
|
|
||||||
# а внутренность патча после подшагов проецируем обратно на грубый уровень (рестрикция).
|
|
||||||
# Неравновесная часть масштабируется множителями Rcf (грубый→тонкий) и Rfc (тонкий→грубый),
|
|
||||||
# т.к. при смене τ масштаб неравновесия меняется.
|
|
||||||
import backend as B
|
|
||||||
import lattice as L
|
|
||||||
from equilibrium import feq, macros
|
|
||||||
|
|
||||||
cp = B.cp; xp = B.xp; DTYPE = B.DTYPE
|
|
||||||
|
|
||||||
def patch(pNx, pNy, ax, bx, ay, by, r):
|
|
||||||
"""Описание патча: размеры (Nfy,Nfx), индексы/веса билинейной интерполяции с грубого уровня,
|
|
||||||
срезы для рестрикции каждого r-го узла."""
|
|
||||||
Wx, Wy = bx - ax, by - ay
|
|
||||||
Nfx, Nfy = r*Wx + 1, r*Wy + 1
|
|
||||||
FX, FY = cp.meshgrid(ax + cp.arange(Nfx)/r, ay + cp.arange(Nfy)/r) # координаты тонких узлов в L0
|
|
||||||
x0 = cp.floor(FX).astype(cp.int64); y0 = cp.floor(FY).astype(cp.int64)
|
|
||||||
x1 = cp.minimum(x0 + 1, pNx - 1); y1 = cp.minimum(y0 + 1, pNy - 1)
|
|
||||||
return dict(Nfx=int(Nfx), Nfy=int(Nfy), ax=ax, bx=bx, ay=ay, by=by, r=r,
|
|
||||||
x0=x0, y0=y0, x1=x1, y1=y1,
|
|
||||||
tx=(FX - x0).astype(DTYPE), ty=(FY - y0).astype(DTYPE),
|
|
||||||
slx=slice(r, r*Wx, r), sly=slice(r, r*Wy, r))
|
|
||||||
|
|
||||||
def pint(fld, P):
|
|
||||||
"""Билинейная интерполяция поля fld (грубого) в узлы тонкого патча."""
|
|
||||||
return (fld[..., P["y0"], P["x0"]]*(1 - P["tx"])*(1 - P["ty"])
|
|
||||||
+ fld[..., P["y0"], P["x1"]]*P["tx"]*(1 - P["ty"])
|
|
||||||
+ fld[..., P["y1"], P["x0"]]*(1 - P["tx"])*P["ty"]
|
|
||||||
+ fld[..., P["y1"], P["x1"]]*P["tx"]*P["ty"])
|
|
||||||
|
|
||||||
def ghost(pf, P, Rcf):
|
|
||||||
"""Граничные значения тонкого уровня из грубого поля pf: равновесие по интерполированным ρ,u
|
|
||||||
плюс масштабированная неравновесная часть Rcf·neq.
|
|
||||||
u в feq передаём кортежем (без cupy.stack) — H2D ломает CUDA-graph capture."""
|
|
||||||
r, u = macros(pf); neq = pf - feq(r, u)
|
|
||||||
return feq(pint(r, P), (pint(u[0], P), pint(u[1], P))) + Rcf*pint(neq, P)
|
|
||||||
|
|
||||||
def fill(cf, gh):
|
|
||||||
"""Записать ghost-значения gh в рамку (1 ряд) тонкого поля cf."""
|
|
||||||
cf[:, 0, :] = gh[:, 0, :]; cf[:, -1, :] = gh[:, -1, :]
|
|
||||||
cf[:, :, 0] = gh[:, :, 0]; cf[:, :, -1] = gh[:, :, -1]
|
|
||||||
return cf
|
|
||||||
|
|
||||||
def restrict(cf, pf, P, Rfc, fluid):
|
|
||||||
"""Спроецировать тонкое поле cf обратно на грубое pf (каждый r-й узел), масштабируя neq на Rfc.
|
|
||||||
Меняется только внутренняя жидкая часть перекрытия (маска fluid)."""
|
|
||||||
r, u = macros(cf); neq = cf - feq(r, u); slx, sly = P["slx"], P["sly"]
|
|
||||||
nv = feq(r[sly, slx], u[:, sly, slx]) + Rfc*neq[:, sly, slx]
|
|
||||||
cur = pf[:, P["ay"]+1:P["by"], P["ax"]+1:P["bx"]]
|
|
||||||
pf[:, P["ay"]+1:P["by"], P["ax"]+1:P["bx"]] = xp.where(fluid[None], nv, cur)
|
|
||||||
return pf
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
# Бэкенд: ТОЛЬКО 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
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
# Граничные условия: (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
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# Оператор СТОЛКНОВЕНИЯ — KBC-N1 (энтропийный), единственный оператор модели.
|
|
||||||
#
|
|
||||||
# KBC-N1 (Karlin/Bösch/Chikatamarla 2014):
|
|
||||||
# Δ = f − feq (неравновесная часть)
|
|
||||||
# Δs = Ps·Δ (проекция на сдвиг-моменты {cx²−cy², cxcy})
|
|
||||||
# Δh = Δ − Δs (остальная часть)
|
|
||||||
# γ = 1/β − (2 − 1/β)·⟨Δs,Δh⟩ / ⟨Δh,Δh⟩, ⟨a,b⟩ = Σ_i a_i b_i / feq_i (H-теорема)
|
|
||||||
# f_post = f − β(2Δs + γΔh), β = 1/(2τ)
|
|
||||||
# γ — энтропийный стабилизатор: подстраивает диссипацию неhydro-мод под энтропийное ограничение
|
|
||||||
# (поэтому KBC устойчив при τ→1/2). Сдвиг-моменты (вязкость) релаксируют с правильным 2β = 1/τ,
|
|
||||||
# то есть физическая вязкость задаётся только τ — стабилизатор её не меняет.
|
|
||||||
#
|
|
||||||
# Порядок шагов дословно совпадает с алгоритмом статьи (Bösch/Chikatamarla/Karlin, PRE 92,
|
|
||||||
# 043309 (2015), разд. III, листинг после ур.39): ρ,u → feq → Δs → Δh = f − feq − Δs → γ → релаксация.
|
|
||||||
# Вырожденный случай ⟨Δh|Δh⟩→0 отсекается ОТНОСИТЕЛЬНЫМ порогом B.GREL (см. backend.py):
|
|
||||||
# абсолютный порог здесь недопустим — den квадратична по неравновесию и физически мала.
|
|
||||||
import backend as B
|
|
||||||
import lattice as L
|
|
||||||
|
|
||||||
cp = B.cp; xp = B.xp
|
|
||||||
Q = L.Q; Ps = L.Ps; GREL = B.GREL; ONE = B.ONE; TWO = B.TWO
|
|
||||||
|
|
||||||
def _project_shift(dfn):
|
|
||||||
"""Δs = Ps·Δ без cuBLAS (gemm при CUDA-graph capture тащит alpha/beta host→device).
|
|
||||||
Реализовано как Σ_k Ps[i,k]·Δ[k] через broadcast+редукцию — только обычные ядра."""
|
|
||||||
dfn2d = dfn.reshape(Q, -1) # (Q, N)
|
|
||||||
return (Ps[:, :, None] * dfn2d[None]).sum(1).reshape(dfn.shape)
|
|
||||||
|
|
||||||
def kbc_collide(f, fe, beta):
|
|
||||||
dfn = f - fe
|
|
||||||
ds = _project_shift(dfn)
|
|
||||||
dh = dfn - ds; inv = 1.0 / fe
|
|
||||||
num = (ds*dh*inv).sum(0); den = (dh*dh*inv).sum(0)
|
|
||||||
nrm = (dfn*dfn*inv).sum(0) # ‖Δ‖² — масштаб неравновесия узла
|
|
||||||
ok = den > GREL*nrm # относительный порог, не абсолютный
|
|
||||||
den_safe = xp.where(ok, den, ONE) # избегаем 0/0 (обе ветки where считаются)
|
|
||||||
binv = 1.0 / beta
|
|
||||||
g = xp.where(ok, binv - (2.0 - binv)*num/den_safe, TWO)
|
|
||||||
return f - beta*(2.0*ds + g[None]*dh)
|
|
||||||
|
|
||||||
# единственный оператор столкновения модели
|
|
||||||
collide = kbc_collide
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
# Конфигурация модели 2×+SDF. Всё в одном месте, чтобы математику/режимы было легко менять.
|
|
||||||
# Единицы — решёточные (lu — длина воксель, ts — шаг по времени).
|
|
||||||
import os
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Config:
|
|
||||||
# --- сетка / геометрия L0 (умеренно расширены: меньше блокировка + больше следа) ---
|
|
||||||
Nx: int = 174 # домен по x (цилиндр на 40 → ~8.4D вниз по потоку до выхода)
|
|
||||||
Ny: int = 90 # домен по y (стенки free-slip; блокировка β=D/Ny — варьируется серией)
|
|
||||||
D: int = 16 # диаметр цилиндра на грубом уровне L0
|
|
||||||
cx: int = 40
|
|
||||||
cy: Optional[int] = None # центр по вертикали; по умолчанию Ny//2 (см. __post_init__)
|
|
||||||
|
|
||||||
# --- физика ---
|
|
||||||
U: float = 0.07 # скорость набегающего потока
|
|
||||||
Re: float = 150.0 # число Рейнольдса по D и U
|
|
||||||
|
|
||||||
# --- время ---
|
|
||||||
steps: int = 100000
|
|
||||||
ramp: int = 1000 # smoothstep-разгон входной скорости (гасит импульсный старт)
|
|
||||||
frame_every: int = 34 # период сохранения кадров для гифки
|
|
||||||
|
|
||||||
# --- патч уровня L1 (в координатах L0); refine=2 → разрешение Δx/2 ---
|
|
||||||
ax1: Optional[int] = None # по умолчанию cx − 1.5D (запас перед телом; см. __post_init__)
|
|
||||||
bx1: Optional[int] = None # по умолчанию cx + 6.25D (расширен в след)
|
|
||||||
ay1: Optional[int] = None # по умолчанию cy − 2D (центрирован по cy)
|
|
||||||
by1: Optional[int] = None # по умолчанию cy + 2D
|
|
||||||
refine: int = 2 # коэффициент измельчения L1 (модель «2×» рассчитана на 2)
|
|
||||||
|
|
||||||
# --- зонд скорости в следе: px = cx + probe_dx*D, py = cy (берётся с тонкой сетки L1) ---
|
|
||||||
probe_dx: int = 3
|
|
||||||
|
|
||||||
# --- стартовое возмущение (триггер вихревой дорожки): поперечный sin-импульс входа ---
|
|
||||||
pert_amp: float = 0.1 # амплитуда u_y входа, доля от U (0 — выключить)
|
|
||||||
pert_dur: int = 300 # длительность импульса в шагах; окно [ramp, ramp+pert_dur]
|
|
||||||
|
|
||||||
# --- кросс-чек силы: интеграл тензора напряжений σ·n по окружности R1+δ на L1 (forces_stress.py) ---
|
|
||||||
stress_every: int = 50 # период выборки в шагах L0 (0 — выключить кросс-чек)
|
|
||||||
stress_ntheta: int = 256 # число точек контура по углу
|
|
||||||
stress_delta: float = 2.0 # отступ контура от поверхности (в тонких ячейках L1);
|
|
||||||
# δ=2 → билинейный стенсиль не трогает Bouzidi-зону (q_max≈0.98)
|
|
||||||
|
|
||||||
# --- ГУ выхода: режим поперечной скорости на Zou-He давление-выходе ---
|
|
||||||
# "zero" — классика: uy=0 на выходе (жёстко; отражает вихри дорожки)
|
|
||||||
# "extrapolate" — uy берётся из соседнего столбца (нуль-градиент); ρ-якорь сохраняется
|
|
||||||
outlet_uy: str = "zero"
|
|
||||||
|
|
||||||
# --- губка (absorbing layer) перед выходом: плавный рост вязкости в последних sponge_len
|
|
||||||
# столбцах L0 (smoothstep, ν → ν·sponge_nu_mult). Гасит вихри И акустику до прихода на
|
|
||||||
# давление-выход. Зачем: канал «вход-скорость + выход-давление» — недодемпфированный
|
|
||||||
# акустический резонатор (затухание моды ∝ ν(π/Nx)²); на длинных доменах мода раскачивается
|
|
||||||
# стартовым транзиентом → смещённый режим ⟨ρ⟩≈1.66 или взрыв (факторное исследование,
|
|
||||||
# эксперимент №1: F1/F2/F3/F5). 0 — выключена.
|
|
||||||
sponge_len: int = 0
|
|
||||||
sponge_nu_mult: float = 30.0
|
|
||||||
|
|
||||||
# --- исполнение ---
|
|
||||||
use_cuda_graph: bool = True
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
# производная геометрия по вертикали: центр и патч следуют за Ny (для серии по блокировке)
|
|
||||||
if self.cy is None:
|
|
||||||
self.cy = self.Ny // 2
|
|
||||||
if self.ay1 is None:
|
|
||||||
self.ay1 = self.cy - 2 * self.D
|
|
||||||
if self.by1 is None:
|
|
||||||
self.by1 = self.cy + 2 * self.D
|
|
||||||
# производная геометрия по горизонтали: патч следует за cx (для факторов вход/выход)
|
|
||||||
if self.ax1 is None:
|
|
||||||
self.ax1 = self.cx - (3 * self.D) // 2
|
|
||||||
if self.bx1 is None:
|
|
||||||
self.bx1 = self.cx + (25 * self.D) // 4
|
|
||||||
assert 1 <= self.ay1 < self.by1 <= self.Ny - 2, (
|
|
||||||
f"патч L1 [{self.ay1},{self.by1}] должен лежать строго внутри (0,{self.Ny-1}) — "
|
|
||||||
f"specular-ряды стенок не накрывать; минимально допустимое Ny ≈ {4*self.D + 6}")
|
|
||||||
assert 2 <= self.ax1 < self.cx < self.bx1 <= self.Nx - 2, (
|
|
||||||
f"патч L1 [{self.ax1},{self.bx1}] должен лежать строго внутри (1,{self.Nx-1}) "
|
|
||||||
f"и накрывать тело (cx={self.cx})")
|
|
||||||
assert self.outlet_uy in ("zero", "extrapolate"), self.outlet_uy
|
|
||||||
if self.sponge_len:
|
|
||||||
assert self.Nx - 1 - self.sponge_len > self.bx1, (
|
|
||||||
f"губка (последние {self.sponge_len} столбцов) не должна накрывать патч L1 "
|
|
||||||
f"(bx1={self.bx1}, Nx={self.Nx})")
|
|
||||||
|
|
||||||
# ---------- производные величины ----------
|
|
||||||
@property
|
|
||||||
def nu(self): return self.U * self.D / self.Re # кинематическая вязкость (lu²/ts)
|
|
||||||
@property
|
|
||||||
def tau0(self): return self.nu / (1.0/3.0) + 0.5 # время релаксации на L0 (cs²=1/3)
|
|
||||||
@property
|
|
||||||
def tau1(self): # связка τ при измельчении в r раз: ν одна и та же ⇒ τ_f = r(τ_c − ½) + ½
|
|
||||||
return self.refine * (self.tau0 - 0.5) + 0.5
|
|
||||||
@property
|
|
||||||
def beta0(self): return 1.0 / (2.0 * self.tau0) # β = 1/(2τ) для KBC/BGK на L0
|
|
||||||
@property
|
|
||||||
def beta1(self): return 1.0 / (2.0 * self.tau1) # β на L1
|
|
||||||
@property
|
|
||||||
def R01(self): # масштаб неравновесной части грубый→тонкий: f^neq ∝ τ·δt ⇒ (τ_f/τ_c)/r
|
|
||||||
return self.tau1 / (self.refine * self.tau0)
|
|
||||||
@property
|
|
||||||
def Rf01(self): return 1.0 / self.R01 # тонкий→грубый
|
|
||||||
@property
|
|
||||||
def DL(self): return self.refine * self.D # эффективный диаметр у тела (на L1)
|
|
||||||
@property
|
|
||||||
def px(self): return self.cx + self.probe_dx * self.D
|
|
||||||
@property
|
|
||||||
def py(self): return self.cy
|
|
||||||
|
|
||||||
def from_env(**overrides):
|
|
||||||
"""Создать Config с учётом переменных окружения и явных overrides.
|
|
||||||
Геометрия (AMR_NY/AMR_NX/AMR_CX) попадает в overrides ДО конструктора —
|
|
||||||
производные cy/ay1/by1/ax1/bx1 строятся в __post_init__."""
|
|
||||||
if "Ny" not in overrides and os.environ.get("AMR_NY"):
|
|
||||||
overrides["Ny"] = int(os.environ["AMR_NY"])
|
|
||||||
if "Nx" not in overrides and os.environ.get("AMR_NX"):
|
|
||||||
overrides["Nx"] = int(os.environ["AMR_NX"])
|
|
||||||
if "cx" not in overrides and os.environ.get("AMR_CX"):
|
|
||||||
overrides["cx"] = int(os.environ["AMR_CX"])
|
|
||||||
if "outlet_uy" not in overrides and os.environ.get("AMR_OUTLET_UY"):
|
|
||||||
overrides["outlet_uy"] = os.environ["AMR_OUTLET_UY"]
|
|
||||||
if "sponge_len" not in overrides and os.environ.get("AMR_SPONGE_LEN"):
|
|
||||||
overrides["sponge_len"] = int(os.environ["AMR_SPONGE_LEN"])
|
|
||||||
if "sponge_nu_mult" not in overrides and os.environ.get("AMR_SPONGE_NU"):
|
|
||||||
overrides["sponge_nu_mult"] = float(os.environ["AMR_SPONGE_NU"])
|
|
||||||
cfg = Config(**overrides)
|
|
||||||
if os.environ.get("AMR_STEPS"):
|
|
||||||
cfg.steps = int(os.environ["AMR_STEPS"])
|
|
||||||
if "AMR_CUDAGRAPH" in os.environ:
|
|
||||||
cfg.use_cuda_graph = (os.environ["AMR_CUDAGRAPH"] != "0")
|
|
||||||
if os.environ.get("AMR_STRESS_EVERY"):
|
|
||||||
cfg.stress_every = int(os.environ["AMR_STRESS_EVERY"])
|
|
||||||
if os.environ.get("AMR_STRESS_NTHETA"):
|
|
||||||
cfg.stress_ntheta = int(os.environ["AMR_STRESS_NTHETA"])
|
|
||||||
if os.environ.get("AMR_STRESS_DELTA"):
|
|
||||||
cfg.stress_delta = float(os.environ["AMR_STRESS_DELTA"])
|
|
||||||
return cfg
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
# Диагностика по собранным рядам сил/скорости. numpy здесь — только host-постобработка малых рядов
|
|
||||||
# (FFT/статистика), это НЕ вычислительный бэкенд солвера.
|
|
||||||
import numpy as np
|
|
||||||
import backend as B
|
|
||||||
|
|
||||||
def strouhal(uy_host, D, U, pad=8192):
|
|
||||||
"""Число Струхаля из ряда поперечной скорости в следе.
|
|
||||||
УЛУЧШЕНО: параболическая (3-точечная) интерполяция пика спектра → суб-биновая точность.
|
|
||||||
(Раньше брался ближайший FFT-бин: шаг по St ≈ D/(U·pad) ≈ 0.028, из-за чего St 'застревал'.)"""
|
|
||||||
sig = uy_host[len(uy_host)//2:] # установившийся режим — вторая половина
|
|
||||||
sig = sig - sig.mean()
|
|
||||||
amp = np.abs(np.fft.rfft(sig, n=pad))
|
|
||||||
if len(amp) <= 3:
|
|
||||||
return 0.0, np.fft.rfftfreq(pad, 1.0), amp
|
|
||||||
k = 1 + int(np.argmax(amp[1:])) # индекс пика (без DC)
|
|
||||||
if 1 <= k < len(amp) - 1: # параболическая интерполяция вершины
|
|
||||||
a0, a1, a2 = amp[k-1], amp[k], amp[k+1]
|
|
||||||
denom = (a0 - 2*a1 + a2)
|
|
||||||
delta = 0.5*(a0 - a2)/denom if denom != 0 else 0.0
|
|
||||||
else:
|
|
||||||
delta = 0.0
|
|
||||||
f_peak = (k + delta) / pad
|
|
||||||
return f_peak * D / U, np.fft.rfftfreq(pad, 1.0), amp
|
|
||||||
|
|
||||||
def analyze(res, cfg):
|
|
||||||
Fx = B.to_cpu(res["Fx"]); Fy = B.to_cpu(res["Fy"]); uy = B.to_cpu(res["uy"])
|
|
||||||
DL, U, D = res["DL"], cfg.U, cfg.D
|
|
||||||
n = len(uy); h = slice(n//2, n)
|
|
||||||
Cd = 2.0*Fx/(U**2*DL); Cl = 2.0*Fy/(U**2*DL)
|
|
||||||
St, freqs, amp = strouhal(uy, D, U)
|
|
||||||
a = dict(Cd=float(Cd[h].mean()), Clrms=float(Cl[h].std()), St=float(St),
|
|
||||||
uyrms=float(uy[h].std()), Cd_s=Cd, Cl_s=Cl, uy=uy, freqs=freqs, amp=amp, h0=n//2)
|
|
||||||
if "Tz" in res: # момент: Cm = 2Tz/(U²·DL²); для цилиндра ⟨Cm⟩≈0
|
|
||||||
Cm = 2.0*B.to_cpu(res["Tz"])/(U**2*DL*DL)
|
|
||||||
a["Cm"] = float(Cm[h].mean()); a["Cmrms"] = float(Cm[h].std()); a["Cm_s"] = Cm
|
|
||||||
K = int(res.get("stress_every", 0) or 0) # кросс-чек σ·n (редкая сетка по времени)
|
|
||||||
if K and len(res.get("Fx_st", ())) > 3:
|
|
||||||
Fxs = B.to_cpu(res["Fx_st"]); Fys = B.to_cpu(res["Fy_st"]); Tzs = B.to_cpu(res["Tz_st"])
|
|
||||||
m = len(Fxs); hs = slice(m//2, m)
|
|
||||||
Cds = 2.0*Fxs/(U**2*DL); Cls = 2.0*Fys/(U**2*DL); Cms = 2.0*Tzs/(U**2*DL*DL)
|
|
||||||
a.update(Cd_st=float(Cds[hs].mean()), Clrms_st=float(Cls[hs].std()),
|
|
||||||
Cm_st=float(Cms[hs].mean()), Cd_st_s=Cds, Cl_st_s=Cls, stress_every=K)
|
|
||||||
return a
|
|
||||||
|
|
||||||
def print_table(a, cfg):
|
|
||||||
# поправка на блокировку канала (приведение к скорости в зазоре U/(1−β)):
|
|
||||||
# St — кинематическая → ×(1−β); Cd, rms Cl ~ скорость² → ×(1−β)²
|
|
||||||
beta = cfg.D / cfg.Ny
|
|
||||||
St_c = a['St'] * (1 - beta)
|
|
||||||
Cd_c = a['Cd'] * (1 - beta)**2
|
|
||||||
Cl_c = a['Clrms'] * (1 - beta)**2
|
|
||||||
cm = a.get('Cm')
|
|
||||||
cms = f"{cm:>10.5f}" if cm is not None else f"{'—':>10}"
|
|
||||||
print("\n=== Модель 2×+SDF — установившийся режим ===")
|
|
||||||
print(f"{'версия':18}{'D у тела':>9}{'St':>8}{'<Cd>':>9}{'rms Cl':>9}{'<Cm>':>10}{'rms u_y':>9}")
|
|
||||||
print(f"{'2×+SDF (raw)':18}{cfg.DL:>9}{a['St']:>8.3f}{a['Cd']:>9.3f}{a['Clrms']:>9.4f}{cms}{a['uyrms']:>9.4f}")
|
|
||||||
print(f"{'2×+SDF (×блок.)':18}{cfg.DL:>9}{St_c:>8.3f}{Cd_c:>9.3f}{Cl_c:>9.4f}{'—':>10}{'—':>9}")
|
|
||||||
print(f"{'лит. Re≈150':18}{'—':>9}{0.183:>8.3f}{1.330:>9.3f}{'~0.3':>9}{0.0:>10.1f}{'':>9}")
|
|
||||||
print(f"(блокировка β=D/Ny={beta:.3f}; поправка: St×(1−β), Cd/rmsCl×(1−β)². "
|
|
||||||
"Лит. — безграничный цилиндр; ⟨Cm⟩≈0 — контроль симметрии; см. README)")
|
|
||||||
|
|
||||||
def print_crosscheck(a):
|
|
||||||
"""Сравнение двух НЕЗАВИСИМЫХ считываний силы: GMEM (обмен импульсом по линкам) vs ∮σ·n ds
|
|
||||||
(интеграл тензора напряжений по контуру R+δ). Сходство — прямое свидетельство корректности
|
|
||||||
считывания; сравниваем средние (мгновенные ряды σ·n сдвинуты по фазе кольцом R..R+δ)."""
|
|
||||||
if "Cd_st" not in a:
|
|
||||||
print("\n[кросс-чек σ·n: выключен (stress_every=0) или слишком мало точек]")
|
|
||||||
return
|
|
||||||
dcd = abs(a["Cd_st"] - a["Cd"]) / max(abs(a["Cd"]), 1e-12)
|
|
||||||
dcl = abs(a["Clrms_st"] - a["Clrms"]) / max(abs(a["Clrms"]), 1e-12)
|
|
||||||
cm = a.get("Cm", float("nan"))
|
|
||||||
print("\n=== Кросс-чек считывания силы: GMEM vs ∮σ·n ds (установившийся режим) ===")
|
|
||||||
print(f"{'метод':26}{'<Cd>':>9}{'rms Cl':>9}{'<Cm>':>10}")
|
|
||||||
print(f"{'GMEM (обмен импульсом)':26}{a['Cd']:>9.3f}{a['Clrms']:>9.4f}{cm:>10.5f}")
|
|
||||||
print(f"{'∮σ·n ds (контур R+δ)':26}{a['Cd_st']:>9.3f}{a['Clrms_st']:>9.4f}{a['Cm_st']:>10.5f}")
|
|
||||||
verdict = "СОГЛАСОВАНО" if (dcd < 0.05 and dcl < 0.10) else "ПРОВЕРИТЬ"
|
|
||||||
print(f"расхождение: dCd={dcd*100:.1f}% (порог 5%), d(rmsCl)={dcl*100:.1f}% (порог 10%) → {verdict}")
|
|
||||||
|
|
||||||
def fit_blockage(betas, vals):
|
|
||||||
"""Экстраполяция величины серии к β→0 двумя моделями: val ≈ a + b·β и val ≈ a + b·β².
|
|
||||||
(Теория solid-blockage даёт ведущий член O(β²), практика поправки (1−β)² содержит и линейный.)
|
|
||||||
Спред интерсептов |a_lin − a_quad| — оценка модельной неопределённости экстраполяции."""
|
|
||||||
b = np.asarray(betas, float); v = np.asarray(vals, float)
|
|
||||||
sl, al = np.polyfit(b, v, 1)
|
|
||||||
sq, aq = np.polyfit(b**2, v, 1)
|
|
||||||
return dict(lin=(float(al), float(sl)), quad=(float(aq), float(sq)),
|
|
||||||
spread=float(abs(al - aq)))
|
|
||||||
|
|
||||||
def print_blockage_table(rows, fits):
|
|
||||||
"""Сводка серии по блокировке. rows — список dict(Ny, beta, St, Cd, Clrms, Cm, Cd_st);
|
|
||||||
fits — dict('Cd'/'Clrms'/'St' → результат fit_blockage)."""
|
|
||||||
print("\n=== Серия по блокировке: D фикс., Ny варьируется ===")
|
|
||||||
print(f"{'Ny':>5}{'β=D/Ny':>9}{'St':>8}{'St(1−β)':>9}{'<Cd>':>9}{'rms Cl':>9}{'<Cm>':>10}{'Cd σ·n':>9}")
|
|
||||||
for r in rows:
|
|
||||||
cdst = f"{r['Cd_st']:>9.3f}" if r.get("Cd_st") is not None else f"{'—':>9}"
|
|
||||||
print(f"{r['Ny']:>5}{r['beta']:>9.3f}{r['St']:>8.3f}{r['St']*(1-r['beta']):>9.3f}"
|
|
||||||
f"{r['Cd']:>9.3f}{r['Clrms']:>9.4f}{r['Cm']:>10.5f}{cdst}")
|
|
||||||
print("экстраполяция β→0 (интерсепт лин. фита / квадр. фита; спред — неопределённость):")
|
|
||||||
print(f" Cd(0) = {fits['Cd']['lin'][0]:.3f} / {fits['Cd']['quad'][0]:.3f} "
|
|
||||||
f"(спред {fits['Cd']['spread']:.3f}; лит. безгранич. 1.33)")
|
|
||||||
print(f" rms Cl(0) = {fits['Clrms']['lin'][0]:.3f} / {fits['Clrms']['quad'][0]:.3f} "
|
|
||||||
f"(спред {fits['Clrms']['spread']:.3f}; лит. ~0.3)")
|
|
||||||
print(f" St(0) = {fits['St']['lin'][0]:.3f} / {fits['St']['quad'][0]:.3f} "
|
|
||||||
f"(спред {fits['St']['spread']:.3f}; лит. 0.183)")
|
|
||||||
|
|
||||||
def convergence_report(res, cfg, nwin=10):
|
|
||||||
"""Эволюция по окнам времени: видно, НАСЫЩЕНО ли решение или ДРЕЙФУЕТ.
|
|
||||||
Ключевое: ⟨ρ⟩ (средняя плотность) и Cd, нормированный на реальную ⟨ρ⟩ (дрейф-устойчивый Cd).
|
|
||||||
Если ⟨ρ⟩ уплывает от 1 — это дрейф массы из-за ГУ входа/выхода, а Cd_raw искажён нормировкой на ρ=1."""
|
|
||||||
Fx = B.to_cpu(res["Fx"]); Fy = B.to_cpu(res["Fy"]); uy = B.to_cpu(res["uy"]); rho = B.to_cpu(res["rho"])
|
|
||||||
n = len(Fx); U = cfg.U; DL = res["DL"]
|
|
||||||
if n < nwin * 2:
|
|
||||||
return
|
|
||||||
Cd = 2.0*Fx/(U**2*DL); Cl = 2.0*Fy/(U**2*DL)
|
|
||||||
w = n // nwin
|
|
||||||
print("\n=== Сходимость по времени / дрейф (по окнам) ===")
|
|
||||||
print(f"{'окно':>4}{'шаги':>16}{'<ρ>':>8}{'<Cd>raw':>9}{'<Cd>/ρ':>9}{'rms Cl':>9}{'rms u_y':>9}")
|
|
||||||
rows = []
|
|
||||||
for k in range(nwin):
|
|
||||||
s = slice(k*w, (k+1)*w if k < nwin-1 else n)
|
|
||||||
rm = float(rho[s].mean()); cdr = float(Cd[s].mean())
|
|
||||||
cdn = cdr / rm if rm != 0 else float("nan")
|
|
||||||
clr = float(Cl[s].std()); uyr = float(uy[s].std())
|
|
||||||
rows.append((rm, cdr, cdn, clr, uyr))
|
|
||||||
print(f"{k+1:>4}{f'{k*w}-{(k+1)*w}':>16}{rm:>8.3f}{cdr:>9.3f}{cdn:>9.3f}{clr:>9.4f}{uyr:>9.4f}")
|
|
||||||
# вердикт по последним двум окнам
|
|
||||||
(rm1, cdr1, cdn1, clr1, _), (rm0, _, _, clr0, _) = rows[-1], rows[-2]
|
|
||||||
drho = rm1 - rm0; dcl = clr1 - clr0
|
|
||||||
verdict = []
|
|
||||||
if abs(rm1 - 1.0) > 0.02:
|
|
||||||
# «стабильна, но не там»: система села на смещённую ветвь (акустическая накачка массы;
|
|
||||||
# ⟨Cd⟩/Cl на ней НЕвалидны для сравнения с литературой) — см. факторное исследование
|
|
||||||
verdict.append(f"плотность: ⟨ρ⟩={rm1:.3f} → РЕЖИМ СМЕЩЁН (⟨ρ⟩ далеко от 1; прогон невалиден)")
|
|
||||||
else:
|
|
||||||
verdict.append(f"плотность: ⟨ρ⟩={rm1:.3f}, Δ за окно={drho:+.4f} → "
|
|
||||||
+ ("ДРЕЙФ массы (правь ГУ выхода: давление/Zou-He)" if abs(drho) > 1e-3 else "стабильна"))
|
|
||||||
verdict.append(f"rms Cl: Δ за окно={dcl:+.4f} → "
|
|
||||||
+ ("ещё растёт (не насыщено)" if dcl > 1e-3 else "насыщено/стабильно"))
|
|
||||||
verdict.append(f"дрейф-устойчивый Cd (⟨Cd⟩/ρ) в последнем окне = {cdn1:.3f}")
|
|
||||||
print("вердикт: " + "; ".join(verdict))
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# Равновесие и макропеременные.
|
|
||||||
#
|
|
||||||
# Используется ЭНТРОПИЙНОЕ равновесие в product-form (как в KBC), а не усечённый полином:
|
|
||||||
# feq_i = w_i · ρ · Π_d (2 − sqrt(1+3 u_d²)) · ((2 u_d + sqrt(1+3 u_d²))/(1 − u_d))^{c_{i,d}}
|
|
||||||
# Показатели c_{i,d} ∈ {−1,0,+1}, поэтому степень НЕ нужна — берём q или 1/q (быстрее и совместимо
|
|
||||||
# с CUDA-graph: нет pow и нет host→device для границ clip).
|
|
||||||
import backend as B
|
|
||||||
import lattice as L
|
|
||||||
|
|
||||||
cp = B.cp; xp = B.xp
|
|
||||||
CXa = L.CXa; CYa = L.CYa
|
|
||||||
|
|
||||||
def _feq9_body(rho, ux, uy):
|
|
||||||
ux = cp.minimum(cp.maximum(ux, -0.95), 0.95) # clip скоростей литералами (запекается в кернел)
|
|
||||||
uy = cp.minimum(cp.maximum(uy, -0.95), 0.95)
|
|
||||||
sx = cp.sqrt(1.0 + 3.0*ux*ux); sy = cp.sqrt(1.0 + 3.0*uy*uy)
|
|
||||||
base = rho * (2.0 - sx) * (2.0 - sy)
|
|
||||||
qx = (2.0*ux + sx) / (1.0 - ux); qy = (2.0*uy + sy) / (1.0 - uy)
|
|
||||||
ix = 1.0/qx; iy = 1.0/qy
|
|
||||||
return (base*(4.0/9.0), # ( 0, 0)
|
|
||||||
base*(1.0/9.0)*qx, base*(1.0/9.0)*qy, # (+1,0) (0,+1)
|
|
||||||
base*(1.0/9.0)*ix, base*(1.0/9.0)*iy, # (-1,0) (0,-1)
|
|
||||||
base*(1.0/36.0)*qx*qy, base*(1.0/36.0)*ix*qy, # (+1,+1) (-1,+1)
|
|
||||||
base*(1.0/36.0)*ix*iy, base*(1.0/36.0)*qx*iy) # (-1,-1) (+1,-1)
|
|
||||||
|
|
||||||
_feq9 = cp.fuse()(_feq9_body) # одно слитное ядро (если cp.fuse доступен)
|
|
||||||
_FUSE_OK = [True]
|
|
||||||
|
|
||||||
def feq(rho, u):
|
|
||||||
"""Равновесные популяции (Q,Ny,Nx) по плотности rho и скорости u (индексируется [0],[1]).
|
|
||||||
Сборка через B.pack (без cupy.stack) — иначе H2D ломает CUDA-graph capture."""
|
|
||||||
if _FUSE_OK[0]:
|
|
||||||
try:
|
|
||||||
return B.pack(_feq9(rho, u[0], u[1]))
|
|
||||||
except Exception as e: # откат на не-fused (тоже без pow), один раз
|
|
||||||
print(f"[equilibrium] cp.fuse отключён ({type(e).__name__}: {e}); обычный путь")
|
|
||||||
_FUSE_OK[0] = False
|
|
||||||
return B.pack(_feq9_body(rho, u[0], u[1]))
|
|
||||||
|
|
||||||
def macros(f):
|
|
||||||
"""Макропеременные: ρ = Σ_i f_i, u = (Σ_i c_i f_i)/ρ. Возвращает (rho, u=(2,Ny,Nx))."""
|
|
||||||
r = f.sum(0)
|
|
||||||
ux = (CXa[:, None, None] * f).sum(0) / r
|
|
||||||
uy = (CYa[:, None, None] * f).sum(0) / r
|
|
||||||
return r, B.pack((ux, uy))
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
# СИЛА и МОМЕНТ на теле — галилей-инвариантный обмен импульсом (GMEM, Wen et al. 2014).
|
|
||||||
#
|
|
||||||
# Для каждого линка жидкость→тело по направлению i (маска cl):
|
|
||||||
# ΔF = (c_i − u_w)·f_i^{после столкновения}(x_f) − (c_ī − u_w)·f_ī^{после стриминга}(x_f)
|
|
||||||
# где u_w — скорость стенки на линке (для неподвижного цилиндра = 0). Т.к. c_ī = −c_i, по компонентам:
|
|
||||||
# F_x += (Cx_i − u_wx)·f_i + (Cx_i + u_wx)·f_ī
|
|
||||||
# F_y += (Cy_i − u_wy)·f_i + (Cy_i + u_wy)·f_ī
|
|
||||||
#
|
|
||||||
# МОМЕНТ (торк) вокруг центра тела: T_z = Σ_links (r_w × ΔF)_z, где точка приложения —
|
|
||||||
# ПЕРЕСЕЧЕНИЕ линка со стенкой r_w = r_f + q·c_i (q — Bouzidi-доля, уже лежит в bc).
|
|
||||||
# Узел жидкости дал бы ошибку плеча до 1 ячейки; q бесплатен. Для кругового цилиндра
|
|
||||||
# ⟨Cm⟩ ≈ 0 — это контроль симметрии считывания; для подвижных/вращающихся тел (цель SimV4) —
|
|
||||||
# рабочая величина.
|
|
||||||
#
|
|
||||||
# Свойства:
|
|
||||||
# * f_i — популяция, уходящая в стенку (после столкновения); f_ī — вернувшаяся (после стриминга/Bouzidi).
|
|
||||||
# f_ī берётся из поля ПОСЛЕ стриминга — иначе ведущий симметричный член ~2ρw_i сокращается и Cd врёт.
|
|
||||||
# * При u_w=0 сводится к классическому Σ c_i (f_i + f_ī); члены с u_w делают оценку галилей-инвариантной
|
|
||||||
# и ГОТОВОЙ к подвижным/вращающимся телам (цель проекта SimV4) — туда передаётся u_w на линке.
|
|
||||||
#
|
|
||||||
# Независимый кросс-чек этой оценки — интегрирование тензора напряжений по контуру вокруг тела:
|
|
||||||
# см. forces_stress.py (σ = −p′·I + σ^v из неравновесных моментов; вызывается редко, вне CUDA-графа).
|
|
||||||
import backend as B
|
|
||||||
import lattice as L
|
|
||||||
|
|
||||||
xp = B.xp; ZERO = B.ZERO
|
|
||||||
Cx = L.Cx; Cy = L.Cy
|
|
||||||
|
|
||||||
def force_gmem(fpost, fnew, bc, rxy=None, u_wall=(0.0, 0.0)):
|
|
||||||
"""GMEM по линкам тела (маска cl). Возвращает (Fx, Fy, Tz) — device-скаляры.
|
|
||||||
rxy=(rx,ry) — координаты узлов решётки относительно центра тела (для торка); None → Tz=0.
|
|
||||||
u_wall — скорость стенки. Graph-safe: только where/арифметика с литералами."""
|
|
||||||
uwx, uwy = u_wall
|
|
||||||
Fx = ZERO; Fy = ZERO; Tz = ZERO
|
|
||||||
if rxy is not None:
|
|
||||||
rx, ry = rxy
|
|
||||||
for (i, ib, mask, q, xff, cl) in bc:
|
|
||||||
fi = fpost[i]; fib = fnew[ib]
|
|
||||||
dFx = xp.where(cl, (Cx[i] - uwx)*fi + (Cx[i] + uwx)*fib, ZERO)
|
|
||||||
dFy = xp.where(cl, (Cy[i] - uwy)*fi + (Cy[i] + uwy)*fib, ZERO)
|
|
||||||
Fx = Fx + dFx.sum(); Fy = Fy + dFy.sum()
|
|
||||||
if rxy is not None: # плечо до точки пересечения линка со стенкой
|
|
||||||
Tz = Tz + ((rx + q*Cx[i])*dFy - (ry + q*Cy[i])*dFx).sum()
|
|
||||||
return Fx, Fy, Tz
|
|
||||||
|
|
||||||
def coefficients(Fx, Fy, U, DL, Tz=None):
|
|
||||||
"""Безразмерные коэффициенты: Cd = 2Fx/(U²·DL), Cl = 2Fy/(U²·DL), Cm = 2Tz/(U²·DL²).
|
|
||||||
DL — диаметр у тела (на L1); сила/плечо тоже в единицах L1, так что нормировка согласована."""
|
|
||||||
Cd = 2.0*Fx/(U**2*DL); Cl = 2.0*Fy/(U**2*DL)
|
|
||||||
if Tz is None:
|
|
||||||
return Cd, Cl
|
|
||||||
return Cd, Cl, 2.0*Tz/(U**2*DL*DL)
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
# НЕЗАВИСИМЫЙ КРОСС-ЧЕК силы/момента: баланс импульса контрольного объёма, ограниченного
|
|
||||||
# замкнутым контуром (окружностью) вокруг тела на тонком уровне L1.
|
|
||||||
#
|
|
||||||
# F_тела = ∮ [σ·n − ρ·u·(u·n)] ds − d/dt ∫_кольцо ρu dV, σ = −p′·I + σ^v
|
|
||||||
#
|
|
||||||
# * давление: p = ρ·c_s²; интегрируем p′ = (ρ − ⟨ρ⟩_контур)·c_s² — константа давления по замкнутому
|
|
||||||
# контуру даёт ∮ p0·n ds ≡ 0, а вычитание убирает катастрофическое сокращение в float32;
|
|
||||||
# * вязкая часть из неравновесных моментов (Чепмен–Энског): σ^v_αβ = −(1 − 1/(2τ))·Π^neq_αβ,
|
|
||||||
# Π^neq_αβ = Σ_i c_iα c_iβ (f_i − f_i^eq).
|
|
||||||
# ВАЖНО (KBC): след Π^neq релаксирует не с 1/τ, а с γβ (поле-зависимый стабилизатор), поэтому
|
|
||||||
# берём строго ДЕВИАТОРНУЮ проекцию (след исключён) — сдвиг-моменты в KBC релаксируют с точным
|
|
||||||
# 2β = 1/τ, для них префактор корректен. Bulk-часть в силу трения и не входит.
|
|
||||||
# * КОНВЕКТИВНЫЙ член −ρu(u·n): поток импульса через контур. Зануляется только при δ→0 (no-slip
|
|
||||||
# на поверхности); при δ=2 скорость на контуре уже заметна, и без этого члена кросс-чек давал
|
|
||||||
# систематический сдвиг +4% по ⟨Cd⟩ на всех сетках (факторное исследование, эксперимент №0).
|
|
||||||
# * член d/dt ∫ρu dV по кольцу R..R1+δ НЕ считается: в среднем по времени он зануляется,
|
|
||||||
# на мгновенных рядах даёт малый сдвиг фазы → кросс-чек ведётся по средним (⟨Cd⟩, rms Cl).
|
|
||||||
# * контур — окружность радиуса R1+δ (δ = cfg.stress_delta тонких ячеек, по умолчанию 2):
|
|
||||||
# билинейный стенсиль выборки не задевает первое жидкое кольцо у стенки, где популяции
|
|
||||||
# реконструированы Bouzidi (q_max ≈ 0.98 < 1).
|
|
||||||
#
|
|
||||||
# Вызывается РЕДКО (каждые cfg.stress_every шагов) в Solver.run() ВНЕ step()/CUDA-графа.
|
|
||||||
import math
|
|
||||||
import backend as B
|
|
||||||
import lattice as L
|
|
||||||
import equilibrium as E
|
|
||||||
|
|
||||||
cp = B.cp; xp = B.xp; DTYPE = B.DTYPE
|
|
||||||
CS2 = L.CS2
|
|
||||||
|
|
||||||
def build_probe(cfg):
|
|
||||||
"""Прекомпьют контура (один раз, вне захвата): N=stress_ntheta точек на окружности R1+δ
|
|
||||||
вокруг центра тела на L1; индексы/веса билинейной интерполяции, нормали, плечи, элемент дуги."""
|
|
||||||
r = cfg.refine
|
|
||||||
R1 = r * float(cfg.D) / 2.0 # радиус цилиндра на L1 (тонкие ячейки)
|
|
||||||
Rp = R1 + float(cfg.stress_delta)
|
|
||||||
ccx = (cfg.cx - cfg.ax1) * r; ccy = (cfg.cy - cfg.ay1) * r
|
|
||||||
N = int(cfg.stress_ntheta)
|
|
||||||
th = (2.0 * math.pi / N) * cp.arange(N)
|
|
||||||
nx = cp.cos(th).astype(DTYPE); ny = cp.sin(th).astype(DTYPE)
|
|
||||||
px = ccx + Rp * nx; py = ccy + Rp * ny
|
|
||||||
x0 = cp.floor(px).astype(cp.int64); y0 = cp.floor(py).astype(cp.int64)
|
|
||||||
# контур обязан лежать внутри патча L1 (он центрирован вокруг тела — выполняется с запасом)
|
|
||||||
Nfx = (cfg.bx1 - cfg.ax1) * r + 1; Nfy = (cfg.by1 - cfg.ay1) * r + 1
|
|
||||||
assert int(x0.min()) >= 0 and int(x0.max()) + 1 <= Nfx - 1, "контур σ·n вышел за патч L1 по x"
|
|
||||||
assert int(y0.min()) >= 0 and int(y0.max()) + 1 <= Nfy - 1, "контур σ·n вышел за патч L1 по y"
|
|
||||||
return dict(x0=x0, y0=y0, x1=x0 + 1, y1=y0 + 1,
|
|
||||||
tx=(px - x0).astype(DTYPE), ty=(py - y0).astype(DTYPE),
|
|
||||||
nx=nx, ny=ny, rx=(Rp * nx), ry=(Rp * ny), ds=2.0 * math.pi * Rp / N)
|
|
||||||
|
|
||||||
def _sample(fld, pr):
|
|
||||||
"""Билинейная выборка 2D-поля в точках контура (аналог amr.pint, но по 1D-набору точек)."""
|
|
||||||
return (fld[pr["y0"], pr["x0"]] * (1 - pr["tx"]) * (1 - pr["ty"])
|
|
||||||
+ fld[pr["y0"], pr["x1"]] * pr["tx"] * (1 - pr["ty"])
|
|
||||||
+ fld[pr["y1"], pr["x0"]] * (1 - pr["tx"]) * pr["ty"]
|
|
||||||
+ fld[pr["y1"], pr["x1"]] * pr["tx"] * pr["ty"])
|
|
||||||
|
|
||||||
def force_stress(F1, pr, tau1):
|
|
||||||
"""Сила/момент на тело из баланса импульса ∮[σ·n − ρu(u·n)]ds по контуру pr на состоянии F1
|
|
||||||
(L1, после стриминга). Возвращает (Fx, Fy, Tz) — device-скаляры. tau1 — релаксация на L1."""
|
|
||||||
rho, u = E.macros(F1)
|
|
||||||
fneq = F1 - E.feq(rho, u)
|
|
||||||
CX = L.CXa[:, None, None]; CY = L.CYa[:, None, None]
|
|
||||||
Pxx = (CX * CX * fneq).sum(0)
|
|
||||||
Pyy = (CY * CY * fneq).sum(0)
|
|
||||||
Pxy = (CX * CY * fneq).sum(0)
|
|
||||||
rho_s = _sample(rho, pr)
|
|
||||||
ux_s = _sample(u[0], pr); uy_s = _sample(u[1], pr)
|
|
||||||
Pxx_s = _sample(Pxx, pr); Pyy_s = _sample(Pyy, pr); Pxy_s = _sample(Pxy, pr)
|
|
||||||
p = (rho_s - rho_s.mean()) * CS2 # p′: давление без константы (см. шапку)
|
|
||||||
pref = 1.0 - 1.0 / (2.0 * tau1)
|
|
||||||
tr = 0.5 * (Pxx_s + Pyy_s) # девиаторная проекция (след исключён)
|
|
||||||
sxx = -pref * (Pxx_s - tr); syy = -pref * (Pyy_s - tr); sxy = -pref * Pxy_s
|
|
||||||
un = ux_s * pr["nx"] + uy_s * pr["ny"] # нормальная скорость на контуре
|
|
||||||
flux = rho_s * un # ρ(u·n): конвективный вынос импульса
|
|
||||||
tx = (-p + sxx) * pr["nx"] + sxy * pr["ny"] - flux * ux_s # t = σ·n − ρu(u·n)
|
|
||||||
ty = sxy * pr["nx"] + (-p + syy) * pr["ny"] - flux * uy_s
|
|
||||||
Fx = tx.sum() * pr["ds"]; Fy = ty.sum() * pr["ds"]
|
|
||||||
Tz = (pr["rx"] * ty - pr["ry"] * tx).sum() * pr["ds"] # давление в торк не входит (n ∥ r)
|
|
||||||
return Fx, Fy, Tz
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
# Геометрия: маски тела/стенок и знаковое поле расстояний (SDF) для L0 и патча L1.
|
|
||||||
# Чистый CuPy. Соглашение по SDF: phi>0 в жидкости, phi<0 в теле; |phi| ≈ расстоянию до границы.
|
|
||||||
import backend as B
|
|
||||||
import lattice as L
|
|
||||||
import amr
|
|
||||||
|
|
||||||
cp = B.cp; xp = B.xp; DTYPE = B.DTYPE
|
|
||||||
|
|
||||||
def _cmask(NX, NY, ccx, ccy, R):
|
|
||||||
"""Булева маска круга радиуса R (центр ccx,ccy) на сетке (NY,NX)."""
|
|
||||||
Y, X = cp.meshgrid(cp.arange(NY), cp.arange(NX), indexing="ij")
|
|
||||||
return (X - ccx)**2 + (Y - ccy)**2 <= R*R
|
|
||||||
|
|
||||||
def _csdf(NX, NY, ccx, ccy, R):
|
|
||||||
"""SDF круга: sqrt((x−ccx)²+(y−ccy)²) − R (отрицательно внутри)."""
|
|
||||||
Y, X = cp.meshgrid(cp.arange(NY), cp.arange(NX), indexing="ij")
|
|
||||||
return cp.sqrt((X - ccx).astype(DTYPE)**2 + (Y - ccy).astype(DTYPE)**2) - R
|
|
||||||
|
|
||||||
class Geometry:
|
|
||||||
"""Контейнер масок/SDF/патча. Поля *_np — host-копии для визуализации."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def build(cfg):
|
|
||||||
Nx, Ny, D, cx, cy = cfg.Nx, cfg.Ny, cfg.D, cfg.cx, cfg.cy
|
|
||||||
r = cfg.refine
|
|
||||||
ax1, bx1, ay1, by1 = cfg.ax1, cfg.bx1, cfg.ay1, cfg.by1
|
|
||||||
g = Geometry()
|
|
||||||
|
|
||||||
# --- L0: только цилиндр как твёрдое тело (стенки канала верх/низ — free-slip, не solid) ---
|
|
||||||
g.cyl0 = _cmask(Nx, Ny, cx, cy, D/2.0)
|
|
||||||
g.solid0 = g.cyl0 # стенки обрабатываются specular-отражением, см. boundary.free_slip_walls
|
|
||||||
g.phi0 = _csdf(Nx, Ny, cx, cy, D/2.0) # SDF только цилиндра (для Bouzidi тела)
|
|
||||||
|
|
||||||
# --- патч L1 (измельчение r) над областью [ax1,bx1]×[ay1,by1] ---
|
|
||||||
g.P1 = amr.patch(Nx, Ny, ax1, bx1, ay1, by1, r)
|
|
||||||
Nfx, Nfy = g.P1["Nfx"], g.P1["Nfy"]
|
|
||||||
# цилиндр на L1: центр и радиус масштабируются на r (диаметр r·D → радиус r·D/2)
|
|
||||||
R1 = r * D / 2.0
|
|
||||||
g.solid1 = _cmask(Nfx, Nfy, (cx - ax1)*r, (cy - ay1)*r, R1)
|
|
||||||
g.phi1 = _csdf(Nfx, Nfy, (cx - ax1)*r, (cy - ay1)*r, R1)
|
|
||||||
|
|
||||||
# маска жидких узлов для рестрикции L1→L0 (внутренняя часть патча на L0)
|
|
||||||
g.fl1 = ~g.solid0[ay1+1:by1, ax1+1:bx1]
|
|
||||||
|
|
||||||
# host-копии для визуализации
|
|
||||||
g.solid0_np = B.to_cpu(g.solid0); g.solid1_np = B.to_cpu(g.solid1)
|
|
||||||
return g
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
# Решётка D2Q9 и KBC-проектор. Чистый CuPy; всё считается один раз при импорте.
|
|
||||||
#
|
|
||||||
# Скорости e_i и веса w_i (стандартная нумерация D2Q9):
|
|
||||||
# 0:( 0, 0) 1:(+1,0) 2:(0,+1) 3:(-1,0) 4:(0,-1)
|
|
||||||
# 5:(+1,+1) 6:(-1,+1) 7:(-1,-1) 8:(+1,-1)
|
|
||||||
# OPP[i] — индекс противоположного направления.
|
|
||||||
import backend as B
|
|
||||||
|
|
||||||
cp = B.cp; xp = B.xp; DTYPE = B.DTYPE
|
|
||||||
|
|
||||||
Q = 9
|
|
||||||
Cx = [0, 1, 0, -1, 0, 1, -1, -1, 1] # python-инты (для roll/индексации)
|
|
||||||
Cy = [0, 0, 1, 0, -1, 1, 1, -1, -1]
|
|
||||||
OPP = [0, 3, 4, 1, 2, 7, 8, 5, 6]
|
|
||||||
_Wlist = [4/9, 1/9, 1/9, 1/9, 1/9, 1/36, 1/36, 1/36, 1/36]
|
|
||||||
CS2 = 1.0 / 3.0 # квадрат скорости звука решётки
|
|
||||||
|
|
||||||
# device-векторы
|
|
||||||
Wa = cp.asarray(_Wlist, DTYPE)
|
|
||||||
CXa = cp.asarray(Cx, DTYPE)
|
|
||||||
CYa = cp.asarray(Cy, DTYPE)
|
|
||||||
|
|
||||||
def _build_projector():
|
|
||||||
"""Проектор Ps на сдвиг-моменты {cx²−cy², cx·cy} (ядро KBC-N1).
|
|
||||||
Ps = M⁻¹ · diag(оставить только эти 2 момента) · M, где M — моментный базис."""
|
|
||||||
cxv = cp.asarray(Cx, cp.float64); cyv = cp.asarray(Cy, cp.float64)
|
|
||||||
M = cp.zeros((Q, Q), cp.float64)
|
|
||||||
M[0] = 1.0; M[1] = cxv; M[2] = cyv; M[3] = 3.0*(cxv**2 + cyv**2) - 2.0
|
|
||||||
M[4] = cxv**2 - cyv**2; M[5] = cxv*cyv
|
|
||||||
M[6] = cxv**2*cyv; M[7] = cxv*cyv**2; M[8] = cxv**2*cyv**2
|
|
||||||
Dm = cp.zeros((Q, Q), cp.float64); Dm[4, 4] = Dm[5, 5] = 1.0
|
|
||||||
return (cp.linalg.inv(M) @ Dm @ M)
|
|
||||||
|
|
||||||
Ps = _build_projector().astype(DTYPE) # (Q,Q) проектор на сдвиг
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
# Точка входа модели 2×+SDF. Запуск из этой папки:
|
|
||||||
# python run.py
|
|
||||||
# AMR_STEPS=2000 python run.py # короткий прогон
|
|
||||||
# AMR_CUDAGRAPH=0 python run.py # без CUDA-графов
|
|
||||||
# AMR_FP64=1 python run.py # двойная точность
|
|
||||||
import os, time
|
|
||||||
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)
|
|
||||||
|
|
||||||
def main():
|
|
||||||
cfg = C.from_env()
|
|
||||||
print(f"[2×+SDF] backend={B.BACKEND}; домен {cfg.Nx}x{cfg.Ny}; D={cfg.D}; steps={cfg.steps}; "
|
|
||||||
f"collision=kbc; force=gmem; "
|
|
||||||
f"graphs={'on' if cfg.use_cuda_graph else 'off'}; "
|
|
||||||
f"L1 {cfg.refine}× = {(cfg.bx1-cfg.ax1)*cfg.refine+1}x{(cfg.by1-cfg.ay1)*cfg.refine+1}; "
|
|
||||||
f"вход {cfg.cx/cfg.D:.1f}D / выход {(cfg.Nx-1-cfg.cx)/cfg.D:.1f}D; outlet_uy={cfg.outlet_uy}")
|
|
||||||
|
|
||||||
t0 = time.perf_counter()
|
|
||||||
sim = S.Solver(cfg)
|
|
||||||
res = sim.run(Viz.make_progress("2x+sdf"))
|
|
||||||
print(" время:", round(time.perf_counter() - t0, 1), "s")
|
|
||||||
|
|
||||||
a = Diag.analyze(res, cfg)
|
|
||||||
Diag.print_table(a, cfg)
|
|
||||||
Diag.print_crosscheck(a)
|
|
||||||
Diag.convergence_report(res, cfg)
|
|
||||||
|
|
||||||
Viz.save_gif(res, sim.geo, cfg, os.path.join(OUT, "cyl_2x_sdf.gif"))
|
|
||||||
Viz.save_series_panel(res, a, cfg, os.path.join(OUT, "series_2x_sdf.png"))
|
|
||||||
import numpy as np
|
|
||||||
np.savez(os.path.join(OUT, "probe_2x_sdf.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")),
|
|
||||||
Cm_st=a.get("Cm_st", float("nan")),
|
|
||||||
Ny=cfg.Ny, beta=cfg.D/cfg.Ny, D=cfg.D, U=cfg.U, Re=cfg.Re, steps=cfg.steps,
|
|
||||||
Nx=cfg.Nx, cx=cfg.cx, outlet_uy=cfg.outlet_uy,
|
|
||||||
stress_every=res["stress_every"],
|
|
||||||
# ряды
|
|
||||||
Cl=a["Cl_s"], uy=a["uy"], Fx=B.to_cpu(res["Fx"]), Fy=B.to_cpu(res["Fy"]),
|
|
||||||
Tz=B.to_cpu(res["Tz"]), rho=B.to_cpu(res["rho"]),
|
|
||||||
Fx_st=B.to_cpu(res["Fx_st"]), Fy_st=B.to_cpu(res["Fy_st"]), Tz_st=B.to_cpu(res["Tz_st"]))
|
|
||||||
print("DONE (2×+SDF)")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
# Серия по блокировке: D фиксирован, Ny растёт → β=D/Ny падает; экстраполяция β→0 даёт
|
|
||||||
# прямое сравнение с БЕЗГРАНИЧНОЙ литературой (Re=150: Cd≈1.33, St≈0.183, rms Cl≈0.3)
|
|
||||||
# без ручной поправки (1−β)². Запуск из этой папки:
|
|
||||||
# python run_blockage.py
|
|
||||||
# AMR_NY_LIST=90,128,180 python run_blockage.py # свой список Ny
|
|
||||||
# AMR_STEPS=20000 AMR_NY_LIST=128 python run_blockage.py # смоук
|
|
||||||
# Стоимость прогона ≈ как у run.py: патч L1 (основная работа) от Ny не зависит (высота 4D фикс.).
|
|
||||||
import os, 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) # безграничный цилиндр, Re≈150
|
|
||||||
|
|
||||||
def run_one(Ny):
|
|
||||||
cfg = C.from_env(Ny=Ny, frame_every=0) # серия без кадров/гифок (только ряды)
|
|
||||||
print(f"\n[блокировка] Ny={cfg.Ny} (β={cfg.D/cfg.Ny:.3f}); домен {cfg.Nx}x{cfg.Ny}; "
|
|
||||||
f"патч y=[{cfg.ay1},{cfg.by1}]; steps={cfg.steps}")
|
|
||||||
t0 = time.perf_counter()
|
|
||||||
sim = S.Solver(cfg)
|
|
||||||
res = sim.run(Viz.make_progress(f"Ny={cfg.Ny}"))
|
|
||||||
print(" время:", round(time.perf_counter() - t0, 1), "s")
|
|
||||||
a = Diag.analyze(res, cfg)
|
|
||||||
Diag.print_table(a, cfg)
|
|
||||||
Diag.print_crosscheck(a)
|
|
||||||
Diag.convergence_report(res, cfg)
|
|
||||||
np.savez(os.path.join(OUT, f"probe_blockage_Ny{cfg.Ny}.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")),
|
|
||||||
Ny=cfg.Ny, beta=cfg.D/cfg.Ny, D=cfg.D, U=cfg.U, Re=cfg.Re, steps=cfg.steps,
|
|
||||||
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"]))
|
|
||||||
row = dict(Ny=cfg.Ny, beta=cfg.D/cfg.Ny, St=a["St"], Cd=a["Cd"], Clrms=a["Clrms"],
|
|
||||||
Cm=a.get("Cm", float("nan")), Cd_st=a.get("Cd_st"))
|
|
||||||
# освободить VRAM перед следующим прогоном (буферы, BC, захваченный CUDA-граф)
|
|
||||||
del sim, res, a
|
|
||||||
B.cp.get_default_memory_pool().free_all_blocks()
|
|
||||||
return row
|
|
||||||
|
|
||||||
def save_extrapolation_figure(rows, fits, path):
|
|
||||||
import matplotlib; matplotlib.use("Agg")
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
betas = np.array([r["beta"] for r in rows]); bb = np.linspace(0, betas.max()*1.05, 100)
|
|
||||||
panels = [("Cd", "⟨Cd⟩", LIT["Cd"], "лит. 1.33"),
|
|
||||||
("Clrms", "rms Cl", LIT["Clrms"], "лит. ~0.3"),
|
|
||||||
("St", "St", LIT["St"], "лит. 0.183")]
|
|
||||||
fig, axs = plt.subplots(1, 3, figsize=(13.5, 4.2), dpi=110)
|
|
||||||
for ax, (key, lab, lit, litlab) in zip(axs, panels):
|
|
||||||
vals = np.array([r[key] for r in rows]); ft = fits[key]
|
|
||||||
ax.plot(betas, vals, "o", ms=7, color="#d32f2f", label="серия (raw)")
|
|
||||||
al, sl = ft["lin"]; aq, sq = ft["quad"]
|
|
||||||
ax.plot(bb, al + sl*bb, "-", lw=1.2, color="#1976d2", label=f"a+b·β → {al:.3f}")
|
|
||||||
ax.plot(bb, aq + sq*bb**2, "--", lw=1.2, color="#388e3c", label=f"a+b·β² → {aq:.3f}")
|
|
||||||
ax.axhline(lit, color="gray", lw=1.0, ls=":", label=litlab)
|
|
||||||
if key == "St": # для St полезен и кинематически поправленный вид
|
|
||||||
ax.plot(betas, vals*(1-betas), "s", ms=6, mfc="none", color="#7b1fa2", label="St·(1−β)")
|
|
||||||
ax.set_xlabel("β = D/Ny"); ax.set_ylabel(lab); ax.set_xlim(0, bb[-1])
|
|
||||||
ax.legend(fontsize=8); ax.set_title(f"{lab}(β): экстраполяция β→0", fontsize=10)
|
|
||||||
fig.suptitle("Серия по блокировке: стеснение каналом — свойство ПОСТАНОВКИ, не считывания силы",
|
|
||||||
fontsize=11)
|
|
||||||
fig.tight_layout(rect=(0, 0, 1, 0.93))
|
|
||||||
fig.savefig(path, bbox_inches="tight"); plt.close(fig)
|
|
||||||
print("saved", os.path.basename(path))
|
|
||||||
|
|
||||||
def main():
|
|
||||||
ny_list = [int(s) for s in os.environ.get("AMR_NY_LIST", "90,128,180").split(",")]
|
|
||||||
print(f"[серия по блокировке] backend={B.BACKEND}; Ny={ny_list}")
|
|
||||||
rows = [run_one(Ny) for Ny in ny_list]
|
|
||||||
if len(rows) < 2:
|
|
||||||
print("\n(одна точка — фит/экстраполяция пропущены)")
|
|
||||||
return
|
|
||||||
betas = [r["beta"] for r in rows]
|
|
||||||
fits = {key: Diag.fit_blockage(betas, [r[key] for r in rows]) for key in ("Cd", "Clrms", "St")}
|
|
||||||
Diag.print_blockage_table(rows, fits)
|
|
||||||
np.savez(os.path.join(OUT, "blockage_summary.npz"),
|
|
||||||
Ny=np.array([r["Ny"] for r in rows]), beta=np.array(betas),
|
|
||||||
St=np.array([r["St"] for r in rows]), Cd=np.array([r["Cd"] for r in rows]),
|
|
||||||
Clrms=np.array([r["Clrms"] for r in rows]), Cm=np.array([r["Cm"] for r in rows]),
|
|
||||||
Cd_st=np.array([r["Cd_st"] if r["Cd_st"] is not None else np.nan for r in rows]),
|
|
||||||
Cd0_lin=fits["Cd"]["lin"][0], Cd0_quad=fits["Cd"]["quad"][0],
|
|
||||||
Cl0_lin=fits["Clrms"]["lin"][0], Cl0_quad=fits["Clrms"]["quad"][0],
|
|
||||||
St0_lin=fits["St"]["lin"][0], St0_quad=fits["St"]["quad"][0],
|
|
||||||
lit_Cd=LIT["Cd"], lit_Clrms=LIT["Clrms"], lit_St=LIT["St"])
|
|
||||||
save_extrapolation_figure(rows, fits, os.path.join(OUT, "blockage_extrapolation.png"))
|
|
||||||
print("DONE (серия по блокировке)")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
# ФАКТОРНОЕ ИССЛЕДОВАНИЕ постановки: какие границы расчётной области отвечают за завышение
|
|
||||||
# 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()
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
# Оркестратор модели 2×+SDF: держит persistent-буферы, собирает один шаг из компонентов и гоняет цикл.
|
|
||||||
# Один шаг = замкнутая step() над фикс. буферами без host→device (готова к захвату в CUDA-граф).
|
|
||||||
import math
|
|
||||||
import backend as B
|
|
||||||
import lattice as L
|
|
||||||
import geometry as G
|
|
||||||
import equilibrium as E
|
|
||||||
import collision as Coll
|
|
||||||
import streaming as Strm
|
|
||||||
import boundary as Bnd
|
|
||||||
import forces as Fz
|
|
||||||
import forces_stress as FzS
|
|
||||||
import amr as Amr
|
|
||||||
|
|
||||||
cp = B.cp; xp = B.xp; DTYPE = B.DTYPE; to_cpu = B.to_cpu
|
|
||||||
CYa = L.CYa
|
|
||||||
|
|
||||||
def _capture(step_fn):
|
|
||||||
"""Захват одного шага в CUDA-граф. При сбое гарантированно выводит поток из режима записи."""
|
|
||||||
cp.cuda.Device().synchronize()
|
|
||||||
s = cp.cuda.Stream(non_blocking=True)
|
|
||||||
with s:
|
|
||||||
s.begin_capture()
|
|
||||||
try:
|
|
||||||
step_fn()
|
|
||||||
except Exception:
|
|
||||||
try: s.end_capture()
|
|
||||||
except Exception: pass
|
|
||||||
raise
|
|
||||||
return s.end_capture()
|
|
||||||
|
|
||||||
class Solver:
|
|
||||||
def __init__(self, cfg):
|
|
||||||
self.cfg = cfg
|
|
||||||
self.collide = Coll.collide # KBC-N1 (единственный оператор столкновения)
|
|
||||||
self.force = Fz.force_gmem # галилей-инвариантный обмен импульсом
|
|
||||||
self.geo = G.build(cfg)
|
|
||||||
self.P1 = self.geo.P1
|
|
||||||
self.beta1 = cfg.beta1
|
|
||||||
if cfg.sponge_len:
|
|
||||||
# губка: τ(x) = τ0 + (mult−1)·ν·s(x)/cs², s — smoothstep 0→1 в последних sponge_len
|
|
||||||
# столбцах. β становится ПОЛЕМ (Nx,) → broadcast в kbc_collide; патч L1 губки не видит
|
|
||||||
# (гарантировано assert'ом в config), там остаётся скалярный beta1.
|
|
||||||
xs = cp.arange(cfg.Nx, dtype=DTYPE)
|
|
||||||
s = cp.clip((xs - (cfg.Nx - 1 - cfg.sponge_len)) / float(cfg.sponge_len), 0.0, 1.0)
|
|
||||||
s = s*s*(3.0 - 2.0*s)
|
|
||||||
tau_x = cfg.tau0 + (cfg.sponge_nu_mult - 1.0) * cfg.nu / (1.0/3.0) * s
|
|
||||||
self.beta0 = (1.0 / (2.0 * tau_x)).astype(DTYPE)[None, :] # (1,Nx) → broadcast по y
|
|
||||||
else:
|
|
||||||
self.beta0 = cfg.beta0
|
|
||||||
self.R01 = cfg.R01; self.Rf01 = cfg.Rf01
|
|
||||||
self.refine = cfg.refine
|
|
||||||
# ЗОНД скорости в следе: физическая точка (px,py) в координатах L0.
|
|
||||||
# Берём её с ТОНКОЙ сетки L1 (меньше численного размытия вихрей → чище St / rms u_y),
|
|
||||||
# если точка внутри патча; иначе fallback на L0. Индексы фиксированы → graph-safe.
|
|
||||||
self.probe_l1 = (cfg.ax1 <= cfg.px <= cfg.bx1) and (cfg.ay1 <= cfg.py <= cfg.by1)
|
|
||||||
if self.probe_l1:
|
|
||||||
self.pfx = (cfg.px - cfg.ax1) * cfg.refine
|
|
||||||
self.pfy = (cfg.py - cfg.ay1) * cfg.refine
|
|
||||||
else:
|
|
||||||
self.pfx = cfg.px; self.pfy = cfg.py
|
|
||||||
# ГУ (SDF/Bouzidi) на L0 (цилиндр+стенки) и L1 (цилиндр)
|
|
||||||
self.BC0 = Bnd.build_bc(self.geo.solid0, self.geo.phi0, self.geo.cyl0)
|
|
||||||
self.BC1 = Bnd.build_bc(self.geo.solid1, self.geo.phi1, self.geo.solid1)
|
|
||||||
self.fl1 = self.geo.fl1
|
|
||||||
# persistent-состояние
|
|
||||||
Ny, Nx = cfg.Ny, cfg.Nx; Nfy, Nfx = self.P1["Nfy"], self.P1["Nfx"]
|
|
||||||
self.F0 = E.feq(xp.ones((Ny, Nx), DTYPE), xp.zeros((2, Ny, Nx), DTYPE)).copy()
|
|
||||||
self.F1 = E.feq(xp.ones((Nfy, Nfx), DTYPE), xp.zeros((2, Nfy, Nfx), DTYPE)).copy()
|
|
||||||
self.onecol = xp.ones((Ny, 1), DTYPE)
|
|
||||||
self.uin = xp.zeros((2, Ny, 1), DTYPE) # вход; обновляется СНАРУЖИ захвата
|
|
||||||
self.Fx_s = xp.zeros((), DTYPE); self.Fy_s = xp.zeros((), DTYPE); self.uy_s = xp.zeros((), DTYPE)
|
|
||||||
self.Tz_s = xp.zeros((), DTYPE)
|
|
||||||
# координаты узлов L1 относительно центра тела — плечи торка (прекомпьют вне захвата)
|
|
||||||
ccx1 = (cfg.cx - cfg.ax1) * cfg.refine; ccy1 = (cfg.cy - cfg.ay1) * cfg.refine
|
|
||||||
Yf, Xf = cp.meshgrid(cp.arange(Nfy), cp.arange(Nfx), indexing="ij")
|
|
||||||
self.rxy1 = ((Xf - ccx1).astype(DTYPE), (Yf - ccy1).astype(DTYPE))
|
|
||||||
# контур кросс-чека силы σ·n (forces_stress; считается редко, вне CUDA-графа)
|
|
||||||
self.sprobe = FzS.build_probe(cfg) if cfg.stress_every else None
|
|
||||||
# разгон входа на device: U*smoothstep(t/ramp)
|
|
||||||
ts = cp.minimum(cp.arange(cfg.steps + 1) / cfg.ramp, 1.0)
|
|
||||||
self.ramp_u = (cfg.U * (ts*ts*(3 - 2*ts))).astype(DTYPE)
|
|
||||||
# стартовое возмущение: поперечный sin-импульс u_y входа в окне [ramp, ramp+pert_dur] (триггер схода)
|
|
||||||
uyp = cp.zeros(cfg.steps + 1, DTYPE)
|
|
||||||
if cfg.pert_dur > 0 and cfg.pert_amp != 0.0:
|
|
||||||
k = cp.arange(cfg.steps + 1)
|
|
||||||
inw = (k >= cfg.ramp) & (k < cfg.ramp + cfg.pert_dur)
|
|
||||||
ph = (k - cfg.ramp).astype(DTYPE) / float(cfg.pert_dur)
|
|
||||||
uyp = cp.where(inw, (cfg.pert_amp * cfg.U) * cp.sin(math.pi * ph), 0.0).astype(DTYPE)
|
|
||||||
self.ramp_uy = uyp
|
|
||||||
# ⟨ρ⟩ — диагностика дрейфа массы. Считать ТОЛЬКО по жидкости: узлы внутри цилиндра —
|
|
||||||
# фиктивные (их популяции не имеют физического смысла, ГУ их не трогает), а их плотность
|
|
||||||
# уезжает независимо. При D=12 на 120×60 это 1.6% узлов и смещение ⟨ρ⟩ на −5.4e-4 —
|
|
||||||
# ровно тот разряд, в котором ⟨ρ⟩ и цитируется в отчётах (1.0000 / 1.001).
|
|
||||||
self.fluid0 = (~self.geo.solid0).astype(DTYPE)
|
|
||||||
self._ncells = float(self.fluid0.sum())
|
|
||||||
|
|
||||||
def step(self):
|
|
||||||
F0 = self.F0; F1 = self.F1; P1 = self.P1
|
|
||||||
feq = E.feq; macros = E.macros; stream = Strm.stream
|
|
||||||
# ----- уровень 0 -----
|
|
||||||
rho, u0 = macros(F0)
|
|
||||||
pre = F0.copy()
|
|
||||||
post0 = self.collide(F0, feq(rho, u0), self.beta0)
|
|
||||||
f0 = stream(post0); f0 = Bnd.apply_bc(f0, post0, self.BC0)
|
|
||||||
# порядок важен: стенки снимают y-заворот roll, затем Zou-He закрывает x-заворот НА ВЕСЬ
|
|
||||||
# столбец, включая угловые узлы (иначе вход и выход связаны напрямую — см. boundary.py)
|
|
||||||
f0 = Bnd.free_slip_walls(f0) # верх/низ — скользящие стенки (specular)
|
|
||||||
f0 = Bnd.channel_bc(f0, self.uin, outlet_uy=self.cfg.outlet_uy) # Zou-He вход/выход
|
|
||||||
# ----- уровень 1: refine подшагов по времени с временно́й интерполяцией ghost -----
|
|
||||||
g1o = Amr.ghost(pre, P1, self.R01); g1n = Amr.ghost(f0, P1, self.R01)
|
|
||||||
f1 = F1
|
|
||||||
# сила/момент: меряем на КАЖДОМ подшаге L1 и усредняем (анти-алиасинг; раньше — мгновенная
|
|
||||||
# на последнем подшаге). Средний Cd не меняется, ряды становятся глаже.
|
|
||||||
fxa = B.ZERO; fya = B.ZERO; tza = B.ZERO
|
|
||||||
for s1 in range(self.refine):
|
|
||||||
r1, u1 = macros(f1); post1 = self.collide(f1, feq(r1, u1), self.beta1)
|
|
||||||
f1 = stream(post1); f1 = Bnd.apply_bc(f1, post1, self.BC1)
|
|
||||||
fx, fy, tz = self.force(post1, f1, self.BC1, self.rxy1)
|
|
||||||
fxa = fxa + fx; fya = fya + fy; tza = tza + tz
|
|
||||||
w = (s1 + 1) / self.refine # временна́я интерполяция ghost: o→n
|
|
||||||
f1 = Amr.fill(f1, (1 - w)*g1o + w*g1n)
|
|
||||||
self.Fx_s[...] = fxa / self.refine; self.Fy_s[...] = fya / self.refine
|
|
||||||
self.Tz_s[...] = tza / self.refine
|
|
||||||
f0 = Amr.restrict(f1, f0, P1, self.Rf01, self.fl1)
|
|
||||||
F1[...] = f1
|
|
||||||
# зонд скорости в следе (с тонкой сетки L1, если точка внутри патча) + запись состояния
|
|
||||||
psrc = f1 if self.probe_l1 else f0
|
|
||||||
col = psrc[:, self.pfy, self.pfx]; self.uy_s[...] = (CYa*col).sum() / col.sum()
|
|
||||||
F0[...] = f0
|
|
||||||
|
|
||||||
def run(self, progress=None):
|
|
||||||
cfg = self.cfg
|
|
||||||
Fx = xp.zeros(cfg.steps + 1, DTYPE); Fy = xp.zeros(cfg.steps + 1, DTYPE); uy = xp.zeros(cfg.steps + 1, DTYPE)
|
|
||||||
Tz = xp.zeros(cfg.steps + 1, DTYPE) # момент (торк) вокруг центра тела
|
|
||||||
rho = xp.zeros(cfg.steps + 1, DTYPE) # ⟨ρ⟩(t) — диагностика дрейфа массы
|
|
||||||
# кросс-чек σ·n: редкая выборка (каждые stress_every шагов), вне CUDA-графа
|
|
||||||
nst = (cfg.steps // cfg.stress_every + 1) if cfg.stress_every else 0
|
|
||||||
Fx_st = xp.zeros(nst, DTYPE); Fy_st = xp.zeros(nst, DTYPE); Tz_st = xp.zeros(nst, DTYPE)
|
|
||||||
kst = 0
|
|
||||||
frames = []
|
|
||||||
graph = None; graph_failed = False
|
|
||||||
def snap(): return (self.F0.copy(), self.F1.copy())
|
|
||||||
def restore(s): self.F0[...] = s[0]; self.F1[...] = s[1]
|
|
||||||
def maxdiff(s): return max(float(xp.abs(s[0] - self.F0).max()), float(xp.abs(s[1] - self.F1).max()))
|
|
||||||
for t in range(cfg.steps + 1):
|
|
||||||
self.uin[0, :, 0] = self.ramp_u[t]
|
|
||||||
self.uin[1, :, 0] = self.ramp_uy[t] # поперечный импульс-триггер (0 вне окна возмущения)
|
|
||||||
did = False
|
|
||||||
if t == 1 and cfg.use_cuda_graph and not graph_failed:
|
|
||||||
# захват + РАНТАЙМ-САМОПРОВЕРКА (граф vs eager на одном шаге); при расхождении — откат
|
|
||||||
try:
|
|
||||||
graph = _capture(self.step)
|
|
||||||
before = snap(); graph.launch(); gres = snap()
|
|
||||||
restore(before); self.step()
|
|
||||||
d = maxdiff(gres)
|
|
||||||
if d > 1e-3:
|
|
||||||
print(f"\n[graph] self-check разошёлся (Δ={d:.2e}) — отключаю графы, работаю eager")
|
|
||||||
graph = None; graph_failed = True
|
|
||||||
else:
|
|
||||||
print(f"\n[graph] self-check OK (Δ={d:.2e}) — CUDA graphs активны")
|
|
||||||
did = True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"\n[graph] недоступны ({type(e).__name__}: {e}) — работаю eager")
|
|
||||||
import traceback as _tb; _tb.print_exc()
|
|
||||||
graph = None; graph_failed = True
|
|
||||||
try: cp.cuda.Device().synchronize()
|
|
||||||
except Exception: pass
|
|
||||||
if not did:
|
|
||||||
if graph is not None and t >= 1:
|
|
||||||
graph.launch()
|
|
||||||
else:
|
|
||||||
self.step()
|
|
||||||
Fx[t] = self.Fx_s; Fy[t] = self.Fy_s; Tz[t] = self.Tz_s; uy[t] = self.uy_s
|
|
||||||
rho[t] = (self.F0.sum(0) * self.fluid0).sum() / self._ncells # ⟨ρ⟩ по жидкости (дрейф массы)
|
|
||||||
if cfg.stress_every and t % cfg.stress_every == 0:
|
|
||||||
# eager-вычисление на текущем F1 после launch/step — упорядочено на стриме,
|
|
||||||
# сам захваченный граф не трогаем (паттерн как у rho[t] выше)
|
|
||||||
sfx, sfy, stz = FzS.force_stress(self.F1, self.sprobe, cfg.tau1)
|
|
||||||
Fx_st[kst] = sfx; Fy_st[kst] = sfy; Tz_st[kst] = stz; kst += 1
|
|
||||||
if progress is not None:
|
|
||||||
progress(t, cfg.steps)
|
|
||||||
if t % 500 == 0 and not bool(xp.isfinite(self.F0).all()):
|
|
||||||
print("BLEW UP", t)
|
|
||||||
Fx = Fx[:t]; Fy = Fy[:t]; Tz = Tz[:t]; uy = uy[:t]; rho = rho[:t]
|
|
||||||
Fx_st = Fx_st[:kst]; Fy_st = Fy_st[:kst]; Tz_st = Tz_st[:kst]
|
|
||||||
break
|
|
||||||
if cfg.frame_every and t % cfg.frame_every == 0:
|
|
||||||
frames.append((t, to_cpu(E.macros(self.F0)[1]), to_cpu(E.macros(self.F1)[1])))
|
|
||||||
return dict(Fx=Fx, Fy=Fy, Tz=Tz, uy=uy, rho=rho, frames=frames, DL=cfg.DL,
|
|
||||||
Fx_st=Fx_st, Fy_st=Fy_st, Tz_st=Tz_st, stress_every=cfg.stress_every)
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# ПЕРЕНОС (streaming): f_i(x, t+1) = f_i(x − c_i, t).
|
|
||||||
# Реализован пул-схемой через cp.roll по осям (y, x). 9 сдвигов в новый буфер.
|
|
||||||
#
|
|
||||||
# Заметка для ревизии: roll создаёт по ядру на направление (9 ядер). Это НЕ влияет на физику,
|
|
||||||
# только на число запусков ядер. Главный способ убрать накладные расходы — CUDA-graph (см. solver.py),
|
|
||||||
# а не переписывание переноса. Альтернатива (один gather-кернел по предвыч. индексам) — точка расширения.
|
|
||||||
import backend as B
|
|
||||||
import lattice as L
|
|
||||||
|
|
||||||
xp = B.xp
|
|
||||||
Q = L.Q; Cx = L.Cx; Cy = L.Cy
|
|
||||||
|
|
||||||
def stream(f):
|
|
||||||
fs = xp.empty_like(f)
|
|
||||||
for i in range(Q):
|
|
||||||
fs[i] = xp.roll(f[i], (Cy[i], Cx[i]), (0, 1))
|
|
||||||
return fs
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
# Визуализация (host): гифка |ω| с наложением тонкого патча L1 и прогресс-бар.
|
|
||||||
# matplotlib/PIL работают только с host-массивами — кадры уже перенесены на host в solver.run().
|
|
||||||
import os
|
|
||||||
import time as _time
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
def make_progress(label, width=32, every=0.1):
|
|
||||||
"""ASCII-шкала прогресса по выполненным шагам из заданных (обновление на месте через \\r)."""
|
|
||||||
st = {"t0": _time.perf_counter(), "last": 0.0}
|
|
||||||
def cb(t, total):
|
|
||||||
now = _time.perf_counter(); done = (t >= total)
|
|
||||||
if not done and (now - st["last"] < every):
|
|
||||||
return
|
|
||||||
st["last"] = now; frac = (t + 1) / (total + 1)
|
|
||||||
filled = int(round(width * frac)); bar = "#"*filled + "-"*(width - filled)
|
|
||||||
el = now - st["t0"]; eta = (el/frac - el) if frac > 0 else 0.0
|
|
||||||
print(f"\r {label:8}[{bar}] {frac*100:5.1f}% ({t}/{total}) {el:5.1f}s ETA {eta:5.1f}s",
|
|
||||||
end=("\n" if done else ""), flush=True)
|
|
||||||
return cb
|
|
||||||
|
|
||||||
def _vort(u):
|
|
||||||
return (np.roll(u[1], -1, 1) - np.roll(u[1], 1, 1))*0.5 - (np.roll(u[0], -1, 0) - np.roll(u[0], 1, 0))*0.5
|
|
||||||
|
|
||||||
def save_series_panel(res, a, cfg, path):
|
|
||||||
"""Готовая png-панель для ноутбука (он код не исполняет — только вставляет картинки):
|
|
||||||
(1) ряды Cd(t)/Cl(t) + редкие точки кросс-чека ∮σ·n ds; (2) Cm(t) — контроль симметрии;
|
|
||||||
(3) спектр зонда u_y с пиком St; (4) ⟨ρ⟩(t) — контроль дрейфа массы."""
|
|
||||||
import backend as B
|
|
||||||
import matplotlib; matplotlib.use("Agg")
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
Cd = a["Cd_s"]; Cl = a["Cl_s"]; n = len(Cd); h0 = a["h0"]; t = np.arange(n)
|
|
||||||
fig, axs = plt.subplots(2, 2, figsize=(12.5, 7.2), dpi=110)
|
|
||||||
ax = axs[0, 0]
|
|
||||||
ax.plot(t, Cd, lw=0.7, color="#d32f2f", label=f"Cd (GMEM), ⟨Cd⟩={a['Cd']:.3f}")
|
|
||||||
ax.plot(t, Cl, lw=0.7, color="#1976d2", label=f"Cl (GMEM), rms={a['Clrms']:.3f}")
|
|
||||||
if "Cd_st_s" in a:
|
|
||||||
K = a["stress_every"]; ts = np.arange(len(a["Cd_st_s"])) * K
|
|
||||||
ax.plot(ts, a["Cd_st_s"], ".", ms=3, color="#ff8f00", label=f"Cd ∮σ·n, ⟨⟩={a['Cd_st']:.3f}")
|
|
||||||
ax.plot(ts, a["Cl_st_s"], ".", ms=3, color="#4fc3f7", label="Cl ∮σ·n")
|
|
||||||
ax.axvline(h0, color="gray", lw=0.8, ls="--")
|
|
||||||
ax.set_xlabel("шаг L0"); ax.set_ylabel("Cd, Cl"); ax.legend(fontsize=8, loc="upper right")
|
|
||||||
ax.set_title("Сила: GMEM (линии) vs ∮σ·n ds (точки)", fontsize=10)
|
|
||||||
ax = axs[0, 1]
|
|
||||||
if "Cm_s" in a:
|
|
||||||
ax.plot(t, a["Cm_s"], lw=0.7, color="#388e3c")
|
|
||||||
ax.axhline(0.0, color="gray", lw=0.8)
|
|
||||||
ax.set_title(f"Cm(t): ⟨Cm⟩={a['Cm']:.5f} (контроль симметрии, ожид. ≈0)", fontsize=10)
|
|
||||||
ax.set_xlabel("шаг L0"); ax.set_ylabel("Cm")
|
|
||||||
ax = axs[1, 0]
|
|
||||||
ax.plot(a["freqs"]*cfg.D/cfg.U, a["amp"], lw=0.9, color="#7b1fa2")
|
|
||||||
ax.axvline(a["St"], color="#d32f2f", lw=1.0, ls="--", label=f"St={a['St']:.3f}")
|
|
||||||
ax.set_xlim(0, 0.6); ax.set_xlabel("St = f·D/U"); ax.set_ylabel("|FFT(u_y)|")
|
|
||||||
ax.legend(fontsize=9); ax.set_title("Спектр зонда в следе (вторая половина ряда)", fontsize=10)
|
|
||||||
ax = axs[1, 1]
|
|
||||||
rho = B.to_cpu(res["rho"]) if not isinstance(res["rho"], np.ndarray) else res["rho"]
|
|
||||||
ax.plot(np.arange(len(rho)), rho, lw=0.9, color="#5d4037")
|
|
||||||
ax.axhline(1.0, color="gray", lw=0.8)
|
|
||||||
ax.set_xlabel("шаг L0"); ax.set_ylabel("⟨ρ⟩")
|
|
||||||
ax.set_title(f"⟨ρ⟩(t): дрейф массы (финально {float(rho[-1]):.4f})", fontsize=10)
|
|
||||||
fig.suptitle(f"2×+SDF · Ny={cfg.Ny} (β={cfg.D/cfg.Ny:.3f}) · Re={cfg.Re:g} · {len(rho)-1} шагов",
|
|
||||||
fontsize=11)
|
|
||||||
fig.tight_layout(rect=(0, 0, 1, 0.96))
|
|
||||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
||||||
fig.savefig(path, bbox_inches="tight"); plt.close(fig)
|
|
||||||
print("saved", os.path.basename(path))
|
|
||||||
|
|
||||||
def save_gif(res, geo, cfg, path, fps=24):
|
|
||||||
import matplotlib; matplotlib.use("Agg")
|
|
||||||
import matplotlib.pyplot as plt, matplotlib.patches as mpatches
|
|
||||||
from PIL import Image
|
|
||||||
s0 = geo.solid0_np; s1 = geo.solid1_np
|
|
||||||
Nx, Ny = cfg.Nx, cfg.Ny; ax1, bx1, ay1, by1 = cfg.ax1, cfg.bx1, cfg.ay1, cfg.by1
|
|
||||||
frames = res["frames"]
|
|
||||||
vm = np.nanpercentile(np.abs(np.where(s0, np.nan, _vort(frames[len(frames)//2][1]))), 99.0)
|
|
||||||
cm = plt.get_cmap("inferno").copy(); cm.set_bad("white"); pil = []
|
|
||||||
for (t, u0, u1) in frames:
|
|
||||||
fig = plt.figure(figsize=(11.2, 5.7), dpi=96); ax = fig.add_subplot(111)
|
|
||||||
ax.imshow(np.abs(np.where(s0, np.nan, _vort(u0))), origin="lower", cmap=cm, vmin=0, vmax=vm,
|
|
||||||
extent=(0, Nx, 0, Ny), interpolation="nearest")
|
|
||||||
ax.imshow(np.abs(np.where(s1, np.nan, _vort(u1)))[1:-1, 1:-1], origin="lower", cmap=cm, vmin=0, vmax=vm,
|
|
||||||
extent=(ax1+0.5, bx1-0.5, ay1+0.5, by1-0.5), interpolation="nearest")
|
|
||||||
ax.add_patch(mpatches.Rectangle((0.2, 0.2), Nx-0.4, Ny-0.4, fill=False, edgecolor="#4fc3f7", lw=1.6))
|
|
||||||
ax.text(1.5, Ny-4, "L0: Δx", color="#4fc3f7", fontsize=9, weight="bold")
|
|
||||||
ax.add_patch(mpatches.Rectangle((ax1, ay1), bx1-ax1, by1-ay1, fill=False, edgecolor="#aed581", lw=2.2))
|
|
||||||
ax.text(ax1+1, by1-3.5, "L1: Δx/2 (тело+след)", color="#aed581", fontsize=9, weight="bold")
|
|
||||||
ax.set_title(f"2×+SDF · |\\omega| · t={t}", fontsize=11)
|
|
||||||
ax.set_xlabel("x [lu]"); ax.set_ylabel("y [lu]"); ax.set_xlim(0, Nx); ax.set_ylim(0, Ny)
|
|
||||||
buf = __import__("io").BytesIO(); fig.savefig(buf, format="png", bbox_inches="tight"); buf.seek(0); plt.close(fig)
|
|
||||||
pil.append(Image.open(buf).convert("RGB").convert("P", palette=Image.ADAPTIVE))
|
|
||||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
||||||
pil[0].save(path, save_all=True, append_images=pil[1:], duration=int(1000/fps), loop=0, optimize=True)
|
|
||||||
print("saved", os.path.basename(path), len(frames), "кадров")
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
@echo off
|
|
||||||
REM Portable launcher for SimVulcan. Resolves the exe path relative to this
|
|
||||||
REM script so the project folder can be moved without breaking the shortcut.
|
|
||||||
setlocal
|
|
||||||
set "SCRIPT_DIR=%~dp0"
|
|
||||||
set "EXE_DIR=%SCRIPT_DIR%build\vs2022\src\app\Release"
|
|
||||||
set "EXE=%EXE_DIR%\SimVulcan.exe"
|
|
||||||
|
|
||||||
if not exist "%EXE%" (
|
|
||||||
echo [ERROR] SimVulcan.exe not found at:
|
|
||||||
echo %EXE%
|
|
||||||
echo.
|
|
||||||
echo Build the project first:
|
|
||||||
echo cmake --preset windows-msvc-release
|
|
||||||
echo cmake --build --preset windows-msvc-release
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
cd /d "%EXE_DIR%"
|
|
||||||
start "" "%EXE%"
|
|
||||||
endlocal
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# Collect all shader sources and feed them to the simv_shaders custom target.
|
|
||||||
file(GLOB_RECURSE SHADER_SOURCES CONFIGURE_DEPENDS
|
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/editor/*.vert"
|
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/editor/*.frag"
|
|
||||||
)
|
|
||||||
|
|
||||||
if(SHADER_SOURCES)
|
|
||||||
# INCLUDE root is `shaders/` so that #include "common/foo.glsl" would resolve
|
|
||||||
# to shaders/common/foo.glsl if a shader needs a shared helper.
|
|
||||||
simv_compile_shaders(simv_shaders
|
|
||||||
${SHADER_SOURCES}
|
|
||||||
INCLUDES "${CMAKE_CURRENT_SOURCE_DIR}"
|
|
||||||
)
|
|
||||||
else()
|
|
||||||
# Empty placeholder so dependent targets don't break when no shaders exist yet.
|
|
||||||
add_custom_target(simv_shaders ALL COMMAND ${CMAKE_COMMAND} -E true)
|
|
||||||
endif()
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#version 460
|
|
||||||
|
|
||||||
layout(location = 0) in vec3 vColor;
|
|
||||||
layout(location = 0) out vec4 outColor;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
outColor = vec4(vColor, 1.0);
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
#version 460
|
|
||||||
// Reference-grid / axis lines: per-vertex colour, transformed by the MVP.
|
|
||||||
|
|
||||||
layout(location = 0) in vec3 inPos;
|
|
||||||
layout(location = 1) in vec3 inColor;
|
|
||||||
|
|
||||||
layout(push_constant) uniform PC {
|
|
||||||
mat4 mvp;
|
|
||||||
} pc;
|
|
||||||
|
|
||||||
layout(location = 0) out vec3 vColor;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
vColor = inColor;
|
|
||||||
gl_Position = pc.mvp * vec4(inPos, 1.0);
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
#version 460
|
|
||||||
// Flat shading: the per-face normal is reconstructed from screen-space
|
|
||||||
// derivatives of the model-space position, so the vertex stream needs no
|
|
||||||
// normals. color.a selects shaded (1) vs constant colour (0, for wireframe).
|
|
||||||
|
|
||||||
layout(location = 0) in vec3 vModelPos;
|
|
||||||
layout(location = 0) out vec4 outColor;
|
|
||||||
|
|
||||||
layout(push_constant) uniform PC {
|
|
||||||
mat4 mvp;
|
|
||||||
vec4 color;
|
|
||||||
} pc;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
vec3 dx = dFdx(vModelPos);
|
|
||||||
vec3 dy = dFdy(vModelPos);
|
|
||||||
vec3 c = cross(dx, dy);
|
|
||||||
float len = length(c);
|
|
||||||
vec3 n = (len > 1e-8) ? c / len : vec3(0.0, 0.0, 1.0);
|
|
||||||
|
|
||||||
const vec3 L = normalize(vec3(0.4, 0.85, 0.55));
|
|
||||||
float diff = abs(dot(n, L)); // two-sided
|
|
||||||
float shade = clamp(0.25 + 0.8 * diff, 0.0, 1.0);
|
|
||||||
|
|
||||||
vec3 base = pc.color.rgb;
|
|
||||||
vec3 rgb = mix(base, base * shade, pc.color.a);
|
|
||||||
outColor = vec4(rgb, 1.0);
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
#version 460
|
|
||||||
// Transforms model vertices by the MVP push constant. The model-space position
|
|
||||||
// is passed through so the fragment shader can derive a flat normal from it.
|
|
||||||
|
|
||||||
layout(location = 0) in vec3 inPos;
|
|
||||||
|
|
||||||
layout(push_constant) uniform PC {
|
|
||||||
mat4 mvp;
|
|
||||||
vec4 color; // .rgb base colour, .a = lit flag (1 = shaded, 0 = flat)
|
|
||||||
} pc;
|
|
||||||
|
|
||||||
layout(location = 0) out vec3 vModelPos;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
vModelPos = inPos;
|
|
||||||
gl_Position = pc.mvp * vec4(inPos, 1.0);
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
add_executable(SimVulcan
|
|
||||||
main.cpp
|
|
||||||
)
|
|
||||||
target_link_libraries(SimVulcan PRIVATE
|
|
||||||
simv_core simv_vk simv_mesh simv_editor
|
|
||||||
glm::glm spdlog::spdlog
|
|
||||||
)
|
|
||||||
add_dependencies(SimVulcan simv_shaders)
|
|
||||||
simv_set_warnings(SimVulcan)
|
|
||||||
|
|
||||||
# Copy assets next to the executable.
|
|
||||||
add_custom_command(TARGET SimVulcan POST_BUILD
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E rm -rf "$<TARGET_FILE_DIR:SimVulcan>/assets"
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
|
||||||
"${CMAKE_SOURCE_DIR}/assets"
|
|
||||||
"$<TARGET_FILE_DIR:SimVulcan>/assets"
|
|
||||||
COMMENT "Copying assets to output directory"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Copy compiled SPIR-V next to the executable.
|
|
||||||
add_custom_command(TARGET SimVulcan POST_BUILD
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/spirv"
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
|
||||||
"${CMAKE_BINARY_DIR}/spirv"
|
|
||||||
"$<TARGET_FILE_DIR:SimVulcan>/spirv"
|
|
||||||
COMMENT "Copying SPIR-V shaders to output directory"
|
|
||||||
)
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
// SimVulcan editor: a 3D viewport with an orbit camera, three reference grid
|
|
||||||
// planes through the origin, and .obj model loading/visualisation.
|
|
||||||
|
|
||||||
#include "core/App.h"
|
|
||||||
#include "core/Logger.h"
|
|
||||||
|
|
||||||
#include "vk/Renderer.h"
|
|
||||||
|
|
||||||
#include "mesh/Mesh.h"
|
|
||||||
#include "mesh/MeshDecimator.h"
|
|
||||||
|
|
||||||
#include "editor/Camera.h"
|
|
||||||
#include "editor/CameraInput.h"
|
|
||||||
#include "editor/EditorUI.h"
|
|
||||||
#include "editor/MeshLoadPanel.h"
|
|
||||||
|
|
||||||
#include <glm/geometric.hpp>
|
|
||||||
#include <glm/vec3.hpp>
|
|
||||||
|
|
||||||
#include <spdlog/spdlog.h>
|
|
||||||
|
|
||||||
#include <cstdio>
|
|
||||||
#include <exception>
|
|
||||||
#include <filesystem>
|
|
||||||
|
|
||||||
int main(int /*argc*/, char** /*argv*/) {
|
|
||||||
try {
|
|
||||||
simv::core::Logger::Instance().Init();
|
|
||||||
|
|
||||||
simv::core::App app;
|
|
||||||
simv::vk::Renderer& renderer = app.Renderer();
|
|
||||||
|
|
||||||
simv::editor::Camera camera;
|
|
||||||
simv::editor::EditorUI editorUI;
|
|
||||||
simv::editor::MeshLoadPanel meshPanel;
|
|
||||||
|
|
||||||
// Locate assets/meshes by probing a few ancestor directories (the binary
|
|
||||||
// runs from build/<preset>/src/app, with assets/ copied alongside it).
|
|
||||||
std::filesystem::path meshDir;
|
|
||||||
for (const auto& candidate : { "../../../../../assets/meshes",
|
|
||||||
"../../../../assets/meshes",
|
|
||||||
"../../../assets/meshes",
|
|
||||||
"../../assets/meshes",
|
|
||||||
"assets/meshes" }) {
|
|
||||||
if (std::filesystem::exists(candidate)) {
|
|
||||||
meshDir = std::filesystem::canonical(candidate);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (meshDir.empty()) meshDir = "assets/meshes";
|
|
||||||
meshPanel.SetMeshDirectory(meshDir);
|
|
||||||
|
|
||||||
meshPanel.SetOnLoaded([&](simv::mesh::Mesh m) {
|
|
||||||
simv::mesh::WeldVertices(m, 1e-4f);
|
|
||||||
simv::core::LogFmt(simv::core::LogCategory::MeshIO,
|
|
||||||
simv::core::LogLevel::Info,
|
|
||||||
"Mesh ready: {} verts, {} tris",
|
|
||||||
m.VertexCount(), m.TriangleCount());
|
|
||||||
renderer.SetMeshCpu(m);
|
|
||||||
// Frame the camera to the model's size (it is recentred on the origin).
|
|
||||||
const float radius = 0.5f * glm::length(m.bboxMax - m.bboxMin);
|
|
||||||
if (radius > 0.0f) camera.SetDistance(radius * 2.5f);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.SetUiCallback([&]() {
|
|
||||||
editorUI.Draw(camera);
|
|
||||||
meshPanel.Draw();
|
|
||||||
simv::editor::ApplyMouseInput(camera);
|
|
||||||
renderer.SetRenderMode(static_cast<simv::vk::RenderMode>(editorUI.RenderMode()));
|
|
||||||
renderer.SetGridVisible(editorUI.GridVisible());
|
|
||||||
renderer.SetViewProj(camera.ViewProj(renderer.AspectRatio()));
|
|
||||||
});
|
|
||||||
|
|
||||||
const int rc = app.Run();
|
|
||||||
simv::core::Logger::Instance().Shutdown();
|
|
||||||
return rc;
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
spdlog::critical("Fatal: {}", e.what());
|
|
||||||
std::fprintf(stderr, "Fatal: %s\n", e.what());
|
|
||||||
return 1;
|
|
||||||
} catch (...) {
|
|
||||||
std::fprintf(stderr, "Fatal: unknown exception\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
#include "App.h"
|
|
||||||
#include "Window.h"
|
|
||||||
|
|
||||||
#include "vk/Renderer.h"
|
|
||||||
|
|
||||||
#include <spdlog/spdlog.h>
|
|
||||||
|
|
||||||
namespace simv::core {
|
|
||||||
|
|
||||||
App::App() {
|
|
||||||
_window = std::make_unique<Window>(WindowDesc{});
|
|
||||||
_renderer = std::make_unique<simv::vk::Renderer>(*_window);
|
|
||||||
}
|
|
||||||
|
|
||||||
App::~App() {
|
|
||||||
if (_renderer) _renderer->WaitIdle();
|
|
||||||
}
|
|
||||||
|
|
||||||
void App::SetUiCallback(UiCallback cb) {
|
|
||||||
_renderer->SetUiCallback(std::move(cb));
|
|
||||||
}
|
|
||||||
|
|
||||||
int App::Run() {
|
|
||||||
spdlog::info("SimVulcan running. Close the window to exit.");
|
|
||||||
while (!_window->ShouldClose()) {
|
|
||||||
_window->PollEvents();
|
|
||||||
_renderer->DrawFrame();
|
|
||||||
}
|
|
||||||
_renderer->WaitIdle();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace simv::core
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
namespace simv::vk { class Renderer; }
|
|
||||||
|
|
||||||
namespace simv::core {
|
|
||||||
|
|
||||||
class Window;
|
|
||||||
|
|
||||||
// Top-level lifetime owner. Creates the window and the Vulkan renderer, runs the
|
|
||||||
// frame loop, and forwards the UI callback. Contains no Vulkan code itself — all
|
|
||||||
// of that lives behind simv::vk::Renderer.
|
|
||||||
class App {
|
|
||||||
public:
|
|
||||||
App();
|
|
||||||
~App();
|
|
||||||
|
|
||||||
App(const App&) = delete;
|
|
||||||
App& operator=(const App&) = delete;
|
|
||||||
|
|
||||||
// ImGui panels are drawn from this callback each frame.
|
|
||||||
using UiCallback = std::function<void()>;
|
|
||||||
void SetUiCallback(UiCallback cb);
|
|
||||||
|
|
||||||
[[nodiscard]] simv::vk::Renderer& Renderer() noexcept { return *_renderer; }
|
|
||||||
|
|
||||||
int Run();
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::unique_ptr<Window> _window;
|
|
||||||
std::unique_ptr<simv::vk::Renderer> _renderer;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace simv::core
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
add_library(simv_core STATIC
|
|
||||||
Logger.h
|
|
||||||
Logger.cpp
|
|
||||||
Window.h
|
|
||||||
Window.cpp
|
|
||||||
App.h
|
|
||||||
App.cpp
|
|
||||||
)
|
|
||||||
target_include_directories(simv_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/..)
|
|
||||||
target_link_libraries(simv_core
|
|
||||||
PUBLIC spdlog::spdlog
|
|
||||||
PRIVATE simv_vk glfw
|
|
||||||
)
|
|
||||||
simv_set_warnings(simv_core)
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
#include "Logger.h"
|
|
||||||
|
|
||||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <atomic>
|
|
||||||
#include <chrono>
|
|
||||||
#include <mutex>
|
|
||||||
|
|
||||||
namespace simv::core {
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
constexpr std::array<const char*, LogCategoryCount> kCategoryNames = {{
|
|
||||||
"core", "vk", "mesh", "ui", "test",
|
|
||||||
}};
|
|
||||||
static_assert(kCategoryNames.size() == LogCategoryCount,
|
|
||||||
"kCategoryNames must have one entry per LogCategory");
|
|
||||||
|
|
||||||
struct CategoryEntry {
|
|
||||||
bool enabled {true};
|
|
||||||
LogLevel minLevel {LogLevel::Info};
|
|
||||||
double throttleSec{0.5};
|
|
||||||
std::atomic<std::int64_t> lastLogMs {INT64_MIN};
|
|
||||||
};
|
|
||||||
|
|
||||||
struct LoggerImpl {
|
|
||||||
std::array<CategoryEntry, LogCategoryCount> entries{};
|
|
||||||
std::shared_ptr<spdlog::logger> sink;
|
|
||||||
std::chrono::steady_clock::time_point epoch{std::chrono::steady_clock::now()};
|
|
||||||
std::mutex initMutex;
|
|
||||||
bool initialized{false};
|
|
||||||
};
|
|
||||||
|
|
||||||
LoggerImpl& Impl() {
|
|
||||||
static LoggerImpl impl;
|
|
||||||
return impl;
|
|
||||||
}
|
|
||||||
|
|
||||||
spdlog::level::level_enum ToSpd(LogLevel l) noexcept {
|
|
||||||
switch (l) {
|
|
||||||
case LogLevel::Trace: return spdlog::level::trace;
|
|
||||||
case LogLevel::Debug: return spdlog::level::debug;
|
|
||||||
case LogLevel::Info: return spdlog::level::info;
|
|
||||||
case LogLevel::Warn: return spdlog::level::warn;
|
|
||||||
case LogLevel::Error: return spdlog::level::err;
|
|
||||||
case LogLevel::Critical: return spdlog::level::critical;
|
|
||||||
default: return spdlog::level::off;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::int64_t NowMs() noexcept {
|
|
||||||
using namespace std::chrono;
|
|
||||||
return duration_cast<milliseconds>(steady_clock::now() - Impl().epoch).count();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
const char* CategoryName(LogCategory cat) noexcept {
|
|
||||||
const auto idx = static_cast<std::size_t>(cat);
|
|
||||||
return idx < LogCategoryCount ? kCategoryNames[idx] : "?";
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger& Logger::Instance() noexcept {
|
|
||||||
static Logger inst;
|
|
||||||
return inst;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Logger::Init() {
|
|
||||||
auto& impl = Impl();
|
|
||||||
std::lock_guard<std::mutex> lk(impl.initMutex);
|
|
||||||
if (impl.initialized) return;
|
|
||||||
|
|
||||||
impl.sink = spdlog::stdout_color_mt("simv");
|
|
||||||
impl.sink->set_pattern("[%H:%M:%S.%e] [%^%l%$] %v");
|
|
||||||
impl.sink->set_level(spdlog::level::trace);
|
|
||||||
|
|
||||||
impl.initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Logger::Shutdown() {
|
|
||||||
auto& impl = Impl();
|
|
||||||
std::lock_guard<std::mutex> lk(impl.initMutex);
|
|
||||||
if (!impl.initialized) return;
|
|
||||||
impl.sink.reset();
|
|
||||||
spdlog::drop("simv");
|
|
||||||
impl.initialized = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Logger::SetMinLevel(LogCategory cat, LogLevel lvl) noexcept {
|
|
||||||
const auto idx = static_cast<std::size_t>(cat);
|
|
||||||
if (idx < LogCategoryCount) Impl().entries[idx].minLevel = lvl;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Logger::SetThrottle(LogCategory cat, double seconds) noexcept {
|
|
||||||
const auto idx = static_cast<std::size_t>(cat);
|
|
||||||
if (idx < LogCategoryCount) Impl().entries[idx].throttleSec = seconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Logger::SetEnabled(LogCategory cat, bool enabled) noexcept {
|
|
||||||
const auto idx = static_cast<std::size_t>(cat);
|
|
||||||
if (idx < LogCategoryCount) Impl().entries[idx].enabled = enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Logger::LogRaw(LogCategory cat, LogLevel lvl, std::string_view msg) noexcept {
|
|
||||||
auto& impl = Impl();
|
|
||||||
if (!impl.initialized) Init();
|
|
||||||
|
|
||||||
const auto idx = static_cast<std::size_t>(cat);
|
|
||||||
if (idx >= LogCategoryCount) return;
|
|
||||||
auto& e = impl.entries[idx];
|
|
||||||
if (!e.enabled) return;
|
|
||||||
if (static_cast<std::uint8_t>(lvl) < static_cast<std::uint8_t>(e.minLevel)) return;
|
|
||||||
|
|
||||||
// Warnings/errors bypass throttling.
|
|
||||||
const bool important = (lvl == LogLevel::Warn) || (lvl == LogLevel::Error)
|
|
||||||
|| (lvl == LogLevel::Critical);
|
|
||||||
if (!important && e.throttleSec > 0.0) {
|
|
||||||
const std::int64_t nowMs = NowMs();
|
|
||||||
const std::int64_t throttle = static_cast<std::int64_t>(e.throttleSec * 1000.0);
|
|
||||||
std::int64_t lastMs = e.lastLogMs.load(std::memory_order_relaxed);
|
|
||||||
if (lastMs != INT64_MIN && (nowMs - lastMs) < throttle) return;
|
|
||||||
e.lastLogMs.store(nowMs, std::memory_order_relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl.sink->log(ToSpd(lvl), "[{}] {}", CategoryName(cat), msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace simv::core
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include <string_view>
|
|
||||||
|
|
||||||
#include <spdlog/spdlog.h>
|
|
||||||
#include <spdlog/fmt/fmt.h>
|
|
||||||
|
|
||||||
namespace simv::core {
|
|
||||||
|
|
||||||
enum class LogLevel : std::uint8_t {
|
|
||||||
Trace, Debug, Info, Warn, Error, Critical, Off,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class LogCategory : std::uint8_t {
|
|
||||||
Core,
|
|
||||||
Vulkan,
|
|
||||||
MeshIO,
|
|
||||||
UI,
|
|
||||||
Test,
|
|
||||||
Count_, // sentinel
|
|
||||||
};
|
|
||||||
|
|
||||||
constexpr std::size_t LogCategoryCount = static_cast<std::size_t>(LogCategory::Count_);
|
|
||||||
|
|
||||||
const char* CategoryName(LogCategory cat) noexcept;
|
|
||||||
|
|
||||||
// Throttled, category-aware logger.
|
|
||||||
class Logger {
|
|
||||||
public:
|
|
||||||
static Logger& Instance() noexcept;
|
|
||||||
|
|
||||||
void Init(); // safe to call multiple times; first call wins
|
|
||||||
void Shutdown();
|
|
||||||
|
|
||||||
void SetMinLevel(LogCategory cat, LogLevel lvl) noexcept;
|
|
||||||
void SetThrottle(LogCategory cat, double minIntervalSeconds) noexcept;
|
|
||||||
void SetEnabled(LogCategory cat, bool enabled) noexcept;
|
|
||||||
|
|
||||||
void LogRaw(LogCategory cat, LogLevel lvl, std::string_view msg) noexcept;
|
|
||||||
|
|
||||||
private:
|
|
||||||
Logger() = default;
|
|
||||||
~Logger() = default;
|
|
||||||
Logger(const Logger&) = delete;
|
|
||||||
Logger& operator=(const Logger&) = delete;
|
|
||||||
};
|
|
||||||
|
|
||||||
template <class... Args>
|
|
||||||
void LogFmt(LogCategory cat, LogLevel lvl,
|
|
||||||
fmt::format_string<Args...> fmtStr, Args&&... args)
|
|
||||||
{
|
|
||||||
Logger::Instance().LogRaw(cat, lvl,
|
|
||||||
fmt::format(fmtStr, std::forward<Args>(args)...));
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace simv::core
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
#include "Window.h"
|
|
||||||
|
|
||||||
#include <GLFW/glfw3.h>
|
|
||||||
#include <stdexcept>
|
|
||||||
|
|
||||||
namespace simv::core {
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
int s_glfwRefCount = 0;
|
|
||||||
|
|
||||||
void GlfwErrorCallback(int code, const char* description) {
|
|
||||||
std::fprintf(stderr, "[GLFW] error %d: %s\n", code, description);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EnsureGlfwInit() {
|
|
||||||
if (s_glfwRefCount++ == 0) {
|
|
||||||
glfwSetErrorCallback(GlfwErrorCallback);
|
|
||||||
if (glfwInit() != GLFW_TRUE) {
|
|
||||||
throw std::runtime_error("glfwInit failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ReleaseGlfw() {
|
|
||||||
if (--s_glfwRefCount == 0) {
|
|
||||||
glfwTerminate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
Window::Window(const WindowDesc& desc) {
|
|
||||||
EnsureGlfwInit();
|
|
||||||
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
|
|
||||||
glfwWindowHint(GLFW_RESIZABLE, desc.resizable ? GLFW_TRUE : GLFW_FALSE);
|
|
||||||
|
|
||||||
_handle = glfwCreateWindow(desc.width, desc.height, desc.title.c_str(),
|
|
||||||
nullptr, nullptr);
|
|
||||||
if (!_handle) {
|
|
||||||
ReleaseGlfw();
|
|
||||||
throw std::runtime_error("glfwCreateWindow failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Window::~Window() {
|
|
||||||
if (_handle) glfwDestroyWindow(_handle);
|
|
||||||
ReleaseGlfw();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Window::ShouldClose() const noexcept {
|
|
||||||
return glfwWindowShouldClose(_handle) == GLFW_TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Window::PollEvents() const noexcept {
|
|
||||||
glfwPollEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Window::GetFramebufferSize(int& w, int& h) const noexcept {
|
|
||||||
glfwGetFramebufferSize(_handle, &w, &h);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<const char*> Window::RequiredInstanceExtensions() {
|
|
||||||
EnsureGlfwInit(); // glfwGetRequiredInstanceExtensions needs GLFW initialised
|
|
||||||
std::uint32_t count = 0;
|
|
||||||
const char** raw = glfwGetRequiredInstanceExtensions(&count);
|
|
||||||
std::vector<const char*> out(raw, raw + count);
|
|
||||||
ReleaseGlfw();
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Need to expose _handle storage — declared inline in header would also work,
|
|
||||||
// kept here so the GLFW header dependency stays in the .cpp.
|
|
||||||
} // namespace simv::core
|
|
||||||