1use glam::{Vec2, Vec4};
6use std::f32::consts::PI;
7
8#[derive(Clone, Debug)]
11pub struct PicParticle {
12 pub x: Vec2,
13 pub v: Vec2,
14 pub charge: f32,
15 pub mass: f32,
16 pub species: u8, }
18
19impl PicParticle {
20 pub fn new(x: Vec2, v: Vec2, charge: f32, mass: f32, species: u8) -> Self {
21 Self { x, v, charge, mass, species }
22 }
23
24 pub fn electron(x: Vec2, v: Vec2) -> Self {
25 Self { x, v, charge: -1.0, mass: 1.0, species: 0 }
26 }
27
28 pub fn ion(x: Vec2, v: Vec2) -> Self {
29 Self { x, v, charge: 1.0, mass: 1836.0, species: 1 }
30 }
31}
32
33#[derive(Clone, Debug)]
36pub struct PicGrid {
37 pub rho: Vec<f32>, pub phi: Vec<f32>, pub ex: Vec<f32>, pub ey: Vec<f32>, pub nx: usize,
42 pub ny: usize,
43 pub dx: f32,
44}
45
46impl PicGrid {
47 pub fn new(nx: usize, ny: usize, dx: f32) -> Self {
48 let size = nx * ny;
49 Self {
50 rho: vec![0.0; size],
51 phi: vec![0.0; size],
52 ex: vec![0.0; size],
53 ey: vec![0.0; size],
54 nx,
55 ny,
56 dx,
57 }
58 }
59
60 fn idx(&self, x: usize, y: usize) -> usize {
61 x.min(self.nx - 1) + y.min(self.ny - 1) * self.nx
62 }
63
64 pub fn clear_rho(&mut self) {
66 for v in &mut self.rho {
67 *v = 0.0;
68 }
69 }
70
71 pub fn field_energy(&self) -> f32 {
73 let mut energy = 0.0f32;
74 let area = self.dx * self.dx;
75 for i in 0..self.ex.len() {
76 energy += 0.5 * (self.ex[i] * self.ex[i] + self.ey[i] * self.ey[i]) * area;
77 }
78 energy
79 }
80}
81
82pub struct PicSimulation {
85 pub particles: Vec<PicParticle>,
86 pub grid: PicGrid,
87 pub dt: f32,
88 pub time: f32,
89}
90
91impl PicSimulation {
92 pub fn new(nx: usize, ny: usize, dx: f32, dt: f32) -> Self {
93 Self {
94 particles: Vec::new(),
95 grid: PicGrid::new(nx, ny, dx),
96 dt,
97 time: 0.0,
98 }
99 }
100
101 pub fn deposit_charge(&mut self) {
103 self.grid.clear_rho();
104 let nx = self.grid.nx;
105 let ny = self.grid.ny;
106 let dx = self.grid.dx;
107 let inv_area = 1.0 / (dx * dx);
108
109 for p in &self.particles {
110 let gx = p.x.x / dx;
112 let gy = p.x.y / dx;
113 let ix = gx.floor() as i32;
114 let iy = gy.floor() as i32;
115 let fx = gx - ix as f32;
116 let fy = gy - iy as f32;
117
118 let weights = [
120 ((1.0 - fx) * (1.0 - fy), ix, iy),
121 (fx * (1.0 - fy), ix + 1, iy),
122 ((1.0 - fx) * fy, ix, iy + 1),
123 (fx * fy, ix + 1, iy + 1),
124 ];
125
126 for &(w, cx, cy) in &weights {
127 let cx = ((cx % nx as i32) + nx as i32) as usize % nx;
129 let cy = ((cy % ny as i32) + ny as i32) as usize % ny;
130 let idx = self.grid.idx(cx, cy);
131 self.grid.rho[idx] += p.charge * w * inv_area;
132 }
133 }
134 }
135
136 pub fn solve_poisson(&mut self) {
138 let nx = self.grid.nx;
139 let ny = self.grid.ny;
140 let dx2 = self.grid.dx * self.grid.dx;
141 let omega = 1.8; let max_iter = 200;
143 let tolerance = 1e-5;
144
145 for _iter in 0..max_iter {
146 let mut max_diff = 0.0f32;
147 for y in 0..ny {
148 for x in 0..nx {
149 let i = self.grid.idx(x, y);
150 let xm = if x == 0 { nx - 1 } else { x - 1 };
152 let xp = if x == nx - 1 { 0 } else { x + 1 };
153 let ym = if y == 0 { ny - 1 } else { y - 1 };
154 let yp = if y == ny - 1 { 0 } else { y + 1 };
155
156 let phi_neighbors = self.grid.phi[self.grid.idx(xm, y)]
157 + self.grid.phi[self.grid.idx(xp, y)]
158 + self.grid.phi[self.grid.idx(x, ym)]
159 + self.grid.phi[self.grid.idx(x, yp)];
160
161 let new_phi = 0.25 * (phi_neighbors + dx2 * self.grid.rho[i]);
162 let diff = (new_phi - self.grid.phi[i]).abs();
163 max_diff = max_diff.max(diff);
164 self.grid.phi[i] = (1.0 - omega) * self.grid.phi[i] + omega * new_phi;
165 }
166 }
167 if max_diff < tolerance {
168 break;
169 }
170 }
171 }
172
173 pub fn compute_field(&mut self) {
175 let nx = self.grid.nx;
176 let ny = self.grid.ny;
177 let inv_2dx = 1.0 / (2.0 * self.grid.dx);
178
179 for y in 0..ny {
180 for x in 0..nx {
181 let i = self.grid.idx(x, y);
182 let xm = if x == 0 { nx - 1 } else { x - 1 };
183 let xp = if x == nx - 1 { 0 } else { x + 1 };
184 let ym = if y == 0 { ny - 1 } else { y - 1 };
185 let yp = if y == ny - 1 { 0 } else { y + 1 };
186
187 self.grid.ex[i] = -(self.grid.phi[self.grid.idx(xp, y)] - self.grid.phi[self.grid.idx(xm, y)]) * inv_2dx;
188 self.grid.ey[i] = -(self.grid.phi[self.grid.idx(x, yp)] - self.grid.phi[self.grid.idx(x, ym)]) * inv_2dx;
189 }
190 }
191 }
192
193 pub fn push_particles(&mut self) {
195 let nx = self.grid.nx;
196 let ny = self.grid.ny;
197 let dx = self.grid.dx;
198 let dt = self.dt;
199 let lx = nx as f32 * dx;
200 let ly = ny as f32 * dx;
201
202 for p in &mut self.particles {
203 let gx = p.x.x / dx;
205 let gy = p.x.y / dx;
206 let ix = gx.floor() as i32;
207 let iy = gy.floor() as i32;
208 let fx = gx - ix as f32;
209 let fy = gy - iy as f32;
210
211 let mut ex_p = 0.0f32;
212 let mut ey_p = 0.0f32;
213
214 let weights = [
215 ((1.0 - fx) * (1.0 - fy), ix, iy),
216 (fx * (1.0 - fy), ix + 1, iy),
217 ((1.0 - fx) * fy, ix, iy + 1),
218 (fx * fy, ix + 1, iy + 1),
219 ];
220
221 for &(w, cx, cy) in &weights {
222 let cx = ((cx % nx as i32) + nx as i32) as usize % nx;
223 let cy = ((cy % ny as i32) + ny as i32) as usize % ny;
224 let idx = cx + cy * nx;
225 ex_p += w * self.grid.ex[idx];
226 ey_p += w * self.grid.ey[idx];
227 }
228
229 let qm = p.charge / p.mass;
231 p.v.x += qm * ex_p * dt;
232 p.v.y += qm * ey_p * dt;
233
234 p.x.x += p.v.x * dt;
236 p.x.y += p.v.y * dt;
237
238 p.x.x = ((p.x.x % lx) + lx) % lx;
240 p.x.y = ((p.x.y % ly) + ly) % ly;
241 }
242 }
243
244 pub fn step(&mut self) {
246 self.deposit_charge();
247 self.solve_poisson();
248 self.compute_field();
249 self.push_particles();
250 self.time += self.dt;
251 }
252
253 pub fn initialize_two_stream(&mut self, n_per_species: usize, v_beam: f32) {
255 let nx = self.grid.nx;
256 let ny = self.grid.ny;
257 let dx = self.grid.dx;
258 let lx = nx as f32 * dx;
259 let ly = ny as f32 * dx;
260
261 self.particles.clear();
262
263 let mut seed = 12345u64;
265 let next_rand = |s: &mut u64| -> f32 {
266 *s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
267 ((*s >> 33) as f32) / (u32::MAX as f32 / 2.0)
268 };
269
270 for _ in 0..n_per_species {
271 let x1 = Vec2::new(next_rand(&mut seed) * lx, next_rand(&mut seed) * ly);
272 let x2 = Vec2::new(next_rand(&mut seed) * lx, next_rand(&mut seed) * ly);
273
274 self.particles.push(PicParticle::new(
276 x1,
277 Vec2::new(v_beam, 0.0),
278 -1.0, 1.0, 0,
279 ));
280 self.particles.push(PicParticle::new(
282 x2,
283 Vec2::new(-v_beam, 0.0),
284 -1.0, 1.0, 0,
285 ));
286 }
287
288 }
291
292 pub fn initialize_landau_damping(&mut self, n: usize, k: f32, amplitude: f32) {
294 let nx = self.grid.nx;
295 let ny = self.grid.ny;
296 let dx = self.grid.dx;
297 let lx = nx as f32 * dx;
298 let ly = ny as f32 * dx;
299
300 self.particles.clear();
301
302 let mut seed = 54321u64;
303 let next_rand = |s: &mut u64| -> f32 {
304 *s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
305 ((*s >> 33) as f32) / (u32::MAX as f32 / 2.0)
306 };
307
308 for _ in 0..n {
309 let mut x = next_rand(&mut seed) * lx;
311 let y = next_rand(&mut seed) * ly;
312
313 x += amplitude / k * (k * x).sin();
315 x = ((x % lx) + lx) % lx;
316
317 let u1 = next_rand(&mut seed).abs().max(1e-10);
319 let u2 = next_rand(&mut seed);
320 let vth = 1.0;
321 let vx = vth * (-2.0 * u1.ln()).sqrt() * (2.0 * PI * u2).cos();
322 let vy = vth * (-2.0 * u1.ln()).sqrt() * (2.0 * PI * u2).sin();
323
324 self.particles.push(PicParticle::new(
325 Vec2::new(x, y),
326 Vec2::new(vx, vy),
327 -1.0, 1.0, 0,
328 ));
329 }
330 }
331
332 pub fn kinetic_energy(&self) -> f32 {
334 self.particles.iter().map(|p| {
335 0.5 * p.mass * p.v.length_squared()
336 }).sum()
337 }
338
339 pub fn field_energy(&self) -> f32 {
341 self.grid.field_energy()
342 }
343
344 pub fn total_energy(&self) -> f32 {
346 self.kinetic_energy() + self.field_energy()
347 }
348
349 pub fn total_charge(&self) -> f32 {
351 self.particles.iter().map(|p| p.charge).sum()
352 }
353}
354
355pub fn debye_length(temp_ev: f32, density: f32) -> f32 {
360 if density < 1e-10 {
361 return f32::INFINITY;
362 }
363 (temp_ev / density).sqrt()
365}
366
367pub fn plasma_frequency(density: f32, mass: f32) -> f32 {
369 if mass < 1e-10 {
370 return 0.0;
371 }
372 (density / mass).sqrt()
374}
375
376pub struct PlasmaRenderer {
380 pub electron_color: Vec4,
381 pub ion_color: Vec4,
382 pub field_scale: f32,
383}
384
385impl PlasmaRenderer {
386 pub fn new() -> Self {
387 Self {
388 electron_color: Vec4::new(0.3, 0.5, 1.0, 0.8),
389 ion_color: Vec4::new(1.0, 0.3, 0.2, 0.8),
390 field_scale: 1.0,
391 }
392 }
393
394 pub fn render_particles(&self, sim: &PicSimulation) -> Vec<(Vec2, Vec4)> {
396 sim.particles.iter().map(|p| {
397 let color = if p.species == 0 {
398 self.electron_color
399 } else {
400 self.ion_color
401 };
402 (p.x, color)
403 }).collect()
404 }
405
406 pub fn render_field(&self, grid: &PicGrid) -> Vec<(usize, usize, Vec4)> {
408 let mut result = Vec::with_capacity(grid.nx * grid.ny);
409 for y in 0..grid.ny {
410 for x in 0..grid.nx {
411 let i = x + y * grid.nx;
412 let e_mag = (grid.ex[i] * grid.ex[i] + grid.ey[i] * grid.ey[i]).sqrt();
413 let brightness = (e_mag * self.field_scale).min(1.0);
414 let color = Vec4::new(brightness, brightness * 0.5, 0.0, brightness * 0.3);
415 result.push((x, y, color));
416 }
417 }
418 result
419 }
420
421 pub fn particle_glyph(species: u8) -> char {
422 match species {
423 0 => '·', 1 => '●', _ => '○',
426 }
427 }
428}
429
430impl Default for PlasmaRenderer {
431 fn default() -> Self {
432 Self::new()
433 }
434}
435
436#[cfg(test)]
439mod tests {
440 use super::*;
441
442 #[test]
443 fn test_charge_conservation() {
444 let mut sim = PicSimulation::new(32, 32, 1.0, 0.1);
445 sim.initialize_two_stream(100, 1.0);
446 let q0 = sim.total_charge();
447 for _ in 0..10 {
448 sim.step();
449 }
450 let q1 = sim.total_charge();
451 assert!((q0 - q1).abs() < 1e-6, "Charge should be conserved: q0={}, q1={}", q0, q1);
452 }
453
454 #[test]
455 fn test_charge_deposition() {
456 let mut sim = PicSimulation::new(8, 8, 1.0, 0.1);
457 sim.particles.push(PicParticle::new(
459 Vec2::new(4.5, 4.5), Vec2::ZERO, 1.0, 1.0, 0,
460 ));
461 sim.deposit_charge();
462 let total: f32 = sim.grid.rho.iter().sum::<f32>() * sim.grid.dx * sim.grid.dx;
464 assert!((total - 1.0).abs() < 0.1, "Total deposited charge: {}", total);
465 }
466
467 #[test]
468 fn test_energy_conservation() {
469 let mut sim = PicSimulation::new(32, 32, 1.0, 0.05);
470 sim.initialize_two_stream(50, 0.5);
471 let e0 = sim.total_energy();
472 for _ in 0..5 {
473 sim.step();
474 }
475 let e1 = sim.total_energy();
476 let relative = if e0.abs() > 1e-10 { (e1 - e0).abs() / e0.abs() } else { (e1 - e0).abs() };
479 assert!(relative < 5.0, "Energy changed too much: e0={}, e1={}, rel={}", e0, e1, relative);
480 }
481
482 #[test]
483 fn test_debye_length() {
484 let ld = debye_length(1.0, 1.0);
485 assert!((ld - 1.0).abs() < 1e-6);
486 let ld2 = debye_length(4.0, 1.0);
487 assert!((ld2 - 2.0).abs() < 1e-6);
488 }
489
490 #[test]
491 fn test_plasma_frequency() {
492 let wp = plasma_frequency(4.0, 1.0);
493 assert!((wp - 2.0).abs() < 1e-6);
494 }
495
496 #[test]
497 fn test_poisson_solver() {
498 let mut sim = PicSimulation::new(16, 16, 1.0, 0.1);
500 for v in &mut sim.grid.rho {
501 *v = 1.0;
502 }
503 sim.solve_poisson();
504 let max_phi = sim.grid.phi.iter().cloned().fold(0.0f32, f32::max);
506 assert!(max_phi > 0.0, "Poisson solver should produce non-zero potential");
507 }
508
509 #[test]
510 fn test_field_from_potential() {
511 let mut sim = PicSimulation::new(16, 16, 1.0, 0.1);
512 for y in 0..16 {
514 for x in 0..16 {
515 sim.grid.phi[x + y * 16] = x as f32;
516 }
517 }
518 sim.compute_field();
519 let mid = sim.grid.ex[8 + 8 * 16];
521 assert!((mid - (-1.0)).abs() < 0.1, "Ex should be -1: {}", mid);
522 }
523
524 #[test]
525 fn test_two_stream_initialization() {
526 let mut sim = PicSimulation::new(32, 32, 1.0, 0.1);
527 sim.initialize_two_stream(100, 2.0);
528 assert_eq!(sim.particles.len(), 200);
529 let right: usize = sim.particles.iter().filter(|p| p.v.x > 0.0).count();
531 let left: usize = sim.particles.iter().filter(|p| p.v.x < 0.0).count();
532 assert_eq!(right, 100);
533 assert_eq!(left, 100);
534 }
535
536 #[test]
537 fn test_debye_screening() {
538 let ld = debye_length(1.0, 1.0);
541 assert!(ld > 0.0);
542 assert!(ld.is_finite());
543 }
544
545 #[test]
546 fn test_renderer() {
547 let sim = PicSimulation::new(8, 8, 1.0, 0.1);
548 let renderer = PlasmaRenderer::new();
549 let field = renderer.render_field(&sim.grid);
550 assert_eq!(field.len(), 64);
551 }
552}