def__intersecting_area_polygon_circle(mid_point,radius,polygon):"""returns the intersecting area of circle and polygon"""# creates a pointpoint=shapely.Point(mid_point)# creates a layer with the size of the radius all around this pointcircle=point.buffer(radius)# returns the size of the intersecting areareturnpolygon.intersection(circle).areadef__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**2returnmin_r**2<=circle_equation<=max_r**2def__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=[],[]forpointincorner_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)forholeinpolygon.interiors:candidate_dist=hole.distance(pt)min_dist=min(min_dist,candidate_dist)returnmin_dist
[docs]defdistribute_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 coordiantes. This function will generate the speficied number of 2D coordiantes where all coordiantes are inside the specified geometry and generated coordinates are constraint by distance_to_agents and distance_to_polygon. 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 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 conastraint violation, default is 10_000 Returns: 2D coordiantes Raises: :class:`AgentNumberError`: if not all agents could be placed. :class:`IncorrectParameterError`: if polygon is not of type :class:`~shapely.Polygon` """ifnotisinstance(polygon,shapely.Polygon):raiseIncorrectParameterError(f"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=0iterations=0whilecreated_points<number_of_agents:ifiterations>max_iterations:msg=(f"Only {created_points} of {number_of_agents} could be placed."f" density: {round(created_points/polygon.area,2)} p/m²")raiseAgentNumberError(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=0created_points+=1else:iterations+=1returngrid.get_samples()
[docs]defdistribute_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 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: 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 coordiantes Raises: :class:`AgentNumberError`: if not all agents could be placed. :class:`IncorrectParameterError`: if polygon is not of type :class:`~shapely.Polygon` """ifnotisinstance(polygon,shapely.Polygon):raiseIncorrectParameterError(f"Polygon is expected to be a shapely Polygon")area=polygon.areanumber=round(density*area)returndistribute_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"""ifnotisinstance(polygon,shapely.Polygon):raiseIncorrectParameterError(f"Polygon is expected to be a shapely Polygon")try:iflen(center_point)!=2:raiseIncorrectParameterError(f"Center_point expected a tuple of 2 numbers, {len(center_point)} were given")exceptTypeError:# center point is no tuple or listraiseIncorrectParameterError(f"Center_point expected a tuple of 2 numbers, given Type: {type(center_point)}")iflen(circle_segment_radii)!=len(fill_parameters):raiseIncorrectParameterError(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")fori,c_s_radiusinenumerate(circle_segment_radii):ifc_s_radius[0]<0orc_s_radius[1]<0:raiseNegativeValueError(f"Circle segment {c_s_radius[0]} : {c_s_radius[1]} is expected to be positiv")ifc_s_radius[0]>=c_s_radius[1]:raiseOverlappingCirclesError(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=0whilej<i:if(c_s_radius[0]<c_s_radius[1]<=circle_segment_radii[j][0]orcircle_segment_radii[j][1]<=c_s_radius[0]<c_s_radius[1]):j=j+1continueelse:raiseOverlappingCirclesError(f"the Circle from {c_s_radius[0]} to {c_s_radius[1]} overlaps with others")
[docs]defdistribute_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 coordiantes 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 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 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 conastraint violation, default is 10_000 Returns: 2D coordiantes 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)forcircle_segment,numberinzip(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 segmententire_circle_area=np.pi*(outer_radius**2-inner_radius**2)# determine the area where a point might be placed around the polygonsec_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_yifentire_circle_area<bounding_box_area:# inside the circle it is more likely to find a random point that is inside the polygonforplaced_countinrange(number):i=0whilei<max_iterations:i+=1# determines a random radius within the circle segmentrho=np.sqrt(np.random.uniform(inner_radius**2,outer_radius**2))# determines a random degreetheta=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)breakifi>=max_iterationsandplaced_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²")raiseAgentNumberError(message)else:# placing point inside the bounding box is more likely to find a random point that is inside the circleplaced_count=0iterations=0whileplaced_count<number:ifiterations>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²")raiseAgentNumberError(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=0placed_count+=1else:iterations+=1returngrid.get_samples()
[docs]defdistribute_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 coordiantes 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 conastraint violation, default is 10_000 Returns: 2D coordiantes 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=[]forcircle_segment,densityinzip(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_areanumber_of_agents.append(int(density*placeable_area))returndistribute_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]defdistribute_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 coordiantes 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 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 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 conastraint 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 coordiantes Raises: :class:`AgentNumberError`: if not all agents could be placed. :class:`IncorrectParameterError`: if polygon is not of type :class:`~shapely.Polygon` """ifnotisinstance(polygon,shapely.Polygon):raiseIncorrectParameterError(f"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 pointsactive=[]grid=Grid(box,distance_to_agents)# initialisation of the first pointiteration=0whileiteration<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)breakiteration=iteration+1ifiteration>=max_iterations:raiseIncorrectParameterError("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 selectedwhileactive:ref_point=active[np.random.randint(0,len(active))]iteration=0# tries to find a point around the reference Pointwhileiteration<k:# determines a random radius within a circle segment# with radius from distance_to_agents to distance_to_agents * 2rho=np.sqrt(np.random.uniform(distance_to_agents**2,4*distance_to_agents**2))# determines a random degreetheta=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)breakiteration=iteration+1# if there was no point found around the reference point it is considered inactiveifiteration>=k:active.remove(ref_point)returngrid.get_samples()
[docs]defdistribute_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 coordiantes 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 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. 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 conastraint 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 coordiantes 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)returnsamples[: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"""ifnotpolygon.contains(shapely.Point(pt)):returnFalseif__min_distance_to_polygon(pt,polygon)<wall_distance:returnFalsereturngrid.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 pointpoint=shapely.Point(center_point)# creates a layer with the size of the radius all around this pointcircle=point.buffer(outer_radius)# returns the size of the intersecting areashapely_bounds=polygon.intersection(circle).boundsreturn[shapely_bounds[:2],shapely_bounds[2:]]