Full Code of 0x23/WaveSimulator2D for AI

main 990124d7c641 cached
17 files
67.7 KB
23.9k tokens
83 symbols
1 requests
Download .txt
Repository: 0x23/WaveSimulator2D
Branch: main
Commit: 990124d7c641
Files: 17
Total size: 67.7 KB

Directory structure:
gitextract_7z9h8v6g/

├── README.md
├── requirements.txt
└── wave_sim2d/
    ├── __init__.py
    ├── develop_tests.py
    ├── examples/
    │   ├── example0.py
    │   ├── example1.py
    │   ├── example2.py
    │   ├── example3.py
    │   └── example4.py
    ├── main.py
    ├── scene_objects/
    │   ├── source.py
    │   ├── static_dampening.py
    │   ├── static_image_scene.py
    │   ├── static_refractive_index.py
    │   └── strain_refractive_index.py
    ├── wave_simulation.py
    └── wave_visualizer.py

================================================
FILE CONTENTS
================================================

================================================
FILE: README.md
================================================
# 2D Wave Simulation on the GPU

This repository contains a lightweight 2D wave simulator running on the GPU using CuPy library (probably requires a NVIDIA GPU). It can be used for 2D light and sound simulations.
A simple visualizer shows the field and its intensity on the screen and writes a movie file for each to disks. The goal is to provide a fast, easy to use but still felxible wave simulator.

<div style="display: flex;">
    <img src="images/simulation_1.jpg" alt="Example Image 1" width="49%">
    <img src="images/simulation_2.jpg" alt="Example Image 2" width="49%">
</div>

### Update 06.04.2025

* Scene objects can now draw to a visualization layer (most of them do not yet, feel free to contribute) !
* Example 4 now shows a two-mirror optical cavity and how standing waves emerge.
* Added new Line Sources
* Added Refractive index Polygon object (StaticRefractiveIndexPolygon)
* Added Refractive index Box object (StaticRefractiveIndexBox)
* Fixed some issues with the examples

<div style="display: flex;">
    <img src="images/optical_cavity.jpg" alt="Example 4 - Optical Cavity with Standing Waves" width="50%">
</div>

### Update 01.04.2024

* Refactored the code to support a more flexible scene description. A simulation scene now consists of a list of objects that add their contribution to the fields.
They can be combined to build complex and time dependent simulations. The refactoring also made the core simulation code even simpler.
* Added a few new custom colormaps that work well for wave simulations.
* Added new examples, which should make it easier to understand the usage of the program and how you can setup your own simulations: [examples](source/examples).

<div style="display: flex;">
    <img src="images/simulation_3.jpg" alt="Example Image 3" width="45%">
    <img src="images/simulation_4.jpg" alt="Example Image 4" width="45%">
</div>

The old image based scene description is still available as a scene object. You can continue to use the convenience of an image editing software and create simulations
without much programming.

###  Image Scene Decsription Usage ###

When using the 'StaticImageScene' class the simulation scenes can given as an 8Bit RGB image with the following channel semantics:
* Red:   The Refractive index times 100 (for refractive index 1.5 you would use value 150)
* Green: Each pixel with a green value above 0 is a sinusoidal wave source. The green value defines its frequency.
* Blue:  Absorbtion field. Larger values correspond to higher dampening of the waves, use graduated transitions to avoid reflections

WARNING: Do not use anti-aliasing for the green channel ! The shades produced are interpreted as different source frequencies, which yields weird results.

<div style="display: flex;">
    <img src="images/source_antialiasing.png" alt="Example Image 5" width="50%">
</div>

### Recommended Installation ###

1. Install Python and PyCharm IDE
2. Clone the Project to you hard disk
3. Open the folder as a Project using PyCharm
4. If prompted to install requirements, accept (or install requirements using pip -r requirements.txt)
5. Right click on one of the examples in wave_sim2d/examples and select run

NOTE: If you have issues installing the `cupy` library
1. Make sure you have the `nvidia-cuda-toolkit` installed. 
You can check it by running `nvcc --version`.
1. In the *requirements.txt* file, replace `cupy` by `cupy-cuda[version-number]x`. 
   Where the version number displayed when running `nvcc --version` (example: `cupy-cuda11x`).





================================================
FILE: requirements.txt
================================================
numpy
opencv-python
matplotlib
cupy

================================================
FILE: wave_sim2d/__init__.py
================================================


================================================
FILE: wave_sim2d/develop_tests.py
================================================
import wave_visualizer
import wave_visualizer as vis
import wave_simulation as sim
import numpy as np
import cv2
import math
import json
from scene_objects.static_dampening import StaticDampening
from scene_objects.static_refractive_index import StaticRefractiveIndex
from scene_objects.static_image_scene import StaticImageScene
from scene_objects.source import PointSource, ModulatorSmoothSquare, ModulatorDiscreteSignal


def build_example_scene1(scene_image):
    """
    This example uses the old image scene description. See 'StaticImageScene' for more information.
    """
    scene_objects = [StaticImageScene(scene_image)]
    return scene_objects


def build_example_scene2(width, height):
    """
    In this example, a new scene is created from scratch and a few emitters are places manually.
    One of the emitters uses an amplitude modulation object to change brightness over time
    """
    objects = []

    # Add a static dampening field without any dampending in the interior (value 1.0 means no dampening)
    # However a dampening layer at the border is added to avoid reflections (see parameter 'border thickness')
    objects.append(StaticDampening(np.ones((height, width)), 48))

    # add a constant refractive index field
    objects.append(StaticRefractiveIndex(np.full((height, width), 1.5)))

    # add a simple point source
    objects.append(PointSource(200, 250, 0.19, 5))

    # add a point source with an amplitude modulator
    amplitude_modulator = ModulatorDiscreteSignal(np.random.randint(2, size=64), 0.0006)
    objects.append(PointSource(200, 350, 0.19, 5, amp_modulator=amplitude_modulator))

    return objects


def simulate(scene_image_fn, num_iterations,
             simulation_steps_per_frame, write_videos,
             field_colormap, intensity_colormap,
             background_image_fn=None):
    # reset random number generator
    np.random.seed(0)

    # load scene image
    scene_image = cv2.cvtColor(cv2.imread(scene_image_fn), cv2.COLOR_BGR2RGB)

    background_image = None
    if background_image_fn is not None:
        background_image = cv2.imread(background_image_fn)
        background_image = cv2.resize(background_image, (scene_image.shape[1], scene_image.shape[0]))

    # create simulator and visualizer objects
    simulator = sim.WaveSimulator2D(scene_image.shape[1], scene_image.shape[0])
    visualizer = vis.WaveVisualizer(field_colormap=field_colormap, intensity_colormap=intensity_colormap)

    # build simulation scene
    simulator.scene_objects = build_example_scene2(scene_image.shape[1], scene_image.shape[0])

    # create video writers
    if write_videos:
        video_writer1 = cv2.VideoWriter('simulation_field.avi', cv2.VideoWriter_fourcc(*'FFV1'),
                                       60, (scene_image.shape[1], scene_image.shape[0]))
        video_writer2 = cv2.VideoWriter('simulation_intensity.avi', cv2.VideoWriter_fourcc(*'FFV1'),
                                       60, (scene_image.shape[1], scene_image.shape[0]))

    # run simulation
    for i in range(num_iterations):
        simulator.update_scene()
        simulator.update_field()
        visualizer.update(simulator)

        if i % simulation_steps_per_frame == 0:
            frame_int = visualizer.render_intensity(1.0)
            frame_field = visualizer.render_field(1.0)

            if background_image is not None:
                frame_int = cv2.add(background_image, frame_int)
                frame_field = cv2.add(background_image, frame_field)

           # frame_int = cv2.pyrDown(frame_int)
           # frame_field = cv2.pyrDown(frame_field)
            cv2.imshow("Wave Simulation", frame_field) #cv2.resize(frame_int, dsize=(1024, 1024)))
            cv2.waitKey(1)

            if write_videos:
                video_writer1.write(frame_field)
                video_writer2.write(frame_int)

        if i % 128 == 0:
            print(f'{int((i+1)/num_iterations*100)}%')


if __name__ == "__main__":
    print('This file contains tests for development and you may not bve able to run it without errors')
    print('Please take a look at the previded examples')

    # increase simulation_steps_per_frame to better utilize GPU
    # good colormaps for field: RdBu[invert=True], colormap_wave1, colormap_wave2, colormap_wave4, icefire
    simulate('../exxample_data/scene_lens_doubleslit.png',
             20000,
             simulation_steps_per_frame=16,
             write_videos=True,
             field_colormap=vis.get_colormap_lut('colormap_wave4', invert=False, black_level=-0.05),
#             field_colormap=vis.get_colormap_lut('RdBu', invert=True, make_symmetric=True),
             intensity_colormap=vis.get_colormap_lut('afmhot', invert=False, black_level=0.0),
             background_image_fn=None)



================================================
FILE: wave_sim2d/examples/example0.py
================================================
import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), '../'))  # noqa

import cv2
import wave_sim2d.wave_visualizer as vis
import wave_sim2d.wave_simulation as sim
from wave_sim2d.scene_objects.source import *
from wave_sim2d.scene_objects.static_refractive_index import *

def build_scene():
    """
    This example creates the simplest possible simulation using a single emitter.
    """
    width = 512
    height = 512
    objects = [PointSource(200, 256, 0.1, 5)]
    # objects.append(StaticRefractiveIndexPolygon([[400, 255], [300, 200], [300, 300]], 1.5))
    # objects = [LineSource((200, 265), (250, 105), 0.2, 0.5)]

    return objects, width, height


def main():
    # create colormaps
    field_colormap = vis.get_colormap_lut('colormap_wave1', invert=False, black_level=-0.05)
    intensity_colormap = vis.get_colormap_lut('afmhot', invert=False, black_level=0.0)

    # build simulation scene
    scene_objects, w, h = build_scene()

    # create simulator and visualizer objects
    simulator = sim.WaveSimulator2D(w, h, scene_objects)
    visualizer = vis.WaveVisualizer(field_colormap=field_colormap, intensity_colormap=intensity_colormap)

    # run simulation
    for i in range(1000):
        simulator.update_scene()
        simulator.update_field()
        visualizer.update(simulator)

        # show field
        frame_field = visualizer.render_field(1.0)
        cv2.imshow("Wave Simulation Field", frame_field)

        # show intensity
        # frame_int = visualizer.render_intensity(1.0)
        # cv2.imshow("Wave Simulation Intensity", frame_int)

        cv2.waitKey(1)


if __name__ == "__main__":
    main()



================================================
FILE: wave_sim2d/examples/example1.py
================================================
import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), '../'))  # noqa

import numpy as np
import cv2

import wave_sim2d.wave_visualizer as vis
import wave_sim2d.wave_simulation as sim
from wave_sim2d.scene_objects.static_image_scene import StaticImageScene


