Решатель KBC-2D на Rust: ядро по статьям, два бэкенда, гифка в реальном времени
Переписанный с нуля двумерный решатель LBM D2Q9 с энтропийным столкновением KBC в варианте «модель D» (табл. I 2D-статьи Bösch/Chikatamarla/Karlin, arXiv:1507.02509; в трёхмерных работах — KBC-N1). Код разложен по ролям на пять файлов: математика решателя, бэкенд под процессор, бэкенд под видеокарту, оркестратор, блок гифок. Ядро сверено с первоисточниками тестами (22 шт.): * проектор на сдвиг, выписанный аналитически из представления популяций через натуральные моменты (ур. 10), совпадает с матричным до 1e-13, идемпотентен; * γ из замкнутой оценки (ур. 17) — корень условия максимума энтропии (ур. 15); * сдвиговые моменты релаксируют ровно с 2β при любой γ, вязкость по ур. (5) воспроизводится затуханием сдвиговой волны с точностью лучше 1%; * сквозной бенчмарк статьи (дважды периодический сдвиговый слой, Re=3e4) сходится с fp64-эталоном питоновского решателя 0.6035. Порог вырожденности γ относительный (доля от ⟨Δ|Δ⟩): абсолютный подменял бы γ на 2 на большинстве узлов, молча превращая KBC в LBGK. Доля таких узлов печатается в отчёте. Бэкенды взаимозаменяемы и согласованы: CPU (rayon, f64) и GPU (wgpu/WGSL, f32) на одной постановке совпадают до 4–5 значащих цифр шаг в шаг; на Intel Iris Xe GPU даёт ~105 MLUPS против ~18 у процессора. Топология задачи строится один раз в cpu.rs и загружается в буферы, дублируется только физика — в WGSL. Анимация привязана к физическому времени потока, а не к скорости счёта: задержка кадра берётся из δt = u_lat·δx/u_phys. Дробная задержка раскладывается по целым сотым долям секунды накопителем (3,3,4,3,3,4,…), поэтому накопленное время кадров не уходит от физического; режим --gif-every auto подбирает шаг под реальное время при заданной частоте. Параметризовано: скорость и направление потока, число Рейнольдса, размер домена и размер ячейки в метрах, коэффициент и границы вложенного патча измельчения, семь форм тела (цилиндр, квадрат, ромб, эллипс, профиль NACA, треугольник, пластина) с углом атаки, время и разгон, оператор столкновения, режим выхода и губка, бэкенд, вся анимация и три уровня подробности отчёта. Известное расхождение с питоновским решателем на канальном случае (Cd выше на 9%, St ниже на 12% при совпадающих ⟨ρ⟩, ⟨Cm⟩ и rms Cl) описано в README вместе с тем, что уже исключено как причина. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,852 @@
|
||||
//! ОСНОВНОЙ ФАЙЛ: разбор параметров, сборка задачи, цикл по шагам, вывод.
|
||||
//!
|
||||
//! Здесь же живёт контракт между оркестратором и бэкендами: `Spec` (что считать),
|
||||
//! `StepRec` (что бэкенд отдаёт за шаг) и `FieldKind` (что рисовать). Бэкенды (cpu.rs,
|
||||
//! gpu.rs) реализуют один и тот же интерфейс и взаимозаменяемы флагом `--backend`.
|
||||
//!
|
||||
//! Раскладка задачи — канал с обтекаемым телом: вход слева (Zou–He по скорости), выход
|
||||
//! справа (Zou–He по давлению), сверху и снизу зеркальные (free-slip) стенки, тело —
|
||||
//! no-slip через SDF + интерполированный отскок Bouzidi.
|
||||
|
||||
mod cpu;
|
||||
mod gif;
|
||||
mod math;
|
||||
#[cfg(feature = "gpu")]
|
||||
mod gpu;
|
||||
|
||||
use std::io::Write as _;
|
||||
use std::time::Instant;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use math::{Body, ShapeKind, Units, R};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Контракт оркестратор ↔ бэкенд
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Collision {
|
||||
/// Энтропийный KBC-D (он же N1) — основной оператор модели.
|
||||
Kbc,
|
||||
/// Классический LBGK — для сравнительных прогонов; это ровно KBC при γ ≡ 2.
|
||||
Bgk,
|
||||
}
|
||||
|
||||
/// Что рисовать в анимации.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum FieldKind {
|
||||
Speed,
|
||||
Vorticity,
|
||||
Density,
|
||||
Gamma,
|
||||
}
|
||||
|
||||
impl FieldKind {
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
FieldKind::Speed => "SPEED",
|
||||
FieldKind::Vorticity => "VORTICITY",
|
||||
FieldKind::Density => "DENSITY",
|
||||
FieldKind::Gamma => "GAMMA",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Запись за один шаг — сырьё для всего отчёта.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct StepRec {
|
||||
pub step: u64,
|
||||
pub fx: R,
|
||||
pub fy: R,
|
||||
pub tz: R,
|
||||
pub uy_probe: R,
|
||||
pub rho_mean: R,
|
||||
pub max_u: R,
|
||||
pub gamma_mean: R,
|
||||
pub gamma_min: R,
|
||||
pub gamma_max: R,
|
||||
pub degenerate_frac: R,
|
||||
pub xi_negative_frac: R,
|
||||
}
|
||||
|
||||
/// Полностью разрешённая постановка задачи в решёточных единицах.
|
||||
#[derive(Clone)]
|
||||
pub struct Spec {
|
||||
pub nx: usize,
|
||||
pub ny: usize,
|
||||
pub body: Body,
|
||||
pub refine: usize,
|
||||
/// Границы патча измельчения в координатах L0: (ax, bx, ay, by).
|
||||
pub patch: Option<(usize, usize, usize, usize)>,
|
||||
pub units: Units,
|
||||
pub beta0: R,
|
||||
pub steps: u64,
|
||||
pub ramp: u64,
|
||||
/// Направление набегающего потока, рад (0 — вдоль +x).
|
||||
pub flow_angle: R,
|
||||
pub pert_amp: R,
|
||||
pub pert_dur: u64,
|
||||
pub outlet_extrapolate: bool,
|
||||
pub sponge_len: usize,
|
||||
pub sponge_mult: R,
|
||||
pub collision: Collision,
|
||||
/// Узел зонда следа в координатах L0.
|
||||
pub probe: (usize, usize),
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Параметры запуска
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(
|
||||
name = "kbc2d",
|
||||
about = "Двумерный решатель LBM D2Q9 с энтропийным столкновением KBC (модель D/N1)",
|
||||
long_about = "Обтекание тела в канале: KBC-D по Bösch/Chikatamarla/Karlin, SDF+Bouzidi на \
|
||||
теле, Zou–He вход/выход, вложенный патч измельчения. Анимация синхронизируется с ФИЗИЧЕСКИМ \
|
||||
временем потока, а не со скоростью счёта."
|
||||
)]
|
||||
struct Cli {
|
||||
// ── физика потока ──
|
||||
/// Скорость набегающего потока, м/с
|
||||
#[arg(long, default_value_t = 30.0, help_heading = "Физика")]
|
||||
u_phys: R,
|
||||
/// Размер ячейки (плотность сетки), м
|
||||
#[arg(long, default_value_t = 0.1, help_heading = "Физика")]
|
||||
dx: R,
|
||||
/// Решёточная скорость потока (ячеек за шаг). Задаёт δt = u_lat·dx/u_phys.
|
||||
/// 0.05 ⇒ Ma≈0.09. Значение 1.0 воспроизводит арифметику «30 м/с ÷ 0.1 м = 300 шагов/с»,
|
||||
/// но это Ma≈1.73 — за пределами применимости LBM.
|
||||
#[arg(long, default_value_t = 0.05, help_heading = "Физика")]
|
||||
u_lat: R,
|
||||
/// Число Рейнольдса по характерному размеру тела
|
||||
#[arg(long, default_value_t = 150.0, help_heading = "Физика")]
|
||||
re: R,
|
||||
/// Направление потока, градусы (0 — вдоль канала)
|
||||
#[arg(long, default_value_t = 0.0, help_heading = "Физика")]
|
||||
flow_angle: R,
|
||||
|
||||
// ── сетка ──
|
||||
/// Размер домена по x, ячеек
|
||||
#[arg(long, default_value_t = 480, help_heading = "Сетка")]
|
||||
nx: usize,
|
||||
/// Размер домена по y, ячеек
|
||||
#[arg(long, default_value_t = 240, help_heading = "Сетка")]
|
||||
ny: usize,
|
||||
/// Коэффициент измельчения вложенного патча (1 — патча нет)
|
||||
#[arg(long, default_value_t = 2, help_heading = "Сетка")]
|
||||
refine: usize,
|
||||
/// Границы патча в координатах L0: ax,bx,ay,by (по умолчанию строятся вокруг тела)
|
||||
#[arg(long, help_heading = "Сетка")]
|
||||
patch: Option<String>,
|
||||
|
||||
// ── тело ──
|
||||
/// Форма обтекаемого тела
|
||||
#[arg(long, default_value = "cylinder",
|
||||
value_parser = ShapeKind::ALL, help_heading = "Тело")]
|
||||
shape: String,
|
||||
/// Характерный размер тела (диаметр/сторона/хорда), ячеек
|
||||
#[arg(long, default_value_t = 24.0, help_heading = "Тело")]
|
||||
size: R,
|
||||
/// Относительная толщина для ellipse / naca / plate
|
||||
#[arg(long, default_value_t = 0.3, help_heading = "Тело")]
|
||||
thickness: R,
|
||||
/// Угол атаки тела, градусы
|
||||
#[arg(long, default_value_t = 0.0, help_heading = "Тело")]
|
||||
body_angle: R,
|
||||
/// Положение центра тела по x, ячеек (по умолчанию nx/4)
|
||||
#[arg(long, help_heading = "Тело")]
|
||||
body_x: Option<R>,
|
||||
/// Положение центра тела по y, ячеек (по умолчанию ny/2)
|
||||
#[arg(long, help_heading = "Тело")]
|
||||
body_y: Option<R>,
|
||||
|
||||
// ── время ──
|
||||
/// Число шагов симуляции
|
||||
#[arg(long, default_value_t = 20000, help_heading = "Время")]
|
||||
steps: u64,
|
||||
/// Длина smoothstep-разгона входа, шагов
|
||||
#[arg(long, default_value_t = 1000, help_heading = "Время")]
|
||||
ramp: u64,
|
||||
/// Амплитуда стартового поперечного импульса (доля от U); 0 — выключить
|
||||
#[arg(long, default_value_t = 0.1, help_heading = "Время")]
|
||||
pert_amp: R,
|
||||
/// Длительность стартового импульса, шагов
|
||||
#[arg(long, default_value_t = 300, help_heading = "Время")]
|
||||
pert_dur: u64,
|
||||
|
||||
// ── численная схема ──
|
||||
/// Оператор столкновения
|
||||
#[arg(long, default_value = "kbc", value_parser = ["kbc", "bgk"], help_heading = "Схема")]
|
||||
collision: 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,
|
||||
/// Во сколько раз губка поднимает вязкость
|
||||
#[arg(long, default_value_t = 30.0, help_heading = "Схема")]
|
||||
sponge_mult: R,
|
||||
/// Бэкенд вычислений
|
||||
#[arg(long, default_value = "cpu", value_parser = ["cpu", "gpu"], help_heading = "Схема")]
|
||||
backend: String,
|
||||
/// Число потоков CPU (0 — по числу ядер)
|
||||
#[arg(long, default_value_t = 0, help_heading = "Схема")]
|
||||
threads: usize,
|
||||
|
||||
// ── анимация ──
|
||||
/// Файл гифки (не указан — анимация не пишется)
|
||||
#[arg(long, help_heading = "Анимация")]
|
||||
gif: Option<String>,
|
||||
/// Шагов на кадр: число либо auto (подобрать под --gif-fps в реальном времени)
|
||||
#[arg(long, default_value = "auto", help_heading = "Анимация")]
|
||||
gif_every: String,
|
||||
/// Целевая частота кадров для режима auto
|
||||
#[arg(long, default_value_t = 30.0, help_heading = "Анимация")]
|
||||
gif_fps: R,
|
||||
/// Скорость воспроизведения: 1 — реальное время, 0.1 — замедление в 10 раз
|
||||
#[arg(long, default_value_t = 1.0, help_heading = "Анимация")]
|
||||
gif_speed: R,
|
||||
/// Что рисовать
|
||||
#[arg(long, default_value = "vorticity",
|
||||
value_parser = ["speed", "vorticity", "density", "gamma"], help_heading = "Анимация")]
|
||||
gif_field: String,
|
||||
/// Палитра
|
||||
#[arg(long, default_value = "coolwarm",
|
||||
value_parser = gif::ColorMap::ALL, help_heading = "Анимация")]
|
||||
gif_cmap: String,
|
||||
/// Целочисленное увеличение картинки
|
||||
#[arg(long, default_value_t = 2, help_heading = "Анимация")]
|
||||
gif_scale: usize,
|
||||
/// Диапазон нормировки цвета: lo,hi (по умолчанию — по скорости потока)
|
||||
#[arg(long, help_heading = "Анимация")]
|
||||
gif_range: Option<String>,
|
||||
/// Не рисовать служебную надпись на кадре
|
||||
#[arg(long, help_heading = "Анимация")]
|
||||
no_hud: bool,
|
||||
|
||||
// ── вывод ──
|
||||
/// Период строк живого отчёта, шагов (0 — выключить)
|
||||
#[arg(long, default_value_t = 1000, help_heading = "Вывод")]
|
||||
report_every: u64,
|
||||
/// Подробность: quiet — только итог, normal — живой отчёт, full — плюс разбор KBC
|
||||
#[arg(long, default_value = "normal", value_parser = ["quiet", "normal", "full"],
|
||||
help_heading = "Вывод")]
|
||||
verbose: String,
|
||||
/// Число окон в отчёте о сходимости
|
||||
#[arg(long, default_value_t = 10, help_heading = "Вывод")]
|
||||
windows: usize,
|
||||
/// Выгрузить временные ряды в CSV
|
||||
#[arg(long, help_heading = "Вывод")]
|
||||
csv: Option<String>,
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Сборка постановки
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn parse_pair(s: &str, what: &str) -> Result<(R, R), String> {
|
||||
let p: Vec<&str> = s.split(',').collect();
|
||||
if p.len() != 2 {
|
||||
return Err(format!("{what}: ожидалось два числа через запятую, получено «{s}»"));
|
||||
}
|
||||
let a = p[0].trim().parse::<R>().map_err(|e| format!("{what}: {e}"))?;
|
||||
let b = p[1].trim().parse::<R>().map_err(|e| format!("{what}: {e}"))?;
|
||||
Ok((a, b))
|
||||
}
|
||||
|
||||
fn build_spec(cli: &Cli) -> Result<Spec, String> {
|
||||
if cli.nx < 16 || cli.ny < 16 {
|
||||
return Err("сетка меньше 16×16 не имеет смысла".into());
|
||||
}
|
||||
if cli.u_lat <= 0.0 || cli.u_lat >= 1.0 {
|
||||
return Err(format!("--u-lat обязан лежать в (0,1), получено {}", cli.u_lat));
|
||||
}
|
||||
if cli.u_phys <= 0.0 || cli.dx <= 0.0 || cli.re <= 0.0 {
|
||||
return Err("--u-phys, --dx и --re обязаны быть положительными".into());
|
||||
}
|
||||
if cli.refine == 0 {
|
||||
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 units = Units::new(cli.dx, cli.u_phys, cli.u_lat, cli.re, cli.size);
|
||||
let beta0 = math::beta_of_nu(units.nu_lat);
|
||||
let tau0 = 1.0 / (2.0 * beta0);
|
||||
if tau0 <= 0.5 {
|
||||
return Err(format!(
|
||||
"τ = {tau0:.4} ≤ ½: вязкость неположительна. Подними --u-lat или опусти --re"
|
||||
));
|
||||
}
|
||||
|
||||
// патч измельчения: по умолчанию охватывает тело и ближний след
|
||||
let patch = if cli.refine > 1 {
|
||||
let d = cli.size;
|
||||
let (ax, bx, ay, by) = match &cli.patch {
|
||||
Some(s) => {
|
||||
let p: Vec<&str> = s.split(',').collect();
|
||||
if p.len() != 4 {
|
||||
return Err("--patch: ожидалось ax,bx,ay,by".into());
|
||||
}
|
||||
let v: Result<Vec<usize>, _> =
|
||||
p.iter().map(|t| t.trim().parse::<usize>()).collect();
|
||||
let v = v.map_err(|e| format!("--patch: {e}"))?;
|
||||
(v[0], v[1], v[2], v[3])
|
||||
}
|
||||
None => (
|
||||
(cx - 1.5 * d).round().max(2.0) as usize,
|
||||
(cx + 6.25 * d).round().min(cli.nx as R - 3.0) as usize,
|
||||
(cy - 2.0 * d).round().max(1.0) as usize,
|
||||
(cy + 2.0 * d).round().min(cli.ny as R - 2.0) as usize,
|
||||
),
|
||||
};
|
||||
// патч обязан лежать строго внутри домена и НЕ накрывать зеркальные ряды стенок:
|
||||
// на них ГУ работает по всему ряду, а рамка патча их бы перезаписала
|
||||
if !(1 <= ay && ay < by && by <= cli.ny - 2) {
|
||||
return Err(format!(
|
||||
"патч по y [{ay},{by}] обязан лежать внутри (0,{}) и не трогать ряды стенок; \
|
||||
при теле {d} ячеек минимальное ny ≈ {}",
|
||||
cli.ny - 1,
|
||||
(4.0 * d) as usize + 6
|
||||
));
|
||||
}
|
||||
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
|
||||
};
|
||||
|
||||
let probe = (
|
||||
((cx + 3.0 * cli.size).round() as usize).min(cli.nx - 2),
|
||||
(cy.round() as usize).min(cli.ny - 2),
|
||||
);
|
||||
|
||||
Ok(Spec {
|
||||
nx: cli.nx,
|
||||
ny: cli.ny,
|
||||
body,
|
||||
refine: cli.refine,
|
||||
patch,
|
||||
units,
|
||||
beta0,
|
||||
steps: cli.steps,
|
||||
ramp: cli.ramp.max(1),
|
||||
flow_angle: cli.flow_angle * std::f64::consts::PI / 180.0,
|
||||
pert_amp: cli.pert_amp,
|
||||
pert_dur: cli.pert_dur,
|
||||
outlet_extrapolate: cli.outlet == "extrapolate",
|
||||
sponge_len: cli.sponge_len,
|
||||
sponge_mult: cli.sponge_mult,
|
||||
collision: if cli.collision == "bgk" { Collision::Bgk } else { Collision::Kbc },
|
||||
probe,
|
||||
})
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Бэкенд
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
enum Backend {
|
||||
Cpu(Box<cpu::Sim>),
|
||||
#[cfg(feature = "gpu")]
|
||||
Gpu(Box<gpu::Sim>),
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
fn step(&mut self) -> StepRec {
|
||||
match self {
|
||||
Backend::Cpu(s) => s.step(),
|
||||
#[cfg(feature = "gpu")]
|
||||
Backend::Gpu(s) => s.step(),
|
||||
}
|
||||
}
|
||||
fn sample_field(&mut self, k: FieldKind) -> (Vec<R>, Vec<bool>) {
|
||||
match self {
|
||||
Backend::Cpu(s) => {
|
||||
let (v, m) = s.sample_field(k);
|
||||
(v, m.to_vec())
|
||||
}
|
||||
#[cfg(feature = "gpu")]
|
||||
Backend::Gpu(s) => s.sample_field(k),
|
||||
}
|
||||
}
|
||||
fn is_finite(&mut self) -> bool {
|
||||
match self {
|
||||
Backend::Cpu(s) => s.is_finite(),
|
||||
#[cfg(feature = "gpu")]
|
||||
Backend::Gpu(s) => s.is_finite(),
|
||||
}
|
||||
}
|
||||
fn force_ref_size(&self) -> R {
|
||||
match self {
|
||||
Backend::Cpu(s) => s.force_ref_size(),
|
||||
#[cfg(feature = "gpu")]
|
||||
Backend::Gpu(s) => s.force_ref_size(),
|
||||
}
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Backend::Cpu(_) => "CPU (rayon, f64)",
|
||||
#[cfg(feature = "gpu")]
|
||||
Backend::Gpu(s) => s.name(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// main
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
if let Err(e) = run(cli) {
|
||||
eprintln!("\nОШИБКА: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn run(cli: Cli) -> Result<(), String> {
|
||||
if cli.threads > 0 {
|
||||
rayon::ThreadPoolBuilder::new()
|
||||
.num_threads(cli.threads)
|
||||
.build_global()
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
let spec = build_spec(&cli)?;
|
||||
let quiet = cli.verbose == "quiet";
|
||||
let full = cli.verbose == "full";
|
||||
|
||||
// ── анимация: план тайминга считается ДО прогона и печатается в шапке ──
|
||||
let stride = if cli.gif_every == "auto" {
|
||||
None
|
||||
} else {
|
||||
Some(cli.gif_every.parse::<u64>().map_err(|e| format!("--gif-every: {e}"))?)
|
||||
};
|
||||
let plan = gif::GifPlan::new(&spec.units, stride, cli.gif_fps, cli.gif_speed);
|
||||
let field_kind = match cli.gif_field.as_str() {
|
||||
"speed" => FieldKind::Speed,
|
||||
"density" => FieldKind::Density,
|
||||
"gamma" => FieldKind::Gamma,
|
||||
_ => FieldKind::Vorticity,
|
||||
};
|
||||
|
||||
if !quiet {
|
||||
print_header(&spec, &cli, &plan, field_kind);
|
||||
}
|
||||
|
||||
// ── бэкенд ──
|
||||
let mut back = match cli.backend.as_str() {
|
||||
"gpu" => {
|
||||
#[cfg(feature = "gpu")]
|
||||
{
|
||||
Backend::Gpu(Box::new(gpu::Sim::new(spec.clone())?))
|
||||
}
|
||||
#[cfg(not(feature = "gpu"))]
|
||||
{
|
||||
return Err("сборка без поддержки GPU (--no-default-features)".into());
|
||||
}
|
||||
}
|
||||
_ => Backend::Cpu(Box::new(cpu::Sim::new(spec.clone()))),
|
||||
};
|
||||
if !quiet {
|
||||
println!("бэкенд: {}", back.name());
|
||||
}
|
||||
|
||||
// ── писатель гифки ──
|
||||
let mut writer = match &cli.gif {
|
||||
Some(path) => {
|
||||
let cmap = gif::ColorMap::from_str(&cli.gif_cmap).ok_or("неизвестная палитра")?;
|
||||
let range = match &cli.gif_range {
|
||||
Some(s) => {
|
||||
let (lo, hi) = parse_pair(s, "--gif-range")?;
|
||||
gif::Range { lo, hi }
|
||||
}
|
||||
None => default_range(field_kind, spec.units.u_lat, spec.units.d_lat),
|
||||
};
|
||||
let hud = gif::Hud {
|
||||
u_phys: spec.units.u_phys,
|
||||
field: field_kind.label(),
|
||||
show: !cli.no_hud,
|
||||
};
|
||||
Some(
|
||||
gif::GifWriter::create(
|
||||
path,
|
||||
spec.nx,
|
||||
spec.ny,
|
||||
cli.gif_scale,
|
||||
cmap,
|
||||
range,
|
||||
plan,
|
||||
hud,
|
||||
spec.patch,
|
||||
)
|
||||
.map_err(|e| format!("не удалось создать {path}: {e}"))?,
|
||||
)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
// ── цикл ──
|
||||
let d_ref = back.force_ref_size();
|
||||
let mut recs: Vec<StepRec> = Vec::with_capacity(spec.steps as usize + 1);
|
||||
let t_start = Instant::now();
|
||||
let mut blew_up = false;
|
||||
let nodes_per_step = (spec.nx * spec.ny
|
||||
+ spec
|
||||
.patch
|
||||
.map(|(ax, bx, ay, by)| {
|
||||
spec.refine * (spec.refine * (bx - ax) + 1) * (spec.refine * (by - ay) + 1)
|
||||
})
|
||||
.unwrap_or(0)) as f64;
|
||||
|
||||
for t in 0..=spec.steps {
|
||||
let rec = back.step();
|
||||
recs.push(rec);
|
||||
|
||||
if let Some(w) = writer.as_mut() {
|
||||
if t % plan.stride == 0 {
|
||||
let (field, solid) = back.sample_field(field_kind);
|
||||
let (cd, _, _) =
|
||||
math::coefficients(rec.fx, rec.fy, rec.tz, spec.units.u_lat, d_ref);
|
||||
w.push(&field, &solid, spec.units.time_of_step(t), Some(cd))
|
||||
.map_err(|e| format!("запись кадра: {e}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
if cli.report_every > 0 && !quiet && t % cli.report_every == 0 {
|
||||
live_line(&spec, &rec, d_ref, t_start.elapsed().as_secs_f64(), nodes_per_step, full);
|
||||
}
|
||||
|
||||
// развал счёта: проверяем редко, проверка стоит полного прохода по полю
|
||||
if t % 500 == 0 && !back.is_finite() {
|
||||
eprintln!("\nСЧЁТ РАЗВАЛИЛСЯ на шаге {t}: в поле появились NaN/inf.");
|
||||
blew_up = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let wall = t_start.elapsed().as_secs_f64();
|
||||
|
||||
if let Some(w) = writer {
|
||||
let frames = w.frames;
|
||||
w.finish().map_err(|e| format!("закрытие гифки: {e}"))?;
|
||||
if !quiet {
|
||||
println!("\nгифка: {} — кадров {frames}", cli.gif.as_ref().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(path) = &cli.csv {
|
||||
write_csv(path, &recs, &spec, d_ref).map_err(|e| format!("CSV: {e}"))?;
|
||||
if !quiet {
|
||||
println!("ряды: {path}");
|
||||
}
|
||||
}
|
||||
|
||||
final_report(&spec, &cli, &recs, d_ref, wall, nodes_per_step, blew_up, &plan, full);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn default_range(k: FieldKind, u_lat: R, d_lat: R) -> gif::Range {
|
||||
match k {
|
||||
FieldKind::Speed => gif::Range { lo: 0.0, hi: 1.7 * u_lat },
|
||||
FieldKind::Vorticity => {
|
||||
let w = 5.0 * u_lat / d_lat;
|
||||
gif::Range { lo: -w, hi: w }
|
||||
}
|
||||
FieldKind::Density => gif::Range { lo: 0.985, hi: 1.015 },
|
||||
FieldKind::Gamma => gif::Range { lo: 0.0, hi: 4.0 },
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Вывод
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
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;
|
||||
|
||||
println!("╔══════════════════════════════════════════════════════════════════════════╗");
|
||||
println!("║ KBC-2D — обтекание тела в канале, D2Q9 + энтропийное столкновение KBC-D ║");
|
||||
println!("╚══════════════════════════════════════════════════════════════════════════╝");
|
||||
|
||||
println!("\n── Физика ──────────────────────────────────────────────────────────────────");
|
||||
println!(" скорость потока {:>12.4} м/с направление {:>6.1}°", u.u_phys, cli.flow_angle);
|
||||
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);
|
||||
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);
|
||||
|
||||
println!("\n── Время ───────────────────────────────────────────────────────────────────");
|
||||
println!(" шаг по времени {:>12.6e} с {:.1} шагов на секунду физ. времени",
|
||||
u.dt, u.steps_per_second());
|
||||
println!(" всего шагов {:>12} = {:.4} с физ. времени = {:.1} конв. времён D/U",
|
||||
spec.steps, u.time_of_step(spec.steps), u.convective_times(spec.steps));
|
||||
println!(" разгон / возмущение {:>12} {} шагов импульса амплитудой {:.2}·U",
|
||||
spec.ramp, spec.pert_dur, spec.pert_amp);
|
||||
|
||||
println!("\n── Сетка ───────────────────────────────────────────────────────────────────");
|
||||
println!(" L0 {:>12} {} узлов", format!("{}×{}", spec.nx, spec.ny),
|
||||
spec.nx * spec.ny);
|
||||
match spec.patch {
|
||||
Some((ax, bx, ay, by)) => {
|
||||
let (nfx, nfy) = (spec.refine * (bx - ax) + 1, spec.refine * (by - ay) + 1);
|
||||
let tau1 = spec.refine as R * (tau - 0.5) + 0.5;
|
||||
println!(" L1 (×{}) {:>12} область x∈[{ax},{bx}] y∈[{ay},{by}], τ₁={:.4}",
|
||||
spec.refine, format!("{nfx}×{nfy}"), tau1);
|
||||
println!(" {:>12} {} подшагов на шаг L0", "", spec.refine);
|
||||
}
|
||||
None => println!(" L1 {:>12} (измельчение выключено)", "нет"),
|
||||
}
|
||||
println!(" оператор {:>12}", if spec.collision == Collision::Kbc { "KBC-D" } else { "LBGK" });
|
||||
println!(" выход по u_y {:>12} губка {} столбцов ×{:.0}",
|
||||
if spec.outlet_extrapolate { "extrapolate" } else { "zero" },
|
||||
spec.sponge_len, spec.sponge_mult);
|
||||
|
||||
// Пара «вход по скорости / выход по давлению» — недодемпфированный акустический резонатор:
|
||||
// затухание продольной моды ~ ν(π/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── Анимация (синхронизация с физическим временем) ───────────────────────────");
|
||||
println!(" поле {:>12} палитра {}", field.label().to_lowercase(), cli.gif_cmap);
|
||||
println!(" шаг кадра {:>12} {}", plan.stride,
|
||||
if plan.auto { "подобран автоматически" } else { "задан явно" });
|
||||
println!(" между кадрами {:>12.6e} с физ. времени", plan.frame_dt);
|
||||
println!(" задержка кадра {:>12.4} сотых {:.2} кадр/с{}", plan.exact_delay_cs,
|
||||
plan.fps,
|
||||
if plan.dithered { " (задержки чередуются — точное время без ухода)" } else { "" });
|
||||
println!(" воспроизведение {:>12.4}× от реального времени (запрошено {:.4}×)",
|
||||
plan.actual_playback, plan.requested_playback);
|
||||
match plan.advice(&spec.units) {
|
||||
Some(a) => println!(" ⚠ {a}"),
|
||||
None if plan.is_synced() => {
|
||||
println!(" ✓ синхронно с физическим временем (расхождение {:.4}%)",
|
||||
(plan.sync_error() - 1.0).abs() * 100.0)
|
||||
}
|
||||
None => println!(" ⚠ расхождение с реальным временем {:.3}×", plan.sync_error()),
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
fn live_line(spec: &Spec, r: &StepRec, d_ref: R, wall: f64, nodes: f64, full: bool) {
|
||||
let (cd, cl, _) = math::coefficients(r.fx, r.fy, r.tz, spec.units.u_lat, d_ref);
|
||||
let done = (r.step + 1) as f64;
|
||||
let sps = done / wall.max(1e-9);
|
||||
let eta = (spec.steps + 1 - r.step.min(spec.steps)) as f64 / sps.max(1e-9);
|
||||
let mlups = sps * nodes / 1e6;
|
||||
// во сколько раз счёт быстрее реального времени
|
||||
let rt = sps / spec.units.steps_per_second();
|
||||
let head = format!(
|
||||
"шаг {:>8} t={:>9.4}с ⟨ρ⟩={:.5} max|u|={:.4} Cd={:>7.3} Cl={:>7.3}",
|
||||
r.step,
|
||||
spec.units.time_of_step(r.step),
|
||||
r.rho_mean,
|
||||
r.max_u,
|
||||
cd,
|
||||
cl
|
||||
);
|
||||
if full {
|
||||
println!(
|
||||
"{head}\n ⟨γ⟩={:.3} [{:.2}…{:.2}] вырожд={:.2}% ξ<0={:.2}% \
|
||||
{:.0} шаг/с ({:.1} MLUPS, {:.2}× реального) ETA {:.0}с",
|
||||
r.gamma_mean,
|
||||
r.gamma_min,
|
||||
r.gamma_max,
|
||||
r.degenerate_frac * 100.0,
|
||||
r.xi_negative_frac * 100.0,
|
||||
sps,
|
||||
mlups,
|
||||
rt,
|
||||
eta
|
||||
);
|
||||
} else {
|
||||
println!("{head} {:.0} шаг/с ETA {:.0}с", sps, eta);
|
||||
}
|
||||
std::io::stdout().flush().ok();
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn final_report(
|
||||
spec: &Spec,
|
||||
cli: &Cli,
|
||||
recs: &[StepRec],
|
||||
d_ref: R,
|
||||
wall: f64,
|
||||
nodes: f64,
|
||||
blew_up: bool,
|
||||
plan: &gif::GifPlan,
|
||||
full: bool,
|
||||
) {
|
||||
let u = &spec.units;
|
||||
let n = recs.len();
|
||||
if n < 8 {
|
||||
println!("слишком мало шагов для отчёта");
|
||||
return;
|
||||
}
|
||||
let h = n / 2; // установившийся режим — вторая половина ряда
|
||||
|
||||
let cd: Vec<R> = recs.iter().map(|r| 2.0 * r.fx / (u.u_lat * u.u_lat * d_ref)).collect();
|
||||
let cl: Vec<R> = recs.iter().map(|r| 2.0 * r.fy / (u.u_lat * u.u_lat * d_ref)).collect();
|
||||
let cm: Vec<R> =
|
||||
recs.iter().map(|r| 2.0 * r.tz / (u.u_lat * u.u_lat * d_ref * d_ref)).collect();
|
||||
let uy: Vec<R> = recs.iter().map(|r| r.uy_probe).collect();
|
||||
let rho: Vec<R> = recs.iter().map(|r| r.rho_mean).collect();
|
||||
|
||||
let (cd_m, _) = math::mean_std(&cd[h..]);
|
||||
let (_, cl_rms) = math::mean_std(&cl[h..]);
|
||||
let (cm_m, _) = math::mean_std(&cm[h..]);
|
||||
let (_, uy_rms) = math::mean_std(&uy[h..]);
|
||||
let (st, _, _) = math::strouhal(&uy, u.d_lat, u.u_lat, 8192);
|
||||
|
||||
println!("\n╔══════════════════════════════════════════════════════════════════════════╗");
|
||||
println!("║ ИТОГОВЫЙ ОТЧЁТ ║");
|
||||
println!("╚══════════════════════════════════════════════════════════════════════════╝");
|
||||
if blew_up {
|
||||
println!("⚠ ПРОГОН ОБОРВАН: счёт развалился. Числа ниже относятся к тому, что успело сойтись.");
|
||||
}
|
||||
|
||||
// ── установившийся режим ──
|
||||
let beta = spec.body.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);
|
||||
// поправка на стеснение канала: St кинематическое ⇒ ×(1−β); Cd и Cl ~ скорость² ⇒ ×(1−β)²
|
||||
println!(
|
||||
" {:<22}{:>10.4}{:>10.4}{:>10.4}{:>10}",
|
||||
"с поправ. на блокир.",
|
||||
st * (1.0 - beta),
|
||||
cd_m * (1.0 - beta).powi(2),
|
||||
cl_rms * (1.0 - beta).powi(2),
|
||||
"—"
|
||||
);
|
||||
if spec.body.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 nw = cli.windows.max(2).min(n / 2);
|
||||
let w = n / nw;
|
||||
println!("\n── Сходимость по времени (окна по {w} шагов) ────────────────────────────────");
|
||||
println!(" {:>4}{:>16}{:>10}{:>10}{:>10}{:>10}{:>10}", "окно", "шаги", "⟨ρ⟩", "⟨Cd⟩", "⟨Cd⟩/ρ", "rms Cl", "rms u_y");
|
||||
let mut rows = Vec::new();
|
||||
for k in 0..nw {
|
||||
let s = k * w;
|
||||
let e = if k + 1 == nw { n } else { (k + 1) * w };
|
||||
let (rm, _) = math::mean_std(&rho[s..e]);
|
||||
let (cdm, _) = math::mean_std(&cd[s..e]);
|
||||
let (_, clr) = math::mean_std(&cl[s..e]);
|
||||
let (_, uyr) = math::mean_std(&uy[s..e]);
|
||||
let cdn = if rm != 0.0 { cdm / rm } else { R::NAN };
|
||||
rows.push((rm, cdm, cdn, clr, uyr));
|
||||
println!(" {:>4}{:>16}{:>10.5}{:>10.4}{:>10.4}{:>10.4}{:>10.5}",
|
||||
k + 1, format!("{s}–{e}"), rm, cdm, cdn, clr, uyr);
|
||||
}
|
||||
let (rm1, _, cdn1, clr1, _) = rows[nw - 1];
|
||||
let (rm0, _, _, clr0, _) = rows[nw - 2];
|
||||
print!(" вердикт: ");
|
||||
if (rm1 - 1.0).abs() > 0.02 {
|
||||
// «стабильно, но не там»: система села на смещённую ветвь (акустическая накачка массы),
|
||||
// на ней Cd/Cl несопоставимы с литературой
|
||||
print!("⟨ρ⟩={rm1:.4} — РЕЖИМ СМЕЩЁН, прогон невалиден для сравнения; ");
|
||||
} else if (rm1 - rm0).abs() > 1e-3 {
|
||||
print!("⟨ρ⟩={rm1:.4}, Δ за окно {:+.5} — ДРЕЙФ МАССЫ (правь ГУ выхода); ", rm1 - rm0);
|
||||
} else {
|
||||
print!("масса стабильна (⟨ρ⟩={rm1:.4}, Δ {:+.5}); ", rm1 - rm0);
|
||||
}
|
||||
if clr1 - clr0 > 1e-3 {
|
||||
println!("rms Cl ещё растёт ({:+.4}) — не насыщено; дрейф-устойчивый Cd = {cdn1:.3}", clr1 - clr0);
|
||||
} else {
|
||||
println!("rms Cl насыщен ({:+.4}); дрейф-устойчивый Cd = {cdn1:.3}", clr1 - clr0);
|
||||
}
|
||||
|
||||
// ── диагностика KBC ──
|
||||
if spec.collision == Collision::Kbc {
|
||||
let (g_m, g_s) = math::mean_std(&recs[h..].iter().map(|r| r.gamma_mean).collect::<Vec<_>>());
|
||||
let gmin = recs[h..].iter().map(|r| r.gamma_min).fold(R::INFINITY, R::min);
|
||||
let gmax = recs[h..].iter().map(|r| r.gamma_max).fold(R::NEG_INFINITY, R::max);
|
||||
let (deg, _) = math::mean_std(&recs[h..].iter().map(|r| r.degenerate_frac).collect::<Vec<_>>());
|
||||
let (xin, _) = math::mean_std(&recs[h..].iter().map(|r| r.xi_negative_frac).collect::<Vec<_>>());
|
||||
println!("\n── Энтропийный стабилизатор γ (формула (17) статьи) ─────────────────────────");
|
||||
println!(" ⟨γ⟩ = {g_m:.4} ± {g_s:.4} размах по узлам [{gmin:.3} … {gmax:.3}]");
|
||||
println!(" вырожденных узлов (⟨Δh|Δh⟩→0, γ подменён на 2 = локальный LBGK): {:.3}%", deg * 100.0);
|
||||
println!(" узлов с отрицательной объёмной вязкостью ξ = c_s²(1/(γβ) − ½): {:.2}%", xin * 100.0);
|
||||
if full {
|
||||
println!(" Пояснение. γ ≡ 2 — это в точности LBGK, поэтому доля вырожденных узлов есть");
|
||||
println!(" доля решётки, где KBC ничего не даёт; при относительном пороге она обязана быть");
|
||||
println!(" ~0 (абсолютный порог загонял её в 77–99% и молча превращал схему в LBGK).");
|
||||
println!(" ξ по формуле (57) у моделей B и D зависит от γ, то есть от точки и времени;");
|
||||
println!(" ξ < 0 означает локальное антизатухание акустики. У моделей A и C ξ = ν всегда.");
|
||||
}
|
||||
}
|
||||
|
||||
// ── производительность и синхронность ──
|
||||
let sps = n as f64 / wall.max(1e-9);
|
||||
println!("\n── Производительность ──────────────────────────────────────────────────────");
|
||||
println!(" время счёта {wall:.1} с {sps:.0} шаг/с {:.1} MLUPS", sps * nodes / 1e6);
|
||||
println!(" физического времени просчитано {:.4} с ⇒ счёт идёт в {:.3}× от реального времени",
|
||||
u.time_of_step(n as u64 - 1), sps / u.steps_per_second());
|
||||
if cli.gif.is_some() {
|
||||
println!(" гифка воспроизводится в {:.4}× от реального времени — это НЕ зависит от скорости",
|
||||
plan.actual_playback);
|
||||
println!(" счёта: задержка кадра берётся из δt, а не из wall-clock.");
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
fn write_csv(path: &str, recs: &[StepRec], spec: &Spec, d_ref: R) -> std::io::Result<()> {
|
||||
let mut f = std::io::BufWriter::new(std::fs::File::create(path)?);
|
||||
writeln!(f, "step,t_phys_s,cd,cl,cm,uy_probe,rho_mean,max_u,gamma_mean,gamma_min,gamma_max,degenerate_frac,xi_negative_frac")?;
|
||||
let s = spec.units.u_lat * spec.units.u_lat * d_ref;
|
||||
for r in recs {
|
||||
writeln!(
|
||||
f,
|
||||
"{},{:.9e},{:.6e},{:.6e},{:.6e},{:.6e},{:.9},{:.6e},{:.6},{:.6},{:.6},{:.6},{:.6}",
|
||||
r.step,
|
||||
spec.units.time_of_step(r.step),
|
||||
2.0 * r.fx / s,
|
||||
2.0 * r.fy / s,
|
||||
2.0 * r.tz / (s * d_ref),
|
||||
r.uy_probe,
|
||||
r.rho_mean,
|
||||
r.max_u,
|
||||
r.gamma_mean,
|
||||
r.gamma_min,
|
||||
r.gamma_max,
|
||||
r.degenerate_frac,
|
||||
r.xi_negative_frac
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user