From 6eeb3cea1c844da235b2883dffe5d697d2cbbf39 Mon Sep 17 00:00:00 2001 From: Teyema Date: Wed, 21 May 2025 13:21:52 -0400 Subject: [PATCH 1/5] WIP voronoi visualization --- src/novel_swarms/metrics/VoronoiDispersal.py | 151 +++++++++++++++++++ src/novel_swarms/metrics/__init__.py | 2 + 2 files changed, 153 insertions(+) create mode 100644 src/novel_swarms/metrics/VoronoiDispersal.py diff --git a/src/novel_swarms/metrics/VoronoiDispersal.py b/src/novel_swarms/metrics/VoronoiDispersal.py new file mode 100644 index 00000000..9ed7ace3 --- /dev/null +++ b/src/novel_swarms/metrics/VoronoiDispersal.py @@ -0,0 +1,151 @@ +import itertools + +import pygame +import numpy as np +from .AbstractMetric import AbstractMetric + +import scipy as sp +from scipy import spatial +import sys + +# typing +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ..world.RectangularWorld import RectangularWorld +else: + RectangularWorld = None + + +class VoronoiRelaxation(AbstractMetric): + __badvars__ = AbstractMetric.__badvars__ + ['population'] # references to population may cause pickling errors + + def __init__(self, history=100, regularize=True): + super().__init__(name="Voronoi Dispersal", history_size=history) + self.population = None + self.regularize = regularize + self.allpairs = [] + self.lines = [] + + def attach_world(self, world: RectangularWorld): + super().attach_world(world) + self.population = world.population + self.world_size = world.config.size + self.world_radius = world.config.radius + + + def calculate(self): + + points = np.array([agent.getPosition() for agent in self.population]) + self.v = v = spatial.Voronoi(points) + allpairs = set() + for vertex in v.ridge_vertices: + vertex = np.asarray(vertex) + if np.all(vertex >= 0): + allpairs.add(tuple(sorted(vertex))) + + self.allpairs = np.asarray(list(allpairs), dtype=np.int32) + self.lines = self.v.vertices[self.allpairs] + self.lines = np.clip(self.lines, 0, self.world_size) + + + # self.lines = np.asarray([[self.v.points[ridge_point[0]], self.v.points[ridge_point[1]]] for ridge_point in self.v.ridge_points], dtype=np.int32) + + # distances = np.array([d.plane_distance(p) for p in points]) + distances = np.array([np.linalg.norm(a - b) for a, b in self.lines]) + var = distances.var() + mean = distances.mean() + # bbox_size = (d.max_bound - d.min_bound) + # bbox_ratio = min(bbox_size) / max(bbox_size) + # self.set_value(bbox_area / 10 - ((1 + var * 10) * mean)) + # dispersal = bbox_size.prod() * bbox_ratio / (1 + var * 10) + # self.set_value(dispersal if dispersal is not None else 0) + + def draw(self, screen, offset): + pan, zoom = np.asarray(offset[0]), offset[1] + super().draw(screen, offset) + + for line in self.lines: + pygame.draw.line(screen, (128, 128, 128), *line * zoom + pan, width=1) + +# This is a modified version of the code from here: +# https://stackoverflow.com/questions/28665491/getting-a-bounded-polygon-coordinates-from-voronoi-cells + + + +# eps = sys.float_info.epsilon + +# n_towers = 100 +# towers = np.random.rand(n_towers, 2) +# bounding_box = np.array([0., 1., 0., 1.]) # [x_min, x_max, y_min, y_max] + +# def in_box(towers, bounding_box): +# return np.logical_and(np.logical_and(bounding_box[0] <= towers[:, 0], +# towers[:, 0] <= bounding_box[1]), +# np.logical_and(bounding_box[2] <= towers[:, 1], +# towers[:, 1] <= bounding_box[3])) + + +# def voronoi(towers, bounding_box): +# # Select towers inside the bounding box +# i = in_box(towers, bounding_box) +# # Mirror points +# points_center = towers[i, :] +# points_left = np.copy(points_center) +# points_left[:, 0] = bounding_box[0] - (points_left[:, 0] - bounding_box[0]) +# points_right = np.copy(points_center) +# points_right[:, 0] = bounding_box[1] + (bounding_box[1] - points_right[:, 0]) +# points_down = np.copy(points_center) +# points_down[:, 1] = bounding_box[2] - (points_down[:, 1] - bounding_box[2]) +# points_up = np.copy(points_center) +# points_up[:, 1] = bounding_box[3] + (bounding_box[3] - points_up[:, 1]) +# points = np.append(points_center, +# np.append(np.append(points_left, +# points_right, +# axis=0), +# np.append(points_down, +# points_up, +# axis=0), +# axis=0), +# axis=0) +# # Compute Voronoi +# vor = spatial.Voronoi(points) +# # Filter regions +# regions = [] +# for region in vor.regions: +# flag = True +# for index in region: +# if index == -1: +# flag = False +# break +# else: +# x = vor.vertices[index, 0] +# y = vor.vertices[index, 1] +# if not(bounding_box[0] - eps <= x and x <= bounding_box[1] + eps and +# bounding_box[2] - eps <= y and y <= bounding_box[3] + eps): +# flag = False +# break +# if region != [] and flag: +# regions.append(region) +# vor.filtered_points = points_center +# vor.filtered_regions = regions +# return vor + +# def centroid_region(vertices): +# # Polygon's signed area +# A = 0 +# # Centroid's x +# C_x = 0 +# # Centroid's y +# C_y = 0 +# for i in range(0, len(vertices) - 1): +# s = (vertices[i, 0] * vertices[i + 1, 1] - vertices[i + 1, 0] * vertices[i, 1]) +# A = A + s +# C_x = C_x + (vertices[i, 0] + vertices[i + 1, 0]) * s +# C_y = C_y + (vertices[i, 1] + vertices[i + 1, 1]) * s +# A = 0.5 * A +# C_x = (1.0 / (6.0 * A)) * C_x +# C_y = (1.0 / (6.0 * A)) * C_y +# return np.array([[C_x, C_y]]) + +# vor = voronoi(towers, bounding_box) diff --git a/src/novel_swarms/metrics/__init__.py b/src/novel_swarms/metrics/__init__.py index 0a132b57..62bb8a9b 100644 --- a/src/novel_swarms/metrics/__init__.py +++ b/src/novel_swarms/metrics/__init__.py @@ -24,6 +24,7 @@ ) from .DistanceSizeRatio import DistanceSizeRatio from .DelaunayDispersal import Dispersal +from .VoronoiDispersal import VoronoiRelaxation __all__ = [ "AbstractMetric", @@ -56,4 +57,5 @@ "InstantKMHCircularity", "DistanceSizeRatio", "Dispersal", + "VoronoiRelaxation", ] From f7d3029b9dd6b244540a004405f27c727dd9dfef Mon Sep 17 00:00:00 2001 From: Teyema Date: Wed, 21 May 2025 22:47:36 -0400 Subject: [PATCH 2/5] still wip voronoi --- src/novel_swarms/metrics/VoronoiDispersal.py | 182 ++++++++++--------- 1 file changed, 93 insertions(+), 89 deletions(-) diff --git a/src/novel_swarms/metrics/VoronoiDispersal.py b/src/novel_swarms/metrics/VoronoiDispersal.py index 9ed7ace3..3d4d258e 100644 --- a/src/novel_swarms/metrics/VoronoiDispersal.py +++ b/src/novel_swarms/metrics/VoronoiDispersal.py @@ -26,6 +26,7 @@ def __init__(self, history=100, regularize=True): self.regularize = regularize self.allpairs = [] self.lines = [] + def attach_world(self, world: RectangularWorld): super().attach_world(world) @@ -36,19 +37,46 @@ def attach_world(self, world: RectangularWorld): def calculate(self): + + bounding_box = np.array([0., self.world_size[0], 0., self.world_size[0]]) # [x_min, x_max, y_min, y_max] + points = np.array([agent.getPosition() for agent in self.population]) - self.v = v = spatial.Voronoi(points) + # self.v = v = spatial.Voronoi(points) + # allpairs = set() + # for vertex in v.ridge_vertices: + # vertex = np.asarray(vertex) + # if np.all(vertex >= 0): + # allpairs.add(tuple(sorted(vertex))) + + # self.allpairs = np.asarray(list(allpairs), dtype=np.int32) + + self.vor = vor = self.voronoi(points, bounding_box) allpairs = set() - for vertex in v.ridge_vertices: - vertex = np.asarray(vertex) - if np.all(vertex >= 0): - allpairs.add(tuple(sorted(vertex))) + # allridgepoints = np.array([[]]) + for region in self.vor.filtered_regions: + vertices = self.vor.vertices[region + [region[0]], :] - self.allpairs = np.asarray(list(allpairs), dtype=np.int32) - self.lines = self.v.vertices[self.allpairs] - self.lines = np.clip(self.lines, 0, self.world_size) + allpairs.update([tuple(vertex) for vertex in vertices]) + + # self.lines = np.asarray([[vertices[ridge_point[0]], vertices[ridge_point[1]]] for ridge_point in ridge_points], dtype=np.int32) + + # # allpairs = [pair for pair in list(allpairs) if pair[0] != pair[1]] + self.allpairs = np.asarray(list(allpairs)) + # # ridgepoints = np.unique(np.concatenate([self.vor.vertices[region, :] for region in self.vor.filtered_regions]), axis=0) + # ridgepoints = np.concatenate([self.vor.vertices[region, :] for region in self.vor.filtered_regions]) + # # ridgepoints = list(np.concatenate((ridgepoints[:]))) + # # allridgepoints = list(ridgepoints) + for i in range(len(self.allpairs)): + for j in range(len(self.allpairs)): + self.lines.append([self.allpairs[i, 0], self.allpairs[j, 1]]) + + + # for region in self.vor.filtered_regions: + # ridge_points = self.vor.vertices[region, :] + + # self.lines = np.asarray([[self.v.points[ridge_point[0]], self.v.points[ridge_point[1]]] for ridge_point in self.v.ridge_points], dtype=np.int32) # distances = np.array([d.plane_distance(p) for p in points]) @@ -65,87 +93,63 @@ def draw(self, screen, offset): pan, zoom = np.asarray(offset[0]), offset[1] super().draw(screen, offset) + + for line in self.lines: pygame.draw.line(screen, (128, 128, 128), *line * zoom + pan, width=1) -# This is a modified version of the code from here: -# https://stackoverflow.com/questions/28665491/getting-a-bounded-polygon-coordinates-from-voronoi-cells - - - -# eps = sys.float_info.epsilon - -# n_towers = 100 -# towers = np.random.rand(n_towers, 2) -# bounding_box = np.array([0., 1., 0., 1.]) # [x_min, x_max, y_min, y_max] - -# def in_box(towers, bounding_box): -# return np.logical_and(np.logical_and(bounding_box[0] <= towers[:, 0], -# towers[:, 0] <= bounding_box[1]), -# np.logical_and(bounding_box[2] <= towers[:, 1], -# towers[:, 1] <= bounding_box[3])) - - -# def voronoi(towers, bounding_box): -# # Select towers inside the bounding box -# i = in_box(towers, bounding_box) -# # Mirror points -# points_center = towers[i, :] -# points_left = np.copy(points_center) -# points_left[:, 0] = bounding_box[0] - (points_left[:, 0] - bounding_box[0]) -# points_right = np.copy(points_center) -# points_right[:, 0] = bounding_box[1] + (bounding_box[1] - points_right[:, 0]) -# points_down = np.copy(points_center) -# points_down[:, 1] = bounding_box[2] - (points_down[:, 1] - bounding_box[2]) -# points_up = np.copy(points_center) -# points_up[:, 1] = bounding_box[3] + (bounding_box[3] - points_up[:, 1]) -# points = np.append(points_center, -# np.append(np.append(points_left, -# points_right, -# axis=0), -# np.append(points_down, -# points_up, -# axis=0), -# axis=0), -# axis=0) -# # Compute Voronoi -# vor = spatial.Voronoi(points) -# # Filter regions -# regions = [] -# for region in vor.regions: -# flag = True -# for index in region: -# if index == -1: -# flag = False -# break -# else: -# x = vor.vertices[index, 0] -# y = vor.vertices[index, 1] -# if not(bounding_box[0] - eps <= x and x <= bounding_box[1] + eps and -# bounding_box[2] - eps <= y and y <= bounding_box[3] + eps): -# flag = False -# break -# if region != [] and flag: -# regions.append(region) -# vor.filtered_points = points_center -# vor.filtered_regions = regions -# return vor - -# def centroid_region(vertices): -# # Polygon's signed area -# A = 0 -# # Centroid's x -# C_x = 0 -# # Centroid's y -# C_y = 0 -# for i in range(0, len(vertices) - 1): -# s = (vertices[i, 0] * vertices[i + 1, 1] - vertices[i + 1, 0] * vertices[i, 1]) -# A = A + s -# C_x = C_x + (vertices[i, 0] + vertices[i + 1, 0]) * s -# C_y = C_y + (vertices[i, 1] + vertices[i + 1, 1]) * s -# A = 0.5 * A -# C_x = (1.0 / (6.0 * A)) * C_x -# C_y = (1.0 / (6.0 * A)) * C_y -# return np.array([[C_x, C_y]]) - -# vor = voronoi(towers, bounding_box) + # This is a modified version of the code from here: + # https://stackoverflow.com/questions/28665491/getting-a-bounded-polygon-coordinates-from-voronoi-cells + @staticmethod + def in_box(towers, bounding_box): + return np.logical_and(np.logical_and(bounding_box[0] <= towers[:, 0], + towers[:, 0] <= bounding_box[1]), + np.logical_and(bounding_box[2] <= towers[:, 1], + towers[:, 1] <= bounding_box[3])) + + @staticmethod + def voronoi(towers, bounding_box): + eps = sys.float_info.epsilon + # Select towers inside the bounding box + i = VoronoiRelaxation.in_box(towers, bounding_box) + # Mirror points + points_center = towers[i, :] + points_left = np.copy(points_center) + points_left[:, 0] = bounding_box[0] - (points_left[:, 0] - bounding_box[0]) + points_right = np.copy(points_center) + points_right[:, 0] = bounding_box[1] + (bounding_box[1] - points_right[:, 0]) + points_down = np.copy(points_center) + points_down[:, 1] = bounding_box[2] - (points_down[:, 1] - bounding_box[2]) + points_up = np.copy(points_center) + points_up[:, 1] = bounding_box[3] + (bounding_box[3] - points_up[:, 1]) + points = np.append(points_center, + np.append(np.append(points_left, + points_right, + axis=0), + np.append(points_down, + points_up, + axis=0), + axis=0), + axis=0) + # Compute Voronoi + vor = spatial.Voronoi(points) + # Filter regions + regions = [] + for region in vor.regions: + flag = True + for index in region: + if index == -1: + flag = False + break + else: + x = vor.vertices[index, 0] + y = vor.vertices[index, 1] + if not(bounding_box[0] - eps <= x and x <= bounding_box[1] + eps and + bounding_box[2] - eps <= y and y <= bounding_box[3] + eps): + flag = False + break + if region != [] and flag: + regions.append(region) + vor.filtered_points = points_center + vor.filtered_regions = regions + return vor \ No newline at end of file From 9b916d975d8c10a4cbf99eab4b2a779854111a4a Mon Sep 17 00:00:00 2001 From: Teyema Date: Thu, 29 May 2025 12:03:00 -0700 Subject: [PATCH 3/5] Bad metric for training but voronoi works --- src/novel_swarms/metrics/VoronoiDispersal.py | 91 ++++++++++++-------- 1 file changed, 53 insertions(+), 38 deletions(-) diff --git a/src/novel_swarms/metrics/VoronoiDispersal.py b/src/novel_swarms/metrics/VoronoiDispersal.py index 3d4d258e..3716ecb2 100644 --- a/src/novel_swarms/metrics/VoronoiDispersal.py +++ b/src/novel_swarms/metrics/VoronoiDispersal.py @@ -51,53 +51,47 @@ def calculate(self): # self.allpairs = np.asarray(list(allpairs), dtype=np.int32) self.vor = vor = self.voronoi(points, bounding_box) - allpairs = set() - # allridgepoints = np.array([[]]) - for region in self.vor.filtered_regions: - vertices = self.vor.vertices[region + [region[0]], :] - - allpairs.update([tuple(vertex) for vertex in vertices]) - - # self.lines = np.asarray([[vertices[ridge_point[0]], vertices[ridge_point[1]]] for ridge_point in ridge_points], dtype=np.int32) - - # # allpairs = [pair for pair in list(allpairs) if pair[0] != pair[1]] - self.allpairs = np.asarray(list(allpairs)) - - # # ridgepoints = np.unique(np.concatenate([self.vor.vertices[region, :] for region in self.vor.filtered_regions]), axis=0) - # ridgepoints = np.concatenate([self.vor.vertices[region, :] for region in self.vor.filtered_regions]) - # # ridgepoints = list(np.concatenate((ridgepoints[:]))) - # # allridgepoints = list(ridgepoints) - - for i in range(len(self.allpairs)): - for j in range(len(self.allpairs)): - self.lines.append([self.allpairs[i, 0], self.allpairs[j, 1]]) - + + allpairs = [] + centroids = [] + if self.vor != []: + for region in self.vor.filtered_regions: + vertices = self.vor.vertices[region + [region[0]], :] + centroid = self.centroid_region(vertices) + centroids.append(list(centroid[0, :])) + + for i in range(1, len(vertices)): + allpairs.append([vertices[i-1], vertices[i]]) + + self.lines = np.asarray(allpairs) + self.centroids = np.asarray(centroids) - # for region in self.vor.filtered_regions: - # ridge_points = self.vor.vertices[region, :] - - - # self.lines = np.asarray([[self.v.points[ridge_point[0]], self.v.points[ridge_point[1]]] for ridge_point in self.v.ridge_points], dtype=np.int32) # distances = np.array([d.plane_distance(p) for p in points]) - distances = np.array([np.linalg.norm(a - b) for a, b in self.lines]) - var = distances.var() - mean = distances.mean() - # bbox_size = (d.max_bound - d.min_bound) - # bbox_ratio = min(bbox_size) / max(bbox_size) - # self.set_value(bbox_area / 10 - ((1 + var * 10) * mean)) - # dispersal = bbox_size.prod() * bbox_ratio / (1 + var * 10) - # self.set_value(dispersal if dispersal is not None else 0) + min_distances = [] + # assert len(self.centroids) == len(points) + for centroid in self.centroids: + for a, b in zip(centroid, points): + distances = np.asarray([np.linalg.norm(a - b)]) + min_distances.append(distances.min()) + + + # distances = np.array([np.linalg.norm(a - b) for centroid in self.centroids for a, b in zip(centroid, points)]) + # var = distances.var() + # mean = distances.mean() + self.set_value(-sum(min_distances)) + def draw(self, screen, offset): pan, zoom = np.asarray(offset[0]), offset[1] super().draw(screen, offset) - - for line in self.lines: pygame.draw.line(screen, (128, 128, 128), *line * zoom + pan, width=1) + for centroid in self.centroids: + pygame.draw.circle(screen, (128, 128, 128), centroid * zoom + pan, radius=5, width=1) + # This is a modified version of the code from here: # https://stackoverflow.com/questions/28665491/getting-a-bounded-polygon-coordinates-from-voronoi-cells @staticmethod @@ -132,7 +126,10 @@ def voronoi(towers, bounding_box): axis=0), axis=0) # Compute Voronoi - vor = spatial.Voronoi(points) + if len(points) != 0: + vor = spatial.Voronoi(points) + else: + return [] # Filter regions regions = [] for region in vor.regions: @@ -152,4 +149,22 @@ def voronoi(towers, bounding_box): regions.append(region) vor.filtered_points = points_center vor.filtered_regions = regions - return vor \ No newline at end of file + return vor + + @staticmethod + def centroid_region(vertices): + # Polygon's signed area + A = 0 + # Centroid's x + C_x = 0 + # Centroid's y + C_y = 0 + for i in range(0, len(vertices) - 1): + s = (vertices[i, 0] * vertices[i + 1, 1] - vertices[i + 1, 0] * vertices[i, 1]) + A = A + s + C_x = C_x + (vertices[i, 0] + vertices[i + 1, 0]) * s + C_y = C_y + (vertices[i, 1] + vertices[i + 1, 1]) * s + A = 0.5 * A + C_x = (1.0 / (6.0 * A)) * C_x + C_y = (1.0 / (6.0 * A)) * C_y + return np.array([[C_x, C_y]]) \ No newline at end of file From a71f203482e08e779a2222369f0b77d3d127a6ac Mon Sep 17 00:00:00 2001 From: Teyema Date: Fri, 30 May 2025 16:05:11 -0400 Subject: [PATCH 4/5] Voronoi Dispersal Metric --- src/novel_swarms/metrics/VoronoiDispersal.py | 22 ++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/novel_swarms/metrics/VoronoiDispersal.py b/src/novel_swarms/metrics/VoronoiDispersal.py index 3716ecb2..126f202a 100644 --- a/src/novel_swarms/metrics/VoronoiDispersal.py +++ b/src/novel_swarms/metrics/VoronoiDispersal.py @@ -71,15 +71,29 @@ def calculate(self): min_distances = [] # assert len(self.centroids) == len(points) for centroid in self.centroids: - for a, b in zip(centroid, points): - distances = np.asarray([np.linalg.norm(a - b)]) - min_distances.append(distances.min()) + distances = [] + for point in points: + distances.append(np.linalg.norm(centroid - point)) + + min_distances.append(min(distances)) + + # for a, b in zip(centroid, points): + # distances = np.asarray([np.linalg.norm(a - b)]) + + # min_distances.append(distances.min()) # distances = np.array([np.linalg.norm(a - b) for centroid in self.centroids for a, b in zip(centroid, points)]) # var = distances.var() # mean = distances.mean() - self.set_value(-sum(min_distances)) + # if len(self.centroids) == len(self.population): + # self.set_value(-sum(min_distances)) + # else: + # self.set_value(-1000) + if np.all(VoronoiRelaxation.in_box(points, bounding_box)): + self.set_value(-sum(min_distances)) + else: + self.set_value(-1000) def draw(self, screen, offset): From 4aa6e9853c209afc30161000ca80a8a8a8222173 Mon Sep 17 00:00:00 2001 From: Teyema Date: Mon, 2 Jun 2025 17:00:52 -0400 Subject: [PATCH 5/5] Working Voronoi Metric cycles >=1500 --- src/novel_swarms/metrics/VoronoiDispersal.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/novel_swarms/metrics/VoronoiDispersal.py b/src/novel_swarms/metrics/VoronoiDispersal.py index 126f202a..d3ea40fc 100644 --- a/src/novel_swarms/metrics/VoronoiDispersal.py +++ b/src/novel_swarms/metrics/VoronoiDispersal.py @@ -38,8 +38,8 @@ def attach_world(self, world: RectangularWorld): def calculate(self): - bounding_box = np.array([0., self.world_size[0], 0., self.world_size[0]]) # [x_min, x_max, y_min, y_max] - + bounding_box = np.array([0.0, self.world_size[0], 0.0, self.world_size[0]]) # [x_min, x_max, y_min, y_max] + points = np.array([agent.getPosition() for agent in self.population]) # self.v = v = spatial.Voronoi(points) # allpairs = set() @@ -57,6 +57,7 @@ def calculate(self): if self.vor != []: for region in self.vor.filtered_regions: vertices = self.vor.vertices[region + [region[0]], :] + # centroid_vertices = self.vor.vertices[] centroid = self.centroid_region(vertices) centroids.append(list(centroid[0, :])) @@ -117,7 +118,8 @@ def in_box(towers, bounding_box): @staticmethod def voronoi(towers, bounding_box): - eps = sys.float_info.epsilon + # eps = sys.float_info.epsilon + eps = 0.01 # Select towers inside the bounding box i = VoronoiRelaxation.in_box(towers, bounding_box) # Mirror points