Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion demo/population_hook_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
world.spawners.append(spawner)

# this is the part we are testing
world.population.addListener("append", print)
world.population.register_add_callback(lambda x: print(f"added {x}"))
world.population.register_del_callback(lambda x: print(f"deleted {x}"))

sim(world, start_paused=True)
7 changes: 6 additions & 1 deletion src/swarmsim/sensors/BinaryFOVSensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ def checkForLOSCollisions(self, world: RectangularWorld) -> None:
return

self.time_since_last_sensing = 0

if not world.quad:
self.determineState(False, None, world)
return

sensor_origin = self.agent.getPosition()

# use world.quad that tracks agent positions to retrieve the agents within the minimal rectangle that contains the FOV sector
Expand Down Expand Up @@ -272,7 +277,7 @@ def yadd(val): # extend either ymin or ymax if outside current range
xadd(radius * yts)

# this padding of the rectangle is to account for and detect agents that would only be seen by the whisker circle intercept correction
padding = 0 if self.detect_only_origins else world.maxAgentRadius
padding = 0 if self.detect_only_origins else world.max_agent_r

# positions are relative until now, make them absolute for the return
return [position[0] + xmin - padding, position[1] + ymin - padding, position[0] + xmax + padding, position[1] + ymax + padding]
Expand Down
83 changes: 83 additions & 0 deletions src/swarmsim/util/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,86 @@ def __lt__(self, other):
return set(self.flags) < set(other.flags)
elif isinstance(other, set):
return set(self.flags) < other


def slice_indices(s: slice, max_len: int = 0) -> list[int]:
if s.step is None or s.step >= 0 or s.stop is None:
stop = max_len if s.stop is None else min(s.stop, max_len)
else: # step is negative, stop is not None. Ignore stop
stop = max_len
return list(range(stop))[s]


class HookList(list):
def __init__(self, iterable=None, add_callbacks=None, del_callbacks=None):
if iterable is None:
iterable = []
super().__init__(iterable)
self._add_callbacks = [] if add_callbacks is None else add_callbacks
self._del_callbacks = [] if del_callbacks is None else del_callbacks
for func in self._add_callbacks:
for obj in self:
func(obj)

def register_add_callback(self, func):
self._add_callbacks.append(func)

def register_del_callback(self, func):
self._del_callbacks.append(func)

def append(self, obj):
for func in self._add_callbacks:
func(obj)
return super().append(obj)

def extend(self, iterable):
li = list(iterable)
for func in self._add_callbacks:
for obj in li:
func(obj)
return super().extend(li)

def insert(self, index, obj):
super().insert(index, obj)
for func in self._add_callbacks:
func(obj)

def __iadd__(self, value):
for func in self._add_callbacks:
for obj in value:
func(obj)
return super().__iadd__(value)

def __imul__(self, value):
for func in self._add_callbacks:
for _i in range(value):
for obj in self:
func(obj)
return super().__imul__(value)

def pop(self, index=-1):
for func in self._del_callbacks:
func(self[index])
return super().pop(index)

def remove(self, value):
for func in self._del_callbacks:
func(value)
return super().remove(value)

def __setitem__(self, key, value):
n = len(self)
if isinstance(key, slice):
indices = slice_indices(key, n)
for func in self._del_callbacks:
for i in indices:
func(self[i])
for func in self._add_callbacks:
for obj in value:
func(obj)
else:
for func in self._del_callbacks:
func(self[key])
for func in self._add_callbacks:
func(value)
super().__setitem__(key, value)
28 changes: 15 additions & 13 deletions src/swarmsim/world/RectangularWorld.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,9 @@ def factor_zoom(self, zoom):


class RectangularWorld(World):

def __init__(self, config: RectangularWorldConfig, initialize=True):
self.maxAgentRadius = 0
self.max_agent_r = 0
# if config is None:
# raise Exception("RectangularWorld must be instantiated with a WorldConfig class")

Expand Down Expand Up @@ -150,7 +150,7 @@ def __init__(self, config: RectangularWorldConfig, initialize=True):

