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,466 +0,0 @@
|
||||
# Геометрические ядра САПР: что доступно и что брать
|
||||
|
||||
Документ по задаче #5. Цель — выбрать геометрическое ядро, поверх которого строится
|
||||
САПР-часть CFDManager, и обосновать выбор, а не объявить его.
|
||||
|
||||
Сейчас в дереве нет ничего, что можно было бы назвать ядром: `simv_mesh` читает `.obj`
|
||||
через tinyobjloader, сваривает вершины и считает AABB. Ни B-rep, ни NURBS, ни булевых
|
||||
операций, ни параметрических форматов. Всё это придётся взять снаружи — своё ядро
|
||||
пишется годами и не является предметом этого проекта.
|
||||
|
||||
## Требования и как они проверялись
|
||||
|
||||
Пять требований заданы в задаче, шестое добавлено здесь, потому что без него выбор
|
||||
некорректен.
|
||||
|
||||
| № | Требование | Как проверялось |
|
||||
|---|---|---|
|
||||
| 1 | Лёгкость и оптимизированность | сборка и запуск на этой машине, вес артефактов |
|
||||
| 2 | Rust-стек или доступное для Rust API | наличие крейта/биндингов, собрал и запустил |
|
||||
| 3 | Базовое редактирование 3D (аналог Plasticity) | наличие булевых, фасок, скруглений, выдавливания |
|
||||
| 4 | NURBS | какие поверхности представимы, что уходит в STEP |
|
||||
| 5 | Стандартные параметрические и полигональные форматы | STEP/IGES на запись **и чтение**, STL/OBJ/glTF |
|
||||
| 6 | **Лицензия** | текст лицензии и то, что она требует при поставке продукта |
|
||||
|
||||
Шестое требование добавлено по следующей причине. Ядро — не библиотека утилит, его
|
||||
нельзя заменить за неделю; лицензионное условие, замеченное после того, как на ядре
|
||||
написан год работы, стоит переписывания продукта. Поэтому GPL-ядра и ядра
|
||||
с некоммерческой лицензией разбираются здесь наравне с техникой, а не в сноске.
|
||||
|
||||
Измерения выполнены на машине разработки: Windows 11, MSVC 19.37.32825 (VS 2022
|
||||
Community), CMake 4.3.2, Ninja 1.13.2, Rust 1.97.1, сборки release. Числа, взятые
|
||||
из внешних источников, помечены явно.
|
||||
|
||||
---
|
||||
|
||||
## Группа 1. Открытый исходный код
|
||||
|
||||
### Open CASCADE Technology (OCCT)
|
||||
|
||||
Единственное зрелое открытое B-rep/NURBS ядро промышленного класса. Версия **8.0.1**
|
||||
от 30 июля 2026; семь модулей (Foundation Classes, Modeling Data, Modeling Algorithms,
|
||||
Visualization, Data Exchange, Application Framework, Mesh).
|
||||
|
||||
**Форматы:** STEP (AP203, AP214, AP242), IGES до 5.3, STL, OBJ, glTF 2.0, VRML 1.0 —
|
||||
на чтение и запись. ACIS, Parasolid, DXF, IFC, JT — **только через платные компоненты**
|
||||
Open Cascade, в открытую поставку не входят.
|
||||
|
||||
**Лицензия — LGPL 2.1 с исключением, и исключение не то, за которое его обычно
|
||||
принимают.** Полный текст `OCCT_LGPL_EXCEPTION.txt` разрешает ровно одно: включать
|
||||
в объектный код материал из заголовочных файлов библиотеки (инлайн-функции, шаблоны,
|
||||
CDL-классы) и распространять такой объектный код на своих условиях — при заметном
|
||||
уведомлении, что продукт использует OCCT. Про линковку исключение не говорит **ничего**.
|
||||
|
||||
Практический вывод: динамическая линковка — штатный путь, обязанности сводятся
|
||||
к уведомлению и к тому, что пользователь может подменить библиотеку. Статическая
|
||||
линковка остаётся под LGPL 2.1 §6 и требует выдать объектные файлы приложения, чтобы
|
||||
пользователь мог пересобрать его с изменённым OCCT. Это важно, потому что
|
||||
`opencascade-rs` со своим режимом `builtin` собирает OCCT **статически**
|
||||
(`BUILD_LIBRARY_TYPE=Static` в вызове CMake) — то есть самый удобный путь сборки
|
||||
оказывается как раз тем, который для проприетарной поставки неудобен.
|
||||
|
||||
Динамический путь существует: без `builtin` обвязка ищет **предустановленную** OCCT
|
||||
и, если та собрана с `BUILD_SHARED_LIBS=ON`, линкуется к ней как `dylib`
|
||||
(`crates/opencascade-sys/build.rs:43`). Но там же, строкой ниже, стоит жёсткая
|
||||
проверка версии: требуется мажор ровно **7** и минор не ниже 8, иначе сборка
|
||||
паникует. Поставить в системе свежую OCCT 8.0.1 и слинковаться с ней **не получится** —
|
||||
обвязка её отвергнет.
|
||||
|
||||
### opencascade-rs — путь к OCCT из Rust
|
||||
|
||||
Крейты `opencascade` / `opencascade-sys` версии 0.3.0 (обновлены 24 августа 2026),
|
||||
биндинги через `cxx`, лицензия LGPL-2.1. Форк `bschwind` живой, 263 звезды; форк
|
||||
`mkovaxx` последний раз трогали в октябре 2025 — его рассматривать не стоит.
|
||||
|
||||
API высокоуровневый и на вид приятный:
|
||||
|
||||
```rust
|
||||
let my_box = Shape::box_with_dimensions(10.0, 10.0, 1.0);
|
||||
let another = Shape::box_with_dimensions(1.0, 1.0, 0.8);
|
||||
my_box.subtract(&another).chamfer(0.07)
|
||||
```
|
||||
|
||||
Есть `write_step` / `write_iges` / `write_stl`, `read_step`, `mesh()`
|
||||
с настраиваемым допуском, фаски, скругления, выдавливание, заметание по траектории.
|
||||
|
||||
**Существенное отставание:** `occt-sys` зафиксирован на OCCT **7.8.1**, тогда как
|
||||
апстрим уже 8.0.1. Это не косметика — между ними мажорный релиз, и всё, что исправлено
|
||||
в 7.9 и 8.0, в этом пути недоступно, пока обвязку не обновят. Причём 8.0 заявлен
|
||||
командой OCCT3D (Capgemini Engineering) как релиз с ломающими изменениями API,
|
||||
чисткой интерфейсов и переработкой булевых операций, тесселяции и обработки допусков —
|
||||
то есть обновление обвязки будет не механическим, и ждать его быстро не стоит.
|
||||
|
||||
### truck — чистый Rust
|
||||
|
||||
Единственный живой чисто-растовый B-rep/NURBS кернел. Apache-2.0, 1559 звёзд,
|
||||
коммиты в репозитории вплоть до 7 сентября 2026 — **но крейты на crates.io не
|
||||
обновлялись с 20 сентября 2024** (`truck-modeling` 0.6.0). Два года git-активности
|
||||
без релиза — характерный признак: пользоваться придётся либо старым релизом,
|
||||
либо git-зависимостью.
|
||||
|
||||
Умеет: NURBS-геометрию, топологию (вершины/рёбра/грани/оболочки/тела), булевы операции
|
||||
(`truck-shapeops`), тесселяцию (`truck-meshalgo`), чтение и запись STEP
|
||||
(`truck-stepio`, есть и `in`, и `out`), полигональные форматы через `truck-polymesh`.
|
||||
|
||||
### Остальные открытые — отсев с причиной
|
||||
|
||||
| Проект | Состояние | Почему не подходит |
|
||||
|---|---|---|
|
||||
| **Fornjot** | архивирован 19 июня 2026 | Автор закрыл проект: «This project has been shut down. Its goals were never reached», код read-only, пригоден лишь для «very simple models» |
|
||||
| **SolveSpace** | живой, 4163★, GPL-3.0 | GPLv3 несовместим с проприетарной поставкой; NURBS-булевы, по признанию самих авторов, медленные, ограничены поверхностями выдавливания и вращения и «not too robust even then» |
|
||||
| **BRL-CAD** | живой, 1029★ | CSG-ориентированное ядро с Tcl-инфраструктурой, не про интерактивное NURBS-моделирование |
|
||||
| **openNURBS** | живой, 562★ | Чтение/запись `.3dm` и NURBS-геометрия, **но моделирования нет**: ни булевых, ни фасок |
|
||||
| **curvo** | живой, MIT, 0.1.91 | NURBS-кривые и поверхности как математика, не B-rep ядро |
|
||||
| **Manifold** | живой, Apache-2.0, есть крейт `manifold3d` 0.4.1 (обновлён 9 сентября 2026) | Быстрые и надёжные булевы, но по **полигональным сеткам**; NURBS и B-rep отсутствуют как понятие. Полезен позже — для подготовки геометрии к расчётной сетке, не как ядро |
|
||||
| **CGAL** | живой | Вычислительная геометрия, не САПР-ядро; ядро GPL с платной альтернативой |
|
||||
|
||||
Отдельно стоит отметить `libslvs` из SolveSpace: это **решатель геометрических
|
||||
ограничений**, то есть ровно та подсистема, которой нет ни у OCCT, ни у truck.
|
||||
К выбору ядра он отношения не имеет, но к параметрике — прямое (см. раздел о том,
|
||||
что придётся писать самим).
|
||||
|
||||
---
|
||||
|
||||
## Группа 2. Закрытый код, бесплатные и некоммерческие лицензии
|
||||
|
||||
Задача изначально говорила про открытые ядра; рамка расширена владельцем проекта.
|
||||
Результат этой группы отрезвляющий: **бесплатного входа в промышленное ядро нет ни
|
||||
у одного из четырёх.**
|
||||
|
||||
### C3D Modeler (C3D Labs)
|
||||
|
||||
Единственное коммерческое геометрическое ядро российской разработки, одно из пяти
|
||||
лицензируемых ядер в мире. B-rep с NURBS, обмен со STEP, IGES, Parasolid, ACIS,
|
||||
полигональными форматами. API — C++ с C-обёрткой; Rust-биндингов нет, их пришлось бы
|
||||
писать самим.
|
||||
|
||||
Публично доступна **только 90-дневная пробная версия** с техподдержкой. Бесплатной
|
||||
некоммерческой или академической лицензии в открытом доступе найти не удалось:
|
||||
у компании есть образовательные партнёрства с вузами, хакатоны и стажировки, но
|
||||
лицензия под них публично не описана и выдаётся по договорённости. Условия
|
||||
коммерческой лицензии компания называет гибкими и определяет индивидуально; цифра
|
||||
в 1 750 000 ₽ в год за лицензию разработчика плюс роялти встречается во вторичных
|
||||
источниках и **здесь не проверена** — уточнять её нужно у самой C3D Labs.
|
||||
|
||||
### Parasolid (Siemens)
|
||||
|
||||
Ядро, на котором построена названная в задаче Plasticity, а также SolidWorks, NX и
|
||||
десятки других систем. Эталон по надёжности булевых и качеству поверхностей.
|
||||
|
||||
Лицензируется коммерчески; публичных условий, прайса, бесплатного или академического
|
||||
тарифа в открытом доступе нет — вход только через переговоры с Siemens, и по отзывам
|
||||
разработчиков процесс и суммы для небольших проектов оказываются заградительными.
|
||||
Rust-биндингов нет.
|
||||
|
||||
### 3D ACIS Modeler и CGM Modeler (Spatial / Dassault Systèmes)
|
||||
|
||||
Второе историческое коммерческое ядро (ACIS, с конца 1980-х) и ядро CATIA (CGM).
|
||||
Технически полноценные B-rep/NURBS ядра. Ситуация с лицензией та же, что у Parasolid:
|
||||
публичных бесплатных или академических условий нет, пробный доступ и академические
|
||||
программы — по запросу. Rust-биндингов нет.
|
||||
|
||||
### Zoo Design API (бывш. KittyCAD)
|
||||
|
||||
Самый близкий к требованию 2 вариант: проприетарное ядро, **написанное на Rust**,
|
||||
GPU-native, выдаёт настоящий B-rep. Есть официальный Rust-клиент `kittycad.rs`,
|
||||
открытые Design Studio и язык KCL.
|
||||
|
||||
Но это **облачный сервис**, а не библиотека: геометрия считается на их серверах,
|
||||
тарификация поминутная, бесплатно даётся 20 минут API (баланс $10), дальше
|
||||
pay-as-you-go. Для настольного САПР с интерактивным редактированием это означает
|
||||
зависимость от сети, задержку на каждую операцию и оплату за использование, которая
|
||||
растёт вместе с числом пользователей. Для проекта, который сам по себе считает
|
||||
гидродинамику локально, такая схема противоречива.
|
||||
|
||||
### Обмен форматами отдельно
|
||||
|
||||
CAD Exchanger SDK, HOOPS Exchange, ODA — не ядра моделирования, а конвертеры.
|
||||
Они закрывают требование 5 там, где ядро не справляется (DWG, JT, CATIA, NX), и
|
||||
имеют смысл как **дополнение** к выбранному ядру, если такие форматы понадобятся.
|
||||
Все три коммерческие. Для OCCT, который сам читает STEP и IGES, необходимости
|
||||
в них на текущем этапе нет.
|
||||
|
||||
---
|
||||
|
||||
## Сводная таблица
|
||||
|
||||
Обозначения: ● полностью, ◐ частично, ○ нет.
|
||||
|
||||
| Кандидат | 1. Лёгкость | 2. Rust | 3. Редактирование | 4. NURBS | 5. Форматы | 6. Лицензия и её риск |
|
||||
|---|---|---|---|---|---|---|
|
||||
| **OCCT + opencascade-rs** | ○ 2,7 ГБ сборки, exe 19,4 МБ | ◐ биндинги есть, но на Windows не собираются без патчей | ● булевы, фаски, скругления | ● полноценно, аналитика сохраняется | ● STEP/IGES/STL/OBJ/glTF | LGPL-2.1+искл.: динамическая линковка — ок, статическая обязывает |
|
||||
| **truck** | ● 98 с сборки, exe 1,6 МБ | ● родной, собрался с первой попытки | ◐ булевы есть (в 4–6 раз медленнее), набор беднее | ◐ есть, но аналитика уходит в B-сплайны | ◐ STEP r/w, полигоны; IGES нет | Apache-2.0 — риска нет |
|
||||
| Fornjot | — | ● | ○ | ○ | ○ | архивирован |
|
||||
| SolveSpace | ● | ○ | ◐ | ◐ слабые булевы | ◐ | **GPL-3.0 — стоп для проприетарного** |
|
||||
| openNURBS | ● | ○ | ○ | ● | ◐ только 3dm | своя, свободная |
|
||||
| Manifold | ● | ● крейт `manifold3d` | ◐ только сетки | ○ | ◐ | Apache-2.0 — риска нет |
|
||||
| C3D Modeler | ◐ | ○ писать самим | ● | ● | ● | 90 дней, дальше платно |
|
||||
| Parasolid | ◐ | ○ | ● эталон | ● | ● | платно, условий публично нет |
|
||||
| ACIS / CGM | ◐ | ○ | ● | ● | ● | платно, условий публично нет |
|
||||
| Zoo Design API | ● клиент лёгкий | ● родной Rust | ● | ● | ● | облако, поминутная оплата |
|
||||
|
||||
---
|
||||
|
||||
## Измерения
|
||||
|
||||
Всё в этом разделе измерено здесь, а не переписано из чужих статей.
|
||||
|
||||
### truck
|
||||
|
||||
Зонд: два куба → булево объединение → тесселяция → экспорт OBJ и STEP. Написан
|
||||
по документации и **скомпилировался с первой попытки без правок** — показатель того,
|
||||
что API соответствует документации.
|
||||
|
||||
| Что | Значение |
|
||||
|---|---|
|
||||
| холодная сборка release | **98,6 с** |
|
||||
| пакетов в графе зависимостей | 103 |
|
||||
| исходники самих крейтов truck | 1,9 МБ, 233 файла, 10 крейтов |
|
||||
| размер `target/` | 446,8 МБ |
|
||||
| исполняемый файл зонда | 1,60 МБ |
|
||||
| построение двух кубов | 116 мкс |
|
||||
| булево объединение (плоскости) | 1,72 мс |
|
||||
| тесселяция результата | 340 мкс, 60 вершин / 36 граней |
|
||||
| экспорт STEP | 445 мкс, 17,6 КБ |
|
||||
| экспорт OBJ | 637 мкс, 8,8 КБ |
|
||||
|
||||
Второй зонд — на кривых поверхностях: цилиндр (через вращательное заметание) против
|
||||
куба, пересечение и объединение.
|
||||
|
||||
| Что | Значение |
|
||||
|---|---|
|
||||
| булево пересечение цилиндр ∩ куб | **32,6 мс** (в 19 раз дороже плоскостного) |
|
||||
| булево объединение цилиндр ∪ куб | 29,5 мс |
|
||||
| результат тесселируется | да, 84 вершины |
|
||||
|
||||
Третий зонд — чтение STEP обратно: файл, записанный первым зондом, разобран за
|
||||
**11,1 мс**, из него восстановлены 1 оболочка, 12 граней и 18 плоскостей. То есть
|
||||
круг «записал — прочитал» у truck замкнут. Мелочь, характеризующая зрелость API:
|
||||
`Table::from_step` возвращает `Option`, а не `Result`, поэтому причина неудачного
|
||||
разбора вызывающему не сообщается вовсе.
|
||||
|
||||
Обе булевы операции отработали — это лучше, чем можно было ожидать от ядра такой
|
||||
зрелости. Но экспорт цилиндра в STEP показал существенное: файл содержит `B_SPLINE_SURFACE` и
|
||||
`PLANE`, и **не содержит `CYLINDRICAL_SURFACE`**. То есть аналитические поверхности
|
||||
truck не сохраняет, всё сводится к сплайнам. Файл валиден, но принимающая система
|
||||
получает вместо цилиндра сплайновую аппроксимацию: хуже для распознавания элементов,
|
||||
хуже для точности, тяжелее.
|
||||
|
||||
### OCCT через opencascade-rs
|
||||
|
||||
Сборка этого пути оказалась мерой сама по себе — она упёрлась в три препятствия
|
||||
подряд, и каждое стоит знать заранее.
|
||||
|
||||
1. **CMake 4.x против OCCT 7.8.1.** `occt-sys` собирает OCCT 7.8.1, чей
|
||||
`cmake_minimum_required` объявляет совместимость ниже 3.5. В CMake 4.3.2 такая
|
||||
совместимость удалена, и конфигурация падает сразу:
|
||||
«Compatibility with CMake < 3.5 has been removed from CMake». Обходится
|
||||
переменной окружения `CMAKE_POLICY_VERSION_MINIMUM=3.5`. В OCCT 8.0.1 этой
|
||||
проблемы нет — но обвязка на него не переведена.
|
||||
2. **Генератор Visual Studio.** Крейт `cmake` по умолчанию берёт генератор
|
||||
«Visual Studio 17 2022», который на этой машине не отработал: установлены
|
||||
и Build Tools 2026, и VS 2022 Community. Лечится `CMAKE_GENERATOR=Ninja`
|
||||
из среды `vcvars64.bat`.
|
||||
3. **Длина пути Windows.** Дерево OCCT глубокое, и в каталоге с длинным путём
|
||||
сборка предупреждает про `CMAKE_OBJECT_PATH_MAX` (187 символов при пределе 250)
|
||||
и падает. Понадобился короткий путь вида `C:\occprobe`.
|
||||
|
||||
После того как все три обойдены, сборка идёт и **само ядро OCCT собирается успешно**.
|
||||
А потом падает компиляция cxx-мостов — тремя однотипными ошибками в собственных
|
||||
заголовках обвязки:
|
||||
|
||||
```
|
||||
b_rep.hxx(16): error C2440: невозможно преобразовать
|
||||
"opencascade::handle<Geom_Surface> *" в "std::unique_ptr<Handle_Geom_Surface, ...>"
|
||||
```
|
||||
|
||||
| Что | Значение |
|
||||
|---|---|
|
||||
| время до провала | **36 мин 09 с** (01:59:43 → 02:35:52) |
|
||||
| объём `target/` к этому моменту | ~1,0 ГБ |
|
||||
| исходники OCCT (`occt-sys` 7.8.1) | 87 МБ, 14 984 файла; скачиваемый архив 13,7 МБ |
|
||||
| ошибок компиляции | 3, все в `b_rep.hxx` |
|
||||
| собралось ли ядро OCCT | **да** |
|
||||
| собрались ли биндинги | **нет** |
|
||||
|
||||
Причина — коллизия имён: обвязка объявляет собственные типы `Handle_Geom_Surface`
|
||||
и им подобные, которые под MSVC сталкиваются с одноимёнными типами самого OCCT.
|
||||
Это **известная незакрытая проблема проекта**, а не особенность этой машины:
|
||||
PR #196 «Handle name conflict to fix Windows build» был влит ещё в январе 2025,
|
||||
PR #230 «Fix MSVC handle name collisions» от 19 августа 2026 — **закрыт, не влит**,
|
||||
а PR #216 «Fix Windows build» открыт с октября 2025 и всё ещё не принят.
|
||||
|
||||
Проверка обхода: патч из непринятого PR #230 (переименование `Handle_*` →
|
||||
`RustHandle_*`, 26 файлов) применяется к текущему master чисто — `git apply` проходит
|
||||
без конфликтов. После него мосты компилируются, **но сборка падает уже на линковке**:
|
||||
23 неразрешённых символа, среди них `InitializeAcl` и `InitializeSecurityDescriptor`,
|
||||
то есть не подключены системные библиотеки Windows. Это ровно предмет второго,
|
||||
до сих пор **открытого** PR #216 — и наложить его поверх первого не получается,
|
||||
два непринятых патча конфликтуют между собой в `build.rs`. Понадобилось дописать
|
||||
четыре строки вручную:
|
||||
|
||||
```rust
|
||||
println!("cargo:rustc-link-lib=dylib=advapi32");
|
||||
println!("cargo:rustc-link-lib=dylib=gdi32");
|
||||
println!("cargo:rustc-link-lib=dylib=shell32");
|
||||
println!("cargo:rustc-link-lib=dylib=comdlg32");
|
||||
```
|
||||
|
||||
**После этого всё собралось и заработало.** Итого, чтобы получить рабочий
|
||||
OCCT-из-Rust на Windows, потребовались: три настройки окружения, один непринятый
|
||||
PR и четыре строки своей правки.
|
||||
|
||||
| Что | Значение |
|
||||
|---|---|
|
||||
| холодная сборка до провала (без патчей) | 36 мин 09 с |
|
||||
| пересборка после патчей (ядро уже собрано) | 1 мин 45 с |
|
||||
| исходники OCCT (`occt-sys` 7.8.1) | 87 МБ, 14 984 файла; архив 13,7 МБ |
|
||||
| размер `target/` | ~2,7 ГБ |
|
||||
| исполняемый файл зонда | **19,4 МБ** (против 1,60 МБ у truck) |
|
||||
|
||||
Работа зонда: куб 10×10×10 минус куб 6×6×6, фаска 0,5, тесселяция, три экспорта
|
||||
и чтение STEP обратно.
|
||||
|
||||
| Операция | Время |
|
||||
|---|---|
|
||||
| два примитива | 503 мкс |
|
||||
| булево вычитание | 7,46 мс |
|
||||
| фаска по всем рёбрам | 20,2 мс |
|
||||
| тесселяция | 12,1 мс, 174 вершины / 86 треугольников |
|
||||
| экспорт STEP | 25,4 мс, 110,8 КБ, 2378 сущностей |
|
||||
| экспорт IGES | 14,6 мс, 87,3 КБ |
|
||||
| экспорт STL | 15,2 мс, 22,4 КБ |
|
||||
| чтение STEP обратно | 48,4 мс, те же 174 вершины |
|
||||
| всего | 145 мс |
|
||||
|
||||
### Прямое сравнение на одной задаче
|
||||
|
||||
Чтобы сравнение было честным, обоим ядрам дана одна и та же работа: цилиндр
|
||||
радиуса 2 высоты 4, пересечение и объединение с кубом 1,5, затем запись цилиндра
|
||||
в STEP.
|
||||
|
||||
| | truck | OCCT |
|
||||
|---|---|---|
|
||||
| цилиндр ∩ куб | 32,6 мс | **7,2 мс** |
|
||||
| цилиндр ∪ куб | 29,5 мс | **4,7 мс** |
|
||||
| тесселяция результата | 84 вершины | 38 вершин |
|
||||
| STEP цилиндра, размер | 13 281 байт | **5 652 байта** |
|
||||
| боковая поверхность в STEP | 9 × `B_SPLINE_SURFACE` | **1 × `CYLINDRICAL_SURFACE`** |
|
||||
|
||||
Последняя строка — самая содержательная во всём документе. Там, где OCCT пишет
|
||||
одну аналитическую цилиндрическую поверхность, truck пишет девять сплайновых
|
||||
заплаток: файл вдвое толще, а принимающая система видит не цилиндр, а его
|
||||
аппроксимацию. Булевы операции при этом у OCCT в 4–6 раз быстрее.
|
||||
|
||||
Мелкая, но заметная деталь эксплуатации: OCCT при записи STEP печатает в stdout
|
||||
собственную статистику трансфера («Statistics on Transfer», рамки из звёздочек).
|
||||
В библиотеке, встраиваемой в приложение с интерфейсом, этот вывод придётся глушить.
|
||||
|
||||
---
|
||||
|
||||
## Рекомендация
|
||||
|
||||
**Основной вариант — Open CASCADE Technology, из Rust через `opencascade-rs`.**
|
||||
|
||||
Причины, по порядку веса:
|
||||
|
||||
1. **Это единственное открытое ядро, которое закрывает требования 3, 4 и 5
|
||||
целиком.** Всё остальное открытое либо не умеет моделировать (openNURBS, curvo,
|
||||
Manifold), либо умеет плохо и под GPL (SolveSpace), либо закрыто (Fornjot).
|
||||
2. **Требование 2 выполняется без написания биндингов.** `opencascade-rs` даёт
|
||||
готовый Rust-API с булевыми, фасками, тесселяцией и обменом форматами.
|
||||
3. **Требование 5 закрывается из коробки:** STEP AP242 и IGES на чтение и запись —
|
||||
это то, чем САПР обменивается с внешним миром, и ни truck, ни полигональные
|
||||
библиотеки этого в таком объёме не дают.
|
||||
4. Зрелость: ядро развивается с 1990-х; на нём построены FreeCAD (его B-rep/NURBS
|
||||
слой — это OCCT), CadQuery и 3D-часть KiCad.
|
||||
|
||||
Цена решения, названная честно и **измеренная**: ядро тяжёлое (87 МБ исходников,
|
||||
2,7 ГБ сборочного каталога, 19,4 МБ исполняемый файл против 1,6 МБ у truck), это
|
||||
C++ с версией 7.8.1 в текущей обвязке, а сама обвязка **на Windows из коробки не
|
||||
собирается**: понадобились три настройки окружения, патч из непринятого PR #230 и
|
||||
четыре строки правки `build.rs`. Плюс лицензия, обязывающая к динамической линковке
|
||||
при проприетарной поставке — а динамический путь требует системной OCCT ветки 7.x.
|
||||
|
||||
Это не аргумент против выбора, но это работа, которую придётся сделать до того, как
|
||||
будет написана первая строка САПР-логики, и её лучше запланировать явно. Разумный
|
||||
первый шаг после утверждения — **форк `opencascade-rs`** с наложенными исправлениями:
|
||||
иначе каждый разработчик проекта будет проходить этот путь заново, а обновление
|
||||
апстрима будет ломать сборку.
|
||||
|
||||
**Запасной вариант — truck**, и он не запасной «на всякий случай», а по конкретному
|
||||
сценарию: если проект останется целиком на Rust и согласится на собственную
|
||||
реализацию недостающего. truck легче, собирается за полторы минуты, не тянет C++
|
||||
и не создаёт лицензионных обязанностей вовсе (Apache-2.0). Но за это придётся
|
||||
заплатить отсутствием IGES, потерей аналитических поверхностей при экспорте,
|
||||
более скудным набором операций и работой с крейтами двухлетней давности либо
|
||||
git-зависимостью.
|
||||
|
||||
**Отдельно про требование 3.** В задаче эталоном названа Plasticity — она построена
|
||||
на Parasolid, и это надо понимать буквально: уровня её прямого моделирования
|
||||
(push/pull по грани с мгновенной перестройкой тела) ни одно открытое ядро само по
|
||||
себе не даёт. OCCT предоставляет операции, из которых такой режим собирается, но
|
||||
собирать его придётся самим.
|
||||
|
||||
**Про закрытые ядра.** Ни одно из четырёх не даёт бесплатного пути в продукт:
|
||||
C3D — 90 дней, Parasolid и ACIS/CGM — только переговоры, Zoo — поминутная оплата
|
||||
и облако. Возвращаться к ним осмысленно тогда, когда у проекта появится бюджет
|
||||
и станет ясно, что именно в OCCT не устраивает: менять ядро на этапе прототипа
|
||||
дороже, чем позже, когда требования известны точно.
|
||||
|
||||
---
|
||||
|
||||
## Что придётся писать самим поверх любого ядра
|
||||
|
||||
Это не недостаток конкретного кандидата, а общее свойство: ядро даёт геометрию,
|
||||
а не САПР.
|
||||
|
||||
- **Решатель геометрических ограничений.** Ни OCCT, ни truck его не содержат.
|
||||
Без него нет параметрического эскиза. Кандидаты: `libslvs` из SolveSpace
|
||||
(GPL-3.0 — то же лицензионное ограничение, что и у самого SolveSpace),
|
||||
либо собственная реализация.
|
||||
- **Дерево построения и история операций.** Ядро выполняет операции, но не помнит,
|
||||
в каком порядке и от чего они зависели.
|
||||
- **Топологические имена.** Устойчивая идентификация граней и рёбер между
|
||||
перестроениями — классическая трудная задача, ядром не решается.
|
||||
- **Связь с расчётной частью.** Для CFD нужна сетка, а не B-rep: понадобится
|
||||
контролируемая тесселяция и, вероятно, воксельное или SDF-представление,
|
||||
согласованное с решателем KBC.
|
||||
|
||||
---
|
||||
|
||||
## Что нужно от владельца проекта
|
||||
|
||||
1. **Утвердить ядро** — критерий готовности задачи #5 сформулирован именно так.
|
||||
2. **Решить вопрос линковки**, если продукт будет проприетарным: удобный путь
|
||||
(`builtin`) даёт статическую сборку OCCT, а динамический требует предустановленной
|
||||
в системе OCCT ветки 7.8/7.9 — версию 8.x обвязка отвергает. Выбор: принять
|
||||
обязанности LGPL 2.1 §6, держать системную 7.x, либо править обвязку под себя.
|
||||
3. **Согласиться на форк `opencascade-rs`** (или явно отказаться): без него сборка
|
||||
на Windows воспроизводится только вручную, по шагам из раздела измерений.
|
||||
4. Если решено смотреть на закрытые ядра всерьёз — **запросить условия** у C3D Labs
|
||||
(90-дневная пробная плюс индивидуальные условия) и у Siemens по Parasolid.
|
||||
Регистрация и подписание — действия владельца, агентом они не выполняются.
|
||||
|
||||
---
|
||||
|
||||
## Источники
|
||||
|
||||
- [Open CASCADE Technology — обзор и форматы](https://occt3d.com/dev/doc/overview/html/index.html)
|
||||
- [FOSDEM 2026: OCCT3D 8.0 — ломающие изменения и развитие ядра](https://archive.fosdem.org/2026/schedule/event/QQRAAF-occt3d-8-kernel-evolution/)
|
||||
- [OCCT_LGPL_EXCEPTION.txt — текст исключения](https://raw.githubusercontent.com/Open-Cascade-SAS/OCCT/master/OCCT_LGPL_EXCEPTION.txt)
|
||||
- [Релизы OCCT (8.0.1, 30.07.2026)](https://github.com/Open-Cascade-SAS/OCCT/releases)
|
||||
- [opencascade-rs](https://github.com/bschwind/opencascade-rs)
|
||||
- [opencascade-rs PR #230 «Fix MSVC handle name collisions» — закрыт, не влит](https://github.com/bschwind/opencascade-rs/pull/230)
|
||||
- [opencascade-rs PR #216 «Fix Windows build: …missing system libraries» — открыт](https://github.com/bschwind/opencascade-rs/pull/216)
|
||||
- [truck — shape processing kernel by Rust](https://github.com/ricosjp/truck)
|
||||
- [Fornjot — архив, заявление о закрытии](https://github.com/hannobraun/fornjot)
|
||||
- [SolveSpace — использование как библиотеки](https://solvespace.com/library.pl)
|
||||
- [openNURBS](https://github.com/mcneel/opennurbs)
|
||||
- [Manifold](https://github.com/elalish/manifold)
|
||||
- [curvo](https://github.com/mattatz/curvo)
|
||||
- [C3D Toolkit — лицензирование](https://c3dlabs.ru/products/c3d-toolkit/licensing/)
|
||||
- [C3D Toolkit — тестовая версия](https://c3dlabs.ru/evaluation/)
|
||||
- [Zoo Design API](https://zoo.dev/design-api)
|
||||
- [Zoo — тарифы API](https://zoo.dev/api-pricing)
|
||||
- [kittycad.rs — Rust-клиент Zoo](https://github.com/KittyCAD/kittycad.rs)
|
||||
@@ -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
|
||||