I've written an algorithm that given a set of 2-D co-ordinates,generates the shortest path to traverse a building given that there are certain prohibited points, i.e., the points where the building is present.
I create a list of these points by using a nested while loops and incrementing between the boundaries by 0.01.
I also then remove these points from a set of potential neighbors that is generated while trying to figure out the next best point to go to.
The heuristic I'm using is to minimize the sum of the distances of the potential point to the start and to the goal.
I have 4 different goals (nodes) and iterate through them and changing the starting points for that iteration when needed.
In spite of this, there are some points being included in the end result that are located where the building is supposed to be, essentially making this a non-viable path.
Any help would be appreciated!
import math
import copy
def distance(point1, point2):
'''
Computers distance between two points, two dimensional for now
'''
dist = math.sqrt(math.pow(point1[0] - point2[0], 2) + math.pow(point1[1] - point2[1], 2))
return dist
def check(point1, point2):
'''
Checks if they are the same point or not
'''
for i in range(len(point1)):
if round(point1[i], 1) != round(point2[i], 1):
return False
return True
def round_point(point):
'''
Rounds all the 2 co-ordinates of a point
'''
for i in range(len(point)):
for j in range(len(point[i])):
point[i][j] = round(point[i][j], 2)
return point
def check_in(points, prohibited, visited):
'''
Removes all prohibited points from points and returns
'''
points = round_point(points)
temp = copy.deepcopy(points)
for i in range(len(points)):
if points[i] in prohibited:
#print('b')
temp.remove(points[i])
elif len(visited) != 0:
if points[i] in visited:
#print('c')
temp.remove(points[i])
return temp
def heuristic(points, start, destination):
'''
Calculates heuristic for each point
'''
l = []
for i in range(len(points)):
dist = distance(start, points[i]) + distance(destination, points[i])
l.append(dist)
return l
def neighbours(point):
'''
Finds all the points at a distance of 0.1m from the given point
'''
points = []
for i in range(360):
y = point[1] + 0.1 * math.sin((i * 2 * math.pi) / 180)
x = point[0] + 0.1 * math.cos((i * 2 * math.pi) / 180)
points.append([x, y])
return points
def best(points, prohibited, start, end, visited):
'''
Chooses the point which is least distance that is not prohibited
'''
points = check_in(points, prohibited, visited)
distances = heuristic(points, start, end)
loc = distances.index(min(distances))
return points[loc]
def search():
'''
Main function that searches
'''
nodes = [[2.5, 1.5], [2.5, 8.5], [7.5, 1.5], [7.5, 8.5]]
# Creates a list of prohibited points
prohibited = []
x = 2.5
y = 1.5
visited = []
while x <= 7.5:
while y <= 8.5:
prohibited.append([x, y])
y += 0.01
x += 0.01
# Path to be traversed
path = []
for i in range(len(nodes)):
if i == 0:
# Start has to be origin for first iteration
start = [0, 0]
else:
start = nodes[i - 1]
destination = nodes[i]
# Calculates points for first iteration
points = neighbours(start)
loc = best(points, prohibited, start, destination, visited)
path.append(loc)
visited.append(loc)
while True:
# If the current location is the destination, loop is done
if check(loc, destination): break
else:
points = neighbours(loc)
loc = best(points, prohibited, start, destination, visited)
path.append(loc)
visited.append(loc)
continue
return path
path = search()
print(path)
print(len(path))
Related
def path(given_map, x, y):
x = given_map[0][0]
y = given_map[0][0]
cnt = 0
if x == len(given_map) and y == len(given_map):
cnt += 1
return cnt
else:
if x < len(given_map) and y < len(given_map):
return path(given_map, x, y)
elif x < len(given_map) and y == len(given_map):
return path(given_map, x, y)
elif x == len(given_map) and y < len(given_map):
return path(given_map, x, y)
else:
cnt = 0
return cnt
if __name__ == '__main__':
input_map = [[1,2,9,4,9],
[1,5,8,7,9],
[9,3,9,9,2],
[2,3,7,5,9],
[1,9,9,1,0]]
print(path(input_map, 0, 0))
input_map = [[1,1,2],
[1,2,2],
[1,2,0]]
print(path(input_map, 0, 0))
The n*n list must be input to create a function that returns the number of paths that can reach from the starting point [0][0] to the ending point [n-1][n-1].
Movement can only be done downward and right, and can only be moved in one direction by the number shown in the corresponding x and y coordinates.
It does not include cases outside the map after movement.
The above code is implemented as much as possible within my ability. How can I modify it to function normally?
Unless you are required to implement this using recursion, I would suggest a BFS (Breadth First Search) approach using a dictionary to track the positions/count that you have reached at each step of the progression:
def path(M):
result = 0 # final result
bounds = range(len(M)) # inside map
paths = {(0,0):1} # one path at start
while paths: # spread counts
nextPos = dict() # track next positions
for (x,y),count in paths.items(): # extend positon/count
if x == y == len(M)-1: result += count # capture final count
dist = M[x][y] # travel distance
if not dist: continue # no movement ...
for x2,y2 in [(x,y+dist),(x+dist,y)]: # travel down & right
if x2 in bounds and y2 in bounds: # propagate count
nextPos[x2,y2] = nextPos.get((x2,y2),0)+count
paths = nextPos # next step on paths
return result
input_map = [[1,2,9,4,9],
[1,5,8,7,9],
[9,3,9,9,2],
[2,3,7,5,9],
[1,9,9,1,0]]
print(path(input_map)) # 2
input_map = [[1,1,2],
[1,2,2],
[1,2,0]]
print(path(input_map)) # 1
If you are required to implement a recursive approach, the function can be adapted like this (returning the sum of recursive path counts) but then it won't be a BFS anymore:
def path(M,x=0,y=0):
if x == y == len(M)-1: return 1 # goal reached
dist = M[x][y] # travel distance
if not dist: return 0 # no movement
bounds = range(len(M)) # inside map
return sum(path(M,x2,y2) for x2,y2 in [(x,y+dist),(x+dist,y)]
if x2 in bounds and y2 in bounds) # sum of paths
A video by Sebastion Lague explained the Bridson's algorithm really well.
To oversimplify,
Create cell grid that has sides of radius/sqrt(2).
Place initial point and list as spawnpoint.
Place point into cell in grid.
For any spawnpoint, spawn a point between radius and 2*radius.
Look at the cells 2 units away from cell of new point.
If contains other points, compare distance.
If any point is closer to new point than the radius, new point is invalid.
If new point is valid, new point is listed as spawnpoint and placed into cell in grid.
If spawnpoint spawns too many invalid points, spawnpoint is removed and turns into point.
Repeat until no more spawnpoints exists.
Return points.
I basically written the same thing down in Python 3.7.2 and pygame 1.7~, but as said in the title, I'm stuck in recursive purgatory.
I used one Point() class for this algorithm, which might seem redundant given that pygame.Vector2() exists, but I needed some elements for a separate algorithm (Delaunay's with infinite vertices) that required this class to work.
For the sake of simplicity I'm going to cut away all the Delaunay-specific elements and show the bare-bones of this class that is needed for this algorithm:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def DistanceToSquared(self,other):
return (self.x-other.x)**2 + (self.y-other.y)**2
The code that is related to the Bridson's algorithm is:
def PoissonDiskSampling(width, height, radius, startPos = None, spawnAttempts = 10):
if startPos == None:
startPos = [width//2,height//2]
cellSize = radius / math.sqrt(2)
cellNumberX = int(width // cellSize + 1) # Initialise a cells grid for optimisation
cellNumberY = int(height // cellSize + 1)
cellGrid = [[None for x in range(cellNumberX)] for y in range(cellNumberY)]
startingPoint = Point(startPos[0],startPos[1]) # Add an iniial point for spawning purposes
cellGrid[startingPoint.x//radius][startingPoint.y//radius] = startingPoint
points = [startingPoint] # Initialise 2 lists tracking all points and active points
spawnpoints = [startingPoint]
while len(spawnpoints) > 0:
spawnIndex = random.randint(0,len(spawnpoints)-1)
spawnpoint = spawnpoints[spawnIndex]
spawned = False
for i in range(spawnAttempts):
r = random.uniform(radius,2*radius)
radian = random.uniform(0,2*math.pi)
newPoint = Point(spawnpoint.x + r*math.cos(radian),
spawnpoint.y + r*math.sin(radian))
if 0 <= newPoint.x <= width and 0 <= newPoint.y <= height:
isValid = True
else:
continue
newPointIndex = [int(newPoint.x//cellSize), int(newPoint.y//cellSize)]
neighbours = FindNeighbours(cellNumberX,cellNumberY,newPointIndex,cellGrid)
for neighbour in neighbours:
if newPoint.DistanceToSquared(neighbour) < radius**2:
isValid = False
break
if isValid:
points.append(newPoint)
spawnpoints.append(newPoint)
spawned = True
break
else:
continue
if spawned == False:
spawnpoints.remove(spawnpoint)
return points
def FindNeighbours(cellNumberX, cellNumberY, index, cellGrid):
neighbours = []
for cellX in range(max(0,(index[0]-2)), min(cellNumberX,(index[1]+2))):
for cellY in range(max(0,(index[0]-2)), min(cellNumberY,(index[1]+2))):
if cellGrid[cellX][cellY] != None:
neighbours.append(cellGrid[cellX][cellY])
return neighbours
Please help.
The probably most important step is missing in your code:
If new point is valid, new point is listed as spawnpoint and placed into cell in grid.
I suggest to add the point to the cellGrid if it is valid:
if isValid:
cellGrid[newPointIndex[0]][newPointIndex[1]] = newPoint
points.append(newPoint)
spawnpoints.append(newPoint)
spawned = True
break
Further, you have to verify if the cell with the index newPointIndex is not already occupied before a point can be add:
newPointIndex = [int(newPoint.x/cellSize), int(newPoint.y/cellSize)]
if cellGrid[newPointIndex[0]][newPointIndex[1]] != None:
continue
neighbours = FindNeighbours(cellNumberX,cellNumberY,newPointIndex,cellGrid)
Finally there is an issue in the function FindNeighbours. range(start, stop) creates a range for x in start <= x < stop.
So the stop has to be index[0]+3 rather than index[0]+2.
Further the ranges which control the 2 nested for loops, run both from x-2 to y+2 rather than from x-2 to x+2 respectively from y-2 to y+2:
for cellX in range(max(0,(index[0]-2)), min(cellNumberX,(index[1]+2))):
for cellY in range(max(0,(index[0]-2)), min(cellNumberY,(index[1]+2)))
The fixed function has to be:
def FindNeighbours(cellNumberX, cellNumberY, index, cellGrid):
neighbours = []
for cellX in range(max(0, index[0]-2), min(cellNumberX, index[0]+3)):
for cellY in range(max(0, index[1]-2), min(cellNumberY, index[1]+3)):
if cellGrid[cellX][cellY] != None:
neighbours.append(cellGrid[cellX][cellY])
return neighbours
See the result, for a size of 300 x 300 and a radius of 15:
An even better result can be achieve, if always the 1st point of spawnpoints is used to rather than a random point:
# spawnIndex = random.randint(0,len(spawnpoints)-1)
spawnIndex = 0 # 0 rather than random
spawnpoint = spawnpoints[spawnIndex]
A video by Sebastion Lague explained the Bridson's algorithm really well.
To oversimplify,
Create cell grid that has sides of radius/sqrt(2).
Place initial point and list as spawnpoint.
Place point into cell in grid.
For any spawnpoint, spawn a point between radius and 2*radius.
Look at the cells 2 units away from cell of new point.
If contains other points, compare distance.
If any point is closer to new point than the radius, new point is invalid.
If new point is valid, new point is listed as spawnpoint and placed into cell in grid.
If spawnpoint spawns too many invalid points, spawnpoint is removed and turns into point.
Repeat until no more spawnpoints exists.
Return points.
I basically written the same thing down in Python 3.7.2 and pygame 1.7~, but as said in the title, I'm stuck in recursive purgatory.
I used one Point() class for this algorithm, which might seem redundant given that pygame.Vector2() exists, but I needed some elements for a separate algorithm (Delaunay's with infinite vertices) that required this class to work.
For the sake of simplicity I'm going to cut away all the Delaunay-specific elements and show the bare-bones of this class that is needed for this algorithm:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def DistanceToSquared(self,other):
return (self.x-other.x)**2 + (self.y-other.y)**2
The code that is related to the Bridson's algorithm is:
def PoissonDiskSampling(width, height, radius, startPos = None, spawnAttempts = 10):
if startPos == None:
startPos = [width//2,height//2]
cellSize = radius / math.sqrt(2)
cellNumberX = int(width // cellSize + 1) # Initialise a cells grid for optimisation
cellNumberY = int(height // cellSize + 1)
cellGrid = [[None for x in range(cellNumberX)] for y in range(cellNumberY)]
startingPoint = Point(startPos[0],startPos[1]) # Add an iniial point for spawning purposes
cellGrid[startingPoint.x//radius][startingPoint.y//radius] = startingPoint
points = [startingPoint] # Initialise 2 lists tracking all points and active points
spawnpoints = [startingPoint]
while len(spawnpoints) > 0:
spawnIndex = random.randint(0,len(spawnpoints)-1)
spawnpoint = spawnpoints[spawnIndex]
spawned = False
for i in range(spawnAttempts):
r = random.uniform(radius,2*radius)
radian = random.uniform(0,2*math.pi)
newPoint = Point(spawnpoint.x + r*math.cos(radian),
spawnpoint.y + r*math.sin(radian))
if 0 <= newPoint.x <= width and 0 <= newPoint.y <= height:
isValid = True
else:
continue
newPointIndex = [int(newPoint.x//cellSize), int(newPoint.y//cellSize)]
neighbours = FindNeighbours(cellNumberX,cellNumberY,newPointIndex,cellGrid)
for neighbour in neighbours:
if newPoint.DistanceToSquared(neighbour) < radius**2:
isValid = False
break
if isValid:
points.append(newPoint)
spawnpoints.append(newPoint)
spawned = True
break
else:
continue
if spawned == False:
spawnpoints.remove(spawnpoint)
return points
def FindNeighbours(cellNumberX, cellNumberY, index, cellGrid):
neighbours = []
for cellX in range(max(0,(index[0]-2)), min(cellNumberX,(index[1]+2))):
for cellY in range(max(0,(index[0]-2)), min(cellNumberY,(index[1]+2))):
if cellGrid[cellX][cellY] != None:
neighbours.append(cellGrid[cellX][cellY])
return neighbours
Please help.
The probably most important step is missing in your code:
If new point is valid, new point is listed as spawnpoint and placed into cell in grid.
I suggest to add the point to the cellGrid if it is valid:
if isValid:
cellGrid[newPointIndex[0]][newPointIndex[1]] = newPoint
points.append(newPoint)
spawnpoints.append(newPoint)
spawned = True
break
Further, you have to verify if the cell with the index newPointIndex is not already occupied before a point can be add:
newPointIndex = [int(newPoint.x/cellSize), int(newPoint.y/cellSize)]
if cellGrid[newPointIndex[0]][newPointIndex[1]] != None:
continue
neighbours = FindNeighbours(cellNumberX,cellNumberY,newPointIndex,cellGrid)
Finally there is an issue in the function FindNeighbours. range(start, stop) creates a range for x in start <= x < stop.
So the stop has to be index[0]+3 rather than index[0]+2.
Further the ranges which control the 2 nested for loops, run both from x-2 to y+2 rather than from x-2 to x+2 respectively from y-2 to y+2:
for cellX in range(max(0,(index[0]-2)), min(cellNumberX,(index[1]+2))):
for cellY in range(max(0,(index[0]-2)), min(cellNumberY,(index[1]+2)))
The fixed function has to be:
def FindNeighbours(cellNumberX, cellNumberY, index, cellGrid):
neighbours = []
for cellX in range(max(0, index[0]-2), min(cellNumberX, index[0]+3)):
for cellY in range(max(0, index[1]-2), min(cellNumberY, index[1]+3)):
if cellGrid[cellX][cellY] != None:
neighbours.append(cellGrid[cellX][cellY])
return neighbours
See the result, for a size of 300 x 300 and a radius of 15:
An even better result can be achieve, if always the 1st point of spawnpoints is used to rather than a random point:
# spawnIndex = random.randint(0,len(spawnpoints)-1)
spawnIndex = 0 # 0 rather than random
spawnpoint = spawnpoints[spawnIndex]
I want to finish my tower defense game as fast as I can.
I have trouble with if the tower is on the path with a given path list.
It will never work even is I try as hard as I can.
I already tried a lot of times to solve that problem like tech with tim's tower defense youtube tutorial. It made almost perfect sense why it was not working.
But no matter how hard I try, It seems to never work properly.
Find the link to the tutorial here.
WARNING:The video is pretty long.
x, y = tower.x, tower.y
for n, point in enumerate(path):
point_x, point_y = point[0], point[1]
dis_x = abs(point_x - x)
dis_y = abs(point_y - y)
dis = math.sqrt((dis_x - x)**2 + (dis_y - y)**2)
print(dis)
if dis < 130:
return False
return True
You might be thinking 'Why did I do this' so I changed it a bit:
import numpy as np
closest = []
x, y = tower.x, tower.y
for n, point in enumerate(path):
point_x, point_y = point[0], point[1]
dis_x = abs(point_x - x)
dis_y = abs(point_y - y)
dis = math.sqrt((dis_x - x)**2 + (dis_y - y)**2)
print(dis)
if len(closest) <= 2:
if dis < 130:
closest.append(point)
p1 = np.array([x, y])
p2 = np.array(closest[0])
p3 = np.array(closest[1])
dis = np.cross(p2-p1,p3-p1)/np.linalg.norm(p2-p1)
if dis < 90:
return False
return True
I did not recive any error messages, but you can still place towers on some spots on the path,
and you can't place towers on some points not on the path, and I was expecting it to be pretty neat.
As #ImperishableNight stated, the issue is that your function only compares each point on the path and checks if the distance is less than a certain threshold (130 pixels in your case). But this is not enough, since we are not only interested in the end points in each segment of the path, but also all of the points in between. For that, we need to calculate the distance between a point and a line segment.
I have written and commented on the following code using a simple Point class to replace whatever pygame provides. I break the problem up into a bunch of tiny functions to solve your problem. Please let me know if any of this is unclear.
import math
import random
class Point:
def __init__(self, x=0, y=0):
"""
A really simple Point class.
Replaces whatever pygame provides for this example.
"""
self.x = x
self.y = y
def __repr__(self):
"""
A convenient representation of a Point class.
So we see the x and y values when printing these objects.
"""
return "({0}, {1})".format(self.x, self.y)
def gen_rand_point(width, height):
"""
Creates random x and y values for a Point object
"""
rand_x = random.randint(0, width)
rand_y = random.randint(0, height)
point = Point(x=rand_x, y=rand_y)
return point
def gen_rand_point_list(width, height, num_points):
"""
Creates a list of random points using the previous function.
"""
points = []
for i in range(num_points):
point = gen_rand_point(width, height)
points.append(point)
return points
def points_to_segments(points, loop=False):
"""
Converts a list of points into a list of segments.
Offsets the point list and zips it to create "segments".
A segment is just a tuple containing two Point objects.
"""
starts = points
ends = points[1:] + [points[0]]
segments = list(zip(starts, ends))
if loop:
return segments
else:
return segments[:-1]
def calc_sqr_dist(point_a, point_b):
"""
Calculates the square distance between two points.
Can be useful to save a wasteful math.sqrt call.
"""
delta_x = point_b.x - point_a.x
delta_y = point_b.y - point_a.y
sqr_dist = (delta_x ** 2) + (delta_y ** 2)
return sqr_dist
def calc_dist(point_a, point_b):
"""
Calculates the distance between two points.
When you need a wasteful math.sqrt call.
"""
sqr_dist = calc_sqr_dist(point_a, point_b)
dist = math.sqrt(sqr_dist)
return dist
def calc_dot_product(segment_a, segment_b):
"""
Calculates the dot product of two segments.
Info about what the dot product represents can be found here:
https://math.stackexchange.com/q/805954
"""
a0, a1 = segment_a
b0, b1 = segment_b
ax = a1.x - a0.x
ay = a1.y - a0.y
bx = b1.x - b0.x
by = b1.y - b0.y
dot = (ax * bx) + (ay * by)
return dot
def calc_point_segment_dist(point, segment):
"""
Gets the distance between a point and a line segment.
Some explanation can be found here:
https://stackoverflow.com/a/1501725/2588654
"""
start, end = segment
sqr_dist = calc_sqr_dist(start, end)
#what if the segment's start and end are the same?
if sqr_dist == 0:
dist = calc_dist(point, start)
return dist
#what if it is not that easy?
else:
segment_a = (start, point)
segment_b = (start, end)#really is just segment...
dot = calc_dot_product(segment_a, segment_b)
t = float(dot) / sqr_dist
clamped_t = max(0, min(1, t))#clamps t to be just within the segment
#the interpolation is basically like a lerp (linear interpolation)
projection = Point(
x = start.x + (t * (end.x - start.x)),
y = start.y + (t * (end.y - start.y)),
)
dist = calc_dist(point, projection)
return dist
def calc_point_path_dist(point, path):
"""
Gets the distances between the point and each segment.
Then returns the minimum distance of all of these distances.
"""
dists = [calc_point_segment_dist(point, segment) for segment in path]
min_dist = min(dists)
return min_dist
if __name__ == "__main__":
"""
A fun example!
"""
width = 800
height = 600
num_path_points = 5
tower_range = 50
tower = gen_rand_point(width, height)
path_points = gen_rand_point_list(width, height, num_path_points)
path = points_to_segments(path_points)
dist = calc_point_path_dist(tower, path)
in_range = dist <= tower_range
print(dist, in_range)
I have code to expand the polygon, it works by multiplying the xs and ys by a factor then re centering the resultant polyon at the center of the original.
I also have code to find the value for the expansion factor, given a point that the polygon needs to reach:
import numpy as np
import itertools as IT
import copy
from shapely.geometry import LineString, Point
def getPolyCenter(points):
"""
http://stackoverflow.com/a/14115494/190597 (mgamba)
"""
area = area_of_polygon(*zip(*points))
result_x = 0
result_y = 0
N = len(points)
points = IT.cycle(points)
x1, y1 = next(points)
for i in range(N):
x0, y0 = x1, y1
x1, y1 = next(points)
cross = (x0 * y1) - (x1 * y0)
result_x += (x0 + x1) * cross
result_y += (y0 + y1) * cross
result_x /= (area * 6.0)
result_y /= (area * 6.0)
return (result_x, result_y)
def expandPoly(points, factor):
points = np.array(points, dtype=np.float64)
expandedPoly = points*factor
expandedPoly -= getPolyCenter(expandedPoly)
expandedPoly += getPolyCenter(points)
return np.array(expandedPoly, dtype=np.int64)
def distanceLine2Point(points, point):
points = np.array(points, dtype=np.float64)
point = np.array(point, dtype=np.float64)
points = LineString(points)
point = Point(point)
return points.distance(point)
def distancePolygon2Point(points, point):
distances = []
for i in range(len(points)):
if i==len(points)-1:
j = 0
else:
j = i+1
line = [points[i], points[j]]
distances.append(distanceLine2Point(line, point))
minDistance = np.min(distances)
#index = np.where(distances==minDistance)[0][0]
return minDistance
"""
Returns the distance from a point to the nearest line of the polygon,
AND the distance from where the normal to the line (to reach the point)
intersets the line to the center of the polygon.
"""
def distancePolygon2PointAndCenter(points, point):
distances = []
for i in range(len(points)):
if i==len(points)-1:
j = 0
else:
j = i+1
line = [points[i], points[j]]
distances.append(distanceLine2Point(line, point))
minDistance = np.min(distances)
i = np.where(distances==minDistance)[0][0]
if i==len(points)-1:
j = 0
else:
j = i+1
line = copy.deepcopy([points[i], points[j]])
centerDistance = distanceLine2Point(line, getPolyCenter(points))
return minDistance, centerDistance
minDistance, centerDistance = distancePolygon2PointAndCenter(points, point)
expandedPoly = expandPoly(points, 1+minDistance/centerDistance)
This code only works when the point is directly opposing one of the polygons lines.
Modify your method distancePolygon2PointAndCenter to instead of
Returns the distance from a point to the nearest line of the polygon
To return the distance from a point to the segment intersected by a ray from the center to the point. This is the line that will intersect the point once the polygon is fully expanded. To get this segment, take both endpoints of each segment of your polygon, and plug them into the equation for the line parallel & intersecting the ray mentioned earlier. That is y = ((centerY-pointY)/(centerX-pointX)) * (x - centerX) + centerY. You want to want to find endpoints where either one of them intersect the line, or the two are on opposite sides of the line.
Then, the only thing left to do is make sure that we pick the segment intersecting the right "side" of the line. To do this, there are a few options. The fail-safe method would be to use the formula cos(theta) = sqrt((centerX**2 + centerY**2)*(pointX**2 + pointY**2)) / (centerX * pointX + centerY * pointY) however, you could use methods such as comparing x and y values, taking the arctan2(), and such to figure out which segment is on the correct "side" of center. You'll just have lots of edge cases to cover. After all this is said and done, your two (unless its not convex, in which case take the segment farthest from you center) endpoints makeup the segment to expand off of.
Determine what is "polygon center" as central point C of expanding. Perhaps it is centroid (or some point with another properties?).
Make a segment from your point P to C. Find intersection point I between PC and polygon edges. If polygon is concave and there are some intersection points, choose the closest one to P.
Calculate coefficient of expanding:
E = Length(PC) / Length(CI)
Calculate new vertex coordinates. For i-th vertex of polygon:
V'[i].X = C.X + (V[i].X - C.X) * E
V'[i].Y = C.Y + (V[i].Y - C.Y) * E
Decide which point you want to reach, then calculate how much % your polygon needs to expand to reach that point and use the shapely.affinity.scale function. For example, in my case I just needed to make the polygon 5% bigger:
region = shapely.affinity.scale(myPolygon,
xfact=1.05, yfact=1.05 )