Merge branch 'implementations_tests'

Implemented check for ants to follow positive pheromone gradient

step() function has been improved.
Previously, self.prev_pos (now: self._prev_pos) was set non-uniformly;
sometimes in step() but also always in advance() which resulted in
unwanted behavior.
As self._prev_pos is now marked as private all assignments are done
simultaneously with self._next_pos in step(), not advance().

Accordingly, future functions should strive not to access
self._prev_pos outside of the agent if possible.

Gradient following behaviour was additionally not observed since the
sensitivity_max variable was set too low, resulting in the adjusted
pheromone concentration gradients to not be present.
This commit is contained in:
Alexander Bocken 2023-05-18 16:02:13 +02:00
commit 83a973f377
Signed by: Alexander
GPG Key ID: 1D237BE83F9B05E8
4 changed files with 87 additions and 20 deletions

View File

@ -19,13 +19,13 @@ class RandomWalkerAnt(Agent):
alpha=0.6, drop_pheromone=None,
betas : dict[str, float]={"A": 0.0512, "B": 0.0512},
sensitivity_decay_rate=0.01,
sensitivity_max = 1
sensitivity_max = 300
) -> None:
super().__init__(unique_id=unique_id, model=model)
self._next_pos : None | Coordinate = None
self.prev_pos : None | Coordinate = None
self._prev_pos : None | Coordinate = None
self.look_for_pheromone = look_for_pheromone
self.drop_pheromone = drop_pheromone
@ -37,7 +37,7 @@ class RandomWalkerAnt(Agent):
self.sensitivity_max = sensitivity_max
self.sensitivity_decay_rate = sensitivity_decay_rate
self.betas = betas
self.threshold : dict[str, float] = {"A": 1, "B": 1}
self.threshold : dict[str, float] = {"A": 0, "B": 0}
def sens_adj(self, props, key) -> npt.NDArray[np.float_] | float:
@ -79,9 +79,11 @@ class RandomWalkerAnt(Agent):
return np.array(arr)
def _choose_next_pos(self):
if self.prev_pos is None:
if self._prev_pos is None:
i = np.random.choice(range(6))
assert(self.pos is not self.neighbors()[i])
self._next_pos = self.neighbors()[i]
self._prev_pos = self.pos
return
if self.searching_food:
@ -91,7 +93,7 @@ class RandomWalkerAnt(Agent):
self.look_for_pheromone = "A"
self.sensitivity = self.sensitivity_0
self.prev_pos = neighbor
self._prev_pos = neighbor
self._next_pos = self.pos
elif self.searching_nest:
@ -101,7 +103,7 @@ class RandomWalkerAnt(Agent):
self.drop_pheromone = "A"
self.sensitivity = self.sensitivity_0
self.prev_pos = neighbor
self._prev_pos = neighbor
self._next_pos = self.pos
# recruit new ants
@ -117,22 +119,26 @@ class RandomWalkerAnt(Agent):
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_pheromone)
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).astype(np.float_)
# TODO: if two or more neighbors have same concentration randomize? Should be unlikely with floats though
index = np.argmax(gradient)
if gradient[index] > 0:
self._next_pos = self.front_neighbors[index]
self._prev_pos = self.pos
return
# do biased random walk
p = np.random.uniform()
if p < self.alpha:
self._next_pos = self.front_neighbor
self._prev_pos = self.pos
else:
# need copy() as we would otherwise remove the tuple from all possible lists (aka python "magic")
other_neighbors = self.neighbors().copy()
other_neighbors.remove(self.front_neighbor)
random_index = np.random.choice(range(len(other_neighbors)))
self._next_pos = other_neighbors[random_index]
self._prev_pos = self.pos
def step(self):
@ -151,8 +157,8 @@ class RandomWalkerAnt(Agent):
def advance(self) -> None:
self.drop_pheromones()
self.prev_pos = self.pos
self.model.grid.move_agent(self, self._next_pos)
self._next_pos = None # so that we rather crash than use wrong data
# TODO: find out how to decorate with property properly
def neighbors(self, pos=None, include_center=False):
@ -173,12 +179,25 @@ class RandomWalkerAnt(Agent):
"""
returns all three neighbors which the ant can see
"""
assert(self.prev_pos is not None)
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)
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
########## DEBUG
try:
assert(self._prev_pos is not None)
assert(self._prev_pos is not self.pos)
assert(self._prev_pos in all_neighbors)
assert(len(front_neighbors) == 3)
except AssertionError:
print(f"{self._prev_pos=}")
print(f"{self.pos=}")
print(f"{all_neighbors=}")
print(f"{neighbors_at_the_back=}")
print(f"{front_neighbors=}")
raise AssertionError
else:
return front_neighbors
@property
def front_neighbor(self):
@ -186,11 +205,11 @@ class RandomWalkerAnt(Agent):
returns neighbor of current pos
which is towards the front of the ant
"""
neighbors_prev_pos = self.neighbors(self.prev_pos)
neighbors__prev_pos = self.neighbors(self._prev_pos)
for candidate in self.front_neighbors:
# neighbor in front direction only shares current pos as neighborhood with prev_pos
# neighbor in front direction only shares current pos as neighborhood with _prev_pos
candidate_neighbors = self.model.grid.get_neighborhood(candidate)
overlap = [x for x in candidate_neighbors if x in neighbors_prev_pos]
overlap = [x for x in candidate_neighbors if x in neighbors__prev_pos]
if len(overlap) == 1:
return candidate

48
main.py
View File

@ -13,10 +13,13 @@ import matplotlib.pyplot as plt
from mesa.space import Coordinate
from mesa.datacollection import DataCollector
from multihex import MultiHexGrid
def main():
check_pheromone_exponential_decay()
check_ant_sensitivity_linear_decay()
check_ant_pheromone_exponential_decay()
check_ants_follow_gradient()
def check_pheromone_exponential_decay():
"""
@ -131,6 +134,51 @@ def check_ant_pheromone_exponential_decay():
plt.show()
def check_ants_follow_gradient():
"""
Create a path of neighbours with a static gradient.
Observe whether ant correctly follows gradient once found. via matrix printouts
8 = ant
anything else: pheromone A density.
The ant does not drop any new pheromones for this test
"""
width, height = 20,20
params = {
"width": width, "height": height,
"num_max_agents": 1,
"num_food_sources": 0,
"nest_position": (10,10),
"num_initial_roamers": 1,
}
model = ActiveWalkerModel(**params)
def place_line(grid : MultiHexGrid, start_pos=None):
strength = 5
if start_pos is None:
start_pos = (9,9)
next_pos = start_pos
for _ in range(width):
grid.fields["A"][next_pos] = strength
strength += 0.01
next_pos = grid.get_neighborhood(next_pos)[0]
place_line(model.grid)
ant = model.schedule._agents[0]
ant.looking_for_pheromone = "A"
ant.drop_pheromone = None
ant.threshold["A"] = 0
ant.sensitivity_max = 100
#model.grid.fields["A"] = np.diag(np.ones(width))
model.decay_rates["A"] = 0
while model.schedule.steps < 100:
display_field = np.copy(model.grid.fields["A"])
display_field[ant.pos] = 8
print(display_field)
print(20*"#")
model.step()
if __name__ == "__main__":
main()

