├── .gitignore ├── new_code.txt ├── old_code.txt └── assess_spherical_Voronoi_algorithm.ipynb /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | .ipynb_checkpoints/ 61 | 62 | -------------------------------------------------------------------------------- /new_code.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | Timer unit: 2.99601e-07 s 4 | 5 | Total time: 23.2093 s 6 | File: spherical_voronoi.py 7 | Function: _calc_vertices_regions at line 266 8 | 9 | Line # Hits Time Per Hit % Time Line Contents 10 | ============================================================== 11 | 266 def _calc_vertices_regions(self): 12 | 267 """ 13 | 268 Calculates the Voronoi vertices and regions of the generators stored 14 | 269 in self.points. The vertices will be stored in self.vertices and the 15 | 270 regions in self.regions. 16 | 271 17 | 272 This algorithm was discussed at PyData London 2015 by 18 | 273 Tyler Reddy, Ross Hemsley and Nikolai Nowaczyk 19 | 274 """ 20 | 275 21 | 276 # perform 3D Delaunay triangulation on data set 22 | 277 # (here ConvexHull can also be used, and is faster) 23 | 278 1 21679 21679.0 0.0 self._tri = scipy.spatial.ConvexHull(self.points) 24 | 279 25 | 280 # add the center to each of the simplices in tri to get the same 26 | 281 # tetrahedrons we'd have gotten from Delaunay tetrahedralization 27 | 282 1 631 631.0 0.0 tetrahedrons = self._tri.points[self._tri.simplices] 28 | 283 1 4 4.0 0.0 tetrahedrons = np.insert( 29 | 284 1 2 2.0 0.0 tetrahedrons, 30 | 285 1 2 2.0 0.0 3, 31 | 286 1 27 27.0 0.0 np.array([self.center]), 32 | 287 1 525 525.0 0.0 axis=1 33 | 288 ) 34 | 289 35 | 290 # produce circumcenters of tetrahedrons from 3D Delaunay 36 | 291 1 38743 38743.0 0.1 circumcenters = calc_circumcenters(tetrahedrons) 37 | 292 38 | 293 # project tetrahedron circumcenters to the surface of the sphere 39 | 294 1 7 7.0 0.0 self.vertices = project_to_sphere( 40 | 295 1 2 2.0 0.0 circumcenters, 41 | 296 1 4 4.0 0.0 self.center, 42 | 297 1 770 770.0 0.0 self.radius 43 | 298 ) 44 | 299 45 | 300 # calculate regions from triangulation 46 | 301 1 4 4.0 0.0 self.regions = [[k for k in range(0, len(self._tri.simplices)) 47 | 302 if n in self._tri.simplices[k]] 48 | 303 7994001 77405061 9.7 99.9 for n in range(0, len(self.points))] 49 | 50 | -------------------------------------------------------------------------------- /old_code.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | Timer unit: 2.99601e-07 s 4 | 5 | Total time: 0.722684 s 6 | File: D:\github\py_sphere_Voronoi\voronoi_utility.py 7 | Function: voronoi_region_vertices_spherical_surface at line 454 8 | 9 | Line # Hits Time Per Hit % Time Line Contents 10 | ============================================================== 11 | 454 def voronoi_region_vertices_spherical_surface(self): 12 | 455 '''Returns a dictionary with the sorted (non-intersecting) polygon vertices for the Voronoi regions associated with each generator (original data point) index. A dictionary entry would be structured as follows: `{generator_index : array_polygon_vertices, ...}`.''' 13 | 456 #use strategy for Voronoi region generation discussed at PyData London 2015 with Ross Hemsley and Nikolai Nowaczyk 14 | 457 #step 2: perform 3D Delaunay triangulation on data set that includes the extra generator 15 | 458 1 21250 21250.0 0.9 tri = scipy.spatial.ConvexHull(self.original_point_array) #using ConvexHull is much faster in scipy (vs. Delaunay), but here we only get the triangles on the sphere surface in the simplices object (no longer adding an extra point at the origin at this stage) 16 | 459 #add the origin to each of the simplices to get the same tetrahedra we'd have gotten from Delaunay tetrahedralization 17 | 460 1 624 624.0 0.0 simplex_coords = tri.points[tri.simplices] #triangles on surface surface 18 | 461 1 536 536.0 0.0 simplex_coords = numpy.insert(simplex_coords, 3, numpy.zeros((1,3)), axis = 1) 19 | 462 #step 3: produce circumspheres / circumcenters of tetrahedra from 3D Delaunay 20 | 463 1 218858 218858.0 9.1 array_circumcenter_coords = circumcircle.calc_circumcenter_circumsphere_tetrahedron_vectorized(simplex_coords) 21 | 464 #step 4: project tetrahedron circumcenters up to the surface of the sphere, to produce the Voronoi vertices 22 | 465 1 414 414.0 0.0 array_vector_lengths = scipy.spatial.distance.cdist(array_circumcenter_coords, numpy.zeros((1,3))) 23 | 466 1 274 274.0 0.0 array_Voronoi_vertices = (self.estimated_sphere_radius / numpy.abs(array_vector_lengths)) * array_circumcenter_coords 24 | 467 #step 5: use the Delaunay tetrahedralization neighbour information to connect the Voronoi vertices around the generators, to produce the Voronoi regions 25 | 468 1 5 5.0 0.0 dictionary_sorted_Voronoi_point_coordinates_for_each_generator = {} 26 | 469 1 4 4.0 0.0 array_tetrahedra = simplex_coords 27 | 470 1 4 4.0 0.0 generator_index = 0 28 | 471 1 27 27.0 0.0 generator_index_array = numpy.arange(self.original_point_array.shape[0]) 29 | 472 1 259047 259047.0 10.7 filter_tuple = numpy.where((numpy.expand_dims(tri.simplices, -1) == generator_index_array).any(axis=1)) 30 | 473 1 2043 2043.0 0.1 df = pandas.DataFrame({'generator_indices' : filter_tuple[1]}, index = filter_tuple[0]) 31 | 474 1 893 893.0 0.0 gb = df.groupby('generator_indices') 32 | 475 1 14260 14260.0 0.6 dictionary_generators_and_triangle_indices_containing_those_generators = gb.groups 33 | 476 2000 9373 4.7 0.4 for generator in tri.points[:-1]: 34 | 477 1999 11395 5.7 0.5 indices_of_triangles_surrounding_generator = dictionary_generators_and_triangle_indices_containing_those_generators[generator_index] 35 | 478 #pick any one of the triangles surrounding the generator and pick a non-generator vertex 36 | 479 1999 7530 3.8 0.3 first_tetrahedron_index = indices_of_triangles_surrounding_generator[0] 37 | 480 1999 9676 4.8 0.4 first_tetrahedron = array_tetrahedra[first_tetrahedron_index] 38 | 481 1999 10723 5.4 0.4 first_triangle = first_tetrahedron[:-1,...] 39 | 482 #pick one of the two non-generator vertices in the first triangle 40 | 483 1999 140105 70.1 5.8 indices_non_generator_vertices_first_triangle = numpy.unique(numpy.where(first_triangle != generator)[0]) 41 | 484 1999 10501 5.3 0.4 ordered_list_tetrahedron_indices_surrounding_current_generator = [first_tetrahedron_index] 42 | 485 #determine the appropriate ordering of Voronoi vertices to close the Voronoi region (polygon) by traversing the Delaunay neighbour data structure from scipy 43 | 486 1999 8609 4.3 0.4 vertices_remaining = len(indices_of_triangles_surrounding_generator) - 1 44 | 487 #choose the neighbour opposite the first non-generator vertex of the first triangle 45 | 488 1999 14352 7.2 0.6 neighbour_tetrahedral_index = tri.neighbors[first_tetrahedron_index][indices_non_generator_vertices_first_triangle[0]] 46 | 489 1999 10805 5.4 0.4 ordered_list_tetrahedron_indices_surrounding_current_generator.append(neighbour_tetrahedral_index) 47 | 490 1999 7410 3.7 0.3 vertices_remaining -= 1 48 | 491 49 | 492 #for all subsequent triangles it is the common non-generator vertex with the previous neighbour that should be used to propagate the connection chain to the following neighbour 50 | 493 #the common vertex with the previous neighbour is the the vertex of the previous neighbour that was NOT used to locate the current neighbour 51 | 494 #since there are only two candidate vertices on the previous neighbour and I've chosen to use the vertex with index 0, the remaining vertex on the previous neighbour is the non-generator vertex with index 1 52 | 495 1999 10859 5.4 0.5 common_vertex_coordinate = first_triangle[indices_non_generator_vertices_first_triangle[1]] 53 | 496 9982 35657 3.6 1.5 while vertices_remaining > 0: 54 | 497 7983 27711 3.5 1.1 current_tetrahedron_index = ordered_list_tetrahedron_indices_surrounding_current_generator[-1] 55 | 498 7983 38124 4.8 1.6 current_tetrahedron_coord_array = array_tetrahedra[current_tetrahedron_index] 56 | 499 7983 42676 5.3 1.8 current_triangle_coord_array = current_tetrahedron_coord_array[:-1,...] 57 | 500 7983 529349 66.3 21.9 indices_candidate_vertices_current_triangle_excluding_generator = numpy.unique(numpy.where(current_triangle_coord_array != generator)[0]) 58 | 501 7983 103850 13.0 4.3 array_candidate_vertices = current_triangle_coord_array[indices_candidate_vertices_current_triangle_excluding_generator] 59 | 502 7983 531960 66.6 22.1 current_tetrahedron_index_for_neighbour_propagation = numpy.unique(numpy.where(current_tetrahedron_coord_array == common_vertex_coordinate)[0]) 60 | 503 7983 63887 8.0 2.6 next_tetrahedron_index_surrounding_generator = tri.neighbors[current_tetrahedron_index][current_tetrahedron_index_for_neighbour_propagation][0] 61 | 504 7983 144059 18.0 6.0 common_vertex_coordinate = array_candidate_vertices[array_candidate_vertices != common_vertex_coordinate] #for the next iteration 62 | 505 7983 40995 5.1 1.7 ordered_list_tetrahedron_indices_surrounding_current_generator.append(next_tetrahedron_index_surrounding_generator) 63 | 506 7983 29176 3.7 1.2 vertices_remaining -= 1 64 | 507 1999 46999 23.5 1.9 dictionary_sorted_Voronoi_point_coordinates_for_each_generator[generator_index] = array_Voronoi_vertices[ordered_list_tetrahedron_indices_surrounding_current_generator] 65 | 508 1999 8132 4.1 0.3 generator_index += 1 66 | 509 1 3 3.0 0.0 return dictionary_sorted_Voronoi_point_coordinates_for_each_generator 67 | 68 | -------------------------------------------------------------------------------- /assess_spherical_Voronoi_algorithm.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": { 7 | "collapsed": false 8 | }, 9 | "outputs": [], 10 | "source": [ 11 | "#compare spherical Voronoi performance of my personal repo against the latest commit of the scipy fork (hash: c7a35c)\n", 12 | "import sys; sys.path.append('D:\\\\github\\\\py_sphere_Voronoi')\n", 13 | "import voronoi_utility\n", 14 | "import scipy\n", 15 | "import numpy as np\n", 16 | "import matplotlib.pyplot as plt\n", 17 | "%matplotlib inline\n", 18 | "%load_ext autoreload\n", 19 | "%load_ext line_profiler\n", 20 | "%autoreload 2" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 2, 26 | "metadata": { 27 | "collapsed": true 28 | }, 29 | "outputs": [], 30 | "source": [ 31 | "dictionary_spherical_Voronoi_performance_profiles = {}\n" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": 15, 37 | "metadata": { 38 | "collapsed": false 39 | }, 40 | "outputs": [ 41 | { 42 | "name": "stdout", 43 | "output_type": "stream", 44 | "text": [ 45 | "generator_count: 10 processed. 0.0090000629425\n", 46 | "generator_count: 100 processed. 0.0429999828339\n", 47 | "generator_count: 1000 processed. 0.422000169754\n", 48 | "generator_count: 2000 processed. 0.880000114441\n" 49 | ] 50 | } 51 | ], 52 | "source": [ 53 | "#for the old code in my personal repo\n", 54 | "import math\n", 55 | "import time\n", 56 | "array_generator_counts = np.array([10,100,1000, 2000]) #getting crashes at large numbers of generators\n", 57 | "prng = np.random.RandomState(117)\n", 58 | "center = np.zeros(3)\n", 59 | "\n", 60 | "for generator_count in array_generator_counts:\n", 61 | " current_array_random_generators = voronoi_utility.generate_random_array_spherical_generators(generator_count,1.0,prng)\n", 62 | " start_time = time.time()\n", 63 | " \n", 64 | " current_Voronoi_instance = voronoi_utility.Voronoi_Sphere_Surface(current_array_random_generators,1.0)\n", 65 | " \n", 66 | " %lprun -f voronoi_utility.Voronoi_Sphere_Surface.voronoi_region_vertices_spherical_surface dict_generators_and_voronoi_regions = current_Voronoi_instance.voronoi_region_vertices_spherical_surface()\n", 67 | " end_time = time.time()\n", 68 | " time_to_produce_voronoi_regions = end_time - start_time\n", 69 | "\n", 70 | " dictionary_spherical_Voronoi_performance_profiles[generator_count] = {}\n", 71 | " dictionary_spherical_Voronoi_performance_profiles[generator_count]['time_to_produce_voronoi_regions'] = time_to_produce_voronoi_regions\n", 72 | " \n", 73 | " print 'generator_count:', generator_count, 'processed. ', time_to_produce_voronoi_regions" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": 11, 79 | "metadata": { 80 | "collapsed": false 81 | }, 82 | "outputs": [ 83 | { 84 | "name": "stdout", 85 | "output_type": "stream", 86 | "text": [ 87 | "generator_count: 10 processed. 0.00399994850159\n", 88 | "generator_count: 100 processed. 0.0729999542236\n", 89 | "generator_count: 1000 processed. 7.08200001717\n", 90 | "generator_count: 2000 processed. 28.614000082\n" 91 | ] 92 | } 93 | ], 94 | "source": [ 95 | "import time\n", 96 | "array_generator_counts_scipy_fork = np.array([10,100,1000, 2000]) #much shorter list for SLOWER performing scipy code for now\n", 97 | "prng = np.random.RandomState(117)\n", 98 | "center = np.zeros(3)\n", 99 | "sys.path.append('D:\\\\github\\\\bench_spherical_voronoi')\n", 100 | "import spherical_voronoi\n", 101 | "\n", 102 | "for generator_count in array_generator_counts_scipy_fork:\n", 103 | " current_array_random_generators = voronoi_utility.generate_random_array_spherical_generators(generator_count,1.0,prng)\n", 104 | " \n", 105 | " #for the new code in my scipy fork\n", 106 | " start_time = time.time()\n", 107 | " %lprun -f spherical_voronoi.SphericalVoronoi._calc_vertices_regions sv = spherical_voronoi.SphericalVoronoi(current_array_random_generators, 1.0, center) #calculates unsorted regions\n", 108 | " #sv.sort_vertices_of_regions() #sorted regions produced \n", 109 | " #list_sorted_voronoi_regions_scipy_fork = sv.regions #commenting in case this assignment is slowing doing the new scipy bench unfairly\n", 110 | " end_time = time.time()\n", 111 | " time_to_produce_voronoi_regions_scipy_fork = end_time - start_time\n", 112 | " \n", 113 | " dictionary_spherical_Voronoi_performance_profiles[generator_count]['time_to_produce_voronoi_regions_scipy_fork'] = time_to_produce_voronoi_regions_scipy_fork\n", 114 | " \n", 115 | " print 'generator_count:', generator_count, 'processed.', time_to_produce_voronoi_regions_scipy_fork" 116 | ] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": 11, 121 | "metadata": { 122 | "collapsed": false 123 | }, 124 | "outputs": [ 125 | { 126 | "data": { 127 | "text/plain": [ 128 | "" 129 | ] 130 | }, 131 | "execution_count": 11, 132 | "metadata": {}, 133 | "output_type": "execute_result" 134 | }, 135 | { 136 | "data": { 137 | "image/png": "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\nY0NDA19//TUTJ04kJiaGI488kiVL9kfqzMrKYubMmRxzzDFER0czbdo0du/e7c5/++23Oeyww4iJ\niWHy5MmsWbPGZ9srV67klFNOIS4ujoSEBB588MEW+wLg0UcfJSkpiZSUFObMmdOozurqam677TbS\n09NJSEjg2muvpaqqGz46/fQOGIeJ/QvQB0gMtMfCZmRVS+dQWVupsz+brX/98q9aV18XaHHaheZ+\nP5/N+qzdPgdDenq6jh49WgsLC7WkpEQnTZqkd955p6qq/ve//9WBAwdqTk6ONjQ06IsvvqgZGRla\nU1Pjvnbs2LFaWFioVVVVumbNGk1NTdWtW7eqqmp+fr5u3LhRVVXvuusuPe6443Tnzp26c+dOnThx\not51112mDz77TENCQnTWrFlaV1en7733nkZERGhpaamqqmZnZ+tPP/2kqqrLly/X+Ph4feutt1RV\nNTc3V0VE6+vrfd6fp4yFhYUaFxen77//vqqqfvzxxxoXF6fFxcWqqnrSSSdpcnKyrly5UisqKvTc\nc8/VSy65RFVV165dq3369NFPPvlE6+rq9JFHHtGhQ4e6+8OTsrIyTUhI0Mcee0yrq6u1vLxcv/nm\nmxb74v3339f4+Hh3+xdeeKGKiLsfb7rpJj3nnHN09+7dWl5ermeddZbOnDnzoL779sTXb9xJP+CZ\n6nMkISJZHopkl6rWOccVqrrVKTO5Y1SXpTuQX5qPokSHRxMcFPCgboc8IsLvfvc7kpOTiYmJ4U9/\n+hMLFiwA4J///Ce//e1vGT9+PCLCpZdeSnh4OF9//bX72htuuIHk5GTCw8MJDg6murqalStXUltb\nS1paGoMHm7hf8+fP5+6776Z///7079+fWbNm8fLLL7vlCA0N5e677yY4OJgzzjiDvn37snbtWgBO\nOukkDjvsMABGjx7NBRdc0Ojtv6X785Rx3rx5nHnmmZx++ukAnHzyyYwbN45Fixa5y1966aWMGjWK\niIgI/vznP7Nw4UIaGhp49dVXmTp1KlOmTCE4OJjbbruNyspKvvzyywPafffdd0lKSuLmm28mLCyM\nvn37MmHChBb7YuHChVx55ZXu9j1HTarKs88+y2OPPUa/fv3o27cvM2fO5JVXXvHz2+46NLdPYqqI\nPAJ8AnwHbMVMTyUA44CTgc+cj6UHkluaC8DgmJ4RVDBrdlagRSA1dX9MrrS0NLZs2QJAfn4+L730\nEn/729/c+bW1te78ptcOHTqUxx9/nNmzZ7Ny5UpOO+00HnvsMRITE9myZQvp6ele2wGIi4sjKGj/\n+2VERAR79+4F4JtvvuGOO+5g5cqV1NTUUF1dzfnnn39Q95efn89rr73GO++8406rq6vjZz/7mc/+\nqK2tpbi4mK1bt5KWlubOExFSU1Mb3YeLgoICt4JsSnN9sXXrVsaPH98oz8XOnTvZt28fRx99tDtN\nVbvlyi6fIwlVvQ2YgontcApwF/AnjHL4CZisqn/oDCEtXZPc3UZJDIqxIb47i82bNzc6Tk42od7T\n0tL405/+xO7du92fvXv38qtf/cpdvqk7hgsvvJClS5eSn5+PiHD77bcDkJSURF5eXqN2kpKS/JLv\noosuYtq0aRQWFlJaWso111zTqgejp4xpaWnMmDGj0T2Vl5fzhz/sf+w07Y/Q0FAGDBhAUlIS+fn5\n7jxVpaCgwN1fnqSlpflctuqtL1x1JCYmHtC+i/79+9O7d29WrVrllr20tJSysjK/+6Kr0KzhWlXL\nVXWeql6rqmc6n2tV9f9UdW9nCWnpelTUVLC9YjshQSGkRKW0fIGlzagqTz31FEVFRZSUlHD//fe7\nlcBVV13FM888Q05ODqpKRUUFixYtcr/hN2XdunV8+umnVFdXEx4eTq9evQgONlOGF154Iffddx/F\nxcUUFxdz7733MmOGf1uj9u7dS0xMDGFhYeTk5DB//vyDdpp4ySWX8M477/DRRx9RX19PVVUV2dnZ\nFBUVuftj3rx5rF69mn379nH33Xdz3nnnISKcd955LFq0iE8//ZTa2lr++te/0qtXLyZOnHhAO1On\nTmXr1q088cQTVFdXU15eTk5Ojs++uOSSSwA4//zzmTt3rrt9z+mmoKAgrrrqKm666SZ27twJQFFR\nER999NFB9UVA8WaoaM8PJgrddmCFl7xbgQYg1iNtJrAeWIPZo+FKPxpY4eQ90Ux7rTTjWA6GFdtX\n6KzPZumLP74YaFHala78+8nIyNCHHnpIR40apf369dPLL79cKysr3fkffPCBjh8/Xvv166eJiYl6\n/vnn6969e93XLl682F12+fLlOmHCBI2MjNTY2Fg966yz3EbsqqoqveGGGzQxMVETExP1xhtv1Orq\nalU1huvU1NQD5HLV/frrr2t6erpGRkbq1KlT9frrr9cZM2aoqjFcBwUF+TRcN5VRVfWbb77Rk046\nSWNjY3XAgAE6depULSgoUFXVrKwsnTlzpk6YMEGjoqL07LPP1l27drmvffPNN3XUqFEaHR2tWVlZ\numrVKp99+9NPP+mUKVM0JiZGExIS9OGHH26xL1RVH3roIU1ISNDk5GSdM2eOBgUFuQ3XVVVV+sc/\n/lEHDx6sUVFROnLkSP3b3/7mU4bOwtdvHB+G6w53yyEiJwB7gZdUdbRHeirwLDAcOFpVS0RkFDAf\nGA8kY+whmaqqIpID/E5Vc0TkPeBJ9RLC1Lrl6BzeWfsO32/9npMHn8zxaccHWpx2oyu75Rg0aBDP\nP/98ozn5nszkyZOZMWMGV155ZaBF6Va0u1uOtqKqS4HdXrIeA5raNM4BFqhqrarmARuAY0QkEYhU\n1Ryn3EvAtA4S2eIHLqP1oH7WHmEJHF1VoR9K+OO76XzH4ysicpeIvCkiR7WlURE5ByhU1eVNspKA\nQo/zQsyIoml6kZNuCQClVaWUVJbQK6QXiZGJgRbH0oOxQaI6Hn8i092lqgtF5HjMaqe/AE8DxxxM\ngyISAfwRs2LKnXwwdfli9uzZ7uOsrKweG5u2o3CtakqPTidIbJj0ziI3NzfQInQpPvvMrr5vC9nZ\n2WRnZ7dYzh8lUe/8nQo8q6rvisif2yDbEEyUu2XOW0AK8L2IHIMZIaR6lE3BjCCKnGPP9CJfDXgq\nCUv709P2R1gshyJNX6B9uVDx5zWwSET+CfwKWCQivfy8ziuqukJV41V1kKoOwiiBo1R1O/A2cIGI\nhInIICATyFHVbUCZiBwjRrPMAN46WBksB4+q2v0RFksPwp+H/fnAh5jlqKWYiHV+uwoXkQXAl8Aw\nESkQkSuaFHFbnlR1FbAQs4HvfeA6j6VK1wHPYZbAbvC2ssnS8eyq3EV5TTl9QvswIGJAoMWxWCwd\njF9LYEUkBBiIx/SUqm72fUXgsEtgO5acohzeW/8ehw88nOmjpgdanHanKy+BtVjag/aMce268Hpg\nFrCD/fYJgNHer7Acymws2QjAkJghAZbEYrF0Bv5MN90EDFfVUao62vXpaMEsXY/6hnrySvMAa7Tu\nLkRGRjbyPdRefPHFF2RmZhIZGcnbb7/d6utnz57tt6uPyy+/nLvuuqvVbTRHTwil2l74oyQ2A93P\nK5Wl3SkqL6K6vpr+Ef2J7hUdaHEsflBeXk5GRka713v33Xdzww03UF5eztlnn93q61uzv0FEus1+\niKVLlxIZGdnoExQUxJtvvunX9ZMnT2bgwIFERUUxcuRInn32WXdednY2QUFBjer2dOHeUfizBDYX\n+ExEFgHCWjdXAAAgAElEQVSukEyqqo91nFiWrsim3cZTph1FWDZv3syoUaMO6tpDOTTpCSecQHl5\nuft8yZIlnHXWWe6YGC3x5JNPMmLECEJDQ8nJyeHEE0/kxBNPZPjw4QAkJydTUFDQIbL7wt+RxCdA\nGNAXiHQ+lh6GtUcEFl/hRuvr63nggQcYOnQoUVFRjBs3zu0pNSgoyO0G+/LLL+eaa67h1FNPJSoq\niqysLLd76//5n//htttua9Te2WefzeOPP36AHEOGDGHTpk2cddZZREVFueNWnH322cTFxZGZmclz\nzz3nLj979mymT5/OjBkziI6O5sUXX2xUX21tLRdeeCHTp0+ntrbW672XlJQwdepUoqKiOPbYYxu5\n9r7xxhtJS0sjOjqacePG8fnnn7vzcnJyGDduHNHR0SQkJHDrrbc2qnfevHmkp6czYMAAHnjggea/\nAA8qKyu59dZbycjIoF+/fpxwwgleQ5POnTuX8847j969e7vlOe6444iJiSEpKYnrr7++0T2PHj2a\n0NBQ93nfvn2JioryS6aSkhKuuOIKkpOTiY2N5Re/+IXf99McLY4kVHU2gIhEOuflzV5gOSSpqqui\nqLyIIAkio19GoMUJCLOzZ7dfXVmtq2vt2rX84x//4LvvviMhIYHNmze738gfe+wxXnnlFd5//30y\nMzNZvny5+6HUlPnz5/Pee+8xYcIE/vCHP3DxxRezdOlSLr/8cqZNm8ajjz6KiFBcXMzixYt5/vnn\nD6hj48aNBzgbvOCCCzjiiCN4/fXXWb16NaeccgpDhgxh8mQTvPLtt9/m9ddf5+WXX6aqqoqHH34Y\ngKqqKs4991zi4+N9uhVXVV555RU++OADxo4dy2WXXdYoKt+ECROYPXs20dHRPP7445x33nnk5+cT\nFhbGjTfeyM0338zFF1/Mvn37GsX9BmNbWbduHWvXrmXChAmce+65DB8+nM8//5yzzjqrUcxsT267\n7TZWr17NV199RXx8PDk5OY0CMQFUVFTwr3/9i3fffdedFhISwhNPPMG4ceMoKCjgjDPO4KmnnuLG\nG290l5k6dSqLFy9GRHjllVdITNzv+mbHjh0kJCQQERHBtGnTuO+++4iIiABgxowZREVFsWrVKvr0\n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\n", 138 | "text/plain": [ 139 | "" 140 | ] 141 | }, 142 | "metadata": {}, 143 | "output_type": "display_data" 144 | } 145 | ], 146 | "source": [ 147 | "#produce a plot of the performance results (i.e., empirical assessment of O(n))\n", 148 | "import scipy.optimize\n", 149 | "import matplotlib.ticker as mtick\n", 150 | "fig_performance = plt.figure()\n", 151 | "ax = fig_performance.add_subplot('111')\n", 152 | "\n", 153 | "def quadratic(n, K):\n", 154 | " '''Quadratic fit with K as the constant.'''\n", 155 | " return K * (n ** 2)\n", 156 | "\n", 157 | "y_data = []\n", 158 | "y_data_scipy = []\n", 159 | "for value in array_generator_counts:\n", 160 | " y_data.append(dictionary_spherical_Voronoi_performance_profiles[value]['time_to_produce_voronoi_regions'])\n", 161 | " if value <= 15000:\n", 162 | " y_data_scipy.append(dictionary_spherical_Voronoi_performance_profiles[value]['time_to_produce_voronoi_regions_scipy_fork'])\n", 163 | "\n", 164 | "for generator_count, subdictionary in dictionary_spherical_Voronoi_performance_profiles.iteritems():\n", 165 | " time_to_produce_Voronoi_regions = subdictionary['time_to_produce_voronoi_regions']\n", 166 | " ax.scatter(generator_count, time_to_produce_Voronoi_regions, edgecolor = 'none', c = 'k')\n", 167 | " if generator_count <= 15000:\n", 168 | " time_to_produce_Voronoi_regions_scipy = subdictionary['time_to_produce_voronoi_regions_scipy_fork']\n", 169 | " ax.scatter(generator_count, time_to_produce_Voronoi_regions_scipy, edgecolor = 'none', c = 'blue')\n", 170 | " \n", 171 | " \n", 172 | "K_quadratic, pcov_quadratic = scipy.optimize.curve_fit(quadratic, array_generator_counts, y_data)\n", 173 | "K_quadratic_scipy, pcov_quadratic_scipy = scipy.optimize.curve_fit(quadratic, array_generator_counts_scipy_fork, y_data_scipy)\n", 174 | "\n", 175 | "ax.set_xlabel('# generators')\n", 176 | "ax.set_ylabel('Time to produce sorted Voronoi regions (s)')\n", 177 | "ax.set_xlim(-10,72000)\n", 178 | "#ax.set_ylim(-2,150)\n", 179 | "ax.set_title('Empirical Assessment of O(n)\\n[quadratic time complexity non-linear fits shown]')\n", 180 | "\n", 181 | "#plot the fit to the data for performance model(s)\n", 182 | "sample_x_data = np.linspace(5,72000,num = 50)\n", 183 | "sample_x_data_scipy = np.linspace(5,20000,num = 30)\n", 184 | "ax.set_xticks([0,20000,40000, 60000])\n", 185 | "sample_y_data_quadratic = quadratic(sample_x_data, K_quadratic[0])\n", 186 | "sample_y_data_quadratic_scipy = quadratic(sample_x_data_scipy, K_quadratic_scipy[0])\n", 187 | "ax.plot(sample_x_data, sample_y_data_quadratic, c = 'purple', label = 'personal repo code', alpha = 0.5, lw = 2)\n", 188 | "ax.plot(sample_x_data_scipy, sample_y_data_quadratic_scipy, c = 'green', label = 'scipy fork hash: c7a35c', alpha = 0.5, lw = 2)\n", 189 | "ax.legend(loc = 1)" 190 | ] 191 | }, 192 | { 193 | "cell_type": "code", 194 | "execution_count": 23, 195 | "metadata": { 196 | "collapsed": false 197 | }, 198 | "outputs": [], 199 | "source": [ 200 | "fig_performance.savefig('performance_spherical_voronoi.png', dpi = 300)" 201 | ] 202 | } 203 | ], 204 | "metadata": { 205 | "kernelspec": { 206 | "display_name": "Python 2", 207 | "language": "python", 208 | "name": "python2" 209 | }, 210 | "language_info": { 211 | "codemirror_mode": { 212 | "name": "ipython", 213 | "version": 2 214 | }, 215 | "file_extension": ".py", 216 | "mimetype": "text/x-python", 217 | "name": "python", 218 | "nbconvert_exporter": "python", 219 | "pygments_lexer": "ipython2", 220 | "version": "2.7.9" 221 | } 222 | }, 223 | "nbformat": 4, 224 | "nbformat_minor": 0 225 | } 226 | --------------------------------------------------------------------------------