Несколько тел в домене, произвольная геометрия и разбор стартовой акустики
НЕСКОЛЬКО ТЕЛ (--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:
@@ -8,7 +8,7 @@
|
||||
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::math::{self, Body, Kbc, Link, LinkKind, WallModel, R, CX, CY, OPP, Q};
|
||||
use crate::math::{self, Kbc, Link, LinkKind, Scene, WallModel, MAX_BODY_BUCKETS, R, CX, CY, OPP, Q};
|
||||
use crate::{Collision, FieldKind, Spec, StepRec};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -30,21 +30,30 @@ pub struct Geom {
|
||||
}
|
||||
|
||||
impl Geom {
|
||||
/// Собрать геометрию уровня по телу. `solid` определяется знаком SDF: φ ≤ 0 — тело.
|
||||
pub fn build(nx: usize, ny: usize, body: &Body) -> Self {
|
||||
/// Собрать геометрию уровня по сцене. `solid` определяется знаком SDF сцены (минимум по
|
||||
/// телам): φ ≤ 0 — тело. Каждому линку проставляется индекс тела, которого он касается,
|
||||
/// чтобы сила считалась по телам отдельно.
|
||||
pub fn build(nx: usize, ny: usize, scene: &Scene) -> Self {
|
||||
let n = nx * ny;
|
||||
let mut phi = vec![0.0; n];
|
||||
let mut solid = vec![false; n];
|
||||
let mut owner = vec![0u8; n];
|
||||
for y in 0..ny {
|
||||
for x in 0..nx {
|
||||
let p = body.sdf(x as R, y as R);
|
||||
phi[y * nx + x] = p;
|
||||
solid[y * nx + x] = p <= 0.0;
|
||||
let k = y * nx + x;
|
||||
let p = scene.sdf(x as R, y as R);
|
||||
phi[k] = p;
|
||||
solid[k] = p <= 0.0;
|
||||
if solid[k] {
|
||||
owner[k] =
|
||||
scene.nearest(x as R, y as R).min(MAX_BODY_BUCKETS - 1) as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
let links = build_links(nx, ny, &solid, &phi);
|
||||
let links = build_links(nx, ny, &solid, &phi, &owner);
|
||||
let wall_nodes = build_wall_nodes(&links);
|
||||
Geom { nx, ny, solid, links, wall_nodes, body_cx: body.cx, body_cy: body.cy }
|
||||
let (bcx, bcy) = scene.center();
|
||||
Geom { nx, ny, solid, links, wall_nodes, body_cx: bcx, body_cy: bcy }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +68,7 @@ fn nb(nx: usize, ny: usize, x: usize, y: usize, i: usize, sign: i32) -> usize {
|
||||
|
||||
/// Для каждого жидкого узла и каждого направления, упирающегося в тело, решаем, какая из трёх
|
||||
/// формул Bouzidi применима, и считаем долю пересечения q из SDF. Делается один раз.
|
||||
fn build_links(nx: usize, ny: usize, solid: &[bool], phi: &[R]) -> Vec<Link> {
|
||||
fn build_links(nx: usize, ny: usize, solid: &[bool], phi: &[R], owner: &[u8]) -> Vec<Link> {
|
||||
let mut links = Vec::new();
|
||||
for y in 0..ny {
|
||||
for x in 0..nx {
|
||||
@@ -88,7 +97,7 @@ fn build_links(nx: usize, ny: usize, solid: &[bool], phi: &[R]) -> Vec<Link> {
|
||||
ib: OPP[i] as u8,
|
||||
kind,
|
||||
q,
|
||||
body: true,
|
||||
body: owner[s],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -435,26 +444,26 @@ impl Level {
|
||||
}
|
||||
}
|
||||
|
||||
/// Сила и момент на теле по GMEM, суммой по линкам тела.
|
||||
fn force(&self) -> (R, R, R) {
|
||||
let (mut fx, mut fy, mut tz) = (0.0, 0.0, 0.0);
|
||||
/// Сила и момент по GMEM, разложенные по телам: `out[b] = (F_x, F_y, T_z)` для тела b.
|
||||
/// Момент считается вокруг центра ПЕРВОГО тела — общей точки отсчёта для всей сцены.
|
||||
fn force(&self) -> [[R; 3]; MAX_BODY_BUCKETS] {
|
||||
let mut out = [[0.0; 3]; MAX_BODY_BUCKETS];
|
||||
let nx = self.nx;
|
||||
for l in &self.geom.links {
|
||||
if !l.body {
|
||||
continue;
|
||||
}
|
||||
let node = l.node as usize;
|
||||
let i = l.i as usize;
|
||||
let (dfx, dfy) = math::gmem_link(i, self.post[node][i], self.f[node][l.ib as usize], 0.0, 0.0);
|
||||
fx += dfx;
|
||||
fy += dfy;
|
||||
let (dfx, dfy) =
|
||||
math::gmem_link(i, self.post[node][i], self.f[node][l.ib as usize], 0.0, 0.0);
|
||||
// плечо — до ТОЧКИ ПЕРЕСЕЧЕНИЯ линка со стенкой r_w = r_f + q·c_i, а не до узла:
|
||||
// узел дал бы ошибку плеча до целой ячейки, а q уже посчитан
|
||||
let rx = (node % nx) as R - self.geom.body_cx + l.q * CX[i] as R;
|
||||
let ry = (node / nx) as R - self.geom.body_cy + l.q * CY[i] as R;
|
||||
tz += rx * dfy - ry * dfx;
|
||||
let b = (l.body as usize).min(MAX_BODY_BUCKETS - 1);
|
||||
out[b][0] += dfx;
|
||||
out[b][1] += dfy;
|
||||
out[b][2] += rx * dfy - ry * dfx;
|
||||
}
|
||||
(fx, fy, tz)
|
||||
out
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -463,6 +472,20 @@ impl Level {
|
||||
}
|
||||
}
|
||||
|
||||
/// Свести стартовую скорость к нулю на подходе к телу, чтобы на первом шаге не возникло
|
||||
/// разрыва. Вдали от тела поле не трогается вовсе.
|
||||
fn taper_start(lvl: &mut Level, scene: &Scene, u0: (R, R), width: R) {
|
||||
let nx = lvl.nx;
|
||||
for (n, cell) in lvl.f.iter_mut().enumerate() {
|
||||
let (x, y) = ((n % nx) as R, (n / nx) as R);
|
||||
let k = math::wall_taper(scene.sdf(x, y), width);
|
||||
if k < 1.0 {
|
||||
*cell = math::feq(1.0, u0.0 * k, u0.1 * k);
|
||||
}
|
||||
}
|
||||
lvl.post.copy_from_slice(&lvl.f);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Статистика KBC за шаг
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -730,22 +753,15 @@ pub struct Sim {
|
||||
impl Sim {
|
||||
pub fn new(spec: Spec) -> Sim {
|
||||
let (nx, ny) = (spec.nx, spec.ny);
|
||||
let geom0 = Geom::build(nx, ny, &spec.body);
|
||||
let geom0 = Geom::build(nx, ny, &spec.scene);
|
||||
|
||||
// губка: плавный рост вязкости в последних sponge_len столбцах. Канал
|
||||
// «скорость-вход + давление-выход» — недодемпфированный акустический резонатор,
|
||||
// губка гасит и вихри, и акустику до прихода на выход.
|
||||
let beta0: Vec<R> = (0..nx)
|
||||
.map(|x| {
|
||||
if spec.sponge_len == 0 {
|
||||
return spec.beta0;
|
||||
}
|
||||
let start = 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))
|
||||
})
|
||||
.collect();
|
||||
let beta0 = math::beta_profile(
|
||||
nx,
|
||||
spec.beta0,
|
||||
spec.sponge_in,
|
||||
spec.sponge_len,
|
||||
spec.sponge_mult,
|
||||
);
|
||||
|
||||
// стартовое поле: либо сразу набегающий поток, либо покой
|
||||
let u0 = if spec.init_uniform {
|
||||
@@ -754,23 +770,30 @@ impl Sim {
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
};
|
||||
let l0 = Level::new(nx, ny, beta0, geom0, u0);
|
||||
let mut l0 = Level::new(nx, ny, beta0, geom0, u0);
|
||||
if spec.init_uniform {
|
||||
taper_start(&mut l0, &spec.scene, u0, spec.init_taper);
|
||||
}
|
||||
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 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 = Geom::build(nfx, nfy, &body1);
|
||||
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);
|
||||
(Some(Level::new(nfx, nfy, vec![beta1; nfx], geom1, u0)), Some(patch))
|
||||
let mut lvl1 = Level::new(nfx, nfy, vec![beta1; nfx], geom1, u0);
|
||||
if spec.init_uniform {
|
||||
taper_start(&mut lvl1, &scene1, u0, spec.init_taper * r as R);
|
||||
}
|
||||
(Some(lvl1), Some(patch))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
@@ -842,7 +865,7 @@ impl Sim {
|
||||
self.l0.channel_bc(ux_in, uy_in, 1.0, outlet_extrap);
|
||||
|
||||
// ── уровень 1: r подшагов с временной интерполяцией рамки ──
|
||||
let (mut fx, mut fy, mut tz) = (0.0, 0.0, 0.0);
|
||||
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);
|
||||
@@ -852,23 +875,24 @@ impl Sim {
|
||||
l1.apply_wall(wall);
|
||||
// силу снимаем на КАЖДОМ подшаге и усредняем — мгновенное значение на
|
||||
// последнем подшаге даёт лишний шум в рядах при том же среднем
|
||||
let (a, b, c) = l1.force();
|
||||
fx += a;
|
||||
fy += b;
|
||||
tz += c;
|
||||
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 inv = 1.0 / p.r as R;
|
||||
fx *= inv;
|
||||
fy *= inv;
|
||||
tz *= inv;
|
||||
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 {
|
||||
let (a, b, c) = self.l0.force();
|
||||
fx = a;
|
||||
fy = b;
|
||||
tz = c;
|
||||
fb = self.l0.force();
|
||||
}
|
||||
|
||||
// ── диагностика ──
|
||||
@@ -892,11 +916,18 @@ impl Sim {
|
||||
.reduce(|| (0.0, 0.0), |a, b| (a.0 + b.0, a.1.max(b.1)));
|
||||
|
||||
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];
|
||||
}
|
||||
StepRec {
|
||||
step: t,
|
||||
fx,
|
||||
fy,
|
||||
tz,
|
||||
body: fb,
|
||||
uy_probe: probe.2,
|
||||
rho_mean: rho_sum / self.fluid_count,
|
||||
max_u,
|
||||
@@ -911,8 +942,8 @@ impl Sim {
|
||||
/// Характерный размер тела в единицах того уровня, где снимается сила.
|
||||
pub fn force_ref_size(&self) -> R {
|
||||
match &self.patch {
|
||||
Some(p) => self.spec.body.d * p.r as R,
|
||||
None => self.spec.body.d,
|
||||
Some(p) => self.spec.scene.ref_size() * p.r as R,
|
||||
None => self.spec.scene.ref_size(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -958,6 +989,22 @@ impl Sim {
|
||||
(out, &self.l0.geom.solid)
|
||||
}
|
||||
|
||||
/// Срез вдоль осевой линии: (ρ, u_x) по каждому столбцу. Из последовательности таких
|
||||
/// срезов складывается x–t диаграмма, по которой видно, бежит возмущение со скоростью
|
||||
/// звука или конвекции и есть ли стоячие узлы.
|
||||
pub fn sample_centerline(&self) -> (Vec<R>, Vec<R>) {
|
||||
let y = self.l0.ny / 2;
|
||||
let nx = self.l0.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);
|
||||
rho.push(r);
|
||||
ux.push(a);
|
||||
}
|
||||
(rho, ux)
|
||||
}
|
||||
|
||||
/// Есть ли в поле NaN/inf — признак развала счёта.
|
||||
pub fn is_finite(&self) -> bool {
|
||||
self.l0.f.par_iter().all(|c| c.iter().all(|v| v.is_finite()))
|
||||
@@ -1052,8 +1099,8 @@ mod tests {
|
||||
fn wall_is_subgrid_not_staircase() {
|
||||
let mut means = Vec::new();
|
||||
for off in [0.0, 0.25, 0.5] {
|
||||
let body = Body::new(math::ShapeKind::Cylinder, 60.0, 40.0 + off, 24.0, 0.0, 0.3);
|
||||
let g = Geom::build(120, 80, &body);
|
||||
let body = math::Body::new(math::ShapeKind::Cylinder, 60.0, 40.0 + off, 24.0, 0.0, 0.3);
|
||||
let g = Geom::build(120, 80, &Scene::single(body));
|
||||
let w = g.wall_stats();
|
||||
assert!(w.links > 100, "смещение {off}: линков всего {}", w.links);
|
||||
assert_eq!(w.simple, 0, "смещение {off}: {} ступенчатых линков", w.simple);
|
||||
@@ -1069,8 +1116,8 @@ mod tests {
|
||||
/// без пересечений, в том же порядке.
|
||||
#[test]
|
||||
fn wall_node_index_partitions_links() {
|
||||
let body = Body::new(math::ShapeKind::Naca, 60.0, 40.0, 30.0, 12.0, 0.25);
|
||||
let g = Geom::build(120, 80, &body);
|
||||
let body = math::Body::new(math::ShapeKind::Naca, 60.0, 40.0, 30.0, 12.0, 0.25);
|
||||
let g = Geom::build(120, 80, &Scene::single(body));
|
||||
let mut covered = 0usize;
|
||||
for w in &g.wall_nodes {
|
||||
assert_eq!(w.first as usize, covered, "разрыв в индексе граничных узлов");
|
||||
|
||||
Reference in New Issue
Block a user