View File

@ -22,7 +22,8 @@ class ActiveWalkerModel(Model):
nest_position : Coordinate,
num_food_sources=5,
food_size=10,
max_steps:int=1000) -> None:
max_steps:int=1000,
) -> None:
super().__init__()
fields=["A", "B", "nests", "food"]
self.schedule = SimultaneousActivation(self)
@ -68,7 +69,6 @@ class ActiveWalkerModel(Model):
for key in ("A", "B"):
field = self.grid.fields[key]
self.grid.fields[key] = field - self.decay_rates[key]*field
# TODO: plot to check whether exponential
self.datacollector.collect(self)

View File

@ -23,9 +23,9 @@ def setup(params=None):
if params is None:
params = {
"width": 50, "height": 50,
"num_max_agents" : 1000,
"num_max_agents" : 100,
"nest_position" : (25,25),
"num_initial_roamers" : 20,
"num_initial_roamers" : 5,
}
@ -127,7 +127,7 @@ def setup(params=None):
[lambda m: "<h3>Ant density</h3><h5>Nest: Red, Food: Green</h5>",
grid_ants,
lambda m: f"<h5>Normalization Value: {norm_ants(m)}</h5>",
lambda m: "<h3>Pheromone Density</h3><h5>Pheromone A: Cyan, Pheromone B: Pink</h5>",
lambda m: "<h3>Pheromone Density</h3><h5>Pheromone A: Cyan, Pheromone B: Magenta</h5>",
grid_pheromones,
lambda m: f"<h5>Normalization Value: {norm_pheromones(m)}</h5>"
],