1use rayon::prelude::*;
11
12#[derive(Debug, Clone, Copy, PartialEq)]
18pub enum ThermalBc {
19 Dirichlet(f64),
21 Neumann(f64),
23}
24
25#[derive(Debug, Clone)]
31pub struct HeatSource {
32 pub cell_index: usize,
34 pub power: f64,
36}
37
38impl HeatSource {
39 pub fn new(cell_index: usize, power: f64) -> Self {
41 Self { cell_index, power }
42 }
43}
44
45#[derive(Debug, Clone)]
59pub struct GpuThermalSolver {
60 pub nx: usize,
62 pub ny: usize,
64 pub nz: usize,
66 pub diffusivity: f64,
68 pub dx: f64,
70 pub dy: f64,
72 pub dz: f64,
74 pub temperature: Vec<f64>,
76}
77
78impl GpuThermalSolver {
79 pub fn new(
81 nx: usize,
82 ny: usize,
83 nz: usize,
84 diffusivity: f64,
85 dx: f64,
86 dy: f64,
87 dz: f64,
88 t0: f64,
89 ) -> Self {
90 let n = nx * ny * nz;
91 Self {
92 nx,
93 ny,
94 nz,
95 diffusivity,
96 dx,
97 dy,
98 dz,
99 temperature: vec![t0; n],
100 }
101 }
102
103 #[inline]
105 pub fn idx(&self, ix: usize, iy: usize, iz: usize) -> usize {
106 iz * self.ny * self.nx + iy * self.nx + ix
107 }
108
109 pub fn n_cells(&self) -> usize {
111 self.nx * self.ny * self.nz
112 }
113}
114
115pub fn gpu_heat_diffusion(solver: &mut GpuThermalSolver, dt: f64) {
131 let nx = solver.nx;
132 let ny = solver.ny;
133 let nz = solver.nz;
134 let alpha = solver.diffusivity;
135 let dx2 = solver.dx * solver.dx;
136 let dy2 = solver.dy * solver.dy;
137 let dz2 = solver.dz * solver.dz;
138 let old = solver.temperature.clone();
139
140 let new_temp: Vec<f64> = (0..nz * ny * nx)
141 .into_par_iter()
142 .map(|idx| {
143 let iz = idx / (ny * nx);
144 let rem = idx % (ny * nx);
145 let iy = rem / nx;
146 let ix = rem % nx;
147
148 if ix == 0 || ix == nx - 1 || iy == 0 || iy == ny - 1 || iz == 0 || iz == nz - 1 {
150 return old[idx];
151 }
152
153 let laplacian_x = (old[idx - 1] - 2.0 * old[idx] + old[idx + 1]) / dx2;
154 let laplacian_y = (old[idx - nx] - 2.0 * old[idx] + old[idx + nx]) / dy2;
155 let laplacian_z = (old[idx - ny * nx] - 2.0 * old[idx] + old[idx + ny * nx]) / dz2;
156
157 old[idx] + dt * alpha * (laplacian_x + laplacian_y + laplacian_z)
158 })
159 .collect();
160
161 solver.temperature = new_temp;
162}
163
164pub fn thermal_boundary_apply(
182 solver: &mut GpuThermalSolver,
183 bc_xmin: ThermalBc,
184 bc_xmax: ThermalBc,
185 bc_ymin: ThermalBc,
186 bc_ymax: ThermalBc,
187 bc_zmin: ThermalBc,
188 bc_zmax: ThermalBc,
189) {
190 let nx = solver.nx;
191 let ny = solver.ny;
192 let nz = solver.nz;
193
194 for iz in 0..nz {
196 for iy in 0..ny {
197 let idx_min = solver.idx(0, iy, iz);
198 let idx_max = solver.idx(nx - 1, iy, iz);
199 apply_bc_to_cell(&mut solver.temperature, idx_min, bc_xmin, solver.dx);
200 apply_bc_to_cell(&mut solver.temperature, idx_max, bc_xmax, solver.dx);
201 }
202 }
203
204 for iz in 0..nz {
206 for ix in 0..nx {
207 let idx_min = solver.idx(ix, 0, iz);
208 let idx_max = solver.idx(ix, ny - 1, iz);
209 apply_bc_to_cell(&mut solver.temperature, idx_min, bc_ymin, solver.dy);
210 apply_bc_to_cell(&mut solver.temperature, idx_max, bc_ymax, solver.dy);
211 }
212 }
213
214 for iy in 0..ny {
216 for ix in 0..nx {
217 let idx_min = solver.idx(ix, iy, 0);
218 let idx_max = solver.idx(ix, iy, nz - 1);
219 apply_bc_to_cell(&mut solver.temperature, idx_min, bc_zmin, solver.dz);
220 apply_bc_to_cell(&mut solver.temperature, idx_max, bc_zmax, solver.dz);
221 }
222 }
223}
224
225fn apply_bc_to_cell(temperature: &mut [f64], idx: usize, bc: ThermalBc, _h: f64) {
227 match bc {
228 ThermalBc::Dirichlet(val) => {
229 temperature[idx] = val;
230 }
231 ThermalBc::Neumann(_flux) => {
232 }
238 }
239}
240
241pub fn gpu_heat_source(solver: &mut GpuThermalSolver, sources: &[HeatSource], dt: f64) {
254 for src in sources {
255 if src.cell_index < solver.temperature.len() {
256 solver.temperature[src.cell_index] += src.power * dt;
257 }
258 }
259}
260
261pub fn temperature_gradient(solver: &GpuThermalSolver) -> Vec<[f64; 3]> {
273 let nx = solver.nx;
274 let ny = solver.ny;
275 let nz = solver.nz;
276 let t = &solver.temperature;
277
278 (0..nz * ny * nx)
279 .into_par_iter()
280 .map(|idx| {
281 let iz = idx / (ny * nx);
282 let rem = idx % (ny * nx);
283 let iy = rem / nx;
284 let ix = rem % nx;
285
286 if ix == 0 || ix == nx - 1 || iy == 0 || iy == ny - 1 || iz == 0 || iz == nz - 1 {
287 return [0.0; 3];
288 }
289
290 let gx = (t[idx + 1] - t[idx - 1]) / (2.0 * solver.dx);
291 let gy = (t[idx + nx] - t[idx - nx]) / (2.0 * solver.dy);
292 let gz = (t[idx + ny * nx] - t[idx - ny * nx]) / (2.0 * solver.dz);
293
294 [gx, gy, gz]
295 })
296 .collect()
297}
298
299pub fn thermal_equilibration(solver: &GpuThermalSolver, old: &[f64], tol: f64) -> bool {
313 if old.len() != solver.temperature.len() {
314 return false;
315 }
316 solver
317 .temperature
318 .par_iter()
319 .zip(old.par_iter())
320 .map(|(&new, &prev)| (new - prev).abs())
321 .reduce(|| 0.0_f64, f64::max)
322 < tol
323}
324
325#[cfg(test)]
330mod tests {
331 use super::*;
332
333 fn make_solver(nx: usize, ny: usize, nz: usize, t0: f64) -> GpuThermalSolver {
334 GpuThermalSolver::new(nx, ny, nz, 1e-4, 0.1, 0.1, 0.1, t0)
335 }
336
337 #[test]
340 fn test_solver_new_size() {
341 let s = make_solver(4, 4, 4, 300.0);
342 assert_eq!(s.n_cells(), 64);
343 assert_eq!(s.temperature.len(), 64);
344 }
345
346 #[test]
347 fn test_solver_uniform_init() {
348 let s = make_solver(3, 3, 3, 273.15);
349 assert!(s.temperature.iter().all(|&t| (t - 273.15).abs() < 1e-12));
350 }
351
352 #[test]
353 fn test_solver_idx_origin() {
354 let s = make_solver(5, 5, 5, 0.0);
355 assert_eq!(s.idx(0, 0, 0), 0);
356 }
357
358 #[test]
359 fn test_solver_idx_last() {
360 let s = make_solver(5, 5, 5, 0.0);
361 assert_eq!(s.idx(4, 4, 4), 124);
362 }
363
364 #[test]
365 fn test_solver_idx_slice() {
366 let s = make_solver(4, 4, 4, 0.0);
367 assert_eq!(s.idx(2, 1, 0), 4 + 2);
368 }
369
370 #[test]
373 fn test_diffusion_boundary_unchanged() {
374 let mut s = make_solver(4, 4, 4, 100.0);
375 let idx = s.idx(2, 2, 2);
377 s.temperature[idx] = 500.0;
378 let boundary_before = s.temperature[s.idx(0, 0, 0)];
379 gpu_heat_diffusion(&mut s, 0.001);
380 assert!((s.temperature[s.idx(0, 0, 0)] - boundary_before).abs() < 1e-12);
382 }
383
384 #[test]
385 fn test_diffusion_uniform_field_unchanged() {
386 let mut s = make_solver(5, 5, 5, 300.0);
387 let before: Vec<f64> = s.temperature.clone();
388 gpu_heat_diffusion(&mut s, 0.01);
389 for (a, b) in s.temperature.iter().zip(before.iter()) {
391 assert!(
392 (a - b).abs() < 1e-12,
393 "uniform field changed under diffusion"
394 );
395 }
396 }
397
398 #[test]
399 fn test_diffusion_hot_spot_cools() {
400 let mut s = make_solver(5, 5, 5, 0.0);
401 let idx = s.idx(2, 2, 2);
402 s.temperature[idx] = 1000.0;
403 let before = s.temperature[idx];
404 gpu_heat_diffusion(&mut s, 0.001);
405 assert!(s.temperature[idx] < before);
407 }
408
409 #[test]
410 fn test_diffusion_cold_spot_warms() {
411 let mut s = make_solver(5, 5, 5, 300.0);
412 let idx = s.idx(2, 2, 2);
413 s.temperature[idx] = 0.0;
414 gpu_heat_diffusion(&mut s, 0.001);
415 assert!(s.temperature[idx] > 0.0);
416 }
417
418 #[test]
419 fn test_diffusion_energy_approximately_conserved_interior() {
420 let mut s = make_solver(5, 5, 5, 0.0);
422 let idx = s.idx(2, 2, 2);
423 s.temperature[idx] = 1000.0;
424 let sum_before: f64 = s.temperature.iter().sum();
425 gpu_heat_diffusion(&mut s, 0.001);
426 let sum_after: f64 = s.temperature.iter().sum();
427 assert!((sum_before - sum_after).abs() < 1e-6 * sum_before.abs() + 1e-9);
428 }
429
430 #[test]
433 fn test_dirichlet_xmin_applied() {
434 let mut s = make_solver(6, 6, 6, 0.0);
435 thermal_boundary_apply(
436 &mut s,
437 ThermalBc::Dirichlet(500.0),
438 ThermalBc::Dirichlet(500.0),
439 ThermalBc::Dirichlet(500.0),
440 ThermalBc::Dirichlet(500.0),
441 ThermalBc::Dirichlet(500.0),
442 ThermalBc::Dirichlet(500.0),
443 );
444 for iz in 0..6 {
446 for iy in 0..6 {
447 let idx = s.idx(0, iy, iz);
448 assert!(
449 (s.temperature[idx] - 500.0).abs() < 1e-12,
450 "x=0 face cell ({iy},{iz}) expected 500, got {}",
451 s.temperature[idx]
452 );
453 }
454 }
455 }
456
457 #[test]
458 fn test_dirichlet_xmax_applied() {
459 let mut s = make_solver(6, 6, 6, 0.0);
460 thermal_boundary_apply(
461 &mut s,
462 ThermalBc::Dirichlet(200.0),
463 ThermalBc::Dirichlet(200.0),
464 ThermalBc::Dirichlet(200.0),
465 ThermalBc::Dirichlet(200.0),
466 ThermalBc::Dirichlet(200.0),
467 ThermalBc::Dirichlet(200.0),
468 );
469 for iz in 0..6 {
471 for iy in 0..6 {
472 let idx = s.idx(5, iy, iz);
473 assert!(
474 (s.temperature[idx] - 200.0).abs() < 1e-12,
475 "x=5 face cell ({iy},{iz}) expected 200, got {}",
476 s.temperature[idx]
477 );
478 }
479 }
480 }
481
482 #[test]
483 fn test_neumann_bc_leaves_interior_unchanged_for_zero_flux() {
484 let mut s = make_solver(4, 4, 4, 300.0);
485 let interior_before = s.temperature[s.idx(2, 2, 2)];
486 thermal_boundary_apply(
487 &mut s,
488 ThermalBc::Neumann(0.0),
489 ThermalBc::Neumann(0.0),
490 ThermalBc::Neumann(0.0),
491 ThermalBc::Neumann(0.0),
492 ThermalBc::Neumann(0.0),
493 ThermalBc::Neumann(0.0),
494 );
495 let interior_after = s.temperature[s.idx(2, 2, 2)];
496 assert!((interior_before - interior_after).abs() < 1e-12);
497 }
498
499 #[test]
502 fn test_heat_source_single_cell() {
503 let mut s = make_solver(4, 4, 4, 0.0);
504 let idx = s.idx(2, 2, 2);
505 let sources = vec![HeatSource::new(idx, 1000.0)];
506 gpu_heat_source(&mut s, &sources, 0.1);
507 assert!((s.temperature[idx] - 100.0).abs() < 1e-10);
508 }
509
510 #[test]
511 fn test_heat_source_multiple_cells() {
512 let mut s = make_solver(4, 4, 4, 0.0);
513 let idx1 = s.idx(1, 1, 1);
514 let idx2 = s.idx(2, 2, 2);
515 let sources = vec![HeatSource::new(idx1, 500.0), HeatSource::new(idx2, 200.0)];
516 gpu_heat_source(&mut s, &sources, 1.0);
517 assert!((s.temperature[idx1] - 500.0).abs() < 1e-10);
518 assert!((s.temperature[idx2] - 200.0).abs() < 1e-10);
519 }
520
521 #[test]
522 fn test_heat_source_out_of_bounds_ignored() {
523 let mut s = make_solver(4, 4, 4, 0.0);
524 let sources = vec![HeatSource::new(9999, 1000.0)];
525 gpu_heat_source(&mut s, &sources, 1.0);
527 assert!(s.temperature.iter().all(|&t| t.abs() < 1e-12));
528 }
529
530 #[test]
531 fn test_heat_source_zero_power() {
532 let mut s = make_solver(4, 4, 4, 100.0);
533 let idx = s.idx(2, 2, 2);
534 let sources = vec![HeatSource::new(idx, 0.0)];
535 gpu_heat_source(&mut s, &sources, 1.0);
536 assert!((s.temperature[idx] - 100.0).abs() < 1e-12);
537 }
538
539 #[test]
542 fn test_gradient_uniform_field_is_zero() {
543 let s = make_solver(5, 5, 5, 300.0);
544 let grad = temperature_gradient(&s);
545 for g in &grad {
546 assert!(g[0].abs() < 1e-12 && g[1].abs() < 1e-12 && g[2].abs() < 1e-12);
547 }
548 }
549
550 #[test]
551 fn test_gradient_boundary_is_zero() {
552 let s = make_solver(4, 4, 4, 100.0);
553 let grad = temperature_gradient(&s);
554 assert_eq!(grad[0], [0.0; 3]);
556 }
557
558 #[test]
559 fn test_gradient_x_linear_field() {
560 let mut s = GpuThermalSolver::new(5, 5, 5, 1e-4, 1.0, 1.0, 1.0, 0.0);
562 for iz in 0..5 {
563 for iy in 0..5 {
564 for ix in 0..5 {
565 let idx = s.idx(ix, iy, iz);
566 s.temperature[idx] = ix as f64;
567 }
568 }
569 }
570 let grad = temperature_gradient(&s);
571 let idx = s.idx(2, 2, 2);
572 assert!((grad[idx][0] - 1.0).abs() < 1e-12, "gx={}", grad[idx][0]);
573 assert!(grad[idx][1].abs() < 1e-12);
574 assert!(grad[idx][2].abs() < 1e-12);
575 }
576
577 #[test]
578 fn test_gradient_y_linear_field() {
579 let mut s = GpuThermalSolver::new(5, 5, 5, 1e-4, 1.0, 1.0, 1.0, 0.0);
580 for iz in 0..5 {
581 for iy in 0..5 {
582 for ix in 0..5 {
583 let idx = s.idx(ix, iy, iz);
584 s.temperature[idx] = iy as f64;
585 }
586 }
587 }
588 let grad = temperature_gradient(&s);
589 let idx = s.idx(2, 2, 2);
590 assert!(grad[idx][0].abs() < 1e-12);
591 assert!((grad[idx][1] - 1.0).abs() < 1e-12, "gy={}", grad[idx][1]);
592 assert!(grad[idx][2].abs() < 1e-12);
593 }
594
595 #[test]
598 fn test_equilibration_identical_fields() {
599 let s = make_solver(4, 4, 4, 300.0);
600 let old = s.temperature.clone();
601 assert!(thermal_equilibration(&s, &old, 1e-6));
602 }
603
604 #[test]
605 fn test_equilibration_large_change() {
606 let s = make_solver(4, 4, 4, 300.0);
607 let old = vec![0.0; s.n_cells()];
608 assert!(!thermal_equilibration(&s, &old, 1e-6));
609 }
610
611 #[test]
612 fn test_equilibration_small_change_below_tol() {
613 let mut s = make_solver(4, 4, 4, 300.0);
614 let old = s.temperature.clone();
615 s.temperature[0] += 1e-8; assert!(thermal_equilibration(&s, &old, 1e-6));
617 }
618
619 #[test]
620 fn test_equilibration_small_change_above_tol() {
621 let mut s = make_solver(4, 4, 4, 300.0);
622 let old = s.temperature.clone();
623 s.temperature[0] += 1.0; assert!(!thermal_equilibration(&s, &old, 1e-6));
625 }
626
627 #[test]
628 fn test_equilibration_length_mismatch_returns_false() {
629 let s = make_solver(4, 4, 4, 300.0);
630 let old = vec![300.0; 10]; assert!(!thermal_equilibration(&s, &old, 1e-6));
632 }
633
634 #[test]
637 fn test_heat_source_fields() {
638 let src = HeatSource::new(42, 999.9);
639 assert_eq!(src.cell_index, 42);
640 assert!((src.power - 999.9).abs() < 1e-12);
641 }
642
643 #[test]
646 fn test_diffusion_and_dirichlet_bc_combined() {
647 let mut s = GpuThermalSolver::new(5, 5, 5, 1e-3, 0.1, 0.1, 0.1, 0.0);
648 thermal_boundary_apply(
650 &mut s,
651 ThermalBc::Dirichlet(100.0),
652 ThermalBc::Dirichlet(0.0),
653 ThermalBc::Dirichlet(0.0),
654 ThermalBc::Dirichlet(0.0),
655 ThermalBc::Dirichlet(0.0),
656 ThermalBc::Dirichlet(0.0),
657 );
658 for _ in 0..10 {
660 gpu_heat_diffusion(&mut s, 0.001);
661 thermal_boundary_apply(
663 &mut s,
664 ThermalBc::Dirichlet(100.0),
665 ThermalBc::Dirichlet(0.0),
666 ThermalBc::Dirichlet(0.0),
667 ThermalBc::Dirichlet(0.0),
668 ThermalBc::Dirichlet(0.0),
669 ThermalBc::Dirichlet(0.0),
670 );
671 }
672 let interior_idx = s.idx(2, 2, 2);
674 assert!(s.temperature[interior_idx] > 0.0, "interior should warm up");
675 let bc_idx = s.idx(0, 2, 2);
677 assert!((s.temperature[bc_idx] - 100.0).abs() < 1e-12);
678 }
679}