From c57f7e874bf8b6269d068afd085cc08676f654f2 Mon Sep 17 00:00:00 2001 From: kenblu24 Date: Mon, 8 Sep 2025 19:15:22 -0400 Subject: [PATCH 1/3] Overhaul hooklist API to fully catch all add/delete events --- demo/population_hook_test.py | 3 +- src/swarmsim/sensors/BinaryFOVSensor.py | 5 +- src/swarmsim/util/collections.py | 81 +++++++++++++++++++++++++ src/swarmsim/world/RectangularWorld.py | 28 +++++---- src/swarmsim/world/World.py | 41 +++++++++---- 5 files changed, 131 insertions(+), 27 deletions(-) diff --git a/demo/population_hook_test.py b/demo/population_hook_test.py index af006dcd..af5dcc57 100644 --- a/demo/population_hook_test.py +++ b/demo/population_hook_test.py @@ -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) \ No newline at end of file diff --git a/src/swarmsim/sensors/BinaryFOVSensor.py b/src/swarmsim/sensors/BinaryFOVSensor.py index 0302ba1e..97bf31ec 100644 --- a/src/swarmsim/sensors/BinaryFOVSensor.py +++ b/src/swarmsim/sensors/BinaryFOVSensor.py @@ -113,6 +113,9 @@ def checkForLOSCollisions(self, world: RectangularWorld) -> None: # only check for LOS collisions every n timesteps. return + if not world.quad: + return + self.time_since_last_sensing = 0 sensor_origin = self.agent.getPosition() @@ -272,7 +275,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] diff --git a/src/swarmsim/util/collections.py b/src/swarmsim/util/collections.py index 9cd1b771..d0afe771 100644 --- a/src/swarmsim/util/collections.py +++ b/src/swarmsim/util/collections.py @@ -204,3 +204,84 @@ 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): + 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) diff --git a/src/swarmsim/world/RectangularWorld.py b/src/swarmsim/world/RectangularWorld.py index 974a1ed7..f724c32b 100644 --- a/src/swarmsim/world/RectangularWorld.py +++ b/src/swarmsim/world/RectangularWorld.py @@ -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") @@ -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 @@ -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] @@ -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) @@ -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: @@ -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() diff --git a/src/swarmsim/world/World.py b/src/swarmsim/world/World.py index 02a3ba58..d83305b6 100644 --- a/src/swarmsim/world/World.py +++ b/src/swarmsim/world/World.py @@ -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 @@ -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: @@ -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 @@ -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) From bca48cbe1dda69131f359c2747a9134afc7018fd Mon Sep 17 00:00:00 2001 From: kenblu24 Date: Mon, 8 Sep 2025 20:29:54 -0400 Subject: [PATCH 2/3] if world.quad is None/Falsy, act like nobody around --- src/swarmsim/sensors/BinaryFOVSensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/swarmsim/sensors/BinaryFOVSensor.py b/src/swarmsim/sensors/BinaryFOVSensor.py index 97bf31ec..672cec55 100644 --- a/src/swarmsim/sensors/BinaryFOVSensor.py +++ b/src/swarmsim/sensors/BinaryFOVSensor.py @@ -113,10 +113,12 @@ def checkForLOSCollisions(self, world: RectangularWorld) -> None: # only check for LOS collisions every n timesteps. return + self.time_since_last_sensing = 0 + if not world.quad: + self.determineState(False, None, world) return - self.time_since_last_sensing = 0 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 From f13ed82765ce4167d27cefc8e5b7d2b7751f93c6 Mon Sep 17 00:00:00 2001 From: kenblu24 Date: Mon, 8 Sep 2025 20:30:45 -0400 Subject: [PATCH 3/3] implement HookList.remove() calling func in HookList._del_callbacks --- src/swarmsim/util/collections.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/swarmsim/util/collections.py b/src/swarmsim/util/collections.py index d0afe771..2f2a4235 100644 --- a/src/swarmsim/util/collections.py +++ b/src/swarmsim/util/collections.py @@ -267,6 +267,8 @@ def pop(self, index=-1): 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):