def build_scene(scene_image_path):
    """
    This example uses the 'old' image scene description. See 'StaticImageScene' for more information.
    """
    # load scene image
    scene_image = cv2.cvtColor(cv2.imread(scene_image_path), cv2.COLOR_BGR2RGB)

    # create the scene object list with an 'StaticImageScene' entry as the only scene object
    # more scene objects can be added to the list to build more complex scenes
    scene_objects = [StaticImageScene(scene_image, source_fequency_scale=2.0)]

    return scene_objects, scene_image.shape[1], scene_image.shape[0]


def main():
    # Set scene image path. The image encodes refractive index, dampening and emitters in its color channels
    # see 'static_image_scene.StaticImageScene' class for a more detailed description.
    # please take a look at the image to understand what is happening in the simulation
    scene_image_path = '../../example_data/scene_lens_doubleslit.png'

    # create colormaps
    field_colormap = vis.get_colormap_lut('colormap_wave1', invert=False, black_level=-0.05)
    intensity_colormap = vis.get_colormap_lut('afmhot', invert=False, black_level=0.0)

    # reset random number generator
    np.random.seed(0)

    # build simulation scene
    scene_objects, w, h = build_scene(scene_image_path)

    # create simulator and visualizer objects
    simulator = sim.WaveSimulator2D(w, h, scene_objects)
    visualizer = vis.WaveVisualizer(field_colormap=field_colormap, intensity_colormap=intensity_colormap)

    # run simulation
    for i in range(2000):
        simulator.update_scene()
        simulator.update_field()
        visualizer.update(simulator)

        # visualize very N frames
        if (i % 4) == 0:
            # show field
            frame_field = visualizer.render_field(1.0)
            cv2.imshow("Wave Simulation Field", frame_field)

            # show intensity
            # frame_int = visualizer.render_intensity(1.0)
            # cv2.imshow("Wave Simulation Intensity", frame_int)

        cv2.waitKey(1)


if __name__ == "__main__":
    main()



================================================
FILE: wave_sim2d/examples/example2.py
================================================
import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), '../'))  # noqa

import numpy as np
import cv2
import wave_sim2d.wave_visualizer as vis
import wave_sim2d.wave_simulation as sim
from wave_sim2d.scene_objects.static_dampening import StaticDampening
from wave_sim2d.scene_objects.static_refractive_index import StaticRefractiveIndex
from wave_sim2d.scene_objects.source import PointSource, ModulatorSmoothSquare


def build_scene():
    """
    In this example, a new scene is created from scratch and a few emitters are places manually.
    One of the emitters uses an amplitude modulation object to change brightness over time
    """
    width = 600
    height = 600
    objects = []

    # Add a static dampening field without any dampending in the interior (value 1.0 means no dampening)
    # However a dampening layer at the border is added to avoid reflections (see parameter 'border thickness')
    objects.append(StaticDampening(np.ones((height, width)), 32))

    # add a constant refractive index field
    objects.append(StaticRefractiveIndex(np.full((height, width), 1.5)))

    # add a simple point source
    objects.append(PointSource(200, 220, 0.2, 8))

    # add a point source with an amplitude modulator
    amplitude_modulator = ModulatorSmoothSquare(0.025, 0.0, smoothness=0.5)
    objects.append(PointSource(200, 380, 0.2, 8, amp_modulator=amplitude_modulator))

    return objects, width, height


def main():
    # create colormaps
    field_colormap = vis.get_colormap_lut('colormap_wave4', invert=False, black_level=-0.05)
    intensity_colormap = vis.get_colormap_lut('afmhot', invert=False, black_level=0.0)

    # reset random number generator
    np.random.seed(0)

    # build simulation scene
    scene_objects, w, h = build_scene()

    # create simulator and visualizer objects
    simulator = sim.WaveSimulator2D(w, h, scene_objects)
    visualizer = vis.WaveVisualizer(field_colormap=field_colormap, intensity_colormap=intensity_colormap)

    # run simulation
    for i in range(2000):
        simulator.update_scene()
        simulator.update_field()
        visualizer.update(simulator)

        # visualize very N frames
        if (i % 2) == 0:
            # show field
            frame_field = visualizer.render_field(1.0)
            cv2.imshow("Wave Simulation Field", frame_field)

            # show intensity
            # frame_int = visualizer.render_intensity(1.0)
            # cv2.imshow("Wave Simulation Intensity", frame_int)

        cv2.waitKey(1)


if __name__ == "__main__":
    main()



================================================
FILE: wave_sim2d/examples/example3.py
================================================
import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), '../'))  # noqa

import numpy as np
import cupy as cp
import math
import cv2

import wave_sim2d.wave_visualizer as vis
import wave_sim2d.wave_simulation as sim
from wave_sim2d.scene_objects.static_dampening import StaticDampening
from wave_sim2d.scene_objects.static_refractive_index import StaticRefractiveIndex


def gaussian_kernel(size, sigma):
    """
    creates gaussian kernel with side length `l` and a sigma of `sig`
    """
    ax = np.linspace(-(size - 1) / 2., (size - 1) / 2., size)
    gauss = np.exp(-0.5 * np.square(ax) / np.square(sigma))
    kernel = np.outer(gauss, gauss)
    return kernel / np.sum(kernel)


class MovingCharge(sim.SceneObject):
    """
    Implements a point source scene object. The amplitude can be optionally modulated using a modulator object.
    :param x: center position x.
    :param y: center position y.
    :param frequency: motion frequency
    :param amplitude: motion amplitude
    """
    def __init__(self, x, y, frequency, amplitude):
        self.x = x
        self.y = y
        self.frequency = frequency
        self.amplitude = amplitude
        self.size = 11

        # create a smooth source shape
        self.source_array = cp.array(gaussian_kernel(self.size, self.size/3))

    def render(self, field, wave_speed_field, dampening_field):
        # no changes to the refractive index or dampening field required for this class
        pass

    def render_visualization(self, image: np.ndarray):
        pass

    def update_field(self, field, t):
        fade_in = math.sin(min(t*0.1, math.pi/2))

        # write the moving charge to the field
        x = self.x + math.sin(self.frequency * t*0.05)*200
        y = self.y + math.sin(self.frequency * t)*self.amplitude

        # copy source shape to current position into field
        wh = self.source_array.shape[1]//2
        hh = self.source_array.shape[0]//2
        field[y-hh:y+hh+1, x-wh:x+wh+1] += self.source_array * fade_in * 0.25


def build_scene():
    """
    In this example, a custom scene object is implemented and used to simulate a moving field disturbance.
    """
    width = 600
    height = 600
    objects = []

    # Add a static dampening field without any dampending in the interior (value 1.0 means no dampening)
    # However a dampening layer at the border is added to avoid reflections (see parameter 'border thickness')
    objects.append(StaticDampening(np.ones((height, width)), 64))

    # add a constant refractive index field
    objects.append(StaticRefractiveIndex(np.full((height, width), 1.5)))

    # add a simple point source
    objects.append(MovingCharge(300, 300, 0.1, 10))

    return objects, width, height


def main():
    # create colormaps
    field_colormap = vis.get_colormap_lut('colormap_wave1', invert=False, black_level=-0.05)
    intensity_colormap = vis.get_colormap_lut('afmhot', invert=False, black_level=0.0)

    # reset random number generator
    np.random.seed(0)

    # build simulation scene
    scene_objects, w, h = build_scene()

    # create simulator and visualizer objects
    simulator = sim.WaveSimulator2D(w, h, scene_objects)
    visualizer = vis.WaveVisualizer(field_colormap=field_colormap, intensity_colormap=intensity_colormap)

    # run simulation
    for i in range(8000):
        simulator.update_scene()
        simulator.update_field()
        visualizer.update(simulator)

        # visualize very N frames
        if (i % 2) == 0:
            # show field
            frame_field = visualizer.render_field(1.0)
            cv2.imshow("Wave Simulation Field", frame_field)

            # show intensity
            # frame_int = visualizer.render_intensity(1.0)
            # cv2.imshow("Wave Simulation Intensity", frame_int)

        cv2.waitKey(1)


if __name__ == "__main__":
    main()



================================================
FILE: wave_sim2d/examples/example4.py
================================================
import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), '../'))  # noqa

import cv2
import numpy as np
import cupy as cp
import wave_sim2d.wave_visualizer as vis
import wave_sim2d.wave_simulation as sim
from wave_sim2d.scene_objects.source import *
from wave_sim2d.scene_objects.static_refractive_index import *
from wave_sim2d.scene_objects.static_dampening import *


