Сверка со статьями: эталон Тейлора–Грина, выбор модели 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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user