Computational Geometry in Python

reference: 'Discrete and Computational Geometry' (2011) Devadoss and O'Rourke.


In [1]:
%load_ext autoreload
%autoreload 2

import itertools
import circumcircle
import triangle
import numpy as np
import scipy
import scipy.spatial
import scipy.optimize
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
%matplotlib inline
import math
from ipywidgets import interact, interactive, fixed
import ipywidgets as widgets
import cPickle as pickle
import shapefile #Python pyshp library
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from matplotlib.collections import PolyCollection
from matplotlib.collections import PatchCollection
from matplotlib import colors
import time


/Users/treddy/anaconda/lib/python2.7/site-packages/matplotlib/font_manager.py:273: UserWarning: Matplotlib is building the font cache using fc-list. This may take a moment.
  warnings.warn('Matplotlib is building the font cache using fc-list. This may take a moment.')

1) Introduction: What is Computational Geometry? Why is it important?

Definition: the study of computer algorithms that perform geometric operations.

Applications:

  • computer graphics
  • computer-aided design / drafting (CAD)
  • robotic motion planning / collision avoidance
  • geographic information systems (GIS) / search and rescue, etc.
  • computer vision (3D reconstruction)
  • computational biochemistry / virology
  • epidemiology (water source contamination)
  • ecology (species habitat estimates based on sightings)
  • integrated circuit design
  • enconomical construction of physical networks
  • cooking ingredient logistics (success polygons)

    A "young field," with modern computational geometry starting in ~ the 1970s.

2) Polygons

2.1 Definition:

"A polygon is the closed region of the plane bounded by a finite collection of line segments forming a closed curve that does not intersect itself."


In [2]:
#plot an example of a polygon and an object that fails to be a polygon


fig_2_1 = matplotlib.pyplot.figure()

polygon_vertices = np.array([[0,2], #top left
                             [2,2], #top right
                             [2,0], #bottom right
                             [0,0]]) #bottom left

self_intersection_vertices = np.array([[0,2], #top left
                                       [2,2], #top right
                                       [0,0], #bottom left
                                       [2,0]]) #bottom right

ax = fig_2_1.add_subplot(121)
ax.scatter(polygon_vertices[...,0], polygon_vertices[...,1], color='black', s=200, label = 'vertices')
polygon = Polygon(polygon_vertices, alpha = 0.5)
ax.add_patch(polygon)
ax.set_title('A Polygon', fontsize=18)

ax2 = fig_2_1.add_subplot(122)
ax2.scatter(self_intersection_vertices[...,0], self_intersection_vertices[...,1], color='black', s=200, label = 'vertices')
polygon = Polygon(self_intersection_vertices, alpha = 0.5)
ax2.add_patch(polygon)
ax2.set_title('Self-Intersection = Not a Polygon', fontsize=18)

for axis in [ax,ax2]:
    axis.set_xlabel('x', fontsize = 16)
    axis.set_ylabel('y', fontsize = 16)

#some text labels for the polygon case
ax.text(0.85,2.1,'Edge', fontsize = 16)
ax.text(0.85,-0.2,'Edge', fontsize = 16)
ax.text(-0.25,1.0,'Edge', fontsize = 16, rotation=90)
ax.text(2.1,1.0,'Edge', fontsize = 16, rotation=90)
ax.text(-0.4,2.3,'Vertex', fontsize = 16, rotation=-45)
ax.text(2.0,2.3,'Vertex', fontsize = 16, rotation=45)
ax.text(-0.4,-0.1,'Vertex', fontsize = 16, rotation=45)
ax.text(2.0,-0.1,'Vertex', fontsize = 16, rotation=-45)

fig_2_1.set_size_inches(17,8.5)


2.2 Every Polygon Has A Triangulation:

Some useful definitions:

  1. Diagonal: a line segment connecting two vertices of P and lying in the interior of P, not touching $\partial$P except at its endpoints
  2. Triangulation: a decomposition of P into triangles by a maximal set of noncrossing diagonals

In [3]:
@interact(y_value_p1 = widgets.FloatSlider(min=-1,max=2.1,step=0.1,value=-1),
          y_value_p2 = widgets.FloatSlider(min=-1,max=2.1,step=0.1,value=-1),
          new_diagonal_p2 = False)
def polygon_sweep(y_value_p1, y_value_p2, new_diagonal_p2):
    fig_2_2 = matplotlib.pyplot.figure()
    fig_2_2.set_size_inches(17,8.5)

    polygon_1 = np.array([[0,2],#top
                          [-1,1],#left
                          [0,-1], #bottom
                          [1,1]])#right

    polygon_2 = np.array([[0,0.7],#top
                          [-1,2],#left
                          [0,-1],#bottom
                          [1,2]])#right

    ax1 = fig_2_2.add_subplot(121)
    p1 = Polygon(polygon_1, color='blue', alpha = 0.2)
    ax1.add_patch(p1)
    ax1.scatter(polygon_1[...,0], polygon_1[...,1], s=80)
    ax2 = fig_2_2.add_subplot(122)
    p2 = Polygon(polygon_2, color='blue', alpha = 0.2)
    ax2.add_patch(p2)
    ax2.scatter(polygon_2[...,0], polygon_2[...,1], s=80)

    for axis in [ax1,ax2]:
        axis.set_xlim(-1.2,1.2)
        axis.set_ylim(-1.1,2.1)
    ax1.axhline(y=y_value_p1, lw=4, color = 'black')
    ax2.axhline(y=y_value_p2, lw=4, color = 'black')
    if new_diagonal_p2:
        ax2.axvline(x=0,ymin=0.03,ymax=0.55,color='green',lw=4)


2.3 Every triangulation of a polygon with n vertices has n - 2 triangles

Although we will deal with triangulations in greater detail later, here we will use a specific type of triangulation (a constrained triangulation) available in the award-winning Triangle library to see a demonstration of the above property on some interesting polygons. The Python bindings were written by Dzhelil Rufat--the original Triangle library was written by Jonathan Shewchuk (Prof. at UC Berkeley).


In [4]:
sf = shapefile.Reader("shapefiles/cb_2015_us_state_20m.shp") #a pain to figure out where / how to download these from Government websites
shapeRecs = sf.shapeRecords()
for shapeRec in shapeRecs:
    if 'Oregon' in shapeRec.record:
        Oregon_vertices = np.array(shapeRec.shape.points)
        
#filter out roughly co-linear points for the purpose of the Triangle library (otherwise get segfault problems)
rows_to_keep = []
index = 0
while index < Oregon_vertices.shape[0] - 1:
    current_x_coord = Oregon_vertices[index,0]
    current_y_coord = Oregon_vertices[index,1]
    next_x_coord = Oregon_vertices[index + 1,0]
    next_y_coord = Oregon_vertices[index + 1,1]
    if abs(current_x_coord - next_x_coord) < 0.001 or abs(current_y_coord - next_y_coord) < 0.001:
        index += 1
        continue
    else:
        rows_to_keep.append(index)
        index += 1
        
Oregon_vertices = Oregon_vertices[rows_to_keep]
Oregon_vertices.shape


Out[4]:
(214, 2)

In [5]:
def generate_regular_polygon_vertices(r, n_vertices):
    iteration_values = np.arange(n_vertices)
    angles = 2 * math.pi * iteration_values / float(n_vertices)
    x_coords = r * np.cos(angles)
    y_coords = r * np.sin(angles)
    polygon_coords = np.stack((x_coords, y_coords), axis=-1)
    return polygon_coords

#generate the 65537-gon, which Hermes worked on for 10 years in a 200-page manuscript
polygon_Hermes = generate_regular_polygon_vertices(1.0, 65537)
polygon_Hermes.shape


Out[5]:
(65537, 2)

In [6]:
#plot the polygons of interest with interactive widget(s) for triangulation, etc.

@interact(triangulate_Oregon= False,
          triangule_Hermes = False)
def triangulation_demo(triangulate_Oregon=False, triangulate_Hermes=False):
    fig_2_3 = plt.figure()
    ax1 = fig_2_3.add_subplot(121)
    ax1.scatter(Oregon_vertices[...,0], Oregon_vertices[...,1])
    p_Oregon = Polygon(Oregon_vertices, color='green', alpha = 0.4)
    ax1.add_patch(p_Oregon)
    ax1.set_title('Oregon', fontsize=18)
    ax2 = fig_2_3.add_subplot(122)
    ax2.scatter(polygon_Hermes[...,0], polygon_Hermes[...,1])
    ax2.set_title('65537-gon', fontsize=18)
    fig_2_3.set_size_inches(17,8.5)
    
    if triangulate_Oregon:
        #need vertex indices for all edges of the polygon
        segment_start_indices = np.arange(Oregon_vertices.shape[0])
        segment_end_indices = segment_start_indices + 1
        segment_end_indices[-1] = 0
        segment_indices = np.array(zip(segment_start_indices, segment_end_indices))
        print 'Oregon_vertices.shape:', Oregon_vertices.shape
        Oregon_vertex_dict = dict(vertices = Oregon_vertices, segments = segment_indices)
        tri = triangle.triangulate(Oregon_vertex_dict, 'p') #'p' for triangulation of planar graph
        print 'Oregon num triangles:', tri['triangles'].shape
        simplex_coords = Oregon_vertices[tri['triangles']]
        triangles = PolyCollection((simplex_coords), alpha = 0.3)
        ax1.add_collection(triangles)
        
    if triangulate_Hermes:
        segment_start_indices = np.arange(polygon_Hermes.shape[0])
        segment_end_indices = segment_start_indices + 1
        segment_end_indices[-1] = 0
        segment_indices = np.array(zip(segment_start_indices, segment_end_indices))
        Hermes_vertex_dict = dict(vertices = polygon_Hermes, segments = segment_indices)
        tri = triangle.triangulate(Hermes_vertex_dict, 'p') #'p' for triangulation of planar graph
        print '65537-gon num triangles:', tri['triangles'].shape
        simplex_coords = polygon_Hermes[tri['triangles']]
        triangles = PolyCollection((simplex_coords), alpha = 0.3)
        ax2.add_collection(triangles)


Oregon_vertices.shape: (214, 2)
Oregon num triangles: (212, 3)
65537-gon num triangles: (65535, 3)

2.4 The Catalan Number

Convex Polygon: every pair of nonadjacent vertices determines a diagonal

Catalan Number: the number of triangulations of a convex polygon with n + 2 vertices

$$C_n = \frac{(2n)!}{(n+1)!n!}$$

In [7]:
def catalan_number(num_vertices):
    n = num_vertices - 2
    Catalan = math.factorial(2 * n) / (math.factorial(n + 1) * math.factorial(n))
    return Catalan

In [8]:
#How many triangulations are possible for the 65537-gon?
triangulation_count_Hermes = catalan_number(65537)
triangulation_count_Hermes


