From 2c07f592f83b1237f77775481faf6b88d6d94e30 Mon Sep 17 00:00:00 2001
From: Chris Maffeo <cmaffeo2@illinois.edu>
Date: Sun, 20 Jan 2019 00:55:46 -0600
Subject: [PATCH] Made it so a bead can be located at multiple distant
 locations within a segment (e.g. for looped constructs)

---
 mrdna/segmentmodel.py | 185 ++++++++++++++++++++++++++----------------
 1 file changed, 114 insertions(+), 71 deletions(-)

diff --git a/mrdna/segmentmodel.py b/mrdna/segmentmodel.py
index 00cb905..5e698c4 100644
--- a/mrdna/segmentmodel.py
+++ b/mrdna/segmentmodel.py
@@ -215,14 +215,14 @@ class SegmentParticle(PointParticle):
         """ Returns bead directly above self """
         # assert( len(self.intrahelical_neighbors) <= 2 )
         for b in self.intrahelical_neighbors:
-            if b.get_contour_position(self.parent) > self.contour_position:
+            if b.get_contour_position(self.parent, self.contour_position) > self.contour_position:
                 return b
 
     def get_intrahelical_below(self):
         """ Returns bead directly below self """
         # assert( len(self.intrahelical_neighbors) <= 2 )
         for b in self.intrahelical_neighbors:
-            if b.get_contour_position(self.parent) < self.contour_position:
+            if b.get_contour_position(self.parent, self.contour_position) < self.contour_position:
                 return b
 
     def _neighbor_should_be_added(self,b):
@@ -230,14 +230,14 @@ class SegmentParticle(PointParticle):
             return True
 
         c1 = self.contour_position
-        c2 = b.get_contour_position(self.parent)
+        c2 = b.get_contour_position(self.parent,c1)
         if c2 < c1:
             b0 = self.get_intrahelical_below()
         else:
             b0 = self.get_intrahelical_above()
 
         if b0 is not None:            
-            c0 = b0.get_contour_position(self.parent)
+            c0 = b0.get_contour_position(self.parent,c1)
             if np.abs(c2-c1) < np.abs(c0-c1):
                 ## remove b0
                 self.intrahelical_neighbors.remove(b0)
@@ -256,17 +256,42 @@ class SegmentParticle(PointParticle):
             self.intrahelical_neighbors.append(b)
             b.intrahelical_neighbors.append(self)
 
-    def get_nt_position(self, seg):
+    def conceptual_get_position(self, context):
+        """ 
+        context: object
+
+Q: does this function do too much?
+Danger of mixing return values
+
+Q: does context describe the system or present an argument?
+        """
+
+        ## Validate Inputs
+        ...
+
+        ## Algorithm
+        """
+context specifies:
+  - kind of output: real space, nt within segment, fraction of segment
+  - absolute or relative
+  - constraints: e.g. if passing through
+        """
+        """
+given context, provide the position
+        input
+"""
+
+    def get_nt_position(self, seg, near_address=None):
         """ Returns the "address" of the nucleotide relative to seg in
         nucleotides, taking the shortest (intrahelical) contour length route to seg
         """
         if seg == self.parent:
-            return seg.contour_to_nt_pos(self.contour_position)
+            pos = self.contour_position
         else:
-            pos = self.get_contour_position(seg)
-            return seg.contour_to_nt_pos(pos)
+            pos = self.get_contour_position(seg,near_address)
+        return seg.contour_to_nt_pos(pos)
 
-    def get_contour_position(self,seg):
+    def get_contour_position(self,seg, address = None):
         """ TODO: fix paradigm where a bead maps to exactly one location in a polymer
         - One way: modify get_contour_position to take an optional argument that indicates where in the polymer you are looking from
         """
@@ -315,8 +340,10 @@ class SegmentParticle(PointParticle):
             results,history = descend_search_tree(self.parent, self.contour_position)
             if results is None or len(results) == 0:
                 raise Exception("Could not find location in segment") # TODO better error
-            return sorted(results,key=lambda x:x[0])[0][1]
-
+            if address is not None:
+                return sorted(results,key=lambda x:(x[0],(x[1]-address)**2))[0][1]
+            else:
+                return sorted(results,key=lambda x:x[0])[0][1]
             # nt_pos = self.get_nt_position(seg)
             # return seg.nt_pos_to_contour(nt_pos)
 
@@ -829,9 +856,7 @@ class Segment(ConnectableElement, Group):
             ret.append( tmp )
         return ret   
 
-    def _add_bead(self,b,set_contour=False):
-        if set_contour:
-            b.contour_position = b.get_contour_position(self)
+    def _add_bead(self,b):
         
         # assert(b.parent is None)
         if b.parent is not None:
@@ -892,27 +917,29 @@ class Segment(ConnectableElement, Group):
         # if self.name == "S001":
         #     pdb.set_trace()
 
