rtx-cfd: curvilinear collocated PISO on a structured patch (overset A-P0, WIP) — PatchMesh (right-handed s,n; periodic seam with shift; face metrics), patch generators (TFI, skewed annulus, sheared/varying-skew channels), CSR + Jacobi-BiCGSTAB, the Zang–Street–Koseff incremental step with the node-based 9-point L_f, LSQ gradients, explicit and line-implicit-n predictors, adjustPhi; tests: mesh metrics (5 green), operators exact on linear fields incl. the seam (green), sparse (2 green), MMS ladder (Cartesian 16/32: 1.37–1.39x the staggered error, order 0.83; n=64 stalls at a |du/dt| floor 2e-4 — open, tolerance-scaling hypothesis), annulus/Poiseuille not yet run
CI / Distributed Training Tests (push) Canceled after 0s
Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
CI / Format Check (push) Canceled after 0s
CI / Clippy Check (push) Canceled after 0s
CI / Build (macos-latest) (push) Canceled after 0s
CI / Build (ubuntu-latest) (push) Canceled after 0s
CI / Test (macos-latest) (push) Canceled after 0s
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Build CPU-Only (Explicit) (push) Canceled after 0s
CI / Python Bindings (maturin) (macos-latest) (push) Canceled after 0s
CI / Python Bindings (maturin) (ubuntu-latest) (push) Canceled after 0s
CI / WASM Build + Size Check (push) Canceled after 0s
CI / CI Success (push) Canceled after 0s
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (push) Canceled after 0s
CI / Distributed Training Tests (push) Canceled after 0s
Performance Benchmarks / Run Benchmarks (push) Canceled after 0s
CI / Format Check (push) Canceled after 0s
CI / Clippy Check (push) Canceled after 0s
CI / Build (macos-latest) (push) Canceled after 0s
CI / Build (ubuntu-latest) (push) Canceled after 0s
CI / Test (macos-latest) (push) Canceled after 0s
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Build CPU-Only (Explicit) (push) Canceled after 0s
CI / Python Bindings (maturin) (macos-latest) (push) Canceled after 0s
CI / Python Bindings (maturin) (ubuntu-latest) (push) Canceled after 0s
CI / WASM Build + Size Check (push) Canceled after 0s
CI / CI Success (push) Canceled after 0s
Documentation / Build API Documentation (push) Canceled after 0s
Documentation / Build User Guide (push) Canceled after 0s
Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01X2GmJXeQ2njUecEKiJZ1G2
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
1347bc6772
commit
52da75a3a9
@@ -0,0 +1,333 @@
|
||||
//! Collocated finite-volume PISO on a structured curvilinear patch — the
|
||||
//! body-fitted half of the overset hybrid (`docs/overset_metal_campaign.md`
|
||||
//! §2.1, §5.3). Phase P0: a STATIC patch, manufactured-solution gated.
|
||||
//!
|
||||
//! The step is the Zang–Street–Koseff (1994) fractional step in the
|
||||
//! incremental form the background PISO uses:
|
||||
//!
|
||||
//! ```text
|
||||
//! û = u^n + dt (−C(F^n, u^n) + ν D(u^n) + f/ρ)
|
||||
//! u* = û − (dt/ρ) G_c p^n cell gradient (least squares)
|
||||
//! F* = interp(û)·S_f − (dt/ρ) L_f(p^n) compact face operator
|
||||
//! Σ_f (dt/ρ) L_f(p') = Σ_f F* pressure correction
|
||||
//! F = F* − (dt/ρ) L_f(p'), u = u* − (dt/ρ) G_c p', p += p'
|
||||
//! ```
|
||||
//!
|
||||
//! so the face flux — the primary variable for convection, divergence and
|
||||
//! the next step — sees the whole pressure through one compact operator
|
||||
//! (no checkerboard), and the cell velocity is a slave. Further correctors
|
||||
//! re-project the STORED fluxes. `L_f` is the orthogonal difference plus a
|
||||
//! node-based tangential correction (a 9-point stencil), assembled and
|
||||
//! applied by the same code.
|
||||
|
||||
mod operators;
|
||||
mod predictor;
|
||||
mod projection;
|
||||
|
||||
pub use operators::Operators;
|
||||
|
||||
use crate::mesh::{PatchMesh, PatchSide};
|
||||
use crate::solvers::incompressible::sparse_bicgstab::CsrMatrix;
|
||||
use crate::{CfdConfig, CfdResult};
|
||||
|
||||
/// What one side of the patch is.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum SideBc {
|
||||
/// Prescribed velocity (a wall or an inflow): the boundary function
|
||||
/// gives the velocity; mass flux `u_b · S_f`; pressure Neumann.
|
||||
#[default]
|
||||
Velocity,
|
||||
/// Open boundary at gauge pressure zero: zero-gradient velocity,
|
||||
/// flux from the predictor, corrected by the projection.
|
||||
Outlet,
|
||||
}
|
||||
|
||||
/// The four sides. `s_start`/`s_end` are ignored on a periodic patch.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct PatchBoundaries {
|
||||
/// `k = 0` (the wall of an O-grid).
|
||||
pub inner: SideBc,
|
||||
/// `k = nn`.
|
||||
pub outer: SideBc,
|
||||
/// `i = 0`.
|
||||
pub s_start: SideBc,
|
||||
/// `i = ns`.
|
||||
pub s_end: SideBc,
|
||||
}
|
||||
|
||||
impl PatchBoundaries {
|
||||
/// The condition on a side.
|
||||
pub fn get(&self, side: PatchSide) -> SideBc {
|
||||
match side {
|
||||
PatchSide::Inner => self.inner,
|
||||
PatchSide::Outer => self.outer,
|
||||
PatchSide::SStart => self.s_start,
|
||||
PatchSide::SEnd => self.s_end,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convection treatment.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum PatchConvection {
|
||||
/// First-order upwind on the face fluxes (the background's scheme).
|
||||
#[default]
|
||||
Upwind,
|
||||
/// No convection: the Stokes limit, for the second-order MMS gate.
|
||||
None,
|
||||
}
|
||||
|
||||
/// How the across-patch diffusion is time-stepped.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum NormalDiffusion {
|
||||
/// Forward Euler, like the background (limit `~Δn²/(4ν)`).
|
||||
#[default]
|
||||
Explicit,
|
||||
/// The orthogonal part of the n-face diffusion implicit along each
|
||||
/// s-line (tridiagonal); the tangential part and everything else
|
||||
/// explicit.
|
||||
LineImplicit,
|
||||
}
|
||||
|
||||
/// Solver parameters.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CurvilinearParameters {
|
||||
/// Projections per step (the first removes the divergence; the rest
|
||||
/// mop up inner-solver truncation).
|
||||
pub corrector_steps: usize,
|
||||
/// Pressure-correction stop, relative to the step's flux scale.
|
||||
pub tolerance: f64,
|
||||
/// Convection scheme.
|
||||
pub convection: PatchConvection,
|
||||
/// Across-patch diffusion treatment.
|
||||
pub normal_diffusion: NormalDiffusion,
|
||||
/// Side conditions.
|
||||
pub boundaries: PatchBoundaries,
|
||||
/// BiCGSTAB iteration cap.
|
||||
pub max_poisson_iterations: usize,
|
||||
}
|
||||
|
||||
impl Default for CurvilinearParameters {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
corrector_steps: 2,
|
||||
tolerance: 1e-8,
|
||||
convection: PatchConvection::Upwind,
|
||||
normal_diffusion: NormalDiffusion::Explicit,
|
||||
boundaries: PatchBoundaries::default(),
|
||||
max_poisson_iterations: 5000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The patch state: cell-centred velocity and pressure, face mass fluxes.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PatchField {
|
||||
/// Cell x-velocity.
|
||||
pub u: Vec<f64>,
|
||||
/// Cell y-velocity.
|
||||
pub v: Vec<f64>,
|
||||
/// Cell pressure.
|
||||
pub p: Vec<f64>,
|
||||
/// Volume flux through every face, oriented +s / +n.
|
||||
pub flux: Vec<f64>,
|
||||
}
|
||||
|
||||
impl PatchField {
|
||||
/// Zero field on `mesh`.
|
||||
pub fn new(mesh: &PatchMesh) -> Self {
|
||||
let n = mesh.cell_count();
|
||||
Self {
|
||||
u: vec![0.0; n],
|
||||
v: vec![0.0; n],
|
||||
p: vec![0.0; n],
|
||||
flux: vec![0.0; mesh.faces().len()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What one step reports.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct CurvilinearResult {
|
||||
/// Correctors performed.
|
||||
pub corrector_steps_performed: usize,
|
||||
/// Largest cell mass imbalance after the last corrector.
|
||||
pub max_divergence: f64,
|
||||
/// Pressure-solver iterations, summed over the correctors.
|
||||
pub poisson_iterations: usize,
|
||||
/// Whether every pressure solve reached its tolerance.
|
||||
pub poisson_converged: bool,
|
||||
/// Boundary-flux defect removed on a closed patch (zero if an outlet exists).
|
||||
pub boundary_flux_adjustment: f64,
|
||||
}
|
||||
|
||||
/// Restorable solver state (the coupling re-runs a step).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CurvilinearSolverState {
|
||||
time: f64,
|
||||
}
|
||||
|
||||
type VelocityFn = Box<dyn Fn(f64, f64, f64) -> (f64, f64) + Send + Sync>;
|
||||
|
||||
/// PISO on a curvilinear patch.
|
||||
pub struct CurvilinearPisoSolver {
|
||||
config: CfdConfig,
|
||||
params: CurvilinearParameters,
|
||||
mesh: PatchMesh,
|
||||
ops: Operators,
|
||||
boundary_velocity: Option<VelocityFn>,
|
||||
momentum_source: Option<VelocityFn>,
|
||||
time: f64,
|
||||
matrix: Option<(f64, CsrMatrix, Option<usize>)>,
|
||||
}
|
||||
|
||||
impl CurvilinearPisoSolver {
|
||||
/// Build on `mesh`.
|
||||
pub fn new(
|
||||
config: CfdConfig,
|
||||
params: CurvilinearParameters,
|
||||
mesh: PatchMesh,
|
||||
) -> CfdResult<Self> {
|
||||
let ops = Operators::new(&mesh, ¶ms.boundaries);
|
||||
Ok(Self {
|
||||
config,
|
||||
params,
|
||||
mesh,
|
||||
ops,
|
||||
boundary_velocity: None,
|
||||
momentum_source: None,
|
||||
time: 0.0,
|
||||
matrix: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Velocity on every `Velocity` side, `(x, y, t) -> (u, v)`.
|
||||
pub fn set_boundary_velocity<F>(&mut self, f: F)
|
||||
where
|
||||
F: Fn(f64, f64, f64) -> (f64, f64) + Send + Sync + 'static,
|
||||
{
|
||||
self.boundary_velocity = Some(Box::new(f));
|
||||
}
|
||||
/// Body force per unit volume, `(x, y, t) -> (fx, fy)`.
|
||||
pub fn set_momentum_source<F>(&mut self, f: F)
|
||||
where
|
||||
F: Fn(f64, f64, f64) -> (f64, f64) + Send + Sync + 'static,
|
||||
{
|
||||
self.momentum_source = Some(Box::new(f));
|
||||
}
|
||||
/// The mesh.
|
||||
pub fn mesh(&self) -> &PatchMesh {
|
||||
&self.mesh
|
||||
}
|
||||
/// The operators.
|
||||
pub fn operators(&self) -> &Operators {
|
||||
&self.ops
|
||||
}
|
||||
/// Parameters.
|
||||
pub fn parameters(&self) -> &CurvilinearParameters {
|
||||
&self.params
|
||||
}
|
||||
/// Configuration.
|
||||
pub fn config(&self) -> &CfdConfig {
|
||||
&self.config
|
||||
}
|
||||
/// Current time.
|
||||
pub fn time(&self) -> f64 {
|
||||
self.time
|
||||
}
|
||||
/// Set the time.
|
||||
pub fn set_time(&mut self, t: f64) {
|
||||
self.time = t;
|
||||
}
|
||||
/// Capture the state.
|
||||
pub fn snapshot(&self) -> CurvilinearSolverState {
|
||||
CurvilinearSolverState { time: self.time }
|
||||
}
|
||||
/// Restore a captured state.
|
||||
pub fn restore(&mut self, state: &CurvilinearSolverState) {
|
||||
self.time = state.time;
|
||||
}
|
||||
|
||||
pub(crate) fn boundary_velocity(&self, x: f64, y: f64, t: f64) -> (f64, f64) {
|
||||
self.boundary_velocity
|
||||
.as_ref()
|
||||
.map_or((0.0, 0.0), |f| f(x, y, t))
|
||||
}
|
||||
pub(crate) fn source_at(&self, xy: [f64; 2], t: f64) -> (f64, f64) {
|
||||
self.momentum_source
|
||||
.as_ref()
|
||||
.map_or((0.0, 0.0), |f| f(xy[0], xy[1], t))
|
||||
}
|
||||
|
||||
/// Set the cell velocities from a function and make the face fluxes
|
||||
/// consistent (interpolated; prescribed on velocity sides).
|
||||
pub fn initialize<F>(&self, field: &mut PatchField, velocity: F)
|
||||
where
|
||||
F: Fn(f64, f64) -> (f64, f64),
|
||||
{
|
||||
let mesh = &self.mesh;
|
||||
for c in 0..mesh.cell_count() {
|
||||
let xy = mesh.centre(c);
|
||||
let (u, v) = velocity(xy[0], xy[1]);
|
||||
field.u[c] = u;
|
||||
field.v[c] = v;
|
||||
}
|
||||
let zero = vec![0.0; mesh.cell_count()];
|
||||
field.flux =
|
||||
self.predicted_fluxes(&field.u.clone(), &field.v.clone(), &zero, 0.0, self.time);
|
||||
}
|
||||
|
||||
/// Advance one step of `dt`.
|
||||
pub async fn advance(
|
||||
&mut self,
|
||||
field: &mut PatchField,
|
||||
dt: f64,
|
||||
) -> CfdResult<CurvilinearResult> {
|
||||
let t_old = self.time;
|
||||
let t_new = t_old + dt;
|
||||
let rho = self.config.density;
|
||||
let mesh = &self.mesh;
|
||||
|
||||
let (uh, vh) = self.predict(field, dt, t_old);
|
||||
let mut flux = self.predicted_fluxes(&uh, &vh, &field.p, dt, t_new);
|
||||
let adjustment = self.adjust_boundary_flux(&mut flux);
|
||||
for c in 0..mesh.cell_count() {
|
||||
let g = self.pressure_gradient(&field.p, c);
|
||||
field.u[c] = uh[c] - dt / rho * g[0];
|
||||
field.v[c] = vh[c] - dt / rho * g[1];
|
||||
}
|
||||
field.flux = flux;
|
||||
|
||||
if self.matrix.as_ref().is_none_or(|(d, _, _)| *d != dt) {
|
||||
let (m, anchor) = self.assemble_pressure_matrix(dt);
|
||||
self.matrix = Some((dt, m, anchor));
|
||||
}
|
||||
let (_, matrix, anchor) = self.matrix.as_ref().expect("assembled");
|
||||
let flux_scale: f64 = field.flux.iter().map(|f| f.abs()).sum::<f64>().max(1e-300);
|
||||
let tolerance = self.params.tolerance * flux_scale;
|
||||
|
||||
let mut iterations = 0;
|
||||
let mut converged = true;
|
||||
let mut performed = 0;
|
||||
let mut max_div = self.max_divergence(&field.flux);
|
||||
for _ in 0..self.params.corrector_steps {
|
||||
let (pc, out) = self.solve_pressure_correction(matrix, *anchor, &field.flux, tolerance);
|
||||
iterations += out.iterations;
|
||||
converged &= out.converged;
|
||||
self.apply_correction(field, &pc, dt);
|
||||
performed += 1;
|
||||
max_div = self.max_divergence(&field.flux);
|
||||
if max_div <= tolerance {
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.time = t_new;
|
||||
Ok(CurvilinearResult {
|
||||
corrector_steps_performed: performed,
|
||||
max_divergence: max_div,
|
||||
poisson_iterations: iterations,
|
||||
poisson_converged: converged,
|
||||
boundary_flux_adjustment: adjustment,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
//! Discrete operators on the patch: linear-exact nodal reconstruction,
|
||||
//! the compact face-gradient operator `L_f` (orthogonal part plus the
|
||||
//! node-based tangential correction), weighted least-squares cell
|
||||
//! gradients, and face interpolation. One `L_f` serves velocity diffusion,
|
||||
//! the pressure term in the face flux, and the pressure equation's matrix
|
||||
//! — the consistency the collocated scheme depends on.
|
||||
|
||||
use super::{PatchBoundaries, SideBc};
|
||||
use crate::mesh::{PatchMesh, PatchSide};
|
||||
|
||||
/// How a node's value is reconstructed from the cells around it.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NodeStencil {
|
||||
/// Cells touching the node.
|
||||
pub cells: Vec<usize>,
|
||||
/// Their weights (sum to one; exact on linear fields for 4-cell nodes).
|
||||
pub weights: Vec<f64>,
|
||||
/// The boundary the node lies on, if any (`Inner`/`Outer` win at corners).
|
||||
pub side: Option<PatchSide>,
|
||||
}
|
||||
|
||||
/// Per-face constants of `L_f`.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct FaceCoefs {
|
||||
/// `|S_f|² / (d_f · S_f)`: the orthogonal (implicit) coefficient.
|
||||
pub alpha: f64,
|
||||
/// `(d_f · t̂) / |f|`: the tangential correction is `−alpha · tang · (φ_n1 − φ_n0)`.
|
||||
pub tang: f64,
|
||||
}
|
||||
|
||||
/// Precomputed operator data for one patch geometry.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Operators {
|
||||
nodes: Vec<NodeStencil>,
|
||||
faces: Vec<FaceCoefs>,
|
||||
}
|
||||
|
||||
impl Operators {
|
||||
/// Build for `mesh`; `boundaries` decides which nodes are Dirichlet
|
||||
/// for pressure (outlet sides).
|
||||
pub fn new(mesh: &PatchMesh, _boundaries: &PatchBoundaries) -> Self {
|
||||
let (ns, nn) = (mesh.ns(), mesh.nn());
|
||||
let mut nodes = Vec::with_capacity((nn + 1) * (ns + 1));
|
||||
for k in 0..=nn {
|
||||
for i in 0..=ns {
|
||||
nodes.push(Self::node_stencil(mesh, k, i));
|
||||
}
|
||||
}
|
||||
let faces = mesh
|
||||
.faces()
|
||||
.iter()
|
||||
.map(|f| {
|
||||
let s2 = f.s[0] * f.s[0] + f.s[1] * f.s[1];
|
||||
let ds = f.d[0] * f.s[0] + f.d[1] * f.s[1];
|
||||
let alpha = if ds != 0.0 { s2 / ds } else { 0.0 };
|
||||
let t = [
|
||||
mesh.node_xy(f.n1)[0] - mesh.node_xy(f.n0)[0],
|
||||
mesh.node_xy(f.n1)[1] - mesh.node_xy(f.n0)[1],
|
||||
];
|
||||
let len = (t[0] * t[0] + t[1] * t[1]).sqrt();
|
||||
let tang = if len > 0.0 {
|
||||
(f.d[0] * t[0] + f.d[1] * t[1]) / (len * len)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
FaceCoefs { alpha, tang }
|
||||
})
|
||||
.collect();
|
||||
Self { nodes, faces }
|
||||
}
|
||||
|
||||
fn node_stencil(mesh: &PatchMesh, k: usize, i: usize) -> NodeStencil {
|
||||
let (ns, nn) = (mesh.ns(), mesh.nn());
|
||||
let side = if k == 0 {
|
||||
Some(PatchSide::Inner)
|
||||
} else if k == nn {
|
||||
Some(PatchSide::Outer)
|
||||
} else if mesh.periodic().is_none() && i == 0 {
|
||||
Some(PatchSide::SStart)
|
||||
} else if mesh.periodic().is_none() && i == ns {
|
||||
Some(PatchSide::SEnd)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let cells = mesh.node_cells(k, i);
|
||||
let xn = mesh.node_xy(mesh.node(k, i));
|
||||
// Cell centres relative to the node, with the periodic shift for
|
||||
// cells reached across the seam (a cell whose column is ns-1 seen
|
||||
// from node column 0, or column 0 seen from node column ns).
|
||||
let rel: Vec<[f64; 2]> = cells
|
||||
.iter()
|
||||
.map(|&c| {
|
||||
let (_, ci) = mesh.cell_ki(c);
|
||||
let mut xc = mesh.centre(c);
|
||||
if let Some(shift) = mesh.periodic() {
|
||||
if i == 0 && ci == ns - 1 {
|
||||
xc = [xc[0] - shift[0], xc[1] - shift[1]];
|
||||
} else if i == ns && ci == 0 {
|
||||
xc = [xc[0] + shift[0], xc[1] + shift[1]];
|
||||
}
|
||||
}
|
||||
[xc[0] - xn[0], xc[1] - xn[1]]
|
||||
})
|
||||
.collect();
|
||||
let weights = linear_exact_weights(&rel);
|
||||
NodeStencil {
|
||||
cells,
|
||||
weights,
|
||||
side,
|
||||
}
|
||||
}
|
||||
|
||||
/// Node stencils.
|
||||
pub fn nodes(&self) -> &[NodeStencil] {
|
||||
&self.nodes
|
||||
}
|
||||
/// Face coefficients.
|
||||
pub fn face(&self, f: usize) -> FaceCoefs {
|
||||
self.faces[f]
|
||||
}
|
||||
|
||||
/// Node values of a cell field. `boundary(side, xy)` supplies a
|
||||
/// prescribed value at nodes on a boundary side (`None` = extrapolate
|
||||
/// from the cells like an interior node).
|
||||
pub fn node_values(
|
||||
&self,
|
||||
mesh: &PatchMesh,
|
||||
cell_vals: &[f64],
|
||||
boundary: &dyn Fn(PatchSide, [f64; 2]) -> Option<f64>,
|
||||
) -> Vec<f64> {
|
||||
self.nodes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(n, st)| {
|
||||
if let Some(side) = st.side {
|
||||
if let Some(v) = boundary(side, mesh.node_xy(n)) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
st.cells
|
||||
.iter()
|
||||
.zip(&st.weights)
|
||||
.map(|(&c, &w)| w * cell_vals[c])
|
||||
.sum()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `L_f(φ) ≈ (∇φ)_f · S_f` for one face, given cell and node values;
|
||||
/// `boundary_value` is the face's Dirichlet value on a boundary face
|
||||
/// (`None` = zero-gradient: the flux is zero).
|
||||
pub fn face_gradient_flux(
|
||||
&self,
|
||||
mesh: &PatchMesh,
|
||||
f: usize,
|
||||
cell_vals: &[f64],
|
||||
node_vals: &[f64],
|
||||
boundary_value: Option<f64>,
|
||||
) -> f64 {
|
||||
let face = &mesh.faces()[f];
|
||||
let FaceCoefs { alpha, tang } = self.faces[f];
|
||||
let (phi_p, phi_n) = match (face.owner, face.neigh) {
|
||||
(Some(p), Some(n)) => (cell_vals[p], cell_vals[n]),
|
||||
(Some(p), None) => match boundary_value {
|
||||
Some(b) => (cell_vals[p], b),
|
||||
None => return 0.0,
|
||||
},
|
||||
(None, Some(n)) => match boundary_value {
|
||||
Some(b) => (b, cell_vals[n]),
|
||||
None => return 0.0,
|
||||
},
|
||||
(None, None) => unreachable!(),
|
||||
};
|
||||
alpha * ((phi_n - phi_p) - tang * (node_vals[face.n1] - node_vals[face.n0]))
|
||||
}
|
||||
|
||||
/// The coefficient list of `L_f` in terms of cell unknowns for the
|
||||
/// pressure-correction equation: interior faces expand the nodal term
|
||||
/// through the node stencils (skipping nodes on Dirichlet sides, whose
|
||||
/// correction is zero); Dirichlet boundary faces contribute `∓alpha`
|
||||
/// on the interior cell; Neumann faces contribute nothing.
|
||||
pub fn face_gradient_coeffs(
|
||||
&self,
|
||||
mesh: &PatchMesh,
|
||||
boundaries: &PatchBoundaries,
|
||||
f: usize,
|
||||
out: &mut Vec<(usize, f64)>,
|
||||
) {
|
||||
out.clear();
|
||||
let face = &mesh.faces()[f];
|
||||
let FaceCoefs { alpha, tang } = self.faces[f];
|
||||
match (face.owner, face.neigh) {
|
||||
(Some(p), Some(n)) => {
|
||||
out.push((p, -alpha));
|
||||
out.push((n, alpha));
|
||||
for (node, sign) in [(face.n0, alpha * tang), (face.n1, -alpha * tang)] {
|
||||
let st = &self.nodes[node];
|
||||
if st.side.is_some_and(|s| boundaries.get(s) == SideBc::Outlet) {
|
||||
continue;
|
||||
}
|
||||
for (&c, &w) in st.cells.iter().zip(&st.weights) {
|
||||
out.push((c, sign * w));
|
||||
}
|
||||
}
|
||||
}
|
||||
(Some(p), None) => {
|
||||
if boundaries.get(mesh.side(f).expect("boundary")) == SideBc::Outlet {
|
||||
out.push((p, -alpha));
|
||||
}
|
||||
}
|
||||
(None, Some(n)) => {
|
||||
if boundaries.get(mesh.side(f).expect("boundary")) == SideBc::Outlet {
|
||||
out.push((n, alpha));
|
||||
}
|
||||
}
|
||||
(None, None) => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Weighted least-squares gradient of a cell field at cell `c`.
|
||||
/// `boundary_value(face)` gives the value at a boundary face centre
|
||||
/// (`None` = the face is left out of the fit).
|
||||
pub fn gradient(
|
||||
&self,
|
||||
mesh: &PatchMesh,
|
||||
c: usize,
|
||||
cell_vals: &[f64],
|
||||
boundary_value: &dyn Fn(usize) -> Option<f64>,
|
||||
) -> [f64; 2] {
|
||||
let (mut sxx, mut sxy, mut syy, mut bx, mut by) = (0.0, 0.0, 0.0, 0.0, 0.0);
|
||||
let phi_p = cell_vals[c];
|
||||
for (f, sign) in mesh.cell_faces(c) {
|
||||
let face = &mesh.faces()[f];
|
||||
let (r, dphi) = match (face.owner, face.neigh) {
|
||||
(Some(p), Some(n)) => {
|
||||
let other = if p == c { n } else { p };
|
||||
(
|
||||
[sign * face.d[0], sign * face.d[1]],
|
||||
cell_vals[other] - phi_p,
|
||||
)
|
||||
}
|
||||
_ => match boundary_value(f) {
|
||||
Some(b) => ([sign * face.d[0], sign * face.d[1]], b - phi_p),
|
||||
None => continue,
|
||||
},
|
||||
};
|
||||
let w = 1.0 / (r[0] * r[0] + r[1] * r[1]);
|
||||
sxx += w * r[0] * r[0];
|
||||
sxy += w * r[0] * r[1];
|
||||
syy += w * r[1] * r[1];
|
||||
bx += w * r[0] * dphi;
|
||||
by += w * r[1] * dphi;
|
||||
}
|
||||
let det = sxx * syy - sxy * sxy;
|
||||
if det.abs() <= 1e-300 {
|
||||
return [0.0, 0.0];
|
||||
}
|
||||
[(syy * bx - sxy * by) / det, (sxx * by - sxy * bx) / det]
|
||||
}
|
||||
}
|
||||
|
||||
/// Weights over points `rel` (relative to the reconstruction point) that
|
||||
/// sum to one and reproduce linear fields when the points allow it
|
||||
/// (Holmes–Connell pseudo-Laplacian); falls back to inverse distance if
|
||||
/// the fit is singular or produces a negative weight, and to the 1-D
|
||||
/// linear fit for two points.
|
||||
fn linear_exact_weights(rel: &[[f64; 2]]) -> Vec<f64> {
|
||||
let n = rel.len();
|
||||
if n == 1 {
|
||||
return vec![1.0];
|
||||
}
|
||||
if n == 2 {
|
||||
let d = [rel[1][0] - rel[0][0], rel[1][1] - rel[0][1]];
|
||||
let dd = d[0] * d[0] + d[1] * d[1];
|
||||
let t = if dd > 0.0 {
|
||||
-(rel[0][0] * d[0] + rel[0][1] * d[1]) / dd
|
||||
} else {
|
||||
0.5
|
||||
};
|
||||
return vec![1.0 - t, t];
|
||||
}
|
||||
let (mut rx, mut ry, mut ixx, mut ixy, mut iyy) = (0.0, 0.0, 0.0, 0.0, 0.0);
|
||||
for r in rel {
|
||||
rx += r[0];
|
||||
ry += r[1];
|
||||
ixx += r[0] * r[0];
|
||||
ixy += r[0] * r[1];
|
||||
iyy += r[1] * r[1];
|
||||
}
|
||||
let det = ixx * iyy - ixy * ixy;
|
||||
let mut weights: Vec<f64> = if det.abs() > 1e-300 {
|
||||
let lx = -(iyy * rx - ixy * ry) / det;
|
||||
let ly = -(ixx * ry - ixy * rx) / det;
|
||||
rel.iter().map(|r| 1.0 + lx * r[0] + ly * r[1]).collect()
|
||||
} else {
|
||||
vec![-1.0]
|
||||
};
|
||||
if weights.iter().any(|&w| w < 0.0) {
|
||||
weights = rel
|
||||
.iter()
|
||||
.map(|r| 1.0 / (r[0] * r[0] + r[1] * r[1]).sqrt().max(1e-300))
|
||||
.collect();
|
||||
}
|
||||
let sum: f64 = weights.iter().sum();
|
||||
weights.iter().map(|w| w / sum).collect()
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
//! The momentum predictor: explicit forward Euler from the old field
|
||||
//! (matching the background PISO), or line-implicit in `n` for the
|
||||
//! orthogonal part of the across-patch diffusion.
|
||||
|
||||
use super::{CurvilinearPisoSolver, NormalDiffusion, PatchConvection, PatchField, SideBc};
|
||||
use crate::mesh::PatchSide;
|
||||
|
||||
impl CurvilinearPisoSolver {
|
||||
/// `û = u^n + dt (−C(F^n, u^n) + ν D(u^n) + f/ρ)`, no pressure.
|
||||
pub(super) fn predict(&self, field: &PatchField, dt: f64, t_old: f64) -> (Vec<f64>, Vec<f64>) {
|
||||
let mesh = &self.mesh;
|
||||
let n = mesh.cell_count();
|
||||
let rho = self.config.density;
|
||||
let nu = self.config.viscosity / rho;
|
||||
let ops = &self.ops;
|
||||
|
||||
let bvel = |side: PatchSide, xy: [f64; 2]| -> Option<(f64, f64)> {
|
||||
match self.params.boundaries.get(side) {
|
||||
SideBc::Velocity => Some(self.boundary_velocity(xy[0], xy[1], t_old)),
|
||||
SideBc::Outlet => None,
|
||||
}
|
||||
};
|
||||
let un = ops.node_values(mesh, &field.u, &|s, xy| bvel(s, xy).map(|v| v.0));
|
||||
let vn = ops.node_values(mesh, &field.v, &|s, xy| bvel(s, xy).map(|v| v.1));
|
||||
|
||||
let implicit_n = matches!(self.params.normal_diffusion, NormalDiffusion::LineImplicit);
|
||||
let mut uh = vec![0.0; n];
|
||||
let mut vh = vec![0.0; n];
|
||||
for c in 0..n {
|
||||
let (mut cu, mut cv, mut du, mut dv) = (0.0, 0.0, 0.0, 0.0);
|
||||
for (f, sign) in mesh.cell_faces(c) {
|
||||
let face = &mesh.faces()[f];
|
||||
let side = mesh.side(f);
|
||||
let bval = side.and_then(|s| bvel(s, face.centre));
|
||||
// Convection: outward flux times the upwind face value.
|
||||
if matches!(self.params.convection, PatchConvection::Upwind) {
|
||||
let out = sign * field.flux[f];
|
||||
let (uf, vf) = match (face.owner, face.neigh) {
|
||||
(Some(p), Some(q)) => {
|
||||
let up = if out >= 0.0 {
|
||||
c
|
||||
} else if p == c {
|
||||
q
|
||||
} else {
|
||||
p
|
||||
};
|
||||
(field.u[up], field.v[up])
|
||||
}
|
||||
_ => match bval {
|
||||
Some(b) => b,
|
||||
None => (field.u[c], field.v[c]),
|
||||
},
|
||||
};
|
||||
cu += out * uf;
|
||||
cv += out * vf;
|
||||
}
|
||||
// Diffusion: ν L_f(u), the orthogonal n-face part deferred
|
||||
// to the tridiagonal solve when line-implicit.
|
||||
let is_n = !mesh.is_sface(f);
|
||||
let (bu, bv) = match bval {
|
||||
Some(b) => (Some(b.0), Some(b.1)),
|
||||
None => (None, None),
|
||||
};
|
||||
let mut lu = ops.face_gradient_flux(mesh, f, &field.u, &un, bu);
|
||||
let mut lv = ops.face_gradient_flux(mesh, f, &field.v, &vn, bv);
|
||||
if implicit_n && is_n && (side.is_none() || bval.is_some()) {
|
||||
let alpha = ops.face(f).alpha;
|
||||
let (pu, pv) = match (face.owner, face.neigh) {
|
||||
(Some(p), Some(q)) => {
|
||||
let other = if p == c { q } else { p };
|
||||
(
|
||||
sign * alpha * (field.u[other] - field.u[c]),
|
||||
sign * alpha * (field.v[other] - field.v[c]),
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
let b = bval.expect("dirichlet");
|
||||
(
|
||||
sign * alpha * (b.0 - field.u[c]),
|
||||
sign * alpha * (b.1 - field.v[c]),
|
||||
)
|
||||
}
|
||||
};
|
||||
lu = sign * lu - pu;
|
||||
lv = sign * lv - pv;
|
||||
} else {
|
||||
lu *= sign;
|
||||
lv *= sign;
|
||||
}
|
||||
du += lu;
|
||||
dv += lv;
|
||||
}
|
||||
let a = mesh.area(c);
|
||||
let (fx, fy) = self.source_at(mesh.centre(c), t_old);
|
||||
uh[c] = field.u[c] + dt * ((-cu + nu * du) / a + fx / rho);
|
||||
vh[c] = field.v[c] + dt * ((-cv + nu * dv) / a + fy / rho);
|
||||
}
|
||||
|
||||
if implicit_n {
|
||||
self.solve_lines(&mut uh, &mut vh, dt, nu, t_old);
|
||||
}
|
||||
(uh, vh)
|
||||
}
|
||||
|
||||
/// `(I − dt ν L_n/A) û = rhs` along every s-line, Thomas algorithm.
|
||||
fn solve_lines(&self, uh: &mut [f64], vh: &mut [f64], dt: f64, nu: f64, t_old: f64) {
|
||||
let mesh = &self.mesh;
|
||||
let (ns, nn) = (mesh.ns(), mesh.nn());
|
||||
let mut lower = vec![0.0; nn];
|
||||
let mut diag = vec![0.0; nn];
|
||||
let mut upper = vec![0.0; nn];
|
||||
let mut ru = vec![0.0; nn];
|
||||
let mut rv = vec![0.0; nn];
|
||||
for i in 0..ns {
|
||||
for k in 0..nn {
|
||||
let c = mesh.cell(k, i);
|
||||
let a = mesh.area(c);
|
||||
let mut d = 1.0;
|
||||
let (mut lo, mut up) = (0.0, 0.0);
|
||||
ru[k] = uh[c];
|
||||
rv[k] = vh[c];
|
||||
for (f, sign) in [(mesh.nface(k, i), -1.0), (mesh.nface(k + 1, i), 1.0)] {
|
||||
let alpha = self.ops.face(f).alpha;
|
||||
let coef = dt * nu * alpha / a;
|
||||
match mesh.side(f) {
|
||||
None => {
|
||||
d += coef;
|
||||
if sign < 0.0 {
|
||||
lo = -coef;
|
||||
} else {
|
||||
up = -coef;
|
||||
}
|
||||
}
|
||||
Some(s) => match self.params.boundaries.get(s) {
|
||||
SideBc::Velocity => {
|
||||
let face = &mesh.faces()[f];
|
||||
let b =
|
||||
self.boundary_velocity(face.centre[0], face.centre[1], t_old);
|
||||
d += coef;
|
||||
ru[k] += coef * b.0;
|
||||
rv[k] += coef * b.1;
|
||||
}
|
||||
SideBc::Outlet => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
lower[k] = lo;
|
||||
diag[k] = d;
|
||||
upper[k] = up;
|
||||
}
|
||||
// Thomas.
|
||||
for k in 1..nn {
|
||||
let m = lower[k] / diag[k - 1];
|
||||
diag[k] -= m * upper[k - 1];
|
||||
ru[k] -= m * ru[k - 1];
|
||||
rv[k] -= m * rv[k - 1];
|
||||
}
|
||||
ru[nn - 1] /= diag[nn - 1];
|
||||
rv[nn - 1] /= diag[nn - 1];
|
||||
for k in (0..nn - 1).rev() {
|
||||
ru[k] = (ru[k] - upper[k] * ru[k + 1]) / diag[k];
|
||||
rv[k] = (rv[k] - upper[k] * rv[k + 1]) / diag[k];
|
||||
}
|
||||
for k in 0..nn {
|
||||
let c = mesh.cell(k, i);
|
||||
uh[c] = ru[k];
|
||||
vh[c] = rv[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
//! The pressure step: face fluxes from the predictor with the compact
|
||||
//! pressure term, boundary-flux adjustment for closed patches, the
|
||||
//! pressure-correction equation on the 9-point operator, and the flux and
|
||||
//! velocity corrections.
|
||||
|
||||
use super::{CurvilinearPisoSolver, PatchField, SideBc};
|
||||
use crate::mesh::PatchSide;
|
||||
use crate::solvers::incompressible::sparse_bicgstab::{
|
||||
BicgstabResult, CsrMatrix, bicgstab_jacobi, project_mean,
|
||||
};
|
||||
|
||||
impl CurvilinearPisoSolver {
|
||||
/// Node values of a pressure-like cell field: zero on outlet sides,
|
||||
/// extrapolated elsewhere.
|
||||
pub(super) fn pressure_nodes(&self, p: &[f64]) -> Vec<f64> {
|
||||
let b = &self.params.boundaries;
|
||||
self.ops
|
||||
.node_values(&self.mesh, p, &|side: PatchSide, _| match b.get(side) {
|
||||
SideBc::Outlet => Some(0.0),
|
||||
SideBc::Velocity => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// `L_f(p)` on every face (zero on Neumann faces, outlet value zero).
|
||||
pub(super) fn pressure_face_gradients(&self, p: &[f64]) -> Vec<f64> {
|
||||
let mesh = &self.mesh;
|
||||
let pn = self.pressure_nodes(p);
|
||||
(0..mesh.faces().len())
|
||||
.map(|f| {
|
||||
let bval = mesh
|
||||
.side(f)
|
||||
.and_then(|s| match self.params.boundaries.get(s) {
|
||||
SideBc::Outlet => Some(0.0),
|
||||
SideBc::Velocity => None,
|
||||
});
|
||||
self.ops.face_gradient_flux(mesh, f, p, &pn, bval)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Least-squares cell gradient of a pressure-like field (outlet faces
|
||||
/// at zero, Neumann faces left out).
|
||||
pub(super) fn pressure_gradient(&self, p: &[f64], c: usize) -> [f64; 2] {
|
||||
let mesh = &self.mesh;
|
||||
self.ops.gradient(mesh, c, p, &|f| match mesh.side(f) {
|
||||
Some(s) if self.params.boundaries.get(s) == SideBc::Outlet => Some(0.0),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// `F* = interp(û)·S − (dt/ρ) L_f(p^n)` on interior and outlet faces,
|
||||
/// the prescribed flux on velocity faces (at `t_new`).
|
||||
pub(super) fn predicted_fluxes(
|
||||
&self,
|
||||
uh: &[f64],
|
||||
vh: &[f64],
|
||||
p: &[f64],
|
||||
dt: f64,
|
||||
t_new: f64,
|
||||
) -> Vec<f64> {
|
||||
let mesh = &self.mesh;
|
||||
let rho = self.config.density;
|
||||
let lp = self.pressure_face_gradients(p);
|
||||
mesh.faces()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(f, face)| match (face.owner, face.neigh) {
|
||||
(Some(o), Some(n)) => {
|
||||
let w = face.w;
|
||||
let uf = w * uh[o] + (1.0 - w) * uh[n];
|
||||
let vf = w * vh[o] + (1.0 - w) * vh[n];
|
||||
uf * face.s[0] + vf * face.s[1] - dt / rho * lp[f]
|
||||
}
|
||||
_ => {
|
||||
let c = mesh.boundary_cell(f);
|
||||
match self.params.boundaries.get(mesh.side(f).expect("boundary")) {
|
||||
SideBc::Velocity => {
|
||||
let (ub, vb) =
|
||||
self.boundary_velocity(face.centre[0], face.centre[1], t_new);
|
||||
ub * face.s[0] + vb * face.s[1]
|
||||
}
|
||||
SideBc::Outlet => uh[c] * face.s[0] + vh[c] * face.s[1] - dt / rho * lp[f],
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// On a patch with no outlet the prescribed boundary fluxes must sum
|
||||
/// to zero for the projection to be solvable; the O(h²) defect of
|
||||
/// face-centre sampling is spread over the velocity faces by area
|
||||
/// (OpenFOAM's `adjustPhi`). Returns the defect removed.
|
||||
pub(super) fn adjust_boundary_flux(&self, flux: &mut [f64]) -> f64 {
|
||||
let mesh = &self.mesh;
|
||||
let has_outlet = [
|
||||
PatchSide::Inner,
|
||||
PatchSide::Outer,
|
||||
PatchSide::SStart,
|
||||
PatchSide::SEnd,
|
||||
]
|
||||
.iter()
|
||||
.any(|&s| self.params.boundaries.get(s) == SideBc::Outlet);
|
||||
if has_outlet {
|
||||
return 0.0;
|
||||
}
|
||||
let (mut net, mut total_len) = (0.0, 0.0);
|
||||
for (f, face) in mesh.faces().iter().enumerate() {
|
||||
if mesh.side(f).is_some() {
|
||||
let out_sign = if face.owner.is_some() { 1.0 } else { -1.0 };
|
||||
net += out_sign * flux[f];
|
||||
total_len += (face.s[0] * face.s[0] + face.s[1] * face.s[1]).sqrt();
|
||||
}
|
||||
}
|
||||
if total_len == 0.0 {
|
||||
return net;
|
||||
}
|
||||
for (f, face) in mesh.faces().iter().enumerate() {
|
||||
if mesh.side(f).is_some() {
|
||||
let out_sign = if face.owner.is_some() { 1.0 } else { -1.0 };
|
||||
let len = (face.s[0] * face.s[0] + face.s[1] * face.s[1]).sqrt();
|
||||
flux[f] -= out_sign * net * len / total_len;
|
||||
}
|
||||
}
|
||||
net
|
||||
}
|
||||
|
||||
/// Assemble `−Σ_f sign (dt/ρ) L_f` (positive diagonal) and pick the
|
||||
/// anchor for the pure-Neumann case.
|
||||
pub(super) fn assemble_pressure_matrix(&self, dt: f64) -> (CsrMatrix, Option<usize>) {
|
||||
let mesh = &self.mesh;
|
||||
let rho = self.config.density;
|
||||
let n = mesh.cell_count();
|
||||
let mut tri = Vec::with_capacity(n * 12);
|
||||
let mut coefs = Vec::new();
|
||||
let mut any_dirichlet = false;
|
||||
for c in 0..n {
|
||||
for (f, sign) in mesh.cell_faces(c) {
|
||||
self.ops
|
||||
.face_gradient_coeffs(mesh, &self.params.boundaries, f, &mut coefs);
|
||||
if mesh.side(f).is_some() && !coefs.is_empty() {
|
||||
any_dirichlet = true;
|
||||
}
|
||||
for &(col, v) in &coefs {
|
||||
tri.push((c, col, -sign * dt / rho * v));
|
||||
}
|
||||
}
|
||||
tri.push((c, c, 0.0)); // guarantee a diagonal entry
|
||||
}
|
||||
let mut a = CsrMatrix::from_triplets(n, &tri);
|
||||
let anchor = if any_dirichlet {
|
||||
None
|
||||
} else {
|
||||
// An interior cell away from the seam: (1, 1).
|
||||
let a_cell = mesh.cell(1.min(mesh.nn() - 1), 1.min(mesh.ns() - 1));
|
||||
a.set_row_identity(a_cell);
|
||||
Some(a_cell)
|
||||
};
|
||||
(a, anchor)
|
||||
}
|
||||
|
||||
/// Solve `−Σ sign (dt/ρ) L_f(p') = −Σ sign F` for `p'` (zero start).
|
||||
pub(super) fn solve_pressure_correction(
|
||||
&self,
|
||||
matrix: &CsrMatrix,
|
||||
anchor: Option<usize>,
|
||||
flux: &[f64],
|
||||
tolerance: f64,
|
||||
) -> (Vec<f64>, BicgstabResult) {
|
||||
let mesh = &self.mesh;
|
||||
let n = mesh.cell_count();
|
||||
let mut rhs = vec![0.0; n];
|
||||
for c in 0..n {
|
||||
let mut div = 0.0;
|
||||
for (f, sign) in mesh.cell_faces(c) {
|
||||
div += sign * flux[f];
|
||||
}
|
||||
rhs[c] = -div;
|
||||
}
|
||||
if let Some(a) = anchor {
|
||||
project_mean(&mut rhs);
|
||||
rhs[a] = 0.0;
|
||||
}
|
||||
let mut pc = vec![0.0; n];
|
||||
let out = bicgstab_jacobi(
|
||||
matrix,
|
||||
&rhs,
|
||||
&mut pc,
|
||||
tolerance,
|
||||
self.params.max_poisson_iterations,
|
||||
);
|
||||
(pc, out)
|
||||
}
|
||||
|
||||
/// `F −= (dt/ρ) L_f(p')`, `u −= (dt/ρ) ∇p'`, `p += p'`.
|
||||
pub(super) fn apply_correction(&self, field: &mut PatchField, pc: &[f64], dt: f64) {
|
||||
let mesh = &self.mesh;
|
||||
let rho = self.config.density;
|
||||
let lp = self.pressure_face_gradients(pc);
|
||||
for f in 0..mesh.faces().len() {
|
||||
field.flux[f] -= dt / rho * lp[f];
|
||||
}
|
||||
for c in 0..mesh.cell_count() {
|
||||
let g = self.pressure_gradient(pc, c);
|
||||
field.u[c] -= dt / rho * g[0];
|
||||
field.v[c] -= dt / rho * g[1];
|
||||
field.p[c] += pc[c];
|
||||
}
|
||||
}
|
||||
|
||||
/// Largest cell mass imbalance `|Σ sign F_f|`.
|
||||
pub(super) fn max_divergence(&self, flux: &[f64]) -> f64 {
|
||||
let mesh = &self.mesh;
|
||||
(0..mesh.cell_count())
|
||||
.map(|c| {
|
||||
mesh.cell_faces(c)
|
||||
.iter()
|
||||
.map(|&(f, sign)| sign * flux[f])
|
||||
.sum::<f64>()
|
||||
.abs()
|
||||
})
|
||||
.fold(0.0, f64::max)
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ use crate::{CfdConfig, CfdError, CfdResult};
|
||||
pub mod ale;
|
||||
/// Boundary conditions
|
||||
pub mod boundary_conditions;
|
||||
/// Collocated PISO on a structured curvilinear patch (the overset patch)
|
||||
pub mod curvilinear;
|
||||
/// PISO on the fixed grid with an embedded body
|
||||
pub mod embedded;
|
||||
/// Embedded-body geometry, classification and loads
|
||||
@@ -32,6 +34,8 @@ pub mod simple;
|
||||
/// GPU-accelerated SIMPLE algorithm implementation
|
||||
#[cfg(feature = "cuda")]
|
||||
pub mod simple_gpu;
|
||||
/// CSR matrix + Jacobi-BiCGSTAB for the curvilinear pressure equation
|
||||
pub mod sparse_bicgstab;
|
||||
|
||||
// Re-export main types
|
||||
pub use ale::{
|
||||
@@ -40,6 +44,10 @@ pub use ale::{
|
||||
pub use boundary_conditions::{
|
||||
BoundaryCondition, BoundaryConditions, BoundaryLocation, BoundaryType,
|
||||
};
|
||||
pub use curvilinear::{
|
||||
CurvilinearParameters, CurvilinearPisoSolver, CurvilinearResult, CurvilinearSolverState,
|
||||
NormalDiffusion, Operators, PatchBoundaries, PatchConvection, PatchField, SideBc,
|
||||
};
|
||||
pub use embedded::{EmbeddedParameters, EmbeddedPisoSolver, EmbeddedResult, EmbeddedSolverState};
|
||||
pub use embedded_body::{
|
||||
EmbeddedBody, EmbeddedMask, FaceKind, SurfaceForce, SurfaceSample, polygon_interface_velocity,
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
//! A small CSR matrix and a Jacobi-preconditioned BiCGSTAB for the
|
||||
//! curvilinear patch's pressure equation (`docs/overset_metal_campaign.md`
|
||||
//! §5.3): the non-orthogonal operator is not symmetric, the patch is a few
|
||||
//! thousand cells, and `solve_multigrid_pcg` is hard-wired to the
|
||||
//! five-point Cartesian stencil.
|
||||
|
||||
/// Compressed sparse rows.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CsrMatrix {
|
||||
n: usize,
|
||||
row_ptr: Vec<usize>,
|
||||
col: Vec<usize>,
|
||||
val: Vec<f64>,
|
||||
}
|
||||
|
||||
impl CsrMatrix {
|
||||
/// Build from `(row, col, value)` triplets; duplicates accumulate,
|
||||
/// columns are sorted within each row.
|
||||
pub fn from_triplets(n: usize, triplets: &[(usize, usize, f64)]) -> Self {
|
||||
let mut rows: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n];
|
||||
for &(r, c, v) in triplets {
|
||||
rows[r].push((c, v));
|
||||
}
|
||||
let mut row_ptr = Vec::with_capacity(n + 1);
|
||||
let mut col = Vec::with_capacity(triplets.len());
|
||||
let mut val = Vec::with_capacity(triplets.len());
|
||||
row_ptr.push(0);
|
||||
for row in rows.iter_mut() {
|
||||
row.sort_by_key(|e| e.0);
|
||||
let mut last: Option<usize> = None;
|
||||
for &(c, v) in row.iter() {
|
||||
if last == Some(c) {
|
||||
*val.last_mut().expect("entry") += v;
|
||||
} else {
|
||||
col.push(c);
|
||||
val.push(v);
|
||||
last = Some(c);
|
||||
}
|
||||
}
|
||||
row_ptr.push(col.len());
|
||||
}
|
||||
Self {
|
||||
n,
|
||||
row_ptr,
|
||||
col,
|
||||
val,
|
||||
}
|
||||
}
|
||||
|
||||
/// Dimension.
|
||||
pub fn n(&self) -> usize {
|
||||
self.n
|
||||
}
|
||||
/// Stored entries.
|
||||
pub fn nnz(&self) -> usize {
|
||||
self.val.len()
|
||||
}
|
||||
/// `y = A x`.
|
||||
pub fn matvec(&self, x: &[f64], y: &mut [f64]) {
|
||||
for r in 0..self.n {
|
||||
let mut acc = 0.0;
|
||||
for k in self.row_ptr[r]..self.row_ptr[r + 1] {
|
||||
acc += self.val[k] * x[self.col[k]];
|
||||
}
|
||||
y[r] = acc;
|
||||
}
|
||||
}
|
||||
/// The diagonal (zero where absent).
|
||||
pub fn diagonal(&self) -> Vec<f64> {
|
||||
let mut d = vec![0.0; self.n];
|
||||
for r in 0..self.n {
|
||||
for k in self.row_ptr[r]..self.row_ptr[r + 1] {
|
||||
if self.col[k] == r {
|
||||
d[r] = self.val[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
d
|
||||
}
|
||||
/// Replace row `r` by the identity row (`x_r = b_r`): the anchor of a
|
||||
/// pure-Neumann problem.
|
||||
pub fn set_row_identity(&mut self, r: usize) {
|
||||
let mut has_diag = false;
|
||||
for k in self.row_ptr[r]..self.row_ptr[r + 1] {
|
||||
self.val[k] = if self.col[k] == r {
|
||||
has_diag = true;
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
}
|
||||
assert!(has_diag, "row {r} has no diagonal entry to anchor");
|
||||
}
|
||||
/// Row `r` as `(columns, values)`.
|
||||
pub fn row(&self, r: usize) -> (&[usize], &[f64]) {
|
||||
let (a, b) = (self.row_ptr[r], self.row_ptr[r + 1]);
|
||||
(&self.col[a..b], &self.val[a..b])
|
||||
}
|
||||
}
|
||||
|
||||
/// Outcome of a BiCGSTAB solve.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[must_use]
|
||||
pub struct BicgstabResult {
|
||||
/// Iterations taken.
|
||||
pub iterations: usize,
|
||||
/// L1 norm of the true residual `b − A x` at exit.
|
||||
pub residual: f64,
|
||||
/// Whether the residual reached the tolerance.
|
||||
pub converged: bool,
|
||||
}
|
||||
|
||||
/// Subtract the mean from `v` (the consistency projection for a singular
|
||||
/// right-hand side).
|
||||
pub fn project_mean(v: &mut [f64]) {
|
||||
let mean = v.iter().sum::<f64>() / v.len() as f64;
|
||||
for x in v.iter_mut() {
|
||||
*x -= mean;
|
||||
}
|
||||
}
|
||||
|
||||
/// Jacobi-preconditioned BiCGSTAB (van der Vorst 1992) with an L1
|
||||
/// true-residual stop; `x` is the initial guess and the result.
|
||||
pub fn bicgstab_jacobi(
|
||||
a: &CsrMatrix,
|
||||
b: &[f64],
|
||||
x: &mut [f64],
|
||||
tolerance: f64,
|
||||
max_iterations: usize,
|
||||
) -> BicgstabResult {
|
||||
let n = a.n();
|
||||
let diag = a.diagonal();
|
||||
let inv_diag: Vec<f64> = diag
|
||||
.iter()
|
||||
.map(|&d| if d != 0.0 { 1.0 / d } else { 1.0 })
|
||||
.collect();
|
||||
let l1 = |v: &[f64]| v.iter().map(|t| t.abs()).sum::<f64>();
|
||||
let dot = |u: &[f64], v: &[f64]| u.iter().zip(v).map(|(p, q)| p * q).sum::<f64>();
|
||||
|
||||
let mut r = vec![0.0; n];
|
||||
a.matvec(x, &mut r);
|
||||
for i in 0..n {
|
||||
r[i] = b[i] - r[i];
|
||||
}
|
||||
let mut res = l1(&r);
|
||||
if res <= tolerance {
|
||||
return BicgstabResult {
|
||||
iterations: 0,
|
||||
residual: res,
|
||||
converged: true,
|
||||
};
|
||||
}
|
||||
let r0 = r.clone();
|
||||
let mut p = vec![0.0; n];
|
||||
let mut v = vec![0.0; n];
|
||||
let mut s = vec![0.0; n];
|
||||
let mut t = vec![0.0; n];
|
||||
let mut y = vec![0.0; n];
|
||||
let mut z = vec![0.0; n];
|
||||
let (mut rho_old, mut alpha, mut omega) = (1.0, 1.0, 1.0);
|
||||
|
||||
for it in 1..=max_iterations {
|
||||
let rho = dot(&r0, &r);
|
||||
if rho == 0.0 || !rho.is_finite() {
|
||||
break;
|
||||
}
|
||||
let beta = (rho / rho_old) * (alpha / omega);
|
||||
for i in 0..n {
|
||||
p[i] = r[i] + beta * (p[i] - omega * v[i]);
|
||||
}
|
||||
for i in 0..n {
|
||||
y[i] = inv_diag[i] * p[i];
|
||||
}
|
||||
a.matvec(&y, &mut v);
|
||||
let r0v = dot(&r0, &v);
|
||||
if r0v == 0.0 || !r0v.is_finite() {
|
||||
break;
|
||||
}
|
||||
alpha = rho / r0v;
|
||||
for i in 0..n {
|
||||
s[i] = r[i] - alpha * v[i];
|
||||
}
|
||||
if l1(&s) <= tolerance {
|
||||
for i in 0..n {
|
||||
x[i] += alpha * y[i];
|
||||
}
|
||||
a.matvec(x, &mut r);
|
||||
for i in 0..n {
|
||||
r[i] = b[i] - r[i];
|
||||
}
|
||||
res = l1(&r);
|
||||
return BicgstabResult {
|
||||
iterations: it,
|
||||
residual: res,
|
||||
converged: res <= tolerance,
|
||||
};
|
||||
}
|
||||
for i in 0..n {
|
||||
z[i] = inv_diag[i] * s[i];
|
||||
}
|
||||
a.matvec(&z, &mut t);
|
||||
let tt = dot(&t, &t);
|
||||
omega = if tt > 0.0 { dot(&t, &s) / tt } else { 0.0 };
|
||||
for i in 0..n {
|
||||
x[i] += alpha * y[i] + omega * z[i];
|
||||
r[i] = s[i] - omega * t[i];
|
||||
}
|
||||
rho_old = rho;
|
||||
// The recurrence residual drifts from the true one; check the true
|
||||
// residual whenever the recurrence claims convergence.
|
||||
if l1(&r) <= tolerance {
|
||||
a.matvec(x, &mut r);
|
||||
for i in 0..n {
|
||||
r[i] = b[i] - r[i];
|
||||
}
|
||||
res = l1(&r);
|
||||
if res <= tolerance {
|
||||
return BicgstabResult {
|
||||
iterations: it,
|
||||
residual: res,
|
||||
converged: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
if omega == 0.0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
a.matvec(x, &mut r);
|
||||
for i in 0..n {
|
||||
r[i] = b[i] - r[i];
|
||||
}
|
||||
res = l1(&r);
|
||||
BicgstabResult {
|
||||
iterations: max_iterations,
|
||||
residual: res,
|
||||
converged: res <= tolerance,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_small_nonsymmetric_system_is_solved_to_rounding() {
|
||||
// Diagonally dominant, non-symmetric.
|
||||
let dense = [
|
||||
[4.0, -1.0, 0.0, 0.5, 0.0],
|
||||
[-0.5, 5.0, -1.0, 0.0, 0.2],
|
||||
[0.0, -1.5, 6.0, -1.0, 0.0],
|
||||
[0.1, 0.0, -1.0, 4.0, -1.0],
|
||||
[0.0, 0.3, 0.0, -0.5, 3.0],
|
||||
];
|
||||
let mut tri = Vec::new();
|
||||
for (r, row) in dense.iter().enumerate() {
|
||||
for (c, &v) in row.iter().enumerate() {
|
||||
if v != 0.0 {
|
||||
tri.push((r, c, v));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Duplicate entry: must accumulate.
|
||||
tri.push((0, 1, -0.5));
|
||||
tri.push((0, 1, 0.5));
|
||||
let a = CsrMatrix::from_triplets(5, &tri);
|
||||
assert_eq!(a.nnz(), 17);
|
||||
let x_true = [1.0, -2.0, 3.0, 0.5, -1.5];
|
||||
let mut b = vec![0.0; 5];
|
||||
a.matvec(&x_true, &mut b);
|
||||
let mut x = vec![0.0; 5];
|
||||
let out = bicgstab_jacobi(&a, &b, &mut x, 1e-13, 100);
|
||||
assert!(out.converged, "{out:?}");
|
||||
for i in 0..5 {
|
||||
assert!(
|
||||
(x[i] - x_true[i]).abs() < 1e-11,
|
||||
"x[{i}] = {} vs {}",
|
||||
x[i],
|
||||
x_true[i]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_anchored_periodic_laplacian_recovers_a_periodic_field_up_to_a_constant() {
|
||||
// 1-D periodic second difference (singular): anchor one row, project
|
||||
// the mean out of the right-hand side.
|
||||
let n = 64;
|
||||
let h = 1.0 / n as f64;
|
||||
let mut tri = Vec::new();
|
||||
for i in 0..n {
|
||||
tri.push((i, i, 2.0 / (h * h)));
|
||||
tri.push((i, (i + 1) % n, -1.0 / (h * h)));
|
||||
tri.push((i, (i + n - 1) % n, -1.0 / (h * h)));
|
||||
}
|
||||
let mut a = CsrMatrix::from_triplets(n, &tri);
|
||||
let phi = |x: f64| (2.0 * std::f64::consts::PI * x).sin();
|
||||
let exact: Vec<f64> = (0..n).map(|i| phi((i as f64 + 0.5) * h)).collect();
|
||||
let mut b = vec![0.0; n];
|
||||
a.matvec(&exact, &mut b);
|
||||
b[3] += 1e-3; // an inconsistent perturbation the projection must remove
|
||||
project_mean(&mut b);
|
||||
a.set_row_identity(0);
|
||||
b[0] = 0.0;
|
||||
let mut x = vec![0.0; n];
|
||||
// 1e-9 absolute: |A| ~ 1/h² = 4e3 and |x| ~ 1 put the rounding floor near 1e-10.
|
||||
let out = bicgstab_jacobi(&a, &b, &mut x, 1e-9, 2000);
|
||||
assert!(out.converged, "{out:?}");
|
||||
let shift = exact[0] - x[0];
|
||||
let worst = (0..n)
|
||||
.map(|i| (x[i] + shift - exact[i]).abs())
|
||||
.fold(0.0, f64::max);
|
||||
assert!(worst < 2e-3, "worst {worst:.3e}"); // the 1e-3 perturbation's response bounds it
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user