oxirs_physics/gpu/
heat_kernel.rs1use super::{GpuError, GpuResult};
20
21#[derive(Debug, Clone, PartialEq)]
23pub struct HeatGrid {
24 pub nx: usize,
26 pub ny: usize,
28 pub dx: f64,
30 pub dy: f64,
32 pub dt: f64,
34 pub alpha: f64,
36 pub temperature: Vec<f64>,
38}
39
40impl HeatGrid {
41 #[inline]
43 pub fn len(&self) -> usize {
44 self.nx * self.ny
45 }
46
47 #[inline]
49 pub fn is_empty(&self) -> bool {
50 self.nx == 0 || self.ny == 0
51 }
52
53 pub fn cfl(&self) -> f64 {
55 let inv_dx2 = if self.dx > 0.0 {
56 1.0 / (self.dx * self.dx)
57 } else {
58 0.0
59 };
60 let inv_dy2 = if self.dy > 0.0 {
61 1.0 / (self.dy * self.dy)
62 } else {
63 0.0
64 };
65 self.alpha * self.dt * (inv_dx2 + inv_dy2)
66 }
67
68 pub fn is_stable(&self) -> bool {
71 self.cfl() <= 0.5 + 1e-12
72 }
73}
74
75#[derive(Debug, Default)]
77pub struct HeatKernelDispatcher {
78 backend_ready: bool,
79}
80
81impl HeatKernelDispatcher {
82 pub fn new() -> Self {
84 Self {
85 backend_ready: super::backend_available(),
86 }
87 }
88
89 pub fn is_available(&self) -> bool {
91 self.backend_ready
92 }
93
94 pub fn dispatch_step(&self, grid: &HeatGrid) -> GpuResult<Vec<f64>> {
102 validate_grid(grid)?;
103 #[cfg(feature = "gpu")]
104 {
105 if !self.backend_ready {
106 return Err(GpuError::BackendUnavailable);
107 }
108 Ok(Self::cpu_reference_step(grid))
109 }
110 #[cfg(not(feature = "gpu"))]
111 {
112 Err(GpuError::BackendUnavailable)
113 }
114 }
115
116 pub fn dispatch_steps(&self, grid: &HeatGrid, n_steps: usize) -> GpuResult<HeatStepOutput> {
124 validate_grid(grid)?;
125 #[cfg(feature = "gpu")]
126 {
127 if !self.backend_ready {
128 return Err(GpuError::BackendUnavailable);
129 }
130 let mut g = grid.clone();
131 let mut max_delta = 0.0f64;
132 for _ in 0..n_steps {
133 let next = Self::cpu_reference_step(&g);
134 for (a, b) in next.iter().zip(g.temperature.iter()) {
135 max_delta = max_delta.max((a - b).abs());
136 }
137 g.temperature = next;
138 }
139 Ok(HeatStepOutput {
140 temperature: g.temperature,
141 max_delta_per_step: max_delta,
142 })
143 }
144 #[cfg(not(feature = "gpu"))]
145 {
146 let _ = n_steps;
147 Err(GpuError::BackendUnavailable)
148 }
149 }
150
151 pub fn cpu_reference_step(grid: &HeatGrid) -> Vec<f64> {
155 let mut next = grid.temperature.clone();
156 if grid.nx < 3 || grid.ny < 3 {
157 return next;
158 }
159 let inv_dx2 = 1.0 / (grid.dx * grid.dx);
160 let inv_dy2 = 1.0 / (grid.dy * grid.dy);
161 let coeff = grid.alpha * grid.dt;
162 for j in 1..grid.ny - 1 {
163 for i in 1..grid.nx - 1 {
164 let idx = j * grid.nx + i;
165 let east = grid.temperature[idx + 1];
166 let west = grid.temperature[idx - 1];
167 let north = grid.temperature[idx + grid.nx];
168 let south = grid.temperature[idx - grid.nx];
169 let centre = grid.temperature[idx];
170 let lap = (east + west - 2.0 * centre) * inv_dx2
171 + (north + south - 2.0 * centre) * inv_dy2;
172 next[idx] = centre + coeff * lap;
173 }
174 }
175 next
176 }
177}
178
179#[derive(Debug, Clone)]
181pub struct HeatStepOutput {
182 pub temperature: Vec<f64>,
184 pub max_delta_per_step: f64,
186}
187
188fn validate_grid(grid: &HeatGrid) -> GpuResult<()> {
189 if grid.is_empty() {
190 return Err(GpuError::InvalidInput(
191 "heat grid has zero extent".to_string(),
192 ));
193 }
194 if grid.dx <= 0.0
195 || grid.dy <= 0.0
196 || !grid.dx.is_finite()
197 || !grid.dy.is_finite()
198 || grid.dt < 0.0
199 || !grid.dt.is_finite()
200 || grid.alpha < 0.0
201 || !grid.alpha.is_finite()
202 {
203 return Err(GpuError::InvalidInput(format!(
204 "heat grid parameters invalid: dx={}, dy={}, dt={}, alpha={}",
205 grid.dx, grid.dy, grid.dt, grid.alpha
206 )));
207 }
208 if grid.temperature.len() != grid.len() {
209 return Err(GpuError::InvalidInput(format!(
210 "temperature buffer has {} entries, expected {}",
211 grid.temperature.len(),
212 grid.len()
213 )));
214 }
215 Ok(())
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 fn uniform_grid(nx: usize, ny: usize, t: f64) -> HeatGrid {
223 HeatGrid {
224 nx,
225 ny,
226 dx: 0.1,
227 dy: 0.1,
228 dt: 0.001,
229 alpha: 1.0e-4,
230 temperature: vec![t; nx * ny],
231 }
232 }
233
234 #[test]
235 fn dispatcher_availability_matches_feature() {
236 let d = HeatKernelDispatcher::new();
237 #[cfg(feature = "gpu")]
238 assert!(d.is_available());
239 #[cfg(not(feature = "gpu"))]
240 assert!(!d.is_available());
241 }
242
243 #[test]
244 fn cpu_reference_constant_field_unchanged() {
245 let g = uniform_grid(7, 7, 300.0);
246 let next = HeatKernelDispatcher::cpu_reference_step(&g);
247 for v in next {
248 assert!((v - 300.0).abs() < 1e-12);
249 }
250 }
251
252 #[test]
253 fn cpu_reference_diffuses_hot_spot() {
254 let mut g = uniform_grid(7, 7, 0.0);
255 let centre = 3 * g.nx + 3;
256 g.temperature[centre] = 1.0;
257 let next = HeatKernelDispatcher::cpu_reference_step(&g);
258 assert!(next[centre] < 1.0);
260 assert!(next[centre - 1] > 0.0);
261 assert!(next[centre + 1] > 0.0);
262 assert!(next[centre - g.nx] > 0.0);
263 assert!(next[centre + g.nx] > 0.0);
264 }
265
266 #[test]
267 fn dispatch_step_no_feature_returns_unavailable() {
268 let d = HeatKernelDispatcher::new();
269 let g = uniform_grid(5, 5, 273.15);
270 let result = d.dispatch_step(&g);
271 #[cfg(not(feature = "gpu"))]
272 assert!(matches!(result, Err(GpuError::BackendUnavailable)));
273 #[cfg(feature = "gpu")]
274 {
275 let next = result.expect("dispatch should succeed under gpu feature");
276 assert_eq!(next.len(), g.len());
277 }
278 }
279
280 #[test]
281 fn dispatch_steps_max_delta_for_constant_field_is_zero() {
282 let d = HeatKernelDispatcher::new();
283 let g = uniform_grid(5, 5, 273.15);
284 let result = d.dispatch_steps(&g, 4);
285 #[cfg(not(feature = "gpu"))]
286 assert!(matches!(result, Err(GpuError::BackendUnavailable)));
287 #[cfg(feature = "gpu")]
288 {
289 let out = result.expect("dispatch should succeed under gpu feature");
290 assert!(out.max_delta_per_step.abs() < 1e-12);
291 }
292 }
293
294 #[test]
295 fn cfl_stability_check() {
296 let g = uniform_grid(5, 5, 0.0);
297 assert!(g.is_stable());
299 let mut bad = g.clone();
300 bad.dt = 1000.0; assert!(!bad.is_stable());
302 }
303
304 #[test]
305 fn invalid_grid_caught_eagerly() {
306 let d = HeatKernelDispatcher::new();
307 let bad = HeatGrid {
308 nx: 3,
309 ny: 3,
310 dx: 0.0,
311 dy: 0.1,
312 dt: 0.01,
313 alpha: 1.0e-5,
314 temperature: vec![0.0; 9],
315 };
316 let result = d.dispatch_step(&bad);
317 assert!(matches!(result, Err(GpuError::InvalidInput(_))));
318 }
319}