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.
### 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
### 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).
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.
### 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))