-        existing_beads0 = {l.particle for l in self.locations if l.particle is not None}
-        existing_beads = sorted( list(existing_beads0), key=lambda b: b.get_contour_position(self) )
+        # pdb.set_trace()
+        existing_beads0 = { (l.particle, l.particle.get_contour_position(self,l.address))
+                            for l in self.locations if l.particle is not None }
+        existing_beads = sorted( list(existing_beads0), key=lambda bc: bc[1] )
 
         # if self.num_nt == 1 and all([l.particle is not None for l in self.locations]):
         #     pdb.set_trace()
         #     return
 
-        for b in existing_beads:
+        for b,c in existing_beads:
             assert(b.parent is not None)
 
         ## Add ends if they don't exist yet
         ## TODOTODO: test 1 nt segments?
-        if len(existing_beads) == 0 or existing_beads[0].get_nt_position(self) >= 0.5:
+        if len(existing_beads) == 0 or existing_beads[0][0].get_nt_position(self,0) >= 0.5:
             # if len(existing_beads) > 0:            
             #     assert(existing_beads[0].get_nt_position(self) >= 0.5)
             b = self._generate_one_bead( self.nt_pos_to_contour(0), 0)
-            existing_beads = [b] + existing_beads
+            existing_beads = [(b,0)] + existing_beads
 
-        if existing_beads[-1].get_nt_position(self)-(self.num_nt-1) < -0.5 or len(existing_beads)==1:
+        if existing_beads[-1][0].get_nt_position(self,1)-(self.num_nt-1) < -0.5 or len(existing_beads)==1:
             b = self._generate_one_bead( self.nt_pos_to_contour(self.num_nt-1), 0)
-            existing_beads.append(b)
+            existing_beads.append( (b,1) )
         assert(len(existing_beads) > 1)
 
         ## Walk through existing_beads, add beads between
@@ -920,15 +947,16 @@ class Segment(ConnectableElement, Group):
         last = None
 
         for I in range(len(existing_beads)-1):
-            eb1,eb2 = [existing_beads[i] for i in (I,I+1)]
-            assert( eb1 is not eb2 )
+            eb1,eb2 = [existing_beads[i][0] for i in (I,I+1)]
+            ec1,ec2 = [existing_beads[i][1] for i in (I,I+1)]
+            assert( (eb1,ec1) is not (eb2,ec2) )
 
             # if np.isclose(eb1.position[2], eb2.position[2]):
             #     import pdb
             #     pdb.set_trace()
 
             # print(" %s working on %d to %d" % (self.name, eb1.position[2], eb2.position[2]))
-            e_ds = eb2.get_contour_position(self) - eb1.get_contour_position(self)
+            e_ds = ec2-ec1
             num_beads = self._get_num_beads( e_ds, max_basepairs_per_bead, max_nucleotides_per_bead )
 
             ## Ensure there is a ssDNA bead between dsDNA beads
@@ -945,7 +973,7 @@ class Segment(ConnectableElement, Group):
             if eb1.parent == self:
                 tmp_children.append(eb1)
 
-            s0 = eb1.get_contour_position(self)
+            s0 = ec1
             if last is not None:
                 last.make_intrahelical_neighbor(eb1)
             last = eb1
@@ -1668,63 +1696,75 @@ class SegmentModel(ArbdModel):
                 print( "WARNING: none type found in connection, skipping" )
                 cabs = [e for e in cabs if e[2].particle is not None]
 
-            beads = set(s.beads + [A.particle for c,A,B in cabs])
-
-            ## Add nearby beads
-            for c,A,B in cabs:
-                ## TODOTODO test?
-                filter_fn = lambda x: x is not None and x not in beads
-                bs = list( filter( filter_fn, B.particle.intrahelical_neighbors ) )
-                beads.update(bs)
-                for i in range(3):
-                    bs = list( filter( filter_fn, [n for b in bs for n in b.intrahelical_neighbors] ) )
+            def get_beads_and_contour_positions(s):
+                ret_list = []
+                def zip_bead_contour(beads,address=None):
+                    if isinstance(address,list):
+                        assert(False)
+                        for b,a in zip(beads,address):
+                            if b is None: continue
+                            try:
+                                ret_list.append((b, b.get_contour_position(s,a)))
+                            except:
+                                ...                                
+                    else:
+                        for b in beads:
+                            if b is None: continue
+                            try:
+                                ret_list.append((b, b.get_contour_position(s,address)))
+                            except:
+                                ...
+                    return ret_list
+                
+                ## Add beads from segment s
+                beads_contours = zip_bead_contour(s.beads)
+                beads_contours.extend( zip_bead_contour([A.particle for c,A,B in cabs]) )
+                beads = set([b for b,c in beads_contours])
+
+                ## Add nearby beads
+                for c,A,B in cabs:
+                    ## TODOTODO test?
+                    filter_fn = lambda x: x is not None and x not in beads
+                    bs = list( filter( filter_fn, B.particle.intrahelical_neighbors ) )
+                    beads_contours.extend( zip_bead_contour( bs, A.address ) )
                     beads.update(bs)
