Эталонные течения статей, машиночитаемая сводка, радиус влияния и прореживание кадра
--case: помимо канала появились три ПЕРИОДИЧЕСКИХ постановки из статей — вихрь Тейлора-Грина
(разд. VI), дважды периодический сдвиговый слой (разд. VII) и затухающая двумерная
турбулентность. Перенос уже был периодичен, поэтому достаточно не накладывать ГУ вовсе.
Стартовые поля строятся общим кодом cpu::initial_field для обоих бэкендов: GPU только
перекладывает результат в свою раскладку, так что эталон задан ровно одним куском кода.
Три вещи, без которых эталоны считались неверно, и все три нашлись замером:
* в периодической постановке всё равно строилось тело — цилиндр по умолчанию стоял прямо
посреди эталонного течения. Теперь сцена пуста;
* Re считался по калибру тела, а статьи определяют его по РАЗМЕРУ ДОМЕНА (Re = u0*N/nu);
* и, главное, включалась выходная губка, поднимающая вязкость в 30 раз на последних столбцах.
Отсюда затухание было в 1.37 раза выше положенного. Губок в периодической постановке нет.
Что эталоны показали. Сдвиговый слой при Re=3e4 воспроизводит главное утверждение статьи:
KBC доживает до конца (энстрофия 0.871 от начальной), LBGK разваливается на шаге 4480.
Тейлор-Грин на CPU даёт порядок сходимости 2.03 и 2.01 — чистый второй.
ЗАМЕРЕНА ГРАНИЦА ПРИМЕНИМОСТИ f32, ровно та, ради которой планировалась отдельная группа
прогонов. Ошибка против точного решения, диффузионное измельчение:
N=64 CPU 9.83e-3 GPU 9.80e-3
N=128 CPU 2.40e-3 GPU 3.29e-3
N=256 CPU 5.97e-4 GPU 2.37e-2
GPU совпадает с f64, пока истинная ошибка выше ~1e-3, и промахивается в 40 раз, как только
она опускается ниже. Правило для кампании: точностные исследования сходимости — только на CPU.
--summary: сводка прогона одним JSON (St, Cd, rms Cl, Cm по каждому телу, ⟨ρ⟩, пульсация,
статистика гамма, радиус влияния, MLUPS, признак развала). Без неё разбор кампании из десятков
прогонов пришлось бы вести глазами.
--gif-downsample: усреднение блока k*k в пиксель. Без него кадр с сетки 4096x2048 неподъёмен;
проверено на 960x480 при k=3 — гифка 320x160 и 0.17 МБ.
РАДИУС ВЛИЯНИЯ в отчёте и сводке: докуда тело возмущает поток больше чем на 1% от U, в
калибрах, с предупреждением, если возмущение достаёт до границы домена. На проверочном прогоне
боковое влияние вышло ровно на границу (5.0 калибра при полуширине 5.0) — сигнал работает.
Также: --case-csv с рядом энергии, энстрофии и палинстрофии; --series-every для прореживания
рядов на сверхдлинных прогонах.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,7 @@ use wgpu::util::DeviceExt;
|
||||
|
||||
use crate::cpu;
|
||||
use crate::math::{self, R};
|
||||
use crate::{Collision, FieldKind, Spec, StepRec};
|
||||
use crate::{Case, Collision, FieldKind, Spec, StepRec};
|
||||
|
||||
const WG: u32 = 64;
|
||||
|
||||
@@ -804,31 +804,14 @@ impl GpuLevel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Стартовое поле в раскладке SoA. Если задана сцена, скорость сводится к нулю на подходе
|
||||
/// к телу (см. `math::wall_taper`) — иначе на первом шаге возникает разрыв и импульс сжатия.
|
||||
fn soa_equilibrium(
|
||||
n: usize,
|
||||
nx: usize,
|
||||
u0: (R, R),
|
||||
taper: Option<(&math::Scene, R)>,
|
||||
) -> Vec<f32> {
|
||||
/// Переложить готовое стартовое поле (построенное общим кодом в `cpu::initial_field`)
|
||||
/// из AoS в раскладку SoA, которой пользуется GPU.
|
||||
fn to_soa(f: &[[R; math::Q]]) -> Vec<f32> {
|
||||
let n = f.len();
|
||||
let mut v = vec![0.0f32; 9 * n];
|
||||
let plain = math::feq(1.0, u0.0, u0.1);
|
||||
for k in 0..n {
|
||||
let fe = match taper {
|
||||
Some((scene, w)) => {
|
||||
let (x, y) = ((k % nx) as R, (k / nx) as R);
|
||||
let c = math::wall_taper(scene.sdf(x, y), w);
|
||||
if c < 1.0 {
|
||||
math::feq(1.0, u0.0 * c, u0.1 * c)
|
||||
} else {
|
||||
plain
|
||||
}
|
||||
}
|
||||
None => plain,
|
||||
};
|
||||
for (k, cell) in f.iter().enumerate() {
|
||||
for i in 0..9 {
|
||||
v[i * n + k] = fe[i] as f32;
|
||||
v[i * n + k] = cell[i] as f32;
|
||||
}
|
||||
}
|
||||
v
|
||||
@@ -844,12 +827,11 @@ fn make_level(
|
||||
beta: &[f32],
|
||||
probe_node: u32,
|
||||
flags: u32,
|
||||
u0: (R, R),
|
||||
init_field: &[[R; math::Q]],
|
||||
wall_layout: &wgpu::BindGroupLayout,
|
||||
taper: Option<(&math::Scene, R)>,
|
||||
) -> GpuLevel {
|
||||
let n = nx * ny;
|
||||
let init = soa_equilibrium(n, nx, u0, taper);
|
||||
let init = to_soa(init_field);
|
||||
let mkf = |label: &str| {
|
||||
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some(label),
|
||||
@@ -885,7 +867,11 @@ fn make_level(
|
||||
_p: 0,
|
||||
})
|
||||
.collect();
|
||||
let links_buf = storage_init(device, "links", bytemuck::cast_slice(&links));
|
||||
// Пустая сцена (эталонные течения) даёт нулевой список линков, а шейдер всё равно
|
||||
// объявляет массив структур: буфер обязан вмещать хотя бы один элемент, иначе валидация
|
||||
// ругается на несоответствие размера. Читать его при этом некому — nlinks = 0.
|
||||
let links_pad = if links.is_empty() { vec![GLink::zeroed()] } else { links.clone() };
|
||||
let links_buf = storage_init(device, "links", bytemuck::cast_slice(&links_pad));
|
||||
let beta_buf = storage_init(device, "beta", bytemuck::cast_slice(beta));
|
||||
|
||||
// индекс граничных узлов: (узел, смещение первого линка, число линков, —)
|
||||
@@ -895,7 +881,8 @@ fn make_level(
|
||||
.map(|w| [w.node, w.first, w.count as u32, 0])
|
||||
.collect();
|
||||
let nwall = wnodes.len() as u32;
|
||||
let wall_buf = storage_init(device, "wall nodes", bytemuck::cast_slice(&wnodes));
|
||||
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 wall_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("wall"),
|
||||
layout: wall_layout,
|
||||
@@ -1155,9 +1142,16 @@ impl Sim {
|
||||
&beta0,
|
||||
probe0 as u32,
|
||||
flags0,
|
||||
u0,
|
||||
&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,
|
||||
if spec.init_uniform { Some((&spec.scene, spec.init_taper)) } else { None },
|
||||
);
|
||||
|
||||
let pre = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
@@ -1186,13 +1180,19 @@ 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,
|
||||
u0, &bgl_wall,
|
||||
if spec.init_uniform {
|
||||
Some((&scene1, spec.init_taper * r as R))
|
||||
} else {
|
||||
None
|
||||
});
|
||||
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,
|
||||
);
|
||||
|
||||
let ghosts: Vec<GGhost> = patch
|
||||
.ghosts()
|
||||
@@ -1363,6 +1363,7 @@ impl Sim {
|
||||
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,
|
||||
@@ -1402,20 +1403,23 @@ impl Sim {
|
||||
p.dispatch_workgroups(ncell, 1, 1);
|
||||
p.set_pipeline(&self.pipes.stream);
|
||||
p.dispatch_workgroups(ncell, 1, 1);
|
||||
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);
|
||||
p.dispatch_workgroups(ceil_div(self.l0.nwall.max(1), WG), 1, 1);
|
||||
} else {
|
||||
p.set_pipeline(&self.pipes.bouzidi);
|
||||
p.dispatch_workgroups(self.l0.link_groups(), 1, 1);
|
||||
// Эталонные течения статей периодичны по обеим осям: ГУ не накладываются вовсе.
|
||||
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);
|
||||
p.dispatch_workgroups(ceil_div(self.l0.nwall.max(1), WG), 1, 1);
|
||||
} else {
|
||||
p.set_pipeline(&self.pipes.bouzidi);
|
||||
p.dispatch_workgroups(self.l0.link_groups(), 1, 1);
|
||||
}
|
||||
// порядок обязателен: стенки снимают заворот по y, затем Zou-He — по x
|
||||
p.set_pipeline(&self.pipes.walls);
|
||||
p.dispatch_workgroups(ceil_div(self.l0.nx as u32, WG), 1, 1);
|
||||
p.set_pipeline(&self.pipes.channel);
|
||||
p.dispatch_workgroups(ceil_div(self.l0.ny as u32, WG), 1, 1);
|
||||
}
|
||||
// порядок обязателен: стенки снимают заворот по y, затем Zou-He — по x, на весь столбец
|
||||
p.set_pipeline(&self.pipes.walls);
|
||||
p.dispatch_workgroups(ceil_div(self.l0.nx as u32, WG), 1, 1);
|
||||
p.set_pipeline(&self.pipes.channel);
|
||||
p.dispatch_workgroups(ceil_div(self.l0.ny as u32, WG), 1, 1);
|
||||
}
|
||||
|
||||
if let (Some(l1), Some(a)) = (self.l1.as_ref(), self.amr.as_ref()) {
|
||||
@@ -1446,14 +1450,16 @@ impl Sim {
|
||||
p.dispatch_workgroups(ncell1, 1, 1);
|
||||
p.set_pipeline(&self.pipes.stream);
|
||||
p.dispatch_workgroups(ncell1, 1, 1);
|
||||
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);
|
||||
p.dispatch_workgroups(ceil_div(l1.nwall.max(1), WG), 1, 1);
|
||||
} else {
|
||||
p.set_pipeline(&self.pipes.bouzidi);
|
||||
p.dispatch_workgroups(nlink1, 1, 1);
|
||||
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);
|
||||
p.dispatch_workgroups(ceil_div(l1.nwall.max(1), WG), 1, 1);
|
||||
} else {
|
||||
p.set_pipeline(&self.pipes.bouzidi);
|
||||
p.dispatch_workgroups(nlink1, 1, 1);
|
||||
}
|
||||
}
|
||||
// силу снимаем на каждом подшаге, усредняется она в k_stats2
|
||||
p.set_pipeline(&self.pipes.force);
|
||||
@@ -1612,6 +1618,21 @@ impl Sim {
|
||||
out
|
||||
}
|
||||
|
||||
/// Полное поле скорости уровня L0 — для метрик эталонных течений и радиуса влияния.
|
||||
pub fn sample_velocity(&self) -> (Vec<R>, Vec<R>) {
|
||||
let n = self.l0.n;
|
||||
let raw = self.download_l0();
|
||||
let mut ux = Vec::with_capacity(n);
|
||||
let mut uy = Vec::with_capacity(n);
|
||||
for k in 0..n {
|
||||
let c: [R; 9] = std::array::from_fn(|i| raw[i * n + k] as R);
|
||||
let (_, a, b) = math::macros(&c);
|
||||
ux.push(a);
|
||||
uy.push(b);
|
||||
}
|
||||
(ux, uy)
|
||||
}
|
||||
|
||||
/// Срез вдоль осевой линии: (ρ, u_x) по столбцам. На GPU это полное скачивание поля,
|
||||
/// поэтому x–t диагностика включается редким шагом и только там, где нужна.
|
||||
pub fn sample_centerline(&self) -> (Vec<R>, Vec<R>) {
|
||||
|
||||
Reference in New Issue
Block a user