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:
commit
044aab26ca
60
agent.py
60
agent.py
@ -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]
|
||||||
|
10
model.py
10
model.py
@ -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={}
|
||||||
|
49
multihex.py
49
multihex.py
@ -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.
|
||||||
|
|
||||||
|
28
server.py
28
server.py
@ -12,14 +12,15 @@ 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
|
||||||
|
if params is None:
|
||||||
params = {
|
params = {
|
||||||
"width": 50, "height": 50,
|
"width": 50, "height": 50,
|
||||||
"num_max_agents" : 100,
|
"num_max_agents" : 100,
|
||||||
@ -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__":
|
||||||
|
Loading…
Reference in New Issue
Block a user