Merge branch 'implementations_tests'

We now have in main.py three functions to check theoretical decays vs
actual decays.
For this a custom testing environment is set up so no disruptions from
for example, finding a food source or an ant depositing pheromones on
the grid.

The functions are otherwise pretty self-explanatory:

    check_pheromone_exponential_decay()
    check_ant_sensitivity_linear_decay()
    check_ant_pheromone_exponential_decay()

Besides this, with this merge we also finally have an upper limit for
ants on the grid using the num_max_agents model variable to check before
new ants are generated.

any use of the word 'chemical' has been replaced by 'pheromone' for
consistency
This commit is contained in:
Alexander Bocken 2023-05-18 12:47:21 +02:00
commit 85f9ecdaec
Signed by: Alexander
GPG Key ID: 1D237BE83F9B05E8
4 changed files with 145 additions and 59 deletions

View File

@ -12,11 +12,11 @@ from mesa.agent import Agent
from mesa.space import Coordinate from mesa.space import Coordinate
class RandomWalkerAnt(Agent): class RandomWalkerAnt(Agent):
def __init__(self, unique_id, model, look_for_chemical=None, def __init__(self, unique_id, model, look_for_pheromone=None,
energy_0=1, energy_0=1,
chemical_drop_rate_0 : dict[str, float]={"A": 80, "B": 80}, pheromone_drop_rate_0 : dict[str, float]={"A": 80, "B": 80},
sensitivity_0=0.99, sensitivity_0=0.99,
alpha=0.6, drop_chemical=None, alpha=0.6, drop_pheromone=None,
betas : dict[str, float]={"A": 0.0512, "B": 0.0512}, betas : dict[str, float]={"A": 0.0512, "B": 0.0512},
sensitivity_decay_rate=0.01, sensitivity_decay_rate=0.01,
sensitivity_max = 1 sensitivity_max = 1
@ -27,12 +27,12 @@ class RandomWalkerAnt(Agent):
self._next_pos : None | Coordinate = None self._next_pos : None | Coordinate = None
self.prev_pos : None | Coordinate = None self.prev_pos : None | Coordinate = None
self.look_for_chemical = look_for_chemical self.look_for_pheromone = look_for_pheromone
self.drop_chemical = drop_chemical self.drop_pheromone = drop_pheromone
self.energy = energy_0 #TODO: use self.energy = energy_0 #TODO: use
self.sensitivity_0 = sensitivity_0 self.sensitivity_0 = sensitivity_0
self.sensitivity = self.sensitivity_0 self.sensitivity = self.sensitivity_0
self.chemical_drop_rate = chemical_drop_rate_0 self.pheromone_drop_rate = pheromone_drop_rate_0
self.alpha = alpha self.alpha = alpha
self.sensitivity_max = sensitivity_max self.sensitivity_max = sensitivity_max
self.sensitivity_decay_rate = sensitivity_decay_rate self.sensitivity_decay_rate = sensitivity_decay_rate
@ -87,7 +87,8 @@ class RandomWalkerAnt(Agent):
if self.searching_food: if self.searching_food:
for neighbor in self.front_neighbors: for neighbor in self.front_neighbors:
if self.model.grid.is_food(neighbor): if self.model.grid.is_food(neighbor):
self.drop_chemical = "B" self.drop_pheromone = "B"
self.look_for_pheromone = "A"
self.sensitivity = self.sensitivity_0 self.sensitivity = self.sensitivity_0
self.prev_pos = neighbor self.prev_pos = neighbor
@ -96,27 +97,26 @@ class RandomWalkerAnt(Agent):
elif self.searching_nest: elif self.searching_nest:
for neighbor in self.front_neighbors: for neighbor in self.front_neighbors:
if self.model.grid.is_nest(neighbor): if self.model.grid.is_nest(neighbor):
self.look_for_chemical = "A" # Is this a correct interpretation? self.look_for_pheromone = "A" # Is this a correct interpretation?
self.drop_chemical = "A" self.drop_pheromone = "A"
self.sensitivity = self.sensitivity_0 self.sensitivity = self.sensitivity_0
#TODO: Do we flip the ant here or reset prev pos?
# For now, flip ant just like at food
self.prev_pos = neighbor self.prev_pos = neighbor
self._next_pos = self.pos self._next_pos = self.pos
# recruit new ants # recruit new ants
for agent_id in self.model.get_unique_ids(self.model.num_new_recruits): 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") if self.model.schedule.get_agent_count() < self.model.num_max_agents:
agent._next_pos = self.pos agent = RandomWalkerAnt(unique_id=agent_id, model=self.model, look_for_pheromone="B", drop_pheromone="A")
self.model.schedule.add(agent) agent._next_pos = self.pos
self.model.grid.place_agent(agent, pos=neighbor) self.model.schedule.add(agent)
self.model.grid.place_agent(agent, pos=neighbor)
# follow positive gradient # follow positive gradient
if self.look_for_chemical is not None: if self.look_for_pheromone 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_pheromone][cell] for cell in self.front_neighbors ]
front_concentration = self.sens_adj(front_concentration, self.look_for_chemical) front_concentration = self.sens_adj(front_concentration, self.look_for_pheromone)
current_pos_concentration = self.sens_adj(self.model.grid.fields[self.look_for_chemical][self.pos], self.look_for_chemical) current_pos_concentration = self.sens_adj(self.model.grid.fields[self.look_for_pheromone][self.pos], self.look_for_pheromone)
gradient = front_concentration - np.repeat(current_pos_concentration, 3) 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:
@ -138,19 +138,19 @@ class RandomWalkerAnt(Agent):
def step(self): def step(self):
self.sensitivity -= self.sensitivity_decay_rate self.sensitivity -= self.sensitivity_decay_rate
self._choose_next_pos() self._choose_next_pos()
self._adjust_chemical_drop_rate() self._adjust_pheromone_drop_rate()
def _adjust_chemical_drop_rate(self): def _adjust_pheromone_drop_rate(self):
if(self.drop_chemical is not None): if(self.drop_pheromone is not None):
self.chemical_drop_rate[self.drop_chemical] -= self.chemical_drop_rate[self.drop_chemical] * self.betas[self.drop_chemical] self.pheromone_drop_rate[self.drop_pheromone] -= self.pheromone_drop_rate[self.drop_pheromone] * self.betas[self.drop_pheromone]
def drop_chemicals(self) -> None: def drop_pheromones(self) -> None:
# should only be called in advance() as we do not use hidden fields # should only be called in advance() as we do not use hidden fields
if self.drop_chemical is not None: if self.drop_pheromone is not None:
self.model.grid.fields[self.drop_chemical][self.pos] += self.chemical_drop_rate[self.drop_chemical] self.model.grid.fields[self.drop_pheromone][self.pos] += self.pheromone_drop_rate[self.drop_pheromone]
def advance(self) -> None: def advance(self) -> None:
self.drop_chemicals() self.drop_pheromones()
self.prev_pos = self.pos self.prev_pos = self.pos
self.model.grid.move_agent(self, self._next_pos) self.model.grid.move_agent(self, self._next_pos)
@ -162,11 +162,11 @@ class RandomWalkerAnt(Agent):
@property @property
def searching_nest(self) -> bool: def searching_nest(self) -> bool:
return self.drop_chemical == "B" return self.drop_pheromone == "B"
@property @property
def searching_food(self) -> bool: def searching_food(self) -> bool:
return self.drop_chemical == "A" return self.drop_pheromone == "A"
@property @property
def front_neighbors(self): def front_neighbors(self):
@ -176,7 +176,9 @@ class RandomWalkerAnt(Agent):
assert(self.prev_pos is not None) assert(self.prev_pos is not None)
all_neighbors = self.neighbors() all_neighbors = self.neighbors()
neighbors_at_the_back = self.neighbors(pos=self.prev_pos, include_center=True) neighbors_at_the_back = self.neighbors(pos=self.prev_pos, include_center=True)
return list(filter(lambda i: i not in neighbors_at_the_back, all_neighbors)) front_neighbors = list(filter(lambda i: i not in neighbors_at_the_back, all_neighbors))
assert(len(front_neighbors) == 3) # not sure whether always the case, used for debugging
return front_neighbors
@property @property
def front_neighbor(self): def front_neighbor(self):

