Сверка со статьями: эталон Тейлора–Грина, выбор модели KBC, лечение продольной акустики

Проверка переориентирована на первоисточники из docs/origins вместо сравнения с
python-прототипом.

ЭТАЛОН ТЕЙЛОРА–ГРИНА (разд. VI 2D-статьи) — единственное течение статьи с точным
аналитическим решением. Второй порядок сходимости воспроизведён: 2.05 и 2.01 на
сетках 64/128/256, отдельно по амплитуде и по форме. Два места потребовали разбора:
* начальное давление. Течение несёт собственное поле p ~ ρu₀², находимое из
  ∇²p = 2ρ(ψ_xx·ψ_yy − ψ_xy²). Старт с ρ ≡ 1 сбрасывает разницу в акустику, которая
  в периодическом ящике не затухает и садится полкой на ошибку. Работа 2024 года
  делает то же самое явно, решая ∂ρ/∂t + ∇·(ρu₀) = D∇²ρ до стационара;
* способ измельчения. При фиксированном u₀ ошибка упирается в полку O(Ma²) (измерено:
  относительная ошибка формы ∝ u₀^1.07). Порядок виден целиком только при диффузионном
  измельчении, ν = const и u₀ ∝ 1/N. Это свойство слабо-сжимаемого метода, а не
  реализации: LBGK на том же тесте даёт ту же полку, что согласуется с утверждением
  статьи о практически одинаковом поведении всех моделей.
Добавлено приближение Града (ур. 58) для согласованного старта эталонов.

СВЕРКА ОПЕРАТОРА с пошаговым листингом работы 2024 года — совпадает дословно, включая
Δh = f − f^eq − Δs и f′ = f − β(2Δs + γΔh). Та же работа подтверждает относительный
порог вырожденности γ: стабилизатор «далеко не постоянен», а MRT с γ = const не
достигает той же устойчивости.

ВЫБОР СОСТАВА СДВИГОВОЙ ЧАСТИ (табл. I) — ключ --kbc-model: n1 = {N, Π_xy} (KBC D),
n2 = {N, Π_xy, T} (KBC C). По точности неразличимы, как и заявляет статья. Разница в
объёмной вязкости (ур. 57): у n2 она фиксирована ξ = ν, у n1 равна c_s²(1/(γβ) − ½) и
при измеренной ⟨γ⟩ ≈ 1.73 < 2 в среднем вчетверо больше ν. Поэтому вопреки ожиданию
именно n1 сильнее демпфирует продольную акустику и оставлен умолчанием.

