use alloc::collections::BTreeMap;
use deep_causality_tensor::CausalTensor;
use deep_causality_topology::{CutCellRegistry, LatticeComplex, Manifold};
use crate::solvers::dec::DecNsScalar;
use deep_causality_physics::PhysicsError;
pub fn pressure_surface_force<const D: usize, R: DecNsScalar>(
registry: &CutCellRegistry<D, R>,
cell_pressure: impl Fn(usize) -> R,
) -> [R; D] {
let mut force = [R::zero(); D];
for (&cell_id, cut) in registry.iter() {
let p = cell_pressure(cell_id);
for fragment in cut.fragments() {
let normal = fragment.outward_normal();
let area = fragment.area();
for (d, f) in force.iter_mut().enumerate() {
*f -= p * normal[d] * area;
}
}
}
force
}
pub fn fragment_area_vector<const D: usize, R: DecNsScalar>(
registry: &CutCellRegistry<D, R>,
) -> [R; D] {
let mut area_vector = [R::zero(); D];
for (_, cut) in registry.iter() {
for fragment in cut.fragments() {
let normal = fragment.outward_normal();
let area = fragment.area();
for (d, a) in area_vector.iter_mut().enumerate() {
*a += normal[d] * area;
}
}
}
area_vector
}
pub fn viscous_surface_force<const D: usize, R: DecNsScalar>(
manifold: &Manifold<LatticeComplex<D, R>, R>,
registry: &CutCellRegistry<D, R>,
edge_form: &CausalTensor<R>,
mu: R,
) -> Result<[R; D], PhysicsError> {
use alloc::format;
let dx = manifold
.metric()
.and_then(|g| g.axis_lengths())
.ok_or_else(|| {
PhysicsError::TopologyError(
"viscous_surface_force requires an axis-aligned geometry (per-axis spacing)".into(),
)
})?;
let vertex_vectors = manifold
.sharp(edge_form)
.map_err(|e| PhysicsError::TopologyError(format!("sharp failed: {e}")))?;
let complex = manifold.complex();
let velocity: BTreeMap<[usize; D], [R; D]> = complex
.iter_cells(0)
.zip(vertex_vectors.as_slice().as_chunks::<D>().0)
.map(|(vertex, v)| (*vertex.position(), *v))
.collect();
let mut force = [R::zero(); D];
for (_, cut) in registry.iter() {
for fragment in cut.fragments() {
let area = fragment.area();
let centroid = fragment.centroid();
let raw_n = fragment.outward_normal();
let mut nn = R::zero();
for &c in raw_n.iter() {
nn += c * c;
}
if nn <= R::zero() {
continue;
}
let inv = R::one() / nn.sqrt();
let mut n = [R::zero(); D];
for (i, ni) in n.iter_mut().enumerate() {
*ni = raw_n[i] * inv;
}
let mut delta_h = R::zero();
let mut sample = [R::zero(); D];
for i in 0..D {
delta_h += n[i].abs() * dx[i];
}
if delta_h <= R::zero() {
continue;
}
for (i, s) in sample.iter_mut().enumerate() {
*s = centroid[i] + delta_h * n[i];
}
let u_sample = sample_velocity(&velocity, &sample, &dx);
let mut u_dot_n = R::zero();
for i in 0..D {
u_dot_n += u_sample[i] * n[i];
}
for (i, f) in force.iter_mut().enumerate() {
let traction_i = mu * (u_sample[i] + n[i] * u_dot_n) / delta_h;
*f += traction_i * area;
}
}
}
Ok(force)
}
fn cell_and_fraction<const D: usize, R: DecNsScalar>(
p: &[R; D],
dx: &[R; D],
) -> ([usize; D], [R; D]) {
let mut lo = [0usize; D];
let mut frac = [R::zero(); D];
for j in 0..D {
let g = p[j] / dx[j];
let f = g.floor();
let (k, k_r) = if f >= R::zero() {
match f.to_usize() {
Some(k) => (k, f),
None => (0usize, R::zero()),
}
} else {
(0usize, R::zero())
};
lo[j] = k;
frac[j] = g - k_r;
}
(lo, frac)
}
fn sample_velocity<const D: usize, R: DecNsScalar>(
velocity: &BTreeMap<[usize; D], [R; D]>,
p: &[R; D],
dx: &[R; D],
) -> [R; D] {
let (lo, frac) = cell_and_fraction(p, dx);
let mut out = [R::zero(); D];
for corner in 0..(1usize << D) {
let mut pos = [0usize; D];
let mut weight = R::one();
for j in 0..D {
let bit = (corner >> j) & 1;
pos[j] = lo[j] + bit;
weight *= if bit == 1 {
frac[j]
} else {
R::one() - frac[j]
};
}
if let Some(v) = velocity.get(&pos) {
for (o, &vi) in out.iter_mut().zip(v.iter()) {
*o += weight * vi;
}
}
}
out
}
pub fn force_coefficient<R: DecNsScalar>(force_component: R, u_ref: R, reference_area: R) -> R {
let half = R::from_f64(0.5).expect("0.5 lifts into every real field");
force_component / (half * u_ref * u_ref * reference_area)
}
pub fn wall_heat_flux<const D: usize, R: DecNsScalar>(
manifold: &Manifold<LatticeComplex<D, R>, R>,
registry: &CutCellRegistry<D, R>,
scalar: &CausalTensor<R>,
t_wall: R,
k: R,
) -> Result<R, PhysicsError> {
use alloc::format;
use deep_causality_topology::ChainComplex;
if !k.is_finite() || !t_wall.is_finite() {
return Err(PhysicsError::NumericalInstability(
"wall_heat_flux: conductivity and wall temperature must be finite".into(),
));
}
let complex = manifold.complex();
let n0 = complex.num_cells(0);
if scalar.len() != n0 {
return Err(PhysicsError::DimensionMismatch(format!(
"wall_heat_flux: expected {} scalar values (one per vertex), got {}",
n0,
scalar.len()
)));
}
let dx = manifold
.metric()
.and_then(|g| g.axis_lengths())
.ok_or_else(|| {
PhysicsError::TopologyError(
"wall_heat_flux requires an axis-aligned geometry (per-axis spacing)".into(),
)
})?;
let temperature: BTreeMap<[usize; D], R> = complex
.iter_cells(0)
.zip(scalar.as_slice().iter())
.map(|(vertex, &t)| (*vertex.position(), t))
.collect();
let num_top_cells = complex.num_cells(D);
let mut flux = R::zero();
for (&cell_id, cut) in registry.iter() {
if cell_id >= num_top_cells {
continue;
}
for fragment in cut.fragments() {
let area = fragment.area();
let centroid = fragment.centroid();
let raw_n = fragment.outward_normal();
let mut nn = R::zero();
for &c in raw_n.iter() {
nn += c * c;
}
if nn <= R::zero() {
continue;
}
let inv = R::one() / nn.sqrt();
let mut n = [R::zero(); D];
for (i, ni) in n.iter_mut().enumerate() {
*ni = raw_n[i] * inv;
}
let mut delta_h = R::zero();
for i in 0..D {
delta_h += n[i].abs() * dx[i];
}
if delta_h <= R::zero() {
continue;
}
let mut sample = [R::zero(); D];
for (i, s) in sample.iter_mut().enumerate() {
*s = centroid[i] + delta_h * n[i];
}
let t_sample = sample_scalar(&temperature, &sample, &dx, t_wall);
let dt_dn = (t_sample - t_wall) / delta_h;
flux += (R::zero() - k) * dt_dn * area;
}
}
Ok(flux)
}
fn sample_scalar<const D: usize, R: DecNsScalar>(
temperature: &BTreeMap<[usize; D], R>,
p: &[R; D],
dx: &[R; D],
fallback: R,
) -> R {
let (lo, frac) = cell_and_fraction(p, dx);
let mut out = R::zero();
for corner in 0..(1usize << D) {
let mut pos = [0usize; D];
let mut weight = R::one();
for j in 0..D {
let bit = (corner >> j) & 1;
pos[j] = lo[j] + bit;
weight *= if bit == 1 {
frac[j]
} else {
R::one() - frac[j]
};
}
out += weight * temperature.get(&pos).copied().unwrap_or(fallback);
}
out
}