Out[8]:
33747323201440556113338056162985588357837875086719254529204776056363889844905557713739048340117996110352921700408951100919433940915769499462407428004752011383006419657963144014042479281804847015572826733723623164429057440898781532797597869815484271075717444684570662682394963408208629099080715745972772946006296106499354887299694402936319753107045557391740445863439160648262001141872452050396588948824465259975163200303038640056430714650059133450796261891032284598017854130128705295226326771545998255504595581964903787472423750457206857259913504189304684939640125496939691164198079307649525960603208631067317975666087499793971587417296924326148219991968402007732708048194308385639765795062086746617174268472543023866024248511351083726209450603555886380626916052013859447519902993348887796932314345276891307538484736421687825391628640809478840813305578682574926433003601702463472852911043608483630418615427003880182747331593679315417192039482123533569719483000440250968754176225549326950785042098699610113101710878897892324308772294967855423675750513298583186960648325532774424214890507083605942388848953275619401724362675918430710426975290691360506884653336621653064690892329824732925845275837230176268495691439942578845868938643637837142788641859506216595475847303300002357743894695114912058095860775609082165533490128147966257310220747246225352197266119675066529403323718172463284564846561515902214961442068347629452357735281973460365712773387179589643898160210925665173486469429257090809577289103806300560691024021868483288916732280232429033383791804523427619913581593088192738155155640689493245442530327824518958137187993095893856884833221704621134899433981352395367589264888033097666054235552662092420733768975530998477809203631438016272704775829523113286779065494129024488290879798931262712838959860387988385921256798404115149812776446387864099619976334184981649781953733175474904314606510455389119458055332710937299173499999020246609985660288903275252681763417364466797820296805596212806591886057148195500887607393539358593991322763610105244137640257663940551594721363605426381268669198424712562157639506274867982668696361068571216091736355764314932253465574154553869247361289079532438719933095238850941912739324061862359436582033742234588919928940664098633424752500402133144779058266756204443778506162437467451584372958087719039361296158334246052072002526339358570474632968653447417064109468960990192115121921317126726285381017151696476109811823463073953245952773212086468408238165863253741772013211565054148459847293731692995115870664092216851538843447102427142239822023579754370473404954153464100832146900148902348882561100912244276646696476984140479461228438739208634307437937458460535626674720659490920600264268924423431774490791859095568373098392043698007184166500542024223211412448558443147168128923502459195152910586068113407441487252811892512577165499056781544109856440089063699466442986466035617079143709531473497529098943302873751119240639226134143874756814557254363412326147153359980247205167376551085684959010985391497040485148850591713335318641457376552268088436825597470463352673067826600599688190349874909218587280793794299777603086879231681747404835481274978721014443077271465463564635241751931183811917137900951050146715419723250002303577581667818654515128040815534790615451837708388768073206228368325199768197792772897098823578280187829771178025197733126120058811555288898570048066545446906923677205068859719468230599701366050603679478057322007856839565030758286237639026178195721500266385843373032766966241214023406782331913455035655030603496884666979425897870400738459962014660342790133761583697947482106656403154265635805823984933117221117725670516201409405814030741478738332422294495023701636610844992761458182973748166630014288329495508475532037190833762581323750980700429440518655161622043458655014219317571399149337547713959849133378618966610650639860501971693537591360290733068967126648140648618608272537235085706790227695953647698561405401378937939475942571581128437970879240039118512380386758451580248459824342581075558256034126000842183676288932288054619617648903766247189793106020217023599702591866761159230096777143028418318151173163677828909243021512253473816060727665521645606359622135698545837816964966909571702155064979816995562946755050691407350513059397276307656527777414138369384126637935650723519477585381459919727517956981235322502851192881786476202450856595755635611089213056532732326103995510442351197895368356023800651243466512190633596214640616879828416572597953313438010970451791702821359948174600903762898906450769218539752179576932038647503464372949287745091343190548957294495184918233980109134667687333637174150957381311170110590611298663119346608720381555864809376823639985408428061929655589749758188735600517623065184416962717246401475499958060548500102293079833428424012509045360950799815286881632927288963525731303818089565115343422999653480631005257112087423429594307521186573728944525209994973577702631792708356582008549145429900828404552747258111803062216262864392759592705453377158260002353268421318060428131677705240445447729865528503001668518245011977952171684404570964831418548264443373731574028779232159690892198522078483660206946682587608566820825601714583843959003937561048273487585200622689646261476552683899549754541826656047414691534868959241546460451185705755052294100074124070044918676370468038183944588229977713329525230382045555686785683848113307083034984108897531782754654072841885416148336933067527347688419959693913173358015546637953751214262870495081214599113928858502008064796387040466983808866211522609592378603396195772023105525518167774929560460647450783122522690617015898985113215696105256066797355195107392374003405683969451785755407747045706382953468733452151400967432852921057933614637793501123962843426332766461212083180179476006416469481770819376276602495991896193884111245388330311993982736275591262829346119953643663203743370704637342131050278769151680281291255933805710692912626528183813928865942664135707486939713427911911660820544593850010999962110801365741506269994069989794552879414974246984705842693345005384245176360300294319150955115744308067839507959280490551436119921413654918998627318204584378447962681924591126653069219026482746075254686386941410415513051839119466251347472336239421587720480449008762414068421391773260438443895958282660118990783616693591792703527960009363976623243907103172479213734272083716642861911792471158648507350445916981987499262819841166613925430808261061213327203847511488806675099410939373764872621796091566894915009438870274701923894479197290311160505888856348318051274179021583601571762419237749252350787262544242671852798670729252588325199621722003885255547738083408648436885665338738122155400279741096799398994046272164976483389612399418098281161553032303686462335270798413921603650308716627260267540708647624425838720200136635661213356325859809536524388428850809349362183996148078013515667289514026070056898310870635052079295861179311773123182816515310709012178698552292571913454902559728444334683737756627918575912405316914629248484493304541868982340504734851126464363779450282214584655548198359797362051304953349035897843937751034206284405674142322616229129395038501812687749209289424418517250097056759206601391019628308475532003983671298452170731122341038727158637814386125548350529992881327853705496055459021168482181366711803787584609445377526175740819848113731791439160646271960716447146358100327554964235913647174055719795129250016652825707988495401914183397378314488212135620001598554169383573579297762153019593336411062443944040834377320841416442373449651043987327293712681018600296492241060115049105082431249859034037991144755257553024733698631483980317740063983829834182229938062802189652106780170791206762305888358532898242453776622396575370393488633612526249633314324318595774510276498423811695068282519097568700226853590124862543966521232303431195096099151872905362628710454681439523511336044539590743833795507493802577247200756668505475183137117273534556983178134614457656650820306870569865468726241248051757153821580999274971423781119852560685689274151062302643353296701312445086034665266530738722661433415922879491185115832411150443593936570912639692769522842692426350740961829299307793496152591457563565330360613396363843528891603977956783901461206918692183054971645018628228714594255688702924543269102918133280678228761290294962083458003536498430898388187469470183971606259269334251282936845563733717754231596180841752897412542663565640005844200605880269905686030929620104283982049833775079238270151413882378838620892317974119465914375690925381923490652333387333211952592550664422590738442071264322030117201652209862394554764340205594181009767690452854044209661404472057099013155195722827348704526074676699031049581712524142768530549067239934844812043889216025572583386592270354646464809171054696087210126848999700864203949085049090727452881626772489313496390736950660757792943944958461870440501594579525843232039927550900513928159534784371721264565069103573384220962239911014819595869013062712718879299728484862258153259950623933718111041768803822965521665684003537685116598360052972466977516475388790725039107815239532628035474097684007699599115494227096919309853270122771461078823905397471202993032310640590677104734337936482327509558697738981254373975032410572362197474602818022597016489410827404266321401759575527682636112549702975392685108344704993682150188140533704761167846247259802530233103468696280679814537640123988843902222535289882874611082936098803998272645774730439060797983686631797837429950877630521574695330943244871103756482976641025706481508162596841746220167076483240581118040383598876643301856779805298439228786950134672284788022406714601496296800903732272779171372799352487179824711416208152442342096431055812988146806769405014385937234812623801211862375063338913553728644037895353117613634719777842016467443674576485445126633776688773581153111662475971790152923746095538870053169432598401583744436880092563504460402756450353624128757617920810799624900102710588606567781319687165283987733694202757001530028158161847401152005759764913461102953798605128664526810610549072008798869704195356480981212610925405811083057727030780813525431722404118960308898679409280399387419753465809591140140609163575553978544362834627818404771928965220125305013130982869984791558732213688723110338222447895095091172971793051649380675581112647822147611286549137880907169221702438563784768706177112824469274807788799924256638125842331100845054275993256697921219333723289564402115106744061060015575783830261852980602690416992130431635802928397949792875655384742152337816668476178581197445462120844493677018941627714604230932910104855945122654818462430677741405778407055296165659572382987081839100252770336513059139943758035652990010069477640878296402989238660435934948783977988042235210004552123905341706464593645645770476379593767071522770646086149596134443446572611858327020317647501058002311610263054681091193083022618904098362465720762816264225051099031861615325338649310727810686031258270152267443207971517382651902461030163110959443281241343875169529260147461495129886551094966797142261405469465082097880284151683636682795100807237280527000747174888114088800844770676638290622140057362543955372369324513991710572644870218957443587951300436358856340570322873941495358923392434111113235477312912805833846069347875332275127754560266259079042046447833916493488062181731465715760831911816982836668501984456917856407429083288465693250741874241700616265623803593668941717030814253123147834256440012000244080570982324338899160109768780301569271780671559354470490042031964865153549487486295301945969826491696017856017759918250591146084123761742713874405451930937582812927587050761667271268271829020027186600620097216918347141730332788689061212617640188985161981699653531004129296068770534656714225077179367603287002317524324932994088671411801677654790195792366232182301444844474029250110261700293994762454321811044547943196971055723376091035042808443814870133982110566594162266077233647180847978042324185502189640708774007352588225433912048139336203009420228369457352242333697001252235872370240846697507678252451503915614570935274817649369980966710001410292916536114843667446191980228492778359421478444331543259124725885456890910342784421784013035191111103231068791327250458425915334772782703252823013662261705894413274243082295714277896527318735038794855482237606882125745934156646626864287408915033344699644600650442235390076016677327685893806449869131799266288764689621783710120656806831813188580853686654058636409846842115916209446476473938643441696248526888517568728057717373186919296512746569958837113322054319302437642746095214075790775710806058499538716181321485224835022989934604403872725282490309709367029153828005982212830356180136096069321265563731108120423644497387649291218564020984280951414817359481870831098099502004970710212979476320897773789102100836887204206050062416456937167438157237779391679884794498297232767461481938181027306476588134200604031548377499733594147166486399821588699065982484070124594219134835247545665140981110237492001072756118295431114687794185906529344638299451287813369031261973820313056621022506220057358818957793677938027938898233991057252688543601173906509190284594083615195105143857454212937466263441479865314235490108406482400153477326336760954866671943670156530368372735195289143925634448739617632775047241864563319148512940395716154091593306275043559213491399897310389206977157603855258000844707602787234619419897997213884544247600634789439113338020520698017400956164275946178827814401539211496786658775266644147754928355367756179937584662184161736007983578836850238174687362286040084738200755257786035053754003823160472494370941063483012562365160478174499983956579338275171496403244199936315336588916853260008682314176933076069644703385074037187851607256132055069564826126617964655524447398007191650582586862103902941869306979254315430164768960943103569260454733031979019718845187839972403696840874645456191947771341015902014740951062850528499962037982930395233916427181257525901456226195042098096731983666735742189394485112892211687319361937660530108653965307697102522925677137247230132427985520883527902758711342586230063335179785299256432044448387578880041264100577019788215425382182894968517423672617881052627900932086567178319386225416446453013008893663050000002667928549212493242496159496899768165094774809483396916051905741845457116376318171236687684563034060532049857742820643206055686114793622136474460656979615035648657412954582881394343521591474852264309896395316623249391284803034402296699458092983920819637023712275696590110488534971992233898276575271151266711420923881795833922290706188908914340766849859153827874264693506154229738589971544010160627816051976710158770085879523156220131612253173316506947845540716637020896108051619908399106907972429285166738428896401373200686918222032246524305329764934685429004596744189932280187656196090596522658209946313778403257693014091350298672158272969200319016880939781280696919314942468038925211977666233066762582020309274841267236619524548892784932562991959602795044903837428171727591480681906751608786390852533391423110720354260003608077082268570380948426273827769960347029463560495898956134372477191322557157231037263223515454314751818264073706661479533354078193606751091307384756210034320035955179202790606979840814410774229228155420123003975911241546737248908980549787373811045871230550507547036629972345754727239463755177971606242017551763643682214564818730189879514629478724288834690358031979085066877856614793092438232266746217500742313375233088224113485938035249075927300418116840381209314154473974979952517900998244534986557169953260374571528224624336334256402796134444440634226552142912997175361540619398622440887201145098342192605124496230460701798202328092563708419169883339579217783050929436575325059021556119553298800127196654140562480564869293375292075364978550914224972938585421881784219705787860688360209232673885572459208664412035771472214357090064010535294377661799122094244121476397378851486221348081574877961816151870070894788576340608152224274147852302855636562928281282946499506321013876691498448219397410515457583337238254197224881444529986712528931903378755446028609683886255578278635970395280266538545416154030053151172380662127954161248773237676619268528266183018150130570052070395047897430179381418489921208526298589251716078889894798895367560328735951919032132199323407727411235278269697086053353428459147846142272086400180276892254090058914730696721336100615740628570163937589755047524133847439845507787706069324657227359109488567284843916555889152937380818801732892272795394176498885107082957261652966283651091581948088980294305842159686597644647564928825605935201746685502312009882284218916631086739423407721442866840619530586546970381121259896453494979927537947432284501657432078222837075952949907757039712797310912124631271261582485032585273795182569172139630131516466606797642863337897615213005620232339264328064256956056037386340522892701727561776136217891618272627859120228054449833865018483059168097865964326960342624093336592723178889355051323315612744971886896151030103496143698212457519070722871479592704500856387092867936678319469158073921473948877828555594093504420724904592163584604370902597311430927020423895242032102480060512509740518066031312840025534037563676967409823336209028109671652772032629188054908717084134730110023455799264138267718550454423809722488434558013597785408001779655347758757713529343989495935505744154729742648508485011752629685972637599431255203351743236935450889643237603062059424653864157042717686750411285977188570070124441144126196939751222583270388388045761976711377986456351342123258364892303473752273449984490396024624590993657346433480191022790032905965981099979250797874964766700120452679755811643133139039864755533630075571694529455429312400478870919703015545814740383914990578688575167066298552394832057842257910979530702260982532882076228564136666784559898320060446281804281460264358423281825445053568122743141349453074430669211098984068471390887745668738688402771208174144068582857452189600845633069917911972293357144253176300208307516996411405680690371261742655482678253739972697762701480994471383289065329550703499487852719064687511058742495169900350998342390657387226868825826041301564071443642426272592015741493286194882256092388217885929769734237809717536073873778271945149898140259973112778393147093412386558276878907504743633590975620654210000456099568743880949632129427815025879956868945352223061340927449012268214919833601854617033443397366493877425805437105004981028945765075152884746478552926354093071617441804139103237990004350895163589851137091022809456929051222554597553113542246292387118185426260702175714625491929640427226806666721626352768242086578934687964506567862865186417974371231632203811522702079281659268366895381236289890060241188807915681683067897992268794312771826717083242147393434614060374324341670386150249994063409229911310943189598516845739497925448169081039906719524869139492873124425138789631355871952076800329445307500865922694111432105874748309814233440629375225341633582142333311985173263483150813271609851555812131670328486390464111520881991293953249249462090121340515559676969977131549190050741131562070863931679449890306137917802931518149245480531761400330932723986778299865567152233524410442452529376473867805608242777846318392910329876462640957818186490676977126402044001717829497823990061609443507971202948946725357852629643486287093976825453121747575199908284685252275974492383096781871431966273540208756347016798375301940426216947118314387000881759957478295874296874820673105392589416977961550343745149592839819676453448894794862263545759089647815467495414901700999613081342524790194109598909097391008447048705267510066344057801867330861000801056235384927106597567862603693429228107337450004271482328505750435950981612546944158511503685539808615546272822706526529489250971406659132821390338495897405476286999108817375561755953990213779457119555484374285030803805763341157078888979021839286914708045290592661662594419429048013891220011413103697903300392156599786056091119863060313819258403983860441663598635727393003578123209404190340697624514750138130762778813395155326068021808857320853842853046178509128057813324010258325462716422964105410049454225044506585528501133999046555654413527291290003967909416765260051256866142711367025563430621866537820960865073697178767011163501772692849022326895050141971938562560175717052610938166301711602218459141987604994153272162173581611471676938424359583440748926747989827418762486476983532019720548079591702279858811617231872929882794562172570178121225027485294171531930107814880626396358575770421127333997436891197448144817986004267795677933528549361175035481654611246189121398214338307641031926526845034544976472798745091022307927490768542000302512328739759272897002473770353258866591608658686286296215935462207439985785355570733509865574716964766905751804511114126221284496183080600025582292465898202369358969853780898755042571141346348975447581044531995728128457528929230224509130670659185819279348840105474290174863949599653665590967829020743520078888746952225605331193487608548664916262180647636910870164738445885470463338785231345602058963684428534002410742709336008188655952350444768227667856998506427749244378223094192678580338796677071399112021544513871855808501288772809813328050317695075064745760598113888915329832299315931253212270865719410939780611140465181659074351215826057175302021897668654431814235580375600979560299630055041890724041644120632371358655698231179192751156160177972623415242684972589759003483786055566253143007729308990283097576394679197438622240450736249214029057644525641994793240895865918154082997870546833649140852358832510365674551995636847103245940575598658435521147186398818945891286127508808044644788419907255568196318327266494709702060222495902880669170958733917011704246892285013625057848660015767595530955750618154138616067334275542191725861993453821853647710360156846131636837920584313941390484177014275131538342713094007227471704631261711594510238296138618890077315226991627570875631396452942918616211771390303512241876460466168570063096270571390096645735756791793381302567400712652823001044016954218419406006147716376961842039600694899330085186928321954922433132351694772121974562664628041095205867995415523972893815206734933473731295190395402112663587915276004068281749277588396659204783406295147432654425422056420084486452486347431370015021563137083093845010623305717211348567419391183684686373820593027347415859741992186893028175468687174831541235739017576150163803973501096930869335419019938376855414946351681986778112630540374393297995549308866365577977739645086222169085483025887111357102833086242246201540742362592751188989608030685365901443148170484046670655288974437198997755291815111431918007327072646625543464029706934539284603387445736345128580739540111231773968093407798344952088886573682036786361811746501546225803015653855762734031655800139638173229634767542388312630790264742628590639065659017275753186318440896461589174679431744202281653926331022406064970686202492610126017442964550997557734989490363397690718198841538831774873600859034610032054111121436581338328307961043850624175893662841645410094510479208794521830300325844643184040447908942265775458621212265284780959641220766647057465660997142116116968752063247867327937944158636861081031438253632096708335192489534186919873895688400087813147266039116523360725847554182233941041157351529045410084352940820092015327194621058424603662534380965804307770601689272213050595388283281706573771541958242720989849089378158421872748964230951542165056946789935109889723973548178512392914286406076267307224202941602325984128685743495117572638024085259526577017290977835249974243041724494060966576994272068206016943055046450529926155551750055206581532425930524039450052438267186321164597378690879160932361737982768327058418591590607917673371381206544351586815364983292285760552436519761863206078959878381046976146636445144881022619845866884308851070171660288146628582633371661818289066846496173636012646601412088541901511690916871958217277051025664984501179856983625003417048138835741049891140336303211920922376797417421969654165344321795897048071573766623085075432151934249877323541160558138709673763581776989880294129558297806817169063711692681074948860173773758018286375245604319541220888116482969485162176348738655840358771791449321884574523038151373850715320751269227917549858576131786134339103472218575371850955667915763174817754448507925616155680018231095906022444112448741358644911533283097095987165433424397409216600289746694332067584574851272995586803738271328601971533927619072126341628945263336760050642969244196679375067492462885864154403637754193063948420652061968212114518477451086228773675110815943281810278655540348789871914137959434399072600617965544076089154061123102414124485603751230766437500317926931789794098270251193184615485360672661609626292125860956533390378598339658819029311937253647046129984585550138161728727215101590261658446949632657080002652184766522849422817988955244017482147169095241722117100886938910946880837408462490024880042675817974731008699347262247423961234064659921580909164768193628432767229939811719934781842305420182986301025866157176856159854825636071158968363217455527507483996016942758149172182271474207440687815626543994602221650228880753089888538579261724010554334084862355064455867715854134361908077319728021431326941032386851245504667974138747384594676160727803114046257022850172252542064727421946475021537439048084002713579418836022172811645105271312579025066359433067440420822941205604565710812063475747415580258615418593848760675162568203290430634877004385088259654587235537744126453322465324914596188103356434332158941650092151845067332862310637649967252761696153974700889731528422712856194710429156717788700948019018919500973473960789229160142346548115298056191647397173428368813572159264686053280232244539033810727004784567573946528278145499264672714539564209367645681803428774569894907420438484623015085647861689893617649381741324749767753591049760228411248880500687227189913787396483339408120155879274935118942578653781940974516800379672137440433442773629047186714470141809796239778010043782972367944698278278643436855838329283429566506428002910436474832854355877123668388361622108655607555314289867765747420318124934068593136737387467956348485216791034368353226035484289079350580437875069440578847227195880733982438244816903068753311978916988968100551180821867418539791436743587029389655861228930569248005697150632794040241628508281481428388238370566818930372620951535429731635086139787402313188926900920140147933711098035198149475165297489830381606028220358405045022611033858524197673195367948879334275019043378829533680108788036126523937084617853762813141531525827863971512477748276983750660676068484491462838809870030221295416707007183296757226159241292886037052296745285295996203243833256389488849900143545254451287797586321723728189598474800345103912763961425522520096628939280293459711339614306688537668206060197497004854897734796361174472027730930847120698783552502193278746962307619782077225484432852624652742843284663481837348696934186098447787170202513063845441296137694914370847731481384194415368533636261280747041884245069893041482636628156631048571165728663240949770120148993874069863114446155283997238669722696024300876023121454454780693328307481124616230125889568090455370104961660103898929941315838281932248387210123250294765386082943253655175933014577579202792383940536738649935847021435514928282564158176109623790231362952551826715602413388939801190172514430051591061078544742471853665029129594142195711764413828559307334234747411224990018071279568684711350324672456687730327930431809143555311498227432518182363355699749800296954381900288280325337330285373940379082831330954611637982099427515746652962635169046322467732832091676174008453566030279276854477259192133085815610136175810499273891257727609560997639915683640890000402009530365689785082019939708622328902139791647158225055912632959508780597960512881744759302737698003140706422515358479741534776002672241199717825637697810429410496280766076449137653777974433353822810096281664973939270276726529615469411101047202431529788533967525203967328334342129646458139411002723407856394206802133894634655029192492885876184286769889913277403711580043224303195315708270797198731260782612343324476120210782293453814595779152358072712076237438843121056780652093540771354962152292803120409955377708012054317585723516041369531120440182863985354495486121987068911318708679638673524876578196300082253900325524709137033533063952414277618383756730360622538778650480523648570749529560972403228366728922688995623025557355428873249937112877249648129555103148168412006654936156654981227172862352064916441396717575093031398761782739912632682063540020199800703427693656983651259703955413686466534612018350916297058253156173144605916819823703253846430885695026313697756922732338474982041134695335391469160243934958225484838287059506645721964372142348164798997907822919212980725292293002890662236448775394221117646535928573315745702857073122115017184013310546642827282787095619691646537448465919793237356504287513864449291476004118085126428367980029798947754876379194288434334905236478081425930781323785298791170318987173744435280627621844605748822910744064534977674162580236394224817469010776425445659718165920685603840829807564569797285042783128905099274912305018440686961068404634349607864251910457110502178082476756169167596486245642265819510787278532226258664325956154008733992152230748001831624768985202713745685356390480200708046235550343108509462459941443316883960489419391329657118333217189713065985528437993555334305083388376864244839040465002899347152788315980198857897513745867288682893533523699005885581508759912127047261999289424667402622980889195141325967975050125803725863874056065396958579445121339011891200098819229194369574703971030482729438928014223624013859459825626284009286424395267541566174030179397103934014711683325462996088208691712444973091468285946300535358865611306762921130498965089341134315861603630003441591381277771803519399372183507130452700199782266311390258464480912344542645004386281937968177682801133348076553837425676229220590705710978475719045228116575108886790263844754875837652121202260294853541580882263364778040584719220726195735444305281151164382223579059867590000794878808765884288386930213903905946964070521923247195032467173278086387384655743697642456202205775402919288008659170621370210002152888477160854945914799421754128760736704017768051443605450342619051842339862371796819728630390646258455077428329494539473487273304026280494691500187034533802022292779047406645519768515269946366674699528057527820184799976760820137609641462297277898876863853337766574334067281571495069103962027475591175739569569437263247652501869402921083026888227480007056555928369650645226446262886500936457697801319376729863040538133365903531812851773795879220232004164175645838948837728757486505109878394257206540497193849242875826678652479610429238200011268477932699407187752710447779431312972702849693171453351724601936621181551646958094042192895209958908815429749564723412916526588063099516253725200189655415708092333519055792866597026811243001846497014639044041974267328907032939518448655203603754358882076241825924481514554631773937367283705541621257337032122348962043042184658973771497667478373458666395227550054026340348023608813989843798032677495151801171353216910693109385005610215407847296962173932134252272293678135292638267956096671942845442277954893885081262612812619533918898381084335998242292855973081892853435067657713840706896849828600146988204113900279078154581731844234871043714453491973257160666619942708889149001310816876984164967361515632942121414450679345626985035339847713428331144644207246053441840522137050829113102489897838536356065392342725887931039166485552856252385132314266650087641686599375942350074313180729983956303019196099541605157094600670474198662295439048729170520404580947275568173164573230828916002163757380811427346908444986078577965975183805782344947190742123658993726216852061565983164525435397010948796934688931008494557214806075868243510799672256577089176390766678442781023076315252231837625418973362346330443427384530868173811936353895605793462649454850139933098252223640466237156298430057489948462510914046866561294258947591952573131496193479370705270850633055491501904649828787406600839396161437739681804227425550938281720747996880956548571398786450825610974995640427403431975943036938977625817266278532527719451253735915641390679809517890169030071945428285400144227946231362326850784205635245286044558655811212654847435908525062411704898943968216537291857569930039307097870449245757494083238172084845161931494570425332228665058144388075354372766609830262842066737971139656921257479239535855474988637579818147763793133473814223517005256637427233582358648364359141900492565705372759727779329106953284879999854505707035845196437014484057368536621728653643700115785033268291310707186224698820648492367193032444520534744890777581440186606746491166944171372290405278535270307394904318654306319386559315782567083954194891915127891199956641549437854913709370600203039482745672575274136920735206764541026608853203909149156648031126704308473134652240871437719229687870182383763417071762624462666128619850857678610656328134518107012138879921544406863988044412655330587562417712370952774649233356783867476065497799669008828439716494397683068332465476684046385643060718534707888804237912733505947765352131352418567256798311439706088171993730089143578592680331618272049933956113722064365014257091678101645642233990755548271622120097376766025200939303504152914017475839885011782520120642233702900375253536982578993008723078926365485422167839387621082588134698359306884507246692742335343047441935843988282122620807627767354934943243831547939798162656505085588968100027340715545509063075478964925854760729641448442105951418789811257658032912062911814459754343650004299065427060414117277337113467905265100191392032126303601022227567080957849048218089937545497651096779919214369128460136782024600617465091297033521217550904509943337868740090292522855044950788912410316631247382380834873345390024927175370776716792713893726859019800125940670357824486311829473291138722839725810772771788119040336869907517940467460553021161187439761829973701235057962896409376863521690643352048355163149374419958661920125490390476891696594800225625988925811952112681546885768652295001104338503688050831484119191107063582492548833464357030077801588812107288574047260079447724301655904207703183414571675463908232750702214242719367015603182278199878270252843612331352285759465178805010214543555098470309449131981626125403584789845672752146726350360228165006257653712339040471741904153833800665666482166603998495017250199151702921526992826109029350158464551207628420672264088690619776587002625151152784503652105008269796348206268324545318974298453671143311345344870960926819529856363719763201327752344820074038933539559606672367788407221114593567797826910809302898967141583819873024295365536903245313993660455233880097808560859255850055826774175991726854339288344801119985425765516596241058119531301429590687801466156620617535346630020706714857667630412786665084687862753095228280233505931635630455662415797234043230159618560013323258211949124899604724564576775803915450457774529034217648903726841776775780029826222777339532572118105465000128913823656631672966446700919902744442549359175221564386389692726509608782361857793940820958576222191703762302356656769827806839694015292143411285718462178336283046179651447000670391588555210041463060970955632014042408111625072538079041163619282898738603952868894367389474472468075903521717244292207940176840779183509739341801518710483713182257342867637466718765322124578890564830004110367753950998960265319651662078490800270033156303960895598320141444484873138202844907477940245637293281929212086651835622369373185025161594635827820986465108332023255936587664965913127551817751172253697826511334807505383152742978512598189770064123660333234361866161691517003864948571774553024911338841080479896375169668871404090881757196016403195536949497333556279667253830045298520913904500104046811927877754534102131969126781103699033929190146550972842692906493333318994435035698141680227813401493491443166282507350697937239541220906033049381063993762210491669791629022255494169418573656708081448767315781444584995757917197499050357416369836348351413242487813608048381091751176785751558277341595629020160164174076006107571474901519593906268265804781672387482643202718427898941860519984559410864568005220163412660608335949613033923945893944503463536738447713496361805385988986722100927485476283589496446971173679186922586502904014007771417485553797880222318196661962784617613801568184702607431131421633585705901968771166121377197544197847438825658300749439001826554074888525539299401235110548976108342494233564595262244808488681382814837907580859989056713903180575664217577041100411705420154628975874211947642608485179351725513799803998065915205737177171615011404324586176813374724282851190990831104895580691070320718047266984911885636777836989880344977452986605779459520765681305520819044689491535348003789417833979949259711277012296586615711218440466852713379105087668399399093370083008220251964960275225704919141022118399227538125048958875060903880019377479760940292930454387489358813474033854016834977695649497624176160208341374055842474237175471592615446789585574755397987848824257825276260339371925104361779426650069151035512943822825885931871489890564311358456737896485622588329366506668674907981126805053320135004617408747808169959445451022320918362533350625655778116914109820301081780616345583608813436849094241814078527356151092720512657975036843348946572738453817591755519965589831616632862132730919272237683664720685262271594949447843927552225002316125792568195558611204478820422201095636178827383497657599848200137953128355856124743877247443977816675348597042514549194295298209905601692037138424759963948513241508900081857989968917807360850046022937537971276819489919734103168311467931412390681319036972009692333682239067528784781679525250504370763174400151962331520974267508308712826244186105673751770231770860380665860097848123546106552391300950925345560737972349037024579500926295458244019182100402179844376288634897449361736444312178767464458189862163326024488016769178193808349316902712199491681457076997472274210096261488565212706689693771223525976623819137037598467437445271614358968342062295568847966947107975627900116821156779092090990290899176796624162752534863098640771253838210123661782934329290505982550868733977877497815937967511667035692108950815908611197026536675045186486670718062794991311593509731695919714227017145826975917727832754183335224973779864144116832555098932479394061598558698138870051094951971052612329408839828342278195616032437082098877391397760119891549910173019157739797422935079719918674307641363589353846601194341063475339371990048837924068452056790699820900597804345431195157725404863208491289610142640095928027363747732412372129157380673162589439661805762446750687633312123718528300931635729776642986399215858840951552772062413199418792636131705029676707140586184524797146083617574241515037699095192569360511296124571431319824775257329158864446698445155077326729146275146779738554527499690172125L

