use numr::runtime::cpu::{CpuClient, CpuRuntime};
use numr::tensor::Tensor;
use crate::pde::error::PdeResult;
use crate::pde::impl_generic::{heat_2d_impl, heat_3d_impl, poisson_impl, wave_impl};
use crate::pde::traits::FiniteDifferenceAlgorithms;
use crate::pde::types::{
BoundarySpec, FdmOptions, FdmResult, Grid2D, Grid3D, TimeDependentOptions, TimeResult,
};
impl FiniteDifferenceAlgorithms<CpuRuntime> for CpuClient {
fn fdm_poisson(
&self,
f: &Tensor<CpuRuntime>,
grid: &Grid2D,
boundary: &[BoundarySpec<CpuRuntime>],
options: &FdmOptions,
) -> PdeResult<FdmResult<CpuRuntime>> {
poisson_impl(self, f, grid, boundary, options)
}
fn fdm_heat_2d(
&self,
u0: &Tensor<CpuRuntime>,
alpha: f64,
source: Option<&Tensor<CpuRuntime>>,
grid: &Grid2D,
boundary: &[BoundarySpec<CpuRuntime>],
time_opts: &TimeDependentOptions,
options: &FdmOptions,
) -> PdeResult<TimeResult<CpuRuntime>> {
heat_2d_impl(self, u0, alpha, source, grid, boundary, time_opts, options)
}
fn fdm_heat_3d(
&self,
u0: &Tensor<CpuRuntime>,
alpha: f64,
source: Option<&Tensor<CpuRuntime>>,
grid: &Grid3D,
boundary: &[BoundarySpec<CpuRuntime>],
time_opts: &TimeDependentOptions,
options: &FdmOptions,
) -> PdeResult<TimeResult<CpuRuntime>> {
heat_3d_impl(self, u0, alpha, source, grid, boundary, time_opts, options)
}
fn fdm_wave(
&self,
u0: &Tensor<CpuRuntime>,
v0: &Tensor<CpuRuntime>,
c: f64,
source: Option<&Tensor<CpuRuntime>>,
grid: &Grid2D,
boundary: &[BoundarySpec<CpuRuntime>],
time_opts: &TimeDependentOptions,
options: &FdmOptions,
) -> PdeResult<TimeResult<CpuRuntime>> {
wave_impl(self, u0, v0, c, source, grid, boundary, time_opts, options)
}
}
#[cfg(test)]
mod tests {
use super::*;
use numr::runtime::cpu::CpuDevice;
fn setup() -> (CpuClient, CpuDevice) {
let device = CpuDevice::new();
let client = CpuClient::new(device.clone());
(client, device)
}
#[test]
fn test_poisson_known_solution() {
let (client, device) = setup();
let nx = 21;
let ny = 21;
let dx = 1.0 / (nx - 1) as f64;
let dy = 1.0 / (ny - 1) as f64;
let grid = Grid2D {
nx,
ny,
dx,
dy,
x_range: [0.0, 1.0],
y_range: [0.0, 1.0],
};
let pi = std::f64::consts::PI;
let mut f_data = vec![0.0; nx * ny];
for i in 0..nx {
for j in 0..ny {
let x = i as f64 * dx;
let y = j as f64 * dy;
f_data[i * ny + j] = 2.0 * pi * pi * (pi * x).sin() * (pi * y).sin();
}
}
let f_tensor = Tensor::<CpuRuntime>::from_slice(&f_data, &[nx, ny], &device);
let result = client
.fdm_poisson(&f_tensor, &grid, &[], &FdmOptions::default())
.expect("Poisson solve failed");
let sol: Vec<f64> = result.solution.to_vec();
let center_i = nx / 2;
let center_j = ny / 2;
let numerical = sol[center_i * ny + center_j];
let exact = (pi * 0.5).sin() * (pi * 0.5).sin();
assert!(
(numerical - exact).abs() < 0.02,
"Poisson center: numerical={}, exact={}, error={}",
numerical,
exact,
(numerical - exact).abs()
);
}
#[test]
fn test_heat_2d_diffusion() {
let (client, device) = setup();
let nx = 11;
let ny = 11;
let dx = 1.0 / (nx - 1) as f64;
let dy = 1.0 / (ny - 1) as f64;
let grid = Grid2D {
nx,
ny,
dx,
dy,
x_range: [0.0, 1.0],
y_range: [0.0, 1.0],
};
let mut u0_data = vec![0.0; nx * ny];
u0_data[(nx / 2) * ny + ny / 2] = 1.0;
let u0 = Tensor::<CpuRuntime>::from_slice(&u0_data, &[nx, ny], &device);
let time_opts = TimeDependentOptions {
t_span: [0.0, 0.01],
dt: None,
save_every: 0,
};
let result = client
.fdm_heat_2d(
&u0,
1.0,
None,
&grid,
&[],
&time_opts,
&FdmOptions::default(),
)
.expect("Heat 2D solve failed");
assert!(!result.solutions.is_empty());
let final_sol: Vec<f64> = result.solutions.last().unwrap().to_vec();
let max_val = final_sol.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
assert!(max_val < 1.0, "Heat should have diffused: max={}", max_val);
}
#[test]
fn test_wave_energy_conservation() {
let (client, device) = setup();
let nx = 11;
let ny = 11;
let dx = 1.0 / (nx - 1) as f64;
let dy = 1.0 / (ny - 1) as f64;
let grid = Grid2D {
nx,
ny,
dx,
dy,
x_range: [0.0, 1.0],
y_range: [0.0, 1.0],
};
let pi = std::f64::consts::PI;
let mut u0_data = vec![0.0; nx * ny];
for i in 1..nx - 1 {
for j in 1..ny - 1 {
let x = i as f64 * dx;
let y = j as f64 * dy;
u0_data[i * ny + j] = (pi * x).sin() * (pi * y).sin();
}
}
let u0 = Tensor::<CpuRuntime>::from_slice(&u0_data, &[nx, ny], &device);
let v0 = Tensor::<CpuRuntime>::from_slice(&vec![0.0; nx * ny], &[nx, ny], &device);
let time_opts = TimeDependentOptions {
t_span: [0.0, 0.1],
dt: None,
save_every: 0,
};
let result = client
.fdm_wave(
&u0,
&v0,
1.0,
None,
&grid,
&[],
&time_opts,
&FdmOptions::default(),
)
.expect("Wave solve failed");
assert!(!result.solutions.is_empty());
}
#[test]
fn test_poisson_neumann_zero_flux_zero_source() {
let (client, device) = setup();
let nx = 7;
let ny = 7;
let dx = 1.0 / (nx - 1) as f64;
let dy = 1.0 / (ny - 1) as f64;
let grid = Grid2D {
nx,
ny,
dx,
dy,
x_range: [0.0, 1.0],
y_range: [0.0, 1.0],
};
let f_data = vec![0.0f64; nx * ny];
let f_tensor = Tensor::<CpuRuntime>::from_slice(&f_data, &[nx, ny], &device);
use crate::pde::types::{BoundaryCondition, BoundarySide, BoundarySpec};
let bc_val = Tensor::<CpuRuntime>::from_slice(&[0.0f64], &[1], &device);
let boundary = vec![BoundarySpec {
side: BoundarySide::All,
condition: BoundaryCondition::Neumann(bc_val),
}];
let opts = FdmOptions {
solver: crate::pde::types::SparseSolver::Gmres,
max_iter: 5000,
tolerance: 1e-8,
..Default::default()
};
let result = client
.fdm_poisson(&f_tensor, &grid, &boundary, &opts)
.expect("Neumann zero-flux Poisson solve failed");
let sol: Vec<f64> = result.solution.to_vec();
let max_err = sol.iter().cloned().map(f64::abs).fold(0.0f64, f64::max);
assert!(
max_err < 1e-6,
"Neumann zero-flux should give u≈0, max error = {:.2e}",
max_err
);
}
#[test]
fn test_poisson_neumann_linear_solution() {
let (client, device) = setup();
let nx = 9;
let ny = 9;
let dx = 1.0 / (nx - 1) as f64;
let dy = 1.0 / (ny - 1) as f64;
let grid = Grid2D {
nx,
ny,
dx,
dy,
x_range: [0.0, 1.0],
y_range: [0.0, 1.0],
};
let f_data = vec![0.0f64; nx * ny];
let f_tensor = Tensor::<CpuRuntime>::from_slice(&f_data, &[nx, ny], &device);
use crate::pde::types::{BoundaryCondition, BoundarySide, BoundarySpec};
let bc_left_val = Tensor::<CpuRuntime>::from_slice(&[-1.0f64], &[1], &device);
let bc_right_val = Tensor::<CpuRuntime>::from_slice(&[1.0f64], &[1], &device);
let bc_tb_val = Tensor::<CpuRuntime>::from_slice(&[0.0f64], &[1], &device);
let bc_tb_val2 = Tensor::<CpuRuntime>::from_slice(&[0.0f64], &[1], &device);
let boundary = vec![
BoundarySpec {
side: BoundarySide::Left,
condition: BoundaryCondition::Neumann(bc_left_val),
},
BoundarySpec {
side: BoundarySide::Right,
condition: BoundaryCondition::Neumann(bc_right_val),
},
BoundarySpec {
side: BoundarySide::Bottom,
condition: BoundaryCondition::Neumann(bc_tb_val),
},
BoundarySpec {
side: BoundarySide::Top,
condition: BoundaryCondition::Neumann(bc_tb_val2),
},
];
let opts = FdmOptions {
solver: crate::pde::types::SparseSolver::Gmres,
max_iter: 5000,
tolerance: 1e-8,
..Default::default()
};
let result = client
.fdm_poisson(&f_tensor, &grid, &boundary, &opts)
.expect("Neumann linear Poisson solve failed");
let sol: Vec<f64> = result.solution.to_vec();
let mut max_err = 0.0f64;
for i in 0..nx {
for j in 0..ny {
let exact = i as f64 * dx;
let err = (sol[i * ny + j] - exact).abs();
if err > max_err {
max_err = err;
}
}
}
assert!(
max_err < 0.02, "Neumann linear solution error too large: max_err = {:.4e}",
max_err
);
}
#[test]
fn test_poisson_mixed_dirichlet_neumann_linear() {
let (client, device) = setup();
let nx = 9;
let ny = 9;
let dx = 1.0 / (nx - 1) as f64;
let dy = 1.0 / (ny - 1) as f64;
let grid = Grid2D {
nx,
ny,
dx,
dy,
x_range: [0.0, 1.0],
y_range: [0.0, 1.0],
};
let f_tensor = Tensor::<CpuRuntime>::from_slice(&vec![0.0f64; nx * ny], &[nx, ny], &device);
use crate::pde::types::{BoundaryCondition, BoundarySide, BoundarySpec};
let left_vals = Tensor::<CpuRuntime>::from_slice(&vec![0.0f64; ny], &[ny], &device);
let right_vals = Tensor::<CpuRuntime>::from_slice(&vec![1.0f64; ny], &[ny], &device);
let zero_flux_b = Tensor::<CpuRuntime>::from_slice(&[0.0f64], &[1], &device);
let zero_flux_t = Tensor::<CpuRuntime>::from_slice(&[0.0f64], &[1], &device);
let boundary = vec![
BoundarySpec {
side: BoundarySide::Left,
condition: BoundaryCondition::Dirichlet(left_vals),
},
BoundarySpec {
side: BoundarySide::Right,
condition: BoundaryCondition::Dirichlet(right_vals),
},
BoundarySpec {
side: BoundarySide::Bottom,
condition: BoundaryCondition::Neumann(zero_flux_b),
},
BoundarySpec {
side: BoundarySide::Top,
condition: BoundaryCondition::Neumann(zero_flux_t),
},
];
let opts = FdmOptions {
solver: crate::pde::types::SparseSolver::Gmres,
max_iter: 5000,
tolerance: 1e-10,
..Default::default()
};
let result = client
.fdm_poisson(&f_tensor, &grid, &boundary, &opts)
.expect("mixed Dirichlet/Neumann Poisson solve failed");
let sol: Vec<f64> = result.solution.to_vec();
let mut max_err = 0.0f64;
for i in 0..nx {
for j in 0..ny {
let exact = i as f64 * dx;
let err = (sol[i * ny + j] - exact).abs();
if err > max_err {
max_err = err;
}
}
}
assert!(
max_err < 1e-6,
"Mixed BC linear solution error too large: max_err = {:.4e}",
max_err
);
}
#[test]
fn test_poisson_mixed_periodic_x_dirichlet_y() {
let (client, device) = setup();
let nx = 8;
let ny = 9;
let dx = 1.0 / nx as f64; let dy = 1.0 / (ny - 1) as f64;
let grid = Grid2D {
nx,
ny,
dx,
dy,
x_range: [0.0, 1.0],
y_range: [0.0, 1.0],
};
let f_tensor = Tensor::<CpuRuntime>::from_slice(&vec![0.0f64; nx * ny], &[nx, ny], &device);
use crate::pde::types::{BoundaryCondition, BoundarySide, BoundarySpec};
let bottom_vals = Tensor::<CpuRuntime>::from_slice(&vec![0.0f64; nx], &[nx], &device);
let top_vals = Tensor::<CpuRuntime>::from_slice(&vec![1.0f64; nx], &[nx], &device);
let boundary = vec![
BoundarySpec {
side: BoundarySide::Left,
condition: BoundaryCondition::Periodic,
},
BoundarySpec {
side: BoundarySide::Right,
condition: BoundaryCondition::Periodic,
},
BoundarySpec {
side: BoundarySide::Bottom,
condition: BoundaryCondition::Dirichlet(bottom_vals),
},
BoundarySpec {
side: BoundarySide::Top,
condition: BoundaryCondition::Dirichlet(top_vals),
},
];
let opts = FdmOptions {
solver: crate::pde::types::SparseSolver::Gmres,
max_iter: 5000,
tolerance: 1e-10,
..Default::default()
};
let result = client
.fdm_poisson(&f_tensor, &grid, &boundary, &opts)
.expect("mixed periodic/Dirichlet Poisson solve failed");
let sol: Vec<f64> = result.solution.to_vec();
let mut max_err = 0.0f64;
for i in 0..nx {
for j in 0..ny {
let exact = j as f64 * dy;
let err = (sol[i * ny + j] - exact).abs();
if err > max_err {
max_err = err;
}
}
}
assert!(
max_err < 1e-6,
"Mixed periodic/Dirichlet solution error too large: max_err = {:.4e}",
max_err
);
}
#[test]
fn test_poisson_periodic_zero_rhs() {
let (client, device) = setup();
let nx = 6;
let ny = 6;
let dx = 1.0 / nx as f64; let dy = 1.0 / ny as f64;
let grid = Grid2D {
nx,
ny,
dx,
dy,
x_range: [0.0, 1.0],
y_range: [0.0, 1.0],
};
let f_data = vec![0.0f64; nx * ny];
let f_tensor = Tensor::<CpuRuntime>::from_slice(&f_data, &[nx, ny], &device);
use crate::pde::types::{BoundaryCondition, BoundarySide, BoundarySpec};
let boundary = vec![BoundarySpec {
side: BoundarySide::All,
condition: BoundaryCondition::Periodic,
}];
let opts = FdmOptions {
solver: crate::pde::types::SparseSolver::Gmres,
max_iter: 5000,
tolerance: 1e-8,
..Default::default()
};
let result = client
.fdm_poisson(&f_tensor, &grid, &boundary, &opts)
.expect("Periodic zero-RHS Poisson solve failed");
let sol: Vec<f64> = result.solution.to_vec();
let max_err = sol.iter().cloned().map(f64::abs).fold(0.0f64, f64::max);
assert!(
max_err < 1e-6,
"Periodic zero-RHS should give u≈0, max error = {:.2e}",
max_err
);
}
#[test]
fn test_poisson_periodic_sinusoidal_source() {
let (client, device) = setup();
let nx = 16;
let ny = 16;
let dx = 1.0 / nx as f64;
let dy = 1.0 / ny as f64;
let grid = Grid2D {
nx,
ny,
dx,
dy,
x_range: [0.0, 1.0],
y_range: [0.0, 1.0],
};
let pi = std::f64::consts::PI;
let mut f_data = vec![0.0f64; nx * ny];
for i in 0..nx {
for j in 0..ny {
let x = i as f64 * dx;
let y = j as f64 * dy;
f_data[i * ny + j] =
2.0 * (2.0 * pi).powi(2) * (2.0 * pi * x).sin() * (2.0 * pi * y).sin();
}
}
let f_tensor = Tensor::<CpuRuntime>::from_slice(&f_data, &[nx, ny], &device);
use crate::pde::types::{BoundaryCondition, BoundarySide, BoundarySpec};
let boundary = vec![BoundarySpec {
side: BoundarySide::All,
condition: BoundaryCondition::Periodic,
}];
let opts = FdmOptions {
solver: crate::pde::types::SparseSolver::Gmres,
max_iter: 10000,
tolerance: 1e-9,
..Default::default()
};
let result = client
.fdm_poisson(&f_tensor, &grid, &boundary, &opts)
.expect("Periodic sinusoidal Poisson solve failed");
let sol: Vec<f64> = result.solution.to_vec();
let mut max_err = 0.0f64;
for i in 1..nx - 1 {
for j in 1..ny - 1 {
let x = i as f64 * dx;
let y = j as f64 * dy;
let exact = (2.0 * pi * x).sin() * (2.0 * pi * y).sin();
let err = (sol[i * ny + j] - exact).abs();
if err > max_err {
max_err = err;
}
}
}
assert!(
max_err < 0.05,
"Periodic sinusoidal Poisson max error = {:.4e}",
max_err
);
}
#[test]
fn test_neumann_and_periodic_do_not_error() {
let (client, device) = setup();
let nx = 5;
let ny = 5;
let dx = 1.0 / (nx - 1) as f64;
let dy = 1.0 / (ny - 1) as f64;
let grid = Grid2D {
nx,
ny,
dx,
dy,
x_range: [0.0, 1.0],
y_range: [0.0, 1.0],
};
let f_data = vec![0.0f64; nx * ny];
let f_n = Tensor::<CpuRuntime>::from_slice(&f_data, &[nx, ny], &device);
let f_p = Tensor::<CpuRuntime>::from_slice(&f_data, &[nx, ny], &device);
use crate::pde::types::{BoundaryCondition, BoundarySide, BoundarySpec};
let bc_n_val = Tensor::<CpuRuntime>::from_slice(&[0.0f64], &[1], &device);
let bcs_neumann = vec![BoundarySpec {
side: BoundarySide::All,
condition: BoundaryCondition::Neumann(bc_n_val),
}];
let bcs_periodic = vec![BoundarySpec {
side: BoundarySide::All,
condition: BoundaryCondition::Periodic,
}];
let opts = FdmOptions {
solver: crate::pde::types::SparseSolver::Gmres,
max_iter: 2000,
tolerance: 1e-8,
..Default::default()
};
let r_n = client.fdm_poisson(&f_n, &grid, &bcs_neumann, &opts);
assert!(r_n.is_ok(), "Neumann BC returned error: {:?}", r_n.err());
let r_p = client.fdm_poisson(&f_p, &grid, &bcs_periodic, &opts);
assert!(r_p.is_ok(), "Periodic BC returned error: {:?}", r_p.err());
}
}