use crate::CfdScalar;
use crate::tensor_bridge::{
QttProjector2d, dequantize_2d, gradient_x, gradient_y, laplacian_2d, quantize_2d,
};
use crate::traits::Marcher;
use alloc::format;
use deep_causality_algebra::ConjugateScalar;
use deep_causality_physics::PhysicsError;
use deep_causality_tensor::{
CausalTensor, CausalTensorTrain, CausalTensorTrainOperator, TensorTrain, TensorTrainOperator,
Truncation,
};
pub struct QttIncompressible2d<R>
where
R: CfdScalar + ConjugateScalar<Real = R>,
{
lx: usize,
ly: usize,
dt: R,
nu: R,
gx: CausalTensorTrainOperator<R>,
gy: CausalTensorTrainOperator<R>,
lap: CausalTensorTrainOperator<R>,
projector: QttProjector2d<R>,
trunc: Truncation<R>,
}
impl<R> QttIncompressible2d<R>
where
R: CfdScalar + ConjugateScalar<Real = R>,
{
#[allow(clippy::too_many_arguments)]
pub fn new(
lx: usize,
ly: usize,
dx: R,
dy: R,
dt: R,
nu: R,
trunc: Truncation<R>,
) -> Result<Self, PhysicsError> {
validate_qtt_envelope(dx, dy, dt, nu)?;
Ok(Self {
lx,
ly,
dt,
nu,
gx: gradient_x::<R>(lx, ly, dx, &trunc)?,
gy: gradient_y::<R>(lx, ly, dy, &trunc)?,
lap: laplacian_2d::<R>(lx, ly, dx, dy, &trunc)?,
projector: QttProjector2d::new(lx, ly, dx, dy, trunc)?,
trunc,
})
}
pub fn modes(&self) -> (usize, usize) {
(self.lx, self.ly)
}
pub fn projector(&self) -> &QttProjector2d<R> {
&self.projector
}
pub fn dt(&self) -> R {
self.dt
}
pub fn trunc(&self) -> Truncation<R> {
self.trunc
}
pub fn rate_pair(
&self,
u: &CausalTensorTrain<R>,
v: &CausalTensorTrain<R>,
) -> Result<(CausalTensorTrain<R>, CausalTensorTrain<R>), PhysicsError> {
Ok((self.rate(u, u, v)?, self.rate(v, u, v)?))
}
pub fn scalar_rate(
&self,
s: &CausalTensorTrain<R>,
u: &CausalTensorTrain<R>,
v: &CausalTensorTrain<R>,
kappa: R,
) -> Result<CausalTensorTrain<R>, PhysicsError> {
let t = &self.trunc;
let neg = R::zero() - R::one();
let dsx = self.gx.apply(s, t)?;
let dsy = self.gy.apply(s, t)?;
let conv = u
.hadamard_rounded(&dsx, t)?
.add(&v.hadamard_rounded(&dsy, t)?)?
.round(t)?;
let diff = self.lap.apply(s, t)?.scale(kappa);
Ok(diff.add(&conv.scale(neg))?.round(t)?)
}
fn rate(
&self,
a: &CausalTensorTrain<R>,
u: &CausalTensorTrain<R>,
v: &CausalTensorTrain<R>,
) -> Result<CausalTensorTrain<R>, PhysicsError> {
let t = &self.trunc;
let neg = R::zero() - R::one();
let dax = self.gx.apply(a, t)?;
let day = self.gy.apply(a, t)?;
let conv = u
.hadamard_rounded(&dax, t)?
.add(&v.hadamard_rounded(&day, t)?)?
.round(t)?;
let diff = self.lap.apply(a, t)?.scale(self.nu);
Ok(diff.add(&conv.scale(neg))?.round(t)?)
}
fn step(
&self,
u: &CausalTensorTrain<R>,
v: &CausalTensorTrain<R>,
) -> Result<(CausalTensorTrain<R>, CausalTensorTrain<R>), PhysicsError> {
let t = &self.trunc;
let ru = self.rate(u, u, v)?;
let rv = self.rate(v, u, v)?;
let ustar = u.add(&ru.scale(self.dt))?.round(t)?;
let vstar = v.add(&rv.scale(self.dt))?.round(t)?;
self.projector.project(&ustar, &vstar)
}
pub fn run(
&self,
u0: &CausalTensor<R>,
v0: &CausalTensor<R>,
steps: usize,
) -> Result<(CausalTensor<R>, CausalTensor<R>), PhysicsError> {
let want = [1usize << self.lx, 1usize << self.ly];
for f in [u0, v0] {
if f.shape() != want {
return Err(PhysicsError::DimensionMismatch(format!(
"field shape {:?} does not match the grid {want:?}",
f.shape()
)));
}
}
let mut u = quantize_2d(u0, &self.trunc)?;
let mut v = quantize_2d(v0, &self.trunc)?;
for _ in 0..steps {
let (un, vn) = self.step(&u, &v)?;
u = un;
v = vn;
}
Ok((
dequantize_2d(&u, self.lx, self.ly)?,
dequantize_2d(&v, self.lx, self.ly)?,
))
}
}
impl<R> Marcher<R> for QttIncompressible2d<R>
where
R: CfdScalar + ConjugateScalar<Real = R>,
{
type State = (CausalTensorTrain<R>, CausalTensorTrain<R>);
type Ambient = ();
type Output = (CausalTensorTrain<R>, CausalTensorTrain<R>);
fn advance(
&self,
state: &Self::State,
_ambient: &Self::Ambient,
) -> Result<Self::Output, PhysicsError> {
self.step(&state.0, &state.1)
}
}
pub(crate) fn validate_qtt_envelope<R: CfdScalar>(
dx: R,
dy: R,
dt: R,
nu: R,
) -> Result<(), PhysicsError> {
if !dx.is_finite() || dx <= R::zero() {
return Err(PhysicsError::PhysicalInvariantBroken(format!(
"QTT solver: dx must be positive and finite, got {dx:?}"
)));
}
if !dy.is_finite() || dy <= R::zero() {
return Err(PhysicsError::PhysicalInvariantBroken(format!(
"QTT solver: dy must be positive and finite, got {dy:?}"
)));
}
if !dt.is_finite() || dt <= R::zero() {
return Err(PhysicsError::PhysicalInvariantBroken(format!(
"QTT solver: dt must be positive and finite, got {dt:?}"
)));
}
if !nu.is_finite() || nu < R::zero() {
return Err(PhysicsError::PhysicalInvariantBroken(format!(
"QTT solver: kinematic viscosity nu must be non-negative and finite, got {nu:?}"
)));
}
if nu > R::zero() {
let four = R::from_f64(4.0).expect("4 lifts into every real field");
let dmin = if dx < dy { dx } else { dy };
let diffusive_limit = dmin * dmin / (four * nu);
if dt > diffusive_limit {
return Err(PhysicsError::PhysicalInvariantBroken(format!(
"QTT solver: dt {dt:?} exceeds the diffusive explicit-stability limit {diffusive_limit:?} \
(min(dx, dy)² {:?} / (4·ν {nu:?})); explicit Euler is unstable beyond it",
dmin * dmin
)));
}
}
Ok(())
}