In [9]:
#Ok, that's a lot of possible triangulations! For a square there should only be two possibilities:
triangulation_count_square = catalan_number(4)
triangulation_count_square


Out[9]:
2

So, that looks sensible (though the derivation is non-trivial for me!) and could be a useful i.e., unit test in some circumstances. It is not so simple to deal with the possible triangulations of non-convex polygons, but convex polygons have the most triangulations, so the total permutations are always between 1 and the catalan number--for any polygon.

The problem posed by Victor Klee in 1973: what is the fewest number of (stationary) guards (capable of 360 degree vision) placed anywhere inside a polygon that would have sufficient line of sight coverage to cover the entire polygon interior?

Many versions of the posed problem are NP-hard! It has been firmly established that floor($\frac{n}{3}$) guards are always sufficient and sometimes necessary to guard a polygon with n vertices. Protecting the exterior (the Fortress Problem) is more difficult--ceiling($\frac{n}{2}$) guards are always sufficient and sometimes necessary to cover the exterior of any given polygon.

Examples


In [10]:
#A Pentagon
max_guards_pentagon = math.floor(5./3.)
max_fortress_pentagon = math.ceil(5./2.)
max_guards_pentagon, max_fortress_pentagon


Out[10]:
(1.0, 3.0)

In [11]:
pentagon_coords = np.array([[0,0],#bottom left
                            [0,0.3],#top left
                            [1.0,0.5],#top
                            [2.0,0.2],#right
                            [1.7,0.0]]) #bottom right
