Длительность в секундах (--time) и граничное условие Града как альтернатива Bouzidi

--time <секунды> задаёт длительность прогона прямо в физическом времени, число шагов
считается как time/δt. Мотив: шаг не есть фиксированная порция времени — δt = u_lat·δx/u_phys
привязан к размеру клетки, поэтому одно и то же число шагов на разных сетках покрывает разное
физическое время (клетка втрое мельче ⇒ вместо 6 секунд получается 2).

ГРАНИЧНОЕ УСЛОВИЕ ГРАДА (--wall grad) по Dorschner, Bösch, Chikatamarla, Boulouchos, Karlin,
J. Fluid Mech. 801 (2016), разд. 2.1 и прил. B: недостающие популяции задаются не напрямую, а
через целевые моменты — скорость (B 1), плотность (B 3) и тензор давлений (2.14)–(2.16), —
после чего собираются приближением Града (2.13). Переиспользует grad_init, уже проверенный на
эталоне Тейлора–Грина. Скорость на момент t берётся из пост-столкновительного поля: столкновение
сохраняет ρ и ρu, поэтому отдельное хранилище прошлого шага не нужно.

Добавлена также заведомо ступенчатая модель (--wall staircase) — не для счёта, а как база
сравнения, показывающая, сколько именно даёт субсеточность.

ИЗМЕРЕНО, насколько каждая модель субсеточна. Тело сдвигается внутри клетки, смотрится разброс
Cd (Re=20, D=16, стационар): staircase 1.11%, grad 0.64%, bouzidi 0.19%. Град оказывается ровно
между ступенькой и Bouzidi, и это следует из его устройства: положение стенки входит туда только
через целевую скорость — одну усреднённую по узлу величину, тогда как Bouzidi подставляет свою
долю пересечения в каждую популяцию отдельно.

Сходимость по разрешению тела (домен 15D×10D, Re=20): bouzidi 2.581/2.529/2.521 и grad
2.618/2.534/2.521 при D=8/16/32. Обе состоятельны, сходятся к одному пределу с наблюдаемым
порядком ≈2.7 и к D=32 неразличимы; на грубой сетке Град заметно хуже. Ступенчатая модель при
D=16 даёт 2.69, то есть +6.7% к пределу против +0.4% у субсеточных.

Заявленного в статье выигрыша Града по устойчивости на высоких Re в здешней канальной постановке
воспроизвести не удалось: обе модели теряют счёт на одном и том же Re, то есть ограничивает не
стенка. Поэтому умолчание остаётся bouzidi.

В шапку добавлена диагностика границы тела: сколько линков идут по интерполяционной формуле,
сколько сваливаются в ступенчатый отскок, каков разброс доли пересечения. На NACA и цилиндре
интерполяция покрывает 100% линков.

