Длительность в секундах (--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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user