fig_2_5 = plt.figure()
ax = fig_2_5.add_subplot(121)
p = Polygon(pentagon_coords, color='orange', alpha = 0.3)
ax.text(1.0,0.2,'G1')
closed_polygon_coords = np.zeros((6,2))
closed_polygon_coords[:-1,...] = pentagon_coords
closed_polygon_coords[-1, ...] = pentagon_coords[0,...]
ax.add_patch(p)
ax.set_title('Single Guard Covers Pentagon Interior')
ax2 = fig_2_5.add_subplot(122)

for axis in [ax,ax2]:
    axis.set_xlim(-0.2,2.1)
    axis.set_ylim(-1,1.3)
    axis.plot(closed_polygon_coords[...,0], closed_polygon_coords[...,1], c = 'black')
    axis.scatter(pentagon_coords[...,0], pentagon_coords[...,1], c = 'black', s=80)

ax2.set_title('Three Guards Cover Pentagon Exterior')
ax2.text(-0.17,0.4, 'G1')
ax2.text(1.75,-0.2, 'G2')
ax2.text(1.6,0.5, 'G3')

#draw the lines of sight for the the guards using triangles
g1_t1 = Polygon(np.concatenate((np.array([[-0.15,0.38]]),pentagon_coords[0:2])), color='blue', alpha = 0.3)
g1_t2 = Polygon(np.concatenate((np.array([[-0.15,0.38]]),pentagon_coords[1:3])), color='blue', alpha = 0.3)
ax2.add_patch(g1_t1)
ax2.add_patch(g1_t2)
g2_t1 = Polygon(np.concatenate((np.array([[1.75,-0.2]]),np.array([closed_polygon_coords[-2]]),np.array([closed_polygon_coords[0]]))), color='violet', alpha = 0.3)
g2_t2 = Polygon(np.concatenate((np.array([[1.75,-0.2]]),np.array([closed_polygon_coords[-2]]),np.array([closed_polygon_coords[-3]]))), color='violet', alpha = 0.3)
ax2.add_patch(g2_t1)
ax2.add_patch(g2_t2)
g3_t1 = Polygon(np.concatenate((np.array([[1.6,0.5]]),np.array([closed_polygon_coords[-3]]),np.array([closed_polygon_coords[-4]]))), color='brown', alpha = 0.3)
ax2.add_patch(g3_t1)

fig_2_5.set_size_inches(17, 8.5)


In the above case the upper bounds for guarding the interior and exterior of the pentagon also appear to be the optimal solutions. It is useful to have these boundaries to assess i.e., if the number of guards you have suggested for a given polygon is within the theoretical upper limit.

However, given an arbitrary polygon and a suggested number of guards, determining if said polygon can be covered by that number of guards is generally NP-hard. As a simple example of the dissociation of the number of sufficient guards (no more than this number are ever required for a given polygon of n vertices) and the minimum number required to cover a given polygon, let's consider hexagons.


In [12]:
#Hexagon Art Gallery
max_guards_hexagon = math.floor(6./3.)
max_guards_hexagon


Out[12]:
2.0

In [13]:
#demonstrate hexagons that require 1 or 2 guards with triangulation overlays
@interact(Fisk1=False,
          Fisk2=False)
