rtx-cfd: OversetPisoSolver::momentum_residual — the background predictor's own staggered stencil (u_rhs/v_rhs, factored out of the predictor bit-identically) evaluated on every face of a NaN-masked field; solved faces read rounding, the active–fringe interface reads the composite's pressure level offset δ·h (cancels in the sum), prescribed fringe–fringe / fringe–hole faces read the stamping's momentum injection; hole ghosts (p, u, v) from a band widened three rows into the hole make every ring face evaluable; overset_cfd1 prints the buckets, the ring x-bands and δ at the settled state; pin: residual vanishes on the solved faces
CI / Format Check (push) Canceled after 0s
Performance Benchmarks / Run Benchmarks (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 / Distributed Training Tests (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 / Format Check (push) Canceled after 0s
Performance Benchmarks / Run Benchmarks (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 / Distributed Training Tests (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
cbec40b999
commit
6f9b0d43b2
@@ -41,7 +41,7 @@ mod projection;
|
|||||||
use super::ale::{AleBoundaries, SideBoundary};
|
use super::ale::{AleBoundaries, SideBoundary};
|
||||||
use super::embedded_body::{EmbeddedBody, EmbeddedMask, FaceKind};
|
use super::embedded_body::{EmbeddedBody, EmbeddedMask, FaceKind};
|
||||||
use super::poisson::{
|
use super::poisson::{
|
||||||
MgPrecision, MultigridParameters, PoissonProblem, PoissonSolverKind, solve_multigrid_pcg,
|
solve_multigrid_pcg, MgPrecision, MultigridParameters, PoissonProblem, PoissonSolverKind,
|
||||||
};
|
};
|
||||||
use super::simple::ConvectionScheme;
|
use super::simple::ConvectionScheme;
|
||||||
use super::{FlowField, SolverResult};
|
use super::{FlowField, SolverResult};
|
||||||
@@ -480,147 +480,280 @@ impl EmbeddedPisoSolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The predictor's right-hand side on the u face `(j, i)`, `i = 1..nx`,
|
||||||
|
/// from `field.u_old`, `field.v_old` and `field.p`: `−conv + diff −
|
||||||
|
/// ∇p/ρ + f/ρ`, expression for expression the fixed-grid PISO's. The
|
||||||
|
/// predictor writes `u_old + dt · rhs` on the fluid faces; the overset
|
||||||
|
/// momentum-residual diagnostic (P4 option B) evaluates the same
|
||||||
|
/// operator on the prescribed faces, so "the solver's own stencil" is
|
||||||
|
/// this function by construction.
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
|
pub(crate) fn u_rhs(&self, field: &FlowField, j: usize, i: usize, t_old: f64) -> f64 {
|
||||||
|
let (nx, ny, dx, dy) = field.grid_info();
|
||||||
|
let rho = self.config.density;
|
||||||
|
let nu = self.config.viscosity / rho;
|
||||||
|
let b = self.parameters.boundaries;
|
||||||
|
let velocity = SideBoundary::Velocity;
|
||||||
|
let uo = &field.u_old;
|
||||||
|
let vo = &field.v_old;
|
||||||
|
let u_p = uo[(j, i)];
|
||||||
|
|
||||||
|
let ue_face = 0.5 * (uo[(j, i)] + uo[(j, i + 1)]);
|
||||||
|
let uw_face = 0.5 * (uo[(j, i - 1)] + uo[(j, i)]);
|
||||||
|
|
||||||
|
let south_is_wall = j == 0;
|
||||||
|
let north_is_wall = j + 1 == ny;
|
||||||
|
|
||||||
|
// Transverse face velocities from the stored v faces — on a
|
||||||
|
// domain side these are the prescribed boundary normals
|
||||||
|
// (zero on a wall, the outflow on an outlet). The fixed-grid
|
||||||
|
// PISO zeroes them on its walls, which is the same number on
|
||||||
|
// a wall and wrong on an outlet: the outgoing mass flux must
|
||||||
|
// carry momentum out, or the last row accumulates it.
|
||||||
|
let vn_face = 0.5 * (vo[(j + 1, i - 1)] + vo[(j + 1, i)]);
|
||||||
|
let vs_face = 0.5 * (vo[(j, i - 1)] + vo[(j, i)]);
|
||||||
|
|
||||||
|
// Upwind value across a domain side: the boundary function's
|
||||||
|
// tangential value on a Velocity side, the interior value
|
||||||
|
// otherwise (zero-gradient).
|
||||||
|
let beyond_north = if b.top == velocity {
|
||||||
|
self.boundary(i as f64 * dx, ny as f64 * dy, t_old).0
|
||||||
|
} else {
|
||||||
|
u_p
|
||||||
|
};
|
||||||
|
let beyond_south = if b.bottom == velocity {
|
||||||
|
self.boundary(i as f64 * dx, 0.0, t_old).0
|
||||||
|
} else {
|
||||||
|
u_p
|
||||||
|
};
|
||||||
|
|
||||||
|
let conv_x = (ue_face * Self::upwind(ue_face, uo[(j, i)], uo[(j, i + 1)])
|
||||||
|
- uw_face * Self::upwind(uw_face, uo[(j, i - 1)], uo[(j, i)]))
|
||||||
|
/ dx;
|
||||||
|
let conv_y = (vn_face
|
||||||
|
* if north_is_wall {
|
||||||
|
Self::upwind(vn_face, u_p, beyond_north)
|
||||||
|
} else {
|
||||||
|
Self::upwind(vn_face, uo[(j, i)], uo[(j + 1, i)])
|
||||||
|
}
|
||||||
|
- vs_face
|
||||||
|
* if south_is_wall {
|
||||||
|
Self::upwind(vs_face, beyond_south, u_p)
|
||||||
|
} else {
|
||||||
|
Self::upwind(vs_face, uo[(j - 1, i)], uo[(j, i)])
|
||||||
|
})
|
||||||
|
/ dy;
|
||||||
|
|
||||||
|
// Limited (TVD) corrections to the four convective face
|
||||||
|
// values; exactly zero-cost on the default upwind scheme.
|
||||||
|
let scheme = self.parameters.convection_scheme;
|
||||||
|
let mut conv_x = conv_x;
|
||||||
|
let mut conv_y = conv_y;
|
||||||
|
if scheme != ConvectionScheme::Upwind {
|
||||||
|
let delta_e = if ue_face >= 0.0 {
|
||||||
|
scheme.face_correction(Some(uo[(j, i - 1)]), uo[(j, i)], uo[(j, i + 1)])
|
||||||
|
} else {
|
||||||
|
let far = (i + 2 <= nx).then(|| uo[(j, i + 2)]);
|
||||||
|
scheme.face_correction(far, uo[(j, i + 1)], uo[(j, i)])
|
||||||
|
};
|
||||||
|
let delta_w = if uw_face >= 0.0 {
|
||||||
|
let far = (i >= 2).then(|| uo[(j, i - 2)]);
|
||||||
|
scheme.face_correction(far, uo[(j, i - 1)], uo[(j, i)])
|
||||||
|
} else {
|
||||||
|
scheme.face_correction(Some(uo[(j, i + 1)]), uo[(j, i)], uo[(j, i - 1)])
|
||||||
|
};
|
||||||
|
let delta_n = if north_is_wall {
|
||||||
|
0.0
|
||||||
|
} else if vn_face >= 0.0 {
|
||||||
|
let far = (j >= 1).then(|| uo[(j - 1, i)]);
|
||||||
|
scheme.face_correction(far, uo[(j, i)], uo[(j + 1, i)])
|
||||||
|
} else {
|
||||||
|
let far = (j + 2 < ny).then(|| uo[(j + 2, i)]);
|
||||||
|
scheme.face_correction(far, uo[(j + 1, i)], uo[(j, i)])
|
||||||
|
};
|
||||||
|
let delta_s = if south_is_wall {
|
||||||
|
0.0
|
||||||
|
} else if vs_face >= 0.0 {
|
||||||
|
let far = (j >= 2).then(|| uo[(j - 2, i)]);
|
||||||
|
scheme.face_correction(far, uo[(j - 1, i)], uo[(j, i)])
|
||||||
|
} else {
|
||||||
|
let far = (j + 1 < ny).then(|| uo[(j + 1, i)]);
|
||||||
|
scheme.face_correction(far, uo[(j, i)], uo[(j - 1, i)])
|
||||||
|
};
|
||||||
|
conv_x += (ue_face * delta_e - uw_face * delta_w) / dx;
|
||||||
|
conv_y += (vn_face * delta_n - vs_face * delta_s) / dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
let diff_x = nu * (uo[(j, i + 1)] - 2.0 * u_p + uo[(j, i - 1)]) / (dx * dx);
|
||||||
|
|
||||||
|
// Wall-adjacent diffusive fluxes act over half a cell on a
|
||||||
|
// Velocity side; a slip wall or outlet carries no shear.
|
||||||
|
let flux_north = if north_is_wall {
|
||||||
|
if b.top == velocity {
|
||||||
|
let u_wall = self.boundary(i as f64 * dx, ny as f64 * dy, t_old).0;
|
||||||
|
nu * (u_wall - u_p) / (0.5 * dy)
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nu * (uo[(j + 1, i)] - u_p) / dy
|
||||||
|
};
|
||||||
|
let flux_south = if south_is_wall {
|
||||||
|
if b.bottom == velocity {
|
||||||
|
let u_wall = self.boundary(i as f64 * dx, 0.0, t_old).0;
|
||||||
|
nu * (u_p - u_wall) / (0.5 * dy)
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nu * (u_p - uo[(j - 1, i)]) / dy
|
||||||
|
};
|
||||||
|
let diff_y = (flux_north - flux_south) / dy;
|
||||||
|
|
||||||
|
let pressure_gradient = -(field.p[(j, i)] - field.p[(j, i - 1)]) / (rho * dx);
|
||||||
|
|
||||||
|
let body_force = self.momentum_source.as_ref().map_or(0.0, |f| {
|
||||||
|
f(i as f64 * dx, (j as f64 + 0.5) * dy, t_old).0 / rho
|
||||||
|
});
|
||||||
|
|
||||||
|
-conv_x - conv_y + diff_x + diff_y + pressure_gradient + body_force
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See [`Self::u_rhs`]: the v face `(j, i)`, `j = 1..ny`.
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
|
pub(crate) fn v_rhs(&self, field: &FlowField, j: usize, i: usize, t_old: f64) -> f64 {
|
||||||
|
let (nx, ny, dx, dy) = field.grid_info();
|
||||||
|
let rho = self.config.density;
|
||||||
|
let nu = self.config.viscosity / rho;
|
||||||
|
let b = self.parameters.boundaries;
|
||||||
|
let velocity = SideBoundary::Velocity;
|
||||||
|
let uo = &field.u_old;
|
||||||
|
let vo = &field.v_old;
|
||||||
|
let v_p = vo[(j, i)];
|
||||||
|
|
||||||
|
let vn_face = 0.5 * (vo[(j, i)] + vo[(j + 1, i)]);
|
||||||
|
let vs_face = 0.5 * (vo[(j - 1, i)] + vo[(j, i)]);
|
||||||
|
|
||||||
|
let west_is_wall = i == 0;
|
||||||
|
let east_is_wall = i + 1 == nx;
|
||||||
|
|
||||||
|
let ue_face = 0.5 * (uo[(j - 1, i + 1)] + uo[(j, i + 1)]);
|
||||||
|
let uw_face = 0.5 * (uo[(j - 1, i)] + uo[(j, i)]);
|
||||||
|
let beyond_east = if b.right == velocity {
|
||||||
|
self.boundary(nx as f64 * dx, j as f64 * dy, t_old).1
|
||||||
|
} else {
|
||||||
|
v_p
|
||||||
|
};
|
||||||
|
let beyond_west = if b.left == velocity {
|
||||||
|
self.boundary(0.0, j as f64 * dy, t_old).1
|
||||||
|
} else {
|
||||||
|
v_p
|
||||||
|
};
|
||||||
|
|
||||||
|
let conv_y = (vn_face * Self::upwind(vn_face, vo[(j, i)], vo[(j + 1, i)])
|
||||||
|
- vs_face * Self::upwind(vs_face, vo[(j - 1, i)], vo[(j, i)]))
|
||||||
|
/ dy;
|
||||||
|
let conv_x = (ue_face
|
||||||
|
* if east_is_wall {
|
||||||
|
Self::upwind(ue_face, v_p, beyond_east)
|
||||||
|
} else {
|
||||||
|
Self::upwind(ue_face, vo[(j, i)], vo[(j, i + 1)])
|
||||||
|
}
|
||||||
|
- uw_face
|
||||||
|
* if west_is_wall {
|
||||||
|
Self::upwind(uw_face, beyond_west, v_p)
|
||||||
|
} else {
|
||||||
|
Self::upwind(uw_face, vo[(j, i - 1)], vo[(j, i)])
|
||||||
|
})
|
||||||
|
/ dx;
|
||||||
|
|
||||||
|
let scheme = self.parameters.convection_scheme;
|
||||||
|
let mut conv_x = conv_x;
|
||||||
|
let mut conv_y = conv_y;
|
||||||
|
if scheme != ConvectionScheme::Upwind {
|
||||||
|
let delta_n = if vn_face >= 0.0 {
|
||||||
|
scheme.face_correction(Some(vo[(j - 1, i)]), vo[(j, i)], vo[(j + 1, i)])
|
||||||
|
} else {
|
||||||
|
let far = (j + 2 <= ny).then(|| vo[(j + 2, i)]);
|
||||||
|
scheme.face_correction(far, vo[(j + 1, i)], vo[(j, i)])
|
||||||
|
};
|
||||||
|
let delta_s = if vs_face >= 0.0 {
|
||||||
|
let far = (j >= 2).then(|| vo[(j - 2, i)]);
|
||||||
|
scheme.face_correction(far, vo[(j - 1, i)], vo[(j, i)])
|
||||||
|
} else {
|
||||||
|
scheme.face_correction(Some(vo[(j + 1, i)]), vo[(j, i)], vo[(j - 1, i)])
|
||||||
|
};
|
||||||
|
let delta_e = if east_is_wall {
|
||||||
|
0.0
|
||||||
|
} else if ue_face >= 0.0 {
|
||||||
|
let far = (i >= 1).then(|| vo[(j, i - 1)]);
|
||||||
|
scheme.face_correction(far, vo[(j, i)], vo[(j, i + 1)])
|
||||||
|
} else {
|
||||||
|
let far = (i + 2 < nx).then(|| vo[(j, i + 2)]);
|
||||||
|
scheme.face_correction(far, vo[(j, i + 1)], vo[(j, i)])
|
||||||
|
};
|
||||||
|
let delta_w = if west_is_wall {
|
||||||
|
0.0
|
||||||
|
} else if uw_face >= 0.0 {
|
||||||
|
let far = (i >= 2).then(|| vo[(j, i - 2)]);
|
||||||
|
scheme.face_correction(far, vo[(j, i - 1)], vo[(j, i)])
|
||||||
|
} else {
|
||||||
|
let far = (i + 1 < nx).then(|| vo[(j, i + 1)]);
|
||||||
|
scheme.face_correction(far, vo[(j, i)], vo[(j, i - 1)])
|
||||||
|
};
|
||||||
|
conv_y += (vn_face * delta_n - vs_face * delta_s) / dy;
|
||||||
|
conv_x += (ue_face * delta_e - uw_face * delta_w) / dx;
|
||||||
|
}
|
||||||
|
|
||||||
|
let diff_y = nu * (vo[(j + 1, i)] - 2.0 * v_p + vo[(j - 1, i)]) / (dy * dy);
|
||||||
|
|
||||||
|
let flux_east = if east_is_wall {
|
||||||
|
if b.right == velocity {
|
||||||
|
let v_wall = self.boundary(nx as f64 * dx, j as f64 * dy, t_old).1;
|
||||||
|
nu * (v_wall - v_p) / (0.5 * dx)
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nu * (vo[(j, i + 1)] - v_p) / dx
|
||||||
|
};
|
||||||
|
let flux_west = if west_is_wall {
|
||||||
|
if b.left == velocity {
|
||||||
|
let v_wall = self.boundary(0.0, j as f64 * dy, t_old).1;
|
||||||
|
nu * (v_p - v_wall) / (0.5 * dx)
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nu * (v_p - vo[(j, i - 1)]) / dx
|
||||||
|
};
|
||||||
|
let diff_x = (flux_east - flux_west) / dx;
|
||||||
|
|
||||||
|
let pressure_gradient = -(field.p[(j, i)] - field.p[(j - 1, i)]) / (rho * dy);
|
||||||
|
|
||||||
|
let body_force = self.momentum_source.as_ref().map_or(0.0, |f| {
|
||||||
|
f((i as f64 + 0.5) * dx, j as f64 * dy, t_old).1 / rho
|
||||||
|
});
|
||||||
|
|
||||||
|
-conv_x - conv_y + diff_x + diff_y + pressure_gradient + body_force
|
||||||
|
}
|
||||||
|
|
||||||
/// Explicit momentum predictor on the fluid faces, expression for
|
/// Explicit momentum predictor on the fluid faces, expression for
|
||||||
/// expression the fixed-grid PISO's (so the no-body case is identical
|
/// expression the fixed-grid PISO's (so the no-body case is identical
|
||||||
/// to the bit), plus the slip-wall / outlet arms of the ALE solver on
|
/// to the bit), plus the slip-wall / outlet arms of the ALE solver on
|
||||||
/// the domain sides. Non-fluid faces keep their prescribed values.
|
/// the domain sides. Non-fluid faces keep their prescribed values.
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
fn momentum_predictor(&self, field: &mut FlowField, dt: f64, t_old: f64) -> CfdResult<()> {
|
fn momentum_predictor(&self, field: &mut FlowField, dt: f64, t_old: f64) -> CfdResult<()> {
|
||||||
let (nx, ny, dx, dy) = field.grid_info();
|
let (nx, ny, _, _) = field.grid_info();
|
||||||
let rho = self.config.density;
|
|
||||||
let nu = self.config.viscosity / rho;
|
|
||||||
let b = self.parameters.boundaries;
|
let b = self.parameters.boundaries;
|
||||||
let velocity = SideBoundary::Velocity;
|
|
||||||
|
|
||||||
for j in 0..ny {
|
for j in 0..ny {
|
||||||
for i in 1..nx {
|
for i in 1..nx {
|
||||||
if !self.u_is_fluid(j, i) {
|
if !self.u_is_fluid(j, i) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let uo = &field.u_old;
|
let rhs = self.u_rhs(field, j, i, t_old);
|
||||||
let vo = &field.v_old;
|
field.u[(j, i)] = field.u_old[(j, i)] + dt * rhs;
|
||||||
let u_p = uo[(j, i)];
|
|
||||||
|
|
||||||
let ue_face = 0.5 * (uo[(j, i)] + uo[(j, i + 1)]);
|
|
||||||
let uw_face = 0.5 * (uo[(j, i - 1)] + uo[(j, i)]);
|
|
||||||
|
|
||||||
let south_is_wall = j == 0;
|
|
||||||
let north_is_wall = j + 1 == ny;
|
|
||||||
|
|
||||||
// Transverse face velocities from the stored v faces — on a
|
|
||||||
// domain side these are the prescribed boundary normals
|
|
||||||
// (zero on a wall, the outflow on an outlet). The fixed-grid
|
|
||||||
// PISO zeroes them on its walls, which is the same number on
|
|
||||||
// a wall and wrong on an outlet: the outgoing mass flux must
|
|
||||||
// carry momentum out, or the last row accumulates it.
|
|
||||||
let vn_face = 0.5 * (vo[(j + 1, i - 1)] + vo[(j + 1, i)]);
|
|
||||||
let vs_face = 0.5 * (vo[(j, i - 1)] + vo[(j, i)]);
|
|
||||||
|
|
||||||
// Upwind value across a domain side: the boundary function's
|
|
||||||
// tangential value on a Velocity side, the interior value
|
|
||||||
// otherwise (zero-gradient).
|
|
||||||
let beyond_north = if b.top == velocity {
|
|
||||||
self.boundary(i as f64 * dx, ny as f64 * dy, t_old).0
|
|
||||||
} else {
|
|
||||||
u_p
|
|
||||||
};
|
|
||||||
let beyond_south = if b.bottom == velocity {
|
|
||||||
self.boundary(i as f64 * dx, 0.0, t_old).0
|
|
||||||
} else {
|
|
||||||
u_p
|
|
||||||
};
|
|
||||||
|
|
||||||
let conv_x = (ue_face * Self::upwind(ue_face, uo[(j, i)], uo[(j, i + 1)])
|
|
||||||
- uw_face * Self::upwind(uw_face, uo[(j, i - 1)], uo[(j, i)]))
|
|
||||||
/ dx;
|
|
||||||
let conv_y = (vn_face
|
|
||||||
* if north_is_wall {
|
|
||||||
Self::upwind(vn_face, u_p, beyond_north)
|
|
||||||
} else {
|
|
||||||
Self::upwind(vn_face, uo[(j, i)], uo[(j + 1, i)])
|
|
||||||
}
|
|
||||||
- vs_face
|
|
||||||
* if south_is_wall {
|
|
||||||
Self::upwind(vs_face, beyond_south, u_p)
|
|
||||||
} else {
|
|
||||||
Self::upwind(vs_face, uo[(j - 1, i)], uo[(j, i)])
|
|
||||||
})
|
|
||||||
/ dy;
|
|
||||||
|
|
||||||
// Limited (TVD) corrections to the four convective face
|
|
||||||
// values; exactly zero-cost on the default upwind scheme.
|
|
||||||
let scheme = self.parameters.convection_scheme;
|
|
||||||
let mut conv_x = conv_x;
|
|
||||||
let mut conv_y = conv_y;
|
|
||||||
if scheme != ConvectionScheme::Upwind {
|
|
||||||
let delta_e = if ue_face >= 0.0 {
|
|
||||||
scheme.face_correction(Some(uo[(j, i - 1)]), uo[(j, i)], uo[(j, i + 1)])
|
|
||||||
} else {
|
|
||||||
let far = (i + 2 <= nx).then(|| uo[(j, i + 2)]);
|
|
||||||
scheme.face_correction(far, uo[(j, i + 1)], uo[(j, i)])
|
|
||||||
};
|
|
||||||
let delta_w = if uw_face >= 0.0 {
|
|
||||||
let far = (i >= 2).then(|| uo[(j, i - 2)]);
|
|
||||||
scheme.face_correction(far, uo[(j, i - 1)], uo[(j, i)])
|
|
||||||
} else {
|
|
||||||
scheme.face_correction(Some(uo[(j, i + 1)]), uo[(j, i)], uo[(j, i - 1)])
|
|
||||||
};
|
|
||||||
let delta_n = if north_is_wall {
|
|
||||||
0.0
|
|
||||||
} else if vn_face >= 0.0 {
|
|
||||||
let far = (j >= 1).then(|| uo[(j - 1, i)]);
|
|
||||||
scheme.face_correction(far, uo[(j, i)], uo[(j + 1, i)])
|
|
||||||
} else {
|
|
||||||
let far = (j + 2 < ny).then(|| uo[(j + 2, i)]);
|
|
||||||
scheme.face_correction(far, uo[(j + 1, i)], uo[(j, i)])
|
|
||||||
};
|
|
||||||
let delta_s = if south_is_wall {
|
|
||||||
0.0
|
|
||||||
} else if vs_face >= 0.0 {
|
|
||||||
let far = (j >= 2).then(|| uo[(j - 2, i)]);
|
|
||||||
scheme.face_correction(far, uo[(j - 1, i)], uo[(j, i)])
|
|
||||||
} else {
|
|
||||||
let far = (j + 1 < ny).then(|| uo[(j + 1, i)]);
|
|
||||||
scheme.face_correction(far, uo[(j, i)], uo[(j - 1, i)])
|
|
||||||
};
|
|
||||||
conv_x += (ue_face * delta_e - uw_face * delta_w) / dx;
|
|
||||||
conv_y += (vn_face * delta_n - vs_face * delta_s) / dy;
|
|
||||||
}
|
|
||||||
|
|
||||||
let diff_x = nu * (uo[(j, i + 1)] - 2.0 * u_p + uo[(j, i - 1)]) / (dx * dx);
|
|
||||||
|
|
||||||
// Wall-adjacent diffusive fluxes act over half a cell on a
|
|
||||||
// Velocity side; a slip wall or outlet carries no shear.
|
|
||||||
let flux_north = if north_is_wall {
|
|
||||||
if b.top == velocity {
|
|
||||||
let u_wall = self.boundary(i as f64 * dx, ny as f64 * dy, t_old).0;
|
|
||||||
nu * (u_wall - u_p) / (0.5 * dy)
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
nu * (uo[(j + 1, i)] - u_p) / dy
|
|
||||||
};
|
|
||||||
let flux_south = if south_is_wall {
|
|
||||||
if b.bottom == velocity {
|
|
||||||
let u_wall = self.boundary(i as f64 * dx, 0.0, t_old).0;
|
|
||||||
nu * (u_p - u_wall) / (0.5 * dy)
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
nu * (u_p - uo[(j - 1, i)]) / dy
|
|
||||||
};
|
|
||||||
let diff_y = (flux_north - flux_south) / dy;
|
|
||||||
|
|
||||||
let pressure_gradient = -(field.p[(j, i)] - field.p[(j, i - 1)]) / (rho * dx);
|
|
||||||
|
|
||||||
let body_force = self.momentum_source.as_ref().map_or(0.0, |f| {
|
|
||||||
f(i as f64 * dx, (j as f64 + 0.5) * dy, t_old).0 / rho
|
|
||||||
});
|
|
||||||
|
|
||||||
field.u[(j, i)] = u_p
|
|
||||||
+ dt * (-conv_x - conv_y + diff_x + diff_y + pressure_gradient + body_force);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -629,116 +762,8 @@ impl EmbeddedPisoSolver {
|
|||||||
if !self.v_is_fluid(j, i) {
|
if !self.v_is_fluid(j, i) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let uo = &field.u_old;
|
let rhs = self.v_rhs(field, j, i, t_old);
|
||||||
let vo = &field.v_old;
|
field.v[(j, i)] = field.v_old[(j, i)] + dt * rhs;
|
||||||
let v_p = vo[(j, i)];
|
|
||||||
|
|
||||||
let vn_face = 0.5 * (vo[(j, i)] + vo[(j + 1, i)]);
|
|
||||||
let vs_face = 0.5 * (vo[(j - 1, i)] + vo[(j, i)]);
|
|
||||||
|
|
||||||
let west_is_wall = i == 0;
|
|
||||||
let east_is_wall = i + 1 == nx;
|
|
||||||
|
|
||||||
let ue_face = 0.5 * (uo[(j - 1, i + 1)] + uo[(j, i + 1)]);
|
|
||||||
let uw_face = 0.5 * (uo[(j - 1, i)] + uo[(j, i)]);
|
|
||||||
let beyond_east = if b.right == velocity {
|
|
||||||
self.boundary(nx as f64 * dx, j as f64 * dy, t_old).1
|
|
||||||
} else {
|
|
||||||
v_p
|
|
||||||
};
|
|
||||||
let beyond_west = if b.left == velocity {
|
|
||||||
self.boundary(0.0, j as f64 * dy, t_old).1
|
|
||||||
} else {
|
|
||||||
v_p
|
|
||||||
};
|
|
||||||
|
|
||||||
let conv_y = (vn_face * Self::upwind(vn_face, vo[(j, i)], vo[(j + 1, i)])
|
|
||||||
- vs_face * Self::upwind(vs_face, vo[(j - 1, i)], vo[(j, i)]))
|
|
||||||
/ dy;
|
|
||||||
let conv_x = (ue_face
|
|
||||||
* if east_is_wall {
|
|
||||||
Self::upwind(ue_face, v_p, beyond_east)
|
|
||||||
} else {
|
|
||||||
Self::upwind(ue_face, vo[(j, i)], vo[(j, i + 1)])
|
|
||||||
}
|
|
||||||
- uw_face
|
|
||||||
* if west_is_wall {
|
|
||||||
Self::upwind(uw_face, beyond_west, v_p)
|
|
||||||
} else {
|
|
||||||
Self::upwind(uw_face, vo[(j, i - 1)], vo[(j, i)])
|
|
||||||
})
|
|
||||||
/ dx;
|
|
||||||
|
|
||||||
let scheme = self.parameters.convection_scheme;
|
|
||||||
let mut conv_x = conv_x;
|
|
||||||
let mut conv_y = conv_y;
|
|
||||||
if scheme != ConvectionScheme::Upwind {
|
|
||||||
let delta_n = if vn_face >= 0.0 {
|
|
||||||
scheme.face_correction(Some(vo[(j - 1, i)]), vo[(j, i)], vo[(j + 1, i)])
|
|
||||||
} else {
|
|
||||||
let far = (j + 2 <= ny).then(|| vo[(j + 2, i)]);
|
|
||||||
scheme.face_correction(far, vo[(j + 1, i)], vo[(j, i)])
|
|
||||||
};
|
|
||||||
let delta_s = if vs_face >= 0.0 {
|
|
||||||
let far = (j >= 2).then(|| vo[(j - 2, i)]);
|
|
||||||
scheme.face_correction(far, vo[(j - 1, i)], vo[(j, i)])
|
|
||||||
} else {
|
|
||||||
scheme.face_correction(Some(vo[(j + 1, i)]), vo[(j, i)], vo[(j - 1, i)])
|
|
||||||
};
|
|
||||||
let delta_e = if east_is_wall {
|
|
||||||
0.0
|
|
||||||
} else if ue_face >= 0.0 {
|
|
||||||
let far = (i >= 1).then(|| vo[(j, i - 1)]);
|
|
||||||
scheme.face_correction(far, vo[(j, i)], vo[(j, i + 1)])
|
|
||||||
} else {
|
|
||||||
let far = (i + 2 < nx).then(|| vo[(j, i + 2)]);
|
|
||||||
scheme.face_correction(far, vo[(j, i + 1)], vo[(j, i)])
|
|
||||||
};
|
|
||||||
let delta_w = if west_is_wall {
|
|
||||||
0.0
|
|
||||||
} else if uw_face >= 0.0 {
|
|
||||||
let far = (i >= 2).then(|| vo[(j, i - 2)]);
|
|
||||||
scheme.face_correction(far, vo[(j, i - 1)], vo[(j, i)])
|
|
||||||
} else {
|
|
||||||
let far = (i + 1 < nx).then(|| vo[(j, i + 1)]);
|
|
||||||
scheme.face_correction(far, vo[(j, i)], vo[(j, i - 1)])
|
|
||||||
};
|
|
||||||
conv_y += (vn_face * delta_n - vs_face * delta_s) / dy;
|
|
||||||
conv_x += (ue_face * delta_e - uw_face * delta_w) / dx;
|
|
||||||
}
|
|
||||||
|
|
||||||
let diff_y = nu * (vo[(j + 1, i)] - 2.0 * v_p + vo[(j - 1, i)]) / (dy * dy);
|
|
||||||
|
|
||||||
let flux_east = if east_is_wall {
|
|
||||||
if b.right == velocity {
|
|
||||||
let v_wall = self.boundary(nx as f64 * dx, j as f64 * dy, t_old).1;
|
|
||||||
nu * (v_wall - v_p) / (0.5 * dx)
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
nu * (vo[(j, i + 1)] - v_p) / dx
|
|
||||||
};
|
|
||||||
let flux_west = if west_is_wall {
|
|
||||||
if b.left == velocity {
|
|
||||||
let v_wall = self.boundary(0.0, j as f64 * dy, t_old).1;
|
|
||||||
nu * (v_p - v_wall) / (0.5 * dx)
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
nu * (v_p - vo[(j, i - 1)]) / dx
|
|
||||||
};
|
|
||||||
let diff_x = (flux_east - flux_west) / dx;
|
|
||||||
|
|
||||||
let pressure_gradient = -(field.p[(j, i)] - field.p[(j - 1, i)]) / (rho * dy);
|
|
||||||
|
|
||||||
let body_force = self.momentum_source.as_ref().map_or(0.0, |f| {
|
|
||||||
f((i as f64 + 0.5) * dx, j as f64 * dy, t_old).1 / rho
|
|
||||||
});
|
|
||||||
|
|
||||||
field.v[(j, i)] = v_p
|
|
||||||
+ dt * (-conv_x - conv_y + diff_x + diff_y + pressure_gradient + body_force);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,13 +53,13 @@ pub use curvilinear::{
|
|||||||
};
|
};
|
||||||
pub use embedded::{EmbeddedParameters, EmbeddedPisoSolver, EmbeddedResult, EmbeddedSolverState};
|
pub use embedded::{EmbeddedParameters, EmbeddedPisoSolver, EmbeddedResult, EmbeddedSolverState};
|
||||||
pub use embedded_body::{
|
pub use embedded_body::{
|
||||||
EmbeddedBody, EmbeddedMask, FaceKind, SurfaceForce, SurfaceSample, polygon_interface_velocity,
|
polygon_interface_velocity, polygon_signed_distance, EmbeddedBody, EmbeddedMask, FaceKind,
|
||||||
polygon_signed_distance,
|
SurfaceForce, SurfaceSample,
|
||||||
};
|
};
|
||||||
pub use flow_field::FlowField;
|
pub use flow_field::FlowField;
|
||||||
pub use overset::{
|
pub use overset::{
|
||||||
CellClass, OverlapMap, OversetField, OversetParameters, OversetPisoSolver, OversetResult,
|
CellClass, MomentumResidual, OverlapMap, OversetField, OversetParameters, OversetPisoSolver,
|
||||||
OversetSolverState,
|
OversetResult, OversetSolverState, ResidualBucket,
|
||||||
};
|
};
|
||||||
pub use piso::{PisoParameters, PisoResult, PisoSolver};
|
pub use piso::{PisoParameters, PisoResult, PisoSolver};
|
||||||
#[cfg(feature = "cuda")]
|
#[cfg(feature = "cuda")]
|
||||||
|
|||||||
@@ -25,8 +25,10 @@
|
|||||||
//! nor the acceptor cells — is measured every step on both sides.
|
//! nor the acceptor cells — is measured every step on both sides.
|
||||||
|
|
||||||
pub mod overlap;
|
pub mod overlap;
|
||||||
|
pub mod residual;
|
||||||
|
|
||||||
pub use overlap::{Acceptor, CellClass, DualDonor, FringeEntry, LatticeDonor, OverlapMap};
|
pub use overlap::{Acceptor, CellClass, DualDonor, FringeEntry, LatticeDonor, OverlapMap};
|
||||||
|
pub use residual::{FaceResidual, MomentumResidual, ResidualBucket};
|
||||||
|
|
||||||
use crate::error::{CfdError, CfdResult};
|
use crate::error::{CfdError, CfdResult};
|
||||||
use crate::mesh::PatchMesh;
|
use crate::mesh::PatchMesh;
|
||||||
|
|||||||
@@ -118,6 +118,15 @@ pub struct OverlapMap {
|
|||||||
pub fringe_u: Vec<FringeEntry>,
|
pub fringe_u: Vec<FringeEntry>,
|
||||||
/// Prescribed v faces with donors.
|
/// Prescribed v faces with donors.
|
||||||
pub fringe_v: Vec<FringeEntry>,
|
pub fringe_v: Vec<FringeEntry>,
|
||||||
|
/// Hole cells within two cells of the fringe that have a patch donor in
|
||||||
|
/// the widened band: ghost pressures for the momentum-residual
|
||||||
|
/// diagnostic (never read by the solver).
|
||||||
|
pub hole_p: Vec<FringeEntry>,
|
||||||
|
/// Hole–hole u faces the solver never stamps, with a widened-band
|
||||||
|
/// donor: ghost velocities for the diagnostic.
|
||||||
|
pub ghost_u: Vec<FringeEntry>,
|
||||||
|
/// See `ghost_u`.
|
||||||
|
pub ghost_v: Vec<FringeEntry>,
|
||||||
/// Acceptor cells on the patch's outer row.
|
/// Acceptor cells on the patch's outer row.
|
||||||
pub acceptors: Vec<Acceptor>,
|
pub acceptors: Vec<Acceptor>,
|
||||||
/// Patch rows searched for fringe donors (`nn − 1 − overlap_rows − 1 ..= nn − 2`).
|
/// Patch rows searched for fringe donors (`nn − 1 − overlap_rows − 1 ..= nn − 2`).
|
||||||
@@ -218,6 +227,29 @@ impl OverlapMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Diagnostic ghosts (never read by the solver): hole cells within
|
||||||
|
// two cells of the fringe and the hole–hole faces around them, with
|
||||||
|
// donors from a band widened three rows into the hole, so every
|
||||||
|
// fringe–hole face's momentum stencil reads a patch value.
|
||||||
|
let wide = QuadIndex::dual(patch, k_lo.saturating_sub(3), k_hi);
|
||||||
|
let near_fringe = |j: usize, i: usize| {
|
||||||
|
let lo_j = j.saturating_sub(2);
|
||||||
|
let lo_i = i.saturating_sub(2);
|
||||||
|
(lo_j..=(j + 2).min(ny - 1)).any(|jj| {
|
||||||
|
(lo_i..=(i + 2).min(nx - 1)).any(|ii| class[jj * nx + ii] == CellClass::Fringe)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let mut hole_p = Vec::new();
|
||||||
|
for j in 0..ny {
|
||||||
|
for i in 0..nx {
|
||||||
|
if class[j * nx + i] == CellClass::Hole && near_fringe(j, i) {
|
||||||
|
let (x, y) = ((i as f64 + 0.5) * dx, (j as f64 + 0.5) * dy);
|
||||||
|
if let Some(donor) = wide.dual_donor(patch, x, y) {
|
||||||
|
hole_p.push(FringeEntry { j, i, donor });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Prescribed faces: interior faces with no active neighbour, that
|
// Prescribed faces: interior faces with no active neighbour, that
|
||||||
// have a donor in the band (deeper ones are never read).
|
// have a donor in the band (deeper ones are never read).
|
||||||
let mut fringe_u = Vec::new();
|
let mut fringe_u = Vec::new();
|
||||||
@@ -259,6 +291,40 @@ impl OverlapMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let hole = |jj: usize, ii: usize| class[jj * nx + ii] == CellClass::Hole;
|
||||||
|
let stamped_u: std::collections::HashSet<(usize, usize)> =
|
||||||
|
fringe_u.iter().map(|e| (e.j, e.i)).collect();
|
||||||
|
let stamped_v: std::collections::HashSet<(usize, usize)> =
|
||||||
|
fringe_v.iter().map(|e| (e.j, e.i)).collect();
|
||||||
|
let mut ghost_u = Vec::new();
|
||||||
|
for j in 0..ny {
|
||||||
|
for i in 1..nx {
|
||||||
|
if hole(j, i - 1) && hole(j, i) && (near_fringe(j, i - 1) || near_fringe(j, i)) {
|
||||||
|
let (x, y) = (i as f64 * dx, (j as f64 + 0.5) * dy);
|
||||||
|
if stamped_u.contains(&(j, i)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(donor) = wide.dual_donor(patch, x, y) {
|
||||||
|
ghost_u.push(FringeEntry { j, i, donor });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut ghost_v = Vec::new();
|
||||||
|
for j in 1..ny {
|
||||||
|
for i in 0..nx {
|
||||||
|
if hole(j - 1, i) && hole(j, i) && (near_fringe(j - 1, i) || near_fringe(j, i)) {
|
||||||
|
let (x, y) = ((i as f64 + 0.5) * dx, j as f64 * dy);
|
||||||
|
if stamped_v.contains(&(j, i)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(donor) = wide.dual_donor(patch, x, y) {
|
||||||
|
ghost_v.push(FringeEntry { j, i, donor });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Acceptors: patch row nn − 1, lattice donors on the background.
|
// 3. Acceptors: patch row nn − 1, lattice donors on the background.
|
||||||
let u_fluid = |jj: usize, ii: usize| {
|
let u_fluid = |jj: usize, ii: usize| {
|
||||||
// A u face is fluid unless both adjacent cells are non-active.
|
// A u face is fluid unless both adjacent cells are non-active.
|
||||||
@@ -319,6 +385,9 @@ impl OverlapMap {
|
|||||||
fringe_cells,
|
fringe_cells,
|
||||||
fringe_u,
|
fringe_u,
|
||||||
fringe_v,
|
fringe_v,
|
||||||
|
hole_p,
|
||||||
|
ghost_u,
|
||||||
|
ghost_v,
|
||||||
acceptors,
|
acceptors,
|
||||||
donor_rows: (k_lo, k_hi),
|
donor_rows: (k_lo, k_hi),
|
||||||
hole_cells,
|
hole_cells,
|
||||||
@@ -384,6 +453,38 @@ impl OverlapMap {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Interpolate a patch cell field to the hole ghost cells (order of
|
||||||
|
/// `hole_p`).
|
||||||
|
pub fn hole_p_values(&self, patch_vals: &[f64]) -> Vec<f64> {
|
||||||
|
self.hole_p
|
||||||
|
.iter()
|
||||||
|
.map(|e| dual_value(&e.donor, patch_vals))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stamp `values` (from [`Self::hole_p_values`]) onto a background
|
||||||
|
/// cell field.
|
||||||
|
pub fn stamp_hole_p(&self, target: &mut nalgebra::DMatrix<f64>, values: &[f64]) {
|
||||||
|
for (e, &v) in self.hole_p.iter().zip(values) {
|
||||||
|
target[(e.j, e.i)] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stamp the diagnostic ghost faces (`ghost_u`, `ghost_v`) from the
|
||||||
|
/// patch cell velocities, onto both `u`/`v` and `u_old`/`v_old`.
|
||||||
|
pub fn stamp_ghost_faces(&self, field: &mut FlowField, patch_u: &[f64], patch_v: &[f64]) {
|
||||||
|
for e in &self.ghost_u {
|
||||||
|
let v = dual_value(&e.donor, patch_u);
|
||||||
|
field.u[(e.j, e.i)] = v;
|
||||||
|
field.u_old[(e.j, e.i)] = v;
|
||||||
|
}
|
||||||
|
for e in &self.ghost_v {
|
||||||
|
let v = dual_value(&e.donor, patch_v);
|
||||||
|
field.v[(e.j, e.i)] = v;
|
||||||
|
field.v_old[(e.j, e.i)] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Stamp `values` (from [`Self::fringe_cell_values`]) onto a
|
/// Stamp `values` (from [`Self::fringe_cell_values`]) onto a
|
||||||
/// background cell field.
|
/// background cell field.
|
||||||
pub fn stamp_fringe_cells(&self, target: &mut nalgebra::DMatrix<f64>, values: &[f64]) {
|
pub fn stamp_fringe_cells(&self, target: &mut nalgebra::DMatrix<f64>, values: &[f64]) {
|
||||||
|
|||||||
@@ -0,0 +1,300 @@
|
|||||||
|
//! P4 option B (`docs/overset_metal_campaign.md` §5.11): the momentum
|
||||||
|
//! residual of the background's OWN staggered predictor stencil on every
|
||||||
|
//! background face, at a settled state.
|
||||||
|
//!
|
||||||
|
//! On a solved face the discrete equation the composite marched is
|
||||||
|
//! `ρ (u^{n+1} − u^n)/dt = ρ · rhs(u^n, p^{n+1})` (predictor plus the
|
||||||
|
//! correctors' `−dt ∇p'/ρ`, with `p^{n+1} = p^n + Σ p'`), so the residual
|
||||||
|
//! `r = ρ [(u^{n+1} − u^n)/dt − rhs] · dx dy` is zero to rounding there
|
||||||
|
//! — the pin that proves the diagnostic IS the solver's operator. On a
|
||||||
|
//! PRESCRIBED face the value is stamped from the patch, the equation is
|
||||||
|
//! not solved, and `r` is the momentum source the stamping injects, in the
|
||||||
|
//! solver's own metric and without the staircase curves' face-formula
|
||||||
|
//! error. Summed over the ring it is the fringe ring's momentum defect
|
||||||
|
//! (`region_force` ring outer − hole boundary, but exact).
|
||||||
|
//!
|
||||||
|
//! Validity is decided by the stencil itself: the background field is
|
||||||
|
//! copied with `NaN` on every value that is neither the solver's own nor
|
||||||
|
//! stamped from the patch (hole cells and hole–hole faces within two cells
|
||||||
|
//! of the ring get the patch's interpolated values, `OverlapMap::hole_p`
|
||||||
|
//! / `ghost_u` / `ghost_v`, from a band widened three rows into the
|
||||||
|
//! hole), the
|
||||||
|
//! operator is evaluated as is, and a `NaN` result means the face read
|
||||||
|
//! something invalid and is not counted. Under the upwind scheme every
|
||||||
|
//! value the stencil reads enters its arithmetic, so the test is exact.
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use super::overlap::CellClass;
|
||||||
|
use super::{OversetField, OversetPisoSolver};
|
||||||
|
use crate::solvers::incompressible::embedded_body::FaceKind;
|
||||||
|
|
||||||
|
/// Sums over one class of faces.
|
||||||
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
pub struct ResidualBucket {
|
||||||
|
/// `Σ r` on the u faces (x-momentum source, N/m).
|
||||||
|
pub fx: f64,
|
||||||
|
/// `Σ r` on the v faces.
|
||||||
|
pub fy: f64,
|
||||||
|
/// `Σ |r|` on the u faces.
|
||||||
|
pub abs_x: f64,
|
||||||
|
/// `Σ |r|` on the v faces.
|
||||||
|
pub abs_y: f64,
|
||||||
|
/// Largest `|r|` on the u faces.
|
||||||
|
pub max_abs_x: f64,
|
||||||
|
/// Largest `|r|` on the v faces.
|
||||||
|
pub max_abs_y: f64,
|
||||||
|
/// Faces whose stencil read only valid values.
|
||||||
|
pub evaluated: usize,
|
||||||
|
/// Of `evaluated`, the u faces.
|
||||||
|
pub evaluated_u: usize,
|
||||||
|
/// Of `evaluated`, the v faces.
|
||||||
|
pub evaluated_v: usize,
|
||||||
|
/// Faces of this class.
|
||||||
|
pub total: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResidualBucket {
|
||||||
|
fn add(&mut self, r: f64, is_u: bool) {
|
||||||
|
self.total += 1;
|
||||||
|
if !r.is_finite() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.evaluated += 1;
|
||||||
|
if is_u {
|
||||||
|
self.evaluated_u += 1;
|
||||||
|
self.fx += r;
|
||||||
|
self.abs_x += r.abs();
|
||||||
|
self.max_abs_x = self.max_abs_x.max(r.abs());
|
||||||
|
} else {
|
||||||
|
self.evaluated_v += 1;
|
||||||
|
self.fy += r;
|
||||||
|
self.abs_y += r.abs();
|
||||||
|
self.max_abs_y = self.max_abs_y.max(r.abs());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One prescribed face's residual.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct FaceResidual {
|
||||||
|
/// A u face (x-momentum) or a v face.
|
||||||
|
pub is_u: bool,
|
||||||
|
/// Row.
|
||||||
|
pub j: usize,
|
||||||
|
/// Column.
|
||||||
|
pub i: usize,
|
||||||
|
/// The residual (N/m), `NaN` when not evaluable.
|
||||||
|
pub r: f64,
|
||||||
|
/// Between two fringe cells (else fringe–hole).
|
||||||
|
pub fringe_fringe: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The momentum residual by face class.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct MomentumResidual {
|
||||||
|
/// Solved faces whose stencil reads only solved values.
|
||||||
|
pub solved_far: ResidualBucket,
|
||||||
|
/// Solved faces whose stencil reads a fringe cell or a prescribed face.
|
||||||
|
pub solved_near: ResidualBucket,
|
||||||
|
/// Prescribed faces between two fringe cells (tangential to the ring).
|
||||||
|
pub fringe_fringe: ResidualBucket,
|
||||||
|
/// Prescribed faces between a fringe and a hole cell (normal to it).
|
||||||
|
pub fringe_hole: ResidualBucket,
|
||||||
|
/// Prescribed faces between two hole cells: not evaluated (their
|
||||||
|
/// control volume lies in the hole).
|
||||||
|
pub hole_hole_skipped: usize,
|
||||||
|
/// Hole cells given a ghost pressure.
|
||||||
|
pub hole_ghosts: usize,
|
||||||
|
/// Hole–hole faces given a ghost velocity (beyond the solver's stamps).
|
||||||
|
pub ghost_faces: usize,
|
||||||
|
/// Solved u faces between an active and a fringe cell (the ring's outer
|
||||||
|
/// boundary); their residual is the pressure LEVEL offset `δ · h`.
|
||||||
|
pub interface_u: usize,
|
||||||
|
/// See `interface_u`.
|
||||||
|
pub interface_v: usize,
|
||||||
|
/// Every prescribed fringe–fringe / fringe–hole face's residual.
|
||||||
|
pub prescribed: Vec<FaceResidual>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MomentumResidual {
|
||||||
|
/// The composite's pressure level offset `δ` (Pa) between the active
|
||||||
|
/// cells and the re-stamped fringe: `max |r| / h` over the interface.
|
||||||
|
pub fn level_offset(&self, h: f64) -> f64 {
|
||||||
|
self.solved_near.max_abs_x.max(self.solved_near.max_abs_y) / h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OversetPisoSolver {
|
||||||
|
/// The momentum residual of the background's own predictor stencil on
|
||||||
|
/// every interior background face, after [`Self::advance`] (the field
|
||||||
|
/// holds `u^{n+1}`, `u_old = u^n`, `p = p^{n+1}` with the fringe
|
||||||
|
/// re-stamped). `dt` is the step just taken.
|
||||||
|
pub fn momentum_residual(&self, field: &OversetField, dt: f64) -> MomentumResidual {
|
||||||
|
let (nx, ny, dx, dy) = self.grid;
|
||||||
|
let rho = self.background.config().density;
|
||||||
|
let t_old = self.background.time() - dt;
|
||||||
|
let mask = self
|
||||||
|
.background
|
||||||
|
.mask()
|
||||||
|
.expect("the overset background carries a mask");
|
||||||
|
let map = &self.overlap;
|
||||||
|
let hole = |j: usize, i: usize| map.class(j, i) == CellClass::Hole;
|
||||||
|
|
||||||
|
// The masked copy.
|
||||||
|
let mut m = field.background.clone();
|
||||||
|
for j in 0..ny {
|
||||||
|
for i in 0..nx {
|
||||||
|
if hole(j, i) {
|
||||||
|
m.p[(j, i)] = f64::NAN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let ghosts = map.hole_p_values(&field.patch.p);
|
||||||
|
map.stamp_hole_p(&mut m.p, &ghosts);
|
||||||
|
map.stamp_ghost_faces(&mut m, &field.patch.u, &field.patch.v);
|
||||||
|
let prescribed_u: HashSet<(usize, usize)> = map
|
||||||
|
.fringe_u
|
||||||
|
.iter()
|
||||||
|
.chain(&map.ghost_u)
|
||||||
|
.map(|e| (e.j, e.i))
|
||||||
|
.collect();
|
||||||
|
let prescribed_v: HashSet<(usize, usize)> = map
|
||||||
|
.fringe_v
|
||||||
|
.iter()
|
||||||
|
.chain(&map.ghost_v)
|
||||||
|
.map(|e| (e.j, e.i))
|
||||||
|
.collect();
|
||||||
|
for j in 0..ny {
|
||||||
|
for i in 0..=nx {
|
||||||
|
let valid = (i > 0 && !hole(j, i - 1))
|
||||||
|
|| (i < nx && !hole(j, i))
|
||||||
|
|| prescribed_u.contains(&(j, i));
|
||||||
|
if !valid {
|
||||||
|
m.u[(j, i)] = f64::NAN;
|
||||||
|
m.u_old[(j, i)] = f64::NAN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for j in 0..=ny {
|
||||||
|
for i in 0..nx {
|
||||||
|
let valid = (j > 0 && !hole(j - 1, i))
|
||||||
|
|| (j < ny && !hole(j, i))
|
||||||
|
|| prescribed_v.contains(&(j, i));
|
||||||
|
if !valid {
|
||||||
|
m.v[(j, i)] = f64::NAN;
|
||||||
|
m.v_old[(j, i)] = f64::NAN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ghost_u = |j: usize, i: usize| mask.u_kind(j, i) == FaceKind::Ghost;
|
||||||
|
let ghost_v = |j: usize, i: usize| mask.v_kind(j, i) == FaceKind::Ghost;
|
||||||
|
let active = |j: usize, i: usize| map.class(j, i) == CellClass::Active;
|
||||||
|
let mut out = MomentumResidual {
|
||||||
|
hole_ghosts: map.hole_p.len(),
|
||||||
|
ghost_faces: map.ghost_u.len() + map.ghost_v.len(),
|
||||||
|
..MomentumResidual::default()
|
||||||
|
};
|
||||||
|
let vol = dx * dy;
|
||||||
|
let mut prescribed = Vec::new();
|
||||||
|
|
||||||
|
// u faces (j, i), i = 1..nx: cells (j, i−1) | (j, i).
|
||||||
|
for j in 0..ny {
|
||||||
|
for i in 1..nx {
|
||||||
|
let (w, e) = (map.class(j, i - 1), map.class(j, i));
|
||||||
|
if (w == CellClass::Active) != (e == CellClass::Active) {
|
||||||
|
out.interface_u += 1;
|
||||||
|
}
|
||||||
|
let bucket = if !ghost_u(j, i) {
|
||||||
|
// Stencil: u (j, i±1), (j±1, i); v (j, i−1), (j, i), (j+1, i−1), (j+1, i); p (j, i−1), (j, i).
|
||||||
|
let near = !active(j, i - 1)
|
||||||
|
|| !active(j, i)
|
||||||
|
|| ghost_u(j, i - 1)
|
||||||
|
|| ghost_u(j, i + 1)
|
||||||
|
|| (j > 0 && ghost_u(j - 1, i))
|
||||||
|
|| (j + 1 < ny && ghost_u(j + 1, i))
|
||||||
|
|| ghost_v(j, i - 1)
|
||||||
|
|| ghost_v(j, i)
|
||||||
|
|| ghost_v(j + 1, i - 1)
|
||||||
|
|| ghost_v(j + 1, i);
|
||||||
|
if near {
|
||||||
|
&mut out.solved_near
|
||||||
|
} else {
|
||||||
|
&mut out.solved_far
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match (w, e) {
|
||||||
|
(CellClass::Fringe, CellClass::Fringe) => &mut out.fringe_fringe,
|
||||||
|
(CellClass::Hole, CellClass::Hole) => {
|
||||||
|
out.hole_hole_skipped += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_ => &mut out.fringe_hole,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let rhs = self.background.u_rhs(&m, j, i, t_old);
|
||||||
|
let r = rho * ((m.u[(j, i)] - m.u_old[(j, i)]) / dt - rhs) * vol;
|
||||||
|
bucket.add(r, true);
|
||||||
|
if ghost_u(j, i) {
|
||||||
|
prescribed.push(FaceResidual {
|
||||||
|
is_u: true,
|
||||||
|
j,
|
||||||
|
i,
|
||||||
|
r,
|
||||||
|
fringe_fringe: w == CellClass::Fringe && e == CellClass::Fringe,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// v faces (j, i), j = 1..ny: cells (j−1, i) | (j, i).
|
||||||
|
for j in 1..ny {
|
||||||
|
for i in 0..nx {
|
||||||
|
let (s, n) = (map.class(j - 1, i), map.class(j, i));
|
||||||
|
if (s == CellClass::Active) != (n == CellClass::Active) {
|
||||||
|
out.interface_v += 1;
|
||||||
|
}
|
||||||
|
let bucket = if !ghost_v(j, i) {
|
||||||
|
let near = !active(j - 1, i)
|
||||||
|
|| !active(j, i)
|
||||||
|
|| ghost_v(j - 1, i)
|
||||||
|
|| ghost_v(j + 1, i)
|
||||||
|
|| (i > 0 && ghost_v(j, i - 1))
|
||||||
|
|| (i + 1 < nx && ghost_v(j, i + 1))
|
||||||
|
|| ghost_u(j - 1, i)
|
||||||
|
|| ghost_u(j, i)
|
||||||
|
|| ghost_u(j - 1, i + 1)
|
||||||
|
|| ghost_u(j, i + 1);
|
||||||
|
if near {
|
||||||
|
&mut out.solved_near
|
||||||
|
} else {
|
||||||
|
&mut out.solved_far
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match (s, n) {
|
||||||
|
(CellClass::Fringe, CellClass::Fringe) => &mut out.fringe_fringe,
|
||||||
|
(CellClass::Hole, CellClass::Hole) => {
|
||||||
|
out.hole_hole_skipped += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_ => &mut out.fringe_hole,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let rhs = self.background.v_rhs(&m, j, i, t_old);
|
||||||
|
let r = rho * ((m.v[(j, i)] - m.v_old[(j, i)]) / dt - rhs) * vol;
|
||||||
|
bucket.add(r, false);
|
||||||
|
if ghost_v(j, i) {
|
||||||
|
prescribed.push(FaceResidual {
|
||||||
|
is_u: false,
|
||||||
|
j,
|
||||||
|
i,
|
||||||
|
r,
|
||||||
|
fringe_fringe: s == CellClass::Fringe && n == CellClass::Fringe,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.prescribed = prescribed;
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,12 +7,13 @@
|
|||||||
//! Reference (FEATFLOW level 6): drag 14.2929, lift 1.11905. The embedded
|
//! Reference (FEATFLOW level 6): drag 14.2929, lift 1.11905. The embedded
|
||||||
//! staircase measured drag 15.71 (surface) / 15.62 (CV) at ny = 41 (+10%).
|
//! staircase measured drag 15.71 (surface) / 15.62 (CV) at ny = 41 (+10%).
|
||||||
|
|
||||||
use rtx_cfd::mesh::PatchSide;
|
|
||||||
use rtx_cfd::mesh::patch_gen::cylinder_flag_patch;
|
use rtx_cfd::mesh::patch_gen::cylinder_flag_patch;
|
||||||
|
use rtx_cfd::mesh::PatchSide;
|
||||||
use rtx_cfd::solvers::incompressible::{
|
use rtx_cfd::solvers::incompressible::{
|
||||||
AleBoundaries, CellClass, CurvilinearParameters, CurvilinearPisoSolver, EmbeddedParameters,
|
AleBoundaries, CellClass, CurvilinearParameters, CurvilinearPisoSolver, EmbeddedParameters,
|
||||||
EmbeddedPisoSolver, FlowField, NormalDiffusion, OversetField, OversetParameters,
|
EmbeddedPisoSolver, FlowField, MomentumResidual, NormalDiffusion, OversetField,
|
||||||
OversetPisoSolver, PatchConvection, PatchField, PoissonSolverKind, SideBoundary,
|
OversetParameters, OversetPisoSolver, PatchConvection, PatchField, PoissonSolverKind,
|
||||||
|
SideBoundary,
|
||||||
};
|
};
|
||||||
use rtx_cfd::{CfdConfig, CfdResult};
|
use rtx_cfd::{CfdConfig, CfdResult};
|
||||||
|
|
||||||
@@ -37,9 +38,10 @@ struct Cfd1 {
|
|||||||
seconds: f64,
|
seconds: f64,
|
||||||
rounds_mean: f64,
|
rounds_mean: f64,
|
||||||
dt: f64,
|
dt: f64,
|
||||||
|
residual: MomentumResidual,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_cfd1(ny: usize) -> CfdResult<Cfd1> {
|
async fn run_cfd1(ny: usize, max_steps: usize) -> CfdResult<Cfd1> {
|
||||||
let h = H / ny as f64;
|
let h = H / ny as f64;
|
||||||
let nx = (L / h).round() as usize;
|
let nx = (L / h).round() as usize;
|
||||||
let mu = RHO * NU;
|
let mu = RHO * NU;
|
||||||
@@ -182,10 +184,6 @@ async fn run_cfd1(ny: usize) -> CfdResult<Cfd1> {
|
|||||||
let mut steps = 0;
|
let mut steps = 0;
|
||||||
let mut rounds_total = 0usize;
|
let mut rounds_total = 0usize;
|
||||||
let mut correctors_total = 0usize;
|
let mut correctors_total = 0usize;
|
||||||
let max_steps: usize = std::env::var("RTX_OVERSET_CFD1_MAX_STEPS")
|
|
||||||
.ok()
|
|
||||||
.and_then(|v| v.parse().ok())
|
|
||||||
.unwrap_or(2_000_000);
|
|
||||||
let trace_first = std::env::var("RTX_OVERSET_CFD1_TRACE").is_ok();
|
let trace_first = std::env::var("RTX_OVERSET_CFD1_TRACE").is_ok();
|
||||||
loop {
|
loop {
|
||||||
let r = solver.advance(&mut field, dt).await?;
|
let r = solver.advance(&mut field, dt).await?;
|
||||||
@@ -285,6 +283,52 @@ async fn run_cfd1(ny: usize) -> CfdResult<Cfd1> {
|
|||||||
100.0 * (ring.0 - hole.0) / wall[0],
|
100.0 * (ring.0 - hole.0) / wall[0],
|
||||||
100.0 * (hole.0 - wall[0]) / wall[0],
|
100.0 * (hole.0 - wall[0]) / wall[0],
|
||||||
);
|
);
|
||||||
|
// P4 option B: the momentum residual of the solver's OWN staggered
|
||||||
|
// upwind stencil on every background face at the settled state. Solved
|
||||||
|
// faces read zero by construction (the pin below); the prescribed
|
||||||
|
// faces' sum is the momentum the stamping injects, in the solver's
|
||||||
|
// metric and without the staircase curves' face-formula error.
|
||||||
|
let mr = solver.momentum_residual(&field, dt);
|
||||||
|
let pct = |b: &rtx_cfd::solvers::incompressible::ResidualBucket| 100.0 * b.fx / wall[0];
|
||||||
|
println!(
|
||||||
|
" momentum residual ny = {ny} [N/m, x / y; % of wall drag; faces evaluated/total]: solved far Σr ({:+.3e}, {:+.3e}) Σ|r| ({:.3e}, {:.3e}) {}/{} | solved near ring Σr ({:+.3e}, {:+.3e}) Σ|r| ({:.4}, {:.4}) max|r| ({:.3e}, {:.3e}) {}/{} | fringe–fringe ({:+.4}, {:+.4}) {:+.2}% {}/{} | fringe–hole ({:+.4}, {:+.4}) {:+.2}% {}/{} | hole–hole skipped {} (ghosts: {} cells, {} faces) | Σ|r| fringe–fringe ({:.4}, {:.4}) fringe–hole ({:.4}, {:.4}); ring total ({:+.4}, {:+.4}) {:+.2}% vs routes' ring defect {:+.4} ({:+.2}%)",
|
||||||
|
mr.solved_far.fx, mr.solved_far.fy, mr.solved_far.abs_x, mr.solved_far.abs_y, mr.solved_far.evaluated, mr.solved_far.total,
|
||||||
|
mr.solved_near.fx, mr.solved_near.fy, mr.solved_near.abs_x, mr.solved_near.abs_y, mr.solved_near.max_abs_x, mr.solved_near.max_abs_y, mr.solved_near.evaluated, mr.solved_near.total,
|
||||||
|
mr.fringe_fringe.fx, mr.fringe_fringe.fy, pct(&mr.fringe_fringe), mr.fringe_fringe.evaluated, mr.fringe_fringe.total,
|
||||||
|
mr.fringe_hole.fx, mr.fringe_hole.fy, pct(&mr.fringe_hole), mr.fringe_hole.evaluated, mr.fringe_hole.total,
|
||||||
|
mr.hole_hole_skipped, mr.hole_ghosts, mr.ghost_faces,
|
||||||
|
mr.fringe_fringe.abs_x, mr.fringe_fringe.abs_y, mr.fringe_hole.abs_x, mr.fringe_hole.abs_y,
|
||||||
|
mr.fringe_fringe.fx + mr.fringe_hole.fx, mr.fringe_fringe.fy + mr.fringe_hole.fy,
|
||||||
|
pct(&mr.fringe_fringe) + pct(&mr.fringe_hole),
|
||||||
|
hole.0 - ring.0,
|
||||||
|
100.0 * (hole.0 - ring.0) / wall[0],
|
||||||
|
);
|
||||||
|
// Where along the ring: the prescribed u faces' x-momentum residual in
|
||||||
|
// x-bands (cylinder front, cylinder–flag junction, flag, trailing edge).
|
||||||
|
let mut bands = [
|
||||||
|
(0.0_f64, 0.20, 0.0_f64, 0usize),
|
||||||
|
(0.20, 0.30, 0.0, 0),
|
||||||
|
(0.30, 0.55, 0.0, 0),
|
||||||
|
(0.55, 1.0, 0.0, 0),
|
||||||
|
];
|
||||||
|
for f in mr.prescribed.iter().filter(|f| f.is_u && f.r.is_finite()) {
|
||||||
|
let x = f.i as f64 * h;
|
||||||
|
if let Some(b) = bands.iter_mut().find(|b| x >= b.0 && x < b.1) {
|
||||||
|
b.2 += f.r;
|
||||||
|
b.3 += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
" ring x-momentum residual by x-band ny = {ny} (N/m, u faces): {}; level offset δ = {:.3e} Pa on {} + {} interface faces",
|
||||||
|
bands
|
||||||
|
.iter()
|
||||||
|
.map(|b| format!("x {:.2}–{:.2}: {:+.4} ({} faces)", b.0, b.1, b.2, b.3))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" | "),
|
||||||
|
mr.level_offset(h),
|
||||||
|
mr.interface_u,
|
||||||
|
mr.interface_v
|
||||||
|
);
|
||||||
// The wall load split and the fringe ring's extent (the tight box in
|
// The wall load split and the fringe ring's extent (the tight box in
|
||||||
// the sensitivity list must stay outside it).
|
// the sensitivity list must stay outside it).
|
||||||
let (mut jmin, mut jmax, mut imin, mut imax) = (usize::MAX, 0, usize::MAX, 0);
|
let (mut jmin, mut jmax, mut imin, mut imax) = (usize::MAX, 0, usize::MAX, 0);
|
||||||
@@ -353,6 +397,7 @@ async fn run_cfd1(ny: usize) -> CfdResult<Cfd1> {
|
|||||||
seconds,
|
seconds,
|
||||||
rounds_mean: rounds_total as f64 / correctors_total.max(1) as f64,
|
rounds_mean: rounds_total as f64 / correctors_total.max(1) as f64,
|
||||||
dt,
|
dt,
|
||||||
|
residual: mr,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,7 +412,11 @@ async fn cfd1_on_the_overset_against_the_featflow_reference() -> CfdResult<()> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
for &ny in &resolutions {
|
for &ny in &resolutions {
|
||||||
let r = run_cfd1(ny).await?;
|
let max_steps: usize = std::env::var("RTX_OVERSET_CFD1_MAX_STEPS")
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(2_000_000);
|
||||||
|
let r = run_cfd1(ny, max_steps).await?;
|
||||||
let rel = |a: f64, b: f64| 100.0 * (a - b) / b;
|
let rel = |a: f64, b: f64| 100.0 * (a - b) / b;
|
||||||
println!(
|
println!(
|
||||||
" CFD1 overset ny = {ny} (h = {:.4}, dt = {:.2e}, patch {}): wall drag {:.4} ({:+.2}%) lift {:.4} ({:+.2}%); control volume drag {:.4} ({:+.2}%) lift {:.4}; routes differ {:.2}%; [{} steps, {:.0} s, Schwarz rounds mean {:.2}] reference {REF_DRAG} / {REF_LIFT}; embedded staircase at ny=41: 15.71 / 15.62 (+10%)",
|
" CFD1 overset ny = {ny} (h = {:.4}, dt = {:.2e}, patch {}): wall drag {:.4} ({:+.2}%) lift {:.4} ({:+.2}%); control volume drag {:.4} ({:+.2}%) lift {:.4}; routes differ {:.2}%; [{} steps, {:.0} s, Schwarz rounds mean {:.2}] reference {REF_DRAG} / {REF_LIFT}; embedded staircase at ny=41: 15.71 / 15.62 (+10%)",
|
||||||
@@ -394,3 +443,67 @@ async fn cfd1_on_the_overset_against_the_featflow_reference() -> CfdResult<()> {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The residual diagnostic is the solver's own operator: on every SOLVED
|
||||||
|
/// background face away from the ring the momentum residual (time term
|
||||||
|
/// included) is zero to rounding; on the solved faces NEXT to the ring it
|
||||||
|
/// is a constant per face that cancels in the sum — the composite's
|
||||||
|
/// pressure LEVEL offset between the active cells (whose `p'` had its mean
|
||||||
|
/// removed) and the fringe cells (re-stamped from the patch, which never
|
||||||
|
/// saw that shift); and the ring buckets are populated. This is what makes
|
||||||
|
/// the prescribed faces' sum readable as the stamping's momentum injection
|
||||||
|
/// in the solver's metric (§5.11, option B).
|
||||||
|
#[tokio::test]
|
||||||
|
async fn momentum_residual_vanishes_on_the_solved_faces() -> CfdResult<()> {
|
||||||
|
let r = run_cfd1(41, 5).await?;
|
||||||
|
let mr = &r.residual;
|
||||||
|
let scale = r.drag_surface.abs().max(1.0);
|
||||||
|
let far = &mr.solved_far;
|
||||||
|
assert_eq!(
|
||||||
|
far.evaluated, far.total,
|
||||||
|
"every far solved face is evaluable"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
far.abs_x <= 1e-9 * scale && far.abs_y <= 1e-9 * scale,
|
||||||
|
"solved far: Σ|r| = ({:.3e}, {:.3e}) is not rounding against {scale:.3}",
|
||||||
|
far.abs_x,
|
||||||
|
far.abs_y
|
||||||
|
);
|
||||||
|
let near = &mr.solved_near;
|
||||||
|
assert_eq!(
|
||||||
|
near.evaluated, near.total,
|
||||||
|
"every near solved face is evaluable"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
near.fx.abs() <= 1e-9 * scale && near.fy.abs() <= 1e-9 * scale,
|
||||||
|
"solved near: Σr = ({:.3e}, {:.3e}) does not cancel against {scale:.3}",
|
||||||
|
near.fx,
|
||||||
|
near.fy
|
||||||
|
);
|
||||||
|
// A pure level offset: every active–fringe INTERFACE face carries the
|
||||||
|
// same |r| = δ·h and every other near face (one that only reads a
|
||||||
|
// prescribed velocity) reads zero, so Σ|r| = N_interface · max|r| on
|
||||||
|
// each lattice.
|
||||||
|
assert!(
|
||||||
|
(near.abs_x - mr.interface_u as f64 * near.max_abs_x).abs() <= 1e-6 * near.abs_x.max(1e-300)
|
||||||
|
&& (near.abs_y - mr.interface_v as f64 * near.max_abs_y).abs()
|
||||||
|
<= 1e-6 * near.abs_y.max(1e-300),
|
||||||
|
"solved near: not a uniform level offset on the interface — Σ|r| ({:.4e}, {:.4e}) vs N·max|r| ({:.4e}, {:.4e}) with N = ({}, {})",
|
||||||
|
near.abs_x,
|
||||||
|
near.abs_y,
|
||||||
|
mr.interface_u as f64 * near.max_abs_x,
|
||||||
|
mr.interface_v as f64 * near.max_abs_y,
|
||||||
|
mr.interface_u,
|
||||||
|
mr.interface_v
|
||||||
|
);
|
||||||
|
assert!(mr.fringe_fringe.evaluated > 0 && mr.fringe_hole.evaluated > 0);
|
||||||
|
assert_eq!(
|
||||||
|
mr.fringe_fringe.evaluated, mr.fringe_fringe.total,
|
||||||
|
"every fringe–fringe face has a fully valid stencil"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
mr.fringe_hole.evaluated, mr.fringe_hole.total,
|
||||||
|
"every fringe–hole face has a fully valid stencil with the ghost band"
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user