KeyError when checking if graph path is valid - python

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.

Related

Class method returns None

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).

Dijkstra's algorithm Python

Given a graph and a source vertex in the graph, find the shortest paths from source to all vertices in the given graph.
Read more here -> Link
Please go through my code and help me out by pointing what's wrong with my logic.
My code:
from collections import defaultdict
global INT_MAX
INT_MAX = 3 ** 38
class Graph:
def __init__(self, numofVertices):
self.vertList = defaultdict(list)
self.numofVertices = numofVertices
def addEdge(self, u, v, cost):
self.vertList[u].append((v, cost))
self.vertList[v].append((u, cost))
def minDist(self, dist, visited):
for v in range(self.numofVertices):
if dist[v] < INT_MAX and v not in visited:
minIndex = v
return minIndex
def dijsktra(self, src):
dist = [INT_MAX] * self.numofVertices
dist[src] = 0
visited = set()
for _ in range(self.numofVertices):
minVertex = self.minDist(dist, visited)
visited.add(minVertex)
for nbr, edgeCost in self.vertList[minVertex]:
if dist[nbr] > dist[minVertex] + edgeCost and nbr not in visited:
dist[nbr] = dist[minVertex] + edgeCost
return dist
g = Graph(9)
g.addEdge(0, 1, 4)
g.addEdge(0, 7, 8)
g.addEdge(1, 7, 11)
g.addEdge(7, 8, 7)
g.addEdge(7, 6, 1)
g.addEdge(7, 1, 11)
g.addEdge(1, 2, 8)
g.addEdge(2, 3, 7)
g.addEdge(2, 5, 4)
g.addEdge(2, 8, 2)
g.addEdge(6, 8, 6)
g.addEdge(6, 5, 2)
g.addEdge(5, 2, 4)
g.addEdge(5, 3, 14)
g.addEdge(5, 4, 10)
g.addEdge(3, 4, 9)
print(g.dijsktra(0))
Current Output:
[0, 4, 15, 25, 21, 11, 9, 8, 15] # Index represents the vertex
Expected Output
[0, 4, 12, 19, 21, 11, 9, 8 ,14]
Here's the graph we are solving:
The problem was with the function that checks the min distance, We need to update the current max so that we can compare it with other unvisited vertices and see if another vertex exists with a lesser value.
def minDist(self, dist, visited):
max1 = 3 ** 38
minIndex = 0
for v in range(self.numofVertices):
if dist[v] < max1 and v not in visited:
max1 = dist[v]
minIndex = v
return minIndex
There is an error in minDist method. It returns the index of first adjacent index, not the one with minimal distance.
This function should look like:
def minDist(self, dist, visited):
m = INT_MAX
for v in range(self.numofVertices):
if dist[v] < m and v not in visited:
m = dist[v]
minIndex = v
return minIndex
Also, I am not sure that you have to assign both vertList[u] and vertList[v] in addEdge.

Question about Drawing a graph with networkx