def hexagon_demonstration(Fisk1=False,Fisk2=False):
    hexagon_1 = np.array([[0,1],#top left
                         [1,2],#top
                         [2,1],#top right
                         [2,0],#bottom right
                         [1,-1],#bottom
                         [0,0]])#bottom left
    fig_hex = plt.figure()
    fig_hex.set_size_inches(17,8.5)
    ax1 = fig_hex.add_subplot(121)
    ax1.scatter(hexagon_1[...,0], hexagon_1[...,1],c='black',s=80)
    closed_polygon_coords = np.zeros((7,2))
    closed_polygon_coords[:-1,...] = hexagon_1
    closed_polygon_coords[-1, ...] = hexagon_1[0,...]
    ax1.plot(closed_polygon_coords[...,0], closed_polygon_coords[...,1], c='black')
    ax1.set_title('Single Guard Covers Hexagon Interior')

    hexagon_2 = np.array([[0,0],#bottom left
                          [0.25,1],#top left
                          [0.5,0.5], #left
                          [1.5,0.5], #right
                          [1.75,1],#top right
                          [2,0]]) #bottom right
    
    ax2 = fig_hex.add_subplot(122)
    ax2.scatter(hexagon_2[...,0], hexagon_2[...,1],c='black',s=80)
    ax2.set_title('Two Guards Needed to Cover Interior of Hexagon')
    closed_polygon_coords = np.zeros((7,2))
    closed_polygon_coords[:-1,...] = hexagon_2
    closed_polygon_coords[-1, ...] = hexagon_2[0,...]
    ax2.plot(closed_polygon_coords[...,0], closed_polygon_coords[...,1], c='black')
    
    if Fisk1:
        #constrained triangulation of the first hexagon:
        segment_start_indices = np.arange(6)
        segment_end_indices = segment_start_indices + 1
        segment_end_indices[-1] = 0
        segment_indices = np.array(zip(segment_start_indices, segment_end_indices))
        hexagon_1_vertex_dict = dict(vertices = hexagon_1, segments = segment_indices)
        tri = triangle.triangulate(hexagon_1_vertex_dict, 'p') 
        simplex_coords = hexagon_1[tri['triangles']]
        triangles = PolyCollection((simplex_coords), alpha = 0.1)
        ax1.add_collection(triangles)
        #use colour scheme from Steve Fisk's proof of the floor(n/3) limit
        colours = ['red','blue','green','blue','red','green'] 
        for vertex in hexagon_1:
            ax1.scatter(vertex[0], vertex[1], color=colours.pop(0), s=81)

    if Fisk2:
        #constrained triangulation of the second hexagon:
        segment_start_indices = np.arange(6)
        segment_end_indices = segment_start_indices + 1
        segment_end_indices[-1] = 0
        segment_indices = np.array(zip(segment_start_indices, segment_end_indices))
        hexagon_2_vertex_dict = dict(vertices = hexagon_2, segments = segment_indices)
        tri = triangle.triangulate(hexagon_2_vertex_dict, 'p') 
        simplex_coords = hexagon_2[tri['triangles']]
        triangles = PolyCollection((simplex_coords), alpha = 0.1)
        ax2.add_collection(triangles)
        #use colour scheme from Steve Fisk's proof of the floor(n/3) limit
        colours = ['red','green','blue','red','blue','green'] 
        for vertex in hexagon_2:
            ax2.scatter(vertex[0], vertex[1], color=colours.pop(0), s=81)


The minimum count of the colours is the sufficient number of guards to guarantee internal coverage, but less guards can be used in some cases.

Once again, the 3D case is more complicated because not all polyhedra can be tetrahedralized. Indeed, even placing a guard at every single vertex of the polyhedron does not necessarily guarantee full coverage.

3) Convex Hulls (and transition to point sets)

3.1 Convexity, Convex Hulls and General Position

Convex Regions: a region is convex if any two points of the region are visible to one another within the region. The convex hull is the smallest convex region containing the point set S.

Convex Hull by Example -- Back to Kindergarden Geoboards

Calculate it yourself with the incredibly-useful scipy.spatial library


In [14]:
#a simple example of the convex hull for a random set of points in the plane
random_2D_array = np.random.random_sample((35,2))
hull = scipy.spatial.ConvexHull(random_2D_array)
sample_hull_plot = scipy.spatial.convex_hull_plot_2d(hull)


Prerequisites: General Position

A set of points (or other geometric objects) are said to be in general position if they avoid troublesome configurations, known as degenerate situations. The precise definition of general position varies depending on the algorithm. A point set is always in general position if the n points are chosen randomly, but this is (of course) not the case with real world data.


In [15]:
#produce an example of a degenerate point set:
fig_degenerate = plt.figure()
degenerate_vertices = np.array([[0,0],
                                [1,0],
                                [2,0],
                                [3,0],
                                [3,1],
                                [2,1],
                                [1,1],
                                [1.5,0.6],
                                [0.4,0.3]])
ax = fig_degenerate.add_subplot(111)
ax.scatter(degenerate_vertices[...,0], degenerate_vertices[...,1], c='k', s=70)
ax.set_title('A degenerate point set')


Out[15]:
<matplotlib.text.Text at 0x115cdb810>

In [16]:
#try calculating the convex hull of the above point set
hull = scipy.spatial.ConvexHull(degenerate_vertices, qhull_options='Qc')
plot = scipy.spatial.convex_hull_plot_2d(hull)



In [17]:
hull.coplanar #the above computation is possible because Qhull is able to ignore a subset of the pathological input points (their indices and neighbour indices are only computed with the additional 'Qc' option sent to Qhull)


Out[17]:
array([[2, 0, 3],
       [1, 0, 0],
       [5, 3, 6]], dtype=int32)

In this case the algorithm actually handles the potentially pathological collinear data, but it is important to note that this kind of data could typically cause issues.

3.2 Practical Algorithm Time & Space Complexities

Convex Hull algorithms are fundamental to computational geometry and it is useful to have a working knowledge of the terminology used to describe their performance.

Complexity analysis captures the speed of an algorithm as a function of data input size using Big-Oh notation.

For a specific algorithm and an input of size $n$, the running time is captured as $O(f(n))$, and $cf(n)$ is the upper bound on the running time of the algorithm, where $c>0$ is a constant. The upper bound means that we typically ignore lower values of $n$ and focus on the asymptotic 'worst-case' scenarios.

Selected Examples:

big-Oh notation name Example
$O(1)$ Constant Adding two numbers
$O(n \textrm{ log } n)$ loglinear Sorting a list
$O(n^2)$ Quadratic Incremental convex hull algorithm
$O(n^k)$ Polynomial Robot Arm Motion Planning
$O(c^n)$ Exponential Some Brute Force Algorithms

We focus on time complexity, but there are also cases where memory usage (space complexity) is critical for an algorithm.


In [18]:
#we will use an empirical approach, although an expert could study the algorithm / source code and identify the 'rate-limiting' step based on the type of operations performed

def linear(n, m, c):
    return m * n + c

def loglinear(n, m, c):
    return m * (n * np.log(n)) + c

def quadratic(n, m, c):
    return m * (n ** 2) + c

points_list = [1000,20000,30000,50000,70000,100000,200000,300000,500000,700000,900000,1000000]
list_times = []

for num_points in points_list:
    random_2D_points = np.random.random_sample((num_points,2))
    start_time = time.time()
    hull = scipy.spatial.ConvexHull(random_2D_points)
    elapsed_time = time.time() - start_time
    list_times.append(elapsed_time)
    print 'benchmarked', num_points, 'points in:', elapsed_time, ' seconds'


benchmarked 1000 points in: 0.000365018844604  seconds
benchmarked 20000 points in: 0.00290107727051  seconds
benchmarked 30000 points in: 0.00400900840759  seconds
benchmarked 50000 points in: 0.00651001930237  seconds
benchmarked 70000 points in: 0.00921893119812  seconds
benchmarked 100000 points in: 0.0138518810272  seconds
benchmarked 200000 points in: 0.0259821414948  seconds
benchmarked 300000 points in: 0.0386691093445  seconds
benchmarked 500000 points in: 0.0647809505463  seconds
benchmarked 700000 points in: 0.0915629863739  seconds
benchmarked 900000 points in: 0.114660024643  seconds
benchmarked 1000000 points in: 0.130273103714  seconds

In [19]:
popt, pcov = scipy.optimize.curve_fit(linear, points_list, list_times)
linear_y_data = linear(np.array(points_list), popt[0], popt[1])
popt, pcov = scipy.optimize.curve_fit(loglinear, points_list, list_times)
loglinear_y_data = loglinear(np.array(points_list), popt[0], popt[1])
popt, pcov = scipy.optimize.curve_fit(quadratic, points_list, list_times)
quadratic_y_data = quadratic(np.array(points_list), popt[0], popt[1])

fig_bench_hull = plt.figure()
ax = fig_bench_hull.add_subplot(111)
ax.scatter(points_list, list_times, c='k', label='original data', s = 80)
ax.plot(points_list, list_times, c='k', label='original data')

ax.plot(points_list, linear_y_data, c = 'blue', lw=7,alpha = 0.3, label = 'linear')
ax.plot(points_list, loglinear_y_data, c = 'red', lw=7,alpha = 0.3, label = 'loglinear')
ax.plot(points_list, quadratic_y_data, c = 'green', lw=7,alpha = 0.3, label = 'quadratic')
ax.legend(loc=2)

ax.set_title('Crude Time Complexity Assessment\nFor Qhull 2D Convex Hull')
ax.set_xlim(-50,1.2e+6)
ax.set_ylim(-0.02,0.20)
ax.set_xlabel('# Points',fontsize=16)
ax.set_ylabel('Time (s)', fontsize=16)
fig_bench_hull.set_size_inches(8,8)


