diff --git a/agent.py b/agent.py index e1f1148..096c0f2 100644 --- a/agent.py +++ b/agent.py @@ -13,43 +13,57 @@ 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: + 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.prev_pos : None | Coordinate = 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 sensitvity_to_concentration(self, prop : float) -> float: + # TODO + return prop + + def step(self): + # Calculate where next ant location should be and store in _next_pos + # TODO + pass + + def drop_chemicals(self): + # drop chemicals (depending on current state) on concentration field + # TODO + # use self.model.grid.add_to_field(key, value, pos) to not interfere with other ants pass def advance(self) -> None: + self.drop_chemicals() self.pos = self._next_pos + @property def front_neighbors(self): if self.prev_pos is not None: + assert(self.pos is not None) x, y = self.pos x_prev, y_prev = self.prev_pos dx, dy = x - x_prev, y - y_prev - front = [ + front = np.array([ (x, y + dy), - (x + dx, y), (x + dx, y + dy), - ] + (x + dx, y), + ]) 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. diff --git a/main.py b/main.py index b92000e..eec1614 100755 --- a/main.py +++ b/main.py @@ -32,6 +32,7 @@ def main(): 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) diff --git a/model.py b/model.py index 9dcf2d0..1b88918 100644 --- a/model.py +++ b/model.py @@ -11,25 +11,37 @@ License: AGPL 3 (see end of file) import numpy as np from mesa.model import Model from mesa.space import Coordinate, HexGrid, Iterable -from multihex import MultiHexGrid +from multihex import MultiHexGrid, MultiHexGridScalarFields from mesa.time import SimultaneousActivation from mesa.datacollection import DataCollector from agent import RandomWalkerAnt +from agent import Pheromone class ActiveWalkerModel(Model): + # TODO: separate food and source into new agents? + # TODO: pheromone concentrations as well as agents? 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__() + fields={"A" : True, # key : also have _next prop (for no interference in step) + "B": True, + "nests": False, + "food" : False, + } 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.grid = MultiHexGridScalarFields(width=width, height=height, torus=True, fields=fields) + self._unique_id_counter = -1 self.max_steps = max_steps self.nest_position : Coordinate = nest_position self.num_max_agents = num_max_agents + self.decay_rates = {"A" :1, + "B": 1, + } + 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) @@ -41,8 +53,18 @@ class ActiveWalkerModel(Model): ) self.datacollector.collect(self) # keep at end of __init___ + + def step(self): - self.schedule.step() + self.schedule.step() # step() and advance() all agents + + # apply decay rate on pheromone levels + for key in ("A", "B"): + field = self.grid.fields[key] + self.grid.fields[key] = field - self.decay_rates[key]*field + + self.grid.step() # actually apply deposits on fields + self.datacollector.collect(self) if self.schedule.steps >= self.max_steps: diff --git a/multihex.py b/multihex.py index 325278d..48c7185 100644 --- a/multihex.py +++ b/multihex.py @@ -12,6 +12,7 @@ License: AGPL 3 (see end of file) from mesa.space import HexGrid from mesa.agent import Agent import numpy as np +import numpy.typing as npt from mesa.space import Coordinate, accept_tuple_argument import itertools from typing import ( @@ -91,6 +92,32 @@ class MultiHexGrid(HexGrid): ) +class MultiHexGridScalarFields(MultiHexGrid): + def __init__(self, fields: dict[str, bool], width : int, height : int, torus : bool, scalar_initial_value : float=0) -> None: + super().__init__(width=width, height=height, torus=torus) + self._field_props = fields + + self.fields : dict[str, npt.NDArray[np.float_]] = {} + + for key, is_step_field in fields.items(): + self.fields[key] = np.ones((width, height)).astype(float) * scalar_initial_value + if is_step_field: + self.fields[f"_next_{key}"] = np.zeros((width, height)).astype(float) + + def reset_field(self, key : str) -> None: + self.fields[key] = np.zeros((self.width, self.height)) + + def add_to_field(self, field_key : str, value : float, pos : Coordinate) -> None: + if self._field_props[field_key]: + self.fields[f"_next_{field_key}"][pos] += value + else: + self.fields[field_key][pos] += value + + def step(self) -> None: + for key, is_step_field in self._field_props.items(): + if is_step_field: + self.fields[key] += self.fields[f"_next_{key}"] + self.reset_field(f"_next_{key}") """ 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.