use super::{MetricProvider, sample_grid};
use crate::CfdScalar;
use crate::tensor_bridge::{gradient_x, gradient_y, quantize_2d};
use deep_causality_algebra::ConjugateScalar;
use deep_causality_physics::PhysicsError;
use deep_causality_tensor::{
CausalTensorTrain, CausalTensorTrainOperator, TensorTrain, TensorTrainOperator, Truncation,
};
const DET_FLOOR_FRACTION: f64 = 1.0e-6;
pub struct BlendedMapConfig<R> {
lx: usize,
ly: usize,
r0: R,
dr: R,
theta0: R,
dtheta: R,
lambda: R,
}
impl<R: CfdScalar> BlendedMapConfig<R> {
pub fn builder() -> BlendedMapConfigBuilder<R> {
BlendedMapConfigBuilder::new()
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(lx: usize, ly: usize, r0: R, dr: R, theta0: R, dtheta: R, lambda: R) -> Self {
Self {
lx,
ly,
r0,
dr,
theta0,
dtheta,
lambda,
}
}
}
pub struct BlendedMapConfigBuilder<R> {
lattice: Option<(usize, usize)>,
radial: Option<(R, R)>,
angular: Option<(R, R)>,
lambda: Option<R>,
}
impl<R: CfdScalar> Default for BlendedMapConfigBuilder<R> {
fn default() -> Self {
Self::new()
}
}
impl<R: CfdScalar> BlendedMapConfigBuilder<R> {
pub fn new() -> Self {
Self {
lattice: None,
radial: None,
angular: None,
lambda: None,
}
}
pub fn lattice(mut self, lx: usize, ly: usize) -> Self {
self.lattice = Some((lx, ly));
self
}
pub fn radial_range(mut self, r0: R, dr: R) -> Self {
self.radial = Some((r0, dr));
self
}
pub fn angular_range(mut self, theta0: R, dtheta: R) -> Self {
self.angular = Some((theta0, dtheta));
self
}
pub fn lambda(mut self, lambda: R) -> Self {
self.lambda = Some(lambda);
self
}
pub fn build(self) -> Result<BlendedMapConfig<R>, PhysicsError> {
let missing = |what: &str| {
PhysicsError::PhysicalInvariantBroken(format!(
"BlendedMapConfig::builder: {what} is required"
))
};
let (lx, ly) = self.lattice.ok_or_else(|| missing("a lattice"))?;
let (r0, dr) = self.radial.ok_or_else(|| missing("a radial range"))?;
let (theta0, dtheta) = self.angular.ok_or_else(|| missing("an angular range"))?;
let lambda = self.lambda.ok_or_else(|| missing("a blend parameter"))?;
let positive = |x: R| x.is_finite() && x > R::zero();
if !positive(r0) || !positive(dr) || !positive(dtheta) {
return Err(PhysicsError::PhysicalInvariantBroken(
"BlendedMap requires r0 > 0, dr > 0, dtheta > 0".into(),
));
}
if !theta0.is_finite() {
return Err(PhysicsError::PhysicalInvariantBroken(
"BlendedMap requires a finite theta0".into(),
));
}
if !lambda.is_finite() || lambda < R::zero() || lambda > R::one() {
return Err(PhysicsError::PhysicalInvariantBroken(
"BlendedMap requires lambda in [0, 1]".into(),
));
}
Ok(BlendedMapConfig::new(
lx, ly, r0, dr, theta0, dtheta, lambda,
))
}
}
pub struct BlendedMap<R>
where
R: CfdScalar + ConjugateScalar<Real = R>,
{
lx: usize,
ly: usize,
lambda: R,
r0: R,
dr: R,
theta0: R,
dtheta: R,
span_y: R,
g_xi: CausalTensorTrainOperator<R>,
g_eta: CausalTensorTrainOperator<R>,
dxi_dx: CausalTensorTrain<R>,
deta_dx: CausalTensorTrain<R>,
dxi_dy: CausalTensorTrain<R>,
deta_dy: CausalTensorTrain<R>,
jacobian: CausalTensorTrain<R>,
min_abs_det: R,
det_floor: R,
trunc: Truncation<R>,
}
impl<R> BlendedMap<R>
where
R: CfdScalar + ConjugateScalar<Real = R>,
{
pub fn new(cfg: BlendedMapConfig<R>, trunc: Truncation<R>) -> Result<Self, PhysicsError> {
let BlendedMapConfig {
lx,
ly,
r0,
dr,
theta0,
dtheta,
lambda,
} = cfg;
let one = R::one();
let two = one + one;
let half = R::from_f64(0.5).unwrap_or_else(R::one);
let nx = 1usize << lx;
let ny = 1usize << ly;
let dxi = one
/ R::from_usize(nx).ok_or_else(|| {
PhysicsError::NumericalInstability("from_usize(nx) failed".into())
})?;
let deta = one
/ R::from_usize(ny).ok_or_else(|| {
PhysicsError::NumericalInstability("from_usize(ny) failed".into())
})?;
let g_xi = gradient_x::<R>(lx, ly, dxi, &trunc)?;
let g_eta = gradient_y::<R>(lx, ly, deta, &trunc)?;
let span_y = two * (r0 + half * dr) * (half * dtheta).sin();
let oml = one - lambda;
let forward = move |xi: R, eta: R| -> (R, R, R, R) {
let theta = theta0 + xi * dtheta;
let r = r0 + eta * dr;
let s = theta.sin();
let c = theta.cos();
let dx_dxi = lambda * (R::zero() - r * s * dtheta);
let dx_deta = oml * dr + lambda * (c * dr);
let dy_dxi = oml * span_y + lambda * (r * c * dtheta);
let dy_deta = lambda * (s * dr);
(dx_dxi, dx_deta, dy_dxi, dy_deta)
};
let det_at = move |xi: R, eta: R| -> R {
let (a, b, c, d) = forward(xi, eta);
a * d - b * c
};
let nxr = R::from_usize(nx)
.ok_or_else(|| PhysicsError::NumericalInstability("from_usize(nx) failed".into()))?;
let nyr = R::from_usize(ny)
.ok_or_else(|| PhysicsError::NumericalInstability("from_usize(ny) failed".into()))?;
let det_scale = dr * span_y;
let det_floor = R::from_f64(DET_FLOOR_FRACTION).unwrap_or_else(R::zero) * det_scale;
let (mut det_min_abs, mut det_sign_positive) = (None::<R>, None::<bool>);
for i in 0..=nx {
let xi = R::from_usize(i)
.ok_or_else(|| PhysicsError::NumericalInstability("from_usize(i) failed".into()))?
/ nxr;
for j in 0..=ny {
let eta = R::from_usize(j).ok_or_else(|| {
PhysicsError::NumericalInstability("from_usize(j) failed".into())
})? / nyr;
let det = det_at(xi, eta);
if !det.is_finite() {
return Err(PhysicsError::PhysicalInvariantBroken(format!(
"BlendedMap: non-finite Jacobian determinant at the (xi, eta) sample \
({i}, {j})"
)));
}
let positive = det > R::zero();
match det_sign_positive {
None => det_sign_positive = Some(positive),
Some(first) if first != positive => {
return Err(PhysicsError::PhysicalInvariantBroken(format!(
"BlendedMap: the map folds — the Jacobian determinant changes sign by \
the (xi, eta) sample ({i}, {j}), so the chart is not invertible over \
the computational domain"
)));
}
Some(_) => {}
}
let mag = det.abs();
if det_min_abs.is_none_or(|m| mag < m) {
det_min_abs = Some(mag);
}
}
}
if let Some(min_abs) = det_min_abs
&& min_abs < det_floor
{
return Err(PhysicsError::PhysicalInvariantBroken(format!(
"BlendedMap: the map is near-singular — min |det J| is below {DET_FLOOR_FRACTION} \
x the geometric scale (dr x span_y); the inverse metric (cofactor / det) would be \
unbounded"
)));
}
let neg = R::zero() - one;
let dxi_dx = quantize_2d(
&sample_grid(lx, ly, |xi, eta| {
let (_, _, _, d) = forward(xi, eta);
d / det_at(xi, eta)
})?,
&trunc,
)?;
let dxi_dy = quantize_2d(
&sample_grid(lx, ly, |xi, eta| {
let (_, b, _, _) = forward(xi, eta);
neg * b / det_at(xi, eta)
})?,
&trunc,
)?;
let deta_dx = quantize_2d(
&sample_grid(lx, ly, |xi, eta| {
let (_, _, c, _) = forward(xi, eta);
neg * c / det_at(xi, eta)
})?,
&trunc,
)?;
let deta_dy = quantize_2d(
&sample_grid(lx, ly, |xi, eta| {
let (a, _, _, _) = forward(xi, eta);
a / det_at(xi, eta)
})?,
&trunc,
)?;
let jacobian = quantize_2d(
&sample_grid(lx, ly, |xi, eta| det_at(xi, eta).abs())?,
&trunc,
)?;
Ok(Self {
lx,
ly,
lambda,
r0,
dr,
theta0,
dtheta,
span_y,
g_xi,
g_eta,
dxi_dx,
deta_dx,
dxi_dy,
deta_dy,
jacobian,
min_abs_det: det_min_abs.unwrap_or_else(R::zero),
det_floor,
trunc,
})
}
pub fn det_margin(&self) -> (R, R) {
(self.min_abs_det, self.det_floor)
}
pub fn lambda(&self) -> R {
self.lambda
}
pub fn position(&self, xi: R, eta: R) -> (R, R) {
let one = R::one();
let half = R::from_f64(0.5).unwrap_or_else(R::one);
let oml = one - self.lambda;
let theta = self.theta0 + xi * self.dtheta;
let r = self.r0 + eta * self.dr;
let xc = self.r0 + eta * self.dr;
let yc = (R::zero() - half) * self.span_y + xi * self.span_y;
let xf = r * theta.cos();
let yf = r * theta.sin();
(oml * xc + self.lambda * xf, oml * yc + self.lambda * yf)
}
pub fn jacobian(&self) -> &CausalTensorTrain<R> {
&self.jacobian
}
pub fn sample<F>(&self, f: F) -> Result<CausalTensorTrain<R>, PhysicsError>
where
F: Fn(R, R) -> R,
{
quantize_2d(&sample_grid(self.lx, self.ly, f)?, &self.trunc)
}
pub fn physical_gradient(
&self,
u: &CausalTensorTrain<R>,
) -> Result<(CausalTensorTrain<R>, CausalTensorTrain<R>), PhysicsError> {
let du_dxi = self.g_xi.apply(u, &self.trunc)?;
let du_deta = self.g_eta.apply(u, &self.trunc)?;
let du_dx = self
.dxi_dx
.hadamard_rounded(&du_dxi, &self.trunc)?
.add(&self.deta_dx.hadamard_rounded(&du_deta, &self.trunc)?)?
.round(&self.trunc)?;
let du_dy = self
.dxi_dy
.hadamard_rounded(&du_dxi, &self.trunc)?
.add(&self.deta_dy.hadamard_rounded(&du_deta, &self.trunc)?)?
.round(&self.trunc)?;
Ok((du_dx, du_dy))
}
}
impl<R> MetricProvider<R> for BlendedMap<R>
where
R: CfdScalar + ConjugateScalar<Real = R>,
{
fn dims(&self) -> (usize, usize) {
(self.lx, self.ly)
}
fn sample<F>(&self, f: F) -> Result<CausalTensorTrain<R>, PhysicsError>
where
F: Fn(R, R) -> R,
{
BlendedMap::sample(self, f)
}
fn physical_gradient(
&self,
u: &CausalTensorTrain<R>,
) -> Result<(CausalTensorTrain<R>, CausalTensorTrain<R>), PhysicsError> {
BlendedMap::physical_gradient(self, u)
}
fn jacobian(&self) -> &CausalTensorTrain<R> {
BlendedMap::jacobian(self)
}
}