Skip to content

Commit d7a5719

Browse files
committed
feat: add sombrero projection system implementation
- Introduce `sombrero_projection_system` with new configs and equations. - Added tests, rollout functions, and integrated into Math-Gymnasium pipeline. - Adjusted related configuration files and simulator entries for testing.
1 parent 9d04ef0 commit d7a5719

File tree

3 files changed

+187
-0
lines changed

3 files changed

+187
-0
lines changed

src/math_gymnasium/dynamical_systems/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
from .non_linear_system.wavy_projection_system import (
2424
rollout_wavy_projection_partial_derivative,
2525
)
26+
from .non_linear_system.sombrero_projection_system import (
27+
rollout_sombrero_projection_partial_derivative,
28+
)
2629
from .linear_debug_system import rollout_linear_debug_system
2730

2831
__all__ = [
@@ -36,5 +39,6 @@
3639
"rollout_damped_oscillator_partial_derivative",
3740
"rollout_simple_limit_cycle_partial_derivative",
3841
"rollout_wavy_projection_partial_derivative",
42+
"rollout_sombrero_projection_partial_derivative",
3943
"rollout_linear_debug_system",
4044
]
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# coding=utf-8
2+
3+
import numpy as np
4+
5+
from tools.math_tools.ndarray_tools.custom_msg import nan_infinity_console_warning
6+
from tools.math_tools.space_conversion_tools.time_to_delta_time import (
7+
convert_state_time_to_state_delta_time,
8+
)
9+
10+
11+
def sombrero_projection_partial_derivative(
12+
xyz: np.ndarray,
13+
vx: float = 1.0,
14+
vy: float = 1.0,
15+
A: float = 1.0,
16+
sigma: float = 5.0,
17+
k: float = 2.0,
18+
dtype: np.dtype = np.float64,
19+
debug: bool = False,
20+
):
21+
"""
22+
Computes the partial derivatives for a sombrero projection system.
23+
This projects 2D linear motion onto a decaying ripple surface.
24+
25+
System equations:
26+
dx/dt = vx
27+
dy/dt = vy
28+
dz/dt = dz/dr * dr/dt
29+
where z = A * exp(-r^2 / sigma^2) * cos(k * r) and r = sqrt(x^2 + y^2)
30+
31+
:param xyz: An array containing the x, y, and z coordinates.
32+
:param vx: Velocity in x direction.
33+
:param vy: Velocity in y direction.
34+
:param A: Amplitude of the ripples.
35+
:param sigma: Decay constant (larger means slower decay).
36+
:param k: Radial frequency of the ripples.
37+
:param dtype: Data type for computations.
38+
:param debug: Warn if nan or infinity values are encountered.
39+
:return: An array containing the partial derivatives [x_dot, y_dot, z_dot].
40+
"""
41+
xyz = np.nan_to_num(xyz)
42+
x, y, z = xyz.astype(dtype)
43+
44+
r = np.sqrt(x**2 + y**2)
45+
46+
x_dot = vx
47+
y_dot = vy
48+
49+
# dz/dt = A * exp(-r^2/sigma^2) * [ (-2r/sigma^2)*cos(kr) - k*sin(kr) ] * dr/dt
50+
# dr/dt = (x*vx + y*vy) / r
51+
# dz/dt = A * exp(-r^2/sigma^2) * [ (-2/sigma^2)*cos(kr) - k*sin(kr)/r ] * (x*vx + y*vy)
52+
53+
# Use sinc to handle r=0 safely: sinc(x) = sin(pi*x)/(pi*x)
54+
# sin(kr)/r = k * sin(pi * (kr/pi)) / (pi * (kr/pi)) = k * sinc(kr/pi)
55+
sinc_val = np.sinc(k * r / np.pi)
56+
57+
decay = np.exp(-(r**2) / (sigma**2))
58+
bracket = (-2.0 / sigma**2) * np.cos(k * r) - k * k * sinc_val
59+
z_dot = A * decay * bracket * (x * vx + y * vy)
60+
61+
xyz_dot = np.array([x_dot, y_dot, z_dot], dtype=dtype)
62+
63+
if debug and not np.all(np.isfinite(xyz_dot)):
64+
nan_infinity_console_warning("xyz_dot")
65+
66+
xyz_dot = np.nan_to_num(xyz_dot)
67+
return xyz_dot.squeeze()
68+
69+
70+
def rollout_sombrero_projection_partial_derivative(
71+
time_space: np.ndarray,
72+
vx: float = 1.0,
73+
vy: float = 1.0,
74+
A: float = 1.0,
75+
sigma: float = 5.0,
76+
k: float = 2.0,
77+
initiale_coordinates=(0.0, 0.0, 1.0), # z(0,0) = A * exp(0) * cos(0) = A
78+
time_space_is_delta_time: bool = False,
79+
dtype: np.dtype = np.float64,
80+
) -> np.ndarray:
81+
"""
82+
Calculates the trajectory of a sombrero projection system over a given time space.
83+
84+
Assume `time_space` values are increasing if `time_space_is_delta_time=True`
85+
86+
:param time_space: Array representing time steps in wallclock time or delta time.
87+
:param vx: Velocity in x direction.
88+
:param vy: Velocity in y direction.
89+
:param A: Amplitude of the ripples.
90+
:param sigma: Decay constant.
91+
:param k: Radial frequency.
92+
:param initiale_coordinates: The state at timestep 0
93+
:param time_space_is_delta_time: Set to True if `time_space` is an array of delta time.
94+
:param dtype: Data type for computations.
95+
:return: Array of computed x, y, z coordinates over the given time space.
96+
"""
97+
assert isinstance(initiale_coordinates, tuple) and len(initiale_coordinates) == 3
98+
assert isinstance(time_space, np.ndarray) and time_space.ndim == 1
99+
100+
xyzs = np.empty((time_space.size, 3), dtype=dtype)
101+
xyzs_partial_derivative = np.empty_like(xyzs)
102+
103+
xyzs[0] = initiale_coordinates
104+
xyzs_partial_derivative[0] = (0.0, 0.0, 0.0)
105+
106+
if not time_space_is_delta_time:
107+
delta_time = convert_state_time_to_state_delta_time(time_space.astype(dtype))
108+
else:
109+
delta_time = time_space.copy().astype(dtype)
110+
111+
for i in np.arange(time_space.size - 1):
112+
xyzs_partial_derivative[i + 1] = sombrero_projection_partial_derivative(
113+
xyzs[i], vx, vy, A, sigma, k, dtype
114+
)
115+
xyzs[i + 1] = xyzs[i] + xyzs_partial_derivative[i + 1] * delta_time[i + 1]
116+
117+
assert np.all(np.isfinite(xyzs_partial_derivative))
118+
assert np.all(np.isfinite(xyzs))
119+
return xyzs
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# coding=utf-8
2+
from typing import Tuple
3+
4+
import numpy as np
5+
import pytest
6+
7+
from math_gymnasium.dynamical_systems.non_linear_system.sombrero_projection_system import (
8+
rollout_sombrero_projection_partial_derivative,
9+
)
10+
11+
12+
@pytest.fixture
13+
def setup_sombrero_projection_sys_spaces() -> Tuple[
14+
np.ndarray,
15+
Tuple[float, float, float],
16+
]:
17+
t_time_space = np.arange(10) * 0.1
18+
t_init_coord = (0.0, 0.0, 1.0)
19+
return t_time_space, t_init_coord
20+
21+
22+
class TestRolloutSombreroProjectionPartialDerivative:
23+
def test_default(self, setup_sombrero_projection_sys_spaces):
24+
(t_time_space, t_init_coord) = setup_sombrero_projection_sys_spaces
25+
t_time_space_freeze = t_time_space.copy()
26+
coords = rollout_sombrero_projection_partial_derivative(
27+
t_time_space, vx=1.0, vy=1.0, initiale_coordinates=t_init_coord,
28+
)
29+
assert np.array_equal(
30+
t_time_space_freeze, t_time_space
31+
), "original array was overridden"
32+
assert coords.shape == (t_time_space.shape[0], 3)
33+
34+
# Check if x and y are linear
35+
assert np.allclose(coords[:, 0], t_time_space)
36+
assert np.allclose(coords[:, 1], t_time_space)
37+
38+
# Initial z should be A = 1.0
39+
assert np.allclose(coords[0, 2], 1.0)
40+
41+
# Check that it's not linear
42+
assert not np.allclose(coords[:, 2], coords[0, 2] + (coords[1, 2] - coords[0, 2]) * np.arange(10))
43+
44+
def test_output_coord_using_delta_time(self, setup_sombrero_projection_sys_spaces):
45+
(t_time_space, t_init_coord) = setup_sombrero_projection_sys_spaces
46+
t_dt_space = np.ones_like(t_time_space) * 0.1
47+
t_dt_space[0] = 0.0
48+
49+
coord = rollout_sombrero_projection_partial_derivative(
50+
t_dt_space, vx=1.0, vy=1.0, initiale_coordinates=t_init_coord, time_space_is_delta_time=True
51+
)
52+
assert coord.shape == (t_time_space.shape[0], 3)
53+
# Initial z is 1.0.
54+
# At r=0, dz/dt = 0 (as calculated in thoughts, since x*vx+y*vy = 0).
55+
# Actually, let's check the first step from origin.
56+
# If x=0, y=0, then x*vx + y*vy = 0, so dz/dt = 0.
57+
# So the second z should still be 1.0 if starting EXACTLY at origin.
58+
assert np.allclose(coord[1, 2], 1.0)
59+
60+
# If we start offset, it should change.
61+
coord_offset = rollout_sombrero_projection_partial_derivative(
62+
t_dt_space, vx=1.0, vy=1.0, initiale_coordinates=(0.1, 0.1, 1.0), time_space_is_delta_time=True
63+
)
64+
assert not np.allclose(coord_offset[1, 2], 1.0)

0 commit comments

Comments
 (0)