import matplotlib.pyplot as plt
Impulse generator
Generator for impulses of different shapes.
::: {#cell-3 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’}
import numpy as np
from typing import Tuple, Optional
from einops import einsum
:::
::: {#cell-5 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’}
class Generator:
pass
:::
::: {#cell-6 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’}
class Gaussian(Generator):
def __init__(
self,
int = 100,
num_points:
):self.x = np.linspace(0, 1, num_points)
self.dx = self.x[1] - self.x[0]
def __call__(
self,
float = 0.5, # in percentage w.r.t. the number of points
mean: float = 0.05, # in percentage w.r.t. the number of points
std:
):= np.max([std, self.dx]) # To avoid too narrow gaussians
std_corr return np.exp(-((self.x - mean) ** 2 / (2 * std_corr**2)))
:::
::: {#cell-7 .cell 0=‘t’ 1=‘e’ 2=‘s’ 3=‘t’}
= Gaussian(num_points=501)(mean=0.5, std=0.0000005)
y assert len(y) == 501
# plot the gaussian
plt.plot(y)
:::
::: {#cell-8 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’}
class Gaussian2d(Generator):
"""This class generates a 2D gaussian distribution."""
def __init__(
self,
int = 100,
num_points_x: float = 1.0, # aspect ratio of the lengths of the two axes, ly/lx
aspect_ratio:
):self.aspect_ratio = aspect_ratio
= int(np.floor((num_points_x - 1) * aspect_ratio)) + 1
num_points_y = np.linspace(0, 1, num_points_x)
x = np.linspace(0, aspect_ratio, num_points_y)
y self.dx = x[1] - x[0]
self.dy = y[1] - y[0]
self.grid_x, self.grid_y = np.meshgrid(x, y, indexing="ij")
def __call__(
self,
tuple[float, float] = (0.5, 0.5), # respect to the plate aspect ratio
mean: float = 0.05, # in percentage w.r.t. the number of points
std:
):= np.max([std, self.dx, self.dy]) # To avoid too narrow gaussians
std_corr return np.exp(
-(
self.grid_x - mean[0]) ** 2
(+ (self.grid_y - self.aspect_ratio * mean[1]) ** 2
)/ (2 * std**2)
)
:::
::: {#cell-9 .cell 0=‘t’ 1=‘e’ 2=‘s’ 3=‘t’}
= 101
nx = 0.7
aspect_ratio = Gaussian2d(num_points_x=nx, aspect_ratio=aspect_ratio)(mean=(0.9, 1.0), std=0.05)
z = int(np.floor((nx - 1) * aspect_ratio)) + 1
ny print(z.shape)
print(nx, ny)
assert z.shape == (nx, ny)
# plot the gaussian
=0, vmax=1) plt.imshow(z, vmin
:::
::: {#cell-10 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’}
class NoiseBurst(Generator):
def __init__(
self,
int = 100,
num_points:
):self.num_points = num_points
self.x = np.linspace(0, 1, num_points)
self.gaussian = Gaussian(num_points)
self.dx = self.x[1] - self.x[0]
def __call__(
self,
rng: np.random.Generator,=[0, 1],
noise_rangefloat = 0.5,
burst_mean: float = 0.1,
burst_std:
):= np.max([burst_std, self.dx]) # To avoid too narrow gaussian envelope
std_corr = rng.uniform(*noise_range, self.num_points) * self.gaussian(
y
burst_mean, burst_std
)return y
:::
::: {#cell-11 .cell 0=‘t’ 1=‘e’ 2=‘s’ 3=‘t’}
= np.random.default_rng(42)
rng = NoiseBurst(num_points=500)(rng, noise_range=[0, 1], burst_mean=0.5, burst_std=0.05)
y assert len(y) == 500
:::
::: {#cell-12 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’}
class Noise(Generator):
def __init__(
self,
int = 100,
num_points:
):self.num_points = num_points
self.x = np.linspace(0, 1, num_points)
self.dx = self.x[1] - self.x[0]
def __call__(
self,
rng: np.random.Generator,=[0, 1],
noise_range
):= rng.uniform(*noise_range, self.num_points)
y return y
:::
::: {#cell-13 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’}
class Noise2d(Generator):
def __init__(
self,
int = 100,
num_points_x: float = 1.0, # aspect ratio of the lengths of the two axes, ly/lx
aspect_ratio:
):self.num_points_x = num_points_x
= int(np.floor((num_points_x - 1) * aspect_ratio)) + 1
num_points_y self.num_points_y = num_points_y
self.dx = 1 / (num_points_x - 1)
def __call__(
self,
rng: np.random.Generator,=[0, 1],
noise_range
):= rng.uniform(*noise_range, (self.num_points_x, self.num_points_y))
z return z
:::
::: {#cell-14 .cell 0=‘t’ 1=‘e’ 2=‘s’ 3=‘t’}
= np.random.default_rng(42)
rng = Noise2d(num_points_x=100, aspect_ratio=0.7)(rng, noise_range=[0, 1])
z assert z.shape == (100, 70)
=0, vmax=1) plt.imshow(z, vmin
:::
::: {#cell-15 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’}
class Triangle(Generator):
def __init__(
self,
int = 100,
num_points: int = 25,
num_modes: float = 1.0,
length:
):self.length = length
self.num_points = num_points
self.x = np.linspace(0, 1, num_points)
self.dx = self.x[1] - self.x[0]
# Wavenumbers
= np.arange(1, num_modes + 1) # Mode numbers
n self.k = n * np.pi / length
def __call__(
self,
float = 1.0, # height of the excitation
height: float = 0.5, # normalized position of the excitation [0, 1]
position:
):# Modal coefficients (based on the pluck excitation)
= (
A_n
height* (
self.length
/ (self.length - position)
* np.sin(self.k * position)
/ (self.k * position)
)/ self.k
)
# Sum of the modes
return np.sum(
None] * np.sin(self.k[:, None] * self.x),
A_n[:, =0,
axis )
:::
::: {#cell-16 .cell 0=‘t’ 1=‘e’ 2=‘s’ 3=‘t’}
= Triangle(num_points=101, num_modes=25)(height=1.0, position=0.1)
y # plot the triangle
plt.plot(y)
:::
::: {#cell-17 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’}
class SineMode(Generator):
def __init__(
self,
int = 100,
num_points:
):self.x = np.linspace(0, 1, num_points)
self.dx = self.x[1] - self.x[0]
def __call__(
self,
int = 1,
k:
):assert k > 0, "k must be positive"
return np.sin(np.pi * k * self.x)
:::
::: {#cell-18 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’}
def make_pluck_hammer(
y: np.ndarray,str = "pluck", # "pluck" or "hammer"
ic_type: -> Tuple[np.ndarray, np.ndarray]:
) if ic_type == "pluck":
return y, np.zeros_like(y)
elif ic_type == "hammer":
return np.zeros_like(y), y
else:
raise ValueError(f"ic_type should be either 'pluck' or 'hammer', got {ic_type}")
def generate_initial_condition(
= np.random.default_rng(42),
rng: np.random.Generator = Gaussian(),
generator: Generator str = "pluck", # "pluck" or "hammer"
ic_type: float = 1.0, # Amplitude of the initial condition, when ic_amplitude_random is True, this is the upper bound
ic_max_amplitude: float = 0.0, # only used when ic_amplitude_random is True
ic_min_amplitude: bool = False, # If True, the amplitude is chosen randomly between ic_min_amplitude and ic_max_amplitude
ic_amplitude_random: int = 1, # only used when ic_type is "sine"
ic_sine_k: -> Tuple[np.ndarray, np.ndarray]: # a tuple of position and velocity
) # Check that ic_max_amplitude is larger than ic_min_amplitude
if ic_amplitude_random:
assert ic_max_amplitude > ic_min_amplitude, (
f"ic_max_amplitude should be larger than ic_min_amplitude, got "
f"ic_max_amplitude={ic_max_amplitude} and ic_min_amplitude={ic_min_amplitude}"
)# TODO: Mean and std are hard-coded, could be made more flexible
= rng.uniform(0.3, 0.7)
mean = 2 * generator.dx
min_std = rng.uniform(min_std, 0.1)
std if isinstance(generator, Gaussian):
= generator(mean, std)
y elif isinstance(generator, NoiseBurst):
= generator(rng, noise_range=[-1, 1], burst_mean=mean, burst_std=std)
y elif isinstance(generator, Noise):
= generator(rng, noise_range=[-1, 1])
y elif isinstance(generator, SineMode):
= generator(k=ic_sine_k)
y elif isinstance(generator, Triangle):
= generator(height=ic_max_amplitude, position=rng.uniform(0.1, 0.9))
y elif isinstance(generator, Gaussian2d):
= (rng.uniform(0.3, 0.7), rng.uniform(0.3, 0.7))
mean = generator(mean, std)
y elif isinstance(generator, Noise2d):
= generator(rng, noise_range=[-1, 1])
y else:
raise TypeError(
f"generator should be either Gaussian, Noise, Sine or NoiseBurst, got {type(generator)}"
)# Normalize the amplitude to the desired value
if ic_amplitude_random:
= rng.uniform(ic_min_amplitude, ic_max_amplitude)
amplitude else:
= ic_max_amplitude
amplitude
= y * amplitude / np.max(np.abs(y))
y return make_pluck_hammer(y, ic_type)
:::
::: {#cell-19 .cell 0=‘t’ 1=‘e’ 2=‘s’ 3=‘t’}
= np.random.default_rng()
rng = "pluck"
ic_type = 0.7
ic_max_amplitude = 0.1
ic_min_amplitude = False
ic_amplitude_random = 500
num_points = generate_initial_condition(
u, v
rng,=num_points),
SineMode(num_points=ic_type,
ic_type=ic_max_amplitude,
ic_max_amplitude=ic_min_amplitude,
ic_min_amplitude=ic_amplitude_random,
ic_amplitude_random=10,
ic_sine_k
)if ic_type == "pluck":
assert np.all(v == 0)
if ic_amplitude_random:
assert np.max(np.abs(u)) <= ic_max_amplitude
assert np.max(np.abs(u)) >= ic_min_amplitude
else:
assert np.max(np.abs(u)) == ic_max_amplitude
elif ic_type == "hammer":
assert np.all(u == 0)
if ic_amplitude_random:
assert np.max(np.abs(v)) <= ic_max_amplitude
assert np.max(np.abs(v)) >= ic_min_amplitude
else:
assert np.max(np.abs(v)) == ic_max_amplitude
# Plot the initial conditions
= plt.subplots()
fig, ax
ax.plot(u) ax.plot(v)
:::
::: {#cell-20 .cell 0=‘t’ 1=‘e’ 2=‘s’ 3=‘t’}
# Test the 2d functions
= np.random.default_rng()
rng = "hammer"
ic_type = 0.5
ic_max_amplitude = 0.1
ic_min_amplitude = False
ic_amplitude_random = 500
num_points = generate_initial_condition(
u, v
rng,=num_points, aspect_ratio=0.7),
Noise2d(num_points_x=ic_type,
ic_type=ic_max_amplitude,
ic_max_amplitude=ic_min_amplitude,
ic_min_amplitude=ic_amplitude_random,
ic_amplitude_random
)print(np.max(np.abs(v)))
if ic_type == "pluck":
assert np.all(v == 0)
if ic_amplitude_random:
assert np.max(np.abs(u)) <= ic_max_amplitude
assert np.max(np.abs(u)) >= ic_min_amplitude
else:
assert np.allclose(np.max(np.abs(u)), ic_max_amplitude)
elif ic_type == "hammer":
assert np.all(u == 0)
if ic_amplitude_random:
assert np.max(np.abs(v)) <= ic_max_amplitude
assert np.max(np.abs(v)) >= ic_min_amplitude
else:
assert np.allclose(np.max(np.abs(v)), ic_max_amplitude)
# Plot the initial conditions
= plt.subplots(1, 2)
fig, ax 0].imshow(u)
ax[1].imshow(v) ax[
:::