GPU-бэкенд условие Града пока не поддерживает и при таком выборе отказывается запускаться явно,
а не считает молча по Bouzidi.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 01:54:22 +03:00
co-authored by Claude Opus 5
parent f174f1c9a0
commit e3c2d417f8
6 changed files with 508 additions and 17 deletions
+69 -4
View File
@@ -95,6 +95,8 @@ pub struct Spec {
pub collision: Collision,
/// Что входит в сдвиговую часть s (см. `math::KbcModel`).
pub kbc_model: math::KbcModel,
/// Чем замыкаются недостающие популяции на теле (см. `math::WallModel`).
pub wall: math::WallModel,
/// Узел зонда следа в координатах L0.
pub probe: (usize, usize),
}
@@ -167,9 +169,14 @@ struct Cli {
body_y: Option<R>,
// ── время ──
/// Число шагов симуляции
#[arg(long, default_value_t = 20000, help_heading = "Время")]
steps: u64,
/// Число шагов симуляции. Взаимоисключающе с --time.
#[arg(long, conflicts_with = "time", help_heading = "Время")]
steps: Option<u64>,
/// Длительность прогона в СЕКУНДАХ физического времени. Число шагов считается как
/// time/δt, где δt = u_lat·dx/u_phys — то есть зависит и от размера клетки, и от
/// решёточной скорости. Взаимоисключающе с --steps.
#[arg(long, help_heading = "Время")]
time: Option<R>,
/// Длина smoothstep-разгона входа, шагов
#[arg(long, default_value_t = 1000, help_heading = "Время")]
ramp: u64,
@@ -190,6 +197,13 @@ struct Cli {
/// Оператор столкновения
#[arg(long, default_value = "kbc", value_parser = ["kbc", "bgk"], help_heading = "Схема")]
collision: String,
/// Модель стенки на теле: bouzidi — интерполированный отскок по линкам; grad — условие
/// Града (Dorschner и др., JFM 801 (2016)), где задаются не популяции, а целевые моменты
/// ρ, u и тензор давлений. Авторы метода предпочитают grad: интерполяционные схемы, по их
/// словам, «ограничены низкими Re, поскольку на границе возникают паразитные скачки».
#[arg(long, default_value = "bouzidi", value_parser = math::WallModel::ALL,
help_heading = "Схема")]
wall: String,
/// Состав сдвиговой части KBC (табл. I 2D-статьи): n1 — только девиатор {N, Π_xy}
/// (KBC D), объёмная вязкость гуляет вместе с γ и может стать отрицательной; n2 —
/// девиатор со следом {N, Π_xy, T} (KBC C), объёмная вязкость фиксирована ξ = ν.
@@ -375,6 +389,33 @@ fn build_spec(cli: &Cli) -> Result<Spec, String> {
(cy.round() as usize).min(cli.ny - 2),
);
// Длительность задаётся либо в шагах, либо в секундах. Шаг не есть фиксированная порция
// времени: δt привязан к размеру клетки, поэтому одно и то же число шагов на разных
// сетках покрывает разное физическое время.
let steps = match (cli.steps, cli.time) {
(Some(s), _) => s,
(None, Some(t)) => {
if !(t > 0.0) {
return Err("--time обязан быть положительным".into());
}
let n = (t / units.dt).round();
if n < 1.0 {
return Err(format!(
"--time {t} с при шаге δt = {:.4e} с даёт меньше одного шага",
units.dt
));
}
if n > 2e9 {
return Err(format!(
"--time {t} с при шаге δt = {:.4e} с требует {n:.3e} шагов — это заведомо неподъёмно. Подними --u-lat или --dx, либо задай --steps явно",
units.dt
));
}
n as u64
}
(None, None) => 20_000,
};
Ok(Spec {
nx: cli.nx,
ny: cli.ny,
@@ -383,7 +424,7 @@ fn build_spec(cli: &Cli) -> Result<Spec, String> {
patch,
units,
beta0,
steps: cli.steps,
steps,
ramp: cli.ramp.max(1),
flow_angle: cli.flow_angle * std::f64::consts::PI / 180.0,
pert_amp: cli.pert_amp,
@@ -394,6 +435,7 @@ fn build_spec(cli: &Cli) -> Result<Spec, String> {
sponge_mult: cli.sponge_mult,
collision: if cli.collision == "bgk" { Collision::Bgk } else { Collision::Kbc },
kbc_model: math::KbcModel::from_str(&cli.kbc_model).ok_or("неизвестная модель KBC")?,
wall: math::WallModel::from_str(&cli.wall).ok_or("неизвестная модель стенки")?,
probe,
})
}
@@ -666,6 +708,29 @@ fn print_header(spec: &Spec, cli: &Cli, plan: &gif::GifPlan, field: FieldKind) {
} else {
"LBGK"
});
// Действительно ли субсеточная граница работает: если большинство линков сваливается в
// простой отскок, стенка де-факто ступенчатая, как её ни называй.
{
let g = cpu::Geom::build(spec.nx, spec.ny, &spec.body);
let w = g.wall_stats();
let pc = |k: usize| 100.0 * k as R / w.links.max(1) as R;
println!("
── Граница тела ────────────────────────────────────────────────────────────");
println!(" граничных узлов {:>12} линков {}", w.nodes, w.links);
println!(" интерполяция {:>11.1}% (ближняя {:.1}% + дальняя {:.1}%)",
pc(w.near) + pc(w.far), pc(w.near), pc(w.far));
println!(" простой отскок {:>11.1}% {}", pc(w.simple),
if w.simple == 0 { "— ступенчатых линков нет" } else { "← ЭТИ ЛИНКИ СТУПЕНЧАТЫЕ" });
println!(" доля пересечения q {:>12} мин {:.3}, средн {:.3}, макс {:.3}; зажато {:.1}%",
"", w.q_min, w.q_mean, w.q_max, pc(w.q_clipped));
}
println!(" стенка тела {:>12}",
match spec.wall {
math::WallModel::Bouzidi => "Bouzidi",
math::WallModel::Grad => "Град (моменты)",
math::WallModel::Staircase => "простой отскок",
});
println!(" выход по u_y {:>12} губка {} столбцов ×{:.0}",
if spec.outlet_extrapolate { "extrapolate" } else { "zero" },
spec.sponge_len, spec.sponge_mult);