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:
commit
83a973f377
49
agent.py
49
agent.py
@ -19,13 +19,13 @@ class RandomWalkerAnt(Agent):
|
|||||||
alpha=0.6, drop_pheromone=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 = 300
|
||||||
) -> 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
|
||||||
self.prev_pos : None | Coordinate = None
|
self._prev_pos : None | Coordinate = None
|
||||||
|
|
||||||
self.look_for_pheromone = look_for_pheromone
|
self.look_for_pheromone = look_for_pheromone
|
||||||
self.drop_pheromone = drop_pheromone
|
self.drop_pheromone = drop_pheromone
|
||||||
@ -37,7 +37,7 @@ class RandomWalkerAnt(Agent):
|
|||||||
self.sensitivity_max = sensitivity_max
|
self.sensitivity_max = sensitivity_max
|
||||||
self.sensitivity_decay_rate = sensitivity_decay_rate
|
self.sensitivity_decay_rate = sensitivity_decay_rate
|
||||||
self.betas = betas
|
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:
|
def sens_adj(self, props, key) -> npt.NDArray[np.float_] | float:
|
||||||
@ -79,9 +79,11 @@ class RandomWalkerAnt(Agent):
|
|||||||
return np.array(arr)
|
return np.array(arr)
|
||||||
|
|
||||||
def _choose_next_pos(self):
|
def _choose_next_pos(self):
|
||||||
if self.prev_pos is None:
|
if self._prev_pos is None:
|
||||||
i = np.random.choice(range(6))
|
i = np.random.choice(range(6))
|
||||||
|
assert(self.pos is not self.neighbors()[i])
|
||||||
self._next_pos = self.neighbors()[i]
|
self._next_pos = self.neighbors()[i]
|
||||||
|
self._prev_pos = self.pos
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.searching_food:
|
if self.searching_food:
|
||||||
@ -91,7 +93,7 @@ class RandomWalkerAnt(Agent):
|
|||||||
self.look_for_pheromone = "A"
|
self.look_for_pheromone = "A"
|
||||||
self.sensitivity = self.sensitivity_0
|
self.sensitivity = self.sensitivity_0
|
||||||
|
|
||||||
self.prev_pos = neighbor
|
self._prev_pos = neighbor
|
||||||
self._next_pos = self.pos
|
self._next_pos = self.pos
|
||||||
|
|
||||||
elif self.searching_nest:
|
elif self.searching_nest:
|
||||||
@ -101,7 +103,7 @@ class RandomWalkerAnt(Agent):
|
|||||||
self.drop_pheromone = "A"
|
self.drop_pheromone = "A"
|
||||||
self.sensitivity = self.sensitivity_0
|
self.sensitivity = self.sensitivity_0
|
||||||
|
|
||||||
self.prev_pos = neighbor
|
self._prev_pos = neighbor
|
||||||
self._next_pos = self.pos
|
self._next_pos = self.pos
|
||||||
|
|
||||||
# recruit new ants
|
# 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.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)
|
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)
|
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)
|
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]
|
||||||
|
self._prev_pos = self.pos
|
||||||
return
|
return
|
||||||
|
|
||||||
# do biased random walk
|
# do biased random walk
|
||||||
p = np.random.uniform()
|
p = np.random.uniform()
|
||||||
if p < self.alpha:
|
if p < self.alpha:
|
||||||
self._next_pos = self.front_neighbor
|
self._next_pos = self.front_neighbor
|
||||||
|
self._prev_pos = self.pos
|
||||||
else:
|
else:
|
||||||
# need copy() as we would otherwise remove the tuple from all possible lists (aka python "magic")
|
# need copy() as we would otherwise remove the tuple from all possible lists (aka python "magic")
|
||||||
other_neighbors = self.neighbors().copy()
|
other_neighbors = self.neighbors().copy()
|
||||||
other_neighbors.remove(self.front_neighbor)
|
other_neighbors.remove(self.front_neighbor)
|
||||||
random_index = np.random.choice(range(len(other_neighbors)))
|
random_index = np.random.choice(range(len(other_neighbors)))
|
||||||
self._next_pos = other_neighbors[random_index]
|
self._next_pos = other_neighbors[random_index]
|
||||||
|
self._prev_pos = self.pos
|
||||||
|
|
||||||
|
|
||||||
def step(self):
|
def step(self):
|
||||||
@ -151,8 +157,8 @@ class RandomWalkerAnt(Agent):
|
|||||||
|
|
||||||
def advance(self) -> None:
|
def advance(self) -> None:
|
||||||
self.drop_pheromones()
|
self.drop_pheromones()
|
||||||
self.prev_pos = self.pos
|
|
||||||
self.model.grid.move_agent(self, self._next_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
|
# TODO: find out how to decorate with property properly
|
||||||
def neighbors(self, pos=None, include_center=False):
|
def neighbors(self, pos=None, include_center=False):
|
||||||
@ -173,12 +179,25 @@ class RandomWalkerAnt(Agent):
|
|||||||
"""
|
"""
|
||||||
returns all three neighbors which the ant can see
|
returns all three neighbors which the ant can see
|
||||||
"""
|
"""
|
||||||
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)
|
||||||
front_neighbors = 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
|
########## 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
|
@property
|
||||||
def front_neighbor(self):
|
def front_neighbor(self):
|
||||||
@ -186,11 +205,11 @@ class RandomWalkerAnt(Agent):
|
|||||||
returns neighbor of current pos
|
returns neighbor of current pos
|
||||||
which is towards the front of the ant
|
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:
|
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)
|
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:
|
if len(overlap) == 1:
|
||||||
return candidate
|
return candidate
|
||||||
|
|
||||||
|
48
main.py
48
main.py
@ -13,10 +13,13 @@ import matplotlib.pyplot as plt
|
|||||||
from mesa.space import Coordinate
|
from mesa.space import Coordinate
|
||||||
from mesa.datacollection import DataCollector
|
from mesa.datacollection import DataCollector
|
||||||
|
|
||||||
|
from multihex import MultiHexGrid
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
check_pheromone_exponential_decay()
|
check_pheromone_exponential_decay()
|
||||||
check_ant_sensitivity_linear_decay()
|
check_ant_sensitivity_linear_decay()
|
||||||
check_ant_pheromone_exponential_decay()
|
check_ant_pheromone_exponential_decay()
|
||||||
|
check_ants_follow_gradient()
|
||||||
|
|
||||||
def check_pheromone_exponential_decay():
|
def check_pheromone_exponential_decay():
|
||||||
"""
|
"""
|
||||||
@ -131,6 +134,51 @@ def check_ant_pheromone_exponential_decay():
|
|||||||
|
|
||||||
plt.show()
|
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__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
||||||
|
4
model.py
4
model.py
@ -22,7 +22,8 @@ class ActiveWalkerModel(Model):
|
|||||||
nest_position : Coordinate,
|
nest_position : Coordinate,
|
||||||
num_food_sources=5,
|
num_food_sources=5,
|
||||||
food_size=10,
|
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"]
|
||||||
self.schedule = SimultaneousActivation(self)
|
self.schedule = SimultaneousActivation(self)
|
||||||
@ -68,7 +69,6 @@ class ActiveWalkerModel(Model):
|
|||||||
for key in ("A", "B"):
|
for key in ("A", "B"):
|
||||||
field = self.grid.fields[key]
|
field = self.grid.fields[key]
|
||||||
self.grid.fields[key] = field - self.decay_rates[key]*field
|
self.grid.fields[key] = field - self.decay_rates[key]*field
|
||||||
# TODO: plot to check whether exponential
|
|
||||||
|
|
||||||
self.datacollector.collect(self)
|
self.datacollector.collect(self)
|
||||||
|
|
||||||
|
@ -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" : 1000,
|
"num_max_agents" : 100,
|
||||||
"nest_position" : (25,25),
|
"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>",
|
[lambda m: "<h3>Ant density</h3><h5>Nest: Red, Food: Green</h5>",
|
||||||
grid_ants,
|
grid_ants,
|
||||||
lambda m: f"<h5>Normalization Value: {norm_ants(m)}</h5>",
|
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,
|
grid_pheromones,
|
||||||
lambda m: f"<h5>Normalization Value: {norm_pheromones(m)}</h5>"
|
lambda m: f"<h5>Normalization Value: {norm_pheromones(m)}</h5>"
|
||||||
],
|
],
|
||||||
|
Loading…
Reference in New Issue
Block a user