Qhull implements the Quickhull algorithm for convex hull [Barber et al. '96]. It has output-sensitive performance that can slightly improve on loglinear in some cases.

While the above Qhull algorithm is highly specialized / optimized and works well across several dimensions, a more straightforward summary of general Convex Hull algorithm performance is shown below:

Paradigm 2D Complexity 3D Complexity Notes
Incremental $O(n^2)$ $O(n^2)$ often implemented because of conceptual simplicity
Gift-wrapping $O(nh)$ $O(nf)$
Graham scan $O(n\:{\log}\:n)$ N/A
divide-and-conquer (recursion) $O(n\:{\log}\:n)$ $O(n\:{\log}\:n)$ 4D and up: $\Omega(n^{d/2})$

3.3.1 Can the Python community do better than scipy.spatial.ConvexHull?

NO, not from an algorithmic standpoint, because loglinear performance is the lower bound on sorting operations and we can reduce the convex hull problem (in the simplest case) to a sorting problem for a parabola:


In [20]:
x = np.arange(12)
y = x ** 2
fig_hull = plt.figure()
ax = fig_hull.add_subplot(111)
ax.scatter(x,y,s=80,c ='k')
closed_parabola = np.zeros((x.shape[0] + 1,2))
closed_parabola[:-1,0] = x
closed_parabola[:-1,1] = y
closed_parabola[:-1] = np.array(x[0],y[0])
parabola = np.array(zip(x,y))
p = Polygon(parabola, alpha = 0.2)
ax.add_patch(p)


Out[20]:
<matplotlib.patches.Polygon at 0x113c0f650>

It turns out that, even if we didn't care about connectivity (i.e., only wanted the hull points without their order), the fastest possible performance is still loglinear. This was discovered relatively recently (1985) by Franco Preparata and Michael Shamos.

3.4 Practical Examples with scipy.spatial.ConvexHull

3.4.1 2D

Problem: Let's say you have a startup that built a long-distance robot that you'd like to send along land from the far West of Oregon to its Eastern Border. However, a sufficient number of Oregon residents have objected to this project such that a straight line through the State will not be possible. Instead, you decide to treat the entire area of Oregon as hazardous for the robot, planning to travel on the borders (or in the more permissive neibhouring States) only. Assuming flat terrain, find the minimum total distance that may be traveled by the robot from the Westernmost point of Oregon to its Easternmost point.


In [21]:
#determine the indices of the West / East limit points (which also fall on the convex hull, by definition):
x_min_index = np.argmin(Oregon_vertices[...,0])
x_max_index = np.argmax(Oregon_vertices[...,0])
x_min_index, x_max_index


Out[21]:
(0, 137)

In [22]:
#confirm positions of the West/ East limits in the context of the state and its convex hull
x_min_coord = Oregon_vertices[0]
x_max_coord = Oregon_vertices[137]
fig_Oregon_limits = plt.figure()
ax = fig_Oregon_limits.add_subplot(111)
ax.scatter(x_min_coord[0], x_min_coord[1], c='red', s = 300)
ax.scatter(x_max_coord[0], x_max_coord[1], c='red', s = 300)

hull = scipy.spatial.ConvexHull(Oregon_vertices)
hull_plot = scipy.spatial.convex_hull_plot_2d(hull, ax)



In [23]:
#plot and assess travel distance for the 2 possible border paths and 2 possible Convex Hull paths:
fig_Oregon_paths = plt.figure()
fig_Oregon_paths.set_size_inches(10,10)

ax_border_1 = fig_Oregon_paths.add_subplot(221)
ax_border_1.plot(Oregon_vertices[:138, 0], Oregon_vertices[:138, 1])
dist_1 = np.diag(scipy.spatial.distance_matrix(Oregon_vertices[:137],Oregon_vertices[1:138])).sum()
ax_border_1.set_title('Border path 1\n D={dist}'.format(dist=dist_1))

ax_border_2 = fig_Oregon_paths.add_subplot(222)
cycled_array = np.concatenate((Oregon_vertices,np.array([Oregon_vertices[0,...]])))
ax_border_2.plot(cycled_array[137:, 0], cycled_array[137:, 1])
dist_2 = np.diag(scipy.spatial.distance_matrix(cycled_array[137:-1],cycled_array[138:])).sum()
ax_border_2.set_title('Border path 2\n D={dist}'.format(dist=dist_2))

#note: in 2D scipy returns hull coords in CCW order
hull_coords = hull.points[hull.vertices]
hull_min_index = np.argmin(hull_coords[...,0])
hull_max_index = np.argmax(hull_coords[...,0])
ax_border_3 = fig_Oregon_paths.add_subplot(223)
ax_border_3.plot(hull_coords[hull_max_index:hull_min_index + 1,0],hull_coords[hull_max_index:hull_min_index + 1,1])
dist_3 = np.diag(scipy.spatial.distance_matrix(hull_coords[hull_max_index:hull_min_index],hull_coords[hull_max_index + 1:hull_min_index + 1])).sum()
ax_border_3.set_title('Hull path 1\n D={dist}'.format(dist=dist_3))

ax_border_4 = fig_Oregon_paths.add_subplot(224)
cycled_hull_coords = np.concatenate((hull_coords,hull_coords))
ax_border_4.plot(cycled_hull_coords[hull_min_index:hull_coords.shape[0] + 2,0], cycled_hull_coords[hull_min_index:hull_coords.shape[0] + 2,1])
dist_4 = np.diag(scipy.spatial.distance_matrix(cycled_hull_coords[hull_min_index:hull_coords.shape[0] + 1],cycled_hull_coords[hull_min_index + 1:hull_coords.shape[0] + 2])).sum()
ax_border_4.set_title('Hull path 2\n D={dist}'.format(dist=dist_4))

for axis in [ax_border_1, ax_border_2, ax_border_3,ax_border_4]:
    axis.scatter(x_min_coord[0], x_min_coord[1], c='red', s = 300, alpha = 0.3)
    axis.scatter(x_max_coord[0], x_max_coord[1], c='red', s = 300, alpha = 0.3)
    axis.set_xlim(-128,-112)
    axis.set_ylim(40,48)


So, clockwise Hull Path 1 is the shortest.

3.4.2 3D

Problem: Estimate the surface area of a spherical influenza A virus based on my simulation coordinates.


In [24]:
#load and plot the data:
fig_flu = plt.figure()
fig_flu.set_size_inches(7,7)
flu_coords = pickle.load(open('flu_coords.p','rb'))
ax = fig_flu.add_subplot(111,projection = '3d')
ax.scatter(flu_coords[...,0], flu_coords[...,1], flu_coords[...,2])
ax.set_xlabel('x ($\AA$)')
ax.set_ylabel('y ($\AA$)')
ax.set_zlabel('z ($\AA$)')
ax.set_xlim3d(400,1200)
ax.set_ylim3d(400,1200)
ax.set_zlim3d(0,800)
ax.set_title('Flu Envelope Coordinates')


Out[24]:
<matplotlib.text.Text at 0x116855ad0>

In [25]:
#calculate the 3D convex hull, plot the facets (triangles) of the hull, and sum together their areas to estimate the overall area of the viral surface
fig_flu_hull = plt.figure()
ax = fig_flu_hull.add_subplot(111, projection = '3d')
flu_hull = scipy.spatial.ConvexHull(flu_coords)
hull_triangle_coords = flu_hull.points[flu_hull.simplices]
flu_triangles = Poly3DCollection(hull_triangle_coords, alpha = 0.1)
ax.add_collection3d(flu_triangles)
ax.set_xlabel('x ($\AA$)')
ax.set_ylabel('y ($\AA$)')
ax.set_zlabel('z ($\AA$)')
ax.set_xlim3d(400,1200)
ax.set_ylim3d(400,1200)
ax.set_zlim3d(0,800)
fig_flu_hull.set_size_inches(7,7)
ax.set_title('Flu Convex Hull')

print hull_triangle_coords.shape
print 'surface area estimate from Convex Hull:', flu_hull.area

#compare again surface area of roughly equivalent sphere
crude_radius = (flu_coords[...,2].max() - flu_coords[...,2].min()) / 2.
sphere_SA = 4. * math.pi * (crude_radius ** 2)
print 'surface area of roughly equivalent sphere:', sphere_SA
print '% reconstitution of sphere:', flu_hull.area / sphere_SA * 100


(1404, 3, 3)
surface area estimate from Convex Hull: 1103521.423
surface area of roughly equivalent sphere: 1134557.42184
% reconstitution of sphere: 97.2644840849

Considering that flu isn't a perfect sphere, that % reconstitution of SA is an excellent indication that the calcluation was an excellent estimate.

4) Triangulations

Triangulating a point set can be quite different from triangulating a polygon.

4.1 Definition and Triangle-Splitting Algorithm

A triangulation of a planar point set S is a subdivision of the plane determined by a maximal set of noncrossing edges whose vertex set is S.

Triangle Splitting Algorithm:

  1. Find the Convex Hull of S
  2. Triangulate the hull as a polygon
  3. Choose an interior point and draw edges to the three vertices of the triangle that contains it
  4. Repeat step 3 until the interior points are exhausted

In [26]:
#demonstration
import triangle

def triangle_splitting(point_set):
    fig_splitting_progress = plt.figure()
    fig_splitting_progress.set_size_inches(16,4)
    ax_step_1 = fig_splitting_progress.add_subplot(121)
    
    hull = scipy.spatial.ConvexHull(point_set)
    step_1_plot = scipy.spatial.convex_hull_plot_2d(hull, ax_step_1)
    
    #we assume that the point set is in general position (no 3 points collinear)
    hull_coordinates = hull.points[hull.vertices]
    segment_start_indices = np.arange(hull_coordinates.shape[0])
    segment_end_indices = segment_start_indices + 1
    segment_end_indices[-1] = 0
    segment_indices = np.array(zip(segment_start_indices, segment_end_indices))
    
    hull_vertex_dict = dict(vertices = hull_coordinates, segments = segment_indices)
    tri = triangle.triangulate(hull_vertex_dict, 'p') #constrained polygon triangulation, as before
    
    simplex_coords = hull_coordinates[tri['triangles']]
    triangles = PolyCollection((simplex_coords), alpha = 0.1)
    ax_step_1.add_collection(triangles)
    ax_step_1.set_title('Step 1: Convex Hull + Polygon Triangulation')
    
    #step 2: identify interior points and draw edges to vertices of triangles that contain them (iteratively)
    ax_step_2 = fig_splitting_progress.add_subplot(122)
    
    all_indices = np.arange(point_set.shape[0])
    interior_indices = all_indices[np.in1d(all_indices, hull.vertices, invert=True)]
    interior_coords = hull.points[interior_indices]
    
    interior_indices = range(interior_coords.shape[0])
    
    triangle_index = 0
    triangles_left = simplex_coords.shape[0]
    while triangles_left > 0:
        triangle_coords = simplex_coords[triangle_index]
        current_path = matplotlib.path.Path(triangle_coords)
        for interior_index in interior_indices:
            interior_point = interior_coords[interior_index]
            if current_path.contains_point(interior_point):
                interior_indices.remove(interior_index)
                ax_step_2.plot([interior_point[0],triangle_coords[0][0]],[interior_point[1],triangle_coords[0][1]], c = 'r')
                ax_step_2.plot([interior_point[0],triangle_coords[1][0]],[interior_point[1],triangle_coords[1][1]], c = 'r')
                ax_step_2.plot([interior_point[0],triangle_coords[2][0]],[interior_point[1],triangle_coords[2][1]], c = 'r')
                new_triangle_1 = np.concatenate((triangle_coords[0], triangle_coords[1], interior_point)).reshape(1,3,2)
                new_triangle_2 = np.concatenate((triangle_coords[1],triangle_coords[2], interior_point)).reshape(1,3,2)
                new_triangle_3 = np.concatenate((triangle_coords[0],triangle_coords[2], interior_point)).reshape(1,3,2)
                simplex_coords = np.concatenate((simplex_coords, new_triangle_1, new_triangle_2, new_triangle_3))
                triangles_left += 3 
                break
        triangles_left -= 1
        triangle_index += 1
                
    #updated plot with new triangles from interior points
    triangles = PolyCollection((simplex_coords), alpha = 0.1)
    ax_step_2.add_collection(triangles)
    step_2_plot = scipy.spatial.convex_hull_plot_2d(hull, ax_step_2)
    ax_step_2.set_title('Step 2: Iterative Edge Drawing from Interior Points')
    
random_points = np.random.random_sample((10,2))
triangle_splitting(random_points)


Based on insights from Euler, we know that the number of triangles in a given point set is always fixed as: $2k + h - 2$

Where $k$ is a count of internal points and $h$ is a count of hull points and $n = k + h$

However, the number of possible triangulations (permutations leading to the same number of triangles) for a given point set is far more difficult to determine, and only recently have mathematicians even been able to place an upper bound on the number of possible triangulations. The upper bound appears to be $30^n$ possible triangulations based on the work of Sharir, Sheffer, and Welzl (2009).

4.2 The Flip Graph

Definition: For a point set $S$, the flip graph of $S$ is a graph whose nodes are the set of triangulations of $S$. Two nodes, $T_1$ and $T_2$, of the flip graph are connected if one diagonal of $T_1$ can be flipped to obtain $T_2$. It is effectively the space of triangulations of $S$.

The flip graph of any point set in the plane is connected (you can convert between different triangulations with edge flipping), which was proven by Charles Lawson in 1971.

An Example of a Flip Graph from the text book:

Flip graphs have many interesting properties and many triangulation algorithms effectively explore the space of the flip graph to achieve desired results. I'm not aware of any python libraries that generate the flip graphs for a point set in the plane, so that might be an interesting exercise.

In 3D it is not known if the flip graph between different tetrahedralizations is connected (or if there may be isolated nodes). Discoveries in this domain may benefit meshing / computer graphics, etc.

4.3 Triangulations and Applications

Type of Triangulation 2D Time Complexity Example Application(s)
Delaunay $O(n \textrm{ log } n)$ Modelling Terrain (3D maps of Earth)
Minimum-weight NP-hard Economical Networking

4.4 Delaunay Triangulations

4.4.1 Definition and Basic Information

Named after Boris Delaunay (Russian Mathematician)

A Delaunay Triangulation is a triangulation that only has legal edges--edges that avoid small angles in triangles. The lexicographically sorted comparison of the full set of angles in two triangulations will always have larger angles for the Delaunay triangulation (other triangulations will have a smaller angle first).

General Position for Delaunay in 2D: no four points are cocircular

Textbook example of terrain reconstruction sensitivity to triangulation:

It is actually possible to construct a 2D Delaunay triangulation starting from any arbitrary triangulation by flipping one edge at a time (progressive illegal to legal edge flips), although this algorithm is slow ($O(n^2)$) and cannot be extended to 3D cases.

4.4.2 The Empty Circle Property


In [27]:
#none of the original data points in S can fall within the circumcircle of a triangle for a Delaunay triangulation (alternative and very important definition)
#demonstration using scipy.spatial once again:
fig_Delaunay_circles = plt.figure()
fig_Delaunay_circles.set_size_inches(8,8)
ax = fig_Delaunay_circles.add_subplot(111, aspect='equal')
random_points = np.random.random_sample((10,2))
tri = scipy.spatial.Delaunay(random_points)
ax.triplot(random_points[...,0], random_points[...,1], tri.simplices, 'go-')

#deal with calculating and plotting the circumcircles
circumcenters, circumradii = circumcircle.calc_circumcircle(tri.points[tri.simplices])
patches = []
for circumcenter_coordinates, circumradius in zip(circumcenters,circumradii):
    patches.append(matplotlib.patches.Circle((circumcenter_coordinates[0],circumcenter_coordinates[1]),circumradius[0],fill=False, alpha=1.0, fc='none', ec='none',))
p = PatchCollection(patches, alpha=0.1,match_original = True)
ax.add_collection(p)

ax.set_title('Delaunay Empty Circle Property')


Out[27]:
<matplotlib.text.Text at 0x117686a50>

Also note that connecting the circumcenters of the circumcircles would produce the Voronoi diagram (they are dual graphs) -- this can be a very useful property and indeed Delaunay was a student of Voronoi.

4.4.3 Practical Applications of scipy.spatial.Delaunay()

2D