def build_scene():
    """
    This example creates fabry pirot cavity and shows the standing waves
    """
    width = 768
    height = 512
    objects = []

    # Add a static dampening field without any dampening in the interior (value 1.0 means no dampening)
    # However a dampening layer at the border is added to avoid reflections (see parameter 'border thickness')
    objects.append(StaticDampening(np.ones((height, width)), 48))

    # add nonlinear refractive index field
    objects.append(StaticRefractiveIndexBox((50, height//2), (50, int(height*0.8)), 0.0, 100.0))
    objects.append(StaticRefractiveIndexBox((width-180, height//2), (40, int(height*0.8)), 0.0, 10.0))

    # add a point source with an amplitude modulator
    # objects.append(LineSource((77, height//2-140), (77, height//2+140), 0.0215, amplitude=0.5))
    objects.append(LineSource((77, height//2-140), (77, height//2+140), 0.1003, amplitude=0.3))

    return objects, width, height


def show_field(field, brightness_scale):
    gray = (cp.clip(field*brightness_scale, -1.0, 1.0) * 127 + 127).astype(np.uint8)
    img = gray.get()
    cv2.imshow("Strain Simulation Field", cv2.cvtColor(img, cv2.COLOR_RGB2BGR))


def main():
    write_videos = False
    write_video_frame_every = 2

    # create colormaps
    field_colormap = vis.get_colormap_lut('colormap_wave1', invert=False, black_level=-0.05)
    intensity_colormap = vis.get_colormap_lut('afmhot', invert=False, black_level=0.0)

    # build simulation scene
    scene_objects, w, h = build_scene()

    # create simulator and visualizer objects
    simulator = sim.WaveSimulator2D(w, h, scene_objects)
    visualizer = vis.WaveVisualizer(field_colormap=field_colormap, intensity_colormap=intensity_colormap)

    # optional create video writers
    if write_videos:
        video_writer1 = cv2.VideoWriter('simulation_field.avi', cv2.VideoWriter_fourcc(*'FFV1'), 60, (w, h))
        video_writer2 = cv2.VideoWriter('simulation_intensity.avi', cv2.VideoWriter_fourcc(*'FFV1'), 60, (w, h))

    # run simulation
    for i in range(100000):
        simulator.update_scene()
        simulator.update_field()

        visualizer.update(simulator)
        # show field
        frame_field = visualizer.render_field(1.0)
        cv2.imshow("Wave Simulation Field", frame_field)

        # show intensity
        frame_int = visualizer.render_intensity(1.0)
        # cv2.imshow("Wave Simulation Intensity", frame_int)

        if write_videos and (i % write_video_frame_every) == 0:
            video_writer1.write(frame_field)
            video_writer2.write(frame_int)

        cv2.waitKey(1)


if __name__ == "__main__":
    main()



================================================
FILE: wave_sim2d/main.py
================================================
if __name__ == "__main__":
  print('please run one of the examples from the source/example folder...')


================================================
FILE: wave_sim2d/scene_objects/source.py
================================================
from wave_sim2d.wave_simulation import SceneObject
import cupy as cp
import numpy as np
import math


class PointSource(SceneObject):
    """
    Implements a point source scene object. The amplitude can be optionally modulated using a modulator object.
    :param x: source position x.
    :param y: source position y.
    :param frequency: emitting frequency.
    :param amplitude: emitting amplitude, not used when an amplitude modulator is given
    :param phase: emitter phase
    :param amp_modulator: optional amplitude modulator. This can be used to change the amplitude of the source
                          over time.
    """
    def __init__(self, x, y, frequency, amplitude=1.0, phase=0, amp_modulator=None):
        self.x = x
        self.y = y
        self.frequency = frequency
        self.amplitude = amplitude
        self.phase = phase
        self.amplitude_modulator = amp_modulator

    def set_amplitude_modulator(self, func):
        self.amplitude_modulator = func

    def render(self, field: cp.ndarray, wave_speed_field: cp.ndarray, dampening_field: cp.ndarray):
        pass

    def update_field(self, field, t):
        if self.amplitude_modulator is not None:
            amplitude = self.amplitude_modulator(t) * self.amplitude
        else:
            amplitude = self.amplitude

        v = cp.sin(self.phase + self.frequency * t) * amplitude
        field[self.y, self.x] = v

    def render_visualization(self, image: np.ndarray):
        """ renders a visualization of the scene object to the image """
        pass


class LineSource(SceneObject):
    """
    Implements a line source scene object. The amplitude can be optionally modulated using a modulator object.
    The source emits along a line defined by a start and end point.
    :param start: starting (x, y) coordinates of the line as a tuple.
    :param end: ending (x, y) coordinates of the line as a tuple.
    :param frequency: emitting frequency.
    :param amplitude: emitting amplitude, not used when an amplitude modulator is given
    :param phase: emitter phase
    :param amp_modulator: optional amplitude modulator. This can be used to change the amplitude of the source
                          over time.
    """
    def __init__(self, start, end, frequency, amplitude=1.0, phase=0, amp_modulator=None):
        self.start = start
        self.end = end
        self.frequency = frequency
        self.amplitude = amplitude
        self.phase = phase
        self.amplitude_modulator = amp_modulator

    def set_amplitude_modulator(self, func):
        self.amplitude_modulator = func

    def render(self, field: cp.ndarray, wave_speed_field: cp.ndarray, dampening_field: cp.ndarray):
        pass

    def update_field(self, field, t):
        if self.amplitude_modulator is not None:
            amplitude = self.amplitude_modulator(t) * self.amplitude
        else:
            amplitude = self.amplitude

        v = cp.sin(self.phase + self.frequency * t) * amplitude

        # Determine the points along the line using NumPy
        x1, y1 = self.start
        x2, y2 = self.end

        distance = np.sqrt((x2 - x1)**2 + (y2 - y1)**2)
        num_points = int(distance) + 1

        if num_points > 0:
            x_coords = cp.linspace(x1, x2, num_points).round().astype(int)
            y_coords = cp.linspace(y1, y2, num_points).round().astype(int)

            # Create boolean masks for valid indices
            valid_x = (x_coords >= 0) & (x_coords < field.shape[1])
            valid_y = (y_coords >= 0) & (y_coords < field.shape[0])
            valid_indices = valid_x & valid_y

            # Use these valid indices to update the field directly
            valid_y_coords = y_coords[valid_indices]
            valid_x_coords = x_coords[valid_indices]
            field[valid_y_coords, valid_x_coords] = v

    def render_visualization(self, image: np.ndarray):
        """ renders a visualization of the scene object to the image """
        pass

# --- Modulators -------------------------------------------------------------------------------------------------------


class ModulatorSmoothSquare:
    """
    A modulator that creates a smoothed square wave
    """
    def __init__(self, frequency, phase, smoothness=0.5):
        self.frequency = frequency
        self.phase = phase
        self.smoothness = min(max(smoothness, 1e-4), 1.0)

    def __call__(self, t):
        s = math.pow(self.smoothness, 4.0)
        a = (0.5 / math.atan(1.0/s)) * math.atan(math.sin(t * self.frequency + self.phase) / s)+0.5
        return a


class ModulatorDiscreteSignal:
    """
    A modulator that creates a smoothed binary signal
    """
    def __init__(self, signal_array, time_factor, transition_slope=8.0):
        self.signal_array = signal_array
        self.time_factor = time_factor
        self.transition_slope = transition_slope

    def __call__(self, t):
        def smooth_step(t):
            return t * t * (3 - 2 * t)

        # Wrap around the position if it's outside the array range
        sl = len(self.signal_array)
        t = math.fmod(t*self.time_factor, sl)

        # Find the indices of the neighboring values
        index_low = int(t)
        index_high = (index_low + 1) % sl

        # Calculate the interpolation factor
        tf = (t - index_low)
        tf = max(0.0, min(1.0, (tf-0.5)*self.transition_slope+0.5))

        # Use smooth step to interpolate between neighboring values
        l = smooth_step(tf)
        interpolated_value = (1 - l) * self.signal_array[index_low] + l * self.signal_array[index_high]

        return interpolated_value


================================================
FILE: wave_sim2d/scene_objects/static_dampening.py
================================================
from wave_sim2d.wave_simulation import SceneObject
import cupy as cp
import numpy as np


class StaticDampening(SceneObject):
    """
    Implements a static dampening field that overwrites the entire domain.
    Therefore, us this as base layer in your scene.
    """

    def __init__(self, dampening_field, border_thickness):
        """
        Creates a static dampening field object
        @param dampening_field: A NxM array with dampening factors (1.0 equals no dampening) of the same size as the simulation domain.
        @param pml_thickness: Thickness of the Perfectly Matched Layer (PML) at the boundaries to prevent reflections.
        """
        w = dampening_field.shape[1]
        h = dampening_field.shape[0]
        self.d = cp.ones((h, w), dtype=cp.float32)
        self.d = cp.clip(cp.array(dampening_field), 0.0, 1.0)

        # apply border dampening
        for i in range(border_thickness):
            v = (i / border_thickness) ** 0.5
            self.d[i, i:w - i] = v
            self.d[-(1 + i), i:w - i] = v
            self.d[i:h - i, i] = v
            self.d[i:h - i, -(1 + i)] = v

    def render(self, field: cp.ndarray, wave_speed_field: cp.ndarray, dampening_field: cp.ndarray):
        assert (dampening_field.shape == self.d.shape)

        # overwrite existing dampening field
        dampening_field[:] = self.d

    def update_field(self, field: cp.ndarray, t):
        pass

    def render_visualization(self, image: np.ndarray):
        """ renders a visualization of the scene object to the image """
        pass


================================================
FILE: wave_sim2d/scene_objects/static_image_scene.py
================================================
from wave_sim2d.wave_simulation import SceneObject

import numpy as np
import cupy as cp
from wave_sim2d.scene_objects.static_dampening import StaticDampening
from wave_sim2d.scene_objects.static_refractive_index import StaticRefractiveIndex


class StaticImageScene(SceneObject):
    """
    Implements static scene, where the RGB channels of the input image encode the refractive index, the dampening and sources.
    This class allows to use an image editor to create scenes.
    """
    def __init__(self, scene_image, source_amplitude=1.0, source_fequency_scale=1.0):
        """
        load source from an image description
        The simulation scenes are given as an 8Bit RGB image with the following channel semantics:
            * Red:   The Refractive index times 100 (for refractive index 1.5 you would use value 150)
            * Green: Each pixel with a green value above 0 is a sinusoidal wave source. The green value
                     defines its frequency. WARNING: Do not use antialiasing for the green channel !
            * Blue:  Absorbtion field. Larger values correspond to higher dampening of the waves,
                     use graduated transitions to avoid reflections
        """
        # Set the opacity of source pixels to incoming waves. If the opacity is 0.0
        # the field will be completely overwritten by the source term
        # a nonzero value (e.g 0.5) allows for antialiasing of sources to work
        self.source_opacity = 0.9

        # set refractive index field
        self.refractive_index = StaticRefractiveIndex(scene_image[:, :, 0] / 100)

        # set absorber field
        self.dampening = StaticDampening(1.0 - scene_image[:, :, 2] / 255, border_thickness=48)

        # set sources, each entry describes a source with the following parameters:
        # (x, y, phase, amplitude, frequency)
        sources_pos = np.flip(np.argwhere(scene_image[:, :, 1] > 0), axis=1)
        phase_amplitude_freq = np.tile(np.array([0, source_amplitude, 0.3]), (sources_pos.shape[0], 1))
        self.sources = np.concatenate((sources_pos, phase_amplitude_freq), axis=1)

        # set source frequency to channel value
        self.sources[:, 4] = scene_image[sources_pos[:, 1], sources_pos[:, 0], 1] / 255 * 0.5 * source_fequency_scale
        self.sources = cp.array(self.sources).astype(cp.float32)

    def render(self, field: cp.ndarray, wave_speed_field: cp.ndarray, dampening_field: cp.ndarray):
        """
        render the stat
        """
        self.dampening.render(field, wave_speed_field, dampening_field)
        self.refractive_index.render(field, wave_speed_field, dampening_field)

    def update_field(self, field: cp.ndarray, t):
        # Update the sources in the simulation field based on their properties.
        v = cp.sin(self.sources[:, 2]+self.sources[:, 4]*t)*self.sources[:, 3]
        coords = self.sources[:, 0:2].astype(cp.int32)

        o = self.source_opacity
        field[coords[:, 1], coords[:, 0]] = field[coords[:, 1], coords[:, 0]]*o + v*(1.0-o)

    def render_visualization(self, image: np.ndarray):
        """ renders a visualization of the scene object to the image """
        pass


================================================
FILE: wave_sim2d/scene_objects/static_refractive_index.py
================================================
from wave_sim2d.wave_simulation import SceneObject
import cupy as cp
import numpy as np
import cv2


class StaticRefractiveIndex(SceneObject):
    """
    Implements a static refractive index field that overwrites the entire domain with a constant IOR value.
    Use this as base layer in your scene.
    """

    def __init__(self, refractive_index_field):
        """
        Creates a static refractive index field object
        :param refractive_index_field: The refractive index field, same size as the source.
                                       Note that values below 0.9 are clipped to prevent the simulation
                                       from becoming instable
        """
        shape = refractive_index_field.shape
        self.c = cp.ones((shape[0], shape[1]), dtype=cp.float32)
        self.c = 1.0/cp.clip(cp.array(refractive_index_field), 0.9, 10.0)

    def render(self, field: cp.ndarray, wave_speed_field: cp.ndarray, dampening_field: cp.ndarray):
        assert (wave_speed_field.shape == self.c.shape)
        wave_speed_field[:] = self.c

    def update_field(self, field: cp.ndarray, t):
        pass

    def render_visualization(self, image: np.ndarray):
        """ renders a visualization of the scene object to the image """
        pass


class StaticRefractiveIndexPolygon(SceneObject):
    """
    Draws a static polygon with a given refractive index into the wave_speed_field using an
    anti-aliased mask and indexing. Caches the pixel coordinates and mask values.
    """

    def __init__(self, vertices, refractive_index):
        """
        Initializes the StaticRefractiveIndexPolygon.

        Args:
            vertices (list or np.ndarray): A list or array of (x, y) coordinates defining the polygon.
            refractive_index (float): The refractive index of the polygon. Values are clamped to [0.9, 10.0].
        """
        self.vertices = np.array(vertices, dtype=np.float32)
        self.refractive_index = min(max(refractive_index, 0.9), 10.0)
        self._cached_coords = None
        self._cached_mask_values = None
        self._cached_field_shape = (0, 0)

    def _create_polygon_data(self, field_shape):
        """
        Creates and caches the pixel coordinates and anti-aliased mask values for the polygon.

        Args:
            field_shape (tuple): The shape (rows, cols) of the simulation field.

        Returns:
            tuple: A tuple containing:
                - coords (tuple of cp.ndarray): (y_coordinates, x_coordinates) of the polygon pixels within the field.
                - mask_values (cp.ndarray): Corresponding anti-aliased mask values (0.0 to 1.0).
        """
        if self._cached_coords is not None and self._cached_field_shape == field_shape:
            return self._cached_coords, self._cached_mask_values

        rows, cols = field_shape

        # Find the bounding box of the polygon
        min_x = np.min(self.vertices[:, 0])
        max_x = np.max(self.vertices[:, 0])
        min_y = np.min(self.vertices[:, 1])
        max_y = np.max(self.vertices[:, 1])

        mask_width = int(np.ceil(max_x - min_x)) + 1
        mask_height = int(np.ceil(max_y - min_y)) + 1
        offset_x = int(np.floor(min_x))
        offset_y = int(np.floor(min_y))

        # Create the mask
        mask = np.zeros((mask_height, mask_width), dtype=np.float32)
        translated_vertices = self.vertices - [offset_x, offset_y]
        translated_vertices_cv = np.round(translated_vertices).astype(np.int32)
        cv2.fillPoly(mask, [translated_vertices_cv], 1.0, lineType=cv2.LINE_AA)

        # Get coordinates and mask values of non-black pixels
        coords_y, coords_x = np.where(mask > 0)
        mask_values = mask[coords_y, coords_x]

        # Adjust coordinates to the position in the main field
        global_coords_y = coords_y + offset_y
        global_coords_x = coords_x + offset_x

        # Perform out-of-bounds check here
        in_bounds = (global_coords_y >= 0) & (global_coords_y < rows) & \
                    (global_coords_x >= 0) & (global_coords_x < cols)

        valid_global_y = global_coords_y[in_bounds]
        valid_global_x = global_coords_x[in_bounds]
        valid_mask_values = mask_values[in_bounds]

        self._cached_coords = (cp.array(valid_global_y), cp.array(valid_global_x))
        self._cached_mask_values = cp.array(valid_mask_values, dtype=cp.float32)
        self._cached_field_shape = field_shape
        return self._cached_coords, self._cached_mask_values

    def render(self, field: cp.ndarray, wave_speed_field: cp.ndarray, dampening_field: cp.ndarray):
        coords, mask_values = self._create_polygon_data(wave_speed_field.shape)

        # Use advanced indexing to update the field and perform alpha blending
        bg_wave_speed = wave_speed_field[coords[0], coords[1]]
        wave_speed_field[coords[0], coords[1]] = (bg_wave_speed * (1.0 - mask_values) +
                                                  mask_values / self.refractive_index)

    def update_field(self, field: cp.ndarray, t):
        pass

    def render_visualization(self, image: np.ndarray):
        vertices = np.round(self.vertices).astype(np.int32)
        cv2.fillPoly(image, [vertices], (60, 60, 60), lineType=cv2.LINE_AA)


class StaticRefractiveIndexBox(StaticRefractiveIndexPolygon):
    """
    Draws a static rotated box with a given refractive index into the wave_speed_field by
    inheriting from StaticRefractiveIndexPolygon.
    """

    def __init__(self, center: tuple, box_size: tuple, box_angle_rad: float, refractive_index: float):
        """
        Initializes the StaticRefractiveIndexBox.

        Args:
            center (tuple): A tuple (center_x, center_y) representing the box's center.
            box_size (tuple): A tuple (width, height) representing the box's dimensions.
            box_angle_rad (float): The rotation angle of the box in radians (counter-clockwise).
            refractive_index (float): The refractive index of the box. Values are clamped to [0.9, 10.0].
        """
        self.center = center
        self.box_size = box_size
        self.box_angle_rad = box_angle_rad
        refractive_index = min(max(refractive_index, 0.9), 10.0)

        # Unpack center and box size
        center_x, center_y = self.center
        width, height = self.box_size

        # Calculate the vertices of the rotated box
        half_width = width / 2
        half_height = height / 2
        local_vertices = np.array([[-half_width, -half_height],
                                   [half_width, -half_height],
                                   [half_width, half_height],
                                   [-half_width, half_height]], dtype=np.float32)

        # Create the rotation matrix
        rotation_matrix = cv2.getRotationMatrix2D((0, 0), np.rad2deg(self.box_angle_rad), 1)

        # Rotate the local vertices
        rotated_vertices = cv2.transform(np.array([local_vertices]), rotation_matrix)[0]

        # Translate the rotated vertices to the center
        translated_vertices = rotated_vertices + [center_x, center_y]

        # Initialize the parent class (StaticRefractiveIndexPolygon) with the vertices
        super().__init__(translated_vertices, refractive_index)


================================================
FILE: wave_sim2d/scene_objects/strain_refractive_index.py
================================================
from wave_sim2d.wave_simulation import SceneObject
import cupy as cp
import cupyx.scipy.signal
import numpy as np

class StrainRefractiveIndex(SceneObject):
    """
    Implements a dynamic refractive index field that linearly depends on the strain of the current field.
    The refractive index within the entire domain is overwritten
    """

    def __init__(self, refractive_index_offset, coupling_constant):
        """
        Creates a strain refractive index field object
        :param coupling_constant: coupling constant between the strain and the refractive index
        """
        self.coupling_constant = coupling_constant
        self.refractive_index_offset = refractive_index_offset

        self.du_dx_kernel = cp.array([[-1, 0.0, 1]])
        self.du_dy_kernel = cp.array([[-1], [0.0], [1]])

        self.strain_field = None

    def render(self, field: cp.ndarray, wave_speed_field: cp.ndarray, dampening_field: cp.ndarray):
        # compute strain
        du_dx = cupyx.scipy.signal.convolve2d(field, self.du_dx_kernel, mode='same', boundary='fill')
        du_dy = cupyx.scipy.signal.convolve2d(field, self.du_dy_kernel, mode='same', boundary='fill')

        self.strain_field = cp.sqrt(du_dx**2 + du_dy**2)

        # compute refractive index from strain
        refractive_index_field = self.refractive_index_offset + self.strain_field*self.coupling_constant

        # assign wave speed using refractive index from above
        wave_speed_field[:] = 1.0/cp.clip(cp.array(refractive_index_field), 0.9, 10.0)

    def update_field(self, field: cp.ndarray, t):
        pass

    def render_visualization(self, image: np.ndarray):
        """ renders a visualization of the scene object to the image """
        pass


================================================
FILE: wave_sim2d/wave_simulation.py
================================================
import cupy
import numpy as np
import cupy as cp
import cupyx.scipy.signal
from abc import ABC, abstractmethod


class SceneObject(ABC):
    """
    Interface for simulation scene objects. A scene object is anything defining or modifying the simulation scene.
    For example: Light sources, Absorbers or regions with specific refractive index. Scene objects can change the
    simulated field and draw their contribution to the wave speed field and dampening field each frame """

    @abstractmethod
    def render(self, field: cupy.ndarray, wave_speed_field: cupy.ndarray, dampening_field: cupy.ndarray):
        """ renders the scene objects contribution to the wave speed field and dampening field """
        pass

    @abstractmethod
    def update_field(self, field: cupy.ndarray, t):
        """ performs updates to the field itself, e.g. for adding sources """
        pass

    @abstractmethod
    def render_visualization(self, image: np.ndarray):
        """ renders a visualization of the scene object to the image """
        pass


class WaveSimulator2D:
    """
    Simulates the 2D wave equation
    The system assumes units, where the wave speed is 1.0 pixel/timestep
    source frequency should be adjusted accordingly
    """
    def __init__(self, w, h, scene_objects, initial_field=None):
        """
        Initialize the 2D wave simulator.
        @param w: Width of the simulation grid.
        @param h: Height of the simulation grid.
        """
        self.global_dampening = 1.0
        self.c = cp.ones((h, w), dtype=cp.float32)                      # wave speed field (from refractive indices)
        self.d = cp.ones((h, w), dtype=cp.float32)                      # dampening field
        self.u = cp.zeros((h, w), dtype=cp.float32)                     # field values
        self.u_prev = cp.zeros((h, w), dtype=cp.float32)                # field values of prev frame

        if initial_field is not None:
            assert w == initial_field.shape[1] and h == initial_field.shape[2], 'width/height of initial field invalid'
            self.u[:] = initial_field
            self.u_prev[:] = initial_field

        # Define Laplacian kernel
        self.laplacian_kernel = cp.array([[0.066, 0.184, 0.066],
                                          [0.184, -1.0, 0.184],
                                          [0.066, 0.184, 0.066]])

        # self.laplacian_kernel = cp.array([[0.05, 0.2, 0.05],
        #                           [0.2, -1.0, 0.2],
        #                           [0.05, 0.2, 0.05]])

        # self.laplacian_kernel = cp.array([[0.103, 0.147, 0.103],
        #                                   [0.147, -1.0, 0.147],
        #                                   [0.103, 0.147, 0.103]])

        self.t = 0
        self.dt = 1.0

        self.scene_objects = scene_objects if scene_objects is not None else []

    def reset_time(self):
        """
        Reset the simulation time to zero.
        """
        self.t = 0.0

    def update_field(self):
        """
        Update the simulation field based on the wave equation.
        """
        # calculate laplacian using convolution
        laplacian = cupyx.scipy.signal.convolve2d(self.u, self.laplacian_kernel, mode='same', boundary='fill')

        # update field
        v = (self.u - self.u_prev) * self.d * self.global_dampening
        r = (self.u + v + laplacian * (self.c * self.dt)**2)

        self.u_prev[:] = self.u
        self.u[:] = r

        self.t += self.dt

    def update_scene(self):
        # clear wave speed field and dampening field
        self.c.fill(1.0)
        self.d.fill(1.0)

        for obj in self.scene_objects:
            obj.render(self.u, self.c, self.d)

        for obj in self.scene_objects:
            obj.update_field(self.u, self.t)

    def get_field(self):
        """
        Get the current state of the simulation field.
        @return: A 2D array representing the simulation field.
        """
        return self.u

    def render_visualization(self, image=None):
        # clear wave speed field and dampening field
        if image is None:
            image = np.zeros((self.c.shape[0], self.c.shape[1], 3), dtype=np.uint8)

        for obj in self.scene_objects:
            obj.render_visualization(image)

        return image




================================================
FILE: wave_sim2d/wave_visualizer.py
================================================
import numpy as np
import cupy as cp
import cv2
import matplotlib.pyplot

colormap_icefire = [[179, 224, 216], [178, 223, 216], [176, 222, 215], [175, 221, 215], [173, 219, 214], [171, 218, 214], [169, 217, 214], [167, 215, 213], [165, 214, 213], [162, 212, 212], [160, 210, 212], [157, 209, 211], [154, 207, 211], [151, 205, 210], [148, 203, 210], [146, 201, 209], [143, 199, 209], [140, 198, 208], [137, 196, 208], [134, 194, 208], [131, 192, 207], [128, 190, 207], [125, 188, 207], [122, 187, 207], [119, 185, 206], [116, 183, 206], [113, 181, 206], [110, 179, 206], [108, 177, 206], [105, 176, 205], [102, 174, 205], [99, 172, 205], [97, 170, 205], [94, 168, 205], [91, 166, 205], [89, 164, 205], [86, 162, 205], [84, 161, 205], [82, 159, 205], [79, 157, 205], [77, 155, 205], [75, 153, 206], [73, 151, 206], [71, 149, 206], [69, 147, 206], [68, 145, 206], [66, 143, 206], [65, 140, 206], [64, 138, 206], [63, 136, 206], [62, 134, 206], [61, 132, 206], [61, 130, 205], [61, 127, 205], [60, 125, 205], [60, 123, 204], [60, 121, 203], [60, 118, 203], [61, 116, 202], [61, 114, 201], [61, 112, 200], [62, 109, 198], [62, 107, 197], [63, 105, 195], [64, 103, 194], [65, 100, 192], [65, 98, 190], [66, 96, 187], [67, 94, 185], [67, 92, 183], [68, 90, 180], [68, 88, 177], [69, 86, 174], [69, 85, 171], [69, 83, 168], [70, 81, 165], [70, 79, 162], [70, 78, 158], [69, 76, 155], [69, 75, 151], [69, 73, 148], [68, 72, 144], [68, 70, 141], [67, 69, 137], [66, 67, 134], [66, 66, 130], [65, 65, 127], [64, 63, 123], [63, 62, 120], [62, 61, 116], [61, 60, 113], [60, 59, 109], [59, 57, 106], [58, 56, 103], [57, 55, 99], [55, 54, 96], [54, 53, 93], [53, 52, 90], [52, 50, 87], [51, 49, 84], [50, 48, 81], [48, 47, 78], [47, 46, 75], [46, 45, 72], [45, 44, 70], [44, 43, 67], [43, 42, 65], [42, 41, 62], [41, 40, 60], [40, 39, 57], [39, 38, 55], [38, 37, 53], [37, 37, 51], [37, 36, 49], [36, 35, 47], [35, 35, 45], [35, 34, 44], [34, 33, 42], [34, 33, 41], [33, 32, 39], [33, 32, 38], [33, 32, 37], [33, 31, 36], [33, 31, 35], [33, 31, 35], [34, 30, 34], [34, 30, 33], [34, 30, 33], [35, 30, 32], [36, 30, 32], [36, 30, 32], [37, 30, 32], [38, 30, 32], [39, 30, 32], [40, 30, 32], [41, 30, 32], [42, 30, 33], [44, 31, 33], [46, 31, 34], [47, 31, 34], [49, 31, 35], [51, 32, 35], [53, 32, 36], [55, 32, 37], [57, 33, 38], [59, 33, 38], [61, 33, 39], [63, 34, 40], [65, 34, 41], [67, 35, 42], [70, 35, 43], [72, 36, 44], [74, 36, 45], [77, 37, 46], [79, 37, 47], [82, 38, 48], [84, 38, 49], [87, 39, 50], [90, 39, 51], [92, 40, 52], [95, 40, 53], [98, 40, 54], [100, 41, 55], [103, 41, 56], [106, 42, 57], [109, 42, 58], [111, 42, 59], [114, 43, 60], [117, 43, 60], [120, 43, 61], [123, 44, 62], [126, 44, 63], [129, 44, 63], [131, 44, 64], [134, 45, 64], [137, 45, 65], [140, 45, 65], [143, 46, 65], [146, 46, 65], [149, 46, 66], [152, 47, 66], [155, 47, 66], [158, 48, 66], [160, 48, 66], [163, 49, 65], [166, 49, 65], [169, 50, 65], [172, 51, 64], [174, 52, 64], [177, 53, 63], [180, 54, 63], [182, 55, 62], [185, 56, 62], [187, 57, 61], [190, 58, 61], [192, 60, 60], [195, 61, 59], [197, 63, 59], [199, 65, 58], [201, 66, 57], [203, 68, 57], [206, 70, 56], [208, 72, 55], [209, 74, 55], [211, 76, 54], [213, 78, 54], [215, 81, 54], [217, 83, 53], [218, 85, 53], [220, 88, 53], [221, 90, 53], [223, 93, 54], [224, 95, 54], [225, 98, 55], [227, 101, 55], [228, 103, 56], [229, 106, 57], [230, 109, 58], [231, 111, 60], [232, 114, 61], [233, 117, 62], [234, 120, 64], [235, 123, 66], [236, 125, 68], [237, 128, 70], [237, 131, 73], [238, 134, 75], [239, 137, 78], [240, 139, 80], [240, 142, 83], [241, 145, 86], [242, 148, 89], [242, 151, 93], [243, 153, 96], [243, 156, 99], [244, 159, 103], [245, 162, 106], [245, 165, 110], [246, 167, 113], [246, 170, 117], [247, 173, 120], [247, 176, 124], [248, 178, 127], [248, 181, 131], [249, 184, 134], [249, 186, 138], [250, 188, 141], [250, 190, 144], [251, 192, 147], [251, 194, 149], [251, 196, 152], [252, 198, 154], [252, 200, 156], [252, 201, 158], [253, 203, 160]]
colormap_wave1 = [[255, 255, 255], [254, 254, 253], [254, 253, 252], [253, 252, 250], [253, 250, 248], [252, 249, 246], [252, 248, 244], [251, 246, 242], [251, 245, 240], [250, 243, 237], [250, 242, 235], [249, 240, 232], [248, 238, 230], [248, 237, 227], [247, 235, 224], [247, 233, 221], [246, 231, 218], [245, 229, 215], [245, 227, 212], [244, 225, 209], [243, 223, 206], [242, 221, 203], [242, 219, 200], [241, 217, 196], [240, 215, 193], [239, 213, 190], [239, 211, 186], [238, 208, 183], [237, 206, 179], [236, 204, 176], [235, 202, 172], [234, 199, 169], [233, 197, 165], [232, 195, 162], [231, 192, 158], [230, 190, 155], [230, 188, 151], [228, 185, 148], [227, 183, 144], [226, 181, 141], [225, 178, 137], [224, 176, 134], [223, 174, 130], [222, 171, 127], [221, 169, 124], [219, 167, 120], [218, 164, 117], [217, 162, 114], [216, 160, 111], [214, 157, 108], [213, 155, 105], [212, 153, 102], [210, 151, 99], [209, 149, 96], [207, 146, 93], [206, 144, 91], [204, 142, 88], [203, 140, 85], [201, 138, 83], [199, 136, 80], [198, 134, 78], [196, 132, 76], [194, 130, 74], [193, 128, 72], [191, 127, 70], [189, 125, 69], [187, 123, 67], [185, 121, 65], [183, 119, 63], [180, 117, 62], [178, 115, 60], [176, 113, 58], [173, 111, 56], [170, 109, 55], [168, 107, 53], [165, 105, 52], [162, 102, 50], [159, 100, 49], [156, 98, 47], [154, 96, 46], [151, 94, 44], [148, 92, 43], [144, 90, 41], [141, 87, 40], [138, 85, 39], [135, 83, 37], [132, 81, 36], [129, 79, 35], [125, 76, 34], [122, 74, 33], [119, 72, 31], [115, 70, 30], [112, 68, 29], [109, 66, 28], [105, 64, 27], [102, 62, 27], [99, 60, 26], [96, 58, 25], [93, 56, 25], [89, 54, 25], [86, 52, 25], [83, 51, 25], [80, 49, 25], [77, 47, 25], [74, 45, 25], [71, 44, 25], [68, 42, 25], [65, 41, 25], [62, 39, 25], [60, 38, 25], [57, 37, 25], [54, 35, 25], [52, 34, 25], [49, 33, 25], [47, 32, 25], [45, 31, 25], [43, 30, 25], [40, 29, 25], [39, 28, 25], [37, 28, 25], [35, 27, 25], [33, 27, 25], [32, 26, 25], [30, 26, 25], [29, 25, 25], [28, 25, 25], [27, 25, 25], [26, 25, 25], [26, 25, 26], [26, 26, 27], [26, 26, 28], [26, 26, 30], [26, 27, 31], [26, 27, 33], [26, 28, 34], [26, 29, 36], [26, 30, 38], [26, 31, 40], [26, 32, 42], [26, 33, 44], [26, 34, 47], [26, 35, 49], [26, 37, 51], [26, 38, 54], [26, 40, 56], [26, 41, 59], [26, 43, 62], [26, 44, 64], [26, 46, 67], [26, 48, 70], [26, 50, 73], [27, 51, 76], [28, 53, 79], [28, 55, 82], [29, 57, 85], [30, 59, 88], [31, 61, 91], [32, 64, 94], [33, 66, 97], [35, 68, 101], [36, 70, 104], [37, 72, 107], [38, 74, 110], [40, 77, 113], [41, 79, 117], [42, 81, 120], [44, 84, 123], [45, 86, 126], [47, 88, 130], [48, 91, 133], [50, 93, 136], [51, 95, 139], [53, 98, 142], [54, 100, 145], [56, 102, 148], [58, 104, 151], [59, 107, 154], [61, 109, 157], [63, 111, 160], [64, 114, 163], [66, 116, 165], [68, 118, 168], [70, 120, 171], [71, 122, 173], [73, 125, 176], [75, 127, 178], [77, 129, 181], [78, 131, 183], [80, 133, 185], [82, 135, 187], [84, 136, 189], [86, 138, 191], [87, 140, 193], [89, 142, 194], [91, 144, 196], [93, 146, 198], [96, 147, 199], [98, 149, 201], [100, 151, 203], [103, 153, 204], [105, 155, 206], [108, 157, 207], [110, 160, 209], [113, 162, 210], [116, 164, 212], [118, 166, 213], [121, 168, 214], [124, 170, 216], [127, 172, 217], [130, 174, 218], [133, 176, 219], [136, 178, 221], [139, 180, 222], [142, 183, 223], [145, 185, 224], [148, 187, 225], [152, 189, 226], [155, 191, 227], [158, 193, 228], [161, 195, 230], [164, 197, 231], [168, 200, 231], [171, 202, 232], [174, 204, 233], [177, 206, 234], [180, 208, 235], [183, 210, 236], [187, 212, 237], [190, 214, 238], [193, 216, 239], [196, 218, 239], [199, 220, 240], [202, 222, 241], [205, 223, 242], [208, 225, 242], [211, 227, 243], [214, 229, 244], [217, 231, 245], [219, 232, 245], [222, 234, 246], [225, 236, 247], [227, 237, 247], [230, 239, 248], [232, 240, 248], [234, 242, 249], [237, 243, 250], [239, 245, 250], [241, 246, 251], [243, 247, 251], [245, 249, 252], [247, 250, 252], [249, 251, 253], [251, 252, 253], [252, 253, 254], [254, 254, 254]]
colormap_wave2 = [[255, 255, 255], [253, 254, 254], [252, 254, 253], [250, 253, 252], [248, 253, 252], [246, 252, 251], [244, 252, 250], [242, 251, 249], [240, 251, 247], [237, 250, 246], [235, 250, 245], [232, 249, 244], [230, 248, 243], [227, 248, 242], [224, 247, 240], [221, 247, 239], [218, 246, 238], [215, 245, 236], [212, 245, 235], [209, 244, 234], [206, 243, 232], [203, 242, 231], [200, 242, 229], [196, 241, 228], [193, 240, 226], [190, 239, 225], [186, 239, 223], [183, 238, 222], [179, 237, 220], [176, 236, 218], [172, 235, 217], [169, 234, 215], [165, 233, 213], [162, 232, 212], [158, 231, 210], [155, 231, 208], [151, 230, 206], [148, 228, 205], [144, 227, 203], [141, 226, 201], [137, 225, 199], [134, 224, 198], [130, 223, 196], [127, 222, 194], [124, 221, 192], [120, 219, 190], [117, 218, 188], [114, 217, 187], [111, 216, 185], [108, 214, 183], [105, 213, 181], [102, 212, 179], [99, 210, 177], [96, 209, 176], [93, 207, 174], [91, 206, 172], [88, 204, 170], [85, 203, 168], [83, 201, 166], [80, 199, 164], [78, 198, 163], [76, 196, 161], [74, 194, 159], [72, 193, 157], [70, 191, 155], [69, 189, 153], [67, 187, 152], [65, 185, 149], [63, 183, 147], [62, 180, 145], [60, 178, 143], [59, 175, 141], [57, 173, 138], [56, 170, 136], [54, 167, 133], [53, 164, 131], [52, 161, 128], [50, 158, 125], [49, 155, 123], [48, 152, 120], [47, 149, 117], [46, 146, 115], [45, 143, 112], [43, 140, 109], [42, 136, 106], [41, 133, 103], [40, 130, 101], [39, 126, 98], [39, 123, 95], [38, 119, 92], [37, 116, 89], [36, 112, 86], [35, 109, 84], [35, 106, 81], [34, 102, 78], [33, 99, 75], [33, 95, 73], [32, 92, 70], [31, 89, 67], [31, 86, 65], [30, 82, 62], [30, 79, 60], [29, 76, 57], [29, 73, 55], [28, 70, 52], [28, 67, 50], [28, 64, 48], [27, 61, 46], [27, 58, 44], [27, 55, 42], [26, 53, 40], [26, 50, 38], [26, 48, 37], [26, 46, 35], [26, 43, 34], [26, 41, 32], [26, 39, 31], [26, 37, 30], [26, 35, 29], [26, 34, 28], [26, 32, 27], [26, 31, 26], [26, 29, 26], [26, 28, 25], [26, 27, 25], [26, 27, 25], [26, 26, 25], [26, 25, 25], [26, 25, 26], [26, 25, 26], [26, 25, 27], [26, 25, 27], [26, 25, 28], [27, 25, 30], [27, 25, 31], [27, 26, 32], [28, 27, 34], [28, 27, 35], [28, 28, 37], [29, 29, 39], [29, 30, 41], [30, 31, 43], [30, 32, 45], [31, 34, 48], [31, 35, 50], [32, 36, 53], [32, 38, 55], [33, 40, 58], [33, 41, 61], [34, 43, 64], [35, 45, 66], [36, 47, 69], [36, 49, 73], [37, 51, 76], [38, 53, 79], [39, 55, 82], [39, 57, 85], [40, 59, 89], [41, 61, 92], [42, 64, 95], [43, 66, 99], [44, 68, 102], [45, 71, 105], [46, 73, 109], [47, 76, 112], [48, 78, 116], [49, 80, 119], [50, 83, 122], [52, 86, 126], [53, 88, 129], [54, 91, 133], [55, 93, 136], [57, 96, 139], [58, 98, 143], [59, 100, 146], [60, 103, 149], [62, 105, 152], [63, 108, 155], [65, 110, 158], [66, 113, 161], [68, 115, 164], [69, 117, 167], [71, 120, 170], [72, 122, 173], [74, 124, 175], [75, 126, 178], [77, 128, 180], [79, 131, 183], [80, 133, 185], [82, 134, 187], [84, 136, 189], [86, 138, 191], [87, 140, 193], [89, 142, 194], [91, 144, 196], [93, 146, 198], [96, 147, 199], [98, 149, 201], [100, 151, 203], [103, 153, 204], [105, 155, 206], [108, 157, 207], [110, 160, 209], [113, 162, 210], [116, 164, 212], [118, 166, 213], [121, 168, 214], [124, 170, 216], [127, 172, 217], [130, 174, 218], [133, 176, 219], [136, 178, 221], [139, 180, 222], [142, 183, 223], [145, 185, 224], [148, 187, 225], [152, 189, 226], [155, 191, 227], [158, 193, 228], [161, 195, 230], [164, 197, 231], [168, 200, 231], [171, 202, 232], [174, 204, 233], [177, 206, 234], [180, 208, 235], [183, 210, 236], [187, 212, 237], [190, 214, 238], [193, 216, 239], [196, 218, 239], [199, 220, 240], [202, 222, 241], [205, 223, 242], [208, 225, 242], [211, 227, 243], [214, 229, 244], [217, 231, 245], [219, 232, 245], [222, 234, 246], [225, 236, 247], [227, 237, 247], [230, 239, 248], [232, 240, 248], [234, 242, 249], [237, 243, 250], [239, 245, 250], [241, 246, 251], [243, 247, 251], [245, 249, 252], [247, 250, 252], [249, 251, 253], [251, 252, 253], [252, 253, 254], [254, 254, 254]]
colormap_wave3 = [[253, 203, 160], [252, 201, 158], [252, 200, 156], [252, 198, 154], [251, 196, 152], [251, 194, 149], [251, 192, 147], [250, 190, 145], [250, 189, 142], [249, 187, 139], [249, 185, 135], [248, 182, 132], [248, 179, 129], [247, 177, 125], [247, 175, 122], [247, 172, 119], [246, 168, 115], [246, 166, 112], [245, 164, 108], [245, 161, 105], [244, 158, 102], [243, 155, 98], [243, 152, 95], [242, 150, 92], [242, 147, 88], [241, 145, 86], [240, 142, 83], [240, 139, 80], [239, 137, 78], [238, 134, 75], [237, 131, 73], [237, 129, 70], [236, 126, 68], [235, 124, 67], [234, 121, 65], [233, 118, 63], [232, 115, 61], [231, 112, 61], [230, 110, 59], [229, 107, 57], [228, 104, 56], [228, 102, 55], [226, 100, 55], [224, 97, 55], [224, 94, 54], [222, 92, 54], [221, 89, 53], [219, 87, 53], [218, 84, 53], [217, 83, 53], [215, 80, 54], [213, 78, 54], [211, 76, 54], [209, 74, 55], [208, 72, 55], [206, 70, 56], [203, 68, 57], [201, 66, 57], [199, 65, 58], [198, 64, 59], [196, 62, 59], [193, 60, 60], [191, 59, 61], [188, 57, 61], [186, 56, 62], [183, 55, 62], [181, 55, 63], [179, 54, 63], [176, 53, 63], [173, 52, 64], [171, 51, 64], [168, 50, 65], [165, 49, 65], [162, 49, 65], [159, 48, 66], [157, 48, 66], [155, 47, 66], [152, 47, 66], [149, 46, 66], [146, 46, 65], [143, 46, 65], [140, 45, 65], [138, 45, 65], [135, 45, 64], [132, 44, 64], [130, 44, 63], [127, 44, 63], [124, 44, 62], [121, 43, 61], [118, 43, 60], [115, 43, 60], [112, 42, 60], [110, 42, 59], [108, 42, 58], [105, 42, 57], [102, 41, 56], [99, 41, 55], [97, 40, 54], [94, 40, 53], [91, 40, 52], [89, 39, 51], [86, 39, 50], [84, 38, 49], [82, 38, 48], [79, 37, 47], [77, 37, 46], [74, 36, 45], [72, 36, 44], [70, 35, 43], [68, 35, 42], [65, 34, 41], [64, 34, 40], [62, 33, 39], [60, 33, 38], [58, 33, 38], [56, 32, 38], [54, 32, 36], [52, 32, 35], [50, 32, 35], [48, 31, 35], [47, 31, 34], [45, 31, 34], [43, 31, 33], [42, 30, 33], [41, 30, 32], [40, 30, 32], [39, 30, 32], [38, 30, 32], [37, 30, 32], [36, 30, 32], [36, 30, 32], [37, 30, 32], [38, 30, 32], [39, 30, 32], [40, 30, 32], [41, 30, 32], [42, 30, 33], [44, 31, 33], [46, 31, 34], [47, 31, 34], [49, 31, 35], [51, 32, 35], [53, 32, 36], [55, 32, 37], [57, 33, 38], [59, 33, 38], [61, 33, 39], [63, 34, 40], [65, 34, 41], [67, 35, 42], [70, 35, 43], [72, 36, 44], [74, 36, 45], [77, 37, 46], [79, 37, 47], [82, 38, 48], [84, 38, 49], [87, 39, 50], [90, 39, 51], [92, 40, 52], [95, 40, 53], [98, 40, 54], [100, 41, 55], [103, 41, 56], [106, 42, 57], [109, 42, 58], [111, 42, 59], [114, 43, 60], [117, 43, 60], [120, 43, 61], [123, 44, 62], [126, 44, 63], [129, 44, 63], [131, 44, 64], [134, 45, 64], [137, 45, 65], [140, 45, 65], [143, 46, 65], [146, 46, 65], [149, 46, 66], [152, 47, 66], [155, 47, 66], [158, 48, 66], [160, 48, 66], [163, 49, 65], [166, 49, 65], [169, 50, 65], [172, 51, 64], [174, 52, 64], [177, 53, 63], [180, 54, 63], [182, 55, 62], [185, 56, 62], [187, 57, 61], [190, 58, 61], [192, 60, 60], [195, 61, 59], [197, 63, 59], [199, 65, 58], [201, 66, 57], [203, 68, 57], [206, 70, 56], [208, 72, 55], [209, 74, 55], [211, 76, 54], [213, 78, 54], [215, 81, 54], [217, 83, 53], [218, 85, 53], [220, 88, 53], [221, 90, 53], [223, 93, 54], [224, 95, 54], [225, 98, 55], [227, 101, 55], [228, 103, 56], [229, 106, 57], [230, 109, 58], [231, 111, 60], [232, 114, 61], [233, 117, 62], [234, 120, 64], [235, 123, 66], [236, 125, 68], [237, 128, 70], [237, 131, 73], [238, 134, 75], [239, 137, 78], [240, 139, 80], [240, 142, 83], [241, 145, 86], [242, 148, 89], [242, 151, 93], [243, 153, 96], [243, 156, 99], [244, 159, 103], [245, 162, 106], [245, 165, 110], [246, 167, 113], [246, 170, 117], [247, 173, 120], [247, 176, 124], [248, 178, 127], [248, 181, 131], [249, 184, 134], [249, 186, 138], [250, 188, 141], [250, 190, 144], [251, 192, 147], [251, 194, 149], [251, 196, 152], [252, 198, 154], [252, 200, 156], [252, 201, 158], [253, 203, 160]]
colormap_wave4 = [[246, 230, 183], [246, 229, 182], [246, 227, 180], [246, 226, 178], [246, 224, 176], [245, 222, 173], [245, 219, 170], [244, 217, 167], [244, 214, 163], [244, 211, 160], [243, 209, 156], [243, 206, 152], [242, 203, 148], [242, 200, 144], [241, 196, 140], [241, 193, 136], [241, 190, 132], [240, 186, 128], [240, 183, 124], [239, 180, 120], [239, 176, 116], [238, 173, 112], [238, 170, 108], [237, 166, 104], [237, 163, 100], [236, 160, 97], [236, 156, 93], [236, 153, 90], [235, 150, 87], [235, 147, 84], [235, 144, 81], [234, 140, 78], [234, 137, 76], [234, 134, 74], [234, 131, 71], [233, 127, 69], [233, 124, 67], [233, 121, 65], [233, 118, 64], [232, 115, 62], [232, 112, 61], [232, 109, 60], [232, 106, 59], [232, 103, 58], [232, 101, 58], [232, 98, 57], [232, 95, 57], [231, 93, 57], [230, 90, 57], [230, 88, 57], [229, 85, 57], [227, 83, 57], [226, 81, 57], [224, 78, 57], [222, 76, 58], [220, 74, 58], [217, 72, 59], [215, 70, 59], [212, 67, 60], [210, 65, 60], [207, 63, 60], [204, 62, 61], [201, 60, 61], [199, 58, 61], [196, 56, 61], [193, 55, 61], [189, 53, 61], [186, 52, 62], [183, 50, 61], [180, 49, 61], [176, 48, 61], [173, 46, 61], [170, 45, 61], [166, 44, 61], [163, 43, 61], [159, 42, 60], [156, 40, 60], [152, 39, 60], [149, 39, 59], [146, 38, 58], [142, 37, 58], [139, 36, 57], [135, 35, 56], [132, 34, 55], [128, 34, 54], [125, 33, 53], [122, 32, 52], [118, 31, 51], [115, 31, 50], [111, 30, 49], [108, 29, 48], [105, 28, 46], [101, 28, 45], [98, 27, 44], [94, 26, 43], [91, 25, 41], [88, 24, 40], [85, 24, 38], [82, 23, 37], [78, 22, 35], [75, 21, 34], [72, 20, 32], [69, 19, 31], [66, 18, 29], [63, 18, 28], [60, 16, 26], [57, 16, 25], [55, 15, 24], [52, 14, 22], [49, 13, 21], [46, 12, 19], [44, 11, 18], [41, 11, 17], [39, 10, 16], [36, 9, 14], [34, 8, 13], [31, 8, 12], [29, 7, 11], [27, 7, 10], [25, 6, 10], [23, 5, 9], [21, 5, 8], [19, 4, 7], [17, 4, 7], [16, 4, 6], [14, 3, 6], [13, 3, 5], [13, 3, 5], [12, 3, 5], [12, 3, 5], [12, 3, 5], [12, 3, 5], [13, 3, 5], [14, 3, 5], [15, 3, 6], [16, 4, 6], [18, 4, 7], [20, 4, 7], [21, 5, 8], [23, 5, 9], [26, 6, 10], [28, 7, 11], [30, 7, 12], [33, 8, 13], [35, 9, 14], [38, 9, 15], [40, 10, 17], [43, 11, 18], [46, 12, 19], [48, 13, 21], [51, 14, 22], [54, 15, 24], [57, 16, 25], [60, 17, 27], [63, 18, 28], [67, 19, 30], [70, 20, 31], [73, 20, 33], [76, 21, 34], [80, 22, 36], [83, 23, 37], [86, 24, 39], [90, 25, 40], [93, 26, 42], [96, 27, 43], [100, 27, 44], [103, 28, 46], [107, 29, 47], [110, 30, 48], [114, 30, 50], [117, 31, 51], [121, 32, 52], [124, 33, 53], [128, 34, 54], [131, 34, 55], [135, 35, 56], [138, 36, 57], [142, 37, 57], [146, 38, 58], [149, 39, 59], [153, 39, 59], [156, 40, 60], [160, 42, 60], [164, 43, 61], [167, 44, 61], [171, 45, 61], [174, 46, 61], [178, 48, 62], [181, 49, 62], [184, 51, 62], [188, 52, 62], [191, 54, 62], [194, 56, 61], [197, 57, 61], [200, 59, 61], [203, 61, 61], [206, 63, 60], [209, 65, 60], [212, 67, 59], [215, 69, 59], [217, 72, 59], [220, 74, 58], [222, 76, 58], [224, 78, 57], [226, 81, 57], [227, 83, 57], [229, 86, 57], [230, 88, 57], [230, 91, 57], [231, 94, 57], [232, 97, 57], [232, 99, 57], [232, 102, 58], [232, 105, 59], [232, 108, 60], [232, 111, 61], [232, 114, 62], [232, 117, 63], [233, 120, 65], [233, 123, 66], [233, 127, 68], [233, 130, 71], [234, 133, 73], [234, 137, 75], [234, 140, 78], [235, 143, 81], [235, 147, 84], [235, 150, 87], [236, 153, 90], [236, 157, 94], [236, 160, 97], [237, 164, 101], [237, 167, 105], [238, 170, 109], [238, 174, 113], [239, 178, 117], [239, 181, 121], [240, 184, 126], [240, 188, 130], [241, 192, 134], [241, 195, 138], [242, 198, 142], [242, 202, 147], [243, 205, 151], [243, 208, 155], [244, 211, 159], [244, 214, 162], [244, 216, 166], [245, 219, 169], [245, 221, 173], [246, 224, 175], [246, 226, 178], [246, 227, 180], [246, 229, 182], [246, 230, 183]]


class WaveVisualizer:
    def __init__(self, field_colormap, intensity_colormap):
        self.field_colormap = field_colormap
        self.intensity_colormap = intensity_colormap
        self.intensity = None
        self.intensity_exp_average_factor = 0.98
        self.field = None
        self.visualization_image = None

    def update(self, wave_sim):
        self.field = wave_sim.get_field()

        if self.intensity is None:
            self.intensity = cp.zeros_like(self.field)

        t = self.intensity_exp_average_factor
        self.intensity = self.intensity*t + (self.field**2)*(1.0-t)
        self.visualization_image = wave_sim.render_visualization()

    def render_intensity(self, brightness_scale=1.0, exp=0.5, overlay_visualization=True):
        gray = (cp.clip((self.intensity**exp)*brightness_scale, 0.0, 1.0) * 254.0).astype(np.uint8)
        img = self.intensity_colormap[gray].get() if self.intensity_colormap is not None else gray.get()
        img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
        if overlay_visualization:
            img = cv2.add(img, self.visualization_image)
        return img

    def render_field(self, brightness_scale=1.0, overlay_visualization=True):
        gray = (cp.clip(self.field*brightness_scale, -1.0, 1.0) * 127 + 127).astype(np.uint8)
        img = self.field_colormap[gray].get() if self.field_colormap is not None else gray.get()
        img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
        if overlay_visualization:
            img = cv2.add(img, self.visualization_image)
        return img


def get_colormap_lut(name, invert, black_level=0.0, make_symmetric=False):
    if name == 'icefire': color_values = np.array(colormap_icefire)/255
    elif name == 'colormap_wave1': color_values = np.array(colormap_wave1)/255
    elif name == 'colormap_wave2':color_values = np.array(colormap_wave2) / 255
    elif name == 'colormap_wave3': color_values = np.array(colormap_wave3) / 255
    elif name == 'colormap_wave4': color_values = np.array(colormap_wave4) / 255
    else:
        colormap = matplotlib.pyplot.get_cmap(name)
        color_values = colormap(np.linspace(0, 1, 255))

    if invert:
        color_values = 1.0-color_values

    if make_symmetric:
        src = color_values.copy()
        color_values[255:126:-1, :] = src[0:255:2, :]
        color_values[0:128, :] = src[0:255:2, :]

    color_values = np.clip(color_values*(1.0-black_level)+black_level, 0, 255)

    return cp.asarray((color_values*255).astype(np.uint8))
Download .txt
gitextract_7z9h8v6g/

├── README.md
├── requirements.txt
└── wave_sim2d/
    ├── __init__.py
    ├── develop_tests.py
    ├── examples/
    │   ├── example0.py
    │   ├── example1.py
    │   ├── example2.py
    │   ├── example3.py
    │   └── example4.py
    ├── main.py
    ├── scene_objects/
    │   ├── source.py
    │   ├── static_dampening.py
    │   ├── static_image_scene.py
    │   ├── static_refractive_index.py
    │   └── strain_refractive_index.py
    ├── wave_simulation.py
    └── wave_visualizer.py
Download .txt
SYMBOL INDEX (83 symbols across 13 files)

FILE: wave_sim2d/develop_tests.py
  function build_example_scene1 (line 14) | def build_example_scene1(scene_image):
  function build_example_scene2 (line 22) | def build_example_scene2(width, height):
  function simulate (line 46) | def simulate(scene_image_fn, num_iterations,

FILE: wave_sim2d/examples/example0.py
  function build_scene (line 11) | def build_scene():
  function main (line 24) | def main():

FILE: wave_sim2d/examples/example1.py
  function build_scene (line 13) | def build_scene(scene_image_path):
  function main (line 27) | def main():

FILE: wave_sim2d/examples/example2.py
  function build_scene (line 14) | def build_scene():
  function main (line 40) | def main():

FILE: wave_sim2d/examples/example3.py
  function gaussian_kernel (line 16) | def gaussian_kernel(size, sigma):
  class MovingCharge (line 26) | class MovingCharge(sim.SceneObject):
    method __init__ (line 34) | def __init__(self, x, y, frequency, amplitude):
    method render (line 44) | def render(self, field, wave_speed_field, dampening_field):
    method render_visualization (line 48) | def render_visualization(self, image: np.ndarray):
    method update_field (line 51) | def update_field(self, field, t):
  function build_scene (line 64) | def build_scene():
  function main (line 85) | def main():

FILE: wave_sim2d/examples/example4.py
  function build_scene (line 15) | def build_scene():
  function show_field (line 38) | def show_field(field, brightness_scale):
  function main (line 44) | def main():

FILE: wave_sim2d/scene_objects/source.py
  class PointSource (line 7) | class PointSource(SceneObject):
    method __init__ (line 18) | def __init__(self, x, y, frequency, amplitude=1.0, phase=0, amp_modula...
    method set_amplitude_modulator (line 26) | def set_amplitude_modulator(self, func):
    method render (line 29) | def render(self, field: cp.ndarray, wave_speed_field: cp.ndarray, damp...
    method update_field (line 32) | def update_field(self, field, t):
    method render_visualization (line 41) | def render_visualization(self, image: np.ndarray):
  class LineSource (line 46) | class LineSource(SceneObject):
    method __init__ (line 58) | def __init__(self, start, end, frequency, amplitude=1.0, phase=0, amp_...
    method set_amplitude_modulator (line 66) | def set_amplitude_modulator(self, func):
    method render (line 69) | def render(self, field: cp.ndarray, wave_speed_field: cp.ndarray, damp...
    method update_field (line 72) | def update_field(self, field, t):
    method render_visualization (line 101) | def render_visualization(self, image: np.ndarray):
  class ModulatorSmoothSquare (line 108) | class ModulatorSmoothSquare:
    method __init__ (line 112) | def __init__(self, frequency, phase, smoothness=0.5):
    method __call__ (line 117) | def __call__(self, t):
  class ModulatorDiscreteSignal (line 123) | class ModulatorDiscreteSignal:
    method __init__ (line 127) | def __init__(self, signal_array, time_factor, transition_slope=8.0):
    method __call__ (line 132) | def __call__(self, t):

FILE: wave_sim2d/scene_objects/static_dampening.py
  class StaticDampening (line 6) | class StaticDampening(SceneObject):
    method __init__ (line 12) | def __init__(self, dampening_field, border_thickness):
    method render (line 31) | def render(self, field: cp.ndarray, wave_speed_field: cp.ndarray, damp...
    method update_field (line 37) | def update_field(self, field: cp.ndarray, t):
    method render_visualization (line 40) | def render_visualization(self, image: np.ndarray):

FILE: wave_sim2d/scene_objects/static_image_scene.py
  class StaticImageScene (line 9) | class StaticImageScene(SceneObject):
    method __init__ (line 14) | def __init__(self, scene_image, source_amplitude=1.0, source_fequency_...
    method render (line 45) | def render(self, field: cp.ndarray, wave_speed_field: cp.ndarray, damp...
    method update_field (line 52) | def update_field(self, field: cp.ndarray, t):
    method render_visualization (line 60) | def render_visualization(self, image: np.ndarray):

FILE: wave_sim2d/scene_objects/static_refractive_index.py
  class StaticRefractiveIndex (line 7) | class StaticRefractiveIndex(SceneObject):
    method __init__ (line 13) | def __init__(self, refractive_index_field):
    method render (line 24) | def render(self, field: cp.ndarray, wave_speed_field: cp.ndarray, damp...
    method update_field (line 28) | def update_field(self, field: cp.ndarray, t):
    method render_visualization (line 31) | def render_visualization(self, image: np.ndarray):
  class StaticRefractiveIndexPolygon (line 36) | class StaticRefractiveIndexPolygon(SceneObject):
    method __init__ (line 42) | def __init__(self, vertices, refractive_index):
    method _create_polygon_data (line 56) | def _create_polygon_data(self, field_shape):
    method render (line 111) | def render(self, field: cp.ndarray, wave_speed_field: cp.ndarray, damp...
    method update_field (line 119) | def update_field(self, field: cp.ndarray, t):
    method render_visualization (line 122) | def render_visualization(self, image: np.ndarray):
  class StaticRefractiveIndexBox (line 127) | class StaticRefractiveIndexBox(StaticRefractiveIndexPolygon):
    method __init__ (line 133) | def __init__(self, center: tuple, box_size: tuple, box_angle_rad: floa...

FILE: wave_sim2d/scene_objects/strain_refractive_index.py
  class StrainRefractiveIndex (line 6) | class StrainRefractiveIndex(SceneObject):
    method __init__ (line 12) | def __init__(self, refractive_index_offset, coupling_constant):
    method render (line 25) | def render(self, field: cp.ndarray, wave_speed_field: cp.ndarray, damp...
    method update_field (line 38) | def update_field(self, field: cp.ndarray, t):
    method render_visualization (line 41) | def render_visualization(self, image: np.ndarray):

FILE: wave_sim2d/wave_simulation.py
  class SceneObject (line 8) | class SceneObject(ABC):
    method render (line 15) | def render(self, field: cupy.ndarray, wave_speed_field: cupy.ndarray, ...
    method update_field (line 20) | def update_field(self, field: cupy.ndarray, t):
    method render_visualization (line 25) | def render_visualization(self, image: np.ndarray):
  class WaveSimulator2D (line 30) | class WaveSimulator2D:
    method __init__ (line 36) | def __init__(self, w, h, scene_objects, initial_field=None):
    method reset_time (line 71) | def reset_time(self):
    method update_field (line 77) | def update_field(self):
    method update_scene (line 93) | def update_scene(self):
    method get_field (line 104) | def get_field(self):
    method render_visualization (line 111) | def render_visualization(self, image=None):

FILE: wave_sim2d/wave_visualizer.py
  class WaveVisualizer (line 13) | class WaveVisualizer:
    method __init__ (line 14) | def __init__(self, field_colormap, intensity_colormap):
    method update (line 22) | def update(self, wave_sim):
    method render_intensity (line 32) | def render_intensity(self, brightness_scale=1.0, exp=0.5, overlay_visu...
    method render_field (line 40) | def render_field(self, brightness_scale=1.0, overlay_visualization=True):
  function get_colormap_lut (line 49) | def get_colormap_lut(name, invert, black_level=0.0, make_symmetric=False):
Condensed preview — 17 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (73K chars).
[
  {
    "path": "README.md",
    "chars": 3538,
    "preview": "# 2D Wave Simulation on the GPU\n\nThis repository contains a lightweight 2D wave simulator running on the GPU using CuPy "
  },
  {
    "path": "requirements.txt",
    "chars": 38,
    "preview": "numpy\r\nopencv-python\r\nmatplotlib\r\ncupy"
  },
  {
    "path": "wave_sim2d/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "wave_sim2d/develop_tests.py",
    "chars": 4917,
    "preview": "import wave_visualizer\r\nimport wave_visualizer as vis\r\nimport wave_simulation as sim\r\nimport numpy as np\r\nimport cv2\r\nim"
  },
  {
    "path": "wave_sim2d/examples/example0.py",
    "chars": 1725,
    "preview": "import sys\r\nimport os\r\nsys.path.append(os.path.join(os.path.dirname(__file__), '../'))  # noqa\r\n\r\nimport cv2\r\nimport wav"
  },
  {
    "path": "wave_sim2d/examples/example1.py",
    "chars": 2426,
    "preview": "import sys\r\nimport os\r\nsys.path.append(os.path.join(os.path.dirname(__file__), '../'))  # noqa\r\n\r\nimport numpy as np\r\nim"
  },
  {
    "path": "wave_sim2d/examples/example2.py",
    "chars": 2650,
    "preview": "import sys\r\nimport os\r\nsys.path.append(os.path.join(os.path.dirname(__file__), '../'))  # noqa\r\n\r\nimport numpy as np\r\nim"
  },
  {
    "path": "wave_sim2d/examples/example3.py",
    "chars": 4000,
    "preview": "import sys\r\nimport os\r\nsys.path.append(os.path.join(os.path.dirname(__file__), '../'))  # noqa\r\n\r\nimport numpy as np\r\nim"
  },
  {
    "path": "wave_sim2d/examples/example4.py",
    "chars": 3150,
    "preview": "import sys\r\nimport os\r\nsys.path.append(os.path.join(os.path.dirname(__file__), '../'))  # noqa\r\n\r\nimport cv2\r\nimport num"
  },
  {
    "path": "wave_sim2d/main.py",
    "chars": 105,
    "preview": "if __name__ == \"__main__\":\r\n  print('please run one of the examples from the source/example folder...')\r\n"
  },
  {
    "path": "wave_sim2d/scene_objects/source.py",
    "chars": 5778,
    "preview": "from wave_sim2d.wave_simulation import SceneObject\r\nimport cupy as cp\r\nimport numpy as np\r\nimport math\r\n\r\n\r\nclass PointS"
  },
  {
    "path": "wave_sim2d/scene_objects/static_dampening.py",
    "chars": 1605,
    "preview": "from wave_sim2d.wave_simulation import SceneObject\r\nimport cupy as cp\r\nimport numpy as np\r\n\r\n\r\nclass StaticDampening(Sce"
  },
  {
    "path": "wave_sim2d/scene_objects/static_image_scene.py",
    "chars": 3250,
    "preview": "from wave_sim2d.wave_simulation import SceneObject\r\n\r\nimport numpy as np\r\nimport cupy as cp\r\nfrom wave_sim2d.scene_objec"
  },
  {
    "path": "wave_sim2d/scene_objects/static_refractive_index.py",
    "chars": 7446,
    "preview": "from wave_sim2d.wave_simulation import SceneObject\r\nimport cupy as cp\r\nimport numpy as np\r\nimport cv2\r\n\r\n\r\nclass StaticR"
  },
  {
    "path": "wave_sim2d/scene_objects/strain_refractive_index.py",
    "chars": 1787,
    "preview": "from wave_sim2d.wave_simulation import SceneObject\r\nimport cupy as cp\r\nimport cupyx.scipy.signal\r\nimport numpy as np\r\n\r\n"
  },
  {
    "path": "wave_sim2d/wave_simulation.py",
    "chars": 4436,
    "preview": "import cupy\r\nimport numpy as np\r\nimport cupy as cp\r\nimport cupyx.scipy.signal\r\nfrom abc import ABC, abstractmethod\r\n\r\n\r\n"
  },
  {
    "path": "wave_sim2d/wave_visualizer.py",
    "chars": 22446,
    "preview": "import numpy as np\r\nimport cupy as cp\r\nimport cv2\r\nimport matplotlib.pyplot\r\n\r\ncolormap_icefire = [[179, 224, 216], [178"
  }
]

About this extraction

This page contains the full source code of the 0x23/WaveSimulator2D GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 17 files (67.7 KB), approximately 23.9k tokens, and a symbol index with 83 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!