Сверка со статьями: эталон Тейлора–Грина, выбор модели 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:
@@ -113,8 +113,11 @@ pub struct Level {
|
||||
}
|
||||
|
||||
impl Level {
|
||||
fn new(nx: usize, ny: usize, beta: Vec<R>, geom: Geom) -> Self {
|
||||
let f0 = math::feq(1.0, 0.0, 0.0);
|
||||
/// `u0` — скорость, которой заполняется поле на старте. Заполнять сразу набегающим
|
||||
/// потоком принципиально: старт из покоя разгоняет весь столб жидкости и закачивает в
|
||||
/// канал продольную акустическую моду, которую вязкость потом почти не гасит.
|
||||
fn new(nx: usize, ny: usize, beta: Vec<R>, geom: Geom, u0: (R, R)) -> Self {
|
||||
let f0 = math::feq(1.0, u0.0, u0.1);
|
||||
let n = nx * ny;
|
||||
Level { nx, ny, f: vec![f0; n], post: vec![f0; n], gamma: vec![2.0; n], beta, geom }
|
||||
}
|
||||
@@ -122,7 +125,7 @@ impl Level {
|
||||
/// Столкновение по всем жидким узлам. Внутри тела не считаем: эти популяции фиктивны
|
||||
/// (ГУ Bouzidi перезаписывает всё, что могло бы прийти из тела в жидкость), а счёт там
|
||||
/// только жжёт такты и способен родить NaN при экстремальных режимах.
|
||||
fn collide(&mut self, op: Collision) -> Stats {
|
||||
fn collide(&mut self, op: Collision, model: math::KbcModel) -> Stats {
|
||||
let nx = self.nx;
|
||||
let beta = &self.beta;
|
||||
let solid = &self.geom.solid;
|
||||
@@ -141,11 +144,11 @@ impl Level {
|
||||
}
|
||||
let b = beta[node % nx];
|
||||
let k: Kbc = match op {
|
||||
Collision::Kbc => math::collide_node(p, b),
|
||||
Collision::Kbc => math::collide_node(p, b, model),
|
||||
Collision::Bgk => math::collide_node_bgk(p, b),
|
||||
};
|
||||
*g = k.gamma;
|
||||
Stats::of(k, b)
|
||||
Stats::of(k, b, model)
|
||||
})
|
||||
.reduce(|| Stats::EMPTY, Stats::merge)
|
||||
}
|
||||
@@ -288,14 +291,14 @@ impl Stats {
|
||||
xi_negative: 0,
|
||||
};
|
||||
#[inline]
|
||||
fn of(k: Kbc, beta: R) -> Stats {
|
||||
fn of(k: Kbc, beta: R, model: math::KbcModel) -> Stats {
|
||||
Stats {
|
||||
n: 1,
|
||||
gsum: k.gamma,
|
||||
gmin: k.gamma,
|
||||
gmax: k.gamma,
|
||||
degenerate: k.degenerate as u64,
|
||||
xi_negative: (math::xi_of_gamma(k.gamma, beta) < 0.0) as u64,
|
||||
xi_negative: (math::xi_of_gamma(k.gamma, beta, model) < 0.0) as u64,
|
||||
}
|
||||
}
|
||||
fn merge(a: Stats, b: Stats) -> Stats {
|
||||
@@ -545,7 +548,14 @@ impl Sim {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let l0 = Level::new(nx, ny, beta0, geom0);
|
||||
// стартовое поле: либо сразу набегающий поток, либо покой
|
||||
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 l0 = Level::new(nx, ny, beta0, geom0, u0);
|
||||
let fluid_count = l0.geom.solid.iter().filter(|s| !**s).count() as R;
|
||||
|
||||
let (l1, patch) = if spec.refine > 1 {
|
||||
@@ -561,7 +571,7 @@ impl Sim {
|
||||
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)), Some(patch))
|
||||
(Some(Level::new(nfx, nfy, vec![beta1; nfx], geom1, u0)), Some(patch))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
@@ -594,12 +604,19 @@ impl Sim {
|
||||
|
||||
/// Скорость на входе в момент t: разгон smoothstep плюс окно поперечного возмущения.
|
||||
///
|
||||
/// Разгон гасит импульсный старт (мгновенное включение входа шлёт по домену ударную волну).
|
||||
/// Возмущение — короткий поперечный импульс, сбивающий симметрию: без него дорожка Кармана
|
||||
/// заводится только на численном шуме и стартует на порядок позже.
|
||||
/// При старте из однородного потока разгон не нужен и пропускается: поле и вход и так
|
||||
/// согласованы, а плавный разгон поверх согласованного поля сам стал бы рассогласованием.
|
||||
/// При старте из покоя разгон обязателен — мгновенное включение входа шлёт по домену
|
||||
/// ударную волну. Возмущение — короткий поперечный импульс, сбивающий симметрию: без него
|
||||
/// дорожка Кармана заводится только на численном шуме и стартует на порядок позже.
|
||||
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 {
|
||||
@@ -617,7 +634,8 @@ impl Sim {
|
||||
|
||||
// ── уровень 0 ──
|
||||
self.pre.copy_from_slice(&self.l0.f);
|
||||
let stats0 = self.l0.collide(sp_collision);
|
||||
let model = self.spec.kbc_model;
|
||||
let stats0 = self.l0.collide(sp_collision, model);
|
||||
self.l0.stream();
|
||||
self.l0.apply_bouzidi();
|
||||
self.l0.free_slip_walls();
|
||||
@@ -629,7 +647,7 @@ impl Sim {
|
||||
p.ghost_values(&self.pre, &mut self.gh_old);
|
||||
p.ghost_values(&self.l0.f, &mut self.gh_new);
|
||||
for s in 0..p.r {
|
||||
l1.collide(sp_collision);
|
||||
l1.collide(sp_collision, model);
|
||||
l1.stream();
|
||||
l1.apply_bouzidi();
|
||||
// силу снимаем на КАЖДОМ подшаге и усредняем — мгновенное значение на
|
||||
@@ -767,15 +785,30 @@ mod tests {
|
||||
body_cx: 0.0,
|
||||
body_cy: 0.0,
|
||||
},
|
||||
(0.0, 0.0),
|
||||
)
|
||||
}
|
||||
|
||||
/// Голый периодический шаг: столкновение + перенос, без единого ГУ.
|
||||
/// Нужен, чтобы отделить ядро схемы от граничных условий.
|
||||
fn periodic_step(f: &mut [[R; Q]], tmp: &mut [[R; Q]], nx: usize, ny: usize, beta: R) {
|
||||
periodic_step_op(f, tmp, nx, ny, beta, Collision::Kbc)
|
||||
}
|
||||
|
||||
fn periodic_step_op(
|
||||
f: &mut [[R; Q]],
|
||||
tmp: &mut [[R; Q]],
|
||||
nx: usize,
|
||||
ny: usize,
|
||||
beta: R,
|
||||
op: Collision,
|
||||
) {
|
||||
for (n, c) in f.iter().enumerate() {
|
||||
tmp[n] = *c;
|
||||
math::collide_node(&mut tmp[n], beta);
|
||||
match op {
|
||||
Collision::Kbc => math::collide_node(&mut tmp[n], beta, math::KbcModel::N1),
|
||||
Collision::Bgk => math::collide_node_bgk(&mut tmp[n], beta),
|
||||
};
|
||||
}
|
||||
for y in 0..ny {
|
||||
for x in 0..nx {
|
||||
@@ -865,6 +898,183 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Диагностика: из чего складывается ошибка Тейлора–Грина. Сетка и вязкость ЗАФИКСИРОВАНЫ,
|
||||
/// меняется только амплитуда u₀. Дискретизационная ошибка от u₀ почти не зависит (задача
|
||||
/// в этом пределе линейна), а сжимаемостная идёт как Ma² ∝ u₀². Наклон и покажет, что
|
||||
/// доминирует.
|
||||
#[test]
|
||||
#[ignore = "диагностика; запуск: cargo test --release -- --ignored taylor_green_error_scaling --nocapture"]
|
||||
fn taylor_green_error_scaling() {
|
||||
let n = 128usize;
|
||||
let nu = 0.0384;
|
||||
let beta = math::beta_of_nu(nu);
|
||||
println!(" N={n}, ν={nu} (τ={:.4}) — меняем только u₀", 1.0 / (2.0 * beta));
|
||||
let mut prev: Option<(R, R)> = None;
|
||||
for &u0 in &[0.04, 0.02, 0.01, 0.005] {
|
||||
let e = taylor_green_error(n, u0, nu, beta);
|
||||
let slope = prev.map(|(p_u, p_e): (R, R)| (p_e / e.shape).ln() / (p_u / u0).ln());
|
||||
match slope {
|
||||
Some(s) => println!(
|
||||
" u₀={u0:<7} Ma={:.4} форма {:.3e} ампл {:.3e} наклон формы по u₀: {s:.2}",
|
||||
u0 / math::CS2.sqrt(), e.shape, e.amplitude),
|
||||
None => println!(" u₀={u0:<7} Ma={:.4} форма {:.3e} ампл {:.3e}",
|
||||
u0 / math::CS2.sqrt(), e.shape, e.amplitude),
|
||||
}
|
||||
prev = Some((u0, e.shape));
|
||||
}
|
||||
println!(" наклон ≈2 ⇒ правит сжимаемость (Ma²); ≈0 ⇒ правит дискретизация");
|
||||
}
|
||||
|
||||
/// Один прогон вихря Тейлора–Грина до полураспада; возвращает Σ|u_x−точн|/Σ|точн|.
|
||||
///
|
||||
/// Начальное состояние ставится ПОЛНОСТЬЮ согласованным: скорость, давление и неравновесная
|
||||
/// часть. Давление здесь не константа — течение несёт собственное поле порядка ρu₀²,
|
||||
/// которое находится из ∇²p = 2ρ(ψ_xx·ψ_yy − ψ_xy²):
|
||||
///
|
||||
/// p = −(ρu₀²/4)[cos(2k₁x) + (k₁²/k₂²)·cos(2k₂y)], δρ = p/c_s².
|
||||
///
|
||||
/// Если стартовать с ρ ≡ 1, эта разница уходит в акустику, которая в периодическом ящике
|
||||
/// почти не затухает и садится полкой на ошибку скорости, ломая порядок сходимости.
|
||||
fn taylor_green_error(n: usize, u0: R, nu: R, beta: R) -> TgError {
|
||||
taylor_green_error_op(n, u0, nu, beta, Collision::Kbc)
|
||||
}
|
||||
|
||||
fn taylor_green_error_op(n: usize, u0: R, nu: R, beta: R, op: Collision) -> TgError {
|
||||
let (k1, k2) = (1.0, 4.0);
|
||||
let tau = 1.0 / (2.0 * beta);
|
||||
let kk1 = 2.0 * std::f64::consts::PI * k1 / n as R;
|
||||
let kk2 = 2.0 * std::f64::consts::PI * k2 / n as R;
|
||||
let decay = nu * (kk1 * kk1 + kk2 * kk2);
|
||||
let tc = ((2.0_f64).ln() / decay).round() as usize;
|
||||
let exact = |x: usize, y: usize, e: R| -> (R, R) {
|
||||
let (a, b) = (kk1 * x as R, kk2 * y as R);
|
||||
(-u0 * a.cos() * b.sin() * e, (k1 / k2) * u0 * a.sin() * b.cos() * e)
|
||||
};
|
||||
let mut f = vec![[0.0; Q]; n * n];
|
||||
for y in 0..n {
|
||||
for x in 0..n {
|
||||
let (ux, uy) = exact(x, y, 1.0);
|
||||
let (a, b) = (kk1 * x as R, kk2 * y as R);
|
||||
let p = -(u0 * u0 / 4.0)
|
||||
* ((2.0 * a).cos() + (k1 * k1) / (k2 * k2) * (2.0 * b).cos());
|
||||
let rho = 1.0 + p / math::CS2;
|
||||
let dxux = u0 * kk1 * a.sin() * b.sin();
|
||||
let dyux = -u0 * kk2 * a.cos() * b.cos();
|
||||
let dxuy = (k1 / k2) * u0 * kk1 * a.cos() * b.cos();
|
||||
let dyuy = -(k1 / k2) * u0 * kk2 * a.sin() * b.sin();
|
||||
// Π = ρc_s²δ + ρuu + Π⁽¹⁾, Π⁽¹⁾ = −τρc_s²(∂_αu_β + ∂_βu_α) — ур. (53)
|
||||
let pref = -tau * rho * math::CS2;
|
||||
let pxx = rho * math::CS2 + rho * ux * ux + pref * 2.0 * dxux;
|
||||
let pyy = rho * math::CS2 + rho * uy * uy + pref * 2.0 * dyuy;
|
||||
let pxy = rho * ux * uy + pref * (dyux + dxuy);
|
||||
f[y * n + x] = math::grad_init(rho, ux, uy, pxx, pxy, pyy);
|
||||
}
|
||||
}
|
||||
let mut tmp = f.clone();
|
||||
for _ in 0..tc {
|
||||
periodic_step_op(&mut f, &mut tmp, n, n, beta, op);
|
||||
}
|
||||
let e = (-decay * tc as R).exp();
|
||||
let (mut num, mut den) = (0.0, 0.0);
|
||||
// заодно раскладываем ошибку: наилучшая подгонка амплитуды к точной форме
|
||||
let (mut dot, mut nrm2) = (0.0, 0.0);
|
||||
for y in 0..n {
|
||||
for x in 0..n {
|
||||
let got = math::macros(&f[y * n + x]).1;
|
||||
let want = exact(x, y, e).0;
|
||||
num += (got - want).abs();
|
||||
den += want.abs();
|
||||
dot += got * want;
|
||||
nrm2 += want * want;
|
||||
}
|
||||
}
|
||||
let amp = dot / nrm2; // 1.0 = амплитуда совпала
|
||||
let (mut snum, mut sden) = (0.0, 0.0);
|
||||
for y in 0..n {
|
||||
for x in 0..n {
|
||||
let got = math::macros(&f[y * n + x]).1;
|
||||
let want = amp * exact(x, y, e).0;
|
||||
snum += (got - want).abs();
|
||||
sden += want.abs();
|
||||
}
|
||||
}
|
||||
TgError { total: num / den, amplitude: (amp - 1.0).abs(), shape: snum / sden }
|
||||
}
|
||||
|
||||
/// Разложение ошибки эталона: полная, вклад амплитуды (скорость затухания) и вклад формы.
|
||||
struct TgError {
|
||||
total: R,
|
||||
amplitude: R,
|
||||
shape: R,
|
||||
}
|
||||
|
||||
/// ВИХРЬ ТЕЙЛОРА–ГРИНА — первый эталон 2D-статьи (разд. VI). Единственное из трёх течений
|
||||
/// статьи, у которого есть ТОЧНОЕ аналитическое решение, поэтому проверяется не «похоже на
|
||||
/// чужой прогон», а прямое совпадение с формулой и заявленный статьёй ВТОРОЙ ПОРЯДОК
|
||||
/// сходимости (рис. 1a: Re = 100, u₀ = 0.03, N ∈ {64, 128, 256}).
|
||||
///
|
||||
/// Постановка ровно по статье:
|
||||
/// u = ∇×[(u₀/k₂)cos(k₁x)cos(k₂y)·exp(−ν(k₁²+k₂²)t)], k₁ = 1, k₂ = 4,
|
||||
/// область 0 < x,y < 2π на сетке N×N, Re = u₀N/ν, полураспад t_c = ln2/[ν(k₁²+k₂²)].
|
||||
/// Волновые числа переводятся в решёточные: K = 2πk/N (узел — единица длины).
|
||||
/// Старт — приближением Града (ур. 58), как в статье.
|
||||
/// Метрика — та же, что на рис. 1: Σ|u_x − u_x^точн| / Σ|u_x^точн| в момент t_c.
|
||||
#[test]
|
||||
#[ignore = "долгий (до 256², t_c растёт как N²); запуск: cargo test --release -- --ignored"]
|
||||
fn taylor_green_converges_at_second_order() {
|
||||
// ДИФФУЗИОННОЕ ИЗМЕЛЬЧЕНИЕ: ν фиксирована, u₀ ∝ 1/N. Тогда Re = u₀N/ν сохраняется
|
||||
// (все три сетки считают ОДНО И ТО ЖЕ течение), а число Маха падает как 1/N — вместе
|
||||
// с ним падает и сжимаемостная ошибка метода. Только при таком измельчении второй
|
||||
// порядок виден целиком; при фиксированном u₀ ошибка упирается в полку O(Ma²), которая
|
||||
// от сетки не зависит вовсе (см. taylor_green_error_scaling).
|
||||
let nu = 0.0192;
|
||||
let beta = math::beta_of_nu(nu);
|
||||
let mut errs = Vec::new();
|
||||
for &n in &[64usize, 128, 256] {
|
||||
let u0 = 0.03 * 64.0 / n as R;
|
||||
let e = taylor_green_error(n, u0, nu, beta);
|
||||
println!(
|
||||
" N={n:>4} u₀={u0:.5} Re={:.0} полная {:.3e} амплитуда {:.3e} форма {:.3e}",
|
||||
u0 * n as R / nu, e.total, e.amplitude, e.shape
|
||||
);
|
||||
errs.push((n, e));
|
||||
}
|
||||
for w in errs.windows(2) {
|
||||
let (n0, e0) = (&w[0].0, &w[0].1);
|
||||
let (n1, e1) = (&w[1].0, &w[1].1);
|
||||
let ord = |a: R, b: R| (a / b).ln() / (*n1 as R / *n0 as R).ln();
|
||||
let (pt, pa, ps) = (
|
||||
ord(e0.total, e1.total),
|
||||
ord(e0.amplitude, e1.amplitude),
|
||||
ord(e0.shape, e1.shape),
|
||||
);
|
||||
println!(" порядок {n0}→{n1}: полная {pt:.2} амплитуда {pa:.2} форма {ps:.2}");
|
||||
assert!(
|
||||
(1.7..2.6).contains(&pt),
|
||||
"порядок {n0}→{n1} = {pt:.2}, статья (разд. VI) заявляет второй"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Диагностика: тот же вихрь Тейлора–Грина оператором LBGK. Статья (рис. 1) утверждает,
|
||||
/// что на этом течении LBGK и все варианты KBC идут практически одинаково; если полка по
|
||||
/// форме есть у обоих — она от схемы D2Q9, а не от энтропийного стабилизатора.
|
||||
#[test]
|
||||
#[ignore = "диагностика; запуск: cargo test --release -- --ignored taylor_green_kbc_vs_bgk --nocapture"]
|
||||
fn taylor_green_kbc_vs_bgk() {
|
||||
let (u0, re) = (0.03, 100.0);
|
||||
for &n in &[64usize, 128, 256] {
|
||||
let nu = u0 * n as R / re;
|
||||
let beta = math::beta_of_nu(nu);
|
||||
let k = taylor_green_error_op(n, u0, nu, beta, Collision::Kbc);
|
||||
let b = taylor_green_error_op(n, u0, nu, beta, Collision::Bgk);
|
||||
println!(
|
||||
" N={n:>4} KBC: форма {:.3e} ампл {:.3e} LBGK: форма {:.3e} ампл {:.3e}",
|
||||
k.shape, k.amplitude, b.shape, b.amplitude
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// СКВОЗНАЯ СВЕРКА С ЭТАЛОНОМ. Дважды периодический сдвиговый слой — один из трёх
|
||||
/// бенчмарков 2D-статьи (разд. VIII). Постановка: N=128, Re=30000, u0=0.04, κ=80, δ=0.05,
|
||||
/// одно конвективное время t_c = N/u0 шагов.
|
||||
|
||||
Reference in New Issue
Block a user