Problem: You're building a network with prohibitively expensive wiring but as long as there is a path from one computer to another the network will function properly. Given a set of 25 computers (nodes) with fixed positions (i.e., in an office), find the minimum amount of wiring and the appropriate connectivity for the network. The key here is to exploit the fact that the Euclidean minimum spanning tree (EMST) is a subgraph of the Delaunay triangulation of the point set.


In [28]:
computer_positions = np.random.random_sample((25,2))
tri = scipy.spatial.Delaunay(computer_positions)
#need undirected graph data in a format ready for scipy.sparse.csgraph.minimum_spanning_tree
#this will be an NxN symmetric matrix with Euclidean distances for direct connections in the triangulation, and 0 for "self" and other points that are not connected

#so we effectively want a special distance matrix, which means we need to know which points are connected, so we will likely have to use the simplices attribute of tri

triangle_indices = tri.simplices #has shape (n_triangles, 3)

#iterate through the triangle indices and populate a template distance matrix (default 0 for no connection)
undirected_graph = np.zeros((25,25))

for triangle_index_array in triangle_indices:
    equivalent_coordinate_array = tri.points[triangle_index_array]
    distance_array = scipy.spatial.distance.pdist(equivalent_coordinate_array)
    undirected_graph[triangle_index_array[0],triangle_index_array[1]] = distance_array[0]
    undirected_graph[triangle_index_array[0],triangle_index_array[2]] = distance_array[1]
    undirected_graph[triangle_index_array[1],triangle_index_array[2]] = distance_array[2]

#sanity check: no point should be connected to itself (all diagonal values zero)
print 'diagonal of undirected_graph:', np.diag(undirected_graph)

sparse_result = scipy.sparse.csgraph.minimum_spanning_tree(undirected_graph)

#iterate through sparse matrix, plotting and adding up distances
fig_emst = plt.figure()
fig_emst.set_size_inches(8,8)
ax = fig_emst.add_subplot(111,aspect='equal')

cx = sparse_result.tocoo() #convert to coordinate representation of matrix
label = 0
total_distance = 0
for i,j,v in itertools.izip(cx.row, cx.col, cx.data):
    if v != 0: #there's an edge if nonzero
        p1 = computer_positions[i]
        p2 = computer_positions[j]
        total_distance += v
        if not label:
            ax.plot([p1[0],p2[0]],[p1[1],p2[1]],c='green',ls='-',marker='o', label = 'EMST', alpha = 0.2, lw =12)
            label += 1
        else: 
            ax.plot([p1[0],p2[0]],[p1[1],p2[1]],c='green',ls='-',marker='o', alpha = 0.2, lw = 12)
ax.legend()
ax.set_title('EMST (D = {dist}) as subgraph of Delaunay Triangulation'.format(dist=total_distance))

#overlay the original triangulation for comparison
ax.triplot(computer_positions[...,0], computer_positions[...,1], triangle_indices, c = 'k',marker = 'o')


diagonal of undirected_graph: [ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.
  0.  0.  0.  0.  0.  0.  0.]
Out[28]:
[<matplotlib.lines.Line2D at 0x114b20f10>,
 <matplotlib.lines.Line2D at 0x115a9e3d0>]

4.4.4 Measuring the time complexity of scipy.spatial.Delaunay()


In [29]:
#use a similar empirical approach to that used above for benchmarking the convex hull code

points_list = [1000,20000,30000,50000,70000,100000,200000,300000,500000,700000,900000,1000000]
list_times = []

for num_points in points_list:
    random_2D_points = np.random.random_sample((num_points,2))
    start_time = time.time()
    tri = scipy.spatial.Delaunay(random_2D_points)
    elapsed_time = time.time() - start_time
    list_times.append(elapsed_time)
    print 'benchmarked', num_points, 'points in:', elapsed_time, ' seconds'


benchmarked 1000 points in: 0.00390791893005  seconds
benchmarked 20000 points in: 0.0975410938263  seconds
benchmarked 30000 points in: 0.156080961227  seconds
benchmarked 50000 points in: 0.269389867783  seconds
benchmarked 70000 points in: 0.399724960327  seconds
benchmarked 100000 points in: 0.651040077209  seconds
benchmarked 200000 points in: 1.43465995789  seconds
benchmarked 300000 points in: 2.21873617172  seconds
benchmarked 500000 points in: 3.92210197449  seconds
benchmarked 700000 points in: 5.79272007942  seconds
benchmarked 900000 points in: 7.65184116364  seconds
benchmarked 1000000 points in: 8.41952896118  seconds

In [30]:
popt, pcov = scipy.optimize.curve_fit(linear, points_list, list_times)
linear_y_data = linear(np.array(points_list), popt[0], popt[1])
popt, pcov = scipy.optimize.curve_fit(loglinear, points_list, list_times)
loglinear_y_data = loglinear(np.array(points_list), popt[0], popt[1])
popt, pcov = scipy.optimize.curve_fit(quadratic, points_list, list_times)
quadratic_y_data = quadratic(np.array(points_list), popt[0], popt[1])

fig_bench_Delaunay = plt.figure()
ax = fig_bench_Delaunay.add_subplot(111)
ax.scatter(points_list, list_times, c='k', label='original data', s = 80)
ax.plot(points_list, list_times, c='k', label='original data')

ax.plot(points_list, linear_y_data, c = 'blue', lw=7,alpha = 0.3, label = 'linear')
ax.plot(points_list, loglinear_y_data, c = 'red', lw=7,alpha = 0.3, label = 'loglinear')
ax.plot(points_list, quadratic_y_data, c = 'green', lw=7,alpha = 0.3, label = 'quadratic')
ax.legend(loc=2)

ax.set_title('Crude Time Complexity Assessment\nFor Qhull 2D Delaunay Triangulation')
#ax.set_xlim(-50,1.2e+6)
#ax.set_ylim(-0.02,0.20)
ax.set_xlabel('# Points',fontsize=16)
ax.set_ylabel('Time (s)', fontsize=16)
fig_bench_Delaunay.set_size_inches(8,8)


So, scipy.spatial.Delaunay appears to have loglinear performance, which is the optimum.

5) Voronoi Diagrams

5.1 Definition and Importance

They were first seriously studied by Georgy Voronoi in 1908, but are also known as Thiessen polygons and Dirichlet tessellations.

A Voronoi diagram defines Voronoi regions around each of the points in the original data set. The Voronoi region for a given point represents the portion of space in which all points not in the input data are closer to that point than to any other.

From a more practical standpoint, if you imagine a data set with all the grocery stores in your city as input points (generators), each grocery store will have its own Voronoi region, where all homes are closer to that grocery store than to any other. Applications span geography, marketing, network coverage, biology, robot motion planning, etc.

Crucially, the Voronoi diagram is the mathematical dual of the Delaunay Triangulation.

All Voronoi regions (i.e., polygons in 2D) are convex. This can be proven (although I won't do it!).

General position in 2D: no four generators are cocircular

5.2 Overview of Algorithms and their Time Complexities (2D)

Authors Year Citations Paradigm Performance
Shamos and Hoey 1975 1101 divide-and-conquer $O(n\:{\log}\:n)$
Green and Sibson 1978 861 incremental $O(n^2)$
Guibas and Stolfi 1985 1506 quad-edge data structure $O(n\:{\log}\:n)$
Fortune 1987 1402 sweepline $O(n\:{\log}\:n)$

Loglinear performance is known to be optimal.

5.3 Practical Problem Solving with scipy.spatial.Voronoi()

2D

Problem: Determine which residential locations are closest to a given water pump during the cholera outbreak in London ~150 years ago. This is the classic John Snow example with modern mapping data obtained from Robin Wilson (see below). The sizes of the red points are proportional to the number of fatalities reported at that residential location.


In [31]:
#load data modified from Robin Wilson's (University of Southampton) blog
array_pump_coords = pickle.load(open('array_pump_data.p','rb')) 
array_cholera_data = pickle.load(open('array_cholera_data.p','rb')) 

#plot
figure_voronoi = plt.figure()
figure_voronoi.set_size_inches(8,8)
ax = figure_voronoi.add_subplot('111', aspect='equal')
vor = scipy.spatial.Voronoi(array_pump_coords)
voronoi_figure = scipy.spatial.voronoi_plot_2d(vor, ax = ax)
ax.scatter(array_cholera_data[...,0], array_cholera_data[...,1], s = array_cholera_data[...,2] * 8, c = 'red', edgecolor='none') #scale scatter point area by number of fatalities
ax.scatter(array_pump_coords[...,0], array_pump_coords[...,1], s = 60, c = 'blue', edgecolor='none') #default pump sizes a bit too small


Out[31]:
<matplotlib.collections.PathCollection at 0x114e19a50>

3D

Problem: You're designing an autonomous drone that will fly in an urban location and will have to avoid obstacles that are both stationary (i.e., tall buildings) and dynamic (helicopter traffic, etc.). Demonstrate an approach to determining the safest routes (i.e., farthest from obstacles) for the drone in 3D space. Assume that using a random set of 3D points is a suitable simulation of the kinds of coordinate data the drone will receive in real time.


In [32]:
air_traffic_mess = np.random.random_sample((50,3))
#the edges of a 3D Voronoi diagram will be the farthest from the obstacle coordinates
vor = scipy.spatial.Voronoi(air_traffic_mess)
        
fig_drone_3d = plt.figure()
fig_drone_3d.set_size_inches(8,8)
ax = fig_drone_3d.add_subplot(111, projection = '3d')

for ridge_indices in vor.ridge_vertices:
    voronoi_ridge_coords = vor.vertices[ridge_indices]
    ax.plot(voronoi_ridge_coords[...,0], voronoi_ridge_coords[...,1], voronoi_ridge_coords[...,2], lw=2, c = 'green', alpha = 0.05)
    
vor_vertex_coords = vor.vertices

ax.scatter(air_traffic_mess[...,0], air_traffic_mess[...,1], air_traffic_mess[...,2], c= 'k', label='obstacles', edgecolor='none')
ax.scatter(vor_vertex_coords[...,0], vor_vertex_coords[...,1], vor_vertex_coords[...,2], c= 'orange', label='Voronoi vertices',edgecolors='white', marker = 'o', alpha = 0.9)

ax.legend()
ax.set_xlim3d(air_traffic_mess[...,0].min(), air_traffic_mess[...,0].max())
ax.set_ylim3d(air_traffic_mess[...,1].min(), air_traffic_mess[...,1].max())
ax.set_zlim3d(air_traffic_mess[...,2].min(), air_traffic_mess[...,2].max())


Out[32]:
(0.021030211302362045, 0.98368466924132036)

We would want the drone to follow the green lines, although the data still needs to be cleaned up a bit. For example, we'd like to deal with the "edges at infinity" (outside the Voronoi diagram, usually denoted as "-1" in the data structures returned by scipy/ qhull) more gracefully.

At the moment it appears to be non-trivial to plot the polyhedra around each of the obstacles. This is probably because the points need to be in the correct order for each polyhedron and again because of the edges at infinity.

A nice take-home exercise would be to attempt to clean this data up and produce a really nice 3D Voronoi diagram.

5.4 Measuring the Time Complexity for 2D scipy.spatial.Voronoi()


In [33]:
#use a similar empirical approach to that used above for benchmarking the convex hull code

points_list = [1000,20000,30000,50000,70000,100000,200000,300000,500000,700000,900000,1000000]
list_times = []

for num_points in points_list:
    random_2D_points = np.random.random_sample((num_points,2))
    start_time = time.time()
    tri = scipy.spatial.Voronoi(random_2D_points)
    elapsed_time = time.time() - start_time
    list_times.append(elapsed_time)
    print 'benchmarked', num_points, 'points in:', elapsed_time, ' seconds'


benchmarked 1000 points in: 0.0939221382141  seconds
benchmarked 20000 points in: 0.167594909668  seconds
benchmarked 30000 points in: 0.477156877518  seconds
benchmarked 50000 points in: 0.683579921722  seconds
benchmarked 70000 points in: 0.951398849487  seconds
benchmarked 100000 points in: 1.33662605286  seconds
benchmarked 200000 points in: 2.81140089035  seconds
benchmarked 300000 points in: 4.4250600338  seconds
benchmarked 500000 points in: 7.48547315598  seconds
benchmarked 700000 points in: 10.3042020798  seconds
benchmarked 900000 points in: 13.4142198563  seconds
benchmarked 1000000 points in: 15.4234688282  seconds

In [34]:
popt, pcov = scipy.optimize.curve_fit(linear, points_list, list_times)
linear_y_data = linear(np.array(points_list), popt[0], popt[1])
popt, pcov = scipy.optimize.curve_fit(loglinear, points_list, list_times)
loglinear_y_data = loglinear(np.array(points_list), popt[0], popt[1])
popt, pcov = scipy.optimize.curve_fit(quadratic, points_list, list_times)
quadratic_y_data = quadratic(np.array(points_list), popt[0], popt[1])

fig_bench_Voronoi = plt.figure()
ax = fig_bench_Voronoi.add_subplot(111)
ax.scatter(points_list, list_times, c='k', label='original data', s = 80)
ax.plot(points_list, list_times, c='k', label='original data')

ax.plot(points_list, linear_y_data, c = 'blue', lw=7,alpha = 0.3, label = 'linear')
ax.plot(points_list, loglinear_y_data, c = 'red', lw=7,alpha = 0.3, label = 'loglinear')
ax.plot(points_list, quadratic_y_data, c = 'green', lw=7,alpha = 0.3, label = 'quadratic')
ax.legend(loc=2)

ax.set_title('Crude Time Complexity Assessment\nFor Qhull 2D Voronoi Diagram')
#ax.set_xlim(-50,1.2e+6)
#ax.set_ylim(-0.02,0.20)
ax.set_xlabel('# Points',fontsize=16)
ax.set_ylabel('Time (s)', fontsize=16)
fig_bench_Voronoi.set_size_inches(8,8)


The loglinear performance is known to be optimal so we can't really improve on this from an algorithmic standpoint.

6) How can we improve the Python computational geometry tool set?

