1use glam::{Vec3, Vec4};
7
8pub struct FdtdGrid {
12 pub ex: Vec<f32>,
13 pub ey: Vec<f32>,
14 pub ez: Vec<f32>,
15 pub hx: Vec<f32>,
16 pub hy: Vec<f32>,
17 pub hz: Vec<f32>,
18 pub nx: usize,
19 pub ny: usize,
20 pub nz: usize,
21 pub dx: f32,
22 pub dt: f32,
23}
24
25impl FdtdGrid {
26 pub fn new(nx: usize, ny: usize, nz: usize, dx: f32, dt: f32) -> Self {
27 let size = nx * ny * nz;
28 Self {
29 ex: vec![0.0; size],
30 ey: vec![0.0; size],
31 ez: vec![0.0; size],
32 hx: vec![0.0; size],
33 hy: vec![0.0; size],
34 hz: vec![0.0; size],
35 nx,
36 ny,
37 nz,
38 dx,
39 dt,
40 }
41 }
42
43 fn idx(&self, x: usize, y: usize, z: usize) -> usize {
44 x + y * self.nx + z * self.nx * self.ny
45 }
46
47 pub fn step(&mut self) {
49 let c = self.dt / self.dx;
50 for z in 0..self.nz - 1 {
52 for y in 0..self.ny - 1 {
53 for x in 0..self.nx - 1 {
54 let i = self.idx(x, y, z);
55 let ix = self.idx(x + 1, y, z);
56 let iy = self.idx(x, y + 1, z);
57 let iz = self.idx(x, y, z + 1);
58
59 self.hx[i] -= c * ((self.ez[iy] - self.ez[i]) - (self.ey[iz] - self.ey[i]));
60 self.hy[i] -= c * ((self.ex[iz] - self.ex[i]) - (self.ez[ix] - self.ez[i]));
61 self.hz[i] -= c * ((self.ey[ix] - self.ey[i]) - (self.ex[iy] - self.ex[i]));
62 }
63 }
64 }
65 for z in 1..self.nz {
67 for y in 1..self.ny {
68 for x in 1..self.nx {
69 let i = self.idx(x, y, z);
70 let ix = self.idx(x - 1, y, z);
71 let iy = self.idx(x, y - 1, z);
72 let iz = self.idx(x, y, z - 1);
73
74 self.ex[i] += c * ((self.hz[i] - self.hz[iy]) - (self.hy[i] - self.hy[iz]));
75 self.ey[i] += c * ((self.hx[i] - self.hx[iz]) - (self.hz[i] - self.hz[ix]));
76 self.ez[i] += c * ((self.hy[i] - self.hy[ix]) - (self.hx[i] - self.hx[iy]));
77 }
78 }
79 }
80 }
81
82 pub fn field_energy(&self) -> f32 {
84 let mut energy = 0.0f32;
85 let vol = self.dx * self.dx * self.dx;
86 for i in 0..self.ex.len() {
87 let e2 = self.ex[i] * self.ex[i] + self.ey[i] * self.ey[i] + self.ez[i] * self.ez[i];
88 let h2 = self.hx[i] * self.hx[i] + self.hy[i] * self.hy[i] + self.hz[i] * self.hz[i];
89 energy += 0.5 * (e2 + h2) * vol;
92 }
93 energy
94 }
95
96 pub fn courant_number(&self) -> f32 {
98 self.dt / self.dx
100 }
101
102 pub fn add_source(&mut self, x: usize, y: usize, z: usize, value: f32) {
104 let i = self.idx(x, y, z);
105 if i < self.ez.len() {
106 self.ez[i] += value;
107 }
108 }
109}
110
111pub struct FdtdGrid2D {
115 pub ez: Vec<f32>,
116 pub hx: Vec<f32>,
117 pub hy: Vec<f32>,
118 pub nx: usize,
119 pub ny: usize,
120 pub dx: f32,
121 pub dt: f32,
122 pub permittivity: Vec<f32>,
123 pub permeability: Vec<f32>,
124 pub conductivity: Vec<f32>,
125 pml_ez: Vec<f32>,
127 pml_hx: Vec<f32>,
128 pml_hy: Vec<f32>,
129 mur_prev_left: Vec<f32>,
131 mur_prev_right: Vec<f32>,
132 mur_prev_top: Vec<f32>,
133 mur_prev_bottom: Vec<f32>,
134}
135
136impl FdtdGrid2D {
137 pub fn new(nx: usize, ny: usize, dx: f32, dt: f32) -> Self {
138 let size = nx * ny;
139 Self {
140 ez: vec![0.0; size],
141 hx: vec![0.0; size],
142 hy: vec![0.0; size],
143 nx,
144 ny,
145 dx,
146 dt,
147 permittivity: vec![1.0; size],
148 permeability: vec![1.0; size],
149 conductivity: vec![0.0; size],
150 pml_ez: vec![0.0; size],
151 pml_hx: vec![0.0; size],
152 pml_hy: vec![0.0; size],
153 mur_prev_left: vec![0.0; ny],
154 mur_prev_right: vec![0.0; ny],
155 mur_prev_top: vec![0.0; nx],
156 mur_prev_bottom: vec![0.0; nx],
157 }
158 }
159
160 fn idx(&self, x: usize, y: usize) -> usize {
161 x + y * self.nx
162 }
163
164 pub fn step(&mut self) {
167 let dt = self.dt;
168 let dx = self.dx;
169
170 for y in 0..self.ny - 1 {
172 for x in 0..self.nx {
173 let i = self.idx(x, y);
174 let iy = self.idx(x, y + 1);
175 let mu = self.permeability[i];
176 self.hx[i] -= (dt / (mu * dx)) * (self.ez[iy] - self.ez[i]);
177 }
178 }
179
180 for y in 0..self.ny {
182 for x in 0..self.nx - 1 {
183 let i = self.idx(x, y);
184 let ix = self.idx(x + 1, y);
185 let mu = self.permeability[i];
186 self.hy[i] += (dt / (mu * dx)) * (self.ez[ix] - self.ez[i]);
187 }
188 }
189
190 for y in 1..self.ny {
192 for x in 1..self.nx {
193 let i = self.idx(x, y);
194 let ix = self.idx(x - 1, y);
195 let iy = self.idx(x, y - 1);
196 let eps = self.permittivity[i];
197 let sigma = self.conductivity[i];
198
199 let curl_h = (self.hy[i] - self.hy[ix]) / dx - (self.hx[i] - self.hx[iy]) / dx;
200
201 let ca = (1.0 - sigma * dt / (2.0 * eps)) / (1.0 + sigma * dt / (2.0 * eps));
203 let cb = (dt / eps) / (1.0 + sigma * dt / (2.0 * eps));
204 self.ez[i] = ca * self.ez[i] + cb * curl_h;
205 }
206 }
207 }
208
209 pub fn add_source(&mut self, x: usize, y: usize, value: f32) {
211 let i = self.idx(x, y);
212 if i < self.ez.len() {
213 self.ez[i] += value;
214 }
215 }
216
217 pub fn field_energy(&self) -> f32 {
219 let mut energy = 0.0f32;
220 let area = self.dx * self.dx;
221 for i in 0..self.ez.len() {
222 let eps = self.permittivity[i];
223 let mu = self.permeability[i];
224 let e2 = self.ez[i] * self.ez[i];
225 let h2 = self.hx[i] * self.hx[i] + self.hy[i] * self.hy[i];
226 energy += 0.5 * (eps * e2 + mu * h2) * area;
227 }
228 energy
229 }
230
231 pub fn courant_number(&self) -> f32 {
233 self.dt / self.dx
235 }
236
237 pub fn apply_pml_boundary(&mut self, layers: usize) {
240 let nx = self.nx;
241 let ny = self.ny;
242 let sigma_max = 3.0; for y in 0..ny {
245 for x in 0..nx {
246 let i = self.idx(x, y);
247
248 let dx_left = if x < layers { (layers - x) as f32 / layers as f32 } else { 0.0 };
250 let dx_right = if x >= nx - layers { (x - (nx - layers - 1)) as f32 / layers as f32 } else { 0.0 };
251 let dy_bottom = if y < layers { (layers - y) as f32 / layers as f32 } else { 0.0 };
252 let dy_top = if y >= ny - layers { (y - (ny - layers - 1)) as f32 / layers as f32 } else { 0.0 };
253
254 let sigma_x = sigma_max * (dx_left.max(dx_right)).powi(2);
255 let sigma_y = sigma_max * (dy_bottom.max(dy_top)).powi(2);
256 let sigma = sigma_x + sigma_y;
257
258 if sigma > 0.0 {
259 let decay = (-sigma * self.dt).exp();
261 self.ez[i] *= decay;
262 self.hx[i] *= decay;
263 self.hy[i] *= decay;
264 }
265 }
266 }
267 }
268
269 pub fn apply_mur_abc(&mut self) {
271 let nx = self.nx;
272 let ny = self.ny;
273 let c = 1.0_f32; let coeff = (c * self.dt - self.dx) / (c * self.dt + self.dx);
276
277 for y in 0..ny {
279 let i0 = self.idx(0, y);
280 let i1 = self.idx(1, y);
281 let new_val = self.mur_prev_left[y] + coeff * (self.ez[i1] - self.ez[i0]);
282 self.mur_prev_left[y] = self.ez[i1];
283 self.ez[i0] = new_val;
284 }
285
286 for y in 0..ny {
288 let i0 = self.idx(nx - 1, y);
289 let i1 = self.idx(nx - 2, y);
290 let new_val = self.mur_prev_right[y] + coeff * (self.ez[i1] - self.ez[i0]);
291 self.mur_prev_right[y] = self.ez[i1];
292 self.ez[i0] = new_val;
293 }
294
295 for x in 0..nx {
297 let i0 = self.idx(x, 0);
298 let i1 = self.idx(x, 1);
299 let new_val = self.mur_prev_bottom[x] + coeff * (self.ez[i1] - self.ez[i0]);
300 self.mur_prev_bottom[x] = self.ez[i1];
301 self.ez[i0] = new_val;
302 }
303
304 for x in 0..nx {
306 let i0 = self.idx(x, ny - 1);
307 let i1 = self.idx(x, ny - 2);
308 let new_val = self.mur_prev_top[x] + coeff * (self.ez[i1] - self.ez[i0]);
309 self.mur_prev_top[x] = self.ez[i1];
310 self.ez[i0] = new_val;
311 }
312 }
313
314 pub fn set_material_region(
316 &mut self,
317 x0: usize, y0: usize,
318 x1: usize, y1: usize,
319 eps: f32, mu: f32, sigma: f32,
320 ) {
321 for y in y0..y1.min(self.ny) {
322 for x in x0..x1.min(self.nx) {
323 let i = self.idx(x, y);
324 self.permittivity[i] = eps;
325 self.permeability[i] = mu;
326 self.conductivity[i] = sigma;
327 }
328 }
329 }
330}
331
332pub fn gaussian_pulse(t: f32, t0: f32, spread: f32) -> f32 {
336 let arg = (t - t0) / spread;
337 (-0.5 * arg * arg).exp()
338}
339
340pub fn sinusoidal_source(t: f32, freq: f32) -> f32 {
342 (2.0 * std::f32::consts::PI * freq * t).sin()
343}
344
345pub struct MaterialGrid {
349 pub permittivity: Vec<f32>,
350 pub permeability: Vec<f32>,
351 pub conductivity: Vec<f32>,
352 pub nx: usize,
353 pub ny: usize,
354}
355
356impl MaterialGrid {
357 pub fn new(nx: usize, ny: usize) -> Self {
358 let size = nx * ny;
359 Self {
360 permittivity: vec![1.0; size],
361 permeability: vec![1.0; size],
362 conductivity: vec![0.0; size],
363 nx,
364 ny,
365 }
366 }
367
368 pub fn set(&mut self, x: usize, y: usize, eps: f32, mu: f32, sigma: f32) {
369 let i = x + y * self.nx;
370 if i < self.permittivity.len() {
371 self.permittivity[i] = eps;
372 self.permeability[i] = mu;
373 self.conductivity[i] = sigma;
374 }
375 }
376
377 pub fn get(&self, x: usize, y: usize) -> (f32, f32, f32) {
378 let i = x + y * self.nx;
379 (self.permittivity[i], self.permeability[i], self.conductivity[i])
380 }
381
382 pub fn apply_to(&self, grid: &mut FdtdGrid2D) {
384 assert_eq!(self.nx, grid.nx);
385 assert_eq!(self.ny, grid.ny);
386 grid.permittivity = self.permittivity.clone();
387 grid.permeability = self.permeability.clone();
388 grid.conductivity = self.conductivity.clone();
389 }
390
391 pub fn add_dielectric_slab(&mut self, x0: usize, x1: usize, eps: f32) {
393 for y in 0..self.ny {
394 for x in x0..x1.min(self.nx) {
395 let i = x + y * self.nx;
396 self.permittivity[i] = eps;
397 }
398 }
399 }
400
401 pub fn add_conductor(&mut self, x0: usize, y0: usize, x1: usize, y1: usize, sigma: f32) {
403 for y in y0..y1.min(self.ny) {
404 for x in x0..x1.min(self.nx) {
405 let i = x + y * self.nx;
406 self.conductivity[i] = sigma;
407 }
408 }
409 }
410}
411
412pub struct FdtdRenderer {
417 pub scale: f32,
418 pub field_component: FieldComponent,
419}
420
421#[derive(Clone, Copy, Debug)]
422pub enum FieldComponent {
423 Ez,
424 Hx,
425 Hy,
426 Energy,
427}
428
429impl FdtdRenderer {
430 pub fn new(scale: f32, component: FieldComponent) -> Self {
431 Self {
432 scale,
433 field_component: component,
434 }
435 }
436
437 pub fn render_grid(&self, grid: &FdtdGrid2D) -> Vec<(usize, usize, Vec4)> {
439 let mut result = Vec::with_capacity(grid.nx * grid.ny);
440 for y in 0..grid.ny {
441 for x in 0..grid.nx {
442 let i = x + y * grid.nx;
443 let val = match self.field_component {
444 FieldComponent::Ez => grid.ez[i],
445 FieldComponent::Hx => grid.hx[i],
446 FieldComponent::Hy => grid.hy[i],
447 FieldComponent::Energy => {
448 let e2 = grid.ez[i] * grid.ez[i];
449 let h2 = grid.hx[i] * grid.hx[i] + grid.hy[i] * grid.hy[i];
450 (e2 + h2).sqrt()
451 }
452 };
453
454 let normalized = (val * self.scale).clamp(-1.0, 1.0);
455 let brightness = normalized.abs();
456 let color = if normalized > 0.0 {
457 Vec4::new(brightness, 0.1 * brightness, 0.05 * brightness, brightness.max(0.01))
458 } else {
459 Vec4::new(0.05 * brightness, 0.1 * brightness, brightness, brightness.max(0.01))
460 };
461 result.push((x, y, color));
462 }
463 }
464 result
465 }
466
467 pub fn glyph_for_magnitude(magnitude: f32) -> char {
469 if magnitude > 0.8 {
470 '█'
471 } else if magnitude > 0.6 {
472 '▓'
473 } else if magnitude > 0.4 {
474 '▒'
475 } else if magnitude > 0.2 {
476 '░'
477 } else if magnitude > 0.05 {
478 '·'
479 } else {
480 ' '
481 }
482 }
483}
484
485#[cfg(test)]
488mod tests {
489 use super::*;
490
491 #[test]
492 fn test_gaussian_pulse() {
493 let peak = gaussian_pulse(5.0, 5.0, 1.0);
494 assert!((peak - 1.0).abs() < 1e-6);
495 let off = gaussian_pulse(0.0, 5.0, 1.0);
496 assert!(off < 0.01);
497 }
498
499 #[test]
500 fn test_sinusoidal_source() {
501 let val = sinusoidal_source(0.0, 1.0);
502 assert!(val.abs() < 1e-6);
503 let quarter = sinusoidal_source(0.25, 1.0);
504 assert!((quarter - 1.0).abs() < 1e-5);
505 }
506
507 #[test]
508 fn test_courant_number_2d() {
509 let grid = FdtdGrid2D::new(50, 50, 1.0, 0.5);
510 let cn = grid.courant_number();
511 assert!((cn - 0.5).abs() < 1e-6);
512 assert!(cn < 1.0 / 2.0_f32.sqrt());
514 }
515
516 #[test]
517 fn test_courant_number_3d() {
518 let grid = FdtdGrid::new(10, 10, 10, 1.0, 0.3);
519 let cn = grid.courant_number();
520 assert!((cn - 0.3).abs() < 1e-6);
521 assert!(cn < 1.0 / 3.0_f32.sqrt());
523 }
524
525 #[test]
526 fn test_energy_conservation_no_source() {
527 let mut grid = FdtdGrid2D::new(30, 30, 1.0, 0.4);
529 grid.add_source(15, 15, 1.0);
531 let e0 = grid.field_energy();
532 assert!(e0 > 0.0);
533
534 for _ in 0..5 {
536 grid.step();
537 }
538 let e1 = grid.field_energy();
539 assert!(e1 > 0.0);
542 assert!(e1 < e0 * 10.0); }
544
545 #[test]
546 fn test_wave_speed() {
547 let nx = 100;
550 let ny = 5;
551 let dx = 1.0;
552 let dt = 0.5; let mut grid = FdtdGrid2D::new(nx, ny, dx, dt);
554 let src_x = 10;
555 let src_y = 2;
556
557 for t in 0..40 {
559 let pulse = gaussian_pulse(t as f32 * dt, 5.0, 2.0);
560 grid.add_source(src_x, src_y, pulse);
561 grid.step();
562 }
563
564 let mut max_val = 0.0f32;
566 let mut max_x = 0usize;
567 for x in src_x + 1..nx {
568 let i = x + src_y * nx;
569 if grid.ez[i].abs() > max_val {
570 max_val = grid.ez[i].abs();
571 max_x = x;
572 }
573 }
574
575 assert!(max_x > src_x, "Wave should propagate away from source");
579 assert!(max_x < src_x + 30, "Wave should not exceed speed of light");
580 }
581
582 #[test]
583 fn test_cfl_stability() {
584 let mut grid = FdtdGrid2D::new(40, 40, 1.0, 0.45);
586 assert!(grid.courant_number() < 1.0 / 2.0_f32.sqrt());
587 grid.add_source(20, 20, 10.0);
588 for _ in 0..100 {
589 grid.step();
590 }
591 for val in &grid.ez {
593 assert!(val.is_finite(), "Field should remain finite with stable CFL");
594 }
595 }
596
597 #[test]
598 fn test_pml_absorbs_energy() {
599 let mut grid = FdtdGrid2D::new(60, 60, 1.0, 0.4);
600 grid.add_source(30, 30, 5.0);
601 for _ in 0..20 {
603 grid.step();
604 }
605 let e_no_pml = grid.field_energy();
606
607 let mut grid2 = FdtdGrid2D::new(60, 60, 1.0, 0.4);
609 grid2.add_source(30, 30, 5.0);
610 for _ in 0..20 {
611 grid2.step();
612 grid2.apply_pml_boundary(8);
613 }
614 let e_pml = grid2.field_energy();
615
616 assert!(e_pml < e_no_pml, "PML should absorb outgoing waves");
618 }
619
620 #[test]
621 fn test_material_grid() {
622 let mut mat = MaterialGrid::new(10, 10);
623 mat.add_dielectric_slab(3, 7, 4.0);
624 assert!((mat.permittivity[5 + 5 * 10] - 4.0).abs() < 1e-6);
625 assert!((mat.permittivity[1 + 5 * 10] - 1.0).abs() < 1e-6);
626 }
627
628 #[test]
629 fn test_3d_grid_step() {
630 let mut grid = FdtdGrid::new(10, 10, 10, 1.0, 0.3);
631 grid.add_source(5, 5, 5, 1.0);
632 let e0 = grid.field_energy();
633 assert!(e0 > 0.0);
634 grid.step();
635 let e1 = grid.field_energy();
636 assert!(e1 > 0.0);
637 for val in &grid.ez {
638 assert!(val.is_finite());
639 }
640 }
641
642 #[test]
643 fn test_mur_abc() {
644 let mut grid = FdtdGrid2D::new(50, 50, 1.0, 0.4);
645 grid.add_source(25, 25, 5.0);
646 for _ in 0..30 {
647 grid.step();
648 grid.apply_mur_abc();
649 }
650 for y in 0..grid.ny {
652 let left = grid.ez[grid.idx(0, y)];
653 let right = grid.ez[grid.idx(grid.nx - 1, y)];
654 assert!(left.abs() < 5.0, "Mur ABC should limit boundary reflections");
655 assert!(right.abs() < 5.0);
656 }
657 }
658
659 #[test]
660 fn test_renderer() {
661 let mut grid = FdtdGrid2D::new(10, 10, 1.0, 0.5);
662 grid.ez[55] = 0.5;
663 grid.ez[44] = -0.3;
664 let renderer = FdtdRenderer::new(2.0, FieldComponent::Ez);
665 let pixels = renderer.render_grid(&grid);
666 assert_eq!(pixels.len(), 100);
667 let (_, _, c) = pixels[55];
669 assert!(c.x > c.z);
670 let (_, _, c) = pixels[44];
672 assert!(c.z > c.x);
673 }
674
675 #[test]
676 fn test_glyph_for_magnitude() {
677 assert_eq!(FdtdRenderer::glyph_for_magnitude(0.9), '█');
678 assert_eq!(FdtdRenderer::glyph_for_magnitude(0.5), '▒');
679 assert_eq!(FdtdRenderer::glyph_for_magnitude(0.01), ' ');
680 }
681}