# Source code for jupedsim.distributions

```
# Copyright © 2012-2024 Forschungszentrum Jülich GmbH
# SPDX-License-Identifier: LGPL-3.0-or-later
import numpy as np
import shapely
from jupedsim.internal.grid import Grid
def __intersecting_area_polygon_circle(mid_point, radius, polygon):
"""returns the intersecting area of circle and polygon"""
# creates a point
point = shapely.Point(mid_point)
# creates a layer with the size of the radius all around this point
circle = point.buffer(radius)
# returns the size of the intersecting area
return polygon.intersection(circle).area
def __is_inside_circle(point, mid, min_r, max_r):
"""checks if a point is inside a Circle segment reaching from minimum radius to maximum radius"""
dif_x = point[0] - mid[0]
dif_y = point[1] - mid[1]
circle_equation = dif_x**2 + dif_y**2
return min_r**2 <= circle_equation <= max_r**2
def __get_bounding_box(polygon):
"""returns an Axis Aligned Bounding Box containing the minimal/maximal x and y values
formatted like : [(min(x_values), min(y_values)), (max(x_values), max(y_values))]
polygon needs to be a shapely polygon
"""
corner_points = list(polygon.exterior.coords)
x_values, y_values = [], []
for point in corner_points:
x_values.append(point[0])
y_values.append(point[1])
return [(min(x_values), min(y_values)), ((max(x_values)), max(y_values))]
def __min_distance_to_polygon(pt, polygon):
"""returns the minimal distance between a point and every line segment of a polygon"""
pt = shapely.Point(pt)
min_dist = polygon.exterior.distance(pt)
for hole in polygon.interiors:
candidate_dist = hole.distance(pt)
min_dist = min(min_dist, candidate_dist)
return min_dist
[docs]def distribute_by_number(
*,
polygon: shapely.Polygon,
number_of_agents: int,
distance_to_agents: float,
distance_to_polygon: float,
seed: int | None = None,
max_iterations: int = 10000,
) -> list[tuple[float, float]]:
"""Generates specified number of randomized 2D coordinates.
This function will generate the speficied number of 2D coordinates where
all coordinates are inside the specified geometry and generated coordinates
are constraint by distance_to_agents and distance_to_polygon. This function
may not always be able to generate the requested coordinate because it
cannot do so without violating the constraints. In this case the function
will stop after max_iterations and raise an Exception.
Arguments:
polygon: polygon where the agents shall be placed
number_of_agents: number of agents to be distributed
distance_to_agents: minimal distance between the centers of agents
distance_to_polygon: minimal distance between the center of agents
and the polygon edges
seed: Will be used to seed the random number generator.
max_iterations: Up to max_iterations are attempts are made to
place a random point without constraint violation, default is 10_000
Returns:
2D coordinates
Raises:
:class:`AgentNumberError`: if not all agents could be placed.
:class:`IncorrectParameterError`: if polygon is not of type
:class:`~shapely.Polygon`
"""
if not isinstance(polygon, shapely.Polygon):
raise IncorrectParameterError(
"Polygon is expected to be a shapely Polygon"
)
box = __get_bounding_box(polygon)
np.random.seed(seed)
grid = Grid(box, distance_to_agents)
created_points = 0
iterations = 0
while created_points < number_of_agents:
if iterations > max_iterations:
msg = (
f"Only {created_points} of {number_of_agents} could be placed."
f" density: {round(created_points / polygon.area, 2)} p/m²"
)
raise AgentNumberError(msg)
temp_point = (
np.random.uniform(box[0][0], box[1][0]),
np.random.uniform(box[0][1], box[1][1]),
)
if __check_distance_constraints(
temp_point, distance_to_polygon, grid, polygon
):
grid.append_point(temp_point)
iterations = 0
created_points += 1
else:
iterations += 1
return grid.get_samples()
[docs]def distribute_by_density(
*,
polygon: shapely.Polygon,
density: float,
distance_to_agents: float,
distance_to_polygon: float,
seed: int | None = None,
max_iterations: int = 10000,
) -> list[tuple[float, float]]:
"""Generates randomized 2D coordinates based on a desired agent density per
square meter.
This function will generate as many 2D coordinates as required to reach the
desired density. Essentially this function tries to place area * density
many agents while adhering to the distance_to_polygon and
distance_to_agents constraints. This function may not always be able to
generate the requested coordinate because it cannot do so without violating
the constraints. In this case the function will stop after max_iterations
and raise an Exception.
Arguments:
polygon: Area where to generate 2D coordinates in.
density: desired density in agents per square meter
distance_to_agents: minimal distance between the centers of agents
distance_to_polygon: minimal distance between the center of agents
and the polygon edges
seed: Will be used to seed the random number generator.
max_iterations: Up to max_iterations are attempts are made to
place a random point without constraint violation, default is 10_000
Returns:
2D coordinates
Raises:
:class:`AgentNumberError`: if not all agents could be placed.
:class:`IncorrectParameterError`: if polygon is not of type
:class:`~shapely.Polygon`
"""
if not isinstance(polygon, shapely.Polygon):
raise IncorrectParameterError(
"Polygon is expected to be a shapely Polygon"
)
area = polygon.area
number = round(density * area)
return distribute_by_number(
polygon=polygon,
number_of_agents=number,
distance_to_agents=distance_to_agents,
distance_to_polygon=distance_to_polygon,
seed=seed,
max_iterations=max_iterations,
)
def __catch_wrong_inputs(
polygon, center_point, circle_segment_radii, fill_parameters
):
"""checks if an input parameter is incorrect and raises an Exception"""
if not isinstance(polygon, shapely.Polygon):
raise IncorrectParameterError(
"Polygon is expected to be a shapely Polygon"
)
try:
if len(center_point) != 2:
raise IncorrectParameterError(
f"Center_point expected a tuple of 2 numbers, {len(center_point)} were given"
)
except TypeError:
# center point is no tuple or list
raise IncorrectParameterError(
f"Center_point expected a tuple of 2 numbers, given Type: {type(center_point)}"
)
if len(circle_segment_radii) != len(fill_parameters):
raise IncorrectParameterError(
f"the number of circle segments does not match the number of fill parameters.\n"
f"radii given for {len(circle_segment_radii)} circle segments,"
f"fill parameter given for {len(fill_parameters)} circle segments"
)
for i, c_s_radius in enumerate(circle_segment_radii):
if c_s_radius[0] < 0 or c_s_radius[1] < 0:
raise NegativeValueError(
f"Circle segment {c_s_radius[0]} : {c_s_radius[1]} is expected to be positiv"
)
if c_s_radius[0] >= c_s_radius[1]:
raise OverlappingCirclesError(
f"inner radius bigger than/equal to outer radius\n"
f"a Circle segment from {c_s_radius[0]} to {c_s_radius[1]} is not possible"
)
j = 0
while j < i:
if (
c_s_radius[0] < c_s_radius[1] <= circle_segment_radii[j][0]
or circle_segment_radii[j][1] <= c_s_radius[0] < c_s_radius[1]
):
j = j + 1
continue
else:
raise OverlappingCirclesError(
f"the Circle from {c_s_radius[0]} to {c_s_radius[1]} overlaps with others"
)
[docs]def distribute_in_circles_by_number(
*,
polygon: shapely.Polygon,
distance_to_agents: float,
distance_to_polygon: float,
center_point: tuple[float, float],
circle_segment_radii: list[tuple[float, float]],
numbers_of_agents: list[int],
seed=None,
max_iterations=10_000,
) -> list[tuple[float, float]]:
"""Generates randomized 2D coordinates in a user defined number of rings.
This function will generate 2D coordinates in the intersection of the
polygon and the rings specified by the centerpoint and the min/max radii of
each ring. `number_of_agents` is expected to contain the number of agents
to be placed for each ring. This function may not always be able to
generate the requested coordinate because it cannot do so without violating
the constraints. In this case the function will stop after max_iterations
and raise an Exception.
Arguments:
polygon: polygon where agents can be placed.
distance_to_agents: minimal distance between the centers of agents
distance_to_polygon: minimal distance between the center of agents
and the polygon edges
center_point: Center point of the rings.
circle_segment_radii: min/max radius per ring, rings may not overlap
number_of_agents: agents to be placed per ring
seed: Will be used to seed the random number generator.
max_iterations: Up to max_iterations are attempts are made to
place a random point without constraint violation, default is 10_000
Returns:
2D coordinates
Raises:
:class:`AgentNumberError`: if not all agents could be placed.
:class:`IncorrectParameterError`: if polygon is not of type
:class:`~shapely.Polygon`
:class:`OverlappingCirclesError`: if rings in circle_segment_radii
overlapp
"""
# catch wrong inputs
__catch_wrong_inputs(
polygon=polygon,
center_point=center_point,
circle_segment_radii=circle_segment_radii,
fill_parameters=numbers_of_agents,
)
np.random.seed(seed)
box = __get_bounding_box(polygon)
grid = Grid(box, distance_to_agents)
for circle_segment, number in zip(circle_segment_radii, numbers_of_agents):
outer_radius = circle_segment[1]
inner_radius = circle_segment[0]
big_circle_area = __intersecting_area_polygon_circle(
center_point, outer_radius, polygon
)
small_circle_area = __intersecting_area_polygon_circle(
center_point, inner_radius, polygon
)
placeable_area = big_circle_area - small_circle_area
# checking whether to place points
# inside the circle segment or
# inside the bounding box of the intersection of polygon and Circle Segment
# determine the entire area of the circle segment
entire_circle_area = np.pi * (outer_radius**2 - inner_radius**2)
# determine the area where a point might be placed around the polygon
sec_box = __box_of_intersection(polygon, center_point, outer_radius)
dif_x, dif_y = (
sec_box[1][0] - sec_box[0][0],
sec_box[1][1] - sec_box[0][1],
)
bounding_box_area = dif_x * dif_y
if entire_circle_area < bounding_box_area:
# inside the circle it is more likely to find a random point that is inside the polygon
for placed_count in range(number):
i = 0
while i < max_iterations:
i += 1
# determines a random radius within the circle segment
rho = np.sqrt(
np.random.uniform(inner_radius**2, outer_radius**2)
)
# determines a random degree
theta = np.random.uniform(0, 2 * np.pi)
pt = (
center_point[0] + rho * np.cos(theta),
center_point[1] + rho * np.sin(theta),
)
if __check_distance_constraints(
pt, distance_to_polygon, grid, polygon
):
grid.append_point(pt)
break
if i >= max_iterations and placed_count != number:
message = (
f"the desired amount of agents in the Circle segment from"
f" {inner_radius} to {outer_radius} could not be achieved."
f"\nOnly {placed_count} of {number} could be placed."
f"\nactual density: {round(placed_count / placeable_area, 2)} p/m²"
)
raise AgentNumberError(message)
else:
# placing point inside the bounding box is more likely to find a random point that is inside the circle
placed_count = 0
iterations = 0
while placed_count < number:
if iterations > max_iterations:
message = (
f"the desired amount of agents in the Circle segment from"
f" {inner_radius} to {outer_radius} could not be achieved."
f"\nOnly {placed_count} of {number} could be placed."
f"\nactual density: {round(placed_count / placeable_area, 2)} p/m²"
)
raise AgentNumberError(message)
temp_point = (
np.random.uniform(sec_box[0][0], sec_box[1][0]),
np.random.uniform(sec_box[0][1], sec_box[1][1]),
)
if __is_inside_circle(
temp_point, center_point, inner_radius, outer_radius
) and __check_distance_constraints(
temp_point, distance_to_polygon, grid, polygon
):
grid.append_point(temp_point)
iterations = 0
placed_count += 1
else:
iterations += 1
return grid.get_samples()
[docs]def distribute_in_circles_by_density(
*,
polygon: shapely.Polygon,
distance_to_agents: float,
distance_to_polygon: float,
center_point: tuple[float, float],
circle_segment_radii: list[tuple[float, float]],
densities: list[float],
seed: int | None = None,
max_iterations: int = 10_000,
) -> list[tuple[float, float]]:
"""Generates randomized 2D coordinates in a user defined number of rings
with defined density.
This function will generate 2D coordinates in the intersection of the
polygon and the rings specified by the centerpoint and the min/max radii of
each ring. The number of positions generated is defined by the desired
density and available space of each ring. This function may not always by
able to generate the requested coordinate because it cannot do so without
violating the constraints. In this case the function will stop after
max_iterations and raise an Exception.
Arguments:
polygon: polygon where agents can be placed.
distance_to_agents: minimal distance between the centers of agents
distance_to_polygon: minimal distance between the center of agents
and the polygon edges
center_point: Center point of the rings.
circle_segment_radii: min/max radius per ring, rings may not overlap
desnities: density in positionsper square meter for each ring
seed: Will be used to seed the random number generator.
max_iterations: Up to max_iterations are attempts are made to place a
random point without constraint violation, default is 10_000
Returns:
2D coordinates
Raises:
:class:`AgentNumberError`: if not all agents could be placed.
:class:`IncorrectParameterError`: if polygon is not of type
:class:`~shapely.Polygon`
:class:`OverlappingCirclesError`: if rings in circle_segment_radii
overlapp
"""
__catch_wrong_inputs(
polygon=polygon,
center_point=center_point,
circle_segment_radii=circle_segment_radii,
fill_parameters=densities,
)
number_of_agents = []
for circle_segment, density in zip(circle_segment_radii, densities):
big_circle_area = __intersecting_area_polygon_circle(
center_point, circle_segment[1], polygon
)
small_circle_area = __intersecting_area_polygon_circle(
center_point, circle_segment[0], polygon
)
placeable_area = big_circle_area - small_circle_area
number_of_agents.append(int(density * placeable_area))
return distribute_in_circles_by_number(
polygon=polygon,
distance_to_agents=distance_to_agents,
distance_to_polygon=distance_to_polygon,
center_point=center_point,
circle_segment_radii=circle_segment_radii,
numbers_of_agents=number_of_agents,
seed=seed,
max_iterations=max_iterations,
)
[docs]def distribute_until_filled(
*,
polygon: shapely.Polygon,
distance_to_agents: float,
distance_to_polygon: float,
seed: int | None = None,
max_iterations: int = 10_000,
k: int = 30,
) -> list[tuple[float, float]]:
"""Generates randomized 2D coordinates that fill the specified area.
This function will generate 2D coordinates in the specified area. The
number of positions generated depends on the ability to place aditional
points. This function may not always be able to generate the requested
coordinate because it cannot do so without violating the constraints. In
this case the function will stop after max_iterations and raise an
Exception.
Arguments:
polygon: polygon where agents can be placed.
distance_to_agents: minimal distance between the centers of agents
distance_to_polygon: minimal distance between the center of agents
and the polygon edges
seed: Will be used to seed the random number generator.
max_iterations: Up to max_iterations are attempts are made to
place a random point without constraint violation, default is 10_000
k: maximum number of attempts to place neighbors to already inserted
points. A higher value will result in a higher density but will greatly
increase runtim.
Returns:
2D coordinates
Raises:
:class:`AgentNumberError`: if not all agents could be placed.
:class:`IncorrectParameterError`: if polygon is not of type
:class:`~shapely.Polygon`
"""
if not isinstance(polygon, shapely.Polygon):
raise IncorrectParameterError(
"Polygon is expected to be a shapely Polygon"
)
box = __get_bounding_box(polygon)
np.random.seed(seed)
# initialises a list for active Points and a Grid administering all created points
active = []
grid = Grid(box, distance_to_agents)
# initialisation of the first point
iteration = 0
while iteration < max_iterations:
first_point = (
np.random.uniform(box[0][0], box[1][0]),
np.random.uniform(box[0][1], box[1][1]),
)
if __check_distance_constraints(
first_point, distance_to_polygon, grid, polygon
):
grid.append_point(first_point)
active.append(first_point)
break
iteration = iteration + 1
if iteration >= max_iterations:
raise IncorrectParameterError(
"The first point could not be placed inside the polygon."
" Check if there is enough space for agents provided inside the polygon"
)
# Uses https://www.cs.ubc.ca/~rbridson/docs/bridson-siggraph07-poissondisk.pdf
# "Fast Poisson Disk Sampling in Arbitrary Dimensions"
# while points are active a random reference point is selected
while active:
ref_point = active[np.random.randint(0, len(active))]
iteration = 0
# tries to find a point around the reference Point
while iteration < k:
# determines a random radius within a circle segment
# with radius from distance_to_agents to distance_to_agents * 2
rho = np.sqrt(
np.random.uniform(
distance_to_agents**2, 4 * distance_to_agents**2
)
)
# determines a random degree
theta = np.random.uniform(0, 2 * np.pi)
pt = (
ref_point[0] + rho * np.cos(theta),
ref_point[1] + rho * np.sin(theta),
)
if __check_distance_constraints(
pt, distance_to_polygon, grid, polygon
):
grid.append_point(pt)
active.append(pt)
break
iteration = iteration + 1
# if there was no point found around the reference point it is considered inactive
if iteration >= k:
active.remove(ref_point)
return grid.get_samples()
[docs]def distribute_by_percentage(
*,
polygon: shapely.Polygon,
percent: float,
distance_to_agents: float,
distance_to_polygon: float,
seed: int | None = None,
max_iterations: int = 10000,
k: int = 30,
):
"""Generates randomized 2D coordinates that fill the specified area to a
percentage of a possible maximum.
This function will generate 2D coordinates in the specified area. The
number of positions generated depends on the ability to place aditional
points. This function may not always be able to generate the requested
coordinate because it cannot do so without violating the constraints. In
this case the function will stop after max_iterations and raise an
Exception.
Arguments:
polygon: polygon where agents can be placed.
percent: percent value of occupancy to generate. needs to be in
the intervall (0, 100]
distance_to_agents: minimal distance between the centers of agents
distance_to_polygon: minimal distance between the center of agents
and the polygon edges
seed: Will be used to seed the random number generator.
max_iterations: Up to max_iterations are attempts are made to
place a random point without constraint violation, default is 10_000
k: maximum number of attempts to place neighbors to already inserted
points. A higher value will result in a higher density but will greatly
increase runtim.
Returns:
2D coordinates
Raises:
:class:`AgentNumberError`: if not all agents could be placed.
:class:`IncorrectParameterError`: if polygon is not of type
:class:`~shapely.Polygon`
"""
samples = distribute_until_filled(
polygon=polygon,
distance_to_agents=distance_to_agents,
distance_to_polygon=distance_to_polygon,
seed=seed,
max_iterations=max_iterations,
k=k,
)
sample_amount = len(samples)
needed_amount = round(sample_amount * (percent / 100))
np.random.seed(seed)
np.random.shuffle(samples)
return samples[:needed_amount]
def __check_distance_constraints(pt, wall_distance, grid, polygon):
"""Determines if a point has enough distance to other points and to the walls
Uses a Grid to determine neighbours
:param grid: the grid of the polygon
:param pt: point that is being checked
:param wall_distance: minimal distance between point and the polygon
:param polygon: shapely Polygon in which the points must lie
:return:True or False"""
if not polygon.contains(shapely.Point(pt)):
return False
if __min_distance_to_polygon(pt, polygon) < wall_distance:
return False
return grid.no_neighbours_in_distance(pt)
def __box_of_intersection(polygon, center_point, outer_radius):
"""returns an Axis Aligned Bounding Box containing the intersection of a Circle and the polygon
@:param polygon is a shapely Polygon
@:param center_point is the Center point of the Circle
@:param outer_radius is the radius of the Circle
@:return bounding box formatted like [(min(x_values), min(y_values)), (max(x_values), max(y_values))]
"""
# creates a point
point = shapely.Point(center_point)
# creates a layer with the size of the radius all around this point
circle = point.buffer(outer_radius)
# returns the size of the intersecting area
shapely_bounds = polygon.intersection(circle).bounds
return [shapely_bounds[:2], shapely_bounds[2:]]
```