Сверка со статьями: эталон Тейлора–Грина, выбор модели 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:
@@ -88,9 +88,13 @@ pub struct Spec {
|
||||
pub pert_amp: R,
|
||||
pub pert_dur: u64,
|
||||
pub outlet_extrapolate: bool,
|
||||
/// Начальное поле уже несёт набегающий поток (иначе — покой с разгоном входа).
|
||||
pub init_uniform: bool,
|
||||
pub sponge_len: usize,
|
||||
pub sponge_mult: R,
|
||||
pub collision: Collision,
|
||||
/// Что входит в сдвиговую часть s (см. `math::KbcModel`).
|
||||
pub kbc_model: math::KbcModel,
|
||||
/// Узел зонда следа в координатах L0.
|
||||
pub probe: (usize, usize),
|
||||
}
|
||||
@@ -175,20 +179,32 @@ struct Cli {
|
||||
/// Длительность стартового импульса, шагов
|
||||
#[arg(long, default_value_t = 300, help_heading = "Время")]
|
||||
pert_dur: u64,
|
||||
/// Начальное поле: uniform — домен сразу заполнен набегающим потоком и вход включён на
|
||||
/// полную (умолчание); rest — покой, вход разгоняется за --ramp шагов. Старт из покоя
|
||||
/// закачивает в канал продольную акустическую моду, которая почти не затухает.
|
||||
#[arg(long, default_value = "uniform", value_parser = ["uniform", "rest"],
|
||||
help_heading = "Время")]
|
||||
init: String,
|
||||
|
||||
// ── численная схема ──
|
||||
/// Оператор столкновения
|
||||
#[arg(long, default_value = "kbc", value_parser = ["kbc", "bgk"], help_heading = "Схема")]
|
||||
collision: String,
|
||||
/// Состав сдвиговой части KBC (табл. I 2D-статьи): n1 — только девиатор {N, Π_xy}
|
||||
/// (KBC D), объёмная вязкость гуляет вместе с γ и может стать отрицательной; n2 —
|
||||
/// девиатор со следом {N, Π_xy, T} (KBC C), объёмная вязкость фиксирована ξ = ν.
|
||||
#[arg(long, default_value = "n1", value_parser = math::KbcModel::ALL, help_heading = "Схема")]
|
||||
kbc_model: String,
|
||||
/// Поперечная скорость на выходе: extrapolate — нуль-градиент (умолчание: в факторном
|
||||
/// исследовании эталонного решателя это лучший вариант по всем метрикам), zero — жёсткий
|
||||
/// ноль (классический Zou–He; отражает вихри дорожки назад к телу и завышает rms Cl)
|
||||
#[arg(long, default_value = "extrapolate", value_parser = ["zero", "extrapolate"],
|
||||
help_heading = "Схема")]
|
||||
outlet: String,
|
||||
/// Длина поглощающей губки перед выходом, столбцов (0 — выключена)
|
||||
#[arg(long, default_value_t = 0, help_heading = "Схема")]
|
||||
sponge_len: usize,
|
||||
/// Длина поглощающей губки перед выходом, столбцов. Не задана — подбирается по домену;
|
||||
/// 0 — выключить. Губка гасит продольную моду, которую вязкость сама не гасит.
|
||||
#[arg(long, help_heading = "Схема")]
|
||||
sponge_len: Option<usize>,
|
||||
/// Во сколько раз губка поднимает вязкость
|
||||
#[arg(long, default_value_t = 30.0, help_heading = "Схема")]
|
||||
sponge_mult: R,
|
||||
@@ -322,17 +338,38 @@ fn build_spec(cli: &Cli) -> Result<Spec, String> {
|
||||
if !(2 <= ax && ax < bx && bx <= cli.nx - 2) {
|
||||
return Err(format!("патч по x [{ax},{bx}] обязан лежать внутри (1,{})", cli.nx - 1));
|
||||
}
|
||||
if cli.sponge_len > 0 && cli.nx - 1 - cli.sponge_len <= bx {
|
||||
return Err(format!(
|
||||
"губка (последние {} столбцов) накрывает патч (bx={bx})",
|
||||
cli.sponge_len
|
||||
));
|
||||
}
|
||||
Some((ax, bx, ay, by))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Губка перед выходом. Задача канала «вход по скорости + выход по давлению» акустически
|
||||
// есть четвертьволновая труба: вход отражает продольные волны как жёсткий поршень, выход —
|
||||
// как открытый конец. Собственная мода затухает как ν(π/2Nx)², то есть практически не
|
||||
// затухает, и любой стартовый удар остаётся в домене навсегда. Губка — единственное, что
|
||||
// её реально ест, поэтому по умолчанию она включена и подобрана под длину домена.
|
||||
let sponge_len = match cli.sponge_len {
|
||||
Some(v) => {
|
||||
if v > 0 {
|
||||
if let Some((_, bx, _, _)) = patch {
|
||||
if cli.nx - 1 - v <= bx {
|
||||
return Err(format!("губка (последние {v} столбцов) накрывает патч (bx={bx})"));
|
||||
}
|
||||
}
|
||||
}
|
||||
v
|
||||
}
|
||||
None => {
|
||||
let want = (cli.nx / 12).max(16);
|
||||
// губка не должна доставать до патча (и вообще до тела)
|
||||
let limit = match patch {
|
||||
Some((_, bx, _, _)) => cli.nx.saturating_sub(bx + 2),
|
||||
None => cli.nx / 4,
|
||||
};
|
||||
want.min(limit)
|
||||
}
|
||||
};
|
||||
|
||||
let probe = (
|
||||
((cx + 3.0 * cli.size).round() as usize).min(cli.nx - 2),
|
||||
(cy.round() as usize).min(cli.ny - 2),
|
||||
@@ -352,9 +389,11 @@ fn build_spec(cli: &Cli) -> Result<Spec, String> {
|
||||
pert_amp: cli.pert_amp,
|
||||
pert_dur: cli.pert_dur,
|
||||
outlet_extrapolate: cli.outlet == "extrapolate",
|
||||
sponge_len: cli.sponge_len,
|
||||
init_uniform: cli.init == "uniform",
|
||||
sponge_len,
|
||||
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")?,
|
||||
probe,
|
||||
})
|
||||
}
|
||||
@@ -618,21 +657,52 @@ fn print_header(spec: &Spec, cli: &Cli, plan: &gif::GifPlan, field: FieldKind) {
|
||||
}
|
||||
None => println!(" L1 {:>12} (измельчение выключено)", "нет"),
|
||||
}
|
||||
println!(" оператор {:>12}", if spec.collision == Collision::Kbc { "KBC-D" } else { "LBGK" });
|
||||
println!(" оператор {:>12}",
|
||||
if spec.collision == Collision::Kbc {
|
||||
match spec.kbc_model {
|
||||
math::KbcModel::N1 => "KBC-N1 (D)",
|
||||
math::KbcModel::N2 => "KBC-N2 (C)",
|
||||
}
|
||||
} else {
|
||||
"LBGK"
|
||||
});
|
||||
println!(" выход по u_y {:>12} губка {} столбцов ×{:.0}",
|
||||
if spec.outlet_extrapolate { "extrapolate" } else { "zero" },
|
||||
spec.sponge_len, spec.sponge_mult);
|
||||
|
||||
// Продольная акустика — не мелочь оформления, а то, что видно глазом: при заметной
|
||||
// амплитуде поток «дышит» рывками. Печатаем период моды и то, чем она гасится.
|
||||
{
|
||||
let cs = math::CS2.sqrt();
|
||||
let period = 4.0 * spec.nx as R / cs; // четвертьволновая труба: lambda = 4L
|
||||
let nu = math::nu_of_beta(spec.beta0);
|
||||
let k = std::f64::consts::PI / (2.0 * spec.nx as R);
|
||||
let efold = 1.0 / (nu * k * k);
|
||||
println!("
|
||||
── Продольная акустика канала ──────────────────────────────────────────────");
|
||||
println!(" вход по скорости отражает звук как жёсткий поршень, выход по давлению — как");
|
||||
println!(" открытый конец: канал работает четвертьволновым резонатором.");
|
||||
println!(" период основной моды {:>10.0} шагов = {:.4} с", period, period * spec.units.dt);
|
||||
println!(" затухание вязкостью {:>10.0} шагов на е-кратное — то есть сама не гаснет", efold);
|
||||
print!(" чем гасится: ");
|
||||
if spec.init_uniform {
|
||||
print!("старт из однородного потока (моду не возбуждаем)");
|
||||
} else {
|
||||
print!("РАЗГОН ИЗ ПОКОЯ за {} шагов", spec.ramp);
|
||||
if (spec.ramp as R) < period {
|
||||
print!(" — короче периода моды, будет звон!");
|
||||
}
|
||||
}
|
||||
if spec.sponge_len > 0 {
|
||||
println!(" + губка {} столбцов ×{:.0}", spec.sponge_len, spec.sponge_mult);
|
||||
} else {
|
||||
println!("; губка ВЫКЛЮЧЕНА");
|
||||
}
|
||||
}
|
||||
|
||||
// Пара «вход по скорости / выход по давлению» — недодемпфированный акустический резонатор:
|
||||
// затухание продольной моды ~ ν(π/Nx)², то есть на длинном домене её почти ничто не гасит,
|
||||
// и стартовый транзиент способен раскачать её до смещённого режима или до развала счёта.
|
||||
if spec.sponge_len == 0 && spec.nx >= 250 {
|
||||
println!("
|
||||
⚠ домен длинный ({} столбцов), губка выключена. Продольная акустическая", spec.nx);
|
||||
println!(" мода затухает как ν(π/Nx)² и на такой длине почти не гасится: возможен уход");
|
||||
println!(" ⟨ρ⟩ от единицы (режим смещён, числа несопоставимы) вплоть до развала счёта.");
|
||||
println!(" Смотрите ⟨ρ⟩ в отчёте о сходимости; лечится ключом --sponge-len 32.");
|
||||
}
|
||||
|
||||
if cli.gif.is_some() {
|
||||
println!("\n── Анимация (синхронизация с физическим временем) ───────────────────────────");
|
||||
@@ -791,6 +861,31 @@ fn final_report(
|
||||
println!("rms Cl насыщен ({:+.4}); дрейф-устойчивый Cd = {cdn1:.3}", clr1 - clr0);
|
||||
}
|
||||
|
||||
// ── измеренная продольная пульсация ──
|
||||
// Колебание средней плотности — это и есть та самая «поршневая» мода. Переводим его в
|
||||
// амплитуду скорости: для бегущей акустической волны u' = (δρ/ρ)·c_s.
|
||||
{
|
||||
let cs = math::CS2.sqrt();
|
||||
let last = &rho[n - n / 10..];
|
||||
let lo = last.iter().cloned().fold(R::INFINITY, R::min);
|
||||
let hi = last.iter().cloned().fold(R::NEG_INFINITY, R::max);
|
||||
let mean = last.iter().sum::<R>() / last.len() as R;
|
||||
let u_ac = 0.5 * (hi - lo) / mean * cs;
|
||||
let ratio = u_ac / u.u_lat;
|
||||
println!("
|
||||
── Продольная пульсация (последняя десятая часть прогона) ───────────────────");
|
||||
println!(" размах ⟨ρ⟩ = {:.5} вокруг {mean:.5} ⇒ амплитуда скорости u' = {:.2}% от U",
|
||||
hi - lo, ratio * 100.0);
|
||||
if ratio > 0.05 {
|
||||
println!(" ⚠ ПОТОК ЗАМЕТНО ПУЛЬСИРУЕТ: это продольная мода канала, а не физика следа.");
|
||||
println!(" Лечится стартом из однородного потока (--init uniform) и губкой");
|
||||
println!(" (--sponge-len {}). Числа Cd/St на таком прогоне недостоверны.",
|
||||
(spec.nx / 12).max(16));
|
||||
} else {
|
||||
println!(" пульсация в пределах нормы — продольная мода не раскачана");
|
||||
}
|
||||
}
|
||||
|
||||
// ── диагностика KBC ──
|
||||
if spec.collision == Collision::Kbc {
|
||||
let (g_m, g_s) = math::mean_std(&recs[h..].iter().map(|r| r.gamma_mean).collect::<Vec<_>>());
|
||||
|
||||
Reference in New Issue
Block a user