diff --git a/.github/images/thants_env.gif b/.github/images/thants_env.gif
new file mode 100644
index 0000000..f371b6a
Binary files /dev/null and b/.github/images/thants_env.gif differ
diff --git a/.github/workflows/pre-merge.yaml b/.github/workflows/pre-merge.yaml
index bfcf48b..27ccace 100644
--- a/.github/workflows/pre-merge.yaml
+++ b/.github/workflows/pre-merge.yaml
@@ -33,18 +33,3 @@ jobs:
installer-parallel: true
- run: poetry install
- run: pytest -vv tests
- build-docs:
- if: github.event.pull_request.draft == false
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v5
- - uses: actions/setup-python@v5
- with:
- python-version: '3.10'
- - uses: snok/install-poetry@v1
- with:
- version: 1.5.1
- virtualenvs-create: false
- installer-parallel: true
- - run: poetry install
- - run: task docs
diff --git a/README.md b/README.md
index 2539eda..0933af2 100644
--- a/README.md
+++ b/README.md
@@ -85,6 +85,13 @@ env.animate(state_history, 100, "multi_colony.gif")
## Environment
+
+

+
+
A Thants environment with two competing colonies.
+
+
+
The environment is modelled as a grid, wrapped at the boundaries. Ants (the agents)
occupy individual cells on the grid (and cannot overlap). Ants can pick up, carry,
and deposit food, or deposit persistent signals that can be observed by other ants
diff --git a/src/thants/common/types.py b/src/thants/common/types.py
index 397ced4..5da1151 100644
--- a/src/thants/common/types.py
+++ b/src/thants/common/types.py
@@ -79,3 +79,18 @@ class Observations:
nest: chex.Array # (n_ants, 9)
terrain: chex.Array # (n_ants, 9)
carrying: chex.Array # (n_ants,)
+
+
+@dataclass
+class ColorScheme:
+ """
+ Visualisation color-scheme, given as an array of rgb values
+
+ ants: Array of colors for each colony
+ food: Color to represent food
+ terrain: Array of blocked/passable terrain cells
+ """
+
+ ants: chex.Array # (n-colonies, 4)
+ food: chex.Array # (4,)
+ terrain: chex.Array # (2, 4)
diff --git a/src/thants/common/utils.py b/src/thants/common/utils.py
index 231b88a..719a070 100644
--- a/src/thants/common/utils.py
+++ b/src/thants/common/utils.py
@@ -2,10 +2,11 @@
import chex
import jax.numpy as jnp
+from matplotlib import color_sequences
from matplotlib.axes import Axes
from matplotlib.figure import Figure
-from thants.common.types import Ants, Colony
+from thants.common.types import Ants, Colony, ColorScheme
def get_rectangular_indices(rec_dims: tuple[int, int]) -> chex.Array:
@@ -109,3 +110,24 @@ def format_plot(
ax.set_ylim(-0.5, env_dims[0] - 0.5)
return fig, ax
+
+
+def get_color_scheme(color_sequence: str, n_colonies: int) -> ColorScheme:
+ """
+ Get a environment visualisation colour scheme from a matplotlib sequence
+
+ Parameters
+ ----------
+ color_sequence
+ Matplotlib color-sequence name
+ n_colonies
+ Number of colonies to visualise
+
+ Returns
+ -------
+ ColorScheme
+ Environment visualisation color-scheme
+ """
+ colors = color_sequences[color_sequence]
+ colors = jnp.array([(*i, 1.0) for i in colors[: 3 + n_colonies]])
+ return ColorScheme(terrain=colors[:2], food=colors[2], ants=colors[3:])
diff --git a/src/thants/mono/viewer.py b/src/thants/mono/viewer.py
index 614bb8a..b66c263 100644
--- a/src/thants/mono/viewer.py
+++ b/src/thants/mono/viewer.py
@@ -1,4 +1,3 @@
-from functools import partial
from typing import Optional, Sequence, Tuple
import chex
@@ -10,27 +9,30 @@
from matplotlib.image import AxesImage
from numpy.typing import NDArray
-from thants.common.utils import format_plot
+from thants.common.types import ColorScheme
+from thants.common.utils import format_plot, get_color_scheme
from thants.mono.types import State
-def _draw_env(state: State) -> tuple[chex.Array, chex.Array]:
- terrain = state.terrain.astype(float)
- terrain = jnp.stack([terrain, terrain, terrain, jnp.ones_like(terrain)], axis=2)
- trans_colors = jnp.array([1.0, 0.0, 0.0, 0.5])
- nest = state.colony.nest[:, :, jnp.newaxis] * trans_colors[jnp.newaxis]
- return terrain, nest
+def _draw_env(
+ state: State, colors: ColorScheme
+) -> tuple[chex.Array, chex.Array, chex.Array]:
+ terrain = state.terrain.astype(int)
+ terrain = colors.terrain.at[terrain].get()
+ nest_colors = jnp.clip(colors.ants + 0.1, 0.0, 1.0)
+ nest = state.colony.nest[:, :, jnp.newaxis] * nest_colors[jnp.newaxis]
+ food = jnp.full((*state.food.shape, 4), colors.food)
+ return terrain, nest, food
-@partial(jax.jit, static_argnames="dims")
-def _draw_ants(dims: tuple[int, int], state: State) -> tuple[chex.Array, chex.Array]:
+@jax.jit
+def _draw_ants(state: State, colors: ColorScheme) -> chex.Array:
+ dims = state.food.shape
ants = jnp.zeros((*dims, 4))
- color = jnp.array([1.0, 0.0, 0.0, 1.0])
- ants = ants.at[state.colony.ants.pos[:, 0], state.colony.ants.pos[:, 1]].set(color)
- food = jnp.stack(
- [jnp.zeros(dims), jnp.ones(dims), jnp.zeros(dims), state.food], axis=2
+ ants = ants.at[state.colony.ants.pos[:, 0], state.colony.ants.pos[:, 1]].set(
+ colors.ants[0]
)
- return ants, food
+ return ants
class ThantsViewer(MatplotlibViewer[State]):
@@ -38,6 +40,7 @@ def __init__(
self,
name: str = "thants",
render_mode: str = "human",
+ color_sequence: str = "tab20",
) -> None:
"""
Thants environment visualiser using Matplotlib
@@ -48,7 +51,10 @@ def __init__(
Plot name, default ``thants``
render_mode
Default ``human``
+ color_sequence
+ Matplotlib colour sequence to sample from
"""
+ self.color_sequence = color_sequence
super().__init__(name, render_mode)
def _set_figure_size(self, dims: tuple[int, int]) -> None:
@@ -77,6 +83,7 @@ def render(
dims = state.food.shape
self._set_figure_size(dims)
fig, ax = self._get_fig_ax(padding=0.01)
+ ax.clear()
fig, ax = format_plot(fig, ax, dims)
self._draw(ax, state)
@@ -114,18 +121,12 @@ def animate(
fig, ax = format_plot(fig, ax, env_dims)
plt.close(fig=fig)
- terrain, nest = _draw_env(states[0])
- ax.imshow(terrain)
- ax.imshow(nest)
-
- ants, food = _draw_ants(env_dims, states[0])
- food_img = ax.imshow(food)
- ants_img = ax.imshow(ants)
+ colors, ants_img, food_img = self._draw(ax, states[0])
def make_frame(state: State) -> tuple[AxesImage, AxesImage]:
- step_ants, step_food = _draw_ants(env_dims, state)
- food_img.set_data(step_food)
+ step_ants = _draw_ants(state, colors)
ants_img.set_data(step_ants)
+ food_img.set_alpha(jnp.clip(state.food, 0.0, 1.0))
return ants_img, food_img
self._animation = matplotlib.animation.FuncAnimation(
@@ -141,17 +142,21 @@ def make_frame(state: State) -> tuple[AxesImage, AxesImage]:
return self._animation
- def _draw(self, ax: plt.Axes, state: State) -> None:
- ax.clear()
- env_dims = state.terrain.shape
+ def _draw(
+ self, ax: plt.Axes, state: State
+ ) -> tuple[ColorScheme, AxesImage, AxesImage]:
+ colors = get_color_scheme(self.color_sequence, 1)
+
+ terrain, nest, food = _draw_env(state, colors)
+ ants = _draw_ants(state, colors)
- terrain, nest = _draw_env(state)
ax.imshow(terrain)
ax.imshow(nest)
- ants, food = _draw_ants(env_dims, state)
- ax.imshow(food)
- ax.imshow(ants)
+ food_img = ax.imshow(food, alpha=state.food)
+ ants_img = ax.imshow(ants)
+
+ return colors, ants_img, food_img
def _get_fig_ax(
self,
diff --git a/src/thants/multi/viewer.py b/src/thants/multi/viewer.py
index da2ee77..41b6e00 100644
--- a/src/thants/multi/viewer.py
+++ b/src/thants/multi/viewer.py
@@ -1,4 +1,3 @@
-from functools import partial
from typing import Optional, Sequence, Tuple
import chex
@@ -6,45 +5,39 @@
import jax.numpy as jnp
import matplotlib.animation
import matplotlib.pyplot as plt
-import numpy as np
from jumanji.viewer import MatplotlibViewer
-from matplotlib import colormaps
from matplotlib.image import AxesImage
from numpy.typing import NDArray
-from thants.common.utils import format_plot
+from thants.common.types import ColorScheme
+from thants.common.utils import format_plot, get_color_scheme
from thants.multi.types import State
-def draw_env(state: State, colors: chex.Array) -> tuple[chex.Array, chex.Array]:
- terrain = state.terrain.astype(float)
- terrain = jnp.stack([terrain, terrain, terrain, jnp.ones_like(terrain)], axis=2)
-
- trans_colors = colors * jnp.array([[1.0, 1.0, 1.0, 0.5]])
-
+def _draw_env(
+ state: State, colors: ColorScheme
+) -> tuple[chex.Array, chex.Array, chex.Array]:
+ terrain = state.terrain.astype(int)
+ terrain = colors.terrain.at[terrain].get()
+ nest_colors = jnp.clip(colors.ants + 0.1, 0.0, 1.0)
nests = [
- colony.nest[:, :, jnp.newaxis] * trans_colors[i, jnp.newaxis]
+ colony.nest[:, :, jnp.newaxis] * nest_colors[i, jnp.newaxis]
for i, colony in enumerate(state.colonies)
]
nests = jnp.sum(jnp.stack(nests, axis=0), axis=0)
+ food = jnp.full((*state.food.shape, 4), colors.food)
+ return terrain, nests, food
- return terrain, nests
-
-@partial(jax.jit, static_argnames="dims")
-def draw_ants(
- dims: tuple[int, int], state: State, colors: chex.Array
-) -> tuple[chex.Array, chex.Array]:
+@jax.jit
+def _draw_ants(state: State, colors: ColorScheme) -> chex.Array:
+ dims = state.food.shape
ants = jnp.zeros((*dims, 4))
for i, colony in enumerate(state.colonies):
- ants = ants.at[colony.ants.pos[:, 0], colony.ants.pos[:, 1]].set(colors[i])
-
- food = jnp.stack(
- [jnp.zeros(dims), jnp.ones(dims), jnp.zeros(dims), state.food], axis=2
- )
+ ants = ants.at[colony.ants.pos[:, 0], colony.ants.pos[:, 1]].set(colors.ants[i])
- return ants, food
+ return ants
class ThantsMultiColonyViewer(MatplotlibViewer[State]):
@@ -52,7 +45,7 @@ def __init__(
self,
name: str = "thants",
render_mode: str = "human",
- colony_colormap: str = "plasma",
+ color_sequence: str = "tab20",
) -> None:
"""
Thants multi-colony environment visualiser using Matplotlib
@@ -63,15 +56,12 @@ def __init__(
Plot name, default ``thants``
render_mode
Default ``human``
- colony_colormap
-
+ color_sequence
+ Matplotlib colour sequence to sample from
"""
- self.cmap = colormaps[colony_colormap]
+ self.color_sequence = color_sequence
super().__init__(name, render_mode)
- def _get_colony_colors(self, n: int) -> chex.Array:
- return jnp.array(self.cmap(np.linspace(0, 1, n)))
-
def _set_figure_size(self, dims: tuple[int, int]) -> None:
longest = max(dims[0], dims[1])
f_dims = (10.0 * dims[1] / longest, 10.0 * dims[0] / longest)
@@ -98,6 +88,7 @@ def render(
dims = state.food.shape
self._set_figure_size(dims)
fig, ax = self._get_fig_ax(padding=0.01)
+ ax.clear()
fig, ax = format_plot(fig, ax, dims)
self._draw(ax, state)
@@ -136,20 +127,12 @@ def animate(
fig, ax = format_plot(fig, ax, dims)
plt.close(fig=fig)
- colors = self._get_colony_colors(len(states[0].colonies))
-
- terrain, nests = draw_env(states[0], colors)
- ants, food = draw_ants(dims, states[0], colors)
-
- ax.imshow(terrain, cmap="grey")
- ax.imshow(nests)
- food_img = ax.imshow(food)
- ants_img = ax.imshow(ants)
+ colors, ants_img, food_img = self._draw(ax, states[0])
def make_frame(state: State) -> tuple[AxesImage, AxesImage]:
- step_ants, step_food = draw_ants(dims, state, colors)
+ step_ants = _draw_ants(state, colors)
ants_img.set_data(step_ants)
- food_img.set_data(step_food)
+ food_img.set_alpha(jnp.clip(state.food, 0.0, 1.0))
return ants_img, food_img
self._animation = matplotlib.animation.FuncAnimation(
@@ -165,18 +148,20 @@ def make_frame(state: State) -> tuple[AxesImage, AxesImage]:
return self._animation
- def _draw(self, ax: plt.Axes, state: State) -> None:
- ax.clear()
- dims = state.food.shape
- colors = self._get_colony_colors(len(state.colonies))
+ def _draw(
+ self, ax: plt.Axes, state: State
+ ) -> tuple[ColorScheme, AxesImage, AxesImage]:
+ colors = get_color_scheme(self.color_sequence, len(state.colonies))
- terrain, nests = draw_env(state, colors)
- ants, food = draw_ants(dims, state, colors)
+ terrain, nests, food = _draw_env(state, colors)
+ ants = _draw_ants(state, colors)
- ax.imshow(terrain, cmap="grey")
+ ax.imshow(terrain)
ax.imshow(nests)
- ax.imshow(food)
- ax.imshow(ants)
+ food_img = ax.imshow(food, alpha=state.food)
+ ants_img = ax.imshow(ants)
+
+ return colors, ants_img, food_img
def _get_fig_ax(
self,