Несколько тел в домене, произвольная геометрия и разбор стартовой акустики
НЕСКОЛЬКО ТЕЛ (--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:
@@ -19,7 +19,7 @@ use std::time::Instant;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use math::{Body, ShapeKind, Units, R};
|
||||
use math::{Body, Scene, ShapeKind, Units, MAX_BODY_BUCKETS, R};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Контракт оркестратор ↔ бэкенд
|
||||
@@ -57,9 +57,13 @@ impl FieldKind {
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct StepRec {
|
||||
pub step: u64,
|
||||
/// Суммарная сила и момент по всей сцене.
|
||||
pub fx: R,
|
||||
pub fy: R,
|
||||
pub tz: R,
|
||||
/// Она же по телам: `body[b] = (F_x, F_y, T_z)`. Тела за пределом набора вёдер
|
||||
/// сваливаются в последнее, поэтому сумма по вёдрам всегда точна.
|
||||
pub body: [[R; 3]; MAX_BODY_BUCKETS],
|
||||
pub uy_probe: R,
|
||||
pub rho_mean: R,
|
||||
pub max_u: R,
|
||||
@@ -75,7 +79,7 @@ pub struct StepRec {
|
||||
pub struct Spec {
|
||||
pub nx: usize,
|
||||
pub ny: usize,
|
||||
pub body: Body,
|
||||
pub scene: Scene,
|
||||
pub refine: usize,
|
||||
/// Границы патча измельчения в координатах L0: (ax, bx, ay, by).
|
||||
pub patch: Option<(usize, usize, usize, usize)>,
|
||||
@@ -90,7 +94,11 @@ pub struct Spec {
|
||||
pub outlet_extrapolate: bool,
|
||||
/// Начальное поле уже несёт набегающий поток (иначе — покой с разгоном входа).
|
||||
pub init_uniform: bool,
|
||||
/// Ширина сведения стартовой скорости к нулю у тела, клеток.
|
||||
pub init_taper: R,
|
||||
pub sponge_len: usize,
|
||||
/// Длина губки после входа, столбцов.
|
||||
pub sponge_in: usize,
|
||||
pub sponge_mult: R,
|
||||
pub collision: Collision,
|
||||
/// Что входит в сдвиговую часть s (см. `math::KbcModel`).
|
||||
@@ -167,6 +175,20 @@ struct Cli {
|
||||
/// Положение центра тела по y, ячеек (по умолчанию ny/2)
|
||||
#[arg(long, help_heading = "Тело")]
|
||||
body_y: Option<R>,
|
||||
/// Четырёхзначное обозначение профиля NACA, например 4412 (кривизна 4%, её максимум на
|
||||
/// 40% хорды, толщина 12%). Действует при --shape naca и перекрывает --thickness.
|
||||
#[arg(long, help_heading = "Тело")]
|
||||
naca: Option<String>,
|
||||
/// Вершины произвольного многоугольника при --shape polygon: "x1,y1;x2,y2;..." в клетках,
|
||||
/// отсчёт от центра тела. Обход любой, SDF точный.
|
||||
#[arg(long, allow_hyphen_values = true, help_heading = "Тело")]
|
||||
poly: Option<String>,
|
||||
/// НЕСКОЛЬКО ТЕЛ вместо одного. Список через точку с запятой, каждое — форма и параметры:
|
||||
/// "cylinder:d=24,x=120,y=120; cylinder:d=24,x=200,y=120". Ключи: d — размер, x/y — центр,
|
||||
/// a — угол атаки (град), t — относительная толщина, naca — код профиля,
|
||||
/// p — вершины многоугольника через |. Перекрывает --shape и прочие одиночные ключи.
|
||||
#[arg(long, allow_hyphen_values = true, help_heading = "Тело")]
|
||||
bodies: Option<String>,
|
||||
|
||||
// ── время ──
|
||||
/// Число шагов симуляции. Взаимоисключающе с --time.
|
||||
@@ -192,6 +214,17 @@ struct Cli {
|
||||
#[arg(long, default_value = "uniform", value_parser = ["uniform", "rest"],
|
||||
help_heading = "Время")]
|
||||
init: String,
|
||||
/// Ширина полосы у тела, на которой стартовая скорость сводится к нулю, клеток.
|
||||
///
|
||||
/// ЗАМЕРЕНО, что это НЕ ПОМОГАЕТ, и ключ оставлен только для повторной проверки.
|
||||
/// Однородный старт заливает потоком место, где стоит тело, и на первом шаге у стенки
|
||||
/// рождается возмущение плотности ~2.5·10⁻². Сглаживание монотонно давит именно его
|
||||
/// (при ширине 24 клетки — до 3.1·10⁻³, восьмикратно), но ПИК ЗА ПРОГОН при этом даже
|
||||
/// подрастает: 2.54·10⁻² → 3.22·10⁻². Возмущение просто переносится во времени — поток
|
||||
/// всё равно обязан разогнаться вокруг тела, и энергия этого переходного процесса задана
|
||||
/// физикой, а не гладкостью начального поля. Продольную моду ест губка, а не это.
|
||||
#[arg(long, default_value_t = 0.0, help_heading = "Время")]
|
||||
init_taper: R,
|
||||
|
||||
// ── численная схема ──
|
||||
/// Оператор столкновения
|
||||
@@ -221,6 +254,10 @@ struct Cli {
|
||||
/// 0 — выключить. Губка гасит продольную моду, которую вязкость сама не гасит.
|
||||
#[arg(long, help_heading = "Схема")]
|
||||
sponge_len: Option<usize>,
|
||||
/// Длина губки ПОСЛЕ входа, столбцов (0 — выключена). Вход по скорости отражает продольные
|
||||
/// волны как жёсткий поршень; губка у входа гасит их до отражения.
|
||||
#[arg(long, default_value_t = 0, help_heading = "Схема")]
|
||||
sponge_in: usize,
|
||||
/// Во сколько раз губка поднимает вязкость
|
||||
#[arg(long, default_value_t = 30.0, help_heading = "Схема")]
|
||||
sponge_mult: R,
|
||||
@@ -273,9 +310,22 @@ struct Cli {
|
||||
/// Число окон в отчёте о сходимости
|
||||
#[arg(long, default_value_t = 10, help_heading = "Вывод")]
|
||||
windows: usize,
|
||||
/// Прореживание временных рядов: хранить каждую N-ю запись. Статистика и спектр от этого
|
||||
/// не страдают, пока N много меньше периода схода вихрей, зато сверхдлинные прогоны
|
||||
/// перестают съедать память (на 5·10⁶ шагов полный ряд — сотни мегабайт).
|
||||
#[arg(long, default_value_t = 1, help_heading = "Вывод")]
|
||||
series_every: u64,
|
||||
/// Выгрузить временные ряды в CSV
|
||||
#[arg(long, help_heading = "Вывод")]
|
||||
csv: Option<String>,
|
||||
/// Файл x–t диаграммы: срез ⟨ρ⟩ и u_x вдоль осевой линии, по строке на срез. По наклону
|
||||
/// полос видно, бежит возмущение со скоростью звука или конвекции, а стоячие узлы
|
||||
/// проявляются сразу. Основной инструмент разбора стартовой акустики.
|
||||
#[arg(long, help_heading = "Вывод")]
|
||||
xt: Option<String>,
|
||||
/// Период записи срезов x–t, шагов
|
||||
#[arg(long, default_value_t = 200, help_heading = "Вывод")]
|
||||
xt_every: u64,
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -292,6 +342,65 @@ fn parse_pair(s: &str, what: &str) -> Result<(R, R), String> {
|
||||
Ok((a, b))
|
||||
}
|
||||
|
||||
/// Разбор одного тела из мини-синтаксиса `форма:ключ=значение,...`.
|
||||
fn parse_body(spec: &str, def_x: R, def_y: R) -> Result<Body, String> {
|
||||
let (name, rest) = match spec.split_once(':') {
|
||||
Some((a, b)) => (a.trim(), b),
|
||||
None => (spec.trim(), ""),
|
||||
};
|
||||
let kind = ShapeKind::from_str(name).ok_or(format!("неизвестная форма «{name}»"))?;
|
||||
let (mut d, mut x, mut y, mut a, mut t) = (24.0, def_x, def_y, 0.0, 0.3);
|
||||
let (mut camber, mut cpos) = (0.0, 0.0);
|
||||
let mut verts: Vec<[R; 2]> = Vec::new();
|
||||
for kv in rest.split(',').filter(|z| !z.trim().is_empty()) {
|
||||
let (k, v) = kv
|
||||
.split_once('=')
|
||||
.ok_or(format!("в «{}» ожидалось ключ=значение", kv.trim()))?;
|
||||
let (k, v) = (k.trim(), v.trim());
|
||||
let num = |v: &str| v.parse::<R>().map_err(|e| format!("{k}: {e}"));
|
||||
match k {
|
||||
"d" | "size" | "c" => d = num(v)?,
|
||||
"x" => x = num(v)?,
|
||||
"y" => y = num(v)?,
|
||||
"a" | "angle" => a = num(v)?,
|
||||
"t" | "thickness" => t = num(v)?,
|
||||
"naca" => {
|
||||
let (m, pp, tt) =
|
||||
Body::naca_code(v).ok_or(format!("naca: ожидалось четыре цифры, «{v}»"))?;
|
||||
camber = m;
|
||||
cpos = pp;
|
||||
t = tt;
|
||||
}
|
||||
"p" | "poly" => {
|
||||
let n: Vec<&str> = v.split('|').collect();
|
||||
if n.len() < 6 || n.len() % 2 != 0 {
|
||||
return Err("p: нужно чётное число координат, минимум три вершины".into());
|
||||
}
|
||||
for pair in n.chunks(2) {
|
||||
verts.push([num(pair[0])?, num(pair[1])?]);
|
||||
}
|
||||
}
|
||||
_ => return Err(format!("неизвестный ключ «{k}»")),
|
||||
}
|
||||
}
|
||||
if kind == ShapeKind::Polygon && verts.len() < 3 {
|
||||
return Err("для polygon нужен ключ p с вершинами".into());
|
||||
}
|
||||
Ok(Body::full(kind, x, y, d, a, t, camber, cpos, &verts))
|
||||
}
|
||||
|
||||
fn parse_poly(v: &str) -> Result<Vec<[R; 2]>, String> {
|
||||
let mut out = Vec::new();
|
||||
for pt in v.split(';').filter(|z| !z.trim().is_empty()) {
|
||||
let (a, b) = parse_pair(pt, "--poly")?;
|
||||
out.push([a, b]);
|
||||
}
|
||||
if out.len() < 3 {
|
||||
return Err("--poly: нужно минимум три вершины".into());
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn build_spec(cli: &Cli) -> Result<Spec, String> {
|
||||
if cli.nx < 16 || cli.ny < 16 {
|
||||
return Err("сетка меньше 16×16 не имеет смысла".into());
|
||||
@@ -306,12 +415,51 @@ fn build_spec(cli: &Cli) -> Result<Spec, String> {
|
||||
return Err("--refine обязан быть ≥ 1".into());
|
||||
}
|
||||
|
||||
let kind = ShapeKind::from_str(&cli.shape).ok_or("неизвестная форма")?;
|
||||
let cx = cli.body_x.unwrap_or(cli.nx as R / 4.0);
|
||||
let cy = cli.body_y.unwrap_or(cli.ny as R / 2.0);
|
||||
let body = Body::new(kind, cx, cy, cli.size, cli.body_angle, cli.thickness);
|
||||
let scene = match &cli.bodies {
|
||||
Some(list) => {
|
||||
let mut v = Vec::new();
|
||||
for one in list.split(';').filter(|z| !z.trim().is_empty()) {
|
||||
v.push(parse_body(one, cx, cy)?);
|
||||
}
|
||||
if v.is_empty() {
|
||||
return Err("--bodies: список пуст".into());
|
||||
}
|
||||
Scene { bodies: v }
|
||||
}
|
||||
None => {
|
||||
let kind = ShapeKind::from_str(&cli.shape).ok_or("неизвестная форма")?;
|
||||
let (mut camber, mut cpos, mut thick) = (0.0, 0.0, cli.thickness);
|
||||
if let Some(code) = &cli.naca {
|
||||
let (m, pp, tt) = Body::naca_code(code)
|
||||
.ok_or("--naca: ожидалось четыре цифры, например 4412")?;
|
||||
camber = m;
|
||||
cpos = pp;
|
||||
thick = tt;
|
||||
}
|
||||
let verts = match &cli.poly {
|
||||
Some(v) => parse_poly(v)?,
|
||||
None => Vec::new(),
|
||||
};
|
||||
if kind == ShapeKind::Polygon && verts.is_empty() {
|
||||
return Err("--shape polygon требует --poly с вершинами".into());
|
||||
}
|
||||
Scene::single(Body::full(
|
||||
kind,
|
||||
cx,
|
||||
cy,
|
||||
cli.size,
|
||||
cli.body_angle,
|
||||
thick,
|
||||
camber,
|
||||
cpos,
|
||||
&verts,
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let units = Units::new(cli.dx, cli.u_phys, cli.u_lat, cli.re, cli.size);
|
||||
let units = Units::new(cli.dx, cli.u_phys, cli.u_lat, cli.re, scene.ref_size());
|
||||
let beta0 = math::beta_of_nu(units.nu_lat);
|
||||
let tau0 = 1.0 / (2.0 * beta0);
|
||||
if tau0 <= 0.5 {
|
||||
@@ -364,6 +512,17 @@ fn build_spec(cli: &Cli) -> Result<Spec, String> {
|
||||
// как открытый конец. Собственная мода затухает как ν(π/2Nx)², то есть практически не
|
||||
// затухает, и любой стартовый удар остаётся в домене навсегда. Губка — единственное, что
|
||||
// её реально ест, поэтому по умолчанию она включена и подобрана под длину домена.
|
||||
// губка входа не должна доставать до патча измельчения
|
||||
if cli.sponge_in > 0 {
|
||||
if let Some((ax, _, _, _)) = patch {
|
||||
if cli.sponge_in + 2 >= ax {
|
||||
return Err(format!(
|
||||
"губка входа ({} столбцов) достаёт до патча (ax={ax})",
|
||||
cli.sponge_in
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
let sponge_len = match cli.sponge_len {
|
||||
Some(v) => {
|
||||
if v > 0 {
|
||||
@@ -421,7 +580,7 @@ fn build_spec(cli: &Cli) -> Result<Spec, String> {
|
||||
Ok(Spec {
|
||||
nx: cli.nx,
|
||||
ny: cli.ny,
|
||||
body,
|
||||
scene,
|
||||
refine: cli.refine,
|
||||
patch,
|
||||
units,
|
||||
@@ -433,7 +592,9 @@ fn build_spec(cli: &Cli) -> Result<Spec, String> {
|
||||
pert_dur: cli.pert_dur,
|
||||
outlet_extrapolate: cli.outlet == "extrapolate",
|
||||
init_uniform: cli.init == "uniform",
|
||||
init_taper: cli.init_taper.max(0.0),
|
||||
sponge_len,
|
||||
sponge_in: cli.sponge_in,
|
||||
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")?,
|
||||
@@ -482,6 +643,13 @@ impl Backend {
|
||||
Backend::Gpu(s) => s.sample_field(k),
|
||||
}
|
||||
}
|
||||
fn sample_centerline(&mut self) -> (Vec<R>, Vec<R>) {
|
||||
match self {
|
||||
Backend::Cpu(s) => s.sample_centerline(),
|
||||
#[cfg(feature = "gpu")]
|
||||
Backend::Gpu(s) => s.sample_centerline(),
|
||||
}
|
||||
}
|
||||
fn is_finite(&mut self) -> bool {
|
||||
match self {
|
||||
Backend::Cpu(s) => s.is_finite(),
|
||||
@@ -598,6 +766,19 @@ fn run(cli: Cli) -> Result<(), String> {
|
||||
None => None,
|
||||
};
|
||||
|
||||
// ── x–t диагностика ──
|
||||
let mut xt_file = match &cli.xt {
|
||||
Some(path) => {
|
||||
let mut f = std::io::BufWriter::new(
|
||||
std::fs::File::create(path).map_err(|e| format!("не создать {path}: {e}"))?,
|
||||
);
|
||||
writeln!(f, "# срез вдоль осевой линии, столбцы: step,t_phys_s,field,v0..v{}", spec.nx - 1)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Some(f)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
// ── цикл ──
|
||||
let d_ref = back.force_ref_size();
|
||||
let mut recs: Vec<StepRec> = Vec::with_capacity(spec.steps as usize + 1);
|
||||
@@ -611,16 +792,24 @@ fn run(cli: Cli) -> Result<(), String> {
|
||||
})
|
||||
.unwrap_or(0)) as f64;
|
||||
|
||||
let series_every = cli.series_every.max(1);
|
||||
let mut scratch: Vec<StepRec> = Vec::with_capacity(256);
|
||||
let mut rec = StepRec::default();
|
||||
for t in 0..=spec.steps {
|
||||
back.advance(&mut recs);
|
||||
back.advance(&mut scratch);
|
||||
|
||||
let need_frame = writer.is_some() && t % plan.stride == 0;
|
||||
let need_report = cli.report_every > 0 && !quiet && t % cli.report_every == 0;
|
||||
// и кадр, и живая строка требуют актуальных данных — синхронизируемся только здесь
|
||||
if need_frame || need_report {
|
||||
back.flush(&mut recs);
|
||||
back.flush(&mut scratch);
|
||||
}
|
||||
for r in scratch.drain(..) {
|
||||
if r.step % series_every == 0 {
|
||||
recs.push(r);
|
||||
}
|
||||
rec = r;
|
||||
}
|
||||
let rec = *recs.last().unwrap_or(&StepRec::default());
|
||||
|
||||
if need_frame {
|
||||
if let Some(w) = writer.as_mut() {
|
||||
@@ -634,18 +823,36 @@ fn run(cli: Cli) -> Result<(), String> {
|
||||
if need_report {
|
||||
live_line(&spec, &rec, d_ref, t_start.elapsed().as_secs_f64(), nodes_per_step, full);
|
||||
}
|
||||
if let Some(f) = xt_file.as_mut() {
|
||||
if t % cli.xt_every.max(1) == 0 {
|
||||
let (rho, ux) = back.sample_centerline();
|
||||
let tp = spec.units.time_of_step(t);
|
||||
for (name, v) in [("rho", &rho), ("ux", &ux)] {
|
||||
write!(f, "{t},{tp:.9e},{name}").map_err(|e| e.to_string())?;
|
||||
for z in v {
|
||||
write!(f, ",{z:.7e}").map_err(|e| e.to_string())?;
|
||||
}
|
||||
writeln!(f).map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Развал счёта ловится по УЖЕ ПОСЧИТАННЫМ величинам, а не полным проходом по полю:
|
||||
// на сетке 4096×2048 такой проход тянет с устройства сотни мегабайт, и делать его
|
||||
// регулярно нельзя. Полная проверка — один раз, для подтверждения.
|
||||
if !rec.max_u.is_finite() || !rec.rho_mean.is_finite() {
|
||||
back.flush(&mut recs);
|
||||
back.flush(&mut scratch);
|
||||
eprintln!("\nСЧЁТ РАЗВАЛИЛСЯ на шаге {t}: в поле появились NaN/inf.");
|
||||
blew_up = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
back.flush(&mut recs);
|
||||
back.flush(&mut scratch);
|
||||
for r in scratch.drain(..) {
|
||||
if r.step % series_every == 0 {
|
||||
recs.push(r);
|
||||
}
|
||||
}
|
||||
if !blew_up && !back.is_finite() {
|
||||
eprintln!("\nСЧЁТ РАЗВАЛИЛСЯ: итоговое поле содержит NaN/inf.");
|
||||
blew_up = true;
|
||||
@@ -690,7 +897,7 @@ fn default_range(k: FieldKind, u_lat: R, d_lat: R) -> gif::Range {
|
||||
fn print_header(spec: &Spec, cli: &Cli, plan: &gif::GifPlan, field: FieldKind) {
|
||||
let u = &spec.units;
|
||||
let tau = 1.0 / (2.0 * spec.beta0);
|
||||
let blockage = spec.body.frontal_extent() / spec.ny as R;
|
||||
let blockage = spec.scene.frontal_extent() / spec.ny as R;
|
||||
|
||||
println!("╔══════════════════════════════════════════════════════════════════════════╗");
|
||||
println!("║ KBC-2D — обтекание тела в канале, D2Q9 + энтропийное столкновение KBC-D ║");
|
||||
@@ -701,7 +908,12 @@ fn print_header(spec: &Spec, cli: &Cli, plan: &gif::GifPlan, field: FieldKind) {
|
||||
println!(" размер ячейки {:>12.5} м домен {:.2} × {:.2} м",
|
||||
u.dx, spec.nx as R * u.dx, spec.ny as R * u.dx);
|
||||
println!(" тело {:>12} {:.3} м ({:.0} ячеек), угол атаки {:.1}°",
|
||||
format!("{:?}", spec.body.kind).to_lowercase(), u.d_phys(), u.d_lat, cli.body_angle);
|
||||
if spec.scene.bodies.len() > 1 {
|
||||
format!("{} тел", spec.scene.bodies.len())
|
||||
} else {
|
||||
format!("{:?}", spec.scene.bodies[0].kind).to_lowercase()
|
||||
},
|
||||
u.d_phys(), u.d_lat, cli.body_angle);
|
||||
println!(" Рейнольдс {:>12.1} вязкость {:.4e} м²/с", u.re, u.nu_phys());
|
||||
println!(" Мах (реш.) {:>12.4} u_lat = {:.4}, τ = {:.4}", u.mach(), u.u_lat, tau);
|
||||
println!(" блокировка канала {:>12.3} (габарит тела / ширина канала)", blockage);
|
||||
@@ -739,7 +951,7 @@ fn print_header(spec: &Spec, cli: &Cli, plan: &gif::GifPlan, field: FieldKind) {
|
||||
// Действительно ли субсеточная граница работает: если большинство линков сваливается в
|
||||
// простой отскок, стенка де-факто ступенчатая, как её ни называй.
|
||||
{
|
||||
let g = cpu::Geom::build(spec.nx, spec.ny, &spec.body);
|
||||
let g = cpu::Geom::build(spec.nx, spec.ny, &spec.scene);
|
||||
let w = g.wall_stats();
|
||||
let pc = |k: usize| 100.0 * k as R / w.links.max(1) as R;
|
||||
println!("
|
||||
@@ -787,8 +999,11 @@ fn print_header(spec: &Spec, cli: &Cli, plan: &gif::GifPlan, field: FieldKind) {
|
||||
print!(" — короче периода моды, будет звон!");
|
||||
}
|
||||
}
|
||||
if spec.sponge_len > 0 {
|
||||
println!(" + губка {} столбцов ×{:.0}", spec.sponge_len, spec.sponge_mult);
|
||||
if spec.sponge_len > 0 || spec.sponge_in > 0 {
|
||||
println!(
|
||||
" + губка: вход {} / выход {} столбцов ×{:.0}",
|
||||
spec.sponge_in, spec.sponge_len, spec.sponge_mult
|
||||
);
|
||||
} else {
|
||||
println!("; губка ВЫКЛЮЧЕНА");
|
||||
}
|
||||
@@ -899,7 +1114,7 @@ fn final_report(
|
||||
}
|
||||
|
||||
// ── установившийся режим ──
|
||||
let beta = spec.body.frontal_extent() / spec.ny as R;
|
||||
let beta = spec.scene.frontal_extent() / spec.ny as R;
|
||||
println!("\n── Установившийся режим (вторая половина ряда, шаги {}–{}) ──────────────────", h, n - 1);
|
||||
println!(" {:<22}{:>10}{:>10}{:>10}{:>10}", "величина", "St", "⟨Cd⟩", "rms Cl", "⟨Cm⟩");
|
||||
println!(" {:<22}{:>10.4}{:>10.4}{:>10.4}{:>10.5}", "как посчитано", st, cd_m, cl_rms, cm_m);
|
||||
@@ -912,13 +1127,55 @@ fn final_report(
|
||||
cl_rms * (1.0 - beta).powi(2),
|
||||
"—"
|
||||
);
|
||||
if spec.body.kind == ShapeKind::Cylinder && u.re > 100.0 && u.re < 200.0 {
|
||||
if spec.scene.bodies.len() == 1
|
||||
&& spec.scene.bodies[0].kind == ShapeKind::Cylinder
|
||||
&& u.re > 100.0
|
||||
&& u.re < 200.0
|
||||
{
|
||||
println!(" {:<22}{:>10.4}{:>10.4}{:>10}{:>10.1}", "литература Re≈150", 0.183, 1.330, "~0.30", 0.0);
|
||||
}
|
||||
println!(" rms u_y в зонде = {uy_rms:.5} (узел {:?}); блокировка β = {beta:.3}", spec.probe);
|
||||
println!(" поправка: St×(1−β), Cd и rms Cl ×(1−β)². Литература — безграничный цилиндр;");
|
||||
println!(" ⟨Cm⟩≈0 для симметричного тела под нулевым углом — это контроль симметрии считывания.");
|
||||
|
||||
// ── силы по телам ──
|
||||
// Для тандема и многоэлементных конфигураций литература даёт коэффициенты КАЖДОГО тела
|
||||
// по отдельности, поэтому общей суммы недостаточно.
|
||||
let nb = spec.scene.bodies.len().min(MAX_BODY_BUCKETS);
|
||||
if nb > 1 {
|
||||
println!("\n── Силы по телам (вторая половина ряда) ────────────────────────────────────");
|
||||
println!(" {:>5}{:>12}{:>10}{:>10}{:>11}", "тело", "положение", "⟨Cd⟩", "rms Cl", "⟨Cm⟩");
|
||||
for b in 0..nb {
|
||||
let cdb: Vec<R> =
|
||||
recs[h..].iter().map(|r| 2.0 * r.body[b][0] / (u.u_lat * u.u_lat * d_ref)).collect();
|
||||
let clb: Vec<R> =
|
||||
recs[h..].iter().map(|r| 2.0 * r.body[b][1] / (u.u_lat * u.u_lat * d_ref)).collect();
|
||||
let cmb: Vec<R> = recs[h..]
|
||||
.iter()
|
||||
.map(|r| 2.0 * r.body[b][2] / (u.u_lat * u.u_lat * d_ref * d_ref))
|
||||
.collect();
|
||||
let (m, _) = math::mean_std(&cdb);
|
||||
let (_, sl) = math::mean_std(&clb);
|
||||
let (mm, _) = math::mean_std(&cmb);
|
||||
let pos = spec
|
||||
.scene
|
||||
.bodies
|
||||
.get(b)
|
||||
.map(|bd| format!("{:.0},{:.0}", bd.cx, bd.cy))
|
||||
.unwrap_or_else(|| "—".into());
|
||||
println!(" {:>5}{:>12}{:>10.4}{:>10.4}{:>11.5}", b + 1, pos, m, sl, mm);
|
||||
}
|
||||
if spec.scene.bodies.len() > MAX_BODY_BUCKETS {
|
||||
println!(
|
||||
" (тел {}, вёдер {} — тела с {}-го и дальше сведены в последнюю строку;",
|
||||
spec.scene.bodies.len(),
|
||||
MAX_BODY_BUCKETS,
|
||||
MAX_BODY_BUCKETS
|
||||
);
|
||||
println!(" сумма по строкам при этом точна и равна общему ⟨Cd⟩ выше)");
|
||||
}
|
||||
}
|
||||
|
||||
// ── сходимость ──
|
||||
let nw = cli.windows.max(2).min(n / 2);
|
||||
let w = n / nw;
|
||||
@@ -970,7 +1227,9 @@ fn final_report(
|
||||
── Продольная пульсация (последняя десятая часть прогона) ───────────────────");
|
||||
println!(" размах ⟨ρ⟩ = {:.5} вокруг {mean:.5} ⇒ амплитуда скорости u' = {:.2}% от U",
|
||||
hi - lo, ratio * 100.0);
|
||||
if ratio > 0.05 {
|
||||
if !ratio.is_finite() {
|
||||
println!(" ⚠ ПУЛЬСАЦИЯ НЕ ОПРЕДЕЛЕНА: в рядах NaN/inf, счёт развалился.");
|
||||
} else if ratio.abs() > 0.05 {
|
||||
println!(" ⚠ ПОТОК ЗАМЕТНО ПУЛЬСИРУЕТ: это продольная мода канала, а не физика следа.");
|
||||
println!(" Лечится стартом из однородного потока (--init uniform) и губкой");
|
||||
println!(" (--sponge-len {}). Числа Cd/St на таком прогоне недостоверны.",
|
||||
|
||||
Reference in New Issue
Block a user