Merge branch 'implement_agent_step'

The agent step() function has been further improved.
Now, ants behave as roamers and follow a positive gradient in
pheromones until they find food, when they find food they will switch
what chemical they are dropping (A -> B) and will look for A.
When they return they will recruit new ants.

Currently, the sensitivity to the pheromone concentration is clamped
linear, meaning that:
			{ 0 		if concentration < lower_threshold
sens(concentration) = 	{ concentration if lower_threshold < c < higher_threshold
			{ higher_threshold else

A correct non-linear response is yet to be added.
Similarily next steps could include adding a sensitivity decay to ants
as described in the paper.

A proper setup for sane initial values such that interesting behaviour
can be observed can also still be implemented (consult the paper)

The visualization function in server.py can also still be adjusted to
automatically (?) adjust the normalization for color values.
This commit is contained in:
Alexander Bocken 2023-05-07 15:25:22 +02:00
commit 044aab26ca
Signed by: Alexander
GPG Key ID: 1D237BE83F9B05E8
4 changed files with 134 additions and 25 deletions

View File

@ -7,14 +7,15 @@ License: AGPL 3 (see end of file)
(C) Alexander Bocken, Viviane Fahrni, Grace Kragho (C) Alexander Bocken, Viviane Fahrni, Grace Kragho
""" """
import numpy as np import numpy as np
import numpy.typing as npt
from mesa.agent import Agent from mesa.agent import Agent
from mesa.space import Coordinate from mesa.space import Coordinate
from typing import overload
class RandomWalkerAnt(Agent): class RandomWalkerAnt(Agent):
def __init__(self, unique_id, model, look_for_chemical=None, def __init__(self, unique_id, model, look_for_chemical=None,
energy_0=1, chemical_drop_rate_0=1, sensitvity_0=1, alpha=0.5, drop_chemical=None) -> None: energy_0=1, chemical_drop_rate_0=1, sensitvity_0=0.1,
alpha=0.5, drop_chemical=None,
) -> None:
super().__init__(unique_id=unique_id, model=model) super().__init__(unique_id=unique_id, model=model)
self._next_pos : None | Coordinate = None self._next_pos : None | Coordinate = None
@ -28,19 +29,62 @@ class RandomWalkerAnt(Agent):
self.alpha = alpha self.alpha = alpha
def sensitvity_to_concentration(self, prop : float) -> float: def sens_adj(self, props) -> npt.NDArray[np.float_] | float:
# TODO # if props iterable create array, otherwise return single value
return prop try:
iter(props)
except TypeError:
if props > self.sensitvity:
# TODO: nonlinear response
return props
else:
return 0
arr : list[float] = []
for prop in props:
arr.append(self.sens_adj(prop))
return np.array(arr)
def step(self): def step(self):
# follow positive gradient # TODO: sensitvity decay
if self.prev_pos is None: if self.prev_pos is None:
i = np.random.choice(range(6)) i = np.random.choice(range(6))
self._next_pos = self.neighbors()[i] self._next_pos = self.neighbors()[i]
return return
# Ants dropping A look for food
if self.drop_chemical == "A":
for neighbor in self.front_neighbors:
if self.model.grid.is_food(neighbor):
self.drop_chemical = "B"
self.prev_pos = neighbor
self._next_pos = self.pos
# Ants dropping B look for nest
elif self.drop_chemical == "B":
for neighbor in self.front_neighbors:
if self.model.grid.is_nest(neighbor):
self.look_for_chemical = "A" # Is this a correct interpretation?
self.drop_chemical = "A"
#TODO: Do we flip the ant here or reset prev pos?
# For now, flip ant just like at food
self.prev_pos = neighbor
self._next_pos = self.pos
for agent_id in self.model.get_unique_ids(self.model.num_new_recruits):
agent = RandomWalkerAnt(unique_id=agent_id, model=self.model, look_for_chemical="B", drop_chemical="A")
agent._next_pos = self.pos
self.model.schedule.add(agent)
self.model.grid.place_agent(agent, pos=neighbor)
# follow positive gradient
if self.look_for_chemical is not None: if self.look_for_chemical is not None:
front_concentration = [self.model.grid.fields[self.look_for_chemical][cell] for cell in self.front_neighbors ] front_concentration = [self.model.grid.fields[self.look_for_chemical][cell] for cell in self.front_neighbors ]
gradient = front_concentration - np.repeat(self.model.grid.fields[self.look_for_chemical][self.pos], 3) front_concentration = self.sens_adj(front_concentration)
current_pos_concentration = self.sens_adj(self.model.grid.fields[self.look_for_chemical][self.pos])
gradient = front_concentration - np.repeat(current_pos_concentration, 3)
index = np.argmax(gradient) index = np.argmax(gradient)
if gradient[index] > 0: if gradient[index] > 0:
self._next_pos = self.front_neighbors[index] self._next_pos = self.front_neighbors[index]

View File

@ -30,11 +30,12 @@ class ActiveWalkerModel(Model):
self._unique_id_counter = -1 self._unique_id_counter = -1
self.max_steps = max_steps self.max_steps = max_steps
self.nest_position : Coordinate = nest_position self.grid.add_nest(nest_position)
self.num_max_agents = num_max_agents self.num_max_agents = num_max_agents
self.num_new_recruits = 5
self.decay_rates : dict[str, float] = {"A" :0.1, self.decay_rates : dict[str, float] = {"A" :0.01,
"B": 0.1, "B": 0.01,
} }
for agent_id in self.get_unique_ids(num_initial_roamers): for agent_id in self.get_unique_ids(num_initial_roamers):
@ -42,6 +43,9 @@ class ActiveWalkerModel(Model):
self.schedule.add(agent) self.schedule.add(agent)
self.grid.place_agent(agent, pos=nest_position) self.grid.place_agent(agent, pos=nest_position)
for _ in range(5):
self.grid.add_food(5)
self.datacollector = DataCollector( self.datacollector = DataCollector(
model_reporters={}, model_reporters={},
agent_reporters={} agent_reporters={}

View File

@ -9,6 +9,7 @@ License: AGPL 3 (see end of file)
(C) Alexander Bocken, Viviane Fahrni, Grace Kragho (C) Alexander Bocken, Viviane Fahrni, Grace Kragho
""" """
from sys import dont_write_bytecode
from mesa.space import HexGrid from mesa.space import HexGrid
from mesa.agent import Agent from mesa.agent import Agent
import numpy as np import numpy as np
@ -104,6 +105,54 @@ class MultiHexGridScalarFields(MultiHexGrid):
def reset_field(self, key : str) -> None: def reset_field(self, key : str) -> None:
self.fields[key] = np.zeros((self.width, self.height)) self.fields[key] = np.zeros((self.width, self.height))
def is_food(self, pos):
assert('food' in self.fields.keys())
return bool(self.fields['food'][pos])
def add_food(self, size : int , pos=None):
"""
Adds food source to grid.
Args:
pos (optional): if None, selects random place on grid which
is not yet occupied by either a nest or another food source
size: how much food should be added to field
"""
assert('food' in self.fields.keys())
if pos is None:
def select_random_place():
i = np.random.randint(0, self.width)
j = np.random.randint(0, self.height)
return i,j
pos = select_random_place()
while(self.is_nest(pos) or self.is_food(pos)):
pos = select_random_place()
self.fields['food'][pos] = size
def is_nest(self, pos : Coordinate) -> bool:
assert('nests' in self.fields.keys())
return bool(self.fields['nests'][pos])
def add_nest(self, pos:None|Coordinate=None):
"""
Adds nest to grid.
Args:
pos: if None, selects random place on grid which
is not yet occupied by either a nest or another food source
"""
assert('nests' in self.fields.keys())
if pos is None:
def select_random_place():
i = np.random.randint(0, self.width)
j = np.random.randint(0, self.height)
return i,j
pos = select_random_place()
while(self.is_nest(pos) or self.is_food(pos)):
pos = select_random_place()
self.fields['nests'][pos] = True
""" """
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 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.

View File

@ -12,20 +12,21 @@ License: AGPL 3 (see end of file)
""" """
import numpy as np import numpy as np
from mesa.visualization.modules import CanvasHexGrid, ChartModule, CanvasGrid from mesa.visualization.modules import CanvasHexGrid, ChartModule, CanvasGrid, TextElement
from mesa.visualization.ModularVisualization import ModularServer from mesa.visualization.ModularVisualization import ModularServer
from mesa.visualization.UserParam import UserSettableParameter from mesa.visualization.UserParam import UserSettableParameter
from model import ActiveWalkerModel from model import ActiveWalkerModel
from collections import defaultdict from collections import defaultdict
def setup(): def setup(params=None):
# Set the model parameters # Set the model parameters
params = { if params is None:
"width": 50, "height": 50, params = {
"num_max_agents" : 100, "width": 50, "height": 50,
"nest_position" : (25,25), "num_max_agents" : 100,
"num_initial_roamers" : 5, "nest_position" : (25,25),
} "num_initial_roamers" : 5,
}
class CanvasHexGridMultiAgents(CanvasHexGrid): class CanvasHexGridMultiAgents(CanvasHexGrid):
@ -59,11 +60,19 @@ def setup():
level: level to calculate color between white and black (linearly) level: level to calculate color between white and black (linearly)
normalization: value for which we want full black color normalization: value for which we want full black color
""" """
rgb = max(int(255 - level * 255 / normalization), 0) return max(int(255 - level * 255 / normalization), 0)
mono = f"{rgb:0{2}x}" # hex value of rgb value with fixed length 2
return f"#{3*mono}"
def portray_ant_density(model, pos): def portray_ant_density(model, pos):
if model.grid.is_nest(pos):
col = "red"
elif model.grid.is_food(pos):
col = "green"
else:
col = get_color(level=len(model.grid[pos]), normalization=5)
col = f"rgb({col}, {col}, {col})"
return { return {
"Shape": "hex", "Shape": "hex",
"r": 1, "r": 1,
@ -71,10 +80,12 @@ def setup():
"Layer": 0, "Layer": 0,
"x": pos[0], "x": pos[0],
"y": pos[1], "y": pos[1],
"Color": get_color(level=len(model.grid[pos]), normalization=5) "Color": col,
} }
def portray_pheromone_density(model, pos): def portray_pheromone_density(model, pos):
col_a = get_color(level=model.grid.fields["A"][pos], normalization=3)
col_b = get_color(level=model.grid.fields["B"][pos], normalization=3)
return { return {
"Shape": "hex", "Shape": "hex",
"r": 1, "r": 1,
@ -82,7 +93,7 @@ def setup():
"Layer": 0, "Layer": 0,
"x": pos[0], "x": pos[0],
"y": pos[1], "y": pos[1],
"Color": get_color(level=model.grid.fields["A"][pos], normalization=3) "Color": f"rgb({col_a}, {col_b}, 255)"
} }
@ -92,7 +103,8 @@ def setup():
pixel_ratio = 10 pixel_ratio = 10
grid_ants = CanvasHexGridMultiAgents(portray_ant_density, width, height, width*pixel_ratio, height*pixel_ratio) grid_ants = CanvasHexGridMultiAgents(portray_ant_density, width, height, width*pixel_ratio, height*pixel_ratio)
grid_pheromones = CanvasHexGridMultiAgents(portray_pheromone_density, width, height, width*pixel_ratio, height*pixel_ratio) grid_pheromones = CanvasHexGridMultiAgents(portray_pheromone_density, width, height, width*pixel_ratio, height*pixel_ratio)
return ModularServer(ActiveWalkerModel, [grid_ants, grid_pheromones], test_text = TextElement()
return ModularServer(ActiveWalkerModel, [lambda m: "<h3>Ant density</h3><h5>Nest: Red, Food: Green</h5>", grid_ants, lambda m: "<h3>Pheromone Density</h3><h5>Pheromone A: Cyan, Pheromone B: Pink</h5>", grid_pheromones],
"Active Random Walker Ants", params) "Active Random Walker Ants", params)
if __name__ == "__main__": if __name__ == "__main__":