It does not directly determine how fast the simulation runs, or the FPS.
"""
self.population.addListener("append", self.updateMaxRadius)
self.population.register_add_callback(self.update_max_agent_r)

#: Agent : currently selected agent.
self.selected = None
Expand All @@ -161,13 +161,15 @@ def __init__(self, config: RectangularWorldConfig, initialize=True):

if initialize:
self.setup_objects(config.objects)
self.update_quadtree()

# update the position of all agents in the quad tree (call this method AFTER updating positions in the tick)
def updateQuad(self):
def update_quadtree(self):
# we don't need to do all this if there are no agents
if not self.population:
self.quad = None
return

# procedure to find the bounds of the quad
def minMax(arr):
minimum = arr[0]
Expand All @@ -184,7 +186,7 @@ def minMax(arr):

# create quad that nicely contains the current population
newQuad = quads.QuadTree(middle, np.ceil(xMax - xMin) + 4, np.ceil(yMax - yMin) + 4)

# add the agents to the quad
for agent in self.population:
newQuad.insert(point=agent.pos.tolist(), data=agent)
Expand Down Expand Up @@ -228,11 +230,11 @@ def setup_objects(self, objects):
# override the inherited setup function to call updateQuad() after setup
def setup(self, step_spawners=True):
super().setup(step_spawners)
self.updateQuad()
self.update_quadtree()

def updateMaxRadius(self, agent):
if hasattr(agent, "radius") and self.maxAgentRadius < agent.radius:
self.maxAgentRadius = agent.radius
def update_max_agent_r(self, agent):
if hasattr(agent, "radius") and self.max_agent_r < agent.radius:
self.max_agent_r = agent.radius

def step_agents(self):
for agent in self.population:
Expand All @@ -242,15 +244,15 @@ def step_agents(self):
world=self,
)
self.handleGoalCollisions(agent)

def step(self):
self.total_steps += 1

self.step_spawners()
self.step_agents()
self.step_objects()
self.updateQuad() # update the position of agents in the quad tree

self.update_quadtree() # update the position of agents in the quad tree

self.step_metrics()

Expand Down
41 changes: 29 additions & 12 deletions src/swarmsim/world/World.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

from ..util.asdict import asdict
from ..util.collections import FlagSet
from ..util.collections import HookList

from ..agent.Agent import Agent
from .spawners.Spawner import Spawner
Expand Down Expand Up @@ -144,16 +145,16 @@ def create_world(self):
# goal.range *= zoom
# # self.init_type.rescale(zoom)

class HookList(list):
def __init__(self):
super().__init__()
self.listeners = {"append": []}
def addListener(self, target, listener):
self.listeners[target].append(listener)
def append(self, item):
super().append(item)
for listener in self.listeners["append"]:
listener(item)
# class HookList(list):
# def __init__(self):
# super().__init__()
# self.listeners = {"append": []}
# def addListener(self, target, listener):
# self.listeners[target].append(listener)
# def append(self, item):
# super().append(item)
# for listener in self.listeners["append"]:
# listener(item)


class World:
Expand All @@ -163,13 +164,13 @@ def __init__(self, config):
self.config = config
config = replace(config)
#: List of agents in the world.
self.population: HookList[Agent] = HookList()
self._population: HookList[Agent] = HookList()
#: List of spawners which create agents or objects.
self.spawners: list[Spawner] = []
#: Metrics to calculate behaviors.
self.metrics: list[AbstractMetric] = []
#: The list of world objects.
self.objects: HookList[Agent] = HookList()
self._objects: HookList[Agent] = HookList()
self.goals = config.goals
self.meta = config.metadata
self.gui = None
Expand All @@ -184,6 +185,22 @@ def __init__(self, config):
self.rng: np.random.Generator
self.flags = FlagSet(config.flags)

@property
def population(self):
return self._population

@population.setter
def population(self, value):
self._population[:] = value

@property
def objects(self):
return self._objects

@objects.setter
def objects(self, value):
self._objects[:] = value

def set_seed(self, seed):
self.seed = np.random.randint(0, 2**31) if seed is None else seed
self.rng = np.random.default_rng(self.seed)
Expand Down
Loading