use rayon::prelude::*;
use volterra_core::ActiveNematicParams3D;
use volterra_fields::{QField3D, VelocityField3D};
use crate::mol_field_3d::{molecular_field_3d, molecular_field_3d_par, co_rotation_3d};
pub fn beris_edwards_rhs_3d(
q: &QField3D,
vel: Option<&VelocityField3D>,
p: &ActiveNematicParams3D,
t: f64,
) -> QField3D {
let h = molecular_field_3d(q, p, t);
let mut rhs = QField3D::zeros(q.nx, q.ny, q.nz, q.dx);
for k in 0..q.len() {
for c in 0..5 {
rhs.q[k][c] = p.gamma_r * h.q[k][c];
}
}
if let Some(v) = vel {
let s = co_rotation_3d(v, q, p.lambda);
let nx = q.nx;
let ny = q.ny;
let nz = q.nz;
let inv_2dx = 1.0 / (2.0 * q.dx);
for i in 0..nx {
for j in 0..ny {
for l in 0..nz {
let k = q.idx(i, j, l);
let ip = q.idx((i + 1) % nx, j, l);
let im = q.idx((i + nx - 1) % nx, j, l);
let jp = q.idx(i, (j + 1) % ny, l);
let jm = q.idx(i, (j + ny - 1) % ny, l);
let lp = q.idx(i, j, (l + 1) % nz);
let lm = q.idx(i, j, (l + nz - 1) % nz);
let [ux, uy, uz] = v.u[k];
for c in 0..5 {
let dqdx = (q.q[ip][c] - q.q[im][c]) * inv_2dx;
let dqdy = (q.q[jp][c] - q.q[jm][c]) * inv_2dx;
let dqdz = (q.q[lp][c] - q.q[lm][c]) * inv_2dx;
rhs.q[k][c] += -ux * dqdx - uy * dqdy - uz * dqdz + s.q[k][c];
}
}
}
}
}
rhs
}
pub struct EulerIntegrator3D;
impl EulerIntegrator3D {
pub fn step(&self, q: &QField3D, dt: f64, rhs: &QField3D) -> QField3D {
let mut out = q.clone();
for k in 0..q.len() {
for c in 0..5 {
out.q[k][c] += dt * rhs.q[k][c];
}
}
out
}
}
pub struct RK4Integrator3D;
impl RK4Integrator3D {
pub fn step<F>(&self, q: &QField3D, dt: f64, rhs_fn: F) -> QField3D
where
F: Fn(&QField3D) -> QField3D,
{
let k1 = rhs_fn(q);
let q2 = add_scaled(q, &k1, dt / 2.0);
let k2 = rhs_fn(&q2);
let q3 = add_scaled(q, &k2, dt / 2.0);
let k3 = rhs_fn(&q3);
let q4 = add_scaled(q, &k3, dt);
let k4 = rhs_fn(&q4);
let mut out = q.clone();
for k in 0..q.len() {
for c in 0..5 {
out.q[k][c] += (dt / 6.0)
* (k1.q[k][c] + 2.0 * k2.q[k][c] + 2.0 * k3.q[k][c] + k4.q[k][c]);
}
}
out
}
}
fn add_scaled(q: &QField3D, dq: &QField3D, scale: f64) -> QField3D {
let mut out = q.clone();
for k in 0..q.len() {
for c in 0..5 {
out.q[k][c] += scale * dq.q[k][c];
}
}
out
}
pub fn beris_edwards_rhs_3d_par_dry(
q: &QField3D,
p: &ActiveNematicParams3D,
t: f64,
) -> QField3D {
let h = molecular_field_3d_par(q, p, t);
let gamma_r = p.gamma_r;
let n = q.len();
let out_data: Vec<[f64; 5]> = (0..n)
.into_par_iter()
.map(|k| {
let mut r = [0.0_f64; 5];
for c in 0..5 {
r[c] = gamma_r * h.q[k][c];
}
r
})
.collect();
QField3D {
q: out_data,
nx: q.nx,
ny: q.ny,
nz: q.nz,
dx: q.dx,
}
}
pub fn euler_step_par(q: &QField3D, dt: f64, rhs: &QField3D) -> QField3D {
let n = q.len();
let out_data: Vec<[f64; 5]> = (0..n)
.into_par_iter()
.map(|k| {
let mut r = [0.0_f64; 5];
for c in 0..5 {
r[c] = q.q[k][c] + dt * rhs.q[k][c];
}
r
})
.collect();
QField3D {
q: out_data,
nx: q.nx,
ny: q.ny,
nz: q.nz,
dx: q.dx,
}
}
#[cfg(test)]
mod tests {
use super::*;
use volterra_core::ActiveNematicParams3D;
use volterra_fields::QField3D;
#[test]
fn test_beris_rhs_dry_shape() {
let p = ActiveNematicParams3D::default_test();
let q = QField3D::random_perturbation(4, 4, 4, 1.0, 0.01, 42);
let dq = beris_edwards_rhs_3d(&q, None, &p, 0.0);
assert_eq!(dq.len(), q.len());
}
#[test]
fn test_beris_rhs_zero_for_ordered_fixed_point() {
let mut p = ActiveNematicParams3D::default_test();
p.zeta_eff = 0.0;
p.chi_a = 0.0;
let a_eff = p.a_eff(); let s_eq = (-3.0 * a_eff / (4.0 * p.c_landau)).sqrt();
let q = QField3D::uniform(4, 4, 4, 1.0, [-s_eq / 3.0, 0.0, 0.0, -s_eq / 3.0, 0.0]);
let dq = beris_edwards_rhs_3d(&q, None, &p, 0.0);
for k in 0..dq.len() {
for c in 0..5 {
assert!(
dq.q[k][c].abs() < 1e-6,
"dQ/dt should be near zero at equilibrium, got {} at vertex {} component {}",
dq.q[k][c],
k,
c
);
}
}
}
#[test]
fn test_parallel_molecular_field_matches_sequential() {
let p = ActiveNematicParams3D::default_test();
let q = QField3D::random_perturbation(8, 8, 8, 1.0, 0.1, 42);
let h_seq = molecular_field_3d(&q, &p, 0.5);
let h_par = molecular_field_3d_par(&q, &p, 0.5);
let max_diff: f64 = h_seq.q.iter().zip(&h_par.q)
.flat_map(|(a, b)| a.iter().zip(b.iter()).map(|(x, y)| (x - y).abs()))
.fold(0.0_f64, f64::max);
assert!(
max_diff < 1e-10,
"parallel should match sequential: max_diff = {max_diff}"
);
}
#[test]
fn test_parallel_rhs_matches_sequential() {
let p = ActiveNematicParams3D::default_test();
let q = QField3D::random_perturbation(8, 8, 8, 1.0, 0.1, 42);
let rhs_seq = beris_edwards_rhs_3d(&q, None, &p, 0.3);
let rhs_par = beris_edwards_rhs_3d_par_dry(&q, &p, 0.3);
let max_diff: f64 = rhs_seq.q.iter().zip(&rhs_par.q)
.flat_map(|(a, b)| a.iter().zip(b.iter()).map(|(x, y)| (x - y).abs()))
.fold(0.0_f64, f64::max);
assert!(
max_diff < 1e-10,
"parallel RHS should match sequential: max_diff = {max_diff}"
);
}
#[test]
fn test_parallel_euler_step() {
let p = ActiveNematicParams3D::default_test();
let q = QField3D::random_perturbation(8, 8, 8, 1.0, 0.1, 42);
let rhs = beris_edwards_rhs_3d_par_dry(&q, &p, 0.0);
let dt = 0.001;
let q_seq = EulerIntegrator3D.step(&q, dt, &rhs);
let q_par = euler_step_par(&q, dt, &rhs);
let max_diff: f64 = q_seq.q.iter().zip(&q_par.q)
.flat_map(|(a, b)| a.iter().zip(b.iter()).map(|(x, y)| (x - y).abs()))
.fold(0.0_f64, f64::max);
assert!(
max_diff < 1e-14,
"parallel Euler should match sequential: max_diff = {max_diff}"
);
}
#[test]
fn test_fused_euler_matches_twostep() {
use crate::mol_field_3d::euler_step_fused_par;
let mut p = ActiveNematicParams3D::default_test();
p.dt = 0.001;
let q_init = QField3D::random_perturbation(8, 8, 8, 1.0, 0.1, 42);
let rhs = beris_edwards_rhs_3d_par_dry(&q_init, &p, 0.5);
let q_twostep = euler_step_par(&q_init, p.dt, &rhs);
let mut q_fused = q_init.clone();
euler_step_fused_par(&mut q_fused, &p, 0.5);
let max_diff: f64 = q_twostep.q.iter().zip(&q_fused.q)
.flat_map(|(a, b)| a.iter().zip(b.iter()).map(|(x, y)| (x - y).abs()))
.fold(0.0_f64, f64::max);
assert!(
max_diff < 1e-10,
"fused should match two-step: max_diff = {max_diff}"
);
}
}