use super::{GpuError, GpuResult};
#[derive(Debug, Clone, PartialEq)]
pub struct HeatGrid {
pub nx: usize,
pub ny: usize,
pub dx: f64,
pub dy: f64,
pub dt: f64,
pub alpha: f64,
pub temperature: Vec<f64>,
}
impl HeatGrid {
#[inline]
pub fn len(&self) -> usize {
self.nx * self.ny
}
#[inline]
pub fn is_empty(&self) -> bool {
self.nx == 0 || self.ny == 0
}
pub fn cfl(&self) -> f64 {
let inv_dx2 = if self.dx > 0.0 {
1.0 / (self.dx * self.dx)
} else {
0.0
};
let inv_dy2 = if self.dy > 0.0 {
1.0 / (self.dy * self.dy)
} else {
0.0
};
self.alpha * self.dt * (inv_dx2 + inv_dy2)
}
pub fn is_stable(&self) -> bool {
self.cfl() <= 0.5 + 1e-12
}
}
#[derive(Debug, Default)]
pub struct HeatKernelDispatcher {
backend_ready: bool,
}
impl HeatKernelDispatcher {
pub fn new() -> Self {
Self {
backend_ready: super::backend_available(),
}
}
pub fn is_available(&self) -> bool {
self.backend_ready
}
pub fn dispatch_step(&self, grid: &HeatGrid) -> GpuResult<Vec<f64>> {
validate_grid(grid)?;
#[cfg(feature = "gpu")]
{
if !self.backend_ready {
return Err(GpuError::BackendUnavailable);
}
Ok(Self::cpu_reference_step(grid))
}
#[cfg(not(feature = "gpu"))]
{
Err(GpuError::BackendUnavailable)
}
}
pub fn dispatch_steps(&self, grid: &HeatGrid, n_steps: usize) -> GpuResult<HeatStepOutput> {
validate_grid(grid)?;
#[cfg(feature = "gpu")]
{
if !self.backend_ready {
return Err(GpuError::BackendUnavailable);
}
let mut g = grid.clone();
let mut max_delta = 0.0f64;
for _ in 0..n_steps {
let next = Self::cpu_reference_step(&g);
for (a, b) in next.iter().zip(g.temperature.iter()) {
max_delta = max_delta.max((a - b).abs());
}
g.temperature = next;
}
Ok(HeatStepOutput {
temperature: g.temperature,
max_delta_per_step: max_delta,
})
}
#[cfg(not(feature = "gpu"))]
{
let _ = n_steps;
Err(GpuError::BackendUnavailable)
}
}
pub fn cpu_reference_step(grid: &HeatGrid) -> Vec<f64> {
let mut next = grid.temperature.clone();
if grid.nx < 3 || grid.ny < 3 {
return next;
}
let inv_dx2 = 1.0 / (grid.dx * grid.dx);
let inv_dy2 = 1.0 / (grid.dy * grid.dy);
let coeff = grid.alpha * grid.dt;
for j in 1..grid.ny - 1 {
for i in 1..grid.nx - 1 {
let idx = j * grid.nx + i;
let east = grid.temperature[idx + 1];
let west = grid.temperature[idx - 1];
let north = grid.temperature[idx + grid.nx];
let south = grid.temperature[idx - grid.nx];
let centre = grid.temperature[idx];
let lap = (east + west - 2.0 * centre) * inv_dx2
+ (north + south - 2.0 * centre) * inv_dy2;
next[idx] = centre + coeff * lap;
}
}
next
}
}
#[derive(Debug, Clone)]
pub struct HeatStepOutput {
pub temperature: Vec<f64>,
pub max_delta_per_step: f64,
}
fn validate_grid(grid: &HeatGrid) -> GpuResult<()> {
if grid.is_empty() {
return Err(GpuError::InvalidInput(
"heat grid has zero extent".to_string(),
));
}
if grid.dx <= 0.0
|| grid.dy <= 0.0
|| !grid.dx.is_finite()
|| !grid.dy.is_finite()
|| grid.dt < 0.0
|| !grid.dt.is_finite()
|| grid.alpha < 0.0
|| !grid.alpha.is_finite()
{
return Err(GpuError::InvalidInput(format!(
"heat grid parameters invalid: dx={}, dy={}, dt={}, alpha={}",
grid.dx, grid.dy, grid.dt, grid.alpha
)));
}
if grid.temperature.len() != grid.len() {
return Err(GpuError::InvalidInput(format!(
"temperature buffer has {} entries, expected {}",
grid.temperature.len(),
grid.len()
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn uniform_grid(nx: usize, ny: usize, t: f64) -> HeatGrid {
HeatGrid {
nx,
ny,
dx: 0.1,
dy: 0.1,
dt: 0.001,
alpha: 1.0e-4,
temperature: vec![t; nx * ny],
}
}
#[test]
fn dispatcher_availability_matches_feature() {
let d = HeatKernelDispatcher::new();
#[cfg(feature = "gpu")]
assert!(d.is_available());
#[cfg(not(feature = "gpu"))]
assert!(!d.is_available());
}
#[test]
fn cpu_reference_constant_field_unchanged() {
let g = uniform_grid(7, 7, 300.0);
let next = HeatKernelDispatcher::cpu_reference_step(&g);
for v in next {
assert!((v - 300.0).abs() < 1e-12);
}
}
#[test]
fn cpu_reference_diffuses_hot_spot() {
let mut g = uniform_grid(7, 7, 0.0);
let centre = 3 * g.nx + 3;
g.temperature[centre] = 1.0;
let next = HeatKernelDispatcher::cpu_reference_step(&g);
assert!(next[centre] < 1.0);
assert!(next[centre - 1] > 0.0);
assert!(next[centre + 1] > 0.0);
assert!(next[centre - g.nx] > 0.0);
assert!(next[centre + g.nx] > 0.0);
}
#[test]
fn dispatch_step_no_feature_returns_unavailable() {
let d = HeatKernelDispatcher::new();
let g = uniform_grid(5, 5, 273.15);
let result = d.dispatch_step(&g);
#[cfg(not(feature = "gpu"))]
assert!(matches!(result, Err(GpuError::BackendUnavailable)));
#[cfg(feature = "gpu")]
{
let next = result.expect("dispatch should succeed under gpu feature");
assert_eq!(next.len(), g.len());
}
}
#[test]
fn dispatch_steps_max_delta_for_constant_field_is_zero() {
let d = HeatKernelDispatcher::new();
let g = uniform_grid(5, 5, 273.15);
let result = d.dispatch_steps(&g, 4);
#[cfg(not(feature = "gpu"))]
assert!(matches!(result, Err(GpuError::BackendUnavailable)));
#[cfg(feature = "gpu")]
{
let out = result.expect("dispatch should succeed under gpu feature");
assert!(out.max_delta_per_step.abs() < 1e-12);
}
}
#[test]
fn cfl_stability_check() {
let g = uniform_grid(5, 5, 0.0);
assert!(g.is_stable());
let mut bad = g.clone();
bad.dt = 1000.0; assert!(!bad.is_stable());
}
#[test]
fn invalid_grid_caught_eagerly() {
let d = HeatKernelDispatcher::new();
let bad = HeatGrid {
nx: 3,
ny: 3,
dx: 0.0,
dy: 0.1,
dt: 0.01,
alpha: 1.0e-5,
temperature: vec![0.0; 9],
};
let result = d.dispatch_step(&bad);
assert!(matches!(result, Err(GpuError::InvalidInput(_))));
}
}