From 0cca4a9eadd216ee29da7f4c3cb450f9c7d4851d Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Wed, 25 May 2016 21:33:38 +0200 Subject: [PATCH 001/153] Find intersections between line segments in 2D Initial upload in computational geometry module --- basics.py | 227 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 basics.py diff --git a/basics.py b/basics.py new file mode 100644 index 0000000000..c124c709ac --- /dev/null +++ b/basics.py @@ -0,0 +1,227 @@ +import numpy as np +from math import sqrt +import sympy + + +def snap_to_grid(pts, box=None, precision=1e-3): + + nd = pts.shape[0] + + if box is None: + box = np.reshape(np.ones(nd), (nd, 1)) + + # Precission in each direction + delta = box * precision + pts = np.rint(pts.astype(np.float64) / delta) * delta + return pts + + +def __nrm(v): + return np.sqrt(np.sum(v * v, axis=0)) + + +def __dist(p1, p2): + return __nrm(p1 - p2) + +# +# def is_unique_vert_array(pts, box=None, precision=1e-3): +# nd = pts.shape[0] +# num_pts = pts.shape[1] +# pts = snap_to_grid(pts, box, precision) +# for iter in range(num_pts): +# + + +def __points_equal(p1, p2, box, precesion=1e-3): + d = __dist(p1, p2) + nd = p1.shape[0] + return d < precesion * sqrt(nd) + + +def split_edge(vertices, edges, edge_ind, new_pt, box, precision): + + start = edges[0, edge_ind] + end = edges[1, edge_ind] + tags = edges[2:, edge_ind] + + vertices, pt_ind, _ = add_point(vertices, new_pt, box, precision) + if start == pt_ind or end == pt_ind: + new_line = False + return vertices, edges, new_line + if tags.size > 0: + new_edges = np.vstack((np.array([[start, pt_ind], + [pt_ind, end]]), + np.tile(tags, 2))) + else: + new_edges = np.array([[start, pt_ind], + [pt_ind, end]]) + edges = np.hstack((edges[:, :edge_ind], new_edges, edges[:, edge_ind+1:])) + new_line = True + return vertices, edges, new_line + + +def add_point(vertices, pt, box=None, precision=1e-3): + nd = vertices.shape[0] + vertices = snap_to_grid(vertices, box, precision) + pt = snap_to_grid(pt, box, precision) + dist = __dist(pt, vertices) + min_dist = np.min(dist) + if min_dist < precision * nd: + ind = np.argmin(dist) + new_point = None + return vertices, ind, new_point + vertices = np.append(vertices, pt, axis=1) + ind = vertices.shape[1]-1 + return vertices, ind, pt + + +def __lines_intersect(start_1, end_1, start_2, end_2): + # Check if lines intersect. For the moment, we do this by methods + # incorpoated in sympy. The implementation can be done by pure algebra + # if necessary (although this is a bit dirty). + p1 = sympy.Point(start_1[0], start_1[1]) + p2 = sympy.Point(end_1[0], end_1[1]) + p3 = sympy.Point(start_2[0], start_2[1]) + p4 = sympy.Point(end_2[0], end_2[1]) + + l1 = sympy.Line(p1, p2) + l2 = sympy.Line(p3, p4) + + isect = l1.intersection(l2) + if isect is None: + return None + else: + p = isect[0] + # Should this be a column vector? + return np.array([[p.x], [p.y]]) + + +def remove_edge_crossings(vertices, edges, box=None, precision=1e-3): + """ + + Parameters + ---------- + vertices + edges + box + precision + + Returns + ------- + + """ + num_edges = edges.shape[1] + nd = vertices.shape[0] + + # Only 2D is considered. 3D should not be too dificult, but it is not + # clear how relevant it is + if nd != 2: + raise NotImplementedError('Only 2D so far') + + edge_counter = 0 + + # Loop over all edges, search for intersections. The number of edges can + # change due to splitting. + while edge_counter < num_edges: + # The direct test of whether two edges intersect is somewhat + # expensive, and it is hard to vectorize this part. Therefore, + # we first identify edges which crosses the extention of this edge ( + # intersection of line and line segment). We then go on to test for + # real intersections. + + + # Find start and stop coordinates for all edges + start_x = vertices[0, edges[0]] + start_y = vertices[1, edges[0]] + end_x = vertices[0, edges[1]] + end_y = vertices[1, edges[1]] + + a = end_y - start_y + b = -(end_x - start_x) + xm = (start_x[edge_counter] + end_x[edge_counter]) / 2 + ym = (start_y[edge_counter] + end_y[edge_counter]) / 2 + + # For all lines, find which side of line i it's two endpoints are. + # If c1 and c2 have different signs, they will be on different sides + # of line i. See + # + # http://stackoverflow.com/questions/385305/efficient-maths-algorithm-to-calculate-intersections + # + # for more information. + c1 = a[edge_counter] * (start_x - xm) \ + + b[edge_counter] * (start_y - ym) + c2 = a[edge_counter] * (end_x - xm) + b[edge_counter] * (end_y - ym) + line_intersections = np.sign(c1) != np.sign(c2) + + # Find elements which may intersect. + intersections = np.argwhere(line_intersections) + # np.argwhere returns an array of dimensions (1, dim), so we reduce + # this to truly 1D, or simply continue with the next edge if there + # are no candidate edges + if intersections.size > 0: + intersections = intersections[0] + else: + # There are no candidates for intersection + edge_counter += 1 + continue + + int_counter = 0 + while intersections.size > 0 and int_counter < intersections.size: + # Line intersect (inner loop) is an intersection if it crosses + # the extension of line edge_counter (outer loop) (ie intsect it + # crosses the infinite line that goes through the endpoints of + # edge_counter), but we don't know if it actually crosses the + # line segment edge_counter. Now we do a more refined search to + # find if the line segments intersects. Note that there is no + # help in vectorizing lines_intersect and computing intersection + # points for all lines in intersections, since line i may be + # split, and the intersection points must recalculated. It may + # be possible to reorganize this while-loop by computing all + # intersection points(vectorized), and only recompuing if line + # edge_counter is split, but we keep things simple for now. + intsect = intersections[int_counter] + + # Check if this point intersects + new_pt = __lines_intersect(vertices[:, edges[0, edge_counter]], + vertices[:, edges[1, edge_counter]], + vertices[:, edges[0, intsect]], + vertices[:, edges[1, intsect]]) + if new_pt is not None: + # Split edge edge_counter (outer loop), unless the + # intersection hits an existing point (in practices this + # means the intersection runs through and endpoint of the + # edge, in which case ) + vertices, edges, split_outer_edge = split_edge(vertices, edges, + edge_counter, + new_pt, box, + precision) + # If the outer edge (represented by edge_counter) was split, + # e.g. inserted into the list of edges we need to increase the + # index of the inner edge + intsect += split_outer_edge + # Possibly split the inner edge + vertices, edges, split_inner_edge = split_edge(vertices, edges, + intsect, + new_pt, box, + precision) + # Update index of possible intersections + intersections += split_outer_edge + split_inner_edge + + # We're done with this candidate edge. Increase index of inner loop + int_counter += 1 + # We're done with all intersectiosn of this loop. increase index of + # outer loop + edge_counter += 1 + return vertices, edges + + +if __name__ == '__main__': + p = np.array([[-1, 1, 0, 0], + [0, 0, -1, 1]]) + lines = np.array([[0, 1], + [2, 3]]) + box = np.array([[2], [2]]) + new_pts, new_lines = remove_edge_crossings(p, lines, box) + assert np.allclose(new_pts, p) + assert np.allclose(new_lines, lines) + From 002b92205bd308038c4d20fc10280fe84f000deb Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Thu, 26 May 2016 12:42:52 +0200 Subject: [PATCH 002/153] Bugfix in splitting of intersecting fractures; fracture tags were not properly handled. The corresponding unit test is also updated. --- basics.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/basics.py b/basics.py index c124c709ac..4e22263686 100644 --- a/basics.py +++ b/basics.py @@ -51,7 +51,7 @@ def split_edge(vertices, edges, edge_ind, new_pt, box, precision): if tags.size > 0: new_edges = np.vstack((np.array([[start, pt_ind], [pt_ind, end]]), - np.tile(tags, 2))) + np.tile(tags[:, np.newaxis], 2))) else: new_edges = np.array([[start, pt_ind], [pt_ind, end]]) @@ -218,8 +218,10 @@ def remove_edge_crossings(vertices, edges, box=None, precision=1e-3): if __name__ == '__main__': p = np.array([[-1, 1, 0, 0], [0, 0, -1, 1]]) - lines = np.array([[0, 1], - [2, 3]]) + lines = np.array([[0, 2], + [1, 3], + [1, 2], + [3, 4]]) box = np.array([[2], [2]]) new_pts, new_lines = remove_edge_crossings(p, lines, box) assert np.allclose(new_pts, p) From aacd5c36a1149e6808f4bd584e2c2986e1af2aa7 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Tue, 7 Jun 2016 10:21:21 +0200 Subject: [PATCH 003/153] Added init files for various folders --- __init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 __init__.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 43ff0a9e1713bcc46d426940935cbda3acbce8cd Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Fri, 21 Oct 2016 13:12:46 +0200 Subject: [PATCH 004/153] Move unit tests in comp geom --- tests/__init__.py | 0 tests/test_intersections.py | 62 +++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/test_intersections.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_intersections.py b/tests/test_intersections.py new file mode 100644 index 0000000000..5106e3ac37 --- /dev/null +++ b/tests/test_intersections.py @@ -0,0 +1,62 @@ +import numpy as np +import unittest + +from compgeom import basics + + +class LineIntersection2DTest(unittest.TestCase): + + def test_lines_crossing_origin(self): + p = np.array([[-1, 1, 0, 0], + [0, 0, -1, 1]]) + lines = np.array([[0, 2], + [1, 3], + [1, 2], + [3, 4]]) + box = np.array([[2], [2]]) + + new_pts, new_lines = basics.remove_edge_crossings(p, lines, box) + + p_known = np.hstack((p, np.array([[0], [0]]))) + p_known = basics.snap_to_grid(p_known, box) + + lines_known = np.array([[0, 4, 2, 4], + [4, 1, 4, 3], + [1, 1, 2, 2], + [3, 3, 4, 4]]) + + assert np.allclose(new_pts, p_known) + assert np.allclose(new_lines, lines_known) + + def test_lines_no_crossing(self): + p = np.array([[-1, 1, 0, 0], + [0, 0, -1, 1]]) + + lines = np.array([[0, 1], + [2, 3]]) + box = np.array([[2], [2]]) + new_pts, new_lines = basics.remove_edge_crossings(p, lines, box) + assert np.allclose(new_pts, p) + assert np.allclose(new_lines, lines) + + if __name__ == '__main__': + unittest.main() + + +class SnapToGridTest(unittest.TestCase): + + def setUp(self): + self.box = np.array([1, 1]) + self.anisobox = np.array([2, 1]) + self.p = np.array([0.6, 0.6]) + + def test_snapping(self): + p_snapped = basics.snap_to_grid(self.p, self.box, precision=1) + assert np.allclose(p_snapped, np.array([1, 1])) + + def test_aniso_snapping(self): + p_snapped = basics.snap_to_grid(self.p, self.anisobox, precision=1) + assert np.allclose(p_snapped, np.array([0, 1])) + + if __name__ == '__main__': + unittest.main() \ No newline at end of file From 8f08d33e94541b537becd9c96f2e7b2d491e8333 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Fri, 28 Oct 2016 13:18:26 +0200 Subject: [PATCH 005/153] Renamed class for unit test --- tests/test_intersections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_intersections.py b/tests/test_intersections.py index 5106e3ac37..68c234981c 100644 --- a/tests/test_intersections.py +++ b/tests/test_intersections.py @@ -4,7 +4,7 @@ from compgeom import basics -class LineIntersection2DTest(unittest.TestCase): +class SplitIntersectingLines2DTest(unittest.TestCase): def test_lines_crossing_origin(self): p = np.array([[-1, 1, 0, 0], From 6613ad07f9599a62b103c7dd0f85559173b6bf90 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Fri, 28 Oct 2016 13:19:37 +0200 Subject: [PATCH 006/153] Made function for line intersections public. --- basics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/basics.py b/basics.py index 4e22263686..83795aa0cb 100644 --- a/basics.py +++ b/basics.py @@ -75,7 +75,7 @@ def add_point(vertices, pt, box=None, precision=1e-3): return vertices, ind, pt -def __lines_intersect(start_1, end_1, start_2, end_2): +def lines_intersect(start_1, end_1, start_2, end_2): # Check if lines intersect. For the moment, we do this by methods # incorpoated in sympy. The implementation can be done by pure algebra # if necessary (although this is a bit dirty). @@ -182,7 +182,7 @@ def remove_edge_crossings(vertices, edges, box=None, precision=1e-3): intsect = intersections[int_counter] # Check if this point intersects - new_pt = __lines_intersect(vertices[:, edges[0, edge_counter]], + new_pt = lines_intersect(vertices[:, edges[0, edge_counter]], vertices[:, edges[1, edge_counter]], vertices[:, edges[0, intsect]], vertices[:, edges[1, intsect]]) From 9f7b8971459fbeed4a97e6c1d0ac5a18148c36f1 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Fri, 28 Oct 2016 13:20:03 +0200 Subject: [PATCH 007/153] Bugfix in line intersections. Misunderstood API in Sympy.intersections --- basics.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/basics.py b/basics.py index 83795aa0cb..4d68f98fcb 100644 --- a/basics.py +++ b/basics.py @@ -84,11 +84,11 @@ def lines_intersect(start_1, end_1, start_2, end_2): p3 = sympy.Point(start_2[0], start_2[1]) p4 = sympy.Point(end_2[0], end_2[1]) - l1 = sympy.Line(p1, p2) - l2 = sympy.Line(p3, p4) + l1 = sympy.Segment(p1, p2) + l2 = sympy.Segment(p3, p4) isect = l1.intersection(l2) - if isect is None: + if isect is None or len(isect) == 0: return None else: p = isect[0] From fd1bda4e4014c597b315b7dde718e7ac062dc356 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Fri, 28 Oct 2016 13:20:38 +0200 Subject: [PATCH 008/153] Snap points to grid before looking for intersections. Not sure if this is correct, but seems logical. --- basics.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/basics.py b/basics.py index 4d68f98fcb..66bd570341 100644 --- a/basics.py +++ b/basics.py @@ -120,6 +120,8 @@ def remove_edge_crossings(vertices, edges, box=None, precision=1e-3): edge_counter = 0 + vertices = snap_to_grid(vertices, box, precision) + # Loop over all edges, search for intersections. The number of edges can # change due to splitting. while edge_counter < num_edges: From 1fa0ef55c5646c2a647d278baf446b9760329c39 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Fri, 28 Oct 2016 13:22:17 +0200 Subject: [PATCH 009/153] New unit test for three lines. Used to be a bug. --- tests/test_intersections.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_intersections.py b/tests/test_intersections.py index 68c234981c..987e1d42be 100644 --- a/tests/test_intersections.py +++ b/tests/test_intersections.py @@ -38,7 +38,21 @@ def test_lines_no_crossing(self): new_pts, new_lines = basics.remove_edge_crossings(p, lines, box) assert np.allclose(new_pts, p) assert np.allclose(new_lines, lines) + + def test_three_lines_no_crossing(self): + # This test gave an error at some point + p = np.array([[ 0., 0., 0.3, 1., 1., 0.5], + [2/3, 1/.7, 0.3, 2/3, 1/.7, 0.5]]) + lines = np.array([[0, 3], [1, 4], [2, 5]]).T + box = np.array([[1], [2]]) + new_pts, new_lines = basics.remove_edge_crossings(p, lines, box) + print(new_pts) + p_known = basics.snap_to_grid(p, box) + assert np.allclose(new_pts, p_known) + assert np.allclose(new_lines, lines) + + if __name__ == '__main__': unittest.main() From 3480749198f58f131babf53732b081fd81a9f1da Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Fri, 28 Oct 2016 13:23:25 +0200 Subject: [PATCH 010/153] Added unit test for case with intersecting lines but not segments --- tests/test_intersections.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_intersections.py b/tests/test_intersections.py index 987e1d42be..fae9df448d 100644 --- a/tests/test_intersections.py +++ b/tests/test_intersections.py @@ -72,5 +72,19 @@ def test_aniso_snapping(self): p_snapped = basics.snap_to_grid(self.p, self.anisobox, precision=1) assert np.allclose(p_snapped, np.array([0, 1])) + if __name__ == '__main__': + unittest.main() + + +class LinesIntersectTest(unittest.TestCase): + + def test_lines_intersect_segments_do_not(self): + s0 = np.array([0.3, 0.3]) + e0 = np.array([0.5, 0.5]) + s1 = np.array([0, 2/3]) + e1 = np.array([1, 2/3]) + pi = basics.lines_intersect(s0, e0, s1, e1) + assert(pi is None or len(pi)==0) + if __name__ == '__main__': unittest.main() \ No newline at end of file From c9f30ff9ef56fce3b74ba74f4eae1a429e493ef6 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Fri, 28 Oct 2016 14:25:15 +0200 Subject: [PATCH 011/153] New unit test for intersections, one isolated, two intersecting --- tests/test_intersections.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/test_intersections.py b/tests/test_intersections.py index fae9df448d..1d076b2af5 100644 --- a/tests/test_intersections.py +++ b/tests/test_intersections.py @@ -47,10 +47,24 @@ def test_three_lines_no_crossing(self): box = np.array([[1], [2]]) new_pts, new_lines = basics.remove_edge_crossings(p, lines, box) - print(new_pts) p_known = basics.snap_to_grid(p, box) assert np.allclose(new_pts, p_known) assert np.allclose(new_lines, lines) + + def test_three_lines_one_crossing(self): + # This test gave an error at some point + p = np.array([[ 0., 0.5, 0.3, 1., 0.3, 0.5], + [2/3, 0.3, 0.3, 2/3, 0.5, 0.5]]) + lines = np.array([[0, 3], [2, 5], [1, 4]]).T + box = np.array([[1], [2]]) + + new_pts, new_lines = basics.remove_edge_crossings(p, lines, box) + p_known = np.hstack((p, np.array([[0.4], [0.4]]))) + p_known = basics.snap_to_grid(p_known, box) + lines_known = np.array([[0, 3], [2, 6], [6, 5], [1, 6], [6, 4]]).T + assert np.allclose(new_pts, p_known) + assert np.allclose(new_lines, lines_known) + if __name__ == '__main__': From a76fe8205d96951715187c7c0ccf67126a41a361 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Fri, 28 Oct 2016 14:32:02 +0200 Subject: [PATCH 012/153] Cast to float in line intersection to work around sympy behavior --- basics.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/basics.py b/basics.py index 66bd570341..33a7f8cfa8 100644 --- a/basics.py +++ b/basics.py @@ -78,7 +78,17 @@ def add_point(vertices, pt, box=None, precision=1e-3): def lines_intersect(start_1, end_1, start_2, end_2): # Check if lines intersect. For the moment, we do this by methods # incorpoated in sympy. The implementation can be done by pure algebra - # if necessary (although this is a bit dirty). + # if necessary (although this becomes cumbersome). + + # It seems that if sympy is provided point coordinates as integers, it may + # do calculations in integers also, with an unknown approach to rounding. + # Cast the values to floats to avoid this. It is not the most pythonic + # style, but tracking down such a bug would have been a nightmare. + start_1 = start_1.astype(np.float) + end_1 = end_1.astype(np.float) + start_2 = start_2.astype(np.float) + end_2 = end_2.astype(np.float) + p1 = sympy.Point(start_1[0], start_1[1]) p2 = sympy.Point(end_1[0], end_1[1]) p3 = sympy.Point(start_2[0], start_2[1]) @@ -93,7 +103,7 @@ def lines_intersect(start_1, end_1, start_2, end_2): else: p = isect[0] # Should this be a column vector? - return np.array([[p.x], [p.y]]) + return np.array([[p.x], [p.y]], dtype='float') def remove_edge_crossings(vertices, edges, box=None, precision=1e-3): From 5fbad513ff412be4de562b889dad10b7df3a5160 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Fri, 28 Oct 2016 14:32:35 +0200 Subject: [PATCH 013/153] Bugfix in removal of fracture crossings. The old behavior seemed to work, don't know exactly what was wrong, but this is better. --- basics.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/basics.py b/basics.py index 33a7f8cfa8..324766bdb3 100644 --- a/basics.py +++ b/basics.py @@ -171,7 +171,7 @@ def remove_edge_crossings(vertices, edges, box=None, precision=1e-3): # this to truly 1D, or simply continue with the next edge if there # are no candidate edges if intersections.size > 0: - intersections = intersections[0] + intersections = intersections.ravel() else: # There are no candidates for intersection edge_counter += 1 @@ -198,6 +198,7 @@ def remove_edge_crossings(vertices, edges, box=None, precision=1e-3): vertices[:, edges[1, edge_counter]], vertices[:, edges[0, intsect]], vertices[:, edges[1, intsect]]) + if new_pt is not None: # Split edge edge_counter (outer loop), unless the # intersection hits an existing point (in practices this From e8d24908e53f3c8da3edd58676ede8f9f5b4680a Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Fri, 28 Oct 2016 17:41:21 +0200 Subject: [PATCH 014/153] Bugfix in splitting of fractures. The previous version failed to update the upper bound in a while loop (that is, increasing number of edges due to splitting) --- basics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basics.py b/basics.py index 324766bdb3..176d24dad2 100644 --- a/basics.py +++ b/basics.py @@ -134,7 +134,7 @@ def remove_edge_crossings(vertices, edges, box=None, precision=1e-3): # Loop over all edges, search for intersections. The number of edges can # change due to splitting. - while edge_counter < num_edges: + while edge_counter < edges.shape[1]: # The direct test of whether two edges intersect is somewhat # expensive, and it is hard to vectorize this part. Therefore, # we first identify edges which crosses the extention of this edge ( From ea4fe8fb136d0da9bb736ff49906e2566373fe02 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Fri, 28 Oct 2016 17:43:17 +0200 Subject: [PATCH 015/153] Minor changes of splitting of fractures. --- basics.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/basics.py b/basics.py index 176d24dad2..db3698a511 100644 --- a/basics.py +++ b/basics.py @@ -171,7 +171,7 @@ def remove_edge_crossings(vertices, edges, box=None, precision=1e-3): # this to truly 1D, or simply continue with the next edge if there # are no candidate edges if intersections.size > 0: - intersections = intersections.ravel() + intersections = intersections.ravel(0) else: # There are no candidates for intersection edge_counter += 1 @@ -202,8 +202,9 @@ def remove_edge_crossings(vertices, edges, box=None, precision=1e-3): if new_pt is not None: # Split edge edge_counter (outer loop), unless the # intersection hits an existing point (in practices this - # means the intersection runs through and endpoint of the - # edge, in which case ) + # means the intersection runs through an endpoint of the + # edge in an L-type configuration, in which case no new point + # is needed) vertices, edges, split_outer_edge = split_edge(vertices, edges, edge_counter, new_pt, box, @@ -213,7 +214,7 @@ def remove_edge_crossings(vertices, edges, box=None, precision=1e-3): # index of the inner edge intsect += split_outer_edge # Possibly split the inner edge - vertices, edges, split_inner_edge = split_edge(vertices, edges, + vertices, edges, split_inner_edge = split_edge(vertices, edges, intsect, new_pt, box, precision) @@ -222,7 +223,7 @@ def remove_edge_crossings(vertices, edges, box=None, precision=1e-3): # We're done with this candidate edge. Increase index of inner loop int_counter += 1 - # We're done with all intersectiosn of this loop. increase index of + # We're done with all intersections of this loop. increase index of # outer loop edge_counter += 1 return vertices, edges From 812913da3b1c1802bfdd64293f1189cc1a576026 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Fri, 18 Nov 2016 15:47:20 +0100 Subject: [PATCH 016/153] Sort line pairs of points so that polygon is connected. --- sort_points.py | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 sort_points.py diff --git a/sort_points.py b/sort_points.py new file mode 100644 index 0000000000..476c121fbb --- /dev/null +++ b/sort_points.py @@ -0,0 +1,54 @@ +def sort_point_pairs(lines, check_circular=True): + """ Sort pairs of numbers to form a chain. + + The target application is to sort lines, defined by their + start end endpoints, so that they form a continuous polyline. + + The algorithm is brute-force, using a double for-loop. This can + surely be imporved. + + Parameters: + lines: np.ndarray, 2xn, the line pairs. + check_circular: Verify that the sorted polyline form a circle. + Defaluts to true. + + Returns: + sorted_lines: np.ndarray, 2xn, sorted line pairs. + + """ + + num_lines = lines.shape[1] + sorted_lines = -np.ones((2, num_lines)) + + # Start with the first line in input + sorted_lines[:, 0] = lines[:, 0] + + # The starting point for the next line + prev = sorted_lines[1, 0] + + # Keep track of which lines have been found, which are still candidates + found = np.zeros(num_lines, dtype='bool') + found[0] = True + + # The sorting algorithm: Loop over all places in sorted_line to be filled, + # for each of these, loop over all members in lines, check if the line is still + # a candidate, and if one of its points equals the current starting point. + # More efficient and more elegant approaches can surely be found, but this + # will do for now. + for i in range(1, num_lines): # The first line has already been found + for j in range(0, num_lines): + if not found[j] and lines[0, j] == prev: + sorted_lines[:, i] = lines[:, j] + found[j] = True + prev = lines[1, i] + break + elif not found[j] and lines[1, j] == prev: + sorted_lines[:, i] = lines[::-1, j] + found[j] = True + prev = lines[1, i] + break + # By now, we should have used all lines + assert(np.all(found)) + if check_circular: + assert sorted_lines[0, 0] == sorted_lines[1, -1] + return sorted_lines From 5ca3199c4f0ea37d252c10f37c1686dc6e4adfb0 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Wed, 23 Nov 2016 10:08:02 +0100 Subject: [PATCH 017/153] Import numpy in sorting of points --- sort_points.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sort_points.py b/sort_points.py index 476c121fbb..c01102e6de 100644 --- a/sort_points.py +++ b/sort_points.py @@ -1,3 +1,5 @@ +import numpy as np + def sort_point_pairs(lines, check_circular=True): """ Sort pairs of numbers to form a chain. From d6295769e9345c46351df493bbc77f08d87703c2 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Mon, 19 Dec 2016 09:24:36 +0100 Subject: [PATCH 018/153] Gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..0d20b6487c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc From 7a33909d221474f1e48cca020a172756325fecde Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Tue, 27 Dec 2016 11:26:41 +0100 Subject: [PATCH 019/153] remove useless spaces --- sort_points.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/sort_points.py b/sort_points.py index c01102e6de..fe686a3d8e 100644 --- a/sort_points.py +++ b/sort_points.py @@ -2,13 +2,13 @@ def sort_point_pairs(lines, check_circular=True): """ Sort pairs of numbers to form a chain. - - The target application is to sort lines, defined by their + + The target application is to sort lines, defined by their start end endpoints, so that they form a continuous polyline. - - The algorithm is brute-force, using a double for-loop. This can + + The algorithm is brute-force, using a double for-loop. This can surely be imporved. - + Parameters: lines: np.ndarray, 2xn, the line pairs. check_circular: Verify that the sorted polyline form a circle. @@ -18,24 +18,24 @@ def sort_point_pairs(lines, check_circular=True): sorted_lines: np.ndarray, 2xn, sorted line pairs. """ - + num_lines = lines.shape[1] sorted_lines = -np.ones((2, num_lines)) # Start with the first line in input sorted_lines[:, 0] = lines[:, 0] - + # The starting point for the next line prev = sorted_lines[1, 0] - + # Keep track of which lines have been found, which are still candidates found = np.zeros(num_lines, dtype='bool') found[0] = True - - # The sorting algorithm: Loop over all places in sorted_line to be filled, + + # The sorting algorithm: Loop over all places in sorted_line to be filled, # for each of these, loop over all members in lines, check if the line is still # a candidate, and if one of its points equals the current starting point. - # More efficient and more elegant approaches can surely be found, but this + # More efficient and more elegant approaches can surely be found, but this # will do for now. for i in range(1, num_lines): # The first line has already been found for j in range(0, num_lines): From 63aebfa91e23cfe2e09fdca861850366acfe5941 Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Tue, 27 Dec 2016 11:27:11 +0100 Subject: [PATCH 020/153] maintain the same type of the input array for sort_point_pairs --- sort_points.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sort_points.py b/sort_points.py index fe686a3d8e..be95ed14f0 100644 --- a/sort_points.py +++ b/sort_points.py @@ -20,8 +20,8 @@ def sort_point_pairs(lines, check_circular=True): """ num_lines = lines.shape[1] - sorted_lines = -np.ones((2, num_lines)) - + sorted_lines = -np.ones( (2, num_lines), dtype = lines.dtype ) + # Start with the first line in input sorted_lines[:, 0] = lines[:, 0] From e408bc789a3661bef6cd3b05bf855afe0535400c Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Tue, 27 Dec 2016 11:27:52 +0100 Subject: [PATCH 021/153] add sort_point_face for a 3d object, given the points and the centre of the face return the ordered point map --- sort_points.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/sort_points.py b/sort_points.py index be95ed14f0..f3dfd74805 100644 --- a/sort_points.py +++ b/sort_points.py @@ -1,5 +1,7 @@ import numpy as np +#------------------------------------------------------------------------------# + def sort_point_pairs(lines, check_circular=True): """ Sort pairs of numbers to form a chain. @@ -54,3 +56,22 @@ def sort_point_pairs(lines, check_circular=True): if check_circular: assert sorted_lines[0, 0] == sorted_lines[1, -1] return sorted_lines + +#------------------------------------------------------------------------------# + +def sort_point_face( _pts, _centre ): + delta = np.array( [ p - _centre for p in _pts.T ] ) + norm = np.linalg.norm( delta ) + map_pts = np.zeros( _pts.shape[1], dtype = np.int ) + not_visited = np.ones( _pts.shape[1], dtype = np.bool ) + not_visited[0] = False + for n in np.arange( 1, _pts.shape[1] ): + dot_prod = [ np.dot( delta[map_pts[n-1], :], d ) for d in delta[ not_visited, : ] ] + min_val = np.argmin( np.arccos( np.divide( dot_prod, norm ) ) ) + mask = np.all( delta == delta[ not_visited, : ][ min_val ], axis = 1 ) + map_pts[n] = np.where( mask )[0] + not_visited[map_pts[n]] = False + + return map_pts + +#------------------------------------------------------------------------------# From 0446a6258320235a295f1f9f7435c0dd962c99fd Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Tue, 27 Dec 2016 11:31:17 +0100 Subject: [PATCH 022/153] add comments to sort_point_face --- sort_points.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/sort_points.py b/sort_points.py index f3dfd74805..c96c6cf90f 100644 --- a/sort_points.py +++ b/sort_points.py @@ -60,6 +60,18 @@ def sort_point_pairs(lines, check_circular=True): #------------------------------------------------------------------------------# def sort_point_face( _pts, _centre ): + """ Sort the points that form a face in 3D. + + The algorithm is brute-force and assume a planar face. + + Parameters: + _pts: np.ndarray, 3xn, the face points. + _centre: np.ndarray, 3x1, the face centre. + + Returns: + map_pts: np.array, 1xn, sorted point ids. + + """ delta = np.array( [ p - _centre for p in _pts.T ] ) norm = np.linalg.norm( delta ) map_pts = np.zeros( _pts.shape[1], dtype = np.int ) From 6ad955e1189da64379da03f6d008fc2bff724725 Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Wed, 28 Dec 2016 15:40:20 +0100 Subject: [PATCH 023/153] update the sort point face, the previous version had a bug --- sort_points.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/sort_points.py b/sort_points.py index c96c6cf90f..607f7751b3 100644 --- a/sort_points.py +++ b/sort_points.py @@ -73,17 +73,7 @@ def sort_point_face( _pts, _centre ): """ delta = np.array( [ p - _centre for p in _pts.T ] ) - norm = np.linalg.norm( delta ) - map_pts = np.zeros( _pts.shape[1], dtype = np.int ) - not_visited = np.ones( _pts.shape[1], dtype = np.bool ) - not_visited[0] = False - for n in np.arange( 1, _pts.shape[1] ): - dot_prod = [ np.dot( delta[map_pts[n-1], :], d ) for d in delta[ not_visited, : ] ] - min_val = np.argmin( np.arccos( np.divide( dot_prod, norm ) ) ) - mask = np.all( delta == delta[ not_visited, : ][ min_val ], axis = 1 ) - map_pts[n] = np.where( mask )[0] - not_visited[map_pts[n]] = False - - return map_pts + delta = np.divide( delta, np.linalg.norm( delta ) ) + return np.argsort( np.arctan2( *delta.T ) ) #------------------------------------------------------------------------------# From c5782afa0ec2e8209f47123b2aae30560a45b60d Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Wed, 28 Dec 2016 19:36:17 +0100 Subject: [PATCH 024/153] Resolves issue #1 on sorting of point pairs. --- sort_points.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sort_points.py b/sort_points.py index c01102e6de..36abddfed6 100644 --- a/sort_points.py +++ b/sort_points.py @@ -42,12 +42,12 @@ def sort_point_pairs(lines, check_circular=True): if not found[j] and lines[0, j] == prev: sorted_lines[:, i] = lines[:, j] found[j] = True - prev = lines[1, i] + prev = lines[1, j] break elif not found[j] and lines[1, j] == prev: sorted_lines[:, i] = lines[::-1, j] found[j] = True - prev = lines[1, i] + prev = lines[1, j] break # By now, we should have used all lines assert(np.all(found)) From 4ddaabb17d4d3e1550d96f2ec9d6d46cecff1ad8 Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Wed, 28 Dec 2016 19:57:11 +0100 Subject: [PATCH 025/153] possible resolution --- sort_points.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/sort_points.py b/sort_points.py index 36abddfed6..66c19a6ef6 100644 --- a/sort_points.py +++ b/sort_points.py @@ -2,13 +2,13 @@ def sort_point_pairs(lines, check_circular=True): """ Sort pairs of numbers to form a chain. - - The target application is to sort lines, defined by their + + The target application is to sort lines, defined by their start end endpoints, so that they form a continuous polyline. - - The algorithm is brute-force, using a double for-loop. This can + + The algorithm is brute-force, using a double for-loop. This can surely be imporved. - + Parameters: lines: np.ndarray, 2xn, the line pairs. check_circular: Verify that the sorted polyline form a circle. @@ -18,24 +18,24 @@ def sort_point_pairs(lines, check_circular=True): sorted_lines: np.ndarray, 2xn, sorted line pairs. """ - + num_lines = lines.shape[1] sorted_lines = -np.ones((2, num_lines)) - + # Start with the first line in input sorted_lines[:, 0] = lines[:, 0] - + # The starting point for the next line prev = sorted_lines[1, 0] - + # Keep track of which lines have been found, which are still candidates - found = np.zeros(num_lines, dtype='bool') + found = np.zeros(num_lines, dtype=np.bool) found[0] = True - - # The sorting algorithm: Loop over all places in sorted_line to be filled, + + # The sorting algorithm: Loop over all places in sorted_line to be filled, # for each of these, loop over all members in lines, check if the line is still # a candidate, and if one of its points equals the current starting point. - # More efficient and more elegant approaches can surely be found, but this + # More efficient and more elegant approaches can surely be found, but this # will do for now. for i in range(1, num_lines): # The first line has already been found for j in range(0, num_lines): @@ -47,7 +47,7 @@ def sort_point_pairs(lines, check_circular=True): elif not found[j] and lines[1, j] == prev: sorted_lines[:, i] = lines[::-1, j] found[j] = True - prev = lines[1, j] + prev = lines[0, j] break # By now, we should have used all lines assert(np.all(found)) From 005f669f7628326c440f55ff66c3a98311b870be Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Wed, 28 Dec 2016 20:14:11 +0100 Subject: [PATCH 026/153] Further fix of issue #1 - swithed start and end of line. --- sort_points.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sort_points.py b/sort_points.py index 36abddfed6..2f341625f7 100644 --- a/sort_points.py +++ b/sort_points.py @@ -47,7 +47,7 @@ def sort_point_pairs(lines, check_circular=True): elif not found[j] and lines[1, j] == prev: sorted_lines[:, i] = lines[::-1, j] found[j] = True - prev = lines[1, j] + prev = lines[0, j] break # By now, we should have used all lines assert(np.all(found)) From 14b75cb0fbac0acd148e8d2540a813d46a51bfbc Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Fri, 30 Dec 2016 09:37:09 +0100 Subject: [PATCH 027/153] add the functionality to compute the normal given three points and to project the point on the local coordinate of a plane --- geometry.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 geometry.py diff --git a/geometry.py b/geometry.py new file mode 100644 index 0000000000..1095524bba --- /dev/null +++ b/geometry.py @@ -0,0 +1,45 @@ +import numpy as np + +#------------------------------------------------------------------------------# + +def project_plane( pts, normal = None ): + """ Project the points on a plane using local coordinates. + + Parameters: + pts: np.ndarray, 3xn, the points. + normal: (optional) the normal of the plane, otherwise three points are + required. + + Returns: + pts: np.array, 2xn, projected points on the plane in the local coordinates. + + """ + + if normal is None: normal = compute_normal( pts ) + else: normal = normal / np.linalg.norm( normal ) + T = np.identity(3) - np.outer( normal, normal ) + pts = np.array( [ np.dot( T, p ) for p in pts.T ] ) + index = np.where( np.sum( np.abs( pts ), axis = 0 ) != 0 )[0] + return pts[:,index] + +#------------------------------------------------------------------------------# + +def compute_normal( pts ): + """ Compute the normal of a set of points. + + The algorithm assume that the points lie on a plane. + Three points are required. + + Parameters: + pts: np.ndarray, 3xn, the points. + + Returns: + normal: np.array, 1x3, the normal. + + """ + + assert( pts.shape[1] > 2 ) + normal = np.cross( pts[:,0] - pts[:,1], pts[:,0] - pts[:,2] ) + return normal / np.linalg.norm( normal ) + +#------------------------------------------------------------------------------# From 9354c561679704fafc0a48a5ec6232d824aec746 Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Fri, 30 Dec 2016 09:39:29 +0100 Subject: [PATCH 028/153] update the sort point face algorithm, now should works also in 3d --- sort_points.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/sort_points.py b/sort_points.py index 607f7751b3..95cb439148 100644 --- a/sort_points.py +++ b/sort_points.py @@ -1,4 +1,5 @@ import numpy as np +from compgeom.geometry import project_plane #------------------------------------------------------------------------------# @@ -59,20 +60,23 @@ def sort_point_pairs(lines, check_circular=True): #------------------------------------------------------------------------------# -def sort_point_face( _pts, _centre ): - """ Sort the points that form a face in 3D. +def sort_point_plane( pts, centre, normal = None ): + """ Sort the points which lie on a plane. - The algorithm is brute-force and assume a planar face. + The algorithm assumes a star-shaped disposition of the points with respect + the centre. Parameters: - _pts: np.ndarray, 3xn, the face points. - _centre: np.ndarray, 3x1, the face centre. + pts: np.ndarray, 3xn, the points. + centre: np.ndarray, 3x1, the face centre. + normal: (optional) the normal of the plane, otherwise three points are + required. Returns: map_pts: np.array, 1xn, sorted point ids. """ - delta = np.array( [ p - _centre for p in _pts.T ] ) + delta = project_plane( np.array( [ p - centre for p in pts.T ] ).T, normal ) delta = np.divide( delta, np.linalg.norm( delta ) ) return np.argsort( np.arctan2( *delta.T ) ) From b76b88095bc90d0b71a3439158b5a1eb335ebd36 Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Fri, 30 Dec 2016 10:04:38 +0100 Subject: [PATCH 029/153] move the functions in geometry into basiscs and remove the last lines in basics --- basics.py | 52 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/basics.py b/basics.py index db3698a511..1109498db0 100644 --- a/basics.py +++ b/basics.py @@ -228,16 +228,46 @@ def remove_edge_crossings(vertices, edges, box=None, precision=1e-3): edge_counter += 1 return vertices, edges +#------------------------------------------------------------------------------# -if __name__ == '__main__': - p = np.array([[-1, 1, 0, 0], - [0, 0, -1, 1]]) - lines = np.array([[0, 2], - [1, 3], - [1, 2], - [3, 4]]) - box = np.array([[2], [2]]) - new_pts, new_lines = remove_edge_crossings(p, lines, box) - assert np.allclose(new_pts, p) - assert np.allclose(new_lines, lines) +def project_plane( pts, normal = None ): + """ Project the points on a plane using local coordinates. + Parameters: + pts: np.ndarray, 3xn, the points. + normal: (optional) the normal of the plane, otherwise three points are + required. + + Returns: + pts: np.array, 2xn, projected points on the plane in the local coordinates. + + """ + + if normal is None: normal = compute_normal( pts ) + else: normal = normal / np.linalg.norm( normal ) + T = np.identity(3) - np.outer( normal, normal ) + pts = np.array( [ np.dot( T, p ) for p in pts.T ] ) + index = np.where( np.sum( np.abs( pts ), axis = 0 ) != 0 )[0] + return pts[:,index] + +#------------------------------------------------------------------------------# + +def compute_normal( pts ): + """ Compute the normal of a set of points. + + The algorithm assume that the points lie on a plane. + Three points are required. + + Parameters: + pts: np.ndarray, 3xn, the points. + + Returns: + normal: np.array, 1x3, the normal. + + """ + + assert( pts.shape[1] > 2 ) + normal = np.cross( pts[:,0] - pts[:,1], pts[:,0] - pts[:,2] ) + return normal / np.linalg.norm( normal ) + +#------------------------------------------------------------------------------# From 13b4b79f18e0ff4e1e439a3ca126736244693dad Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Fri, 30 Dec 2016 10:05:09 +0100 Subject: [PATCH 030/153] small changes in basics (separation line between functions) --- basics.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/basics.py b/basics.py index 1109498db0..59c1edcd34 100644 --- a/basics.py +++ b/basics.py @@ -2,6 +2,7 @@ from math import sqrt import sympy +#------------------------------------------------------------------------------# def snap_to_grid(pts, box=None, precision=1e-3): @@ -15,14 +16,17 @@ def snap_to_grid(pts, box=None, precision=1e-3): pts = np.rint(pts.astype(np.float64) / delta) * delta return pts +#------------------------------------------------------------------------------# def __nrm(v): return np.sqrt(np.sum(v * v, axis=0)) +#------------------------------------------------------------------------------# def __dist(p1, p2): return __nrm(p1 - p2) +#------------------------------------------------------------------------------# # # def is_unique_vert_array(pts, box=None, precision=1e-3): # nd = pts.shape[0] @@ -31,12 +35,14 @@ def __dist(p1, p2): # for iter in range(num_pts): # +#------------------------------------------------------------------------------# def __points_equal(p1, p2, box, precesion=1e-3): d = __dist(p1, p2) nd = p1.shape[0] return d < precesion * sqrt(nd) +#------------------------------------------------------------------------------# def split_edge(vertices, edges, edge_ind, new_pt, box, precision): @@ -59,6 +65,7 @@ def split_edge(vertices, edges, edge_ind, new_pt, box, precision): new_line = True return vertices, edges, new_line +#------------------------------------------------------------------------------# def add_point(vertices, pt, box=None, precision=1e-3): nd = vertices.shape[0] @@ -74,15 +81,16 @@ def add_point(vertices, pt, box=None, precision=1e-3): ind = vertices.shape[1]-1 return vertices, ind, pt +#------------------------------------------------------------------------------# def lines_intersect(start_1, end_1, start_2, end_2): # Check if lines intersect. For the moment, we do this by methods # incorpoated in sympy. The implementation can be done by pure algebra # if necessary (although this becomes cumbersome). - + # It seems that if sympy is provided point coordinates as integers, it may # do calculations in integers also, with an unknown approach to rounding. - # Cast the values to floats to avoid this. It is not the most pythonic + # Cast the values to floats to avoid this. It is not the most pythonic # style, but tracking down such a bug would have been a nightmare. start_1 = start_1.astype(np.float) end_1 = end_1.astype(np.float) @@ -105,6 +113,7 @@ def lines_intersect(start_1, end_1, start_2, end_2): # Should this be a column vector? return np.array([[p.x], [p.y]], dtype='float') +#------------------------------------------------------------------------------# def remove_edge_crossings(vertices, edges, box=None, precision=1e-3): """ @@ -131,7 +140,7 @@ def remove_edge_crossings(vertices, edges, box=None, precision=1e-3): edge_counter = 0 vertices = snap_to_grid(vertices, box, precision) - + # Loop over all edges, search for intersections. The number of edges can # change due to splitting. while edge_counter < edges.shape[1]: @@ -203,7 +212,7 @@ def remove_edge_crossings(vertices, edges, box=None, precision=1e-3): # Split edge edge_counter (outer loop), unless the # intersection hits an existing point (in practices this # means the intersection runs through an endpoint of the - # edge in an L-type configuration, in which case no new point + # edge in an L-type configuration, in which case no new point # is needed) vertices, edges, split_outer_edge = split_edge(vertices, edges, edge_counter, From cffea1ef53c614e9d26941a008c9bde6fd6836b2 Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Fri, 30 Dec 2016 10:05:36 +0100 Subject: [PATCH 031/153] change the importing in sort_points --- sort_points.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sort_points.py b/sort_points.py index 95cb439148..fff91a9eaf 100644 --- a/sort_points.py +++ b/sort_points.py @@ -1,5 +1,5 @@ import numpy as np -from compgeom.geometry import project_plane +from compgeom.basics import project_plane #------------------------------------------------------------------------------# From ae9957621584f2b74902dbe712d8506c66e5cc80 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Fri, 30 Dec 2016 20:10:25 +0100 Subject: [PATCH 032/153] Comments in geometry.py --- geometry.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/geometry.py b/geometry.py index 1095524bba..d94c931982 100644 --- a/geometry.py +++ b/geometry.py @@ -15,10 +15,16 @@ def project_plane( pts, normal = None ): """ - if normal is None: normal = compute_normal( pts ) - else: normal = normal / np.linalg.norm( normal ) + if normal is None: + normal = compute_normal( pts ) + else: + normal = normal / np.linalg.norm( normal ) + + # Projection matrix onto tangential plane T = np.identity(3) - np.outer( normal, normal ) + # Projected points pts = np.array( [ np.dot( T, p ) for p in pts.T ] ) + # Disregard points on the origin?? index = np.where( np.sum( np.abs( pts ), axis = 0 ) != 0 )[0] return pts[:,index] @@ -27,11 +33,13 @@ def project_plane( pts, normal = None ): def compute_normal( pts ): """ Compute the normal of a set of points. - The algorithm assume that the points lie on a plane. - Three points are required. + The algorithm computes the normal of the plane defined by the first three + points in the set. + TODO: Introduce optional check that all points lay in the same plane + (should be separate subroutine). Parameters: - pts: np.ndarray, 3xn, the points. + pts: np.ndarray, 3xn, the points. Need n > 2. Returns: normal: np.array, 1x3, the normal. From aa014984188d45c2bf1dcba71e7e72432e3249fa Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Mon, 2 Jan 2017 15:46:55 +0100 Subject: [PATCH 033/153] update the compute normal --- basics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/basics.py b/basics.py index 59c1edcd34..1e1ce6f4b7 100644 --- a/basics.py +++ b/basics.py @@ -274,9 +274,9 @@ def compute_normal( pts ): normal: np.array, 1x3, the normal. """ - assert( pts.shape[1] > 2 ) - normal = np.cross( pts[:,0] - pts[:,1], pts[:,0] - pts[:,2] ) + if pts.shape[0] == 2: pts = to_3d(pts) + normal = np.cross( pts[:,0] - pts[:,1], pts[:,0] - np.mean( pts, axis = 1 ) ) return normal / np.linalg.norm( normal ) #------------------------------------------------------------------------------# From efb02a18b793204f871aa64768debf2d2b267c35 Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Mon, 2 Jan 2017 15:47:56 +0100 Subject: [PATCH 034/153] introduce the rotation matrix for a generic direction --- basics.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/basics.py b/basics.py index 1e1ce6f4b7..408e23a73d 100644 --- a/basics.py +++ b/basics.py @@ -258,6 +258,26 @@ def project_plane( pts, normal = None ): pts = np.array( [ np.dot( T, p ) for p in pts.T ] ) index = np.where( np.sum( np.abs( pts ), axis = 0 ) != 0 )[0] return pts[:,index] +#------------------------------------------------------------------------------# + +def rot( a, vect ): + """ Compute the rotation matrix about a vector by an angle using the matrix + form of Rodrigues formula. + + Parameters: + a: double, the angle. + vect: np.array, 3, the vector. + + Returns: + matrix: np.ndarray, 3x3, the rotation matrix. + + """ + vect = vect / np.linalg.norm( vect ) + W = np.array( [ [ 0., -vect[2], vect[1] ], + [ vect[2], 0., -vect[0] ], + [ -vect[1], vect[0], 0. ] ] ) + return np.identity(3) + np.sin(a)*W + \ + (1.-np.cos(a))*np.linalg.matrix_power(W,2) #------------------------------------------------------------------------------# From d28c31b0d46b3157bd6bff7b154155717a0ed501 Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Mon, 2 Jan 2017 15:48:17 +0100 Subject: [PATCH 035/153] update the project_plane now to project_plane_matrix --- basics.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/basics.py b/basics.py index 408e23a73d..5155f29452 100644 --- a/basics.py +++ b/basics.py @@ -239,7 +239,7 @@ def remove_edge_crossings(vertices, edges, box=None, precision=1e-3): #------------------------------------------------------------------------------# -def project_plane( pts, normal = None ): +def project_plane_matrix( pts, normal = None ): """ Project the points on a plane using local coordinates. Parameters: @@ -248,16 +248,20 @@ def project_plane( pts, normal = None ): required. Returns: - pts: np.array, 2xn, projected points on the plane in the local coordinates. + matrix: np.ndarray, 3x3, projection matrix. + The projected points are computed by a dot product. + example: np.array( [ np.dot( R, p ) for p in pts.T ] ).T """ - if normal is None: normal = compute_normal( pts ) else: normal = normal / np.linalg.norm( normal ) - T = np.identity(3) - np.outer( normal, normal ) - pts = np.array( [ np.dot( T, p ) for p in pts.T ] ) - index = np.where( np.sum( np.abs( pts ), axis = 0 ) != 0 )[0] - return pts[:,index] + + reference = np.array( [0., 0., 1.] ) + angle = np.arccos( np.dot( normal, reference ) ) + if angle == 0: return np.identity(3) + vect = np.cross( normal, reference ) + return rot( angle, vect ) + #------------------------------------------------------------------------------# def rot( a, vect ): From d1f1485496e4b61ca5e2708cf5aaa2152bedeffe Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Mon, 2 Jan 2017 15:51:16 +0100 Subject: [PATCH 036/153] add test for the basic computations --- tests/test_basics.py | 54 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 tests/test_basics.py diff --git a/tests/test_basics.py b/tests/test_basics.py new file mode 100644 index 0000000000..df7d1da957 --- /dev/null +++ b/tests/test_basics.py @@ -0,0 +1,54 @@ +import numpy as np +import unittest + +from compgeom import basics + +#------------------------------------------------------------------------------# + +class BasicsTest( unittest.TestCase ): + +#------------------------------------------------------------------------------# + + def test_compute_normal_2d( self ): + pts = np.array( [ [ 0., 2., -1 ], + [ 0., 4., 2 ] ] ) + normal = basics.compute_normal( pts ) + normal_test = np.array([0.,0.,1.]) + pt = basics.to_3d_pt( pts[:,0] ) + + assert np.allclose( np.linalg.norm( normal ), 1. ) + assert np.allclose( [ np.dot( normal, p - pt ) \ + for p in basics.to_3d(pts[:,1:]).T ], + np.zeros( pts.shape[1] - 1 ) ) + assert np.allclose( normal, normal_test ) + +#------------------------------------------------------------------------------# + + def test_compute_normal_3d( self ): + pts = np.array( [ [ 2., 0., 1., 1. ], + [ 1., -2., -1., 1. ], + [ -1., 0., 2., -8. ] ] ) + normal_test = np.array( [7., -5., -1.] ) + normal_test = normal_test / np.linalg.norm( normal_test ) + normal = basics.compute_normal( pts ) + pt = pts[:,0] + + assert np.allclose( np.linalg.norm( normal ), 1. ) + assert np.allclose( [ np.dot( normal, p - pt ) \ + for p in pts[:,1:].T ], + np.zeros( pts.shape[1] - 1 ) ) + assert np.allclose( normal, normal_test ) or \ + np.allclose( normal, -1. * normal_test ) + +#------------------------------------------------------------------------------# + + def test_project_plane( self ): + pts = np.array( [ [ 2., 0., 1., 1. ], + [ 1., -2., -1., 1. ], + [ -1., 0., 2., -8. ] ] ) + R = basics.project_plane_matrix( pts ) + P_pts = np.array( [ np.dot( R, p ) for p in pts.T ] ).T + + assert np.allclose( P_pts[2,:], 1.15470054 * np.ones(4) ) + +#------------------------------------------------------------------------------# From 72175423d830889c7c9c693ca4202aaee425855b Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Mon, 2 Jan 2017 15:57:44 +0100 Subject: [PATCH 037/153] remove the to_3d mapping, we assume the coordinates are 3d --- basics.py | 1 - 1 file changed, 1 deletion(-) diff --git a/basics.py b/basics.py index 5155f29452..378fa6fcef 100644 --- a/basics.py +++ b/basics.py @@ -299,7 +299,6 @@ def compute_normal( pts ): """ assert( pts.shape[1] > 2 ) - if pts.shape[0] == 2: pts = to_3d(pts) normal = np.cross( pts[:,0] - pts[:,1], pts[:,0] - np.mean( pts, axis = 1 ) ) return normal / np.linalg.norm( normal ) From accf730daba55afb0e5a3d95f08e5adedd108b17 Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Mon, 2 Jan 2017 15:58:02 +0100 Subject: [PATCH 038/153] update the sort point plane --- sort_points.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/sort_points.py b/sort_points.py index fff91a9eaf..80b9e1b105 100644 --- a/sort_points.py +++ b/sort_points.py @@ -1,5 +1,5 @@ import numpy as np -from compgeom.basics import project_plane +from compgeom.basics import project_plane_matrix #------------------------------------------------------------------------------# @@ -76,8 +76,11 @@ def sort_point_plane( pts, centre, normal = None ): map_pts: np.array, 1xn, sorted point ids. """ - delta = project_plane( np.array( [ p - centre for p in pts.T ] ).T, normal ) - delta = np.divide( delta, np.linalg.norm( delta ) ) - return np.argsort( np.arctan2( *delta.T ) ) + R = project_plane_matrix( pts, normal ) + pts = np.array( [ np.dot(R, p) for p in pts.T ] ).T + centre = np.dot(R, centre) + delta = np.array( [ p - centre for p in pts.T] ).T[0:2,:] + delta = np.array( [ d / np.linalg.norm(d) for d in delta.T] ).T + return np.argsort( np.arctan2( *delta ) ) #------------------------------------------------------------------------------# From d4dc76c054a5ae38bd723c73d55e245bc8f0f227 Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Mon, 2 Jan 2017 15:58:39 +0100 Subject: [PATCH 039/153] in the test_basics all the coordinates are 3d --- tests/test_basics.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_basics.py b/tests/test_basics.py index df7d1da957..98099ea902 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -10,15 +10,16 @@ class BasicsTest( unittest.TestCase ): #------------------------------------------------------------------------------# def test_compute_normal_2d( self ): - pts = np.array( [ [ 0., 2., -1 ], - [ 0., 4., 2 ] ] ) + pts = np.array( [ [ 0., 2., -1. ], + [ 0., 4., 2. ], + [ 0., 0., 0. ] ] ) normal = basics.compute_normal( pts ) normal_test = np.array([0.,0.,1.]) - pt = basics.to_3d_pt( pts[:,0] ) + pt = pts[:,0] assert np.allclose( np.linalg.norm( normal ), 1. ) assert np.allclose( [ np.dot( normal, p - pt ) \ - for p in basics.to_3d(pts[:,1:]).T ], + for p in pts[:,1:].T ], np.zeros( pts.shape[1] - 1 ) ) assert np.allclose( normal, normal_test ) From 13be5080f3c3e8317f478b36f67f1ee6cd377679 Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Mon, 2 Jan 2017 16:25:15 +0100 Subject: [PATCH 040/153] update the rotation matrix computation --- basics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basics.py b/basics.py index 378fa6fcef..1bcb16066e 100644 --- a/basics.py +++ b/basics.py @@ -258,7 +258,6 @@ def project_plane_matrix( pts, normal = None ): reference = np.array( [0., 0., 1.] ) angle = np.arccos( np.dot( normal, reference ) ) - if angle == 0: return np.identity(3) vect = np.cross( normal, reference ) return rot( angle, vect ) @@ -276,6 +275,7 @@ def rot( a, vect ): matrix: np.ndarray, 3x3, the rotation matrix. """ + if np.allclose( vect, [0.,0.,0.] ): return np.identity(3) vect = vect / np.linalg.norm( vect ) W = np.array( [ [ 0., -vect[2], vect[1] ], [ vect[2], 0., -vect[0] ], From be16d3cb2ac071a13f60e04f70daf8cb24ffe2ef Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Sun, 1 Jan 2017 20:08:37 +0100 Subject: [PATCH 041/153] Documentation of basic geometry functions. --- basics.py | 232 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 197 insertions(+), 35 deletions(-) diff --git a/basics.py b/basics.py index 1bcb16066e..08c6fe64b2 100644 --- a/basics.py +++ b/basics.py @@ -1,3 +1,10 @@ +""" +Various utility functions related to computational geometry. + +Some functions (add_point, split_edges, ...?) are mainly aimed at finding +intersection between lines, with grid generation in mind, and should perhaps +be moved to a separate module. +""" import numpy as np from math import sqrt import sympy @@ -5,11 +12,41 @@ #------------------------------------------------------------------------------# def snap_to_grid(pts, box=None, precision=1e-3): + """ + Snap points to an underlying Cartesian grid. + Used e.g. for avoiding rounding issues when testing for equality between + points. + + Anisotropy in the rounding rules can be enforced by the parameter box. + + >>> snap_to_grid([[0.2445], [0.501]]) + array([[0.245], [0.501]) + + >>> snap_to_grid([[0.2445], [0.501]], box=[[10], 1]) + array([[0.24], [0.501]) + + >>> snap_to_grid([[0.2445], [0.501]], precision=0.01) + array([[0.24], [0.5]]) + + Parameters: + pts (np.ndarray, nd x n_pts): Points to be rounded. + box (np.ndarray, nd x 1, optional): Size of the domain, precision will + be taken relative to the size. Defaults to unit box. + precision (double, optional): Resolution of the underlying grid. + + Returns: + np.ndarray, nd x n_pts: Rounded coordinates. + + """ + + pts = np.asarray(pts) nd = pts.shape[0] if box is None: box = np.reshape(np.ones(nd), (nd, 1)) + else: + box = np.asarray(box) # Precission in each direction delta = box * precision @@ -45,22 +82,70 @@ def __points_equal(p1, p2, box, precesion=1e-3): #------------------------------------------------------------------------------# def split_edge(vertices, edges, edge_ind, new_pt, box, precision): + """ + Split a line into two by introcuding a new point. + Function is used e.g. for gridding purposes. + + The input can be a set of points, and lines between them, of which one is + to be split. + + A new line will be inserted, unless the new point coincides with the + start or endpoint of the edge (under the given precision). + + The code is intended for 2D, in 3D use with caution. + + Examples: + >>> p = [[0, 0], [0, 1]] + >>> edges = [[0], [1]] + >>> new_pt = [[0], [0.5]] + >>> v, e, nl = split_edge(p, edges, 0, new_pt) + >>> e + array([[0, 2], [2, 1]]) + + Parameters: + vertices (np.ndarray, nd x num_pt): Points of the vertex sets. + edges (np.ndarray, n x num_edges): Connections between lines. If n>2, + the additional rows are treated as tags, that are preserved under + splitting. + edge_ind (int): index of edge to be split, refering to edges. + new_pt (np.ndarray, nd x 1): new point to be inserted. Assumed to be + on the edge to be split. + box (np.ndarray, nd x 1): bounding box of the domain, see snap_to_grid + for usage. + precission (double): precision of underlying grid. See snap_to_grid + for usage. + + Returns: + np.ndarray, nd x n_pt: new point set, possibly with new point inserted. + np.ndarray, n x n_con: new edge set, possibly with new lines defined. + boolean: True if a new line is created, otherwise false. + + + """ start = edges[0, edge_ind] end = edges[1, edge_ind] + # Save tags associated with the edge. tags = edges[2:, edge_ind] + # Add a new point vertices, pt_ind, _ = add_point(vertices, new_pt, box, precision) + # If the new point coincide with the start point, nothing happens if start == pt_ind or end == pt_ind: new_line = False return vertices, edges, new_line + + # If we get here, we know that a new point has been created. + + # Add any tags to the new edge. if tags.size > 0: new_edges = np.vstack((np.array([[start, pt_ind], [pt_ind, end]]), np.tile(tags[:, np.newaxis], 2))) else: new_edges = np.array([[start, pt_ind], - [pt_ind, end]]) + [pt_ind, end]]) + # Insert the new edge in the midle of the set of edges. edges = np.hstack((edges[:, :edge_ind], new_edges, edges[:, edge_ind+1:])) new_line = True return vertices, edges, new_line @@ -68,34 +153,92 @@ def split_edge(vertices, edges, edge_ind, new_pt, box, precision): #------------------------------------------------------------------------------# def add_point(vertices, pt, box=None, precision=1e-3): + """ + Add a point to a point set, unless the point already exist in the set. + + Point coordinates are compared relative to an underlying Cartesian grid, + see snap_to_grid for details. + + The function is created with gridding in mind, but may be useful for other + purposes as well. + + Parameters: + vertices (np.ndarray, nd x num_pts): existing point set + pt (np.ndarray, nd x 1): Point to be added + box, precision: Parameters bassed to snap_to_grid, see that function + for details. + + Returns: + np.ndarray, nd x n_pt: New point set, possibly containing a new point + int: Index of the new point added (if any). If not, index of the + closest existing point, i.e. the one that made a new point unnecessary. + np.ndarray, nd x 1: The new point, or None if no new point was needed. + + """ + nd = vertices.shape[0] + # Before comparing coordinates, snap both existing and new point to the + # underlying grid vertices = snap_to_grid(vertices, box, precision) pt = snap_to_grid(pt, box, precision) + + # Distance dist = __dist(pt, vertices) min_dist = np.min(dist) - if min_dist < precision * nd: + + if min_dist < precision * np.sqrt(nd): + # No new point is needed ind = np.argmin(dist) new_point = None return vertices, ind, new_point - vertices = np.append(vertices, pt, axis=1) - ind = vertices.shape[1]-1 - return vertices, ind, pt + else: + # Append the new point. + vertices = np.append(vertices, pt, axis=1) + ind = vertices.shape[1]-1 + return vertices, ind, pt #------------------------------------------------------------------------------# def lines_intersect(start_1, end_1, start_2, end_2): - # Check if lines intersect. For the moment, we do this by methods - # incorpoated in sympy. The implementation can be done by pure algebra - # if necessary (although this becomes cumbersome). + """ + Check if two line segments defined by their start end endpoints, intersect. + + The lines are assumed to be in 2D. + + The function uses sympy to find intersections. At the moment (Jan 2017), + sympy is not very effective, so this may become a bottleneck if the method + is called repeatedly. An purely algebraic implementation is simple, but + somewhat cumbersome. + + Example: + >>> lines_intersect([0, 0], [1, 1], [0, 1], [1, 0]) + array([0.5, 0.5]) + + >>> lines_intersect([0, 0], [1, 0], [0, 1], [1, 1]) + None + + Parameters: + start_1 (np.ndarray or list): coordinates of start point for first + line. + end_1 (np.ndarray or list): coordinates of end point for first line. + start_2 (np.ndarray or list): coordinates of start point for first + line. + end_2 (np.ndarray or list): coordinates of end point for first line. + + Returns: + np.ndarray: coordinates of intersection point, or None if the lines do + not intersect. + """ + # It seems that if sympy is provided point coordinates as integers, it may # do calculations in integers also, with an unknown approach to rounding. # Cast the values to floats to avoid this. It is not the most pythonic # style, but tracking down such a bug would have been a nightmare. - start_1 = start_1.astype(np.float) - end_1 = end_1.astype(np.float) - start_2 = start_2.astype(np.float) - end_2 = end_2.astype(np.float) + start_1 = np.asarray(start_1).astype(np.float) + end_1 = np.asarray(end_1).astype(np.float) + start_2 = np.asarray(start_2).astype(np.float) + end_2 = np.asarray(end_2).astype(np.float) p1 = sympy.Point(start_1[0], start_1[1]) p2 = sympy.Point(end_1[0], end_1[1]) @@ -110,23 +253,38 @@ def lines_intersect(start_1, end_1, start_2, end_2): return None else: p = isect[0] - # Should this be a column vector? return np.array([[p.x], [p.y]], dtype='float') #------------------------------------------------------------------------------# def remove_edge_crossings(vertices, edges, box=None, precision=1e-3): """ + Process a set of points and connections between them so that the result + is an extended point set and new connections that do not intersect. + + The function is written for gridding of fractured domains, but may be + of use in other cases as well. The geometry is assumed to be 2D, (the + corresponding operation in 3D requires intersection between planes, and + is a rather complex, although doable, task). + + The connections are defined by their start and endpoints, and can also + have tags assigned. If so, the tags are preserved as connections are split. + + Parameters: + vertices (np.ndarray, 2 x n_pt): Coordinates of points to be processed + edges (np.ndarray, n x n_con): Connections between lines. n >= 2, row + 0 and 1 are index of start and endpoints, additional rows are tags + box (np.ndarray, nd): Size of domain, passed to snap_to_grid, see that + function for comments. + precission (double): Resolution of underlying Cartesian grid, see + snap_to_grid for details. - Parameters - ---------- - vertices - edges - box - precision + Returns: + np.ndarray, (2 x n_pt), array of points, possibly expanded. + np.ndarray, (n x n_edges), array of new edges. Non-intersecting. - Returns - ------- + Raises: + NotImplementedError if a 3D point array is provided. """ num_edges = edges.shape[1] @@ -242,19 +400,22 @@ def remove_edge_crossings(vertices, edges, box=None, precision=1e-3): def project_plane_matrix( pts, normal = None ): """ Project the points on a plane using local coordinates. + The projected points are computed by a dot product. + example: np.array( [ np.dot( R, p ) for p in pts.T ] ).T + Parameters: - pts: np.ndarray, 3xn, the points. - normal: (optional) the normal of the plane, otherwise three points are - required. + pts (np.ndarray, 3xn): the points. + normal: (optional) the normal of the plane, otherwise three points are + required. Returns: - matrix: np.ndarray, 3x3, projection matrix. - The projected points are computed by a dot product. - example: np.array( [ np.dot( R, p ) for p in pts.T ] ).T + np.ndarray, 3x3, projection matrix. """ - if normal is None: normal = compute_normal( pts ) - else: normal = normal / np.linalg.norm( normal ) + if normal is None: + normal = compute_normal( pts ) + else: + normal = normal / np.linalg.norm( normal ) reference = np.array( [0., 0., 1.] ) angle = np.arccos( np.dot( normal, reference ) ) @@ -268,14 +429,15 @@ def rot( a, vect ): form of Rodrigues formula. Parameters: - a: double, the angle. - vect: np.array, 3, the vector. + a: double, the angle. + vect: np.array, 3, the vector. Returns: - matrix: np.ndarray, 3x3, the rotation matrix. + matrix: np.ndarray, 3x3, the rotation matrix. """ - if np.allclose( vect, [0.,0.,0.] ): return np.identity(3) + if np.allclose( vect, [0.,0.,0.] ): + return np.identity(3) vect = vect / np.linalg.norm( vect ) W = np.array( [ [ 0., -vect[2], vect[1] ], [ vect[2], 0., -vect[0] ], @@ -292,10 +454,10 @@ def compute_normal( pts ): Three points are required. Parameters: - pts: np.ndarray, 3xn, the points. + pts: np.ndarray, 3xn, the points. Returns: - normal: np.array, 1x3, the normal. + normal: np.array, 1x3, the normal. """ assert( pts.shape[1] > 2 ) From 5ec9c07fa313a6c35c811e06eee1800c746a6166 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Mon, 2 Jan 2017 09:50:21 +0100 Subject: [PATCH 042/153] Improved documentation and fixed doctests in basic geometry --- basics.py | 101 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 56 insertions(+), 45 deletions(-) diff --git a/basics.py b/basics.py index 08c6fe64b2..c97c81aaed 100644 --- a/basics.py +++ b/basics.py @@ -4,7 +4,9 @@ Some functions (add_point, split_edges, ...?) are mainly aimed at finding intersection between lines, with grid generation in mind, and should perhaps be moved to a separate module. + """ + import numpy as np from math import sqrt import sympy @@ -13,25 +15,28 @@ def snap_to_grid(pts, box=None, precision=1e-3): """ - Snap points to an underlying Cartesian grid. + Snap points to an underlying Cartesian grid. Used e.g. for avoiding rounding issues when testing for equality between points. Anisotropy in the rounding rules can be enforced by the parameter box. - >>> snap_to_grid([[0.2445], [0.501]]) - array([[0.245], [0.501]) + >>> snap_to_grid([[0.2443], [0.501]]) + array([[ 0.244], + [ 0.501]]) - >>> snap_to_grid([[0.2445], [0.501]], box=[[10], 1]) - array([[0.24], [0.501]) + >>> snap_to_grid([[0.2443], [0.501]], box=[[10], [1]]) + array([[ 0.24 ], + [ 0.501]]) - >>> snap_to_grid([[0.2445], [0.501]], precision=0.01) - array([[0.24], [0.5]]) + >>> snap_to_grid([[0.2443], [0.501]], precision=0.01) + array([[ 0.24], + [ 0.5 ]]) Parameters: pts (np.ndarray, nd x n_pts): Points to be rounded. box (np.ndarray, nd x 1, optional): Size of the domain, precision will - be taken relative to the size. Defaults to unit box. + be taken relative to the size. Defaults to unit box. precision (double, optional): Resolution of the underlying grid. Returns: @@ -46,7 +51,7 @@ def snap_to_grid(pts, box=None, precision=1e-3): if box is None: box = np.reshape(np.ones(nd), (nd, 1)) else: - box = np.asarray(box) + box = np.asarray(box) # Precission in each direction delta = box * precision @@ -86,43 +91,42 @@ def split_edge(vertices, edges, edge_ind, new_pt, box, precision): Split a line into two by introcuding a new point. Function is used e.g. for gridding purposes. - The input can be a set of points, and lines between them, of which one is + The input can be a set of points, and lines between them, of which one is to be split. - A new line will be inserted, unless the new point coincides with the + A new line will be inserted, unless the new point coincides with the start or endpoint of the edge (under the given precision). The code is intended for 2D, in 3D use with caution. Examples: - >>> p = [[0, 0], [0, 1]] - >>> edges = [[0], [1]] - >>> new_pt = [[0], [0.5]] - >>> v, e, nl = split_edge(p, edges, 0, new_pt) + >>> p = np.array([[0, 0], [0, 1]]) + >>> edges = np.array([[0], [1]]) + >>> new_pt = np.array([[0], [0.5]]) + >>> v, e, nl = split_edge(p, edges, 0, new_pt, [[1], [1]], 1e-3) >>> e - array([[0, 2], [2, 1]]) + array([[0, 2], + [2, 1]]) Parameters: vertices (np.ndarray, nd x num_pt): Points of the vertex sets. - edges (np.ndarray, n x num_edges): Connections between lines. If n>2, - the additional rows are treated as tags, that are preserved under - splitting. + edges (np.ndarray, n x num_edges): Connections between lines. If n>2, + the additional rows are treated as tags, that are preserved under + splitting. edge_ind (int): index of edge to be split, refering to edges. new_pt (np.ndarray, nd x 1): new point to be inserted. Assumed to be - on the edge to be split. + on the edge to be split. box (np.ndarray, nd x 1): bounding box of the domain, see snap_to_grid - for usage. - precission (double): precision of underlying grid. See snap_to_grid - for usage. + for usage. + precission (double): precision of underlying grid. See snap_to_grid + for usage. Returns: np.ndarray, nd x n_pt: new point set, possibly with new point inserted. np.ndarray, n x n_con: new edge set, possibly with new lines defined. boolean: True if a new line is created, otherwise false. - """ - start = edges[0, edge_ind] end = edges[1, edge_ind] # Save tags associated with the edge. @@ -134,7 +138,7 @@ def split_edge(vertices, edges, edge_ind, new_pt, box, precision): if start == pt_ind or end == pt_ind: new_line = False return vertices, edges, new_line - + # If we get here, we know that a new point has been created. # Add any tags to the new edge. @@ -165,15 +169,16 @@ def add_point(vertices, pt, box=None, precision=1e-3): Parameters: vertices (np.ndarray, nd x num_pts): existing point set pt (np.ndarray, nd x 1): Point to be added - box, precision: Parameters bassed to snap_to_grid, see that function - for details. + box, precision: Parameters bassed to snap_to_grid, see that function + for details. Returns: np.ndarray, nd x n_pt: New point set, possibly containing a new point - int: Index of the new point added (if any). If not, index of the - closest existing point, i.e. the one that made a new point unnecessary. + int: Index of the new point added (if any). If not, index of the + closest existing point, i.e. the one that made a new point + unnecessary. np.ndarray, nd x 1: The new point, or None if no new point was needed. - + """ nd = vertices.shape[0] @@ -192,9 +197,10 @@ def add_point(vertices, pt, box=None, precision=1e-3): new_point = None return vertices, ind, new_point else: - # Append the new point. - vertices = np.append(vertices, pt, axis=1) ind = vertices.shape[1]-1 + # Append the new point at the end of the point list + vertices = np.append(vertices, pt, axis=1) + ind = vertices.shape[1] - 1 return vertices, ind, pt #------------------------------------------------------------------------------# @@ -210,26 +216,31 @@ def lines_intersect(start_1, end_1, start_2, end_2): is called repeatedly. An purely algebraic implementation is simple, but somewhat cumbersome. + Note that, oposed to other functions related to grid generation such as + remove_edge_crossings, this function does not use the concept of + snap_to_grid. This may cause problems at some point, although no issues + have been discovered so far. + Example: >>> lines_intersect([0, 0], [1, 1], [0, 1], [1, 0]) - array([0.5, 0.5]) + array([[ 0.5], + [ 0.5]]) >>> lines_intersect([0, 0], [1, 0], [0, 1], [1, 1]) - None Parameters: - start_1 (np.ndarray or list): coordinates of start point for first - line. + start_1 (np.ndarray or list): coordinates of start point for first + line. end_1 (np.ndarray or list): coordinates of end point for first line. - start_2 (np.ndarray or list): coordinates of start point for first - line. + start_2 (np.ndarray or list): coordinates of start point for first + line. end_2 (np.ndarray or list): coordinates of end point for first line. Returns: np.ndarray: coordinates of intersection point, or None if the lines do - not intersect. + not intersect. """ - + # It seems that if sympy is provided point coordinates as integers, it may # do calculations in integers also, with an unknown approach to rounding. @@ -273,15 +284,15 @@ def remove_edge_crossings(vertices, edges, box=None, precision=1e-3): Parameters: vertices (np.ndarray, 2 x n_pt): Coordinates of points to be processed edges (np.ndarray, n x n_con): Connections between lines. n >= 2, row - 0 and 1 are index of start and endpoints, additional rows are tags + 0 and 1 are index of start and endpoints, additional rows are tags box (np.ndarray, nd): Size of domain, passed to snap_to_grid, see that - function for comments. + function for comments. precission (double): Resolution of underlying Cartesian grid, see - snap_to_grid for details. + snap_to_grid for details. Returns: np.ndarray, (2 x n_pt), array of points, possibly expanded. - np.ndarray, (n x n_edges), array of new edges. Non-intersecting. + np.ndarray, (n x n_edges), array of new edges. Non-intersecting. Raises: NotImplementedError if a 3D point array is provided. From 2771df675a7267a1298fcc256cd4098c80002f4b Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Mon, 2 Jan 2017 09:51:10 +0100 Subject: [PATCH 043/153] Wrapper to run doctests as a unit test --- tests/test_doctests.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 tests/test_doctests.py diff --git a/tests/test_doctests.py b/tests/test_doctests.py new file mode 100644 index 0000000000..e84af3db3c --- /dev/null +++ b/tests/test_doctests.py @@ -0,0 +1,8 @@ +import unittest +import doctest +from compgeom import basics + +test_suite = unittest.TestSuite() +test_suite.addTest(doctest.DocTestSuite(basics)) + +unittest.TextTestRunner().run(test_suite) From 8ffea3547cd3f5f267fbfa63c78e0a26b15c3686 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Mon, 2 Jan 2017 09:52:44 +0100 Subject: [PATCH 044/153] Some comments in unit test of intersection finder --- tests/test_intersections.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_intersections.py b/tests/test_intersections.py index 1d076b2af5..447e67c6c0 100644 --- a/tests/test_intersections.py +++ b/tests/test_intersections.py @@ -5,7 +5,14 @@ class SplitIntersectingLines2DTest(unittest.TestCase): + """ + Various tests of remove_edge_crossings. + Note that since this function in itself uses several subfunctions, this is + somewhat against the spirit of unit testing. The subfunctions are also + fairly well covered by unit tests, in the form of doctests. + + """ def test_lines_crossing_origin(self): p = np.array([[-1, 1, 0, 0], [0, 0, -1, 1]]) From 24a97794bd99bf159c878511e53d9afba84cff1f Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Mon, 2 Jan 2017 10:00:27 +0100 Subject: [PATCH 045/153] Removed blanks in test of intersections --- tests/test_intersections.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_intersections.py b/tests/test_intersections.py index 447e67c6c0..55b849e8ac 100644 --- a/tests/test_intersections.py +++ b/tests/test_intersections.py @@ -45,7 +45,7 @@ def test_lines_no_crossing(self): new_pts, new_lines = basics.remove_edge_crossings(p, lines, box) assert np.allclose(new_pts, p) assert np.allclose(new_lines, lines) - + def test_three_lines_no_crossing(self): # This test gave an error at some point p = np.array([[ 0., 0., 0.3, 1., 1., 0.5], @@ -57,7 +57,7 @@ def test_three_lines_no_crossing(self): p_known = basics.snap_to_grid(p, box) assert np.allclose(new_pts, p_known) assert np.allclose(new_lines, lines) - + def test_three_lines_one_crossing(self): # This test gave an error at some point p = np.array([[ 0., 0.5, 0.3, 1., 0.3, 0.5], @@ -71,9 +71,9 @@ def test_three_lines_one_crossing(self): lines_known = np.array([[0, 3], [2, 6], [6, 5], [1, 6], [6, 4]]).T assert np.allclose(new_pts, p_known) assert np.allclose(new_lines, lines_known) - - + + if __name__ == '__main__': unittest.main() @@ -95,10 +95,10 @@ def test_aniso_snapping(self): if __name__ == '__main__': unittest.main() - + class LinesIntersectTest(unittest.TestCase): - + def test_lines_intersect_segments_do_not(self): s0 = np.array([0.3, 0.3]) e0 = np.array([0.5, 0.5]) @@ -106,6 +106,6 @@ def test_lines_intersect_segments_do_not(self): e1 = np.array([1, 2/3]) pi = basics.lines_intersect(s0, e0, s1, e1) assert(pi is None or len(pi)==0) - + if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() From 5b9dfc84a2fc62dbac57ca68102ab3b461f6dc9a Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Mon, 2 Jan 2017 10:00:56 +0100 Subject: [PATCH 046/153] Gitignore .swp --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0d20b6487c..c9b568f7ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ *.pyc +*.swp From f0429755ed33124507f5e6b8a46393f86716cd19 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Mon, 2 Jan 2017 10:01:11 +0100 Subject: [PATCH 047/153] Unit test for sorting of points --- tests/test_sort_points.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tests/test_sort_points.py diff --git a/tests/test_sort_points.py b/tests/test_sort_points.py new file mode 100644 index 0000000000..735dc6d1d3 --- /dev/null +++ b/tests/test_sort_points.py @@ -0,0 +1,17 @@ +import unittest +import numpy as np + +from compgeom import sort_points + +class SortLinePairTest(unittest.TestCase): + + def test_quad(self): + p = np.array([[1, 2], [5, 1], [2, 7], [7, 5]]).T + sp = sort_points.sort_point_pairs(p) + # Use numpy arrays to ease comparison of points + truth = np.array([[1, 2], [2, 7], [7, 5], [5, 1]]).T + assert np.allclose(truth, sp) + + if __name__ == '__main__': + unittest.main() + From eef56ad916dea712d818ccae892a921fa5ee6ad9 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Mon, 2 Jan 2017 11:38:15 +0100 Subject: [PATCH 048/153] Introduced **kwargs in methods to detect line intersections. The previous code passed arguments from one function to the next, leading to messy code. --- basics.py | 42 +++++++++++++++++-------------------- tests/test_intersections.py | 18 ++++++++-------- 2 files changed, 28 insertions(+), 32 deletions(-) diff --git a/basics.py b/basics.py index c97c81aaed..463ceb2f41 100644 --- a/basics.py +++ b/basics.py @@ -13,7 +13,7 @@ #------------------------------------------------------------------------------# -def snap_to_grid(pts, box=None, precision=1e-3): +def snap_to_grid(pts, precision=1e-3, box=None): """ Snap points to an underlying Cartesian grid. Used e.g. for avoiding rounding issues when testing for equality between @@ -86,7 +86,7 @@ def __points_equal(p1, p2, box, precesion=1e-3): #------------------------------------------------------------------------------# -def split_edge(vertices, edges, edge_ind, new_pt, box, precision): +def split_edge(vertices, edges, edge_ind, new_pt, **kwargs): """ Split a line into two by introcuding a new point. Function is used e.g. for gridding purposes. @@ -103,7 +103,7 @@ def split_edge(vertices, edges, edge_ind, new_pt, box, precision): >>> p = np.array([[0, 0], [0, 1]]) >>> edges = np.array([[0], [1]]) >>> new_pt = np.array([[0], [0.5]]) - >>> v, e, nl = split_edge(p, edges, 0, new_pt, [[1], [1]], 1e-3) + >>> v, e, nl = split_edge(p, edges, 0, new_pt) >>> e array([[0, 2], [2, 1]]) @@ -116,10 +116,7 @@ def split_edge(vertices, edges, edge_ind, new_pt, box, precision): edge_ind (int): index of edge to be split, refering to edges. new_pt (np.ndarray, nd x 1): new point to be inserted. Assumed to be on the edge to be split. - box (np.ndarray, nd x 1): bounding box of the domain, see snap_to_grid - for usage. - precission (double): precision of underlying grid. See snap_to_grid - for usage. + **kwargs: Arguments passed to snap_to_grid Returns: np.ndarray, nd x n_pt: new point set, possibly with new point inserted. @@ -133,7 +130,7 @@ def split_edge(vertices, edges, edge_ind, new_pt, box, precision): tags = edges[2:, edge_ind] # Add a new point - vertices, pt_ind, _ = add_point(vertices, new_pt, box, precision) + vertices, pt_ind, _ = add_point(vertices, new_pt, **kwargs) # If the new point coincide with the start point, nothing happens if start == pt_ind or end == pt_ind: new_line = False @@ -156,7 +153,7 @@ def split_edge(vertices, edges, edge_ind, new_pt, box, precision): #------------------------------------------------------------------------------# -def add_point(vertices, pt, box=None, precision=1e-3): +def add_point(vertices, pt, precision=1e-3, **kwargs): """ Add a point to a point set, unless the point already exist in the set. @@ -169,8 +166,8 @@ def add_point(vertices, pt, box=None, precision=1e-3): Parameters: vertices (np.ndarray, nd x num_pts): existing point set pt (np.ndarray, nd x 1): Point to be added - box, precision: Parameters bassed to snap_to_grid, see that function - for details. + precesion (double): Precision of underlying Cartesian grid + **kwargs: Arguments passed to snap_to_grid Returns: np.ndarray, nd x n_pt: New point set, possibly containing a new point @@ -180,12 +177,14 @@ def add_point(vertices, pt, box=None, precision=1e-3): np.ndarray, nd x 1: The new point, or None if no new point was needed. """ + if not 'precision' in kwargs: + kwargs['precision'] = precision nd = vertices.shape[0] # Before comparing coordinates, snap both existing and new point to the # underlying grid - vertices = snap_to_grid(vertices, box, precision) - pt = snap_to_grid(pt, box, precision) + vertices = snap_to_grid(vertices, **kwargs) + pt = snap_to_grid(pt, **kwargs) # Distance dist = __dist(pt, vertices) @@ -268,7 +267,7 @@ def lines_intersect(start_1, end_1, start_2, end_2): #------------------------------------------------------------------------------# -def remove_edge_crossings(vertices, edges, box=None, precision=1e-3): +def remove_edge_crossings(vertices, edges, **kwargs): """ Process a set of points and connections between them so that the result is an extended point set and new connections that do not intersect. @@ -285,10 +284,7 @@ def remove_edge_crossings(vertices, edges, box=None, precision=1e-3): vertices (np.ndarray, 2 x n_pt): Coordinates of points to be processed edges (np.ndarray, n x n_con): Connections between lines. n >= 2, row 0 and 1 are index of start and endpoints, additional rows are tags - box (np.ndarray, nd): Size of domain, passed to snap_to_grid, see that - function for comments. - precission (double): Resolution of underlying Cartesian grid, see - snap_to_grid for details. + **kwargs: Arguments passed to snap_to_grid Returns: np.ndarray, (2 x n_pt), array of points, possibly expanded. @@ -308,7 +304,7 @@ def remove_edge_crossings(vertices, edges, box=None, precision=1e-3): edge_counter = 0 - vertices = snap_to_grid(vertices, box, precision) + vertices = snap_to_grid(vertices, **kwargs) # Loop over all edges, search for intersections. The number of edges can # change due to splitting. @@ -385,8 +381,8 @@ def remove_edge_crossings(vertices, edges, box=None, precision=1e-3): # is needed) vertices, edges, split_outer_edge = split_edge(vertices, edges, edge_counter, - new_pt, box, - precision) + new_pt, + **kwargs) # If the outer edge (represented by edge_counter) was split, # e.g. inserted into the list of edges we need to increase the # index of the inner edge @@ -394,8 +390,8 @@ def remove_edge_crossings(vertices, edges, box=None, precision=1e-3): # Possibly split the inner edge vertices, edges, split_inner_edge = split_edge(vertices, edges, intsect, - new_pt, box, - precision) + new_pt, + **kwargs) # Update index of possible intersections intersections += split_outer_edge + split_inner_edge diff --git a/tests/test_intersections.py b/tests/test_intersections.py index 55b849e8ac..c0eee65b9e 100644 --- a/tests/test_intersections.py +++ b/tests/test_intersections.py @@ -22,10 +22,10 @@ def test_lines_crossing_origin(self): [3, 4]]) box = np.array([[2], [2]]) - new_pts, new_lines = basics.remove_edge_crossings(p, lines, box) + new_pts, new_lines = basics.remove_edge_crossings(p, lines, box=box) p_known = np.hstack((p, np.array([[0], [0]]))) - p_known = basics.snap_to_grid(p_known, box) + p_known = basics.snap_to_grid(p_known, box=box) lines_known = np.array([[0, 4, 2, 4], [4, 1, 4, 3], @@ -42,7 +42,7 @@ def test_lines_no_crossing(self): lines = np.array([[0, 1], [2, 3]]) box = np.array([[2], [2]]) - new_pts, new_lines = basics.remove_edge_crossings(p, lines, box) + new_pts, new_lines = basics.remove_edge_crossings(p, lines, box=box) assert np.allclose(new_pts, p) assert np.allclose(new_lines, lines) @@ -53,8 +53,8 @@ def test_three_lines_no_crossing(self): lines = np.array([[0, 3], [1, 4], [2, 5]]).T box = np.array([[1], [2]]) - new_pts, new_lines = basics.remove_edge_crossings(p, lines, box) - p_known = basics.snap_to_grid(p, box) + new_pts, new_lines = basics.remove_edge_crossings(p, lines, box=box) + p_known = basics.snap_to_grid(p, box=box) assert np.allclose(new_pts, p_known) assert np.allclose(new_lines, lines) @@ -65,9 +65,9 @@ def test_three_lines_one_crossing(self): lines = np.array([[0, 3], [2, 5], [1, 4]]).T box = np.array([[1], [2]]) - new_pts, new_lines = basics.remove_edge_crossings(p, lines, box) + new_pts, new_lines = basics.remove_edge_crossings(p, lines, box=box) p_known = np.hstack((p, np.array([[0.4], [0.4]]))) - p_known = basics.snap_to_grid(p_known, box) + p_known = basics.snap_to_grid(p_known, box=box) lines_known = np.array([[0, 3], [2, 6], [6, 5], [1, 6], [6, 4]]).T assert np.allclose(new_pts, p_known) assert np.allclose(new_lines, lines_known) @@ -86,11 +86,11 @@ def setUp(self): self.p = np.array([0.6, 0.6]) def test_snapping(self): - p_snapped = basics.snap_to_grid(self.p, self.box, precision=1) + p_snapped = basics.snap_to_grid(self.p, box=self.box, precision=1) assert np.allclose(p_snapped, np.array([1, 1])) def test_aniso_snapping(self): - p_snapped = basics.snap_to_grid(self.p, self.anisobox, precision=1) + p_snapped = basics.snap_to_grid(self.p, box=self.anisobox, precision=1) assert np.allclose(p_snapped, np.array([0, 1])) if __name__ == '__main__': From 62348a1f0142ad909af5cf297969deea51b12a50 Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Thu, 5 Jan 2017 11:31:00 +0100 Subject: [PATCH 049/153] add a function to check if a set of points lie on a plane or not --- basics.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/basics.py b/basics.py index 463ceb2f41..5b9bd3c555 100644 --- a/basics.py +++ b/basics.py @@ -404,6 +404,30 @@ def remove_edge_crossings(vertices, edges, **kwargs): #------------------------------------------------------------------------------# +def is_planar( pts, normal = None ): + """ Check if the points lie on a plane. + + Parameters: + pts (np.ndarray, 3xn): the points. + normal: (optional) the normal of the plane, otherwise three points are + required. + + Returns: + check, bool, if the points lie on a plane or not. + + """ + + if normal is None: + normal = compute_normal( pts ) + else: + normal = normal / np.linalg.norm( normal ) + + check = np.array( [ np.isclose( np.dot( normal, pts[:,0] - p ), 0. ) \ + for p in pts[:,1:].T ], dtype=np.bool ) + return np.all( check ) + +#------------------------------------------------------------------------------# + def project_plane_matrix( pts, normal = None ): """ Project the points on a plane using local coordinates. From a5f3bbd97c3a78e490130cde6c76e6b317669132 Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Thu, 5 Jan 2017 11:33:45 +0100 Subject: [PATCH 050/153] update the spacing in the comments --- basics.py | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/basics.py b/basics.py index 5b9bd3c555..e040901880 100644 --- a/basics.py +++ b/basics.py @@ -23,7 +23,7 @@ def snap_to_grid(pts, precision=1e-3, box=None): >>> snap_to_grid([[0.2443], [0.501]]) array([[ 0.244], - [ 0.501]]) + [ 0.501]]) >>> snap_to_grid([[0.2443], [0.501]], box=[[10], [1]]) array([[ 0.24 ], @@ -186,12 +186,12 @@ def add_point(vertices, pt, precision=1e-3, **kwargs): vertices = snap_to_grid(vertices, **kwargs) pt = snap_to_grid(pt, **kwargs) - # Distance + # Distance dist = __dist(pt, vertices) min_dist = np.min(dist) if min_dist < precision * np.sqrt(nd): - # No new point is needed + # No new point is needed ind = np.argmin(dist) new_point = None return vertices, ind, new_point @@ -281,17 +281,17 @@ def remove_edge_crossings(vertices, edges, **kwargs): have tags assigned. If so, the tags are preserved as connections are split. Parameters: - vertices (np.ndarray, 2 x n_pt): Coordinates of points to be processed - edges (np.ndarray, n x n_con): Connections between lines. n >= 2, row + vertices (np.ndarray, 2 x n_pt): Coordinates of points to be processed + edges (np.ndarray, n x n_con): Connections between lines. n >= 2, row 0 and 1 are index of start and endpoints, additional rows are tags **kwargs: Arguments passed to snap_to_grid Returns: - np.ndarray, (2 x n_pt), array of points, possibly expanded. - np.ndarray, (n x n_edges), array of new edges. Non-intersecting. + np.ndarray, (2 x n_pt), array of points, possibly expanded. + np.ndarray, (n x n_edges), array of new edges. Non-intersecting. Raises: - NotImplementedError if a 3D point array is provided. + NotImplementedError if a 3D point array is provided. """ num_edges = edges.shape[1] @@ -435,15 +435,16 @@ def project_plane_matrix( pts, normal = None ): example: np.array( [ np.dot( R, p ) for p in pts.T ] ).T Parameters: - pts (np.ndarray, 3xn): the points. - normal: (optional) the normal of the plane, otherwise three points are - required. + pts (np.ndarray, 3xn): the points. + normal: (optional) the normal of the plane, otherwise three points are + required. Returns: - np.ndarray, 3x3, projection matrix. + np.ndarray, 3x3, projection matrix. """ - if normal is None: + + if normal is None: normal = compute_normal( pts ) else: normal = normal / np.linalg.norm( normal ) @@ -460,14 +461,15 @@ def rot( a, vect ): form of Rodrigues formula. Parameters: - a: double, the angle. - vect: np.array, 3, the vector. + a: double, the angle. + vect: np.array, 1x3, the vector. Returns: - matrix: np.ndarray, 3x3, the rotation matrix. + matrix: np.ndarray, 3x3, the rotation matrix. """ - if np.allclose( vect, [0.,0.,0.] ): + + if np.allclose( vect, [0.,0.,0.] ): return np.identity(3) vect = vect / np.linalg.norm( vect ) W = np.array( [ [ 0., -vect[2], vect[1] ], @@ -482,13 +484,13 @@ def compute_normal( pts ): """ Compute the normal of a set of points. The algorithm assume that the points lie on a plane. - Three points are required. + Three non-aligned points are required. Parameters: - pts: np.ndarray, 3xn, the points. + pts: np.ndarray, 3xn, the points. Need n > 2. Returns: - normal: np.array, 1x3, the normal. + normal: np.array, 1x3, the normal. """ assert( pts.shape[1] > 2 ) From 8bd2357b53c88c545ec5e3579d7b42ddae849c53 Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Thu, 5 Jan 2017 11:33:59 +0100 Subject: [PATCH 051/153] add an assert to check if the normal is not null --- basics.py | 1 + 1 file changed, 1 insertion(+) diff --git a/basics.py b/basics.py index e040901880..cc50089fac 100644 --- a/basics.py +++ b/basics.py @@ -495,6 +495,7 @@ def compute_normal( pts ): """ assert( pts.shape[1] > 2 ) normal = np.cross( pts[:,0] - pts[:,1], pts[:,0] - np.mean( pts, axis = 1 ) ) + assert( not np.allclose( normal, np.zeros(3) ) ) return normal / np.linalg.norm( normal ) #------------------------------------------------------------------------------# From 6295711c9667329fef2dd3919838fef4dd4f5125 Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Thu, 5 Jan 2017 11:34:24 +0100 Subject: [PATCH 052/153] add two additional tests for the is_planar test --- tests/test_basics.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_basics.py b/tests/test_basics.py index 98099ea902..e7bc04df9f 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -41,6 +41,22 @@ def test_compute_normal_3d( self ): assert np.allclose( normal, normal_test ) or \ np.allclose( normal, -1. * normal_test ) +#------------------------------------------------------------------------------# + + def test_is_planar_2d( self ): + pts = np.array( [ [ 0., 2., -1. ], + [ 0., 4., 2. ], + [ 2., 2., 2. ] ] ) + assert basics.is_planar( pts ) + +#------------------------------------------------------------------------------# + + def test_is_planar_3d( self ): + pts = np.array( [ [ 0., 1., 0., 4./7. ], + [ 0., 1., 1., 0. ], + [ 5./8., 7./8., 7./4., 1./8. ] ] ) + assert basics.is_planar( pts ) + #------------------------------------------------------------------------------# def test_project_plane( self ): From 9b25770cb6786f7b930cdfdbf1e1982dbd6793be Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Thu, 26 Jan 2017 18:54:08 +0100 Subject: [PATCH 053/153] improve the documentation for a function --- basics.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/basics.py b/basics.py index cc50089fac..e62d49b60c 100644 --- a/basics.py +++ b/basics.py @@ -432,8 +432,8 @@ def project_plane_matrix( pts, normal = None ): """ Project the points on a plane using local coordinates. The projected points are computed by a dot product. - example: np.array( [ np.dot( R, p ) for p in pts.T ] ).T - + example: np.dot( R, pts ) + Parameters: pts (np.ndarray, 3xn): the points. normal: (optional) the normal of the plane, otherwise three points are @@ -471,7 +471,7 @@ def rot( a, vect ): if np.allclose( vect, [0.,0.,0.] ): return np.identity(3) - vect = vect / np.linalg.norm( vect ) + vect = vect / np.linalg.norm(vect) W = np.array( [ [ 0., -vect[2], vect[1] ], [ vect[2], 0., -vect[0] ], [ -vect[1], vect[0], 0. ] ] ) From b12ec2bbebb89f6c92e334ded35e2932d63f30e6 Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Thu, 26 Jan 2017 18:54:47 +0100 Subject: [PATCH 054/153] add a function to compute a tangent vector from a set of co-planar points --- basics.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/basics.py b/basics.py index e62d49b60c..1fa20faee5 100644 --- a/basics.py +++ b/basics.py @@ -499,3 +499,22 @@ def compute_normal( pts ): return normal / np.linalg.norm( normal ) #------------------------------------------------------------------------------# + +def compute_tangent(pts): + """ Compute a tangent of a set of points. + + The algorithm assume that the points lie on a plane. + + Parameters: + pts: np.ndarray, 3xn, the points. + + Returns: + tangent: np.array, 1x3, the tangent. + + """ + + tangent = pts[:,0] - np.mean( pts, axis = 1 ) + assert not np.allclose( tangent, np.zeros(3) ) + return tangent / np.linalg.norm(tangent) + +#------------------------------------------------------------------------------# From 69d91a8b64fdd47ce1799b8772d6b42e05de076e Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Thu, 26 Jan 2017 18:55:14 +0100 Subject: [PATCH 055/153] use the tangent functionality in the computation of the normal vector --- basics.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/basics.py b/basics.py index 1fa20faee5..9d60333f54 100644 --- a/basics.py +++ b/basics.py @@ -480,7 +480,7 @@ def rot( a, vect ): #------------------------------------------------------------------------------# -def compute_normal( pts ): +def compute_normal(pts): """ Compute the normal of a set of points. The algorithm assume that the points lie on a plane. @@ -493,9 +493,10 @@ def compute_normal( pts ): normal: np.array, 1x3, the normal. """ - assert( pts.shape[1] > 2 ) - normal = np.cross( pts[:,0] - pts[:,1], pts[:,0] - np.mean( pts, axis = 1 ) ) - assert( not np.allclose( normal, np.zeros(3) ) ) + + assert pts.shape[1] > 2 + normal = np.cross( pts[:,0] - pts[:,1], compute_tangent(pts) ) + assert not np.allclose( normal, np.zeros(3) ) return normal / np.linalg.norm( normal ) #------------------------------------------------------------------------------# From 03e6933eb7e90a249cc72f3e1870b8404a5485fc Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Thu, 26 Jan 2017 18:56:08 +0100 Subject: [PATCH 056/153] small change in the test, without change the numbers involved --- tests/test_basics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_basics.py b/tests/test_basics.py index e7bc04df9f..65ffc7847e 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -64,7 +64,7 @@ def test_project_plane( self ): [ 1., -2., -1., 1. ], [ -1., 0., 2., -8. ] ] ) R = basics.project_plane_matrix( pts ) - P_pts = np.array( [ np.dot( R, p ) for p in pts.T ] ).T + P_pts = np.dot( R, pts ) assert np.allclose( P_pts[2,:], 1.15470054 * np.ones(4) ) From 316484b880c94f0ce034c014382587c489dcb9f9 Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Fri, 27 Jan 2017 14:40:38 +0100 Subject: [PATCH 057/153] add a test to validate if a set of points is collinear --- basics.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/basics.py b/basics.py index 9d60333f54..a9cfadfdb5 100644 --- a/basics.py +++ b/basics.py @@ -519,3 +519,26 @@ def compute_tangent(pts): return tangent / np.linalg.norm(tangent) #------------------------------------------------------------------------------# + +def is_collinear(pts): + """ Check if the points lie on a line. + + Parameters: + pts (np.ndarray, 3xn): the points. + + Returns: + check, bool, if the points lie on a line or not. + + """ + + assert pts.shape[1] > 1 + if pts.shape[1] == 2: return True + + pt0 = pts[:,0] + pt1 = pts[:,1] + + coll = np.array( [ np.linalg.norm( np.cross( p - pt0, pt1 - pt0 ) ) \ + for p in pts[:,1:-1].T ] ) + return np.allclose(coll, np.zeros(coll.size)) + +#------------------------------------------------------------------------------# From c6701a322852222d6b4e2d8bfe2f4c57e388dd60 Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Fri, 27 Jan 2017 14:42:05 +0100 Subject: [PATCH 058/153] Remove gitignore --- .gitignore | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .gitignore diff --git a/.gitignore b/.gitignore deleted file mode 100644 index c9b568f7ea..0000000000 --- a/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.pyc -*.swp From 8caaa0a94f1073a9718b530de417c6949965833b Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Fri, 27 Jan 2017 14:43:06 +0100 Subject: [PATCH 059/153] add gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..74f6527809 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.pyc +*~ +*.py~ +*.swp From fdc5f4506ad6dbc84d6a2e454d4efabc2facbf21 Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Mon, 30 Jan 2017 13:09:35 +0100 Subject: [PATCH 060/153] add a function to compute a pair of normals for a 1d grid --- basics.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/basics.py b/basics.py index a9cfadfdb5..69137b8830 100644 --- a/basics.py +++ b/basics.py @@ -501,6 +501,13 @@ def compute_normal(pts): #------------------------------------------------------------------------------# +def compute_normals_1d(pts): + t = compute_tangent(pts) + n = np.array([t[1], -t[0], 0]) / np.sqrt(t[0]**2+t[1]**2) + return np.r_['1,2,0', n, np.dot(rot(np.pi/2., t), n)] + +#------------------------------------------------------------------------------# + def compute_tangent(pts): """ Compute a tangent of a set of points. From 8a291e1115bc3dd0f89285e63bd2767d3446727d Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Mon, 30 Jan 2017 13:10:11 +0100 Subject: [PATCH 061/153] add a function to map a 2d or 1d grid from an embedded framework to a 2d, respectively 1d, framework --- basics.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/basics.py b/basics.py index 69137b8830..7018b79cb3 100644 --- a/basics.py +++ b/basics.py @@ -549,3 +549,38 @@ def is_collinear(pts): return np.allclose(coll, np.zeros(coll.size)) #------------------------------------------------------------------------------# + +def map_grid(g): + """ If a 2d or a 1d grid is passed, the function return the cell_centers, + face_normals, and face_centers using local coordinates. If a 3d grid is + passed nothing is applied. The return vectors have a reduced number of rows. + + Parameters: + g (grid): the grid. + + Returns: + cell_centers: (g.dim x g.num_cells) the mapped centers of the cells. + face_normals: (g.dim x g.num_faces) the mapped normals of the faces. + face_centers: (g.dim x g.num_faces) the mapped centers of the faces. + R: (3 x 3) the rotation matrix used. + + """ + + cell_centers = g.cell_centers + face_normals = g.face_normals + face_centers = g.face_centers + R = np.eye(3) + + if g.dim != 3: + v = compute_normal(g.nodes) if g.dim==2 else compute_tangent(g.nodes) + R = project_plane_matrix(g.nodes, v) + face_centers = np.dot(R, face_centers) + dim = np.logical_not(np.isclose(np.sum(np.abs(face_centers),axis=1),0)) + assert g.dim == np.sum(dim) + face_centers = face_centers[dim,:] + cell_centers = np.dot(R, cell_centers)[dim,:] + face_normals = np.dot(R, face_normals)[dim,:] + + return cell_centers, face_normals, face_centers, R + +#------------------------------------------------------------------------------# From ee5dbc5581992739e0bf1e550b686e4a8ce7c18b Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Tue, 31 Jan 2017 14:16:25 +0100 Subject: [PATCH 062/153] Function to compute distance between line segments. --- basics.py | 98 +++++++++++++++++++++++++++++++++ tests/test_distance_segments.py | 68 +++++++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 tests/test_distance_segments.py diff --git a/basics.py b/basics.py index 7018b79cb3..1cb3ce1da5 100644 --- a/basics.py +++ b/basics.py @@ -584,3 +584,101 @@ def map_grid(g): return cell_centers, face_normals, face_centers, R #------------------------------------------------------------------------------# + +def distance_segment_segment(s1_start, s1_end, s2_start, s2_end): + """ + Compute the distance between two line segments. + + The implementaion is based on http://geomalgorithms.com/a07-_distance.html + (C++ code can be found somewhere on the page). Also confer that page for + explanation of the algorithm. + + Implementation note: + It should be possible to rewrite the algorithm to allow for one of (or + both?) segments to be a set of segments, thus exploiting + vectorization. + + Parameters: + s1_start (np.array, size nd): Start point for the first segment + s1_end (np.array, size nd): End point for the first segment + s2_start (np.array, size nd): Start point for the second segment + s2_end (np.array, size nd): End point for the second segment + + Returns: + double: Minimum distance between the segments + + """ + + # Variable used to fine almost parallel lines. Sensitivity to this value has not been tested. + SMALL_TOLERANCE = 1e-6 + + # For the rest of the algorithm, see the webpage referred to above for details. + d1 = s1_end - s1_start + d2 = s2_end - s2_start + d_starts = s1_start - s2_start + + dot_1_1 = d1.dot(d1) + dot_1_2 = d1.dot(d2) + dot_2_2 = d2.dot(d2) + dot_1_starts = d1.dot(d_starts) + dot_2_starts = d2.dot(d_starts) + discr = dot_1_1 * dot_2_2 - dot_1_2 ** 2 + # Sanity check + assert discr >= 0 + + sc = sN = sD = discr + tc = tN = tD = discr + + if discr < SMALL_TOLERANCE: + sN = 0 + sD = 1 + tN = dot_2_starts + tD = dot_2_2 + else: + sN = dot_1_2 * dot_2_starts - dot_2_2 * dot_1_starts + tN = dot_1_1 * dot_2_starts - dot_1_2 * dot_1_starts + if sN < 0.0: # sc < 0 => the s=0 edge is visible + sN = 0.0 + tN = dot_2_starts + tD = dot_2_2 + + elif sN > sD: # sc > 1 => the s=1 edge is visible + sN = sD + tN = dot_1_2 + dot_2_starts + tD = dot_2_2 + + if tN < 0.0: # tc < 0 => the t=0 edge is visible + tN = 0.0 + # recompute sc for this edge + if -dot_1_starts < 0.0: + sN = 0.0 + elif (-dot_1_starts > dot_1_1): + sN = sD + else: + sN = -dot_1_starts + sD = dot_1_1 + elif tN > tD: # tc > 1 => the t=1 edge is visible + tN = tD + # recompute sc for this edge + if (-dot_1_starts + dot_1_2) < 0.0: + sN = 0 + elif (-dot_1_starts+ dot_1_2) > dot_1_1: + sN = sD + else: + sN = (-dot_1_starts + dot_1_2) + sD = dot_1_1 + + # finally do the division to get sc and tc + if abs(sN) < SMALL_TOLERANCE: + sc = 0.0 + else: + sc = sN / sD + if abs(tN) < SMALL_TOLERANCE: + tc = 0.0 + else: + tc = tN / tD + + # get the difference of the two closest points + dist = d_starts + sc * d1 - tc * d2 + return np.sqrt(dist.dot(dist)) + diff --git a/tests/test_distance_segments.py b/tests/test_distance_segments.py new file mode 100644 index 0000000000..bd5fe8b25d --- /dev/null +++ b/tests/test_distance_segments.py @@ -0,0 +1,68 @@ +import unittest +import numpy as np + +from compgeom import basics + +class TestSegmentDistance(unittest.TestCase): + def setup_2d_unit_square(self): + p00 = np.array([0, 0]) + p10 = np.array([1, 0]) + p01 = np.array([0, 1]) + p11 = np.array([1, 1]) + return p00, p10, p11, p01 + + def test_segment_no_intersect_2d(self): + p00, p10, p11, p01 = self.setup_2d_unit_square() + d = basics.distance_segment_segment(p00, p01, p11, p10) + assert d == 1 + + def test_segment_intersect_2d(self): + p00, p10, p11, p01 = self.setup_2d_unit_square() + d = basics.distance_segment_segment(p00, p11, p10, p01) + assert d == 0 + + def test_line_passing(self): + # Lines not crossing + p1 = np.array([0, 0]) + p2 = np.array([1, 0]) + p3 = np.array([2, -1]) + p4 = np.array([2, 1]) + d = basics.distance_segment_segment(p1, p2, p3, p4) + assert d == 1 + + def test_share_point(self): + # Two lines share a point + p1 = np.array([0, 0]) + p2 = np.array([0, 1]) + p3 = np.array([1, 1]) + d = basics.distance_segment_segment(p1, p2, p2, p3) + assert d == 0 + + def test_intersection_3d(self): + p000 = np.array([0, 0, 0]) + p111 = np.array([1, 1, 1]) + p100 = np.array([1, 0, 0]) + p011 = np.array([0, 1, 1]) + d = basics.distance_segment_segment(p000, p111, p100, p011) + assert d == 0 + + def test_changed_order_3d(self): + # The order of the start and endpoints of the segments should not matter + dim = 3 + p1 = np.random.rand(1, 3)[0] + p2 = np.random.rand(1, 3)[0] + p3 = np.random.rand(1, 3)[0] + p4 = np.random.rand(1, 3)[0] + d1 = basics.distance_segment_segment(p1, p2, p3, p4) + d2 = basics.distance_segment_segment(p2, p1, p3, p4) + d3 = basics.distance_segment_segment(p1, p2, p4, p3) + d4 = basics.distance_segment_segment(p2, p1, p4, p3) + d5 = basics.distance_segment_segment(p4, p3, p2, p1) + assert np.allclose(d1, d2) + assert np.allclose(d1, d3) + assert np.allclose(d1, d4) + assert np.allclose(d1, d5) + + if __name__ == '__main__': + unittest.main() + From 4a392d7997cdedc63ab24c00f787ee326c1f011f Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Fri, 3 Feb 2017 12:26:26 +0100 Subject: [PATCH 063/153] add a new output to sort_point_pairs to get the order of the origin line list, store if they're flip or not to form the chain --- sort_points.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/sort_points.py b/sort_points.py index 537e05f558..7401a9d7e0 100644 --- a/sort_points.py +++ b/sort_points.py @@ -3,7 +3,7 @@ #------------------------------------------------------------------------------# -def sort_point_pairs(lines, check_circular=True): +def sort_point_pairs(lines, check_circular=True, ordering=False): """ Sort pairs of numbers to form a chain. The target application is to sort lines, defined by their @@ -16,6 +16,7 @@ def sort_point_pairs(lines, check_circular=True): lines: np.ndarray, 2xn, the line pairs. check_circular: Verify that the sorted polyline form a circle. Defaluts to true. + ordering: np.array, return in the original order if a line is flipped or not Returns: sorted_lines: np.ndarray, 2xn, sorted line pairs. @@ -35,6 +36,10 @@ def sort_point_pairs(lines, check_circular=True): found = np.zeros(num_lines, dtype=np.bool) found[0] = True + # Order of the origin line list, store if they're flip or not to form the chain + is_ordered = np.zeros(num_lines, dtype=np.bool) + is_ordered[0] = True + # The sorting algorithm: Loop over all places in sorted_line to be filled, # for each of these, loop over all members in lines, check if the line is still # a candidate, and if one of its points equals the current starting point. @@ -46,6 +51,7 @@ def sort_point_pairs(lines, check_circular=True): sorted_lines[:, i] = lines[:, j] found[j] = True prev = lines[1, j] + is_ordered[j] = True break elif not found[j] and lines[1, j] == prev: sorted_lines[:, i] = lines[::-1, j] @@ -56,7 +62,8 @@ def sort_point_pairs(lines, check_circular=True): assert(np.all(found)) if check_circular: assert sorted_lines[0, 0] == sorted_lines[1, -1] - return sorted_lines + if ordering: return sorted_lines, is_ordered + else: return sorted_lines #------------------------------------------------------------------------------# From 9af31946fae847d062ce3815c91c3f0ef28f0df7 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Fri, 3 Feb 2017 17:06:56 +0100 Subject: [PATCH 064/153] Function to compute line segment intersections in 3D. --- basics.py | 97 +++++++++++++++++++++++++++ tests/test_segment_intersection_3d.py | 76 +++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 tests/test_segment_intersection_3d.py diff --git a/basics.py b/basics.py index 1cb3ce1da5..b334da6ac0 100644 --- a/basics.py +++ b/basics.py @@ -220,6 +220,9 @@ def lines_intersect(start_1, end_1, start_2, end_2): snap_to_grid. This may cause problems at some point, although no issues have been discovered so far. + Implementation note: + This function can be replaced by a call to segments_intersect_3d. Todo. + Example: >>> lines_intersect([0, 0], [1, 1], [0, 1], [1, 0]) array([[ 0.5], @@ -238,6 +241,7 @@ def lines_intersect(start_1, end_1, start_2, end_2): Returns: np.ndarray: coordinates of intersection point, or None if the lines do not intersect. + """ @@ -267,6 +271,99 @@ def lines_intersect(start_1, end_1, start_2, end_2): #------------------------------------------------------------------------------# +def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): + """ + Check if two line segments defined in 3D intersect. + + Note that, oposed to other functions related to grid generation such as + remove_edge_crossings, this function does not use the concept of + snap_to_grid. This may cause problems at some point, although no issues + have been discovered so far. + + Parameters: + start_1 (np.ndarray or list): coordinates of start point for first + line. + end_1 (np.ndarray or list): coordinates of end point for first line. + start_2 (np.ndarray or list): coordinates of start point for first + line. + end_2 (np.ndarray or list): coordinates of end point for first line. + + Returns: + np.ndarray: coordinates of intersection point, or None if the lines do + not intersect. + + """ + + # Convert input to numpy if necessary + start_1 = np.asarray(start_1).astype(np.float) + end_1 = np.asarray(end_1).astype(np.float) + start_2 = np.asarray(start_2).astype(np.float) + end_2 = np.asarray(end_2).astype(np.float) + + # Short hand for component of start and end points, as well as vectors + # along lines. + xs_1 = start_1[0] + ys_1 = start_1[1] + zs_1 = start_1[2] + + xe_1 = end_1[0] + ye_1 = end_1[1] + ze_1 = end_1[2] + + dx_1 = xe_1 - xs_1 + dy_1 = ye_1 - ys_1 + dz_1 = ze_1 - zs_1 + + xs_2 = start_2[0] + ys_2 = start_2[1] + zs_2 = start_2[2] + + xe_2 = end_2[0] + ye_2 = end_2[1] + ze_2 = end_2[2] + + dx_2 = xe_2 - xs_2 + dy_2 = ye_2 - ys_2 + dz_2 = ze_2 - zs_2 + + + + # An intersection will be a solution of the linear system + # xs_1 + dx_1 * t_1 = xs_2 + dx_2 * t_2 (1) + # ys_1 + dy_1 * t_1 = ys_2 + dy_2 * t_2 (2) + # + # In addition, the solution should satisfy + # zs_1 + dz_1 * t_1 = zs_2 + dz_2 * t_2 (3) + # + # The intersection is on the line segments if 0 <= (t_1, t_2) <= 1 + + # Minus in front of _2 after reorganizing (1)-(2) + discr = dx_1 * (-dy_2) - dy_1 *(-dx_2) + + if np.abs(discr) < tol: + # If the lines are (almost) parallel, there is no intersection + return None + + # Solve 2x2 system by Cramer's rule + t_1 = ((xs_2 - xs_1) * (-dy_2) - (ys_2 - ys_1) * (-dx_2)) / discr + t_2 = (dx_1 * (ys_2 - ys_1) - dy_1 * (xs_2 - xs_1)) / discr + + # Check that we are on line segment + if t_1 < 0 or t_1 > 1 or t_2 < 0 or t_2 > 1: + return None + + # Compute the z-coordinates of the intersection points + z_1_isect = zs_1 + t_1 * dz_1 + z_2_isect = zs_2 + t_2 * dz_2 + + if np.abs(z_1_isect - z_2_isect) < tol: + return np.array([xs_1 + t_1 * dx_1, ys_1 + t_1 * dy_1, z_1_isect]) + else: + return None + + +#-----------------------------------------------------------------------------# + def remove_edge_crossings(vertices, edges, **kwargs): """ Process a set of points and connections between them so that the result diff --git a/tests/test_segment_intersection_3d.py b/tests/test_segment_intersection_3d.py new file mode 100644 index 0000000000..c284200709 --- /dev/null +++ b/tests/test_segment_intersection_3d.py @@ -0,0 +1,76 @@ +import numpy as np +import unittest + +from compgeom import basics + +class TestSegmentSegmentIntersection(unittest.TestCase): + + def test_intersection_origin(self): + # 3D lines cross in the origin + p_1 = np.array([0, -1, -1]) + p_2 = np.array([0, 1, 1]) + p_3 = np.array([-1, 0, 1]) + p_4 = np.array([1, 0, -1]) + + p_i = basics.segments_intersect_3d(p_1, p_2, p_3, p_4) + assert np.allclose(p_i, np.zeros(3)) + + def test_argument_order_arbitrary(self): + # Order of input arguments should be arbitrary + p_1 = np.array([0, -1, -1]) + p_2 = np.array([0, 1, 1]) + p_3 = np.array([-1, 0, 1]) + p_4 = np.array([1, 0, -1]) + + p_known = np.zeros(3) + + p_i_1 = basics.segments_intersect_3d(p_1, p_2, p_3, p_4) + p_i_2 = basics.segments_intersect_3d(p_2, p_1, p_3, p_4) + p_i_3 = basics.segments_intersect_3d(p_1, p_2, p_4, p_3) + p_i_4 = basics.segments_intersect_3d(p_2, p_1, p_4, p_3) + + assert np.allclose(p_i_1, p_known) + assert np.allclose(p_i_2, p_known) + assert np.allclose(p_i_3, p_known) + assert np.allclose(p_i_4, p_known) + + def test_pass_in_z_coord(self): + # The (x,y) coordinates gives intersection in origin, but z coordinates + # do not match + p_1 = np.array([-1, -1, -1]) + p_2 = np.array([1, 1, -1]) + p_3 = np.array([1, -1, 1]) + p_4 = np.array([-1, 1, 1]) + + p_i = basics.segments_intersect_3d(p_1, p_2, p_3, p_4) + assert p_i is None + + + def test_lines_cross_segments_not(self): + p_1 = np.array([-1, 0, -1]) + p_2 = np.array([0, 0, 0]) + p_3 = np.array([1, -1, 1]) + p_4 = np.array([1, 1, 1]) + + p_i = basics.segments_intersect_3d(p_1, p_2, p_3, p_4) + assert p_i is None + + def test_parallel_lines(self): + p_1 = np.zeros(3) + p_2 = np.array([1, 0, 0]) + p_3 = np.array([0, 1, 0]) + p_4 = np.array([1, 1, 0]) + + p_i = basics.segments_intersect_3d(p_1, p_2, p_3, p_4) + assert p_i is None + + def test_L_intersection(self): + p_1 = np.zeros(3) + p_2 = np.random.rand(3) + p_3 = np.random.rand(3) + + p_i = basics.segments_intersect_3d(p_1, p_2, p_2, p_3) + assert np.allclose(p_i, p_2) + + if __name__ == '__main__': + unittest.main() From 949a40306b7646c544b61bffff06a32711d4e795 Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Fri, 3 Feb 2017 18:15:40 +0100 Subject: [PATCH 065/153] Fix conflict with stash --- basics.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/basics.py b/basics.py index 1cb3ce1da5..aac50b1340 100644 --- a/basics.py +++ b/basics.py @@ -10,6 +10,7 @@ import numpy as np from math import sqrt import sympy +import scipy.optimize as opt #------------------------------------------------------------------------------# @@ -221,11 +222,12 @@ def lines_intersect(start_1, end_1, start_2, end_2): have been discovered so far. Example: - >>> lines_intersect([0, 0], [1, 1], [0, 1], [1, 0]) + >>> lines_intersect([0, 0, 0], [1, 1, 0], [0, 1, 0], [1, 0, 0]) array([[ 0.5], - [ 0.5]]) + [ 0.5], + [ 0]]) - >>> lines_intersect([0, 0], [1, 0], [0, 1], [1, 1]) + >>> lines_intersect([0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0]) Parameters: start_1 (np.ndarray or list): coordinates of start point for first From dabee0f9a755c4d2c34ef126d4b16091fc0d2adb Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Sun, 5 Feb 2017 20:02:23 +0100 Subject: [PATCH 066/153] Bugfix in intersection of line segments. Previous version failed to handle overlapping segments. --- basics.py | 84 ++++++++++++++++++++++---- tests/test_segment_intersection_3d.py | 87 +++++++++++++++++++++++++++ 2 files changed, 158 insertions(+), 13 deletions(-) diff --git a/basics.py b/basics.py index b334da6ac0..e851b46a2d 100644 --- a/basics.py +++ b/basics.py @@ -341,23 +341,81 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): discr = dx_1 * (-dy_2) - dy_1 *(-dx_2) if np.abs(discr) < tol: - # If the lines are (almost) parallel, there is no intersection - return None + # If the lines are (almost) parallel, there is no single intersection, + # but it may be a segment + + # First check if the third dimension is also parallel, if not, no + # intersection + + + # The lines are parallel in the x-y plane, but we don't know about the + # z-direction. CHeck this + deltas_1 = np.array([dx_1, dy_1, dz_1]) + deltas_2 = np.array([dx_2, dy_2, dz_2]) + + # Use masked arrays to avoid divisions by zero + mask_1 = np.ma.greater(np.abs(deltas_1), tol) + mask_2 = np.ma.greater(np.abs(deltas_2), tol) + + # A first, simple test + if np.any(mask_1 != mask_2): + return None + + t = deltas_1[mask_1] / deltas_2[mask_2] + + # Second, test for alignment in all directions + if not np.allclose(t, t.mean(), tol): + return None + + # If we have made it this far, the lines are indeed parallel. Next, + # check if they overlap, and if so, test if the segments are overlapping + + # For dimensions with an incline, the vector between segment start points should be parallel to the segments + # Since the masks are equal, we can use any of them + t_1_2 = (start_1[mask_1] - start_2[mask_1]) / deltas_1[mask_1] + if (not np.allclose(t_1_2, t_1_2, tol)): + return None + # For dimensions with no incline, the start cooordinates should be the same + if not np.allclose(start_1[~mask_1], start_2[~mask_1], tol): + return None + + # We have overlapping lines! finally check if segments are overlapping. + + # Since everything is parallel, it suffices to work with a single coordinate + s_1 = start_1[mask_1][0] + e_1 = end_1[mask_1][0] + s_2 = start_2[mask_1][0] + e_2 = end_2[mask_1][0] + + d = deltas_1[mask_1][0] + max_1 = max(s_1, e_1) + min_1 = min(s_1, e_1) + max_2 = max(s_2, e_2) + min_2 = min(s_2, e_2) + + # Rule out case with non-overlapping segments + if max_1 < min_2: + return None + elif max_2 < min_1: + return None + + + # The lines are overlapping, we need to find their common line + lines = np.array([s_1, e_1, s_2, e_2]) + sort_ind = np.argsort(lines) + + # The overlap will be between the middle two points in the sorted list + target = sort_ind[1:3] + + # Array of the full coordinates - same order as lines + lines_full = np.vstack((start_1, end_1, start_2, end_2)).transpose() + # Our segment consists of the second and third column. We're done! + return lines_full[:, target] + - # Solve 2x2 system by Cramer's rule - t_1 = ((xs_2 - xs_1) * (-dy_2) - (ys_2 - ys_1) * (-dx_2)) / discr - t_2 = (dx_1 * (ys_2 - ys_1) - dy_1 * (xs_2 - xs_1)) / discr - # Check that we are on line segment - if t_1 < 0 or t_1 > 1 or t_2 < 0 or t_2 > 1: - return None - # Compute the z-coordinates of the intersection points - z_1_isect = zs_1 + t_1 * dz_1 - z_2_isect = zs_2 + t_2 * dz_2 - if np.abs(z_1_isect - z_2_isect) < tol: - return np.array([xs_1 + t_1 * dx_1, ys_1 + t_1 * dy_1, z_1_isect]) else: return None diff --git a/tests/test_segment_intersection_3d.py b/tests/test_segment_intersection_3d.py index c284200709..6440eedf9d 100644 --- a/tests/test_segment_intersection_3d.py +++ b/tests/test_segment_intersection_3d.py @@ -64,6 +64,7 @@ def test_parallel_lines(self): p_i = basics.segments_intersect_3d(p_1, p_2, p_3, p_4) assert p_i is None + def test_L_intersection(self): p_1 = np.zeros(3) p_2 = np.random.rand(3) @@ -72,5 +73,91 @@ def test_L_intersection(self): p_i = basics.segments_intersect_3d(p_1, p_2, p_2, p_3) assert np.allclose(p_i, p_2) + def test_equal_lines_segments_not_overlapping(self): + p_1 = np.ones(3) + p_2 = 0 * p_1 + p_3 = 2 * p_1 + p_4 = 3 * p_1 + + p_int = basics.segments_intersect_3d(p_1, p_2, p_3, p_4) + assert p_int is None + + + def test_segment_fully_overlapped(self): + # One line is fully covered by another + p_1 = np.ones(3) + p_2 = 2 * p_1 + p_3 = 0 * p_1 + p_4 = 3 * p_1 + + p_int = basics.segments_intersect_3d(p_1, p_2, p_3, p_4) + p_known_1 = p_1.reshape((-1, 1)) + p_known_2 = p_2.reshape((-1, 1)) + assert np.min(np.sum(np.abs(p_int - p_known_1), axis=0)) < 1e-8 + assert np.min(np.sum(np.abs(p_int - p_known_2), axis=0)) < 1e-8 + + def test_segments_overlap_input_order(self): + # Test the order of inputs + p_1 = np.ones(3) + p_2 = 2 * p_1 + p_3 = 0 * p_1 + p_4 = 3 * p_1 + + p_int_1 = basics.segments_intersect_3d(p_1, p_2, p_3, p_4) + p_int_2 = basics.segments_intersect_3d(p_2, p_1, p_3, p_4) + p_int_3 = basics.segments_intersect_3d(p_1, p_2, p_4, p_3) + p_int_4 = basics.segments_intersect_3d(p_2, p_1, p_4, p_3) + + p_known_1 = p_1.reshape((-1, 1)) + p_known_2 = p_2.reshape((-1, 1)) + + assert np.min(np.sum(np.abs(p_int_1 - p_known_1), axis=0)) < 1e-8 + assert np.min(np.sum(np.abs(p_int_1 - p_known_2), axis=0)) < 1e-8 + assert np.min(np.sum(np.abs(p_int_2 - p_known_1), axis=0)) < 1e-8 + assert np.min(np.sum(np.abs(p_int_2 - p_known_2), axis=0)) < 1e-8 + assert np.min(np.sum(np.abs(p_int_3 - p_known_1), axis=0)) < 1e-8 + assert np.min(np.sum(np.abs(p_int_3 - p_known_2), axis=0)) < 1e-8 + assert np.min(np.sum(np.abs(p_int_4 - p_known_1), axis=0)) < 1e-8 + assert np.min(np.sum(np.abs(p_int_4 - p_known_2), axis=0)) < 1e-8 + + def test_segments_partly_overlap(self): + p_1 = np.ones(3) + p_2 = 3 * p_1 + p_3 = 0 * p_1 + p_4 = 2 * p_1 + + p_int = basics.segments_intersect_3d(p_1, p_2, p_3, p_4) + p_known_1 = p_1.reshape((-1, 1)) + p_known_2 = p_4.reshape((-1, 1)) + assert np.min(np.sum(np.abs(p_int - p_known_1), axis=0)) < 1e-8 + assert np.min(np.sum(np.abs(p_int - p_known_2), axis=0)) < 1e-8 + + + def test_random_incline(self): + p_1 = np.random.rand(3) + p_2 = 3 * p_1 + p_3 = 0 * p_1 + p_4 = 2 * p_1 + + p_int = basics.segments_intersect_3d(p_1, p_2, p_3, p_4) + p_known_1 = p_1.reshape((-1, 1)) + p_known_2 = p_4.reshape((-1, 1)) + assert np.min(np.sum(np.abs(p_int - p_known_1), axis=0)) < 1e-8 + assert np.min(np.sum(np.abs(p_int - p_known_2), axis=0)) < 1e-8 + + def test_segments_aligned_with_axis(self): + p_1 = np.array([0, 1, 1]) + p_2 = 3 * p_1 + p_3 = 0 * p_1 + p_4 = 2 * p_1 + + p_int = basics.segments_intersect_3d(p_1, p_2, p_3, p_4) + p_known_1 = p_1.reshape((-1, 1)) + p_known_2 = p_4.reshape((-1, 1)) + assert np.min(np.sum(np.abs(p_int - p_known_1), axis=0)) < 1e-8 + assert np.min(np.sum(np.abs(p_int - p_known_2), axis=0)) < 1e-8 + + + if __name__ == '__main__': unittest.main() From 4ec603e4f731d964640c87bb65ee89c008a89925 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Sun, 5 Feb 2017 20:03:01 +0100 Subject: [PATCH 067/153] Find intersection between polygon and bounding segments of other polygons. --- basics.py | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/basics.py b/basics.py index e851b46a2d..b6796956a0 100644 --- a/basics.py +++ b/basics.py @@ -417,8 +417,117 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): else: + # Solve 2x2 system by Cramer's rule + t_1 = ((xs_2 - xs_1) * (-dy_2) - (ys_2 - ys_1) * (-dx_2)) / discr + t_2 = (dx_1 * (ys_2 - ys_1) - dy_1 * (xs_2 - xs_1)) / discr + + # Check that we are on line segment + if t_1 < 0 or t_1 > 1 or t_2 < 0 or t_2 > 1: + return None + + # Compute the z-coordinates of the intersection points + z_1_isect = zs_1 + t_1 * dz_1 + z_2_isect = zs_2 + t_2 * dz_2 + + if np.abs(z_1_isect - z_2_isect) < tol: + return np.array([xs_1 + t_1 * dx_1, ys_1 + t_1 * dy_1, z_1_isect]) + else: + return None + +#----------------------------------------------------------------------------# +from compgeom import basics + +def polygon_boundaries_intersect(poly_1, poly_2, tol=1e-8): + """ + Find intersections between polygons embeded in 3D. + + The intersections are defined as between the interior of the first polygon and the second. + + Raises: + NotImplementedError if the two polygons overlap in a 2D area. An extension should not be difficult, but the function is not intended for this use. + + """ + + # First translate the points so that the first plane is located at the origin + center_1 = np.mean(poly_1, axis=1).reshape((-1, 1)) + poly_1 = poly_1 - center_1 + poly_2 = poly_2 - center_1 + + # Obtain the rotation matrix that projects p1 to the xy-plane + rot = basics.project_plane_matrix(poly_1) + irot = rot.transpose() + poly_1_xy = rot.dot(poly_1) + + # Sanity check: The points should lay on a plane + assert np.all(np.abs(poly_1_xy[2]) < tol) + # Drop the z-coordinate + poly_1_xy = poly_1_xy[:2] + # Convert the first polygon to sympy format + poly_1_sp = geom.Polygon(*_np2p(poly_1_xy)) + + # Rotate the second polygon with the same rotation matrix + poly_2_rot = rot.dot(poly_2) + + # If the rotation of whole second point cloud lies on the same side of z=0, there are no intersections + if poly_2_rot[2].min() > 0: + return None + elif poly_2_rot[2].max() < 0: return None + # Check if the second plane is parallel to the first (same xy-plane) + dz_2 = poly_2_rot[2].max() - poly_2_rot[2].min() + if dz_2 < tol: + if poly_2_rot[2].max() < tol: + # The polygons are parallel, and in the same plane + # Represent second polygon by sympy, and use sympy function to detect intersection. + poly_2_sp = geom.Polygon(*_np2p(poly_2_rot[:2])) + print('2D intersect') + + isect = poly_1_sp.intersection(poly_2_sp) + if (isinstance(isect, list) and len(isect) > 0): + # It would have been possible to return the intersecting area, but this is not the intended behavior of the function. Instead raise an error, and leave it to the user to deal with tihs. + raise NotImplementedError + else: + return None + else: + print('Different planes') + # Polygons lies in different parallel planes. No intersection + return None + else: + # Loop over all boundary segments of the second plane. Check if they intersect with the first polygon. + num_p2 = poly_2.shape[1] + # Roling indexing + ind = np.append(np.arange(num_p2), np.zeros(1)).astype('int') + + isect = [] + + for i in range(num_p2): + # Indices of points of this segment + i1 = ind[i] + i2 = ind[i+1] + pt_1 = poly_2_rot[:, ind[i]] + pt_2 = poly_2_rot[:, ind[i+1]] + dx = pt_1[0] - pt_2[0] + dy = pt_1[1] - pt_2[1] + dz = pt_1[2] - pt_2[2] + if np.abs(dz) > tol: + # We are on a plane, and we know that dz_2 is non-zero, so all individiual segments must have an incline. + # Parametrize the line, find parameter value for intersection with z=0 + t = (pt_1[2] - 0) / dz + # x and y-coordinate for z=0 + x0 = pt_1[0] + dx * t + y0 = pt_1[1] + dy * t + # Sympy representation + p_00 = geom.Point2D(x0, y0) + # Check if the first polygon encloses the point. If the intersection is on the border, this will not be detected. + if poly_1_sp.encloses_point(p_00): + # Back to physical coordinates by 1) converting to numpy format, 2) expand to 3D, 3) inverse rotation, 4) translate to original coordinate + isect.append(irot.dot(_to3D(_p2np(p_00))) + center_1) + + if len(isect) == 0: + isect = None + return isect + #-----------------------------------------------------------------------------# From 25bbd8993cc5e761a7187133a09e3b2425d65b61 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Sun, 5 Feb 2017 20:13:00 +0100 Subject: [PATCH 068/153] Utility functions for sympy conversion in basic computational geometry --- basics.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/basics.py b/basics.py index b6796956a0..93d3626bc8 100644 --- a/basics.py +++ b/basics.py @@ -10,6 +10,7 @@ import numpy as np from math import sqrt import sympy +from sympy import geometry as geom #------------------------------------------------------------------------------# @@ -437,6 +438,26 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): #----------------------------------------------------------------------------# from compgeom import basics +# Represent the polygon as a sympy polygon +def _np2p(p): + # Convert a numpy point array (3xn) to sympy points + if p.ndim == 1: + return geom.Point(p[:]) + else: + return [geom.Point(p[:, i]) for i in range(p.shape[1])] + +def _p2np(p): + # Convert sympy points to numpy format. If more than one point, these should be sent as a list + if isinstance(p, list): + return np.array(list([i.args for i in p]), dtype='float').transpose() + else: + return np.array(list(p.args), dtype='float').reshape((-1, 1)) + +def _to3D(p): + # Add a third dimension + return np.vstack((p, np.zeros(p.shape[1]))) + + def polygon_boundaries_intersect(poly_1, poly_2, tol=1e-8): """ Find intersections between polygons embeded in 3D. From 2ce50844e82ffef377ff1eb69287abb0f9a889c4 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Sun, 5 Feb 2017 20:13:22 +0100 Subject: [PATCH 069/153] Unit tests for polygon line segment intersection. --- tests/test_polygon_segment_intersection.py | 108 +++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 tests/test_polygon_segment_intersection.py diff --git a/tests/test_polygon_segment_intersection.py b/tests/test_polygon_segment_intersection.py new file mode 100644 index 0000000000..c47f42d51b --- /dev/null +++ b/tests/test_polygon_segment_intersection.py @@ -0,0 +1,108 @@ +import numpy as np +from compgeom import basics +import unittest + + +class PolygonSegmentIntersectionTest(unittest.TestCase): + + + def setup_polygons(self): + p_1 = np.array([[-1, 1, 1, -1 ], [0, 0, 0, 0], [-1, -1, 1, 1]]) + p_2 = np.array([[0, 0, 0, 0], [-1, 1, 1, -1 ], [-.7, -.7, .8, .8]]) + p_3 = np.array([[0, 0, 0, 0], [-1, 1, 1, -1 ], [.5, .5, 1.5, 1.5]]) + p_4 = np.array([[0, 0, 0, 0], [-1, 1, 1, -1 ], [-1, -1, 1, 1]]) + return p_1, p_2, p_3, p_4 + + + def test_one_intersection(self): + p_1, p_2, *rest = self.setup_polygons() + + # First intersection of 1 by edges of 2. It should be two of these + p_1_2 = basics.polygon_boundaries_intersect(p_1, p_2) + p_i_known_1 = np.array([0, 0, -0.7]) + p_i_known_2 = np.array([0, 0, 0.8]) + assert np.min(np.sum(np.abs(p_1_2 - p_i_known_1), axis=0)) < 1e-5 + assert np.min(np.sum(np.abs(p_1_2 - p_i_known_2), axis=0)) < 1e-5 + + # Then intersection of plane of 2 by edges of 1. This should be empty + p_2_1 = basics.polygon_boundaries_intersect(p_2, p_1) + assert p_2_1 is None + + def test_mutual_intersection(self): + p1, _, p3, *rest = self.setup_polygons() + + # First intersection of 1 by edges of 3 + p_1_3 = basics.polygon_boundaries_intersect(p1, p3) + p_i_known_1 = np.array([0, 0, 0.5]) + p_i_known_2 = np.array([0, 0, 1.0]) + assert np.min(np.sum(np.abs(p_1_3 - p_i_known_1), axis=0)) < 1e-5 + + # Then intersection of plane of 3 by edges of 1. + p_3_1 = basics.polygon_boundaries_intersect(p3, p1) + p_i_known_2 = np.array([0, 0, 1.0]) + + assert np.min(np.sum(np.abs(p_3_1 - p_i_known_1), axis=0)) < 1e-5 + + def test_mutual_intersection_not_at_origin(self): + p1, _, p3, *rest = self.setup_polygons() + + incr = np.array([1, 2, 3]).reshape((-1, 1)) + p1 += incr + p3 += incr + + # First intersection of 1 by edges of 3 + p_1_3 = basics.polygon_boundaries_intersect(p1, p3) + p_i_known_1 = np.array([0, 0, 0.5]) + incr + assert np.min(np.sum(np.abs(p_1_3 - p_i_known_1), axis=0)) < 1e-5 + + # Then intersection of plane of 3 by edges of 1. + p_3_1 = basics.polygon_boundaries_intersect(p3, p1) + p_i_known_2 = np.array([0, 0, 1.0]) + incr + + assert np.min(np.sum(np.abs(p_3_1 - p_i_known_1), axis=0)) < 1e-5 + + def test_parallel_planes(self): + p_1, *rest = self.setup_polygons() + p_2 = p_1 + np.array([0, 1, 0]).reshape((-1, 1)) + isect = basics.polygon_boundaries_intersect(p_1, p_2) + assert isect is None + + def test_extension_would_intersect(self): + # The extension of p_2 would intersect, but should detect nothing + p_1, p_2, *rest = self.setup_polygons() + p_2 += np.array([2, 0, 0]).reshape((-1, 1)) + isect = basics.polygon_boundaries_intersect(p_1, p_2) + assert isect is None + + def test_segments_intersect(self): + # Test where the planes intersect in a way where segments only touches segments, which does not qualify as intersection + p_1, _, _, p_4, *rest = self.setup_polygons() + isect = basics.polygon_boundaries_intersect(p_1, p_4) + assert isect is None + # Also try the other way around + isect = basics.polygon_boundaries_intersect(p_4, p_1) + assert isect is None + + def test_segments_same_plane_no_isect(self): + # Polygons in the same plane, but no intersection + p_1, *rest = self.setup_polygons() + p_2 = p_1 + np.array([3, 0, 0]).reshape((-1, 1)) + isect = basics.polygon_boundaries_intersect(p_1, p_2) + assert isect is None + isect = basics.polygon_boundaries_intersect(p_2, p_1) + assert isect is None + + def test_segments_same_plane_isect(self): + # Polygons in the same plane, and intersection. Should raise an exception + p_1, *rest = self.setup_polygons() + p_2 = p_1 + np.array([1, 0, 0]).reshape((-1, 1)) + caught_exp = False + try: + isect = basics.polygon_boundaries_intersect(p_1, p_2) + except NotImplementedError: + caught_exp = True + assert caught_exp + + if __name__ == '__main__': + unittest.main() + From 2ee9c35e1c4c77d6fc767271c00f222f885f423a Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Tue, 7 Feb 2017 06:29:04 +0100 Subject: [PATCH 070/153] Fix of unit test for segment intersections. Compared row and column vector. Not sure why this did not give problems previously. --- tests/test_segment_intersection_3d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_segment_intersection_3d.py b/tests/test_segment_intersection_3d.py index 6440eedf9d..9650a921ca 100644 --- a/tests/test_segment_intersection_3d.py +++ b/tests/test_segment_intersection_3d.py @@ -71,7 +71,7 @@ def test_L_intersection(self): p_3 = np.random.rand(3) p_i = basics.segments_intersect_3d(p_1, p_2, p_2, p_3) - assert np.allclose(p_i, p_2) + assert np.allclose(p_i, p_2.reshape((-1, 1))) def test_equal_lines_segments_not_overlapping(self): p_1 = np.ones(3) From 3bec5ed442cce202ace91e78316bbcd7d9c1388d Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Tue, 7 Feb 2017 06:30:20 +0100 Subject: [PATCH 071/153] Bugfix in intersection of line segments. The previous version failed for lines in the (x,z)-plane. New unittest that covers this configuration is added. --- basics.py | 51 +++++++++++++++++++-------- tests/test_segment_intersection_3d.py | 12 +++++++ 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/basics.py b/basics.py index 93d3626bc8..b8d4c39369 100644 --- a/basics.py +++ b/basics.py @@ -329,6 +329,27 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): + # The lines are parallel in the x-y plane, but we don't know about the + # z-direction. CHeck this + deltas_1 = np.array([dx_1, dy_1, dz_1]) + deltas_2 = np.array([dx_2, dy_2, dz_2]) + + # Use masked arrays to avoid divisions by zero + mask_1 = np.ma.greater(np.abs(deltas_1), tol) + mask_2 = np.ma.greater(np.abs(deltas_2), tol) + + if mask_1.sum() > 1: + in_discr = np.argwhere(mask_1)[:2] + elif mask_2.sum() > 1: + in_discr = np.argwhere(mask_2)[:2] + else: + # We're going to have a zero discreminant anyhow, just pick some dimensions. + in_discr = np.arange(2) + + not_in_discr = np.setdiff1d(np.arange(3), in_discr)[0] + discr = deltas_1[in_discr[0]] * deltas_2[in_discr[1]] - deltas_1[in_discr[1]] * deltas_2[in_discr[0]] + + # An intersection will be a solution of the linear system # xs_1 + dx_1 * t_1 = xs_2 + dx_2 * t_2 (1) # ys_1 + dy_1 * t_1 = ys_2 + dy_2 * t_2 (2) @@ -339,8 +360,9 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): # The intersection is on the line segments if 0 <= (t_1, t_2) <= 1 # Minus in front of _2 after reorganizing (1)-(2) - discr = dx_1 * (-dy_2) - dy_1 *(-dx_2) +# discr = dx_1 * (-dy_2) - dy_1 *(-dx_2) + if np.abs(discr) < tol: # If the lines are (almost) parallel, there is no single intersection, # but it may be a segment @@ -349,14 +371,6 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): # intersection - # The lines are parallel in the x-y plane, but we don't know about the - # z-direction. CHeck this - deltas_1 = np.array([dx_1, dy_1, dz_1]) - deltas_2 = np.array([dx_2, dy_2, dz_2]) - - # Use masked arrays to avoid divisions by zero - mask_1 = np.ma.greater(np.abs(deltas_1), tol) - mask_2 = np.ma.greater(np.abs(deltas_2), tol) # A first, simple test if np.any(mask_1 != mask_2): @@ -414,21 +428,28 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): return lines_full[:, target] + else: + # Solve 2x2 system by Cramer's rule + discr = deltas_1[in_discr[0]] * (-deltas_2[in_discr[1]]) - deltas_1[in_discr[1]] * (-deltas_2[in_discr[0]]) + #import pdb + #pdb.set_trace() + t_1 = ((start_2[in_discr[0]] - start_1[in_discr[0]]) * (-deltas_2[in_discr[1]]) - \ + (start_2[in_discr[1]] - start_1[in_discr[1]]) * (-deltas_2[in_discr[0]]))/discr + t_2 = (deltas_1[in_discr[0]] * (start_2[in_discr[1]] - start_1[in_discr[1]]) - \ + deltas_1[in_discr[1]] * (start_2[in_discr[0]] - start_1[in_discr[0]])) / discr - else: - # Solve 2x2 system by Cramer's rule - t_1 = ((xs_2 - xs_1) * (-dy_2) - (ys_2 - ys_1) * (-dx_2)) / discr - t_2 = (dx_1 * (ys_2 - ys_1) - dy_1 * (xs_2 - xs_1)) / discr +# t_1 = ((xs_2 - xs_1) * (-dy_2) - (ys_2 - ys_1) * (-dx_2)) / discr +# t_2 = (dx_1 * (ys_2 - ys_1) - dy_1 * (xs_2 - xs_1)) / discr # Check that we are on line segment if t_1 < 0 or t_1 > 1 or t_2 < 0 or t_2 > 1: return None # Compute the z-coordinates of the intersection points - z_1_isect = zs_1 + t_1 * dz_1 - z_2_isect = zs_2 + t_2 * dz_2 + z_1_isect = start_1[not_in_discr] + t_1 * deltas_1[not_in_discr] + z_2_isect = start_2[not_in_discr] + t_2 * deltas_2[not_in_discr] if np.abs(z_1_isect - z_2_isect) < tol: return np.array([xs_1 + t_1 * dx_1, ys_1 + t_1 * dy_1, z_1_isect]) diff --git a/tests/test_segment_intersection_3d.py b/tests/test_segment_intersection_3d.py index 9650a921ca..1fdc4ae062 100644 --- a/tests/test_segment_intersection_3d.py +++ b/tests/test_segment_intersection_3d.py @@ -158,6 +158,18 @@ def test_segments_aligned_with_axis(self): assert np.min(np.sum(np.abs(p_int - p_known_2), axis=0)) < 1e-8 + def test_constant_y_axis(self): + p_1 = np.array([1, 0, -1]) + p_2 = np.array([1, 0, 1]) + p_3 = np.array([1.5, 0, 0]) + p_4 = np.array([0, 0, 1.5]) + + + p_int = basics.segments_intersect_3d(p_1, p_2, p_3, p_4) + p_known = np.array([1, 0, 0]).reshape((-1, 1)) + assert np.min(np.sum(np.abs(p_int - p_known), axis=0)) < 1e-8 + + if __name__ == '__main__': unittest.main() From ab59cb00f486d4ea41028029519faa16b9bf6c95 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Tue, 7 Feb 2017 06:45:30 +0100 Subject: [PATCH 072/153] Bugfix for line segment intersection. Update of output vector for successful intersections. --- basics.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/basics.py b/basics.py index b8d4c39369..22eaf62b53 100644 --- a/basics.py +++ b/basics.py @@ -432,8 +432,6 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): # Solve 2x2 system by Cramer's rule discr = deltas_1[in_discr[0]] * (-deltas_2[in_discr[1]]) - deltas_1[in_discr[1]] * (-deltas_2[in_discr[0]]) - #import pdb - #pdb.set_trace() t_1 = ((start_2[in_discr[0]] - start_1[in_discr[0]]) * (-deltas_2[in_discr[1]]) - \ (start_2[in_discr[1]] - start_1[in_discr[1]]) * (-deltas_2[in_discr[0]]))/discr @@ -452,7 +450,11 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): z_2_isect = start_2[not_in_discr] + t_2 * deltas_2[not_in_discr] if np.abs(z_1_isect - z_2_isect) < tol: - return np.array([xs_1 + t_1 * dx_1, ys_1 + t_1 * dy_1, z_1_isect]) + vec = np.zeros(3) + vec[in_discr] = start_1[in_discr] + t_1 * deltas_1[in_discr] + vec[not_in_discr] = z_1_isect + return vec.reshape((-1, 1)) +# return np.array([xs_1 + t_1 * dx_1, ys_1 + t_1 * dy_1, z_1_isect]) else: return None From 71bbf0b317f13a358274aec64140b14a9e02912b Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Tue, 7 Feb 2017 06:52:03 +0100 Subject: [PATCH 073/153] Fix wrong coordinate in unit test for segment intersection. --- tests/test_segment_intersection_3d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_segment_intersection_3d.py b/tests/test_segment_intersection_3d.py index 1fdc4ae062..97b743117e 100644 --- a/tests/test_segment_intersection_3d.py +++ b/tests/test_segment_intersection_3d.py @@ -166,7 +166,7 @@ def test_constant_y_axis(self): p_int = basics.segments_intersect_3d(p_1, p_2, p_3, p_4) - p_known = np.array([1, 0, 0]).reshape((-1, 1)) + p_known = np.array([1, 0, 0.5]).reshape((-1, 1)) assert np.min(np.sum(np.abs(p_int - p_known), axis=0)) < 1e-8 From 9952ce8e2098a7bc3c662e8fcfc07871863fb076 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Tue, 7 Feb 2017 15:50:59 +0100 Subject: [PATCH 074/153] Changed name of method for polygon line segment intersection. --- basics.py | 6 ++++- tests/test_polygon_segment_intersection.py | 26 +++++++++++----------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/basics.py b/basics.py index 22eaf62b53..772c2ad2b2 100644 --- a/basics.py +++ b/basics.py @@ -480,8 +480,10 @@ def _to3D(p): # Add a third dimension return np.vstack((p, np.zeros(p.shape[1]))) +#------------------------------------------------------------------- -def polygon_boundaries_intersect(poly_1, poly_2, tol=1e-8): + +def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): """ Find intersections between polygons embeded in 3D. @@ -573,6 +575,8 @@ def polygon_boundaries_intersect(poly_1, poly_2, tol=1e-8): return isect + + #-----------------------------------------------------------------------------# def remove_edge_crossings(vertices, edges, **kwargs): diff --git a/tests/test_polygon_segment_intersection.py b/tests/test_polygon_segment_intersection.py index c47f42d51b..c1bf306b03 100644 --- a/tests/test_polygon_segment_intersection.py +++ b/tests/test_polygon_segment_intersection.py @@ -18,27 +18,27 @@ def test_one_intersection(self): p_1, p_2, *rest = self.setup_polygons() # First intersection of 1 by edges of 2. It should be two of these - p_1_2 = basics.polygon_boundaries_intersect(p_1, p_2) + p_1_2 = basics.polygon_segment_intersect(p_1, p_2) p_i_known_1 = np.array([0, 0, -0.7]) p_i_known_2 = np.array([0, 0, 0.8]) assert np.min(np.sum(np.abs(p_1_2 - p_i_known_1), axis=0)) < 1e-5 assert np.min(np.sum(np.abs(p_1_2 - p_i_known_2), axis=0)) < 1e-5 # Then intersection of plane of 2 by edges of 1. This should be empty - p_2_1 = basics.polygon_boundaries_intersect(p_2, p_1) + p_2_1 = basics.polygon_segment_intersect(p_2, p_1) assert p_2_1 is None def test_mutual_intersection(self): p1, _, p3, *rest = self.setup_polygons() # First intersection of 1 by edges of 3 - p_1_3 = basics.polygon_boundaries_intersect(p1, p3) + p_1_3 = basics.polygon_segment_intersect(p1, p3) p_i_known_1 = np.array([0, 0, 0.5]) p_i_known_2 = np.array([0, 0, 1.0]) assert np.min(np.sum(np.abs(p_1_3 - p_i_known_1), axis=0)) < 1e-5 # Then intersection of plane of 3 by edges of 1. - p_3_1 = basics.polygon_boundaries_intersect(p3, p1) + p_3_1 = basics.polygon_segment_intersect(p3, p1) p_i_known_2 = np.array([0, 0, 1.0]) assert np.min(np.sum(np.abs(p_3_1 - p_i_known_1), axis=0)) < 1e-5 @@ -51,12 +51,12 @@ def test_mutual_intersection_not_at_origin(self): p3 += incr # First intersection of 1 by edges of 3 - p_1_3 = basics.polygon_boundaries_intersect(p1, p3) + p_1_3 = basics.polygon_segment_intersect(p1, p3) p_i_known_1 = np.array([0, 0, 0.5]) + incr assert np.min(np.sum(np.abs(p_1_3 - p_i_known_1), axis=0)) < 1e-5 # Then intersection of plane of 3 by edges of 1. - p_3_1 = basics.polygon_boundaries_intersect(p3, p1) + p_3_1 = basics.polygon_segment_intersect(p3, p1) p_i_known_2 = np.array([0, 0, 1.0]) + incr assert np.min(np.sum(np.abs(p_3_1 - p_i_known_1), axis=0)) < 1e-5 @@ -64,32 +64,32 @@ def test_mutual_intersection_not_at_origin(self): def test_parallel_planes(self): p_1, *rest = self.setup_polygons() p_2 = p_1 + np.array([0, 1, 0]).reshape((-1, 1)) - isect = basics.polygon_boundaries_intersect(p_1, p_2) + isect = basics.polygon_segment_intersect(p_1, p_2) assert isect is None def test_extension_would_intersect(self): # The extension of p_2 would intersect, but should detect nothing p_1, p_2, *rest = self.setup_polygons() p_2 += np.array([2, 0, 0]).reshape((-1, 1)) - isect = basics.polygon_boundaries_intersect(p_1, p_2) + isect = basics.polygon_segment_intersect(p_1, p_2) assert isect is None def test_segments_intersect(self): # Test where the planes intersect in a way where segments only touches segments, which does not qualify as intersection p_1, _, _, p_4, *rest = self.setup_polygons() - isect = basics.polygon_boundaries_intersect(p_1, p_4) + isect = basics.polygon_segment_intersect(p_1, p_4) assert isect is None # Also try the other way around - isect = basics.polygon_boundaries_intersect(p_4, p_1) + isect = basics.polygon_segment_intersect(p_4, p_1) assert isect is None def test_segments_same_plane_no_isect(self): # Polygons in the same plane, but no intersection p_1, *rest = self.setup_polygons() p_2 = p_1 + np.array([3, 0, 0]).reshape((-1, 1)) - isect = basics.polygon_boundaries_intersect(p_1, p_2) + isect = basics.polygon_segment_intersect(p_1, p_2) assert isect is None - isect = basics.polygon_boundaries_intersect(p_2, p_1) + isect = basics.polygon_segment_intersect(p_2, p_1) assert isect is None def test_segments_same_plane_isect(self): @@ -98,7 +98,7 @@ def test_segments_same_plane_isect(self): p_2 = p_1 + np.array([1, 0, 0]).reshape((-1, 1)) caught_exp = False try: - isect = basics.polygon_boundaries_intersect(p_1, p_2) + isect = basics.polygon_segment_intersect(p_1, p_2) except NotImplementedError: caught_exp = True assert caught_exp From 75e8e7cc666973ecf1dd64fd28982891c6d51e8a Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Tue, 7 Feb 2017 15:52:03 +0100 Subject: [PATCH 075/153] Method for computing intersections between bounding segments of polygons. Also unit tests. --- basics.py | 47 +++++++++++ tests/test_polygon_boundaries_intersect.py | 92 ++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 tests/test_polygon_boundaries_intersect.py diff --git a/basics.py b/basics.py index 772c2ad2b2..1f2504b496 100644 --- a/basics.py +++ b/basics.py @@ -483,6 +483,53 @@ def _to3D(p): #------------------------------------------------------------------- +def polygon_boundaries_intersect(poly_1, poly_2, tol=1e-8): + """ + Test for intersection between the bounding segments of two 3D polygons. + + No tests are done for intersections with polygons interiors. The code has only been tested for convex polygons, status for non-convex polygons is unknown. + + A segment can either be intersected by segments of the other polygon as + i) a single point + ii) two points, hit by different segments (cannot be more than 2 for convex polygons) + iii) along a line, if the segments are parallel. + + Note that if the polygons share a vertex, this point will be found multiple times (depending on the configuration of the polygons). + + Each intersection is represented as a list of three items. The first two are the segment index (numbered according to start point) of the intersecting segments. The third is the coordinates of the intersection point, this can either be a single point (3x1 nd.array), or a 3x2 array with columns representing the same (shared vertex) or different (shared segment) points. + + Paremeters: + poly_1 (np.array, 3 x n_pt): First polygon, assumed to be non-intersecting. The closing segment is defined by the last and first column. + poly_2 (np.array, 3 x n_pt): Second polygon, assumed to be non-intersecting. The closing segment is defined by the last and first column. + tol (float, optional): Tolerance used for equality. + + Returns: + list: of intersections. See above for description of data format. If no intersections are found, an empty list is returned. + + """ + + l_1 = poly_1.shape[1] + ind_1 = np.append(np.arange(l_1), 0) + l_2 = poly_2.shape[1] + ind_2 = np.append(np.arange(l_2), 0) + + isect = [] + + for i in range(l_1): + p_1_1 = poly_1[:, ind_1[i]] + p_1_2 = poly_1[:, ind_1[i+1]] + + for j in range(l_2): + p_2_1 = poly_2[:, ind_2[j]] + p_2_2 = poly_2[:, ind_2[j+1]] + isect_loc = segments_intersect_3d(p_1_1, p_1_2, p_2_1, p_2_2) + if isect_loc is not None: + isect.append([i, j, isect_loc]) + + return isect + +#---------------------------------------------------------- + def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): """ Find intersections between polygons embeded in 3D. diff --git a/tests/test_polygon_boundaries_intersect.py b/tests/test_polygon_boundaries_intersect.py new file mode 100644 index 0000000000..1becd11e3c --- /dev/null +++ b/tests/test_polygon_boundaries_intersect.py @@ -0,0 +1,92 @@ +import unittest +import numpy as np + +from compgeom import basics + + +class TestPolygonBoundariesIntersect(unittest.TestCase): + + + def setup_polygons(self): + p_1 = np.array([[-1, 1, 1, -1 ], [0, 0, 0, 0], [-1, -1, 1, 1]]) + p_2 = np.array([[0, 0, 0, 0], [-1, 1, 1, -1 ], [-.7, -.7, .8, .8]]) + p_3 = np.array([[0, 0, 0, 0], [-1, 1, 1, -1 ], [.5, .5, 1.5, 1.5]]) + p_4 = np.array([[0, 0, 0, 0], [-1, 1, 1, -1 ], [-1, -1, 1, 1]]) + return p_1, p_2, p_3, p_4 + + def test_point_segment_intersection(self): + # Polygons on top of each other, cross at a single point + p_1, _, _, p_4 = self.setup_polygons() + p_4 += np.array([0, 0, 2]).reshape((-1, 1)) + isect = basics.polygon_boundaries_intersect(p_1, p_4) + p_known = np.array([0, 0, 1]).reshape((-1, 1)) + assert len(isect) == 1 + assert np.min(np.sum(np.abs(isect[0][2] - p_known), axis=0)) < 1e-5 + + def test_segment_plane_intersection(self): + # One intersection in a segment. Another in the interior, but should not be detected + p_1, p_2, *rest = self.setup_polygons() + p_2 -= np.array([0, 0, 0.3]).reshape((-1, 1)) + isect = basics.polygon_boundaries_intersect(p_1, p_2) + p_known = np.array([0, 0, -1]).reshape((-1, 1)) + assert len(isect) == 1 + assert np.min(np.sum(np.abs(isect[0][2] - p_known), axis=0)) < 1e-5 + + def test_overlapping_segments(self): + # The function should find the segment (1, 0, [-1,1]) + # In addition, each the points (1, 0, +-1) will be found twice (they are corners of both polygons) + p_1, *rest = self.setup_polygons() + p_2 = p_1 + np.array([2, 0, 0]).reshape((-1, 1)) + isect = basics.polygon_boundaries_intersect(p_1, p_2) + p_int = isect[0] + + p_known_1 = np.array([1, 0, -1]).reshape((-1, 1)) + p_known_2 = np.array([1, 0, 1]).reshape((-1, 1)) + + found_1 = 0 + found_2 = 0 + found_1_2 = 0 + for i in isect: + p_int = i[2] + eq_p_1 = np.sum(np.sum(np.abs(p_int - p_known_1), axis=0) < 1e-8) + eq_p_2 = np.sum(np.sum(np.abs(p_int - p_known_2), axis=0) < 1e-8) + + if eq_p_1 == 2: + found_1 += 1 + if eq_p_2 == 2: + found_2 += 1 + if eq_p_1 == 1 and eq_p_2 == 1: + found_1_2 += 1 + + assert found_1 == 1 + assert found_2 == 1 + assert found_1_2 == 1 + + def test_one_segment_crosses_two(self): + # A segment of one polygon crosses two segments of the other (and also its interior) + p_1 = np.array([[-1, 1, 1, -1 ], [0, 0, 0, 0], [-1, -1, 1, 1]]) + p_2 = np.array([[1.5, 1.5, 0], [0, 0, 0], [0, 1.5, 1.5]]) + + isect = basics.polygon_boundaries_intersect(p_1, p_2) + + p_known_1 = np.array([1, 0, 0.5]).reshape((-1, 1)) + p_known_2 = np.array([0.5, 0, 1]).reshape((-1, 1)) + + found_1 = 0 + found_2 = 0 + for i in isect: + p_int = i[2] + eq_p_1 = np.sum(np.sum(np.abs(p_int - p_known_1), axis=0) < 1e-8) + eq_p_2 = np.sum(np.sum(np.abs(p_int - p_known_2), axis=0) < 1e-8) + + if eq_p_1 == 1: + found_1 += 1 + if eq_p_2 == 1: + found_2 += 1 + + assert found_1 == 1 + assert found_2 == 1 + + if __name__ == '__main__': + unittest.main() + From 57d43bc00375ae561605c9d58dc685158fd9342f Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Wed, 8 Feb 2017 05:36:38 +0100 Subject: [PATCH 076/153] Compute distance between point and point set --- basics.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/basics.py b/basics.py index 1f2504b496..bd9a8fe1b9 100644 --- a/basics.py +++ b/basics.py @@ -203,6 +203,32 @@ def add_point(vertices, pt, precision=1e-3, **kwargs): ind = vertices.shape[1] - 1 return vertices, ind, pt +#----------------------------------------------------------- + +def dist_point_pointset(p, pset, exponent=2): + """ + Compute distance between a point and a set of points. + + Parameters: + p (np.ndarray): Point from which distances will be computed + pset (nd.array): Point cloud to which we compute distances + exponent (double, optional): Exponent of the norm used. Defaults to 2. + + Return: + np.ndarray: Array of distances. + + """ + + # If p is 1D, do a reshape to facilitate broadcasting, but on a copy + if p.ndim == 1: + pt = p.reshape((-1, 1)) + else: + pt = p + + return np.power(np.sum(np.power(np.abs(pt - pset), exponent), + axis=0), 1/exponent) + + #------------------------------------------------------------------------------# def lines_intersect(start_1, end_1, start_2, end_2): From 3ebbd1f0c89a359937eb8665859d98aa3bef5727 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Thu, 9 Feb 2017 05:42:27 +0100 Subject: [PATCH 077/153] Improved comments for basic polygon_boundary_intersection. --- basics.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/basics.py b/basics.py index bd9a8fe1b9..8529106d4c 100644 --- a/basics.py +++ b/basics.py @@ -513,24 +513,38 @@ def polygon_boundaries_intersect(poly_1, poly_2, tol=1e-8): """ Test for intersection between the bounding segments of two 3D polygons. - No tests are done for intersections with polygons interiors. The code has only been tested for convex polygons, status for non-convex polygons is unknown. + No tests are done for intersections with polygons interiors. The code has + only been tested for convex polygons, status for non-convex polygons is + unknown. A segment can either be intersected by segments of the other polygon as i) a single point - ii) two points, hit by different segments (cannot be more than 2 for convex polygons) + ii) two points, hit by different segments (cannot be more than 2 for + convex polygons) iii) along a line, if the segments are parallel. - Note that if the polygons share a vertex, this point will be found multiple times (depending on the configuration of the polygons). + Note that if the polygons share a vertex, this point will be found multiple + times (depending on the configuration of the polygons). - Each intersection is represented as a list of three items. The first two are the segment index (numbered according to start point) of the intersecting segments. The third is the coordinates of the intersection point, this can either be a single point (3x1 nd.array), or a 3x2 array with columns representing the same (shared vertex) or different (shared segment) points. + Each intersection is represented as a list of three items. The first two + are the segment index (numbered according to start point) of the + intersecting segments. The third is the coordinates of the intersection + point, this can either be a single point (3x1 nd.array), or a 3x2 array + with columns representing the same (shared vertex) or different (shared + segment) points. Paremeters: - poly_1 (np.array, 3 x n_pt): First polygon, assumed to be non-intersecting. The closing segment is defined by the last and first column. - poly_2 (np.array, 3 x n_pt): Second polygon, assumed to be non-intersecting. The closing segment is defined by the last and first column. + poly_1 (np.array, 3 x n_pt): First polygon, assumed to be + non-intersecting. The closing segment is defined by the last and first + column. + poly_2 (np.array, 3 x n_pt): Second polygon, assumed to be + non-intersecting. The closing segment is defined by the last and first + column. tol (float, optional): Tolerance used for equality. Returns: - list: of intersections. See above for description of data format. If no intersections are found, an empty list is returned. + list: of intersections. See above for description of data format. If no + intersections are found, an empty list is returned. """ From c9488169f2e9f25c700c460cbefae4acb7259a7f Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Thu, 9 Feb 2017 05:47:05 +0100 Subject: [PATCH 078/153] Polygon intersection function returns np.array rather than list. --- basics.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/basics.py b/basics.py index 8529106d4c..52484258a4 100644 --- a/basics.py +++ b/basics.py @@ -632,7 +632,7 @@ def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): # Roling indexing ind = np.append(np.arange(num_p2), np.zeros(1)).astype('int') - isect = [] + isect = np.empty((3, 0)) for i in range(num_p2): # Indices of points of this segment @@ -655,15 +655,16 @@ def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): # Check if the first polygon encloses the point. If the intersection is on the border, this will not be detected. if poly_1_sp.encloses_point(p_00): # Back to physical coordinates by 1) converting to numpy format, 2) expand to 3D, 3) inverse rotation, 4) translate to original coordinate - isect.append(irot.dot(_to3D(_p2np(p_00))) + center_1) + isect = np.hstack((isect, + irot.dot(_to3D(_p2np(p_00))) + + center_1)) - if len(isect) == 0: + if isect.shape[1] == 0: isect = None + return isect - - #-----------------------------------------------------------------------------# def remove_edge_crossings(vertices, edges, **kwargs): From 8f3adc8b2efc0c51d624a9547d9a5b10bc517aee Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Thu, 9 Feb 2017 05:55:43 +0100 Subject: [PATCH 079/153] Improved comments in polygon boundary intersection --- basics.py | 63 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/basics.py b/basics.py index 52484258a4..df17ceb8bd 100644 --- a/basics.py +++ b/basics.py @@ -574,10 +574,31 @@ def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): """ Find intersections between polygons embeded in 3D. - The intersections are defined as between the interior of the first polygon and the second. - + The intersections are defined as between the interior of the first polygon + and the boundary of the second. + + TODO: + 1) Also cover case where the one polygon ends in the plane of the other. + 2) Replace sympy.in_polygon with self-implemented check, should be + simple using ccw. + + Parameters: + poly_1 (np.ndarray, 3xn1): Vertexes of polygon, assumed ordered as cw or + ccw. + poly_2 (np.ndarray, 3xn2): Vertexes of second polygon, assumed ordered + as cw or ccw. + tol (double, optional): Tolerance for when two points are equal. + Defaults to 1e-8. + + Returns: + np.ndarray, size 3 x num_isect, coordinates of intersection points; or + None if no intersection is found (may change to empty array of size + (3, 0)). + Raises: - NotImplementedError if the two polygons overlap in a 2D area. An extension should not be difficult, but the function is not intended for this use. + NotImplementedError if the two polygons overlap in a 2D area. An + extension should not be difficult, but the function is not intended for + this use. """ @@ -601,7 +622,8 @@ def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): # Rotate the second polygon with the same rotation matrix poly_2_rot = rot.dot(poly_2) - # If the rotation of whole second point cloud lies on the same side of z=0, there are no intersections + # If the rotation of whole second point cloud lies on the same side of z=0, + # there are no intersections. if poly_2_rot[2].min() > 0: return None elif poly_2_rot[2].max() < 0: @@ -612,22 +634,27 @@ def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): if dz_2 < tol: if poly_2_rot[2].max() < tol: # The polygons are parallel, and in the same plane - # Represent second polygon by sympy, and use sympy function to detect intersection. + # Represent second polygon by sympy, and use sympy function to + # detect intersection. poly_2_sp = geom.Polygon(*_np2p(poly_2_rot[:2])) - print('2D intersect') isect = poly_1_sp.intersection(poly_2_sp) if (isinstance(isect, list) and len(isect) > 0): - # It would have been possible to return the intersecting area, but this is not the intended behavior of the function. Instead raise an error, and leave it to the user to deal with tihs. + # It would have been possible to return the intersecting area, + # but this is not the intended behavior of the function. + # Instead raise an error, and leave it to the user to deal with + # this. raise NotImplementedError else: return None else: - print('Different planes') # Polygons lies in different parallel planes. No intersection return None else: - # Loop over all boundary segments of the second plane. Check if they intersect with the first polygon. + # Loop over all boundary segments of the second plane. Check if they + # intersect with the first polygon. + # TODO: Special treatment of the case where one or two vertexes lies in + # the plane of the poly_1 num_p2 = poly_2.shape[1] # Roling indexing ind = np.append(np.arange(num_p2), np.zeros(1)).astype('int') @@ -638,23 +665,33 @@ def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): # Indices of points of this segment i1 = ind[i] i2 = ind[i+1] + + # Coordinates of this segment pt_1 = poly_2_rot[:, ind[i]] pt_2 = poly_2_rot[:, ind[i+1]] dx = pt_1[0] - pt_2[0] dy = pt_1[1] - pt_2[1] dz = pt_1[2] - pt_2[2] if np.abs(dz) > tol: - # We are on a plane, and we know that dz_2 is non-zero, so all individiual segments must have an incline. - # Parametrize the line, find parameter value for intersection with z=0 + # We are on a plane, and we know that dz_2 is non-zero, so all + # individiual segments must have an incline. + # Parametrize the line, find parameter value for intersection + # with z=0. t = (pt_1[2] - 0) / dz # x and y-coordinate for z=0 x0 = pt_1[0] + dx * t y0 = pt_1[1] + dy * t # Sympy representation p_00 = geom.Point2D(x0, y0) - # Check if the first polygon encloses the point. If the intersection is on the border, this will not be detected. + # Check if the first polygon encloses the point. If the + # intersection is on the border, this will not be detected, see + # documentation is sympy. + # TODO: Replace this with a self-written test to improve + # performance if poly_1_sp.encloses_point(p_00): - # Back to physical coordinates by 1) converting to numpy format, 2) expand to 3D, 3) inverse rotation, 4) translate to original coordinate + # Back to physical coordinates by 1) converting to numpy + # format, 2) expand to 3D, 3) inverse rotation, 4) + # translate to original coordinate. isect = np.hstack((isect, irot.dot(_to3D(_p2np(p_00))) + center_1)) From db7d72fe8e7ff192aee48b5931bb5f29ff8fc60a Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Thu, 9 Feb 2017 06:03:15 +0100 Subject: [PATCH 080/153] Fix of unit test to reflect column-type output from polygon segment interseciton. --- tests/test_polygon_segment_intersection.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_polygon_segment_intersection.py b/tests/test_polygon_segment_intersection.py index c1bf306b03..8655f0bb3a 100644 --- a/tests/test_polygon_segment_intersection.py +++ b/tests/test_polygon_segment_intersection.py @@ -19,8 +19,8 @@ def test_one_intersection(self): # First intersection of 1 by edges of 2. It should be two of these p_1_2 = basics.polygon_segment_intersect(p_1, p_2) - p_i_known_1 = np.array([0, 0, -0.7]) - p_i_known_2 = np.array([0, 0, 0.8]) + p_i_known_1 = np.array([0, 0, -0.7]).reshape((-1, 1)) + p_i_known_2 = np.array([0, 0, 0.8]).reshape((-1, 1)) assert np.min(np.sum(np.abs(p_1_2 - p_i_known_1), axis=0)) < 1e-5 assert np.min(np.sum(np.abs(p_1_2 - p_i_known_2), axis=0)) < 1e-5 @@ -33,13 +33,13 @@ def test_mutual_intersection(self): # First intersection of 1 by edges of 3 p_1_3 = basics.polygon_segment_intersect(p1, p3) - p_i_known_1 = np.array([0, 0, 0.5]) - p_i_known_2 = np.array([0, 0, 1.0]) + p_i_known_1 = np.array([0, 0, 0.5]).reshape((-1, 1)) + p_i_known_2 = np.array([0, 0, 1.0]).reshape((-1, 1)) assert np.min(np.sum(np.abs(p_1_3 - p_i_known_1), axis=0)) < 1e-5 # Then intersection of plane of 3 by edges of 1. p_3_1 = basics.polygon_segment_intersect(p3, p1) - p_i_known_2 = np.array([0, 0, 1.0]) + p_i_known_2 = np.array([0, 0, 1.0]).reshape((-1, 1)) assert np.min(np.sum(np.abs(p_3_1 - p_i_known_1), axis=0)) < 1e-5 @@ -52,12 +52,12 @@ def test_mutual_intersection_not_at_origin(self): # First intersection of 1 by edges of 3 p_1_3 = basics.polygon_segment_intersect(p1, p3) - p_i_known_1 = np.array([0, 0, 0.5]) + incr + p_i_known_1 = np.array([0, 0, 0.5]).reshape((-1, 1)) + incr assert np.min(np.sum(np.abs(p_1_3 - p_i_known_1), axis=0)) < 1e-5 # Then intersection of plane of 3 by edges of 1. p_3_1 = basics.polygon_segment_intersect(p3, p1) - p_i_known_2 = np.array([0, 0, 1.0]) + incr + p_i_known_2 = np.array([0, 0, 1.0]).reshape((-1, 1)) + incr assert np.min(np.sum(np.abs(p_3_1 - p_i_known_1), axis=0)) < 1e-5 From 98c4d430086456ec67e71bd7a2953285bb9abc8d Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Thu, 9 Feb 2017 06:04:12 +0100 Subject: [PATCH 081/153] Fix of unit test to detect correct intersection points between polygon and segment. No idea why the test did not fail previously.. --- tests/test_polygon_segment_intersection.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_polygon_segment_intersection.py b/tests/test_polygon_segment_intersection.py index 8655f0bb3a..c8462687f5 100644 --- a/tests/test_polygon_segment_intersection.py +++ b/tests/test_polygon_segment_intersection.py @@ -41,7 +41,7 @@ def test_mutual_intersection(self): p_3_1 = basics.polygon_segment_intersect(p3, p1) p_i_known_2 = np.array([0, 0, 1.0]).reshape((-1, 1)) - assert np.min(np.sum(np.abs(p_3_1 - p_i_known_1), axis=0)) < 1e-5 + assert np.min(np.sum(np.abs(p_3_1 - p_i_known_2), axis=0)) < 1e-5 def test_mutual_intersection_not_at_origin(self): p1, _, p3, *rest = self.setup_polygons() @@ -59,7 +59,7 @@ def test_mutual_intersection_not_at_origin(self): p_3_1 = basics.polygon_segment_intersect(p3, p1) p_i_known_2 = np.array([0, 0, 1.0]).reshape((-1, 1)) + incr - assert np.min(np.sum(np.abs(p_3_1 - p_i_known_1), axis=0)) < 1e-5 + assert np.min(np.sum(np.abs(p_3_1 - p_i_known_2), axis=0)) < 1e-5 def test_parallel_planes(self): p_1, *rest = self.setup_polygons() @@ -106,3 +106,4 @@ def test_segments_same_plane_isect(self): if __name__ == '__main__': unittest.main() + From 105fc834abafbf5bd146f6d03f5dec6b12a95428 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Thu, 9 Feb 2017 06:05:57 +0100 Subject: [PATCH 082/153] Gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 74f6527809..c885702599 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *~ *.py~ *.swp +*.swo From ce09112d31233475f0c91047d0d98777198a2db5 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Thu, 9 Feb 2017 11:47:08 +0100 Subject: [PATCH 083/153] Bugfix in intersection of line segments. Treated special case with two lines aligned with separate axis --- basics.py | 13 ++++++------- tests/test_segment_intersection_3d.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/basics.py b/basics.py index df17ceb8bd..dae7a4c70f 100644 --- a/basics.py +++ b/basics.py @@ -364,10 +364,10 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): mask_1 = np.ma.greater(np.abs(deltas_1), tol) mask_2 = np.ma.greater(np.abs(deltas_2), tol) - if mask_1.sum() > 1: - in_discr = np.argwhere(mask_1)[:2] - elif mask_2.sum() > 1: - in_discr = np.argwhere(mask_2)[:2] + # Check for two dimensions that are not parallel with at least one line + mask_sum = mask_1 + mask_2 + if mask_sum.sum() > 1: + in_discr = np.argwhere(mask_sum)[:2] else: # We're going to have a zero discreminant anyhow, just pick some dimensions. in_discr = np.arange(2) @@ -389,6 +389,7 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): # discr = dx_1 * (-dy_2) - dy_1 *(-dx_2) + # Either the lines are parallel in two directions if np.abs(discr) < tol: # If the lines are (almost) parallel, there is no single intersection, # but it may be a segment @@ -396,8 +397,6 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): # First check if the third dimension is also parallel, if not, no # intersection - - # A first, simple test if np.any(mask_1 != mask_2): return None @@ -453,7 +452,7 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): # Our segment consists of the second and third column. We're done! return lines_full[:, target] - + # or we are looking for a point intersection else: # Solve 2x2 system by Cramer's rule diff --git a/tests/test_segment_intersection_3d.py b/tests/test_segment_intersection_3d.py index 97b743117e..22d35eae1b 100644 --- a/tests/test_segment_intersection_3d.py +++ b/tests/test_segment_intersection_3d.py @@ -81,7 +81,19 @@ def test_equal_lines_segments_not_overlapping(self): p_int = basics.segments_intersect_3d(p_1, p_2, p_3, p_4) assert p_int is None + + + def test_both_aligned_with_axis(self): + # Both lines are aligned an axis, + p_1 = np.array([-1, -1, 0]) + p_2 = np.array([-1, 1, 0]) + p_3 = np.array([-1, 0, -1]) + p_4 = np.array([-1, 0, 1]) + p_int = basics.segments_intersect_3d(p_1, p_2, p_3, p_4) + p_known = np.array([-1, 0, 0]).reshape((-1, 1)) + assert np.allclose(p_int, p_known) + def test_segment_fully_overlapped(self): # One line is fully covered by another From 4cb1a709dca74280d16258bf26549f3863ba6049 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Fri, 10 Feb 2017 13:20:17 +0100 Subject: [PATCH 084/153] Function to check if polyline is aranged in counter-clockwise manner. --- basics.py | 28 ++++++++++++++++++++++++++++ tests/test_ccw.py | 25 +++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 tests/test_ccw.py diff --git a/basics.py b/basics.py index dae7a4c70f..dffc6be665 100644 --- a/basics.py +++ b/basics.py @@ -205,6 +205,34 @@ def add_point(vertices, pt, precision=1e-3, **kwargs): #----------------------------------------------------------- +def is_ccw(p1, p2, p3): + """ + Check if the line segments formed by three points is part of a + conuter-clockwise circle. + + The test is positiv if p3 lies to left of the line running through p1 and + p2. + + The function is intended for 2D points; higher-dimensional coordinates will + be ignored. + + Parameters: + p1 (np.ndarray, length 2): Point on dividing line + p2 (np.ndarray, length 2): Point on dividing line + p3 (np.ndarray, length 2): Point to be tested + + Returns: + boolean, true if the points form a ccw polyline. + """ + + # Compute cross product between p1-p2 and p1-p3. Right hand rule gives that + # p3 is to the left if the cross product is positive. + return (p2[0] - p1[0]) * (p3[1] - p1[1]) \ + > (p2[1] - p1[1]) * (p3[0] - p1[0]) + +#----------------------------------------------------------------------------- + + def dist_point_pointset(p, pset, exponent=2): """ Compute distance between a point and a set of points. diff --git a/tests/test_ccw.py b/tests/test_ccw.py new file mode 100644 index 0000000000..19cba42813 --- /dev/null +++ b/tests/test_ccw.py @@ -0,0 +1,25 @@ +import numpy as np +import unittest + +from compgeom import basics as cg + + +class TestCCW(unittest.TestCase): + + def setup(self): + p1 = np.array([0, 0]) + p2 = np.array([1, 0]) + p3 = np.array([1, 1]) + return p1, p2, p3 + + def test_is_ccw(self): + p1, p2, p3 = self.setup() + assert cg.is_ccw(p1, p2, p3) + + def test_not_ccw(self): + p1, p2, p3 = self.setup() + assert not cg.is_ccw(p1, p3, p2) + + if __name__ == '__main__': + unittest.main() + From 55a876f694378e81c0b104955035a97628ef5ed8 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Fri, 10 Feb 2017 14:31:55 +0100 Subject: [PATCH 085/153] Function to test if points are inside a polygon. A similar functionality can be found in sympy.geometry, but this version should be faster. --- basics.py | 30 +++++++++++++++++++ tests/test_inside_polygon.py | 56 ++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 tests/test_inside_polygon.py diff --git a/basics.py b/basics.py index dffc6be665..ca85652866 100644 --- a/basics.py +++ b/basics.py @@ -232,6 +232,36 @@ def is_ccw(p1, p2, p3): #----------------------------------------------------------------------------- +def is_inside_polygon(poly, p): + """ + Check if a set of points are inside a polygon. + + Paremeters: + poly (np.ndarray, 2 x n): vertexes of polygon. The segments are formed by + connecting subsequent columns of poly + p (np.ndarray, 2 x n2): Points to be tested. + + Returns: + np.ndarray, boolean: Length equal to p, true if the point is inside the + polygon. + + """ + if p.ndim == 1: + pt = p.reshape((-1, 1)) + else: + pt = p + + poly_size = poly.shape[1] + + inside = np.ones(pt.shape[1], dtype=np.bool) + for i in range(pt.shape[1]): + for j in range(poly.shape[1]): + if not is_ccw(poly[:, j], poly[:, (j+1) % poly_size], pt[:, i]): + inside[i] = False + return inside + + +#----------------------------------------------------------------------------- def dist_point_pointset(p, pset, exponent=2): """ diff --git a/tests/test_inside_polygon.py b/tests/test_inside_polygon.py new file mode 100644 index 0000000000..4256d5cf29 --- /dev/null +++ b/tests/test_inside_polygon.py @@ -0,0 +1,56 @@ +import numpy as np +import unittest + +from compgeom import basics as cg + +class TestInsidePolygon(unittest.TestCase): + + def setup(self): + return np.array([[0, 1, 1, 0], [0, 0, 1, 1]]) + + def test_inside(self): + poly = self.setup() + + p = np.array([0.5, 0.5]) + assert np.all(cg.is_inside_polygon(poly, p)) + + def test_outside(self): + poly = self.setup() + p = np.array([2, 2]) + + inside = cg.is_inside_polygon(poly, p) + + assert not inside[0] + + def test_on_line(self): + # Point on the line, but not the segment, of the polygon + poly = self.setup() + p = np.array([2, 0]) + + inside = cg.is_inside_polygon(poly, p) + assert not inside[0] + + def test_on_boundary(self): + poly = self.setup() + p = np.array([0, 0.5]) + + inside = cg.is_inside_polygon(poly, p) + assert not inside[0] + + def test_just_inside(self): + poly = self.setup() + p = np.array([0.5, 1e-6]) + assert cg.is_inside_polygon(poly, p) + + def test_multiple_points(self): + poly = self.setup() + p = np.array([[0.5, 0.5], [0.5, 1.5]]) + + inside = cg.is_inside_polygon(poly, p) + + assert inside[0] + assert not inside[1] + + if __name__ == '__main__': + unittest.main() + From 589aac39d814f2bdcb9ff09530bdf8568725b513 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Fri, 10 Feb 2017 14:54:35 +0100 Subject: [PATCH 086/153] Added TODOS to functions in basics. --- basics.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/basics.py b/basics.py index ca85652866..19c860422b 100644 --- a/basics.py +++ b/basics.py @@ -216,6 +216,10 @@ def is_ccw(p1, p2, p3): The function is intended for 2D points; higher-dimensional coordinates will be ignored. + TODO: + Include the option of a tolerance, and a default value (if within the + tolerance). + Parameters: p1 (np.ndarray, length 2): Point on dividing line p2 (np.ndarray, length 2): Point on dividing line @@ -236,6 +240,10 @@ def is_inside_polygon(poly, p): """ Check if a set of points are inside a polygon. + TODO: + Include the option of a tolerance, and a default value (if within the + tolerance). + Paremeters: poly (np.ndarray, 2 x n): vertexes of polygon. The segments are formed by connecting subsequent columns of poly From aab3e3e90cdcdb3e5d29383d9566aaba18b83e35 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Fri, 10 Feb 2017 14:56:37 +0100 Subject: [PATCH 087/153] Slight performance increase in test if points are inside polygons. --- basics.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/basics.py b/basics.py index 19c860422b..f8a4879b83 100644 --- a/basics.py +++ b/basics.py @@ -266,6 +266,8 @@ def is_inside_polygon(poly, p): for j in range(poly.shape[1]): if not is_ccw(poly[:, j], poly[:, (j+1) % poly_size], pt[:, i]): inside[i] = False + # No need to check the remaining segments of the polygon. + break return inside From e28ca5041538cb3f014d568e63bcf30a215a0208 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Fri, 10 Feb 2017 15:26:12 +0100 Subject: [PATCH 088/153] Check for polyline ccw can now use tolerance to account for rounding errors. --- basics.py | 23 ++++++++++++++++------- tests/test_ccw.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/basics.py b/basics.py index f8a4879b83..6cca51bff9 100644 --- a/basics.py +++ b/basics.py @@ -205,7 +205,7 @@ def add_point(vertices, pt, precision=1e-3, **kwargs): #----------------------------------------------------------- -def is_ccw(p1, p2, p3): +def is_ccw(p1, p2, p3, tol=0, default=False): """ Check if the line segments formed by three points is part of a conuter-clockwise circle. @@ -216,14 +216,15 @@ def is_ccw(p1, p2, p3): The function is intended for 2D points; higher-dimensional coordinates will be ignored. - TODO: - Include the option of a tolerance, and a default value (if within the - tolerance). - Parameters: p1 (np.ndarray, length 2): Point on dividing line p2 (np.ndarray, length 2): Point on dividing line p3 (np.ndarray, length 2): Point to be tested + tol (double, optional): Tolerance used in the comparison, can be used + to account for rounding errors. Defaults to zero. + default (boolean, optional): Mode returned if the point is within the + tolerance. Should be set according to what is desired behavior of + the function (will vary with application). Defaults to False. Returns: boolean, true if the points form a ccw polyline. @@ -231,8 +232,16 @@ def is_ccw(p1, p2, p3): # Compute cross product between p1-p2 and p1-p3. Right hand rule gives that # p3 is to the left if the cross product is positive. - return (p2[0] - p1[0]) * (p3[1] - p1[1]) \ - > (p2[1] - p1[1]) * (p3[0] - p1[0]) + cross_product = (p2[0] - p1[0]) * (p3[1] - p1[1])\ + -(p2[1] - p1[1]) * (p3[0] - p1[0]) + + # Should there be a scaling of the tolerance relative to the distance + # between the points? + + if np.abs(cross_product) <= tol: + return default + else: + return cross_product > 0 #----------------------------------------------------------------------------- diff --git a/tests/test_ccw.py b/tests/test_ccw.py index 19cba42813..08ea3ac65d 100644 --- a/tests/test_ccw.py +++ b/tests/test_ccw.py @@ -12,6 +12,12 @@ def setup(self): p3 = np.array([1, 1]) return p1, p2, p3 + def setup_close(self, y): + p1 = np.array([0, 0]) + p2 = np.array([2, 0]) + p3 = np.array([1, y]) + return p1, p2, p3 + def test_is_ccw(self): p1, p2, p3 = self.setup() assert cg.is_ccw(p1, p2, p3) @@ -20,6 +26,31 @@ def test_not_ccw(self): p1, p2, p3 = self.setup() assert not cg.is_ccw(p1, p3, p2) + def test_tolerance(self): + p1, p2, p3 = self.setup_close(1e-6) + + # Safety margin saves ut + assert cg.is_ccw(p1, p2, p3, tol=1e-4, default=True) + + # Default kills us, even though we're inside safety margin + assert not cg.is_ccw(p1, p2, p3, tol=1e-4, default=False) + + # Outside safety margin, and on the ccw side + assert cg.is_ccw(p1, p2, p3, tol=1e-8, default=False) + + def test_tolerance_outside(self): + p1, p2, p3 = self.setup_close(-1e-6) + + # Safety margin saves ut + assert cg.is_ccw(p1, p2, p3, tol=1e-4, default=True) + + # Default kills us, even though we're inside safety margin + assert not cg.is_ccw(p1, p2, p3, tol=1e-4, default=False) + + # Outside safety margin, and not on the ccw side + assert not cg.is_ccw(p1, p2, p3, tol=1e-8, default=False) + + if __name__ == '__main__': unittest.main() From e8148981f6454bd753d1698b4cda3a046cc75e20 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Fri, 10 Feb 2017 15:30:43 +0100 Subject: [PATCH 089/153] Test for point inside polygons checks can use tolerance in computations. --- basics.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/basics.py b/basics.py index 6cca51bff9..98fe0060eb 100644 --- a/basics.py +++ b/basics.py @@ -245,18 +245,18 @@ def is_ccw(p1, p2, p3, tol=0, default=False): #----------------------------------------------------------------------------- -def is_inside_polygon(poly, p): +def is_inside_polygon(poly, p, tol=0, default=False): """ Check if a set of points are inside a polygon. - TODO: - Include the option of a tolerance, and a default value (if within the - tolerance). - Paremeters: poly (np.ndarray, 2 x n): vertexes of polygon. The segments are formed by connecting subsequent columns of poly p (np.ndarray, 2 x n2): Points to be tested. + tol (double, optional): Tolerance for rounding errors. Defaults to + zero. + default (boolean, optional): Default behavior if the point is close to + the boundary of the polygon. Defaults to False. Returns: np.ndarray, boolean: Length equal to p, true if the point is inside the @@ -273,7 +273,8 @@ def is_inside_polygon(poly, p): inside = np.ones(pt.shape[1], dtype=np.bool) for i in range(pt.shape[1]): for j in range(poly.shape[1]): - if not is_ccw(poly[:, j], poly[:, (j+1) % poly_size], pt[:, i]): + if not is_ccw(poly[:, j], poly[:, (j+1) % poly_size], pt[:, i], + tol=tol, default=default): inside[i] = False # No need to check the remaining segments of the polygon. break From ea0dabd47526f34fe63b5e2df04d600f64035029 Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Mon, 13 Feb 2017 13:52:14 +0100 Subject: [PATCH 090/153] if the compute_normal fails try again with less points --- basics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basics.py b/basics.py index aac50b1340..96e8b3ae19 100644 --- a/basics.py +++ b/basics.py @@ -498,7 +498,7 @@ def compute_normal(pts): assert pts.shape[1] > 2 normal = np.cross( pts[:,0] - pts[:,1], compute_tangent(pts) ) - assert not np.allclose( normal, np.zeros(3) ) + if np.allclose( normal, np.zeros(3) ): return compute_normal(pts[:, 1:]) return normal / np.linalg.norm( normal ) #------------------------------------------------------------------------------# From 8bf330ccc9bd4d06535df11cac764c321e2db67b Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Thu, 16 Feb 2017 17:27:15 +0100 Subject: [PATCH 091/153] bug fix in the map_grid function, now we care also for translated grids --- basics.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/basics.py b/basics.py index 96e8b3ae19..eac9450576 100644 --- a/basics.py +++ b/basics.py @@ -577,7 +577,8 @@ def map_grid(g): v = compute_normal(g.nodes) if g.dim==2 else compute_tangent(g.nodes) R = project_plane_matrix(g.nodes, v) face_centers = np.dot(R, face_centers) - dim = np.logical_not(np.isclose(np.sum(np.abs(face_centers),axis=1),0)) + dim = np.logical_not(np.isclose(np.sum(np.abs(face_centers).T- + np.abs(face_centers[:,0]), axis=0),0)) assert g.dim == np.sum(dim) face_centers = face_centers[dim,:] cell_centers = np.dot(R, cell_centers)[dim,:] From 8a755e7a8ecf53c2f3dba7bb9583c2f99c5967ce Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Thu, 16 Feb 2017 17:27:39 +0100 Subject: [PATCH 092/153] return the parameter dim which indicates the active coordinates --- basics.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/basics.py b/basics.py index eac9450576..735b007b54 100644 --- a/basics.py +++ b/basics.py @@ -565,6 +565,7 @@ def map_grid(g): face_normals: (g.dim x g.num_faces) the mapped normals of the faces. face_centers: (g.dim x g.num_faces) the mapped centers of the faces. R: (3 x 3) the rotation matrix used. + dim: indicates which are the dimensions active """ @@ -584,7 +585,7 @@ def map_grid(g): cell_centers = np.dot(R, cell_centers)[dim,:] face_normals = np.dot(R, face_normals)[dim,:] - return cell_centers, face_normals, face_centers, R + return cell_centers, face_normals, face_centers, R, dim #------------------------------------------------------------------------------# From 52a86ab8a34c27324c26d84a0e176cb9c443e115 Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Fri, 17 Feb 2017 17:32:22 +0100 Subject: [PATCH 093/153] bug fix in map_grid --- basics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/basics.py b/basics.py index 735b007b54..d88b53dcdb 100644 --- a/basics.py +++ b/basics.py @@ -578,8 +578,8 @@ def map_grid(g): v = compute_normal(g.nodes) if g.dim==2 else compute_tangent(g.nodes) R = project_plane_matrix(g.nodes, v) face_centers = np.dot(R, face_centers) - dim = np.logical_not(np.isclose(np.sum(np.abs(face_centers).T- - np.abs(face_centers[:,0]), axis=0),0)) + dim = np.logical_not(np.isclose(np.sum(np.abs(face_centers.T- + face_centers[:,0]), axis=0),0)) assert g.dim == np.sum(dim) face_centers = face_centers[dim,:] cell_centers = np.dot(R, cell_centers)[dim,:] From 4bdfda0fd3ce6c08a71c1dd9928d399a3b9e5702 Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Fri, 17 Feb 2017 17:32:39 +0100 Subject: [PATCH 094/153] add 0d case in map_grid --- basics.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/basics.py b/basics.py index d88b53dcdb..a32ffd496c 100644 --- a/basics.py +++ b/basics.py @@ -568,13 +568,15 @@ def map_grid(g): dim: indicates which are the dimensions active """ - cell_centers = g.cell_centers face_normals = g.face_normals face_centers = g.face_centers R = np.eye(3) - if g.dim != 3: + if g.dim == 0: + return cell_centers, face_normals, face_centers, R, np.ones(3,dtype=bool) + + if g.dim == 1 or g.dim == 2: v = compute_normal(g.nodes) if g.dim==2 else compute_tangent(g.nodes) R = project_plane_matrix(g.nodes, v) face_centers = np.dot(R, face_centers) From 1aa1caf4b08a3c5c66bfedb1229b54cf4513a4a4 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Wed, 22 Feb 2017 09:55:21 +0100 Subject: [PATCH 095/153] Method to test if polygon vertices have ccw ordering. --- basics.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/basics.py b/basics.py index 98fe0060eb..a503228ac5 100644 --- a/basics.py +++ b/basics.py @@ -205,6 +205,46 @@ def add_point(vertices, pt, precision=1e-3, **kwargs): #----------------------------------------------------------- +def is_polygon_ccw(poly): + """ + Determine if the vertices of a polygon are sorted counter clockwise. + + The method computes the winding number of the polygon, see + http://stackoverflow.com/questions/1165647/how-to-determine-if-a-list-of-polygon-points-are-in-clockwise-order + and + http://blog.element84.com/polygon-winding.html + + for descriptions of the algorithm. + + The algorithm should work for non-convex polygons. If the polygon is + self-intersecting (shaped like the number 8), the number returned will + reflect whether the method is 'mostly' cw or ccw. + + Parameters: + poly (np.ndarray, 2xn): Polygon vertices. + + Returns: + boolean, True if the polygon is ccw. + + Examples: + >>> is_polygon_ccw(np.array([[0, 1, 0], [0, 0, 1]])) + True + + >>> is_polygon_ccw(np.array([[0, 0, 1], [0, 1, 0]])) + False + + """ + p_0 = np.append(poly[0], poly[0, 0]) + p_1 = np.append(poly[1], poly[1, 0]) + + num_p = poly.shape[1] + value = 0 + for i in range(num_p): + value += (p_1[i+1] + p_1[i]) * (p_0[i+1] - p_0[i]) + return value < 0 + +#---------------------------------------------------------- + def is_ccw(p1, p2, p3, tol=0, default=False): """ Check if the line segments formed by three points is part of a From ac0af267416387e9f76c89f0c18af104de3e21a0 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Wed, 22 Feb 2017 09:55:50 +0100 Subject: [PATCH 096/153] Made it possible to run doctests for basic functions. --- basics.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/basics.py b/basics.py index a503228ac5..e0315d1292 100644 --- a/basics.py +++ b/basics.py @@ -1236,3 +1236,6 @@ def distance_segment_segment(s1_start, s1_end, s2_start, s2_end): dist = d_starts + sc * d1 - tc * d2 return np.sqrt(dist.dot(dist)) +if __name__ == "__main__": + import doctest + doctest.testmod() From 491aa70a6cf664891f8caddbbcaf1c616b36a586 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Wed, 22 Feb 2017 10:03:33 +0100 Subject: [PATCH 097/153] Changed name of function to test if polygon is ccw The previous name made less sense. --- basics.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/basics.py b/basics.py index e0315d1292..b62276cbfb 100644 --- a/basics.py +++ b/basics.py @@ -205,7 +205,7 @@ def add_point(vertices, pt, precision=1e-3, **kwargs): #----------------------------------------------------------- -def is_polygon_ccw(poly): +def is_ccw_polygon(poly): """ Determine if the vertices of a polygon are sorted counter clockwise. @@ -226,12 +226,15 @@ def is_polygon_ccw(poly): Returns: boolean, True if the polygon is ccw. + See also: + is_ccw_polyline + Examples: - >>> is_polygon_ccw(np.array([[0, 1, 0], [0, 0, 1]])) - True + >>> is_ccw_polygon(np.array([[0, 1, 0], [0, 0, 1]])) + True - >>> is_polygon_ccw(np.array([[0, 0, 1], [0, 1, 0]])) - False + >>> is_ccw_polygon(np.array([[0, 0, 1], [0, 1, 0]])) + False """ p_0 = np.append(poly[0], poly[0, 0]) From d020085dd9b306460e647c5306d2a9a6440777a0 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Wed, 22 Feb 2017 10:08:23 +0100 Subject: [PATCH 098/153] Changed name of function to test for polyline ccw. This should reduce chances of conflict with test for polygon ccw. --- basics.py | 15 +++++++++++---- tests/test_ccw.py | 16 ++++++++-------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/basics.py b/basics.py index b62276cbfb..d306381a67 100644 --- a/basics.py +++ b/basics.py @@ -248,7 +248,7 @@ def is_ccw_polygon(poly): #---------------------------------------------------------- -def is_ccw(p1, p2, p3, tol=0, default=False): +def is_ccw_polyline(p1, p2, p3, tol=0, default=False): """ Check if the line segments formed by three points is part of a conuter-clockwise circle. @@ -259,6 +259,9 @@ def is_ccw(p1, p2, p3, tol=0, default=False): The function is intended for 2D points; higher-dimensional coordinates will be ignored. + Extensions to lines with more than three points should be straightforward, + the input points should be merged into a 2d array. + Parameters: p1 (np.ndarray, length 2): Point on dividing line p2 (np.ndarray, length 2): Point on dividing line @@ -271,13 +274,17 @@ def is_ccw(p1, p2, p3, tol=0, default=False): Returns: boolean, true if the points form a ccw polyline. + + See also: + is_ccw_polygon + """ # Compute cross product between p1-p2 and p1-p3. Right hand rule gives that # p3 is to the left if the cross product is positive. cross_product = (p2[0] - p1[0]) * (p3[1] - p1[1])\ -(p2[1] - p1[1]) * (p3[0] - p1[0]) - + # Should there be a scaling of the tolerance relative to the distance # between the points? @@ -316,8 +323,8 @@ def is_inside_polygon(poly, p, tol=0, default=False): inside = np.ones(pt.shape[1], dtype=np.bool) for i in range(pt.shape[1]): for j in range(poly.shape[1]): - if not is_ccw(poly[:, j], poly[:, (j+1) % poly_size], pt[:, i], - tol=tol, default=default): + if not is_ccw_polyline(poly[:, j], poly[:, (j+1) % poly_size], + pt[:, i], tol=tol, default=default): inside[i] = False # No need to check the remaining segments of the polygon. break diff --git a/tests/test_ccw.py b/tests/test_ccw.py index 08ea3ac65d..0fbae50e22 100644 --- a/tests/test_ccw.py +++ b/tests/test_ccw.py @@ -20,35 +20,35 @@ def setup_close(self, y): def test_is_ccw(self): p1, p2, p3 = self.setup() - assert cg.is_ccw(p1, p2, p3) + assert cg.is_ccw_polyline(p1, p2, p3) def test_not_ccw(self): p1, p2, p3 = self.setup() - assert not cg.is_ccw(p1, p3, p2) + assert not cg.is_ccw_polyline(p1, p3, p2) def test_tolerance(self): p1, p2, p3 = self.setup_close(1e-6) # Safety margin saves ut - assert cg.is_ccw(p1, p2, p3, tol=1e-4, default=True) + assert cg.is_ccw_polyline(p1, p2, p3, tol=1e-4, default=True) # Default kills us, even though we're inside safety margin - assert not cg.is_ccw(p1, p2, p3, tol=1e-4, default=False) + assert not cg.is_ccw_polyline(p1, p2, p3, tol=1e-4, default=False) # Outside safety margin, and on the ccw side - assert cg.is_ccw(p1, p2, p3, tol=1e-8, default=False) + assert cg.is_ccw_polyline(p1, p2, p3, tol=1e-8, default=False) def test_tolerance_outside(self): p1, p2, p3 = self.setup_close(-1e-6) # Safety margin saves ut - assert cg.is_ccw(p1, p2, p3, tol=1e-4, default=True) + assert cg.is_ccw_polyline(p1, p2, p3, tol=1e-4, default=True) # Default kills us, even though we're inside safety margin - assert not cg.is_ccw(p1, p2, p3, tol=1e-4, default=False) + assert not cg.is_ccw_polyline(p1, p2, p3, tol=1e-4, default=False) # Outside safety margin, and not on the ccw side - assert not cg.is_ccw(p1, p2, p3, tol=1e-8, default=False) + assert not cg.is_ccw_polyline(p1, p2, p3, tol=1e-8, default=False) if __name__ == '__main__': From 30192c3dac896dd68b6798622bf0d8ea3260a414 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Wed, 1 Mar 2017 12:06:54 +0100 Subject: [PATCH 099/153] Added comment in basic functions. --- basics.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/basics.py b/basics.py index d306381a67..27a64e0294 100644 --- a/basics.py +++ b/basics.py @@ -299,6 +299,8 @@ def is_inside_polygon(poly, p, tol=0, default=False): """ Check if a set of points are inside a polygon. + The method assumes that the polygon is convex. + Paremeters: poly (np.ndarray, 2 x n): vertexes of polygon. The segments are formed by connecting subsequent columns of poly From 373268cec44c84d450cf69c8b02e27a4acaa39e0 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Wed, 1 Mar 2017 12:07:31 +0100 Subject: [PATCH 100/153] Removed dependency on sympy in intersection between polygons. Enhanced computational speed. --- basics.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/basics.py b/basics.py index 27a64e0294..8f1d166c42 100644 --- a/basics.py +++ b/basics.py @@ -810,24 +810,21 @@ def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): # x and y-coordinate for z=0 x0 = pt_1[0] + dx * t y0 = pt_1[1] + dy * t - # Sympy representation - p_00 = geom.Point2D(x0, y0) + # Representation as point + p_00 = np.array([x0, y0]).reshape((-1, 1)) + # Check if the first polygon encloses the point. If the - # intersection is on the border, this will not be detected, see - # documentation is sympy. - # TODO: Replace this with a self-written test to improve - # performance - if poly_1_sp.encloses_point(p_00): - # Back to physical coordinates by 1) converting to numpy - # format, 2) expand to 3D, 3) inverse rotation, 4) - # translate to original coordinate. - isect = np.hstack((isect, - irot.dot(_to3D(_p2np(p_00))) + + # intersection is on the border, this will not be detected. + + if is_inside_polygon(poly_1_xy, p_00, tol=tol): + # Back to physical coordinates by 1) expand to 3D, 2) + # inverse rotation, 3) translate to original coordinate. + isect = np.hstack((isect, irot.dot(_to3D(p_00)) + center_1)) if isect.shape[1] == 0: isect = None - + return isect From 978030a8435b1a45fb7d1a6573d53e0f76af6a8f Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Wed, 1 Mar 2017 12:08:15 +0100 Subject: [PATCH 101/153] Test for point colinearity accepts tolerance. Default value is set to the same as used in numpy.all_close, thus the behavior of the function should be fixed. --- basics.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/basics.py b/basics.py index 8f1d166c42..4bd4d449a4 100644 --- a/basics.py +++ b/basics.py @@ -1090,14 +1090,15 @@ def compute_tangent(pts): #------------------------------------------------------------------------------# -def is_collinear(pts): +def is_collinear(pts, tol=1e-5): """ Check if the points lie on a line. Parameters: - pts (np.ndarray, 3xn): the points. + pts (np.ndarray, 3xn): the points. + tol (double, optional): Tolerance used in comparison. Defaults to 1e-5. Returns: - check, bool, if the points lie on a line or not. + boolean, True if the points lie on a line. """ @@ -1109,7 +1110,7 @@ def is_collinear(pts): coll = np.array( [ np.linalg.norm( np.cross( p - pt0, pt1 - pt0 ) ) \ for p in pts[:,1:-1].T ] ) - return np.allclose(coll, np.zeros(coll.size)) + return np.allclose(coll, np.zeros(coll.size), tol) #------------------------------------------------------------------------------# From 6d474ee52992cea46f625f89d0dfec851a220227 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Wed, 1 Mar 2017 12:09:13 +0100 Subject: [PATCH 102/153] Minor formating of unit test for polygon intersections. --- tests/test_polygon_segment_intersection.py | 40 +++++++++++----------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/test_polygon_segment_intersection.py b/tests/test_polygon_segment_intersection.py index c8462687f5..a651deaf6b 100644 --- a/tests/test_polygon_segment_intersection.py +++ b/tests/test_polygon_segment_intersection.py @@ -5,7 +5,6 @@ class PolygonSegmentIntersectionTest(unittest.TestCase): - def setup_polygons(self): p_1 = np.array([[-1, 1, 1, -1 ], [0, 0, 0, 0], [-1, -1, 1, 1]]) p_2 = np.array([[0, 0, 0, 0], [-1, 1, 1, -1 ], [-.7, -.7, .8, .8]]) @@ -16,73 +15,74 @@ def setup_polygons(self): def test_one_intersection(self): p_1, p_2, *rest = self.setup_polygons() - + # First intersection of 1 by edges of 2. It should be two of these p_1_2 = basics.polygon_segment_intersect(p_1, p_2) p_i_known_1 = np.array([0, 0, -0.7]).reshape((-1, 1)) p_i_known_2 = np.array([0, 0, 0.8]).reshape((-1, 1)) assert np.min(np.sum(np.abs(p_1_2 - p_i_known_1), axis=0)) < 1e-5 assert np.min(np.sum(np.abs(p_1_2 - p_i_known_2), axis=0)) < 1e-5 - + # Then intersection of plane of 2 by edges of 1. This should be empty p_2_1 = basics.polygon_segment_intersect(p_2, p_1) assert p_2_1 is None - + def test_mutual_intersection(self): p1, _, p3, *rest = self.setup_polygons() - + # First intersection of 1 by edges of 3 p_1_3 = basics.polygon_segment_intersect(p1, p3) p_i_known_1 = np.array([0, 0, 0.5]).reshape((-1, 1)) p_i_known_2 = np.array([0, 0, 1.0]).reshape((-1, 1)) assert np.min(np.sum(np.abs(p_1_3 - p_i_known_1), axis=0)) < 1e-5 - + # Then intersection of plane of 3 by edges of 1. p_3_1 = basics.polygon_segment_intersect(p3, p1) p_i_known_2 = np.array([0, 0, 1.0]).reshape((-1, 1)) - + assert np.min(np.sum(np.abs(p_3_1 - p_i_known_2), axis=0)) < 1e-5 - + def test_mutual_intersection_not_at_origin(self): p1, _, p3, *rest = self.setup_polygons() - + incr = np.array([1, 2, 3]).reshape((-1, 1)) p1 += incr p3 += incr - + # First intersection of 1 by edges of 3 p_1_3 = basics.polygon_segment_intersect(p1, p3) p_i_known_1 = np.array([0, 0, 0.5]).reshape((-1, 1)) + incr assert np.min(np.sum(np.abs(p_1_3 - p_i_known_1), axis=0)) < 1e-5 - + # Then intersection of plane of 3 by edges of 1. p_3_1 = basics.polygon_segment_intersect(p3, p1) p_i_known_2 = np.array([0, 0, 1.0]).reshape((-1, 1)) + incr - + assert np.min(np.sum(np.abs(p_3_1 - p_i_known_2), axis=0)) < 1e-5 - + def test_parallel_planes(self): p_1, *rest = self.setup_polygons() p_2 = p_1 + np.array([0, 1, 0]).reshape((-1, 1)) isect = basics.polygon_segment_intersect(p_1, p_2) assert isect is None - + def test_extension_would_intersect(self): # The extension of p_2 would intersect, but should detect nothing p_1, p_2, *rest = self.setup_polygons() p_2 += np.array([2, 0, 0]).reshape((-1, 1)) isect = basics.polygon_segment_intersect(p_1, p_2) assert isect is None - + def test_segments_intersect(self): - # Test where the planes intersect in a way where segments only touches segments, which does not qualify as intersection + # Test where the planes intersect in a way where segments only touches + # segments, which does not qualify as intersection. p_1, _, _, p_4, *rest = self.setup_polygons() isect = basics.polygon_segment_intersect(p_1, p_4) assert isect is None # Also try the other way around isect = basics.polygon_segment_intersect(p_4, p_1) assert isect is None - + def test_segments_same_plane_no_isect(self): # Polygons in the same plane, but no intersection p_1, *rest = self.setup_polygons() @@ -91,9 +91,10 @@ def test_segments_same_plane_no_isect(self): assert isect is None isect = basics.polygon_segment_intersect(p_2, p_1) assert isect is None - + def test_segments_same_plane_isect(self): - # Polygons in the same plane, and intersection. Should raise an exception + # Polygons in the same plane, and intersection. Should raise an + # exception. p_1, *rest = self.setup_polygons() p_2 = p_1 + np.array([1, 0, 0]).reshape((-1, 1)) caught_exp = False @@ -106,4 +107,3 @@ def test_segments_same_plane_isect(self): if __name__ == '__main__': unittest.main() - From 493a60157b2b8ef8a194a43dc2f50d2c0ecdc9d5 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Wed, 1 Mar 2017 12:12:34 +0100 Subject: [PATCH 103/153] Removed unnecessary import from basic geometry computation --- basics.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/basics.py b/basics.py index 4bd4d449a4..5937a6223e 100644 --- a/basics.py +++ b/basics.py @@ -614,7 +614,6 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): return None #----------------------------------------------------------------------------# -from compgeom import basics # Represent the polygon as a sympy polygon def _np2p(p): @@ -708,8 +707,6 @@ def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): TODO: 1) Also cover case where the one polygon ends in the plane of the other. - 2) Replace sympy.in_polygon with self-implemented check, should be - simple using ccw. Parameters: poly_1 (np.ndarray, 3xn1): Vertexes of polygon, assumed ordered as cw or @@ -737,7 +734,7 @@ def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): poly_2 = poly_2 - center_1 # Obtain the rotation matrix that projects p1 to the xy-plane - rot = basics.project_plane_matrix(poly_1) + rot = project_plane_matrix(poly_1) irot = rot.transpose() poly_1_xy = rot.dot(poly_1) From 27307c56ff516838298501aed96fb31726540c96 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Wed, 1 Mar 2017 12:16:02 +0100 Subject: [PATCH 104/153] Formating of basic functions. --- basics.py | 80 ++++++++++++++++++++++++++----------------------------- 1 file changed, 38 insertions(+), 42 deletions(-) diff --git a/basics.py b/basics.py index 5937a6223e..fdd9e12932 100644 --- a/basics.py +++ b/basics.py @@ -338,23 +338,23 @@ def is_inside_polygon(poly, p, tol=0, default=False): def dist_point_pointset(p, pset, exponent=2): """ Compute distance between a point and a set of points. - + Parameters: p (np.ndarray): Point from which distances will be computed pset (nd.array): Point cloud to which we compute distances exponent (double, optional): Exponent of the norm used. Defaults to 2. - + Return: np.ndarray: Array of distances. - + """ - + # If p is 1D, do a reshape to facilitate broadcasting, but on a copy if p.ndim == 1: pt = p.reshape((-1, 1)) else: pt = p - + return np.power(np.sum(np.power(np.abs(pt - pset), exponent), axis=0), 1/exponent) @@ -483,8 +483,6 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): dy_2 = ye_2 - ys_2 dz_2 = ze_2 - zs_2 - - # The lines are parallel in the x-y plane, but we don't know about the # z-direction. CHeck this deltas_1 = np.array([dx_1, dy_1, dz_1]) @@ -504,7 +502,6 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): not_in_discr = np.setdiff1d(np.arange(3), in_discr)[0] discr = deltas_1[in_discr[0]] * deltas_2[in_discr[1]] - deltas_1[in_discr[1]] * deltas_2[in_discr[0]] - # An intersection will be a solution of the linear system # xs_1 + dx_1 * t_1 = xs_2 + dx_2 * t_2 (1) @@ -515,10 +512,6 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): # # The intersection is on the line segments if 0 <= (t_1, t_2) <= 1 - # Minus in front of _2 after reorganizing (1)-(2) -# discr = dx_1 * (-dy_2) - dy_1 *(-dx_2) - - # Either the lines are parallel in two directions if np.abs(discr) < tol: # If the lines are (almost) parallel, there is no single intersection, @@ -537,20 +530,21 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): if not np.allclose(t, t.mean(), tol): return None - # If we have made it this far, the lines are indeed parallel. Next, + # If we have made it this far, the lines are indeed parallel. Next, # check if they overlap, and if so, test if the segments are overlapping - # For dimensions with an incline, the vector between segment start points should be parallel to the segments - # Since the masks are equal, we can use any of them + # For dimensions with an incline, the vector between segment start + # points should be parallel to the segments. + # Since the masks are equal, we can use any of them. t_1_2 = (start_1[mask_1] - start_2[mask_1]) / deltas_1[mask_1] if (not np.allclose(t_1_2, t_1_2, tol)): - return None + return None # For dimensions with no incline, the start cooordinates should be the same if not np.allclose(start_1[~mask_1], start_2[~mask_1], tol): return None # We have overlapping lines! finally check if segments are overlapping. - + # Since everything is parallel, it suffices to work with a single coordinate s_1 = start_1[mask_1][0] e_1 = end_1[mask_1][0] @@ -586,15 +580,17 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): else: # Solve 2x2 system by Cramer's rule - discr = deltas_1[in_discr[0]] * (-deltas_2[in_discr[1]]) - deltas_1[in_discr[1]] * (-deltas_2[in_discr[0]]) - t_1 = ((start_2[in_discr[0]] - start_1[in_discr[0]]) * (-deltas_2[in_discr[1]]) - \ - (start_2[in_discr[1]] - start_1[in_discr[1]]) * (-deltas_2[in_discr[0]]))/discr + discr = deltas_1[in_discr[0]] * (-deltas_2[in_discr[1]]) -\ + deltas_1[in_discr[1]] * (-deltas_2[in_discr[0]]) + t_1 = ((start_2[in_discr[0]] - start_1[in_discr[0]]) \ + * (-deltas_2[in_discr[1]]) - \ + (start_2[in_discr[1]] - start_1[in_discr[1]]) \ + * (-deltas_2[in_discr[0]]))/discr - t_2 = (deltas_1[in_discr[0]] * (start_2[in_discr[1]] - start_1[in_discr[1]]) - \ - deltas_1[in_discr[1]] * (start_2[in_discr[0]] - start_1[in_discr[0]])) / discr - -# t_1 = ((xs_2 - xs_1) * (-dy_2) - (ys_2 - ys_1) * (-dx_2)) / discr -# t_2 = (dx_1 * (ys_2 - ys_1) - dy_1 * (xs_2 - xs_1)) / discr + t_2 = (deltas_1[in_discr[0]] * (start_2[in_discr[1]] - + start_1[in_discr[1]]) - \ + deltas_1[in_discr[1]] * (start_2[in_discr[0]] - + start_1[in_discr[0]])) / discr # Check that we are on line segment if t_1 < 0 or t_1 > 1 or t_2 < 0 or t_2 > 1: @@ -609,7 +605,6 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): vec[in_discr] = start_1[in_discr] + t_1 * deltas_1[in_discr] vec[not_in_discr] = z_1_isect return vec.reshape((-1, 1)) -# return np.array([xs_1 + t_1 * dx_1, ys_1 + t_1 * dy_1, z_1_isect]) else: return None @@ -640,27 +635,27 @@ def _to3D(p): def polygon_boundaries_intersect(poly_1, poly_2, tol=1e-8): """ Test for intersection between the bounding segments of two 3D polygons. - + No tests are done for intersections with polygons interiors. The code has only been tested for convex polygons, status for non-convex polygons is unknown. - - A segment can either be intersected by segments of the other polygon as + + A segment can either be intersected by segments of the other polygon as i) a single point ii) two points, hit by different segments (cannot be more than 2 for convex polygons) iii) along a line, if the segments are parallel. - + Note that if the polygons share a vertex, this point will be found multiple times (depending on the configuration of the polygons). - + Each intersection is represented as a list of three items. The first two are the segment index (numbered according to start point) of the intersecting segments. The third is the coordinates of the intersection point, this can either be a single point (3x1 nd.array), or a 3x2 array with columns representing the same (shared vertex) or different (shared segment) points. - + Paremeters: poly_1 (np.array, 3 x n_pt): First polygon, assumed to be non-intersecting. The closing segment is defined by the last and first @@ -669,20 +664,20 @@ def polygon_boundaries_intersect(poly_1, poly_2, tol=1e-8): non-intersecting. The closing segment is defined by the last and first column. tol (float, optional): Tolerance used for equality. - + Returns: list: of intersections. See above for description of data format. If no intersections are found, an empty list is returned. - + """ - + l_1 = poly_1.shape[1] ind_1 = np.append(np.arange(l_1), 0) l_2 = poly_2.shape[1] ind_2 = np.append(np.arange(l_2), 0) - + isect = [] - + for i in range(l_1): p_1_1 = poly_1[:, ind_1[i]] p_1_2 = poly_1[:, ind_1[i+1]] @@ -693,15 +688,15 @@ def polygon_boundaries_intersect(poly_1, poly_2, tol=1e-8): isect_loc = segments_intersect_3d(p_1_1, p_1_2, p_2_1, p_2_2) if isect_loc is not None: isect.append([i, j, isect_loc]) - + return isect #---------------------------------------------------------- def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): """ - Find intersections between polygons embeded in 3D. - + Find intersections between polygons embeded in 3D. + The intersections are defined as between the interior of the first polygon and the boundary of the second. @@ -725,9 +720,9 @@ def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): NotImplementedError if the two polygons overlap in a 2D area. An extension should not be difficult, but the function is not intended for this use. - + """ - + # First translate the points so that the first plane is located at the origin center_1 = np.mean(poly_1, axis=1).reshape((-1, 1)) poly_1 = poly_1 - center_1 @@ -1246,3 +1241,4 @@ def distance_segment_segment(s1_start, s1_end, s2_start, s2_end): if __name__ == "__main__": import doctest doctest.testmod() + From 67ccbadc6c607c00f106d2f15502ba07ed869a3d Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Wed, 1 Mar 2017 13:31:51 +0100 Subject: [PATCH 105/153] Partial reimplementation of intersection of line segments in 2d. Removed much of sympy dependencies, will improve speed. Further improvements possible. --- basics.py | 54 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/basics.py b/basics.py index fdd9e12932..aba622dca9 100644 --- a/basics.py +++ b/basics.py @@ -361,7 +361,7 @@ def dist_point_pointset(p, pset, exponent=2): #------------------------------------------------------------------------------# -def lines_intersect(start_1, end_1, start_2, end_2): +def lines_intersect(start_1, end_1, start_2, end_2, tol=1e-8): """ Check if two line segments defined by their start end endpoints, intersect. @@ -400,31 +400,48 @@ def lines_intersect(start_1, end_1, start_2, end_2): not intersect. """ - - - # It seems that if sympy is provided point coordinates as integers, it may - # do calculations in integers also, with an unknown approach to rounding. - # Cast the values to floats to avoid this. It is not the most pythonic - # style, but tracking down such a bug would have been a nightmare. start_1 = np.asarray(start_1).astype(np.float) end_1 = np.asarray(end_1).astype(np.float) start_2 = np.asarray(start_2).astype(np.float) end_2 = np.asarray(end_2).astype(np.float) - p1 = sympy.Point(start_1[0], start_1[1]) - p2 = sympy.Point(end_1[0], end_1[1]) - p3 = sympy.Point(start_2[0], start_2[1]) - p4 = sympy.Point(end_2[0], end_2[1]) + d_1 = end_1 - start_1 + d_2 = end_2 - start_2 - l1 = sympy.Segment(p1, p2) - l2 = sympy.Segment(p3, p4) + d_s = start_2 - start_1 - isect = l1.intersection(l2) - if isect is None or len(isect) == 0: - return None + discr = d_1[0] * d_2[1] - d_1[1] * d_2[0] + + if np.abs(discr) < tol: + start_cross_line = d_s[0] * d_1[1] - d_s[1] * d_1[0] + if np.abs(start_cross_line) < tol: + # Lines are co-linear + # This can be treated by straightforward algebra, but for the + # moment, we call upon sympy to solve this. + p1 = sympy.Point(start_1[0], start_1[1]) + p2 = sympy.Point(end_1[0], end_1[1]) + p3 = sympy.Point(start_2[0], start_2[1]) + p4 = sympy.Point(end_2[0], end_2[1]) + + l1 = sympy.Segment(p1, p2) + l2 = sympy.Segment(p3, p4) + + isect = l1.intersection(l2) + if isect is None or len(isect) == 0: + return None + else: + p = isect[0] + return np.array([[p.x], [p.y]], dtype='float') + else: + # Lines are parallel, but not colinear + return None else: - p = isect[0] - return np.array([[p.x], [p.y]], dtype='float') + t = (d_s[0] * d_2[1] - d_s[1] * d_2[0]) / discr + isect = start_1 + t * d_1 + if t >= 0 and t <= 1: + return np.array([[isect[0]], [isect[1]]]) + else: + return None #------------------------------------------------------------------------------# @@ -821,7 +838,6 @@ def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): #-----------------------------------------------------------------------------# - def remove_edge_crossings(vertices, edges, **kwargs): """ Process a set of points and connections between them so that the result From 5fe7cac715f490b2ace66230099591399a7973af Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Tue, 7 Mar 2017 09:30:23 +0100 Subject: [PATCH 106/153] Minor modification to polygon polygon intersection. Moved usage of sympy to where it is actually needed. Should speed things up a little bit. --- basics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/basics.py b/basics.py index aba622dca9..b5cbfd1195 100644 --- a/basics.py +++ b/basics.py @@ -754,8 +754,6 @@ def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): assert np.all(np.abs(poly_1_xy[2]) < tol) # Drop the z-coordinate poly_1_xy = poly_1_xy[:2] - # Convert the first polygon to sympy format - poly_1_sp = geom.Polygon(*_np2p(poly_1_xy)) # Rotate the second polygon with the same rotation matrix poly_2_rot = rot.dot(poly_2) @@ -774,6 +772,8 @@ def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): # The polygons are parallel, and in the same plane # Represent second polygon by sympy, and use sympy function to # detect intersection. + # Convert the first polygon to sympy format + poly_1_sp = geom.Polygon(*_np2p(poly_1_xy)) poly_2_sp = geom.Polygon(*_np2p(poly_2_rot[:2])) isect = poly_1_sp.intersection(poly_2_sp) From 46243cc14d79a74cf995f9637dff9f7681b5dbbf Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Tue, 7 Mar 2017 09:57:19 +0100 Subject: [PATCH 107/153] Bugfix in intersection between polygons. --- basics.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/basics.py b/basics.py index b5cbfd1195..11dffc8c78 100644 --- a/basics.py +++ b/basics.py @@ -807,15 +807,15 @@ def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): # Coordinates of this segment pt_1 = poly_2_rot[:, ind[i]] pt_2 = poly_2_rot[:, ind[i+1]] - dx = pt_1[0] - pt_2[0] - dy = pt_1[1] - pt_2[1] - dz = pt_1[2] - pt_2[2] + dx = pt_2[0] - pt_1[0] + dy = pt_2[1] - pt_1[1] + dz = pt_2[2] - pt_1[2] if np.abs(dz) > tol: # We are on a plane, and we know that dz_2 is non-zero, so all # individiual segments must have an incline. # Parametrize the line, find parameter value for intersection # with z=0. - t = (pt_1[2] - 0) / dz + t = (-pt_1[2] - 0) / dz # x and y-coordinate for z=0 x0 = pt_1[0] + dx * t y0 = pt_1[1] + dy * t From b440b4c820d4492f81f180498534d66cc3e62f54 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Tue, 7 Mar 2017 10:20:56 +0100 Subject: [PATCH 108/153] Line segment intersection in 2d handles overlapping segments (colinear lines) Also new unit tests. --- basics.py | 58 ++++++++++++++++++++------------- tests/test_intersections.py | 65 +++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 23 deletions(-) diff --git a/basics.py b/basics.py index 11dffc8c78..cb08898e9e 100644 --- a/basics.py +++ b/basics.py @@ -367,11 +367,6 @@ def lines_intersect(start_1, end_1, start_2, end_2, tol=1e-8): The lines are assumed to be in 2D. - The function uses sympy to find intersections. At the moment (Jan 2017), - sympy is not very effective, so this may become a bottleneck if the method - is called repeatedly. An purely algebraic implementation is simple, but - somewhat cumbersome. - Note that, oposed to other functions related to grid generation such as remove_edge_crossings, this function does not use the concept of snap_to_grid. This may cause problems at some point, although no issues @@ -396,8 +391,12 @@ def lines_intersect(start_1, end_1, start_2, end_2, tol=1e-8): end_2 (np.ndarray or list): coordinates of end point for first line. Returns: - np.ndarray: coordinates of intersection point, or None if the lines do - not intersect. + np.ndarray (2 x num_pts): coordinates of intersection point, or the + endpoints of the intersection segments if relevant. If the lines do + not intersect, None is returned. + + Raises: + ValueError if the start and endpoints of a line are the same. """ start_1 = np.asarray(start_1).astype(np.float) @@ -413,25 +412,38 @@ def lines_intersect(start_1, end_1, start_2, end_2, tol=1e-8): discr = d_1[0] * d_2[1] - d_1[1] * d_2[0] if np.abs(discr) < tol: + # The lines are parallel, and will only cross if they are also colinear + + # Cross product between line 1 and line between start points on line start_cross_line = d_s[0] * d_1[1] - d_s[1] * d_1[0] if np.abs(start_cross_line) < tol: - # Lines are co-linear - # This can be treated by straightforward algebra, but for the - # moment, we call upon sympy to solve this. - p1 = sympy.Point(start_1[0], start_1[1]) - p2 = sympy.Point(end_1[0], end_1[1]) - p3 = sympy.Point(start_2[0], start_2[1]) - p4 = sympy.Point(end_2[0], end_2[1]) - - l1 = sympy.Segment(p1, p2) - l2 = sympy.Segment(p3, p4) - - isect = l1.intersection(l2) - if isect is None or len(isect) == 0: - return None + # The lines are co-linear + + # Write l1 on the form start_1 + t * d_1, find the parameter value + # needed for equality with start_2 and end_2 + if np.abs(d_1[0]) > tol: + t_start_2 = (start_2[0] - start_1[0])/d_1[0] + t_end_2 = (end_2[0] - start_1[0])/d_1[0] + elif np.abs(d_1[1]) > tol: + t_start_2 = (start_2[1] - start_1[1])/d_1[1] + t_end_2 = (end_2[1] - start_1[1])/d_1[1] else: - p = isect[0] - return np.array([[p.x], [p.y]], dtype='float') + # d_1 is zero + raise ValueError('Start and endpoint of line should be\ + different') + if t_start_2 < 0 and t_end_2 < 0: + return None + elif t_start_2 > 1 and t_end_2 > 1: + return None + # We have an overlap, find its parameter values + t_min = max(min(t_start_2, t_end_2), 0) + t_max = min(max(t_start_2, t_end_2), 1) + + assert t_max - t_min > tol + p_1 = start_1 + d_1 * t_min + p_2 = start_1 + d_1 * t_max + return np.array([[p_1[0], p_2[0]], [p_1[1], p_2[1]]]) + else: # Lines are parallel, but not colinear return None diff --git a/tests/test_intersections.py b/tests/test_intersections.py index c0eee65b9e..6bc09dc82e 100644 --- a/tests/test_intersections.py +++ b/tests/test_intersections.py @@ -107,5 +107,70 @@ def test_lines_intersect_segments_do_not(self): pi = basics.lines_intersect(s0, e0, s1, e1) assert(pi is None or len(pi)==0) + def test_parallel_not_colinear(self): + s0 = np.array([0, 0]) + e0 = np.array([1, 0]) + s1 = np.array([0, 1]) + e1 = np.array([1, 1]) + + pi = basics.lines_intersect(s0, e0, s1, e1) + assert pi is None + + def test_colinear_not_intersecting(self): + s0 = np.array([0, 0]) + e0 = np.array([1, 0]) + s1 = np.array([2, 0]) + e1 = np.array([3, 0]) + + pi = basics.lines_intersect(s0, e0, s1, e1) + assert pi is None + + def test_partly_overlapping_segments(self): + s0 = np.array([0, 0]) + e0 = np.array([2, 0]) + s1 = np.array([1, 0]) + e1 = np.array([3, 0]) + + pi = basics.lines_intersect(s0, e0, s1, e1) + assert (pi[0, 0] == 1 and pi[0, 1] == 2) or \ + (pi[0, 0] == 2 and pi[0, 1] == 1) + assert np.allclose(pi[1], 0) + + # Then test order of arguments + pi = basics.lines_intersect(e0, s0, s1, e1) + assert (pi[0, 0] == 1 and pi[0, 1] == 2) or \ + (pi[0, 0] == 2 and pi[0, 1] == 1) + assert np.allclose(pi[1], 0) + + pi = basics.lines_intersect(s0, e0, e1, s1) + assert (pi[0, 0] == 1 and pi[0, 1] == 2) or \ + (pi[0, 0] == 2 and pi[0, 1] == 1) + assert np.allclose(pi[1], 0) + + pi = basics.lines_intersect(e0, s0, e1, s1) + assert (pi[0, 0] == 1 and pi[0, 1] == 2) or \ + (pi[0, 0] == 2 and pi[0, 1] == 1) + assert np.allclose(pi[1], 0) + + def test_fully_overlapping_segments(self): + s0 = np.array([0, 0]) + e0 = np.array([3, 0]) + s1 = np.array([1, 0]) + e1 = np.array([2, 0]) + + pi = basics.lines_intersect(s0, e0, s1, e1) + assert (pi[0, 0] == 1 and pi[0, 1] == 2) or \ + (pi[0, 0] == 2 and pi[0, 1] == 1) + assert np.allclose(pi[1], 0) + + def test_meeting_in_point(self): + s0 = np.array([0, 0]) + e0 = np.array([1, 0]) + s1 = np.array([1, 0]) + e1 = np.array([2, 0]) + + pi = basics.lines_intersect(s0, e0, s1, e1) + assert pi[0, 0] == 1 and pi[1, 0] == 1 + if __name__ == '__main__': unittest.main() From b77f706241a2ed14ac03800bb4a9fc403a2c23bb Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Tue, 7 Mar 2017 10:49:42 +0100 Subject: [PATCH 109/153] Bugfix in line intersection in 2d Previous case failed for some lines sharing a single point. --- basics.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/basics.py b/basics.py index cb08898e9e..d498bc9ad8 100644 --- a/basics.py +++ b/basics.py @@ -439,7 +439,12 @@ def lines_intersect(start_1, end_1, start_2, end_2, tol=1e-8): t_min = max(min(t_start_2, t_end_2), 0) t_max = min(max(t_start_2, t_end_2), 1) - assert t_max - t_min > tol + if t_max - t_min < tol: + # It seems this can only happen if they are also equal to 0 or + # 1, that is, the lines share a single point + p_1 = start_1 + d_1 * t_min + return p_1.reshape((-1, 1)) + p_1 = start_1 + d_1 * t_min p_2 = start_1 + d_1 * t_max return np.array([[p_1[0], p_2[0]], [p_1[1], p_2[1]]]) From 58bc464b1d46de0d3b06d8e5a2918141452f1d3b Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Tue, 7 Mar 2017 10:52:42 +0100 Subject: [PATCH 110/153] Minor bug in unit test. --- tests/test_intersections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_intersections.py b/tests/test_intersections.py index 6bc09dc82e..da5d5c7ce8 100644 --- a/tests/test_intersections.py +++ b/tests/test_intersections.py @@ -170,7 +170,7 @@ def test_meeting_in_point(self): e1 = np.array([2, 0]) pi = basics.lines_intersect(s0, e0, s1, e1) - assert pi[0, 0] == 1 and pi[1, 0] == 1 + assert pi[0, 0] == 1 and pi[1, 0] == 0 if __name__ == '__main__': unittest.main() From 62a7d93e96715956438057476f28d2a68c30c2ae Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Tue, 7 Mar 2017 15:19:23 +0100 Subject: [PATCH 111/153] Splitting of edge (2d) intersections now tackles overlapping segments. This turned into a major, but rather technical update. --- basics.py | 282 ++++++++++++++++++++++++++++-------- tests/test_intersections.py | 62 ++++++++ 2 files changed, 282 insertions(+), 62 deletions(-) diff --git a/basics.py b/basics.py index d498bc9ad8..00020fe12d 100644 --- a/basics.py +++ b/basics.py @@ -95,7 +95,7 @@ def split_edge(vertices, edges, edge_ind, new_pt, **kwargs): The input can be a set of points, and lines between them, of which one is to be split. - A new line will be inserted, unless the new point coincides with the + New lines will be inserted, unless the new points coincide with the start or endpoint of the edge (under the given precision). The code is intended for 2D, in 3D use with caution. @@ -114,9 +114,12 @@ def split_edge(vertices, edges, edge_ind, new_pt, **kwargs): edges (np.ndarray, n x num_edges): Connections between lines. If n>2, the additional rows are treated as tags, that are preserved under splitting. - edge_ind (int): index of edge to be split, refering to edges. - new_pt (np.ndarray, nd x 1): new point to be inserted. Assumed to be - on the edge to be split. + edge_ind (int): index of edges to be split. + new_pt (np.ndarray, nd x n): new points to be inserted. Assumed to be + on the edge to be split. If more than one point is inserted + (segment intersection), it is assumed that new_pt[:, 0] is the one + closest to edges[0, edge_ind] (conforming with the output of + lines_intersect). **kwargs: Arguments passed to snap_to_grid Returns: @@ -125,32 +128,167 @@ def split_edge(vertices, edges, edge_ind, new_pt, **kwargs): boolean: True if a new line is created, otherwise false. """ - start = edges[0, edge_ind] - end = edges[1, edge_ind] + + # Some back and forth with the index of the edges to be split, depending on + # whether it is one or two + edge_ind = np.asarray(edge_ind) + if edge_ind.size > 1: + edge_ind_first = edge_ind[0] + else: + edge_ind_first = edge_ind + + # Start and end of the first (possibly only) edge + start = edges[0, edge_ind_first] + end = edges[1, edge_ind_first] + + # Number of points before edges is modified. Used for sanity check below. + orig_num_pts = edges[:2].max() + # Save tags associated with the edge. - tags = edges[2:, edge_ind] + # NOTE: For segment intersetions where the edges have different tags, one + # of the values will be overridden now. Fix later. + tags = edges[2:, edge_ind_first] - # Add a new point + # Try to add new points vertices, pt_ind, _ = add_point(vertices, new_pt, **kwargs) - # If the new point coincide with the start point, nothing happens - if start == pt_ind or end == pt_ind: - new_line = False + + # Sanity check + assert len(pt_ind) <= 2 + + # Check for a single intersection point + if len(pt_ind) < 2: + pi = pt_ind[0] + # Intersection at a point. + if start == pi or end == pi: + # Nothing happens + new_line = 0 + return vertices, edges, new_line + else: + new_edges = np.array([[start, pi], + [pi, end]]) + # Add any tags to the new edge. + if tags.size > 0: + new_edges = np.vstack((new_edges, + np.tile(tags[:, np.newaxis], 2))) + # Insert the new edge in the midle of the set of edges. + edges = np.hstack((edges[:, :edge_ind_first], new_edges, + edges[:, edge_ind_first+1:])) + # We have added a single new line + new_line = 1 return vertices, edges, new_line + else: + # Without this, we will delete something we should not further below. + assert edge_ind[0] < edge_ind[1] + + # Intersection along a segment. + # No new points should have been made + assert pt_ind[0] <= orig_num_pts and pt_ind[1] <= orig_num_pts + + # There are three (four) possible configurations + # a) The intersection is contained within (start, end). edge_ind[0] + # should be split into three segments, and edge_ind[1] should be + # deleted (it will be identical to the middle of the new segments). + # b) The intersection is identical with (start, end). edge_ind[1] + # should be split into three segments, and edge_ind[0] is deleted. + # c) and d) The intersection consists of one of (start, end), and another + # point. Both edge_ind[0] and edge_ind[1] should be split into two + # segments. + + i0 = pt_ind[0] + i1 = pt_ind[1] + if i0 != start and i1 != end: + # Delete the second segment + edges = np.delete(edges, edge_ind[1], axis=1) + if edges.shape[0] == 1: + edges = edges.reshape((-1, 1)) + # We know that i0 will be closest to start, thus (start, i0) is a + # pair. + # New segments (i0, i1) is identical to the old edge_ind[1] + new_edges = np.array([[start, i0, i1], + [i0, i1, end]]) + if tags.size > 0: + new_edges = np.vstack((new_edges, + np.tile(tags[:, np.newaxis], + new_edges.shape[1]))) + # Combine everything. + edges = np.hstack((edges[:, :edge_ind[0]], + new_edges, + edges[:, edge_ind[0]+1:])) + new_line = [2, 0] + elif i0 == start and i1 == end: + # We don't know if i0 is closest to the start or end of edges[:, + # edges_ind[1]]. Find the nearest. + if __dist(vertices[:, i0], vertices[:, edges[0, edge_ind[1]]]) < \ + __dist(vertices[:, i1], vertices[:, edges[0, edge_ind[1]]]): + other_start = edges[0, edge_ind[1]] + other_end = edges[1, edge_ind[1]] + else: + other_start = edges[1, edge_ind[1]] + other_end = edges[0, edge_ind[1]] + + # New segments (i0, i1) is identical to the old edge_ind[0] + new_edges = np.array([[other_start, i0, i1], + [i0, i1, other_end]]) + if tags.size > 0: + new_edges = np.vstack((new_edges, + np.tile(tags[:, np.newaxis], + new_edges.shape[1]))) + # Combine everything. + edges = np.hstack((edges[:, :edge_ind[1]], + new_edges, + edges[:, (edge_ind[1]+1):])) + # Delete the second segment. This is most easily handled after + # edges is expanded. + edges = np.delete(edges, edge_ind[0], axis=1) + new_line = [0, 2] + + # Note that we know that i0 is closest to start, thus no need to test + # for i1 == start + elif i0 == start and i1 != end: + # Modify edge_ind[1] to end in start + if edges[0, edge_ind[1]] == i1: + edges[0, edge_ind[1]] = start + elif edges[1, edge_ind[1]] == i1: + edges[1, edge_ind[1]] = start + else: + raise ValueError('This should not happen') + + new_edges = np.array([[start, i1], + [i1, end]]) + if tags.size > 0: + new_edges = np.vstack((new_edges, + np.tile(tags[:, np.newaxis], + new_edges.shape[1]))) + + edges = np.hstack((edges[:, :edge_ind[1]], + new_edges, + edges[:, (edge_ind[1]+1):])) + new_line = [1, 0] + + elif i0 != start and i1 == end: + # Modify edge_ind[1] to end in start + if edges[0, edge_ind[1]] == i0: + edges[0, edge_ind[1]] = end + elif edges[1, edge_ind[1]] == i0: + edges[1, edge_ind[1]] = end + else: + raise ValueError('This should not happen') + new_edges = np.array([[start, i0], + [i0, end]]) + if tags.size > 0: + new_edges = np.vstack((new_edges, + np.tile(tags[:, np.newaxis], + new_edges.shape[1]))) + + edges = np.hstack((edges[:, :edge_ind[0]], + new_edges, + edges[:, (edge_ind[0]+1):])) + new_line = [1, 0] + else: + raise ValueError('How did it come to this') - # If we get here, we know that a new point has been created. - # Add any tags to the new edge. - if tags.size > 0: - new_edges = np.vstack((np.array([[start, pt_ind], - [pt_ind, end]]), - np.tile(tags[:, np.newaxis], 2))) - else: - new_edges = np.array([[start, pt_ind], - [pt_ind, end]]) - # Insert the new edge in the midle of the set of edges. - edges = np.hstack((edges[:, :edge_ind], new_edges, edges[:, edge_ind+1:])) - new_line = True - return vertices, edges, new_line + return vertices, edges, new_line #------------------------------------------------------------------------------# @@ -187,21 +325,27 @@ def add_point(vertices, pt, precision=1e-3, **kwargs): vertices = snap_to_grid(vertices, **kwargs) pt = snap_to_grid(pt, **kwargs) + new_pt = np.empty((nd, 0)) + ind = [] # Distance - dist = __dist(pt, vertices) - min_dist = np.min(dist) - - if min_dist < precision * np.sqrt(nd): - # No new point is needed - ind = np.argmin(dist) - new_point = None - return vertices, ind, new_point - else: - ind = vertices.shape[1]-1 - # Append the new point at the end of the point list - vertices = np.append(vertices, pt, axis=1) - ind = vertices.shape[1] - 1 - return vertices, ind, pt + for i in range(pt.shape[-1]): + dist = __dist(pt[:, i].reshape((-1, 1)), vertices) + min_dist = np.min(dist) + + if min_dist < precision * np.sqrt(nd): + # No new point is needed + ind.append(np.argmin(dist)) +# return vertices, ind, new_point + else: + # Append the new point at the end of the point list + ind.append(vertices.shape[1]) + vertices = np.append(vertices, pt, axis=1) + new_pt = np.hstack((new_pt, pt[:, i].reshape((-1, 1)))) +# return vertices, ind, pt + if new_pt.shape[1] == 0: + new_pt = None + return vertices, ind, new_pt + #----------------------------------------------------------- @@ -392,8 +536,9 @@ def lines_intersect(start_1, end_1, start_2, end_2, tol=1e-8): Returns: np.ndarray (2 x num_pts): coordinates of intersection point, or the - endpoints of the intersection segments if relevant. If the lines do - not intersect, None is returned. + endpoints of the intersection segments if relevant. In the case of + a segment, the first point (column) will be closest to start_1. If + the lines do not intersect, None is returned. Raises: ValueError if the start and endpoints of a line are the same. @@ -894,6 +1039,7 @@ def remove_edge_crossings(vertices, edges, **kwargs): vertices = snap_to_grid(vertices, **kwargs) + # Loop over all edges, search for intersections. The number of edges can # change due to splitting. while edge_counter < edges.shape[1]: @@ -925,7 +1071,8 @@ def remove_edge_crossings(vertices, edges, **kwargs): c1 = a[edge_counter] * (start_x - xm) \ + b[edge_counter] * (start_y - ym) c2 = a[edge_counter] * (end_x - xm) + b[edge_counter] * (end_y - ym) - line_intersections = np.sign(c1) != np.sign(c2) + line_intersections = np.logical_or(np.sign(c1) != np.sign(c2), + np.logical_and(c1 == 0, c2 == 0)) # Find elements which may intersect. intersections = np.argwhere(line_intersections) @@ -939,7 +1086,7 @@ def remove_edge_crossings(vertices, edges, **kwargs): edge_counter += 1 continue - int_counter = 0 + int_counter = 1 while intersections.size > 0 and int_counter < intersections.size: # Line intersect (inner loop) is an intersection if it crosses # the extension of line edge_counter (outer loop) (ie intsect it @@ -954,34 +1101,45 @@ def remove_edge_crossings(vertices, edges, **kwargs): # intersection points(vectorized), and only recompuing if line # edge_counter is split, but we keep things simple for now. intsect = intersections[int_counter] + if intsect == edge_counter: + int_counter += 1 + continue # Check if this point intersects new_pt = lines_intersect(vertices[:, edges[0, edge_counter]], vertices[:, edges[1, edge_counter]], vertices[:, edges[0, intsect]], vertices[:, edges[1, intsect]]) - if new_pt is not None: - # Split edge edge_counter (outer loop), unless the - # intersection hits an existing point (in practices this - # means the intersection runs through an endpoint of the - # edge in an L-type configuration, in which case no new point - # is needed) - vertices, edges, split_outer_edge = split_edge(vertices, edges, - edge_counter, - new_pt, - **kwargs) - # If the outer edge (represented by edge_counter) was split, - # e.g. inserted into the list of edges we need to increase the - # index of the inner edge - intsect += split_outer_edge - # Possibly split the inner edge - vertices, edges, split_inner_edge = split_edge(vertices, edges, - intsect, - new_pt, - **kwargs) + # The case of segment intersections need special treatment. + if new_pt.shape[-1] == 1: + # Split edge edge_counter (outer loop), unless the + # intersection hits an existing point (in practices this + # means the intersection runs through an endpoint of the + # edge in an L-type configuration, in which case no new point + # is needed) + vertices, edges, split_outer_edge = split_edge(vertices, edges, + edge_counter, + new_pt, + **kwargs) + # If the outer edge (represented by edge_counter) was split, + # e.g. inserted into the list of edges we need to increase the + # index of the inner edge + intsect += split_outer_edge + # Possibly split the inner edge + vertices, edges, split_inner_edge = split_edge(vertices, edges, + intsect, + new_pt, + **kwargs) + intersections += split_outer_edge + split_inner_edge + else: + vertices, edges, splits = split_edge(vertices, edges, + [edge_counter, + intsect], + new_pt, **kwargs) + intersections += splits[0] + intersections[intsect:] += splits[1] # Update index of possible intersections - intersections += split_outer_edge + split_inner_edge # We're done with this candidate edge. Increase index of inner loop int_counter += 1 diff --git a/tests/test_intersections.py b/tests/test_intersections.py index da5d5c7ce8..44c6fcdc0e 100644 --- a/tests/test_intersections.py +++ b/tests/test_intersections.py @@ -72,6 +72,68 @@ def test_three_lines_one_crossing(self): assert np.allclose(new_pts, p_known) assert np.allclose(new_lines, lines_known) + def test_split_segment_partly_overlapping(self): + p = np.array([[0, 1, 2, 3], + [0, 0, 0, 0]]) + lines = np.array([[0, 2], [1, 3]]).T + box = np.array([[1], [1]]) + + new_pts, new_lines = basics.remove_edge_crossings(p, lines, box=box) + + p_known = basics.snap_to_grid(p, box=box) + lines_known = np.array([[0, 1], [1, 2], [2, 3]]).T + + assert np.allclose(new_pts, p_known) + assert np.allclose(new_lines, lines_known) + + def test_split_segment_partly_overlapping_switched_order(self): + # Same partly overlapping test, but switch order of edge-point + # connection. Should make no difference + p = np.array([[0, 1, 2, 3], + [0, 0, 0, 0]]) + lines = np.array([[0, 2], [3, 1]]).T + box = np.array([[1], [1]]) + + new_pts, new_lines = basics.remove_edge_crossings(p, lines, box=box) + + new_lines = np.sort(new_lines, axis=0) + p_known = basics.snap_to_grid(p, box=box) + lines_known = np.array([[0, 1], [1, 2], [2, 3]]).T + + assert np.allclose(new_pts, p_known) + assert np.allclose(new_lines, lines_known) + + def test_split_segment_fully_overlapping(self): + p = np.array([[0, 1, 2, 3], + [0, 0, 0, 0]]) + lines = np.array([[0, 3], [1, 2]]).T + box = np.array([[1], [1]]) + + new_pts, new_lines = basics.remove_edge_crossings(p, lines, box=box) + + new_lines = np.sort(new_lines, axis=0) + p_known = basics.snap_to_grid(p, box=box) + lines_known = np.array([[0, 1], [1, 2], [2, 3]]).T + + assert np.allclose(new_pts, p_known) + assert np.allclose(new_lines, lines_known) + + + def test_split_segment_fully_overlapping_switched_order(self): + p = np.array([[0, 1, 2, 3], + [0, 0, 0, 0]]) + lines = np.array([[0, 3], [2, 1]]).T + box = np.array([[1], [1]]) + + new_pts, new_lines = basics.remove_edge_crossings(p, lines, box=box) + new_lines = np.sort(new_lines, axis=0) + + p_known = basics.snap_to_grid(p, box=box) + lines_known = np.array([[0, 1], [1, 2], [2, 3]]).T + + assert np.allclose(new_pts, p_known) + assert np.allclose(new_lines, lines_known) + if __name__ == '__main__': From b9f1d027ef41e17268d5e27e224436ed6b72280f Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Tue, 7 Mar 2017 15:24:10 +0100 Subject: [PATCH 112/153] Renamed helper functions for edge intersections to mark them as private --- basics.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/basics.py b/basics.py index 00020fe12d..d805e7751f 100644 --- a/basics.py +++ b/basics.py @@ -87,7 +87,7 @@ def __points_equal(p1, p2, box, precesion=1e-3): #------------------------------------------------------------------------------# -def split_edge(vertices, edges, edge_ind, new_pt, **kwargs): +def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): """ Split a line into two by introcuding a new point. Function is used e.g. for gridding purposes. @@ -104,7 +104,7 @@ def split_edge(vertices, edges, edge_ind, new_pt, **kwargs): >>> p = np.array([[0, 0], [0, 1]]) >>> edges = np.array([[0], [1]]) >>> new_pt = np.array([[0], [0.5]]) - >>> v, e, nl = split_edge(p, edges, 0, new_pt) + >>> v, e, nl = _split_edge(p, edges, 0, new_pt) >>> e array([[0, 2], [2, 1]]) @@ -150,7 +150,7 @@ def split_edge(vertices, edges, edge_ind, new_pt, **kwargs): tags = edges[2:, edge_ind_first] # Try to add new points - vertices, pt_ind, _ = add_point(vertices, new_pt, **kwargs) + vertices, pt_ind, _ = _add_point(vertices, new_pt, **kwargs) # Sanity check assert len(pt_ind) <= 2 @@ -292,7 +292,7 @@ def split_edge(vertices, edges, edge_ind, new_pt, **kwargs): #------------------------------------------------------------------------------# -def add_point(vertices, pt, precision=1e-3, **kwargs): +def _add_point(vertices, pt, precision=1e-3, **kwargs): """ Add a point to a point set, unless the point already exist in the set. @@ -1118,7 +1118,7 @@ def remove_edge_crossings(vertices, edges, **kwargs): # means the intersection runs through an endpoint of the # edge in an L-type configuration, in which case no new point # is needed) - vertices, edges, split_outer_edge = split_edge(vertices, edges, + vertices, edges, split_outer_edge = _split_edge(vertices, edges, edge_counter, new_pt, **kwargs) @@ -1127,13 +1127,13 @@ def remove_edge_crossings(vertices, edges, **kwargs): # index of the inner edge intsect += split_outer_edge # Possibly split the inner edge - vertices, edges, split_inner_edge = split_edge(vertices, edges, + vertices, edges, split_inner_edge = _split_edge(vertices, edges, intsect, new_pt, **kwargs) intersections += split_outer_edge + split_inner_edge else: - vertices, edges, splits = split_edge(vertices, edges, + vertices, edges, splits = _split_edge(vertices, edges, [edge_counter, intsect], new_pt, **kwargs) From b43c336fb39c395fd0d994c23ea82ae832162ad5 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Tue, 7 Mar 2017 15:25:50 +0100 Subject: [PATCH 113/153] Moved function for edge crossing removal inside basics. It is now located together with related helper functions. --- basics.py | 300 +++++++++++++++++++++++++++--------------------------- 1 file changed, 149 insertions(+), 151 deletions(-) diff --git a/basics.py b/basics.py index d805e7751f..0b53f1c9a0 100644 --- a/basics.py +++ b/basics.py @@ -347,6 +347,155 @@ def _add_point(vertices, pt, precision=1e-3, **kwargs): return vertices, ind, new_pt +#-----------------------------------------------------------------------------# +def remove_edge_crossings(vertices, edges, **kwargs): + """ + Process a set of points and connections between them so that the result + is an extended point set and new connections that do not intersect. + + The function is written for gridding of fractured domains, but may be + of use in other cases as well. The geometry is assumed to be 2D, (the + corresponding operation in 3D requires intersection between planes, and + is a rather complex, although doable, task). + + The connections are defined by their start and endpoints, and can also + have tags assigned. If so, the tags are preserved as connections are split. + + Parameters: + vertices (np.ndarray, 2 x n_pt): Coordinates of points to be processed + edges (np.ndarray, n x n_con): Connections between lines. n >= 2, row + 0 and 1 are index of start and endpoints, additional rows are tags + **kwargs: Arguments passed to snap_to_grid + + Returns: + np.ndarray, (2 x n_pt), array of points, possibly expanded. + np.ndarray, (n x n_edges), array of new edges. Non-intersecting. + + Raises: + NotImplementedError if a 3D point array is provided. + + """ + num_edges = edges.shape[1] + nd = vertices.shape[0] + + # Only 2D is considered. 3D should not be too dificult, but it is not + # clear how relevant it is + if nd != 2: + raise NotImplementedError('Only 2D so far') + + edge_counter = 0 + + vertices = snap_to_grid(vertices, **kwargs) + + + # Loop over all edges, search for intersections. The number of edges can + # change due to splitting. + while edge_counter < edges.shape[1]: + # The direct test of whether two edges intersect is somewhat + # expensive, and it is hard to vectorize this part. Therefore, + # we first identify edges which crosses the extention of this edge ( + # intersection of line and line segment). We then go on to test for + # real intersections. + + + # Find start and stop coordinates for all edges + start_x = vertices[0, edges[0]] + start_y = vertices[1, edges[0]] + end_x = vertices[0, edges[1]] + end_y = vertices[1, edges[1]] + + a = end_y - start_y + b = -(end_x - start_x) + xm = (start_x[edge_counter] + end_x[edge_counter]) / 2 + ym = (start_y[edge_counter] + end_y[edge_counter]) / 2 + + # For all lines, find which side of line i it's two endpoints are. + # If c1 and c2 have different signs, they will be on different sides + # of line i. See + # + # http://stackoverflow.com/questions/385305/efficient-maths-algorithm-to-calculate-intersections + # + # for more information. + c1 = a[edge_counter] * (start_x - xm) \ + + b[edge_counter] * (start_y - ym) + c2 = a[edge_counter] * (end_x - xm) + b[edge_counter] * (end_y - ym) + line_intersections = np.logical_or(np.sign(c1) != np.sign(c2), + np.logical_and(c1 == 0, c2 == 0)) + + # Find elements which may intersect. + intersections = np.argwhere(line_intersections) + # np.argwhere returns an array of dimensions (1, dim), so we reduce + # this to truly 1D, or simply continue with the next edge if there + # are no candidate edges + if intersections.size > 0: + intersections = intersections.ravel(0) + else: + # There are no candidates for intersection + edge_counter += 1 + continue + + int_counter = 1 + while intersections.size > 0 and int_counter < intersections.size: + # Line intersect (inner loop) is an intersection if it crosses + # the extension of line edge_counter (outer loop) (ie intsect it + # crosses the infinite line that goes through the endpoints of + # edge_counter), but we don't know if it actually crosses the + # line segment edge_counter. Now we do a more refined search to + # find if the line segments intersects. Note that there is no + # help in vectorizing lines_intersect and computing intersection + # points for all lines in intersections, since line i may be + # split, and the intersection points must recalculated. It may + # be possible to reorganize this while-loop by computing all + # intersection points(vectorized), and only recompuing if line + # edge_counter is split, but we keep things simple for now. + intsect = intersections[int_counter] + if intsect == edge_counter: + int_counter += 1 + continue + + # Check if this point intersects + new_pt = lines_intersect(vertices[:, edges[0, edge_counter]], + vertices[:, edges[1, edge_counter]], + vertices[:, edges[0, intsect]], + vertices[:, edges[1, intsect]]) + if new_pt is not None: + # The case of segment intersections need special treatment. + if new_pt.shape[-1] == 1: + # Split edge edge_counter (outer loop), unless the + # intersection hits an existing point (in practices this + # means the intersection runs through an endpoint of the + # edge in an L-type configuration, in which case no new point + # is needed) + vertices, edges, split_outer_edge = _split_edge(vertices, edges, + edge_counter, + new_pt, + **kwargs) + # If the outer edge (represented by edge_counter) was split, + # e.g. inserted into the list of edges we need to increase the + # index of the inner edge + intsect += split_outer_edge + # Possibly split the inner edge + vertices, edges, split_inner_edge = _split_edge(vertices, edges, + intsect, + new_pt, + **kwargs) + intersections += split_outer_edge + split_inner_edge + else: + vertices, edges, splits = _split_edge(vertices, edges, + [edge_counter, + intsect], + new_pt, **kwargs) + intersections += splits[0] + intersections[intsect:] += splits[1] + # Update index of possible intersections + + # We're done with this candidate edge. Increase index of inner loop + int_counter += 1 + # We're done with all intersections of this loop. increase index of + # outer loop + edge_counter += 1 + return vertices, edges + #----------------------------------------------------------- def is_ccw_polygon(poly): @@ -999,157 +1148,6 @@ def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): return isect -#-----------------------------------------------------------------------------# -def remove_edge_crossings(vertices, edges, **kwargs): - """ - Process a set of points and connections between them so that the result - is an extended point set and new connections that do not intersect. - - The function is written for gridding of fractured domains, but may be - of use in other cases as well. The geometry is assumed to be 2D, (the - corresponding operation in 3D requires intersection between planes, and - is a rather complex, although doable, task). - - The connections are defined by their start and endpoints, and can also - have tags assigned. If so, the tags are preserved as connections are split. - - Parameters: - vertices (np.ndarray, 2 x n_pt): Coordinates of points to be processed - edges (np.ndarray, n x n_con): Connections between lines. n >= 2, row - 0 and 1 are index of start and endpoints, additional rows are tags - **kwargs: Arguments passed to snap_to_grid - - Returns: - np.ndarray, (2 x n_pt), array of points, possibly expanded. - np.ndarray, (n x n_edges), array of new edges. Non-intersecting. - - Raises: - NotImplementedError if a 3D point array is provided. - - """ - num_edges = edges.shape[1] - nd = vertices.shape[0] - - # Only 2D is considered. 3D should not be too dificult, but it is not - # clear how relevant it is - if nd != 2: - raise NotImplementedError('Only 2D so far') - - edge_counter = 0 - - vertices = snap_to_grid(vertices, **kwargs) - - - # Loop over all edges, search for intersections. The number of edges can - # change due to splitting. - while edge_counter < edges.shape[1]: - # The direct test of whether two edges intersect is somewhat - # expensive, and it is hard to vectorize this part. Therefore, - # we first identify edges which crosses the extention of this edge ( - # intersection of line and line segment). We then go on to test for - # real intersections. - - - # Find start and stop coordinates for all edges - start_x = vertices[0, edges[0]] - start_y = vertices[1, edges[0]] - end_x = vertices[0, edges[1]] - end_y = vertices[1, edges[1]] - - a = end_y - start_y - b = -(end_x - start_x) - xm = (start_x[edge_counter] + end_x[edge_counter]) / 2 - ym = (start_y[edge_counter] + end_y[edge_counter]) / 2 - - # For all lines, find which side of line i it's two endpoints are. - # If c1 and c2 have different signs, they will be on different sides - # of line i. See - # - # http://stackoverflow.com/questions/385305/efficient-maths-algorithm-to-calculate-intersections - # - # for more information. - c1 = a[edge_counter] * (start_x - xm) \ - + b[edge_counter] * (start_y - ym) - c2 = a[edge_counter] * (end_x - xm) + b[edge_counter] * (end_y - ym) - line_intersections = np.logical_or(np.sign(c1) != np.sign(c2), - np.logical_and(c1 == 0, c2 == 0)) - - # Find elements which may intersect. - intersections = np.argwhere(line_intersections) - # np.argwhere returns an array of dimensions (1, dim), so we reduce - # this to truly 1D, or simply continue with the next edge if there - # are no candidate edges - if intersections.size > 0: - intersections = intersections.ravel(0) - else: - # There are no candidates for intersection - edge_counter += 1 - continue - - int_counter = 1 - while intersections.size > 0 and int_counter < intersections.size: - # Line intersect (inner loop) is an intersection if it crosses - # the extension of line edge_counter (outer loop) (ie intsect it - # crosses the infinite line that goes through the endpoints of - # edge_counter), but we don't know if it actually crosses the - # line segment edge_counter. Now we do a more refined search to - # find if the line segments intersects. Note that there is no - # help in vectorizing lines_intersect and computing intersection - # points for all lines in intersections, since line i may be - # split, and the intersection points must recalculated. It may - # be possible to reorganize this while-loop by computing all - # intersection points(vectorized), and only recompuing if line - # edge_counter is split, but we keep things simple for now. - intsect = intersections[int_counter] - if intsect == edge_counter: - int_counter += 1 - continue - - # Check if this point intersects - new_pt = lines_intersect(vertices[:, edges[0, edge_counter]], - vertices[:, edges[1, edge_counter]], - vertices[:, edges[0, intsect]], - vertices[:, edges[1, intsect]]) - if new_pt is not None: - # The case of segment intersections need special treatment. - if new_pt.shape[-1] == 1: - # Split edge edge_counter (outer loop), unless the - # intersection hits an existing point (in practices this - # means the intersection runs through an endpoint of the - # edge in an L-type configuration, in which case no new point - # is needed) - vertices, edges, split_outer_edge = _split_edge(vertices, edges, - edge_counter, - new_pt, - **kwargs) - # If the outer edge (represented by edge_counter) was split, - # e.g. inserted into the list of edges we need to increase the - # index of the inner edge - intsect += split_outer_edge - # Possibly split the inner edge - vertices, edges, split_inner_edge = _split_edge(vertices, edges, - intsect, - new_pt, - **kwargs) - intersections += split_outer_edge + split_inner_edge - else: - vertices, edges, splits = _split_edge(vertices, edges, - [edge_counter, - intsect], - new_pt, **kwargs) - intersections += splits[0] - intersections[intsect:] += splits[1] - # Update index of possible intersections - - # We're done with this candidate edge. Increase index of inner loop - int_counter += 1 - # We're done with all intersections of this loop. increase index of - # outer loop - edge_counter += 1 - return vertices, edges - -#------------------------------------------------------------------------------# - def is_planar( pts, normal = None ): """ Check if the points lie on a plane. From afe99e28dcbe4846071883b2f2fbf8309051bc87 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Wed, 8 Mar 2017 09:00:19 +0100 Subject: [PATCH 114/153] Started sectioning functions in basic module for increased readability. --- basics.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/basics.py b/basics.py index 0b53f1c9a0..4e3a98dc60 100644 --- a/basics.py +++ b/basics.py @@ -12,6 +12,11 @@ import sympy from sympy import geometry as geom + +#----------------------------------------------------------------------------- +# +# START OF FUNCTIONS RELATED TO SPLITTING OF INTERSECTING LINES IN 2D +# #------------------------------------------------------------------------------# def snap_to_grid(pts, precision=1e-3, box=None): @@ -496,6 +501,10 @@ def remove_edge_crossings(vertices, edges, **kwargs): edge_counter += 1 return vertices, edges +#---------------------------------------------------------- +# +# END OF FUNCTIONS RELATED TO SPLITTING OF INTERSECTING LINES IN 2D +# #----------------------------------------------------------- def is_ccw_polygon(poly): From ef5969894e22e6404daf0089ba7576994585d9d0 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Wed, 8 Mar 2017 09:01:07 +0100 Subject: [PATCH 115/153] Clarified comments in for segment intersection in 3d --- basics.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/basics.py b/basics.py index 4e3a98dc60..5b6477b902 100644 --- a/basics.py +++ b/basics.py @@ -767,7 +767,7 @@ def lines_intersect(start_1, end_1, start_2, end_2, tol=1e-8): def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): """ - Check if two line segments defined in 3D intersect. + Find intersection points (or segments) of two 3d lines. Note that, oposed to other functions related to grid generation such as remove_edge_crossings, this function does not use the concept of @@ -783,8 +783,10 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): end_2 (np.ndarray or list): coordinates of end point for first line. Returns: - np.ndarray: coordinates of intersection point, or None if the lines do - not intersect. + np.ndarray, dimension 3xn_pts): coordinates of intersection points + (number of columns will be either 1 for a point intersection, or 2 + for a segment intersection). If the lines do not intersect, None is + returned. """ From 56a58180d518bbde247fc1a10bb5f5d7f764f0b3 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Mon, 13 Mar 2017 11:01:52 +0100 Subject: [PATCH 116/153] Fix split of edges in the case of partly overlapping segments. --- basics.py | 46 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/basics.py b/basics.py index 5b6477b902..c5d9224d31 100644 --- a/basics.py +++ b/basics.py @@ -12,7 +12,6 @@ import sympy from sympy import geometry as geom - #----------------------------------------------------------------------------- # # START OF FUNCTIONS RELATED TO SPLITTING OF INTERSECTING LINES IN 2D @@ -182,7 +181,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): new_line = 1 return vertices, edges, new_line else: - # Without this, we will delete something we should not further below. + # Without this, we will delete something we should not delete below. assert edge_ind[0] < edge_ind[1] # Intersection along a segment. @@ -247,14 +246,34 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): edges = np.delete(edges, edge_ind[0], axis=1) new_line = [0, 2] + # Note that we know that i0 is closest to start, thus no need to test # for i1 == start elif i0 == start and i1 != end: - # Modify edge_ind[1] to end in start + # The intersection starts in start of edge_ind[0], and end before + # the end of edge_ind[0] (if not we would have i1==end). + # The intersection should be split into intervals (start, i1), (i1, + # end) and possibly (edge_ind[1][0 or 1], start); with the latter + # representing the part of edge_ind[1] laying on the other side of + # start compared than i1. The latter part will should not be + # included if start is also a node of edge_ind[1]. + # + # Examples in 1d (really needed to make this concrete right now): + # edge_ind[0] = (0, 2), edge_ind[1] = (-1, 1) is split into + # (0, 1), (1, 2) and (-1, 1) (listed in the same order as above). + # + # edge_ind[0] = (0, 2), edge_ind[1] = (0, 1) is split into + # (0, 1), (1, 2) if edges[0, edge_ind[1]] == i1: - edges[0, edge_ind[1]] = start + if edges[1, edge_ind[1]] == start: + edges = np.delete(edges, edge_ind[1], axis=1) + else: + edges[0, edge_ind[1]] = start elif edges[1, edge_ind[1]] == i1: - edges[1, edge_ind[1]] = start + if edges[0, edge_ind[1]] == start: + edges = np.delete(edges, edge_ind[1], axis=1) + else: + edges[1, edge_ind[1]] = start else: raise ValueError('This should not happen') @@ -265,17 +284,24 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): np.tile(tags[:, np.newaxis], new_edges.shape[1]))) - edges = np.hstack((edges[:, :edge_ind[1]], + edges = np.hstack((edges[:, :edge_ind[0]], new_edges, - edges[:, (edge_ind[1]+1):])) + edges[:, (edge_ind[0]+1):])) new_line = [1, 0] elif i0 != start and i1 == end: - # Modify edge_ind[1] to end in start + # Analogous configuration as the one above, but with i0 replaced by + # i1 and start by end. if edges[0, edge_ind[1]] == i0: - edges[0, edge_ind[1]] = end + if edges[1, edge_ind[1]] == end: + edges = np.delete(edges, edge_ind[1], axis=1) + else: + edges[0, edge_ind[1]] = end elif edges[1, edge_ind[1]] == i0: - edges[1, edge_ind[1]] = end + if edges[0, edge_ind[1]] == end: + edges = np.delete(edges, edge_ind[1], axis=1) + else: + edges[1, edge_ind[1]] = end else: raise ValueError('This should not happen') new_edges = np.array([[start, i0], From 0e75f946d9a601d4edf0cf59eac66435549e95e4 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Mon, 20 Mar 2017 08:55:18 +0100 Subject: [PATCH 117/153] Bugfix in intersection between polygon and polygon segments. --- basics.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/basics.py b/basics.py index 9761f03dcb..cff711aa53 100644 --- a/basics.py +++ b/basics.py @@ -1154,6 +1154,11 @@ def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): # Coordinates of this segment pt_1 = poly_2_rot[:, ind[i]] pt_2 = poly_2_rot[:, ind[i+1]] + + # Check if segment crosses z=0 in the rotated coordinates + if max(pt_1[2], pt_2[2]) < 0 or min(pt_1[2], pt_2[2]) > 0: + continue + dx = pt_2[0] - pt_1[0] dy = pt_2[1] - pt_1[1] dz = pt_2[2] - pt_1[2] @@ -1163,6 +1168,11 @@ def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): # Parametrize the line, find parameter value for intersection # with z=0. t = (-pt_1[2] - 0) / dz + + # Sanity check. We have ruled out segments not crossing the + # origin above. + assert t >= 0 and t <= 1 + # x and y-coordinate for z=0 x0 = pt_1[0] + dx * t y0 = pt_1[1] + dy * t From bc0e39e2b183769f8de442618499f80920c71202 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Mon, 20 Mar 2017 09:48:17 +0100 Subject: [PATCH 118/153] Bugfix in intersection between polygon segments. --- basics.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/basics.py b/basics.py index cff711aa53..210381144a 100644 --- a/basics.py +++ b/basics.py @@ -895,14 +895,26 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): return None # If we have made it this far, the lines are indeed parallel. Next, - # check if they overlap, and if so, test if the segments are overlapping + # check that they lay along the same line. + diff_start = start_2 - start_1 + + dstart_x_delta_x = diff_start[1] * deltas_1[2] -\ + diff_start[2] * deltas_1[1] + if np.abs(dstart_x_delta_x) > tol: + return None + dstart_x_delta_y = diff_start[2] * deltas_1[0] -\ + diff_start[0] * deltas_1[2] + if np.abs(dstart_x_delta_y) > tol: + return None + dstart_x_delta_z = diff_start[0] * deltas_1[1] -\ + diff_start[1] * deltas_1[0] + if np.abs(dstart_x_delta_z) > tol: + return None # For dimensions with an incline, the vector between segment start # points should be parallel to the segments. # Since the masks are equal, we can use any of them. t_1_2 = (start_1[mask_1] - start_2[mask_1]) / deltas_1[mask_1] - if (not np.allclose(t_1_2, t_1_2, tol)): - return None # For dimensions with no incline, the start cooordinates should be the same if not np.allclose(start_1[~mask_1], start_2[~mask_1], tol): return None From 7e97eba0b56f2f0b261584c4a4c1bfbc43bf1330 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Mon, 20 Mar 2017 12:40:39 +0100 Subject: [PATCH 119/153] Bugfix in splitting of edges in basic geometry --- basics.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/basics.py b/basics.py index 210381144a..1bc887bd07 100644 --- a/basics.py +++ b/basics.py @@ -218,7 +218,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): edges = np.hstack((edges[:, :edge_ind[0]], new_edges, edges[:, edge_ind[0]+1:])) - new_line = [2, 0] + new_line = [2, -1] elif i0 == start and i1 == end: # We don't know if i0 is closest to the start or end of edges[:, # edges_ind[1]]. Find the nearest. @@ -242,9 +242,9 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): new_edges, edges[:, (edge_ind[1]+1):])) # Delete the second segment. This is most easily handled after - # edges is expanded. + # edges is expanded, to avoid accounting for changing edge indices. edges = np.delete(edges, edge_ind[0], axis=1) - new_line = [0, 2] + new_line = [-1, 2] # Note that we know that i0 is closest to start, thus no need to test @@ -264,14 +264,17 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): # # edge_ind[0] = (0, 2), edge_ind[1] = (0, 1) is split into # (0, 1), (1, 2) + did_delete = 0 if edges[0, edge_ind[1]] == i1: if edges[1, edge_ind[1]] == start: edges = np.delete(edges, edge_ind[1], axis=1) + did_delete = 1 else: edges[0, edge_ind[1]] = start elif edges[1, edge_ind[1]] == i1: if edges[0, edge_ind[1]] == start: edges = np.delete(edges, edge_ind[1], axis=1) + did_delete = 1 else: edges[1, edge_ind[1]] = start else: @@ -287,19 +290,22 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): edges = np.hstack((edges[:, :edge_ind[0]], new_edges, edges[:, (edge_ind[0]+1):])) - new_line = [1, 0] + new_line = [1, -did_delete] elif i0 != start and i1 == end: # Analogous configuration as the one above, but with i0 replaced by # i1 and start by end. + did_delete = 0 if edges[0, edge_ind[1]] == i0: if edges[1, edge_ind[1]] == end: edges = np.delete(edges, edge_ind[1], axis=1) + did_delete = 1 else: edges[0, edge_ind[1]] = end elif edges[1, edge_ind[1]] == i0: if edges[0, edge_ind[1]] == end: edges = np.delete(edges, edge_ind[1], axis=1) + did_delete = -1 else: edges[1, edge_ind[1]] = end else: @@ -314,7 +320,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): edges = np.hstack((edges[:, :edge_ind[0]], new_edges, edges[:, (edge_ind[0]+1):])) - new_line = [1, 0] + new_line = [1, -did_delete] else: raise ValueError('How did it come to this') From 1f8e3186c7b535ba183c8872c092bdfe9c68b668 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Mon, 20 Mar 2017 12:41:11 +0100 Subject: [PATCH 120/153] Bugfix in removal of intersecting edges. --- basics.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/basics.py b/basics.py index 1bc887bd07..b2837b1000 100644 --- a/basics.py +++ b/basics.py @@ -470,7 +470,7 @@ def remove_edge_crossings(vertices, edges, **kwargs): edge_counter += 1 continue - int_counter = 1 + int_counter = 0 while intersections.size > 0 and int_counter < intersections.size: # Line intersect (inner loop) is an intersection if it crosses # the extension of line edge_counter (outer loop) (ie intsect it @@ -485,7 +485,7 @@ def remove_edge_crossings(vertices, edges, **kwargs): # intersection points(vectorized), and only recompuing if line # edge_counter is split, but we keep things simple for now. intsect = intersections[int_counter] - if intsect == edge_counter: + if intsect <= edge_counter: int_counter += 1 continue @@ -521,8 +521,7 @@ def remove_edge_crossings(vertices, edges, **kwargs): [edge_counter, intsect], new_pt, **kwargs) - intersections += splits[0] - intersections[intsect:] += splits[1] + intersections += splits[0] + splits[1] # Update index of possible intersections # We're done with this candidate edge. Increase index of inner loop From 4e57b0fbdf51932e11a926be3142dfd054a93a88 Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Mon, 20 Mar 2017 16:49:35 +0100 Subject: [PATCH 121/153] add the 3d case (trivial) for the map_grid --- basics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basics.py b/basics.py index 210381144a..1ad7cc3738 100644 --- a/basics.py +++ b/basics.py @@ -1374,7 +1374,7 @@ def map_grid(g): face_centers = g.face_centers R = np.eye(3) - if g.dim == 0: + if g.dim == 0 or g.dim == 3: return cell_centers, face_normals, face_centers, R, np.ones(3,dtype=bool) if g.dim == 1 or g.dim == 2: From ae77692d04427007cb63897f393dcda7ae97bd47 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Wed, 22 Mar 2017 09:27:26 +0100 Subject: [PATCH 122/153] Removal of edge crossings in 2d explicitly uses tolerance as argument. --- basics.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/basics.py b/basics.py index b2837b1000..2424be5641 100644 --- a/basics.py +++ b/basics.py @@ -384,7 +384,7 @@ def _add_point(vertices, pt, precision=1e-3, **kwargs): #-----------------------------------------------------------------------------# -def remove_edge_crossings(vertices, edges, **kwargs): +def remove_edge_crossings(vertices, edges, tol=1e-8, **kwargs): """ Process a set of points and connections between them so that the result is an extended point set and new connections that do not intersect. @@ -398,10 +398,12 @@ def remove_edge_crossings(vertices, edges, **kwargs): have tags assigned. If so, the tags are preserved as connections are split. Parameters: - vertices (np.ndarray, 2 x n_pt): Coordinates of points to be processed - edges (np.ndarray, n x n_con): Connections between lines. n >= 2, row + vertices (np.ndarray, 2 x n_pt): Coordinates of points to be processed + edges (np.ndarray, n x n_con): Connections between lines. n >= 2, row 0 and 1 are index of start and endpoints, additional rows are tags - **kwargs: Arguments passed to snap_to_grid + tol (double, optional, default=1e-8): Tolerance used for comparing + equal points. + **kwargs: Arguments passed to snap_to_grid. Returns: np.ndarray, (2 x n_pt), array of points, possibly expanded. @@ -421,6 +423,10 @@ def remove_edge_crossings(vertices, edges, **kwargs): edge_counter = 0 + # Add tolerance to kwargs, this is later passed to split_edges, and further + # on. + kwargs['precision'] = tol + vertices = snap_to_grid(vertices, **kwargs) From f0eb3885f3da95243674981c54ad1c6da39627fa Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Wed, 22 Mar 2017 09:28:08 +0100 Subject: [PATCH 123/153] More careful treatment of almost intersecting edges in intersection removal. --- basics.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/basics.py b/basics.py index 2424be5641..590483a633 100644 --- a/basics.py +++ b/basics.py @@ -461,8 +461,18 @@ def remove_edge_crossings(vertices, edges, tol=1e-8, **kwargs): c1 = a[edge_counter] * (start_x - xm) \ + b[edge_counter] * (start_y - ym) c2 = a[edge_counter] * (end_x - xm) + b[edge_counter] * (end_y - ym) - line_intersections = np.logical_or(np.sign(c1) != np.sign(c2), - np.logical_and(c1 == 0, c2 == 0)) + + # We check for three cases + # 1) Lines crossing + lines_cross = np.sign(c1) != np.sign(c2) + # 2) Lines parallel + parallel_lines = np.logical_and(np.abs(c1) < tol, np.abs(c2) < tol) + # 3) One line look to end on the other + lines_almost_cross = np.logical_or(np.abs(c1) < tol, np.abs(c2) < tol) + # Any of the three above deserves a closer look + line_intersections = np.logical_or(np.logical_or(parallel_lines, + lines_cross), + lines_almost_cross) # Find elements which may intersect. intersections = np.argwhere(line_intersections) @@ -500,6 +510,7 @@ def remove_edge_crossings(vertices, edges, tol=1e-8, **kwargs): vertices[:, edges[1, edge_counter]], vertices[:, edges[0, intsect]], vertices[:, edges[1, intsect]]) + if new_pt is not None: # The case of segment intersections need special treatment. if new_pt.shape[-1] == 1: From aeb5388674411803795b52c08985e332bb50395d Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Wed, 22 Mar 2017 14:31:04 +0100 Subject: [PATCH 124/153] Change of keyword for comparing points in computational geometry. Now use tol all over the place. --- basics.py | 18 +++++++++--------- tests/test_intersections.py | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/basics.py b/basics.py index ba47302f80..591d5b2692 100644 --- a/basics.py +++ b/basics.py @@ -18,7 +18,7 @@ # #------------------------------------------------------------------------------# -def snap_to_grid(pts, precision=1e-3, box=None): +def snap_to_grid(pts, tol=1e-3, box=None): """ Snap points to an underlying Cartesian grid. Used e.g. for avoiding rounding issues when testing for equality between @@ -34,7 +34,7 @@ def snap_to_grid(pts, precision=1e-3, box=None): array([[ 0.24 ], [ 0.501]]) - >>> snap_to_grid([[0.2443], [0.501]], precision=0.01) + >>> snap_to_grid([[0.2443], [0.501]], tol=0.01) array([[ 0.24], [ 0.5 ]]) @@ -59,7 +59,7 @@ def snap_to_grid(pts, precision=1e-3, box=None): box = np.asarray(box) # Precission in each direction - delta = box * precision + delta = box * tol pts = np.rint(pts.astype(np.float64) / delta) * delta return pts @@ -329,7 +329,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): #------------------------------------------------------------------------------# -def _add_point(vertices, pt, precision=1e-3, **kwargs): +def _add_point(vertices, pt, tol=1e-3, **kwargs): """ Add a point to a point set, unless the point already exist in the set. @@ -353,8 +353,8 @@ def _add_point(vertices, pt, precision=1e-3, **kwargs): np.ndarray, nd x 1: The new point, or None if no new point was needed. """ - if not 'precision' in kwargs: - kwargs['precision'] = precision + if not 'tol' in kwargs: + kwargs['tol'] = tol nd = vertices.shape[0] # Before comparing coordinates, snap both existing and new point to the @@ -369,7 +369,7 @@ def _add_point(vertices, pt, precision=1e-3, **kwargs): dist = __dist(pt[:, i].reshape((-1, 1)), vertices) min_dist = np.min(dist) - if min_dist < precision * np.sqrt(nd): + if min_dist < tol * np.sqrt(nd): # No new point is needed ind.append(np.argmin(dist)) else: @@ -384,7 +384,7 @@ def _add_point(vertices, pt, precision=1e-3, **kwargs): #-----------------------------------------------------------------------------# -def remove_edge_crossings(vertices, edges, tol=1e-8, **kwargs): +def remove_edge_crossings(vertices, edges, tol=1e-3, **kwargs): """ Process a set of points and connections between them so that the result is an extended point set and new connections that do not intersect. @@ -425,7 +425,7 @@ def remove_edge_crossings(vertices, edges, tol=1e-8, **kwargs): # Add tolerance to kwargs, this is later passed to split_edges, and further # on. - kwargs['precision'] = tol + kwargs['tol'] = tol vertices = snap_to_grid(vertices, **kwargs) diff --git a/tests/test_intersections.py b/tests/test_intersections.py index 44c6fcdc0e..6a818760a3 100644 --- a/tests/test_intersections.py +++ b/tests/test_intersections.py @@ -148,11 +148,11 @@ def setUp(self): self.p = np.array([0.6, 0.6]) def test_snapping(self): - p_snapped = basics.snap_to_grid(self.p, box=self.box, precision=1) + p_snapped = basics.snap_to_grid(self.p, box=self.box, tol=1) assert np.allclose(p_snapped, np.array([1, 1])) def test_aniso_snapping(self): - p_snapped = basics.snap_to_grid(self.p, box=self.anisobox, precision=1) + p_snapped = basics.snap_to_grid(self.p, box=self.anisobox, tol=1) assert np.allclose(p_snapped, np.array([0, 1])) if __name__ == '__main__': From 97426916e1331de656e8169052f1ae36a9de9dd9 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Thu, 23 Mar 2017 19:42:08 +0100 Subject: [PATCH 125/153] Modified tolerances in removal of edge intersections in 2d. Not sure if this is the final version, but it is surely a step in the right direction. --- basics.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/basics.py b/basics.py index 591d5b2692..a71c33f72a 100644 --- a/basics.py +++ b/basics.py @@ -448,6 +448,8 @@ def remove_edge_crossings(vertices, edges, tol=1e-3, **kwargs): a = end_y - start_y b = -(end_x - start_x) + + # Midpoint of this edge xm = (start_x[edge_counter] + end_x[edge_counter]) / 2 ym = (start_y[edge_counter] + end_y[edge_counter]) / 2 @@ -457,18 +459,22 @@ def remove_edge_crossings(vertices, edges, tol=1e-3, **kwargs): # # http://stackoverflow.com/questions/385305/efficient-maths-algorithm-to-calculate-intersections # - # for more information. + # answer by PolyThinker and comments by Jason S, for more information. c1 = a[edge_counter] * (start_x - xm) \ + b[edge_counter] * (start_y - ym) c2 = a[edge_counter] * (end_x - xm) + b[edge_counter] * (end_y - ym) + tol_scaled = tol * np.max([np.sqrt(np.abs(c1)), np.sqrt(np.abs(c2))]) + # We check for three cases # 1) Lines crossing lines_cross = np.sign(c1) != np.sign(c2) # 2) Lines parallel - parallel_lines = np.logical_and(np.abs(c1) < tol, np.abs(c2) < tol) + parallel_lines = np.logical_and(np.abs(c1) < tol_scaled, + np.abs(c2) < tol_scaled) # 3) One line look to end on the other - lines_almost_cross = np.logical_or(np.abs(c1) < tol, np.abs(c2) < tol) + lines_almost_cross = np.logical_or(np.abs(c1) < tol_scaled, + np.abs(c2) < tol_scaled) # Any of the three above deserves a closer look line_intersections = np.logical_or(np.logical_or(parallel_lines, lines_cross), From 230a1153222ac2efffa04e3daefeb450fd485c84 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Mon, 27 Mar 2017 11:54:21 +0200 Subject: [PATCH 126/153] More on-the-fly checks in line intersection routines. --- basics.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/basics.py b/basics.py index a71c33f72a..d49a4d129f 100644 --- a/basics.py +++ b/basics.py @@ -179,6 +179,15 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): edges[:, edge_ind_first+1:])) # We have added a single new line new_line = 1 + + # Sanity check of new edge + if np.any(np.diff(edges[:2], axis=0) == 0): + raise ValueError('Have created a point edge') + edge_copy = np.sort(edges[:2], axis=0) + edge_unique, *new_2_old = setmembership.unique_columns_tol(edge_copy) + if edge_unique.shape[1] < edges.shape[1]: + raise ValueError('Have created the same edge twice') + return vertices, edges, new_line else: # Without this, we will delete something we should not delete below. @@ -324,6 +333,13 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): else: raise ValueError('How did it come to this') + # Check validity of the new edge configuration + if np.any(np.diff(edges[:2], axis=0) == 0): + raise ValueError('Have created a point edge') + edge_copy = np.sort(edges[:2], axis=0) + edge_unique, *new_2_old = setmembership.unique_columns_tol(edge_copy) + if edge_unique.shape[1] < edges.shape[1]: + raise ValueError('Have created the same edge twice') return vertices, edges, new_line @@ -547,6 +563,10 @@ def remove_edge_crossings(vertices, edges, tol=1e-3, **kwargs): intersections += splits[0] + splits[1] # Update index of possible intersections + # Sanity check - turned out to be useful for debugging. + if np.any(np.diff(edges[:2], axis=0) == 0): + raise ValueError('Have somehow created a point edge') + # We're done with this candidate edge. Increase index of inner loop int_counter += 1 # We're done with all intersections of this loop. increase index of From ca20114b89c05d9d3bb40f1adb6edea0d9ed45ba Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Mon, 27 Mar 2017 11:55:18 +0200 Subject: [PATCH 127/153] Bugfix in method for splitting lines (2d line interseciton routine) --- basics.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/basics.py b/basics.py index d49a4d129f..c05fd79a90 100644 --- a/basics.py +++ b/basics.py @@ -11,6 +11,7 @@ from math import sqrt import sympy from sympy import geometry as geom +from utils import setmembership #----------------------------------------------------------------------------- # @@ -164,16 +165,36 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): pi = pt_ind[0] # Intersection at a point. if start == pi or end == pi: - # Nothing happens + # Nothing happens, the intersection between the edges coincides + # with a shared vertex for the edges new_line = 0 return vertices, edges, new_line else: + # We may need to split the edge (start, end) into two new_edges = np.array([[start, pi], [pi, end]]) + # ... however, the new edges may already exist in the set (this + # apparently can happen for complex networks with several fractures + # sharing a line). + # Check if the new candidate edges already are defined in the set + # of edges + ismem, _ = setmembership.ismember_rows(new_edges, edges[:2]) + if any(ismem): + new_edges = np.delete(new_edges, + np.squeeze(np.argwhere(ismem)), + axis=0) + if new_edges.shape[0] == 1: + new_edges = new_edges.reshape((-1, 1)) + + if new_edges.size == 0: + new_line = 0 + return vertices, edges, new_line + # Add any tags to the new edge. if tags.size > 0: new_edges = np.vstack((new_edges, - np.tile(tags[:, np.newaxis], 2))) + np.tile(tags[:, np.newaxis], + new_edges.shape[1]))) # Insert the new edge in the midle of the set of edges. edges = np.hstack((edges[:, :edge_ind_first], new_edges, edges[:, edge_ind_first+1:])) From ac720f656804da8525fe5084de405d2d5e54f92b Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Mon, 27 Mar 2017 11:56:18 +0200 Subject: [PATCH 128/153] Minor bugfix in splitting of lines due to intersections. --- basics.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/basics.py b/basics.py index c05fd79a90..45b939336e 100644 --- a/basics.py +++ b/basics.py @@ -252,14 +252,15 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): elif i0 == start and i1 == end: # We don't know if i0 is closest to the start or end of edges[:, # edges_ind[1]]. Find the nearest. - if __dist(vertices[:, i0], vertices[:, edges[0, edge_ind[1]]]) < \ - __dist(vertices[:, i1], vertices[:, edges[0, edge_ind[1]]]): + if __dist(np.squeeze(vertices[:, i0]), + vertices[:, edges[0, edge_ind[1]]]) < \ + __dist(np.squeeze(vertices[:, i1]), + vertices[:, edges[0, edge_ind[1]]]): other_start = edges[0, edge_ind[1]] other_end = edges[1, edge_ind[1]] else: other_start = edges[1, edge_ind[1]] other_end = edges[0, edge_ind[1]] - # New segments (i0, i1) is identical to the old edge_ind[0] new_edges = np.array([[other_start, i0, i1], [i0, i1, other_end]]) From 4ca356a4fbb74ad4a6ff31beec3b71ef49e26b7f Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Mon, 27 Mar 2017 11:56:49 +0200 Subject: [PATCH 129/153] Fix of scaling error in search for line intersections. The previous code sometime resulted in a tolerance of zero. --- basics.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/basics.py b/basics.py index 45b939336e..799ac8e348 100644 --- a/basics.py +++ b/basics.py @@ -502,7 +502,8 @@ def remove_edge_crossings(vertices, edges, tol=1e-3, **kwargs): + b[edge_counter] * (start_y - ym) c2 = a[edge_counter] * (end_x - xm) + b[edge_counter] * (end_y - ym) - tol_scaled = tol * np.max([np.sqrt(np.abs(c1)), np.sqrt(np.abs(c2))]) + tol_scaled = tol * max(1, np.max([np.sqrt(np.abs(c1)), + np.sqrt(np.abs(c2))])) # We check for three cases # 1) Lines crossing From 53eb831a6c0ddeaa5c53b2521d2ec0c87a2113f1 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Mon, 27 Mar 2017 14:58:13 +0200 Subject: [PATCH 130/153] Splitting of edge returns indicator of applied strategy. Edge cross removals saves information. Useful for debugging. --- basics.py | 61 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/basics.py b/basics.py index 799ac8e348..0213b9adb5 100644 --- a/basics.py +++ b/basics.py @@ -131,6 +131,8 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): np.ndarray, nd x n_pt: new point set, possibly with new point inserted. np.ndarray, n x n_con: new edge set, possibly with new lines defined. boolean: True if a new line is created, otherwise false. + int: Splitting type, indicating which splitting strategy was used. + Intended for debugging. """ @@ -168,7 +170,8 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): # Nothing happens, the intersection between the edges coincides # with a shared vertex for the edges new_line = 0 - return vertices, edges, new_line + split_type = 0 + return vertices, edges, new_line, split_type else: # We may need to split the edge (start, end) into two new_edges = np.array([[start, pi], @@ -188,7 +191,8 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): if new_edges.size == 0: new_line = 0 - return vertices, edges, new_line + split_type = 1 + return vertices, edges, new_line, split_type # Add any tags to the new edge. if tags.size > 0: @@ -209,7 +213,8 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): if edge_unique.shape[1] < edges.shape[1]: raise ValueError('Have created the same edge twice') - return vertices, edges, new_line + split_type = 2 + return vertices, edges, new_line, split_type else: # Without this, we will delete something we should not delete below. assert edge_ind[0] < edge_ind[1] @@ -218,6 +223,15 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): # No new points should have been made assert pt_ind[0] <= orig_num_pts and pt_ind[1] <= orig_num_pts + pt_ind = np.reshape(np.array(pt_ind), (-1, 1)) + edge_exists, _ = setmembership.ismember_rows(pt_ind, edges[:2]) + if np.any(edge_exists): + import pdb + pdb.set_trace() + new_line = np.zeros(2, dtype='int') + split_type = 3 + return vertices, edges, new_line, split_type + # There are three (four) possible configurations # a) The intersection is contained within (start, end). edge_ind[0] # should be split into three segments, and edge_ind[1] should be @@ -249,6 +263,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): new_edges, edges[:, edge_ind[0]+1:])) new_line = [2, -1] + split_type = 4 elif i0 == start and i1 == end: # We don't know if i0 is closest to the start or end of edges[:, # edges_ind[1]]. Find the nearest. @@ -277,6 +292,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): edges = np.delete(edges, edge_ind[0], axis=1) new_line = [-1, 2] + split_type = 5 # Note that we know that i0 is closest to start, thus no need to test # for i1 == start @@ -322,6 +338,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): new_edges, edges[:, (edge_ind[0]+1):])) new_line = [1, -did_delete] + split_type = 5 elif i0 != start and i1 == end: # Analogous configuration as the one above, but with i0 replaced by @@ -352,6 +369,8 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): new_edges, edges[:, (edge_ind[0]+1):])) new_line = [1, -did_delete] + split_type = 6 + else: raise ValueError('How did it come to this') @@ -363,7 +382,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): if edge_unique.shape[1] < edges.shape[1]: raise ValueError('Have created the same edge twice') - return vertices, edges, new_line + return vertices, edges, new_line, split_type #------------------------------------------------------------------------------# @@ -467,6 +486,7 @@ def remove_edge_crossings(vertices, edges, tol=1e-3, **kwargs): vertices = snap_to_grid(vertices, **kwargs) + split_type = [] # Loop over all edges, search for intersections. The number of edges can # change due to splitting. @@ -564,25 +584,34 @@ def remove_edge_crossings(vertices, edges, tol=1e-3, **kwargs): # means the intersection runs through an endpoint of the # edge in an L-type configuration, in which case no new point # is needed) - vertices, edges, split_outer_edge = _split_edge(vertices, edges, - edge_counter, - new_pt, - **kwargs) + vertices, edges, split_outer_edge,\ + split = _split_edge(vertices, edges, + edge_counter, new_pt, + **kwargs) + split_type.append(split) + # If the outer edge (represented by edge_counter) was split, # e.g. inserted into the list of edges we need to increase the # index of the inner edge intsect += split_outer_edge # Possibly split the inner edge - vertices, edges, split_inner_edge = _split_edge(vertices, edges, - intsect, - new_pt, - **kwargs) + vertices, edges, split_inner_edge, \ + split = _split_edge(vertices, edges, intsect, + new_pt, **kwargs) + split_type.append(split) + intersections += split_outer_edge + split_inner_edge else: - vertices, edges, splits = _split_edge(vertices, edges, - [edge_counter, - intsect], - new_pt, **kwargs) + if verbose > 2: + print(' Found two intersections: (' + + str(new_pt[0, 0]) + ', ' + str(new_pt[1, 0]) + + 'and ' + str(new_pt[0, 1]) + ', ' + + str(new_pt[1, 1])) + vertices, edges, splits,\ + s_type = _split_edge(vertices, edges, + [edge_counter, intsect], + new_pt, **kwargs) + split_type.append(s_type) intersections += splits[0] + splits[1] # Update index of possible intersections From 9194521fff9896b0d2cc2dcc198e5c72ca513bf8 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Mon, 27 Mar 2017 14:59:42 +0200 Subject: [PATCH 131/153] Edge intersection removal prints progress, according to verbosity level. --- basics.py | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/basics.py b/basics.py index 0213b9adb5..6911c0cf48 100644 --- a/basics.py +++ b/basics.py @@ -11,6 +11,8 @@ from math import sqrt import sympy from sympy import geometry as geom +import time + from utils import setmembership #----------------------------------------------------------------------------- @@ -161,7 +163,6 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): # Sanity check assert len(pt_ind) <= 2 - # Check for a single intersection point if len(pt_ind) < 2: pi = pt_ind[0] @@ -441,7 +442,7 @@ def _add_point(vertices, pt, tol=1e-3, **kwargs): #-----------------------------------------------------------------------------# -def remove_edge_crossings(vertices, edges, tol=1e-3, **kwargs): +def remove_edge_crossings(vertices, edges, tol=1e-3, verbose=0, **kwargs): """ Process a set of points and connections between them so that the result is an extended point set and new connections that do not intersect. @@ -470,6 +471,11 @@ def remove_edge_crossings(vertices, edges, tol=1e-3, **kwargs): NotImplementedError if a 3D point array is provided. """ + if verbose > 1: + start_time = time.time() + num_edges_orig = edges.shape[1] + print(' Find intersections between ' + str(num_edges_orig) + ' edges') + num_edges = edges.shape[1] nd = vertices.shape[0] @@ -497,7 +503,6 @@ def remove_edge_crossings(vertices, edges, tol=1e-3, **kwargs): # intersection of line and line segment). We then go on to test for # real intersections. - # Find start and stop coordinates for all edges start_x = vertices[0, edges[0]] start_y = vertices[1, edges[0]] @@ -551,7 +556,13 @@ def remove_edge_crossings(vertices, edges, tol=1e-3, **kwargs): edge_counter += 1 continue - int_counter = 0 + if verbose > 2: + print(' ------') + print(' Splitting edge no ' + str(edge_counter) + '. Vertices:') + print(' ' + str(vertices[:, edges[0, edge_counter]])) + print(' ' + str(vertices[:, edges[1, edge_counter]])) + + int_counter = 0 while intersections.size > 0 and int_counter < intersections.size: # Line intersect (inner loop) is an intersection if it crosses # the extension of line edge_counter (outer loop) (ie intsect it @@ -570,6 +581,12 @@ def remove_edge_crossings(vertices, edges, tol=1e-3, **kwargs): int_counter += 1 continue + if verbose > 2: + print(' Intersection with edge ' + str(intsect) + + '. Vertices:') + print(' ' + str(vertices[:, edges[0, intsect]])) + print(' ' + str(vertices[:, edges[1, intsect]])) + # Check if this point intersects new_pt = lines_intersect(vertices[:, edges[0, edge_counter]], vertices[:, edges[1, edge_counter]], @@ -579,6 +596,10 @@ def remove_edge_crossings(vertices, edges, tol=1e-3, **kwargs): if new_pt is not None: # The case of segment intersections need special treatment. if new_pt.shape[-1] == 1: + if verbose > 2: + print(' Found intersection: (' + str(new_pt[0]) + + ', ' + str(new_pt[1])) + # Split edge edge_counter (outer loop), unless the # intersection hits an existing point (in practices this # means the intersection runs through an endpoint of the @@ -624,6 +645,13 @@ def remove_edge_crossings(vertices, edges, tol=1e-3, **kwargs): # We're done with all intersections of this loop. increase index of # outer loop edge_counter += 1 + + if verbose > 1: + print(' Edge intersection removal complete. Elapsed time ' +\ + str(time.time() - start_time)) + print(' Introduced ' + str(edges.shape[1] - num_edges_orig) + \ + ' new edges') + return vertices, edges #---------------------------------------------------------- From c38ba372c3b5083b9ffc23dac835b39a048c3f6e Mon Sep 17 00:00:00 2001 From: Runar Date: Tue, 28 Mar 2017 10:41:20 +0200 Subject: [PATCH 132/153] added **kwargs to snap_to_grid for consistency. So far the function will not use **kwargs --- basics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/basics.py b/basics.py index a71c33f72a..902255f29a 100644 --- a/basics.py +++ b/basics.py @@ -18,7 +18,7 @@ # #------------------------------------------------------------------------------# -def snap_to_grid(pts, tol=1e-3, box=None): +def snap_to_grid(pts, tol=1e-3, box=None, **kwargs): """ Snap points to an underlying Cartesian grid. Used e.g. for avoiding rounding issues when testing for equality between @@ -327,7 +327,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): return vertices, edges, new_line -#------------------------------------------------------------------------------# +#------------------------------------------------------**kwargs------------------------# def _add_point(vertices, pt, tol=1e-3, **kwargs): """ From 2b04906fcd10b4fe5b9ab289b423408a0e9e3e8d Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Tue, 28 Mar 2017 13:51:32 +0200 Subject: [PATCH 133/153] Removed unused functions in basic computational geometry. --- basics.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/basics.py b/basics.py index 6911c0cf48..7506a02345 100644 --- a/basics.py +++ b/basics.py @@ -76,22 +76,6 @@ def __nrm(v): def __dist(p1, p2): return __nrm(p1 - p2) -#------------------------------------------------------------------------------# -# -# def is_unique_vert_array(pts, box=None, precision=1e-3): -# nd = pts.shape[0] -# num_pts = pts.shape[1] -# pts = snap_to_grid(pts, box, precision) -# for iter in range(num_pts): -# - -#------------------------------------------------------------------------------# - -def __points_equal(p1, p2, box, precesion=1e-3): - d = __dist(p1, p2) - nd = p1.shape[0] - return d < precesion * sqrt(nd) - #------------------------------------------------------------------------------# def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): From 6c5d1064c23f49bce72bc83c0ed4ac6ad7902426 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Tue, 28 Mar 2017 13:53:56 +0200 Subject: [PATCH 134/153] Bugfix in splitting of edges, counting of number of new lines. Gave further counting errors in remove_edge_intersections. --- basics.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/basics.py b/basics.py index 7506a02345..290d1860f9 100644 --- a/basics.py +++ b/basics.py @@ -187,8 +187,9 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): # Insert the new edge in the midle of the set of edges. edges = np.hstack((edges[:, :edge_ind_first], new_edges, edges[:, edge_ind_first+1:])) - # We have added a single new line - new_line = 1 + # We have added as many new edges as there are columns in new_edges, + # minus 1 (which was removed / ignored). + new_line = new_edges.shape[1] - 1 # Sanity check of new edge if np.any(np.diff(edges[:2], axis=0) == 0): From a40845d0d30eaf95c594964c4d0f5537f59f98ce Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Tue, 28 Mar 2017 13:56:44 +0200 Subject: [PATCH 135/153] Handle creation of duplicate edges in edge split. --- basics.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/basics.py b/basics.py index 290d1860f9..9ceac7a003 100644 --- a/basics.py +++ b/basics.py @@ -363,10 +363,17 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): # Check validity of the new edge configuration if np.any(np.diff(edges[:2], axis=0) == 0): raise ValueError('Have created a point edge') - edge_copy = np.sort(edges[:2], axis=0) - edge_unique, *new_2_old = setmembership.unique_columns_tol(edge_copy) - if edge_unique.shape[1] < edges.shape[1]: - raise ValueError('Have created the same edge twice') + + # We may have created an edge that already existed in the grid. Remove + # this by uniquifying the edges. + # Hopefully, we do not mess up the edges here. + edges_copy = np.sort(edges[:2], axis=0) + edges_unique, *new_2_old = setmembership.unique_columns_tol(edges_copy) + if edges_unique.shape[1] < edges.shape[1]: + new_line[0] -= edges.shape[1] - edges_unique.shape[1] + edges = edges_unique + # Also signify that we have carried out this operation. + split_type = [split_type, 7] return vertices, edges, new_line, split_type From 91fede9141772a2e64cf5de359e08c64ef43f057 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Wed, 29 Mar 2017 08:36:52 +0200 Subject: [PATCH 136/153] Minor fix of doctest in basic computational geometry. --- basics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basics.py b/basics.py index 9ceac7a003..9e20375a18 100644 --- a/basics.py +++ b/basics.py @@ -95,7 +95,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): >>> p = np.array([[0, 0], [0, 1]]) >>> edges = np.array([[0], [1]]) >>> new_pt = np.array([[0], [0.5]]) - >>> v, e, nl = _split_edge(p, edges, 0, new_pt) + >>> v, e, nl, _ = _split_edge(p, edges, 0, new_pt) >>> e array([[0, 2], [2, 1]]) From 63da8fc30a533bc71c5355510afafbea9eac9509 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Wed, 29 Mar 2017 08:37:31 +0200 Subject: [PATCH 137/153] Removed premature test of existing edges in edge splitting, comp geom. The right approach is to remove duplicate edges after the splitting is complete, as is done at the very end of the function. --- basics.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/basics.py b/basics.py index 9e20375a18..552ca2771b 100644 --- a/basics.py +++ b/basics.py @@ -210,13 +210,6 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): assert pt_ind[0] <= orig_num_pts and pt_ind[1] <= orig_num_pts pt_ind = np.reshape(np.array(pt_ind), (-1, 1)) - edge_exists, _ = setmembership.ismember_rows(pt_ind, edges[:2]) - if np.any(edge_exists): - import pdb - pdb.set_trace() - new_line = np.zeros(2, dtype='int') - split_type = 3 - return vertices, edges, new_line, split_type # There are three (four) possible configurations # a) The intersection is contained within (start, end). edge_ind[0] From 1104858c55ac6f40c45e78e7d734d58d542f2049 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Wed, 29 Mar 2017 08:43:09 +0200 Subject: [PATCH 138/153] Bugfix in line_intersections (2d) in computational geometry module. The previous version failed to verify that the intersections were on both segments. --- basics.py | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/basics.py b/basics.py index 552ca2771b..fac50e78ac 100644 --- a/basics.py +++ b/basics.py @@ -845,12 +845,25 @@ def lines_intersect(start_1, end_1, start_2, end_2, tol=1e-8): start_2 = np.asarray(start_2).astype(np.float) end_2 = np.asarray(end_2).astype(np.float) + # Vectors along first and second line d_1 = end_1 - start_1 d_2 = end_2 - start_2 + # Vector between the start points d_s = start_2 - start_1 - discr = d_1[0] * d_2[1] - d_1[1] * d_2[0] + # An intersection point is characterized by + # start_1 + d_1 * t_1 = start_2 + d_2 * t_2 + # + # which on component form becomes + # + # d_1[0] * t_1 - d_2[0] * t_2 = d_s[0] + # d_1[1] * t_1 - d_2[1] * t_2 = d_s[1] + # + # First check for solvability of the system (e.g. parallel lines) by the + # determinant of the matrix. + + discr = d_1[0] *(-d_2[1]) - d_1[1] * (-d_2[0]) if np.abs(discr) < tol: # The lines are parallel, and will only cross if they are also colinear @@ -894,10 +907,19 @@ def lines_intersect(start_1, end_1, start_2, end_2, tol=1e-8): # Lines are parallel, but not colinear return None else: - t = (d_s[0] * d_2[1] - d_s[1] * d_2[0]) / discr - isect = start_1 + t * d_1 - if t >= 0 and t <= 1: - return np.array([[isect[0]], [isect[1]]]) + # Solve linear system using Cramer's rule + t_1 = (d_s[0] * (-d_2[1]) - d_s[1] * (-d_2[0])) / discr + t_2 = (d_1[0] * d_s[1] - d_1[1] * d_s[0]) / discr + + isect_1 = start_1 + t_1 * d_1 + isect_2 = start_2 + t_2 * d_2 + # Safeguarding + assert np.allclose(isect_1, isect_2, tol) + + # The intersection lies on both segments if both t_1 and t_2 are on the + # unit interval + if t_1 >= 0 and t_1 <= 1 and t_2 >= 0 and t_2 <=1: + return np.array([[isect_1[0]], [isect_1[1]]]) else: return None From 40d0ec862fbfaa1a070380d3c087e4f569467805 Mon Sep 17 00:00:00 2001 From: Alessio Fumagalli Date: Thu, 30 Mar 2017 16:01:20 +0200 Subject: [PATCH 139/153] add the computation of the normal and tangential projection matrix --- basics.py | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/basics.py b/basics.py index fac50e78ac..1114390f85 100644 --- a/basics.py +++ b/basics.py @@ -1417,6 +1417,53 @@ def rot( a, vect ): #------------------------------------------------------------------------------# +def normal_matrix(pts=None, normal=None): + """ Compute the normal projection matrix of a plane. + + The algorithm assume that the points lie on a plane. + Three non-aligned points are required. + + Either points or normal are mandatory. + + Parameters: + pts (optional): np.ndarray, 3xn, the points. Need n > 2. + normal (optional): np.array, 1x3, the normal. + + Returns: + normal matrix: np.array, 3x3, the normal matrix. + + """ + if normal is not None: + normal = normal / np.linalg.norm( normal ) + elif pts is not None: + normal = compute_normal( pts ) + else: + assert False, "Points or normal are mandatory" + + return np.tensordot(normal, normal, axes=0) + +#------------------------------------------------------------------------------# + +def tangent_matrix(pts=None, normal=None): + """ Compute the tangential projection matrix of a plane. + + The algorithm assume that the points lie on a plane. + Three non-aligned points are required. + + Either points or normal are mandatory. + + Parameters: + pts (optional): np.ndarray, 3xn, the points. Need n > 2. + normal (optional): np.array, 1x3, the normal. + + Returns: + tangential matrix: np.array, 3x3, the tangential matrix. + + """ + return np.eye(3) - normal_matrix(pts, normal) + +#------------------------------------------------------------------------------# + def compute_normal(pts): """ Compute the normal of a set of points. From e0f1a118f005d070f9c5e3d7f75bfad7d7113a12 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Fri, 31 Mar 2017 08:26:49 +0200 Subject: [PATCH 140/153] Better treatment of tolerances in computational geometry --- basics.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/basics.py b/basics.py index fac50e78ac..8085a41748 100644 --- a/basics.py +++ b/basics.py @@ -122,6 +122,8 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): """ + tol = kwargs['tol'] + # Some back and forth with the index of the edges to be split, depending on # whether it is one or two edge_ind = np.asarray(edge_ind) @@ -195,7 +197,8 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): if np.any(np.diff(edges[:2], axis=0) == 0): raise ValueError('Have created a point edge') edge_copy = np.sort(edges[:2], axis=0) - edge_unique, *new_2_old = setmembership.unique_columns_tol(edge_copy) + edge_unique, *new_2_old = setmembership.unique_columns_tol(edge_copy, + tol=tol) if edge_unique.shape[1] < edges.shape[1]: raise ValueError('Have created the same edge twice') @@ -361,7 +364,8 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): # this by uniquifying the edges. # Hopefully, we do not mess up the edges here. edges_copy = np.sort(edges[:2], axis=0) - edges_unique, *new_2_old = setmembership.unique_columns_tol(edges_copy) + edges_unique, *new_2_old = setmembership.unique_columns_tol(edges_copy, + tol=tol) if edges_unique.shape[1] < edges.shape[1]: new_line[0] -= edges.shape[1] - edges_unique.shape[1] edges = edges_unique @@ -385,7 +389,7 @@ def _add_point(vertices, pt, tol=1e-3, **kwargs): Parameters: vertices (np.ndarray, nd x num_pts): existing point set pt (np.ndarray, nd x 1): Point to be added - precesion (double): Precision of underlying Cartesian grid + tol (double): Precision of underlying Cartesian grid **kwargs: Arguments passed to snap_to_grid Returns: @@ -412,7 +416,10 @@ def _add_point(vertices, pt, tol=1e-3, **kwargs): dist = __dist(pt[:, i].reshape((-1, 1)), vertices) min_dist = np.min(dist) - if min_dist < tol * np.sqrt(nd): + # The tolerance parameter here turns out to be critical in an edge + # intersection removal procedure. The scaling factor is somewhat + # arbitrary, and should be looked into. + if min_dist < tol * np.sqrt(3): # No new point is needed ind.append(np.argmin(dist)) else: From 911ad3d147130de456c8636e1921674132dba3c7 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Fri, 31 Mar 2017 08:27:09 +0200 Subject: [PATCH 141/153] Various updates to removal of edge intersections in comp geom. --- basics.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/basics.py b/basics.py index 8085a41748..e1a8bf6948 100644 --- a/basics.py +++ b/basics.py @@ -484,6 +484,8 @@ def remove_edge_crossings(vertices, edges, tol=1e-3, verbose=0, **kwargs): vertices = snap_to_grid(vertices, **kwargs) + # Field used for debugging of edge splits. To see the meaning of the values + # of each split, look in the source code of split_edges. split_type = [] # Loop over all edges, search for intersections. The number of edges can @@ -583,9 +585,22 @@ def remove_edge_crossings(vertices, edges, tol=1e-3, verbose=0, **kwargs): new_pt = lines_intersect(vertices[:, edges[0, edge_counter]], vertices[:, edges[1, edge_counter]], vertices[:, edges[0, intsect]], - vertices[:, edges[1, intsect]]) + vertices[:, edges[1, intsect]], + tol=tol) + + def __min_dist(p): + md = np.inf + for pi in [edges[0, edge_counter], + edges[1, edge_counter], + edges[0, intsect], edges[1, intsect]]: + md = min(md, __dist(np.squeeze(p), vertices[:, pi])) + return md + + orig_vertex_num = vertices.shape[1] + orig_edge_num = edges.shape[1] if new_pt is not None: + new_pt = snap_to_grid(new_pt, tol=tol) # The case of segment intersections need special treatment. if new_pt.shape[-1] == 1: if verbose > 2: @@ -597,43 +612,76 @@ def remove_edge_crossings(vertices, edges, tol=1e-3, verbose=0, **kwargs): # means the intersection runs through an endpoint of the # edge in an L-type configuration, in which case no new point # is needed) + md = __min_dist(new_pt) vertices, edges, split_outer_edge,\ split = _split_edge(vertices, edges, edge_counter, new_pt, **kwargs) split_type.append(split) - + if verbose > 2 and split_outer_edge > 0 and \ + vertices.shape[1] > orig_vertex_num: + print(' Introduced new point. Min length of edges:' + + str(md)) + if md < tol: + import pdb + pdb.set_trace() + + if edges.shape[1] > orig_edge_num + split_outer_edge: + raise ValueError('Have created edge without bookkeeping') # If the outer edge (represented by edge_counter) was split, # e.g. inserted into the list of edges we need to increase the # index of the inner edge intsect += split_outer_edge + # Possibly split the inner edge vertices, edges, split_inner_edge, \ split = _split_edge(vertices, edges, intsect, new_pt, **kwargs) - split_type.append(split) + if edges.shape[1] > \ + orig_edge_num + split_inner_edge + split_outer_edge: + raise ValueError('Have created edge without bookkeeping') + split_type.append(split) + if verbose > 2 and split_inner_edge > 0 and \ + vertices.shape[1] > orig_vertex_num: + print(' Introduced new point. Min length of edges:' + + str(md)) + if md < tol: + import pdb + pdb.set_trace() intersections += split_outer_edge + split_inner_edge else: + # We have found an intersection along a line segment if verbose > 2: print(' Found two intersections: (' + str(new_pt[0, 0]) + ', ' + str(new_pt[1, 0]) + 'and ' + str(new_pt[0, 1]) + ', ' + str(new_pt[1, 1])) + vertices, edges, splits,\ s_type = _split_edge(vertices, edges, [edge_counter, intsect], new_pt, **kwargs) split_type.append(s_type) intersections += splits[0] + splits[1] - # Update index of possible intersections + if verbose > 2 and (splits[0] > 0 or splits[1] > 0): + print(' Introduced new point') - # Sanity check - turned out to be useful for debugging. + # Sanity checks - turned out to be useful for debugging. if np.any(np.diff(edges[:2], axis=0) == 0): + if verbose > 3: + import pdb + pdb.set_trace() raise ValueError('Have somehow created a point edge') + if intersections.max() > edges.shape[1]: + raise ValueError('Intersection pointer outside edge array') + if verbose > 3: + import pdb + pdb.set_trace() # We're done with this candidate edge. Increase index of inner loop int_counter += 1 + # We're done with all intersections of this loop. increase index of # outer loop edge_counter += 1 From a2c3e99cd21711dbd21d9558fa4b1933efc1028f Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Mon, 3 Apr 2017 08:59:15 +0200 Subject: [PATCH 142/153] Line intersection in comp geom accounts for rounding errors. It is not yet clear whether it is meaningful to use the same tolerances throughout the function. Not to mention is different functions.. --- basics.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/basics.py b/basics.py index 1159d4f17b..f207edf35b 100644 --- a/basics.py +++ b/basics.py @@ -972,8 +972,10 @@ def lines_intersect(start_1, end_1, start_2, end_2, tol=1e-8): assert np.allclose(isect_1, isect_2, tol) # The intersection lies on both segments if both t_1 and t_2 are on the - # unit interval - if t_1 >= 0 and t_1 <= 1 and t_2 >= 0 and t_2 <=1: + # unit interval. + # Use tol to allow some approximations + if t_1 >= -tol and t_1 <= (1 + tol) and \ + t_2 >= -tol and t_2 <= (1 + tol): return np.array([[isect_1[0]], [isect_1[1]]]) else: return None From 3f8d845b30707c2ea96c260cb2e0608ce3e73c61 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Mon, 3 Apr 2017 11:48:24 +0200 Subject: [PATCH 143/153] Minor bugfix in splitting of edges in comp geom. --- basics.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/basics.py b/basics.py index f207edf35b..7ebc92c972 100644 --- a/basics.py +++ b/basics.py @@ -320,7 +320,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): new_edges, edges[:, (edge_ind[0]+1):])) new_line = [1, -did_delete] - split_type = 5 + split_type = 6 elif i0 != start and i1 == end: # Analogous configuration as the one above, but with i0 replaced by @@ -351,7 +351,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): new_edges, edges[:, (edge_ind[0]+1):])) new_line = [1, -did_delete] - split_type = 6 + split_type = 7 else: raise ValueError('How did it come to this') @@ -370,7 +370,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): new_line[0] -= edges.shape[1] - edges_unique.shape[1] edges = edges_unique # Also signify that we have carried out this operation. - split_type = [split_type, 7] + split_type = [split_type, 8] return vertices, edges, new_line, split_type From a2539c430e2b88efca571f70cac7e4cfd2a9c297 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Mon, 3 Apr 2017 12:18:58 +0200 Subject: [PATCH 144/153] Bugfix in splitting of edges; we sometimes created point edges --- basics.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/basics.py b/basics.py index 7ebc92c972..f26996f5b9 100644 --- a/basics.py +++ b/basics.py @@ -261,6 +261,10 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): # New segments (i0, i1) is identical to the old edge_ind[0] new_edges = np.array([[other_start, i0, i1], [i0, i1, other_end]]) + # For some reason we sometimes create point-edges here (start and + # end are identical). Delete these if necessary + del_ind = np.squeeze(np.where(np.diff(new_edges, axis=0)[0] == 0)) + new_edges = np.delete(new_edges, del_ind, axis=1) if tags.size > 0: new_edges = np.vstack((new_edges, np.tile(tags[:, np.newaxis], @@ -272,8 +276,8 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): # Delete the second segment. This is most easily handled after # edges is expanded, to avoid accounting for changing edge indices. edges = np.delete(edges, edge_ind[0], axis=1) - new_line = [-1, 2] - + # Number of new lines. Should account for deletion of edges. + new_line = [-1, 2 - del_ind.size] split_type = 5 # Note that we know that i0 is closest to start, thus no need to test From 10133306aa6dbcba3bd2d3b68f1681933eca8b2b Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Mon, 3 Apr 2017 12:19:25 +0200 Subject: [PATCH 145/153] Bugfix in splitting of edges, comp geom. When uniquifying the edges, we need to preserve the tags as well. --- basics.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/basics.py b/basics.py index f26996f5b9..ea962bef4f 100644 --- a/basics.py +++ b/basics.py @@ -368,11 +368,13 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): # this by uniquifying the edges. # Hopefully, we do not mess up the edges here. edges_copy = np.sort(edges[:2], axis=0) - edges_unique, *new_2_old = setmembership.unique_columns_tol(edges_copy, - tol=tol) + edges_unique, new_2_old, _ \ + = setmembership.unique_columns_tol(edges_copy, tol=tol) + # Refer to unique edges if necessary if edges_unique.shape[1] < edges.shape[1]: new_line[0] -= edges.shape[1] - edges_unique.shape[1] - edges = edges_unique + # Also copy tags + edges = np.vstack((edges_unique, edges[2, new_2_old])) # Also signify that we have carried out this operation. split_type = [split_type, 8] From 16a54d6fea1779e50f2aa277f6050e28d6a404f5 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Wed, 5 Apr 2017 20:00:08 +0200 Subject: [PATCH 146/153] Better treatment of number of new lines in splitting of edges in comp geom. The approach for overlapping segments was not optimal. --- basics.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/basics.py b/basics.py index ea962bef4f..aeaf19dfa8 100644 --- a/basics.py +++ b/basics.py @@ -138,6 +138,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): # Number of points before edges is modified. Used for sanity check below. orig_num_pts = edges[:2].max() + orig_num_edges = edges.shape[1] # Save tags associated with the edge. # NOTE: For segment intersetions where the edges have different tags, one @@ -244,7 +245,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): edges = np.hstack((edges[:, :edge_ind[0]], new_edges, edges[:, edge_ind[0]+1:])) - new_line = [2, -1] + split_type = 4 elif i0 == start and i1 == end: # We don't know if i0 is closest to the start or end of edges[:, @@ -276,8 +277,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): # Delete the second segment. This is most easily handled after # edges is expanded, to avoid accounting for changing edge indices. edges = np.delete(edges, edge_ind[0], axis=1) - # Number of new lines. Should account for deletion of edges. - new_line = [-1, 2 - del_ind.size] + split_type = 5 # Note that we know that i0 is closest to start, thus no need to test @@ -297,17 +297,14 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): # # edge_ind[0] = (0, 2), edge_ind[1] = (0, 1) is split into # (0, 1), (1, 2) - did_delete = 0 if edges[0, edge_ind[1]] == i1: if edges[1, edge_ind[1]] == start: edges = np.delete(edges, edge_ind[1], axis=1) - did_delete = 1 else: edges[0, edge_ind[1]] = start elif edges[1, edge_ind[1]] == i1: if edges[0, edge_ind[1]] == start: edges = np.delete(edges, edge_ind[1], axis=1) - did_delete = 1 else: edges[1, edge_ind[1]] = start else: @@ -323,7 +320,6 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): edges = np.hstack((edges[:, :edge_ind[0]], new_edges, edges[:, (edge_ind[0]+1):])) - new_line = [1, -did_delete] split_type = 6 elif i0 != start and i1 == end: @@ -333,13 +329,11 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): if edges[0, edge_ind[1]] == i0: if edges[1, edge_ind[1]] == end: edges = np.delete(edges, edge_ind[1], axis=1) - did_delete = 1 else: edges[0, edge_ind[1]] = end elif edges[1, edge_ind[1]] == i0: if edges[0, edge_ind[1]] == end: edges = np.delete(edges, edge_ind[1], axis=1) - did_delete = -1 else: edges[1, edge_ind[1]] = end else: @@ -354,7 +348,6 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): edges = np.hstack((edges[:, :edge_ind[0]], new_edges, edges[:, (edge_ind[0]+1):])) - new_line = [1, -did_delete] split_type = 7 else: @@ -372,12 +365,14 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): = setmembership.unique_columns_tol(edges_copy, tol=tol) # Refer to unique edges if necessary if edges_unique.shape[1] < edges.shape[1]: - new_line[0] -= edges.shape[1] - edges_unique.shape[1] - # Also copy tags + # Copy tags edges = np.vstack((edges_unique, edges[2, new_2_old])) # Also signify that we have carried out this operation. split_type = [split_type, 8] + # Number of new lines created + new_line = edges.shape[1] - orig_num_edges + return vertices, edges, new_line, split_type #------------------------------------------------------------------------------# @@ -669,7 +664,7 @@ def __min_dist(p): [edge_counter, intsect], new_pt, **kwargs) split_type.append(s_type) - intersections += splits[0] + splits[1] + intersections += splits if verbose > 2 and (splits[0] > 0 or splits[1] > 0): print(' Introduced new point') From 1e45a549a0b98ea835079b4373206ad0a8672f7a Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Mon, 10 Apr 2017 08:49:37 +0200 Subject: [PATCH 147/153] Minor fix of doctest in compgeom. --- basics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basics.py b/basics.py index aeaf19dfa8..ac751e7b7e 100644 --- a/basics.py +++ b/basics.py @@ -95,7 +95,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): >>> p = np.array([[0, 0], [0, 1]]) >>> edges = np.array([[0], [1]]) >>> new_pt = np.array([[0], [0.5]]) - >>> v, e, nl, _ = _split_edge(p, edges, 0, new_pt) + >>> v, e, nl, _ = _split_edge(p, edges, 0, new_pt, tol=1e-3) >>> e array([[0, 2], [2, 1]]) From 4f13d7126806766cc502869d6caba89b366cd1bb Mon Sep 17 00:00:00 2001 From: IvarStefansson Date: Fri, 21 Apr 2017 14:37:55 +0200 Subject: [PATCH 148/153] Added node projection in map_grid --- basics.py | 248 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 143 insertions(+), 105 deletions(-) diff --git a/basics.py b/basics.py index 7f455ad335..54b4090430 100644 --- a/basics.py +++ b/basics.py @@ -21,6 +21,7 @@ # #------------------------------------------------------------------------------# + def snap_to_grid(pts, tol=1e-3, box=None, **kwargs): """ Snap points to an underlying Cartesian grid. @@ -68,16 +69,19 @@ def snap_to_grid(pts, tol=1e-3, box=None, **kwargs): #------------------------------------------------------------------------------# + def __nrm(v): return np.sqrt(np.sum(v * v, axis=0)) #------------------------------------------------------------------------------# + def __dist(p1, p2): return __nrm(p1 - p2) #------------------------------------------------------------------------------# + def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): """ Split a line into two by introcuding a new point. @@ -189,7 +193,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): new_edges.shape[1]))) # Insert the new edge in the midle of the set of edges. edges = np.hstack((edges[:, :edge_ind_first], new_edges, - edges[:, edge_ind_first+1:])) + edges[:, edge_ind_first + 1:])) # We have added as many new edges as there are columns in new_edges, # minus 1 (which was removed / ignored). new_line = new_edges.shape[1] - 1 @@ -244,7 +248,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): # Combine everything. edges = np.hstack((edges[:, :edge_ind[0]], new_edges, - edges[:, edge_ind[0]+1:])) + edges[:, edge_ind[0] + 1:])) split_type = 4 elif i0 == start and i1 == end: @@ -273,7 +277,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): # Combine everything. edges = np.hstack((edges[:, :edge_ind[1]], new_edges, - edges[:, (edge_ind[1]+1):])) + edges[:, (edge_ind[1] + 1):])) # Delete the second segment. This is most easily handled after # edges is expanded, to avoid accounting for changing edge indices. edges = np.delete(edges, edge_ind[0], axis=1) @@ -319,7 +323,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): edges = np.hstack((edges[:, :edge_ind[0]], new_edges, - edges[:, (edge_ind[0]+1):])) + edges[:, (edge_ind[0] + 1):])) split_type = 6 elif i0 != start and i1 == end: @@ -347,7 +351,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): edges = np.hstack((edges[:, :edge_ind[0]], new_edges, - edges[:, (edge_ind[0]+1):])) + edges[:, (edge_ind[0] + 1):])) split_type = 7 else: @@ -362,7 +366,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): # Hopefully, we do not mess up the edges here. edges_copy = np.sort(edges[:2], axis=0) edges_unique, new_2_old, _ \ - = setmembership.unique_columns_tol(edges_copy, tol=tol) + = setmembership.unique_columns_tol(edges_copy, tol=tol) # Refer to unique edges if necessary if edges_unique.shape[1] < edges.shape[1]: # Copy tags @@ -377,6 +381,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): #------------------------------------------------------**kwargs------------------------# + def _add_point(vertices, pt, tol=1e-3, **kwargs): """ Add a point to a point set, unless the point already exist in the set. @@ -519,7 +524,7 @@ def remove_edge_crossings(vertices, edges, tol=1e-3, verbose=0, **kwargs): # # answer by PolyThinker and comments by Jason S, for more information. c1 = a[edge_counter] * (start_x - xm) \ - + b[edge_counter] * (start_y - ym) + + b[edge_counter] * (start_y - ym) c2 = a[edge_counter] * (end_x - xm) + b[edge_counter] * (end_y - ym) tol_scaled = tol * max(1, np.max([np.sqrt(np.abs(c1)), @@ -584,10 +589,10 @@ def remove_edge_crossings(vertices, edges, tol=1e-3, verbose=0, **kwargs): # Check if this point intersects new_pt = lines_intersect(vertices[:, edges[0, edge_counter]], - vertices[:, edges[1, edge_counter]], - vertices[:, edges[0, intsect]], - vertices[:, edges[1, intsect]], - tol=tol) + vertices[:, edges[1, edge_counter]], + vertices[:, edges[0, intsect]], + vertices[:, edges[1, intsect]], + tol=tol) def __min_dist(p): md = np.inf @@ -606,7 +611,7 @@ def __min_dist(p): if new_pt.shape[-1] == 1: if verbose > 2: print(' Found intersection: (' + str(new_pt[0]) + - ', ' + str(new_pt[1])) + ', ' + str(new_pt[1])) # Split edge edge_counter (outer loop), unless the # intersection hits an existing point (in practices this @@ -615,20 +620,21 @@ def __min_dist(p): # is needed) md = __min_dist(new_pt) vertices, edges, split_outer_edge,\ - split = _split_edge(vertices, edges, - edge_counter, new_pt, - **kwargs) + split = _split_edge(vertices, edges, + edge_counter, new_pt, + **kwargs) split_type.append(split) if verbose > 2 and split_outer_edge > 0 and \ vertices.shape[1] > orig_vertex_num: print(' Introduced new point. Min length of edges:' - + str(md)) + + str(md)) if md < tol: import pdb pdb.set_trace() if edges.shape[1] > orig_edge_num + split_outer_edge: - raise ValueError('Have created edge without bookkeeping') + raise ValueError( + 'Have created edge without bookkeeping') # If the outer edge (represented by edge_counter) was split, # e.g. inserted into the list of edges we need to increase the # index of the inner edge @@ -636,15 +642,16 @@ def __min_dist(p): # Possibly split the inner edge vertices, edges, split_inner_edge, \ - split = _split_edge(vertices, edges, intsect, - new_pt, **kwargs) + split = _split_edge(vertices, edges, intsect, + new_pt, **kwargs) if edges.shape[1] > \ orig_edge_num + split_inner_edge + split_outer_edge: - raise ValueError('Have created edge without bookkeeping') + raise ValueError( + 'Have created edge without bookkeeping') split_type.append(split) if verbose > 2 and split_inner_edge > 0 and \ - vertices.shape[1] > orig_vertex_num: + vertices.shape[1] > orig_vertex_num: print(' Introduced new point. Min length of edges:' + str(md)) if md < tol: @@ -655,14 +662,14 @@ def __min_dist(p): # We have found an intersection along a line segment if verbose > 2: print(' Found two intersections: (' - + str(new_pt[0, 0]) + ', ' + str(new_pt[1, 0]) + - 'and ' + str(new_pt[0, 1]) + ', ' + + + str(new_pt[0, 0]) + ', ' + str(new_pt[1, 0]) + + 'and ' + str(new_pt[0, 1]) + ', ' + str(new_pt[1, 1])) vertices, edges, splits,\ - s_type = _split_edge(vertices, edges, - [edge_counter, intsect], - new_pt, **kwargs) + s_type = _split_edge(vertices, edges, + [edge_counter, intsect], + new_pt, **kwargs) split_type.append(s_type) intersections += splits if verbose > 2 and (splits[0] > 0 or splits[1] > 0): @@ -688,9 +695,9 @@ def __min_dist(p): edge_counter += 1 if verbose > 1: - print(' Edge intersection removal complete. Elapsed time ' +\ + print(' Edge intersection removal complete. Elapsed time ' + str(time.time() - start_time)) - print(' Introduced ' + str(edges.shape[1] - num_edges_orig) + \ + print(' Introduced ' + str(edges.shape[1] - num_edges_orig) + ' new edges') return vertices, edges @@ -701,6 +708,7 @@ def __min_dist(p): # #----------------------------------------------------------- + def is_ccw_polygon(poly): """ Determine if the vertices of a polygon are sorted counter clockwise. @@ -739,11 +747,12 @@ def is_ccw_polygon(poly): num_p = poly.shape[1] value = 0 for i in range(num_p): - value += (p_1[i+1] + p_1[i]) * (p_0[i+1] - p_0[i]) + value += (p_1[i + 1] + p_1[i]) * (p_0[i + 1] - p_0[i]) return value < 0 #---------------------------------------------------------- + def is_ccw_polyline(p1, p2, p3, tol=0, default=False): """ Check if the line segments formed by three points is part of a @@ -779,7 +788,7 @@ def is_ccw_polyline(p1, p2, p3, tol=0, default=False): # Compute cross product between p1-p2 and p1-p3. Right hand rule gives that # p3 is to the left if the cross product is positive. cross_product = (p2[0] - p1[0]) * (p3[1] - p1[1])\ - -(p2[1] - p1[1]) * (p3[0] - p1[0]) + - (p2[1] - p1[1]) * (p3[0] - p1[0]) # Should there be a scaling of the tolerance relative to the distance # between the points? @@ -791,6 +800,7 @@ def is_ccw_polyline(p1, p2, p3, tol=0, default=False): #----------------------------------------------------------------------------- + def is_inside_polygon(poly, p, tol=0, default=False): """ Check if a set of points are inside a polygon. @@ -821,7 +831,7 @@ def is_inside_polygon(poly, p, tol=0, default=False): inside = np.ones(pt.shape[1], dtype=np.bool) for i in range(pt.shape[1]): for j in range(poly.shape[1]): - if not is_ccw_polyline(poly[:, j], poly[:, (j+1) % poly_size], + if not is_ccw_polyline(poly[:, j], poly[:, (j + 1) % poly_size], pt[:, i], tol=tol, default=default): inside[i] = False # No need to check the remaining segments of the polygon. @@ -852,7 +862,7 @@ def dist_point_pointset(p, pset, exponent=2): pt = p return np.power(np.sum(np.power(np.abs(pt - pset), exponent), - axis=0), 1/exponent) + axis=0), 1 / exponent) #------------------------------------------------------------------------------# @@ -874,7 +884,7 @@ def lines_intersect(start_1, end_1, start_2, end_2, tol=1e-8): Example: >>> lines_intersect([0, 0], [1, 1], [0, 1], [1, 0]) array([[ 0.5], - [ 0.5]]) + [ 0.5]]) >>> lines_intersect([0, 0], [1, 0], [0, 1], [1, 1]) @@ -919,7 +929,7 @@ def lines_intersect(start_1, end_1, start_2, end_2, tol=1e-8): # First check for solvability of the system (e.g. parallel lines) by the # determinant of the matrix. - discr = d_1[0] *(-d_2[1]) - d_1[1] * (-d_2[0]) + discr = d_1[0] * (-d_2[1]) - d_1[1] * (-d_2[0]) if np.abs(discr) < tol: # The lines are parallel, and will only cross if they are also colinear @@ -932,11 +942,11 @@ def lines_intersect(start_1, end_1, start_2, end_2, tol=1e-8): # Write l1 on the form start_1 + t * d_1, find the parameter value # needed for equality with start_2 and end_2 if np.abs(d_1[0]) > tol: - t_start_2 = (start_2[0] - start_1[0])/d_1[0] - t_end_2 = (end_2[0] - start_1[0])/d_1[0] + t_start_2 = (start_2[0] - start_1[0]) / d_1[0] + t_end_2 = (end_2[0] - start_1[0]) / d_1[0] elif np.abs(d_1[1]) > tol: - t_start_2 = (start_2[1] - start_1[1])/d_1[1] - t_end_2 = (end_2[1] - start_1[1])/d_1[1] + t_start_2 = (start_2[1] - start_1[1]) / d_1[1] + t_end_2 = (end_2[1] - start_1[1]) / d_1[1] else: # d_1 is zero raise ValueError('Start and endpoint of line should be\ @@ -983,6 +993,7 @@ def lines_intersect(start_1, end_1, start_2, end_2, tol=1e-8): #------------------------------------------------------------------------------# + def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): """ Find intersection points (or segments) of two 3d lines. @@ -1054,11 +1065,13 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): if mask_sum.sum() > 1: in_discr = np.argwhere(mask_sum)[:2] else: - # We're going to have a zero discreminant anyhow, just pick some dimensions. + # We're going to have a zero discreminant anyhow, just pick some + # dimensions. in_discr = np.arange(2) not_in_discr = np.setdiff1d(np.arange(3), in_discr)[0] - discr = deltas_1[in_discr[0]] * deltas_2[in_discr[1]] - deltas_1[in_discr[1]] * deltas_2[in_discr[0]] + discr = deltas_1[in_discr[0]] * deltas_2[in_discr[1]] - \ + deltas_1[in_discr[1]] * deltas_2[in_discr[0]] # An intersection will be a solution of the linear system # xs_1 + dx_1 * t_1 = xs_2 + dx_2 * t_2 (1) @@ -1092,15 +1105,15 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): diff_start = start_2 - start_1 dstart_x_delta_x = diff_start[1] * deltas_1[2] -\ - diff_start[2] * deltas_1[1] + diff_start[2] * deltas_1[1] if np.abs(dstart_x_delta_x) > tol: return None dstart_x_delta_y = diff_start[2] * deltas_1[0] -\ - diff_start[0] * deltas_1[2] + diff_start[0] * deltas_1[2] if np.abs(dstart_x_delta_y) > tol: return None dstart_x_delta_z = diff_start[0] * deltas_1[1] -\ - diff_start[1] * deltas_1[0] + diff_start[1] * deltas_1[0] if np.abs(dstart_x_delta_z) > tol: return None @@ -1108,13 +1121,15 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): # points should be parallel to the segments. # Since the masks are equal, we can use any of them. t_1_2 = (start_1[mask_1] - start_2[mask_1]) / deltas_1[mask_1] - # For dimensions with no incline, the start cooordinates should be the same + # For dimensions with no incline, the start cooordinates should be the + # same if not np.allclose(start_1[~mask_1], start_2[~mask_1], tol): return None # We have overlapping lines! finally check if segments are overlapping. - # Since everything is parallel, it suffices to work with a single coordinate + # Since everything is parallel, it suffices to work with a single + # coordinate s_1 = start_1[mask_1][0] e_1 = end_1[mask_1][0] s_2 = start_2[mask_1][0] @@ -1132,7 +1147,6 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): elif max_2 < min_1: return None - # The lines are overlapping, we need to find their common line lines = np.array([s_1, e_1, s_2, e_2]) sort_ind = np.argsort(lines) @@ -1150,14 +1164,14 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): # Solve 2x2 system by Cramer's rule discr = deltas_1[in_discr[0]] * (-deltas_2[in_discr[1]]) -\ - deltas_1[in_discr[1]] * (-deltas_2[in_discr[0]]) - t_1 = ((start_2[in_discr[0]] - start_1[in_discr[0]]) \ - * (-deltas_2[in_discr[1]]) - \ - (start_2[in_discr[1]] - start_1[in_discr[1]]) \ - * (-deltas_2[in_discr[0]]))/discr + deltas_1[in_discr[1]] * (-deltas_2[in_discr[0]]) + t_1 = ((start_2[in_discr[0]] - start_1[in_discr[0]]) + * (-deltas_2[in_discr[1]]) - + (start_2[in_discr[1]] - start_1[in_discr[1]]) + * (-deltas_2[in_discr[0]])) / discr t_2 = (deltas_1[in_discr[0]] * (start_2[in_discr[1]] - - start_1[in_discr[1]]) - \ + start_1[in_discr[1]]) - deltas_1[in_discr[1]] * (start_2[in_discr[0]] - start_1[in_discr[0]])) / discr @@ -1180,6 +1194,8 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): #----------------------------------------------------------------------------# # Represent the polygon as a sympy polygon + + def _np2p(p): # Convert a numpy point array (3xn) to sympy points if p.ndim == 1: @@ -1187,13 +1203,16 @@ def _np2p(p): else: return [geom.Point(p[:, i]) for i in range(p.shape[1])] + def _p2np(p): - # Convert sympy points to numpy format. If more than one point, these should be sent as a list + # Convert sympy points to numpy format. If more than one point, these + # should be sent as a list if isinstance(p, list): return np.array(list([i.args for i in p]), dtype='float').transpose() else: return np.array(list(p.args), dtype='float').reshape((-1, 1)) + def _to3D(p): # Add a third dimension return np.vstack((p, np.zeros(p.shape[1]))) @@ -1249,11 +1268,11 @@ def polygon_boundaries_intersect(poly_1, poly_2, tol=1e-8): for i in range(l_1): p_1_1 = poly_1[:, ind_1[i]] - p_1_2 = poly_1[:, ind_1[i+1]] + p_1_2 = poly_1[:, ind_1[i + 1]] for j in range(l_2): p_2_1 = poly_2[:, ind_2[j]] - p_2_2 = poly_2[:, ind_2[j+1]] + p_2_2 = poly_2[:, ind_2[j + 1]] isect_loc = segments_intersect_3d(p_1_1, p_1_2, p_2_1, p_2_2) if isect_loc is not None: isect.append([i, j, isect_loc]) @@ -1262,6 +1281,7 @@ def polygon_boundaries_intersect(poly_1, poly_2, tol=1e-8): #---------------------------------------------------------- + def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): """ Find intersections between polygons embeded in 3D. @@ -1292,7 +1312,8 @@ def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): """ - # First translate the points so that the first plane is located at the origin + # First translate the points so that the first plane is located at the + # origin center_1 = np.mean(poly_1, axis=1).reshape((-1, 1)) poly_1 = poly_1 - center_1 poly_2 = poly_2 - center_1 @@ -1354,11 +1375,11 @@ def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): for i in range(num_p2): # Indices of points of this segment i1 = ind[i] - i2 = ind[i+1] + i2 = ind[i + 1] # Coordinates of this segment pt_1 = poly_2_rot[:, ind[i]] - pt_2 = poly_2_rot[:, ind[i+1]] + pt_2 = poly_2_rot[:, ind[i + 1]] # Check if segment crosses z=0 in the rotated coordinates if max(pt_1[2], pt_2[2]) < 0 or min(pt_1[2], pt_2[2]) > 0: @@ -1399,7 +1420,7 @@ def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): return isect -def is_planar( pts, normal = None ): +def is_planar(pts, normal=None): """ Check if the points lie on a plane. Parameters: @@ -1413,17 +1434,18 @@ def is_planar( pts, normal = None ): """ if normal is None: - normal = compute_normal( pts ) + normal = compute_normal(pts) else: - normal = normal / np.linalg.norm( normal ) + normal = normal / np.linalg.norm(normal) - check = np.array( [ np.isclose( np.dot( normal, pts[:,0] - p ), 0. ) \ - for p in pts[:,1:].T ], dtype=np.bool ) - return np.all( check ) + check = np.array([np.isclose(np.dot(normal, pts[:, 0] - p), 0.) + for p in pts[:, 1:].T], dtype=np.bool) + return np.all(check) #------------------------------------------------------------------------------# -def project_plane_matrix( pts, normal = None ): + +def project_plane_matrix(pts, normal=None): """ Project the points on a plane using local coordinates. The projected points are computed by a dot product. @@ -1440,18 +1462,19 @@ def project_plane_matrix( pts, normal = None ): """ if normal is None: - normal = compute_normal( pts ) + normal = compute_normal(pts) else: - normal = normal / np.linalg.norm( normal ) + normal = normal / np.linalg.norm(normal) - reference = np.array( [0., 0., 1.] ) - angle = np.arccos( np.dot( normal, reference ) ) - vect = np.cross( normal, reference ) - return rot( angle, vect ) + reference = np.array([0., 0., 1.]) + angle = np.arccos(np.dot(normal, reference)) + vect = np.cross(normal, reference) + return rot(angle, vect) #------------------------------------------------------------------------------# -def rot( a, vect ): + +def rot(a, vect): """ Compute the rotation matrix about a vector by an angle using the matrix form of Rodrigues formula. @@ -1464,17 +1487,18 @@ def rot( a, vect ): """ - if np.allclose( vect, [0.,0.,0.] ): + if np.allclose(vect, [0., 0., 0.]): return np.identity(3) vect = vect / np.linalg.norm(vect) - W = np.array( [ [ 0., -vect[2], vect[1] ], - [ vect[2], 0., -vect[0] ], - [ -vect[1], vect[0], 0. ] ] ) - return np.identity(3) + np.sin(a)*W + \ - (1.-np.cos(a))*np.linalg.matrix_power(W,2) + W = np.array([[0., -vect[2], vect[1]], + [vect[2], 0., -vect[0]], + [-vect[1], vect[0], 0.]]) + return np.identity(3) + np.sin(a) * W + \ + (1. - np.cos(a)) * np.linalg.matrix_power(W, 2) #------------------------------------------------------------------------------# + def normal_matrix(pts=None, normal=None): """ Compute the normal projection matrix of a plane. @@ -1492,9 +1516,9 @@ def normal_matrix(pts=None, normal=None): """ if normal is not None: - normal = normal / np.linalg.norm( normal ) + normal = normal / np.linalg.norm(normal) elif pts is not None: - normal = compute_normal( pts ) + normal = compute_normal(pts) else: assert False, "Points or normal are mandatory" @@ -1502,6 +1526,7 @@ def normal_matrix(pts=None, normal=None): #------------------------------------------------------------------------------# + def tangent_matrix(pts=None, normal=None): """ Compute the tangential projection matrix of a plane. @@ -1522,6 +1547,7 @@ def tangent_matrix(pts=None, normal=None): #------------------------------------------------------------------------------# + def compute_normal(pts): """ Compute the normal of a set of points. @@ -1537,19 +1563,22 @@ def compute_normal(pts): """ assert pts.shape[1] > 2 - normal = np.cross( pts[:,0] - pts[:,1], compute_tangent(pts) ) - if np.allclose( normal, np.zeros(3) ): return compute_normal(pts[:, 1:]) - return normal / np.linalg.norm( normal ) + normal = np.cross(pts[:, 0] - pts[:, 1], compute_tangent(pts)) + if np.allclose(normal, np.zeros(3)): + return compute_normal(pts[:, 1:]) + return normal / np.linalg.norm(normal) #------------------------------------------------------------------------------# + def compute_normals_1d(pts): t = compute_tangent(pts) - n = np.array([t[1], -t[0], 0]) / np.sqrt(t[0]**2+t[1]**2) - return np.r_['1,2,0', n, np.dot(rot(np.pi/2., t), n)] + n = np.array([t[1], -t[0], 0]) / np.sqrt(t[0]**2 + t[1]**2) + return np.r_['1,2,0', n, np.dot(rot(np.pi / 2., t), n)] #------------------------------------------------------------------------------# + def compute_tangent(pts): """ Compute a tangent of a set of points. @@ -1563,12 +1592,13 @@ def compute_tangent(pts): """ - tangent = pts[:,0] - np.mean( pts, axis = 1 ) - assert not np.allclose( tangent, np.zeros(3) ) + tangent = pts[:, 0] - np.mean(pts, axis=1) + assert not np.allclose(tangent, np.zeros(3)) return tangent / np.linalg.norm(tangent) #------------------------------------------------------------------------------# + def is_collinear(pts, tol=1e-5): """ Check if the points lie on a line. @@ -1582,17 +1612,19 @@ def is_collinear(pts, tol=1e-5): """ assert pts.shape[1] > 1 - if pts.shape[1] == 2: return True + if pts.shape[1] == 2: + return True - pt0 = pts[:,0] - pt1 = pts[:,1] + pt0 = pts[:, 0] + pt1 = pts[:, 1] - coll = np.array( [ np.linalg.norm( np.cross( p - pt0, pt1 - pt0 ) ) \ - for p in pts[:,1:-1].T ] ) + coll = np.array([np.linalg.norm(np.cross(p - pt0, pt1 - pt0)) + for p in pts[:, 1:-1].T]) return np.allclose(coll, np.zeros(coll.size), tol) #------------------------------------------------------------------------------# + def map_grid(g): """ If a 2d or a 1d grid is passed, the function return the cell_centers, face_normals, and face_centers using local coordinates. If a 3d grid is @@ -1606,32 +1638,36 @@ def map_grid(g): face_normals: (g.dim x g.num_faces) the mapped normals of the faces. face_centers: (g.dim x g.num_faces) the mapped centers of the faces. R: (3 x 3) the rotation matrix used. - dim: indicates which are the dimensions active + dim: indicates which are the dimensions active. + nodes: (g.dim x g.num_nodes) the mapped nodes. """ cell_centers = g.cell_centers face_normals = g.face_normals face_centers = g.face_centers + nodes = g.nodes R = np.eye(3) if g.dim == 0 or g.dim == 3: - return cell_centers, face_normals, face_centers, R, np.ones(3,dtype=bool) + return cell_centers, face_normals, face_centers, R, np.ones(3, dtype=bool), nodes if g.dim == 1 or g.dim == 2: - v = compute_normal(g.nodes) if g.dim==2 else compute_tangent(g.nodes) + v = compute_normal(g.nodes) if g.dim == 2 else compute_tangent(g.nodes) R = project_plane_matrix(g.nodes, v) face_centers = np.dot(R, face_centers) - dim = np.logical_not(np.isclose(np.sum(np.abs(face_centers.T- - face_centers[:,0]), axis=0),0)) + dim = np.logical_not(np.isclose(np.sum(np.abs(face_centers.T - + face_centers[:, 0]), axis=0), 0)) assert g.dim == np.sum(dim) - face_centers = face_centers[dim,:] - cell_centers = np.dot(R, cell_centers)[dim,:] - face_normals = np.dot(R, face_normals)[dim,:] + face_centers = face_centers[dim, :] + cell_centers = np.dot(R, cell_centers)[dim, :] + face_normals = np.dot(R, face_normals)[dim, :] + nodes = np.dot(R, nodes)[dim, :] - return cell_centers, face_normals, face_centers, R, dim + return cell_centers, face_normals, face_centers, R, dim, nodes #------------------------------------------------------------------------------# + def distance_segment_segment(s1_start, s1_end, s2_start, s2_end): """ Compute the distance between two line segments. @@ -1656,10 +1692,12 @@ def distance_segment_segment(s1_start, s1_end, s2_start, s2_end): """ - # Variable used to fine almost parallel lines. Sensitivity to this value has not been tested. + # Variable used to fine almost parallel lines. Sensitivity to this value + # has not been tested. SMALL_TOLERANCE = 1e-6 - # For the rest of the algorithm, see the webpage referred to above for details. + # For the rest of the algorithm, see the webpage referred to above for + # details. d1 = s1_end - s1_start d2 = s2_end - s2_start d_starts = s1_start - s2_start @@ -1709,7 +1747,7 @@ def distance_segment_segment(s1_start, s1_end, s2_start, s2_end): # recompute sc for this edge if (-dot_1_starts + dot_1_2) < 0.0: sN = 0 - elif (-dot_1_starts+ dot_1_2) > dot_1_1: + elif (-dot_1_starts + dot_1_2) > dot_1_1: sN = sD else: sN = (-dot_1_starts + dot_1_2) @@ -1729,7 +1767,7 @@ def distance_segment_segment(s1_start, s1_end, s2_start, s2_end): dist = d_starts + sc * d1 - tc * d2 return np.sqrt(dist.dot(dist)) + if __name__ == "__main__": import doctest doctest.testmod() - From 5cc37206031e730ee6b9b7ae1dcd2f9b1ff502ea Mon Sep 17 00:00:00 2001 From: IvarStefansson Date: Fri, 21 Apr 2017 15:09:40 +0200 Subject: [PATCH 149/153] Fixed wrong previous commit. Back to the commit before ( 1f6cfc3 ) + the changes to map_grid --- basics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/basics.py b/basics.py index 54b4090430..290053c670 100644 --- a/basics.py +++ b/basics.py @@ -1638,7 +1638,7 @@ def map_grid(g): face_normals: (g.dim x g.num_faces) the mapped normals of the faces. face_centers: (g.dim x g.num_faces) the mapped centers of the faces. R: (3 x 3) the rotation matrix used. - dim: indicates which are the dimensions active. + dim: indicates which are the dimensions active nodes: (g.dim x g.num_nodes) the mapped nodes. """ @@ -1665,8 +1665,8 @@ def map_grid(g): return cell_centers, face_normals, face_centers, R, dim, nodes -#------------------------------------------------------------------------------# +#------------------------------------------------------------------------------# def distance_segment_segment(s1_start, s1_end, s2_start, s2_end): """ From f85de5c02e3ccf9915f432008af5b0746ea91b5c Mon Sep 17 00:00:00 2001 From: IvarStefansson Date: Fri, 21 Apr 2017 16:04:49 +0200 Subject: [PATCH 150/153] Fixed wrong previous commit. Back to the commit before ( 1f6cfc3 ) + the changes to map_grid --- basics.py | 245 +++++++++++++++++++++++------------------------------- 1 file changed, 105 insertions(+), 140 deletions(-) diff --git a/basics.py b/basics.py index 290053c670..2d50fd0b78 100644 --- a/basics.py +++ b/basics.py @@ -21,7 +21,6 @@ # #------------------------------------------------------------------------------# - def snap_to_grid(pts, tol=1e-3, box=None, **kwargs): """ Snap points to an underlying Cartesian grid. @@ -69,19 +68,16 @@ def snap_to_grid(pts, tol=1e-3, box=None, **kwargs): #------------------------------------------------------------------------------# - def __nrm(v): return np.sqrt(np.sum(v * v, axis=0)) #------------------------------------------------------------------------------# - def __dist(p1, p2): return __nrm(p1 - p2) #------------------------------------------------------------------------------# - def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): """ Split a line into two by introcuding a new point. @@ -193,7 +189,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): new_edges.shape[1]))) # Insert the new edge in the midle of the set of edges. edges = np.hstack((edges[:, :edge_ind_first], new_edges, - edges[:, edge_ind_first + 1:])) + edges[:, edge_ind_first+1:])) # We have added as many new edges as there are columns in new_edges, # minus 1 (which was removed / ignored). new_line = new_edges.shape[1] - 1 @@ -248,7 +244,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): # Combine everything. edges = np.hstack((edges[:, :edge_ind[0]], new_edges, - edges[:, edge_ind[0] + 1:])) + edges[:, edge_ind[0]+1:])) split_type = 4 elif i0 == start and i1 == end: @@ -277,7 +273,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): # Combine everything. edges = np.hstack((edges[:, :edge_ind[1]], new_edges, - edges[:, (edge_ind[1] + 1):])) + edges[:, (edge_ind[1]+1):])) # Delete the second segment. This is most easily handled after # edges is expanded, to avoid accounting for changing edge indices. edges = np.delete(edges, edge_ind[0], axis=1) @@ -323,7 +319,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): edges = np.hstack((edges[:, :edge_ind[0]], new_edges, - edges[:, (edge_ind[0] + 1):])) + edges[:, (edge_ind[0]+1):])) split_type = 6 elif i0 != start and i1 == end: @@ -351,7 +347,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): edges = np.hstack((edges[:, :edge_ind[0]], new_edges, - edges[:, (edge_ind[0] + 1):])) + edges[:, (edge_ind[0]+1):])) split_type = 7 else: @@ -366,7 +362,7 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): # Hopefully, we do not mess up the edges here. edges_copy = np.sort(edges[:2], axis=0) edges_unique, new_2_old, _ \ - = setmembership.unique_columns_tol(edges_copy, tol=tol) + = setmembership.unique_columns_tol(edges_copy, tol=tol) # Refer to unique edges if necessary if edges_unique.shape[1] < edges.shape[1]: # Copy tags @@ -381,7 +377,6 @@ def _split_edge(vertices, edges, edge_ind, new_pt, **kwargs): #------------------------------------------------------**kwargs------------------------# - def _add_point(vertices, pt, tol=1e-3, **kwargs): """ Add a point to a point set, unless the point already exist in the set. @@ -524,7 +519,7 @@ def remove_edge_crossings(vertices, edges, tol=1e-3, verbose=0, **kwargs): # # answer by PolyThinker and comments by Jason S, for more information. c1 = a[edge_counter] * (start_x - xm) \ - + b[edge_counter] * (start_y - ym) + + b[edge_counter] * (start_y - ym) c2 = a[edge_counter] * (end_x - xm) + b[edge_counter] * (end_y - ym) tol_scaled = tol * max(1, np.max([np.sqrt(np.abs(c1)), @@ -589,10 +584,10 @@ def remove_edge_crossings(vertices, edges, tol=1e-3, verbose=0, **kwargs): # Check if this point intersects new_pt = lines_intersect(vertices[:, edges[0, edge_counter]], - vertices[:, edges[1, edge_counter]], - vertices[:, edges[0, intsect]], - vertices[:, edges[1, intsect]], - tol=tol) + vertices[:, edges[1, edge_counter]], + vertices[:, edges[0, intsect]], + vertices[:, edges[1, intsect]], + tol=tol) def __min_dist(p): md = np.inf @@ -611,7 +606,7 @@ def __min_dist(p): if new_pt.shape[-1] == 1: if verbose > 2: print(' Found intersection: (' + str(new_pt[0]) + - ', ' + str(new_pt[1])) + ', ' + str(new_pt[1])) # Split edge edge_counter (outer loop), unless the # intersection hits an existing point (in practices this @@ -620,21 +615,20 @@ def __min_dist(p): # is needed) md = __min_dist(new_pt) vertices, edges, split_outer_edge,\ - split = _split_edge(vertices, edges, - edge_counter, new_pt, - **kwargs) + split = _split_edge(vertices, edges, + edge_counter, new_pt, + **kwargs) split_type.append(split) if verbose > 2 and split_outer_edge > 0 and \ vertices.shape[1] > orig_vertex_num: print(' Introduced new point. Min length of edges:' - + str(md)) + + str(md)) if md < tol: import pdb pdb.set_trace() if edges.shape[1] > orig_edge_num + split_outer_edge: - raise ValueError( - 'Have created edge without bookkeeping') + raise ValueError('Have created edge without bookkeeping') # If the outer edge (represented by edge_counter) was split, # e.g. inserted into the list of edges we need to increase the # index of the inner edge @@ -642,16 +636,15 @@ def __min_dist(p): # Possibly split the inner edge vertices, edges, split_inner_edge, \ - split = _split_edge(vertices, edges, intsect, - new_pt, **kwargs) + split = _split_edge(vertices, edges, intsect, + new_pt, **kwargs) if edges.shape[1] > \ orig_edge_num + split_inner_edge + split_outer_edge: - raise ValueError( - 'Have created edge without bookkeeping') + raise ValueError('Have created edge without bookkeeping') split_type.append(split) if verbose > 2 and split_inner_edge > 0 and \ - vertices.shape[1] > orig_vertex_num: + vertices.shape[1] > orig_vertex_num: print(' Introduced new point. Min length of edges:' + str(md)) if md < tol: @@ -662,14 +655,14 @@ def __min_dist(p): # We have found an intersection along a line segment if verbose > 2: print(' Found two intersections: (' - + str(new_pt[0, 0]) + ', ' + str(new_pt[1, 0]) + - 'and ' + str(new_pt[0, 1]) + ', ' + + + str(new_pt[0, 0]) + ', ' + str(new_pt[1, 0]) + + 'and ' + str(new_pt[0, 1]) + ', ' + str(new_pt[1, 1])) vertices, edges, splits,\ - s_type = _split_edge(vertices, edges, - [edge_counter, intsect], - new_pt, **kwargs) + s_type = _split_edge(vertices, edges, + [edge_counter, intsect], + new_pt, **kwargs) split_type.append(s_type) intersections += splits if verbose > 2 and (splits[0] > 0 or splits[1] > 0): @@ -695,9 +688,9 @@ def __min_dist(p): edge_counter += 1 if verbose > 1: - print(' Edge intersection removal complete. Elapsed time ' + + print(' Edge intersection removal complete. Elapsed time ' +\ str(time.time() - start_time)) - print(' Introduced ' + str(edges.shape[1] - num_edges_orig) + + print(' Introduced ' + str(edges.shape[1] - num_edges_orig) + \ ' new edges') return vertices, edges @@ -708,7 +701,6 @@ def __min_dist(p): # #----------------------------------------------------------- - def is_ccw_polygon(poly): """ Determine if the vertices of a polygon are sorted counter clockwise. @@ -747,12 +739,11 @@ def is_ccw_polygon(poly): num_p = poly.shape[1] value = 0 for i in range(num_p): - value += (p_1[i + 1] + p_1[i]) * (p_0[i + 1] - p_0[i]) + value += (p_1[i+1] + p_1[i]) * (p_0[i+1] - p_0[i]) return value < 0 #---------------------------------------------------------- - def is_ccw_polyline(p1, p2, p3, tol=0, default=False): """ Check if the line segments formed by three points is part of a @@ -788,7 +779,7 @@ def is_ccw_polyline(p1, p2, p3, tol=0, default=False): # Compute cross product between p1-p2 and p1-p3. Right hand rule gives that # p3 is to the left if the cross product is positive. cross_product = (p2[0] - p1[0]) * (p3[1] - p1[1])\ - - (p2[1] - p1[1]) * (p3[0] - p1[0]) + -(p2[1] - p1[1]) * (p3[0] - p1[0]) # Should there be a scaling of the tolerance relative to the distance # between the points? @@ -800,7 +791,6 @@ def is_ccw_polyline(p1, p2, p3, tol=0, default=False): #----------------------------------------------------------------------------- - def is_inside_polygon(poly, p, tol=0, default=False): """ Check if a set of points are inside a polygon. @@ -831,7 +821,7 @@ def is_inside_polygon(poly, p, tol=0, default=False): inside = np.ones(pt.shape[1], dtype=np.bool) for i in range(pt.shape[1]): for j in range(poly.shape[1]): - if not is_ccw_polyline(poly[:, j], poly[:, (j + 1) % poly_size], + if not is_ccw_polyline(poly[:, j], poly[:, (j+1) % poly_size], pt[:, i], tol=tol, default=default): inside[i] = False # No need to check the remaining segments of the polygon. @@ -862,7 +852,7 @@ def dist_point_pointset(p, pset, exponent=2): pt = p return np.power(np.sum(np.power(np.abs(pt - pset), exponent), - axis=0), 1 / exponent) + axis=0), 1/exponent) #------------------------------------------------------------------------------# @@ -884,7 +874,7 @@ def lines_intersect(start_1, end_1, start_2, end_2, tol=1e-8): Example: >>> lines_intersect([0, 0], [1, 1], [0, 1], [1, 0]) array([[ 0.5], - [ 0.5]]) + [ 0.5]]) >>> lines_intersect([0, 0], [1, 0], [0, 1], [1, 1]) @@ -929,7 +919,7 @@ def lines_intersect(start_1, end_1, start_2, end_2, tol=1e-8): # First check for solvability of the system (e.g. parallel lines) by the # determinant of the matrix. - discr = d_1[0] * (-d_2[1]) - d_1[1] * (-d_2[0]) + discr = d_1[0] *(-d_2[1]) - d_1[1] * (-d_2[0]) if np.abs(discr) < tol: # The lines are parallel, and will only cross if they are also colinear @@ -942,11 +932,11 @@ def lines_intersect(start_1, end_1, start_2, end_2, tol=1e-8): # Write l1 on the form start_1 + t * d_1, find the parameter value # needed for equality with start_2 and end_2 if np.abs(d_1[0]) > tol: - t_start_2 = (start_2[0] - start_1[0]) / d_1[0] - t_end_2 = (end_2[0] - start_1[0]) / d_1[0] + t_start_2 = (start_2[0] - start_1[0])/d_1[0] + t_end_2 = (end_2[0] - start_1[0])/d_1[0] elif np.abs(d_1[1]) > tol: - t_start_2 = (start_2[1] - start_1[1]) / d_1[1] - t_end_2 = (end_2[1] - start_1[1]) / d_1[1] + t_start_2 = (start_2[1] - start_1[1])/d_1[1] + t_end_2 = (end_2[1] - start_1[1])/d_1[1] else: # d_1 is zero raise ValueError('Start and endpoint of line should be\ @@ -993,7 +983,6 @@ def lines_intersect(start_1, end_1, start_2, end_2, tol=1e-8): #------------------------------------------------------------------------------# - def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): """ Find intersection points (or segments) of two 3d lines. @@ -1065,13 +1054,11 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): if mask_sum.sum() > 1: in_discr = np.argwhere(mask_sum)[:2] else: - # We're going to have a zero discreminant anyhow, just pick some - # dimensions. + # We're going to have a zero discreminant anyhow, just pick some dimensions. in_discr = np.arange(2) not_in_discr = np.setdiff1d(np.arange(3), in_discr)[0] - discr = deltas_1[in_discr[0]] * deltas_2[in_discr[1]] - \ - deltas_1[in_discr[1]] * deltas_2[in_discr[0]] + discr = deltas_1[in_discr[0]] * deltas_2[in_discr[1]] - deltas_1[in_discr[1]] * deltas_2[in_discr[0]] # An intersection will be a solution of the linear system # xs_1 + dx_1 * t_1 = xs_2 + dx_2 * t_2 (1) @@ -1105,15 +1092,15 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): diff_start = start_2 - start_1 dstart_x_delta_x = diff_start[1] * deltas_1[2] -\ - diff_start[2] * deltas_1[1] + diff_start[2] * deltas_1[1] if np.abs(dstart_x_delta_x) > tol: return None dstart_x_delta_y = diff_start[2] * deltas_1[0] -\ - diff_start[0] * deltas_1[2] + diff_start[0] * deltas_1[2] if np.abs(dstart_x_delta_y) > tol: return None dstart_x_delta_z = diff_start[0] * deltas_1[1] -\ - diff_start[1] * deltas_1[0] + diff_start[1] * deltas_1[0] if np.abs(dstart_x_delta_z) > tol: return None @@ -1121,15 +1108,13 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): # points should be parallel to the segments. # Since the masks are equal, we can use any of them. t_1_2 = (start_1[mask_1] - start_2[mask_1]) / deltas_1[mask_1] - # For dimensions with no incline, the start cooordinates should be the - # same + # For dimensions with no incline, the start cooordinates should be the same if not np.allclose(start_1[~mask_1], start_2[~mask_1], tol): return None # We have overlapping lines! finally check if segments are overlapping. - # Since everything is parallel, it suffices to work with a single - # coordinate + # Since everything is parallel, it suffices to work with a single coordinate s_1 = start_1[mask_1][0] e_1 = end_1[mask_1][0] s_2 = start_2[mask_1][0] @@ -1147,6 +1132,7 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): elif max_2 < min_1: return None + # The lines are overlapping, we need to find their common line lines = np.array([s_1, e_1, s_2, e_2]) sort_ind = np.argsort(lines) @@ -1164,14 +1150,14 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): # Solve 2x2 system by Cramer's rule discr = deltas_1[in_discr[0]] * (-deltas_2[in_discr[1]]) -\ - deltas_1[in_discr[1]] * (-deltas_2[in_discr[0]]) - t_1 = ((start_2[in_discr[0]] - start_1[in_discr[0]]) - * (-deltas_2[in_discr[1]]) - - (start_2[in_discr[1]] - start_1[in_discr[1]]) - * (-deltas_2[in_discr[0]])) / discr + deltas_1[in_discr[1]] * (-deltas_2[in_discr[0]]) + t_1 = ((start_2[in_discr[0]] - start_1[in_discr[0]]) \ + * (-deltas_2[in_discr[1]]) - \ + (start_2[in_discr[1]] - start_1[in_discr[1]]) \ + * (-deltas_2[in_discr[0]]))/discr t_2 = (deltas_1[in_discr[0]] * (start_2[in_discr[1]] - - start_1[in_discr[1]]) - + start_1[in_discr[1]]) - \ deltas_1[in_discr[1]] * (start_2[in_discr[0]] - start_1[in_discr[0]])) / discr @@ -1194,8 +1180,6 @@ def segments_intersect_3d(start_1, end_1, start_2, end_2, tol=1e-8): #----------------------------------------------------------------------------# # Represent the polygon as a sympy polygon - - def _np2p(p): # Convert a numpy point array (3xn) to sympy points if p.ndim == 1: @@ -1203,16 +1187,13 @@ def _np2p(p): else: return [geom.Point(p[:, i]) for i in range(p.shape[1])] - def _p2np(p): - # Convert sympy points to numpy format. If more than one point, these - # should be sent as a list + # Convert sympy points to numpy format. If more than one point, these should be sent as a list if isinstance(p, list): return np.array(list([i.args for i in p]), dtype='float').transpose() else: return np.array(list(p.args), dtype='float').reshape((-1, 1)) - def _to3D(p): # Add a third dimension return np.vstack((p, np.zeros(p.shape[1]))) @@ -1268,11 +1249,11 @@ def polygon_boundaries_intersect(poly_1, poly_2, tol=1e-8): for i in range(l_1): p_1_1 = poly_1[:, ind_1[i]] - p_1_2 = poly_1[:, ind_1[i + 1]] + p_1_2 = poly_1[:, ind_1[i+1]] for j in range(l_2): p_2_1 = poly_2[:, ind_2[j]] - p_2_2 = poly_2[:, ind_2[j + 1]] + p_2_2 = poly_2[:, ind_2[j+1]] isect_loc = segments_intersect_3d(p_1_1, p_1_2, p_2_1, p_2_2) if isect_loc is not None: isect.append([i, j, isect_loc]) @@ -1281,7 +1262,6 @@ def polygon_boundaries_intersect(poly_1, poly_2, tol=1e-8): #---------------------------------------------------------- - def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): """ Find intersections between polygons embeded in 3D. @@ -1312,8 +1292,7 @@ def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): """ - # First translate the points so that the first plane is located at the - # origin + # First translate the points so that the first plane is located at the origin center_1 = np.mean(poly_1, axis=1).reshape((-1, 1)) poly_1 = poly_1 - center_1 poly_2 = poly_2 - center_1 @@ -1375,11 +1354,11 @@ def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): for i in range(num_p2): # Indices of points of this segment i1 = ind[i] - i2 = ind[i + 1] + i2 = ind[i+1] # Coordinates of this segment pt_1 = poly_2_rot[:, ind[i]] - pt_2 = poly_2_rot[:, ind[i + 1]] + pt_2 = poly_2_rot[:, ind[i+1]] # Check if segment crosses z=0 in the rotated coordinates if max(pt_1[2], pt_2[2]) < 0 or min(pt_1[2], pt_2[2]) > 0: @@ -1420,7 +1399,7 @@ def polygon_segment_intersect(poly_1, poly_2, tol=1e-8): return isect -def is_planar(pts, normal=None): +def is_planar( pts, normal = None ): """ Check if the points lie on a plane. Parameters: @@ -1434,18 +1413,17 @@ def is_planar(pts, normal=None): """ if normal is None: - normal = compute_normal(pts) + normal = compute_normal( pts ) else: - normal = normal / np.linalg.norm(normal) + normal = normal / np.linalg.norm( normal ) - check = np.array([np.isclose(np.dot(normal, pts[:, 0] - p), 0.) - for p in pts[:, 1:].T], dtype=np.bool) - return np.all(check) + check = np.array( [ np.isclose( np.dot( normal, pts[:,0] - p ), 0. ) \ + for p in pts[:,1:].T ], dtype=np.bool ) + return np.all( check ) #------------------------------------------------------------------------------# - -def project_plane_matrix(pts, normal=None): +def project_plane_matrix( pts, normal = None ): """ Project the points on a plane using local coordinates. The projected points are computed by a dot product. @@ -1462,19 +1440,18 @@ def project_plane_matrix(pts, normal=None): """ if normal is None: - normal = compute_normal(pts) + normal = compute_normal( pts ) else: - normal = normal / np.linalg.norm(normal) + normal = normal / np.linalg.norm( normal ) - reference = np.array([0., 0., 1.]) - angle = np.arccos(np.dot(normal, reference)) - vect = np.cross(normal, reference) - return rot(angle, vect) + reference = np.array( [0., 0., 1.] ) + angle = np.arccos( np.dot( normal, reference ) ) + vect = np.cross( normal, reference ) + return rot( angle, vect ) #------------------------------------------------------------------------------# - -def rot(a, vect): +def rot( a, vect ): """ Compute the rotation matrix about a vector by an angle using the matrix form of Rodrigues formula. @@ -1487,18 +1464,17 @@ def rot(a, vect): """ - if np.allclose(vect, [0., 0., 0.]): + if np.allclose( vect, [0.,0.,0.] ): return np.identity(3) vect = vect / np.linalg.norm(vect) - W = np.array([[0., -vect[2], vect[1]], - [vect[2], 0., -vect[0]], - [-vect[1], vect[0], 0.]]) - return np.identity(3) + np.sin(a) * W + \ - (1. - np.cos(a)) * np.linalg.matrix_power(W, 2) + W = np.array( [ [ 0., -vect[2], vect[1] ], + [ vect[2], 0., -vect[0] ], + [ -vect[1], vect[0], 0. ] ] ) + return np.identity(3) + np.sin(a)*W + \ + (1.-np.cos(a))*np.linalg.matrix_power(W,2) #------------------------------------------------------------------------------# - def normal_matrix(pts=None, normal=None): """ Compute the normal projection matrix of a plane. @@ -1516,9 +1492,9 @@ def normal_matrix(pts=None, normal=None): """ if normal is not None: - normal = normal / np.linalg.norm(normal) + normal = normal / np.linalg.norm( normal ) elif pts is not None: - normal = compute_normal(pts) + normal = compute_normal( pts ) else: assert False, "Points or normal are mandatory" @@ -1526,7 +1502,6 @@ def normal_matrix(pts=None, normal=None): #------------------------------------------------------------------------------# - def tangent_matrix(pts=None, normal=None): """ Compute the tangential projection matrix of a plane. @@ -1547,7 +1522,6 @@ def tangent_matrix(pts=None, normal=None): #------------------------------------------------------------------------------# - def compute_normal(pts): """ Compute the normal of a set of points. @@ -1563,22 +1537,19 @@ def compute_normal(pts): """ assert pts.shape[1] > 2 - normal = np.cross(pts[:, 0] - pts[:, 1], compute_tangent(pts)) - if np.allclose(normal, np.zeros(3)): - return compute_normal(pts[:, 1:]) - return normal / np.linalg.norm(normal) + normal = np.cross( pts[:,0] - pts[:,1], compute_tangent(pts) ) + if np.allclose( normal, np.zeros(3) ): return compute_normal(pts[:, 1:]) + return normal / np.linalg.norm( normal ) #------------------------------------------------------------------------------# - def compute_normals_1d(pts): t = compute_tangent(pts) - n = np.array([t[1], -t[0], 0]) / np.sqrt(t[0]**2 + t[1]**2) - return np.r_['1,2,0', n, np.dot(rot(np.pi / 2., t), n)] + n = np.array([t[1], -t[0], 0]) / np.sqrt(t[0]**2+t[1]**2) + return np.r_['1,2,0', n, np.dot(rot(np.pi/2., t), n)] #------------------------------------------------------------------------------# - def compute_tangent(pts): """ Compute a tangent of a set of points. @@ -1592,13 +1563,12 @@ def compute_tangent(pts): """ - tangent = pts[:, 0] - np.mean(pts, axis=1) - assert not np.allclose(tangent, np.zeros(3)) + tangent = pts[:,0] - np.mean( pts, axis = 1 ) + assert not np.allclose( tangent, np.zeros(3) ) return tangent / np.linalg.norm(tangent) #------------------------------------------------------------------------------# - def is_collinear(pts, tol=1e-5): """ Check if the points lie on a line. @@ -1612,19 +1582,17 @@ def is_collinear(pts, tol=1e-5): """ assert pts.shape[1] > 1 - if pts.shape[1] == 2: - return True + if pts.shape[1] == 2: return True - pt0 = pts[:, 0] - pt1 = pts[:, 1] + pt0 = pts[:,0] + pt1 = pts[:,1] - coll = np.array([np.linalg.norm(np.cross(p - pt0, pt1 - pt0)) - for p in pts[:, 1:-1].T]) + coll = np.array( [ np.linalg.norm( np.cross( p - pt0, pt1 - pt0 ) ) \ + for p in pts[:,1:-1].T ] ) return np.allclose(coll, np.zeros(coll.size), tol) #------------------------------------------------------------------------------# - def map_grid(g): """ If a 2d or a 1d grid is passed, the function return the cell_centers, face_normals, and face_centers using local coordinates. If a 3d grid is @@ -1638,34 +1606,32 @@ def map_grid(g): face_normals: (g.dim x g.num_faces) the mapped normals of the faces. face_centers: (g.dim x g.num_faces) the mapped centers of the faces. R: (3 x 3) the rotation matrix used. - dim: indicates which are the dimensions active + dim: indicates which are the dimensions active. nodes: (g.dim x g.num_nodes) the mapped nodes. """ cell_centers = g.cell_centers face_normals = g.face_normals face_centers = g.face_centers - nodes = g.nodes R = np.eye(3) if g.dim == 0 or g.dim == 3: - return cell_centers, face_normals, face_centers, R, np.ones(3, dtype=bool), nodes + return cell_centers, face_normals, face_centers, R, np.ones(3,dtype=bool), nodes if g.dim == 1 or g.dim == 2: - v = compute_normal(g.nodes) if g.dim == 2 else compute_tangent(g.nodes) + v = compute_normal(g.nodes) if g.dim==2 else compute_tangent(g.nodes) R = project_plane_matrix(g.nodes, v) face_centers = np.dot(R, face_centers) - dim = np.logical_not(np.isclose(np.sum(np.abs(face_centers.T - - face_centers[:, 0]), axis=0), 0)) + dim = np.logical_not(np.isclose(np.sum(np.abs(face_centers.T- + face_centers[:,0]), axis=0),0)) assert g.dim == np.sum(dim) - face_centers = face_centers[dim, :] - cell_centers = np.dot(R, cell_centers)[dim, :] - face_normals = np.dot(R, face_normals)[dim, :] + face_centers = face_centers[dim,:] + cell_centers = np.dot(R, cell_centers)[dim,:] + face_normals = np.dot(R, face_normals)[dim,:] nodes = np.dot(R, nodes)[dim, :] return cell_centers, face_normals, face_centers, R, dim, nodes - #------------------------------------------------------------------------------# def distance_segment_segment(s1_start, s1_end, s2_start, s2_end): @@ -1692,12 +1658,10 @@ def distance_segment_segment(s1_start, s1_end, s2_start, s2_end): """ - # Variable used to fine almost parallel lines. Sensitivity to this value - # has not been tested. + # Variable used to fine almost parallel lines. Sensitivity to this value has not been tested. SMALL_TOLERANCE = 1e-6 - # For the rest of the algorithm, see the webpage referred to above for - # details. + # For the rest of the algorithm, see the webpage referred to above for details. d1 = s1_end - s1_start d2 = s2_end - s2_start d_starts = s1_start - s2_start @@ -1747,7 +1711,7 @@ def distance_segment_segment(s1_start, s1_end, s2_start, s2_end): # recompute sc for this edge if (-dot_1_starts + dot_1_2) < 0.0: sN = 0 - elif (-dot_1_starts + dot_1_2) > dot_1_1: + elif (-dot_1_starts+ dot_1_2) > dot_1_1: sN = sD else: sN = (-dot_1_starts + dot_1_2) @@ -1767,7 +1731,8 @@ def distance_segment_segment(s1_start, s1_end, s2_start, s2_end): dist = d_starts + sc * d1 - tc * d2 return np.sqrt(dist.dot(dist)) - if __name__ == "__main__": import doctest doctest.testmod() + + From d3a6657789dbdb5704e5bd96d057a58b9bec8d92 Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Fri, 21 Apr 2017 21:08:42 +0200 Subject: [PATCH 151/153] Fix of map_grid in compgeom. The variable nodes was referenced without being decleared. --- basics.py | 1 + 1 file changed, 1 insertion(+) diff --git a/basics.py b/basics.py index 2d50fd0b78..8897134233 100644 --- a/basics.py +++ b/basics.py @@ -1613,6 +1613,7 @@ def map_grid(g): cell_centers = g.cell_centers face_normals = g.face_normals face_centers = g.face_centers + nodes = g.nodes R = np.eye(3) if g.dim == 0 or g.dim == 3: From 8c0848148f4a20d9d60dfc36340a222e72779e3f Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Mon, 24 Apr 2017 12:53:30 +0200 Subject: [PATCH 152/153] Move comp geom repo to new location --- __init__.py => src/porepy/utils/__init__.py | 0 basics.py => src/porepy/utils/basics.py | 0 geometry.py => src/porepy/utils/geometry.py | 0 sort_points.py => src/porepy/utils/sort_points.py | 0 {tests => src/porepy/utils/tests}/__init__.py | 0 {tests => src/porepy/utils/tests}/test_basics.py | 0 {tests => src/porepy/utils/tests}/test_ccw.py | 0 {tests => src/porepy/utils/tests}/test_distance_segments.py | 0 {tests => src/porepy/utils/tests}/test_doctests.py | 0 {tests => src/porepy/utils/tests}/test_inside_polygon.py | 0 {tests => src/porepy/utils/tests}/test_intersections.py | 0 .../porepy/utils/tests}/test_polygon_boundaries_intersect.py | 0 .../porepy/utils/tests}/test_polygon_segment_intersection.py | 0 {tests => src/porepy/utils/tests}/test_segment_intersection_3d.py | 0 {tests => src/porepy/utils/tests}/test_sort_points.py | 0 15 files changed, 0 insertions(+), 0 deletions(-) rename __init__.py => src/porepy/utils/__init__.py (100%) rename basics.py => src/porepy/utils/basics.py (100%) rename geometry.py => src/porepy/utils/geometry.py (100%) rename sort_points.py => src/porepy/utils/sort_points.py (100%) rename {tests => src/porepy/utils/tests}/__init__.py (100%) rename {tests => src/porepy/utils/tests}/test_basics.py (100%) rename {tests => src/porepy/utils/tests}/test_ccw.py (100%) rename {tests => src/porepy/utils/tests}/test_distance_segments.py (100%) rename {tests => src/porepy/utils/tests}/test_doctests.py (100%) rename {tests => src/porepy/utils/tests}/test_inside_polygon.py (100%) rename {tests => src/porepy/utils/tests}/test_intersections.py (100%) rename {tests => src/porepy/utils/tests}/test_polygon_boundaries_intersect.py (100%) rename {tests => src/porepy/utils/tests}/test_polygon_segment_intersection.py (100%) rename {tests => src/porepy/utils/tests}/test_segment_intersection_3d.py (100%) rename {tests => src/porepy/utils/tests}/test_sort_points.py (100%) diff --git a/__init__.py b/src/porepy/utils/__init__.py similarity index 100% rename from __init__.py rename to src/porepy/utils/__init__.py diff --git a/basics.py b/src/porepy/utils/basics.py similarity index 100% rename from basics.py rename to src/porepy/utils/basics.py diff --git a/geometry.py b/src/porepy/utils/geometry.py similarity index 100% rename from geometry.py rename to src/porepy/utils/geometry.py diff --git a/sort_points.py b/src/porepy/utils/sort_points.py similarity index 100% rename from sort_points.py rename to src/porepy/utils/sort_points.py diff --git a/tests/__init__.py b/src/porepy/utils/tests/__init__.py similarity index 100% rename from tests/__init__.py rename to src/porepy/utils/tests/__init__.py diff --git a/tests/test_basics.py b/src/porepy/utils/tests/test_basics.py similarity index 100% rename from tests/test_basics.py rename to src/porepy/utils/tests/test_basics.py diff --git a/tests/test_ccw.py b/src/porepy/utils/tests/test_ccw.py similarity index 100% rename from tests/test_ccw.py rename to src/porepy/utils/tests/test_ccw.py diff --git a/tests/test_distance_segments.py b/src/porepy/utils/tests/test_distance_segments.py similarity index 100% rename from tests/test_distance_segments.py rename to src/porepy/utils/tests/test_distance_segments.py diff --git a/tests/test_doctests.py b/src/porepy/utils/tests/test_doctests.py similarity index 100% rename from tests/test_doctests.py rename to src/porepy/utils/tests/test_doctests.py diff --git a/tests/test_inside_polygon.py b/src/porepy/utils/tests/test_inside_polygon.py similarity index 100% rename from tests/test_inside_polygon.py rename to src/porepy/utils/tests/test_inside_polygon.py diff --git a/tests/test_intersections.py b/src/porepy/utils/tests/test_intersections.py similarity index 100% rename from tests/test_intersections.py rename to src/porepy/utils/tests/test_intersections.py diff --git a/tests/test_polygon_boundaries_intersect.py b/src/porepy/utils/tests/test_polygon_boundaries_intersect.py similarity index 100% rename from tests/test_polygon_boundaries_intersect.py rename to src/porepy/utils/tests/test_polygon_boundaries_intersect.py diff --git a/tests/test_polygon_segment_intersection.py b/src/porepy/utils/tests/test_polygon_segment_intersection.py similarity index 100% rename from tests/test_polygon_segment_intersection.py rename to src/porepy/utils/tests/test_polygon_segment_intersection.py diff --git a/tests/test_segment_intersection_3d.py b/src/porepy/utils/tests/test_segment_intersection_3d.py similarity index 100% rename from tests/test_segment_intersection_3d.py rename to src/porepy/utils/tests/test_segment_intersection_3d.py diff --git a/tests/test_sort_points.py b/src/porepy/utils/tests/test_sort_points.py similarity index 100% rename from tests/test_sort_points.py rename to src/porepy/utils/tests/test_sort_points.py From 7c73d83570b6bdbb26d59a4fdc4b9ad8a502305a Mon Sep 17 00:00:00 2001 From: Eirik Keilegavlen Date: Mon, 24 Apr 2017 12:53:56 +0200 Subject: [PATCH 153/153] Renamed basic comp geom module to comp_geom.py --- src/porepy/utils/{basics.py => comp_geom.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/porepy/utils/{basics.py => comp_geom.py} (100%) diff --git a/src/porepy/utils/basics.py b/src/porepy/utils/comp_geom.py similarity index 100% rename from src/porepy/utils/basics.py rename to src/porepy/utils/comp_geom.py