125
main.py
View File

@ -11,14 +11,24 @@ from agent import RandomWalkerAnt
import numpy as np import numpy as np
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from mesa.space import Coordinate from mesa.space import Coordinate
from mesa.datacollection import DataCollector
def main(): def main():
check_pheromone_exponential_decay()
check_ant_sensitivity_linear_decay()
check_ant_pheromone_exponential_decay()
def check_pheromone_exponential_decay():
"""
Check whether wanted exponential decay of pheromones on grid is done correctly
shows plot of pheromone placed on grid vs. equivalent exponential decay function
"""
width = 21 width = 21
height = width height = width
num_initial_roamers = 5 num_initial_roamers = 0
num_max_agents = 100 num_max_agents = 100
nest_position : Coordinate = (width //2, height //2) nest_position : Coordinate = (width //2, height //2)
max_steps = 100 max_steps = 1000
model = ActiveWalkerModel(width=width, height=height, model = ActiveWalkerModel(width=width, height=height,
num_initial_roamers=num_initial_roamers, num_initial_roamers=num_initial_roamers,
@ -26,30 +36,101 @@ def main():
num_max_agents=num_max_agents, num_max_agents=num_max_agents,
max_steps=max_steps) max_steps=max_steps)
# just initial testing of MultiHexGrid model.grid.fields["A"][5,5] = 10
a = model.agent_density() model.datacollector = DataCollector(
for loc in model.grid.iter_neighborhood(nest_position): model_reporters={"pheromone_a": lambda m: m.grid.fields["A"][5,5] },
a[loc] = 3 agent_reporters={}
for agent in model.grid.get_neighbors(pos=nest_position, include_center=True): )
if agent.unique_id == 2: model.run_model()
agent.look_for_chemical = "A" a_test = model.datacollector.get_model_vars_dataframe()["pheromone_a"]
agent.prev_pos = (9,10)
a[agent.prev_pos] = 1
for pos in agent.front_neighbors:
a[pos] = 6
agent.step()
print(f"{agent._next_pos=}")
agent.advance()
print(agent.front_neighbor)
a[agent.front_neighbor] = 5
print(agent.pos, agent.unique_id, agent.look_for_chemical) plt.figure()
neighbors = model.grid.get_neighborhood(nest_position) xx = np.linspace(0,1000, 10000)
print(neighbors) yy = a_test[0]*np.exp(-model.decay_rates["A"]*xx)
plt.plot(xx, yy, label="correct exponential function")
plt.scatter(range(len(a_test)), a_test, label="modeled decay", marker='o')
plt.title("Exponential grid pheromone decay test")
plt.legend(loc='best')
print(a) plt.show()
def check_ant_sensitivity_linear_decay():
"""
Check whether wanted linear decay of ant sensitivity is done correctly
shows plot of ant sensitivity placed on grid vs. equivalent linear decay function
not food sources are on the grid for this run to not reset sensitivities
"""
width = 50
height = width
num_initial_roamers = 1
num_max_agents = 100
nest_position : Coordinate = (width //2, height //2)
max_steps = 1000
num_food_sources = 0
model = ActiveWalkerModel(width=width, height=height,
num_initial_roamers=num_initial_roamers,
nest_position=nest_position,
num_max_agents=num_max_agents,
num_food_sources=num_food_sources,
max_steps=max_steps)
model.datacollector = DataCollector(
model_reporters={},
agent_reporters={"sensitivity": lambda a: a.sensitivity}
)
start = model.schedule.agents[0].sensitivity_decay_rate
model.run_model()
a_test = model.datacollector.get_agent_vars_dataframe().reset_index()["sensitivity"]
plt.figure()
xx = np.linspace(0,1000, 10000)
yy = a_test[0] - start*xx
plt.title("Linear Ant Sensitivity decay test")
plt.plot(xx, yy, label="correct linear function")
plt.scatter(range(len(a_test)), a_test, label="modeled decay", marker='o')
plt.legend(loc='best')
plt.show()
def check_ant_pheromone_exponential_decay():
"""
Check whether wanted exponential decay of pheromone drop rate for ants is correctly modeled
shows plot of pheromone placed on grid vs. equivalent exponential decay function
"""
width = 50
height = width
num_initial_roamers = 1
num_max_agents = 100
nest_position : Coordinate = (width //2, height //2)
max_steps = 1000
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)
model.datacollector = DataCollector(
model_reporters={},
agent_reporters={"pheromone_drop_rate": lambda a: a.pheromone_drop_rate["A"]}
)
start = model.schedule.agents[0].pheromone_drop_rate["A"]
model.run_model()
a_test = model.datacollector.get_agent_vars_dataframe().reset_index()["pheromone_drop_rate"]
plt.figure()
xx = np.linspace(0,1000, 10000)
yy = a_test[0]*np.exp(-model.schedule.agents[0].betas["A"]*xx)
plt.plot(xx, yy, label="correct exponential function")
plt.scatter(range(len(a_test)), a_test, label="modeled decay", marker='o')
plt.title("Exponential pheromone drop rate decay test")
plt.legend(loc='best')
plt.show()
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -20,6 +20,8 @@ class ActiveWalkerModel(Model):
def __init__(self, width : int, height : int , num_max_agents : int, def __init__(self, width : int, height : int , num_max_agents : int,
num_initial_roamers : int, num_initial_roamers : int,
nest_position : Coordinate, nest_position : Coordinate,
num_food_sources=5,
food_size=10,
max_steps:int=1000) -> None: max_steps:int=1000) -> None:
super().__init__() super().__init__()
fields=["A", "B", "nests", "food"] fields=["A", "B", "nests", "food"]
@ -37,12 +39,13 @@ class ActiveWalkerModel(Model):
} }
for agent_id in self.get_unique_ids(num_initial_roamers): for agent_id in self.get_unique_ids(num_initial_roamers):
agent = RandomWalkerAnt(unique_id=agent_id, model=self, look_for_chemical="A", drop_chemical="A") if self.schedule.get_agent_count() < self.num_max_agents:
self.schedule.add(agent) agent = RandomWalkerAnt(unique_id=agent_id, model=self, look_for_pheromone="A", drop_pheromone="A")
self.grid.place_agent(agent, pos=nest_position) self.schedule.add(agent)
self.grid.place_agent(agent, pos=nest_position)
for _ in range(5): for _ in range(num_food_sources):
self.grid.add_food(5) self.grid.add_food(food_size)
self.datacollector = DataCollector( self.datacollector = DataCollector(
model_reporters={}, model_reporters={},

View File

@ -23,9 +23,9 @@ def setup(params=None):
if params is None: if params is None:
params = { params = {
"width": 50, "height": 50, "width": 50, "height": 50,
"num_max_agents" : 100, "num_max_agents" : 1000,
"nest_position" : (25,25), "nest_position" : (25,25),
"num_initial_roamers" : 5, "num_initial_roamers" : 20,
} }