use crate::{
core::{
scratch::Scratch,
state::{BlockStateMut, FieldHandle, State, StateBuilder},
storage::StorageBackend,
},
discretization::{
finite_volume::KarmaRappelFlux,
operators::{AnisotropicDivergence, Discretizes, Laplacian},
stencil::Stencil,
},
geometry::{
cartesian::{CartesianGrid, fill_ghosts_mirror, for_each_interior},
grid::{BlockId, Grid},
},
physics::model::{Model, RhsContext},
};
pub const A1: f64 = 0.8836;
pub const A2: f64 = 0.6267;
#[derive(Debug, Clone)]
pub struct ModelC {
pub eps4: f64,
pub lambda: f64,
pub d_thermal: f64,
pub noise_amplitude: f64,
pub anisotropy_tol: f64,
a_bar: f64,
eps_prime: f64,
orientations: bool,
phi: Option<FieldHandle<f64>>,
u: Option<FieldHandle<f64>>,
theta: Option<FieldHandle<f64>>,
}
#[derive(Debug, Clone, Copy)]
pub struct Grain {
pub center: [f64; 2],
pub radius: f64,
pub orientation: f64,
}
impl ModelC {
#[must_use]
pub fn classic() -> Self {
Self::new(0.06, 3.19, 0.0)
}
#[must_use]
pub fn new(eps4: f64, lambda: f64, noise_amplitude: f64) -> Self {
let a_bar = 3.0f64.mul_add(-eps4, 1.0);
Self {
eps4,
lambda,
d_thermal: A2 * lambda,
noise_amplitude,
anisotropy_tol: 1e-8,
a_bar,
eps_prime: 3.0 * eps4 / a_bar,
orientations: false,
phi: None,
u: None,
theta: None,
}
}
#[must_use]
pub const fn with_orientations(mut self) -> Self {
self.orientations = true;
self
}
#[must_use]
pub const fn phi(&self) -> FieldHandle<f64> {
self.phi.expect("model fields not yet registered")
}
#[must_use]
pub const fn u(&self) -> FieldHandle<f64> {
self.u.expect("model fields not yet registered")
}
#[must_use]
pub const fn theta0(&self) -> Option<FieldHandle<f64>> {
self.theta
}
#[inline(always)]
fn center_anisotropy(
&self,
phi: &<CartesianGrid<2> as Grid>::View<'_, f64>,
[i, j]: [isize; 2],
) -> f64 {
let dx = 0.5 * (phi.get([i + 1, j]) - phi.get([i - 1, j]));
let dy = 0.5 * (phi.get([i, j + 1]) - phi.get([i, j - 1]));
let dx2 = dx * dx;
let dy2 = dy * dy;
let sum = dx2 + dy2;
let mag2 = sum * sum;
if mag2 <= self.anisotropy_tol {
self.a_bar
} else {
self.a_bar * (1.0 + self.eps_prime * (dx2 * dx2 + dy2 * dy2) / mag2)
}
}
pub fn initialize<S: StorageBackend<f64>>(
&self,
grid: &CartesianGrid<2>,
state: &mut State<f64, S>,
seed_center: [f64; 2],
seed_radius: f64,
u0: f64,
) {
self.initialize_seeds(grid, state, &[(seed_center, seed_radius)], u0);
}
pub fn initialize_seeds<S: StorageBackend<f64>>(
&self,
grid: &CartesianGrid<2>,
state: &mut State<f64, S>,
seeds: &[([f64; 2], f64)],
u0: f64,
) {
let grains: Vec<Grain> = seeds
.iter()
.map(|&(center, radius)| Grain {
center,
radius,
orientation: 0.0,
})
.collect();
self.initialize_grains(grid, state, &grains, u0);
}
pub fn initialize_grains<S: StorageBackend<f64>>(
&self,
grid: &CartesianGrid<2>,
state: &mut State<f64, S>,
grains: &[Grain],
u0: f64,
) {
const CUTOFF: f64 = 27.0;
let (phi_h, u_h) = (self.phi(), self.u());
let interior = grid.block_cells();
let [nx, ny] = interior;
for b in 0..grid.num_blocks() {
let block = BlockId(b as u32);
let [hx, hy] = grid.spacing(block);
let c0 = grid.cell_center(block, [0, 0]);
let c1 = grid.cell_center(block, [nx as isize - 1, ny as isize - 1]);
let (lo, hi) = (
[0.5f64.mul_add(-hx, c0[0]), 0.5f64.mul_add(-hy, c0[1])],
[0.5f64.mul_add(hx, c1[0]), 0.5f64.mul_add(hy, c1[1])],
);
let dist_box = |cx: f64, cy: f64| {
let dx = (lo[0] - cx).max(cx - hi[0]).max(0.0);
let dy = (lo[1] - cy).max(cy - hi[1]).max(0.0);
dx.hypot(dy)
};
let dist_far = |cx: f64, cy: f64| {
let dx = (cx - lo[0]).abs().max((cx - hi[0]).abs());
let dy = (cy - lo[1]).abs().max((cy - hi[1]).abs());
dx.hypot(dy)
};
let near: Vec<Grain> = grains
.iter()
.copied()
.filter(|g| dist_box(g.center[0], g.center[1]) <= g.radius + CUTOFF)
.collect();
{
let mut v = state.view_mut(grid, block, phi_h);
if near.is_empty() {
for_each_interior(interior, |idx| v.set(idx, -1.0));
} else {
for_each_interior(interior, |idx| {
let [x, y] = grid.cell_center(block, idx);
let phi = near
.iter()
.map(|g| {
let r = (x - g.center[0]).hypot(y - g.center[1]);
-((r - g.radius) / f64::sqrt(2.0)).tanh()
})
.fold(-1.0f64, f64::max);
v.set(idx, phi);
});
}
}
{
let mut v = state.view_mut(grid, block, u_h);
for_each_interior(interior, |idx| v.set(idx, -u0));
}
if let Some(theta_h) = self.theta {
let threshold = grains
.iter()
.map(|g| dist_far(g.center[0], g.center[1]))
.fold(f64::MAX, f64::min);
let candidates: Vec<Grain> = grains
.iter()
.copied()
.filter(|g| dist_box(g.center[0], g.center[1]) <= threshold)
.collect();
let mut v = state.view_mut(grid, block, theta_h);
for_each_interior(interior, |idx| {
let [x, y] = grid.cell_center(block, idx);
let nearest = candidates
.iter()
.min_by(|a, b| {
let da = (y - a.center[1])
.mul_add(y - a.center[1], (x - a.center[0]).powi(2));
let db = (y - b.center[1])
.mul_add(y - b.center[1], (x - b.center[0]).powi(2));
da.total_cmp(&db)
})
.map_or(0.0, |g| g.orientation);
v.set(idx, nearest);
});
}
}
}
}
impl<D> Model<CartesianGrid<2>, D> for ModelC
where
D: Discretizes<CartesianGrid<2>, AnisotropicDivergence, Stencil = KarmaRappelFlux>
+ Discretizes<CartesianGrid<2>, Laplacian>,
{
type Scalar = f64;
fn register_fields(&mut self, builder: &mut StateBuilder<f64>) {
self.phi = Some(builder.register("phi", 1));
self.u = Some(builder.register("u", 1));
if self.orientations {
self.theta = Some(builder.register_static("theta0", 0));
}
}
fn fill_ghosts<S: StorageBackend<f64>>(
&self,
grid: &CartesianGrid<2>,
state: &mut State<f64, S>,
_t: f64,
) {
fill_ghosts_mirror(grid, state, self.phi());
fill_ghosts_mirror(grid, state, self.u());
}
fn rhs_block<S: StorageBackend<f64>>(
&self,
ctx: &RhsContext<'_, CartesianGrid<2>, D>,
state: &State<f64, S>,
out: &mut BlockStateMut<'_, f64, S>,
_scratch: &mut Scratch<f64, S>,
) {
let (grid, block) = (ctx.grid, ctx.block);
let (phi_h, u_h) = (self.phi(), self.u());
let phi = state.view(grid, block, phi_h);
let u = state.view(grid, block, u_h);
{
let flux = ctx
.disc
.build(grid, AnisotropicDivergence { eps4: self.eps4 });
let mut dphi = out.view_mut(grid, block, phi_h);
if let Some(theta_h) = self.theta {
let theta = state.view(grid, block, theta_h);
flux.apply_oriented(grid, block, phi, theta, &mut dphi);
for_each_interior(phi.interior(), |idx| {
let p = phi.get(idx);
let g_prime = (p * p).mul_add(p, -p);
let one_m_p2 = p.mul_add(-p, 1.0);
let p_prime = one_m_p2 * one_m_p2;
let (s0, c0) = theta.get(idx).sin_cos();
let a = flux.center_anisotropy_rotated(&phi, idx, c0, s0);
let val = (self.lambda * u.get(idx)).mul_add(-p_prime, dphi.get(idx) - g_prime)
/ (a * a);
dphi.set(idx, val);
});
} else {
flux.apply(grid, block, phi, &mut dphi);
for_each_interior(phi.interior(), |idx| {
let p = phi.get(idx);
let g_prime = (p * p).mul_add(p, -p);
let one_m_p2 = p.mul_add(-p, 1.0);
let p_prime = one_m_p2 * one_m_p2;
let a = self.center_anisotropy(&phi, idx);
let val = (self.lambda * u.get(idx)).mul_add(-p_prime, dphi.get(idx) - g_prime)
/ (a * a);
dphi.set(idx, val);
});
}
}
let lap = ctx.disc.build(grid, Laplacian);
let (mut du, dphi) = out.view_split(grid, block, u_h, phi_h);
lap.apply(grid, block, u, &mut du);
for_each_interior(u.interior(), |idx| {
du.set(
idx,
du.get(idx).mul_add(self.d_thermal, 0.5 * dphi.get(idx)),
);
});
}
fn has_noise(&self) -> bool {
self.noise_amplitude != 0.0
}
fn noise_block<S: StorageBackend<f64>>(
&self,
ctx: &RhsContext<'_, CartesianGrid<2>, D>,
_state: &State<f64, S>,
out: &mut BlockStateMut<'_, f64, S>,
_scratch: &mut Scratch<f64, S>,
) {
let mut amp = out.view_mut(ctx.grid, ctx.block, self.phi());
for_each_interior(amp.interior(), |idx| amp.set(idx, self.noise_amplitude));
}
fn stable_dt(&self, grid: &CartesianGrid<2>) -> Option<f64> {
let [hx, hy] = grid.spacing(BlockId(0));
Some(0.2 * hx.min(hy).powi(2) / self.d_thermal)
}
}