rtx-cfd: overset A-P1 GATED — the curvilinear patch moves and deforms under an exact 2-D DGCL
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 / 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
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 / 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
StepGeometry (motion.rs): time-averaged face vectors S̄_f = ½(S^n + S^{n+1}) and
swept volumes δV_f = S̄_f·δc_f — exact for linear node motion on any quad, so
Σ sign δV_f = V^{n+1} − V^n is algebra (1.8e-14 measured; the EndOfStep control
2.1e-3). CurvilinearPisoSolver::set_mesh(next) names the end-of-step geometry;
advance swaps it in, rebuilds operators + pressure matrix on it (L_f, LSQ
gradients, no mesh-velocity term in the projection), keeps the old mesh for
V^n and the explicit boundary data; predictor in the conservative ALE form
V^{n+1} û = V^n u^n + dt(−Σ sign (F − δV/dt) u_f + ν D + f V^n), written as
u·(V^n/V^{n+1}) + … so a stationary mesh is bitwise the static path; fluxes on
S̄_f; snapshot carries both meshes; swept_face_rule knob (Trapezoidal default,
EndOfStep = negative control). Stokes limit keeps the mesh flux (was dropped
with convection) and centres it (upwinding it cost an order: 1.06/1.00).
Gates (tests/curvilinear_ale.rs, 11 tests, 83 s): uniform flow on a wiggling
AND bending annulus 4.44e-15 over 400 steps, p exactly 0, 0 pressure
iterations; control deviates 4.9e-5; stationary mesh through the moving path
bit-identical (both diffusion variants); snapshot/restore on the moving mesh
bit-identical; Taylor–Green orders unchanged — upwind 0.995/0.976 vs fixed
0.987/0.975 at 1.05× error, Stokes 1.92/1.97 vs 1.94/1.99 at 2.4×, moving
annulus 2.11/2.02; linear-field falsifier 1.95/1.92 (annulus), 1.91/1.43
(square, sliding wall nodes). P0 ladders re-run identical to every digit.
Rule from the diagnosis: start a moving run ON the t = 0 mesh and sweep less
than a cell per step — a first step that jumped 2–4 cells imprinted an
O(displacement) error no refinement removed (dt- and motion-independent).
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
c63d79c300
commit
d46fb0b7a7
@@ -19,16 +19,29 @@
|
||||
//! 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.
|
||||
//!
|
||||
//! Phase P1: the patch MOVES. [`CurvilinearPisoSolver::set_mesh`] names the
|
||||
//! geometry at the end of the coming step; the step then runs in the
|
||||
//! conservative ALE form `(V^{n+1} û − V^n u^n)/dt = …` with the
|
||||
//! convecting flux relative to the mesh, `F_f − δV_f/dt`, on the
|
||||
//! time-averaged face vectors `S̄_f` (`motion.rs`: the 2-D discrete GCL is
|
||||
//! exact algebra under that rule). `L_f`, the cell gradients and the
|
||||
//! pressure matrix live on the end-of-step geometry; the projection is the
|
||||
//! static one (no mesh-velocity term). A stationary mesh through this path
|
||||
//! is bit-identical to the static path.
|
||||
|
||||
mod motion;
|
||||
mod operators;
|
||||
mod predictor;
|
||||
mod projection;
|
||||
|
||||
pub use motion::StepGeometry;
|
||||
pub use operators::Operators;
|
||||
|
||||
use crate::mesh::{PatchMesh, PatchSide};
|
||||
use crate::solvers::incompressible::ale::SweptFaceRule;
|
||||
use crate::solvers::incompressible::sparse_bicgstab::CsrMatrix;
|
||||
use crate::{CfdConfig, CfdResult};
|
||||
use crate::{CfdConfig, CfdError, CfdResult};
|
||||
|
||||
/// What one side of the patch is.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
@@ -110,6 +123,10 @@ pub struct CurvilinearParameters {
|
||||
pub boundaries: PatchBoundaries,
|
||||
/// BiCGSTAB iteration cap.
|
||||
pub max_poisson_iterations: usize,
|
||||
/// Face-area rule on a moving mesh: `Trapezoidal` (the DGCL-exact
|
||||
/// choice, the default) or `EndOfStep` (the negative control, GCL-
|
||||
/// violating). Irrelevant when the mesh does not move.
|
||||
pub swept_face_rule: SweptFaceRule,
|
||||
}
|
||||
|
||||
impl Default for CurvilinearParameters {
|
||||
@@ -121,6 +138,7 @@ impl Default for CurvilinearParameters {
|
||||
normal_diffusion: NormalDiffusion::Explicit,
|
||||
boundaries: PatchBoundaries::default(),
|
||||
max_poisson_iterations: 5000,
|
||||
swept_face_rule: SweptFaceRule::Trapezoidal,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,10 +184,15 @@ pub struct CurvilinearResult {
|
||||
pub boundary_flux_adjustment: f64,
|
||||
}
|
||||
|
||||
/// Restorable solver state (the coupling re-runs a step).
|
||||
/// Restorable solver state (the coupling re-runs a step): the time, the
|
||||
/// current mesh and any mesh already named for the next step. Restoring
|
||||
/// rebuilds the operators and drops the cached matrix, so a re-run from
|
||||
/// the snapshot is bit-identical to the first run.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CurvilinearSolverState {
|
||||
time: f64,
|
||||
mesh: PatchMesh,
|
||||
pending: Option<PatchMesh>,
|
||||
}
|
||||
|
||||
type VelocityFn = Box<dyn Fn(f64, f64, f64) -> (f64, f64) + Send + Sync>;
|
||||
@@ -179,6 +202,8 @@ pub struct CurvilinearPisoSolver {
|
||||
config: CfdConfig,
|
||||
params: CurvilinearParameters,
|
||||
mesh: PatchMesh,
|
||||
/// The mesh the next `advance` ends on (`set_mesh`), if it moves.
|
||||
pending: Option<PatchMesh>,
|
||||
ops: Operators,
|
||||
boundary_velocity: Option<VelocityFn>,
|
||||
momentum_source: Option<VelocityFn>,
|
||||
@@ -198,6 +223,7 @@ impl CurvilinearPisoSolver {
|
||||
config,
|
||||
params,
|
||||
mesh,
|
||||
pending: None,
|
||||
ops,
|
||||
boundary_velocity: None,
|
||||
momentum_source: None,
|
||||
@@ -220,10 +246,37 @@ impl CurvilinearPisoSolver {
|
||||
{
|
||||
self.momentum_source = Some(Box::new(f));
|
||||
}
|
||||
/// The mesh.
|
||||
/// The mesh the field currently lives on (the end of the last step).
|
||||
pub fn mesh(&self) -> &PatchMesh {
|
||||
&self.mesh
|
||||
}
|
||||
/// The mesh named for the end of the next step, if any.
|
||||
pub fn next_mesh(&self) -> Option<&PatchMesh> {
|
||||
self.pending.as_ref()
|
||||
}
|
||||
/// Name the geometry the NEXT `advance` ends on. Same topology as the
|
||||
/// current mesh (`ns`, `nn`, periodicity); the node motion between the
|
||||
/// two is taken as linear in time. Calling it again before `advance`
|
||||
/// replaces the earlier choice (the coupling loop re-tries a step);
|
||||
/// not calling it leaves the mesh where it is.
|
||||
pub fn set_mesh(&mut self, next: PatchMesh) -> CfdResult<()> {
|
||||
if next.ns() != self.mesh.ns()
|
||||
|| next.nn() != self.mesh.nn()
|
||||
|| next.periodic().is_some() != self.mesh.periodic().is_some()
|
||||
{
|
||||
return Err(CfdError::mesh(format!(
|
||||
"set_mesh: topology changed ({}x{}, periodic {}) -> ({}x{}, periodic {})",
|
||||
self.mesh.ns(),
|
||||
self.mesh.nn(),
|
||||
self.mesh.periodic().is_some(),
|
||||
next.ns(),
|
||||
next.nn(),
|
||||
next.periodic().is_some()
|
||||
)));
|
||||
}
|
||||
self.pending = Some(next);
|
||||
Ok(())
|
||||
}
|
||||
/// The operators.
|
||||
pub fn operators(&self) -> &Operators {
|
||||
&self.ops
|
||||
@@ -244,13 +297,21 @@ impl CurvilinearPisoSolver {
|
||||
pub fn set_time(&mut self, t: f64) {
|
||||
self.time = t;
|
||||
}
|
||||
/// Capture the state.
|
||||
/// Capture the state (time and both meshes).
|
||||
pub fn snapshot(&self) -> CurvilinearSolverState {
|
||||
CurvilinearSolverState { time: self.time }
|
||||
CurvilinearSolverState {
|
||||
time: self.time,
|
||||
mesh: self.mesh.clone(),
|
||||
pending: self.pending.clone(),
|
||||
}
|
||||
}
|
||||
/// Restore a captured state.
|
||||
/// Restore a captured state: operators rebuilt, matrix cache dropped.
|
||||
pub fn restore(&mut self, state: &CurvilinearSolverState) {
|
||||
self.time = state.time;
|
||||
self.mesh = state.mesh.clone();
|
||||
self.pending = state.pending.clone();
|
||||
self.ops = Operators::new(&self.mesh, &self.params.boundaries);
|
||||
self.matrix = None;
|
||||
}
|
||||
|
||||
pub(crate) fn boundary_velocity(&self, x: f64, y: f64, t: f64) -> (f64, f64) {
|
||||
@@ -278,8 +339,15 @@ impl CurvilinearPisoSolver {
|
||||
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);
|
||||
let geo = StepGeometry::stationary(mesh);
|
||||
field.flux = self.predicted_fluxes(
|
||||
&field.u.clone(),
|
||||
&field.v.clone(),
|
||||
&zero,
|
||||
0.0,
|
||||
self.time,
|
||||
&geo,
|
||||
);
|
||||
}
|
||||
|
||||
/// Advance one step of `dt`.
|
||||
@@ -291,10 +359,28 @@ impl CurvilinearPisoSolver {
|
||||
let t_old = self.time;
|
||||
let t_new = t_old + dt;
|
||||
let rho = self.config.density;
|
||||
|
||||
// The mesh moves: `self.mesh` becomes the end-of-step geometry
|
||||
// (operators and matrix follow it); the start-of-step geometry is
|
||||
// kept for this step's volumes and swept faces only.
|
||||
let old = match self.pending.take() {
|
||||
Some(next) => {
|
||||
let old = std::mem::replace(&mut self.mesh, next);
|
||||
self.ops = Operators::new(&self.mesh, &self.params.boundaries);
|
||||
self.matrix = None;
|
||||
Some(old)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let geo = match &old {
|
||||
Some(o) => StepGeometry::new(o, &self.mesh, self.params.swept_face_rule),
|
||||
None => StepGeometry::stationary(&self.mesh),
|
||||
};
|
||||
let old_mesh = old.as_ref().unwrap_or(&self.mesh);
|
||||
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 (uh, vh) = self.predict(field, dt, t_old, old_mesh, &geo);
|
||||
let mut flux = self.predicted_fluxes(&uh, &vh, &field.p, dt, t_new, &geo);
|
||||
let adjustment = self.adjust_boundary_flux(&mut flux);
|
||||
for c in 0..mesh.cell_count() {
|
||||
let g = self.pressure_gradient(&field.p, c);
|
||||
|
||||
Reference in New Issue
Block a user