Skip to main content

ringkernel_wavesim/simulation/
educational.rs

1//! Educational simulation modes demonstrating the evolution of parallel computing.
2//!
3//! These modes provide visual demonstrations of how computing paradigms have evolved:
4//!
5//! 1. **CellByCell (1950s Sequential)**: Processes one cell at a time, showing how early
6//!    computers executed instructions sequentially.
7//!
8//! 2. **RowByRow (1970s Vectorization)**: Processes entire rows at once, demonstrating
9//!    the vectorization paradigm introduced by machines like the Cray-1.
10//!
11//! 3. **ChaoticParallel (1990s Naive Parallelism)**: Multiple cells processed without
12//!    coordination, showing race conditions and data inconsistency issues.
13//!
14//! 4. **SynchronizedParallel (2000s Barrier Sync)**: Parallel execution with barriers,
15//!    demonstrating the dominant parallel computing model of the 2000s.
16//!
17//! 5. **ActorBased (Modern)**: Tile-based actors with Hybrid Logical Clocks for
18//!    causal consistency - the RingKernel approach.
19
20use rand::prelude::*;
21use rand::rngs::ThreadRng;
22
23/// Educational simulation modes demonstrating computing evolution.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
25pub enum SimulationMode {
26    /// Standard full-speed mode (no visualization of processing).
27    #[default]
28    Standard,
29
30    /// Sequential cell-by-cell processing (1950s era).
31    /// Processes one cell per frame with visual indicator.
32    CellByCell,
33
34    /// Row-by-row vectorized processing (1970s era).
35    /// Processes one row per frame.
36    RowByRow,
37
38    /// Chaotic parallel without synchronization (1990s era).
39    /// Shows data races and inconsistent results.
40    ChaoticParallel,
41
42    /// Synchronized parallel with barriers (2000s era).
43    /// Shows proper synchronization but with overhead.
44    SynchronizedParallel,
45
46    /// Actor/tile-based with HLC (Modern approach).
47    /// Demonstrates causal consistency with message passing.
48    ActorBased,
49}
50
51impl std::fmt::Display for SimulationMode {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        match self {
54            SimulationMode::Standard => write!(f, "Standard"),
55            SimulationMode::CellByCell => write!(f, "Cell-by-Cell (1950s)"),
56            SimulationMode::RowByRow => write!(f, "Row-by-Row (1970s)"),
57            SimulationMode::ChaoticParallel => write!(f, "Chaotic (1990s)"),
58            SimulationMode::SynchronizedParallel => write!(f, "Barrier Sync (2000s)"),
59            SimulationMode::ActorBased => write!(f, "Actor/HLC (Modern)"),
60        }
61    }
62}
63
64impl SimulationMode {
65    /// Get all available modes for the UI.
66    pub fn all() -> &'static [SimulationMode] {
67        &[
68            SimulationMode::Standard,
69            SimulationMode::CellByCell,
70            SimulationMode::RowByRow,
71            SimulationMode::ChaoticParallel,
72            SimulationMode::SynchronizedParallel,
73            SimulationMode::ActorBased,
74        ]
75    }
76
77    /// Get a description of the mode for educational purposes.
78    pub fn description(&self) -> &'static str {
79        match self {
80            SimulationMode::Standard => "Full-speed parallel simulation",
81            SimulationMode::CellByCell => "1950s: Sequential processing, one cell at a time",
82            SimulationMode::RowByRow => {
83                "1970s: Vector processing, entire rows at once (Cray-style)"
84            }
85            SimulationMode::ChaoticParallel => "1990s: Parallel without sync - watch for glitches!",
86            SimulationMode::SynchronizedParallel => "2000s: Parallel with barriers - safe but slow",
87            SimulationMode::ActorBased => "Modern: Actor model with HLC for causal ordering",
88        }
89    }
90
91    /// Whether this mode shows processing indicators.
92    pub fn shows_processing(&self) -> bool {
93        matches!(
94            self,
95            SimulationMode::CellByCell
96                | SimulationMode::RowByRow
97                | SimulationMode::ChaoticParallel
98                | SimulationMode::SynchronizedParallel
99                | SimulationMode::ActorBased
100        )
101    }
102}
103
104/// Result of a single frame of educational processing.
105#[derive(Debug, Clone)]
106pub struct StepResult {
107    /// Whether the full simulation step is complete (buffers should be swapped).
108    pub step_complete: bool,
109    /// Whether buffers should be swapped after this frame.
110    pub should_swap: bool,
111}
112
113impl StepResult {
114    fn in_progress() -> Self {
115        Self {
116            step_complete: false,
117            should_swap: false,
118        }
119    }
120
121    fn complete() -> Self {
122        Self {
123            step_complete: true,
124            should_swap: true,
125        }
126    }
127}
128
129/// State for tracking incremental processing in educational modes.
130#[derive(Debug, Clone)]
131pub struct ProcessingState {
132    /// Current cell being processed (for CellByCell mode).
133    pub current_cell: Option<(u32, u32)>,
134
135    /// Current row being processed (for RowByRow mode).
136    pub current_row: Option<u32>,
137
138    /// Set of cells currently being processed (for parallel modes).
139    pub active_cells: Vec<(u32, u32)>,
140
141    /// Cells processed in the current step (for visualization).
142    pub just_processed: Vec<(u32, u32)>,
143
144    /// Current tile being processed (for ActorBased mode).
145    pub active_tiles: Vec<(u32, u32)>,
146
147    /// Whether processing is complete for the current simulation step.
148    pub step_complete: bool,
149
150    /// Logical clock value (for ActorBased mode).
151    pub hlc_tick: u64,
152
153    /// Messages in flight (for ActorBased visualization).
154    pub messages_in_flight: Vec<TileMessage>,
155}
156
157/// A message between tiles (for ActorBased visualization).
158#[derive(Debug, Clone)]
159pub struct TileMessage {
160    /// Source tile.
161    pub from: (u32, u32),
162    /// Destination tile.
163    pub to: (u32, u32),
164    /// HLC timestamp.
165    pub timestamp: u64,
166}
167
168impl Default for ProcessingState {
169    fn default() -> Self {
170        Self {
171            current_cell: None,
172            current_row: None,
173            active_cells: Vec::new(),
174            just_processed: Vec::new(),
175            active_tiles: Vec::new(),
176            step_complete: true,
177            hlc_tick: 0,
178            messages_in_flight: Vec::new(),
179        }
180    }
181}
182
183impl ProcessingState {
184    /// Reset state for a new step.
185    pub fn begin_step(&mut self) {
186        self.current_cell = None;
187        self.current_row = None;
188        self.active_cells.clear();
189        self.just_processed.clear();
190        self.active_tiles.clear();
191        self.step_complete = false;
192    }
193
194    /// Mark step as complete.
195    pub fn complete_step(&mut self) {
196        self.step_complete = true;
197        self.hlc_tick += 1;
198    }
199}
200
201/// Educational step processor for SimulationGrid.
202pub struct EducationalProcessor {
203    /// Processing state for visualization.
204    pub state: ProcessingState,
205
206    /// Simulation mode.
207    pub mode: SimulationMode,
208
209    /// Cells per frame for parallel modes.
210    pub cells_per_frame: usize,
211
212    /// Random number generator for chaotic mode.
213    rng: ThreadRng,
214
215    /// Internal position tracking for incremental processing.
216    cell_x: u32,
217    cell_y: u32,
218
219    /// Tile size for actor-based mode.
220    tile_size: u32,
221}
222
223impl Default for EducationalProcessor {
224    fn default() -> Self {
225        Self {
226            state: ProcessingState::default(),
227            mode: SimulationMode::Standard,
228            cells_per_frame: 16,
229            rng: thread_rng(),
230            cell_x: 1,
231            cell_y: 1,
232            tile_size: 16,
233        }
234    }
235}
236
237impl EducationalProcessor {
238    /// Create a new processor with the given mode.
239    pub fn new(mode: SimulationMode) -> Self {
240        Self {
241            mode,
242            ..Default::default()
243        }
244    }
245
246    /// Set the simulation mode.
247    pub fn set_mode(&mut self, mode: SimulationMode) {
248        self.mode = mode;
249        self.reset();
250    }
251
252    /// Reset processing state.
253    pub fn reset(&mut self) {
254        self.state = ProcessingState::default();
255        self.cell_x = 1;
256        self.cell_y = 1;
257    }
258
259    /// Perform one frame of educational processing on the grid.
260    /// Returns StepResult indicating whether step is complete and if buffers should be swapped.
261    pub fn step_frame(
262        &mut self,
263        pressure: &mut [f32],
264        pressure_prev: &mut [f32],
265        width: usize,
266        height: usize,
267        c2: f32,
268        damping: f32,
269    ) -> StepResult {
270        match self.mode {
271            SimulationMode::Standard => {
272                self.step_standard(pressure, pressure_prev, width, height, c2, damping)
273            }
274            SimulationMode::CellByCell => {
275                self.step_cell_by_cell(pressure, pressure_prev, width, height, c2, damping)
276            }
277            SimulationMode::RowByRow => {
278                self.step_row_by_row(pressure, pressure_prev, width, height, c2, damping)
279            }
280            SimulationMode::ChaoticParallel => {
281                self.step_chaotic_parallel(pressure, pressure_prev, width, height, c2, damping)
282            }
283            SimulationMode::SynchronizedParallel => {
284                self.step_synchronized_parallel(pressure, pressure_prev, width, height, c2, damping)
285            }
286            SimulationMode::ActorBased => {
287                self.step_actor_based(pressure, pressure_prev, width, height, c2, damping)
288            }
289        }
290    }
291
292    /// Standard full-speed processing.
293    fn step_standard(
294        &mut self,
295        pressure: &[f32],
296        pressure_prev: &mut [f32],
297        width: usize,
298        height: usize,
299        c2: f32,
300        damping: f32,
301    ) -> StepResult {
302        // Process all interior cells
303        for y in 1..height - 1 {
304            for x in 1..width - 1 {
305                let idx = y * width + x;
306                let p_curr = pressure[idx];
307                let p_prev = pressure_prev[idx];
308                let p_north = pressure[idx - width];
309                let p_south = pressure[idx + width];
310                let p_west = pressure[idx - 1];
311                let p_east = pressure[idx + 1];
312
313                let laplacian = p_north + p_south + p_east + p_west - 4.0 * p_curr;
314                let p_new = 2.0 * p_curr - p_prev + c2 * laplacian;
315                pressure_prev[idx] = p_new * damping;
316            }
317        }
318
319        self.state.complete_step();
320        StepResult::complete()
321    }
322
323    /// Cell-by-cell sequential processing (1950s).
324    /// Processes a small number of cells per frame for visualization.
325    fn step_cell_by_cell(
326        &mut self,
327        pressure: &[f32],
328        pressure_prev: &mut [f32],
329        width: usize,
330        height: usize,
331        c2: f32,
332        damping: f32,
333    ) -> StepResult {
334        self.state.just_processed.clear();
335
336        let cells_this_frame = self.cells_per_frame.min(32);
337
338        for _ in 0..cells_this_frame {
339            if self.cell_y >= height as u32 - 1 {
340                // Done with this step
341                self.cell_x = 1;
342                self.cell_y = 1;
343                self.state.complete_step();
344                return StepResult::complete();
345            }
346
347            let x = self.cell_x as usize;
348            let y = self.cell_y as usize;
349
350            // Process this cell
351            let idx = y * width + x;
352            let p_curr = pressure[idx];
353            let p_prev = pressure_prev[idx];
354            let p_north = pressure[idx - width];
355            let p_south = pressure[idx + width];
356            let p_west = pressure[idx - 1];
357            let p_east = pressure[idx + 1];
358
359            let laplacian = p_north + p_south + p_east + p_west - 4.0 * p_curr;
360            let p_new = 2.0 * p_curr - p_prev + c2 * laplacian;
361            pressure_prev[idx] = p_new * damping;
362
363            self.state.just_processed.push((self.cell_x, self.cell_y));
364            self.state.current_cell = Some((self.cell_x, self.cell_y));
365
366            // Advance to next cell
367            self.cell_x += 1;
368            if self.cell_x >= width as u32 - 1 {
369                self.cell_x = 1;
370                self.cell_y += 1;
371            }
372        }
373
374        StepResult::in_progress()
375    }
376
377    /// Row-by-row vectorized processing (1970s).
378    fn step_row_by_row(
379        &mut self,
380        pressure: &[f32],
381        pressure_prev: &mut [f32],
382        width: usize,
383        height: usize,
384        c2: f32,
385        damping: f32,
386    ) -> StepResult {
387        self.state.just_processed.clear();
388
389        if self.cell_y >= height as u32 - 1 {
390            // Done with this step
391            self.cell_y = 1;
392            self.state.current_row = None;
393            self.state.complete_step();
394            return StepResult::complete();
395        }
396
397        let y = self.cell_y as usize;
398        self.state.current_row = Some(self.cell_y);
399
400        // Process entire row at once (vectorized style)
401        for x in 1..width - 1 {
402            let idx = y * width + x;
403            let p_curr = pressure[idx];
404            let p_prev = pressure_prev[idx];
405            let p_north = pressure[idx - width];
406            let p_south = pressure[idx + width];
407            let p_west = pressure[idx - 1];
408            let p_east = pressure[idx + 1];
409
410            let laplacian = p_north + p_south + p_east + p_west - 4.0 * p_curr;
411            let p_new = 2.0 * p_curr - p_prev + c2 * laplacian;
412            pressure_prev[idx] = p_new * damping;
413
414            self.state.just_processed.push((x as u32, self.cell_y));
415        }
416
417        self.cell_y += 1;
418        StepResult::in_progress()
419    }
420
421    /// Chaotic parallel processing without synchronization (1990s).
422    /// Deliberately shows data races and inconsistent results.
423    fn step_chaotic_parallel(
424        &mut self,
425        pressure: &mut [f32],
426        pressure_prev: &mut [f32],
427        width: usize,
428        height: usize,
429        c2: f32,
430        damping: f32,
431    ) -> StepResult {
432        self.state.just_processed.clear();
433        self.state.active_cells.clear();
434
435        // Pick random cells to process (simulating unsynchronized parallel access)
436        let num_cells = (width * height / 16).min(64);
437
438        for _ in 0..num_cells {
439            let x = self.rng.gen_range(1..width - 1);
440            let y = self.rng.gen_range(1..height - 1);
441
442            self.state.active_cells.push((x as u32, y as u32));
443
444            // Process this cell - note: reading from already-modified neighbors causes chaos
445            let idx = y * width + x;
446            let p_curr = pressure[idx];
447            let p_prev = pressure_prev[idx];
448
449            // Sometimes read from pressure_prev (old data), sometimes from pressure (in-progress)
450            // This simulates race conditions
451            let (p_north, p_south, p_east, p_west) = if self.rng.gen_bool(0.5) {
452                // Read from "current" buffer (race condition!)
453                (
454                    pressure[idx - width],
455                    pressure[idx + width],
456                    pressure[idx + 1],
457                    pressure[idx - 1],
458                )
459            } else {
460                // Read from "previous" buffer (also partially updated - chaos!)
461                (
462                    pressure_prev[idx - width],
463                    pressure_prev[idx + width],
464                    pressure_prev[idx + 1],
465                    pressure_prev[idx - 1],
466                )
467            };
468
469            let laplacian = p_north + p_south + p_east + p_west - 4.0 * p_curr;
470            let p_new = 2.0 * p_curr - p_prev + c2 * laplacian;
471
472            // Sometimes write to wrong buffer too!
473            if self.rng.gen_bool(0.3) {
474                pressure[idx] = p_new * damping; // Wrong buffer!
475            } else {
476                pressure_prev[idx] = p_new * damping;
477            }
478        }
479
480        self.state.just_processed = self.state.active_cells.clone();
481
482        // After enough frames, complete the step (but with inconsistent data!)
483        self.cell_y += 1;
484        if self.cell_y >= 8 {
485            self.cell_y = 0;
486            self.state.complete_step();
487            return StepResult::complete();
488        }
489
490        StepResult::in_progress()
491    }
492
493    /// Synchronized parallel with barriers (2000s).
494    fn step_synchronized_parallel(
495        &mut self,
496        pressure: &[f32],
497        pressure_prev: &mut [f32],
498        width: usize,
499        height: usize,
500        c2: f32,
501        damping: f32,
502    ) -> StepResult {
503        self.state.just_processed.clear();
504        self.state.active_cells.clear();
505
506        // Process in wavefronts for correct dependencies
507        // A wavefront is all cells where x + y = constant
508        let max_wavefront = (width + height - 4) as u32;
509
510        if self.cell_y > max_wavefront {
511            self.cell_y = 0;
512            self.state.complete_step();
513            return StepResult::complete();
514        }
515
516        let wavefront = self.cell_y;
517
518        // Process all cells in this wavefront (they're independent!)
519        for x in 1..width - 1 {
520            let y_calc = wavefront as i32 - x as i32 + 1;
521            if y_calc >= 1 && y_calc < (height - 1) as i32 {
522                let y = y_calc as usize;
523                let idx = y * width + x;
524                let p_curr = pressure[idx];
525                let p_prev = pressure_prev[idx];
526                let p_north = pressure[idx - width];
527                let p_south = pressure[idx + width];
528                let p_west = pressure[idx - 1];
529                let p_east = pressure[idx + 1];
530
531                let laplacian = p_north + p_south + p_east + p_west - 4.0 * p_curr;
532                let p_new = 2.0 * p_curr - p_prev + c2 * laplacian;
533                pressure_prev[idx] = p_new * damping;
534
535                self.state.active_cells.push((x as u32, y as u32));
536            }
537        }
538
539        self.state.just_processed = self.state.active_cells.clone();
540        self.cell_y += 1; // Move to next wavefront
541        StepResult::in_progress()
542    }
543
544    /// Actor/tile-based processing with HLC (Modern).
545    fn step_actor_based(
546        &mut self,
547        pressure: &[f32],
548        pressure_prev: &mut [f32],
549        width: usize,
550        height: usize,
551        c2: f32,
552        damping: f32,
553    ) -> StepResult {
554        self.state.just_processed.clear();
555        self.state.active_tiles.clear();
556        self.state.messages_in_flight.clear();
557
558        let tile_size = self.tile_size as usize;
559        // Interior region is [1, width-2] x [1, height-2]
560        let interior_width = width - 2;
561        let interior_height = height - 2;
562        // Use ceiling division to ensure all cells are covered
563        let tiles_x = interior_width.div_ceil(tile_size);
564        let tiles_y = interior_height.div_ceil(tile_size);
565
566        let current_tile = self.cell_y as usize;
567        let total_tiles = tiles_x * tiles_y;
568
569        if current_tile >= total_tiles {
570            self.cell_y = 0;
571            self.state.hlc_tick += 1;
572            self.state.complete_step();
573            return StepResult::complete();
574        }
575
576        // Process one tile at a time with HLC-ordered messages
577        let tile_x = current_tile % tiles_x;
578        let tile_y = current_tile / tiles_x;
579
580        self.state.active_tiles.push((tile_x as u32, tile_y as u32));
581
582        // Create visual messages to/from neighbors
583        if tile_x > 0 {
584            self.state.messages_in_flight.push(TileMessage {
585                from: ((tile_x - 1) as u32, tile_y as u32),
586                to: (tile_x as u32, tile_y as u32),
587                timestamp: self.state.hlc_tick,
588            });
589        }
590        if tile_y > 0 {
591            self.state.messages_in_flight.push(TileMessage {
592                from: (tile_x as u32, (tile_y - 1) as u32),
593                to: (tile_x as u32, tile_y as u32),
594                timestamp: self.state.hlc_tick,
595            });
596        }
597
598        // Process all cells in this tile
599        // Tile starts at offset 1 (skip boundary) + tile_x * tile_size
600        let start_x = 1 + tile_x * tile_size;
601        let start_y = 1 + tile_y * tile_size;
602        // Clamp to interior region (exclude boundary cells)
603        let end_x = (start_x + tile_size).min(width - 1);
604        let end_y = (start_y + tile_size).min(height - 1);
605
606        for y in start_y..end_y {
607            for x in start_x..end_x {
608                let idx = y * width + x;
609                let p_curr = pressure[idx];
610                let p_prev = pressure_prev[idx];
611                let p_north = pressure[idx - width];
612                let p_south = pressure[idx + width];
613                let p_west = pressure[idx - 1];
614                let p_east = pressure[idx + 1];
615
616                let laplacian = p_north + p_south + p_east + p_west - 4.0 * p_curr;
617                let p_new = 2.0 * p_curr - p_prev + c2 * laplacian;
618                pressure_prev[idx] = p_new * damping;
619
620                self.state.just_processed.push((x as u32, y as u32));
621            }
622        }
623
624        self.cell_y += 1;
625        StepResult::in_progress()
626    }
627}
628
629#[cfg(test)]
630mod tests {
631    use super::*;
632
633    #[test]
634    fn test_mode_display() {
635        assert_eq!(
636            format!("{}", SimulationMode::CellByCell),
637            "Cell-by-Cell (1950s)"
638        );
639        assert_eq!(
640            format!("{}", SimulationMode::RowByRow),
641            "Row-by-Row (1970s)"
642        );
643        assert_eq!(
644            format!("{}", SimulationMode::ChaoticParallel),
645            "Chaotic (1990s)"
646        );
647    }
648
649    #[test]
650    fn test_mode_all() {
651        let modes = SimulationMode::all();
652        assert_eq!(modes.len(), 6);
653    }
654
655    #[test]
656    fn test_processing_state_reset() {
657        let mut state = ProcessingState {
658            current_cell: Some((5, 5)),
659            step_complete: false,
660            ..Default::default()
661        };
662        state.begin_step();
663        assert!(state.current_cell.is_none());
664        assert!(!state.step_complete);
665    }
666
667    #[test]
668    fn test_cell_by_cell_processing() {
669        let mut processor = EducationalProcessor::new(SimulationMode::CellByCell);
670        let width = 8;
671        let height = 8;
672        let mut pressure = vec![0.0; width * height];
673        let mut pressure_prev = vec![0.0; width * height];
674
675        // Set initial impulse
676        pressure[3 * width + 3] = 1.0;
677
678        let c2 = 0.25;
679        let damping = 0.99;
680
681        // Run until step completes
682        let mut iterations = 0;
683        loop {
684            let result = processor.step_frame(
685                &mut pressure,
686                &mut pressure_prev,
687                width,
688                height,
689                c2,
690                damping,
691            );
692            if result.step_complete {
693                if result.should_swap {
694                    std::mem::swap(&mut pressure, &mut pressure_prev);
695                }
696                break;
697            }
698            iterations += 1;
699            assert!(
700                iterations < 100,
701                "Should complete within reasonable iterations"
702            );
703        }
704
705        // Should have processed all interior cells
706        assert!(processor.state.step_complete);
707    }
708
709    #[test]
710    fn test_row_by_row_processing() {
711        let mut processor = EducationalProcessor::new(SimulationMode::RowByRow);
712        let width = 8;
713        let height = 8;
714        let mut pressure = vec![0.0; width * height];
715        let mut pressure_prev = vec![0.0; width * height];
716
717        pressure[3 * width + 3] = 1.0;
718
719        let c2 = 0.25;
720        let damping = 0.99;
721
722        // First frame should process row 1
723        let result = processor.step_frame(
724            &mut pressure,
725            &mut pressure_prev,
726            width,
727            height,
728            c2,
729            damping,
730        );
731        assert!(!result.step_complete);
732        assert_eq!(processor.state.current_row, Some(1));
733
734        // Continue until complete
735        let mut iterations = 0;
736        loop {
737            let result = processor.step_frame(
738                &mut pressure,
739                &mut pressure_prev,
740                width,
741                height,
742                c2,
743                damping,
744            );
745            if result.step_complete {
746                break;
747            }
748            iterations += 1;
749            assert!(iterations < 20);
750        }
751    }
752
753    #[test]
754    fn test_actor_based_processing() {
755        let mut processor = EducationalProcessor::new(SimulationMode::ActorBased);
756        processor.tile_size = 4; // Small tiles for testing
757
758        let width = 16;
759        let height = 16;
760        let mut pressure = vec![0.0; width * height];
761        let mut pressure_prev = vec![0.0; width * height];
762
763        pressure[8 * width + 8] = 1.0;
764
765        let c2 = 0.25;
766        let damping = 0.99;
767
768        // First frame should process first tile
769        let result = processor.step_frame(
770            &mut pressure,
771            &mut pressure_prev,
772            width,
773            height,
774            c2,
775            damping,
776        );
777        assert!(!result.step_complete);
778        assert!(!processor.state.active_tiles.is_empty());
779
780        // Continue until complete
781        let mut iterations = 0;
782        loop {
783            let result = processor.step_frame(
784                &mut pressure,
785                &mut pressure_prev,
786                width,
787                height,
788                c2,
789                damping,
790            );
791            if result.step_complete {
792                break;
793            }
794            iterations += 1;
795            assert!(iterations < 50);
796        }
797    }
798}