Skip to main content

proof_engine/electromagnetic/
lightning.rs

1//! Lightning generation using the Dielectric Breakdown Model (DBM).
2//! Solves Laplace's equation to determine growth direction, with branching
3//! and return stroke animation.
4
5use glam::{Vec2, Vec4};
6use std::f32::consts::PI;
7
8// ── Dielectric Breakdown ──────────────────────────────────────────────────
9
10/// Grid for dielectric breakdown simulation.
11pub struct DielectricBreakdown {
12    pub grid: Vec<f32>,        // potential field
13    pub width: usize,
14    pub height: usize,
15    pub threshold: f32,        // breakdown threshold
16    pub bolt_mask: Vec<bool>,  // cells that are part of the bolt
17}
18
19impl DielectricBreakdown {
20    pub fn new(width: usize, height: usize, threshold: f32) -> Self {
21        Self {
22            grid: vec![0.0; width * height],
23            width,
24            height,
25            threshold,
26            bolt_mask: vec![false; width * height],
27        }
28    }
29
30    fn idx(&self, x: usize, y: usize) -> usize {
31        x.min(self.width - 1) + y.min(self.height - 1) * self.width
32    }
33
34    /// Set boundary conditions: top = high voltage, bottom = ground, bolt cells = ground.
35    pub fn set_boundary_conditions(&mut self, start_y: usize) {
36        // Top boundary (source) at high potential
37        for x in 0..self.width {
38            let i = self.idx(x, start_y);
39            self.grid[i] = 1.0;
40        }
41        // Bottom boundary (ground) at zero
42        for x in 0..self.width {
43            let i = self.idx(x, self.height - 1);
44            self.grid[i] = 0.0;
45        }
46        // Bolt cells are at source potential
47        for y in 0..self.height {
48            for x in 0..self.width {
49                let i = self.idx(x, y);
50                if self.bolt_mask[i] {
51                    self.grid[i] = 1.0;
52                }
53            }
54        }
55    }
56}
57
58// ── Lightning Bolt ────────────────────────────────────────────────────────
59
60/// A lightning bolt represented as line segments with brightness.
61#[derive(Clone, Debug)]
62pub struct LightningBolt {
63    pub segments: Vec<(Vec2, Vec2)>,
64    pub brightness: Vec<f32>,
65}
66
67impl LightningBolt {
68    pub fn new() -> Self {
69        Self {
70            segments: Vec::new(),
71            brightness: Vec::new(),
72        }
73    }
74
75    pub fn add_segment(&mut self, start: Vec2, end: Vec2, brightness: f32) {
76        self.segments.push((start, end));
77        self.brightness.push(brightness);
78    }
79
80    /// Total length of the bolt.
81    pub fn total_length(&self) -> f32 {
82        self.segments.iter().map(|(a, b)| (*b - *a).length()).sum()
83    }
84
85    /// Number of segments.
86    pub fn segment_count(&self) -> usize {
87        self.segments.len()
88    }
89
90    /// Get the endpoints of the bolt (first start, last end).
91    pub fn endpoints(&self) -> Option<(Vec2, Vec2)> {
92        if self.segments.is_empty() {
93            return None;
94        }
95        Some((self.segments[0].0, self.segments.last().unwrap().1))
96    }
97}
98
99impl Default for LightningBolt {
100    fn default() -> Self {
101        Self::new()
102    }
103}
104
105// ── Laplace Solver ────────────────────────────────────────────────────────
106
107/// Solve Laplace's equation ∇²φ = 0 on a grid using Jacobi/SOR iteration.
108/// `boundary_mask[i]` = true means the cell is a fixed boundary.
109pub fn laplace_solve(
110    grid: &mut Vec<f32>,
111    boundary_mask: &[bool],
112    width: usize,
113    height: usize,
114    iterations: usize,
115) {
116    let omega = 1.6; // SOR relaxation factor
117    let mut scratch = grid.clone();
118
119    for _ in 0..iterations {
120        for y in 1..height - 1 {
121            for x in 1..width - 1 {
122                let i = x + y * width;
123                if boundary_mask[i] {
124                    continue; // Skip fixed boundary cells
125                }
126                let avg = 0.25 * (
127                    scratch[(x - 1) + y * width]
128                    + scratch[(x + 1) + y * width]
129                    + scratch[x + (y - 1) * width]
130                    + scratch[x + (y + 1) * width]
131                );
132                scratch[i] = (1.0 - omega) * scratch[i] + omega * avg;
133            }
134        }
135        grid.copy_from_slice(&scratch);
136    }
137}
138
139// ── Lightning Generation ──────────────────────────────────────────────────
140
141/// Generate a lightning bolt from `start` to `end` using the Dielectric Breakdown Model.
142///
143/// The algorithm:
144/// 1. Initialize the bolt at the start position.
145/// 2. Solve Laplace's equation for the potential field.
146/// 3. Find candidate growth sites (neighbors of the bolt not yet part of it).
147/// 4. Weight candidates by the local electric field (potential gradient).
148/// 5. Select a growth site (deterministically using a hash for reproducibility).
149/// 6. Extend the bolt and repeat.
150pub fn generate_bolt(
151    start: Vec2,
152    end: Vec2,
153    grid_size: usize,
154    branch_prob: f32,
155) -> LightningBolt {
156    let width = grid_size;
157    let height = grid_size;
158
159    let mut bolt = LightningBolt::new();
160    let mut breakdown = DielectricBreakdown::new(width, height, 0.5);
161
162    // Map start and end to grid coordinates
163    let sx = (start.x * (width - 1) as f32).clamp(0.0, (width - 1) as f32) as usize;
164    let sy = (start.y * (height - 1) as f32).clamp(0.0, (height - 1) as f32) as usize;
165    let ex = (end.x * (width - 1) as f32).clamp(0.0, (width - 1) as f32) as usize;
166    let ey = (end.y * (height - 1) as f32).clamp(0.0, (height - 1) as f32) as usize;
167
168    // Set initial bolt point
169    breakdown.bolt_mask[sx + sy * width] = true;
170    let mut bolt_points: Vec<(usize, usize)> = vec![(sx, sy)];
171
172    // Set boundary: bolt cells at high potential, target at ground
173    let mut boundary_mask = vec![false; width * height];
174    // Top and bottom boundaries
175    for x in 0..width {
176        boundary_mask[x] = true; // top
177        breakdown.grid[x] = 0.0;
178        boundary_mask[x + (height - 1) * width] = true; // bottom
179        breakdown.grid[x + (height - 1) * width] = 0.0;
180    }
181    // Left and right boundaries
182    for y in 0..height {
183        boundary_mask[y * width] = true;
184        boundary_mask[(width - 1) + y * width] = true;
185    }
186
187    // Target point at ground
188    breakdown.grid[ex + ey * width] = 0.0;
189    boundary_mask[ex + ey * width] = true;
190
191    // Simple seed for pseudo-random selection
192    let mut hash_seed = 42u64;
193    let hash_next = |seed: &mut u64| -> f32 {
194        *seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
195        ((*seed >> 33) as f32) / (u32::MAX as f32 / 2.0)
196    };
197
198    let max_steps = width * height / 2;
199    let mut reached_target = false;
200
201    for _step in 0..max_steps {
202        // Update boundary mask for bolt cells
203        for &(bx, by) in &bolt_points {
204            let i = bx + by * width;
205            breakdown.grid[i] = 1.0;
206            boundary_mask[i] = true;
207        }
208
209        // Solve Laplace equation
210        laplace_solve(&mut breakdown.grid, &boundary_mask, width, height, 50);
211
212        // Find candidate growth sites: neighbors of bolt cells that aren't bolt cells
213        let mut candidates: Vec<(usize, usize, f32)> = Vec::new();
214        for &(bx, by) in &bolt_points {
215            let neighbors = [
216                (bx.wrapping_sub(1), by),
217                (bx + 1, by),
218                (bx, by.wrapping_sub(1)),
219                (bx, by + 1),
220            ];
221            for (nx, ny) in neighbors {
222                if nx >= width || ny >= height {
223                    continue;
224                }
225                let ni = nx + ny * width;
226                if breakdown.bolt_mask[ni] {
227                    continue;
228                }
229                // Weight by potential gradient (field strength)
230                let gradient = (1.0 - breakdown.grid[ni]).max(0.0);
231                let eta = 2.0; // growth exponent
232                let weight = gradient.powf(eta);
233                if weight > 1e-10 {
234                    candidates.push((nx, ny, weight));
235                }
236            }
237        }
238
239        if candidates.is_empty() {
240            break;
241        }
242
243        // Select growth site weighted by field
244        let total_weight: f32 = candidates.iter().map(|c| c.2).sum();
245        if total_weight < 1e-10 {
246            break;
247        }
248        let r = hash_next(&mut hash_seed).abs() * total_weight;
249        let mut cumulative = 0.0f32;
250        let mut chosen = candidates[0];
251        for c in &candidates {
252            cumulative += c.2;
253            if cumulative >= r {
254                chosen = *c;
255                break;
256            }
257        }
258
259        let (nx, ny, _) = chosen;
260        let ni = nx + ny * width;
261        breakdown.bolt_mask[ni] = true;
262
263        // Find the closest existing bolt point to connect from
264        let mut best_dist = f32::MAX;
265        let mut from = (sx, sy);
266        for &(bx, by) in &bolt_points {
267            let dx = nx as f32 - bx as f32;
268            let dy = ny as f32 - by as f32;
269            let dist = dx * dx + dy * dy;
270            if dist < best_dist {
271                best_dist = dist;
272                from = (bx, by);
273            }
274        }
275
276        let seg_start = Vec2::new(from.0 as f32 / (width - 1) as f32, from.1 as f32 / (height - 1) as f32);
277        let seg_end = Vec2::new(nx as f32 / (width - 1) as f32, ny as f32 / (height - 1) as f32);
278        bolt.add_segment(seg_start, seg_end, 1.0);
279        bolt_points.push((nx, ny));
280
281        // Check if we reached the target
282        let dx = nx as i32 - ex as i32;
283        let dy = ny as i32 - ey as i32;
284        if dx * dx + dy * dy <= 2 {
285            reached_target = true;
286            // Final segment to target
287            let final_end = Vec2::new(ex as f32 / (width - 1) as f32, ey as f32 / (height - 1) as f32);
288            bolt.add_segment(seg_end, final_end, 1.0);
289            break;
290        }
291    }
292
293    // Add branches
294    add_branches(&mut bolt, branch_prob, &mut hash_seed);
295
296    bolt
297}
298
299/// Add branches to an existing bolt.
300/// Each segment has a probability of spawning a short branch.
301pub fn add_branches(bolt: &mut LightningBolt, probability: f32, seed: &mut u64) {
302    let hash_next = |s: &mut u64| -> f32 {
303        *s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
304        ((*s >> 33) as f32) / (u32::MAX as f32 / 2.0)
305    };
306
307    let original_count = bolt.segments.len();
308    let mut new_segments = Vec::new();
309    let mut new_brightness = Vec::new();
310
311    for i in 0..original_count {
312        let r = hash_next(seed).abs();
313        if r < probability {
314            let (start, end) = bolt.segments[i];
315            let mid = (start + end) * 0.5;
316            let dir = end - start;
317            let len = dir.length() * 0.7;
318
319            // Branch off at an angle
320            let angle_offset = hash_next(seed) * PI * 0.5;
321            let base_angle = dir.y.atan2(dir.x);
322            let branch_angle = base_angle + angle_offset;
323            let branch_end = mid + Vec2::new(branch_angle.cos(), branch_angle.sin()) * len;
324
325            new_segments.push((mid, branch_end));
326            new_brightness.push(bolt.brightness[i] * 0.5);
327
328            // Sub-branch
329            if hash_next(seed).abs() < probability * 0.5 {
330                let sub_len = len * 0.5;
331                let sub_angle = branch_angle + hash_next(seed) * PI * 0.3;
332                let sub_end = branch_end + Vec2::new(sub_angle.cos(), sub_angle.sin()) * sub_len;
333                new_segments.push((branch_end, sub_end));
334                new_brightness.push(bolt.brightness[i] * 0.25);
335            }
336        }
337    }
338
339    bolt.segments.extend(new_segments);
340    bolt.brightness.extend(new_brightness);
341}
342
343/// Apply a return stroke effect: brighten the main channel.
344pub fn bolt_with_return_stroke(mut bolt: LightningBolt) -> LightningBolt {
345    // The main channel is the original segments (before branches).
346    // Brighten them significantly.
347    let main_channel_len = bolt.segments.len();
348    for i in 0..main_channel_len {
349        bolt.brightness[i] = (bolt.brightness[i] * 3.0).min(1.0);
350    }
351    bolt
352}
353
354/// Animate a bolt: leader progresses downward, then return stroke brightens.
355/// Returns segments with animated brightness at the given time.
356pub fn animate_bolt(bolt: &LightningBolt, time: f32) -> Vec<(Vec2, Vec2, f32)> {
357    let total_segments = bolt.segments.len() as f32;
358    if total_segments < 1.0 {
359        return Vec::new();
360    }
361
362    let leader_duration = 1.0; // seconds for leader to reach ground
363    let return_duration = 0.1; // seconds for return stroke
364    let decay_duration = 0.5;  // seconds for brightness decay
365
366    let mut result = Vec::new();
367
368    for (i, &(start, end)) in bolt.segments.iter().enumerate() {
369        let segment_time = (i as f32 / total_segments) * leader_duration;
370        let base_brightness = bolt.brightness[i];
371
372        if time < segment_time {
373            // Not yet reached by leader
374            continue;
375        }
376
377        let brightness;
378        if time < leader_duration {
379            // Leader phase: dim glow
380            brightness = base_brightness * 0.3;
381        } else if time < leader_duration + return_duration {
382            // Return stroke: maximum brightness
383            brightness = base_brightness;
384        } else {
385            // Decay phase
386            let decay_t = (time - leader_duration - return_duration) / decay_duration;
387            brightness = base_brightness * (1.0 - decay_t).max(0.0);
388        }
389
390        if brightness > 0.01 {
391            result.push((start, end, brightness));
392        }
393    }
394
395    result
396}
397
398/// Generate a stepped leader: a series of step-wise advances with random jitter.
399pub fn stepped_leader(
400    start: Vec2,
401    direction: Vec2,
402    steps: usize,
403    seed: &mut u64,
404) -> Vec<Vec2> {
405    let hash_next = |s: &mut u64| -> f32 {
406        *s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
407        ((*s >> 33) as f32) / (u32::MAX as f32 / 2.0)
408    };
409
410    let step_length = direction.length() / steps as f32;
411    let dir_norm = direction.normalize();
412
413    let mut points = Vec::with_capacity(steps + 1);
414    let mut pos = start;
415    points.push(pos);
416
417    for _ in 0..steps {
418        // Advance in the general direction with random jitter
419        let jitter_angle = hash_next(seed) * PI * 0.4 - PI * 0.2;
420        let angle = dir_norm.y.atan2(dir_norm.x) + jitter_angle;
421        let step = Vec2::new(angle.cos(), angle.sin()) * step_length;
422        pos += step;
423        points.push(pos);
424    }
425
426    points
427}
428
429// ── Lightning Renderer ────────────────────────────────────────────────────
430
431/// Renderer for lightning bolts.
432pub struct LightningRenderer {
433    pub core_color: Vec4,
434    pub glow_color: Vec4,
435    pub glow_radius: f32,
436}
437
438impl LightningRenderer {
439    pub fn new() -> Self {
440        Self {
441            core_color: Vec4::new(1.0, 1.0, 1.0, 1.0),
442            glow_color: Vec4::new(0.6, 0.7, 1.0, 0.5),
443            glow_radius: 3.0,
444        }
445    }
446
447    /// Color for a bolt segment based on brightness.
448    pub fn color_for_brightness(&self, brightness: f32) -> Vec4 {
449        let t = brightness.clamp(0.0, 1.0);
450        let core = self.core_color * t;
451        Vec4::new(
452            core.x.max(self.glow_color.x * t * 0.5),
453            core.y.max(self.glow_color.y * t * 0.5),
454            core.z.max(self.glow_color.z * t * 0.5),
455            t,
456        )
457    }
458
459    /// Glyph for rendering lightning segments.
460    pub fn bolt_glyph(direction: Vec2) -> char {
461        let angle = direction.y.atan2(direction.x);
462        let octant = ((angle / (PI / 4.0)).round() as i32).rem_euclid(8);
463        match octant {
464            0 | 4 => '─',
465            1 | 5 => '╲',
466            2 | 6 => '│',
467            3 | 7 => '╱',
468            _ => '⚡',
469        }
470    }
471
472    /// Render a bolt as a list of (position, glyph, color).
473    pub fn render_bolt(&self, bolt: &LightningBolt) -> Vec<(Vec2, char, Vec4)> {
474        let mut result = Vec::new();
475        for (i, &(start, end)) in bolt.segments.iter().enumerate() {
476            let brightness = bolt.brightness[i];
477            let color = self.color_for_brightness(brightness);
478            let dir = end - start;
479            let ch = Self::bolt_glyph(dir);
480            let mid = (start + end) * 0.5;
481            result.push((mid, ch, color));
482        }
483        result
484    }
485}
486
487impl Default for LightningRenderer {
488    fn default() -> Self {
489        Self::new()
490    }
491}
492
493// ── Tests ─────────────────────────────────────────────────────────────────
494
495#[cfg(test)]
496mod tests {
497    use super::*;
498
499    #[test]
500    fn test_laplace_solver() {
501        let width = 10;
502        let height = 10;
503        let mut grid = vec![0.0; width * height];
504        let mut boundary = vec![false; width * height];
505
506        // Top boundary = 1, bottom = 0
507        for x in 0..width {
508            grid[x] = 1.0;
509            boundary[x] = true;
510            grid[x + (height - 1) * width] = 0.0;
511            boundary[x + (height - 1) * width] = true;
512        }
513        // Left and right = linear interpolation
514        for y in 0..height {
515            grid[y * width] = 1.0 - y as f32 / (height - 1) as f32;
516            boundary[y * width] = true;
517            grid[(width - 1) + y * width] = 1.0 - y as f32 / (height - 1) as f32;
518            boundary[(width - 1) + y * width] = true;
519        }
520
521        laplace_solve(&mut grid, &boundary, width, height, 200);
522
523        // Interior should be monotonically decreasing from top to bottom
524        let mid_x = width / 2;
525        for y in 1..height - 1 {
526            let val = grid[mid_x + y * width];
527            assert!(val >= 0.0 && val <= 1.0, "Value at ({},{}) = {} out of range", mid_x, y, val);
528        }
529        // Middle should be approximately 0.5
530        let mid_val = grid[mid_x + (height / 2) * width];
531        assert!((mid_val - 0.5).abs() < 0.15, "Mid value should be ~0.5: {}", mid_val);
532    }
533
534    #[test]
535    fn test_bolt_connects_start_to_end() {
536        let bolt = generate_bolt(
537            Vec2::new(0.5, 0.1),
538            Vec2::new(0.5, 0.9),
539            20,
540            0.0, // no branching for this test
541        );
542
543        assert!(bolt.segments.len() > 0, "Bolt should have segments");
544
545        // Check that the bolt starts near the start point
546        if let Some((first_start, _)) = bolt.segments.first() {
547            let dist_to_start = (*first_start - Vec2::new(0.5, 0.1)).length();
548            assert!(dist_to_start < 0.3, "Bolt should start near start point: {}", dist_to_start);
549        }
550    }
551
552    #[test]
553    fn test_branching_creates_tree() {
554        let bolt_no_branch = generate_bolt(
555            Vec2::new(0.5, 0.1),
556            Vec2::new(0.5, 0.9),
557            20,
558            0.0,
559        );
560        let bolt_with_branch = generate_bolt(
561            Vec2::new(0.5, 0.1),
562            Vec2::new(0.5, 0.9),
563            20,
564            0.8, // high branching probability
565        );
566
567        // Bolt with branching should have more segments
568        assert!(
569            bolt_with_branch.segments.len() >= bolt_no_branch.segments.len(),
570            "Branching should add segments: {} vs {}",
571            bolt_with_branch.segments.len(),
572            bolt_no_branch.segments.len()
573        );
574    }
575
576    #[test]
577    fn test_return_stroke() {
578        let mut bolt = LightningBolt::new();
579        bolt.add_segment(Vec2::ZERO, Vec2::new(0.0, 0.5), 0.3);
580        bolt.add_segment(Vec2::new(0.0, 0.5), Vec2::new(0.0, 1.0), 0.3);
581
582        let bright = bolt_with_return_stroke(bolt);
583        assert!(bright.brightness[0] > 0.3, "Return stroke should brighten");
584    }
585
586    #[test]
587    fn test_animate_bolt() {
588        let mut bolt = LightningBolt::new();
589        for i in 0..10 {
590            let y0 = i as f32 * 0.1;
591            let y1 = (i + 1) as f32 * 0.1;
592            bolt.add_segment(Vec2::new(0.5, y0), Vec2::new(0.5, y1), 1.0);
593        }
594
595        // At t=0, no segments visible yet (first segment_time = 0.0 so it should show)
596        let anim_early = animate_bolt(&bolt, 0.01);
597        assert!(!anim_early.is_empty(), "Some segments should be visible early");
598
599        // At t=1.0+, all should be visible (return stroke)
600        let anim_return = animate_bolt(&bolt, 1.05);
601        assert_eq!(anim_return.len(), 10, "All segments visible during return stroke");
602    }
603
604    #[test]
605    fn test_stepped_leader() {
606        let mut seed = 99u64;
607        let points = stepped_leader(
608            Vec2::new(0.5, 0.0),
609            Vec2::new(0.0, 1.0),
610            20,
611            &mut seed,
612        );
613        assert_eq!(points.len(), 21);
614        assert!((points[0] - Vec2::new(0.5, 0.0)).length() < 1e-6);
615        // Should generally move in the direction specified
616        let last = points.last().unwrap();
617        assert!(last.y > 0.0, "Leader should progress in direction");
618    }
619
620    #[test]
621    fn test_renderer_glyph() {
622        assert_eq!(LightningRenderer::bolt_glyph(Vec2::new(0.0, 1.0)), '│');
623        assert_eq!(LightningRenderer::bolt_glyph(Vec2::new(1.0, 0.0)), '─');
624    }
625
626    #[test]
627    fn test_bolt_total_length() {
628        let mut bolt = LightningBolt::new();
629        bolt.add_segment(Vec2::ZERO, Vec2::new(1.0, 0.0), 1.0);
630        bolt.add_segment(Vec2::new(1.0, 0.0), Vec2::new(1.0, 1.0), 1.0);
631        assert!((bolt.total_length() - 2.0).abs() < 1e-6);
632    }
633}