diff --git a/docs/theory/2d_solver/README.md b/docs/theory/2d_solver/README.md index a0ddec1..f4b30a3 100644 --- a/docs/theory/2d_solver/README.md +++ b/docs/theory/2d_solver/README.md @@ -311,8 +311,11 @@ c_s²(1/(γβ) − ½) и, поскольку измеренная ⟨γ⟩ ≈ низкими числами Рейнольдса, поскольку на границе возникают паразитные скачки») и естественная форма для подвижных стенок: скорость стенки входит в целевые значения, а не отдельной поправкой. В здешней канальной постановке преимущества по устойчивости воспроизвести не удалось: при росте -Re обе модели теряют счёт на одном и том же значении (Re ≈ 5·10⁴ при теле в 16 клеток), то есть -ограничивает не стенка, а что-то другое — вероятнее всего Zou–He при τ → ½. +Re обе модели теряют счёт на одном и том же значении, то есть ограничивает не стенка, а +что-то другое — вероятнее всего Zou–He при τ → ½. (Сама оценка «Re ≈ 5·10⁴ при теле в 16 +клеток» была сделана по коротким прогонам; длинные дают предел τ − ½ ≈ 6·10⁻⁴, то есть при +D = 16 уже Re ≈ 4000, — см. раздел «Дальше». Вывод «стенка не ограничивает» он не меняет: +Bouzidi и HRR со Spalding разваливаются при одних и тех же Re.) **Умолчание — `hrr`, и это решение временное.** Оно принято по устройству схемы (третий порядок Эрмита фильтрует высокочастотный мусор у стенки, чего обрыв на Π не делает), а не по здешним @@ -326,6 +329,59 @@ Re обе модели теряют счёт на одном и том же зн это только потому, что две модели дали побитово одинаковый результат там, где обязаны были разойтись. +### Пристеночная функция Spalding + +Ключ `--wall-function none|spalding` (умолчание `none` — всё прежнее не меняется побитово) +ставит поверх `bouzidi`, `grad` или `hrr` закон стенки Spalding (1961): одна формула на весь +пристеночный слой, от вязкого подслоя (u⁺ = y⁺) до логарифмической зоны, κ = 0.41, B = 5.2. +С `staircase` не сочетается: ступенька не знает, где стенка. + +Как устроено: + +- у каждого граничного узла есть нормаль (градиент SDF), расстояние до стенки y_w и точка + отбора скорости на одну клетку дальше по нормали (Malaspinas, Sagaut 2014); скорость там — + билинейно по полю на момент t. Геометрия строится один раз в `cpu::Geom` и загружается на + GPU как есть; +- по касательной скорости в точке отбора Ньютоном находится u_τ, по нему — модельная скорость + узла u_node = u_τ·u⁺(y⁺_w) и напряжение τ_w = ρu_τ²; +- **`grad`/`hrr`**: касательная составляющая целевой скорости (B 1) заменяется на u_node, а в + тензоре давлений нормальная производная касательной скорости — на u_τ²/ν. Плотность (B 3) и + сборка популяций не меняются; +- **`bouzidi`**: стенка получает скорость скольжения u_s = u_node − u_τ²·y_w/ν и входит в + отскок поправкой подвижной стенки. Скольжение согласовано по **напряжению**: отскок передаёт + стенке ρν(u_node − u_s)/y_w = τ_w. В вязком подслое u⁺ = y⁺, и u_s тождественно ноль; в + буферной и логарифмической зонах u_s < 0 — стенка «отступает», добирая трение до τ_w. + Модуль скольжения зажат скоростью в точке отбора. + +Первая версия согласовывала скольжение по **скорости** (прямая через стенку, узел и точку +отбора). В решателе без турбулентной вязкости это даёт большое положительное скольжение — +стенка становится почти свободной: цилиндр Re = 10⁴, D = 16 выдал Cd = 0.016. Вторая брала +разрешённую скорость узла вместо модельной и при Re = 20 расходилась с прилипанием на 2 %: +у искривлённой стенки узел не лежит на прямой от стенки к точке отбора. Нынешняя формула +свободна от обоих изъянов. + +**Сведение к прилипанию при малом y⁺** (цилиндр Re = 20, D = 16, 320×192, y⁺ ≈ 0.25, GPU): + +| модель | прилипание | Spalding | разница | +|---|---|---|---| +| `bouzidi` | 2.38252 | 2.38252 | 0 (до 7-го знака) | +| `grad` | 2.38774 | 2.38989 | +0.09 % | +| `hrr` | 2.38835 | 2.39053 | +0.09 % | + +У моментных схем остаточные 0.09 % — от того, что целевая скорость строится теперь по нормали +к стенке, а не усреднением по линкам (B 1). + +**Закон стенки описывает турбулентный слой, а при умеренных Re он ламинарный.** На цилиндре при +Re = 2000 (D = 16, y⁺ ≈ 5, местами до 19, 16 тыс. шагов) функция заметно двигает только Bouzidi: +Cd 2.10 против 1.56 без неё; у `grad` 1.59 против 1.65, у `hrr` 1.70 против 1.69. Bouzidi +навязывает модельное трение через обмен импульсом жёстко, моментные схемы — через тензор +давлений одного слоя узлов, мягче. Какая схема ближе к эталону — меряет группа J кампании. + +Каждый прогон канала пишет в сводку y⁺ первого узла на самом тонком уровне (`yplus_mean`, +`yplus_max`, долю узлов с y⁺ > 30 `yplus_frac_log`, `u_tau_mean`) — два десятка снимков во +второй половине прогона, с функцией и без неё. Без y⁺ сравнение схем стенки не +интерпретировать. + ### Паритет бэкендов и согласованность уровней CPU (f64) и GPU (f32) на одной постановке совпадают до 4–5 значащих цифр шаг в шаг: ⟨ρ⟩ @@ -335,6 +391,18 @@ CPU (f64) и GPU (f32) на одной постановке совпадают Один и тот же случай с патчем ×2 и вовсе без измельчения (`--refine 1`) даёт St 0.1951 против 0.1970 и ⟨Cd⟩ 1.956 против 1.943 — связка уровней систематики не вносит. +**Исправлено: сила на GPU при `--refine 3` и выше.** Рабочие слоты силы в буфере итогов были +рассчитаны ровно на два подшага тонкого уровня; сила третьего и следующих писалась за край +буфера и терялась, а делилась сумма всё равно на r. Cd на GPU выходил занижен в r/2 раза: +два тела при `--refine 3` — 1.316 против 1.972 на CPU. Теперь слотов столько, сколько подшагов +самого тонкого уровня на шаг L0: 1.9733 против 1.9716. Это затрагивало прогон `F09_refine3` +кампании и любой ручной запуск с `--refine ≥ 3` на GPU; при `--refine 2` и без патча +результат прежний побитово. + +`bench/parity.py` сверяет CPU и GPU на четырёх случаях: Тейлор–Грин, стационарный цилиндр, +цилиндр с `grad` + Spalding и три уровня ×2×2. Замерено на Iris Xe (Vulkan): Cd совпадает до +1.7·10⁻⁵ (Spalding) и 7.3·10⁻⁵ (три уровня), y⁺ — до 6.4·10⁻⁵. + **Предел на размер сетки снят.** У GPU есть жёсткий предел `maxComputeWorkgroupsPerDimension` = 65535, а диспетчеризация была одномерной: при 64 узлах на рабочую группу это упирало сетку в 4.2 миллиона узлов, то есть примерно 2048×2048. Всё, что крупнее, падало ошибкой валидации — @@ -470,15 +538,56 @@ rms Cl 0.81 вместо 0.40 — то есть 75-процентная прод просто переносится во времени: поток всё равно обязан разогнаться вокруг тела, и энергия этого переходного процесса задана физикой, а не гладкостью начального поля. Умолчание — 0. +## Многоуровневый домен + +`--levels "r:up,down,side; r:up,down,side; …"` строит цепочку вложенных уровней вместо одного +патча: L0 — самый грубый, на весь домен, каждый следующий в r раз тоньше родителя и лежит +внутри него. `up`, `down`, `side` — отступы границ уровня от центра тела в его диаметрах +(вверх по потоку, вниз, в стороны). Так строится внешний домен с грубыми буферами: большой и +дешёвый L0, к телу сетка мельчает ступенями. `--levels` перекрывает `--refine`/`--patch`; +одиночный патч — частный случай цепочки из одного звена, и прежние постановки дают **побитово +тот же** результат на обоих бэкендах (сверено на пяти постановках: без патча, ×2, ×3, два тела, +периодический случай; единственное исключение — исправленная сила при `--refine 3` на GPU). + +```sh +# L0 в 4 раза грубее тела; ступени ×2: 4D вверх, 15D вниз, ±4D, затем 1.5D, 10D следа, ±2D +./target/release/kbc2d --shape cylinder --size 8 --nx 480 --ny 320 --body-x 160 --re 150 \ + --levels "2:4,15,4; 2:1.5,10,2" --wall bouzidi --steps 96000 --backend gpu +``` + +Важно: `--size`, `--nx`, `--ny`, `--steps` и Re задаются **в клетках и шагах L0**. Тело в 8 +клеток L0 при цепочке ×2×2 — это 32 клетки на самом тонком уровне; отчёт печатает D и τ +каждого уровня. τ растёт к тонким уровням (τ_k = r_k(τ_{k−1} − ½) + ½, ν одна на всех), так +что ближе всего к пределу устойчивости τ → ½ именно L0. + +Шаг рекурсивный: уровень делает шаг, затем его потомок — r подшагов с временной интерполяцией +рамки, затем рестрикция обратно; сила снимается на самом тонком уровне на каждом его подшаге. +Стенки канала, вход, выход и губки есть только у L0. На GPU это та же рекурсия в одном +командном буфере; ядра связки не менялись. + +Проверено: однородный поток без тела — неподвижная точка через три уровня с точностью 10⁻¹²; +стационарный цилиндр Re = 20 при одном скачке ×4 и при цепочке ×2×2 с тем же разрешением тела +даёт Cd 2.2104 и 2.2156 (0.24 %); CPU и GPU на трёх уровнях совпадают до 7.3·10⁻⁵ по Cd. + +**Боковые губки** `--sponge-side N` — N рядов L0 у верхней и нижней стенок с той же плавной +надбавкой вязкости, что у входа и выхода; β теперь поле по узлам, а не по столбцам. Нужны, +чтобы отличать отражения от боковых стенок от отражений на стыках уровней. + +В сводку добавлены `levels` (цепочка с размерами), `nodes_per_step` (обновлений узлов за шаг L0 +по всем уровням), `wall_function`, `sponge_side`, `rho_probe_rms` (пульсация плотности в зонде +во второй половине — мера паразитной акустики) и `cl_mean`; в `series.csv` — столбец +`rho_probe`. + ## Валидационная кампания -`bench/` — 115 прогонов на ≈90 машинных часов, разложенных по девяти группам: эталоны +`bench/` — 205 прогонов на ≈101 машинный час, разложенных по одиннадцати группам: эталоны первоисточников, цилиндр против литературы, модели стенки, профили крыла, сложная и множественная геометрия, границы домена, старт и время жизни, внутренние инварианты, -сверхмелкие сетки до 4096×2048. Из них 108 идут на GPU (≈87 часов) и 7 — на CPU (≈3 часа, -исследования сходимости группы A). Каждый прогон кладёт в собственную папку `cmd.txt`, -`log.txt`, `report.txt`, `series.csv` и `summary.json`; гифку на всю свою длительность -пишут **59 прогонов из 115**, остальным `--gif` не передаётся вовсе. +сверхмелкие сетки до 4096×2048, схемы стенки с пристеночной функцией (J) и внешний домен с +грубыми буферами (K). Из них 198 идут на GPU и 7 — на CPU (≈3 часа, исследования сходимости +группы A). Каждый прогон кладёт в собственную папку `cmd.txt`, `log.txt`, `report.txt`, +`series.csv` и `summary.json`; гифку на всю свою длительность пишут **59 прогонов из 205** — +группы J и K гифок не пишут. Итоги J и K сводит `bench/compare.py`. ```sh cd bench @@ -497,7 +606,7 @@ python preflight.py # каждый сценарий старту ## Развёртывание (Docker) -Собранный образ опубликован: **`notbigghost/kbc2d:1.3.0`** (он же `latest`, платформа `linux/amd64`). +Собранный образ опубликован: **`notbigghost/kbc2d:1.4.0`** (он же `latest`, платформа `linux/amd64`). Исходники на сервере не нужны — достаточно перенести туда один файл `docker-compose.server.yml`: @@ -525,7 +634,7 @@ docker run --rm --gpus all kbc2d --calibrate ### Пять долей на пяти машинах -Кампания разложена на **пять равных по времени долей** (≈24 ч каждая, поле `shard` в +Кампания разложена на **пять равных по времени долей** (≈27 ч каждая, поле `shard` в `scenarios.json`), чтобы считать её одновременно на пяти чужих ПК с видеокартой под Windows или WSL2. На машину переносятся два файла — `docker-compose.shards.yml` и `shard.bat` — и выполняется `shard.bat 3` (или `docker compose -f docker-compose.shards.yml up -d shard3`). @@ -618,7 +727,11 @@ GPU не найден. Согласие даётся явно, переменн стартовый импульс от появления тела в потоке; - подвижные и вращающиеся тела: GMEM уже записан в галилей-инвариантной форме и принимает скорость стенки на линке, но подача этой скорости не подключена; -- разбор нерешённых 12% по Cd и предела устойчивости Re ≈ 5·10⁴ — обе задачи вынесены в - кампанию (группы B и G), выводы делать по её данным; +- разбор нерешённых 12% по Cd и предела устойчивости — обе задачи вынесены в кампанию + (группы B и G), выводы делать по её данным. Оценка предела Re ≈ 5·10⁴ при D = 16 была сделана + по коротким прогонам и не подтвердилась: в длинных (цилиндр 20D×12D, Bouzidi) Re = 10⁴ + разваливается при D = 16 и 32 на 13 и 27 тысячах шагов, а держится при τ − ½ ≥ 6·10⁻⁴ + (Re = 2000 при D = 8, 5000 при D = 32, 10⁴ при D = 64); пристеночная функция предел не + сдвигает; - больше четырёх тел в домене: сейчас сила считается по четырём вёдрам (`MAX_BODY_BUCKETS`), геометрия при этом собирается из любого числа тел, но силы сверх четвёртого сливаются вместе. diff --git a/docs/theory/2d_solver/bench/README.md b/docs/theory/2d_solver/bench/README.md index b3fa08d..2e8e291 100644 --- a/docs/theory/2d_solver/bench/README.md +++ b/docs/theory/2d_solver/bench/README.md @@ -1,10 +1,10 @@ # Валидационная кампания -115 прогонов, ≈90 машинных часов: 108 на GPU (≈87 часов на RTX 4070 Ti) и 7 на CPU (≈3 часа — -исследования сходимости группы A, где нужен f64). Проверяет решатель по трём независимым линиям: +205 прогонов в одиннадцати группах, ≈101 машинный час: 198 на GPU (≈98 часов при 1200 MLUPS) +и 7 на CPU (≈3 часа — исследования сходимости группы A, где нужен f64). Проверяет решатель по трём независимым линиям: эталонам из статей авторов метода, литературе по обтеканию тел и внутренним инвариантам самой схемы. Каждый прогон кладёт в собственную папку `cmd.txt`, `log.txt`, `report.txt`, `series.csv` -и `summary.json`; гифку пишут **59 прогонов из 115** — остальным `--gif` не передаётся. +и `summary.json`; гифку пишут **59 прогонов из 205** — остальным `--gif` не передаётся. ## Пять долей: чужие ПК под Windows / WSL2 @@ -44,11 +44,11 @@ Desktop с бэкендом WSL2 (так по умолчанию) либо Docke | доля | прогонов | ≈ часов | |---|---|---| -| 1 | 22 | 24.0 | -| 2 | 23 | 24.0 | -| 3 | 23 | 24.0 | -| 4 | 23 | 24.0 | -| 5 | 24 | 24.0 | +| 1 | 40 | 26.7 | +| 2 | 40 | 26.7 | +| 3 | 41 | 26.7 | +| 4 | 42 | 26.7 | +| 5 | 42 | 26.7 | Часы — оценка при 1200 MLUPS на крупной сетке **с поправкой на размер сетки**: на dzn мелкие сетки считаются медленнее на узел, а сверхмелкие из группы I идут через раздельные привязки @@ -66,7 +66,7 @@ Desktop с бэкендом WSL2 (так по умолчанию) либо Docke ## Быстрый старт на сервере -Образ опубликован, собирать ничего не нужно: **`notbigghost/kbc2d:1.3.0`**. Исходники на +Образ опубликован, собирать ничего не нужно: **`notbigghost/kbc2d:1.4.0`**. Исходники на сервере тоже не нужны — переносится один файл `docker-compose.server.yml`. ```sh @@ -75,7 +75,7 @@ mkdir -p ~/kbc2d && cd ~/kbc2d # сюда же ляжет ./out с ре C=docker-compose.server.yml docker compose -f $C --profile check run --rm vulkan # 1. карта видна из контейнера? -docker compose -f $C --profile check run --rm preflight # 2. все 115 сценариев стартуют? +docker compose -f $C --profile check run --rm preflight # 2. все 205 сценариев стартуют? docker compose -f $C --profile check run --rm calibrate # 3. сколько MLUPS на этой машине? docker compose -f $C --profile check run --rm plan # 4. смета в часах по замеренному docker compose -f $C up -d # 5. кампания @@ -277,6 +277,38 @@ out//xt_*.csv x–t диаграммы (группа G) | **G** | 14 | старт, акустика, время жизни: x–t диаграммы, губки, предел по Re, прогон на 10⁷ шагов | | **H** | 9 | инварианты: симметрия, зеркальность, зависимость от числа Маха, расхождение f32 против f64 | | **I** | 9 | сверхмелкие сетки 4096×2048 по всем формам и композиции из трёх тел | +| **J** | 65 | схемы стенки с пристеночной функцией: Bouzidi без неё против Bouzidi, Grad и HRR со Spalding — цилиндр Re=150 / 2000 (D=8…64) и 5000 (D=32, 64), NACA 0012 α=4° Re=2000 (хорда 24…96) и 10⁴ (48, 96); эталон каждой серии — Bouzidi при удвоенном разрешении | +| **K** | 25 | внешний домен: грубые буферы до тела, после и по бокам, переход разрешения скачком ×4 / ×8 против ступеней ×2, длина тонкого следа, ширина промежуточного уровня, боковые губки — против однородной тонкой сетки на домене 30D+60D, ±30D | + +### Группы J и K: как читать + +Обе группы отвечают на практические вопросы, и обе меряются против эталона внутри группы, а не +против литературы — для турбулентных Re и для домена её просто нет. + +**J** — что точнее: Bouzidi (лучшая геометрия) или моментные схемы с пристеночной функцией +Spalding. Bouzidi + Spalding добавлен, чтобы отделить вклад функции от вклада схемы. Сетка +однородная, разрешение задано числом клеток на тело. Re выбраны с оглядкой на устойчивость: +предел здесь задаёт не модель стенки, а τ − ½ = 3uD/Re — при τ − ½ < 6·10⁻⁴ счёт +разваливается у всех схем одинаково (замерено: цилиндр Re = 10⁴ при D = 16 и 32), и такой +прогон сравнивал бы не схемы, а предел устойчивости. Отсюда Re = 5000 вместо 10⁴ у цилиндра и +хорды от 48 у профиля при Re = 10⁴. В каждой сводке — y⁺ первого узла: при y⁺ < 5 функция +обязана почти совпасть с прилипанием, разница возможна только там, где первая клетка дальше. + +**K** — как строить внешний домен (ключ `--levels` решателя). Тело везде одно: цилиндр с +D = 32 на самом тонком уровне. База — L0 в 4 раза грубее тела, домен 20D до тела, 40D после, +±20D, две ступени ×2. Серии меняют по одному параметру. Кроме St, ⟨Cd⟩ и rms Cl сравнивается +`rho_probe_rms` — пульсация плотности в зонде, мера паразитной акустики от отражений на границах +и стыках уровней, — и стоимость: конфигурации K обходятся в 1–2 % стоимости эталона. + +Сводка обеих групп: + +```sh +python compare.py # Markdown-таблицы против эталонов, читает out/ +python compare.py --out ../all_out --csv jk.csv # по сведённым результатам пяти долей +``` + +Не посчитанный прогон помечается «нет», развалившийся — «развал»; под каждой серией J — какая +схема ближе всех к эталону на каждом разрешении (по |ΔCd| у цилиндра, по |ΔCl| у профиля). ## Стоимость и время @@ -298,6 +330,12 @@ python gen_scenarios.py --scale 3 # все прогоны в полтора группы A идут на CPU — это указано в самих сценариях. - **Развалы ожидаемы** в группе G (предел по Re) и у прогона `A09_shear_n512_lbgk`: LBGK при Re=3·10⁴ обязан развалиться там, где KBC доживает — это и есть проверяемое утверждение. + Длинные прогоны канала показали предел около τ − ½ ≈ 6·10⁻⁴ (τ − ½ = 3uD/Re), а не + Re ≈ 5·10⁴ при D = 16, как оценивалось раньше по коротким прогонам: при этом пределе + `G11_limit_re10000` (τ − ½ = 7.2·10⁻⁴) на грани, а `G12` и `G13` обязаны развалиться. +- **`F09_refine3` до версии 1.4.0 считался на GPU с заниженным в 1.5 раза Cd** — сила третьего + подшага терялась за краем буфера. Исправлено; результаты этого прогона из образов до 1.4.0 + не использовать. - **Гифки** пишутся на всю длительность прогона, в реальном времени, 10 кадр/с. Число кадров этим задано жёстко (у самого длинного прогона их 16 666), поэтому единственный рычаг — размер кадра. Замерено: 0.103 байта на пиксель после LZW; отсюда бюджет diff --git a/docs/theory/2d_solver/bench/compare.py b/docs/theory/2d_solver/bench/compare.py new file mode 100644 index 0000000..b329615 --- /dev/null +++ b/docs/theory/2d_solver/bench/compare.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""Сводные таблицы групп J и K против их эталонов. + + python compare.py # Markdown в консоль, читает out/ + python compare.py --out ../out_all # другой каталог результатов (например, сведённый + # из пяти долей) + python compare.py --csv compare.csv # те же строки в CSV + +Группа J — схемы стенки. Каждая серия (тело, Re) имеет эталон: Bouzidi без пристеночной +функции при удвоенном разрешении. Для каждого прогона печатается отклонение St, ⟨Cd⟩, +rms Cl (у профиля — ещё ⟨Cl⟩) от эталона и y⁺ первого узла; в конце серии — какая схема +ближе к эталону на каждом разрешении. + +Группа K — внешний домен. Эталон каждого Re — однородная тонкая сетка на самом большом +домене (K…_ref). Для каждой конфигурации — отклонения тех же величин, пульсация плотности в +зонде (паразитная акустика) и стоимость (обновлений узлов и фактические секунды). + +Прогон без summary.json (не посчитан) или развалившийся показывается строкой с пометкой — +дыры в таблице видны сразу, а не теряются. +""" + +import argparse +import csv +import json +import os +import re +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) + + +def load(out_dir, scen_path): + runs = json.load(open(scen_path, encoding="utf-8"))["runs"] + res = {} + for r in runs: + p = os.path.join(out_dir, r["id"], "summary.json") + if os.path.exists(p): + try: + res[r["id"]] = json.load(open(p, encoding="utf-8")) + except json.JSONDecodeError: + res[r["id"]] = None + return runs, res + + +def dev(v, ref): + """Относительное отклонение в процентах; None, если сравнивать нечего.""" + if v is None or ref is None or ref == 0: + return None + return 100.0 * (v - ref) / abs(ref) + + +def fmt(v, spec="{:+.2f}"): + return "—" if v is None else spec.format(v) + + +def status(s): + if s is None: + return "нет" + return "развал" if s.get("blew_up") else "ok" + + +# ── J ──────────────────────────────────────────────────────────────────────── + +J_RE = re.compile(r"^J\d+_(cyl|naca)_re(\d+)_(?:(?:d|c)(\d+)_(\w+)|ref_(?:d|c)(\d+))$") + + +def table_j(runs, res): + series = {} + for r in runs: + m = J_RE.match(r["id"]) + if not m: + continue + body, re_, n, scheme, nref = m.groups() + key = (body, int(re_)) + s = series.setdefault(key, {"ref": None, "rows": []}) + if nref: + s["ref"] = r["id"] + else: + s["rows"].append((int(n), scheme, r["id"])) + + rows, lines = [], [] + for (body, re_), s in sorted(series.items()): + ref = res.get(s["ref"]) if s["ref"] else None + name = "цилиндр" if body == "cyl" else "NACA 0012, α=4°" + unit = "D" if body == "cyl" else "хорда" + lines.append(f"\n### J: {name}, Re = {re_}\n") + lines.append(f"Эталон `{s['ref']}`: " + ( + "не посчитан" if ref is None else + f"St {fmt(ref.get('strouhal'), '{:.4f}')}, Cd {fmt(ref.get('cd'), '{:.4f}')}, " + f"Cl {fmt(ref.get('cl_mean'), '{:.4f}')}, rms Cl {fmt(ref.get('cl_rms'), '{:.4f}')}")) + lines.append("") + lines.append(f"| {unit} | схема | статус | ΔSt % | ΔCd % | ΔCl % | Δrms Cl % | y⁺ ср | y⁺ макс |") + lines.append("|---|---|---|---|---|---|---|---|---|") + best = {} + for n, scheme, rid in sorted(s["rows"]): + v = res.get(rid) + ok = v is not None and not v.get("blew_up") + g = (lambda k: v.get(k)) if ok else (lambda k: None) + rf = (lambda k: ref.get(k)) if ref else (lambda k: None) + d_st, d_cd = dev(g("strouhal"), rf("strouhal")), dev(g("cd"), rf("cd")) + d_cl, d_clr = dev(g("cl_mean"), rf("cl_mean")), dev(g("cl_rms"), rf("cl_rms")) + lines.append( + f"| {n} | {scheme} | {status(v)} | {fmt(d_st)} | {fmt(d_cd)} | " + f"{fmt(d_cl if body == 'naca' else None)} | {fmt(d_clr)} | " + f"{fmt(g('yplus_mean'), '{:.2f}')} | {fmt(g('yplus_max'), '{:.1f}')} |") + rows.append({"group": "J", "body": body, "re": re_, "n": n, "scheme": scheme, + "id": rid, "status": status(v), "d_st": d_st, "d_cd": d_cd, + "d_cl": d_cl, "d_cl_rms": d_clr, "yplus_mean": g("yplus_mean"), + "yplus_max": g("yplus_max")}) + # для профиля главное — подъёмная сила, для цилиндра — сопротивление + score = d_cl if body == "naca" else d_cd + if score is not None: + b = best.get(n) + if b is None or abs(score) < abs(b[1]): + best[n] = (scheme, score) + if best: + what = "|ΔCl|" if body == "naca" else "|ΔCd|" + lines.append("") + lines.append(f"Ближе всех к эталону по {what}: " + "; ".join( + f"{unit} {n} — **{sc}** ({sv:+.2f} %)" for n, (sc, sv) in sorted(best.items()))) + return rows, lines + + +# ── K ──────────────────────────────────────────────────────────────────────── + +K_RE = re.compile(r"^K\d+_re(\d+)_(\w+)$") + + +def table_k(runs, res): + by_re = {} + meta = {r["id"]: r for r in runs} + for r in runs: + m = K_RE.match(r["id"]) + if m: + by_re.setdefault(int(m.group(1)), []).append((m.group(2), r["id"])) + + rows, lines = [], [] + for re_, items in sorted(by_re.items()): + ref_id = next((rid for tag, rid in items if tag == "ref"), None) + ref = res.get(ref_id) if ref_id else None + lines.append(f"\n### K: внешний домен, Re = {re_}\n") + lines.append(f"Эталон `{ref_id}`: " + ( + "не посчитан" if ref is None else + f"St {fmt(ref.get('strouhal'), '{:.4f}')}, Cd {fmt(ref.get('cd'), '{:.4f}')}, " + f"rms Cl {fmt(ref.get('cl_rms'), '{:.4f}')}, ρ′ {fmt(ref.get('rho_probe_rms'), '{:.2e}')}")) + lines.append("") + lines.append("| конфигурация | статус | ΔSt % | ΔCd % | Δrms Cl % | ρ′ зонда | " + "стоимость, отн. эталона | время, с |") + lines.append("|---|---|---|---|---|---|---|---|") + ref_cost = meta[ref_id]["cost"] if ref_id else None + for tag, rid in items: + if tag == "ref": + continue + v = res.get(rid) + ok = v is not None and not v.get("blew_up") + g = (lambda k: v.get(k)) if ok else (lambda k: None) + rf = (lambda k: ref.get(k)) if ref else (lambda k: None) + d_st, d_cd = dev(g("strouhal"), rf("strouhal")), dev(g("cd"), rf("cd")) + d_clr = dev(g("cl_rms"), rf("cl_rms")) + cost = meta[rid]["cost"] / ref_cost if ref_cost else None + lines.append( + f"| {tag} | {status(v)} | {fmt(d_st)} | {fmt(d_cd)} | {fmt(d_clr)} | " + f"{fmt(g('rho_probe_rms'), '{:.2e}')} | {fmt(cost, '{:.3f}')} | " + f"{fmt(g('wall_time_s'), '{:.0f}')} |") + rows.append({"group": "K", "re": re_, "config": tag, "id": rid, + "status": status(v), "d_st": d_st, "d_cd": d_cd, "d_cl_rms": d_clr, + "rho_probe_rms": g("rho_probe_rms"), "cost_rel": cost, + "wall_time_s": g("wall_time_s")}) + return rows, lines + + +def main(): + ap = argparse.ArgumentParser(description="Сводка групп J и K против эталонов") + ap.add_argument("--out", default=os.path.join(HERE, "out"), help="каталог результатов") + ap.add_argument("--scenarios", default=os.path.join(HERE, "scenarios.json")) + ap.add_argument("--csv", help="записать строки таблиц в CSV") + args = ap.parse_args() + + runs, res = load(args.out, args.scenarios) + rj, lj = table_j(runs, res) + rk, lk = table_k(runs, res) + print("\n".join(["# Сравнение схем стенки (J) и конфигураций домена (K)"] + lj + lk)) + if args.csv: + keys = sorted({k for row in rj + rk for k in row}) + with open(args.csv, "w", newline="", encoding="utf-8") as f: + w = csv.DictWriter(f, fieldnames=keys) + w.writeheader() + for row in rj + rk: + w.writerow(row) + print(f"\nCSV: {args.csv}", file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/docs/theory/2d_solver/bench/gen_scenarios.py b/docs/theory/2d_solver/bench/gen_scenarios.py index 6ace5f3..409dd3a 100644 --- a/docs/theory/2d_solver/bench/gen_scenarios.py +++ b/docs/theory/2d_solver/bench/gen_scenarios.py @@ -485,6 +485,207 @@ def group_i(scale): nx * ny, st, gif="fine_multi.gif", nx=nx) +# ══════════════════════════════════════════════════════════════════════════════ +# J. Схемы стенки с пристеночной функцией +# ══════════════════════════════════════════════════════════════════════════════ +# Главный практический вопрос: что точнее — Bouzidi (лучшая геометрия, без пристеночной +# функции) или моментные схемы с пристеночной функцией Spalding, и как это меняется с Re и +# разрешением. Bouzidi + Spalding добавлен, чтобы отделить вклад функции от вклада схемы. +# Сетка однородная (--refine 1): разрешение задаётся прямо числом клеток на тело, без +# вклада связки уровней. Эталон каждой серии — Bouzidi без функции при удвоенном разрешении. +J_SCHEMES = [ + ("bouzidi", "none"), + ("bouzidi", "spalding"), + ("grad", "spalding"), + ("hrr", "spalding"), +] + + +def jid(): + return f"J{len([r for r in RUNS if r['group'] == 'J']) + 1:02d}" + + +def wf_tag(wall, wf): + return wall if wf == "none" else f"{wall}_wf" + + +# Разрешения по Re. Предел устойчивости здесь задаёт не модель стенки, а τ − ½ = 3·u·D/Re на +# грубейшем уровне: замерено на цилиндре 20D×12D (обычный Bouzidi, длинные прогоны), что при +# τ − ½ = 2.4·10⁻⁴ и 4.8·10⁻⁴ (Re = 10⁴, D = 16 и 32) счёт разваливается — на 13 и 27 тысячах +# шагов, у HRR со Spalding так же, — а при 6·10⁻⁴ и выше (Re = 2000 при D = 8, Re = 5000 при +# D = 32, Re = 10⁴ при D = 64) держится. Поэтому большой Re цилиндра — 5000, а разрешения +# выбраны так, чтобы τ − ½ ≥ 6·10⁻⁴. Прогоны ниже предела схемы бы не сравнивали, а лишь +# повторяли предел устойчивости, который меряет группа G. +J_CYL_D = {150: (8, 16, 32, 64), 2000: (8, 16, 32, 64), 5000: (32, 64)} +J_FOIL_C = {2000: (24, 48, 96), 10000: (48, 96)} + + +def group_j(scale): + # цилиндр: домен 20D × 12D, как сходимость по D в группе B + for re in (150, 2000, 5000): + for d in J_CYL_D[re]: + nx, ny = 20 * d, 12 * d + st = int(conv_steps(d, 300) * scale) + for wall, wf in J_SCHEMES: + add(f"{jid()}_cyl_re{re}_d{d}_{wf_tag(wall, wf)}", "J", + f"Цилиндр Re={re}, D={d}, стенка {wall}, пристеночная функция {wf}", + "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + cyl_args(d, re, nx, ny, wall=wall, wall_function=wf), nx * ny, st) + d = 128 + nx, ny = 20 * d, 12 * d + add(f"{jid()}_cyl_re{re}_ref_d{d}", "J", + f"Цилиндр Re={re}, ЭТАЛОН серии: Bouzidi без функции, D={d}", + "сеточно сошедшееся значение, к которому меряются схемы при D=8…64", + cyl_args(d, re, nx, ny, wall="bouzidi", wall_function="none"), nx * ny, + int(conv_steps(d, 300) * scale)) + + # профиль: NACA 0012 под углом 4°, домен 12c × 6c + for re in (2000, 10000): + for c in J_FOIL_C[re]: + nx, ny = 12 * c, 6 * c + st = int(conv_steps(c, 200) * scale) + for wall, wf in J_SCHEMES: + add(f"{jid()}_naca_re{re}_c{c}_{wf_tag(wall, wf)}", "J", + f"NACA 0012, α=4°, Re={re}, хорда {c}, стенка {wall}, функция {wf}", + "отклонение Cl, Cd от эталона серии (Bouzidi при хорде 192); y⁺ в сводке", + foil_args(c, 4, re, nx, ny, wall=wall, wall_function=wf), nx * ny, st) + c = 192 + nx, ny = 12 * c, 6 * c + add(f"{jid()}_naca_re{re}_ref_c{c}", "J", + f"NACA 0012, α=4°, Re={re}, ЭТАЛОН серии: Bouzidi без функции, хорда {c}", + "сеточно сошедшееся значение для серии по хорде", + foil_args(c, 4, re, nx, ny, wall="bouzidi", wall_function="none"), nx * ny, + int(conv_steps(c, 200) * scale)) + + +# ══════════════════════════════════════════════════════════════════════════════ +# K. Внешний домен: грубые буферы, переходы разрешения, губки +# ══════════════════════════════════════════════════════════════════════════════ +# Тело одно и то же во всех прогонах — цилиндр с D = 32 клетки на САМОМ ТОНКОМ уровне. +# Меняется только то, что вокруг: размеры домена до тела, после и по бокам; как сетка +# грубеет к краю (одним скачком или ступенями ×2); сколько тонкой сетки остаётся в следе; +# есть ли губки на боковых кромках. Эталон — однородная тонкая сетка на самом большом +# домене. Метрики: St, ⟨Cd⟩, rms Cl против эталона, пульсация плотности в зонде +# (rho_probe_rms — паразитная акустика) и стоимость. +K_D = 32 # клеток на тело на самом тонком уровне +K_CONV = 300 # конвективных времён D/U до умножения на scale + + +def _rround(x): + """Округление как у Rust f64::round — половина от нуля, а не к чётному.""" + return math.floor(x + 0.5) if x >= 0 else -math.floor(-x + 0.5) + + +def level_chain(levels, cx, cy, d, nx, ny): + """Тот же расчёт границ уровней, что `parse_levels` в main.rs. Возвращает обновлений + узлов за шаг L0 по всем уровням и строку для --levels.""" + pnx, pny, scale, ox, oy = nx, ny, 1.0, 0.0, 0.0 + total, sub = nx * ny, 1 + for r, up, down, side in levels: + def to(x, o): + return _rround((x - o) * scale) + ax = int(max(to(cx - up * d, ox), 2.0)) + bx = int(max(min(to(cx + down * d, ox), pnx - 2.0), 0.0)) + ay = int(max(to(cy - side * d, oy), 1.0)) + by = int(max(min(to(cy + side * d, oy), pny - 2.0), 0.0)) + assert ax + 1 < bx and ay + 1 < by, (levels, ax, bx, ay, by) + ox += ax / scale + oy += ay / scale + scale *= r + pnx, pny = r * (bx - ax) + 1, r * (by - ay) + 1 + sub *= r + total += sub * pnx * pny + spec = "; ".join(f"{r}:{up:g},{down:g},{side:g}" for r, up, down, side in levels) + return total, spec + + +def kid(): + return f"K{len([r for r in RUNS if r['group'] == 'K']) + 1:02d}" + + +def k_run(tag, title, expect, re, scale, up, down, side, levels, sponge_side_d=0.0): + """Прогон группы K. `up`, `down`, `side` — размеры домена L0 в диаметрах; `levels` — + цепочка [(r, up, down, side), …] от внешнего уровня к внутреннему, отступы в D. + Пустая цепочка — однородная тонкая сетка.""" + s = 1 + for lv in levels: + s *= lv[0] + d0 = K_D / s # тело в клетках L0 + nx = int(round((up + down) * d0)) + ny = int(round(2 * side * d0)) + cx, cy = up * d0, ny / 2 + args = {"--shape": "cylinder", "--size": f"{d0:g}", "--nx": nx, "--ny": ny, + "--body-x": f"{cx:g}", "--re": re, "--wall": "bouzidi"} + if levels: + nodes, spec = level_chain(levels, cx, cy, d0, nx, ny) + args["--levels"] = spec + else: + nodes = nx * ny + args["--refine"] = 1 + if sponge_side_d > 0: + args["--sponge-side"] = int(round(sponge_side_d * d0)) + # шагов L0 на одно и то же физическое время: тело на L0 в s раз мельче + st = int(conv_steps(d0, K_CONV) * scale) + add(f"{kid()}_re{re}_{tag}", "K", title, expect, flatten(args), nodes, st) + + +# Базовая конфигурация: L0 в 4 раза грубее тела, к телу две ступени ×2. Промежуточный +# уровень — 4D вверх, 15D вниз, 4D в стороны; тонкий — 1.5D вверх, 10D следа, 2D в стороны. +K_MID = (2, 4, 15, 4) + + +def k_fine(wake=10): + return (2, 1.5, wake, 2) + + +def group_k(scale): + for re in (150, 2000): + k_run("ref", f"Re={re}, ЭТАЛОН: однородная тонкая сетка 30D+60D, ±30D", + "значения, к которым меряются все конфигурации буферов", re, scale, + 30, 60, 30, []) + k_run("base", f"Re={re}, база: домен 20D+40D, ±20D, ступени ×2×2", + "отклонение от эталона при умеренных буферах", re, scale, + 20, 40, 20, [K_MID, k_fine()]) + # переход: одно и то же суммарное огрубление — скачком или ступенями + k_run("jump4", f"Re={re}, огрубление ×4 одним скачком", + "скачок разрешения против ступеней ×2×2 (база)", re, scale, + 20, 40, 20, [(4, 1.5, 10, 2)]) + k_run("jump8", f"Re={re}, огрубление ×8 одним скачком", + "скачок ×8 против цепочки ×2×2×2", re, scale, + 20, 40, 20, [(8, 1.5, 10, 2)]) + k_run("chain8", f"Re={re}, огрубление ×8 цепочкой ×2×2×2", + "плавный переход при сильном огрублении", re, scale, + 20, 40, 20, [(2, 8, 25, 8), K_MID, k_fine()]) + k_run("sponge5", f"Re={re}, боковые губки 5D", + "гасят ли губки отражения от боковых стенок", re, scale, + 20, 40, 20, [K_MID, k_fine()], sponge_side_d=5) + if re != 150: + continue + # остальные серии — только Re=150: там есть литература и чистая дорожка + for side in (5, 10, 30): + k_run(f"side{side}", f"Re={re}, боковой буфер ±{side}D", + "с какого размера бока перестают влиять (блокировка + отражения)", re, scale, + 20, 40, side, [K_MID, k_fine()]) + for up in (5, 10, 30): + k_run(f"up{up}", f"Re={re}, буфер до тела {up}D", + "с какого отступа вход перестаёт влиять", re, scale, + up, 40, 20, [(2, min(4, up - 1), 15, 4), k_fine()]) + for down in (15, 30, 60): + k_run(f"down{down}", f"Re={re}, буфер после тела {down}D", + "с какой длины выход перестаёт влиять", re, scale, + 20, down, 20, [(2, 4, min(15, down - 2), 4), k_fine(min(10, down - 4))]) + for wake in (5, 20): + k_run(f"wake{wake}", f"Re={re}, тонкий след {wake}D за телом", + "сколько тонкой сетки нужно в следе до первого огрубления", re, scale, + 20, 40, 20, [(2, 4, max(15, wake + 5), 4), k_fine(wake)]) + k_run("midwide", f"Re={re}, широкий промежуточный уровень (8D вверх, 25D вниз, 8D вбок)", + "влияет ли ширина ступени перехода", re, scale, + 20, 40, 20, [(2, 8, 25, 8), k_fine()]) + k_run("sponge2", f"Re={re}, боковые губки 2D", + "ширина губки: 2D против 5D", re, scale, + 20, 40, 20, [K_MID, k_fine()], sponge_side_d=2) + + def est_hours(r): """Оценка времени прогона в часах для раскладки по долям (см. DZN_MLUPS).""" if r["backend"] == "cpu": @@ -526,7 +727,8 @@ def main(): ap.add_argument("--out", default=os.path.join(os.path.dirname(__file__), "scenarios.json")) args = ap.parse_args() - for g in (group_a, group_b, group_c, group_d, group_e, group_f, group_g, group_h, group_i): + for g in (group_a, group_b, group_c, group_d, group_e, group_f, group_g, group_h, group_i, + group_j, group_k): g(args.scale) shard_hours = assign_shards(RUNS, args.shards) diff --git a/docs/theory/2d_solver/bench/parity.py b/docs/theory/2d_solver/bench/parity.py index 83a2f58..2777af2 100644 --- a/docs/theory/2d_solver/bench/parity.py +++ b/docs/theory/2d_solver/bench/parity.py @@ -79,6 +79,26 @@ CASES = [ "degenerate_frac": 5.0}, "report": ["strouhal"], }, + # Пристеночная функция переписана в WGSL отдельно от math.rs (закон Spalding, Ньютон в + # f32), поэтому сверяется отдельно. При Re=20 y⁺ < 1 и функция почти не меняет + # результат — проверяется именно совпадение двух реализаций, а не физика. + { + "name": "цилиндр Re=20, Град + Spalding, 8000 шагов", + "args": ["--shape", "cylinder", "--size", "16", "--nx", "320", "--ny", "192", + "--re", "20", "--refine", "1", "--steps", "8000", + "--wall", "grad", "--wall-function", "spalding"], + "fields": {"cd": 1.0, "rho_mean": 0.05, "yplus_mean": 1.0}, + "report": ["strouhal"], + }, + # Три уровня: рекурсивная связка уровней и сквозная нумерация подшагов силы на GPU. + { + "name": "цилиндр Re=20, три уровня ×2×2, 3000 шагов", + "args": ["--shape", "cylinder", "--size", "8", "--nx", "240", "--ny", "160", + "--re", "20", "--levels", "2:4,10,4; 2:1.5,4,1.5", "--wall", "bouzidi", + "--steps", "3000"], + "fields": {"cd": 1.0, "rho_mean": 0.05}, + "report": ["strouhal"], + }, ] diff --git a/docs/theory/2d_solver/bench/scenarios.json b/docs/theory/2d_solver/bench/scenarios.json index 42f637a..2c4b581 100644 --- a/docs/theory/2d_solver/bench/scenarios.json +++ b/docs/theory/2d_solver/bench/scenarios.json @@ -7,11 +7,11 @@ "assumed_cpu_mlups": 22, "shards": 5, "shard_hours": [ - 24.0, - 24.0, - 24.0, - 24.0, - 24.0 + 26.71, + 26.71, + 26.71, + 26.71, + 26.71 ] }, "runs": [ @@ -85,7 +85,7 @@ "outputs": [ "case_csv" ], - "shard": 5 + "shard": 4 }, { "id": "A03_tg_diff_n256", @@ -157,7 +157,7 @@ "outputs": [ "case_csv" ], - "shard": 1 + "shard": 5 }, { "id": "A05_tg_fixed_n128", @@ -193,7 +193,7 @@ "outputs": [ "case_csv" ], - "shard": 5 + "shard": 4 }, { "id": "A06_tg_fixed_n256", @@ -229,7 +229,7 @@ "outputs": [ "case_csv" ], - "shard": 5 + "shard": 1 }, { "id": "A07_shear_n512_kbc_n1", @@ -283,7 +283,7 @@ "outputs": [ "case_csv" ], - "shard": 1 + "shard": 5 }, { "id": "A08_shear_n512_kbc_n2", @@ -443,7 +443,7 @@ "outputs": [ "case_csv" ], - "shard": 3 + "shard": 2 }, { "id": "A11_turb_n2048_bgk", @@ -495,7 +495,7 @@ "outputs": [ "case_csv" ], - "shard": 5 + "shard": 1 }, { "id": "A12_turb_n4096_kbc", @@ -547,7 +547,7 @@ "outputs": [ "case_csv" ], - "shard": 4 + "shard": 2 }, { "id": "B01_cyl_re20", @@ -595,7 +595,7 @@ "steps": 115200, "cost": 63700992000, "outputs": [], - "shard": 3 + "shard": 2 }, { "id": "B02_cyl_re40", @@ -691,7 +691,7 @@ "steps": 3840000, "cost": 3774873600000, "outputs": [], - "shard": 1 + "shard": 3 }, { "id": "B04_cyl_re150", @@ -739,7 +739,7 @@ "steps": 3840000, "cost": 3774873600000, "outputs": [], - "shard": 5 + "shard": 4 }, { "id": "B05_cyl_re200", @@ -883,7 +883,7 @@ "steps": 512000, "cost": 31457280000, "outputs": [], - "shard": 3 + "shard": 2 }, { "id": "B08_cyl_d32", @@ -979,7 +979,7 @@ "steps": 2048000, "cost": 2013265920000, "outputs": [], - "shard": 3 + "shard": 4 }, { "id": "B10_cyl_d128", @@ -1061,7 +1061,7 @@ "steps": 1920000, "cost": 530841600000, "outputs": [], - "shard": 5 + "shard": 1 }, { "id": "B12_cyl_block8", @@ -1095,7 +1095,7 @@ "steps": 1920000, "cost": 707788800000, "outputs": [], - "shard": 5 + "shard": 2 }, { "id": "B13_cyl_block12", @@ -1129,7 +1129,7 @@ "steps": 1920000, "cost": 1061683200000, "outputs": [], - "shard": 5 + "shard": 2 }, { "id": "B14_cyl_block16", @@ -1281,7 +1281,7 @@ "steps": 512000, "cost": 125829120000, "outputs": [], - "shard": 2 + "shard": 3 }, { "id": "C02_wall_hrr_re2000", @@ -1317,7 +1317,7 @@ "steps": 512000, "cost": 125829120000, "outputs": [], - "shard": 5 + "shard": 2 }, { "id": "C03_wall_grad_re150", @@ -1367,7 +1367,7 @@ "steps": 512000, "cost": 125829120000, "outputs": [], - "shard": 4 + "shard": 3 }, { "id": "C04_wall_grad_re2000", @@ -1403,7 +1403,7 @@ "steps": 512000, "cost": 125829120000, "outputs": [], - "shard": 2 + "shard": 5 }, { "id": "C05_wall_bouzidi_re150", @@ -1453,7 +1453,7 @@ "steps": 512000, "cost": 125829120000, "outputs": [], - "shard": 5 + "shard": 1 }, { "id": "C06_wall_bouzidi_re2000", @@ -1489,7 +1489,7 @@ "steps": 512000, "cost": 125829120000, "outputs": [], - "shard": 1 + "shard": 4 }, { "id": "C07_wall_staircase_re150", @@ -1539,7 +1539,7 @@ "steps": 512000, "cost": 125829120000, "outputs": [], - "shard": 3 + "shard": 2 }, { "id": "C08_wall_staircase_re2000", @@ -1575,7 +1575,7 @@ "steps": 512000, "cost": 125829120000, "outputs": [], - "shard": 4 + "shard": 3 }, { "id": "C09_sub_hrr_00", @@ -1651,7 +1651,7 @@ "steps": 38400, "cost": 2359296000, "outputs": [], - "shard": 4 + "shard": 5 }, { "id": "C11_sub_hrr_50", @@ -1689,7 +1689,7 @@ "steps": 38400, "cost": 2359296000, "outputs": [], - "shard": 2 + "shard": 4 }, { "id": "C12_sub_grad_00", @@ -1727,7 +1727,7 @@ "steps": 38400, "cost": 2359296000, "outputs": [], - "shard": 1 + "shard": 3 }, { "id": "C13_sub_grad_25", @@ -1765,7 +1765,7 @@ "steps": 38400, "cost": 2359296000, "outputs": [], - "shard": 3 + "shard": 5 }, { "id": "C14_sub_grad_50", @@ -1879,7 +1879,7 @@ "steps": 38400, "cost": 2359296000, "outputs": [], - "shard": 5 + "shard": 1 }, { "id": "C17_sub_bouzidi_50", @@ -1917,7 +1917,7 @@ "steps": 38400, "cost": 2359296000, "outputs": [], - "shard": 1 + "shard": 3 }, { "id": "C18_sub_staircase_00", @@ -1955,7 +1955,7 @@ "steps": 38400, "cost": 2359296000, "outputs": [], - "shard": 3 + "shard": 5 }, { "id": "C19_sub_staircase_25", @@ -2083,7 +2083,7 @@ "steps": 2048000, "cost": 4294967296000, "outputs": [], - "shard": 5 + "shard": 3 }, { "id": "D02_naca0012_a04", @@ -2187,7 +2187,7 @@ "steps": 2048000, "cost": 4294967296000, "outputs": [], - "shard": 3 + "shard": 5 }, { "id": "D04_naca0012_a12", @@ -2239,7 +2239,7 @@ "steps": 2048000, "cost": 4294967296000, "outputs": [], - "shard": 5 + "shard": 3 }, { "id": "D05_naca0012_a16", @@ -2291,7 +2291,7 @@ "steps": 2048000, "cost": 4294967296000, "outputs": [], - "shard": 2 + "shard": 4 }, { "id": "D06_naca0012_re500", @@ -2343,7 +2343,7 @@ "steps": 2048000, "cost": 4294967296000, "outputs": [], - "shard": 4 + "shard": 1 }, { "id": "D07_naca0012_re2000", @@ -2395,7 +2395,7 @@ "steps": 2048000, "cost": 4294967296000, "outputs": [], - "shard": 3 + "shard": 2 }, { "id": "D08_naca0012_re10000", @@ -2499,7 +2499,7 @@ "steps": 2048000, "cost": 4294967296000, "outputs": [], - "shard": 2 + "shard": 3 }, { "id": "D10_naca4412_a06", @@ -2603,7 +2603,7 @@ "steps": 2048000, "cost": 4294967296000, "outputs": [], - "shard": 3 + "shard": 1 }, { "id": "D12_naca_chord48", @@ -2641,7 +2641,7 @@ "steps": 576000, "cost": 169869312000, "outputs": [], - "shard": 3 + "shard": 1 }, { "id": "D13_naca_chord96", @@ -2761,7 +2761,7 @@ "steps": 1536000, "cost": 1592524800000, "outputs": [], - "shard": 4 + "shard": 1 }, { "id": "E02_tandem_ld30", @@ -2805,7 +2805,7 @@ "steps": 1536000, "cost": 1592524800000, "outputs": [], - "shard": 2 + "shard": 5 }, { "id": "E03_tandem_ld50", @@ -2849,7 +2849,7 @@ "steps": 1536000, "cost": 1592524800000, "outputs": [], - "shard": 5 + "shard": 3 }, { "id": "E04_array_sparse", @@ -2893,7 +2893,7 @@ "steps": 576000, "cost": 597196800000, "outputs": [], - "shard": 2 + "shard": 4 }, { "id": "E05_array_dense", @@ -2981,7 +2981,7 @@ "steps": 800000, "cost": 576000000000, "outputs": [], - "shard": 1 + "shard": 3 }, { "id": "E07_perf_open50", @@ -3069,7 +3069,7 @@ "steps": 2400000, "cost": 5033164800000, "outputs": [], - "shard": 3 + "shard": 5 }, { "id": "E09_thin_t05", @@ -3169,7 +3169,7 @@ "steps": 1000000, "cost": 720000000000, "outputs": [], - "shard": 4 + "shard": 5 }, { "id": "E11_wedge", @@ -3216,7 +3216,7 @@ "steps": 1200000, "cost": 864000000000, "outputs": [], - "shard": 1 + "shard": 3 }, { "id": "E12_serrated", @@ -3297,7 +3297,7 @@ "steps": 1152000, "cost": 700710912000, "outputs": [], - "shard": 3 + "shard": 4 }, { "id": "F02_inlet6d", @@ -3331,7 +3331,7 @@ "steps": 1152000, "cost": 828112896000, "outputs": [], - "shard": 3 + "shard": 5 }, { "id": "F03_inlet16d", @@ -3365,7 +3365,7 @@ "steps": 1152000, "cost": 1146617856000, "outputs": [], - "shard": 2 + "shard": 4 }, { "id": "F04_outlet5d", @@ -3467,7 +3467,7 @@ "steps": 1152000, "cost": 1465122816000, "outputs": [], - "shard": 4 + "shard": 5 }, { "id": "F07_refine1", @@ -3501,7 +3501,7 @@ "steps": 768000, "cost": 188743680000, "outputs": [], - "shard": 2 + "shard": 5 }, { "id": "F08_refine2", @@ -3535,7 +3535,7 @@ "steps": 768000, "cost": 384935424000, "outputs": [], - "shard": 1 + "shard": 2 }, { "id": "F09_refine3", @@ -3569,7 +3569,7 @@ "steps": 768000, "cost": 849588480000, "outputs": [], - "shard": 2 + "shard": 1 }, { "id": "G01_xt_re150_uniform", @@ -3627,7 +3627,7 @@ "outputs": [ "xt" ], - "shard": 2 + "shard": 3 }, { "id": "G02_xt_re150_rest", @@ -3743,7 +3743,7 @@ "outputs": [ "xt" ], - "shard": 4 + "shard": 2 }, { "id": "G04_xt_re2000_rest", @@ -3801,7 +3801,7 @@ "outputs": [ "xt" ], - "shard": 2 + "shard": 5 }, { "id": "G05_xt_re20000_uniform", @@ -3859,7 +3859,7 @@ "outputs": [ "xt" ], - "shard": 1 + "shard": 4 }, { "id": "G06_xt_re20000_rest", @@ -3917,7 +3917,7 @@ "outputs": [ "xt" ], - "shard": 5 + "shard": 1 }, { "id": "G07_sponge0", @@ -3953,7 +3953,7 @@ "steps": 768000, "cost": 424673280000, "outputs": [], - "shard": 4 + "shard": 2 }, { "id": "G08_sponge40", @@ -3989,7 +3989,7 @@ "steps": 768000, "cost": 424673280000, "outputs": [], - "shard": 1 + "shard": 5 }, { "id": "G09_spongein0", @@ -4061,7 +4061,7 @@ "steps": 768000, "cost": 424673280000, "outputs": [], - "shard": 1 + "shard": 2 }, { "id": "G11_limit_re10000", @@ -4093,7 +4093,7 @@ "steps": 384000, "cost": 212336640000, "outputs": [], - "shard": 3 + "shard": 4 }, { "id": "G12_limit_re50000", @@ -4157,7 +4157,7 @@ "steps": 384000, "cost": 212336640000, "outputs": [], - "shard": 5 + "shard": 4 }, { "id": "G14_lifetime", @@ -4205,7 +4205,7 @@ "steps": 10000000, "cost": 5529600000000, "outputs": [], - "shard": 2 + "shard": 5 }, { "id": "H01_sym_cyl", @@ -4241,7 +4241,7 @@ "steps": 512000, "cost": 125829120000, "outputs": [], - "shard": 2 + "shard": 5 }, { "id": "H02_sym_foil", @@ -4281,7 +4281,7 @@ "steps": 768000, "cost": 402653184000, "outputs": [], - "shard": 5 + "shard": 1 }, { "id": "H03_mirror_plus", @@ -4319,7 +4319,7 @@ "steps": 768000, "cost": 402653184000, "outputs": [], - "shard": 2 + "shard": 3 }, { "id": "H04_mirror_minus", @@ -4357,7 +4357,7 @@ "steps": 768000, "cost": 402653184000, "outputs": [], - "shard": 3 + "shard": 5 }, { "id": "H05_mach02", @@ -4393,7 +4393,7 @@ "steps": 1280000, "cost": 314572800000, "outputs": [], - "shard": 5 + "shard": 1 }, { "id": "H06_mach05", @@ -4429,7 +4429,7 @@ "steps": 512000, "cost": 125829120000, "outputs": [], - "shard": 5 + "shard": 1 }, { "id": "H07_mach10", @@ -4531,7 +4531,7 @@ "steps": 1024000, "cost": 251658240000, "outputs": [], - "shard": 4 + "shard": 2 }, { "id": "I01_fine_cylinder", @@ -4971,6 +4971,3340 @@ "cost": 25769803776000, "outputs": [], "shard": 4 + }, + { + "id": "J01_cyl_re150_d8_bouzidi", + "group": "J", + "title": "Цилиндр Re=150, D=8, стенка bouzidi, пристеночная функция none", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "8", + "--nx", + "160", + "--ny", + "96", + "--body-x", + "32", + "--re", + "150", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "none", + "--steps", + "96000", + "--backend", + "gpu" + ], + "nodes_per_step": 15360, + "steps": 96000, + "cost": 1474560000, + "outputs": [], + "shard": 1 + }, + { + "id": "J02_cyl_re150_d8_bouzidi_wf", + "group": "J", + "title": "Цилиндр Re=150, D=8, стенка bouzidi, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "8", + "--nx", + "160", + "--ny", + "96", + "--body-x", + "32", + "--re", + "150", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "spalding", + "--steps", + "96000", + "--backend", + "gpu" + ], + "nodes_per_step": 15360, + "steps": 96000, + "cost": 1474560000, + "outputs": [], + "shard": 3 + }, + { + "id": "J03_cyl_re150_d8_grad_wf", + "group": "J", + "title": "Цилиндр Re=150, D=8, стенка grad, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "8", + "--nx", + "160", + "--ny", + "96", + "--body-x", + "32", + "--re", + "150", + "--refine", + "1", + "--wall", + "grad", + "--wall-function", + "spalding", + "--steps", + "96000", + "--backend", + "gpu" + ], + "nodes_per_step": 15360, + "steps": 96000, + "cost": 1474560000, + "outputs": [], + "shard": 5 + }, + { + "id": "J04_cyl_re150_d8_hrr_wf", + "group": "J", + "title": "Цилиндр Re=150, D=8, стенка hrr, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "8", + "--nx", + "160", + "--ny", + "96", + "--body-x", + "32", + "--re", + "150", + "--refine", + "1", + "--wall", + "hrr", + "--wall-function", + "spalding", + "--steps", + "96000", + "--backend", + "gpu" + ], + "nodes_per_step": 15360, + "steps": 96000, + "cost": 1474560000, + "outputs": [], + "shard": 4 + }, + { + "id": "J05_cyl_re150_d16_bouzidi", + "group": "J", + "title": "Цилиндр Re=150, D=16, стенка bouzidi, пристеночная функция none", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "16", + "--nx", + "320", + "--ny", + "192", + "--body-x", + "64", + "--re", + "150", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "none", + "--steps", + "192000", + "--backend", + "gpu" + ], + "nodes_per_step": 61440, + "steps": 192000, + "cost": 11796480000, + "outputs": [], + "shard": 4 + }, + { + "id": "J06_cyl_re150_d16_bouzidi_wf", + "group": "J", + "title": "Цилиндр Re=150, D=16, стенка bouzidi, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "16", + "--nx", + "320", + "--ny", + "192", + "--body-x", + "64", + "--re", + "150", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "spalding", + "--steps", + "192000", + "--backend", + "gpu" + ], + "nodes_per_step": 61440, + "steps": 192000, + "cost": 11796480000, + "outputs": [], + "shard": 2 + }, + { + "id": "J07_cyl_re150_d16_grad_wf", + "group": "J", + "title": "Цилиндр Re=150, D=16, стенка grad, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "16", + "--nx", + "320", + "--ny", + "192", + "--body-x", + "64", + "--re", + "150", + "--refine", + "1", + "--wall", + "grad", + "--wall-function", + "spalding", + "--steps", + "192000", + "--backend", + "gpu" + ], + "nodes_per_step": 61440, + "steps": 192000, + "cost": 11796480000, + "outputs": [], + "shard": 1 + }, + { + "id": "J08_cyl_re150_d16_hrr_wf", + "group": "J", + "title": "Цилиндр Re=150, D=16, стенка hrr, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "16", + "--nx", + "320", + "--ny", + "192", + "--body-x", + "64", + "--re", + "150", + "--refine", + "1", + "--wall", + "hrr", + "--wall-function", + "spalding", + "--steps", + "192000", + "--backend", + "gpu" + ], + "nodes_per_step": 61440, + "steps": 192000, + "cost": 11796480000, + "outputs": [], + "shard": 3 + }, + { + "id": "J09_cyl_re150_d32_bouzidi", + "group": "J", + "title": "Цилиндр Re=150, D=32, стенка bouzidi, пристеночная функция none", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "32", + "--nx", + "640", + "--ny", + "384", + "--body-x", + "128", + "--re", + "150", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "none", + "--steps", + "384000", + "--backend", + "gpu" + ], + "nodes_per_step": 245760, + "steps": 384000, + "cost": 94371840000, + "outputs": [], + "shard": 4 + }, + { + "id": "J10_cyl_re150_d32_bouzidi_wf", + "group": "J", + "title": "Цилиндр Re=150, D=32, стенка bouzidi, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "32", + "--nx", + "640", + "--ny", + "384", + "--body-x", + "128", + "--re", + "150", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "spalding", + "--steps", + "384000", + "--backend", + "gpu" + ], + "nodes_per_step": 245760, + "steps": 384000, + "cost": 94371840000, + "outputs": [], + "shard": 2 + }, + { + "id": "J11_cyl_re150_d32_grad_wf", + "group": "J", + "title": "Цилиндр Re=150, D=32, стенка grad, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "32", + "--nx", + "640", + "--ny", + "384", + "--body-x", + "128", + "--re", + "150", + "--refine", + "1", + "--wall", + "grad", + "--wall-function", + "spalding", + "--steps", + "384000", + "--backend", + "gpu" + ], + "nodes_per_step": 245760, + "steps": 384000, + "cost": 94371840000, + "outputs": [], + "shard": 4 + }, + { + "id": "J12_cyl_re150_d32_hrr_wf", + "group": "J", + "title": "Цилиндр Re=150, D=32, стенка hrr, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "32", + "--nx", + "640", + "--ny", + "384", + "--body-x", + "128", + "--re", + "150", + "--refine", + "1", + "--wall", + "hrr", + "--wall-function", + "spalding", + "--steps", + "384000", + "--backend", + "gpu" + ], + "nodes_per_step": 245760, + "steps": 384000, + "cost": 94371840000, + "outputs": [], + "shard": 3 + }, + { + "id": "J13_cyl_re150_d64_bouzidi", + "group": "J", + "title": "Цилиндр Re=150, D=64, стенка bouzidi, пристеночная функция none", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "64", + "--nx", + "1280", + "--ny", + "768", + "--body-x", + "256", + "--re", + "150", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "none", + "--steps", + "768000", + "--backend", + "gpu", + "--series-every", + "3" + ], + "nodes_per_step": 983040, + "steps": 768000, + "cost": 754974720000, + "outputs": [], + "shard": 2 + }, + { + "id": "J14_cyl_re150_d64_bouzidi_wf", + "group": "J", + "title": "Цилиндр Re=150, D=64, стенка bouzidi, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "64", + "--nx", + "1280", + "--ny", + "768", + "--body-x", + "256", + "--re", + "150", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "spalding", + "--steps", + "768000", + "--backend", + "gpu", + "--series-every", + "3" + ], + "nodes_per_step": 983040, + "steps": 768000, + "cost": 754974720000, + "outputs": [], + "shard": 3 + }, + { + "id": "J15_cyl_re150_d64_grad_wf", + "group": "J", + "title": "Цилиндр Re=150, D=64, стенка grad, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "64", + "--nx", + "1280", + "--ny", + "768", + "--body-x", + "256", + "--re", + "150", + "--refine", + "1", + "--wall", + "grad", + "--wall-function", + "spalding", + "--steps", + "768000", + "--backend", + "gpu", + "--series-every", + "3" + ], + "nodes_per_step": 983040, + "steps": 768000, + "cost": 754974720000, + "outputs": [], + "shard": 1 + }, + { + "id": "J16_cyl_re150_d64_hrr_wf", + "group": "J", + "title": "Цилиндр Re=150, D=64, стенка hrr, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "64", + "--nx", + "1280", + "--ny", + "768", + "--body-x", + "256", + "--re", + "150", + "--refine", + "1", + "--wall", + "hrr", + "--wall-function", + "spalding", + "--steps", + "768000", + "--backend", + "gpu", + "--series-every", + "3" + ], + "nodes_per_step": 983040, + "steps": 768000, + "cost": 754974720000, + "outputs": [], + "shard": 5 + }, + { + "id": "J17_cyl_re150_ref_d128", + "group": "J", + "title": "Цилиндр Re=150, ЭТАЛОН серии: Bouzidi без функции, D=128", + "expect": "сеточно сошедшееся значение, к которому меряются схемы при D=8…64", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "128", + "--nx", + "2560", + "--ny", + "1536", + "--body-x", + "512", + "--re", + "150", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "none", + "--steps", + "1536000", + "--backend", + "gpu", + "--series-every", + "7" + ], + "nodes_per_step": 3932160, + "steps": 1536000, + "cost": 6039797760000, + "outputs": [], + "shard": 2 + }, + { + "id": "J18_cyl_re2000_d8_bouzidi", + "group": "J", + "title": "Цилиндр Re=2000, D=8, стенка bouzidi, пристеночная функция none", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "8", + "--nx", + "160", + "--ny", + "96", + "--body-x", + "32", + "--re", + "2000", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "none", + "--steps", + "96000", + "--backend", + "gpu" + ], + "nodes_per_step": 15360, + "steps": 96000, + "cost": 1474560000, + "outputs": [], + "shard": 1 + }, + { + "id": "J19_cyl_re2000_d8_bouzidi_wf", + "group": "J", + "title": "Цилиндр Re=2000, D=8, стенка bouzidi, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "8", + "--nx", + "160", + "--ny", + "96", + "--body-x", + "32", + "--re", + "2000", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "spalding", + "--steps", + "96000", + "--backend", + "gpu" + ], + "nodes_per_step": 15360, + "steps": 96000, + "cost": 1474560000, + "outputs": [], + "shard": 3 + }, + { + "id": "J20_cyl_re2000_d8_grad_wf", + "group": "J", + "title": "Цилиндр Re=2000, D=8, стенка grad, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "8", + "--nx", + "160", + "--ny", + "96", + "--body-x", + "32", + "--re", + "2000", + "--refine", + "1", + "--wall", + "grad", + "--wall-function", + "spalding", + "--steps", + "96000", + "--backend", + "gpu" + ], + "nodes_per_step": 15360, + "steps": 96000, + "cost": 1474560000, + "outputs": [], + "shard": 2 + }, + { + "id": "J21_cyl_re2000_d8_hrr_wf", + "group": "J", + "title": "Цилиндр Re=2000, D=8, стенка hrr, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "8", + "--nx", + "160", + "--ny", + "96", + "--body-x", + "32", + "--re", + "2000", + "--refine", + "1", + "--wall", + "hrr", + "--wall-function", + "spalding", + "--steps", + "96000", + "--backend", + "gpu" + ], + "nodes_per_step": 15360, + "steps": 96000, + "cost": 1474560000, + "outputs": [], + "shard": 5 + }, + { + "id": "J22_cyl_re2000_d16_bouzidi", + "group": "J", + "title": "Цилиндр Re=2000, D=16, стенка bouzidi, пристеночная функция none", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "16", + "--nx", + "320", + "--ny", + "192", + "--body-x", + "64", + "--re", + "2000", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "none", + "--steps", + "192000", + "--backend", + "gpu" + ], + "nodes_per_step": 61440, + "steps": 192000, + "cost": 11796480000, + "outputs": [], + "shard": 4 + }, + { + "id": "J23_cyl_re2000_d16_bouzidi_wf", + "group": "J", + "title": "Цилиндр Re=2000, D=16, стенка bouzidi, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "16", + "--nx", + "320", + "--ny", + "192", + "--body-x", + "64", + "--re", + "2000", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "spalding", + "--steps", + "192000", + "--backend", + "gpu" + ], + "nodes_per_step": 61440, + "steps": 192000, + "cost": 11796480000, + "outputs": [], + "shard": 2 + }, + { + "id": "J24_cyl_re2000_d16_grad_wf", + "group": "J", + "title": "Цилиндр Re=2000, D=16, стенка grad, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "16", + "--nx", + "320", + "--ny", + "192", + "--body-x", + "64", + "--re", + "2000", + "--refine", + "1", + "--wall", + "grad", + "--wall-function", + "spalding", + "--steps", + "192000", + "--backend", + "gpu" + ], + "nodes_per_step": 61440, + "steps": 192000, + "cost": 11796480000, + "outputs": [], + "shard": 1 + }, + { + "id": "J25_cyl_re2000_d16_hrr_wf", + "group": "J", + "title": "Цилиндр Re=2000, D=16, стенка hrr, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "16", + "--nx", + "320", + "--ny", + "192", + "--body-x", + "64", + "--re", + "2000", + "--refine", + "1", + "--wall", + "hrr", + "--wall-function", + "spalding", + "--steps", + "192000", + "--backend", + "gpu" + ], + "nodes_per_step": 61440, + "steps": 192000, + "cost": 11796480000, + "outputs": [], + "shard": 3 + }, + { + "id": "J26_cyl_re2000_d32_bouzidi", + "group": "J", + "title": "Цилиндр Re=2000, D=32, стенка bouzidi, пристеночная функция none", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "32", + "--nx", + "640", + "--ny", + "384", + "--body-x", + "128", + "--re", + "2000", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "none", + "--steps", + "384000", + "--backend", + "gpu" + ], + "nodes_per_step": 245760, + "steps": 384000, + "cost": 94371840000, + "outputs": [], + "shard": 5 + }, + { + "id": "J27_cyl_re2000_d32_bouzidi_wf", + "group": "J", + "title": "Цилиндр Re=2000, D=32, стенка bouzidi, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "32", + "--nx", + "640", + "--ny", + "384", + "--body-x", + "128", + "--re", + "2000", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "spalding", + "--steps", + "384000", + "--backend", + "gpu" + ], + "nodes_per_step": 245760, + "steps": 384000, + "cost": 94371840000, + "outputs": [], + "shard": 1 + }, + { + "id": "J28_cyl_re2000_d32_grad_wf", + "group": "J", + "title": "Цилиндр Re=2000, D=32, стенка grad, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "32", + "--nx", + "640", + "--ny", + "384", + "--body-x", + "128", + "--re", + "2000", + "--refine", + "1", + "--wall", + "grad", + "--wall-function", + "spalding", + "--steps", + "384000", + "--backend", + "gpu" + ], + "nodes_per_step": 245760, + "steps": 384000, + "cost": 94371840000, + "outputs": [], + "shard": 2 + }, + { + "id": "J29_cyl_re2000_d32_hrr_wf", + "group": "J", + "title": "Цилиндр Re=2000, D=32, стенка hrr, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "32", + "--nx", + "640", + "--ny", + "384", + "--body-x", + "128", + "--re", + "2000", + "--refine", + "1", + "--wall", + "hrr", + "--wall-function", + "spalding", + "--steps", + "384000", + "--backend", + "gpu" + ], + "nodes_per_step": 245760, + "steps": 384000, + "cost": 94371840000, + "outputs": [], + "shard": 4 + }, + { + "id": "J30_cyl_re2000_d64_bouzidi", + "group": "J", + "title": "Цилиндр Re=2000, D=64, стенка bouzidi, пристеночная функция none", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "64", + "--nx", + "1280", + "--ny", + "768", + "--body-x", + "256", + "--re", + "2000", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "none", + "--steps", + "768000", + "--backend", + "gpu", + "--series-every", + "3" + ], + "nodes_per_step": 983040, + "steps": 768000, + "cost": 754974720000, + "outputs": [], + "shard": 4 + }, + { + "id": "J31_cyl_re2000_d64_bouzidi_wf", + "group": "J", + "title": "Цилиндр Re=2000, D=64, стенка bouzidi, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "64", + "--nx", + "1280", + "--ny", + "768", + "--body-x", + "256", + "--re", + "2000", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "spalding", + "--steps", + "768000", + "--backend", + "gpu", + "--series-every", + "3" + ], + "nodes_per_step": 983040, + "steps": 768000, + "cost": 754974720000, + "outputs": [], + "shard": 2 + }, + { + "id": "J32_cyl_re2000_d64_grad_wf", + "group": "J", + "title": "Цилиндр Re=2000, D=64, стенка grad, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "64", + "--nx", + "1280", + "--ny", + "768", + "--body-x", + "256", + "--re", + "2000", + "--refine", + "1", + "--wall", + "grad", + "--wall-function", + "spalding", + "--steps", + "768000", + "--backend", + "gpu", + "--series-every", + "3" + ], + "nodes_per_step": 983040, + "steps": 768000, + "cost": 754974720000, + "outputs": [], + "shard": 3 + }, + { + "id": "J33_cyl_re2000_d64_hrr_wf", + "group": "J", + "title": "Цилиндр Re=2000, D=64, стенка hrr, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "64", + "--nx", + "1280", + "--ny", + "768", + "--body-x", + "256", + "--re", + "2000", + "--refine", + "1", + "--wall", + "hrr", + "--wall-function", + "spalding", + "--steps", + "768000", + "--backend", + "gpu", + "--series-every", + "3" + ], + "nodes_per_step": 983040, + "steps": 768000, + "cost": 754974720000, + "outputs": [], + "shard": 1 + }, + { + "id": "J34_cyl_re2000_ref_d128", + "group": "J", + "title": "Цилиндр Re=2000, ЭТАЛОН серии: Bouzidi без функции, D=128", + "expect": "сеточно сошедшееся значение, к которому меряются схемы при D=8…64", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "128", + "--nx", + "2560", + "--ny", + "1536", + "--body-x", + "512", + "--re", + "2000", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "none", + "--steps", + "1536000", + "--backend", + "gpu", + "--series-every", + "7" + ], + "nodes_per_step": 3932160, + "steps": 1536000, + "cost": 6039797760000, + "outputs": [], + "shard": 3 + }, + { + "id": "J35_cyl_re5000_d32_bouzidi", + "group": "J", + "title": "Цилиндр Re=5000, D=32, стенка bouzidi, пристеночная функция none", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "32", + "--nx", + "640", + "--ny", + "384", + "--body-x", + "128", + "--re", + "5000", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "none", + "--steps", + "384000", + "--backend", + "gpu" + ], + "nodes_per_step": 245760, + "steps": 384000, + "cost": 94371840000, + "outputs": [], + "shard": 3 + }, + { + "id": "J36_cyl_re5000_d32_bouzidi_wf", + "group": "J", + "title": "Цилиндр Re=5000, D=32, стенка bouzidi, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "32", + "--nx", + "640", + "--ny", + "384", + "--body-x", + "128", + "--re", + "5000", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "spalding", + "--steps", + "384000", + "--backend", + "gpu" + ], + "nodes_per_step": 245760, + "steps": 384000, + "cost": 94371840000, + "outputs": [], + "shard": 5 + }, + { + "id": "J37_cyl_re5000_d32_grad_wf", + "group": "J", + "title": "Цилиндр Re=5000, D=32, стенка grad, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "32", + "--nx", + "640", + "--ny", + "384", + "--body-x", + "128", + "--re", + "5000", + "--refine", + "1", + "--wall", + "grad", + "--wall-function", + "spalding", + "--steps", + "384000", + "--backend", + "gpu" + ], + "nodes_per_step": 245760, + "steps": 384000, + "cost": 94371840000, + "outputs": [], + "shard": 1 + }, + { + "id": "J38_cyl_re5000_d32_hrr_wf", + "group": "J", + "title": "Цилиндр Re=5000, D=32, стенка hrr, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "32", + "--nx", + "640", + "--ny", + "384", + "--body-x", + "128", + "--re", + "5000", + "--refine", + "1", + "--wall", + "hrr", + "--wall-function", + "spalding", + "--steps", + "384000", + "--backend", + "gpu" + ], + "nodes_per_step": 245760, + "steps": 384000, + "cost": 94371840000, + "outputs": [], + "shard": 2 + }, + { + "id": "J39_cyl_re5000_d64_bouzidi", + "group": "J", + "title": "Цилиндр Re=5000, D=64, стенка bouzidi, пристеночная функция none", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "64", + "--nx", + "1280", + "--ny", + "768", + "--body-x", + "256", + "--re", + "5000", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "none", + "--steps", + "768000", + "--backend", + "gpu", + "--series-every", + "3" + ], + "nodes_per_step": 983040, + "steps": 768000, + "cost": 754974720000, + "outputs": [], + "shard": 5 + }, + { + "id": "J40_cyl_re5000_d64_bouzidi_wf", + "group": "J", + "title": "Цилиндр Re=5000, D=64, стенка bouzidi, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "64", + "--nx", + "1280", + "--ny", + "768", + "--body-x", + "256", + "--re", + "5000", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "spalding", + "--steps", + "768000", + "--backend", + "gpu", + "--series-every", + "3" + ], + "nodes_per_step": 983040, + "steps": 768000, + "cost": 754974720000, + "outputs": [], + "shard": 4 + }, + { + "id": "J41_cyl_re5000_d64_grad_wf", + "group": "J", + "title": "Цилиндр Re=5000, D=64, стенка grad, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "64", + "--nx", + "1280", + "--ny", + "768", + "--body-x", + "256", + "--re", + "5000", + "--refine", + "1", + "--wall", + "grad", + "--wall-function", + "spalding", + "--steps", + "768000", + "--backend", + "gpu", + "--series-every", + "3" + ], + "nodes_per_step": 983040, + "steps": 768000, + "cost": 754974720000, + "outputs": [], + "shard": 2 + }, + { + "id": "J42_cyl_re5000_d64_hrr_wf", + "group": "J", + "title": "Цилиндр Re=5000, D=64, стенка hrr, пристеночная функция spalding", + "expect": "отклонение St, ⟨Cd⟩, rms Cl от эталона серии (Bouzidi при D=128); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "64", + "--nx", + "1280", + "--ny", + "768", + "--body-x", + "256", + "--re", + "5000", + "--refine", + "1", + "--wall", + "hrr", + "--wall-function", + "spalding", + "--steps", + "768000", + "--backend", + "gpu", + "--series-every", + "3" + ], + "nodes_per_step": 983040, + "steps": 768000, + "cost": 754974720000, + "outputs": [], + "shard": 3 + }, + { + "id": "J43_cyl_re5000_ref_d128", + "group": "J", + "title": "Цилиндр Re=5000, ЭТАЛОН серии: Bouzidi без функции, D=128", + "expect": "сеточно сошедшееся значение, к которому меряются схемы при D=8…64", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "128", + "--nx", + "2560", + "--ny", + "1536", + "--body-x", + "512", + "--re", + "5000", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "none", + "--steps", + "1536000", + "--backend", + "gpu", + "--series-every", + "7" + ], + "nodes_per_step": 3932160, + "steps": 1536000, + "cost": 6039797760000, + "outputs": [], + "shard": 4 + }, + { + "id": "J44_naca_re2000_c24_bouzidi", + "group": "J", + "title": "NACA 0012, α=4°, Re=2000, хорда 24, стенка bouzidi, функция none", + "expect": "отклонение Cl, Cd от эталона серии (Bouzidi при хорде 192); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "naca", + "--naca", + "0012", + "--size", + "24", + "--body-angle", + "4", + "--nx", + "288", + "--ny", + "144", + "--body-x", + "72", + "--re", + "2000", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "none", + "--steps", + "192000", + "--backend", + "gpu" + ], + "nodes_per_step": 41472, + "steps": 192000, + "cost": 7962624000, + "outputs": [], + "shard": 2 + }, + { + "id": "J45_naca_re2000_c24_bouzidi_wf", + "group": "J", + "title": "NACA 0012, α=4°, Re=2000, хорда 24, стенка bouzidi, функция spalding", + "expect": "отклонение Cl, Cd от эталона серии (Bouzidi при хорде 192); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "naca", + "--naca", + "0012", + "--size", + "24", + "--body-angle", + "4", + "--nx", + "288", + "--ny", + "144", + "--body-x", + "72", + "--re", + "2000", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "spalding", + "--steps", + "192000", + "--backend", + "gpu" + ], + "nodes_per_step": 41472, + "steps": 192000, + "cost": 7962624000, + "outputs": [], + "shard": 1 + }, + { + "id": "J46_naca_re2000_c24_grad_wf", + "group": "J", + "title": "NACA 0012, α=4°, Re=2000, хорда 24, стенка grad, функция spalding", + "expect": "отклонение Cl, Cd от эталона серии (Bouzidi при хорде 192); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "naca", + "--naca", + "0012", + "--size", + "24", + "--body-angle", + "4", + "--nx", + "288", + "--ny", + "144", + "--body-x", + "72", + "--re", + "2000", + "--refine", + "1", + "--wall", + "grad", + "--wall-function", + "spalding", + "--steps", + "192000", + "--backend", + "gpu" + ], + "nodes_per_step": 41472, + "steps": 192000, + "cost": 7962624000, + "outputs": [], + "shard": 3 + }, + { + "id": "J47_naca_re2000_c24_hrr_wf", + "group": "J", + "title": "NACA 0012, α=4°, Re=2000, хорда 24, стенка hrr, функция spalding", + "expect": "отклонение Cl, Cd от эталона серии (Bouzidi при хорде 192); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "naca", + "--naca", + "0012", + "--size", + "24", + "--body-angle", + "4", + "--nx", + "288", + "--ny", + "144", + "--body-x", + "72", + "--re", + "2000", + "--refine", + "1", + "--wall", + "hrr", + "--wall-function", + "spalding", + "--steps", + "192000", + "--backend", + "gpu" + ], + "nodes_per_step": 41472, + "steps": 192000, + "cost": 7962624000, + "outputs": [], + "shard": 5 + }, + { + "id": "J48_naca_re2000_c48_bouzidi", + "group": "J", + "title": "NACA 0012, α=4°, Re=2000, хорда 48, стенка bouzidi, функция none", + "expect": "отклонение Cl, Cd от эталона серии (Bouzidi при хорде 192); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "naca", + "--naca", + "0012", + "--size", + "48", + "--body-angle", + "4", + "--nx", + "576", + "--ny", + "288", + "--body-x", + "144", + "--re", + "2000", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "none", + "--steps", + "384000", + "--backend", + "gpu" + ], + "nodes_per_step": 165888, + "steps": 384000, + "cost": 63700992000, + "outputs": [], + "shard": 4 + }, + { + "id": "J49_naca_re2000_c48_bouzidi_wf", + "group": "J", + "title": "NACA 0012, α=4°, Re=2000, хорда 48, стенка bouzidi, функция spalding", + "expect": "отклонение Cl, Cd от эталона серии (Bouzidi при хорде 192); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "naca", + "--naca", + "0012", + "--size", + "48", + "--body-angle", + "4", + "--nx", + "576", + "--ny", + "288", + "--body-x", + "144", + "--re", + "2000", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "spalding", + "--steps", + "384000", + "--backend", + "gpu" + ], + "nodes_per_step": 165888, + "steps": 384000, + "cost": 63700992000, + "outputs": [], + "shard": 3 + }, + { + "id": "J50_naca_re2000_c48_grad_wf", + "group": "J", + "title": "NACA 0012, α=4°, Re=2000, хорда 48, стенка grad, функция spalding", + "expect": "отклонение Cl, Cd от эталона серии (Bouzidi при хорде 192); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "naca", + "--naca", + "0012", + "--size", + "48", + "--body-angle", + "4", + "--nx", + "576", + "--ny", + "288", + "--body-x", + "144", + "--re", + "2000", + "--refine", + "1", + "--wall", + "grad", + "--wall-function", + "spalding", + "--steps", + "384000", + "--backend", + "gpu" + ], + "nodes_per_step": 165888, + "steps": 384000, + "cost": 63700992000, + "outputs": [], + "shard": 5 + }, + { + "id": "J51_naca_re2000_c48_hrr_wf", + "group": "J", + "title": "NACA 0012, α=4°, Re=2000, хорда 48, стенка hrr, функция spalding", + "expect": "отклонение Cl, Cd от эталона серии (Bouzidi при хорде 192); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "naca", + "--naca", + "0012", + "--size", + "48", + "--body-angle", + "4", + "--nx", + "576", + "--ny", + "288", + "--body-x", + "144", + "--re", + "2000", + "--refine", + "1", + "--wall", + "hrr", + "--wall-function", + "spalding", + "--steps", + "384000", + "--backend", + "gpu" + ], + "nodes_per_step": 165888, + "steps": 384000, + "cost": 63700992000, + "outputs": [], + "shard": 1 + }, + { + "id": "J52_naca_re2000_c96_bouzidi", + "group": "J", + "title": "NACA 0012, α=4°, Re=2000, хорда 96, стенка bouzidi, функция none", + "expect": "отклонение Cl, Cd от эталона серии (Bouzidi при хорде 192); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "naca", + "--naca", + "0012", + "--size", + "96", + "--body-angle", + "4", + "--nx", + "1152", + "--ny", + "576", + "--body-x", + "288", + "--re", + "2000", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "none", + "--steps", + "768000", + "--backend", + "gpu", + "--series-every", + "3" + ], + "nodes_per_step": 663552, + "steps": 768000, + "cost": 509607936000, + "outputs": [], + "shard": 5 + }, + { + "id": "J53_naca_re2000_c96_bouzidi_wf", + "group": "J", + "title": "NACA 0012, α=4°, Re=2000, хорда 96, стенка bouzidi, функция spalding", + "expect": "отклонение Cl, Cd от эталона серии (Bouzidi при хорде 192); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "naca", + "--naca", + "0012", + "--size", + "96", + "--body-angle", + "4", + "--nx", + "1152", + "--ny", + "576", + "--body-x", + "288", + "--re", + "2000", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "spalding", + "--steps", + "768000", + "--backend", + "gpu", + "--series-every", + "3" + ], + "nodes_per_step": 663552, + "steps": 768000, + "cost": 509607936000, + "outputs": [], + "shard": 4 + }, + { + "id": "J54_naca_re2000_c96_grad_wf", + "group": "J", + "title": "NACA 0012, α=4°, Re=2000, хорда 96, стенка grad, функция spalding", + "expect": "отклонение Cl, Cd от эталона серии (Bouzidi при хорде 192); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "naca", + "--naca", + "0012", + "--size", + "96", + "--body-angle", + "4", + "--nx", + "1152", + "--ny", + "576", + "--body-x", + "288", + "--re", + "2000", + "--refine", + "1", + "--wall", + "grad", + "--wall-function", + "spalding", + "--steps", + "768000", + "--backend", + "gpu", + "--series-every", + "3" + ], + "nodes_per_step": 663552, + "steps": 768000, + "cost": 509607936000, + "outputs": [], + "shard": 1 + }, + { + "id": "J55_naca_re2000_c96_hrr_wf", + "group": "J", + "title": "NACA 0012, α=4°, Re=2000, хорда 96, стенка hrr, функция spalding", + "expect": "отклонение Cl, Cd от эталона серии (Bouzidi при хорде 192); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "naca", + "--naca", + "0012", + "--size", + "96", + "--body-angle", + "4", + "--nx", + "1152", + "--ny", + "576", + "--body-x", + "288", + "--re", + "2000", + "--refine", + "1", + "--wall", + "hrr", + "--wall-function", + "spalding", + "--steps", + "768000", + "--backend", + "gpu", + "--series-every", + "3" + ], + "nodes_per_step": 663552, + "steps": 768000, + "cost": 509607936000, + "outputs": [], + "shard": 2 + }, + { + "id": "J56_naca_re2000_ref_c192", + "group": "J", + "title": "NACA 0012, α=4°, Re=2000, ЭТАЛОН серии: Bouzidi без функции, хорда 192", + "expect": "сеточно сошедшееся значение для серии по хорде", + "backend": "gpu", + "args": [ + "--shape", + "naca", + "--naca", + "0012", + "--size", + "192", + "--body-angle", + "4", + "--nx", + "2304", + "--ny", + "1152", + "--body-x", + "576", + "--re", + "2000", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "none", + "--steps", + "1536000", + "--backend", + "gpu", + "--series-every", + "7" + ], + "nodes_per_step": 2654208, + "steps": 1536000, + "cost": 4076863488000, + "outputs": [], + "shard": 2 + }, + { + "id": "J57_naca_re10000_c48_bouzidi", + "group": "J", + "title": "NACA 0012, α=4°, Re=10000, хорда 48, стенка bouzidi, функция none", + "expect": "отклонение Cl, Cd от эталона серии (Bouzidi при хорде 192); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "naca", + "--naca", + "0012", + "--size", + "48", + "--body-angle", + "4", + "--nx", + "576", + "--ny", + "288", + "--body-x", + "144", + "--re", + "10000", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "none", + "--steps", + "384000", + "--backend", + "gpu" + ], + "nodes_per_step": 165888, + "steps": 384000, + "cost": 63700992000, + "outputs": [], + "shard": 2 + }, + { + "id": "J58_naca_re10000_c48_bouzidi_wf", + "group": "J", + "title": "NACA 0012, α=4°, Re=10000, хорда 48, стенка bouzidi, функция spalding", + "expect": "отклонение Cl, Cd от эталона серии (Bouzidi при хорде 192); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "naca", + "--naca", + "0012", + "--size", + "48", + "--body-angle", + "4", + "--nx", + "576", + "--ny", + "288", + "--body-x", + "144", + "--re", + "10000", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "spalding", + "--steps", + "384000", + "--backend", + "gpu" + ], + "nodes_per_step": 165888, + "steps": 384000, + "cost": 63700992000, + "outputs": [], + "shard": 4 + }, + { + "id": "J59_naca_re10000_c48_grad_wf", + "group": "J", + "title": "NACA 0012, α=4°, Re=10000, хорда 48, стенка grad, функция spalding", + "expect": "отклонение Cl, Cd от эталона серии (Bouzidi при хорде 192); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "naca", + "--naca", + "0012", + "--size", + "48", + "--body-angle", + "4", + "--nx", + "576", + "--ny", + "288", + "--body-x", + "144", + "--re", + "10000", + "--refine", + "1", + "--wall", + "grad", + "--wall-function", + "spalding", + "--steps", + "384000", + "--backend", + "gpu" + ], + "nodes_per_step": 165888, + "steps": 384000, + "cost": 63700992000, + "outputs": [], + "shard": 3 + }, + { + "id": "J60_naca_re10000_c48_hrr_wf", + "group": "J", + "title": "NACA 0012, α=4°, Re=10000, хорда 48, стенка hrr, функция spalding", + "expect": "отклонение Cl, Cd от эталона серии (Bouzidi при хорде 192); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "naca", + "--naca", + "0012", + "--size", + "48", + "--body-angle", + "4", + "--nx", + "576", + "--ny", + "288", + "--body-x", + "144", + "--re", + "10000", + "--refine", + "1", + "--wall", + "hrr", + "--wall-function", + "spalding", + "--steps", + "384000", + "--backend", + "gpu" + ], + "nodes_per_step": 165888, + "steps": 384000, + "cost": 63700992000, + "outputs": [], + "shard": 5 + }, + { + "id": "J61_naca_re10000_c96_bouzidi", + "group": "J", + "title": "NACA 0012, α=4°, Re=10000, хорда 96, стенка bouzidi, функция none", + "expect": "отклонение Cl, Cd от эталона серии (Bouzidi при хорде 192); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "naca", + "--naca", + "0012", + "--size", + "96", + "--body-angle", + "4", + "--nx", + "1152", + "--ny", + "576", + "--body-x", + "288", + "--re", + "10000", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "none", + "--steps", + "768000", + "--backend", + "gpu", + "--series-every", + "3" + ], + "nodes_per_step": 663552, + "steps": 768000, + "cost": 509607936000, + "outputs": [], + "shard": 5 + }, + { + "id": "J62_naca_re10000_c96_bouzidi_wf", + "group": "J", + "title": "NACA 0012, α=4°, Re=10000, хорда 96, стенка bouzidi, функция spalding", + "expect": "отклонение Cl, Cd от эталона серии (Bouzidi при хорде 192); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "naca", + "--naca", + "0012", + "--size", + "96", + "--body-angle", + "4", + "--nx", + "1152", + "--ny", + "576", + "--body-x", + "288", + "--re", + "10000", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "spalding", + "--steps", + "768000", + "--backend", + "gpu", + "--series-every", + "3" + ], + "nodes_per_step": 663552, + "steps": 768000, + "cost": 509607936000, + "outputs": [], + "shard": 4 + }, + { + "id": "J63_naca_re10000_c96_grad_wf", + "group": "J", + "title": "NACA 0012, α=4°, Re=10000, хорда 96, стенка grad, функция spalding", + "expect": "отклонение Cl, Cd от эталона серии (Bouzidi при хорде 192); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "naca", + "--naca", + "0012", + "--size", + "96", + "--body-angle", + "4", + "--nx", + "1152", + "--ny", + "576", + "--body-x", + "288", + "--re", + "10000", + "--refine", + "1", + "--wall", + "grad", + "--wall-function", + "spalding", + "--steps", + "768000", + "--backend", + "gpu", + "--series-every", + "3" + ], + "nodes_per_step": 663552, + "steps": 768000, + "cost": 509607936000, + "outputs": [], + "shard": 1 + }, + { + "id": "J64_naca_re10000_c96_hrr_wf", + "group": "J", + "title": "NACA 0012, α=4°, Re=10000, хорда 96, стенка hrr, функция spalding", + "expect": "отклонение Cl, Cd от эталона серии (Bouzidi при хорде 192); y⁺ в сводке", + "backend": "gpu", + "args": [ + "--shape", + "naca", + "--naca", + "0012", + "--size", + "96", + "--body-angle", + "4", + "--nx", + "1152", + "--ny", + "576", + "--body-x", + "288", + "--re", + "10000", + "--refine", + "1", + "--wall", + "hrr", + "--wall-function", + "spalding", + "--steps", + "768000", + "--backend", + "gpu", + "--series-every", + "3" + ], + "nodes_per_step": 663552, + "steps": 768000, + "cost": 509607936000, + "outputs": [], + "shard": 3 + }, + { + "id": "J65_naca_re10000_ref_c192", + "group": "J", + "title": "NACA 0012, α=4°, Re=10000, ЭТАЛОН серии: Bouzidi без функции, хорда 192", + "expect": "сеточно сошедшееся значение для серии по хорде", + "backend": "gpu", + "args": [ + "--shape", + "naca", + "--naca", + "0012", + "--size", + "192", + "--body-angle", + "4", + "--nx", + "2304", + "--ny", + "1152", + "--body-x", + "576", + "--re", + "10000", + "--refine", + "1", + "--wall", + "bouzidi", + "--wall-function", + "none", + "--steps", + "1536000", + "--backend", + "gpu", + "--series-every", + "7" + ], + "nodes_per_step": 2654208, + "steps": 1536000, + "cost": 4076863488000, + "outputs": [], + "shard": 5 + }, + { + "id": "K01_re150_ref", + "group": "K", + "title": "Re=150, ЭТАЛОН: однородная тонкая сетка 30D+60D, ±30D", + "expect": "значения, к которым меряются все конфигурации буферов", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "32", + "--nx", + "2880", + "--ny", + "1920", + "--body-x", + "960", + "--re", + "150", + "--wall", + "bouzidi", + "--refine", + "1", + "--steps", + "384000", + "--backend", + "gpu" + ], + "nodes_per_step": 5529600, + "steps": 384000, + "cost": 2123366400000, + "outputs": [], + "shard": 5 + }, + { + "id": "K02_re150_base", + "group": "K", + "title": "Re=150, база: домен 20D+40D, ±20D, ступени ×2×2", + "expect": "отклонение от эталона при умеренных буферах", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "8", + "--nx", + "480", + "--ny", + "320", + "--body-x", + "160", + "--re", + "150", + "--wall", + "bouzidi", + "--levels", + "2:4,15,4; 2:1.5,10,2", + "--steps", + "96000", + "--backend", + "gpu" + ], + "nodes_per_step": 422694, + "steps": 96000, + "cost": 40578624000, + "outputs": [], + "shard": 5 + }, + { + "id": "K03_re150_jump4", + "group": "K", + "title": "Re=150, огрубление ×4 одним скачком", + "expect": "скачок разрешения против ступеней ×2×2 (база)", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "8", + "--nx", + "480", + "--ny", + "320", + "--body-x", + "160", + "--re", + "150", + "--wall", + "bouzidi", + "--levels", + "4:1.5,10,2", + "--steps", + "96000", + "--backend", + "gpu" + ], + "nodes_per_step": 344004, + "steps": 96000, + "cost": 33024384000, + "outputs": [], + "shard": 4 + }, + { + "id": "K04_re150_jump8", + "group": "K", + "title": "Re=150, огрубление ×8 одним скачком", + "expect": "скачок ×8 против цепочки ×2×2×2", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "4", + "--nx", + "240", + "--ny", + "160", + "--body-x", + "80", + "--re", + "150", + "--wall", + "bouzidi", + "--levels", + "8:1.5,10,2", + "--steps", + "48000", + "--backend", + "gpu" + ], + "nodes_per_step": 419208, + "steps": 48000, + "cost": 20121984000, + "outputs": [], + "shard": 5 + }, + { + "id": "K05_re150_chain8", + "group": "K", + "title": "Re=150, огрубление ×8 цепочкой ×2×2×2", + "expect": "плавный переход при сильном огрублении", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "4", + "--nx", + "240", + "--ny", + "160", + "--body-x", + "80", + "--re", + "150", + "--wall", + "bouzidi", + "--levels", + "2:8,25,8; 2:4,15,4; 2:1.5,10,2", + "--steps", + "48000", + "--backend", + "gpu" + ], + "nodes_per_step": 644958, + "steps": 48000, + "cost": 30957984000, + "outputs": [], + "shard": 3 + }, + { + "id": "K06_re150_sponge5", + "group": "K", + "title": "Re=150, боковые губки 5D", + "expect": "гасят ли губки отражения от боковых стенок", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "8", + "--nx", + "480", + "--ny", + "320", + "--body-x", + "160", + "--re", + "150", + "--wall", + "bouzidi", + "--levels", + "2:4,15,4; 2:1.5,10,2", + "--sponge-side", + "40", + "--steps", + "96000", + "--backend", + "gpu" + ], + "nodes_per_step": 422694, + "steps": 96000, + "cost": 40578624000, + "outputs": [], + "shard": 1 + }, + { + "id": "K07_re150_side5", + "group": "K", + "title": "Re=150, боковой буфер ±5D", + "expect": "с какого размера бока перестают влиять (блокировка + отражения)", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "8", + "--nx", + "480", + "--ny", + "80", + "--body-x", + "160", + "--re", + "150", + "--wall", + "bouzidi", + "--levels", + "2:4,15,4; 2:1.5,10,2", + "--steps", + "96000", + "--backend", + "gpu" + ], + "nodes_per_step": 307494, + "steps": 96000, + "cost": 29519424000, + "outputs": [], + "shard": 2 + }, + { + "id": "K08_re150_side10", + "group": "K", + "title": "Re=150, боковой буфер ±10D", + "expect": "с какого размера бока перестают влиять (блокировка + отражения)", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "8", + "--nx", + "480", + "--ny", + "160", + "--body-x", + "160", + "--re", + "150", + "--wall", + "bouzidi", + "--levels", + "2:4,15,4; 2:1.5,10,2", + "--steps", + "96000", + "--backend", + "gpu" + ], + "nodes_per_step": 345894, + "steps": 96000, + "cost": 33205824000, + "outputs": [], + "shard": 5 + }, + { + "id": "K09_re150_side30", + "group": "K", + "title": "Re=150, боковой буфер ±30D", + "expect": "с какого размера бока перестают влиять (блокировка + отражения)", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "8", + "--nx", + "480", + "--ny", + "480", + "--body-x", + "160", + "--re", + "150", + "--wall", + "bouzidi", + "--levels", + "2:4,15,4; 2:1.5,10,2", + "--steps", + "96000", + "--backend", + "gpu" + ], + "nodes_per_step": 499494, + "steps": 96000, + "cost": 47951424000, + "outputs": [], + "shard": 5 + }, + { + "id": "K10_re150_up5", + "group": "K", + "title": "Re=150, буфер до тела 5D", + "expect": "с какого отступа вход перестаёт влиять", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "8", + "--nx", + "360", + "--ny", + "320", + "--body-x", + "40", + "--re", + "150", + "--wall", + "bouzidi", + "--levels", + "2:4,15,4; 2:1.5,10,2", + "--steps", + "96000", + "--backend", + "gpu" + ], + "nodes_per_step": 384294, + "steps": 96000, + "cost": 36892224000, + "outputs": [], + "shard": 3 + }, + { + "id": "K11_re150_up10", + "group": "K", + "title": "Re=150, буфер до тела 10D", + "expect": "с какого отступа вход перестаёт влиять", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "8", + "--nx", + "400", + "--ny", + "320", + "--body-x", + "80", + "--re", + "150", + "--wall", + "bouzidi", + "--levels", + "2:4,15,4; 2:1.5,10,2", + "--steps", + "96000", + "--backend", + "gpu" + ], + "nodes_per_step": 397094, + "steps": 96000, + "cost": 38121024000, + "outputs": [], + "shard": 2 + }, + { + "id": "K12_re150_up30", + "group": "K", + "title": "Re=150, буфер до тела 30D", + "expect": "с какого отступа вход перестаёт влиять", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "8", + "--nx", + "560", + "--ny", + "320", + "--body-x", + "240", + "--re", + "150", + "--wall", + "bouzidi", + "--levels", + "2:4,15,4; 2:1.5,10,2", + "--steps", + "96000", + "--backend", + "gpu" + ], + "nodes_per_step": 448294, + "steps": 96000, + "cost": 43036224000, + "outputs": [], + "shard": 4 + }, + { + "id": "K13_re150_down15", + "group": "K", + "title": "Re=150, буфер после тела 15D", + "expect": "с какой длины выход перестаёт влиять", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "8", + "--nx", + "280", + "--ny", + "320", + "--body-x", + "160", + "--re", + "150", + "--wall", + "bouzidi", + "--levels", + "2:4,13,4; 2:1.5,10,2", + "--steps", + "96000", + "--backend", + "gpu" + ], + "nodes_per_step": 350438, + "steps": 96000, + "cost": 33642048000, + "outputs": [], + "shard": 2 + }, + { + "id": "K14_re150_down30", + "group": "K", + "title": "Re=150, буфер после тела 30D", + "expect": "с какой длины выход перестаёт влиять", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "8", + "--nx", + "400", + "--ny", + "320", + "--body-x", + "160", + "--re", + "150", + "--wall", + "bouzidi", + "--levels", + "2:4,15,4; 2:1.5,10,2", + "--steps", + "96000", + "--backend", + "gpu" + ], + "nodes_per_step": 397094, + "steps": 96000, + "cost": 38121024000, + "outputs": [], + "shard": 1 + }, + { + "id": "K15_re150_down60", + "group": "K", + "title": "Re=150, буфер после тела 60D", + "expect": "с какой длины выход перестаёт влиять", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "8", + "--nx", + "640", + "--ny", + "320", + "--body-x", + "160", + "--re", + "150", + "--wall", + "bouzidi", + "--levels", + "2:4,15,4; 2:1.5,10,2", + "--steps", + "96000", + "--backend", + "gpu" + ], + "nodes_per_step": 473894, + "steps": 96000, + "cost": 45493824000, + "outputs": [], + "shard": 2 + }, + { + "id": "K16_re150_wake5", + "group": "K", + "title": "Re=150, тонкий след 5D за телом", + "expect": "сколько тонкой сетки нужно в следе до первого огрубления", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "8", + "--nx", + "480", + "--ny", + "320", + "--body-x", + "160", + "--re", + "150", + "--wall", + "bouzidi", + "--levels", + "2:4,15,4; 2:1.5,5,2", + "--steps", + "96000", + "--backend", + "gpu" + ], + "nodes_per_step": 340134, + "steps": 96000, + "cost": 32652864000, + "outputs": [], + "shard": 3 + }, + { + "id": "K17_re150_wake20", + "group": "K", + "title": "Re=150, тонкий след 20D за телом", + "expect": "сколько тонкой сетки нужно в следе до первого огрубления", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "8", + "--nx", + "480", + "--ny", + "320", + "--body-x", + "160", + "--re", + "150", + "--wall", + "bouzidi", + "--levels", + "2:4,25,4; 2:1.5,20,2", + "--steps", + "96000", + "--backend", + "gpu" + ], + "nodes_per_step": 629094, + "steps": 96000, + "cost": 60393024000, + "outputs": [], + "shard": 1 + }, + { + "id": "K18_re150_midwide", + "group": "K", + "title": "Re=150, широкий промежуточный уровень (8D вверх, 25D вниз, 8D вбок)", + "expect": "влияет ли ширина ступени перехода", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "8", + "--nx", + "480", + "--ny", + "320", + "--body-x", + "160", + "--re", + "150", + "--wall", + "bouzidi", + "--levels", + "2:8,25,8; 2:1.5,10,2", + "--steps", + "96000", + "--backend", + "gpu" + ], + "nodes_per_step": 615910, + "steps": 96000, + "cost": 59127360000, + "outputs": [], + "shard": 3 + }, + { + "id": "K19_re150_sponge2", + "group": "K", + "title": "Re=150, боковые губки 2D", + "expect": "ширина губки: 2D против 5D", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "8", + "--nx", + "480", + "--ny", + "320", + "--body-x", + "160", + "--re", + "150", + "--wall", + "bouzidi", + "--levels", + "2:4,15,4; 2:1.5,10,2", + "--sponge-side", + "16", + "--steps", + "96000", + "--backend", + "gpu" + ], + "nodes_per_step": 422694, + "steps": 96000, + "cost": 40578624000, + "outputs": [], + "shard": 3 + }, + { + "id": "K20_re2000_ref", + "group": "K", + "title": "Re=2000, ЭТАЛОН: однородная тонкая сетка 30D+60D, ±30D", + "expect": "значения, к которым меряются все конфигурации буферов", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "32", + "--nx", + "2880", + "--ny", + "1920", + "--body-x", + "960", + "--re", + "2000", + "--wall", + "bouzidi", + "--refine", + "1", + "--steps", + "384000", + "--backend", + "gpu" + ], + "nodes_per_step": 5529600, + "steps": 384000, + "cost": 2123366400000, + "outputs": [], + "shard": 3 + }, + { + "id": "K21_re2000_base", + "group": "K", + "title": "Re=2000, база: домен 20D+40D, ±20D, ступени ×2×2", + "expect": "отклонение от эталона при умеренных буферах", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "8", + "--nx", + "480", + "--ny", + "320", + "--body-x", + "160", + "--re", + "2000", + "--wall", + "bouzidi", + "--levels", + "2:4,15,4; 2:1.5,10,2", + "--steps", + "96000", + "--backend", + "gpu" + ], + "nodes_per_step": 422694, + "steps": 96000, + "cost": 40578624000, + "outputs": [], + "shard": 5 + }, + { + "id": "K22_re2000_jump4", + "group": "K", + "title": "Re=2000, огрубление ×4 одним скачком", + "expect": "скачок разрешения против ступеней ×2×2 (база)", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "8", + "--nx", + "480", + "--ny", + "320", + "--body-x", + "160", + "--re", + "2000", + "--wall", + "bouzidi", + "--levels", + "4:1.5,10,2", + "--steps", + "96000", + "--backend", + "gpu" + ], + "nodes_per_step": 344004, + "steps": 96000, + "cost": 33024384000, + "outputs": [], + "shard": 1 + }, + { + "id": "K23_re2000_jump8", + "group": "K", + "title": "Re=2000, огрубление ×8 одним скачком", + "expect": "скачок ×8 против цепочки ×2×2×2", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "4", + "--nx", + "240", + "--ny", + "160", + "--body-x", + "80", + "--re", + "2000", + "--wall", + "bouzidi", + "--levels", + "8:1.5,10,2", + "--steps", + "48000", + "--backend", + "gpu" + ], + "nodes_per_step": 419208, + "steps": 48000, + "cost": 20121984000, + "outputs": [], + "shard": 4 + }, + { + "id": "K24_re2000_chain8", + "group": "K", + "title": "Re=2000, огрубление ×8 цепочкой ×2×2×2", + "expect": "плавный переход при сильном огрублении", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "4", + "--nx", + "240", + "--ny", + "160", + "--body-x", + "80", + "--re", + "2000", + "--wall", + "bouzidi", + "--levels", + "2:8,25,8; 2:4,15,4; 2:1.5,10,2", + "--steps", + "48000", + "--backend", + "gpu" + ], + "nodes_per_step": 644958, + "steps": 48000, + "cost": 30957984000, + "outputs": [], + "shard": 5 + }, + { + "id": "K25_re2000_sponge5", + "group": "K", + "title": "Re=2000, боковые губки 5D", + "expect": "гасят ли губки отражения от боковых стенок", + "backend": "gpu", + "args": [ + "--shape", + "cylinder", + "--size", + "8", + "--nx", + "480", + "--ny", + "320", + "--body-x", + "160", + "--re", + "2000", + "--wall", + "bouzidi", + "--levels", + "2:4,15,4; 2:1.5,10,2", + "--sponge-side", + "40", + "--steps", + "96000", + "--backend", + "gpu" + ], + "nodes_per_step": 422694, + "steps": 96000, + "cost": 40578624000, + "outputs": [], + "shard": 4 } ] } \ No newline at end of file diff --git a/docs/theory/2d_solver/docker-compose.server.yml b/docs/theory/2d_solver/docker-compose.server.yml index 6522d1a..146b4f9 100644 --- a/docs/theory/2d_solver/docker-compose.server.yml +++ b/docs/theory/2d_solver/docker-compose.server.yml @@ -22,7 +22,7 @@ name: kbc2d # ── общая часть всех сервисов ──────────────────────────────────────────────── x-kbc2d: &kbc2d - image: notbigghost/kbc2d:1.3.0 + image: notbigghost/kbc2d:1.4.0 pull_policy: missing volumes: - ./out:/work/bench/out diff --git a/docs/theory/2d_solver/docker-compose.shards.yml b/docs/theory/2d_solver/docker-compose.shards.yml index f8835ad..c35b06a 100644 --- a/docs/theory/2d_solver/docker-compose.shards.yml +++ b/docs/theory/2d_solver/docker-compose.shards.yml @@ -15,7 +15,7 @@ # * драйвер видеокарты под Windows — Vulkan внутри контейнера транслируется в D3D12 (dzn); # * Docker Desktop с бэкендом WSL2 (по умолчанию так и есть) либо Docker внутри WSL2. # -# Доли посчитаны так, чтобы на ОДИНАКОВЫХ картах закончиться одновременно (≈24 ч каждая при +# Доли посчитаны так, чтобы на ОДИНАКОВЫХ картах закончиться одновременно (≈27 ч каждая при # 1200 MLUPS, с поправкой на размер сетки — см. bench/gen_scenarios.py). На разных картах # закончатся в разное время. Результаты — в ./out рядом с этим файлом; собрать их с пяти # машин = скопировать все out/ в одну папку (каталоги прогонов не пересекаются, у каждой доли @@ -26,13 +26,13 @@ name: kbc2d x-kbc2d: &kbc2d - image: notbigghost/kbc2d:1.3.0 + image: notbigghost/kbc2d:1.4.0 # Если образа нет ни локально, ни в реестре — `docker compose -f docker-compose.shards.yml # build` соберёт его из каталога с этим файлом (нужны исходники). build: context: . args: - VERSION: "1.3.0" + VERSION: "1.4.0" pull_policy: missing devices: # сама видеокарта (WDDM); есть в любом WSL2, в том числе внутри Docker Desktop diff --git a/docs/theory/2d_solver/docker-compose.wsl.yml b/docs/theory/2d_solver/docker-compose.wsl.yml index b38f563..9c1da55 100644 --- a/docs/theory/2d_solver/docker-compose.wsl.yml +++ b/docs/theory/2d_solver/docker-compose.wsl.yml @@ -32,14 +32,14 @@ name: kbc2d # ── общая часть всех сервисов ──────────────────────────────────────────────── x-kbc2d: &kbc2d - image: notbigghost/kbc2d:1.3.0 + image: notbigghost/kbc2d:1.4.0 # Контекст сборки — каталог с этим файлом. Если образа нет ни локально, ни в реестре, # достаточно `docker compose -f docker-compose.wsl.yml build`: доступ к Docker Hub # для запуска не обязателен. build: context: . args: - VERSION: "1.3.0" + VERSION: "1.4.0" pull_policy: missing devices: - /dev/dxg:/dev/dxg diff --git a/docs/theory/2d_solver/src/cpu.rs b/docs/theory/2d_solver/src/cpu.rs index b2e2fa0..e02a62a 100644 --- a/docs/theory/2d_solver/src/cpu.rs +++ b/docs/theory/2d_solver/src/cpu.rs @@ -8,8 +8,10 @@ use rayon::prelude::*; -use crate::math::{self, Kbc, Link, LinkKind, Scene, WallModel, MAX_BODY_BUCKETS, R, CX, CY, OPP, Q}; -use crate::{Case, Collision, FieldKind, Spec, StepRec}; +use crate::math::{ + self, Kbc, Link, LinkKind, Scene, WallFunction, WallModel, MAX_BODY_BUCKETS, R, CX, CY, OPP, Q, W, +}; +use crate::{Case, Collision, FieldKind, PatchSpec, Spec, StepRec}; // ───────────────────────────────────────────────────────────────────────────── // Стартовые поля @@ -186,6 +188,8 @@ pub struct Geom { pub links: Vec, /// Индекс по граничным узлам: условие Града ставится сразу на весь узел, а не полинково. pub wall_nodes: Vec, + /// Геометрия пристеночной функции, параллельно `wall_nodes` (тот же индекс). + pub wf_nodes: Vec, /// Плечи для момента: координаты узлов относительно центра тела. pub body_cx: R, pub body_cy: R, @@ -214,8 +218,9 @@ impl Geom { } let links = build_links(nx, ny, &solid, &phi, &owner); let wall_nodes = build_wall_nodes(&links); + let wf_nodes = build_wf_nodes(nx, ny, &solid, &wall_nodes, scene); let (bcx, bcy) = scene.center(); - Geom { nx, ny, solid, links, wall_nodes, body_cx: bcx, body_cy: bcy } + Geom { nx, ny, solid, links, wall_nodes, wf_nodes, body_cx: bcx, body_cy: bcy } } } @@ -320,6 +325,82 @@ pub struct WallNode { pub count: u8, } +/// Геометрия пристеночной функции одного граничного узла (Malaspinas, Sagaut 2014): +/// нормаль к стенке, расстояние от неё до узла и до точки отбора скорости, отстоящей от узла +/// на одну клетку вдоль нормали, и билинейный стенсиль этой точки. Строится один раз; GPU +/// загружает её как есть — топология, как и линки, существует в единственном экземпляре. +#[derive(Clone, Copy, Debug)] +pub struct WfNode { + /// Единичная нормаль, направленная из тела в жидкость (∇φ). + pub nx: R, + pub ny: R, + /// Расстояние от стенки до граничного узла. + pub y_w: R, + /// Расстояние от стенки до точки отбора. + pub y_s: R, + /// Узлы и веса билинейного стенсиля точки отбора. Твёрдые узлы стенсиля исключены, веса + /// перенормированы; если жидкого веса почти нет, все веса нулевые — функция в этом + /// узле не применяется, остаётся прилипание. + pub st: [u32; 4], + pub w: [R; 4], +} + +impl WfNode { + pub fn valid(&self) -> bool { + self.w.iter().any(|v| *v > 0.0) + } +} + +fn build_wf_nodes( + nx: usize, + ny: usize, + solid: &[bool], + wall_nodes: &[WallNode], + scene: &Scene, +) -> Vec { + // SDF тел точный, поэтому нормаль — его градиент центральной разностью + let h = 0.25; + wall_nodes + .iter() + .map(|wn| { + let node = wn.node as usize; + let (x, y) = ((node % nx) as R, (node / nx) as R); + let gx = scene.sdf(x + h, y) - scene.sdf(x - h, y); + let gy = scene.sdf(x, y + h) - scene.sdf(x, y - h); + let gn = (gx * gx + gy * gy).sqrt(); + let (ex, ey) = if gn > 1e-12 { (gx / gn, gy / gn) } else { (0.0, 1.0) }; + let y_w = scene.sdf(x, y).max(1e-3); + let (sx, sy) = (x + ex, y + ey); + let y_s = scene.sdf(sx, sy); + let mut out = WfNode { nx: ex, ny: ey, y_w, y_s, st: [0; 4], w: [0.0; 4] }; + let (x0, y0) = (sx.floor(), sy.floor()); + if y_s <= y_w || x0 < 0.0 || y0 < 0.0 || x0 + 1.0 >= nx as R || y0 + 1.0 >= ny as R { + return out; + } + let (tx, ty) = (sx - x0, sy - y0); + let (x0, y0) = (x0 as usize, y0 as usize); + let idx = [y0 * nx + x0, y0 * nx + x0 + 1, (y0 + 1) * nx + x0, (y0 + 1) * nx + x0 + 1]; + let wts = [(1.0 - tx) * (1.0 - ty), tx * (1.0 - ty), (1.0 - tx) * ty, tx * ty]; + let mut sum = 0.0; + for k in 0..4 { + out.st[k] = idx[k] as u32; + if !solid[idx[k]] { + out.w[k] = wts[k]; + sum += wts[k]; + } + } + if sum < 0.5 { + out.w = [0.0; 4]; + } else { + for v in out.w.iter_mut() { + *v /= sum; + } + } + out + }) + .collect() +} + fn build_wall_nodes(links: &[Link]) -> Vec { let mut out: Vec = Vec::new(); for (k, l) in links.iter().enumerate() { @@ -345,7 +426,7 @@ pub struct Level { pub post: Vec<[R; Q]>, /// Энтропийный стабилизатор поузлово (для диагностики и картинки). pub gamma: Vec, - /// β по столбцам x (губка делает его полем); длина nx. + /// β по узлам (губки делают его полем); длина nx·ny. pub beta: Vec, pub geom: Geom, } @@ -372,7 +453,6 @@ impl Level { /// (ГУ Bouzidi перезаписывает всё, что могло бы прийти из тела в жидкость), а счёт там /// только жжёт такты и способен родить NaN при экстремальных режимах. fn collide(&mut self, op: Collision, model: math::KbcModel) -> Stats { - let nx = self.nx; let beta = &self.beta; let solid = &self.geom.solid; let post = &mut self.post; @@ -388,7 +468,7 @@ impl Level { *g = 2.0; return Stats::EMPTY; } - let b = beta[node % nx]; + let b = beta[node]; let k: Kbc = match op { Collision::Kbc => math::collide_node(p, b, model), Collision::Bgk => math::collide_node_bgk(p, b), @@ -426,6 +506,67 @@ impl Level { } } + /// Пристеночная функция граничного узла `wi` по текущему полю: скорость в точке отбора — + /// билинейно по пост-столкновительному полю (то есть на момент t, как и у Града). + fn wall_fn_at(&self, wi: usize) -> Option { + let g = &self.geom.wf_nodes[wi]; + if !g.valid() { + return None; + } + let (mut ux, mut uy) = (0.0, 0.0); + for k in 0..4 { + if g.w[k] > 0.0 { + let (a, b) = self.u_at_t(g.st[k] as usize); + ux += g.w[k] * a; + uy += g.w[k] * b; + } + } + let node = self.geom.wall_nodes[wi].node as usize; + let nu = math::nu_of_beta(self.beta[node]); + math::wall_function(ux, uy, g.nx, g.ny, g.y_w, g.y_s, nu) + } + + /// Bouzidi с пристеночной функцией: стенка получает касательную скорость скольжения, + /// согласованную по напряжению (см. `math::wall_slip`), и входит в отскок стандартной + /// поправкой подвижной стенки + /// δ = 2W_iρ(c_ī·u_s)/c_s² (Lallemand, Luo 2003); для q ≥ ½ поправка делится на 2q. + fn apply_bouzidi_wf(&mut self) { + for wi in 0..self.geom.wall_nodes.len() { + let wn = self.geom.wall_nodes[wi]; + let node = wn.node as usize; + let (usx, usy) = match self.wall_fn_at(wi) { + Some(w) => { + let g = &self.geom.wf_nodes[wi]; + let nu = math::nu_of_beta(self.beta[node]); + let s = math::wall_slip(&w, g.y_w, nu); + (s * w.ex, s * w.ey) + } + None => (0.0, 0.0), + }; + let rho = math::macros(&self.post[node]).0; + let lo = wn.first as usize; + for l in &self.geom.links[lo..lo + wn.count as usize] { + let i = l.i as usize; + let ib = l.ib as usize; + let fi = self.post[node][i]; + // c_ī = −c_i + let delta = + -2.0 * W[i] * rho * (CX[i] as R * usx + CY[i] as R * usy) / math::CS2; + let v = match l.kind { + LinkKind::Near => { + 2.0 * l.q * fi + (1.0 - 2.0 * l.q) * self.post[l.far as usize][i] + delta + } + LinkKind::Far => { + let h = 1.0 / (2.0 * l.q); + h * fi + (1.0 - h) * self.post[node][ib] + h * delta + } + LinkKind::Simple => fi + delta, + }; + self.f[node][ib] = v; + } + } + } + /// Интерполированный отскок Bouzidi по всем линкам тела. /// `f` — поле ПОСЛЕ переноса (его правим), `post` — ПОСЛЕ столкновения (до переноса). fn apply_bouzidi(&mut self) { @@ -459,8 +600,7 @@ impl Level { /// /// после чего недостающие популяции собираются приближением Града (2.13). Скорости /// соседей и градиенты берутся с прошлого шага — см. `u_prev`. - fn apply_moment_wall(&mut self, third_order: bool) { - let nx = self.nx; + fn apply_moment_wall(&mut self, third_order: bool, wf: WallFunction) { // стенка неподвижна; для подвижного тела сюда пойдёт её скорость на линке, // и добавится динамическая часть плотности (B 4) let (uwx, uwy) = (0.0, 0.0); @@ -497,7 +637,21 @@ impl Level { rho += if missing[i] { self.post[node][OPP[i]] } else { self.f[node][i] }; } - let (dudx, dudy, dvdx, dvdy) = self.grad_u_at_t(node); + let (mut dudx, mut dudy, mut dvdx, mut dvdy) = self.grad_u_at_t(node); + // Пристеночная функция: касательная составляющая целевой скорости и нормальная + // производная касательной скорости в тензоре давлений берутся из закона стенки, + // нормальная составляющая скорости и плотность (B 3) — прежние. + if wf.is_on() { + if let Some(w) = self.wall_fn_at(wi) { + let g = &self.geom.wf_nodes[wi]; + let un = ux * g.nx + uy * g.ny; + ux = un * g.nx + w.u_node * w.ex; + uy = un * g.ny + w.u_node * w.ey; + let nu = math::nu_of_beta(self.beta[node]); + (dudx, dudy, dvdx, dvdy) = + math::wall_function_gradient((dudx, dudy, dvdx, dvdy), &w, g.nx, g.ny, nu); + } + } let g = math::moment_wall( rho, ux, @@ -506,7 +660,7 @@ impl Level { dudy, dvdx, dvdy, - self.beta[node % nx], + self.beta[node], third_order, ); for i in 0..Q { @@ -523,7 +677,7 @@ impl Level { /// что было в f до переноса, то есть u(x, t) — именно то, что требует прил. B. Отдельное /// хранилище «поля предыдущего шага» при этом не нужно. #[inline] - fn u_at_t(&self, node: usize) -> (R, R) { + pub fn u_at_t(&self, node: usize) -> (R, R) { let (_, a, b) = math::macros(&self.post[node]); (a, b) } @@ -553,11 +707,12 @@ impl Level { } /// Замкнуть недостающие популяции выбранной моделью стенки. - fn apply_wall(&mut self, model: WallModel) { + fn apply_wall(&mut self, model: WallModel, wf: WallFunction) { match model { + WallModel::Bouzidi if wf.is_on() => self.apply_bouzidi_wf(), WallModel::Bouzidi => self.apply_bouzidi(), - WallModel::Grad => self.apply_moment_wall(false), - WallModel::Hrr => self.apply_moment_wall(true), + WallModel::Grad => self.apply_moment_wall(false, wf), + WallModel::Hrr => self.apply_moment_wall(true, wf), WallModel::Staircase => self.apply_staircase(), } } @@ -729,12 +884,7 @@ pub struct Ghost { /// Неравновесная часть при смене уровня масштабируется: f^neq ∝ τ·δt, поэтому /// коэффициент грубый→тонкий равен R01 = τ_f/(r·τ_c), обратно — 1/R01. pub struct Patch { - pub ax: usize, - pub bx: usize, - pub ay: usize, - pub by: usize, pub r: usize, - pub nfx: usize, pub r01: R, ghosts: Vec, /// Пары (узел L0, узел L1) для рестрикции — только внутренние жидкие узлы перекрытия. @@ -753,9 +903,9 @@ impl Patch { &self.restrict } - pub fn new(spec: &Spec, coarse: &Geom, fine_solid: &[bool], r01: R) -> Patch { - let (ax, bx, ay, by) = spec.patch.expect("патч запрошен без границ"); - let r = spec.refine; + pub fn new(ps: &PatchSpec, coarse: &Geom, fine_solid: &[bool], r01: R) -> Patch { + let (ax, bx, ay, by) = (ps.ax, ps.bx, ps.ay, ps.by); + let r = ps.r; let nfx = r * (bx - ax) + 1; let nfy = r * (by - ay) + 1; let cnx = coarse.nx; @@ -806,7 +956,7 @@ impl Patch { } // рамка — ровно периметр тонкого поля: два ряда по nfx плюс два столбца без углов assert_eq!(ghosts.len(), 2 * nfx + 2 * (nfy - 2)); - Patch { ax, bx, ay, by, r, nfx, r01, ghosts, restrict } + Patch { r, r01, ghosts, restrict } } /// Ghost-значения из грубого поля: равновесие по интерполированным ρ, u плюс @@ -885,31 +1035,72 @@ impl Patch { pub struct Sim { pub spec: Spec, - pub l0: Level, - pub l1: Option, - pub patch: Option, - /// Состояние L0 до столкновения — «старый» край для временной интерполяции рамки. - pre: Vec<[R; Q]>, - gh_old: Vec<[R; Q]>, - gh_new: Vec<[R; Q]>, + /// Уровни от L0 (весь домен) к самому тонкому. + pub levels: Vec, + /// `patches[k]` связывает уровень k с уровнем k + 1. + pub patches: Vec, + /// Состояние уровня k до столкновения — «старый» край для временной интерполяции рамки + /// уровня k + 1. Заведено только у уровней, у которых есть потомок. + pre: Vec>, + gh_old: Vec>, + gh_new: Vec>, fluid_count: R, + /// Зонд следа: уровень и узел на нём — самый тонкий уровень, накрывающий точку. + probe_level: usize, probe_node: usize, - probe_on_fine: bool, pub step_index: u64, } +/// Сводка пристеночной функции по граничным узлам самого тонкого уровня. +#[derive(Clone, Copy, Debug, Default)] +pub struct YPlus { + /// Граничных узлов, где функция определена (есть точка отбора и касательный поток). + pub nodes: usize, + pub mean: R, + pub max: R, + /// Доля узлов с y⁺ > 30 — там первая клетка лежит в логарифмической зоне. + pub frac_log: R, + pub u_tau_mean: R, +} + +/// y⁺ первого узла по полю скорости `u(node)` уровня с геометрией `geom` и вязкостью `nu`. +/// Общая для обоих бэкендов: GPU скачивает поле и считает здесь же. +pub fn yplus_stats(geom: &Geom, nu: R, u: impl Fn(usize) -> (R, R)) -> YPlus { + let mut y = YPlus::default(); + for g in &geom.wf_nodes { + if !g.valid() { + continue; + } + let (mut ux, mut uy) = (0.0, 0.0); + for k in 0..4 { + if g.w[k] > 0.0 { + let (a, b) = u(g.st[k] as usize); + ux += g.w[k] * a; + uy += g.w[k] * b; + } + } + if let Some(w) = math::wall_function(ux, uy, g.nx, g.ny, g.y_w, g.y_s, nu) { + y.nodes += 1; + y.mean += w.y_plus; + y.max = y.max.max(w.y_plus); + y.u_tau_mean += w.u_tau; + if w.y_plus > 30.0 { + y.frac_log += 1.0; + } + } + } + if y.nodes > 0 { + let n = y.nodes as R; + y.mean /= n; + y.u_tau_mean /= n; + y.frac_log /= n; + } + y +} + impl Sim { pub fn new(spec: Spec) -> Sim { - let (nx, ny) = (spec.nx, spec.ny); - let geom0 = Geom::build(nx, ny, &spec.scene); - - let beta0 = math::beta_profile( - nx, - spec.beta0, - spec.sponge_in, - spec.sponge_len, - spec.sponge_mult, - ); + let lv = spec.levels(); // стартовое поле: либо сразу набегающий поток, либо покой let u0 = if spec.init_uniform { @@ -918,67 +1109,78 @@ impl Sim { } else { (0.0, 0.0) }; - let init0 = initial_field(&Init { - case: spec.case, - nx, - ny, - u0, - beta: spec.beta0, - scene: Some(&spec.scene), - taper: if spec.init_uniform { spec.init_taper } else { 0.0 }, - }); - let l0 = Level::new(nx, ny, beta0, geom0, init0); - let fluid_count = l0.geom.solid.iter().filter(|s| !**s).count() as R; - let (l1, patch) = if spec.refine > 1 { - let (ax, bx, ay, by) = spec.patch.expect("refine > 1 требует патч"); - let r = spec.refine; - let scene1 = spec.scene.refined(r as R, ax as R, ay as R); - let nfx = r * (bx - ax) + 1; - let nfy = r * (by - ay) + 1; - let geom1 = Geom::build(nfx, nfy, &scene1); - // τ_f = r(τ_c − ½) + ½ ⇒ одинаковая ν на обоих уровнях - let tau0 = 1.0 / (2.0 * spec.beta0); - let tau1 = r as R * (tau0 - 0.5) + 0.5; - let beta1 = 1.0 / (2.0 * tau1); - let r01 = tau1 / (r as R * tau0); - let patch = Patch::new(&spec, &l0.geom, &geom1.solid, r01); - let init1 = initial_field(&Init { + let mut levels: Vec = Vec::with_capacity(lv.len()); + let mut patches: Vec = Vec::with_capacity(spec.patches.len()); + for (k, g) in lv.iter().enumerate() { + let scene = spec.level_scene(g); + let geom = Geom::build(g.nx, g.ny, &scene); + // губки живут только на L0; тонкие уровни однородны, β_k = 1/(2τ_k) + let (beta, beta_init) = if k == 0 { + ( + math::beta_field( + g.nx, + g.ny, + spec.beta0, + spec.sponge_in, + spec.sponge_len, + spec.sponge_side, + spec.sponge_mult, + ), + spec.beta0, + ) + } else { + let b = 1.0 / (2.0 * g.tau); + (vec![b; g.nx * g.ny], b) + }; + let init = initial_field(&Init { case: spec.case, - nx: nfx, - ny: nfy, + nx: g.nx, + ny: g.ny, u0, - beta: 1.0 / (2.0 * tau1), - scene: Some(&scene1), - taper: if spec.init_uniform { spec.init_taper * r as R } else { 0.0 }, + beta: beta_init, + scene: Some(&scene), + taper: if spec.init_uniform { spec.init_taper * g.scale } else { 0.0 }, }); - (Some(Level::new(nfx, nfy, vec![beta1; nfx], geom1, init1)), Some(patch)) - } else { - (None, None) - }; - - // зонд следа: берём с тонкой сетки, если точка внутри патча (меньше численного - // размытия вихрей → чище спектр и St), иначе с грубой - let (px, py) = spec.probe; - let (probe_node, probe_on_fine) = match &patch { - Some(p) if px >= p.ax && px <= p.bx && py >= p.ay && py <= p.by => { - (((py - p.ay) * p.r) * p.nfx + (px - p.ax) * p.r, true) + if k > 0 { + // f^neq ∝ τ·δt ⇒ коэффициент грубый→тонкий R01 = τ_f/(r·τ_c) + let ps = &spec.patches[k - 1]; + let r01 = g.tau / (ps.r as R * lv[k - 1].tau); + patches.push(Patch::new(ps, &levels[k - 1].geom, &geom.solid, r01)); } - _ => (py * nx + px, false), - }; + levels.push(Level::new(g.nx, g.ny, beta, geom, init)); + } + let fluid_count = levels[0].geom.solid.iter().filter(|s| !**s).count() as R; - let n = nx * ny; + // зонд следа: с самого тонкого уровня, накрывающего точку (меньше численного + // размытия вихрей → чище спектр и St), иначе с грубого + let (mut px, mut py) = spec.probe; + let mut probe_level = 0; + for (k, p) in spec.patches.iter().enumerate() { + if px >= p.ax && px <= p.bx && py >= p.ay && py <= p.by { + px = (px - p.ax) * p.r; + py = (py - p.ay) * p.r; + probe_level = k + 1; + } else { + break; + } + } + let probe_node = py * lv[probe_level].nx + px; + + let nlev = levels.len(); + let pre = (0..nlev) + .map(|k| if k + 1 < nlev { vec![[0.0; Q]; lv[k].nx * lv[k].ny] } else { Vec::new() }) + .collect(); Sim { spec, - l0, - l1, - patch, - pre: vec![[0.0; Q]; n], - gh_old: Vec::new(), - gh_new: Vec::new(), + levels, + patches, + pre, + gh_old: vec![Vec::new(); nlev], + gh_new: vec![Vec::new(); nlev], fluid_count, + probe_level, probe_node, - probe_on_fine, step_index: 0, } } @@ -1007,69 +1209,79 @@ impl Sim { (u * c, uy) } + /// Один шаг уровня k, а за ним — r_k подшагов его потомка (рекурсивно) с временной + /// интерполяцией рамки и проекцией обратно. Сила снимается на самом тонком уровне на + /// каждом его подшаге и копится в `fb`; `nf` — сколько подшагов в неё вошло. + fn advance_level( + &mut self, + k: usize, + inlet: (R, R), + fb: &mut [[R; 3]; MAX_BODY_BUCKETS], + nf: &mut usize, + ) -> Stats { + let has_child = k < self.patches.len(); + let channel = self.spec.case == Case::Channel; + let (wall, wf) = (self.spec.wall, self.spec.wall_fn); + if has_child { + self.pre[k].copy_from_slice(&self.levels[k].f); + } + let stats = self.levels[k].collide(self.spec.collision, self.spec.kbc_model); + self.levels[k].stream(); + // Эталонные течения статей периодичны по обеим осям и ГУ не имеют вовсе: перенос + // уже периодичен, поэтому достаточно ничего не накладывать. Стенки канала, вход и + // выход есть только у L0 — тонкие уровни лежат строго внутри. + if channel { + self.levels[k].apply_wall(wall, wf); + if k == 0 { + self.levels[0].free_slip_walls(); + self.levels[0].channel_bc(inlet.0, inlet.1, 1.0, self.spec.outlet_extrapolate); + } + } + if has_child { + self.patches[k].ghost_values(&self.pre[k], &mut self.gh_old[k]); + self.patches[k].ghost_values(&self.levels[k].f, &mut self.gh_new[k]); + let r = self.patches[k].r; + for s in 0..r { + self.advance_level(k + 1, inlet, fb, nf); + let w = (s + 1) as R / r as R; + self.patches[k].fill(&mut self.levels[k + 1].f, &self.gh_old[k], &self.gh_new[k], w); + } + let (lo, hi) = self.levels.split_at_mut(k + 1); + self.patches[k].restrict_to(&hi[0].f, &mut lo[k].f); + } else { + // силу снимаем на КАЖДОМ подшаге и усредняем — мгновенное значение на + // последнем подшаге даёт лишний шум в рядах при том же среднем + let g = self.levels[k].force(); + for b in 0..MAX_BODY_BUCKETS { + for c in 0..3 { + fb[b][c] += g[b][c]; + } + } + *nf += 1; + } + stats + } + pub fn step(&mut self) -> StepRec { let t = self.step_index; - let (ux_in, uy_in) = self.inlet(t); - let sp_collision = self.spec.collision; - let outlet_extrap = self.spec.outlet_extrapolate; + let inlet = self.inlet(t); - // ── уровень 0 ── - self.pre.copy_from_slice(&self.l0.f); - let model = self.spec.kbc_model; - let wall = self.spec.wall; - let stats0 = self.l0.collide(sp_collision, model); - self.l0.stream(); - // Эталонные течения статей периодичны по обеим осям и ГУ не имеют вовсе: перенос - // уже периодичен, поэтому достаточно ничего не накладывать. - if self.spec.case == Case::Channel { - self.l0.apply_wall(wall); - self.l0.free_slip_walls(); - self.l0.channel_bc(ux_in, uy_in, 1.0, outlet_extrap); - } - - // ── уровень 1: r подшагов с временной интерполяцией рамки ── let mut fb = [[0.0 as R; 3]; MAX_BODY_BUCKETS]; - if let (Some(l1), Some(p)) = (self.l1.as_mut(), self.patch.as_ref()) { - p.ghost_values(&self.pre, &mut self.gh_old); - p.ghost_values(&self.l0.f, &mut self.gh_new); - for s in 0..p.r { - l1.collide(sp_collision, model); - l1.stream(); - if self.spec.case == Case::Channel { - l1.apply_wall(wall); - } - // силу снимаем на КАЖДОМ подшаге и усредняем — мгновенное значение на - // последнем подшаге даёт лишний шум в рядах при том же среднем - let g = l1.force(); - for k in 0..MAX_BODY_BUCKETS { - for c in 0..3 { - fb[k][c] += g[k][c]; - } - } - let w = (s + 1) as R / p.r as R; - p.fill(&mut l1.f, &self.gh_old, &self.gh_new, w); + let mut nf = 0usize; + let stats0 = self.advance_level(0, inlet, &mut fb, &mut nf); + let inv = 1.0 / nf.max(1) as R; + for b in fb.iter_mut() { + for c in b.iter_mut() { + *c *= inv; } - let inv = 1.0 / p.r as R; - for k in 0..MAX_BODY_BUCKETS { - for c in 0..3 { - fb[k][c] *= inv; - } - } - p.restrict_to(&l1.f, &mut self.l0.f); - } else { - fb = self.l0.force(); } // ── диагностика ── - let probe = if self.probe_on_fine { - self.l1.as_ref().unwrap().macros_at(self.probe_node) - } else { - self.l0.macros_at(self.probe_node) - }; + let probe = self.levels[self.probe_level].macros_at(self.probe_node); - let solid = &self.l0.geom.solid; - let (rho_sum, max_u) = self - .l0 + let l0 = &self.levels[0]; + let solid = &l0.geom.solid; + let (rho_sum, max_u) = l0 .f .par_iter() .enumerate() @@ -1082,10 +1294,10 @@ impl Sim { self.step_index += 1; let (mut fx, mut fy, mut tz) = (0.0, 0.0, 0.0); - for k in 0..MAX_BODY_BUCKETS { - fx += fb[k][0]; - fy += fb[k][1]; - tz += fb[k][2]; + for b in &fb { + fx += b[0]; + fy += b[1]; + tz += b[2]; } StepRec { step: t, @@ -1094,6 +1306,7 @@ impl Sim { tz, body: fb, uy_probe: probe.2, + rho_probe: probe.0, rho_mean: rho_sum / self.fluid_count, max_u, gamma_mean: stats0.gamma_mean(), @@ -1104,22 +1317,27 @@ impl Sim { } } + /// y⁺ первого узла на самом тонком уровне по текущему полю. + pub fn yplus(&self) -> YPlus { + let l = self.levels.last().unwrap(); + let tau = self.spec.levels().last().unwrap().tau; + let nu = math::CS2 * (tau - 0.5); + yplus_stats(&l.geom, nu, |n| l.u_at_t(n)) + } + /// Характерный размер тела в единицах того уровня, где снимается сила. pub fn force_ref_size(&self) -> R { - match &self.patch { - Some(p) => self.spec.scene.ref_size() * p.r as R, - None => self.spec.scene.ref_size(), - } + self.spec.scene.ref_size() * self.spec.levels().last().unwrap().scale } /// Поле для картинки: (значение, маска тела) на сетке L0. pub fn sample_field(&self, kind: FieldKind) -> (Vec, &[bool]) { - let (nx, ny) = (self.l0.nx, self.l0.ny); + let (nx, ny) = (self.levels[0].nx, self.levels[0].ny); let mut out = vec![0.0; nx * ny]; match kind { FieldKind::Speed => { for n in 0..nx * ny { - let (_, ux, uy) = self.l0.macros_at(n); + let (_, ux, uy) = self.levels[0].macros_at(n); out[n] = (ux * ux + uy * uy).sqrt(); } } @@ -1128,7 +1346,7 @@ impl Sim { let mut ux = vec![0.0; nx * ny]; let mut uy = vec![0.0; nx * ny]; for n in 0..nx * ny { - let (_, a, b) = self.l0.macros_at(n); + let (_, a, b) = self.levels[0].macros_at(n); ux[n] = a; uy[n] = b; } @@ -1146,21 +1364,21 @@ impl Sim { } FieldKind::Density => { for n in 0..nx * ny { - out[n] = self.l0.macros_at(n).0; + out[n] = self.levels[0].macros_at(n).0; } } - FieldKind::Gamma => out.copy_from_slice(&self.l0.gamma), + FieldKind::Gamma => out.copy_from_slice(&self.levels[0].gamma), } - (out, &self.l0.geom.solid) + (out, &self.levels[0].geom.solid) } /// Полное поле скорости уровня L0 — для метрик эталонных течений и радиуса влияния. pub fn sample_velocity(&self) -> (Vec, Vec) { - let n = self.l0.nx * self.l0.ny; + let n = self.levels[0].nx * self.levels[0].ny; let mut ux = Vec::with_capacity(n); let mut uy = Vec::with_capacity(n); for k in 0..n { - let (_, a, b) = self.l0.macros_at(k); + let (_, a, b) = self.levels[0].macros_at(k); ux.push(a); uy.push(b); } @@ -1171,12 +1389,12 @@ impl Sim { /// срезов складывается x–t диаграмма, по которой видно, бежит возмущение со скоростью /// звука или конвекции и есть ли стоячие узлы. pub fn sample_centerline(&self) -> (Vec, Vec) { - let y = self.l0.ny / 2; - let nx = self.l0.nx; + let y = self.levels[0].ny / 2; + let nx = self.levels[0].nx; let mut rho = Vec::with_capacity(nx); let mut ux = Vec::with_capacity(nx); for x in 0..nx { - let (r, a, _) = self.l0.macros_at(y * nx + x); + let (r, a, _) = self.levels[0].macros_at(y * nx + x); rho.push(r); ux.push(a); } @@ -1185,7 +1403,7 @@ impl Sim { /// Есть ли в поле NaN/inf — признак развала счёта. pub fn is_finite(&self) -> bool { - self.l0.f.par_iter().all(|c| c.iter().all(|v| v.is_finite())) + self.levels[0].f.par_iter().all(|c| c.iter().all(|v| v.is_finite())) } } @@ -1201,13 +1419,14 @@ mod tests { Level::new( nx, ny, - vec![0.5; nx], + vec![0.5; nx * ny], Geom { nx, ny, solid: vec![false; nx * ny], links: Vec::new(), wall_nodes: Vec::new(), + wf_nodes: Vec::new(), body_cx: 0.0, body_cy: 0.0, }, diff --git a/docs/theory/2d_solver/src/gif.rs b/docs/theory/2d_solver/src/gif.rs index b880e29..2dfe8b5 100644 --- a/docs/theory/2d_solver/src/gif.rs +++ b/docs/theory/2d_solver/src/gif.rs @@ -382,7 +382,8 @@ pub struct GifWriter { h: usize, range: Range, hud: Hud, - patch: Option<(usize, usize, usize, usize)>, + /// Контуры патчей измельчения в клетках L0, от внешнего к внутреннему. + patches: Vec<(usize, usize, usize, usize)>, dither: DelayDither, pub frames: u32, } @@ -399,7 +400,7 @@ impl GifWriter { range: Range, plan: GifPlan, hud: Hud, - patch: Option<(usize, usize, usize, usize)>, + patches: Vec<(usize, usize, usize, usize)>, ) -> std::io::Result { let scale = scale.max(1); let down = down.max(1); @@ -426,7 +427,7 @@ impl GifWriter { h, range, hud, - patch, + patches, dither: DelayDither::default(), frames: 0, }) @@ -483,7 +484,7 @@ impl GifWriter { } } } - if let Some((ax, bx, ay, by)) = self.patch { + for &(ax, bx, ay, by) in &self.patches { let d = self.down; self.draw_patch_outline(&mut buf, ax / d, bx / d, ay / d, by / d); } diff --git a/docs/theory/2d_solver/src/gpu.rs b/docs/theory/2d_solver/src/gpu.rs index 3bb15fc..9bb5c09 100644 --- a/docs/theory/2d_solver/src/gpu.rs +++ b/docs/theory/2d_solver/src/gpu.rs @@ -72,7 +72,9 @@ struct Dyn { slot: u32, /// Режим стенки: 0 — Bouzidi, 1 — Град, 2 — HRR, 3 — простой отскок. wall_mode: u32, - _pad: [u32; 2], + /// 1 — пристеночная функция Spalding поверх модели стенки. + wf: u32, + _pad: u32, } #[repr(C)] @@ -106,7 +108,21 @@ struct GLink { kind: u32, q: f32, body: u32, - _p: u32, + /// Индекс граничного узла, которому принадлежит линк (нужен Bouzidi с пристеночной + /// функцией: скольжение считается на узел, а применяется полинково). + wnode: u32, +} + +/// Геометрия пристеночной функции граничного узла — копия `cpu::WfNode` в f32. +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +struct GWf { + nx: f32, + ny: f32, + yw: f32, + ys: f32, + st: [u32; 4], + w: [f32; 4], } #[repr(C)] @@ -140,6 +156,9 @@ struct Results { xineg: f32, /// Сила и момент по телам: [b*3 + c]. body: [f32; 12], + /// Плотность в зонде. + rho_probe: f32, + _pad: [f32; 3], } // ───────────────────────────────────────────────────────────────────────────── @@ -150,10 +169,11 @@ const SHADER: &str = r#" // ───── структуры (обязаны совпадать с gpu.rs) ───── struct LevelParams { n:u32, nx:u32, ny:u32, nlinks:u32, bcx:f32, bcy:f32, probe_node:u32, flags:u32, nwall:u32, stride:u32, lp2:u32, lp3:u32 }; // wall_mode: 0 — Bouzidi, 1 — Град, 2 — HRR, 3 — простой отскок -struct Dyn { ux_in:f32, uy_in:f32, rho_out:f32, kbc_model:u32, outlet_extrap:u32, nparts:u32, refine:u32, collision:u32, slot:u32, wall_mode:u32, dp2:u32, dp3:u32 }; +struct Dyn { ux_in:f32, uy_in:f32, rho_out:f32, kbc_model:u32, outlet_extrap:u32, nparts:u32, refine:u32, collision:u32, slot:u32, wall_mode:u32, wf:u32, dp3:u32 }; struct Substep { idx:u32, p0:u32, p1:u32, p2:u32 }; struct AmrParams { nghost:u32, nrestrict:u32, cstride:u32, fstride:u32, r01:f32, rfc:f32, w:f32, pad:f32 }; -struct GLink { node:u32, far:u32, i:u32, ib:u32, kind:u32, q:f32, body:u32, p1:u32 }; +struct GLink { node:u32, far:u32, i:u32, ib:u32, kind:u32, q:f32, body:u32, wnode:u32 }; +struct GWf { nx:f32, ny:f32, yw:f32, ys:f32, st:vec4, w:vec4 }; struct GGhost { fine:u32, c00:u32, c10:u32, c01:u32, c11:u32, tx:f32, ty:f32, p0:u32 }; struct Partial { rho:f32, maxu:f32, gsum:f32, gmin:f32, gmax:f32, cnt:f32, degen:f32, xineg:f32 }; @@ -182,6 +202,10 @@ struct Partial { rho:f32, maxu:f32, gsum:f32, gmin:f32, gmax:f32, cnt:f32, degen // (узел, смещение первого линка, число линков, —). Линки одного узла лежат подряд. @group(3) @binding(0) var wnodes : array>; +// геометрия пристеночной функции, параллельно wnodes +@group(3) @binding(1) var wfs : array; +// скольжение стенки на узел для Bouzidi с пристеночной функцией: (u_sx, u_sy, rho, —) +@group(3) @binding(2) var slips : array>; const GREL: f32 = 1e-8; const CS2 : f32 = 0.3333333333; @@ -190,9 +214,9 @@ const CS2 : f32 = 0.3333333333; // рабочие слоты силы по подшагам. История нужна, чтобы не синхронизироваться с устройством // на каждом шаге: результаты копятся и читаются пачкой (см. HIST в gpu.rs). const HIST: u32 = 128u; -const SLOT: u32 = 24u; // чисел на шаг: 12 общих + 4 тела по 3 +const SLOT: u32 = 28u; // чисел на шаг: 12 общих + 4 тела по 3 + ρ зонда + 3 запасных const MAXB: u32 = 4u; // вёдер силы по телам -const FORCE_BASE: u32 = 3072u; // = HIST*SLOT +const FORCE_BASE: u32 = 3584u; // = HIST*SLOT // Компенсированное сложение Кэхена–Ноймайера: возвращает (сумма, накопленная поправка). // Наивная сумма по 10^5…10^7 значений в f32 съедает ~log2(N) бит; здесь потеря не копится, @@ -333,7 +357,7 @@ fn k_collide(@builtin(global_invocation_id) gid: vec3, gam[nd] = 2.0; return; } - var r = collide9(fv, beta[nd % P.nx], D.collision, D.kbc_model); + var r = collide9(fv, beta[nd], D.collision, D.kbc_model); for (var i = 0u; i < 9u; i = i + 1u) { pset(i, nd, r.fv[i]); } gam[nd] = r.gamma; } @@ -499,7 +523,7 @@ fn k_stats1(@builtin(global_invocation_id) gid: vec3, // вырожденный узел помечен ровно gamma = 2 (см. k_collide) p.degen = select(0.0, 1.0, g == 2.0); // объёмная вязкость модели D: xi = cs^2 (1/(gamma*beta) - 1/2) - let b = beta[nd % P.nx]; + let b = beta[nd]; // при следе в сдвиговой части (N2) объёмная вязкость равна сдвиговой и всегда > 0 var xi = CS2*(1.0/(b + b) - 0.5); if (D.kbc_model == 0u) { xi = CS2*(1.0/(g*b) - 0.5); } @@ -511,6 +535,7 @@ fn k_stats1(@builtin(global_invocation_id) gid: vec3, load9(&fv, nd); let m = macros9(fv); results[D.slot*SLOT + 3u] = m.z; + results[D.slot*SLOT + 24u] = m.x; } wp[t] = p; workgroupBarrier(); @@ -739,14 +764,143 @@ fn k_moment_wall(@builtin(global_invocation_id) gid: vec3, else { rho = rho + fget(i, node); } } - let g4 = grad_u_at_t(node); + var g4 = grad_u_at_t(node); + // пристеночная функция: касательная часть целевой скорости и нормальная производная + // касательной скорости — из закона стенки (построчный перенос cpu.rs) + if (D.wf == 1u) { + let o = wf_at(w, node); + if (o.ok) { + let gw = wfs[w]; + let un = ux*gw.nx + uy*gw.ny; + ux = un*gw.nx + o.unode*o.ex; + uy = un*gw.ny + o.unode*o.ey; + let nu = CS2*(1.0/(2.0*beta[node]) - 0.5); + let gen = o.ex*(g4.x*gw.nx + g4.y*gw.ny) + o.ey*(g4.z*gw.nx + g4.w*gw.ny); + let d = o.utau*o.utau/nu - gen; + g4 = vec4(g4.x + d*o.ex*gw.nx, g4.y + d*o.ex*gw.ny, + g4.z + d*o.ey*gw.nx, g4.w + d*o.ey*gw.ny); + } + } var g = moment_wall9(rho, ux, uy, g4.x, g4.y, g4.z, g4.w, - beta[node % P.nx], select(0u, 1u, D.wall_mode == 2u)); + beta[node], select(0u, 1u, D.wall_mode == 2u)); for (var i = 0u; i < 9u; i = i + 1u) { if (missing[i] == 1u) { fset(i, node, g[i]); } } } + +// ───── пристеночная функция Spalding (построчный перенос math.rs) ───── + +const KAPPA: f32 = 0.41; +const LOGB : f32 = 5.2; + +// (y⁺, dy⁺/du⁺) по закону Spalding +fn sp_y(up: f32) -> vec2 { + let k = KAPPA*up; + let e = exp(-KAPPA*LOGB); + let ek = exp(k); + let y = up + e*(ek - 1.0 - k - 0.5*k*k - k*k*k/6.0); + let dy = 1.0 + e*KAPPA*(ek - 1.0 - k - 0.5*k*k); + return vec2(y, dy); +} + +fn sp_uplus(yp: f32) -> f32 { + if (yp <= 0.0) { return 0.0; } + var u = yp; + if (yp >= 11.0) { u = max(log(yp)/KAPPA + LOGB, 1e-6); } + for (var it = 0u; it < 30u; it = it + 1u) { + let g = sp_y(u); + let du = (g.x - yp)/g.y; + u = max(u - du, 0.0); + if (abs(du) <= 1e-6*(1.0 + u)) { break; } + } + return u; +} + +fn sp_utau(u: f32, y: f32, nu: f32) -> f32 { + if (u <= 1e-12 || y <= 0.0) { return 0.0; } + let re = y*u/nu; + var x = sqrt(re); + for (var it = 0u; it < 40u; it = it + 1u) { + let g = sp_y(x); + let dx = (x*g.x - re)/(g.x + x*g.y); + x = max(x - dx, 1e-9); + if (abs(dx) <= 1e-6*(1.0 + x)) { break; } + } + return u/x; +} + +struct WfOut { ok: bool, unode: f32, utau: f32, ex: f32, ey: f32, us: f32 }; + +// пристеночная функция граничного узла w (узел node): скорость в точке отбора — билинейно +// по пост-столкновительному полю, то есть на момент t +fn wf_at(w: u32, node: u32) -> WfOut { + var o: WfOut; + o.ok = false; o.unode = 0.0; o.utau = 0.0; o.ex = 0.0; o.ey = 0.0; o.us = 0.0; + let g = wfs[w]; + if (g.w.x + g.w.y + g.w.z + g.w.w <= 0.0) { return o; } + var u = vec2(0.0, 0.0); + for (var k = 0u; k < 4u; k = k + 1u) { + if (g.w[k] > 0.0) { u = u + g.w[k]*u_at_t(g.st[k]); } + } + let un = u.x*g.nx + u.y*g.ny; + let t = vec2(u.x - un*g.nx, u.y - un*g.ny); + let ut = length(t); + if (ut <= 1e-12) { return o; } + let nu = CS2*(1.0/(2.0*beta[node]) - 0.5); + o.utau = sp_utau(ut, g.ys, nu); + o.unode = o.utau*sp_uplus(g.yw*o.utau/nu); + o.ex = t.x/ut; o.ey = t.y/ut; o.us = ut; o.ok = true; + return o; +} + +// скольжение стенки на каждый граничный узел (для Bouzidi с пристеночной функцией), +// согласованное по напряжению +@compute @workgroup_size(64) +fn k_wf_slip(@builtin(global_invocation_id) gid: vec3, + @builtin(num_workgroups) nwg: vec3) { + let w = lin(gid, nwg); + if (w >= P.nwall) { return; } + let node = wnodes[w].x; + var ff: array; + for (var i = 0u; i < 9u; i = i + 1u) { ff[i] = pget(i, node); } + let rho = macros9(ff).x; + let o = wf_at(w, node); + var sv = vec2(0.0, 0.0); + if (o.ok) { + // согласование по напряжению: u_s = u_node − u_τ²·y_w/ν (см. math::wall_slip) + let g = wfs[w]; + let nu = CS2*(1.0/(2.0*beta[node]) - 0.5); + let s = clamp(o.unode - o.utau*o.utau*g.yw/nu, -o.us, o.us); + sv = vec2(s*o.ex, s*o.ey); + } + slips[w] = vec4(sv.x, sv.y, rho, 0.0); +} + +// Bouzidi с подвижной (скользящей) стенкой: δ = 2W_iρ(c_ī·u_s)/c_s², для q ≥ ½ — δ/(2q) +@compute @workgroup_size(64) +fn k_bouzidi_wf(@builtin(global_invocation_id) gid: vec3, + @builtin(num_workgroups) nwg: vec3) { + let k = lin(gid, nwg); + if (k >= P.nlinks) { return; } + let L = links[k]; + var wv = array(4.0/9.0, 1.0/9.0, 1.0/9.0, 1.0/9.0, 1.0/9.0, + 1.0/36.0, 1.0/36.0, 1.0/36.0, 1.0/36.0); + var cx = array(0.0, 1.0, 0.0, -1.0, 0.0, 1.0, -1.0, -1.0, 1.0); + var cy = array(0.0, 0.0, 1.0, 0.0, -1.0, 1.0, 1.0, -1.0, -1.0); + let sl = slips[L.wnode]; + let delta = -2.0*wv[L.i]*sl.z*(cx[L.i]*sl.x + cy[L.i]*sl.y)/CS2; + let fi = pget(L.i, L.node); + var v = fi + delta; + if (L.kind == 0u) { + v = 2.0*L.q*fi + (1.0 - 2.0*L.q)*pget(L.i, L.far) + delta; + } else if (L.kind == 1u) { + let h = 1.0/(2.0*L.q); + v = h*fi + (1.0 - h)*pget(L.ib, L.node) + h*delta; + } + fset(L.ib, L.node, v); +} + // ───── AMR ───── @compute @workgroup_size(64) @@ -897,10 +1051,18 @@ fn make_level( }); let solid: Vec = geom.solid.iter().map(|s| *s as u32).collect(); let solid_buf = storage_init(device, "solid", bytemuck::cast_slice(&solid)); + // линк → индекс его граничного узла (линки одного узла лежат подряд) + let mut link_wnode = vec![0u32; geom.links.len()]; + for (wi, w) in geom.wall_nodes.iter().enumerate() { + for k in w.first..w.first + w.count as u32 { + link_wnode[k as usize] = wi as u32; + } + } let links: Vec = geom .links .iter() - .map(|l| GLink { + .zip(link_wnode.iter()) + .map(|(l, &wn)| GLink { node: l.node, far: l.far, i: l.i as u32, @@ -912,7 +1074,7 @@ fn make_level( }, q: l.q as f32, body: l.body as u32, - _p: 0, + wnode: wn, }) .collect(); // Пустая сцена (эталонные течения) даёт нулевой список линков, а шейдер всё равно @@ -931,10 +1093,25 @@ fn make_level( let nwall = wnodes.len() as u32; let wnodes_pad = if wnodes.is_empty() { vec![[0u32; 4]] } else { wnodes.clone() }; let wall_buf = storage_init(device, "wall nodes", bytemuck::cast_slice(&wnodes_pad)); + let wfs: Vec = geom + .wf_nodes + .iter() + .map(|g| GWf { + nx: g.nx as f32, + ny: g.ny as f32, + yw: g.y_w as f32, + ys: g.y_s as f32, + st: g.st, + w: [g.w[0] as f32, g.w[1] as f32, g.w[2] as f32, g.w[3] as f32], + }) + .collect(); + let wfs_pad = if wfs.is_empty() { vec![GWf::zeroed()] } else { wfs }; + let wf_buf = storage_init(device, "wall function", bytemuck::cast_slice(&wfs_pad)); + let slip_buf = storage(device, "wall slip", (wnodes_pad.len() * 16) as u64); let wall_bind = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("wall"), layout: wall_layout, - entries: &[bind(0, &wall_buf)], + entries: &[bind(0, &wall_buf), bind(1, &wf_buf), bind(2, &slip_buf)], }); let parts_count = ceil_div(n as u32, WG); @@ -998,21 +1175,29 @@ pub struct Sim { queue: wgpu::Queue, adapter_name: String, - l0: GpuLevel, - l1: Option, - pre: wgpu::Buffer, + /// Уровни от L0 (весь домен) к самому тонкому. + levels: Vec, + /// Состояние уровня k до столкновения — старый край рамки уровня k + 1. + pre: Vec>, + /// `amr[k]` связывает уровень k с уровнем k + 1. + amr: Vec, dyn_buf: wgpu::Buffer, results_buf: wgpu::Buffer, staging: wgpu::Buffer, - /// по одной группе на подшаг — различаются только индексом подшага в `Substep` + /// по одной группе на подшаг самого тонкого уровня — различаются только индексом + /// подшага в `Substep`, куда `k_force` кладёт силу dyn_binds: Vec, - amr: Option, - pipes: Pipes, readback: wgpu::Buffer, + /// Уровень, на котором лежит зонд. + probe_level: usize, + /// Геометрия и вязкость самого тонкого уровня — для сводки y⁺. + fine_geom: cpu::Geom, + fine_nu: R, + fluid_count: R, step_index: u64, d_ref: R, @@ -1022,6 +1207,7 @@ pub struct Sim { } struct AmrRes { + r: usize, nghost: u32, nrestrict: u32, bg_ghost_old: wgpu::BindGroup, @@ -1034,7 +1220,11 @@ struct Pipes { collide: wgpu::ComputePipeline, stream: wgpu::ComputePipeline, bouzidi: wgpu::ComputePipeline, - moment_wall: wgpu::ComputePipeline, + /// Конвейеры с группой 3 (граничные узлы) — собираются, только если нужны: их связка + /// требует больше storage-биндингов, чем гарантируют лимиты по умолчанию. + moment_wall: Option, + wf_slip: Option, + bouzidi_wf: Option, walls: wgpu::ComputePipeline, channel: wgpu::ComputePipeline, force: wgpu::ComputePipeline, @@ -1090,31 +1280,11 @@ impl Sim { )) .map_err(|e| format!("не удалось получить устройство: {e}"))?; - // Моментная стенка добавляет к связке уровня ещё один storage-биндинг (индекс - // граничных узлов), итого 9 против 8, гарантируемых лимитами по умолчанию. На - // десктопных адаптерах их обычно >= 16, но проверить надо явно. - if spec.wall.is_moment_based() { - let lim = adapter.limits().max_storage_buffers_per_shader_stage; - if lim < 9 { - return Err(format!( - "адаптер даёт только {lim} storage-биндингов на стадию, моментной стенке \ - нужно 9. Запусти с --wall bouzidi либо --backend cpu" - )); - } - } + let lv = spec.levels(); // Влезает ли массив популяций в ОДНУ привязку. Считаем по самому крупному // уровню: у патча измельчения своих узлов может оказаться больше, чем у L0. - let max_nodes = { - let n0 = spec.nx * spec.ny; - let n1 = match (spec.refine > 1, spec.patch) { - (true, Some((ax, bx, ay, by))) => { - (spec.refine * (bx - ax) + 1) * (spec.refine * (by - ay) + 1) - } - _ => 0, - }; - n0.max(n1) - }; + let max_nodes = lv.iter().map(|g| g.nx * g.ny).max().unwrap_or(0); let need = (math::Q * dir_stride(max_nodes) * 4) as u64; // Переменная окружения нужна не для работы, а для проверки: без неё раздельный // вариант включается только на драйверах с малым пределом, и сверить два @@ -1128,29 +1298,38 @@ impl Sim { let align = adapter.limits().min_storage_buffer_offset_alignment as usize; if (DIR_ALIGN * 4) % align != 0 { return Err(format!( - "адаптер требует выравнивания смещений на {align} байт, а раскладка популяций рассчитана на {}. Считай на CPU либо на другом драйвере", + "адаптер требует выравнивания смещений на {align} байт, а раскладка \ + популяций рассчитана на {}. Считай на CPU либо на другом драйвере", DIR_ALIGN * 4 )); } let per = (dir_stride(max_nodes) * 4) as u64; if per > adapter.limits().max_storage_buffer_binding_size as u64 { return Err(format!( - "адаптер ограничивает привязку {} МиБ, а одному направлению нужно {} МиБ. Сетка слишком крупная для этого драйвера — считай на CPU либо на драйвере без такого предела", + "адаптер ограничивает привязку {} МиБ, а одному направлению нужно {} МиБ. \ + Сетка слишком крупная для этого драйвера — считай на CPU либо на драйвере \ + без такого предела", adapter.limits().max_storage_buffer_binding_size / 1048576, per / 1048576 )); } } - // ── топология берётся из процессорного бэкенда, а не строится заново ── - let geom0 = cpu::Geom::build(spec.nx, spec.ny, &spec.scene); - let fluid_count = geom0.solid.iter().filter(|s| !**s).count() as R; - - let beta0: Vec = - math::beta_profile(spec.nx, spec.beta0, spec.sponge_in, spec.sponge_len, spec.sponge_mult) - .into_iter() - .map(|v| v as f32) - .collect(); + // Моментная стенка и пристеночная функция добавляют к связке уровня группу 3 из + // трёх storage-биндингов (граничные узлы, их геометрия, скольжение). Проверяем явно: + // невнятная ошибка валидации на середине сборки хуже прямого сообщения. + let need_wall = spec.wall.is_moment_based() || spec.wall_fn.is_on(); + if need_wall { + let lim = adapter.limits().max_storage_buffers_per_shader_stage; + let want = if split { 5 + 2 * math::Q as u32 } else { 7 } + 1 + 3; + if lim < want { + return Err(format!( + "адаптер даёт только {lim} storage-биндингов на стадию, моментной стенке и \ + пристеночной функции нужно {want}. Запусти с --wall bouzidi без \ + --wall-function либо --backend cpu" + )); + } + } let module = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("kbc2d.wgsl"), @@ -1162,7 +1341,7 @@ impl Sim { let bgl_amr = amr_layout(&device); let bgl_wall = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("wall"), - entries: &[ro(0)], + entries: &[ro(0), ro(1), rw(2)], }); let bgl_empty = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("empty"), @@ -1174,11 +1353,6 @@ impl Sim { bind_group_layouts: &[&bgl_level, &bgl_dyn], push_constant_ranges: &[], }); - let pl_wall = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("moment wall"), - bind_group_layouts: &[&bgl_level, &bgl_dyn, &bgl_empty, &bgl_wall], - push_constant_ranges: &[], - }); let pl_amr = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("amr"), bind_group_layouts: &[&bgl_empty, &bgl_empty, &bgl_amr], @@ -1197,17 +1371,21 @@ impl Sim { }; // ── общие буферы ── + let nsub = spec.substeps().max(1); let dyn_buf = device.create_buffer(&wgpu::BufferDescriptor { label: Some("dyn"), size: std::mem::size_of::() as u64, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); - // история на HIST шагов + рабочие слоты силы на 8 подшагов + // История на HIST шагов + рабочие слоты силы: по MAXB вёдер из четырёх чисел на + // КАЖДЫЙ подшаг самого тонкого уровня. Раньше слотов было ровно на два подшага, и + // при r ≥ 3 сила лишних подшагов писалась за край буфера и терялась — Cd на GPU + // выходил занижен в r/2 раза. let hist_bytes = (HIST * std::mem::size_of::()) as u64; let results_buf = device.create_buffer(&wgpu::BufferDescriptor { label: Some("results"), - size: hist_bytes + 8 * 4 * 4, + size: hist_bytes + (nsub * crate::MAX_BODY_BUCKETS * 4 * 4) as u64, usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC, mapped_at_creation: false, }); @@ -1218,7 +1396,6 @@ impl Sim { mapped_at_creation: false, }); - let nsub = spec.refine.max(1); let dyn_binds: Vec = (0..nsub) .map(|s| { let sb = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { @@ -1242,77 +1419,96 @@ impl Sim { (0.0, 0.0) }; - // ── зонд ── - let (px, py) = spec.probe; - let probe_on_fine = matches!(spec.patch, Some((ax, bx, ay, by)) - if px >= ax && px <= bx && py >= ay && py <= by) - && spec.refine > 1; + // ── зонд: самый тонкий уровень, накрывающий точку (как в cpu.rs) ── + let (mut px, mut py) = spec.probe; + let mut probe_level = 0; + for (k, p) in spec.patches.iter().enumerate() { + if px >= p.ax && px <= p.bx && py >= p.ay && py <= p.by { + px = (px - p.ax) * p.r; + py = (py - p.ay) * p.r; + probe_level = k + 1; + } else { + break; + } + } + let probe_node = py * lv[probe_level].nx + px; - // ── уровень 0 ── - let probe0 = if probe_on_fine { 0 } else { py * spec.nx + px }; - let flags0 = 1 | if probe_on_fine { 0 } else { 2 }; - let l0 = make_level( - &device, - &bgl_level, - spec.nx, - spec.ny, - &geom0, - &beta0, - probe0 as u32, - flags0, - &cpu::initial_field(&cpu::Init { - case: spec.case, - nx: spec.nx, - ny: spec.ny, - u0, - beta: spec.beta0, - scene: Some(&spec.scene), - taper: if spec.init_uniform { spec.init_taper } else { 0.0 }, - }), - &bgl_wall, - split, - ); + // ── уровни: топология берётся из процессорного бэкенда, а не строится заново ── + let nlev = lv.len(); + let mut geoms: Vec = Vec::with_capacity(nlev); + let mut levels: Vec = Vec::with_capacity(nlev); + for (k, g) in lv.iter().enumerate() { + let scene = spec.level_scene(g); + let geom = cpu::Geom::build(g.nx, g.ny, &scene); + let (beta, beta_init): (Vec, R) = if k == 0 { + ( + math::beta_field( + g.nx, + g.ny, + spec.beta0, + spec.sponge_in, + spec.sponge_len, + spec.sponge_side, + spec.sponge_mult, + ) + .into_iter() + .map(|v| v as f32) + .collect(), + spec.beta0, + ) + } else { + let b = 1.0 / (2.0 * g.tau); + (vec![b as f32; g.nx * g.ny], b) + }; + let on_probe = k == probe_level; + let flags = (if k == 0 { 1 } else { 0 }) | if on_probe { 2 } else { 0 }; + let lvl = make_level( + &device, + &bgl_level, + g.nx, + g.ny, + &geom, + &beta, + if on_probe { probe_node as u32 } else { 0 }, + flags, + &cpu::initial_field(&cpu::Init { + case: spec.case, + nx: g.nx, + ny: g.ny, + u0, + beta: beta_init, + scene: Some(&scene), + taper: if spec.init_uniform { spec.init_taper * g.scale } else { 0.0 }, + }), + &bgl_wall, + split, + ); + levels.push(lvl); + geoms.push(geom); + } + let fluid_count = geoms[0].solid.iter().filter(|s| !**s).count() as R; - let pre = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("pre"), - size: (math::Q * l0.stride * 4) as u64, - usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); + // ── «до»-буферы и связки уровней ── + let pre: Vec> = (0..nlev) + .map(|k| { + (k + 1 < nlev).then(|| { + device.create_buffer(&wgpu::BufferDescriptor { + label: Some("pre"), + size: (math::Q * levels[k].stride * 4) as u64, + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }) + }) + }) + .collect(); - // ── уровень 1 и AMR ── - let mut l1 = None; - let mut amr = None; - if spec.refine > 1 { - let (ax, bx, ay, by) = spec.patch.ok_or("refine > 1 требует патч")?; - let r = spec.refine; - let scene1 = spec.scene.refined(r as R, ax as R, ay as R); - let nfx = r * (bx - ax) + 1; - let nfy = r * (by - ay) + 1; - let geom1 = cpu::Geom::build(nfx, nfy, &scene1); - let tau0 = 1.0 / (2.0 * spec.beta0); - let tau1 = r as R * (tau0 - 0.5) + 0.5; - let beta1 = vec![(1.0 / (2.0 * tau1)) as f32; nfx]; - let r01 = tau1 / (r as R * tau0); - let patch = cpu::Patch::new(&spec, &geom0, &geom1.solid, r01); - - let probe1 = if probe_on_fine { ((py - ay) * r) * nfx + (px - ax) * r } else { 0 }; - let flags1 = if probe_on_fine { 2 } else { 0 }; - let lvl1 = - make_level( - &device, &bgl_level, nfx, nfy, &geom1, &beta1, probe1 as u32, flags1, - &cpu::initial_field(&cpu::Init { - case: spec.case, - nx: nfx, - ny: nfy, - u0, - beta: 1.0 / (2.0 * tau1), - scene: Some(&scene1), - taper: if spec.init_uniform { spec.init_taper * r as R } else { 0.0 }, - }), - &bgl_wall, - split, - ); + let dummy = storage(&device, "dummy", 16); + let mut amr: Vec = Vec::with_capacity(spec.patches.len()); + for (k, ps) in spec.patches.iter().enumerate() { + let r = ps.r; + let r01 = lv[k + 1].tau / (r as R * lv[k].tau); + let patch = cpu::Patch::new(ps, &geoms[k], &geoms[k + 1].solid, r01); + let (coarse, fine) = (&levels[k], &levels[k + 1]); let ghosts: Vec = patch .ghosts() @@ -1336,13 +1532,12 @@ impl Sim { let gh_new = storage(&device, "gh_new", ghost_bytes); let ghosts_buf = storage_init(&device, "ghosts", bytemuck::cast_slice(&ghosts)); let rest_buf = storage_init(&device, "rest", bytemuck::cast_slice(&rest)); - let dummy = storage(&device, "dummy", 16); let base = AmrParams { nghost: ghosts.len() as u32, nrestrict: rest.len() as u32, - cstride: l0.stride as u32, - fstride: lvl1.stride as u32, + cstride: coarse.stride as u32, + fstride: fine.stride as u32, r01: r01 as f32, rfc: (1.0 / r01) as f32, w: 0.0, @@ -1373,18 +1568,20 @@ impl Sim { }) }; - let bg_ghost_old = mk_amr_bg(&pre, &gh_old, &dummy, &dummy, base); - let bg_ghost_new = mk_amr_bg(&l0.f, &gh_new, &dummy, &dummy, base); + let pre_k = pre[k].as_ref().unwrap(); + let bg_ghost_old = mk_amr_bg(pre_k, &gh_old, &dummy, &dummy, base); + let bg_ghost_new = mk_amr_bg(&coarse.f, &gh_new, &dummy, &dummy, base); let bg_fill: Vec = (0..r) .map(|s| { let mut p = base; p.w = ((s + 1) as R / r as R) as f32; - mk_amr_bg(&dummy, &lvl1.f, &gh_old, &gh_new, p) + mk_amr_bg(&dummy, &fine.f, &gh_old, &gh_new, p) }) .collect(); - let bg_restrict = mk_amr_bg(&lvl1.f, &l0.f, &dummy, &dummy, base); + let bg_restrict = mk_amr_bg(&fine.f, &coarse.f, &dummy, &dummy, base); - amr = Some(AmrRes { + amr.push(AmrRes { + r, nghost: ghosts.len() as u32, nrestrict: rest.len() as u32, bg_ghost_old, @@ -1392,14 +1589,22 @@ impl Sim { bg_fill, bg_restrict, }); - l1 = Some(lvl1); } + let pl_wall = need_wall.then(|| { + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("wall"), + bind_group_layouts: &[&bgl_level, &bgl_dyn, &bgl_empty, &bgl_wall], + push_constant_ranges: &[], + }) + }); let pipes = Pipes { collide: mk(&pl_level, "k_collide"), stream: mk(&pl_level, "k_stream"), bouzidi: mk(&pl_level, "k_bouzidi"), - moment_wall: mk(&pl_wall, "k_moment_wall"), + moment_wall: pl_wall.as_ref().map(|l| mk(l, "k_moment_wall")), + wf_slip: pl_wall.as_ref().map(|l| mk(l, "k_wf_slip")), + bouzidi_wf: pl_wall.as_ref().map(|l| mk(l, "k_bouzidi_wf")), walls: mk(&pl_level, "k_walls"), channel: mk(&pl_level, "k_channel"), force: mk(&pl_level, "k_force"), @@ -1415,33 +1620,36 @@ impl Sim { }), }; + // приёмный буфер — под самое крупное поле популяций среди уровней + let max_stride = levels.iter().map(|l| l.stride).max().unwrap_or(0); let readback = device.create_buffer(&wgpu::BufferDescriptor { label: Some("readback"), - size: (math::Q * l0.stride * 4) as u64, + size: (math::Q * max_stride * 4) as u64, usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); - let d_ref = match spec.patch { - Some(_) if spec.refine > 1 => spec.scene.ref_size() * spec.refine as R, - _ => spec.scene.ref_size(), - }; + let fine = *lv.last().unwrap(); + let d_ref = spec.scene.ref_size() * fine.scale; + let fine_geom = geoms.pop().unwrap(); Ok(Sim { spec, device, queue, adapter_name, - l0, - l1, + levels, pre, + amr, dyn_buf, results_buf, staging, dyn_binds, - amr, pipes, readback, + probe_level, + fine_geom, + fine_nu: math::CS2 * (fine.tau - 0.5), fluid_count, step_index: 0, pending: Vec::with_capacity(HIST), @@ -1475,15 +1683,116 @@ impl Sim { (u * c, uy) } + /// Записать шаг уровня k, а за ним — r_k подшагов потомка (рекурсивно) с рамкой и + /// проекцией обратно. `sub` — сквозной номер подшага самого тонкого уровня: им + /// выбирается слот, куда `k_force` положит силу. + fn encode_level(&self, enc: &mut wgpu::CommandEncoder, k: usize, sub: &mut usize) { + let lvl = &self.levels[k]; + let has_child = k < self.amr.len(); + let channel = self.spec.case == Case::Channel; + + // «до» — состояние уровня перед столкновением, старый край рамки потомка + if let Some(pre) = &self.pre[k] { + enc.copy_buffer_to_buffer(&lvl.f, 0, pre, 0, (math::Q * lvl.stride * 4) as u64); + } + { + let mut p = enc.begin_compute_pass(&wgpu::ComputePassDescriptor { + label: Some("level"), + timestamp_writes: None, + }); + p.set_bind_group(0, &lvl.bind, &[]); + let dyn_idx = if has_child { 0 } else { *sub }; + p.set_bind_group(1, &self.dyn_binds[dyn_idx], &[]); + let ncell = ceil_div(lvl.n as u32, WG); + p.set_pipeline(&self.pipes.collide); + dispatch(&mut p, ncell); + p.set_pipeline(&self.pipes.stream); + dispatch(&mut p, ncell); + // Эталонные течения статей периодичны по обеим осям: ГУ не накладываются вовсе. + if channel { + let wall_groups = ceil_div(lvl.nwall.max(1), WG); + if self.spec.wall.is_moment_based() { + p.set_bind_group(2, &self.pipes.empty_bg, &[]); + p.set_bind_group(3, &lvl.wall_bind, &[]); + p.set_pipeline(self.pipes.moment_wall.as_ref().unwrap()); + dispatch(&mut p, wall_groups); + } else if self.spec.wall == math::WallModel::Bouzidi && self.spec.wall_fn.is_on() { + // сначала скольжение на каждый граничный узел, затем полинковый отскок + p.set_bind_group(2, &self.pipes.empty_bg, &[]); + p.set_bind_group(3, &lvl.wall_bind, &[]); + p.set_pipeline(self.pipes.wf_slip.as_ref().unwrap()); + dispatch(&mut p, wall_groups); + p.set_pipeline(self.pipes.bouzidi_wf.as_ref().unwrap()); + dispatch(&mut p, lvl.link_groups()); + } else { + p.set_pipeline(&self.pipes.bouzidi); + dispatch(&mut p, lvl.link_groups()); + } + // стенки и вход/выход есть только у L0; порядок обязателен: стенки снимают + // заворот по y, затем Zou-He — по x + if k == 0 { + p.set_pipeline(&self.pipes.walls); + dispatch(&mut p, ceil_div(lvl.nx as u32, WG)); + p.set_pipeline(&self.pipes.channel); + dispatch(&mut p, ceil_div(lvl.ny as u32, WG)); + } + } + // силу снимаем на каждом подшаге самого тонкого уровня, усредняется она в k_stats2 + if !has_child { + p.set_pipeline(&self.pipes.force); + p.dispatch_workgroups(1, 1, 1); + *sub += 1; + } + } + if !has_child { + return; + } + + let a = &self.amr[k]; + { + let mut p = enc.begin_compute_pass(&wgpu::ComputePassDescriptor { + label: Some("ghost"), + timestamp_writes: None, + }); + p.set_bind_group(0, &self.pipes.empty_bg, &[]); + p.set_bind_group(1, &self.pipes.empty_bg, &[]); + p.set_pipeline(&self.pipes.ghost); + p.set_bind_group(2, &a.bg_ghost_old, &[]); + dispatch(&mut p, ceil_div(a.nghost, WG)); + p.set_bind_group(2, &a.bg_ghost_new, &[]); + dispatch(&mut p, ceil_div(a.nghost, WG)); + } + for s in 0..a.r { + self.encode_level(enc, k + 1, sub); + let mut p = enc.begin_compute_pass(&wgpu::ComputePassDescriptor { + label: Some("fill"), + timestamp_writes: None, + }); + p.set_bind_group(0, &self.pipes.empty_bg, &[]); + p.set_bind_group(1, &self.pipes.empty_bg, &[]); + p.set_bind_group(2, &a.bg_fill[s], &[]); + p.set_pipeline(&self.pipes.fill); + dispatch(&mut p, ceil_div(a.nghost, WG)); + } + { + let mut p = enc.begin_compute_pass(&wgpu::ComputePassDescriptor { + label: Some("restrict"), + timestamp_writes: None, + }); + p.set_bind_group(0, &self.pipes.empty_bg, &[]); + p.set_bind_group(1, &self.pipes.empty_bg, &[]); + p.set_bind_group(2, &a.bg_restrict, &[]); + p.set_pipeline(&self.pipes.restrict); + dispatch(&mut p, ceil_div(a.nrestrict, WG)); + } + } + /// Посчитать один шаг. Результаты не читаются сразу: они копятся в истории на устройстве /// и попадают в `out` пачкой — либо когда история заполнится, либо по явному `flush`. pub fn advance(&mut self, out: &mut Vec) { let t = self.step_index; let slot = self.pending.len() as u32; let (ux_in, uy_in) = self.inlet(t); - let refine = self.spec.refine.max(1); - let moment_wall = self.spec.wall.is_moment_based(); - let channel = self.spec.case == Case::Channel; self.queue.write_buffer( &self.dyn_buf, 0, @@ -1493,8 +1802,8 @@ impl Sim { rho_out: 1.0, kbc_model: (self.spec.kbc_model == math::KbcModel::N2) as u32, outlet_extrap: self.spec.outlet_extrapolate as u32, - nparts: self.l0.parts_count, - refine: refine as u32, + nparts: self.levels[0].parts_count, + refine: self.spec.substeps().max(1) as u32, collision: (self.spec.collision == Collision::Bgk) as u32, slot, wall_mode: match self.spec.wall { @@ -1503,126 +1812,16 @@ impl Sim { math::WallModel::Hrr => 2, math::WallModel::Staircase => 3, }, - _pad: [0; 2], + wf: self.spec.wall_fn.is_on() as u32, + _pad: 0, }), ); let mut enc = self .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("step") }); - - // «до» — состояние L0 перед столкновением, нужно как старый край рамки патча - if self.amr.is_some() { - enc.copy_buffer_to_buffer(&self.l0.f, 0, &self.pre, 0, (math::Q * self.l0.stride * 4) as u64); - } - - { - let mut p = enc.begin_compute_pass(&wgpu::ComputePassDescriptor { - label: Some("L0"), - timestamp_writes: None, - }); - p.set_bind_group(0, &self.l0.bind, &[]); - p.set_bind_group(1, &self.dyn_binds[0], &[]); - let ncell = ceil_div(self.l0.n as u32, WG); - p.set_pipeline(&self.pipes.collide); - dispatch(&mut p, ncell); - p.set_pipeline(&self.pipes.stream); - dispatch(&mut p, ncell); - // Эталонные течения статей периодичны по обеим осям: ГУ не накладываются вовсе. - if channel { - if moment_wall { - p.set_bind_group(2, &self.pipes.empty_bg, &[]); - p.set_bind_group(3, &self.l0.wall_bind, &[]); - p.set_pipeline(&self.pipes.moment_wall); - dispatch(&mut p, ceil_div(self.l0.nwall.max(1), WG)); - } else { - p.set_pipeline(&self.pipes.bouzidi); - dispatch(&mut p, self.l0.link_groups()); - } - // порядок обязателен: стенки снимают заворот по y, затем Zou-He — по x - p.set_pipeline(&self.pipes.walls); - dispatch(&mut p, ceil_div(self.l0.nx as u32, WG)); - p.set_pipeline(&self.pipes.channel); - dispatch(&mut p, ceil_div(self.l0.ny as u32, WG)); - } - } - - if let (Some(l1), Some(a)) = (self.l1.as_ref(), self.amr.as_ref()) { - { - let mut p = enc.begin_compute_pass(&wgpu::ComputePassDescriptor { - label: Some("ghost"), - timestamp_writes: None, - }); - p.set_bind_group(0, &self.pipes.empty_bg, &[]); - p.set_bind_group(1, &self.pipes.empty_bg, &[]); - p.set_pipeline(&self.pipes.ghost); - p.set_bind_group(2, &a.bg_ghost_old, &[]); - dispatch(&mut p, ceil_div(a.nghost, WG)); - p.set_bind_group(2, &a.bg_ghost_new, &[]); - dispatch(&mut p, ceil_div(a.nghost, WG)); - } - let ncell1 = ceil_div(l1.n as u32, WG); - let nlink1 = l1.link_groups(); - for s in 0..refine { - { - let mut p = enc.begin_compute_pass(&wgpu::ComputePassDescriptor { - label: Some("L1"), - timestamp_writes: None, - }); - p.set_bind_group(0, &l1.bind, &[]); - p.set_bind_group(1, &self.dyn_binds[s], &[]); - p.set_pipeline(&self.pipes.collide); - dispatch(&mut p, ncell1); - p.set_pipeline(&self.pipes.stream); - dispatch(&mut p, ncell1); - if channel { - if moment_wall { - p.set_bind_group(2, &self.pipes.empty_bg, &[]); - p.set_bind_group(3, &l1.wall_bind, &[]); - p.set_pipeline(&self.pipes.moment_wall); - dispatch(&mut p, ceil_div(l1.nwall.max(1), WG)); - } else { - p.set_pipeline(&self.pipes.bouzidi); - dispatch(&mut p, nlink1); - } - } - // силу снимаем на каждом подшаге, усредняется она в k_stats2 - p.set_pipeline(&self.pipes.force); - p.dispatch_workgroups(1, 1, 1); - } - { - let mut p = enc.begin_compute_pass(&wgpu::ComputePassDescriptor { - label: Some("fill"), - timestamp_writes: None, - }); - p.set_bind_group(0, &self.pipes.empty_bg, &[]); - p.set_bind_group(1, &self.pipes.empty_bg, &[]); - p.set_bind_group(2, &a.bg_fill[s], &[]); - p.set_pipeline(&self.pipes.fill); - dispatch(&mut p, ceil_div(a.nghost, WG)); - } - } - { - let mut p = enc.begin_compute_pass(&wgpu::ComputePassDescriptor { - label: Some("restrict"), - timestamp_writes: None, - }); - p.set_bind_group(0, &self.pipes.empty_bg, &[]); - p.set_bind_group(1, &self.pipes.empty_bg, &[]); - p.set_bind_group(2, &a.bg_restrict, &[]); - p.set_pipeline(&self.pipes.restrict); - dispatch(&mut p, ceil_div(a.nrestrict, WG)); - } - } else { - let mut p = enc.begin_compute_pass(&wgpu::ComputePassDescriptor { - label: Some("force L0"), - timestamp_writes: None, - }); - p.set_bind_group(0, &self.l0.bind, &[]); - p.set_bind_group(1, &self.dyn_binds[0], &[]); - p.set_pipeline(&self.pipes.force); - p.dispatch_workgroups(1, 1, 1); - } + let mut sub = 0usize; + self.encode_level(&mut enc, 0, &mut sub); { let mut p = enc.begin_compute_pass(&wgpu::ComputePassDescriptor { @@ -1630,14 +1829,15 @@ impl Sim { timestamp_writes: None, }); p.set_bind_group(1, &self.dyn_binds[0], &[]); - p.set_bind_group(0, &self.l0.bind, &[]); + p.set_bind_group(0, &self.levels[0].bind, &[]); p.set_pipeline(&self.pipes.stats1); - dispatch(&mut p, self.l0.parts_count); - if let Some(l1) = self.l1.as_ref() { + dispatch(&mut p, self.levels[0].parts_count); + if self.probe_level > 0 { // на тонком уровне stats1 нужен только чтобы снять зонд (флаг записи сумм снят) - p.set_bind_group(0, &l1.bind, &[]); - dispatch(&mut p, ceil_div(l1.n as u32, WG)); - p.set_bind_group(0, &self.l0.bind, &[]); + let l = &self.levels[self.probe_level]; + p.set_bind_group(0, &l.bind, &[]); + dispatch(&mut p, ceil_div(l.n as u32, WG)); + p.set_bind_group(0, &self.levels[0].bind, &[]); } p.set_pipeline(&self.pipes.stats2); p.dispatch_workgroups(1, 1, 1); @@ -1651,6 +1851,19 @@ impl Sim { } } + /// y⁺ первого узла на самом тонком уровне: поле скачивается и сводится общим кодом + /// процессорного бэкенда. + pub fn yplus(&self) -> cpu::YPlus { + let l = self.levels.last().unwrap(); + let stride = l.stride; + let raw = self.download(&l.f, (math::Q * stride * 4) as u64); + cpu::yplus_stats(&self.fine_geom, self.fine_nu, |k| { + let c: [R; 9] = std::array::from_fn(|i| raw[i * stride + k] as R); + let (_, a, b) = math::macros(&c); + (a, b) + }) + } + /// Дочитать всё, что уже посчитано устройством. Обязательно вызывать перед тем, как /// смотреть на поля (кадр гифки, проверка на NaN) — иначе записи отстанут от состояния. pub fn flush(&mut self, out: &mut Vec) { @@ -1701,6 +1914,7 @@ impl Sim { tz: res.tz as R, body, uy_probe: res.uy_probe as R, + rho_probe: res.rho_probe as R, rho_mean: res.rho_sum as R / self.fluid_count, max_u: res.max_u as R, gamma_mean: res.g_sum as R / cnt, @@ -1713,7 +1927,7 @@ impl Sim { /// Скачать популяции L0 на хост (нужно для кадров и проверки на NaN). fn download_l0(&self) -> Vec { - self.download(&self.l0.f, (math::Q * self.l0.stride * 4) as u64) + self.download(&self.levels[0].f, (math::Q * self.levels[0].stride * 4) as u64) } /// Скачать произвольный буфер уровня. Буфер приёма выделен один раз при сборке под самый @@ -1745,8 +1959,8 @@ impl Sim { /// Полное поле скорости уровня L0 — для метрик эталонных течений и радиуса влияния. pub fn sample_velocity(&self) -> (Vec, Vec) { - let n = self.l0.n; - let stride = self.l0.stride; + let n = self.levels[0].n; + let stride = self.levels[0].stride; let raw = self.download_l0(); let mut ux = Vec::with_capacity(n); let mut uy = Vec::with_capacity(n); @@ -1762,8 +1976,8 @@ impl Sim { /// Срез вдоль осевой линии: (ρ, u_x) по столбцам. На GPU это полное скачивание поля, /// поэтому x–t диагностика включается редким шагом и только там, где нужна. pub fn sample_centerline(&self) -> (Vec, Vec) { - let (nx, ny) = (self.l0.nx, self.l0.ny); - let stride = self.l0.stride; + let (nx, ny) = (self.levels[0].nx, self.levels[0].ny); + let stride = self.levels[0].stride; let raw = self.download_l0(); let y = ny / 2; let mut rho = Vec::with_capacity(nx); @@ -1783,8 +1997,8 @@ impl Sim { } pub fn sample_field(&self, kind: FieldKind) -> (Vec, Vec) { - let (nx, ny, n) = (self.l0.nx, self.l0.ny, self.l0.n); - let stride = self.l0.stride; + let (nx, ny, n) = (self.levels[0].nx, self.levels[0].ny, self.levels[0].n); + let stride = self.levels[0].stride; let raw = self.download_l0(); let node = |k: usize| -> [R; 9] { std::array::from_fn(|i| raw[i * stride + k] as R) @@ -1825,7 +2039,7 @@ impl Sim { FieldKind::Gamma => { // берём ровно то, что посчитало устройство: пересчёт на хосте с базовым β // соврал бы всюду, где β — поле (включённая губка) - let g = self.download(&self.l0.gam, (n * 4) as u64); + let g = self.download(&self.levels[0].gam, (n * 4) as u64); for k in 0..n { out[k] = g[k] as R; } diff --git a/docs/theory/2d_solver/src/main.rs b/docs/theory/2d_solver/src/main.rs index 82d6e62..083a42a 100644 --- a/docs/theory/2d_solver/src/main.rs +++ b/docs/theory/2d_solver/src/main.rs @@ -97,6 +97,9 @@ pub struct StepRec { /// сваливаются в последнее, поэтому сумма по вёдрам всегда точна. pub body: [[R; 3]; MAX_BODY_BUCKETS], pub uy_probe: R, + /// Плотность в том же зонде: её пульсации после установления — мера паразитной + /// акустики (отражений от границ и от стыков уровней). + pub rho_probe: R, pub rho_mean: R, pub max_u: R, pub gamma_mean: R, @@ -112,9 +115,9 @@ pub struct Spec { pub nx: usize, pub ny: usize, pub scene: Scene, - pub refine: usize, - /// Границы патча измельчения в координатах L0: (ax, bx, ay, by). - pub patch: Option<(usize, usize, usize, usize)>, + /// Цепочка вложенных патчей измельчения, от внешнего к внутреннему. Пусто — один + /// уровень L0. Каждый патч задан в координатах СВОЕГО РОДИТЕЛЯ (для первого — L0). + pub patches: Vec, pub units: Units, pub beta0: R, pub steps: u64, @@ -132,17 +135,116 @@ pub struct Spec { /// Длина губки после входа, столбцов. pub sponge_in: usize, pub sponge_mult: R, + /// Боковые губки у верхней и нижней стенок канала, рядов L0 (0 — нет). + pub sponge_side: usize, pub collision: Collision, /// Что входит в сдвиговую часть s (см. `math::KbcModel`). pub kbc_model: math::KbcModel, /// Чем замыкаются недостающие популяции на теле (см. `math::WallModel`). pub wall: math::WallModel, + /// Пристеночная функция поверх модели стенки (см. `math::WallFunction`). + pub wall_fn: math::WallFunction, /// Постановка задачи. pub case: Case, /// Узел зонда следа в координатах L0. pub probe: (usize, usize), } +/// Патч измельчения: прямоугольник узлов РОДИТЕЛЬСКОГО уровня [ax, bx] × [ay, by] и +/// коэффициент r. Тонкий уровень имеет r·(bx − ax) + 1 узлов по x, его узел (0, 0) совпадает +/// с узлом (ax, ay) родителя, и за один шаг родителя он делает r подшагов. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PatchSpec { + pub r: usize, + pub ax: usize, + pub bx: usize, + pub ay: usize, + pub by: usize, +} + +/// Геометрия уровня, выведенная из цепочки патчей. +#[derive(Clone, Copy, Debug)] +pub struct LevelGeo { + pub nx: usize, + pub ny: usize, + /// Клеток этого уровня на клетку L0. + pub scale: R, + /// Положение узла (0, 0) уровня в координатах L0. + pub ox: R, + pub oy: R, + /// Время релаксации уровня: τ_k = r_k(τ_{k−1} − ½) + ½ — одна и та же ν на всех уровнях. + pub tau: R, +} + +impl Spec { + /// Все уровни, от L0 к самому тонкому. + pub fn levels(&self) -> Vec { + let tau0 = 1.0 / (2.0 * self.beta0); + let mut out = vec![LevelGeo { + nx: self.nx, + ny: self.ny, + scale: 1.0, + ox: 0.0, + oy: 0.0, + tau: tau0, + }]; + for p in &self.patches { + let par = *out.last().unwrap(); + out.push(LevelGeo { + nx: p.r * (p.bx - p.ax) + 1, + ny: p.r * (p.by - p.ay) + 1, + scale: par.scale * p.r as R, + ox: par.ox + p.ax as R / par.scale, + oy: par.oy + p.ay as R / par.scale, + tau: p.r as R * (par.tau - 0.5) + 0.5, + }); + } + out + } + + /// Сцена в координатах уровня `g`. + pub fn level_scene(&self, g: &LevelGeo) -> Scene { + if g.scale == 1.0 && g.ox == 0.0 && g.oy == 0.0 { + self.scene.clone() + } else { + self.scene.refined(g.scale, g.ox, g.oy) + } + } + + /// Подшагов самого тонкого уровня на один шаг L0. + pub fn substeps(&self) -> usize { + self.patches.iter().map(|p| p.r).product() + } + + /// Обновлений узлов за один шаг L0 по всем уровням — мера стоимости. + pub fn nodes_per_step(&self) -> f64 { + let mut sub = 1usize; + let mut total = 0usize; + for (k, g) in self.levels().iter().enumerate() { + if k > 0 { + sub *= self.patches[k - 1].r; + } + total += sub * g.nx * g.ny; + } + total as f64 + } + + /// Контуры патчей в клетках L0 — для рамок на гифке. + pub fn patch_boxes_l0(&self) -> Vec<(usize, usize, usize, usize)> { + let lv = self.levels(); + self.patches + .iter() + .enumerate() + .map(|(k, p)| { + let par = &lv[k]; + let x = |v: usize| (par.ox + v as R / par.scale).round() as usize; + let y = |v: usize| (par.oy + v as R / par.scale).round() as usize; + (x(p.ax), x(p.bx), y(p.ay), y(p.by)) + }) + .collect() + } +} + // ───────────────────────────────────────────────────────────────────────────── // Параметры запуска // ───────────────────────────────────────────────────────────────────────────── @@ -197,6 +299,13 @@ struct Cli { /// Границы патча в координатах L0: ax,bx,ay,by (по умолчанию строятся вокруг тела) #[arg(long, help_heading = "Сетка")] patch: Option, + /// ЦЕПОЧКА вложенных уровней вместо одного патча: "r:up,down,side; r:up,down,side; …" — + /// от внешнего к внутреннему. r — во сколько раз уровень тоньше родителя; up, down, side — + /// отступы его границ от центра тела в диаметрах тела (вверх по потоку, вниз, в стороны). + /// Так строится внешний домен с грубыми буферами: L0 крупный и большой, к телу сетка + /// мельчает ступенями. Перекрывает --refine и --patch. + #[arg(long, help_heading = "Сетка")] + levels: Option, // ── тело ── /// Форма обтекаемого тела @@ -282,6 +391,13 @@ struct Cli { #[arg(long, default_value = "hrr", value_parser = math::WallModel::ALL, help_heading = "Схема")] wall: String, + /// Пристеночная функция поверх модели стенки: none — обычное прилипание; spalding — закон + /// Spalding для неразрешённого пограничного слоя (для grad/hrr через целевую скорость и + /// тензор давлений, для bouzidi через скорость скольжения стенки). В вязком подслое сама + /// сводится к прилипанию. С --wall staircase не сочетается. + #[arg(long, default_value = "none", value_parser = math::WallFunction::ALL, + help_heading = "Схема")] + wall_function: String, /// Состав сдвиговой части KBC (табл. I 2D-статьи): n1 — только девиатор {N, Π_xy} /// (KBC D), объёмная вязкость гуляет вместе с γ и может стать отрицательной; n2 — /// девиатор со следом {N, Π_xy, T} (KBC C), объёмная вязкость фиксирована ξ = ν. @@ -301,6 +417,10 @@ struct Cli { /// волны как жёсткий поршень; губка у входа гасит их до отражения. #[arg(long, default_value_t = 0, help_heading = "Схема")] sponge_in: usize, + /// Боковые губки у верхней и нижней стенок канала, рядов L0 (0 — выключены). Гасят + /// волны, бегущие поперёк потока, до отражения от зеркальных стенок. + #[arg(long, default_value_t = 0, help_heading = "Схема")] + sponge_side: usize, /// Во сколько раз губка поднимает вязкость #[arg(long, default_value_t = 30.0, help_heading = "Схема")] sponge_mult: R, @@ -455,6 +575,57 @@ fn parse_poly(v: &str) -> Result, String> { Ok(out) } +/// Разбор --levels: "r:up,down,side; …" от внешнего уровня к внутреннему. Отступы — от центра +/// тела в его диаметрах; граница каждого уровня пересчитывается в координаты родителя и +/// зажимается внутрь него (не ближе двух узлов к краю по x и одного по y — как у одиночного +/// патча: рамка тонкого уровня не должна лечь на рамку или ряды стенок родителя). +fn parse_levels( + s: &str, + cx: R, + cy: R, + d: R, + nx: usize, + ny: usize, +) -> Result, String> { + let mut out = Vec::new(); + // родитель: размеры, масштаб и начало в координатах L0 + let (mut pnx, mut pny, mut scale, mut ox, mut oy) = (nx, ny, 1.0 as R, 0.0 as R, 0.0 as R); + for item in s.split(';').map(str::trim).filter(|z| !z.is_empty()) { + let (rs, rest) = item + .split_once(':') + .ok_or(format!("--levels: в «{item}» ожидалось r:up,down,side"))?; + let r: usize = rs.trim().parse().map_err(|e| format!("--levels, r в «{item}»: {e}"))?; + if r < 2 { + return Err(format!("--levels: r = {r} в «{item}», уровень обязан быть тоньше родителя")); + } + let v: Result, _> = rest.split(',').map(|t| t.trim().parse::()).collect(); + let v = v.map_err(|e| format!("--levels, отступы в «{item}»: {e}"))?; + if v.len() != 3 || v.iter().any(|z| !(*z > 0.0)) { + return Err(format!("--levels: в «{item}» нужны три положительных отступа up,down,side")); + } + let to = |x: R, o: R| ((x - o) * scale).round(); + let ax = to(cx - v[0] * d, ox).max(2.0) as usize; + let bx = to(cx + v[1] * d, ox).min(pnx as R - 2.0).max(0.0) as usize; + let ay = to(cy - v[2] * d, oy).max(1.0) as usize; + let by = to(cy + v[2] * d, oy).min(pny as R - 2.0).max(0.0) as usize; + if ax + 1 >= bx || ay + 1 >= by { + return Err(format!( + "--levels: уровень «{item}» не помещается в родителя {pnx}×{pny} (получилось x [{ax},{bx}], y [{ay},{by}])" + )); + } + out.push(PatchSpec { r, ax, bx, ay, by }); + ox += ax as R / scale; + oy += ay as R / scale; + scale *= r as R; + pnx = r * (bx - ax) + 1; + pny = r * (by - ay) + 1; + } + if out.is_empty() { + return Err("--levels: список уровней пуст".into()); + } + Ok(out) +} + fn build_spec(cli: &Cli) -> Result { let case = Case::from_str(&cli.case).ok_or("неизвестная постановка")?; if cli.nx < 16 || cli.ny < 16 { @@ -532,11 +703,22 @@ fn build_spec(cli: &Cli) -> Result { )); } - if case.is_periodic() && cli.refine > 1 { + let wall = math::WallModel::from_str(&cli.wall).ok_or("неизвестная модель стенки")?; + let wall_fn = + math::WallFunction::from_str(&cli.wall_function).ok_or("неизвестная пристеночная функция")?; + if wall_fn.is_on() && wall == math::WallModel::Staircase { + return Err("--wall-function не сочетается с --wall staircase: ступенька не знает, где стенка, а пристеночной функции нужно расстояние до неё" + .into()); + } + if case.is_periodic() && (cli.refine > 1 || cli.levels.is_some()) { return Err("эталонные течения периодичны и патча измельчения не имеют: --refine 1".into()); } - // патч измельчения: по умолчанию охватывает тело и ближний след - let patch = if cli.refine > 1 { + // Цепочка патчей. --levels строит её по отступам от тела; без него — прежний одиночный + // патч из --refine/--patch, который по умолчанию охватывает тело и ближний след. + let patches: Vec = if let Some(lv) = &cli.levels { + let (bcx, bcy) = scene.center(); + parse_levels(lv, bcx, bcy, scene.ref_size(), cli.nx, cli.ny)? + } else if cli.refine > 1 { let d = cli.size; let (ax, bx, ay, by) = match &cli.patch { Some(s) => { @@ -560,8 +742,7 @@ fn build_spec(cli: &Cli) -> Result { // на них ГУ работает по всему ряду, а рамка патча их бы перезаписала if !(1 <= ay && ay < by && by <= cli.ny - 2) { return Err(format!( - "патч по y [{ay},{by}] обязан лежать внутри (0,{}) и не трогать ряды стенок; \ - при теле {d} ячеек минимальное ny ≈ {}", + "патч по y [{ay},{by}] обязан лежать внутри (0,{}) и не трогать ряды стенок; при теле {d} ячеек минимальное ny ≈ {}", cli.ny - 1, (4.0 * d) as usize + 6 )); @@ -569,10 +750,26 @@ fn build_spec(cli: &Cli) -> Result { if !(2 <= ax && ax < bx && bx <= cli.nx - 2) { return Err(format!("патч по x [{ax},{bx}] обязан лежать внутри (1,{})", cli.nx - 1)); } - Some((ax, bx, ay, by)) + vec![PatchSpec { r: cli.refine, ax, bx, ay, by }] } else { - None + Vec::new() }; + // Губки живут на L0 и не должны доставать до первого патча: внутри патча L0 перезаписывается + // рестрикцией, и губка на нём просто бы не действовала. + let patch = patches.first().map(|p| (p.ax, p.bx, p.ay, p.by)); + if cli.sponge_side > 0 { + if 2 * cli.sponge_side + 4 >= cli.ny { + return Err(format!("боковые губки ({} рядов) перекрывают весь канал", cli.sponge_side)); + } + if let Some((_, _, ay, by)) = patch { + if cli.sponge_side + 1 >= ay || by + cli.sponge_side + 1 >= cli.ny - 1 { + return Err(format!( + "боковые губки ({} рядов) достают до патча (ay={ay}, by={by})", + cli.sponge_side + )); + } + } + } // Губка перед выходом. Задача канала «вход по скорости + выход по давлению» акустически // есть четвертьволновая труба: вход отражает продольные волны как жёсткий поршень, выход — @@ -651,8 +848,7 @@ fn build_spec(cli: &Cli) -> Result { nx: cli.nx, ny: cli.ny, scene, - refine: 1, - patch: None, + patches: Vec::new(), units, beta0, steps, @@ -666,9 +862,11 @@ fn build_spec(cli: &Cli) -> Result { sponge_len: 0, sponge_in: 0, sponge_mult: 1.0, + sponge_side: 0, collision: if cli.collision == "bgk" { Collision::Bgk } else { Collision::Kbc }, kbc_model: math::KbcModel::from_str(&cli.kbc_model).ok_or("неизвестная модель KBC")?, - wall: math::WallModel::from_str(&cli.wall).ok_or("неизвестная модель стенки")?, + wall, + wall_fn, case, probe: (cli.nx / 2, cli.ny / 2), }); @@ -678,8 +876,7 @@ fn build_spec(cli: &Cli) -> Result { nx: cli.nx, ny: cli.ny, scene, - refine: cli.refine, - patch, + patches, units, beta0, steps, @@ -693,10 +890,12 @@ fn build_spec(cli: &Cli) -> Result { sponge_len, sponge_in: cli.sponge_in, sponge_mult: cli.sponge_mult, + sponge_side: cli.sponge_side, case, collision: if cli.collision == "bgk" { Collision::Bgk } else { Collision::Kbc }, kbc_model: math::KbcModel::from_str(&cli.kbc_model).ok_or("неизвестная модель KBC")?, - wall: math::WallModel::from_str(&cli.wall).ok_or("неизвестная модель стенки")?, + wall, + wall_fn, probe, }) } @@ -762,6 +961,13 @@ impl Backend { Backend::Gpu(s) => s.is_finite(), } } + fn yplus(&mut self) -> cpu::YPlus { + match self { + Backend::Cpu(s) => s.yplus(), + #[cfg(feature = "gpu")] + Backend::Gpu(s) => s.yplus(), + } + } fn force_ref_size(&self) -> R { match self { Backend::Cpu(s) => s.force_ref_size(), @@ -864,7 +1070,7 @@ fn run(cli: Cli) -> Result<(), String> { range, plan, hud, - spec.patch, + spec.patch_boxes_l0(), ) .map_err(|e| format!("не удалось создать {path}: {e}"))?, ) @@ -890,13 +1096,7 @@ fn run(cli: Cli) -> Result<(), String> { let mut recs: Vec = Vec::with_capacity(spec.steps as usize + 1); let t_start = Instant::now(); let mut blew_up = false; - let nodes_per_step = (spec.nx * spec.ny - + spec - .patch - .map(|(ax, bx, ay, by)| { - spec.refine * (spec.refine * (bx - ax) + 1) * (spec.refine * (by - ay) + 1) - }) - .unwrap_or(0)) as f64; + let nodes_per_step = spec.nodes_per_step(); let mut extras = Extras::default(); let series_every = cli.series_every.max(1); @@ -930,6 +1130,15 @@ fn run(cli: Cli) -> Result<(), String> { if need_report { live_line(&spec, &rec, d_ref, t_start.elapsed().as_secs_f64(), nodes_per_step, full); } + // y⁺ на теле: два десятка снимков во второй половине прогона. На GPU каждый снимок — + // скачивание самого тонкого уровня, поэтому редко. + if spec.case == Case::Channel && !spec.scene.bodies.is_empty() { + let half = spec.steps / 2; + let every = (spec.steps / 40).max(1); + if t >= half && (t - half) % every == 0 { + extras.yplus.push(back.yplus()); + } + } if spec.case.is_periodic() && t % cli.case_every.max(1) == 0 { back.flush(&mut scratch); let (vx, vy) = back.sample_velocity(); @@ -1057,6 +1266,8 @@ struct Extras { influence: Option<(R, R, R)>, /// Относительная ошибка поля u_x против точного решения Тейлора–Грина. tg_error: Option, + /// Снимки y⁺ первого узла на теле во второй половине прогона (канал). + yplus: Vec, } /// Интегральные метрики периодического течения: энергия, энстрофия, палинстрофия. @@ -1183,15 +1394,20 @@ fn print_header(spec: &Spec, cli: &Cli, plan: &gif::GifPlan, field: FieldKind) { println!("\n── Сетка ───────────────────────────────────────────────────────────────────"); println!(" L0 {:>12} {} узлов", format!("{}×{}", spec.nx, spec.ny), spec.nx * spec.ny); - match spec.patch { - Some((ax, bx, ay, by)) => { - let (nfx, nfy) = (spec.refine * (bx - ax) + 1, spec.refine * (by - ay) + 1); - let tau1 = spec.refine as R * (tau - 0.5) + 0.5; - println!(" L1 (×{}) {:>12} область x∈[{ax},{bx}] y∈[{ay},{by}], τ₁={:.4}", - spec.refine, format!("{nfx}×{nfy}"), tau1); - println!(" {:>12} {} подшагов на шаг L0", "", spec.refine); - } - None => println!(" L1 {:>12} (измельчение выключено)", "нет"), + let _ = tau; + let lv = spec.levels(); + if lv.len() == 1 { + println!(" L1 {:>12} (измельчение выключено)", "нет"); + } + for (k, p) in spec.patches.iter().enumerate() { + let g = &lv[k + 1]; + let (ax, bx, ay, by) = (p.ax, p.bx, p.ay, p.by); + println!(" L{} (×{}) {:>12} область L{k} x∈[{ax},{bx}] y∈[{ay},{by}], τ={:.4}, D={:.1}", + k + 1, p.r, format!("{}×{}", g.nx, g.ny), g.tau, spec.scene.ref_size() * g.scale); + } + if !spec.patches.is_empty() { + println!(" {:>12} {} подшагов тонкого уровня на шаг L0, {:.3e} обновлений узлов на шаг L0", + "", spec.substeps(), spec.nodes_per_step()); } println!(" оператор {:>12}", if spec.collision == Collision::Kbc { @@ -1574,12 +1790,12 @@ fn final_report( fn write_csv(path: &str, recs: &[StepRec], spec: &Spec, d_ref: R) -> std::io::Result<()> { let mut f = std::io::BufWriter::new(std::fs::File::create(path)?); - writeln!(f, "step,t_phys_s,cd,cl,cm,uy_probe,rho_mean,max_u,gamma_mean,gamma_min,gamma_max,degenerate_frac,xi_negative_frac")?; + writeln!(f, "step,t_phys_s,cd,cl,cm,uy_probe,rho_mean,max_u,gamma_mean,gamma_min,gamma_max,degenerate_frac,xi_negative_frac,rho_probe")?; let s = spec.units.u_lat * spec.units.u_lat * d_ref; for r in recs { writeln!( f, - "{},{:.9e},{:.6e},{:.6e},{:.6e},{:.6e},{:.9},{:.6e},{:.6},{:.6},{:.6},{:.6},{:.6}", + "{},{:.9e},{:.6e},{:.6e},{:.6e},{:.6e},{:.9},{:.6e},{:.6},{:.6},{:.6},{:.6},{:.6},{:.9}", r.step, spec.units.time_of_step(r.step), 2.0 * r.fx / s, @@ -1592,7 +1808,8 @@ fn write_csv(path: &str, recs: &[StepRec], spec: &Spec, d_ref: R) -> std::io::Re r.gamma_min, r.gamma_max, r.degenerate_frac, - r.xi_negative_frac + r.xi_negative_frac, + r.rho_probe )?; } Ok(()) @@ -1623,7 +1840,19 @@ fn write_summary( writeln!(f, " \"backend\": \"{}\",", cli.backend)?; writeln!(f, " \"wall\": \"{}\", \"kbc_model\": \"{}\", \"collision\": \"{}\",", cli.wall, cli.kbc_model, cli.collision)?; - writeln!(f, " \"nx\": {}, \"ny\": {}, \"refine\": {},", spec.nx, spec.ny, spec.refine)?; + writeln!(f, " \"nx\": {}, \"ny\": {}, \"refine\": {},", spec.nx, spec.ny, spec.substeps())?; + let lv = spec.levels(); + let levels: Vec = spec + .patches + .iter() + .zip(lv.iter().skip(1)) + .map(|(p, g)| { + format!("{{\"r\": {}, \"nx\": {}, \"ny\": {}, \"box\": [{}, {}, {}, {}]}}", + p.r, g.nx, g.ny, p.ax, p.bx, p.ay, p.by) + }) + .collect(); + writeln!(f, " \"levels\": [{}], \"nodes_per_step\": {}, \"wall_function\": \"{}\", \"sponge_side\": {},", + levels.join(", "), spec.nodes_per_step(), spec.wall_fn.name(), spec.sponge_side)?; writeln!(f, " \"bodies\": {}, \"body_size_cells\": {},", spec.scene.bodies.len(), q(spec.scene.ref_size()))?; writeln!(f, " \"re\": {}, \"u_lat\": {}, \"mach\": {}, \"tau\": {},", @@ -1655,10 +1884,11 @@ fn write_summary( let rho: Vec = recs[h..].iter().map(|r| r.rho_mean).collect(); let (st, _, _) = math::strouhal(&uy, u.d_lat, u.u_lat, 8192); let (cdm, _) = math::mean_std(&cd); - let (_, clr) = math::mean_std(&cl); + let (clm, clr) = math::mean_std(&cl); let (cmm, _) = math::mean_std(&cm); let (rm, _) = math::mean_std(&rho); let bl = spec.scene.frontal_extent() / spec.ny as R; + writeln!(f, " \"cl_mean\": {},", q(clm))?; writeln!(f, " \"strouhal\": {}, \"cd\": {}, \"cl_rms\": {}, \"cm\": {},", q(st), q(cdm), q(clr), q(cmm))?; writeln!(f, " \"blockage\": {}, \"strouhal_corr\": {}, \"cd_corr\": {}, \"cl_rms_corr\": {},", @@ -1690,6 +1920,18 @@ fn write_summary( math::mean_std(&recs[h..].iter().map(|r| r.degenerate_frac).collect::>()); let (xn, _) = math::mean_std(&recs[h..].iter().map(|r| r.xi_negative_frac).collect::>()); + // Пульсация плотности в зонде во второй половине: мера паразитной акустики — + // волн, отражённых от границ домена и от стыков уровней. + let (_, rp) = math::mean_std(&recs[h..].iter().map(|r| r.rho_probe).collect::>()); + writeln!(f, " \"rho_probe_rms\": {},", q(rp))?; + if !extras.yplus.is_empty() { + let k = extras.yplus.len() as R; + let avg = |g: &dyn Fn(&cpu::YPlus) -> R| extras.yplus.iter().map(g).sum::() / k; + let ymax = extras.yplus.iter().map(|y| y.max).fold(0.0, R::max); + writeln!(f, " \"yplus_mean\": {}, \"yplus_max\": {}, \"yplus_frac_log\": {}, \"u_tau_mean\": {}, \"yplus_nodes\": {},", + q(avg(&|y| y.mean)), q(ymax), q(avg(&|y| y.frac_log)), + q(avg(&|y| y.u_tau_mean)), extras.yplus[0].nodes)?; + } writeln!(f, " \"gamma_mean\": {}, \"degenerate_frac\": {}, \"xi_negative_frac\": {}", q(gm), q(dg), q(xn))?; } else { @@ -1698,3 +1940,100 @@ fn write_summary( writeln!(f, "}}")?; Ok(()) } + +// ───────────────────────────────────────────────────────────────────────────── +// Тесты постановки и многоуровневой сетки +// ───────────────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + + fn spec_of(args: &[&str]) -> Spec { + let mut v = vec!["kbc2d"]; + v.extend_from_slice(args); + build_spec(&Cli::try_parse_from(v).expect("ключи")).expect("постановка") + } + + /// Цепочка уровней: каждый следующий в r раз тоньше, лежит внутри родителя, а τ растёт + /// так, что ν одинакова на всех уровнях: ν = c_s²(τ − ½)/scale в единицах L0. + #[test] + fn levels_chain_is_consistent() { + let s = spec_of(&[ + "--shape", "cylinder", "--size", "8", "--nx", "240", "--ny", "160", "--re", "20", + "--levels", "2:4,10,4; 2:1.5,4,1.5", + ]); + let lv = s.levels(); + assert_eq!(lv.len(), 3); + assert_eq!(s.substeps(), 4); + let nu0 = math::CS2 * (lv[0].tau - 0.5); + for (k, p) in s.patches.iter().enumerate() { + let (par, g) = (&lv[k], &lv[k + 1]); + assert!(p.ax >= 2 && p.bx + 2 <= par.nx && p.ay >= 1 && p.by + 2 <= par.ny); + assert_eq!(g.nx, p.r * (p.bx - p.ax) + 1); + assert_eq!(g.scale, par.scale * p.r as R); + let nu = math::CS2 * (g.tau - 0.5) / g.scale; + assert!((nu - nu0).abs() < 1e-14 * nu0, "ν уровня {} = {nu}, L0 = {nu0}", k + 1); + } + // тело лежит внутри самого тонкого уровня + let (cx, cy) = s.scene.center(); + let f = lv[2]; + let (x, y) = ((cx - f.ox) * f.scale, (cy - f.oy) * f.scale); + assert!(x > 0.0 && x < f.nx as R && y > 0.0 && y < f.ny as R); + } + + /// Однородный поток без тела — неподвижная точка всей схемы: столкновения, переноса, + /// стенок, входа/выхода и связки трёх уровней. Любая ошибка в рамке, рестрикции или + /// пересчёте неравновесия между уровнями сдвинула бы его с места. + #[test] + fn uniform_flow_is_fixed_point_across_levels() { + let mut s = spec_of(&[ + "--shape", "cylinder", "--size", "6", "--nx", "120", "--ny", "80", "--re", "50", + "--levels", "2:3,8,3; 2:1.5,4,1.5", "--sponge-len", "0", + ]); + s.scene.bodies.clear(); + let u = s.units.u_lat; + let mut sim = cpu::Sim::new(s); + for _ in 0..200 { + sim.step(); + } + for (k, l) in sim.levels.iter().enumerate() { + let mut worst: R = 0.0; + for n in 0..l.nx * l.ny { + let (rho, ux, uy) = l.macros_at(n); + worst = worst.max((rho - 1.0).abs()).max((ux - u).abs()).max(uy.abs()); + } + assert!(worst < 1e-12, "уровень {k}: отклонение {worst:.3e}"); + } + } + + /// Три уровня (×2, ×2) против одного патча ×4 с тем же разрешением тела — стационарное + /// обтекание при Re = 20 обязано дать тот же Cd. Различие — только в огрублении снаружи. + #[test] + #[ignore = "длинный прогон (~10 с): cargo test --release -- --ignored levels_match"] + fn levels_match_single_patch_on_steady_cylinder() { + let common = [ + "--shape", "cylinder", "--size", "6", "--nx", "180", "--ny", "120", "--re", "20", + "--wall", "bouzidi", "--steps", "2500", + ]; + let cd = |extra: &[&str]| { + let mut a: Vec<&str> = common.to_vec(); + a.extend_from_slice(extra); + let s = spec_of(&a); + let mut sim = cpu::Sim::new(s.clone()); + let d = sim.force_ref_size(); + let mut acc = Vec::new(); + for t in 0..s.steps { + let r = sim.step(); + if t > s.steps * 4 / 5 { + acc.push(math::coefficients(r.fx, r.fy, r.tz, s.units.u_lat, d).0); + } + } + acc.iter().sum::() / acc.len() as R + }; + let one = cd(&["--levels", "4:2.5,6,2.5"]); + let two = cd(&["--levels", "2:2.5,6,2.5; 2:1.5,4,1.5"]); + println!("Cd: один патч ×4 {one:.5}, цепочка ×2×2 {two:.5}"); + assert!((one - two).abs() < 0.01 * one, "Cd {one} против {two}"); + } +} diff --git a/docs/theory/2d_solver/src/math.rs b/docs/theory/2d_solver/src/math.rs index 6b9b187..fc05864 100644 --- a/docs/theory/2d_solver/src/math.rs +++ b/docs/theory/2d_solver/src/math.rs @@ -290,6 +290,43 @@ pub fn beta_profile(nx: usize, beta0: R, sponge_in: usize, sponge_out: usize, mu .collect() } +/// Поле β по ВСЕМ узлам уровня (узел = y·nx + x): губки у входа и выхода, как в +/// `beta_profile`, плюс боковые губки шириной `sponge_side` рядов у верхней и нижней стенок. +/// Губки не складываются — в каждом узле берётся сильнейшая. При `sponge_side = 0` значения +/// совпадают с `beta_profile` побитово. +pub fn beta_field( + nx: usize, + ny: usize, + beta0: R, + sponge_in: usize, + sponge_out: usize, + sponge_side: usize, + mult: R, +) -> Vec { + let cols = beta_profile(nx, beta0, sponge_in, sponge_out, mult); + if sponge_side == 0 || mult == 1.0 { + let mut out = Vec::with_capacity(nx * ny); + for _ in 0..ny { + out.extend_from_slice(&cols); + } + return out; + } + let nu = nu_of_beta(beta0); + let strength = |b: R| if b == beta0 { 0.0 } else { (nu_of_beta(b) / nu - 1.0) / (mult - 1.0) }; + let w = sponge_side as R; + let mut out = Vec::with_capacity(nx * ny); + for y in 0..ny { + let lo = smoothstep((w - y as R) / w); + let hi = smoothstep((y as R - (ny - 1) as R + w) / w); + let sy = lo.max(hi); + for &bc in &cols { + // где боковая губка не сильнее продольной, остаётся значение столбца как есть + out.push(if sy <= strength(bc) { bc } else { beta_of_nu(nu * (1.0 + (mult - 1.0) * sy)) }); + } + } + out +} + /// Кинематическая вязкость по β, формула (5): ν = c_s²(1/(2β) − 1/2). #[inline] pub fn nu_of_beta(beta: R) -> R { @@ -985,6 +1022,195 @@ pub fn grad_target_velocity_term(q: R, u_far: R, u_wall: R) -> R { (q * u_far + u_wall) / (1.0 + q) } +// ───────────────────────────────────────────────────────────────────────────── +// Пристеночная функция +// ───────────────────────────────────────────────────────────────────────────── + +/// Пристеночная функция поверх модели стенки: чем задаётся профиль скорости между стенкой и +/// первым жидким узлом, когда сетка пограничный слой не разрешает. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum WallFunction { + /// Нет: стенка — обычное прилипание выбранной модели. + None, + /// Закон Spalding (1961) — ОДНА формула на весь пристеночный слой, от вязкого подслоя + /// (u⁺ = y⁺) до логарифмической зоны. Поэтому он безопасен и там, где первая клетка + /// лежит в подслое: тогда профиль сам сводится к линейному, то есть к прилипанию. + Spalding, +} + +impl WallFunction { + pub fn from_str(s: &str) -> Option { + Some(match s.to_ascii_lowercase().as_str() { + "none" | "off" => WallFunction::None, + "spalding" => WallFunction::Spalding, + _ => return None, + }) + } + pub const ALL: [&'static str; 2] = ["none", "spalding"]; + pub fn is_on(&self) -> bool { + !matches!(self, WallFunction::None) + } + pub fn name(&self) -> &'static str { + match self { + WallFunction::None => "none", + WallFunction::Spalding => "spalding", + } + } +} + +/// Постоянные закона стенки (Spalding 1961, классические значения). +pub const KAPPA: R = 0.41; +pub const LOG_B: R = 5.2; + +/// y⁺(u⁺) по Spalding: +/// +/// y⁺ = u⁺ + e^{−κB}·[e^{κu⁺} − 1 − κu⁺ − (κu⁺)²/2 − (κu⁺)³/6] +/// +/// Возвращает (y⁺, dy⁺/du⁺). Функция монотонно растёт и выпукла при u⁺ > 0. +#[inline] +pub fn spalding_yplus(up: R) -> (R, R) { + let k = KAPPA * up; + let e = (-KAPPA * LOG_B).exp(); + let ek = k.exp(); + let y = up + e * (ek - 1.0 - k - 0.5 * k * k - k * k * k / 6.0); + let dy = 1.0 + e * KAPPA * (ek - 1.0 - k - 0.5 * k * k); + (y, dy) +} + +/// u⁺(y⁺) — обращение закона Spalding методом Ньютона. +/// +/// Старт берётся из нужной асимптоты: в подслое u⁺ ≈ y⁺, в логарифмической зоне +/// u⁺ ≈ ln(y⁺)/κ + B. Функция y⁺(u⁺) выпукла, поэтому Ньютон от старта не меньше корня +/// сходится монотонно; на всякий случай итерация зажата снизу нулём. +pub fn spalding_uplus(yp: R) -> R { + if yp <= 0.0 { + return 0.0; + } + let mut u = if yp < 11.0 { yp } else { (yp.ln() / KAPPA + LOG_B).max(1e-6) }; + for _ in 0..50 { + let (y, dy) = spalding_yplus(u); + let du = (y - yp) / dy; + u = (u - du).max(0.0); + if du.abs() <= 1e-12 * (1.0 + u) { + break; + } + } + u +} + +/// Динамическая скорость u_τ по касательной скорости `u` в точке на расстоянии `y` от стенки +/// и кинематической вязкости `nu` (всё в решёточных единицах). +/// +/// Неизвестным удобнее брать x = u⁺ = u/u_τ: тогда y⁺ = y·u/(ν·x), и уравнение +/// x·y⁺(x) = Re_y, где Re_y = y·u/ν, — с монотонной левой частью. Старт x = √Re_y — точный +/// корень в подслое (там y⁺(x) = x). +pub fn spalding_utau(u: R, y: R, nu: R) -> R { + if u <= 1e-14 || y <= 0.0 { + return 0.0; + } + let re_y = y * u / nu; + let mut x = re_y.sqrt(); + for _ in 0..60 { + let (g, dg) = spalding_yplus(x); + let h = x * g - re_y; + let dh = g + x * dg; + let dx = h / dh; + x = (x - dx).max(1e-9); + if dx.abs() <= 1e-12 * (1.0 + x) { + break; + } + } + u / x +} + +/// Итог пристеночной функции для одного граничного узла. +#[derive(Clone, Copy, Debug, Default)] +pub struct WallFnOut { + /// Модельная касательная скорость в самом граничном узле (вдоль `e`). + pub u_node: R, + /// Динамическая скорость. + pub u_tau: R, + /// y⁺ граничного узла: y_w·u_τ/ν. + pub y_plus: R, + /// Единичное касательное направление течения у стенки. + pub ex: R, + pub ey: R, + /// Касательная скорость в точке отбора. + pub u_sample: R, +} + +/// Пристеночная функция в одном узле: по скорости (usx, usy) в точке отбора на расстоянии +/// `y_s` от стенки и нормали (nx, ny) — u_τ и модельная касательная скорость в самом узле на +/// расстоянии `y_w`. `None`, если касательной скорости в точке отбора нет вовсе. +#[allow(clippy::too_many_arguments)] +pub fn wall_function( + usx: R, + usy: R, + nx: R, + ny: R, + y_w: R, + y_s: R, + nu: R, +) -> Option { + let un = usx * nx + usy * ny; + let tx = usx - un * nx; + let ty = usy - un * ny; + let ut = (tx * tx + ty * ty).sqrt(); + if ut <= 1e-14 { + return None; + } + let u_tau = spalding_utau(ut, y_s, nu); + let y_plus = y_w * u_tau / nu; + let u_node = u_tau * spalding_uplus(y_plus); + Some(WallFnOut { u_node, u_tau, y_plus, ex: tx / ut, ey: ty / ut, u_sample: ut }) +} + +/// Скорость скольжения стенки для полинковой схемы (Bouzidi), согласованная по НАПРЯЖЕНИЮ: +/// отскок от подвижной стенки передаёт ей импульс ≈ ρν(u_узла − u_s)/y_w, и чтобы при +/// модельной скорости узла это было модельным τ_w = ρu_τ², стенка должна двигаться со +/// скоростью +/// +/// u_s = u_node − u_τ²·y_w/ν = u_τ·(u⁺(y⁺_w) − y⁺_w), зажато в [−u_sample, u_sample]. +/// +/// В вязком подслое u⁺ = y⁺ и скольжение ТОЖДЕСТВЕННО ноль — обычное прилипание, побитово. +/// В буферной и логарифмической зонах u⁺ < y⁺, и стенка «отступает» против потока (u_s < 0), +/// добирая трение до τ_w. Это та же пара (u_node, τ_w), что моментные схемы ставят через +/// целевую скорость и тензор давлений, — так сравнение схем честное. +/// +/// Разрешённую скорость узла вместо модельной брать нельзя: у искривлённой стенки она не +/// лежит на прямой от стенки к точке отбора, и скольжение переставало обращаться в ноль +/// даже при y⁺ ≪ 1 (цилиндр Re = 20: −2 % по Cd против прилипания). +/// +/// Почему не согласование по СКОРОСТИ (прямая через стенку, узел и точку отбора): решатель +/// не имеет турбулентной вязкости, и такое скольжение выходит положительным и большим — +/// стенка становится почти свободной, исчезают трение и отрыв. Замерено: цилиндр Re = 10⁴, +/// D = 16 давал Cd = 0.016. Зажим по модулю скорости в точке отбора держит скорость стенки +/// в пределах скоростей самого течения — иначе при y⁺ ≫ 30 она выходила бы за число Маха. +#[inline] +pub fn wall_slip(w: &WallFnOut, y_w: R, nu: R) -> R { + (w.u_node - w.u_tau * w.u_tau * y_w / nu).clamp(-w.u_sample, w.u_sample) +} + +/// Поправка градиента скорости граничного узла под пристеночную функцию: нормальная +/// производная касательной скорости заменяется модельной u_τ²/ν, остальные компоненты +/// остаются конечно-разностными. Добавка имеет вид d·(e⊗n), поэтому дивергенцию не меняет +/// (e ⊥ n). Возвращает исправленные (∂u/∂x, ∂u/∂y, ∂v/∂x, ∂v/∂y). +#[allow(clippy::too_many_arguments)] +#[inline] +pub fn wall_function_gradient( + g: (R, R, R, R), + w: &WallFnOut, + nx: R, + ny: R, + nu: R, +) -> (R, R, R, R) { + let (dudx, dudy, dvdx, dvdy) = g; + let (ex, ey) = (w.ex, w.ey); + let g_en = ex * (dudx * nx + dudy * ny) + ey * (dvdx * nx + dvdy * ny); + let d = w.u_tau * w.u_tau / nu - g_en; + (dudx + d * ex * nx, dudy + d * ex * ny, dvdx + d * ey * nx, dvdy + d * ey * ny) +} + // ───────────────────────────────────────────────────────────────────────────── // Сила на теле // ───────────────────────────────────────────────────────────────────────────── @@ -1529,4 +1755,82 @@ mod tests { } inv } + + /// Закон Spalding в обеих асимптотах: в вязком подслое u⁺ = y⁺, в логарифмической зоне + /// u⁺ = ln(y⁺)/κ + B. Формула одна, асимптоты — её свойства, а не ветки кода. + #[test] + fn spalding_matches_both_asymptotes() { + for yp in [0.01, 0.1, 1.0] { + let up = spalding_uplus(yp); + assert!((up - yp).abs() < 1e-3 * yp.max(1e-3), "подслой: y⁺={yp}, u⁺={up}"); + } + for yp in [300.0, 1000.0, 5000.0] { + let up = spalding_uplus(yp); + let log = yp.ln() / KAPPA + LOG_B; + assert!((up - log).abs() < 0.02 * log, "log-зона: y⁺={yp}, u⁺={up}, лог {log}"); + } + } + + /// Обращение точное и монотонное: y⁺(u⁺(y⁺)) = y⁺ на всём диапазоне. + #[test] + fn spalding_inverse_roundtrip_and_monotone() { + let mut prev = 0.0; + let mut yp = 1e-3; + while yp < 1e5 { + let up = spalding_uplus(yp); + assert!(up > prev, "u⁺ обязан расти: y⁺={yp}"); + prev = up; + let (back, _) = spalding_yplus(up); + assert!((back - yp).abs() < 1e-9 * yp.max(1.0), "y⁺={yp} → u⁺={up} → {back}"); + yp *= 1.7; + } + } + + /// u_τ по скорости в точке: подставленная обратно, она даёт ту же скорость. В подслое + /// u_τ = √(ν·u/y) — линейный профиль, то есть ровно прилипание. + #[test] + fn spalding_utau_consistent() { + let nu = 1e-3; + for (u, y) in [(0.05, 1.5), (0.05, 30.0), (1e-4, 1.0), (0.1, 200.0)] { + let ut = spalding_utau(u, y, nu); + let up = spalding_uplus(y * ut / nu); + assert!((ut * up - u).abs() < 1e-9 * u, "u={u} y={y}: u_τ·u⁺ = {}", ut * up); + } + let (u, y) = (1e-5, 0.5); + let ut = spalding_utau(u, y, nu); + assert!((ut - (nu * u / y).sqrt()).abs() < 1e-6 * ut, "подслой: u_τ = {ut}"); + assert_eq!(spalding_utau(0.0, 1.0, nu), 0.0); + } + + /// В вязком подслое пристеночная функция обязана свестись к прилипанию: модельная + /// скорость узла — линейная интерполяция от стенки, скольжение — ноль, а модельный + /// градиент — тот же наклон прямой. + #[test] + fn wall_function_reduces_to_no_slip_in_sublayer() { + let nu = 0.05; // низкий Re: y⁺ первого узла заведомо < 1 + let (y_w, y_s) = (0.4, 1.4); + let (ux, uy) = (0.01, 0.0); // стенка снизу, нормаль +y + let w = wall_function(ux, uy, 0.0, 1.0, y_w, y_s, nu).unwrap(); + assert!(w.y_plus < 1.0, "y⁺ = {}", w.y_plus); + let lin = ux * y_w / y_s; + assert!((w.u_node - lin).abs() < 1e-3 * lin, "u_node {} против {lin}", w.u_node); + assert!(wall_slip(&w, y_w, nu).abs() < 1e-3 * lin); + let g = wall_function_gradient((0.0, 0.0, 0.0, 0.0), &w, 0.0, 1.0, nu); + assert!((g.1 - ux / y_s).abs() < 1e-3 * ux / y_s, "∂u/∂y = {}", g.1); + assert_eq!((g.0, g.2, g.3), (0.0, 0.0, 0.0)); + } + + /// В логарифмической зоне разрешённый градиент меньше модельного напряжения, и стенка + /// отступает против потока: скольжение отрицательно, но по модулю не больше скорости + /// в точке отбора. + #[test] + fn wall_function_slips_in_log_region() { + let nu = 1e-5; + let (y_w, y_s) = (0.5, 1.5); + let w = wall_function(0.05, 0.0, 0.0, 1.0, y_w, y_s, nu).unwrap(); + assert!(w.y_plus > 30.0, "y⁺ = {}", w.y_plus); + let s = wall_slip(&w, y_w, nu); + assert!(s < 0.0 && s >= -w.u_sample, "скольжение {s}, u_sample {}", w.u_sample); + assert!(wall_function(0.0, 0.03, 0.0, 1.0, y_w, y_s, nu).is_none(), "нормальный поток"); + } }