pub(in crate::solvers::dec) mod no_slip;
mod pressure;
mod run;
mod seed;
mod step;
use alloc::format;
use deep_causality_topology::{ChainComplex, HodgeDecomposeOptions, LatticeComplex, Manifold};
use crate::solvers::dec::DecNsScalar;
use crate::solvers::dec::dec_ns_rate::DecNsRate;
use deep_causality_physics::BodyForceOneForm;
use deep_causality_physics::PhysicsError;
#[derive(Debug)]
pub struct DecNsSolver<'m, const D: usize, R: DecNsScalar> {
pub(super) rate: DecNsRate<'m, D, R>,
pub(super) manifold: &'m Manifold<LatticeComplex<D, R>, R>,
pub(super) dt: R,
pub(super) cg_options: HodgeDecomposeOptions<R>,
pub(super) cfl_advective: R,
pub(super) cfl_diffusive: R,
pub(super) dx_min: R,
pub(super) lift: Vec<(usize, R)>,
}
impl<'m, const D: usize, R: DecNsScalar> DecNsSolver<'m, D, R> {
pub fn new(
manifold: &'m Manifold<LatticeComplex<D, R>, R>,
nu: R,
dt: R,
body_force: Option<&BodyForceOneForm<R>>,
) -> Result<Self, PhysicsError> {
let rate = DecNsRate::new(manifold, nu, body_force)?;
if !dt.is_finite() || dt <= R::zero() {
return Err(PhysicsError::PhysicalInvariantBroken(format!(
"DecNsSolver: dt must be finite and positive, got {dt}"
)));
}
let complex = manifold.complex();
let metric = manifold
.metric()
.expect("metric presence validated by DecNsRate::new");
let mut dx_min: Option<R> = None;
for cell in complex.iter_cells(1) {
let len = metric.cell_volume(complex, &cell);
dx_min = Some(match dx_min {
Some(m) if m <= len => m,
_ => len,
});
}
let dx_min = dx_min.ok_or_else(|| {
PhysicsError::DimensionMismatch(
"DecNsSolver: the lattice has no edges (empty shape)".into(),
)
})?;
let default_safety = R::from_f64(0.9)
.expect("0.9 lifts into R");
Ok(Self {
rate,
manifold,
dt,
cg_options: HodgeDecomposeOptions::default(),
cfl_advective: default_safety,
cfl_diffusive: default_safety,
dx_min,
lift: alloc::vec::Vec::new(),
})
}
pub fn with_zones<Z>(
manifold: &'m Manifold<LatticeComplex<D, R>, R>,
nu: R,
dt: R,
zones: Z,
) -> Result<Self, PhysicsError>
where
Z: crate::solvers::dec::boundary::BoundaryZone<D, R>,
{
let n1 = manifold.complex().num_cells(1);
let mut source = alloc::vec![R::zero(); n1];
zones.collect_rate_source(manifold, &mut source);
let body_force = if source.iter().any(|v| *v != R::zero()) {
let tensor = deep_causality_tensor::CausalTensor::new(source, alloc::vec![n1])
.expect("1-D tensor allocation cannot fail");
Some(BodyForceOneForm::new(tensor, manifold)?)
} else {
None
};
let mut solver = Self::new(manifold, nu, dt, body_force.as_ref())?;
let mut lift = alloc::vec::Vec::new();
zones.collect_lift(manifold, 0, &mut lift);
solver.lift = lift;
let mut slip = alloc::vec::Vec::new();
zones.collect_slip_edges(manifold, &mut slip);
solver.rate.apply_slip(&slip);
let mut constrained = alloc::vec::Vec::new();
zones.collect_constrained_edges(manifold, &mut constrained);
if !constrained.is_empty() {
solver.rate.set_zone_constrained(constrained);
}
let mut prescribed = alloc::vec::Vec::new();
zones.collect_prescribed_edges(manifold, &mut prescribed);
let mut reference = alloc::vec::Vec::new();
zones.collect_reference_vertices(manifold, &mut reference);
if !prescribed.is_empty() || !reference.is_empty() {
solver.rate.set_open_boundary(prescribed, reference);
}
Ok(solver)
}
pub fn with_moving_wall(
mut self,
wall_axis: usize,
max_side: bool,
velocity: [R; D],
) -> Result<Self, PhysicsError> {
if wall_axis >= D {
return Err(PhysicsError::DimensionMismatch(format!(
"with_moving_wall: wall axis {wall_axis} out of range for D = {D}"
)));
}
let complex = self.manifold.complex();
if complex.periodic()[wall_axis] {
return Err(PhysicsError::PhysicalInvariantBroken(format!(
"with_moving_wall: axis {wall_axis} is periodic — there is no wall to move"
)));
}
if velocity.iter().any(|v| !v.is_finite()) {
return Err(PhysicsError::NumericalInstability(
"with_moving_wall: velocity must be finite".into(),
));
}
if velocity[wall_axis] != R::zero() {
return Err(PhysicsError::PhysicalInvariantBroken(format!(
"with_moving_wall: the wall-normal velocity component (axis {wall_axis}) \
must be zero — wall-normal flux is the projection's Neumann condition"
)));
}
let metric = self
.manifold
.metric()
.expect("metric presence validated by DecNsRate::new");
let shape = complex.shape();
let wall_pos = if max_side { shape[wall_axis] - 1 } else { 0 };
let mut lift = alloc::vec::Vec::new();
for (idx, cell) in complex.iter_cells(1).enumerate() {
let axis = cell.orientation().trailing_zeros() as usize;
if axis == wall_axis
|| velocity[axis] == R::zero()
|| cell.position()[wall_axis] != wall_pos
{
continue;
}
let length = metric.cell_volume(complex, &cell);
lift.push((idx, velocity[axis] * length));
}
self.lift = lift;
Ok(self)
}
pub fn with_cg_options(mut self, opts: HodgeDecomposeOptions<R>) -> Self {
self.cg_options = opts;
self
}
pub fn with_warm_start(mut self) -> Self {
self.rate.set_warm_start(true);
self
}
pub fn with_staircase_noslip(mut self) -> Self {
self.rate.set_staircase_noslip();
self
}
pub fn with_spectral_diffusion(mut self) -> Result<Self, PhysicsError> {
self.rate = self.rate.with_spectral_diffusion()?;
Ok(self)
}
pub fn with_cfl_factors(mut self, advective: R, diffusive: R) -> Result<Self, PhysicsError> {
if !advective.is_finite()
|| advective <= R::zero()
|| !diffusive.is_finite()
|| diffusive <= R::zero()
{
return Err(PhysicsError::PhysicalInvariantBroken(format!(
"DecNsSolver: CFL safety factors must be finite and positive, \
got advective {advective}, diffusive {diffusive}"
)));
}
self.cfl_advective = advective;
self.cfl_diffusive = diffusive;
Ok(self)
}
pub fn dt(&self) -> R {
self.dt
}
pub fn nu(&self) -> R {
self.rate.nu()
}
pub fn dx_min(&self) -> R {
self.dx_min
}
pub fn rate(&self) -> &DecNsRate<'m, D, R> {
&self.rate
}
}