Related
I did a few modifications to the Bellman-Ford to get some data I need to compare but I can't seem to be able to get some of the information I need from the return the PrintArr method, while it prints the 'dist_list' the return won't take it nor am I being able to create the object using list comprehension either.
Class Graph:
def __init__(self, vertices):
self.V = vertices
self.graph = []
def addEdge(self, u, v, w):
self.graph.append([u, v, w])
def printArr(self, dist, src):
#print("Source End Distance")
dist_list = []
for i in range(self.V):
#print("{0}\t{1}\t{2}".format(src, i, dist[i]))
#print(src, i, dist[i])
dist_list.append([src, i, dist[i]])
print(dist_list)
print(dist_list == [[src, i, dist[i]] for i in range(self.V)])
return [[src, i, dist[i]] for i in range(self.V)]
def BellmanFord(self, src):
dist = [float("Inf")] * self.V
dist[src] = 0
for _ in range(self.V - 1):
for u, v, w in self.graph:
if dist[u] != float("Inf") and dist[u] + w < dist[v]:
dist[v] = dist[u] + w
for u, v, w in self.graph:
if dist[u] != float("Inf") and dist[u] + w < dist[v]:
print("Graph contains negative weight cycle")
return
self.printArr(dist, src)
matrix = [[0, 2, 2, 2, -1], [9, 0, 2, 2, -1], [9, 3, 0, 2, -1], [9, 3, 2, 0, -1], [9, 3, 2, 2, 0]]
g = Graph(len(matrix))
[[g.addEdge(i, j, element) for j, element in enumerate(array) if i != j] for i, array in enumerate(matrix)]
print(g.BellmanFord(0))
Output:
[[0, 0, 0], [0, 1, 2], [0, 2, 1], [0, 3, 1], [0, 4, -1]]
True
None
Print: OK
List A = List B
Why return None??? What am I missing?
The None comes from:
print(g.BellmanFord(0))
BellmanFord never returns anything useful under any circumstances (it either falls off the end and implicitly returns None, or executes a plain return, which also return None). Remove the print() around the call, and you'll avoid the None output.
Alternatively, change self.printArr(dist, src) to return self.printArr(dist, src) so it does return something useful (assuming the early return, which should probably be raising an exception rather than silently returning None, isn't invoked).
For example this array of GPS Coordinates:
GPSS = [{"Lat":40.641099,"Lon": -73.917094},{"Lat":40.60442,"Lon": -74.054873},{"Lat":40.779582,"Lon": -73.920213},{"Lat":40.651616,"Lon": -73.89097},{"Lat":40.755183,"Lon": -73.846248}]
I have already calculated the Distances below for each possible combination:
Distances = [{'GPSS': [0, 1], 'Distance': 12.34895151892164}, {'GPSS': [0, 2], 'Distance': 15.380561959360797}, {'GPSS': [0, 3], 'Distance': 2.499303143635897}, {'GPSS': [0, 4], 'Distance': 14.012560598709298}, {'GPSS': [1, 2], 'Distance': 22.53687775052488}, {'GPSS': [1, 3], 'Distance': 14.824576927209662}, {'GPSS': [1, 4], 'Distance': 24.318038568441654}, {'GPSS': [2, 3], 'Distance': 14.423642658224264}, {'GPSS': [2, 4], 'Distance': 6.807346029310139}, {'GPSS': [3, 4], 'Distance': 12.106031672624894}]
0,1 = referring to 40.641099,-73.917094 and 40.60442,-74.054873
1,4 = 40.641099,-73.917094 and 40.755183,-73.846248
I would now like to find out the shortest Distance (route) to visit each set of coordinates, so it's most likely not going to be point 0 to 1 to 2 to 3 to 4.
But something like 1 to 3 to 4 to 2 to 0.
How would I accomplish something like this?
This is as far as I got:
for index, d in enumerate(Distances):
print(d['GPSS'])
Total = d['Distance']
Start = d['GPSS'][1] #[0]
CheckPoints = []
CheckPoints.append(d['GPSS'][0])
CheckPoints.append(d['GPSS'][1])
for index2, d2 in enumerate(Distances):
if index != index2:
if Start == d2['GPSS'][0]: #0-1, 1-2, 2-3
Total += d2['Distance']
Start += 1
if d2['GPSS'][0] not in CheckPoints:
CheckPoints.append(d2['GPSS'][0])
if d2['GPSS'][1] not in CheckPoints:
CheckPoints.append(d2['GPSS'][1])
#print(CheckPoints)
print("+"+str(d2['Distance'])+" = "+str(Total)+" | "+str(Start)+" - "+str(d2['GPSS']))
if len(CheckPoints) <= len(GPSS)-1: #GPPS - is from above
for x in range(len(GPSS)-1):
if x not in CheckPoints:
for d3 in Distances:
if d3['GPSS'][0] == x and d3['GPSS'][1] == CheckPoints[-1]:
print("HERE")
print(d3)
Total += d3['Distance']
break
print(Total)
Any help would be much appreciated.
Thanks
The best way to do what you are looking for is to create a Graph. If you do not know what that is, you should look it up as it's a very important data structure. You will probably also need to know what it is to fully understand the following code. Python does not have a built in graph so you need to create your own.
The type of graph you are going to need is a un-directed weighted graph with all of the nodes, or in your case GPS coordinates, connected to each other. Then you can sort the graph by using a form of "Dijkstra's Algorithm" to find the shortest path to all of the points.
Below is an implementation of what you are looking for. However I coded this to work with a list containing lists of paired coordinates. It also includes a driver, driver(), you can call to test it out.
I wrote this up quick and didn't code it as a class, but in the real world you most definitely should.
As a note, when you run the driver function it will execute the code and print out all of the possible paths and their weights for the provided coordinate list. "Weight" in your case refers to the distance between the points. The list printed shows the path it took with "1" referring to the pair of points at index "0" of the coordinate list. The next number in the list is the pair of points it went to next.
If you have any further questions feel free to ask
from collections import defaultdict
from math import sqrt
# Shortest path to all coordinates from any node
# Coordinates must be provided as a list containing lists of
# x/y pairs. ie [[23.2321, 58.3123], [x.xxx, y.yyy]]
def distance_between_coords(x1, y1, x2, y2):
distance = sqrt(((x2 - x1) ** 2) + ((y2 - y1) ** 2))
return distance
# Adds "names" to coordinates to use as keys for edge detection
def name_coords(coords):
coord_count = 0
for coord in coords:
coord_count += 1
coord.append(coord_count)
return coords
# Creates a weighted and undirected graph
# Returns named coordinates and their connected edges as a dictonary
def graph(coords):
coords = name_coords(coords)
graph = defaultdict(list)
edges = {}
for current in coords:
for comparer in coords:
if comparer == current:
continue
else:
weight = distance_between_coords(current[0], current[1],
comparer[0], comparer[1])
graph[current[2]].append(comparer[2])
edges[current[2], comparer[2]] = weight
return coords, edges
# Returns a path to all nodes with least weight as a list of names
# from a specific node
def shortest_path(node_list, edges, start):
neighbor = 0
unvisited = []
visited = []
total_weight = 0
current_node = start
for node in node_list:
if node[2] == start:
visited.append(start)
else:
unvisited.append(node[2])
while unvisited:
for index, neighbor in enumerate(unvisited):
if index == 0:
current_weight = edges[start, neighbor]
current_node = neighbor
elif edges[start, neighbor] < current_weight:
current_weight = edges[start, neighbor]
current_node = neighbor
total_weight += current_weight
unvisited.remove(current_node)
visited.append(current_node)
return visited, total_weight
def driver():
coords = [[1.7592675, 92.4836507], [17.549836, 32.457398],
[23.465896, 45], [25.195462, 37.462742],
[42.925274, 63.234028], [2.484631, 5.364871],
[50.748376, 36.194797]]
coords, edges = graph(coords)
shortest_path(coords, edges, 3)
shortest_path_taken = []
shortest_path_weight = 0
for index, node in enumerate(coords):
path, weight = shortest_path(coords, edges, index + 1)
print('--------------------------------------')
print("Path", index + 1, "=", path)
print("Weight =", weight)
if index == 0:
shortest_path_weight = weight
shortest_path_taken = path
elif weight < shortest_path_weight:
shortest_path_weight = weight
shortest_path_taken = path
print('--------------------------------------')
print("The shortest path to all nodes is:", shortest_path_taken)
print("The weight of the path is:", shortest_path_weight)
Edit:
Here is what the output will look like when you call the driver function:
--------------------------------------
Path 1 = [1, 5, 3, 4, 2, 7, 6]
Weight = 386.3252849770695
--------------------------------------
Path 2 = [2, 4, 3, 6, 7, 5, 1]
Weight = 189.3710721663407
--------------------------------------
Path 3 = [3, 4, 2, 5, 7, 6, 1]
Weight = 173.99235180101968
--------------------------------------
Path 4 = [4, 3, 2, 7, 5, 6, 1]
Weight = 172.86112533927678
--------------------------------------
Path 5 = [5, 3, 7, 4, 2, 1, 6]
Weight = 247.08415835699554
--------------------------------------
Path 6 = [6, 2, 4, 3, 7, 5, 1]
Weight = 330.1567215845902
--------------------------------------
Path 7 = [7, 4, 5, 3, 2, 6, 1]
Weight = 247.70066871941674
--------------------------------------
The shortest path to all nodes is: [4, 3, 2, 7, 5, 6, 1]
The weight of the path is: 172.86112533927678
[Finished in 0.1s]*
I am using itertools.product to find the possible weights an asset can take given that the sum of all weights adds up to 100.
min_wt = 10
max_wt = 50
step = 10
nb_Assets = 5
weight_mat = []
for i in itertools.product(range(min_wt, (max_wt+1), step), repeat = nb_Assets):
if sum(i) == 100:
weight = [i]
if np.shape(weight_mat)[0] == 0:
weight_mat = weight
else:
weight_mat = np.concatenate((weight_mat, weight), axis = 0)
The above code works, but it is too slow as it goes through the combinations that are not acceptable, example [50,50,50,50,50] eventually testing 3125 combinations instead of 121 possible combinations. Is there any way we can add the 'sum' condition within the loop to speed things up?
Many improvements are possible.
For starters, the search space can be reduced using itertools.combinations_with_replacement() because summation is commutative.
Also, the last addend should be computed rather than tested. For example if t[:4] was (10, 20, 30, 35), you could compute t[4] as 1 - sum(t), giving a value of 5. This will give a 100-fold speed-up over trying one-hundred values of x in (10, 20, 30, 35, x).
You can write up a recursive algorithm for that which prunes all the impossible options early on:
def make_weight_combs(min_wt, max_wt, step, nb_assets, req_wt):
weights = range(min_wt, max_wt + 1, step)
current = []
yield from _make_weight_combs_rec(weights, nb_assets, req_wt, current)
def _make_weight_combs_rec(weights, nb_assets, req_wt, current):
if nb_assets <= 0:
yield tuple(current)
else:
# Discard weights that cannot possibly be used
while weights and weights[0] + weights[-1] * (nb_assets - 1) < req_wt:
weights = weights[1:]
while weights and weights[-1] + weights[0] * (nb_assets - 1) > req_wt:
weights = weights[:-1]
# Add all possible weights
for w in weights:
current.append(w)
yield from _make_weight_combs_rec(weights, nb_assets - 1, req_wt - w, current)
current.pop()
min_wt = 10
max_wt = 50
step = 10
nb_assets = 5
req_wt = 100
for comb in make_weight_combs(min_wt, max_wt, step, nb_assets, req_wt):
print(comb, sum(comb))
Output:
(10, 10, 10, 20, 50) 100
(10, 10, 10, 30, 40) 100
(10, 10, 10, 40, 30) 100
(10, 10, 10, 50, 20) 100
(10, 10, 20, 10, 50) 100
(10, 10, 20, 20, 40) 100
(10, 10, 20, 30, 30) 100
(10, 10, 20, 40, 20) 100
...
If order of the weights does not matter (so, for example, (10, 10, 10, 20, 50) and (50, 20, 10, 10, 10) are the same), then you can modify the for loop as follows:
for i, w in enumerate(weights):
current.append(w)
yield from _make_weight_combs_rec(weights[i:], nb_assets - 1, req_wt - w, current)
current.pop()
Which gives the output:
(10, 10, 10, 20, 50) 100
(10, 10, 10, 30, 40) 100
(10, 10, 20, 20, 40) 100
(10, 10, 20, 30, 30) 100
(10, 20, 20, 20, 30) 100
(20, 20, 20, 20, 20) 100
Comparing performance of the offered solutions:
import itertools
import timeit
import numpy as np
# original code from question
def f1():
min_wt = 10
max_wt = 50
step = 10
nb_assets = 5
weight_mat = []
for i in itertools.product(range(min_wt, (max_wt+1), step), repeat=nb_assets):
if sum(i) == 100:
weight = [i, ]
if np.shape(weight_mat)[0] == 0:
weight_mat = weight
else:
weight_mat = np.concatenate((weight_mat, weight), axis=0)
return weight_mat
# code from question using list instead of numpy array
def f1b():
min_wt = 10
max_wt = 50
step = 10
nb_assets = 5
weight_list = []
for i in itertools.product(range(min_wt, (max_wt+1), step), repeat=nb_assets):
if sum(i) == 100:
weight_list.append(i)
return weight_list
# calculating the last element of each tuple
def f2():
min_wt = 10
max_wt = 50
step = 10
nb_assets = 5
weight_list = []
for i in itertools.product(range(min_wt, (max_wt+1), step), repeat=nb_assets-1):
the_sum = sum(i)
if the_sum < 100:
last_elem = 100 - the_sum
if min_wt <= last_elem <= max_wt:
weight_list.append(i + (last_elem, ))
return weight_list
# recursive solution from user kaya3 (https://stackoverflow.com/a/58823843/9225671)
def constrained_partitions(n, k, min_w, max_w, w_step=1):
if k < 0:
raise ValueError('Number of parts must be at least 0')
elif k == 0:
if n == 0:
yield ()
else:
for w in range(min_w, max_w+1, w_step):
for p in constrained_partitions(n-w, k-1, min_w, max_w, w_step):
yield (w,) + p
def f3():
return list(constrained_partitions(100, 5, 10, 50, 10))
# recursive solution from user jdehesa (https://stackoverflow.com/a/58823990/9225671)
def make_weight_combs(min_wt, max_wt, step, nb_assets, req_wt):
weights = range(min_wt, max_wt + 1, step)
current = []
yield from _make_weight_combs_rec(weights, nb_assets, req_wt, current)
def _make_weight_combs_rec(weights, nb_assets, req_wt, current):
if nb_assets <= 0:
yield tuple(current)
else:
# Discard weights that cannot possibly be used
while weights and weights[0] + weights[-1] * (nb_assets - 1) < req_wt:
weights = weights[1:]
while weights and weights[-1] + weights[0] * (nb_assets - 1) > req_wt:
weights = weights[:-1]
# Add all possible weights
for w in weights:
current.append(w)
yield from _make_weight_combs_rec(weights, nb_assets - 1, req_wt - w, current)
current.pop()
def f4():
return list(make_weight_combs(10, 50, 10, 5, 100))
I tested these functions using timeit like this:
print(timeit.timeit('f()', 'from __main__ import f1 as f', number=100))
The results using the parameters from the question:
# min_wt = 10
# max_wt = 50
# step = 10
# nb_assets = 5
0.07021828400320373 # f1 - original code from question
0.041302188008558005 # f1b - code from question using list instead of numpy array
0.009902548001264222 # f2 - calculating the last element of each tuple
0.10601829699589871 # f3 - recursive solution from user kaya3
0.03329997700348031 # f4 - recursive solution from user jdehesa
If I expand the search space (reduced step and increased assets):
# min_wt = 10
# max_wt = 50
# step = 5
# nb_assets = 6
7.6620834979985375 # f1 - original code from question
7.31425816299452 # f1b - code from question using list instead of numpy array
0.809070186005556 # f2 - calculating the last element of each tuple
14.88188026699936 # f3 - recursive solution from user kaya3
0.39385621099791024 # f4 - recursive solution from user jdehesa
Seems like f2 and f4 are the fastest (for the tested size of the data).
Let's generalise this problem; you want to iterate over k-tuples whose sum is n, and whose elements are within range(min_w, max_w+1, w_step). This is a kind of integer partitioning problem, with some extra constraints on the size of the partition and the sizes of its components.
To do this, we can write a recursive generator function; for each w in the range, the remainder of the tuple is a (k - 1)-tuple whose sum is (n - w). The base case is a 0-tuple, which is possible only if the required sum is 0.
As Raymond Hettinger notes, you can also improve the efficiency when k = 1 by just testing whether the required sum is one of the allowed weights.
def constrained_partitions(n, k, min_w, max_w, w_step=1):
if k < 0:
raise ValueError('Number of parts must be at least 0')
elif k == 0:
if n == 0:
yield ()
elif k == 1:
if n in range(min_w, max_w+1, w_step):
yield (n,)
elif min_w*k <= n <= max_w*k:
for w in range(min_w, max_w+1, w_step):
for p in constrained_partitions(n-w, k-1, min_w, max_w, w_step):
yield (w,) + p
Usage:
>>> for p in constrained_partitions(5, 3, 1, 5, 1):
... print(p)
...
(1, 1, 3)
(1, 2, 2)
(1, 3, 1)
(2, 1, 2)
(2, 2, 1)
(3, 1, 1)
>>> len(list(constrained_partitions(100, 5, 10, 50, 10)))
121
Whenever you're iterating over all solutions to some sort of combinatorial problem, it's generally best to generate actual solutions directly, rather than generate more than you need (e.g. with product or combinations_with_replacement) and reject the ones you don't want. For larger inputs, the vast majority of time would be spent generating solutions which will get rejected, due to combinatorial explosion.
Note that if you don't want repeats in different orders (e.g. 1, 1, 3 and 1, 3, 1), you can change the recursive call to constrained_partitions(n-w, k-1, min_w, w, w_step) to only generate partitions where the weights are in non-increasing order.
Note that when you have N weights that sum up to 100, and you chose N - 1 weights, the remaining weight is already defined as 100 - sum of already chosen weights, which should be positive. The same limitation applies to any number of already chosen weights.
Next, you don't want combinations that are just permutations of the same weights. This is why you can order weights by value, and choose the next weight in the combination to be below or equal of the previous one.
This immediately makes the search space much smaller, and you can break a particular branch of search earlier.
Probably writing it with explicit loops first, or as a recursive algorithm, should be much easier for understanding and implementing.
I am implementing a graph class and would like to write a function that calculates whether a given path is valid. I'm getting a key error in my is_path_valid function.
My graph is represented as {a:{b:c}} where a and b are a vertex connected to each other, and c is the weight of the edge
Given:
{0: {1: 5.0, 2: 10.0}, 1: {3: 3.0, 4: 6.0}, 3: {2: 2.0, 4: 2.0, 5: 2.0}, 4: {6: 6.0}, 5: {6: 2.0}, 7: {9: 1.0}, 8: {7: 2.0, 9: 4.0}}
Vertex 2 to 3 is a valid path.
My graph class:
class Graph:
def __init__(self, n):
"""
Constructor
:param n: Number of vertices
"""
self.order = n
self.size = 0
self.vertex = {}
def insert_edge(self, u, v, w): #works fine
if u in self.vertex and v < self.order:
if not v in self.vertex[u]:
self.vertex[u][v] = w
self.size += 1
elif u not in self.vertex and u < self.order and v < self.order:
self.vertex[u] = {}
self.vertex[u][v] = w
self.size += 1
else:
raise IndexError
def is_path_valid(self, path):
while True:
try:
s = path.pop(0)
except IndexError:
break
if path:
d = path.pop(0)
if s not in self.vertex and d not in self.vertex[s]: #ERROR
return False
s = d
return True
My main function:
def main():
g = Graph(10)
g.insert_edge(0,1,5.0)
g.insert_edge(0,2,10.0)
g.insert_edge(1,3,3.0)
g.insert_edge(1,4,6.0)
g.insert_edge(3,2,2.0)
g.insert_edge(3,4,2.0)
g.insert_edge(3,5,2.0)
g.insert_edge(4,6,6.0)
g.insert_edge(5,6,2.0)
g.insert_edge(7,9,1.0)
g.insert_edge(8,7,2.0)
g.insert_edge(8,9,4.0)
True(g.is_path_valid([0, 2]))
True(g.is_path_valid([2, 3]))
True(g.is_path_valid([0, 2, 3]))
False(g.is_path_valid([0, 1, 8]))
False(g.is_path_valid([0, 4, 3]))
print(g.vertex) #to see the graph
print(g.is_path_valid([2,3]))
if __name__ == '__main__':
main()
My error:
if s not in self.vertex and d not in self.vertex[s]:
KeyError: 2
You just are mixing together arcs and edges which leads to some unexpected things happening, you have to choose between either of the two.
On the other hand you can have an oriented graph and still have a function that will add "edges" in the sense that it will add both arcs (u, v) and (v, u). I have edges in quotes because they're not really edges (the term edge only have meaning in a non-oriented graph).
from collections import defaultdict
class Graph:
def __init__(self):
self._arcs = defaultdict(dict)
def insert_arc(self, u, v, w):
self._arcs[u][v] = w
def is_arc(self, u, v):
return u in self._arcs and v in self._arcs[u]
def is_path_valid(self, path):
for u, v in zip(path, path[1:]):
if not self.is_arc(u, v):
return False
return True
# We add the notion of "edges" with the following methods:
def insert_edge(self, u, v, w):
self.insert_arc(u, v, w)
self.insert_arc(v, u, w)
#property
def edges(self):
return {((u, v), w) for u, Nu in self._arcs.items() for v, w in Nu.items() if self.is_edge(u, v)}
def is_edge(self, u, v):
is_symetric = self.is_arc(u, v) and self.is_arc(v, u)
if not is_symetric:
return False
return self._arcs[u][v] == self._arcs[v][u]
You can now add either edges or arcs to your graph:
g = Graph()
# This is an arc:
g.insert_arc(1, 8, 1.)
# Weight is not symmetric but this still look like an edge:
g.insert_arc(1, 0, 3.)
g.insert_arc(0, 1, 2.)
# These are all symmetric (ie. "edges")
g.insert_edge(1, 2, 7.)
g.insert_edge(2, 3, 5.)
g.insert_edge(0, 3, 13.)
# we added an arc (1, 8):
print(g.is_path_valid([1, 8])) # True
print(g.is_path_valid([8, 1])) # False
# All true:
print(g.is_path_valid([0, 3]))
print(g.is_path_valid([2, 3]))
print(g.is_path_valid([0, 1, 2, 3, 0]))
# Adding one step make this false since (0, 2) doesn't exist:
print(g.is_path_valid([0, 1, 2, 3, 0, 2]))
We can use the edges property to find all "edges" (symmetric arcs with the same weight in both directions):
>>> print(g.edges)
{((3, 0), 13.0), ((3, 2), 5.0), ((2, 1), 7.0), ((1, 2), 7.0), ((2, 3), 5.0), ((0, 3), 13.0)}
Notice how (0, 1) is not part of the set of edges, that's because the link exists in both directions but the weight is not the same. The arc (1, 8) is obviously not here as (8, 1) is not part of the graph.
Given a list in Python containing 8 x, y coordinate values (all positive) of 4 points as [x1, x2, x3, x4, y1, y2, y3, y4] ((xi, yi) are x and y coordinates of ith point ),
How can I sort it such that new list [a1, a2, a3, a4, b1, b2, b3, b4] is such that coordinates (ai, bi) of 1 2 3 4 are clockwise in order with 1 closest to origin of xy plane, i.e. something like
2--------3
| |
| |
| |
1--------4
Points will roughly form a parallelogram.
Currently, I am thinking of finding point with least value of (x+y) as 1, then 2 by the point with least x in remaining coordinates, 3 by largest value of (x + y) and 4 as the remaining point
You should use a list of 2-item tuples as your data structure to represent a variable number of coordinates in a meaningful way.
from functools import reduce
import operator
import math
coords = [(0, 1), (1, 0), (1, 1), (0, 0)]
center = tuple(map(operator.truediv, reduce(lambda x, y: map(operator.add, x, y), coords), [len(coords)] * 2))
print(sorted(coords, key=lambda coord: (-135 - math.degrees(math.atan2(*tuple(map(operator.sub, coord, center))[::-1]))) % 360))
This outputs:
[(0, 0), (0, 1), (1, 1), (1, 0)]
import math
def centeroidpython(data):
x, y = zip(*data)
l = len(x)
return sum(x) / l, sum(y) / l
xy = [405952.0, 408139.0, 407978.0, 405978.0, 6754659.0, 6752257.0, 6754740.0, 6752378.0]
xy_pairs = list(zip(xy[:int(len(xy)/2)], xy[int(len(xy)/2):]))
centroid_x, centroid_y = centeroidpython(xy_pairs)
xy_sorted = sorted(xy_pairs, key = lambda x: math.atan2((x[1]-centroid_y),(x[0]-centroid_x)))
xy_sorted_x_first_then_y = [coord for pair in list(zip(*xy_sorted)) for coord in pair]
# P4=8,10 P1=3,5 P2=8,5 P3=3,10
points=[8,3,8,3,10,5,5,10]
k=0
#we know these numbers are extreme and data won't be bigger than these
xmin=1000
xmax=-1000
ymin=1000
ymax=-1000
#finding min and max values of x and y
for i in points:
if k<4:
if (xmin>i): xmin=i
if (xmax<i): xmax=i
else:
if (ymin>i): ymin=i
if (ymax<i): ymax=i
k +=1
sortedlist=[xmin,xmin,xmax,xmax,ymin,ymax,ymax,ymin]
print(sortedlist)
output:[3, 3, 8, 8, 5, 10, 10, 5]
for other regions you need to change sortedlist line. if center is inside the box then it will require more condition controlling
What we want to sort by is the angle from the start coordinate. I've used numpy here to interpret each vector from the starting coordinate as a complex number, for which there is an easy way of computing the angle (counterclockwise along the unit sphere)
def angle_with_start(coord, start):
vec = coord - start
return np.angle(np.complex(vec[0], vec[1]))
Full code:
import itertools
import numpy as np
def angle_with_start(coord, start):
vec = coord - start
return np.angle(np.complex(vec[0], vec[1]))
def sort_clockwise(points):
# convert into a coordinate system
# (1, 1, 1, 2) -> (1, 1), (1, 2)
coords = [np.array([points[i], points[i+4]]) for i in range(len(points) // 2)]
# find the point closest to the origin,
# this becomes our starting point
coords = sorted(coords, key=lambda coord: np.linalg.norm(coord))
start = coords[0]
rest = coords[1:]
# sort the remaining coordinates by angle
# with reverse=True because we want to sort by clockwise angle
rest = sorted(rest, key=lambda coord: angle_with_start(coord, start), reverse=True)
# our first coordinate should be our starting point
rest.insert(0, start)
# convert into the proper coordinate format
# (1, 1), (1, 2) -> (1, 1, 1, 2)
return list(itertools.chain.from_iterable(zip(*rest)))
Behavior on some sample inputs:
In [1]: a
Out[1]: [1, 1, 2, 2, 1, 2, 1, 2]
In [2]: sort_clockwise(a)
Out[2]: [1, 1, 2, 2, 1, 2, 2, 1]
In [3]: b
Out[3]: [1, 2, 0, 2, 1, 2, 3, 1]
In [4]: sort_clockwise(b)
Out[4]: [1, 0, 2, 2, 1, 3, 2, 1]
Based on BERA's answer but as a class:
code
import math
def class Sorter:
#staticmethod
def centerXY(xylist):
x, y = zip(*xylist)
l = len(x)
return sum(x) / l, sum(y) / l
#staticmethod
def sortPoints(xylist):
cx, cy = Sorter.centerXY(xylist)
xy_sorted = sorted(xylist, key = lambda x: math.atan2((x[1]-cy),(x[0]-cx)))
return xy_sorted
test
def test_SortPoints():
points=[(0,0),(0,1),(1,1),(1,0)]
center=Sorter.centerXY(points)
assert center==(0.5,0.5)
sortedPoints=Sorter.sortPoints(points)
assert sortedPoints==[(0, 0), (1, 0), (1, 1), (0, 1)]
As suggested by IgnacioVazquez-Abrams, we can also do sorting according to atan2 angles:
Code:
import math
import copy
import matplotlib.pyplot as plt
a = [2, 4, 5, 1, 0.5, 4, 0, 4]
print(a)
def clock(a):
angles = []
(x0, y0) = ((a[0]+a[1]+a[2]+a[3])/4, (a[4]+ a[5] + a[6] + a[7])/4) # centroid
for j in range(4):
(dx, dy) = (a[j] - x0, a[j+4] - y0)
angles.append(math.degrees(math.atan2(float(dy), float(dx))))
for k in range(4):
angles.append(angles[k] + 800)
# print(angles)
z = [copy.copy(x) for (y,x) in sorted(zip(angles,a), key=lambda pair: pair[0])]
print("z is: ", z)
plt.scatter(a[:4], a[4:8])
plt.show()
clock(a)
Output is :
[2, 4, 5, 1, 0.5, 4, 0, 4]
[-121.60750224624891, 61.92751306414704, -46.73570458892839, 136.8476102659946, 678.3924977537511, 861.9275130641471, 753.2642954110717, 936.8476102659946]
z is: [2, 5, 4, 1, 0.5, 0, 4, 4]
Try this line of code
def sort_clockwise(pts):
rect = np.zeros((4, 2), dtype="float32")
s = pts.sum(axis=1)
rect[0] = pts[np.argmin(s)]
rect[2] = pts[np.argmax(s)]
diff = np.diff(pts, axis=1)
rect[1] = pts[np.argmin(diff)]
rect[3] = pts[np.argmax(diff)]
return rect