From 776412995c1dcc177209e8ceb1be4326876575e2 Mon Sep 17 00:00:00 2001 From: "Casper V. Kristensen" Date: Thu, 18 Oct 2018 18:41:18 +0200 Subject: [PATCH] Profiling. --- h2/gift_wrapper.py | 21 ++++++---- h2/graham.py | 45 ++++++++++------------ h2/mbc.py | 81 +++++++++++++++++++++++++++++---------- h2/profile.py | 26 +++++++++++++ h2/quick_hull.py | 25 ++++++++---- h2/tmptest.py | 95 +++++++++++++++++++++++++++++----------------- h2/util.py | 7 +++- 7 files changed, 204 insertions(+), 96 deletions(-) create mode 100644 h2/profile.py diff --git a/h2/gift_wrapper.py b/h2/gift_wrapper.py index 8670a98..7ee3b32 100644 --- a/h2/gift_wrapper.py +++ b/h2/gift_wrapper.py @@ -1,9 +1,11 @@ # Use atan2 instead of acos to calc angle; atan2(x,y) of the point we potentially want to add from math import acos, sqrt +from profile import Profiler from util import Vector, Point, gen_point +@Profiler("calculating angle") def calc_angle(v1: Vector, v2: Vector) -> float: dot = (v1.x * v2.x) + (v1.y * v2.y) len_1 = sqrt(v1.x**2 + v1.y**2) @@ -13,22 +15,25 @@ def calc_angle(v1: Vector, v2: Vector) -> float: return acos(max(min(tmp, 1), -1)) # acos is only defined in [-1,1] +@Profiler("calculating vector") def calc_vector(p1: Point, p2: Point) -> Vector: - return Vector((p2.x - p1.x), (p2.y - p1.y)) + return Vector((p2.x - p1.x), (p2. y - p1.y)) +@Profiler("gift_wrapper") def rapper(points: set): min_pt = min(points) hull = [min_pt] comp_vec = Vector(0, 1) - while True: - hull.append(min(points - {hull[-1]}, - key=lambda p: calc_angle(comp_vec, - calc_vector(hull[-1], p)))) - comp_vec = calc_vector(hull[-2], hull[-1]) + with Profiler("iterating points", excluded=("calculating angle", "calculating vector")): + while True: + hull.append(min(points - {hull[-1]}, + key=lambda p: calc_angle(comp_vec, calc_vector(hull[-1], p)))) - if hull[-1] == min_pt: - return hull + comp_vec = calc_vector(hull[-2], hull[-1]) + + if hull[-1] == min_pt: + return hull if __name__ == '__main__': diff --git a/h2/graham.py b/h2/graham.py index 48af38c..22efc0f 100644 --- a/h2/graham.py +++ b/h2/graham.py @@ -1,8 +1,10 @@ -from util import gen_point, Side, Point, display +from profile import Profiler + +from util import gen_point, Side, display +@Profiler("calculating sidedness") def sidedness(p1, p2, p3, eps=0.0000001): - # Find line from p1 to p2, ask where p3 is in relation to this y = p3.y * (p2.x - p1.x) @@ -18,37 +20,30 @@ def sidedness(p1, p2, p3, eps=0.0000001): return Side.BELOW -# test - - -p1 = Point(4, 4) -p2 = Point(0, 0) -p3 = Point(5, 2) - - -# print(sidedness(p1, p2, p3)) - - +@Profiler("graham_scan") def graham_scan(points): # A funky issue where both a and b become negative in the sidedness test causes us to have to use # Side.ABOVE for both tests, regardless of UH or LH. - sorted_points = sorted(points) + with Profiler("sorting points"): + sorted_points = sorted(points) UH = sorted_points[:2] + with Profiler("iterating upper hull", excluded=("calculating sidedness",)): + for s in sorted_points[2:]: + while len(UH) > 1 and (sidedness(UH[-2], UH[-1], s) != Side.ABOVE): + del UH[-1] + UH.append(s) - for s in sorted_points[2:]: - while len(UH) > 1 and (sidedness(UH[-2], UH[-1], s) != Side.ABOVE): - del UH[-1] - UH.append(s) - - reversed_list = list(reversed(sorted_points)) - reversed_list.append(UH[0]) + with Profiler("reversing list"): + reversed_list = list(reversed(sorted_points)) + reversed_list.append(UH[0]) LH = reversed_list[:2] - for s in reversed_list[2:]: - while len(LH) > 1 and (sidedness(LH[-2], LH[-1], s) != Side.ABOVE): - del LH[-1] - LH.append(s) + with Profiler("iterating lower hull", excluded=("calculating sidedness",)): + for s in reversed_list[2:]: + while len(LH) > 1 and (sidedness(LH[-2], LH[-1], s) != Side.ABOVE): + del LH[-1] + LH.append(s) return UH + LH diff --git a/h2/mbc.py b/h2/mbc.py index bb37cee..c29ae65 100644 --- a/h2/mbc.py +++ b/h2/mbc.py @@ -1,8 +1,8 @@ import random -from math import sqrt from typing import Set -from util import Side, Point, gen_point, display, gen_circular_point, gen_triangular_point +from profile import Profiler +from util import Side, Point, gen_point, display, gen_circular_point def sidedness(slope: float, intersection: float, p3: Point, flipper: callable, eps=0.0000001) -> Side: @@ -14,6 +14,7 @@ def sidedness(slope: float, intersection: float, p3: Point, flipper: callable, e return Side.BELOW +@Profiler("solving 1D LP") def solve_1dlp(c, constraints): c1, c2 = c ((a1, a2), b) = constraints[-1] @@ -22,7 +23,6 @@ def solve_1dlp(c, constraints): interval = [-10_000, 10_000] for (lel_a1, lel_a2), lel_b in constraints: - bj, aj = (lel_b - lel_a2 * q), (lel_a1 - lel_a2 * p) if aj < 0 and bj / aj > interval[0]: interval[0] = bj / aj @@ -36,6 +36,7 @@ def solve_1dlp(c, constraints): return interval[1], q - (p * interval[1]) +@Profiler("solving 2D LP") def solve_2dlp(c, constraints): c1, c2 = c x1 = -10_000 if c1 > 0 else 10_000 @@ -47,6 +48,7 @@ def solve_2dlp(c, constraints): return x1, x2 +@Profiler("finding median") def find_median(points): num_candidates = min(5, len(points)) candidates = random.sample(points, num_candidates) @@ -59,40 +61,77 @@ def find_median(points): return median[0] -def mbc_ch(points: Set[Point], flipper: callable) -> Set[Point]: +def is_left(a: Point, b: Point, c: Point): + return ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)) > 0 + + +def mbc_ch(points: Set[Point], flipper: callable, extra_prune=False, shuffle=True) -> Set[Point]: if len(points) < 2: return points + # Extra pruning step + if extra_prune: + with Profiler("extra pruning step"): + left_point = min(points, key=lambda p: (p.x, -p.y)) + right_point = max(points, key=lambda p: (p.x, -p.y)) + if flipper(1) == 1: + points = {p for p in points if is_left(left_point, right_point, p)}.union({left_point, right_point}) + else: + points = {p for p in points if not is_left(left_point, right_point, p)}.union({left_point, right_point}) + # Find the point with median x-coordinate, and partition the points on this point med_x = find_median(points) # Find left and right points in regards to median - pl = {p for p in points if p.x < med_x} - pr = {p for p in points if p.x >= med_x} + with Profiler("partitioning set"): + pl = {p for p in points if p.x < med_x} + pr = {p for p in points if p.x >= med_x} + + # Shuffle + constraints = [((flipper(-p.x), flipper(-1)), flipper(-p.y)) for p in points] + if shuffle: + with Profiler("shuffling constraints"): + random.shuffle(constraints) # Find the bridge over the vertical line in pm - slope, intercept = solve_2dlp((flipper(med_x), flipper(1)), - [((flipper(-p.x), flipper(-1)), flipper(-p.y)) for p in points]) + slope, intercept = solve_2dlp((flipper(med_x), flipper(1)), constraints) - - left_point = next(p for p in pl if sidedness(slope, intercept, p, flipper) == Side.ON) - right_point = next(p for p in pr if sidedness(slope, intercept, p, flipper) == Side.ON) - - - # Find the two points which are on the line - #dist_to_line = lambda p: abs(intercept + slope * p.x - p.y)/sqrt(1 + slope**2) - #left_point = min(pl, key=dist_to_line) - #right_point = min(pr, key=dist_to_line) + with Profiler("finding bridge points"): + left_point = next(p for p in pl if sidedness(slope, intercept, p, flipper) == Side.ON) + right_point = next(p for p in pr if sidedness(slope, intercept, p, flipper) == Side.ON) # Prune the points between the two line points - pl = {p for p in pl if p.x <= left_point.x} - pr = {p for p in pr if p.x >= right_point.x} + with Profiler("pruning between line points"): + pl = {p for p in pl if p.x <= left_point.x} + pr = {p for p in pr if p.x >= right_point.x} - return set.union(mbc_ch(pl, flipper), {left_point, right_point}, mbc_ch(pr, flipper)) + return set.union(mbc_ch(pl, flipper, extra_prune=extra_prune, shuffle=shuffle), + {left_point, right_point}, + mbc_ch(pr, flipper, extra_prune=extra_prune, shuffle=shuffle)) +@Profiler("mbc") def mbc(points: Set[Point]) -> Set[Point]: - return set.union(mbc_ch(points, lambda x: x), mbc_ch(points, lambda x: -x)) + return set.union(mbc_ch(points, lambda x: x, extra_prune=False, shuffle=True), + mbc_ch(points, lambda x: -x, extra_prune=False, shuffle=True)) + + +@Profiler("mbc2") +def mbc2(points: Set[Point]) -> Set[Point]: + return set.union(mbc_ch(points, lambda x: x, extra_prune=True, shuffle=True), + mbc_ch(points, lambda x: -x, extra_prune=True, shuffle=True)) + + +@Profiler("mbc_no_shuffle") +def mbc_no_shuffle(points: Set[Point]) -> Set[Point]: + return set.union(mbc_ch(points, lambda x: x, extra_prune=False, shuffle=False), + mbc_ch(points, lambda x: -x, extra_prune=False, shuffle=False)) + + +@Profiler("mbc2_no_shuffle") +def mbc2_no_shuffle(points: Set[Point]) -> Set[Point]: + return set.union(mbc_ch(points, lambda x: x, extra_prune=True, shuffle=False), + mbc_ch(points, lambda x: -x, extra_prune=True, shuffle=False)) if __name__ == '__main__': diff --git a/h2/profile.py b/h2/profile.py new file mode 100644 index 0000000..7d01044 --- /dev/null +++ b/h2/profile.py @@ -0,0 +1,26 @@ +import time +from collections import defaultdict +from contextlib import ContextDecorator +from typing import Tuple + + +class Profiler(ContextDecorator): + results = defaultdict(float) + + def __init__(self, name: str, excluded: Tuple = None) -> None: + self.name = name + self.excluded = excluded or () + + def __enter__(self): + self.start = time.time() + return self + + def __exit__(self, *exc): + stop = time.time() + excluded = sum(self.results[e] for e in self.excluded) + self.results[self.name] += stop - self.start - excluded + return False + + @classmethod + def reset(cls): + cls.results.clear() diff --git a/h2/quick_hull.py b/h2/quick_hull.py index 218eeae..9db0e9b 100644 --- a/h2/quick_hull.py +++ b/h2/quick_hull.py @@ -1,6 +1,7 @@ from math import sqrt from typing import Set +from profile import Profiler from util import Point, gen_point, display @@ -14,6 +15,7 @@ def is_left(a: Point, b: Point, c: Point): return ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)) > 0 +@Profiler("quick_hull") def quick_hull(points: Set[Point]): left = min(points) right = max(points) @@ -21,12 +23,16 @@ def quick_hull(points: Set[Point]): hull = {left, right} points = points - hull - find_hull({p for p in points if not is_left(left, right, p)}, + with Profiler("partitioning set"): + partition = {p for p in points if not is_left(left, right, p)} + find_hull(partition, left, right, hull) - find_hull({p for p in points if not is_left(right, left, p)}, + with Profiler("partitioning set"): + partition = {p for p in points if not is_left(right, left, p)} + find_hull(partition, right, left, hull) @@ -38,16 +44,21 @@ def find_hull(points: Set[Point], p: Point, q: Point, hull: Set[Point]): if not points: return - farthest = max(points, key=lambda point: abs(distance(p, q, point))) - hull.add(farthest) - points.remove(farthest) + with Profiler("finding farthest point from line"): + farthest = max(points, key=lambda point: abs(distance(p, q, point))) + hull.add(farthest) + points.remove(farthest) - find_hull({po for po in points if not is_left(p, farthest, po)}, + with Profiler("partitioning set"): + partition = {po for po in points if not is_left(p, farthest, po)} + find_hull(partition, p, farthest, hull) - find_hull({po for po in points if not is_left(farthest, q, po)}, + with Profiler("partitioning set"): + partition = {po for po in points if not is_left(farthest, q, po)} + find_hull(partition, farthest, q, hull) diff --git a/h2/tmptest.py b/h2/tmptest.py index a2c74a3..c9d1d93 100644 --- a/h2/tmptest.py +++ b/h2/tmptest.py @@ -4,7 +4,8 @@ from collections import namedtuple import util from gift_wrapper import rapper from graham import graham_scan -from mbc import mbc +from mbc import mbc, mbc_no_shuffle, mbc2_no_shuffle, mbc2 +from profile import Profiler from quick_hull import quick_hull import os.path @@ -13,10 +14,9 @@ import os.path TimedResult = namedtuple("TimedResult", "algorithm points running_time") -def time_it(f: callable, args: tuple = (), iterations=1): +def time_it(f: callable, args: tuple = ()): start = time() - for i in range(iterations): - f(*args) + f(*args) return str(time() - start) @@ -26,7 +26,6 @@ def initiate_file(file): def write_to_log(file, data): - if not os.path.isfile(file): initiate_file(file) @@ -42,49 +41,41 @@ def write_to_log(file, data): open_file.write(write_string) -def do_square_tests(amount_of_points): - points_square = {util.gen_point(0, 100) for _ in range(amount_of_points)} - amount_of_points = str(amount_of_points) +def calculate_hulls(number_of_points, points): + return [TimedResult("graham", number_of_points, time_it(graham_scan, args=(points,))), + TimedResult("gift", number_of_points, time_it(rapper, args=(points,))), + TimedResult("quick", number_of_points, time_it(quick_hull, args=(points,))), + TimedResult("mbch", number_of_points, time_it(mbc, args=(points,))), + TimedResult("mbch2", number_of_points, time_it(mbc2, args=(points,)))] - results = [TimedResult("graham", amount_of_points, time_it(graham_scan, args=(points_square,))), - TimedResult("gift", amount_of_points, time_it(rapper, args=(points_square,))), - TimedResult("quick", amount_of_points, time_it(quick_hull, args=(points_square,))), - TimedResult("mbch", amount_of_points, time_it(mbc, args=(points_square,)))] +def do_square_tests(number_of_points): + points_square = {util.gen_point(0, 100) for _ in range(number_of_points)} + number_of_points = str(number_of_points) + + results = calculate_hulls(number_of_points, points_square) write_to_log("square_tests.log", results) -def do_circular_tests(amount_of_points): - points_circular = {util.gen_point(0, 100) for _ in range(amount_of_points)} - - results = [TimedResult("graham", amount_of_points, time_it(graham_scan, args=(points_circular,))), - TimedResult("gift", amount_of_points, time_it(rapper, args=(points_circular,))), - TimedResult("quick", amount_of_points, time_it(quick_hull, args=(points_circular,))), - TimedResult("mbc", amount_of_points, time_it(mbc, args=(points_circular,)))] +def do_circular_tests(number_of_points): + points_circular = {util.gen_point(0, 100) for _ in range(number_of_points)} + results = calculate_hulls(number_of_points, points_circular) write_to_log("circular_tests.log", results) -def do_triangular_tests(amount_of_points): +def do_triangular_tests(number_of_points): left, right, top = util.Point(1,1), util.Point(51,1), util.Point(26,40) - points = {util.gen_triangular_point(left, right, top) for _ in range(amount_of_points)} - - results = [TimedResult("graham", amount_of_points, time_it(graham_scan, args=(points,))), - TimedResult("gift", amount_of_points, time_it(rapper, args=(points,))), - TimedResult("quick", amount_of_points, time_it(quick_hull, args=(points,))), - TimedResult("mbc", amount_of_points, time_it(mbc, args=(points,)))] + points = {util.gen_triangular_point(left, right, top) for _ in range(number_of_points)} + results = calculate_hulls(number_of_points, points) write_to_log("triangular_tests.log", results) -def do_quadratic_tests(amount_of_points): - points = {util.gen_weird_point(-10, 10) for _ in range(amount_of_points)} - - results = [TimedResult("graham", amount_of_points, time_it(graham_scan, args=(points,))), - TimedResult("gift", amount_of_points, time_it(rapper, args=(points,))), - TimedResult("quick", amount_of_points, time_it(quick_hull, args=(points,))), - TimedResult("mbc", amount_of_points, time_it(mbc, args=(points,)))] +def do_quadratic_tests(number_of_points): + points = {util.gen_weird_point(-10, 10) for _ in range(number_of_points)} + results = calculate_hulls(number_of_points, points) write_to_log("quadratic_tests.log", results) @@ -94,10 +85,46 @@ def sanity_check(): gift = set(rapper(points)) quick = quick_hull(points) mbch = set.union(mbc(points)) - assert gift == graham == quick == mbch + mbch2 = set.union(mbc2(points)) + assert gift == graham == quick == mbch == mbch2 + + +def do_profile(): + print("==================================== PROFILE RESULTS ====================================") + random.seed(6) + points = {util.gen_point(0, 100) for _ in range(60_000)} + + tests = [ + ("graham_scan", graham_scan), + ("gift_wrapper", rapper), + ("quick_hull", quick_hull), + ("mbc", mbc), + ("mbc2", mbc2), + ("mbc_no_shuffle", mbc_no_shuffle), + ("mbc2_no_shuffle", mbc2_no_shuffle), + ] + + for algorithm, func in tests: + Profiler.reset() + func(points) + + times = Profiler.results + + print(f"-------------- {algorithm} --------------") + print("Times:", times) + + total = times[algorithm] + print("Total:", total) + + sum_profiled = sum(times.values()) - total + print("Total Profiled:", sum_profiled) + + print("Unaccounted:", total - sum_profiled) if __name__ == '__main__': sanity_check() + do_profile() + exit() for i in range(50, 1000, 50): do_square_tests(i) diff --git a/h2/util.py b/h2/util.py index b8632a5..9357ce8 100644 --- a/h2/util.py +++ b/h2/util.py @@ -72,6 +72,7 @@ def gen_graph(data): graham = data['graham'] quick = data['quick'] mbch = data['mbch'] + mbch2 = data['mbch2'] gift = data['gift'] graham_x = [p[0] for p in graham] @@ -83,15 +84,19 @@ def gen_graph(data): mbch_x = [p[0] for p in mbch] mbch_y = [p[1] for p in mbch] + mbch2_x = [p[0] for p in mbch2] + mbch2_y = [p[1] for p in mbch2] + gift_x = [p[0] for p in gift] gift_y = [p[1] for p in gift] plt.plot(graham_x, graham_y) plt.plot(quick_x, quick_y) plt.plot(mbch_x, mbch_y) + plt.plot(mbch2_x, mbch2_y) plt.plot(gift_x, gift_y) - plt.legend(['graham', 'quick', 'mbch', 'gift'], loc='upper left') + plt.legend(['graham', 'quick', 'mbch', 'mbch2', 'gift'], loc='upper left') plt.show()