diff --git a/README.md b/README.md index 32ea606..7ac7797 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,10 @@ # ants A reimplementation of Schweitzer et al. 1996 as well as additional improvemnts for the Course Agent Based Modelling for Social Systems FS2023 ETH Zürich + + +For the course [Agent Based Modelling for Social Systems FS2023](https://www.vorlesungen.ethz.ch/Vorlesungsverzeichnis/lerneinheit.view?lerneinheitId=167292&semkez=2023S&ansicht=LEHRVERANSTALTUNGEN&lang=de) we were tasked to implement a model of our own (in groups). +For this, we decided to implement an enhanced version of [Active random walkers simulate trunk trail formation by ants (Schweitzer et al. 1996)](https://www.sciencedirect.com/science/article/pii/S030326479601670X?casa_token=fv82ToWDN3cAAAAA:wB5hHIlxnYBBvyuHb98YUFpXqWqGt50xDRnmAZ_UaMS5khR9IiH8K6m1b5gdqkAe1ACXx_lEy2U) using Python and Mesa. + + +For now, wanted features can be found in our [shortlist](shortlist.md). +For everything else start at [main py](main.py) diff --git a/agent.py b/agent.py new file mode 100644 index 0000000..e1f1148 --- /dev/null +++ b/agent.py @@ -0,0 +1,59 @@ +""" +agent.py - Part of ants project + +This model implements the actual agents on the grid (a.k.a. the ants) + +License: AGPL 3 (see end of file) +(C) Alexander Bocken, Viviane Fahrni, Grace Kragho +""" +import numpy as np +from mesa.agent import Agent +from mesa.space import Coordinate + + +class RandomWalkerAnt(Agent): + def __init__(self, unique_id, model, do_follow_chemical_A=True, + energy_0=1, chemical_drop_rate_0=1, sensitvity_0=1, alpha=0.5)-> None: + super().__init__(unique_id=unique_id, model=model) + + self._next_pos : None | Coordinate = None + + self.prev_pos = None + self.do_follow_chemical_A : bool = True # False -> follow_chemical_B = True + self.energy : float = energy_0 + self.sensitvity : float = sensitvity_0 + self.chemical_drop_rate : float = chemical_drop_rate_0 #TODO: check whether needs to be separated into A and B + self.alpha = alpha + + def step(self): + pass + + def advance(self) -> None: + self.pos = self._next_pos + + @property + def front_neighbors(self): + if self.prev_pos is not None: + x, y = self.pos + x_prev, y_prev = self.prev_pos + dx, dy = x - x_prev, y - y_prev + front = [ + (x, y + dy), + (x + dx, y), + (x + dx, y + dy), + ] + return front #TODO: verify (do we need to sperate into even/odd?) + else: + # TODO: return all neighbors or raise Exception? + pass + + + + +""" +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License along with this program. If not, see +""" diff --git a/main.py b/main.py new file mode 100755 index 0000000..b92000e --- /dev/null +++ b/main.py @@ -0,0 +1,49 @@ +#!/bin/python +""" +main.py - Part of ants project +execute via `python main.py` in terminal or only UNIX: `./main.py` + +License: AGPL 3 (see end of file) +(C) Alexander Bocken, Viviane Fahrni, Grace Kragho +""" +from model import ActiveWalkerModel +from agent import RandomWalkerAnt +import numpy as np +import matplotlib.pyplot as plt +from mesa.space import Coordinate + +def main(): + width = 21 + height = width + num_initial_roamers = 5 + num_max_agents = 100 + nest_position : Coordinate = (width //2, height //2) + max_steps = 100 + + model = ActiveWalkerModel(width=width, height=height, + num_initial_roamers=num_initial_roamers, + nest_position=nest_position, + num_max_agents=num_max_agents, + max_steps=max_steps) + + # just initial testing of MultiHexGrid + for agent in model.grid.get_neighbors(pos=nest_position, include_center=True): + if agent.unique_id == 2: + agent.do_follow_chemical_A = False + agent.prev_pos = (9,10) + print(agent.front_neighbors) + print(agent.pos, agent.unique_id, agent.do_follow_chemical_A) + + +if __name__ == "__main__": + main() + + + +""" +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License along with this program. If not, see +""" diff --git a/model.py b/model.py new file mode 100644 index 0000000..9dcf2d0 --- /dev/null +++ b/model.py @@ -0,0 +1,65 @@ +""" +model.py - Part of ants project + +This file implements the mesa model on which our ActiveRandomWalkerAnts +will act + +License: AGPL 3 (see end of file) +(C) Alexander Bocken, Viviane Fahrni, Grace Kragho +""" + +import numpy as np +from mesa.model import Model +from mesa.space import Coordinate, HexGrid, Iterable +from multihex import MultiHexGrid +from mesa.time import SimultaneousActivation +from mesa.datacollection import DataCollector +from agent import RandomWalkerAnt + +class ActiveWalkerModel(Model): + def __init__(self, width : int, height : int , num_max_agents : int, + num_initial_roamers : int, + nest_position : Coordinate, + max_steps:int=1000) -> None: + super().__init__() + self.schedule = SimultaneousActivation(self) + self.grid = MultiHexGrid(width=width, height=height, torus=True) # TODO: replace with MultiHexGrid + self._unique_id_counter : int = -1 # only touch via get_unique_id() or get_unique_ids(num_ids) + + self.max_steps = max_steps + self.nest_position : Coordinate = nest_position + self.num_max_agents = num_max_agents + + for agent_id in self.get_unique_ids(num_initial_roamers): + agent = RandomWalkerAnt(unique_id=agent_id, model=self, do_follow_chemical_A=True) + self.schedule.add(agent) + self.grid.place_agent(agent, pos=nest_position) + + self.datacollector = DataCollector( + model_reporters={}, + agent_reporters={} + ) + self.datacollector.collect(self) # keep at end of __init___ + + def step(self): + self.schedule.step() + self.datacollector.collect(self) + + if self.schedule.steps >= self.max_steps: + self.running = False + + def get_unique_id(self) -> int: + self._unique_id_counter += 1 + return self._unique_id_counter + + def get_unique_ids(self, num_ids : int): + for _ in range(num_ids): + yield self.get_unique_id() + +""" +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License along with this program. If not, see +""" diff --git a/multihex.py b/multihex.py new file mode 100644 index 0000000..325278d --- /dev/null +++ b/multihex.py @@ -0,0 +1,101 @@ +""" +multihex.py - Part of ants project + +This file impements a Mesa HexGrid while allowing for multiple agents to be +at the same location. The base for this code comes from the MultiGrid class +in mesa/space.py + +License: AGPL 3 (see end of file) +(C) Alexander Bocken, Viviane Fahrni, Grace Kragho +""" + +from mesa.space import HexGrid +from mesa.agent import Agent +import numpy as np +from mesa.space import Coordinate, accept_tuple_argument +import itertools +from typing import ( + Any, + Callable, + Iterable, + Iterator, + List, + Sequence, + Tuple, + TypeVar, + Union, + cast, + overload, +) + +MultiGridContent = list[Agent] + +class MultiHexGrid(HexGrid): + """Hexagonal grid where each cell can contain more than one agent. + Mostly based of mesa's HexGrid + Functions according to odd-q rules. + See http://www.redblobgames.com/grids/hexagons/#coordinates for more. + + Properties: + width, height: The grid's width and height. + torus: Boolean which determines whether to treat the grid as a torus. + + Methods: + get_neighbors: Returns the objects surrounding a given cell. + get_neighborhood: Returns the cells surrounding a given cell. + iter_neighbors: Iterates over position neighbors. + iter_neighborhood: Returns an iterator over cell coordinates that are + in the neighborhood of a certain point. + """ + grid: list[list[MultiGridContent]] + + @staticmethod + def default_val() -> MultiGridContent: + """Default value for new cell elements.""" + return [] + + def place_agent(self, agent: Agent, pos: Coordinate) -> None: + """Place the agent at the specified location, and set its pos variable.""" + x, y = pos + if agent.pos is None or agent not in self._grid[x][y]: + self._grid[x][y].append(agent) + agent.pos = pos + if self._empties_built: + self._empties.discard(pos) + + def remove_agent(self, agent: Agent) -> None: + """Remove the agent from the given location and set its pos attribute to None.""" + pos = agent.pos + x, y = pos + self._grid[x][y].remove(agent) + if self._empties_built and self.is_cell_empty(pos): + self._empties.add(pos) + agent.pos = None + + @accept_tuple_argument + def iter_cell_list_contents( + self, cell_list: Iterable[Coordinate] + ) -> Iterator[Agent]: + """Returns an iterator of the agents contained in the cells identified + in `cell_list`; cells with empty content are excluded. + + Args: + cell_list: Array-like of (x, y) tuples, or single tuple. + + Returns: + An iterator of the agents contained in the cells identified in `cell_list`. + """ + return itertools.chain.from_iterable( + self._grid[x][y] + for x, y in itertools.filterfalse(self.is_cell_empty, cell_list) + ) + + + +""" +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License along with this program. If not, see +""" diff --git a/shortlist.md b/shortlist.md new file mode 100644 index 0000000..ca563a8 --- /dev/null +++ b/shortlist.md @@ -0,0 +1,45 @@ +# SHORTLIST + +- nonlinear response to concentration of pheromones (with upper and lower threshold) + -> needs a separation of sensitivity and internal energy + +## More chaos +- what happens if you disrupt the trail with an obstacle? +- limited node capacity + + +# Model setup + +## Agents +- previous_position +- position +- sensitivity +- internal energy +- pheromone drop rate (A/B) +- what chemical we´re looking for +- optional: how much food we have with us (and decrement to prevent dying if energy is low) +- alpha + +### step function +- probablistic forward step based on concentrations and sensitvity + - follow highest concentration probabilistically and be random otherwise +- drop pheromones +- have we found food -> change behaviour and decrease food amount + reset stuff +- are we at the nest (having found food)? -> recruit new ants + reset stuff +- decrement sensitivity +- decrement energy (optional: only without food) + - do i need to die + +## Model + +### hexagonal grid + - pheromone a/b concentration + - nest location + - food location/concentration + - for later: node capacity +- N total ants +- a few other constants which need to be set + +### step + advance ants (call ants step function) + decrease pheromone concentration on grid