6.1 Some of my Efforts

6.1.1 scipy.spatial.SphericalVoronoi() [upcoming scipy 0.18]

Nikolai Nowaczyk and I worked on this PR (https://github.com/scipy/scipy/pull/5232) for quite some time but it was eventually merged in and should be available in scipy 0.18, which probably won't be out for quite some time. You could, however, compile scipy from the master branch to use it if you need it.

Specific applications include my work on spherical viruses, geography, astrophysics, MRI analysis, and any other field that deals with roughly spherical objects. A concise example follows below. Note that this algorithm has quadratic time complexity, but loglinear is optimal--so this is an opportunity for improvement within the Python community. That said, many of the better-performing algorithms appear challenging to implement (I tried one of them for a few months without much luck).


In [35]:
#simple example for usage of scipy.spatial.SphericalVoronoi using random points on the surface of a sphere (but could easily imagine these points representing i.e., airports or cities on the surface of the earth, etc.)

num_points = 600

def marsaglia(num_points):
    '''generate random points on surface of sphere using Marsaglia's method (see: http://mathworld.wolfram.com/SpherePointPicking.html)'''
    x1 = np.random.uniform(low=-1.0, high=1.0,size=num_points)
    x2 = np.random.uniform(low=-1.0, high=1.0,size=num_points)

    #filter out points for which the sum of squares between the two distribution is >= 1
    mask = np.where(x1 ** 2 + x2 ** 2 < 1)
    x1 = x1[mask]
    x2 = x2[mask]

    x_coords = 2 * x1 * np.sqrt(1 - x1 ** 2 - x2 ** 2)
    y_coords = 2 * x2 * np.sqrt(1 - x1 ** 2 - x2 ** 2)
    z_coords = 1 - 2 * (x1 ** 2 + x2 ** 2)
    return np.stack((x_coords,y_coords,z_coords), axis=-1) 

points = marsaglia(num_points=num_points)

fig_spherical_Voronoi = plt.figure()
fig_spherical_Voronoi.set_size_inches(16,8)
ax1 = fig_spherical_Voronoi.add_subplot(121, projection = '3d')
ax1.scatter(points[...,0], points[...,1], points[...,2],c='blue')
ax1.set_title('Random Points on Sphere')

ax2 = fig_spherical_Voronoi.add_subplot(122, projection = '3d')
#calculate the Voronoi diagram on the surface of the sphere and plot the Voronoi region polygons
sv = scipy.spatial.SphericalVoronoi(points, radius=1)
sv.sort_vertices_of_regions() #generally needed for plotting / SA calculation

for region in sv.regions:
    random_color = colors.rgb2hex(np.random.rand(3))
    polygon = Poly3DCollection([sv.vertices[region]], alpha=1.0)
    polygon.set_color(random_color)
    ax2.add_collection3d(polygon)
    
ax2.set_title('Voronoi Regions')

for axis in [ax1,ax2]:
    axis.set_xlim(-1.5,1.5)
    axis.set_ylim(-1.5,1.5)
    axis.set_zlim(-1.5,1.5)


6.1.2 Calculating Surface Area of Spherical Polygons

Not only have I had to perform this calculation extensively in my own work on viruses, but I frequently get emails from geographers and MRI scientists asking about this calculation for polygons on the surface of a sphere. I recently wrote a proposal (https://github.com/scipy/scipy/issues/6069) to incorporate calculations of this nature into scipy. The best code I have for doing this at the moment was improved by Edd Edmondson (University of Portsmouth). Let's see if we get a sensible result from the sum of the surface areas of all polygons in the above example.


In [36]:
#draft of code to calculate spherical polygon surface area
def convert_cartesian_array_to_spherical_array(coord_array,angle_measure='radians'):
    '''Take shape (N,3) cartesian coord_array and return an array of the same shape in spherical polar form (r, theta, phi). Based on StackOverflow response: http://stackoverflow.com/a/4116899
    use radians for the angles by default, degrees if angle_measure == 'degrees' '''
    spherical_coord_array = np.zeros(coord_array.shape)
    xy = coord_array[...,0]**2 + coord_array[...,1]**2
    spherical_coord_array[...,0] = np.sqrt(xy + coord_array[...,2]**2)
    spherical_coord_array[...,1] = np.arctan2(coord_array[...,1], coord_array[...,0])
    spherical_coord_array[...,2] = np.arccos(coord_array[...,2] / spherical_coord_array[...,0])
    if angle_measure == 'degrees':
        spherical_coord_array[...,1] = np.degrees(spherical_coord_array[...,1])
        spherical_coord_array[...,2] = np.degrees(spherical_coord_array[...,2])
    return spherical_coord_array

def convert_spherical_array_to_cartesian_array(spherical_coord_array,angle_measure='radians'):
    '''Take shape (N,3) spherical_coord_array (r,theta,phi) and return an array of the same shape in cartesian coordinate form (x,y,z). Based on the equations provided at: http://en.wikipedia.org/wiki/List_of_common_coordinate_transformations#From_spherical_coordinates
    use radians for the angles by default, degrees if angle_measure == 'degrees' '''
    cartesian_coord_array = np.zeros(spherical_coord_array.shape)
    #convert to radians if degrees are used in input (prior to Cartesian conversion process)
    if angle_measure == 'degrees':
        spherical_coord_array[...,1] = np.deg2rad(spherical_coord_array[...,1])
        spherical_coord_array[...,2] = np.deg2rad(spherical_coord_array[...,2])
    #now the conversion to Cartesian coords
    cartesian_coord_array[...,0] = spherical_coord_array[...,0] * np.cos(spherical_coord_array[...,1]) * np.sin(spherical_coord_array[...,2])
    cartesian_coord_array[...,1] = spherical_coord_array[...,0] * np.sin(spherical_coord_array[...,1]) * np.sin(spherical_coord_array[...,2])
    cartesian_coord_array[...,2] = spherical_coord_array[...,0] * np.cos(spherical_coord_array[...,2])
    return cartesian_coord_array

def calculate_haversine_distance_between_spherical_points(cartesian_array_1,cartesian_array_2,sphere_radius):
    '''Calculate the haversine-based distance between two points on the surface of a sphere. Should be more accurate than the arc cosine strategy. See, for example: http://en.wikipedia.org/wiki/Haversine_formula'''
    spherical_array_1 = convert_cartesian_array_to_spherical_array(cartesian_array_1)
    spherical_array_2 = convert_cartesian_array_to_spherical_array(cartesian_array_2)
    lambda_1 = spherical_array_1[1]
    lambda_2 = spherical_array_2[1]
    phi_1 = spherical_array_1[2]
    phi_2 = spherical_array_2[2]
    #we rewrite the standard Haversine slightly as long/lat is not the same as spherical coordinates - phi differs by pi/4
    spherical_distance = 2.0 * sphere_radius * math.asin(math.sqrt( ((1 - math.cos(phi_2-phi_1))/2.) + math.sin(phi_1) * math.sin(phi_2) * ( (1 - math.cos(lambda_2-lambda_1))/2.)  ))
    return spherical_distance

def calculate_surface_area_of_a_spherical_Voronoi_polygon(array_ordered_Voronoi_polygon_vertices,sphere_radius):
    '''Calculate the surface area of a polygon on the surface of a sphere. Based on equation provided here: http://mathworld.wolfram.com/LHuiliersTheorem.html
    Decompose into triangles, calculate excess for each'''
    #have to convert to unit sphere before applying the formula
    spherical_coordinates = convert_cartesian_array_to_spherical_array(array_ordered_Voronoi_polygon_vertices)
    spherical_coordinates[...,0] = 1.0
    array_ordered_Voronoi_polygon_vertices = convert_spherical_array_to_cartesian_array(spherical_coordinates)
    n = array_ordered_Voronoi_polygon_vertices.shape[0]
    #point we start from
    root_point = array_ordered_Voronoi_polygon_vertices[0]
    totalexcess = 0
    #loop from 1 to n-2, with point 2 to n-1 as other vertex of triangle
    # this could definitely be written more nicely
    b_point = array_ordered_Voronoi_polygon_vertices[1]
    root_b_dist = calculate_haversine_distance_between_spherical_points(root_point, b_point, 1.0)
    for i in 1 + np.arange(n - 2):
        a_point = b_point
        b_point = array_ordered_Voronoi_polygon_vertices[i+1]
        root_a_dist = root_b_dist
        root_b_dist = calculate_haversine_distance_between_spherical_points(root_point, b_point, 1.0)
        a_b_dist = calculate_haversine_distance_between_spherical_points(a_point, b_point, 1.0)
        s = (root_a_dist + root_b_dist + a_b_dist) / 2.
        totalexcess += 4 * math.atan(math.sqrt( math.tan(0.5 * s) * math.tan(0.5 * (s-root_a_dist)) * math.tan(0.5 * (s-root_b_dist)) * math.tan(0.5 * (s-a_b_dist))))
    return totalexcess * (sphere_radius ** 2)

In [37]:
#sum the polygon areas from above and compare with theoretical surface area of unit sphere
calculated_SA = 0
for region in sv.regions:
    SA = calculate_surface_area_of_a_spherical_Voronoi_polygon(sv.vertices[region], 1.0)
    calculated_SA += SA
    
print 'calculated_SA:', calculated_SA
print 'theoretical SA:', 4 * math.pi


calculated_SA: 12.5663706144
theoretical SA: 12.5663706144

Well, that certainly looks like a sensible result! However, as you would be able to see in the above proposal, there are probably a few mysteries left to solve (if only to determine the range of pathological inputs). Also, what if we wanted to avoid a python for loop? Can we vectorize this code in numpy ufunc style if each polygon can have a different shape? Usually ufuncs operate on 'rectangular' arrays.

6.2 Other Opportunities

6.2.1 Improve Matplotlib Handling of Spherical Polygons

Nikolai Nowaczyk is working on this. I'm sure additional help could be useful!

See matplotlib Issue 5294 and PR 6248

Consider the case of plotting the spherical Voronoi result for a much smaller set of generators:


In [40]:
points = marsaglia(num_points=25)
sv = scipy.spatial.SphericalVoronoi(points,radius = 1)
sv.sort_vertices_of_regions()

fig_problematic_polygons = plt.figure()
ax = fig_problematic_polygons.add_subplot(111, projection = '3d')

for region in sv.regions:
    random_color = colors.rgb2hex(np.random.rand(3))
    polygon = Poly3DCollection([sv.vertices[region]], alpha=1.0)
    polygon.set_color(random_color)
    ax.add_collection3d(polygon)

ax.set_xlim(-1.5,1.5)
ax.set_ylim(-1.5,1.5)
ax.set_zlim(-1.5,1.5)


Out[40]:
(-1.5, 1.5)

6.2.2 Working with imperfect spheres (like our planet), cylinders and other shapes? Your ideas?