I am using NetworkX for drawing graph, when I searching in NetworkX documentation I saw a code from Antigraph class that was confusing and I can't understand some line of this code. Help me for understanding this code, please.
I attached this code:
import networkx as nx
from networkx.exception import NetworkXError
import matplotlib.pyplot as plt
class AntiGraph(nx.Graph):
"""
Class for complement graphs.
The main goal is to be able to work with big and dense graphs with
a low memory footprint.
In this class you add the edges that *do not exist* in the dense graph,
the report methods of the class return the neighbors, the edges and
the degree as if it was the dense graph. Thus it's possible to use
an instance of this class with some of NetworkX functions.
"""
all_edge_dict = {"weight": 1}
def single_edge_dict(self):
return self.all_edge_dict
edge_attr_dict_factory = single_edge_dict
def __getitem__(self, n):
"""Return a dict of neighbors of node n in the dense graph.
Parameters
----------
n : node
A node in the graph.
Returns
-------
adj_dict : dictionary
The adjacency dictionary for nodes connected to n.
"""
return {
node: self.all_edge_dict for node in set(self.adj) - set(self.adj[n]) - {n}
}
def neighbors(self, n):
"""Return an iterator over all neighbors of node n in the
dense graph.
"""
try:
return iter(set(self.adj) - set(self.adj[n]) - {n})
except KeyError as e:
raise NetworkXError(f"The node {n} is not in the graph.") from e
def degree(self, nbunch=None, weight=None):
"""Return an iterator for (node, degree) in the dense graph.
The node degree is the number of edges adjacent to the node.
Parameters
----------
nbunch : iterable container, optional (default=all nodes)
A container of nodes. The container will be iterated
through once.
weight : string or None, optional (default=None)
The edge attribute that holds the numerical value used
as a weight. If None, then each edge has weight 1.
The degree is the sum of the edge weights adjacent to the node.
Returns
-------
nd_iter : iterator
The iterator returns two-tuples of (node, degree).
See Also
--------
degree
Examples
--------
>>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc
>>> list(G.degree(0)) # node 0 with degree 1
[(0, 1)]
>>> list(G.degree([0, 1]))
[(0, 1), (1, 2)]
"""
if nbunch is None:
nodes_nbrs = (
(
n,
{
v: self.all_edge_dict
for v in set(self.adj) - set(self.adj[n]) - {n}
},
)
for n in self.nodes()
)
elif nbunch in self:
nbrs = set(self.nodes()) - set(self.adj[nbunch]) - {nbunch}
return len(nbrs)
else:
nodes_nbrs = (
(
n,
{
v: self.all_edge_dict
for v in set(self.nodes()) - set(self.adj[n]) - {n}
},
)
for n in self.nbunch_iter(nbunch)
)
if weight is None:
return ((n, len(nbrs)) for n, nbrs in nodes_nbrs)
else:
# AntiGraph is a ThinGraph so all edges have weight 1
return (
(n, sum((nbrs[nbr].get(weight, 1)) for nbr in nbrs))
for n, nbrs in nodes_nbrs
)
def adjacency_iter(self):
"""Return an iterator of (node, adjacency set) tuples for all nodes
in the dense graph.
This is the fastest way to look at every edge.
For directed graphs, only outgoing adjacencies are included.
Returns
-------
adj_iter : iterator
An iterator of (node, adjacency set) for all nodes in
the graph.
"""
for n in self.adj:
yield (n, set(self.adj) - set(self.adj[n]) - {n})
# Build several pairs of graphs, a regular graph
# and the AntiGraph of it's complement, which behaves
# as if it were the original graph.
Gnp = nx.gnp_random_graph(20, 0.8, seed=42)
Anp = AntiGraph(nx.complement(Gnp))
Gd = nx.davis_southern_women_graph()
Ad = AntiGraph(nx.complement(Gd))
Gk = nx.karate_club_graph()
Ak = AntiGraph(nx.complement(Gk))
pairs = [(Gnp, Anp), (Gd, Ad), (Gk, Ak)]
# test connected components
for G, A in pairs:
gc = [set(c) for c in nx.connected_components(G)]
ac = [set(c) for c in nx.connected_components(A)]
for comp in ac:
assert comp in gc
# test biconnected components
for G, A in pairs:
gc = [set(c) for c in nx.biconnected_components(G)]
ac = [set(c) for c in nx.biconnected_components(A)]
for comp in ac:
assert comp in gc
# test degree
for G, A in pairs:
node = list(G.nodes())[0]
nodes = list(G.nodes())[1:4]
assert G.degree(node) == A.degree(node)
assert sum(d for n, d in G.degree()) == sum(d for n, d in A.degree())
# AntiGraph is a ThinGraph, so all the weights are 1
assert sum(d for n, d in A.degree()) == sum(d for n, d in A.degree(weight="weight"))
assert sum(d for n, d in G.degree(nodes)) == sum(d for n, d in A.degree(nodes))
nx.draw(Gnp)
plt.show()
I can't understand in these 2 lines:
(1) for v in set(self.adj) - set(self.adj[n]) - {n}
(2) nbrs = set(self.nodes()) - set(self.adj[nbunch]) - {nbunch}
To understand these lines, lets break each term carefully. For the purpose of explaination, I will create the following Graph:
import networkx as nx
source = [1, 2, 3, 4, 2, 3]
dest = [2, 3, 4, 6, 5, 5]
edge_list = [(u, v) for u, v in zip(source, dest)]
G = nx.Graph()
G.add_edges_from(ed_ls)
The Graph has the following edges:
print(G.edges())
# EdgeView([(1, 2), (2, 3), (2, 5), (3, 4), (3, 5), (4, 6)])
Now lets understand the terms in the above code:
set(self.adj)
If we print this out, we can see it is the set of nodes in the Graph:
print(set(self.adj))
# {1, 2, 3, 4, 5, 6}
set(self.adj[n])
This is the set of nodes adjacent to node n:
print(set(G.adj[2]))
# {1, 3, 5}
Now lets look at the first line that you asked in your question
for v in set(self.adj) - set(self.adj[n]) - {n}
This can be translated as follows:
for v in set of all nodes - set of nodes adjacent to node N - node N
So, this set of all nodes - set of nodes adjacent to node N returns the set of nodes that are not adjacent to node N (and this includes node N itself). (Essentially this will create the complement of the Graph).
Lets, look at an example:
nodes_nbrs = (
(
n,
{
v: {'weight': 1}
for v in set(G.adj) - set(G.adj[n]) - {n}
},
)
for n in G.nodes()
)
This will have the following value:
Node 1: {3: {'weight': 1}, 4: {'weight': 1}, 5: {'weight': 1}, 6: {'weight': 1}}
Node 2: {4: {'weight': 1}, 6: {'weight': 1}}
Node 3: {1: {'weight': 1}, 6: {'weight': 1}}
Node 4: {1: {'weight': 1}, 2: {'weight': 1}, 5: {'weight': 1}}
Node 6: {1: {'weight': 1}, 2: {'weight': 1}, 3: {'weight': 1}, 5: {'weight': 1}}
Node 5: {1: {'weight': 1}, 4: {'weight': 1}, 6: {'weight': 1}}
So if you look closely, for each node, we get the a list of nodes that were not adjacent to the node.
For say, node 2, the calculation would look something like this:
{1, 2, 3, 4, 5, 6} - {1, 3, 5} - {2} = {4, 6}
Now lets come to the second line:
nbrs = set(self.nodes()) - set(self.adj[nbunch]) - {nbunch}
Here set(self.adj[nbunch]) is basically the set of nodes adjacent to nodes in nbunch. nbunch is nothing but an iterator of nodes, so instead of set(self.adj[n]) where we get neighbors of a single node, here we get neighbors of multiple nodes.
So the expression can be translated as follows:
Set of all nodes - Set of all nodes adjacent to each node in nbunch - Set of nodes in nbunch
Which is same as the first expression that you asked except that this one is for multiple nodes, i.e. This will also return the list of nodes that are not adjacent to nodes in nbunch