+                    for i in range(3):
+                        bs = list( filter( filter_fn, [n for b in bs for n in b.intrahelical_neighbors] ) )
+                        beads_contours.extend( zip_bead_contour( bs, A.address ) )
+                        beads.update(bs)
 
-            beads = list(beads)
-                        
-            ## Skip beads that are None (some locations were not assigned a particle to avoid double-counting) 
-            # beads = [b for b in beads if b is not None]
-            beads = list(filter(lambda x: x is not None, beads))
-            if isinstance(s, DoubleStrandedSegment):
-                beads = list(filter(lambda x: x.type_.name[0] == "D", beads))
-
-            contours = []
-            remove = []
-            for b in beads:
-                try:
-                    # if s.name == "61-1" and b.parent.name == "60-1":
-                    #     pdb.set_trace()
-                    ## might fail if s is an entire segment away
-                    contours.append(b.get_contour_position(s))
-                except:
-                    ## ignore failed attempts
-                    remove.append(b)
-            beads = list(filter(lambda x: x not in remove, beads))
+                beads_contours = list(set(beads_contours))
+                beads = list(beads)
+
+                ## Skip beads that are None (some locations were not assigned a particle to avoid double-counting) 
+                # beads = [b for b in beads if b is not None]
+                assert( np.any([b is None for b,c in beads_contours]) == False )
+                # beads = list(filter(lambda x: x[0] is not None, beads))
 
+                if isinstance(s, DoubleStrandedSegment):
+                    beads_contours = list(filter(lambda x: x[0].type_.name[0] == "D", beads_contours))
+                return beads_contours
+
+            beads_contours = get_beads_and_contour_positions(s)
+            contours = [c for b,c in beads_contours]
             contours = np.array(contours, dtype=np.float16) # deliberately use low precision
-            contours,ids = np.unique(contours, return_index=True)
-            if np.any( (contours[:-1] - contours[1:])**2 < 1e-8 ):
-                pdb.set_trace()
+            contours,ids1 = np.unique(contours, return_index=True)
+            beads_contours = [beads_contours[i] for i in ids1]
+
+            assert( np.any( (contours[:-1] - contours[1:])**2 >= 1e-8 ) )
 
             ## TODO: keep closest beads beyond +-1.5 if there are fewer than 2 beads
             tmp = []
             dist = 1
             while len(tmp) < 5 and dist < 3:
-                tmp = [ci for ci in zip(contours,ids) if np.abs(ci[0]-0.5) < dist]
+                tmp = list(filter(lambda bc: np.abs(bc[1]-0.5) < dist, beads_contours))
                 dist += 0.1
 
             if len(tmp) <= 1:
                 raise Exception("Failed to fit spline into segment {}".format(s))
 
-            contours = [ci[0] for ci in tmp]
-            ids = [ci[1] for ci in tmp]
-
-            # cb = sorted( zip(contours,beads), key=lambda a:a[0] )
-            # beads = [b for c,b in cb] 
-            # contours = [c for c,b in cb] 
-            
-            beads = [beads[i] for i in ids]
-
+            beads = [b for b,c in tmp]
+            contours = [c for b,c in tmp]
             ids = [b.idx for b in beads]
             
             if len(beads) <= 1:
@@ -1929,11 +1969,13 @@ class SegmentModel(ArbdModel):
                 assert( np.isclose(a,0) or np.isclose(a,1) )
             
             ## TODO improve this for combinations of ssDNA and dsDNA (maybe a1/a2 should be calculated differently)
-
+            """ Search to see whether bead at location is already found """
             b = None
             if isinstance(s1,DoubleStrandedSegment):
-                b = s1.get_nearest_bead(a1)
+                b = s1.get_nearest_bead(a1) 
                 if b is not None:
+                    assert( b.parent is s1 )
+                    """ if above assertion is true, no problem here """
                     if np.abs(b.get_nt_position(s1) - s1.contour_to_nt_pos(a1)) > 0.5:
                         b = None
             if b is None and isinstance(s2,DoubleStrandedSegment):
@@ -2014,6 +2056,7 @@ class SegmentModel(ArbdModel):
         # pdb.set_trace()
 
         """ Add intrahelical neighbors at connections """
+
         for c,A,B in self.get_connections("intrahelical"):
             b1,b2 = [l.particle for l in (A,B)]
             if b1 is b2:
@@ -2402,7 +2445,7 @@ class SegmentModel(ArbdModel):
                 c1,A1,B1,dir1 = crossovers[i]
                 c2,A2,B2,dir2 = crossovers[i+1]
                 s1,s2 = [l.container for l in (A1,A2)]
-                sep = A1.particle.get_nt_position(s1) - A2.particle.get_nt_position(s2)
+                sep = A1.particle.get_nt_position(s1,near_address=A1.address) - A2.particle.get_nt_position(s2,near_address=A2.address)
                 sep = np.abs(sep)
 
                 assert(sep >= 0)
-- 
GitLab