diff --git a/README.md b/README.md index 1540ab6..2d82af2 100644 --- a/README.md +++ b/README.md @@ -34,11 +34,15 @@ pip install thants #### Single Colony +The single colony environment follows the [Jumanji](https://github.com/instadeepai/jumanji) +environment API, with actions provided as an array of individual +actions: + ```python -from thants.envs import ThantsMonoColony +from thants.envs import ThantsMono import jax -env = ThantsMonoColony(dims=(50, 50)) +env = ThantsMono(dims=(50, 50)) key = jax.random.PRNGKey(101) state, obs = env.reset(key) state_history = [state] @@ -57,15 +61,15 @@ env.animate(state_history, 100, "mono_colony.gif") #### Multi-Colony In the multi-colony case each colony is treated independently (and can be -different sizes), so actions and observations are list/tuples of arrays/structs. +different sizes), so actions, observations, timesteps are list/tuples of +arrays/structs: ```python -from thants.envs import ThantsMultiColony +from thants.envs import Thants import jax import jax.numpy as jnp - -env = ThantsMultiColony((50, 100)) +env = Thants((50, 100)) key = jax.random.PRNGKey(101) state, obs = env.reset(key) state_history = [state] @@ -83,6 +87,9 @@ for _ in range(50): env.animate(state_history, 100, "multi_colony.gif") ``` +Preset simple environments can be imported from `thants.envs.ThantsDual` and +`thants.envs.ThantsQuad` with 2 and 4 colonies respectively. + ## Environment
diff --git a/src/thants/envs/__init__.py b/src/thants/envs/__init__.py index 1626d9a..7551fba 100644 --- a/src/thants/envs/__init__.py +++ b/src/thants/envs/__init__.py @@ -1,2 +1,3 @@ -from thants.envs.mono import ThantsMonoColony -from thants.envs.multi import ThantsMultiColony +from thants.envs.mono import ThantsMono +from thants.envs.multi import Thants +from thants.envs.presets import ThantsDual, ThantsQuad diff --git a/src/thants/envs/mono.py b/src/thants/envs/mono.py index f8caa13..a66b7e7 100644 --- a/src/thants/envs/mono.py +++ b/src/thants/envs/mono.py @@ -7,7 +7,7 @@ from jumanji.viewer import Viewer from matplotlib.animation import FuncAnimation -from thants.envs.multi import ThantsMultiColony +from thants.envs.multi import Thants from thants.generators.colonies.mono import ( BasicColonyGenerator, ColonyGenerator, @@ -20,7 +20,7 @@ from thants.types import Observations, State -class ThantsMonoColony(Environment): +class ThantsMono(Environment): """Environment with a single colony""" def __init__( @@ -78,7 +78,7 @@ def __init__( """ colony_generator = colony_generator or BasicColonyGenerator(25, 2, (5, 5)) colony_generator = SingleColonyWrapper(colony_generator) - self.env = ThantsMultiColony( + self.env = Thants( dims=dims, colonies_generator=colony_generator, food_generator=food_generator, @@ -141,6 +141,10 @@ def step( state, timestep = self.env.step(state, [actions]) return state, timestep[0] + @cached_property + def dims(self) -> tuple[int, int]: + return self.env.dims + @cached_property def num_agents(self) -> int: return self.env.num_agents[0] diff --git a/src/thants/envs/multi.py b/src/thants/envs/multi.py index 3908e0a..a893d92 100644 --- a/src/thants/envs/multi.py +++ b/src/thants/envs/multi.py @@ -11,8 +11,8 @@ from thants.actions import derive_actions from thants.generators.colonies.multi import ( - BasicColoniesGenerator, ColoniesGenerator, + DualBasicColoniesGenerator, ) from thants.generators.food import BasicFoodGenerator, FoodGenerator from thants.generators.terrain import OpenTerrainGenerator, TerrainGenerator @@ -31,7 +31,7 @@ from thants.viewer import ThantsMultiColonyViewer -class ThantsMultiColony(Environment): +class Thants(Environment): """ Thants environment with multiple colonies """ @@ -95,7 +95,7 @@ def __init__( self.deposit_food_amount = deposit_food_amount self.signal_deposit_amount = signal_deposit_amount self.max_steps = max_steps - self._colonies_generator = colonies_generator or BasicColoniesGenerator( + self._colonies_generator = colonies_generator or DualBasicColoniesGenerator( [25, 25], 2, (5, 5) ) self._food_generator = food_generator or BasicFoodGenerator((5, 5), 100, 1.0) diff --git a/src/thants/envs/presets.py b/src/thants/envs/presets.py new file mode 100644 index 0000000..2fd2f4b --- /dev/null +++ b/src/thants/envs/presets.py @@ -0,0 +1,132 @@ +from thants.envs.multi import Thants +from thants.generators.colonies.multi import ( + DualBasicColoniesGenerator, + QuadBasicColoniesGenerator, +) +from thants.generators.food import BasicFoodGenerator + + +class ThantsDual(Thants): + """ + Environment with two evenly sized and spaced rectangular colonies + """ + + def __init__( + self, + dims: tuple[int, int] = (50, 100), + n_agents: int = 36, + n_signals: int = 2, + nest_dims: tuple[int, int] = (5, 5), + food_drop_dims: tuple[int, int] = (5, 5), + food_drop_interval: int = 50, + max_steps: int = 10_000, + carry_capacity: float = 1.0, + take_food_amount: float = 0.1, + deposit_food_amount: float = 0.1, + signal_deposit_amount: float = 0.1, + ) -> None: + """ + Initialise the environment + + Parameters + ---------- + dims + Env dimensions + n_agents + Number of agents in each colony + n_signals + Number of signal channels + nest_dims + Rectangular dimensions of each colonies nest + food_drop_dims + Rectangular dimensions of food deposits + food_drop_interval + Interval between new randomly placed food deposits + max_steps + Maximum environment steps + carry_capacity + Ant food carrying capacity + take_food_amount + Max food that can be picked up by an ant in a single step + deposit_food_amount + Max food that can be dropped by an ant in a single step + signal_deposit_amount + Amount of signal deposited in a single step + """ + food_generator = BasicFoodGenerator(food_drop_dims, food_drop_interval) + super().__init__( + dims=dims, + colonies_generator=DualBasicColoniesGenerator( + n_agents=(n_agents, n_agents), n_signals=n_signals, nest_dims=nest_dims + ), + food_generator=food_generator, + max_steps=max_steps, + carry_capacity=carry_capacity, + take_food_amount=take_food_amount, + deposit_food_amount=deposit_food_amount, + signal_deposit_amount=signal_deposit_amount, + ) + + +class ThantsQuad(Thants): + """ + Environment with four evenly sized and spaced rectangular colonies + """ + + def __init__( + self, + dims: tuple[int, int] = (100, 100), + n_agents: int = 36, + n_signals: int = 2, + nest_dims: tuple[int, int] = (5, 5), + food_drop_dims: tuple[int, int] = (5, 5), + food_drop_interval: int = 100, + max_steps: int = 10_000, + carry_capacity: float = 1.0, + take_food_amount: float = 0.1, + deposit_food_amount: float = 0.1, + signal_deposit_amount: float = 0.1, + ) -> None: + """ + Initialise the environment + + Parameters + ---------- + dims + Env dimensions + n_agents + Number of agents in each colony + n_signals + Number of signal channels + nest_dims + Rectangular dimensions of each colonies nest + food_drop_dims + Rectangular dimensions of food deposits + food_drop_interval + Interval between new randomly placed food deposits + max_steps + Maximum environment steps + carry_capacity + Ant food carrying capacity + take_food_amount + Max food that can be picked up by an ant in a single step + deposit_food_amount + Max food that can be dropped by an ant in a single step + signal_deposit_amount + Amount of signal deposited in a single step + """ + food_generator = BasicFoodGenerator(food_drop_dims, food_drop_interval) + super().__init__( + dims=dims, + colonies_generator=QuadBasicColoniesGenerator( + n_agents=(n_agents, n_agents, n_agents, n_agents), + n_signals=n_signals, + nest_dims=nest_dims, + ), + food_generator=food_generator, + max_steps=max_steps, + carry_capacity=carry_capacity, + take_food_amount=take_food_amount, + deposit_food_amount=deposit_food_amount, + signal_deposit_amount=signal_deposit_amount, + ) diff --git a/src/thants/generators/colonies/mono.py b/src/thants/generators/colonies/mono.py index 316f04c..dbf0198 100644 --- a/src/thants/generators/colonies/mono.py +++ b/src/thants/generators/colonies/mono.py @@ -2,7 +2,7 @@ import chex -from thants.generators.colonies.utils import init_colony +from thants.generators.colonies.utils import BBox, init_colony from thants.types import Colony @@ -94,6 +94,5 @@ def __call__(self, dims: tuple[int, int], key: chex.PRNGKey) -> Colony: Colony Colony state containing ants, signals, and nest states """ - return init_colony( - dims, (0, 0), dims, self.nest_dims, self.n_agents, self.n_signals - ) + bounds = BBox(x0=(0, 0), x1=dims) + return init_colony(dims, bounds, self.nest_dims, self.n_agents, self.n_signals) diff --git a/src/thants/generators/colonies/multi.py b/src/thants/generators/colonies/multi.py index b9548b4..2169da4 100644 --- a/src/thants/generators/colonies/multi.py +++ b/src/thants/generators/colonies/multi.py @@ -4,7 +4,7 @@ import chex from thants.generators.colonies.mono import ColonyGenerator -from thants.generators.colonies.utils import init_colony +from thants.generators.colonies.utils import BBox, init_colonies from thants.types import Colony @@ -82,9 +82,9 @@ def __call__(self, dims: tuple[int, int], key: chex.PRNGKey) -> Sequence[Colony] return [self.generator(dims, key)] -class BasicColoniesGenerator(ColoniesGenerator): +class DualBasicColoniesGenerator(ColoniesGenerator): """ - Basic generator that create 2 evenly spaced colonies + Basic generator that create 2 evenly spaced rectangular colonies """ def __init__( @@ -122,21 +122,65 @@ def __call__(self, dims: tuple[int, int], key: chex.PRNGKey) -> Sequence[Colony] List of initialised colonies """ mid = dims[1] // 2 - return [ - init_colony( - dims, - (0, 0), - (dims[0], mid), - self.nest_dims, - self.n_agents[0], - self.n_signals, - ), - init_colony( - dims, - (0, mid), - (dims[0], dims[1]), - self.nest_dims, - self.n_agents[1], - self.n_signals, - ), + bounds = [ + BBox(x0=(0, 0), x1=(dims[0], mid)), + BBox(x0=(0, mid), x1=dims), ] + return init_colonies( + dims, self.nest_dims, self.n_agents, self.n_signals, bounds + ) + + +class QuadBasicColoniesGenerator(ColoniesGenerator): + """ + Basic generator that create 4 evenly spaced rectangular colonies + """ + + def __init__( + self, + n_agents: tuple[int, int, int, int], + n_signals: int, + nest_dims: tuple[int, int], + ) -> None: + """ + Initialise a basic generator + + Parameters + ---------- + n_agents + Number of agents in each colony + n_signals + Number of colony signal-channels + nest_dims + Rectangular nest dimensions + """ + self.nest_dims = nest_dims + super().__init__(n_agents, n_signals) + + def __call__(self, dims: tuple[int, int], key: chex.PRNGKey) -> Sequence[Colony]: + """ + Initialise the pair of colonies + + Parameters + ---------- + dims + Dimensions of the environment + key + JAX random key + + Returns + ------- + Sequence[Colony] + List of initialised colonies + """ + + mid = (dims[0] // 2, dims[1] // 2) + bounds = [ + BBox(x0=(0, 0), x1=mid), + BBox(x0=mid, x1=dims), + BBox(x0=(mid[0], 0), x1=(dims[0], mid[1])), + BBox(x0=(0, mid[1]), x1=(mid[0], dims[1])), + ] + return init_colonies( + dims, self.nest_dims, self.n_agents, self.n_signals, bounds + ) diff --git a/src/thants/generators/colonies/utils.py b/src/thants/generators/colonies/utils.py index 4c39be4..7593858 100644 --- a/src/thants/generators/colonies/utils.py +++ b/src/thants/generators/colonies/utils.py @@ -1,4 +1,6 @@ import math +from dataclasses import dataclass +from typing import Sequence import chex import jax.numpy as jnp @@ -25,10 +27,17 @@ def get_rectangular_indices(rec_dims: tuple[int, int]) -> chex.Array: return idxs +@dataclass +class BBox: + """Rectangular region bounding box""" + + x0: tuple[int, int] + x1: tuple[int, int] + + def init_colony( dims: tuple[int, int], - x0: tuple[int, int], - x1: tuple[int, int], + bounds: BBox, nest_dims: tuple[int, int], n_agents: int, n_signals: int, @@ -40,10 +49,8 @@ def init_colony( ---------- dims Environment dimensions - x0 - Ids of the origin of the rectangular region - x1 - Dimensions of the rectangular region + bounds + Bounding box of the rectangular region nest_dims Rectangular nest dimensions n_agents @@ -56,8 +63,8 @@ def init_colony( Colony Initialised colony """ - x0 = jnp.array(x0) - x1 = jnp.array(x1) + x0 = jnp.array(bounds.x0) + x1 = jnp.array(bounds.x1) dims = jnp.array(dims) centre = (x0 + ((x1 - x0) // 2))[jnp.newaxis] d = math.ceil(math.sqrt(n_agents)) @@ -79,3 +86,43 @@ def init_colony( signals = jnp.zeros((n_signals, *dims)) return Colony(ants=ants, signals=signals, nest=nest) + + +def init_colonies( + env_dims: tuple[int, int], + nest_dims: tuple[int, int], + n_agents: Sequence[BBox], + n_signals: int, + bounds: Sequence[BBox], +) -> Sequence[Colony]: + """ + Initialise multiple rectangular colonies + + Parameters + ---------- + env_dims + Environment dimensions + nest_dims + Rectangular nest dimensions + n_agents + Number of agents in each colony + n_signals + Number of signal channels + bounds + Bounding boxes of each colony + + Returns + ------- + Sequence[Colony] + List of initialised colonies + """ + return [ + init_colony( + env_dims, + b, + nest_dims, + n, + n_signals, + ) + for b, n in zip(bounds, n_agents) + ] diff --git a/tests/test_envs/test_mono.py b/tests/test_envs/test_mono.py index b493fb9..1716884 100644 --- a/tests/test_envs/test_mono.py +++ b/tests/test_envs/test_mono.py @@ -1,31 +1,32 @@ import chex import jax +import jax.numpy as jnp import pytest from jumanji.testing.env_not_smoke import ( check_env_does_not_smoke, check_env_specs_does_not_smoke, ) -from thants.envs.mono import ThantsMonoColony +from thants.envs.mono import ThantsMono from thants.generators.colonies.mono import BasicColonyGenerator from thants.generators.food import BasicFoodGenerator -from thants.types import Observations +from thants.types import Observations, State @pytest.fixture -def env() -> ThantsMonoColony: - dims = (50, 50) - colony_generator = BasicColonyGenerator(100, 2, (5, 5)) +def env() -> ThantsMono: + dims = (20, 20) + colony_generator = BasicColonyGenerator(16, 2, (5, 5)) food_generator = BasicFoodGenerator( - (2, 2), - 50, + (5, 5), + 5, ) - return ThantsMonoColony( + return ThantsMono( dims=dims, colony_generator=colony_generator, food_generator=food_generator ) -def test_env_does_not_smoke(env: ThantsMonoColony) -> None: +def test_env_does_not_smoke(env: ThantsMono) -> None: """Test that we can run an episode without any errors.""" env.max_steps = 20 @@ -35,6 +36,28 @@ def select_action(action_key: chex.PRNGKey, _state: Observations) -> chex.Array: check_env_does_not_smoke(env, select_action=select_action) -def test_env_specs_do_not_smoke(env: ThantsMonoColony) -> None: +def test_env_specs_do_not_smoke(env: ThantsMono) -> None: """Test that we can access specs without any errors.""" check_env_specs_does_not_smoke(env) + + +def state_checks(env: ThantsMono, state: State) -> None: + assert isinstance(state, State) + assert state.food.shape == env.dims + assert state.terrain.shape == env.dims + assert state.colonies.ants.pos.shape == (env.num_agents, 2) + assert state.colonies.nests.shape == env.dims + assert jnp.all( + jnp.logical_not(jnp.logical_and(state.food > 0.0, state.colonies.nests > 0)) + ) + + +def test_env_outputs(key: chex.Array, env: ThantsMono) -> None: + state, obs = env.reset(key) + + state_checks(env, state) + + actions = jnp.zeros((env.num_agents,), dtype=int) + state, obs = env.step(state, actions) + + state_checks(env, state) diff --git a/tests/test_envs/test_multi.py b/tests/test_envs/test_multi.py index e495801..428f797 100644 --- a/tests/test_envs/test_multi.py +++ b/tests/test_envs/test_multi.py @@ -1,29 +1,31 @@ import chex -import jax.random +import jax +import jax.numpy as jnp import pytest -from thants.envs.multi import ThantsMultiColony -from thants.generators.colonies.multi import BasicColoniesGenerator +from thants.envs.multi import Thants +from thants.generators.colonies.multi import DualBasicColoniesGenerator from thants.generators.food import BasicFoodGenerator from thants.types import Observations, State @pytest.fixture -def env() -> ThantsMultiColony: - dims = (50, 100) - colony_generator = BasicColoniesGenerator((64, 36), 2, (5, 5)) +def env() -> Thants: + dims = (20, 40) + colony_generator = DualBasicColoniesGenerator((16, 9), 2, (5, 5)) food_generator = BasicFoodGenerator( - (2, 2), - 50, + (5, 5), + 5, ) - return ThantsMultiColony( + return Thants( dims=dims, colonies_generator=colony_generator, food_generator=food_generator ) -def test_env_does_not_smoke(key: chex.Array, env: ThantsMultiColony) -> None: +def test_env_does_not_smoke(key: chex.Array, env: Thants) -> None: """Test that we can run an episode without any errors.""" env.max_steps = 100 + n_steps = 50 def step(_state: State, _: None) -> tuple[State, list[Observations]]: k1, k2 = jax.random.split(_state.key, 2) @@ -36,9 +38,20 @@ def step(_state: State, _: None) -> tuple[State, list[Observations]]: state, _ = env.reset(key) - state, obs = jax.lax.scan(step, state, None, 50) + state, obs = jax.lax.scan(step, state, None, n_steps) assert isinstance(state, State) + assert state.food.shape == env.dims + assert state.terrain.shape == env.dims + assert state.colonies.ants.pos.shape == (sum(env.num_agents), 2) + assert state.colonies.nests.shape == env.dims + assert jnp.all( + jnp.logical_not(jnp.logical_and(state.food > 0.0, state.colonies.nests > 0)) + ) + assert isinstance(obs, list) assert len(obs) == 2 assert all([isinstance(x, Observations) for x in obs]) + + for n, o in zip(env.num_agents, obs): + assert o.ants.shape == (n_steps, n, 2, 9) diff --git a/tests/test_envs/test_presets.py b/tests/test_envs/test_presets.py new file mode 100644 index 0000000..219324e --- /dev/null +++ b/tests/test_envs/test_presets.py @@ -0,0 +1,47 @@ +from typing import Type + +import chex +import jax +import pytest + +from thants.envs import ThantsDual, ThantsQuad +from thants.types import Observations + + +@pytest.mark.parametrize( + "env_type, dims", + [ + (ThantsDual, (20, 10)), + (ThantsQuad, (20, 20)), + ], +) +def test_env_runs( + key: chex.PRNGKey, + env_type: Type[ThantsDual | ThantsQuad], + dims: tuple[int, int], +) -> None: + n_steps = 10 + n_agents = 9 + n_signals = 2 + + env = env_type(dims=dims, n_agents=n_agents, nest_dims=(3, 3), n_signals=n_signals) + + env.max_steps = 100 + state, _ = env.reset(key) + + def step(_state, _): + k = jax.random.split(_state.key, env.num_colonies) + actions = [jax.random.choice(_k, 9, (n,)) for _k, n in zip(k, env.num_agents)] + _state, timesteps = env.step(_state, actions) + return _state, [t.observation for t in timesteps] + + final_state, obs = jax.lax.scan(step, state, None, length=n_steps) + + assert isinstance(obs, list) + assert all([isinstance(x, Observations)] for x in obs) + + for o in obs: + assert o.food.shape == (n_steps, n_agents, 9) + assert o.signals.shape == (n_steps, n_agents, n_signals, 9) + assert o.terrain.shape == (n_steps, n_agents, 9) + assert o.ants.shape == (n_steps, n_agents, env.num_colonies, 9) diff --git a/tests/test_generators/test_food_generator.py b/tests/test_generators/test_food_generator.py index 53bf27d..94756b2 100644 --- a/tests/test_generators/test_food_generator.py +++ b/tests/test_generators/test_food_generator.py @@ -1,13 +1,27 @@ +import chex +import jax import jax.numpy as jnp from thants.generators.food import BasicFoodGenerator -def test_food_generator(key) -> None: +def test_food_generator(key: chex.PRNGKey) -> None: env_dims = (5, 10) + k1, k2 = jax.random.split(key) + food_generator = BasicFoodGenerator((2, 2), 10, 0.5) - food = food_generator.init(env_dims, key) + food = food_generator.init(env_dims, k1) + + assert food.shape == env_dims + assert jnp.isclose(jnp.sum(food), 2.0) + + food = food_generator.update(k2, 4, food) assert food.shape == env_dims assert jnp.isclose(jnp.sum(food), 2.0) + + food = food_generator.update(k2, 9, food) + + assert food.shape == env_dims + assert jnp.isclose(jnp.sum(food), 4.0) diff --git a/tests/test_generators/test_mono_colony_generators.py b/tests/test_generators/test_mono_colony_generators.py index 291af36..26013b5 100644 --- a/tests/test_generators/test_mono_colony_generators.py +++ b/tests/test_generators/test_mono_colony_generators.py @@ -6,8 +6,9 @@ def test_basic_colony_generator(key) -> None: env_dims = (5, 10) n_agents = 8 + n_signals = 2 - colony_generator = BasicColonyGenerator(n_agents, 2, (2, 2)) + colony_generator = BasicColonyGenerator(n_agents, n_signals, (2, 2)) colony = colony_generator(env_dims, key) @@ -21,3 +22,5 @@ def test_basic_colony_generator(key) -> None: assert colony.nest.shape == env_dims assert jnp.isclose(jnp.sum(colony.nest), 4) + assert colony.ants.carrying.shape == (n_agents,) + assert colony.signals.shape == (n_signals, *env_dims) diff --git a/tests/test_generators/test_multi_colony_generators.py b/tests/test_generators/test_multi_colony_generators.py index 7849d43..26e0f38 100644 --- a/tests/test_generators/test_multi_colony_generators.py +++ b/tests/test_generators/test_multi_colony_generators.py @@ -1,23 +1,28 @@ import jax.numpy as jnp -from thants.generators.colonies.multi import BasicColoniesGenerator +from thants.generators.colonies.multi import DualBasicColoniesGenerator from thants.types import Colony -def test_colony_generator(key): - dims = (50, 100) - generator = BasicColoniesGenerator([25, 25], 2, (5, 5)) +def test_colony_generator(key) -> None: + dims = (25, 50) + n_agents = (25, 16) + generator = DualBasicColoniesGenerator(n_agents, 2, (5, 5)) colonies = generator(dims, key) assert isinstance(colonies, list) assert len(colonies) == 2 assert all([isinstance(c, Colony) for c in colonies]) - assert all([c.ants.pos.shape == (25, 2) for c in colonies]) + assert all( + [c.ants.pos.shape == (n, 2) for c, n in zip(colonies, generator.n_agents)] + ) occupation = jnp.zeros(dims, dtype=int) - pos_0 = colonies[0].ants.pos - occupation = occupation.at[pos_0[:, 0], pos_0[:, 1]].add(1) - pos_1 = colonies[1].ants.pos - occupation = occupation.at[pos_1[:, 0], pos_1[:, 1]].add(1) - assert jnp.sum(occupation) == 50 + for n, colony in zip(generator.n_agents, colonies): + pos = colony.ants.pos + assert pos.shape == (n, 2) + assert jnp.sum(colony.nest) == 25 + occupation = occupation.at[pos[:, 0], pos[:, 1]].add(1) + + assert jnp.sum(occupation) == sum(n_agents) assert jnp.max(occupation) == 1 diff --git a/tests/test_rewards.py b/tests/test_rewards.py index 8546cba..a98a21a 100644 --- a/tests/test_rewards.py +++ b/tests/test_rewards.py @@ -5,7 +5,7 @@ from thants.types import Ants, Colony, State -def test_delivered_food_rewards(): +def test_delivered_food_rewards() -> None: dims = (3, 1) nest = jnp.ones(dims, dtype=bool).at[0, 0].set(False) pos = jnp.array([[0, 0], [1, 0], [2, 0]]) diff --git a/tests/test_viewer.py b/tests/test_viewer.py new file mode 100644 index 0000000..835e6c6 --- /dev/null +++ b/tests/test_viewer.py @@ -0,0 +1,42 @@ +import chex +import jax.numpy as jnp +import pytest +from matplotlib.animation import FuncAnimation +from matplotlib.pyplot import Figure + +from thants.envs.multi import Thants +from thants.generators.colonies.multi import DualBasicColoniesGenerator +from thants.generators.food import BasicFoodGenerator + + +@pytest.fixture +def env() -> Thants: + dims = (20, 40) + colony_generator = DualBasicColoniesGenerator((16, 9), 2, (5, 5)) + food_generator = BasicFoodGenerator( + (5, 5), + 5, + ) + return Thants( + dims=dims, colonies_generator=colony_generator, food_generator=food_generator + ) + + +def test_render(monkeypatch, key: chex.PRNGKey, env: Thants) -> None: + monkeypatch.setattr(Figure, "show", lambda _: None) + state, _ = env.reset(key) + env.render(state) + + +def test_animation(key: chex.PRNGKey, env: Thants) -> None: + state, _ = env.reset(key) + + states = [state] + + for _ in range(2): + actions = [jnp.zeros((n,), dtype=int) for n in env.num_agents] + state, _ = env.step(state, actions) + states.append(state) + + animation = env.animate(states) + assert isinstance(animation, FuncAnimation)