ПРОДОЛЬНАЯ АКУСТИКА КАНАЛА — найдена причина «поршневого» поведения потока. Вход по
скорости акустически есть жёсткий поршень, выход по давлению — открытый конец; канал
работает четвертьволновым резонатором. Измерено: период пульсации 3332 шага против
расчётных 4·Nx/c_s = 3325, первый ноль автокорреляции ровно на Nx/c_s, затухание за
30000 шагов — 3.8%, то есть мода не гаснет. Возбуждал её сам старт из покоя.
Исправлено умолчаниями: --init uniform (домен сразу заполнен потоком) и автоподбор
губки по длине домена. Пульсация упала с 75% до 1.0% от скорости потока, а числа
выправились сами: St 0.183 против прежних 0.194 (литература 0.183), rms Cl 0.38
против 0.84, ⟨ρ⟩ = 1.0000. Отчёт теперь печатает период моды, время её вязкого
затухания и измеренную пульсацию с предупреждением при превышении 5%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 21:39:08 +03:00
co-authored by Claude Opus 5
parent 5d762ad9d6
commit f174f1c9a0
5 changed files with 709 additions and 198 deletions
+40 -15
View File
@@ -49,7 +49,8 @@ struct Dyn {
ux_in: f32,
uy_in: f32,
rho_out: f32,
_pad0: f32,
/// 0 — сдвиговая часть только девиатор (N1), 1 — девиатор со следом (N2)
kbc_model: u32,
outlet_extrap: u32,
nparts: u32,
refine: u32,
@@ -126,7 +127,7 @@ struct Results {
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 };
struct Dyn { ux_in:f32, uy_in:f32, rho_out:f32, p0:f32, outlet_extrap:u32, nparts:u32, refine:u32, collision:u32 };
struct Dyn { ux_in:f32, uy_in:f32, rho_out:f32, kbc_model:u32, outlet_extrap:u32, nparts:u32, refine:u32, collision:u32 };
struct Substep { idx:u32, p0:u32, p1:u32, p2:u32 };
struct AmrParams { nghost:u32, nrestrict:u32, ccount:u32, fcount:u32, r01:f32, rfc:f32, w:f32, pad:f32 };
struct GLink { node:u32, far:u32, i:u32, ib:u32, kind:u32, q:f32, p0:u32, p1:u32 };
@@ -186,11 +187,18 @@ fn macros9(fv: array<f32,9>) -> vec3<f32> {
return vec3<f32>(rho, mx/rho, my/rho);
}
// Проекция неравновесия на сдвиговую часть модели KBC D: моменты N = M20-M02 и Pi_xy = M11
fn shift9(d: array<f32,9>) -> array<f32,9> {
// Проекция неравновесия на сдвиговую часть. model=0: {N, Pi_xy} (N1); model=1: плюс след T (N2)
fn shift9(d: array<f32,9>, model: u32) -> array<f32,9> {
let a = 0.25*(d[1] + d[3] - d[2] - d[4]);
let b = 0.25*(d[5] + d[7] - d[6] - d[8]);
return array<f32,9>(0.0, a, -a, a, -a, b, -b, b, -b);
var s = array<f32,9>(0.0, a, -a, a, -a, b, -b, b, -b);
if (model == 1u) {
let dt = d[1] + d[2] + d[3] + d[4] + 2.0*(d[5] + d[6] + d[7] + d[8]);
let q = 0.25*dt;
s[0] = s[0] - dt;
s[1] = s[1] + q; s[2] = s[2] + q; s[3] = s[3] + q; s[4] = s[4] + q;
}
return s;
}
fn load9(base: ptr<function, array<f32,9>>, off: u32, n: u32) {
@@ -200,7 +208,7 @@ fn load9(base: ptr<function, array<f32,9>>, off: u32, n: u32) {
// Возвращает (пост-столкновительные популяции, gamma, признак вырождения)
struct CollOut { fv: array<f32,9>, gamma: f32, degen: f32 };
fn collide9(fin: array<f32,9>, b: f32, op: u32) -> CollOut {
fn collide9(fin: array<f32,9>, b: f32, op: u32, model: u32) -> CollOut {
var out: CollOut;
// WGSL разрешает переменный индекс только по памяти (var), а не по значению (let/параметр),
// поэтому всё, что индексируется в цикле, кладётся в var
@@ -215,7 +223,7 @@ fn collide9(fin: array<f32,9>, b: f32, op: u32) -> CollOut {
}
var d: array<f32,9>;
for (var i = 0u; i < 9u; i = i + 1u) { d[i] = fv[i] - fe[i]; }
var ds = shift9(d);
var ds = shift9(d, model);
var num = 0.0; var den = 0.0; var nrm = 0.0;
for (var i = 0u; i < 9u; i = i + 1u) {
let inv = 1.0 / fe[i];
@@ -273,7 +281,7 @@ fn k_collide(@builtin(global_invocation_id) gid: vec3<u32>) {
gam[nd] = 2.0;
return;
}
var r = collide9(fv, beta[nd % P.nx], D.collision);
var r = collide9(fv, beta[nd % P.nx], D.collision, D.kbc_model);
for (var i = 0u; i < 9u; i = i + 1u) { post[i*P.n + nd] = r.fv[i]; }
gam[nd] = r.gamma;
}
@@ -422,7 +430,9 @@ fn k_stats1(@builtin(global_invocation_id) gid: vec3<u32>,
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 xi = CS2*(1.0/(g*b) - 0.5);
// при следе в сдвиговой части (N2) объёмная вязкость равна сдвиговой и всегда > 0
var xi = CS2*(1.0/(b + b) - 0.5);
if (D.kbc_model == 0u) { xi = CS2*(1.0/(g*b) - 0.5); }
p.xineg = select(0.0, 1.0, xi < 0.0);
}
// зонд следа снимается с того уровня, на котором он лежит
@@ -596,8 +606,8 @@ impl GpuLevel {
}
}
fn soa_equilibrium(n: usize) -> Vec<f32> {
let fe = math::feq(1.0, 0.0, 0.0);
fn soa_equilibrium(n: usize, u0: (R, R)) -> Vec<f32> {
let fe = math::feq(1.0, u0.0, u0.1);
let mut v = vec![0.0f32; 9 * n];
for i in 0..9 {
for k in 0..n {
@@ -617,9 +627,10 @@ fn make_level(
beta: &[f32],
probe_node: u32,
flags: u32,
u0: (R, R),
) -> GpuLevel {
let n = nx * ny;
let init = soa_equilibrium(n);
let init = soa_equilibrium(n, u0);
let mkf = |label: &str| {
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(label),
@@ -861,6 +872,14 @@ impl Sim {
})
.collect();
// стартовое поле: либо сразу набегающий поток, либо покой (см. cpu.rs)
let u0 = if spec.init_uniform {
let (sn, cs) = spec.flow_angle.sin_cos();
(spec.units.u_lat * cs, spec.units.u_lat * sn)
} else {
(0.0, 0.0)
};
// ── зонд ──
let (px, py) = spec.probe;
let probe_on_fine = matches!(spec.patch, Some((ax, bx, ay, by))
@@ -879,6 +898,7 @@ impl Sim {
&beta0,
probe0 as u32,
flags0,
u0,
);
let pre = device.create_buffer(&wgpu::BufferDescriptor {
@@ -907,7 +927,7 @@ impl Sim {
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);
make_level(&device, &bgl_level, nfx, nfy, &geom1, &beta1, probe1 as u32, flags1, u0);
let ghosts: Vec<GGhost> = patch
.ghosts()
@@ -1053,7 +1073,12 @@ impl Sim {
fn inlet(&self, t: u64) -> (R, R) {
let sp = &self.spec;
let u = sp.units.u_lat * math::smoothstep(t as R / sp.ramp.max(1) as R);
let ramp = if sp.init_uniform {
1.0
} else {
math::smoothstep(t as R / sp.ramp.max(1) as R)
};
let u = sp.units.u_lat * ramp;
let (s, c) = sp.flow_angle.sin_cos();
let mut uy = u * s;
if sp.pert_dur > 0 && t >= sp.ramp && t < sp.ramp + sp.pert_dur {
@@ -1074,7 +1099,7 @@ impl Sim {
ux_in: ux_in as f32,
uy_in: uy_in as f32,
rho_out: 1.0,
_pad0: 0.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,