Несколько тел в домене, произвольная геометрия и разбор стартовой акустики
НЕСКОЛЬКО ТЕЛ (--bodies). SDF сцены — минимум по телам, поэтому маски, поле SDF и Bouzidi-линки строятся ровно теми же процедурами, что и для одного тела. Каждому линку проставляется индекс тела, и сила считается по телам отдельно: для тандема и многоэлементных конфигураций литература даёт коэффициенты каждого тела, общей суммы недостаточно. Разнос идёт по четырём вёдрам; тела за пределом набора сваливаются в последнее, так что сумма по вёдрам всегда точна. Синтаксис: "cylinder:d=24,x=120,y=120; cylinder:d=24,x=192,y=120". Проверка на тандеме L/D=3 при Re=150 даёт физически правильную картину: передний цилиндр Cd=1.22 при rms Cl=0.02, задний Cd=-0.10 при rms Cl=0.19. Отрицательное сопротивление заднего — классический режим экранирования, когда сдвиговые слои переднего замыкаются на задний, а восьмикратный рост rms Cl отвечает тому, что задний треплет след переднего. ПРОИЗВОЛЬНЫЙ МНОГОУГОЛЬНИК (--shape polygon --poly) — точный SDF уже был, добавлен разбор. Открывает клинья, зазубренные кромки, любые обводы. Проверено на клине: 100% линков идут по интерполяционной формуле. ПРОФИЛИ С ИЗГИБОМ (--naca 4412). Средняя линия по четырёхзначной серии, поверхности откладываются по НОРМАЛИ к ней, а не по вертикали — иначе у заметно изогнутого профиля толщина у носка завышается. При нулевой кривизне вырождается в прежний симметричный 00xx. ПРОРЕЖИВАНИЕ РЯДОВ (--series-every): на 5·10^6 шагов полный ряд занимал бы сотни мегабайт. РАЗБОР СТАРТОВОЙ АКУСТИКИ. Добавлена x-t диагностика (--xt): срез ⟨ρ⟩ и u_x вдоль осевой линии, по строке на срез. Что она показала: * возмущение рождается НА ТЕЛЕ, а не на входе. На нулевом шаге |ρ−1| ≈ 1.3·10^-2 у задней кромки при 10^-8 у входа и выхода; причина — поле стартует однородным потоком сквозь то место, где стоит тело; * дальше импульс уходит полосой через весь домен — ровно то, что наблюдалось на низких Re; * на Re ≥ 1000 при выключенной губке он раскачивает неустойчивость у ВЫХОДА, и счёт гибнет около шага 17000. С губкой (умолчание) Re=2000 доживает. Попытка лечения сглаживанием стартовой скорости у тела (--init-taper) ЗАМЕРЕНА И ОТВЕРГНУТА: ширина 24 клетки давит возмущение нулевого шага восьмикратно (2.54·10^-2 → 3.13·10^-3), но пик за прогон при этом даже растёт (2.54·10^-2 → 3.22·10^-2). Возмущение просто переносится во времени. Ключ оставлен выключенным, замер записан в его описании. Добавлена губка у входа (--sponge-in); профиль β вынесен в общую math::beta_profile, раньше он дублировался в обоих бэкендах. Исправлен вердикт о пульсации: при развале счёта отношение уходило в минус и проверка «> 0.05» молча не срабатывала. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -102,7 +102,8 @@ struct GLink {
|
||||
ib: u32,
|
||||
kind: u32,
|
||||
q: f32,
|
||||
_p: [u32; 2],
|
||||
body: u32,
|
||||
_p: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
@@ -134,6 +135,8 @@ struct Results {
|
||||
cnt: f32,
|
||||
degen: f32,
|
||||
xineg: f32,
|
||||
/// Сила и момент по телам: [b*3 + c].
|
||||
body: [f32; 12],
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -146,7 +149,7 @@ struct LevelParams { n:u32, nx:u32, ny:u32, nlinks:u32, bcx:f32, bcy:f32, probe_
|
||||
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_hrr:u32, dp2:u32, dp3: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 };
|
||||
struct GLink { node:u32, far:u32, i:u32, ib:u32, kind:u32, q:f32, body:u32, p1:u32 };
|
||||
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 };
|
||||
|
||||
@@ -184,7 +187,9 @@ const CS2 : f32 = 0.3333333333;
|
||||
// рабочие слоты силы по подшагам. История нужна, чтобы не синхронизироваться с устройством
|
||||
// на каждом шаге: результаты копятся и читаются пачкой (см. HIST в gpu.rs).
|
||||
const HIST: u32 = 128u;
|
||||
const FORCE_BASE: u32 = 1536u; // = HIST*12u
|
||||
const SLOT: u32 = 24u; // чисел на шаг: 12 общих + 4 тела по 3
|
||||
const MAXB: u32 = 4u; // вёдер силы по телам
|
||||
const FORCE_BASE: u32 = 3072u; // = HIST*SLOT
|
||||
|
||||
// Компенсированное сложение Кэхена–Ноймайера: возвращает (сумма, накопленная поправка).
|
||||
// Наивная сумма по 10^5…10^7 значений в f32 съедает ~log2(N) бит; здесь потеря не копится,
|
||||
@@ -404,9 +409,10 @@ fn k_force(@builtin(local_invocation_id) lid: vec3<u32>) {
|
||||
let t = lid.x;
|
||||
var cx = array<f32,9>(0.0, 1.0, 0.0, -1.0, 0.0, 1.0, -1.0, -1.0, 1.0);
|
||||
var cy = array<f32,9>(0.0, 0.0, 1.0, 0.0, -1.0, 1.0, 1.0, -1.0, -1.0);
|
||||
var sx = vec2<f32>(0.0, 0.0);
|
||||
var sy = vec2<f32>(0.0, 0.0);
|
||||
var sz = vec2<f32>(0.0, 0.0);
|
||||
// накопители на каждое тело держатся в регистрах, редукция потом идёт по телам подряд
|
||||
var pfx = array<f32,4>(0.0, 0.0, 0.0, 0.0);
|
||||
var pfy = array<f32,4>(0.0, 0.0, 0.0, 0.0);
|
||||
var ptz = array<f32,4>(0.0, 0.0, 0.0, 0.0);
|
||||
var k = t;
|
||||
loop {
|
||||
if (k >= P.nlinks) { break; }
|
||||
@@ -415,31 +421,36 @@ fn k_force(@builtin(local_invocation_id) lid: vec3<u32>) {
|
||||
let fb = f[L.ib*P.n + L.node];
|
||||
let dfx = cx[L.i]*fp + cx[L.i]*fb;
|
||||
let dfy = cy[L.i]*fp + cy[L.i]*fb;
|
||||
sx = kadd(sx.x, sx.y, dfx);
|
||||
sy = kadd(sy.x, sy.y, dfy);
|
||||
// плечо до точки пересечения линка со стенкой, а не до узла
|
||||
let rx = f32(L.node % P.nx) - P.bcx + L.q*cx[L.i];
|
||||
let ry = f32(L.node / P.nx) - P.bcy + L.q*cy[L.i];
|
||||
sz = kadd(sz.x, sz.y, rx*dfy - ry*dfx);
|
||||
let b = min(L.body, MAXB - 1u);
|
||||
pfx[b] = pfx[b] + dfx;
|
||||
pfy[b] = pfy[b] + dfy;
|
||||
ptz[b] = ptz[b] + rx*dfy - ry*dfx;
|
||||
k = k + 256u;
|
||||
}
|
||||
wfx[t] = sx.x + sx.y; wfy[t] = sy.x + sy.y; wtz[t] = sz.x + sz.y;
|
||||
workgroupBarrier();
|
||||
var s = 128u;
|
||||
loop {
|
||||
if (s == 0u) { break; }
|
||||
if (t < s) {
|
||||
wfx[t] = wfx[t] + wfx[t + s];
|
||||
wfy[t] = wfy[t] + wfy[t + s];
|
||||
wtz[t] = wtz[t] + wtz[t + s];
|
||||
for (var b = 0u; b < MAXB; b = b + 1u) {
|
||||
wfx[t] = pfx[b]; wfy[t] = pfy[b]; wtz[t] = ptz[b];
|
||||
workgroupBarrier();
|
||||
var s = 128u;
|
||||
loop {
|
||||
if (s == 0u) { break; }
|
||||
if (t < s) {
|
||||
wfx[t] = wfx[t] + wfx[t + s];
|
||||
wfy[t] = wfy[t] + wfy[t + s];
|
||||
wtz[t] = wtz[t] + wtz[t + s];
|
||||
}
|
||||
workgroupBarrier();
|
||||
s = s >> 1u;
|
||||
}
|
||||
if (t == 0u) {
|
||||
let o = FORCE_BASE + (S.idx*MAXB + b)*4u;
|
||||
results[o + 0u] = wfx[0];
|
||||
results[o + 1u] = wfy[0];
|
||||
results[o + 2u] = wtz[0];
|
||||
}
|
||||
workgroupBarrier();
|
||||
s = s >> 1u;
|
||||
}
|
||||
if (t == 0u) {
|
||||
results[FORCE_BASE + S.idx*4u + 0u] = wfx[0];
|
||||
results[FORCE_BASE + S.idx*4u + 1u] = wfy[0];
|
||||
results[FORCE_BASE + S.idx*4u + 2u] = wtz[0];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,7 +488,7 @@ fn k_stats1(@builtin(global_invocation_id) gid: vec3<u32>,
|
||||
var fv: array<f32,9>;
|
||||
load9(&fv, nd, P.n);
|
||||
let m = macros9(fv);
|
||||
results[D.slot*12u + 3u] = m.z;
|
||||
results[D.slot*SLOT + 3u] = m.z;
|
||||
}
|
||||
wp[t] = p;
|
||||
workgroupBarrier();
|
||||
@@ -554,17 +565,26 @@ fn k_stats2(@builtin(local_invocation_id) lid: vec3<u32>) {
|
||||
qgmin = min(qgmin, o.gmin);
|
||||
qgmax = max(qgmax, o.gmax);
|
||||
}
|
||||
var fx = 0.0; var fy = 0.0; var tz = 0.0;
|
||||
for (var s = 0u; s < D.refine; s = s + 1u) {
|
||||
fx = fx + results[FORCE_BASE + s*4u + 0u];
|
||||
fy = fy + results[FORCE_BASE + s*4u + 1u];
|
||||
tz = tz + results[FORCE_BASE + s*4u + 2u];
|
||||
}
|
||||
// сила усредняется по подшагам L1 и раскладывается по телам; общая — их сумма
|
||||
let inv = 1.0 / f32(D.refine);
|
||||
let o = D.slot*12u;
|
||||
results[o + 0u] = fx*inv;
|
||||
results[o + 1u] = fy*inv;
|
||||
results[o + 2u] = tz*inv;
|
||||
let o = D.slot*SLOT;
|
||||
var tfx = 0.0; var tfy = 0.0; var ttz = 0.0;
|
||||
for (var b = 0u; b < MAXB; b = b + 1u) {
|
||||
var fx = 0.0; var fy = 0.0; var tz = 0.0;
|
||||
for (var sb = 0u; sb < D.refine; sb = sb + 1u) {
|
||||
let g = FORCE_BASE + (sb*MAXB + b)*4u;
|
||||
fx = fx + results[g + 0u];
|
||||
fy = fy + results[g + 1u];
|
||||
tz = tz + results[g + 2u];
|
||||
}
|
||||
results[o + 12u + b*3u + 0u] = fx*inv;
|
||||
results[o + 12u + b*3u + 1u] = fy*inv;
|
||||
results[o + 12u + b*3u + 2u] = tz*inv;
|
||||
tfx = tfx + fx*inv; tfy = tfy + fy*inv; ttz = ttz + tz*inv;
|
||||
}
|
||||
results[o + 0u] = tfx;
|
||||
results[o + 1u] = tfy;
|
||||
results[o + 2u] = ttz;
|
||||
results[o + 4u] = qrho.x + qrho.y;
|
||||
results[o + 5u] = qmaxu;
|
||||
results[o + 6u] = qgsum.x + qgsum.y;
|
||||
@@ -784,11 +804,30 @@ impl GpuLevel {
|
||||
}
|
||||
}
|
||||
|
||||
fn soa_equilibrium(n: usize, u0: (R, R)) -> Vec<f32> {
|
||||
let fe = math::feq(1.0, u0.0, u0.1);
|
||||
/// Стартовое поле в раскладке SoA. Если задана сцена, скорость сводится к нулю на подходе
|
||||
/// к телу (см. `math::wall_taper`) — иначе на первом шаге возникает разрыв и импульс сжатия.
|
||||
fn soa_equilibrium(
|
||||
n: usize,
|
||||
nx: usize,
|
||||
u0: (R, R),
|
||||
taper: Option<(&math::Scene, R)>,
|
||||
) -> Vec<f32> {
|
||||
let mut v = vec![0.0f32; 9 * n];
|
||||
for i in 0..9 {
|
||||
for k in 0..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 i in 0..9 {
|
||||
v[i * n + k] = fe[i] as f32;
|
||||
}
|
||||
}
|
||||
@@ -807,9 +846,10 @@ fn make_level(
|
||||
flags: u32,
|
||||
u0: (R, R),
|
||||
wall_layout: &wgpu::BindGroupLayout,
|
||||
taper: Option<(&math::Scene, R)>,
|
||||
) -> GpuLevel {
|
||||
let n = nx * ny;
|
||||
let init = soa_equilibrium(n, u0);
|
||||
let init = soa_equilibrium(n, nx, u0, taper);
|
||||
let mkf = |label: &str| {
|
||||
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some(label),
|
||||
@@ -841,7 +881,8 @@ fn make_level(
|
||||
math::LinkKind::Simple => 2,
|
||||
},
|
||||
q: l.q as f32,
|
||||
_p: [0; 2],
|
||||
body: l.body as u32,
|
||||
_p: 0,
|
||||
})
|
||||
.collect();
|
||||
let links_buf = storage_init(device, "links", bytemuck::cast_slice(&links));
|
||||
@@ -997,20 +1038,14 @@ impl Sim {
|
||||
}
|
||||
|
||||
// ── топология берётся из процессорного бэкенда, а не строится заново ──
|
||||
let geom0 = cpu::Geom::build(spec.nx, spec.ny, &spec.body);
|
||||
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<f32> = (0..spec.nx)
|
||||
.map(|x| {
|
||||
if spec.sponge_len == 0 {
|
||||
return spec.beta0 as f32;
|
||||
}
|
||||
let start = spec.nx - 1 - spec.sponge_len;
|
||||
let s = math::smoothstep((x as R - start as R) / spec.sponge_len as R);
|
||||
let nu = math::nu_of_beta(spec.beta0);
|
||||
math::beta_of_nu(nu * (1.0 + (spec.sponge_mult - 1.0) * s)) as f32
|
||||
})
|
||||
.collect();
|
||||
let beta0: Vec<f32> =
|
||||
math::beta_profile(spec.nx, spec.beta0, spec.sponge_in, spec.sponge_len, spec.sponge_mult)
|
||||
.into_iter()
|
||||
.map(|v| v as f32)
|
||||
.collect();
|
||||
|
||||
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("kbc2d.wgsl"),
|
||||
@@ -1122,6 +1157,7 @@ impl Sim {
|
||||
flags0,
|
||||
u0,
|
||||
&bgl_wall,
|
||||
if spec.init_uniform { Some((&spec.scene, spec.init_taper)) } else { None },
|
||||
);
|
||||
|
||||
let pre = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
@@ -1137,10 +1173,10 @@ impl Sim {
|
||||
if spec.refine > 1 {
|
||||
let (ax, bx, ay, by) = spec.patch.ok_or("refine > 1 требует патч")?;
|
||||
let r = spec.refine;
|
||||
let body1 = spec.body.refined(r as R, ax as R, ay as R);
|
||||
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, &body1);
|
||||
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];
|
||||
@@ -1151,7 +1187,12 @@ impl Sim {
|
||||
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);
|
||||
u0, &bgl_wall,
|
||||
if spec.init_uniform {
|
||||
Some((&scene1, spec.init_taper * r as R))
|
||||
} else {
|
||||
None
|
||||
});
|
||||
|
||||
let ghosts: Vec<GGhost> = patch
|
||||
.ghosts()
|
||||
@@ -1262,8 +1303,8 @@ impl Sim {
|
||||
});
|
||||
|
||||
let d_ref = match spec.patch {
|
||||
Some(_) if spec.refine > 1 => spec.body.d * spec.refine as R,
|
||||
_ => spec.body.d,
|
||||
Some(_) if spec.refine > 1 => spec.scene.ref_size() * spec.refine as R,
|
||||
_ => spec.scene.ref_size(),
|
||||
};
|
||||
|
||||
Ok(Sim {
|
||||
@@ -1516,11 +1557,18 @@ impl Sim {
|
||||
|
||||
fn make_rec(&self, t: u64, res: &Results) -> StepRec {
|
||||
let cnt = if res.cnt > 0.0 { res.cnt as R } else { self.fluid_count };
|
||||
let mut body = [[0.0 as R; 3]; crate::MAX_BODY_BUCKETS];
|
||||
for b in 0..crate::MAX_BODY_BUCKETS.min(4) {
|
||||
for c in 0..3 {
|
||||
body[b][c] = res.body[b * 3 + c] as R;
|
||||
}
|
||||
}
|
||||
StepRec {
|
||||
step: t,
|
||||
fx: res.fx as R,
|
||||
fy: res.fy as R,
|
||||
tz: res.tz as R,
|
||||
body,
|
||||
uy_probe: res.uy_probe as R,
|
||||
rho_mean: res.rho_sum as R / self.fluid_count,
|
||||
max_u: res.max_u as R,
|
||||
@@ -1564,6 +1612,24 @@ impl Sim {
|
||||
out
|
||||
}
|
||||
|
||||
/// Срез вдоль осевой линии: (ρ, u_x) по столбцам. На GPU это полное скачивание поля,
|
||||
/// поэтому x–t диагностика включается редким шагом и только там, где нужна.
|
||||
pub fn sample_centerline(&self) -> (Vec<R>, Vec<R>) {
|
||||
let (nx, ny, n) = (self.l0.nx, self.l0.ny, self.l0.n);
|
||||
let raw = self.download_l0();
|
||||
let y = ny / 2;
|
||||
let mut rho = Vec::with_capacity(nx);
|
||||
let mut ux = Vec::with_capacity(nx);
|
||||
for x in 0..nx {
|
||||
let k = y * nx + x;
|
||||
let c: [R; 9] = std::array::from_fn(|i| raw[i * n + k] as R);
|
||||
let (r, a, _) = math::macros(&c);
|
||||
rho.push(r);
|
||||
ux.push(a);
|
||||
}
|
||||
(rho, ux)
|
||||
}
|
||||
|
||||
pub fn is_finite(&self) -> bool {
|
||||
self.download_l0().iter().all(|v| v.is_finite())
|
||||
}
|
||||
@@ -1616,7 +1682,7 @@ impl Sim {
|
||||
}
|
||||
}
|
||||
}
|
||||
let solid = cpu::Geom::build(nx, ny, &self.spec.body).solid;
|
||||
let solid = cpu::Geom::build(nx, ny, &self.spec.scene).solid;
|
||||
(out, solid)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user