Branched Polymers

138 days ago by pub

cos = math.cos sin = math.sin pi = math.pi class diskObj: """ A disk object for use in a KWPolymer object. Has a label, a radius, a center, and a dictionary of the form {adjacent:angleToAdjacent for each adjacent disk}. """ def __init__(self,label,radius,center=None): """ By default, center is None. """ self.label = label self.radius = radius self.center = center def __str__(self): """ Print disk label. """ return str(self.label) def branchbranch(x0,y0,a0,b0,r0,x1,y1,a1,b1,r1): a = a0^2 + b0^2 - 2*a0*a1 + a1^2 - 2*b0*b1 + b1^2 b = 2*x0*a0 + 2*y0*b0 - 2*a0*x1 - 2*b0*y1 - 2*x0*a1 + 2*x1*a1 - 2*y0*b1 + 2*y1*b1 c = x0^2 + y0^2 - r0^2 - 2*x0*x1 + x1^2 - 2*y0*y1 + y1^2 - 2*r0*r1 - r1^2 if a == 0: return -b/c disc = b^2-4*a*c if disc < 0: return -1 r0 = (-b+sqrt(disc))/(2*a) r1 = (-b-sqrt(disc))/(2*a) if r1 > r0 > 0: return r0 if r0 > r1 > 0: return r1 return -1 class KWPolymer: """ A branched polymer class implementing the Kenyon-Winkler algorithm for generating random branched polymers in the plane. """ def __init__(self, radii = None, printOn=False): """ Constructs a new polymer consisting of a disk at the origin. Radii must be iterable. """ if radii is None: def unit(): while True: yield 1 radii = unit() self.nextRadius = radii.__iter__().next self.G = DiGraph() self.root = diskObj(label = 1, radius = self.nextRadius(), center = (0,0)) self.G.add_vertex(self.root) self.printOn = printOn def grow(self): """ Add a new disk to the polymer using the Kenyon-Winkler algorithm. The algorithm works inductively, providing a recipe for generating a uniformly random polymer of order N+1 from one of order N. Given that an order N polymer has already been generated, our implementation of the algorithm consists of the following steps: 1. Place a new disk of radius zero at a uniformly chosen point along the boundary of the polymer. 2. Find the smallest radius of the new disk less than one at which a cycle forms in the polymer. 3. If there is no such radius, increase the new disk's radius to 1, recalculate the centers of the other disks while holding the new disk fixed, and skip step 4. Otherwise, increase the new disk's radius until a cycle forms, recalculate the centers of the other disks while holding the new disk fixed, and continue to step 4. 4. If L_i is the local volume change near the polymer in the configuration space of the tree formed by removing the i-th edge of the cycle, with respect to a small increase in the radius of the new disk, trees with positive L_i are those that allow us to continue increasing the radius of the new disk without causing an overlap. Choose among these trees, weighting them by the L_i. Remove the corresponding edge from the polymer. Return to step 2. """ ## Step 1 ## disks = self.G.vertices() parent = disks[ randint(0,len(self.G)-1) ] angle_from_parent_to_new = random() * 2 * pi targetRadius = self.nextRadius() new = diskObj(label = len(self.G) + 1, radius = 0) self.G.add_vertex(new) self.addEdge(parent, new, angle_from_parent_to_new) self.getCenterWithRespectTo(new, parent) while True: branches = [ ( neighbor, self.getBranch(neighbor,new), self.G.edge_label(new,neighbor) ) for neighbor in self.G.neighbors(new) ] currentRadius = new.radius ## Step 2 ## collRadius, A, B = self.detectCollision(targetRadius, new, branches) if self.printOn: print collRadius, A, B ## Step 3 ## if A is None: self.moveDisks(targetRadius - currentRadius, new, branches) new.radius = targetRadius if self.printOn: plot = self.plotPolymer(True, True, True) plot.show(axes=False, aspect_ratio=1) break else: self.moveDisks(collRadius - currentRadius, new, branches) new.radius = collRadius ## Step 4 ## self.breakCycle(A, B, new) if self.printOn: plot = self.plotPolymer(True, True, True) plot.show(axes=False, aspect_ratio=1) def getBranch(self, inside, outside): """ Precondition: inside is adjacent to outside. Returns the set of all disks connected to inside except through outside, i.e. the maximal subtree containing inside but not outside. """ angle = self.G.edge_label(inside, outside) self.deleteEdge(inside, outside) branch = self.G.connected_component_containing_vertex(inside) self.addEdge(inside, outside, angle) return branch def detectCollision(self, targetRadius, new, branches): """ If new's radius can increase to targetRadius without any collisions occurring between disks (i.e., without any cycles forming), return (0,None,None). Otherwise, return (collRadius,A,B). collRadius is the smallest radius greater than new's current radius but less than targetRadius such that a collision occurs. A and B are the disks that collide. Let a branch be a maximal subtree of the tree representing the polymer that does not contain new. There are two cases to consider when checking for collisions: 1. A disk in one branch collides with new. 2. A disk in one branch collides with a disk in another branch. Note that disks never collide within a branch, since new pushes the entire branch in the same direction. Also, new can only have as many branches as the number of disks that can be packed around it. """ collRadius, A, B = targetRadius, None, None oldRadius = new.radius ## Case 1 ## x0, y0 = new.center for (n, branch, angle) in branches: for disk in branch: if disk is n: continue x1, y1 = disk.center dx = x1 - x0 dy = y1 - y0 p = disk.radius + oldRadius bottom = 2 * (dx*cos(angle) + dy*sin(angle) - p) if bottom != 0: r = (p^2 - dx^2 - dy^2) / bottom if 0 < r < collRadius - oldRadius: collRadius = r + oldRadius A = disk if self.printOn: print A, '\n', collRadius, '\n', '\n', x1, '\n', y1, '\n', x0, '\n', y0, '\n' if A is not None: B = new ## Case 2 ## branchPairs = Combinations(branches, 2) for [(n0, branch0, angle0), (n1, branch1, angle1)] in branchPairs: for disk0 in branch0: x0, y0 = disk0.center r0 = disk0.radius cos0 = cos(angle0) sin0 = sin(angle0) for disk1 in branch1: x1, y1 = disk1.center r1 = disk1.radius cos1 = cos(angle1) sin1 = sin(angle1) dist = disk0.radius + disk1.radius a = cos1 - cos0 b = sin1 - sin0 c = x1 - x0 d = y1 - y0 bottom = a^2 + b^2 r = branchbranch(x0, y0, cos0, sin0, r0, x1, y1, cos1, sin1, r1) if 0 < r < collRadius - oldRadius: collRadius = r + oldRadius A, B = disk0, disk1 if self.printOn: print A print B print collRadius print dist print print 'a = ', a print 'b = ', b print 'c = ', c print 'd = ', d print 'discriminant = ', discriminant return collRadius, A, B # if bottom != 0: # discriminant = (a*c+b*d)^2-(a^2*b^2)*(c^2+d^2-dist^2) # if discriminant >= 0: # r = -(a*c+b*d + sqrt(discriminant)) / bottom # if 0 < r < collRadius - oldRadius: # collRadius = r + oldRadius # A, B = disk0, disk1 # if self.printOn: # print A # print B # print collRadius # print dist # print # print 'a = ', a # print 'b = ', b # print 'c = ', c # print 'd = ', d # print 'discriminant = ', discriminant # return collRadius, A, B def moveDisks(self, changeInRadius, new, branches): """ Keep the root branch fixed. Move new and its other branches appropriately. """ parent = self.G.shortest_path(new, self.root)[1] parentAngle = self.G.edge_label(parent, new) dx0 = changeInRadius * cos(parentAngle) dy0 = changeInRadius * sin(parentAngle) x0, y0 = new.center new.center = (x0 + dx0, y0 + dy0) for (neighbor, branch, angle) in branches: if neighbor is not parent: dx1 = changeInRadius * cos(angle) dy1 = changeInRadius * sin(angle) for disk in branch: x1, y1 = disk.center disk.center = (x1 + dx0 + dx1, y1 + dy0 + dy1) def breakCycle(self, A, B, new): """ Remove one of the edges in the cycle that formed when A and B touched and add an edge between A and B. (Since A and B touched, the edge between them is conveniently never the one that is removed.) Choose among the edges for which the local volume change is positive as the radius of new increases, weighting each according to the magnitude of the volume change. """ new_to_A = self.G.shortest_path(new, A) B_to_new = self.G.shortest_path(B, new) B_to_new.pop() cycle = new_to_A + B_to_new averageAngle = (self.getAngle(cycle[-1],cycle[0]) + \ self.getAngle(cycle[0],cycle[1])) / 2 L = len(cycle) dp0 = self.dot(cycle[0], cycle[1], averageAngle) dotproducts = [ ( (cycle[i], cycle[(i+1)%L]), dp0 * self.dot(cycle[i], cycle[(i+1)%L], averageAngle) ) for i in range(L) ] goodTrees = [(edge, -dp) for (edge, dp) in dotproducts if dp < 0] choice = self.chooseAmong(goodTrees) self.deleteEdge(choice[0], choice[1]) self.addEdge(A, B, self.getAngle(A, B)) def dot(self, A, B, angle): """ Returns the dot product of the vector from A to B with the unit vector in the direction angle. """ a,b = A.center c,d = B.center return (c-a)*cos(angle) + (d-b)*sin(angle) def chooseAmong(self, choices): """ Given a list a tuples of the form (choice, weight), weight each choice with probability weight/sum(weights), sample a choice, and return it. Weights must be positive. """ sumOfWeights = sum(weight for (choice,weight) in choices) normalizedChoices = [(choice, weight/sumOfWeights) for (choice,weight) in choices] rand = random() for (choice, probability) in normalizedChoices: if rand > probability: rand -= probability else: return choice def getCenterWithRespectTo(self, A, B): """ Precondition: the two disks are adjacent. Compute disk A's center given disk B's center and the angle of the edge from disk B to disk A. """ x, y = B.center magnitude = A.radius + B.radius angle = self.G.edge_label(B,A) dx = magnitude * cos(angle) dy = magnitude * sin(angle) A.center = (x+dx, y+dy) def addEdge(self, A, B, angle_from_A_to_B): """ Adds an edge between A and B to the graph of the polymer, weighted with the given angle in the direction from A to B. """ self.G.add_edge(A, B, angle_from_A_to_B) self.G.add_edge(B, A, angle_from_A_to_B + pi) def deleteEdge(self, A, B): """ Precondition: there exists an edge between A and B. Removes this edge. """ self.G.delete_edge(A, B) self.G.delete_edge(B, A) def getAngle(self, A, B): """ Returns the angle of the vector from the center of disk A to the center of disk B. """ x = B.center[0] - A.center[0] y = B.center[1] - A.center[1] return CC(x,y).arg() def plotPolymer(self, disksOn=False, labelsOn=False, linesOn=False): """ Return a plot of the polymer. """ g = Graphics() for disk in self.G.vertices(): if disksOn: g += circle(disk.center, disk.radius, rgbcolor='blue') if labelsOn: g += text(disk.label, disk.center, rgbcolor='blue', vertical_alignment='top', horizontal_alignment='left') if linesOn: for edge in self.G.edges(): g += line((edge[0].center, edge[1].center), rgbcolor='gray') return g def disks(self): return self.G.vertices() def geometricRadius(self): """ Maximal distance from center of mass realized in the polymer. """ X,Y = self.centerOfMass() D = [] for disk in self.disks(): x,y = disk.center d = sqrt((x-X)^2 + (y-Y)^2) D.append(d + disk.radius) return max(D) def geometricDiameter(self): """ Maximal distance between pairs of points in the polymer. """ m = 0 for disk0,disk1 in Combinations(self.disks(),2): x0,y0 = disk0.center x1,y1 = disk1.center d = disk0.radius + disk1.radius + sqrt((x1-x0)^2 + (y1-y0)^2) if m < d: m = d return m def totalArea(self): """ Total area = sum of area of disks. """ return sum(pi*(d.radius^2) for d in self.disks()) def centerOfMass(self): """ Center of mass = weighted average of centers of disks, X = 1/A sum( pi * x_i * r_i^2 : i = 1 .. n ) Y = 1/A sum( pi * x_i * r_i^2 : i = 1 .. n ) """ A = self.totalArea() X = sum(pi*(d.center[0])*(d.radius^2) for d in self.disks()) / A Y = sum(pi*(d.center[1])*(d.radius^2) for d in self.disks()) / A return (X,Y) 
       
P = KWPolymer() for k in range(20): P.grow() P.plotPolymer(True,False,False).show(axes=False,aspect_ratio=1)