Skip to content

Commit 6d9e1a2

Browse files
Add MarlGrid matrix observations (#424)
## Summary - add semantic `matrix` and `full_matrix` observation formats for MarlGrid - keep existing pixel observations as the default via `observation_format="pixels"` - add direct MarlGrid tests for partial and full matrix observation shapes and channels - replace the `.dockerignore` symlink with explicit Docker ignore rules - fix property docstrings that fail the current ruff lint rule ## Tests - make bazel-test BAZEL_TEST_TARGETS=//envpool/marlgrid:marlgrid_test - uvx ruff check envpool/python/envpool.py envpool/python/protocol.py scripts/coverage_summary.py - uvx ruff format --check envpool/python/envpool.py envpool/python/protocol.py scripts/coverage_summary.py envpool/marlgrid/marlgrid_test.py - uvx clang-format --style=file -i envpool/marlgrid/marlgrid.h -n --Werror - git diff --check origin/main..HEAD Note: the branch was rebuilt on top of current `origin/main` after native MarlGrid prestige coloring landed upstream in #419. --------- Co-authored-by: Jiayi Weng <jiayi@openai.com>
1 parent 3446f28 commit 6d9e1a2

10 files changed

Lines changed: 415 additions & 135 deletions

File tree

docs/env/marlgrid.rst

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,40 @@ Options
3434
* ``prestige_beta (float)``: per-step prestige decay factor, default to
3535
``0.95``;
3636
* ``prestige_scale (float)``: reward-history scale used when mapping prestige
37-
to color, default to ``2.0``.
37+
to color, default to ``2.0``;
38+
* ``observation_format (str)``: observation representation. ``"pixels"``
39+
(default) returns the existing RGB partial view, ``"matrix"`` returns an
40+
egocentric semantic partial view, and ``"full_matrix"`` returns the
41+
world-oriented semantic grid.
3842

3943

4044
Observation Space
4145
-----------------
4246

43-
MarlGrid returns one RGB partial-view image per player. The default registered
44-
tasks use ``view_tile_size=8`` and expose ``obs`` as a uint8 tensor with shape
45-
``(view_tile_size * view_size, view_tile_size * view_size, 3)`` per player.
47+
MarlGrid returns one uint8 observation per player. Its shape depends on
48+
``observation_format``:
49+
50+
* ``pixels``: ``(view_tile_size * view_size, view_tile_size * view_size, 3)``.
51+
This is the existing RGB partial view. The default registered tasks use
52+
``view_tile_size=8``;
53+
* ``matrix``: ``(view_size, view_size, 13)``. This is the agent's egocentric
54+
partial view. Occluded cells and every cell for an inactive player are all
55+
zero;
56+
* ``full_matrix``: ``(grid_size, grid_size, 13)``. This is the global grid in
57+
world coordinates. Every cell for an inactive player is zero.
58+
59+
For both matrix formats, channels are:
60+
61+
* ``0``--``4``: one-hot empty, wall, goal, bonus, and lava base tiles;
62+
* ``5``: agent presence;
63+
* ``6``--``8``: agent red, green, and blue values. These contain the prestige
64+
color when ``prestige_coloring=True``;
65+
* ``9``--``12``: one-hot agent direction: right, down, left, and up.
66+
67+
One visible base-tile channel and every active one-hot channel use value
68+
``255``. An agent can occupy a base object, so base-tile and agent channels may
69+
both be set. Directions in ``matrix`` use the rotated egocentric coordinates;
70+
directions in ``full_matrix`` use world coordinates.
4671

4772
Player metadata is returned under ``info["players"]``:
4873

envpool/highway/highway_official_align_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ def _envpool_info(info: dict[str, Any]) -> dict[str, Any]:
149149

150150

151151
def _official_info(info: dict[str, Any]) -> dict[str, Any]:
152-
exposed = {
152+
exposed: dict[str, Any] = {
153153
key: info[key]
154154
for key in ("speed", "crashed", "is_success")
155155
if key in info

envpool/marlgrid/marlgrid.h

Lines changed: 142 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ using Rgb = std::array<std::uint8_t, 3>;
6666

6767
inline constexpr int kTilePixels = 32;
6868
inline constexpr int kTileSubdivs = 3;
69+
inline constexpr int kMatrixAgentChannel = 5;
70+
inline constexpr int kMatrixColorChannel = 6;
71+
inline constexpr int kMatrixDirectionChannel = 9;
72+
inline constexpr int kMatrixObsChannels = 13;
6973
inline constexpr double kPi = 3.14159265358979323846;
7074
inline constexpr Rgb kShadowColor = {35, 25, 30};
7175
inline constexpr Rgb kWorstColor = {74, 65, 42};
@@ -376,15 +380,32 @@ class MarlGridEnvFns {
376380
"reset_on_mistake"_.Bind(false), "reward_decay"_.Bind(true),
377381
"respawn"_.Bind(false), "ghost_mode"_.Bind(true),
378382
"prestige_coloring"_.Bind(false),
379-
"prestige_beta"_.Bind(0.95f), "prestige_scale"_.Bind(2.0f));
383+
"prestige_beta"_.Bind(0.95f), "prestige_scale"_.Bind(2.0f),
384+
"observation_format"_.Bind(std::string("pixels")));
380385
}
381386

382387
template <typename Config>
383388
static decltype(auto) StateSpec(const Config& conf) {
389+
const std::string observation_format = conf["observation_format"_];
390+
if (observation_format != "pixels" && observation_format != "matrix" &&
391+
observation_format != "full_matrix") {
392+
throw std::runtime_error(
393+
"MarlGrid observation_format must be 'pixels', 'matrix', or "
394+
"'full_matrix'");
395+
}
384396
int obs_size = conf["view_size"_] * conf["view_tile_size"_];
397+
int obs_channels = 3;
398+
if (observation_format == "matrix") {
399+
obs_size = conf["view_size"_];
400+
obs_channels = detail::kMatrixObsChannels;
401+
} else if (observation_format == "full_matrix") {
402+
obs_size = conf["grid_size"_];
403+
obs_channels = detail::kMatrixObsChannels;
404+
}
385405
int bound = conf["grid_size"_];
386406
return MakeDict(
387-
"obs"_.Bind(Spec<std::uint8_t>({-1, obs_size, obs_size, 3}, {0, 255})),
407+
"obs"_.Bind(Spec<std::uint8_t>({-1, obs_size, obs_size, obs_channels},
408+
{0, 255})),
388409
"info:players.id"_.Bind(Spec<int>({-1}, {0, conf["max_num_players"_]})),
389410
"info:players.done"_.Bind(Spec<bool>({-1})),
390411
"info:players.active"_.Bind(Spec<bool>({-1})),
@@ -423,6 +444,7 @@ class MarlGridEnv : public Env<MarlGridEnvSpec>, public RenderableEnv {
423444
prestige_coloring_(spec.config["prestige_coloring"_]),
424445
prestige_beta_(spec.config["prestige_beta"_]),
425446
prestige_scale_(spec.config["prestige_scale"_]),
447+
observation_format_(spec.config["observation_format"_]),
426448
max_episode_steps_(spec.config["max_episode_steps"_]) {
427449
CHECK_GE(max_num_players_, n_agents_);
428450
CHECK_GE(grid_size_, 3);
@@ -546,6 +568,7 @@ class MarlGridEnv : public Env<MarlGridEnvSpec>, public RenderableEnv {
546568
bool prestige_coloring_{false};
547569
float prestige_beta_{0.95f};
548570
float prestige_scale_{2.0f};
571+
std::string observation_format_{"pixels"};
549572
int max_episode_steps_{100};
550573
int step_count_{0};
551574
bool done_{true};
@@ -844,25 +867,29 @@ class MarlGridEnv : public Env<MarlGridEnvSpec>, public RenderableEnv {
844867
return detail::RenderAgentTile(agent.color, agent.dir, tile_size);
845868
}
846869

870+
[[nodiscard]] int TopAgentForCell(const detail::Cell* cell,
871+
int top_agent_id) const {
872+
if (cell == nullptr) {
873+
return -1;
874+
}
875+
if (top_agent_id >= 0 &&
876+
std::find(cell->agents.begin(), cell->agents.end(), top_agent_id) !=
877+
cell->agents.end() &&
878+
agents_[top_agent_id].active) {
879+
return top_agent_id;
880+
}
881+
for (int agent_id : cell->agents) {
882+
if (agents_[agent_id].active) {
883+
return agent_id;
884+
}
885+
}
886+
return -1;
887+
}
888+
847889
[[nodiscard]] std::vector<std::uint8_t> RenderTile(const detail::Cell* cell,
848890
int top_agent_id,
849891
int tile_size) const {
850-
int chosen_agent = -1;
851-
if (cell != nullptr) {
852-
if (top_agent_id >= 0 &&
853-
std::find(cell->agents.begin(), cell->agents.end(), top_agent_id) !=
854-
cell->agents.end() &&
855-
agents_[top_agent_id].active) {
856-
chosen_agent = top_agent_id;
857-
} else {
858-
for (int agent_id : cell->agents) {
859-
if (agents_[agent_id].active) {
860-
chosen_agent = agent_id;
861-
break;
862-
}
863-
}
864-
}
865-
}
892+
int chosen_agent = TopAgentForCell(cell, top_agent_id);
866893
if (cell == nullptr || cell->type == detail::CellType::kEmpty) {
867894
if (chosen_agent >= 0) {
868895
auto tile = RenderAgentTile(chosen_agent, tile_size);
@@ -900,38 +927,41 @@ class MarlGridEnv : public Env<MarlGridEnvSpec>, public RenderableEnv {
900927
}
901928
}
902929

903-
void RenderAgentObs(int agent_id, std::uint8_t* output) const {
904-
int obs_size = view_size_ * view_tile_size_;
905-
std::vector<std::uint8_t> image(obs_size * obs_size * 3, 0);
906-
for (int i = 0; i < obs_size * obs_size; ++i) {
907-
image[i * 3 + 0] = detail::kShadowColor[0];
908-
image[i * 3 + 1] = detail::kShadowColor[1];
909-
image[i * 3 + 2] = detail::kShadowColor[2];
910-
}
911-
const detail::Agent& agent = agents_[agent_id];
912-
if (!agent.active) {
913-
std::memcpy(output, image.data(), image.size());
914-
return;
915-
}
930+
[[nodiscard]] auto AgentViewCells(const detail::Agent& agent) const {
916931
int rot_k = (agent.dir + 1) % 4;
917-
int orientation = (4 - rot_k) % 4;
918932
auto [top_x, top_y] = ViewTopLeft(agent);
919-
std::vector<const detail::Cell*> view_cells(view_size_ * view_size_,
920-
nullptr);
921-
std::vector<bool> transparent(view_size_ * view_size_, true);
933+
std::vector<const detail::Cell*> cells(view_size_ * view_size_, nullptr);
934+
std::vector<bool> transparent(cells.size(), true);
922935
for (int y = 0; y < view_size_; ++y) {
923936
for (int x = 0; x < view_size_; ++x) {
924937
auto [sx, sy] = detail::RotateCoord(x, y, view_size_, rot_k);
925938
int wx = top_x + sx;
926939
int wy = top_y + sy;
927940
if (InBounds(wx, wy)) {
928941
const detail::Cell& cell = CellAt(wx, wy);
929-
view_cells[detail::Offset(x, y, view_size_)] = &cell;
942+
cells[detail::Offset(x, y, view_size_)] = &cell;
930943
transparent[detail::Offset(x, y, view_size_)] = cell.CanSeeBehind();
931944
}
932945
}
933946
}
934-
std::vector<bool> visible = VisibilityMask(transparent);
947+
return std::pair{std::move(cells), VisibilityMask(transparent)};
948+
}
949+
950+
void RenderAgentObs(int agent_id, std::uint8_t* output) const {
951+
int obs_size = view_size_ * view_tile_size_;
952+
std::vector<std::uint8_t> image(obs_size * obs_size * 3, 0);
953+
for (int i = 0; i < obs_size * obs_size; ++i) {
954+
image[i * 3 + 0] = detail::kShadowColor[0];
955+
image[i * 3 + 1] = detail::kShadowColor[1];
956+
image[i * 3 + 2] = detail::kShadowColor[2];
957+
}
958+
const detail::Agent& agent = agents_[agent_id];
959+
if (!agent.active) {
960+
std::memcpy(output, image.data(), image.size());
961+
return;
962+
}
963+
int orientation = 3 - agent.dir;
964+
auto [view_cells, visible] = AgentViewCells(agent);
935965
for (int y = 0; y < view_size_; ++y) {
936966
for (int x = 0; x < view_size_; ++x) {
937967
if (!visible[detail::Offset(x, y, view_size_)]) {
@@ -953,6 +983,73 @@ class MarlGridEnv : public Env<MarlGridEnvSpec>, public RenderableEnv {
953983
std::memcpy(output, image.data(), image.size());
954984
}
955985

986+
static int MatrixObsOffset(int x, int y, int channel, int obs_size) {
987+
return (y * obs_size + x) * detail::kMatrixObsChannels + channel;
988+
}
989+
990+
void WriteMatrixBaseTile(const detail::Cell* cell, int x, int y, int obs_size,
991+
std::uint8_t* output) const {
992+
int base_channel = cell == nullptr ? 0 : static_cast<int>(cell->type);
993+
output[MatrixObsOffset(x, y, base_channel, obs_size)] = 255;
994+
}
995+
996+
void WriteMatrixAgentTile(int chosen_agent, int x, int y, int obs_size,
997+
int orientation, std::uint8_t* output) const {
998+
if (chosen_agent < 0) {
999+
return;
1000+
}
1001+
detail::Rgb color =
1002+
prestige_coloring_
1003+
? detail::PrestigeColor(agents_[chosen_agent].prestige,
1004+
prestige_scale_)
1005+
: detail::ColorValue(agents_[chosen_agent].color);
1006+
output[MatrixObsOffset(x, y, detail::kMatrixAgentChannel, obs_size)] = 255;
1007+
std::copy(
1008+
color.begin(), color.end(),
1009+
output + MatrixObsOffset(x, y, detail::kMatrixColorChannel, obs_size));
1010+
int direction = (agents_[chosen_agent].dir + orientation) % 4;
1011+
output[MatrixObsOffset(x, y, detail::kMatrixDirectionChannel + direction,
1012+
obs_size)] = 255;
1013+
}
1014+
1015+
void WriteAgentMatrixObs(int agent_id, std::uint8_t* output) const {
1016+
int obs_values = view_size_ * view_size_ * detail::kMatrixObsChannels;
1017+
std::fill(output, output + obs_values, 0);
1018+
const detail::Agent& agent = agents_[agent_id];
1019+
if (!agent.active) {
1020+
return;
1021+
}
1022+
int orientation = 3 - agent.dir;
1023+
auto [view_cells, visible] = AgentViewCells(agent);
1024+
for (int y = 0; y < view_size_; ++y) {
1025+
for (int x = 0; x < view_size_; ++x) {
1026+
if (!visible[detail::Offset(x, y, view_size_)]) {
1027+
continue;
1028+
}
1029+
const detail::Cell* cell = view_cells[detail::Offset(x, y, view_size_)];
1030+
WriteMatrixBaseTile(cell, x, y, view_size_, output);
1031+
WriteMatrixAgentTile(TopAgentForCell(cell, agent_id), x, y, view_size_,
1032+
orientation, output);
1033+
}
1034+
}
1035+
}
1036+
1037+
void WriteAgentFullMatrixObs(int agent_id, std::uint8_t* output) const {
1038+
int obs_values = grid_size_ * grid_size_ * detail::kMatrixObsChannels;
1039+
std::fill(output, output + obs_values, 0);
1040+
if (!agents_[agent_id].active) {
1041+
return;
1042+
}
1043+
for (int y = 0; y < grid_size_; ++y) {
1044+
for (int x = 0; x < grid_size_; ++x) {
1045+
const detail::Cell& cell = CellAt(x, y);
1046+
WriteMatrixBaseTile(&cell, x, y, grid_size_, output);
1047+
WriteMatrixAgentTile(TopAgentForCell(&cell, agent_id), x, y, grid_size_,
1048+
0, output);
1049+
}
1050+
}
1051+
}
1052+
9561053
void ResizeNearest(const std::uint8_t* src, int src_width, int src_height,
9571054
std::uint8_t* dst, int dst_width, int dst_height) const {
9581055
for (int y = 0; y < dst_height; ++y) {
@@ -975,7 +1072,14 @@ class MarlGridEnv : public Env<MarlGridEnvSpec>, public RenderableEnv {
9751072
state["info:players.pos"_](i, 1) = agents_[i].y;
9761073
state["info:players.dir"_][i] = agents_[i].dir;
9771074
state["reward"_][i] = last_rewards_[i];
978-
RenderAgentObs(i, static_cast<std::uint8_t*>(state["obs"_][i].Data()));
1075+
auto* obs = static_cast<std::uint8_t*>(state["obs"_][i].Data());
1076+
if (observation_format_ == "matrix") {
1077+
WriteAgentMatrixObs(i, obs);
1078+
} else if (observation_format_ == "full_matrix") {
1079+
WriteAgentFullMatrixObs(i, obs);
1080+
} else {
1081+
RenderAgentObs(i, obs);
1082+
}
9791083
}
9801084
}
9811085
};

0 commit comments

Comments
 (0)