use rayon::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ThermalBc {
Dirichlet(f64),
Neumann(f64),
}
#[derive(Debug, Clone)]
pub struct HeatSource {
pub cell_index: usize,
pub power: f64,
}
impl HeatSource {
pub fn new(cell_index: usize, power: f64) -> Self {
Self { cell_index, power }
}
}
#[derive(Debug, Clone)]
pub struct GpuThermalSolver {
pub nx: usize,
pub ny: usize,
pub nz: usize,
pub diffusivity: f64,
pub dx: f64,
pub dy: f64,
pub dz: f64,
pub temperature: Vec<f64>,
}
impl GpuThermalSolver {
#[allow(clippy::too_many_arguments)]
pub fn new(
nx: usize,
ny: usize,
nz: usize,
diffusivity: f64,
dx: f64,
dy: f64,
dz: f64,
t0: f64,
) -> Self {
let n = nx * ny * nz;
Self {
nx,
ny,
nz,
diffusivity,
dx,
dy,
dz,
temperature: vec![t0; n],
}
}
#[inline]
pub fn idx(&self, ix: usize, iy: usize, iz: usize) -> usize {
iz * self.ny * self.nx + iy * self.nx + ix
}
pub fn n_cells(&self) -> usize {
self.nx * self.ny * self.nz
}
}
pub fn gpu_heat_diffusion(solver: &mut GpuThermalSolver, dt: f64) {
let nx = solver.nx;
let ny = solver.ny;
let nz = solver.nz;
let alpha = solver.diffusivity;
let dx2 = solver.dx * solver.dx;
let dy2 = solver.dy * solver.dy;
let dz2 = solver.dz * solver.dz;
let old = solver.temperature.clone();
let new_temp: Vec<f64> = (0..nz * ny * nx)
.into_par_iter()
.map(|idx| {
let iz = idx / (ny * nx);
let rem = idx % (ny * nx);
let iy = rem / nx;
let ix = rem % nx;
if ix == 0 || ix == nx - 1 || iy == 0 || iy == ny - 1 || iz == 0 || iz == nz - 1 {
return old[idx];
}
let laplacian_x = (old[idx - 1] - 2.0 * old[idx] + old[idx + 1]) / dx2;
let laplacian_y = (old[idx - nx] - 2.0 * old[idx] + old[idx + nx]) / dy2;
let laplacian_z = (old[idx - ny * nx] - 2.0 * old[idx] + old[idx + ny * nx]) / dz2;
old[idx] + dt * alpha * (laplacian_x + laplacian_y + laplacian_z)
})
.collect();
solver.temperature = new_temp;
}
#[allow(clippy::too_many_arguments)]
pub fn thermal_boundary_apply(
solver: &mut GpuThermalSolver,
bc_xmin: ThermalBc,
bc_xmax: ThermalBc,
bc_ymin: ThermalBc,
bc_ymax: ThermalBc,
bc_zmin: ThermalBc,
bc_zmax: ThermalBc,
) {
let nx = solver.nx;
let ny = solver.ny;
let nz = solver.nz;
for iz in 0..nz {
for iy in 0..ny {
let idx_min = solver.idx(0, iy, iz);
let idx_max = solver.idx(nx - 1, iy, iz);
apply_bc_to_cell(&mut solver.temperature, idx_min, bc_xmin, solver.dx);
apply_bc_to_cell(&mut solver.temperature, idx_max, bc_xmax, solver.dx);
}
}
for iz in 0..nz {
for ix in 0..nx {
let idx_min = solver.idx(ix, 0, iz);
let idx_max = solver.idx(ix, ny - 1, iz);
apply_bc_to_cell(&mut solver.temperature, idx_min, bc_ymin, solver.dy);
apply_bc_to_cell(&mut solver.temperature, idx_max, bc_ymax, solver.dy);
}
}
for iy in 0..ny {
for ix in 0..nx {
let idx_min = solver.idx(ix, iy, 0);
let idx_max = solver.idx(ix, iy, nz - 1);
apply_bc_to_cell(&mut solver.temperature, idx_min, bc_zmin, solver.dz);
apply_bc_to_cell(&mut solver.temperature, idx_max, bc_zmax, solver.dz);
}
}
}
#[allow(dead_code)]
fn apply_bc_to_cell(temperature: &mut [f64], idx: usize, bc: ThermalBc, _h: f64) {
match bc {
ThermalBc::Dirichlet(val) => {
temperature[idx] = val;
}
ThermalBc::Neumann(_flux) => {
}
}
}
pub fn gpu_heat_source(solver: &mut GpuThermalSolver, sources: &[HeatSource], dt: f64) {
for src in sources {
if src.cell_index < solver.temperature.len() {
solver.temperature[src.cell_index] += src.power * dt;
}
}
}
pub fn temperature_gradient(solver: &GpuThermalSolver) -> Vec<[f64; 3]> {
let nx = solver.nx;
let ny = solver.ny;
let nz = solver.nz;
let t = &solver.temperature;
(0..nz * ny * nx)
.into_par_iter()
.map(|idx| {
let iz = idx / (ny * nx);
let rem = idx % (ny * nx);
let iy = rem / nx;
let ix = rem % nx;
if ix == 0 || ix == nx - 1 || iy == 0 || iy == ny - 1 || iz == 0 || iz == nz - 1 {
return [0.0; 3];
}
let gx = (t[idx + 1] - t[idx - 1]) / (2.0 * solver.dx);
let gy = (t[idx + nx] - t[idx - nx]) / (2.0 * solver.dy);
let gz = (t[idx + ny * nx] - t[idx - ny * nx]) / (2.0 * solver.dz);
[gx, gy, gz]
})
.collect()
}
pub fn thermal_equilibration(solver: &GpuThermalSolver, old: &[f64], tol: f64) -> bool {
if old.len() != solver.temperature.len() {
return false;
}
solver
.temperature
.par_iter()
.zip(old.par_iter())
.map(|(&new, &prev)| (new - prev).abs())
.reduce(|| 0.0_f64, f64::max)
< tol
}
#[cfg(test)]
mod tests {
use super::*;
fn make_solver(nx: usize, ny: usize, nz: usize, t0: f64) -> GpuThermalSolver {
GpuThermalSolver::new(nx, ny, nz, 1e-4, 0.1, 0.1, 0.1, t0)
}
#[test]
fn test_solver_new_size() {
let s = make_solver(4, 4, 4, 300.0);
assert_eq!(s.n_cells(), 64);
assert_eq!(s.temperature.len(), 64);
}
#[test]
fn test_solver_uniform_init() {
let s = make_solver(3, 3, 3, 273.15);
assert!(s.temperature.iter().all(|&t| (t - 273.15).abs() < 1e-12));
}
#[test]
fn test_solver_idx_origin() {
let s = make_solver(5, 5, 5, 0.0);
assert_eq!(s.idx(0, 0, 0), 0);
}
#[test]
fn test_solver_idx_last() {
let s = make_solver(5, 5, 5, 0.0);
assert_eq!(s.idx(4, 4, 4), 124);
}
#[test]
fn test_solver_idx_slice() {
let s = make_solver(4, 4, 4, 0.0);
assert_eq!(s.idx(2, 1, 0), 4 + 2);
}
#[test]
fn test_diffusion_boundary_unchanged() {
let mut s = make_solver(4, 4, 4, 100.0);
let idx = s.idx(2, 2, 2);
s.temperature[idx] = 500.0;
let boundary_before = s.temperature[s.idx(0, 0, 0)];
gpu_heat_diffusion(&mut s, 0.001);
assert!((s.temperature[s.idx(0, 0, 0)] - boundary_before).abs() < 1e-12);
}
#[test]
fn test_diffusion_uniform_field_unchanged() {
let mut s = make_solver(5, 5, 5, 300.0);
let before: Vec<f64> = s.temperature.clone();
gpu_heat_diffusion(&mut s, 0.01);
for (a, b) in s.temperature.iter().zip(before.iter()) {
assert!(
(a - b).abs() < 1e-12,
"uniform field changed under diffusion"
);
}
}
#[test]
fn test_diffusion_hot_spot_cools() {
let mut s = make_solver(5, 5, 5, 0.0);
let idx = s.idx(2, 2, 2);
s.temperature[idx] = 1000.0;
let before = s.temperature[idx];
gpu_heat_diffusion(&mut s, 0.001);
assert!(s.temperature[idx] < before);
}
#[test]
fn test_diffusion_cold_spot_warms() {
let mut s = make_solver(5, 5, 5, 300.0);
let idx = s.idx(2, 2, 2);
s.temperature[idx] = 0.0;
gpu_heat_diffusion(&mut s, 0.001);
assert!(s.temperature[idx] > 0.0);
}
#[test]
fn test_diffusion_energy_approximately_conserved_interior() {
let mut s = make_solver(5, 5, 5, 0.0);
let idx = s.idx(2, 2, 2);
s.temperature[idx] = 1000.0;
let sum_before: f64 = s.temperature.iter().sum();
gpu_heat_diffusion(&mut s, 0.001);
let sum_after: f64 = s.temperature.iter().sum();
assert!((sum_before - sum_after).abs() < 1e-6 * sum_before.abs() + 1e-9);
}
#[test]
fn test_dirichlet_xmin_applied() {
let mut s = make_solver(6, 6, 6, 0.0);
thermal_boundary_apply(
&mut s,
ThermalBc::Dirichlet(500.0),
ThermalBc::Dirichlet(500.0),
ThermalBc::Dirichlet(500.0),
ThermalBc::Dirichlet(500.0),
ThermalBc::Dirichlet(500.0),
ThermalBc::Dirichlet(500.0),
);
for iz in 0..6 {
for iy in 0..6 {
let idx = s.idx(0, iy, iz);
assert!(
(s.temperature[idx] - 500.0).abs() < 1e-12,
"x=0 face cell ({iy},{iz}) expected 500, got {}",
s.temperature[idx]
);
}
}
}
#[test]
fn test_dirichlet_xmax_applied() {
let mut s = make_solver(6, 6, 6, 0.0);
thermal_boundary_apply(
&mut s,
ThermalBc::Dirichlet(200.0),
ThermalBc::Dirichlet(200.0),
ThermalBc::Dirichlet(200.0),
ThermalBc::Dirichlet(200.0),
ThermalBc::Dirichlet(200.0),
ThermalBc::Dirichlet(200.0),
);
for iz in 0..6 {
for iy in 0..6 {
let idx = s.idx(5, iy, iz);
assert!(
(s.temperature[idx] - 200.0).abs() < 1e-12,
"x=5 face cell ({iy},{iz}) expected 200, got {}",
s.temperature[idx]
);
}
}
}
#[test]
fn test_neumann_bc_leaves_interior_unchanged_for_zero_flux() {
let mut s = make_solver(4, 4, 4, 300.0);
let interior_before = s.temperature[s.idx(2, 2, 2)];
thermal_boundary_apply(
&mut s,
ThermalBc::Neumann(0.0),
ThermalBc::Neumann(0.0),
ThermalBc::Neumann(0.0),
ThermalBc::Neumann(0.0),
ThermalBc::Neumann(0.0),
ThermalBc::Neumann(0.0),
);
let interior_after = s.temperature[s.idx(2, 2, 2)];
assert!((interior_before - interior_after).abs() < 1e-12);
}
#[test]
fn test_heat_source_single_cell() {
let mut s = make_solver(4, 4, 4, 0.0);
let idx = s.idx(2, 2, 2);
let sources = vec![HeatSource::new(idx, 1000.0)];
gpu_heat_source(&mut s, &sources, 0.1);
assert!((s.temperature[idx] - 100.0).abs() < 1e-10);
}
#[test]
fn test_heat_source_multiple_cells() {
let mut s = make_solver(4, 4, 4, 0.0);
let idx1 = s.idx(1, 1, 1);
let idx2 = s.idx(2, 2, 2);
let sources = vec![HeatSource::new(idx1, 500.0), HeatSource::new(idx2, 200.0)];
gpu_heat_source(&mut s, &sources, 1.0);
assert!((s.temperature[idx1] - 500.0).abs() < 1e-10);
assert!((s.temperature[idx2] - 200.0).abs() < 1e-10);
}
#[test]
fn test_heat_source_out_of_bounds_ignored() {
let mut s = make_solver(4, 4, 4, 0.0);
let sources = vec![HeatSource::new(9999, 1000.0)];
gpu_heat_source(&mut s, &sources, 1.0);
assert!(s.temperature.iter().all(|&t| t.abs() < 1e-12));
}
#[test]
fn test_heat_source_zero_power() {
let mut s = make_solver(4, 4, 4, 100.0);
let idx = s.idx(2, 2, 2);
let sources = vec![HeatSource::new(idx, 0.0)];
gpu_heat_source(&mut s, &sources, 1.0);
assert!((s.temperature[idx] - 100.0).abs() < 1e-12);
}
#[test]
fn test_gradient_uniform_field_is_zero() {
let s = make_solver(5, 5, 5, 300.0);
let grad = temperature_gradient(&s);
for g in &grad {
assert!(g[0].abs() < 1e-12 && g[1].abs() < 1e-12 && g[2].abs() < 1e-12);
}
}
#[test]
fn test_gradient_boundary_is_zero() {
let s = make_solver(4, 4, 4, 100.0);
let grad = temperature_gradient(&s);
assert_eq!(grad[0], [0.0; 3]);
}
#[test]
fn test_gradient_x_linear_field() {
let mut s = GpuThermalSolver::new(5, 5, 5, 1e-4, 1.0, 1.0, 1.0, 0.0);
for iz in 0..5 {
for iy in 0..5 {
for ix in 0..5 {
let idx = s.idx(ix, iy, iz);
s.temperature[idx] = ix as f64;
}
}
}
let grad = temperature_gradient(&s);
let idx = s.idx(2, 2, 2);
assert!((grad[idx][0] - 1.0).abs() < 1e-12, "gx={}", grad[idx][0]);
assert!(grad[idx][1].abs() < 1e-12);
assert!(grad[idx][2].abs() < 1e-12);
}
#[test]
fn test_gradient_y_linear_field() {
let mut s = GpuThermalSolver::new(5, 5, 5, 1e-4, 1.0, 1.0, 1.0, 0.0);
for iz in 0..5 {
for iy in 0..5 {
for ix in 0..5 {
let idx = s.idx(ix, iy, iz);
s.temperature[idx] = iy as f64;
}
}
}
let grad = temperature_gradient(&s);
let idx = s.idx(2, 2, 2);
assert!(grad[idx][0].abs() < 1e-12);
assert!((grad[idx][1] - 1.0).abs() < 1e-12, "gy={}", grad[idx][1]);
assert!(grad[idx][2].abs() < 1e-12);
}
#[test]
fn test_equilibration_identical_fields() {
let s = make_solver(4, 4, 4, 300.0);
let old = s.temperature.clone();
assert!(thermal_equilibration(&s, &old, 1e-6));
}
#[test]
fn test_equilibration_large_change() {
let s = make_solver(4, 4, 4, 300.0);
let old = vec![0.0; s.n_cells()];
assert!(!thermal_equilibration(&s, &old, 1e-6));
}
#[test]
fn test_equilibration_small_change_below_tol() {
let mut s = make_solver(4, 4, 4, 300.0);
let old = s.temperature.clone();
s.temperature[0] += 1e-8; assert!(thermal_equilibration(&s, &old, 1e-6));
}
#[test]
fn test_equilibration_small_change_above_tol() {
let mut s = make_solver(4, 4, 4, 300.0);
let old = s.temperature.clone();
s.temperature[0] += 1.0; assert!(!thermal_equilibration(&s, &old, 1e-6));
}
#[test]
fn test_equilibration_length_mismatch_returns_false() {
let s = make_solver(4, 4, 4, 300.0);
let old = vec![300.0; 10]; assert!(!thermal_equilibration(&s, &old, 1e-6));
}
#[test]
fn test_heat_source_fields() {
let src = HeatSource::new(42, 999.9);
assert_eq!(src.cell_index, 42);
assert!((src.power - 999.9).abs() < 1e-12);
}
#[test]
fn test_diffusion_and_dirichlet_bc_combined() {
let mut s = GpuThermalSolver::new(5, 5, 5, 1e-3, 0.1, 0.1, 0.1, 0.0);
thermal_boundary_apply(
&mut s,
ThermalBc::Dirichlet(100.0),
ThermalBc::Dirichlet(0.0),
ThermalBc::Dirichlet(0.0),
ThermalBc::Dirichlet(0.0),
ThermalBc::Dirichlet(0.0),
ThermalBc::Dirichlet(0.0),
);
for _ in 0..10 {
gpu_heat_diffusion(&mut s, 0.001);
thermal_boundary_apply(
&mut s,
ThermalBc::Dirichlet(100.0),
ThermalBc::Dirichlet(0.0),
ThermalBc::Dirichlet(0.0),
ThermalBc::Dirichlet(0.0),
ThermalBc::Dirichlet(0.0),
ThermalBc::Dirichlet(0.0),
);
}
let interior_idx = s.idx(2, 2, 2);
assert!(s.temperature[interior_idx] > 0.0, "interior should warm up");
let bc_idx = s.idx(0, 2, 2);
assert!((s.temperature[bc_idx] - 100.0).abs() < 1e-12);
}
}