Verbose output for networkx pagerank

Suppose i create the following directed Graph using networkx and perform the pagerank algorithm on it
adj_lists={
'A': 'B C'.split(' '),
'B': 'C',
'C': 'A',
'D': 'C'
}
G=nx.DiGraph()
for k in adj_lists.keys():
G.add_node(k)
for k in adj_lists.keys():
G.add_edges_from([(k, t) for t in adj_lists[k]])
nx.pagerank(G, alpha=1)
Is ist possible to get a verbose output telling me the devolopment of each node's value or even to generate a list which shows their progress? I am thinking about something like this:
[
{'A:0.25, 'B':0.25, 'C':0.25, 'D':0.25},
{'A:0.25, 'B':0.125, 'C':0.625, 'D':0},
{'A:0.625, 'B':0.3125, 'C':0.4375, 'D':0},
...
]
I've made a direct modification of networkx.pagerank algorithm to store the values of each iteration in a list.
import networkx as nx
from networkx.utils import not_implemented_for
def verbose_pagerank(
G,
alpha=0.85,
personalization=None,
max_iter=100,
tol=1.0e-6,
nstart=None,
weight="weight",
dangling=None,
):
if len(G) == 0:
return {}
if not G.is_directed():
D = G.to_directed()
else:
D = G
# Create a copy in (right) stochastic form
W = nx.stochastic_graph(D, weight=weight)
N = W.number_of_nodes()
# Choose fixed starting vector if not given
if nstart is None:
x = dict.fromkeys(W, 1.0 / N)
else:
# Normalized nstart vector
s = float(sum(nstart.values()))
x = {k: v / s for k, v in nstart.items()}
if personalization is None:
# Assign uniform personalization vector if not given
p = dict.fromkeys(W, 1.0 / N)
else:
s = float(sum(personalization.values()))
p = {k: v / s for k, v in personalization.items()}
if dangling is None:
# Use personalization vector if dangling vector not specified
dangling_weights = p
else:
s = float(sum(dangling.values()))
dangling_weights = {k: v / s for k, v in dangling.items()}
dangling_nodes = [n for n in W if W.out_degree(n, weight=weight) == 0.0]
# power iteration: make up to max_iter iterations
iterprogress = []
for i in range(max_iter):
xlast = x
iterprogress.append(x)
x = dict.fromkeys(xlast.keys(), 0)
danglesum = alpha * sum(xlast[n] for n in dangling_nodes)
for n in x:
# this matrix multiply looks odd because it is
# doing a left multiply x^T=xlast^T*W
for nbr in W[n]:
x[nbr] += alpha * xlast[n] * W[n][nbr][weight]
x[n] += danglesum * dangling_weights.get(n, 0) + (1.0 - alpha) * p.get(n, 0)
# check convergence, l1 norm
err = sum([abs(x[n] - xlast[n]) for n in x])
if err < N * tol:
iterprogress.append(x)
return iterprogress
raise nx.PowerIterationFailedConvergence(max_iter)
Then use the function verbose_pagerank the same as you did with nx.pagerank
adj_lists={
'A': 'B C'.split(' '),
'B': 'C',
'C': 'A',
'D': 'C'
}
G=nx.DiGraph()
for k in adj_lists.keys():
G.add_node(k)
for k in adj_lists.keys():
G.add_edges_from([(k, t) for t in adj_lists[k]])
pr = verbose_pagerank(G, alpha=1)
for i in pr:
print(i)
Output:
{'A': 0.25, 'B': 0.25, 'C': 0.25, 'D': 0.25}
{'A': 0.25, 'B': 0.125, 'C': 0.625, 'D': 0.0}
{'A': 0.625, 'B': 0.125, 'C': 0.25, 'D': 0.0}
...
{'A': 0.40000057220458984, 'B': 0.20000028610229492, 'C': 0.39999914169311523, 'D': 0.0}

Sorting according to clockwise point coordinates

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

Categories

Resources