##///////////////////////////////////////////////////////////// ##Copyright 2017 Michele Zito and Marco Bertamini ## ##Occupancy.py is free software; you can redistribute it and/or modify ##it under the terms of the GNU General Public License as published by the ##Free Software Foundation. ## ##Occupancy.py is distributed in the hope that it will be useful, ##but WITHOUT ANY WARRANTY; without even the implied warranty of ##MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ##GNU General Public License for more details. ## ##For copy of the GNU General Public License see . ## ##Please retain this note in the code. If you use the program for academic research ##The most relevant published citation is the following: ##Bertamini, M. Zito, M. Scott-Samuel, N.E. & Hulleman, J. (2016). Spatial Clustering and Its Effect on Perceived Clustering, ##Numerosity and Dispersion Attention, Perception & Psychophysics, 78(5), 1460-1471. ##///////////////////////////////////////////////////////////////////// ##///////////////////////////////////////////////////////////////////// ##README ## ##The script is called occupancy.py ## ##You do not need to edit the script. Prepare a text file in which ##the first line is the radius of the area of influence. ## ##After the first line, each line has one pair of x,y coordinates separated by a comma. ##An example file is provided called coords.txt ## ##The units are the same for the radius and for the coordinates. ##For example pixels or mm. It does not matter how the coordinates have been ##generated. ## ##The occupancy area computed is the area of all regions of influence ##taking into account the overlap. This value is written in the output and ##also saved in a file called result.txt. In the file the first column is ##a timestamp, the second is the occupancy area. ## ##The program has been tested on a Mac, not on Windows. ## ##Known issues: ##Remove any blank lines at the end of your text file. ##For number of elements above 50 the program is slow and ##exponentially more so as the number increases. ## ##For copyright and reference see the top of the script. ## ##For information contact: ##Marco Bertamini ##Michele Zito from numpy import * #many different maths functions import os #handy system and path functions from math import * from itertools import * import sys, copy, datetime #### EXTRA #euclidian distance # usage d = distance([0,0],[10,10]) def d2(p1,p2): return sqrt((p2[0] - p1[0]) ** 2 +(p2[1] - p1[1]) ** 2) # area of any convex polygon, starting from the vertices (see it.wikipedia.org/wiki/Poligono) # usage area = gaussarea(p) where p is a list of lists: [[0,0],[2,3],[30,23]] # # VERY IMPORTANT!! The vertices of the polygon must be given in the order in which they are met # during a walk around the polygon edges (either clockwise or anticlockwise). Any other ordering # is bound to produce meaningless results. [http://geomalgorithms.com/a01-_area.html] # def gaussarea(poly): temp1 = 0 temp2 = 0 poly2 = copy.deepcopy(poly) poly1 = copy.deepcopy(poly) poly1.append(poly[0]) for i in range(len(poly2)): temp1 = temp1 + poly1[i][0]*poly1[i+1][1] for i in range(len(poly2)): temp2 = temp2 + poly1[i+1][0]*poly1[i][1] return (0.5 * abs(temp1 - temp2)) # return the xy coordinate of the centoid of a set of points # usage area = analysecentroid(p) where p is a list of lists: [[0,0],[2,3],[30,23]] def analysecentroid(matrix): sxy =0.0 sx =0.0 sy =0.0 svalue=0 centroid=[0,0] for i in range(len(matrix)): sx = sx + matrix[i][0] sy = sy + matrix[i][1] svalue = len(matrix) centroid[0] = sx/svalue centroid[1] = sy/svalue return centroid #### CONVEX HULL # This code returns the vertices of a convex hull for a set of arbitrary vertices # the vertices of the convex hull are returned ordered # ## [MZ, 2014/10/15] If we know that points are indeed the points in the convex hull and they just ## need to be reallanged then a simpler computation suffices. ## TURN_LEFT, TURN_RIGHT, TURN_NONE = (1, -1, 0) def turn(p, q, r): """Returns -1, 0, 1 if p,q,r forms a right, straight, or left turn.""" return cmp((q[0] - p[0])*(r[1] - p[1]) - (r[0] - p[0])*(q[1] - p[1]), 0) def clockOrder(points): if len(points) > 2: p = 0 q = 1 hull = [points[p],points[q]] prevTurn = -2 repeat = True while repeat: for k in range(2,len(points)+1): kk = k if ki and degrees[i]>0: ## PICKS THE FIRST i FOR WHICH ## degree[i]>0 there is a vertex of degree i ## AND ## sum>i there is enough vertices of degree at least i to build a K_{i+1} ## ## Function returns i+1 as the first NON-FEASIBLE VALUE. return neighbors, i+1 ## DEFAULT RETURN (returnableDeg is always zero) return neighbors, returnableDeg #end refined Bound # compute the coordinates that intersect two circles with centres c1 and c2 and radius r (fixed) def cIntersect2(centre0, radius0, centre1, radius1): # This function checks for the intersection of two circles. # If one circle is wholly contained within the other c2cintersect is -1 # If there is no intersection of the two circles c2cintersect is 0 # If the circles intersect c2cintersect is 1 and the function return xi1, yi1, xi2, yi2 x0 = centre0[0] y0 = centre0[1] x1 =centre1[0] y1 =centre1[1] r0 = radius0 r1 = radius1 #dx and dy are the vertical and horizontal distances between the circle centers. dx = x1 - x0 dy = y1 - y0 #Determine the straight-Line distance between the centers. d = sqrt((dy*dy) + (dx*dx)) #Check for solvability. if (d > (r0 + r1)): #no solution. circles do Not intersect c2cIntersect = 0 xi1 = yi1 = xi2 = yi2 =[] elif (d < abs(r0 - r1)): # no solution. one circle is contained in the other c2cIntersect = -1 xi1 = yi1 = xi2 = yi2 =[] else: # Determine the distance from point 0 To point 2. point 2' is the point where the Line through the circle # intersection points crosses the Line between the circle centers. a = ((r0*r0) - (r1*r1) + (d*d)) / (2.0 * d) # ' Determine the coordinates of point 2. x2 = x0 + (dx * a/d) y2 = y0 + (dy * a/d) # Determine the distance from point 2 To either of the intersection points. h = sqrt((r0*r0) - (a*a)) # Now determine the offsets of the intersection points from point 2. rx = (0-dy) * (h/d) ry = dx * (h/d) # Determine the absolute intersection points. xi1 = x2 + rx xi2 = x2 - rx yi1 = y2 + ry yi2 = y2 - ry c2cIntersect = 1 return xi1, yi1, xi2, yi2 def overlap(c, r): # Compute the set P of points P1 = (x1,y1),...,Pk = (xk,yk), belonging to the intersection of two circles and to all others. p =[] q1 =[[0,0],[0,0]] q2=[[0,0],[0,0]] q3=[[0,0],[0,0]] q4=[[0,0],[0,0]] q5=[[0,0],[0,0]] q6=[[0,0],[0,0]] k = len(c) # Compute the set of points belonging to the intersection of two circles and to all others for i in list(combinations(range(0, k), 2)): q1=[[0,0],[0,0]] q2=[[0,0],[0,0]] # Find Q1 and Q2 the intersection between Ci0 and Ci1 q1[0], q1[1], q2[0], q2[1] =cIntersect2(c[i[0]], r, c[i[1]], r) if q1 == [[],[]] or q2 == [[],[]]: return 0 # variable q stores the point in Ci0 cap Ci1 that belongs to all other Cj's q =[] for h in range(0, k): # Low dimension cases if h != i[0] and h != i[1]: dq1c = d2(q1,c[h]) dq2c = d2(q2,c[h]) if (dq1c= r) and (dq2c >= r): p = [] break elif q==[]: q=q1 if (dq1c2 and len(p)>2: p = clockOrder(p) T = gaussarea(p) else: T=0 if (k>2) and len(p)>2: centroid =analysecentroid(p) aa = array(p) # same as p but as a numpy array aaa = aa - centroid # subtract the centroid ppp = aaa[argsort(arctan2(aaa[:,1], aaa[:,0]))] #sort CW else: ppp = p; #Compute the areas S1, . . . , Sk of the k circular segments on P1,P2, P2,P3, ..., and Pk,P1 esse = 0 for index in range(len(ppp)): first = index second = index +1 if index ==len(ppp) -1: second =0 a1 = d2(ppp[first], ppp[second]) tetha1 = 2. * asin(a1/(2.*r)) s1 = r*r * (tetha1 - sin(tetha1))/2 esse = esse + s1 return T + esse def occupancy(a,r): # print "Occupancy(", a, ",", r,")" area1 = pi* r * r # the area of a full circle diam = 2*r # diameter area = area1 * len(a) # total area without # print "Starting Area: \t\t", area overarea = 0 parity = -1 space = " " # refinedBound based on groups of vcs of max deg. # This time DD is the largest size of a maximum clique as for any larger value there is # at most d vertices of degree d (and a complete graph of degree d is K_{d+1} neighbors, DD = refinedBound(a,r) # print "Maximum number of intersecting circles: %2d\n " % (DD), # [MZ, 27/11/2014] # FIRST OPTIMIZATION: the main cycle looks at intersections between k circles (k-intersections). # If a vertex is not part of any (k-1)-intersection it definitely cannot be part of a k-intersection # (in fact it needs to be part of at least k-1 (k-1)-intersections, we are not checking this though # and it is questionable if this woudl actually give any big saving (we would need counters for # all elements that appear in a (k-1)-intersection: only those appearing in at least k-1 different # intersections will end up in groundset next time around)!) # # # In each iteration groundset contains reference to the set of circles that belong to at least one # (k-1)-intersection. # groundset = set(range(0,len(a))) for k in range(2, DD+1): for i in range(1,k): space = space + "=" loop = 0 # needed to update groundset newGroundset = set() cliques = 0 for i in combinations(groundset, k): Good = True j = [] # [MZ, 27/11/2014] # FIRST FILTERING: if the degree of each of the k-tuple elements is not at least # k-1 there is no change they will all intersect so the joint interesection must # be empty (tested in O(k) steps) for jj in range(0,k): if len(neighbors[i[jj]]) < k-1: Good = False break else: j.append(a[i[jj]]) if Good: # [MZ, 27/11/2014] # SECOND FILTERING: clique checking, if k vertices have passed the "degree test" # we need to check that they form a k-clique, or else their **joint** intersection # will be empty (tested in O(k^2) steps) for jj in range(0,k): for kk in range(0,jj): if d2(j[jj],j[kk])>=2*r: Good = False break if Good: temp = overlap(j, r) if temp>0: cliques+=1 for jj in range(0,k): newGroundset.add(i[jj]) overarea = overarea + parity * temp # if (parity == 1): # print space, ' +', j, '\t', temp # else: # print space, ' -', j, '\t', temp groundset = newGroundset parity = parity * -1 # [MZ, 27/11/2014] # SECOND OPTIMIZATION: if there is less than k-1 cliques leading to non-empty # intersection we can break the whole computation, without the need to go up to # the maximum degree DD. if cliques < k+1: break # print "\b\b\b\b\b\b", finalarea = area + overarea return finalarea # end OCCUPANCY #### TEST CASES: #a = [ [ -66.15480776, 282.8991993], [-209.06454318, -10.18274172], [ 60.66724238, 236.43864335]] #d = 100 #print occupancy(a,d) # #a = [ [-104.72204258, 207.51844101], [ 247.23932665, -50.34252937], [ 171.52499851, -115.89956405]] #print occupancy(a,d) # 81990.3695666 # #a = [[32.962280490376813, 263.28537883335321], [146.1166459578493, 46.855807912736168], [60.819644919131711, 274.76062738498462]] #print occupancy(a,d) # 68834.643707 # #a = [[12,240],[0,0],[-1,-244]] #print occupancy(a,d) # 94247.7796077 # #a = [[-109.76498786290672, -109.5252985846228], [78.211475775509754, -191.19216664600384], [-1.4888916703626593, -14.787963349172513], [141.55342151677559, 124.95656006682363], [83.921273534137043, -122.00046857637021]] #print occupancy(a,d) # 127463.288765 # #a = [[103, 2], [-65, -85], [53, -113], [58, -36], [-44, 133], [7, 35], [-58, 100], [95, -72], [36, 142], [-129, -80], [-108, 78], [-110, -28], [135, -24], [111, 73], [87, 88], [-87, 25], [-64, -117], [-84, 94], [-22, 136], [-23, 86], [-74, -141], [-104, 8], [-79, -99], [-35, -63], [69, 58], [64, -144], [151, -37], [-11, -117]] #print occupancy(a,d) # 126325.471429 #from here the next few lines simply read a string and turn it into a list of lists fin = open('coords.txt') fout = open('result.txt', "a") a =[] with open('coords.txt', 'r') as f: regionInfluence = int(f.readline()) for line in f: geta = "".join(line.split()) c = map(int, geta.split(",")) a.append(c) occupancyResult =occupancy(a, regionInfluence) print occupancyResult fout.write(str(datetime.datetime.utcnow())) fout.write('\t') fout.write(str(occupancyResult)) fout.write('\n') fin.close() fout.close()