miniworld-maze 1.0.0__py3-none-any.whl → 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of miniworld-maze might be problematic. Click here for more details.
- miniworld_maze/__init__.py +17 -9
- miniworld_maze/core/constants.py +55 -14
- miniworld_maze/core/miniworld_gymnasium/__init__.py +1 -1
- miniworld_maze/core/miniworld_gymnasium/unified_env.py +45 -29
- miniworld_maze/environments/__init__.py +0 -3
- miniworld_maze/environments/base_grid_rooms.py +213 -2
- miniworld_maze/environments/factory.py +38 -151
- miniworld_maze/environments/nine_rooms.py +8 -11
- miniworld_maze/environments/spiral_nine_rooms.py +8 -11
- miniworld_maze/environments/twenty_five_rooms.py +8 -27
- miniworld_maze/tools/__init__.py +1 -3
- miniworld_maze/utils.py +286 -0
- miniworld_maze-1.2.0.dist-info/METADATA +261 -0
- {miniworld_maze-1.0.0.dist-info → miniworld_maze-1.2.0.dist-info}/RECORD +15 -18
- {miniworld_maze-1.0.0.dist-info → miniworld_maze-1.2.0.dist-info}/WHEEL +1 -1
- miniworld_maze/tools/generate_observations.py +0 -199
- miniworld_maze/wrappers/__init__.py +0 -5
- miniworld_maze/wrappers/image_transforms.py +0 -40
- miniworld_maze-1.0.0.dist-info/METADATA +0 -108
- miniworld_maze-1.0.0.dist-info/entry_points.txt +0 -3
|
@@ -1,155 +1,42 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
import gymnasium as gym
|
|
4
|
-
import numpy as np
|
|
1
|
+
"""Gymnasium environment registrations for Nine Rooms environment variants."""
|
|
5
2
|
|
|
3
|
+
from gymnasium.envs.registration import register
|
|
6
4
|
from ..core import ObservationLevel
|
|
7
5
|
from ..core.constants import FACTORY_DOOR_SIZE, FACTORY_ROOM_SIZE
|
|
8
|
-
from ..wrappers.image_transforms import ImageToPyTorch
|
|
9
|
-
from .nine_rooms import NineRooms
|
|
10
|
-
from .spiral_nine_rooms import SpiralNineRooms
|
|
11
|
-
from .twenty_five_rooms import TwentyFiveRooms
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class NineRoomsEnvironmentWrapper(gym.Wrapper):
|
|
15
|
-
"""Unified wrapper for all Nine Rooms environment variants."""
|
|
16
|
-
|
|
17
|
-
def __init__(
|
|
18
|
-
self,
|
|
19
|
-
variant="NineRooms",
|
|
20
|
-
obs_level=ObservationLevel.TOP_DOWN_PARTIAL,
|
|
21
|
-
continuous=False,
|
|
22
|
-
size=64,
|
|
23
|
-
room_size=FACTORY_ROOM_SIZE,
|
|
24
|
-
door_size=FACTORY_DOOR_SIZE,
|
|
25
|
-
agent_mode=None,
|
|
26
|
-
):
|
|
27
|
-
"""
|
|
28
|
-
Create a Nine Rooms environment variant.
|
|
29
|
-
|
|
30
|
-
Args:
|
|
31
|
-
variant: Environment variant ("NineRooms", "SpiralNineRooms", "TwentyFiveRooms")
|
|
32
|
-
obs_level: Observation level (ObservationLevel enum)
|
|
33
|
-
continuous: Whether to use continuous actions
|
|
34
|
-
size: Observation image size (rendered directly at this size to avoid resizing)
|
|
35
|
-
room_size: Size of each room in environment units
|
|
36
|
-
door_size: Size of doors between rooms
|
|
37
|
-
agent_mode: Agent rendering mode ('empty', 'circle', 'triangle', or None for default)
|
|
38
|
-
"""
|
|
39
|
-
self.variant = variant
|
|
40
|
-
|
|
41
|
-
# Select the appropriate environment class
|
|
42
|
-
env_classes = {
|
|
43
|
-
"NineRooms": NineRooms,
|
|
44
|
-
"SpiralNineRooms": SpiralNineRooms,
|
|
45
|
-
"TwentyFiveRooms": TwentyFiveRooms,
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if variant not in env_classes:
|
|
49
|
-
raise ValueError(
|
|
50
|
-
f"Unknown variant '{variant}'. Available: {list(env_classes.keys())}"
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
env_class = env_classes[variant]
|
|
54
|
-
|
|
55
|
-
# Create base environment with direct rendering size
|
|
56
|
-
base_env = env_class(
|
|
57
|
-
room_size=room_size,
|
|
58
|
-
door_size=door_size,
|
|
59
|
-
obs_level=obs_level,
|
|
60
|
-
continuous=continuous,
|
|
61
|
-
obs_width=size,
|
|
62
|
-
obs_height=size,
|
|
63
|
-
agent_mode=agent_mode,
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
# Apply wrappers - no resize needed since we render at target size
|
|
67
|
-
env = ImageToPyTorch(base_env)
|
|
68
|
-
|
|
69
|
-
# Initialize gym.Wrapper with the wrapped environment
|
|
70
|
-
super().__init__(env)
|
|
71
|
-
|
|
72
|
-
def render_on_pos(self, pos):
|
|
73
|
-
"""Render observation from a specific position."""
|
|
74
|
-
# Get access to the base environment
|
|
75
|
-
base_env = self.env
|
|
76
|
-
while hasattr(base_env, "env") or hasattr(base_env, "_env"):
|
|
77
|
-
if hasattr(base_env, "env"):
|
|
78
|
-
base_env = base_env.env
|
|
79
|
-
elif hasattr(base_env, "_env"):
|
|
80
|
-
base_env = base_env._env
|
|
81
|
-
else:
|
|
82
|
-
break
|
|
83
|
-
|
|
84
|
-
# Store original position
|
|
85
|
-
original_pos = base_env.agent.pos.copy()
|
|
86
|
-
|
|
87
|
-
# Move agent to target position
|
|
88
|
-
base_env.place_agent(pos=pos)
|
|
89
|
-
|
|
90
|
-
# Get first-person observation from the agent's perspective at this position
|
|
91
|
-
obs = base_env.render_obs()
|
|
92
|
-
|
|
93
|
-
# Restore original position
|
|
94
|
-
base_env.place_agent(pos=original_pos)
|
|
95
|
-
|
|
96
|
-
# Apply wrapper transformations manually for consistency
|
|
97
|
-
# Convert to PyTorch format (CHW) - no resize needed since we render at target size
|
|
98
|
-
obs = np.transpose(obs, (2, 0, 1))
|
|
99
|
-
|
|
100
|
-
return obs
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def create_drstrategy_env(variant="NineRooms", **kwargs):
|
|
104
|
-
"""
|
|
105
|
-
Factory function to create DrStrategy environment variants.
|
|
106
|
-
|
|
107
|
-
Args:
|
|
108
|
-
variant: Environment variant ("NineRooms", "SpiralNineRooms", "TwentyFiveRooms")
|
|
109
|
-
**kwargs: Additional arguments passed to NineRoomsEnvironmentWrapper
|
|
110
|
-
|
|
111
|
-
Returns:
|
|
112
|
-
NineRoomsEnvironmentWrapper instance
|
|
113
|
-
"""
|
|
114
|
-
return NineRoomsEnvironmentWrapper(variant=variant, **kwargs)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
# Backward compatibility alias
|
|
118
|
-
def create_nine_rooms_env(variant="NineRooms", **kwargs):
|
|
119
|
-
"""
|
|
120
|
-
Legacy factory function for backward compatibility.
|
|
121
|
-
|
|
122
|
-
Deprecated: Use create_drstrategy_env() instead.
|
|
123
|
-
"""
|
|
124
|
-
import warnings
|
|
125
|
-
|
|
126
|
-
warnings.warn(
|
|
127
|
-
"create_nine_rooms_env() is deprecated. Use create_drstrategy_env() instead.",
|
|
128
|
-
DeprecationWarning,
|
|
129
|
-
stacklevel=2,
|
|
130
|
-
)
|
|
131
|
-
return create_drstrategy_env(variant=variant, **kwargs)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
# Legacy function - deprecated
|
|
135
|
-
def NineRoomsFullyPureGymnasium(
|
|
136
|
-
name="NineRooms",
|
|
137
|
-
obs_level=ObservationLevel.TOP_DOWN_PARTIAL,
|
|
138
|
-
continuous=False,
|
|
139
|
-
size=64,
|
|
140
|
-
):
|
|
141
|
-
"""
|
|
142
|
-
Legacy function for backward compatibility.
|
|
143
|
-
|
|
144
|
-
Deprecated: Use create_drstrategy_env() instead.
|
|
145
|
-
"""
|
|
146
|
-
import warnings
|
|
147
6
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
7
|
+
# Register environment variants with factory defaults matching the original wrapper
|
|
8
|
+
register(
|
|
9
|
+
id="NineRooms-v0",
|
|
10
|
+
entry_point="miniworld_maze.environments.nine_rooms:NineRooms",
|
|
11
|
+
max_episode_steps=1000,
|
|
12
|
+
kwargs={
|
|
13
|
+
"room_size": FACTORY_ROOM_SIZE,
|
|
14
|
+
"door_size": FACTORY_DOOR_SIZE,
|
|
15
|
+
"obs_level": ObservationLevel.TOP_DOWN_PARTIAL,
|
|
16
|
+
"agent_mode": None, # becomes "empty" by default
|
|
17
|
+
},
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
register(
|
|
21
|
+
id="SpiralNineRooms-v0",
|
|
22
|
+
entry_point="miniworld_maze.environments.spiral_nine_rooms:SpiralNineRooms",
|
|
23
|
+
max_episode_steps=1000,
|
|
24
|
+
kwargs={
|
|
25
|
+
"room_size": FACTORY_ROOM_SIZE,
|
|
26
|
+
"door_size": FACTORY_DOOR_SIZE,
|
|
27
|
+
"obs_level": ObservationLevel.TOP_DOWN_PARTIAL,
|
|
28
|
+
"agent_mode": None,
|
|
29
|
+
},
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
register(
|
|
33
|
+
id="TwentyFiveRooms-v0",
|
|
34
|
+
entry_point="miniworld_maze.environments.twenty_five_rooms:TwentyFiveRooms",
|
|
35
|
+
max_episode_steps=1000,
|
|
36
|
+
kwargs={
|
|
37
|
+
"room_size": FACTORY_ROOM_SIZE,
|
|
38
|
+
"door_size": FACTORY_DOOR_SIZE,
|
|
39
|
+
"obs_level": ObservationLevel.TOP_DOWN_PARTIAL,
|
|
40
|
+
"agent_mode": None,
|
|
41
|
+
},
|
|
42
|
+
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""NineRooms environment implementation."""
|
|
2
2
|
|
|
3
3
|
from ..core import ObservationLevel
|
|
4
|
+
from ..core.constants import TextureThemes
|
|
4
5
|
from .base_grid_rooms import GridRoomsEnvironment
|
|
5
6
|
|
|
6
7
|
|
|
@@ -46,22 +47,18 @@ class NineRooms(GridRoomsEnvironment):
|
|
|
46
47
|
(6, 7),
|
|
47
48
|
(7, 8),
|
|
48
49
|
]
|
|
49
|
-
default_textures =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"lightcobaltgreen",
|
|
56
|
-
"oakbrown",
|
|
57
|
-
"navyblue",
|
|
58
|
-
"cobaltgreen",
|
|
59
|
-
]
|
|
50
|
+
default_textures = TextureThemes.NINE_ROOMS
|
|
51
|
+
|
|
52
|
+
# Initialize goal positions for each room (2 goals per room)
|
|
53
|
+
goal_positions = GridRoomsEnvironment._generate_goal_positions(
|
|
54
|
+
3, room_size, goals_per_room=2
|
|
55
|
+
)
|
|
60
56
|
|
|
61
57
|
super().__init__(
|
|
62
58
|
grid_size=3,
|
|
63
59
|
connections=connections or default_connections,
|
|
64
60
|
textures=textures or default_textures,
|
|
61
|
+
goal_positions=goal_positions,
|
|
65
62
|
placed_room=placed_room,
|
|
66
63
|
obs_level=obs_level,
|
|
67
64
|
continuous=continuous,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""SpiralNineRooms environment implementation."""
|
|
2
2
|
|
|
3
3
|
from ..core import ObservationLevel
|
|
4
|
+
from ..core.constants import TextureThemes
|
|
4
5
|
from .base_grid_rooms import GridRoomsEnvironment
|
|
5
6
|
|
|
6
7
|
|
|
@@ -42,22 +43,18 @@ class SpiralNineRooms(GridRoomsEnvironment):
|
|
|
42
43
|
(6, 7),
|
|
43
44
|
(7, 8),
|
|
44
45
|
]
|
|
45
|
-
default_textures =
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"lightcobaltgreen",
|
|
52
|
-
"oakbrown",
|
|
53
|
-
"navyblue",
|
|
54
|
-
"cobaltgreen",
|
|
55
|
-
]
|
|
46
|
+
default_textures = TextureThemes.SPIRAL_NINE_ROOMS
|
|
47
|
+
|
|
48
|
+
# Initialize goal positions for each room (2 goals per room)
|
|
49
|
+
goal_positions = GridRoomsEnvironment._generate_goal_positions(
|
|
50
|
+
3, room_size, goals_per_room=2
|
|
51
|
+
)
|
|
56
52
|
|
|
57
53
|
super().__init__(
|
|
58
54
|
grid_size=3,
|
|
59
55
|
connections=connections or default_connections,
|
|
60
56
|
textures=textures or default_textures,
|
|
57
|
+
goal_positions=goal_positions,
|
|
61
58
|
placed_room=placed_room,
|
|
62
59
|
obs_level=obs_level,
|
|
63
60
|
continuous=continuous,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""TwentyFiveRooms environment implementation."""
|
|
2
2
|
|
|
3
3
|
from ..core import ObservationLevel
|
|
4
|
+
from ..core.constants import TextureThemes
|
|
4
5
|
from .base_grid_rooms import GridRoomsEnvironment
|
|
5
6
|
|
|
6
7
|
|
|
@@ -78,38 +79,18 @@ class TwentyFiveRooms(GridRoomsEnvironment):
|
|
|
78
79
|
(22, 23),
|
|
79
80
|
(23, 24),
|
|
80
81
|
]
|
|
81
|
-
default_textures =
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
"lightcobaltgreen",
|
|
88
|
-
"oakbrown",
|
|
89
|
-
"copperred",
|
|
90
|
-
"lightgray",
|
|
91
|
-
"lime",
|
|
92
|
-
"turquoise",
|
|
93
|
-
"violet",
|
|
94
|
-
"beige",
|
|
95
|
-
"morningglory",
|
|
96
|
-
"silver",
|
|
97
|
-
"magenta",
|
|
98
|
-
"sunnyyellow",
|
|
99
|
-
"blueberry",
|
|
100
|
-
"lightbeige",
|
|
101
|
-
"seablue",
|
|
102
|
-
"lemongrass",
|
|
103
|
-
"orchid",
|
|
104
|
-
"redbean",
|
|
105
|
-
"orange",
|
|
106
|
-
"realblueberry",
|
|
107
|
-
]
|
|
82
|
+
default_textures = TextureThemes.TWENTY_FIVE_ROOMS
|
|
83
|
+
|
|
84
|
+
# Initialize goal positions for each room (1 goal per room at center)
|
|
85
|
+
goal_positions = GridRoomsEnvironment._generate_goal_positions(
|
|
86
|
+
5, room_size, goals_per_room=1
|
|
87
|
+
)
|
|
108
88
|
|
|
109
89
|
super().__init__(
|
|
110
90
|
grid_size=5,
|
|
111
91
|
connections=connections or default_connections,
|
|
112
92
|
textures=textures or default_textures,
|
|
93
|
+
goal_positions=goal_positions,
|
|
113
94
|
placed_room=placed_room,
|
|
114
95
|
obs_level=obs_level,
|
|
115
96
|
continuous=continuous,
|
miniworld_maze/tools/__init__.py
CHANGED
miniworld_maze/utils.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""Utility functions for miniworld_maze package."""
|
|
2
|
+
|
|
3
|
+
from typing import Tuple, Union
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def environment_to_pixel_coords(
|
|
8
|
+
env_pos: np.ndarray,
|
|
9
|
+
env_min: np.ndarray,
|
|
10
|
+
env_max: np.ndarray,
|
|
11
|
+
image_size: Union[int, Tuple[int, int]],
|
|
12
|
+
) -> Tuple[int, int]:
|
|
13
|
+
"""
|
|
14
|
+
Convert environment coordinates to pixel coordinates.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
env_pos: Environment position as ndarray [x, z] or [x, y]
|
|
18
|
+
env_min: Environment minimum bounds as ndarray [min_x, min_z] or [min_x, min_y]
|
|
19
|
+
env_max: Environment maximum bounds as ndarray [max_x, max_z] or [max_x, max_y]
|
|
20
|
+
image_size: Image size (width, height) or single size for square image
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Tuple of (pixel_x, pixel_y) coordinates
|
|
24
|
+
"""
|
|
25
|
+
env_x, env_y = env_pos[:2]
|
|
26
|
+
env_min_x, env_min_y = env_min[:2]
|
|
27
|
+
env_max_x, env_max_y = env_max[:2]
|
|
28
|
+
|
|
29
|
+
if isinstance(image_size, int):
|
|
30
|
+
width = height = image_size
|
|
31
|
+
else:
|
|
32
|
+
width, height = image_size
|
|
33
|
+
|
|
34
|
+
# Normalize to [0, 1] range and scale to pixel coordinates
|
|
35
|
+
pixel_x = int((env_x - env_min_x) / (env_max_x - env_min_x) * width)
|
|
36
|
+
pixel_y = int((env_y - env_min_y) / (env_max_y - env_min_y) * height)
|
|
37
|
+
|
|
38
|
+
return pixel_x, pixel_y
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def pixel_to_environment_coords(
|
|
42
|
+
pixel_pos: np.ndarray,
|
|
43
|
+
env_min: np.ndarray,
|
|
44
|
+
env_max: np.ndarray,
|
|
45
|
+
image_size: Union[int, Tuple[int, int]],
|
|
46
|
+
) -> Tuple[float, float]:
|
|
47
|
+
"""
|
|
48
|
+
Convert pixel coordinates to environment coordinates.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
pixel_pos: Pixel position as ndarray [x, y]
|
|
52
|
+
env_min: Environment minimum bounds as ndarray [min_x, min_z] or [min_x, min_y]
|
|
53
|
+
env_max: Environment maximum bounds as ndarray [max_x, max_z] or [max_x, max_y]
|
|
54
|
+
image_size: Image size (width, height) or single size for square image
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Tuple of (env_x, env_y) coordinates
|
|
58
|
+
"""
|
|
59
|
+
pixel_x, pixel_y = pixel_pos[:2]
|
|
60
|
+
env_min_x, env_min_y = env_min[:2]
|
|
61
|
+
env_max_x, env_max_y = env_max[:2]
|
|
62
|
+
|
|
63
|
+
if isinstance(image_size, int):
|
|
64
|
+
width = height = image_size
|
|
65
|
+
else:
|
|
66
|
+
width, height = image_size
|
|
67
|
+
|
|
68
|
+
# Convert to normalized [0, 1] range and scale to environment coordinates
|
|
69
|
+
env_x = pixel_x / width * (env_max_x - env_min_x) + env_min_x
|
|
70
|
+
env_y = pixel_y / height * (env_max_y - env_min_y) + env_min_y
|
|
71
|
+
|
|
72
|
+
return env_x, env_y
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def clamp_to_bounds(
|
|
76
|
+
value: Union[int, float, np.ndarray],
|
|
77
|
+
min_val: Union[int, float, np.ndarray],
|
|
78
|
+
max_val: Union[int, float, np.ndarray],
|
|
79
|
+
) -> Union[int, float, np.ndarray]:
|
|
80
|
+
"""
|
|
81
|
+
Clamp a value or array of values to specified bounds.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
value: Value(s) to clamp (scalar or ndarray)
|
|
85
|
+
min_val: Minimum bound(s) (scalar or ndarray)
|
|
86
|
+
max_val: Maximum bound(s) (scalar or ndarray)
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Clamped value(s)
|
|
90
|
+
"""
|
|
91
|
+
if isinstance(value, np.ndarray):
|
|
92
|
+
return np.clip(value, min_val, max_val)
|
|
93
|
+
else:
|
|
94
|
+
return max(min_val, min(max_val, value))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def clamp_pixel_coords(
|
|
98
|
+
pixel_x: int, pixel_y: int, image_size: Union[int, Tuple[int, int]]
|
|
99
|
+
) -> Tuple[int, int]:
|
|
100
|
+
"""
|
|
101
|
+
Clamp pixel coordinates to image bounds.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
pixel_x: X pixel coordinate
|
|
105
|
+
pixel_y: Y pixel coordinate
|
|
106
|
+
image_size: Image size (width, height) or single size for square image
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Tuple of clamped (pixel_x, pixel_y) coordinates
|
|
110
|
+
"""
|
|
111
|
+
if isinstance(image_size, int):
|
|
112
|
+
width = height = image_size
|
|
113
|
+
else:
|
|
114
|
+
width, height = image_size
|
|
115
|
+
|
|
116
|
+
clamped_x = max(0, min(width - 1, pixel_x))
|
|
117
|
+
clamped_y = max(0, min(height - 1, pixel_y))
|
|
118
|
+
|
|
119
|
+
return clamped_x, clamped_y
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def normalize_coordinates(
|
|
123
|
+
coords: np.ndarray,
|
|
124
|
+
min_bounds: np.ndarray,
|
|
125
|
+
max_bounds: np.ndarray,
|
|
126
|
+
) -> Tuple[float, float]:
|
|
127
|
+
"""
|
|
128
|
+
Normalize coordinates to [0, 1] range based on given bounds.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
coords: Coordinates to normalize as ndarray
|
|
132
|
+
min_bounds: Minimum bounds as ndarray
|
|
133
|
+
max_bounds: Maximum bounds as ndarray
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Normalized coordinates as (x, y) tuple
|
|
137
|
+
"""
|
|
138
|
+
x, y = coords[:2]
|
|
139
|
+
min_x, min_y = min_bounds[:2]
|
|
140
|
+
max_x, max_y = max_bounds[:2]
|
|
141
|
+
|
|
142
|
+
norm_x = (x - min_x) / (max_x - min_x)
|
|
143
|
+
norm_y = (y - min_y) / (max_y - min_y)
|
|
144
|
+
|
|
145
|
+
return norm_x, norm_y
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def denormalize_coordinates(
|
|
149
|
+
normalized_coords: np.ndarray,
|
|
150
|
+
min_bounds: np.ndarray,
|
|
151
|
+
max_bounds: np.ndarray,
|
|
152
|
+
) -> Tuple[float, float]:
|
|
153
|
+
"""
|
|
154
|
+
Convert normalized [0, 1] coordinates back to original coordinate space.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
normalized_coords: Normalized coordinates in [0, 1] range as ndarray
|
|
158
|
+
min_bounds: Original minimum bounds as ndarray
|
|
159
|
+
max_bounds: Original maximum bounds as ndarray
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Denormalized coordinates as (x, y) tuple
|
|
163
|
+
"""
|
|
164
|
+
norm_x, norm_y = normalized_coords[:2]
|
|
165
|
+
min_x, min_y = min_bounds[:2]
|
|
166
|
+
max_x, max_y = max_bounds[:2]
|
|
167
|
+
|
|
168
|
+
x = norm_x * (max_x - min_x) + min_x
|
|
169
|
+
y = norm_y * (max_y - min_y) + min_y
|
|
170
|
+
|
|
171
|
+
return x, y
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def get_environment_bounds(env) -> Tuple[Tuple[float, float], Tuple[float, float]]:
|
|
175
|
+
"""
|
|
176
|
+
Extract environment bounds from an environment object.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
env: Environment object (may be wrapped)
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Tuple of ((min_x, min_z), (max_x, max_z))
|
|
183
|
+
"""
|
|
184
|
+
# Unwrap environment to access base environment
|
|
185
|
+
base_env = env
|
|
186
|
+
while hasattr(base_env, "env"):
|
|
187
|
+
base_env = base_env.env
|
|
188
|
+
|
|
189
|
+
min_bounds = (base_env.min_x, base_env.min_z)
|
|
190
|
+
max_bounds = (base_env.max_x, base_env.max_z)
|
|
191
|
+
|
|
192
|
+
return min_bounds, max_bounds
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def calculate_view_size_from_bounds(
|
|
196
|
+
min_bounds: np.ndarray,
|
|
197
|
+
max_bounds: np.ndarray,
|
|
198
|
+
) -> Tuple[float, float]:
|
|
199
|
+
"""
|
|
200
|
+
Calculate view size (width, height) from coordinate bounds.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
min_bounds: Minimum bounds as ndarray (min_x, min_y)
|
|
204
|
+
max_bounds: Maximum bounds as ndarray (max_x, max_y)
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Tuple of (width, height)
|
|
208
|
+
"""
|
|
209
|
+
min_x, min_y = min_bounds[:2]
|
|
210
|
+
max_x, max_y = max_bounds[:2]
|
|
211
|
+
|
|
212
|
+
width = max_x - min_x
|
|
213
|
+
height = max_y - min_y
|
|
214
|
+
|
|
215
|
+
return width, height
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def scale_coordinates(
|
|
219
|
+
coords: np.ndarray,
|
|
220
|
+
scale_factor: Union[float, Tuple[float, float]],
|
|
221
|
+
) -> Tuple[float, float]:
|
|
222
|
+
"""
|
|
223
|
+
Scale coordinates by a given factor.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
coords: Coordinates to scale as ndarray
|
|
227
|
+
scale_factor: Scale factor (uniform) or (scale_x, scale_y)
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Scaled coordinates as (x, y) tuple
|
|
231
|
+
"""
|
|
232
|
+
x, y = coords[:2]
|
|
233
|
+
|
|
234
|
+
if isinstance(scale_factor, (tuple, list)):
|
|
235
|
+
scale_x, scale_y = scale_factor
|
|
236
|
+
else:
|
|
237
|
+
scale_x = scale_y = scale_factor
|
|
238
|
+
|
|
239
|
+
return x * scale_x, y * scale_y
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def distance_2d(
|
|
243
|
+
pos1: np.ndarray,
|
|
244
|
+
pos2: np.ndarray,
|
|
245
|
+
) -> float:
|
|
246
|
+
"""
|
|
247
|
+
Calculate 2D Euclidean distance between two positions.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
pos1: First position as ndarray (x, y)
|
|
251
|
+
pos2: Second position as ndarray (x, y)
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Euclidean distance
|
|
255
|
+
"""
|
|
256
|
+
x1, y1 = pos1[:2]
|
|
257
|
+
x2, y2 = pos2[:2]
|
|
258
|
+
|
|
259
|
+
return np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def lerp_2d(
|
|
263
|
+
pos1: np.ndarray,
|
|
264
|
+
pos2: np.ndarray,
|
|
265
|
+
t: float,
|
|
266
|
+
) -> Tuple[float, float]:
|
|
267
|
+
"""
|
|
268
|
+
Linear interpolation between two 2D positions.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
pos1: Start position as ndarray (x, y)
|
|
272
|
+
pos2: End position as ndarray (x, y)
|
|
273
|
+
t: Interpolation parameter [0, 1]
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Interpolated position as (x, y) tuple
|
|
277
|
+
"""
|
|
278
|
+
x1, y1 = pos1[:2]
|
|
279
|
+
x2, y2 = pos2[:2]
|
|
280
|
+
|
|
281
|
+
t = clamp_to_bounds(t, 0.0, 1.0)
|
|
282
|
+
|
|
283
|
+
x = x1 + t * (x2 - x1)
|
|
284
|
+
y = y1 + t * (y2 - y1)
|
|
285
|
+
|
|
286
|
+
return x, y
|