diff --git a/arbdmodel/__init__.py b/arbdmodel/__init__.py index 7e66eabc4d02c7f965bce891c703686a907f01c0..2e648bd5a8e1afc6884c5a1e2169e0e869b2dd06 100644 --- a/arbdmodel/__init__.py +++ b/arbdmodel/__init__.py @@ -9,6 +9,9 @@ from copy import copy, deepcopy from inspect import ismethod import os, sys, subprocess +_RESOURCE_DIR = Path(__file__).parent / 'resources' +def get_resource_path(relative_path): + return _RESOURCE_DIR / relative_path ## Abstract classes class Transformable(): @@ -310,22 +313,32 @@ class ParticleType(): "parent", "excludedAttributes", ) - def __init__(self, name, charge=0, **kargs): - # parent = None - # if "parent" in kargs: parent = kargs["parent"] - # Child.__init__(self, parent) + def __init__(self, name, charge=0, parent=None, **kargs): + """ Parent type is used to fall back on for nonbonded interactions if this type is not specifically referenced """ self.name = name self.charge = charge + self.parent = parent + for key in ParticleType.excludedAttributes: assert( key not in kargs ) for key,val in kargs.items(): self.__dict__[key] = val + def is_same_type(self, other): + assert(isinstance(other,ParticleType)) + if self == other: + return True + elif self.parent is not None and self.parent == other: + return True + else: + return False + def __hash_key(self): l = [self.name,self.charge] for keyval in sorted(self.__dict__.items()): + if isinstance(keyval[1], list): keyval = (keyval[0],tuple(keyval[1])) l.extend(keyval) return tuple(l) @@ -352,7 +365,10 @@ class ParticleType(): def __ge__(a,b): a._equal_check(b) return a.name >= b.name - + + def __repr__(self): + return '<{} {}{}>'.format( type(self), self.name, '[{}]'.format(self.parent) if self.parent is not None else '' ) + class PointParticle(Transformable, Child): def __init__(self, type_, position, name="A", **kwargs): @@ -661,11 +677,11 @@ class ArbdModel(PdbModel): if A is None or B is None: if A is None and B is None: return s - elif A is None and typeB == B: + elif A is None and typeB.is_same_type(B): return s - elif B is None and typeA == A: + elif B is None and typeA.is_same_type(A): return s - elif typeA == A and typeB == B: + elif typeA.is_same_type(A) and typeB.is_same_type(B): return s raise Exception("No nonbonded scheme found for %s and %s" % (typeA.name, typeB.name)) @@ -996,7 +1012,7 @@ component "data" value 3 def _writeArbdBondFile( self ): for b in list( set( [b for i,j,b,ex in self.get_bonds()] ) ): - if type(b) is not str: + if type(b) is not str and not isinstance(b, Path): b.write_file() with open(self._bond_filename,'w') as fh: @@ -1009,7 +1025,7 @@ component "data" value 3 def _writeArbdAngleFile( self ): for b in list( set( [b for i,j,k,b in self.get_angles()] ) ): - if type(b) is not str: + if type(b) is not str and not isinstance(b, Path): b.write_file() with open(self._angle_filename,'w') as fh: @@ -1019,7 +1035,7 @@ component "data" value 3 def _writeArbdDihedralFile( self ): for b in list( set( [b for i,j,k,l,b in self.get_dihedrals()] ) ): - if type(b) is not str: + if type(b) is not str and not isinstance(b, Path): b.write_file() with open(self._dihedral_filename,'w') as fh: diff --git a/arbdmodel/abstract_polymer.py b/arbdmodel/abstract_polymer.py index 3a45e74697b56bb89a5610e59823b972b52b093e..3a25e1169435359859ca455af7c7ade53a51439d 100644 --- a/arbdmodel/abstract_polymer.py +++ b/arbdmodel/abstract_polymer.py @@ -1,67 +1,36 @@ -import pdb from pathlib import Path -import numpy as np -import random -from .model.arbdmodel import PointParticle, ParticleType, Group, ArbdModel -from .coords import rotationAboutAxis, quaternion_from_matrix, quaternion_to_matrix -from .model.nonbonded import * from copy import copy, deepcopy -from .model.nbPot import nbDnaScheme -from scipy.special import erf -import scipy.optimize as opt +import numpy as np from scipy import interpolate -from .model.CanonicalNucleotideAtoms import canonicalNtFwd, canonicalNtRev, seqComplement -from .model.CanonicalNucleotideAtoms import enmTemplateHC, enmTemplateSQ, enmCorrectionsHC +from . import ArbdModel +from .coords import rotationAboutAxis, quaternion_from_matrix, quaternion_to_matrix + -from .model.spring_from_lp import k_angle as angle_spring_from_lp -# import pdb """ TODO: - + fix handling of crossovers for atomic representation - + map to atomic representation - + add nicks - + transform ssDNA nucleotides - - shrink ssDNA - + shrink dsDNA backbone - + make orientation continuous - + sequence - + handle circular dna - + ensure crossover bead potentials aren't applied twice - + remove performance bottlenecks - test for large systems - + assign sequence - + ENM - rework Location class - remove recursive calls - document - develop unit test suite """ -class CircularDnaError(Exception): - pass -class ParticleNotConnectedError(Exception): - pass +# class ParticleNotConnectedError(Exception): +# pass class Location(): """ Site for connection within an object """ - def __init__(self, container, address, type_, on_fwd_strand = True): + def __init__(self, parent, contour_position, type_, on_fwd_strand = True): ## TODO: remove cyclic references(?) - self.container = container - self.address = address # represents position along contour length in segment - # assert( type_ in ("end3","end5") ) # TODO remove or make conditional - self.on_fwd_strand = on_fwd_strand + assert( isinstance(parent, ConnectableElement) ) + self.parent = parent + self.contour_position = contour_position # represents position along contour length in polymer self.type_ = type_ - self.particle = None + # self.particle = None self.connection = None - self.is_3prime_side_of_connection = None - - self.prev_in_strand = None - self.next_in_strand = None - - self.combine = None # some locations might be combined in bead model def get_connected_location(self): if self.connection is None: @@ -69,31 +38,30 @@ class Location(): else: return self.connection.other(self) - def set_connection(self, connection, is_3prime_side_of_connection): + def set_connection(self, connection): self.connection = connection # TODO weakref? - self.is_3prime_side_of_connection = is_3prime_side_of_connection - def get_nt_pos(self): + def get_monomer_index(self): try: - pos = self.container.contour_to_nt_pos(self.address, round_nt=True) + idx = self.parent.contour_to_monomer_index(self.contour_position, nearest_monomer=True) except: - if self.address == 0: - pos = 0 - elif self.address == 1: - pos = self.container.num_nt-1 + if self.contour_position == 0: + idx = 0 + elif self.contour_position == 1: + idx = self.parent.num_monomers-1 else: raise - return pos + return idx def __repr__(self): if self.on_fwd_strand: on_fwd = "on_fwd_strand" else: on_fwd = "on_rev_strand" - return "<Location {}.{}[{:.2f},{:d}]>".format( self.container.name, self.type_, self.address, self.on_fwd_strand) + return "<Location {}.{}[{:.2f},{:d}]>".format( self.parent.name, self.type_, self.contour_position, self.on_fwd_strand) class Connection(): - """ Abstract base class for connection between two elements """ + """ Class for connecting two elements """ def __init__(self, A, B, type_ = None): assert( isinstance(A,Location) ) assert( isinstance(B,Location) ) @@ -107,21 +75,18 @@ class Connection(): elif location is self.B: return self.A else: - raise Exception("OutOfBoundsError") + raise ValueError def delete(self): - self.A.container.connections.remove(self) - if self.B.container is not self.A.container: - self.B.container.connections.remove(self) + self.A.parent.connections.remove(self) + if self.B.parent is not self.A.parent: + self.B.parent.connections.remove(self) self.A.connection = None self.B.connection = None def __repr__(self): return "<Connection {}--{}--{}]>".format( self.A, self.type_, self.B ) - - -# class ConnectableElement(Transformable): class ConnectableElement(): """ Abstract base class """ def __init__(self, connection_locations=None, connections=None): @@ -143,11 +108,9 @@ class ConnectableElement(): assert( np.all( [counter[l] == 1 for l in locs] ) ) return locs - def get_location_at(self, address, on_fwd_strand=True, new_type="crossover"): + def get_location_at(self, contour_position, on_fwd_strand=True, new_type="crossover"): loc = None - if (self.num_nt == 1): - # import pdb - # pdb.set_trace() + if (self.num_monomers == 1): ## Assumes that intrahelical connections have been made before crossovers for l in self.locations: if l.on_fwd_strand == on_fwd_strand and l.connection is None: @@ -156,11 +119,11 @@ class ConnectableElement(): # assert( loc is not None ) else: for l in self.locations: - if l.address == address and l.on_fwd_strand == on_fwd_strand: + if l.contour_position == contour_position and l.on_fwd_strand == on_fwd_strand: assert(loc is None) loc = l if loc is None: - loc = Location( self, address=address, type_=new_type, on_fwd_strand=on_fwd_strand ) + loc = Location( self, contour_position=contour_position, type_=new_type, on_fwd_strand=on_fwd_strand ) return loc def get_connections_and_locations(self, connection_type=None, exclude=()): @@ -170,9 +133,9 @@ class ConnectableElement(): ret = [] for c in self.connections: if (type_ is None or c.type_ == type_) and c.type_ not in exclude: - if c.A.container is self: + if c.A.parent is self: ret.append( [c, c.A, c.B] ) - elif c.B.container is self: + elif c.B.parent is self: ret.append( [c, c.B, c.A] ) else: import pdb @@ -180,259 +143,56 @@ class ConnectableElement(): raise Exception("Object contains connection that fails to refer to object") return ret - def _connect(self, other, connection, in_3prime_direction=None): + def _connect(self, other, connection): ## TODO fix circular references A,B = [connection.A, connection.B] - if in_3prime_direction is not None: - A.is_3prime_side_of_connection = not in_3prime_direction - B.is_3prime_side_of_connection = in_3prime_direction A.connection = B.connection = connection self.connections.append(connection) if other is not self: other.connections.append(connection) else: - raise NotImplementedError("Segments cannot yet be connected to themselves; if you are attempting to make a circular object, try breaking the object into multiple segments") - l = A.container.locations + raise NotImplementedError("Objects cannot yet be connected to themselves; if you are attempting to make a circular object, try breaking the object into multiple components") + l = A.parent.locations if A not in l: l.append(A) - l = B.container.locations + l = B.parent.locations if B not in l: l.append(B) - - - # def _find_connections(self, loc): - # return [c for c in self.connections if c.A == loc or c.B == loc] - -class SegmentParticle(PointParticle): - def __init__(self, type_, position, name="A", **kwargs): - self.name = name - self.contour_position = None - PointParticle.__init__(self, type_, position, name=name, **kwargs) - self.intrahelical_neighbors = [] - self.other_neighbors = [] - self.locations = [] - - def get_intrahelical_above(self): - """ 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) > 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) < self.contour_position: - return b - - def _neighbor_should_be_added(self,b): - if type(self.parent) != type(b.parent): - return True - - c1 = self.contour_position - 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,c1) - if np.abs(c2-c1) < np.abs(c0-c1): - ## remove b0 - self.intrahelical_neighbors.remove(b0) - b0.intrahelical_neighbors.remove(self) - return True - else: - return False - return True - - def make_intrahelical_neighbor(self,b): - add1 = self._neighbor_should_be_added(b) - add2 = b._neighbor_should_be_added(self) - if add1 and add2: - # assert(len(b.intrahelical_neighbors) <= 1) - # assert(len(self.intrahelical_neighbors) <= 1) - self.intrahelical_neighbors.append(b) - b.intrahelical_neighbors.append(self) - - 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: - pos = self.contour_position - else: - pos = self.get_contour_position(seg,near_address) - return seg.contour_to_nt_pos(pos) - 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 - """ - - if seg == self.parent: - return self.contour_position - else: - cutoff = 30*3 - target_seg = seg - - ## depth-first search - ## TODO cache distances to nearby locations? - def descend_search_tree(seg, contour_in_seg, distance=0, visited_segs=None): - nonlocal cutoff - if visited_segs is None: visited_segs = [] - - if seg == target_seg: - # pdb.set_trace() - ## Found a segment in our target - sign = 1 if contour_in_seg == 1 else -1 - if sign == -1: assert( contour_in_seg == 0 ) - if distance < cutoff: # TODO: check if this does anything - cutoff = distance - return [[distance, contour_in_seg+sign*seg.nt_pos_to_contour(distance)]], [(seg, contour_in_seg, distance)] - if distance > cutoff: - return None,None - - ret_list = [] - hist_list = [] - ## Find intrahelical locations in seg that we might pass through - conn_locs = seg.get_connections_and_locations("intrahelical") - if isinstance(target_seg, SingleStrandedSegment): - tmp = seg.get_connections_and_locations("sscrossover") - conn_locs = conn_locs + list(filter(lambda x: x[2].container == target_seg, tmp)) - for c,A,B in conn_locs: - if B.container in visited_segs: continue - dx = seg.contour_to_nt_pos( A.address, round_nt=False ) - seg.contour_to_nt_pos( contour_in_seg, round_nt=False) - dx = np.abs(dx) - results,history = descend_search_tree( B.container, B.address, - distance+dx, visited_segs + [seg] ) - if results is not None: - ret_list.extend( results ) - hist_list.extend( history ) - return ret_list,hist_list - - 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 - 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) - - def update_position(self, contour_position): - self.contour_position = contour_position - self.position = self.parent.contour_to_position(contour_position) - if 'orientation_bead' in self.__dict__: - o = self.orientation_bead - o.contour_position = contour_position - orientation = self.parent.contour_to_orientation(contour_position) - if orientation is None: - print("WARNING: local_twist is True, but orientation is None; using identity") - orientation = np.eye(3) - o.position = self.position + orientation.dot( np.array((Segment.orientation_bond.r0,0,0)) ) - - def __repr__(self): - return "<SegmentParticle {} on {}[{:.2f}]>".format( self.name, self.parent, self.contour_position) +class PolymerSection(ConnectableElement): + """ Base class that describes a linear section of a polymer """ -## TODO break this class into smaller, better encapsulated pieces -class Segment(ConnectableElement, Group): - - """ Base class that describes a segment of DNA. When built from - cadnano models, should not span helices """ - - """Define basic particle types""" - dsDNA_particle = ParticleType("D", - diffusivity = 43.5, - mass = 300, - radius = 3, - ) - orientation_particle = ParticleType("O", - diffusivity = 100, - mass = 300, - radius = 1, - ) - - # orientation_bond = HarmonicBond(10,2) - orientation_bond = HarmonicBond(30,1.5, rRange = (0,500) ) - - ssDNA_particle = ParticleType("S", - diffusivity = 43.5, - mass = 150, - radius = 3, - ) - - def __init__(self, name, num_nt, + def __init__(self, name, num_monomers, + monomer_length, start_position = None, end_position = None, - segment_model = None, + parent = None, **kwargs): - - if start_position is None: start_position = np.array((0,0,0)) - - Group.__init__(self, name, children=[], **kwargs) + ConnectableElement.__init__(self, connection_locations=[], connections=[]) if 'segname' not in kwargs: self.segname = name - # self.resname = name - self.start_orientation = None - self.twist_per_nt = 0 - - self.beads = [c for c in self.children] # self.beads will not contain orientation beads - self._bead_model_generation = 0 # TODO: remove? - self.segment_model = segment_model # TODO: remove? - - self.strand_pieces = dict() - for d in ('fwd','rev'): - self.strand_pieces[d] = [] + self.num_monomers = int(num_monomers) + self.monomer_length = monomer_length + + if start_position is None: start_position = np.array((0,0,0)) - self.num_nt = int(num_nt) if end_position is None: - end_position = np.array((0,0,self.distance_per_nt*num_nt)) + start_position + end_position = np.array((0,0,self.monomer_length*num_monomers)) + start_position self.start_position = start_position self.end_position = end_position - ## Used to assign cadnano names to beads - self._generate_bead_callbacks = [] - self._generate_nucleotide_callbacks = [] + self.start_orientation = None ## Set up interpolation for positions self._set_splines_from_ends() - self.sequence = None + # self.sequence = None def __repr__(self): - return "<{} {}[{:d}]>".format( type(self), self.name, self.num_nt ) + return "<{} {}[{:d}]>".format( type(self), self.name, self.num_monomers ) def set_splines(self, contours, coords): tck, u = interpolate.splprep( coords.T, u=contours, s=0, k=1) @@ -447,65 +207,65 @@ class Segment(ConnectableElement, Group): return np.mean(self.contour_to_position(u), axis=0) def _get_location_positions(self): - return [self.contour_to_nt_pos(l.address) for l in self.locations] + return [self.contour_to_monomer_index(l.contour_position) for l in self.locations] - def insert_dna(self, at_nt: int, num_nt: int, seq=tuple()): - assert(np.isclose(np.around(num_nt),num_nt)) - if at_nt < 0: + def insert_monomers(self, at_monomer: int, num_monomer: int, seq=tuple()): + assert(np.isclose(np.around(num_monomer),num_monomer)) + if at_monomer < 0: raise ValueError("Attempted to insert DNA into {} at a negative location".format(self)) - if at_nt > self.num_nt-1: - raise ValueError("Attempted to insert DNA into {} at beyond the end of the Segment".format(self)) - if num_nt < 0: + if at_monomer > self.num_monomer-1: + raise ValueError("Attempted to insert DNA into {} at beyond the end of the Polymer".format(self)) + if num_monomers < 0: raise ValueError("Attempted to insert DNA a negative amount of DNA into {}".format(self)) - num_nt = np.around(num_nt) - nt_positions = self._get_location_positions() - new_nt_positions = [p if p <= at_nt else p+num_nt for p in nt_positions] + num_monomers = np.around(num_monomer) + monomer_positions = self._get_location_positions() + new_monomer_positions = [p if p <= at_monomer else p+num_monomers for p in monomer_positions] ## TODO: handle sequence - self.num_nt = self.num_nt+num_nt + self.num_monomers = self.num_monomer+num_monomer - for l,p in zip(self.locations, new_nt_positions): - l.address = self.nt_pos_to_contour(p) + for l,p in zip(self.locations, new_monomer_positions): + l.contour_position = self.monomer_index_to_contour(p) - def remove_dna(self, first_nt: int, last_nt: int): - """ Removes nucleotides between first_nt and last_nt, inclusive """ - assert(np.isclose(np.around(first_nt),first_nt)) - assert(np.isclose(np.around(last_nt),last_nt)) - tmp = min((first_nt,last_nt)) - last_nt = max((first_nt,last_nt)) - fist_nt = tmp + def remove_monomers(self, first_monomer: int, last_monomer: int): + """ Removes nucleotides between first_monomer and last_monomer, inclusive """ + assert(np.isclose(np.around(first_monomer),first_monomer)) + assert(np.isclose(np.around(last_monomer),last_monomer)) + tmp = min((first_monomer,last_monomer)) + last_monomer = max((first_monomer,last_monomer)) + fist_monomer = tmp - if first_nt < 0 or first_nt > self.num_nt-2: - raise ValueError("Attempted to remove DNA from {} starting at an invalid location {}".format(self, first_nt)) - if last_nt < 1 or last_nt > self.num_nt-1: - raise ValueError("Attempted to remove DNA from {} ending at an invalid location {}".format(self, last_nt)) - if first_nt == last_nt: + if first_monomer < 0 or first_monomer > self.num_monomer-2: + raise ValueError("Attempted to remove DNA from {} starting at an invalid location {}".format(self, first_monomer)) + if last_monomer < 1 or last_monomer > self.num_monomer-1: + raise ValueError("Attempted to remove DNA from {} ending at an invalid location {}".format(self, last_monomer)) + if first_monomer == last_monomer: return - first_nt = np.around(first_nt) - last_nt = np.around(last_nt) + first_monomer = np.around(first_monomer) + last_monomer = np.around(last_monomer) - nt_positions = self._get_location_positions() + monomer_positions = self._get_location_positions() - bad_locations = list(filter(lambda p: p >= first_nt and p <= last_nt, nt_positions)) + bad_locations = list(filter(lambda p: p >= first_monomer and p <= last_monomer, monomer_positions)) if len(bad_locations) > 0: - raise Exception("Attempted to remove DNA containing locations {} from {} between {} and {}".format(bad_locations,self,first_nt,last_nt)) + raise Exception("Attempted to remove DNA containing locations {} from {} between {} and {}".format(bad_locations,self,first_monomer,last_monomer)) - removed_nt = last_nt-first_nt+1 - new_nt_positions = [p if p <= last_nt else p-removed_nt for p in nt_positions] - num_nt = self.num_nt-removed_nt + removed_monomer = last_monomer-first_monomer+1 + new_monomer_positions = [p if p <= last_monomer else p-removed_monomer for p in monomer_positions] + num_monomers = self.num_monomer-removed_monomer - if self.sequence is not None and len(self.sequence) == self.num_nt: - self.sequence = [s for s,i in zip(self.sequence,range(self.num_nt)) - if i < first_nt or i > last_nt] - assert( len(self.sequence) == num_nt ) + if self.sequence is not None and len(self.sequence) == self.num_monomer: + self.sequence = [s for s,i in zip(self.sequence,range(self.num_monomer)) + if i < first_monomer or i > last_monomer] + assert( len(self.sequence) == num_monomers ) - self.num_nt = num_nt + self.num_monomers = num_monomers - for l,p in zip(self.locations, new_nt_positions): - l.address = self.nt_pos_to_contour(p) + for l,p in zip(self.locations, new_monomer_positions): + l.contour_position = self.monomer_position_to_contour(p) def __filter_contours(contours, positions, position_filter, contour_filter): u = contours @@ -524,7 +284,7 @@ class Segment(ConnectableElement, Group): tck, u = self.position_spline_params r = self.contour_to_position(u) - ids = Segment.__filter_contours(u, r, position_filter, contour_filter) + ids = PolymerSection.__filter_contours(u, r, position_filter, contour_filter) if len(ids) == 0: return ## Translate @@ -535,7 +295,7 @@ class Segment(ConnectableElement, Group): tck, u = self.position_spline_params r = self.contour_to_position(u) - ids = Segment.__filter_contours(u, r, position_filter, contour_filter) + ids = PolymerSection.__filter_contours(u, r, position_filter, contour_filter) if len(ids) == 0: return if about is None: @@ -561,29 +321,20 @@ class Segment(ConnectableElement, Group): self.quaternion_spline_params = None r0 = np.array(self.start_position)[np.newaxis,:] r1 = np.array(self.end_position)[np.newaxis,:] - u = np.linspace(0,1, max(3,self.num_nt//int(resolution))) + u = np.linspace(0,1, max(3,self.num_monomers//int(resolution))) s = u[:,np.newaxis] coords = (1-s)*r0 + s*r1 self.set_splines(u, coords) - def clear_all(self): - Group.clear_all(self) # TODO: use super? - self.beads = [] - # for c,loc,other in self.get_connections_and_locations(): - # loc.particle = None - # other.particle = None - for l in self.locations: - l.particle = None - - def contour_to_nt_pos(self, contour_pos, round_nt=False): - nt = contour_pos*(self.num_nt) - 0.5 - if round_nt: - assert( np.isclose(np.around(nt),nt) ) - nt = np.around(nt) - return nt - - def nt_pos_to_contour(self,nt_pos): - return (nt_pos+0.5)/(self.num_nt) + def contour_to_monomer_index(self, contour_pos, nearest_monomer=False): + idx = contour_pos*(self.num_monomer) - 0.5 + if nearest_monomer: + assert( np.isclose(np.around(idx),idx) ) + idx = np.around(idx) + return idx + + def monomer_index_to_contour(self,monomer_pos): + return (monomer_pos+0.5)/(self.num_monomers) def contour_to_position(self,s): p = interpolate.splev( s, self.position_spline_params[0] ) @@ -616,8 +367,9 @@ class Segment(ConnectableElement, Group): if self.start_orientation is not None: orientation0 = orientation0.dot(self.start_orientation) - orientation = rotationAboutAxis( axis, self.twist_per_nt*self.contour_to_nt_pos(s), normalizeAxis=False ) - orientation = orientation.dot(orientation0) + # orientation = rotationAboutAxis( axis, self.twist_per_nt*self.contour_to_monomer_index(s), normalizeAxis=False ) + # orientation = orientation.dot(orientation0) + orientation = orientation0 else: q = interpolate.splev( s, self.quaternion_spline_params[0] ) if len(q) > 1: q = np.array(q).T # TODO: is this needed? @@ -626,2336 +378,101 @@ class Segment(ConnectableElement, Group): return orientation def get_contour_sorted_connections_and_locations(self,type_): - sort_fn = lambda c: c[1].address + sort_fn = lambda c: c[1].contour_position cl = self.get_connections_and_locations(type_) return sorted(cl, key=sort_fn) - def randomize_unset_sequence(self): - bases = list(seqComplement.keys()) - # bases = ['T'] ## FOR DEBUG - if self.sequence is None: - self.sequence = [random.choice(bases) for i in range(self.num_nt)] - else: - assert(len(self.sequence) == self.num_nt) # TODO move - for i in range(len(self.sequence)): - if self.sequence[i] is None: - self.sequence[i] = random.choice(bases) - - def _get_num_beads(self, max_basepairs_per_bead, max_nucleotides_per_bead ): - raise NotImplementedError - - def _generate_one_bead(self, contour_position, nts): - raise NotImplementedError - - def _generate_atomic_nucleotide(self, contour_position, is_fwd, seq, scale, strand_segment): - """ Seq should include modifications like 5T, T3 Tsinglet; direction matters too """ - - # print("Generating nucleotide at {}".format(contour_position)) - - pos = self.contour_to_position(contour_position) - orientation = self.contour_to_orientation(contour_position) - - """ deleteme - ## TODO: move this code (?) - if orientation is None: - import pdb - pdb.set_trace() - axis = self.contour_to_tangent(contour_position) - angleVec = np.array([1,0,0]) - if axis.dot(angleVec) > 0.9: angleVec = np.array([0,1,0]) - angleVec = angleVec - angleVec.dot(axis)*axis - angleVec = angleVec/np.linalg.norm(angleVec) - y = np.cross(axis,angleVec) - orientation = np.array([angleVec,y,axis]).T - ## TODO: improve placement of ssDNA - # rot = rotationAboutAxis( axis, contour_position*self.twist_per_nt*self.num_nt, normalizeAxis=True ) - # orientation = rot.dot(orientation) - else: - orientation = orientation - """ - key = seq - nt_dict = canonicalNtFwd if is_fwd else canonicalNtRev - - atoms = nt_dict[ key ].generate() # TODO: clone? - atoms.orientation = orientation.dot(atoms.orientation) - if isinstance(self, SingleStrandedSegment): - if scale is not None and scale != 1: - for a in atoms: - a.position = scale*a.position - atoms.position = pos - atoms.atoms_by_name["C1'"].collapsedPosition() - else: - if scale is not None and scale != 1: - if atoms.sequence in ("A","G"): - r0 = atoms.atoms_by_name["N9"].position - else: - r0 = atoms.atoms_by_name["N1"].position - for a in atoms: - if a.name[-1] in ("'","P","T"): - a.position = scale*(a.position-r0) + r0 - else: - a.fixed = 1 - atoms.position = pos - - atoms.contour_position = contour_position - strand_segment.add(atoms) - - for callback in self._generate_nucleotide_callbacks: - callback(atoms) - - return atoms - def add_location(self, nt, type_, on_fwd_strand=True): - ## Create location if needed, add to segment - c = self.nt_pos_to_contour(nt) + ## Create location if needed, add to polymer + c = self.monomer_index_to_contour(nt) assert(c >= 0 and c <= 1) - # TODO? loc = self.Location( address=c, type_=type_, on_fwd_strand=is_fwd ) - loc = Location( self, address=c, type_=type_, on_fwd_strand=on_fwd_strand ) + # TODO? loc = self.Location( contour_position=c, type_=type_, on_fwd_strand=is_fwd ) + loc = Location( self, contour_position=c, type_=type_, on_fwd_strand=on_fwd_strand ) self.locations.append(loc) - ## TODO? Replace with abstract strand-based model? - - def add_nick(self, nt, on_fwd_strand=True): - self.add_3prime(nt,on_fwd_strand) - self.add_5prime(nt+1,on_fwd_strand) - - def add_5prime(self, nt, on_fwd_strand=True): - if isinstance(self,SingleStrandedSegment): - on_fwd_strand = True - self.add_location(nt,"5prime",on_fwd_strand) - - def add_3prime(self, nt, on_fwd_strand=True): - if isinstance(self,SingleStrandedSegment): - on_fwd_strand = True - self.add_location(nt,"3prime",on_fwd_strand) - - def get_3prime_locations(self): - return sorted(self.get_locations("3prime"),key=lambda x: x.address) - - def get_5prime_locations(self): - ## TODO? ensure that data is consistent before _build_model calls - return sorted(self.get_locations("5prime"),key=lambda x: x.address) - def iterate_connections_and_locations(self, reverse=False): - ## connections to other segments + ## connections to other polymers cl = self.get_contour_sorted_connections_and_locations() if reverse: cl = cl[::-1] for c in cl: yield c - - ## TODO rename - def _add_strand_piece(self, strand_piece): - """ Registers a strand segment within this object """ - - ## TODO use weakref - d = 'fwd' if strand_piece.is_fwd else 'rev' - - ## Validate strand_piece (ensure no clashes) - for s in self.strand_pieces[d]: - l,h = sorted((s.start,s.end)) - for value in (strand_piece.start,strand_piece.end): - assert( value < l or value > h ) - - ## Add strand_piece in correct order - self.strand_pieces[d].append(strand_piece) - self.strand_pieces[d] = sorted(self.strand_pieces[d], - key = lambda x: x.start) - - ## TODO rename - def get_strand_segment(self, nt_pos, is_fwd, move_at_least=0.5): - """ Walks through locations, checking for crossovers """ - # if self.name in ("6-1","1-1"): - # import pdb - # pdb.set_trace() - move_at_least = 0 - - ## Iterate through locations - # locations = sorted(self.locations, key=lambda l:(l.address,not l.on_fwd_strand), reverse=(not is_fwd)) - def loc_rank(l): - nt = l.get_nt_pos() - ## optionally add logic about type of connection - return (nt, not l.on_fwd_strand) - # locations = sorted(self.locations, key=lambda l:(l.address,not l.on_fwd_strand), reverse=(not is_fwd)) - locations = sorted(self.locations, key=loc_rank, reverse=(not is_fwd)) - # print(locations) - - for l in locations: - # TODOTODO probably okay - if l.address == 0: - pos = 0.0 - elif l.address == 1: - pos = self.num_nt-1 - else: - pos = self.contour_to_nt_pos(l.address, round_nt=True) - - ## DEBUG - - - ## Skip locations encountered before our strand - # tol = 0.1 - # if is_fwd: - # if pos-nt_pos <= tol: continue - # elif nt_pos-pos <= tol: continue - if (pos-nt_pos)*(2*is_fwd-1) < move_at_least: continue - ## TODO: remove move_at_least - if np.isclose(pos,nt_pos): - if l.is_3prime_side_of_connection: continue - - ## Stop if we found the 3prime end - if l.on_fwd_strand == is_fwd and l.type_ == "3prime" and l.connection is None: - # print(" found end at",l) - return pos, None, None, None, None - - ## Check location connections - c = l.connection - if c is None: continue - B = c.other(l) - - ## Found a location on the same strand? - if l.on_fwd_strand == is_fwd: - # print(" passing through",l) - # print("from {}, connection {} to {}".format(nt_pos,l,B)) - Bpos = B.get_nt_pos() - return pos, B.container, Bpos, B.on_fwd_strand, 0.5 - - ## Stop at other strand crossovers so basepairs line up - elif c.type_ == "crossover": - if nt_pos == pos: continue - # print(" pausing at",l) - return pos, l.container, pos+(2*is_fwd-1), is_fwd, 0 - - raise Exception("Shouldn't be here") - # print("Shouldn't be here") - ## Made it to the end of the segment without finding a connection - return 1*is_fwd, None, None, None - - def get_nearest_bead(self, contour_position): - if len(self.beads) < 1: return None - cs = np.array([b.contour_position for b in self.beads]) # TODO: cache - # TODO: include beads in connections? - i = np.argmin((cs - contour_position)**2) - - return self.beads[i] - - def _get_atomic_nucleotide(self, nucleotide_idx, is_fwd=True): - d = 'fwd' if is_fwd else 'rev' - for s in self.strand_pieces[d]: - try: - return s.get_nucleotide(nucleotide_idx) - except: - pass - raise Exception("Could not find nucleotide in {} at {}.{}".format( self, nucleotide_idx, d )) - - def get_all_consecutive_beads(self, number): - assert(number >= 1) - ## Assume that consecutive beads in self.beads are bonded - ret = [] - for i in range(len(self.beads)-number+1): - tmp = [self.beads[i+j] for j in range(0,number)] - ret.append( tmp ) - return ret - - def _add_bead(self,b): - - # assert(b.parent is None) - if b.parent is not None: - b.parent.children.remove(b) - self.add(b) - self.beads.append(b) # don't add orientation bead - if "orientation_bead" in b.__dict__: # TODO: think of a cleaner approach - o = b.orientation_bead - o.contour_position = b.contour_position - if o.parent is not None: - o.parent.children.remove(o) - self.add(o) - self.add_bond(b,o, Segment.orientation_bond, exclude=True) - - def _rebuild_children(self, new_children): - # print("_rebuild_children on %s" % self.name) - old_children = self.children - old_beads = self.beads - self.children = [] - self.beads = [] - - if True: - ## TODO: remove this if duplicates are never found - # print("Searching for duplicate particles...") - ## Remove duplicates, preserving order - tmp = [] - for c in new_children: - if c not in tmp: - tmp.append(c) - else: - print(" DUPLICATE PARTICLE FOUND!") - new_children = tmp - - for b in new_children: - self.beads.append(b) - self.children.append(b) - if "orientation_bead" in b.__dict__: # TODO: think of a cleaner approach - self.children.append(b.orientation_bead) - # tmp = [c for c in self.children if c not in old_children] - # assert(len(tmp) == 0) - # tmp = [c for c in old_children if c not in self.children] - # assert(len(tmp) == 0) - assert(len(old_children) == len(self.children)) - assert(len(old_beads) == len(self.beads)) - - - def _generate_beads(self, bead_model, max_basepairs_per_bead, max_nucleotides_per_bead): - - """ Generate beads (positions, types, etc) and bonds, angles, dihedrals, exclusions """ - ## TODO: decide whether to remove bead_model argument - ## (currently unused) - - ## First find points between-which beads must be generated - # conn_locs = self.get_contour_sorted_connections_and_locations() - # locs = [A for c,A,B in conn_locs] - # existing_beads = [l.particle for l in locs if l.particle is not None] - # if self.name == "S001": - # pdb.set_trace() - - # 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,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][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,0) + existing_beads - - 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,1) ) - assert(len(existing_beads) > 1) - - ## Walk through existing_beads, add beads between - tmp_children = [] # build list of children in nice order - last = None - - for I in range(len(existing_beads)-1): - 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 = 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 - if num_beads == 0 and isinstance(self,SingleStrandedSegment) and isinstance(eb1.parent,DoubleStrandedSegment) and isinstance(eb2.parent,DoubleStrandedSegment): - num_beads = 1 - ## TODO similarly ensure there is a dsDNA bead between ssDNA beads - - ds = e_ds / (num_beads+1) - nts = ds*self.num_nt - eb1.num_nt += 0.5*nts - eb2.num_nt += 0.5*nts - - ## Add beads - if eb1.parent == self: - tmp_children.append(eb1) - - s0 = ec1 - if last is not None: - last.make_intrahelical_neighbor(eb1) - last = eb1 - for j in range(num_beads): - s = ds*(j+1) + s0 - # if self.name in ("51-2","51-3"): - # if self.name in ("31-2",): - # print(" adding bead at {}".format(s)) - b = self._generate_one_bead(s,nts) - - last.make_intrahelical_neighbor(b) - last = b - tmp_children.append(b) - - last.make_intrahelical_neighbor(eb2) - - if eb2.parent == self: - tmp_children.append(eb2) - # if self.name in ("31-2",): - # pdb.set_trace() - self._rebuild_children(tmp_children) - - for callback in self._generate_bead_callbacks: - callback(self) - - def _regenerate_beads(self, max_nts_per_bead=4, ): - ... - - -class DoubleStrandedSegment(Segment): - - """ Class that describes a segment of ssDNA. When built from - cadnano models, should not span helices """ - - def __init__(self, name, num_bp, start_position = np.array((0,0,0)), - end_position = None, - segment_model = None, - local_twist = False, - num_turns = None, - start_orientation = None, - twist_persistence_length = 90, +class AbstractPolymerGroup(): + def __init__(self, polymers=[], **kwargs): - - self.helical_rise = 10.44 - self.distance_per_nt = 3.4 - Segment.__init__(self, name, num_bp, - start_position, - end_position, - segment_model, - **kwargs) - self.num_bp = self.num_nt - - self.local_twist = local_twist - if num_turns is None: - num_turns = float(num_bp) / self.helical_rise - self.twist_per_nt = float(360 * num_turns) / num_bp - - if start_orientation is None: - start_orientation = np.eye(3) # np.array(((1,0,0),(0,1,0),(0,0,1))) - self.start_orientation = start_orientation - self.twist_persistence_length = twist_persistence_length - - self.nicks = [] - - self.start = self.start5 = Location( self, address=0, type_= "end5" ) - self.start3 = Location( self, address=0, type_ = "end3", on_fwd_strand=False ) - - self.end = self.end3 = Location( self, address=1, type_ = "end3" ) - self.end5 = Location( self, address=1, type_= "end5", on_fwd_strand=False ) - # for l in (self.start5,self.start3,self.end3,self.end5): - # self.locations.append(l) - - ## TODO: initialize sensible spline for orientation - - ## Convenience methods - ## TODO: add errors if unrealistic connections are made - ## TODO: make connections automatically between unconnected strands - def connect_start5(self, end3, type_="intrahelical", force_connection=False): - if isinstance(end3, SingleStrandedSegment): - end3 = end3.end3 - self._connect_ends( self.start5, end3, type_, force_connection = force_connection ) - def connect_start3(self, end5, type_="intrahelical", force_connection=False): - if isinstance(end5, SingleStrandedSegment): - end5 = end5.start5 - self._connect_ends( self.start3, end5, type_, force_connection = force_connection ) - def connect_end3(self, end5, type_="intrahelical", force_connection=False): - if isinstance(end5, SingleStrandedSegment): - end5 = end5.start5 - self._connect_ends( self.end3, end5, type_, force_connection = force_connection ) - def connect_end5(self, end3, type_="intrahelical", force_connection=False): - if isinstance(end3, SingleStrandedSegment): - end3 = end3.end3 - self._connect_ends( self.end5, end3, type_, force_connection = force_connection ) - - def add_crossover(self, nt, other, other_nt, strands_fwd=(True,False), nt_on_5prime=True, type_="crossover"): - """ Add a crossover between two helices """ - ## Validate other, nt, other_nt - ## TODO - - if isinstance(other,SingleStrandedSegment): - other.add_crossover(other_nt, self, nt, strands_fwd[::-1], not nt_on_5prime) - else: - - ## Create locations, connections and add to segments - c = self.nt_pos_to_contour(nt) - assert(c >= 0 and c <= 1) - - loc = self.get_location_at(c, strands_fwd[0]) - - c = other.nt_pos_to_contour(other_nt) - # TODOTODO: may need to subtract or add a little depending on 3prime/5prime - assert(c >= 0 and c <= 1) - other_loc = other.get_location_at(c, strands_fwd[1]) - self._connect(other, Connection( loc, other_loc, type_=type_ )) - if nt_on_5prime: - loc.is_3prime_side_of_connection = False - other_loc.is_3prime_side_of_connection = True - else: - loc.is_3prime_side_of_connection = True - other_loc.is_3prime_side_of_connection = False - - ## Real work - def _connect_ends(self, end1, end2, type_, force_connection): - debug = False - ## TODO remove self? - ## validate the input - for end in (end1, end2): - assert( isinstance(end, Location) ) - assert( end.type_ in ("end3","end5") ) - assert( end1.type_ != end2.type_ ) - - ## Remove other connections involving these points - if end1.connection is not None: - if debug: print("WARNING: reconnecting {}".format(end1)) - end1.connection.delete() - if end2.connection is not None: - if debug: print("WARNING: reconnecting {}".format(end2)) - end2.connection.delete() - - ## Create and add connection - if end2.type_ == "end5": - end1.container._connect( end2.container, Connection( end1, end2, type_=type_ ), in_3prime_direction=True ) - else: - end2.container._connect( end1.container, Connection( end2, end1, type_=type_ ), in_3prime_direction=True ) - def _get_num_beads(self, contour, max_basepairs_per_bead, max_nucleotides_per_bead): - # return int(contour*self.num_nt // max_basepairs_per_bead) - return int(contour*(self.num_nt**2/(self.num_nt+1)) // max_basepairs_per_bead) - - def _generate_one_bead(self, contour_position, nts): - pos = self.contour_to_position(contour_position) - if self.local_twist: - orientation = self.contour_to_orientation(contour_position) - if orientation is None: - print("WARNING: local_twist is True, but orientation is None; using identity") - orientation = np.eye(3) - opos = pos + orientation.dot( np.array((Segment.orientation_bond.r0,0,0)) ) - # if np.linalg.norm(pos) > 1e3: - # pdb.set_trace() - assert(np.linalg.norm(opos-pos) < 10 ) - o = SegmentParticle( Segment.orientation_particle, opos, name="O", - contour_position = contour_position, - num_nt=nts, parent=self ) - bead = SegmentParticle( Segment.dsDNA_particle, pos, name="DNA", - num_nt=nts, parent=self, - orientation_bead=o, - contour_position=contour_position ) - - else: - bead = SegmentParticle( Segment.dsDNA_particle, pos, name="DNA", - num_nt=nts, parent=self, - contour_position=contour_position ) - self._add_bead(bead) - return bead - -class SingleStrandedSegment(Segment): - - """ Class that describes a segment of ssDNA. When built from - cadnano models, should not span helices """ - - def __init__(self, name, num_nt, start_position = None, - end_position = None, - segment_model = None, - **kwargs): - - if start_position is None: start_position = np.array((0,0,0)) - self.distance_per_nt = 5 - Segment.__init__(self, name, num_nt, - start_position, - end_position, - segment_model, - **kwargs) - - self.start = self.start5 = Location( self, address=0, type_= "end5" ) # TODO change type_? - self.end = self.end3 = Location( self, address=1, type_ = "end3" ) - # for l in (self.start5,self.end3): - # self.locations.append(l) - - def connect_end3(self, end5, force_connection=False): - self._connect_end( end5, _5_to_3 = True, force_connection = force_connection ) - - def connect_start5(self, end3, force_connection=False): - self._connect_end( end3, _5_to_3 = False, force_connection = force_connection ) - - def connect_5end(self, end3, force_connection=False): # TODO: change name or possibly deprecate - print("WARNING: 'connect_5end' will be deprecated") - return self.connect_start5( end3, force_connection=False) - - def _connect_end(self, other, _5_to_3, force_connection): - assert( isinstance(other, Location) ) - if _5_to_3 == True: - seg1 = self - seg2 = other.container - end1 = self.end3 - end2 = other - assert(other.type_ != "end3") - # if (other.type_ is not "end5"): - # print("WARNING: code does not prevent connecting 3prime to 3prime, etc") - else: - seg1 = other.container - seg2 = self - end1 = other - end2 = self.start - assert(other.type_ != "end5") - # if (other.type_ is not "end3"): - # print("WARNING: code does not prevent connecting 3prime to 3prime, etc") - - ## Remove other connections involving these points - if end1.connection is not None: - print("WARNING: reconnecting {}".format(end1)) - end1.connection.delete() - if end2.connection is not None: - print("WARNING: reconnecting {}".format(end2)) - end2.connection.delete() - - conn = Connection( end1, end2, type_="intrahelical" ) - seg1._connect( seg2, conn, in_3prime_direction=True ) - - - def add_crossover(self, nt, other, other_nt, strands_fwd=(True,False), nt_on_5prime=True, type_='sscrossover'): - """ Add a crossover between two helices """ - ## TODO Validate other, nt, other_nt - - assert(nt < self.num_nt) - assert(other_nt < other.num_nt) - if nt in (0,1,self.num_nt-1) and other_nt in (0,1,other.num_nt-1): - if nt_on_5prime == True: - other_end = other.start5 if strands_fwd[1] else other.end5 - self.connect_end3( other_end ) - else: - other_end = other.end3 if strands_fwd[1] else other.start3 - self.connect_start5( other_end ) - return - - # c1 = self.nt_pos_to_contour(nt) - # # TODOTODO - # ## Ensure connections occur at ends, otherwise the structure doesn't make sense - # # assert(np.isclose(c1,0) or np.isclose(c1,1)) - # assert(np.isclose(nt,0) or np.isclose(nt,self.num_nt-1)) - if nt == 0 and (self.num_nt > 1 or not nt_on_5prime): - c1 = 0 - elif nt == self.num_nt-1: - c1 = 1 - else: - raise Exception("Crossovers can only be at the ends of an ssDNA segment") - loc = self.get_location_at(c1, True) - - if other_nt == 0: - c2 = 0 - elif other_nt == other.num_nt-1: - c2 = 1 - else: - c2 = other.nt_pos_to_contour(other_nt) - - if isinstance(other,SingleStrandedSegment): - ## Ensure connections occur at opposing ends - assert(np.isclose(other_nt,0) or np.isclose(other_nt,self.num_nt-1)) - other_loc = other.get_location_at( c2, True ) - # if ("22-2" in (self.name, other.name)): - # pdb.set_trace() - if nt_on_5prime: - self.connect_end3( other_loc ) - else: - other.connect_end3( self ) - - else: - assert(c2 >= 0 and c2 <= 1) - other_loc = other.get_location_at( c2, strands_fwd[1] ) - if nt_on_5prime: - self._connect(other, Connection( loc, other_loc, type_="sscrossover" ), in_3prime_direction=True ) - else: - other._connect(self, Connection( other_loc, loc, type_="sscrossover" ), in_3prime_direction=True ) - - def _get_num_beads(self, contour, max_basepairs_per_bead, max_nucleotides_per_bead): - return int(contour*(self.num_nt**2/(self.num_nt+1)) // max_basepairs_per_bead) - # return int(contour*self.num_nt // max_nucleotides_per_bead) - - def _generate_one_bead(self, contour_position, nts): - pos = self.contour_to_position(contour_position) - b = SegmentParticle( Segment.ssDNA_particle, pos, - name="NAS", - num_nt=nts, parent=self, - contour_position=contour_position ) - self._add_bead(b) - return b - -class StrandInSegment(Group): - """ Represents a piece of an ssDNA strand within a segment """ - - def __init__(self, segment, start, end, is_fwd): - """ start/end should be provided expressed in nt coordinates, is_fwd tuples """ - Group.__init__(self) - self.num_nt = 0 - # self.sequence = [] - self.segment = segment - self.start = start - self.end = end - self.is_fwd = is_fwd - - nts = np.abs(end-start)+1 - self.num_nt = int(round(nts)) - assert( np.isclose(self.num_nt,nts) ) - segment._add_strand_piece(self) - - def _nucleotide_ids(self): - nt0 = self.start # seg.contour_to_nt_pos(self.start) - assert( np.abs(nt0 - round(nt0)) < 1e-5 ) - nt0 = int(round(nt0)) - assert( (self.end-self.start) >= 0 or not self.is_fwd ) - - direction = (2*self.is_fwd-1) - return range(nt0,nt0 + direction*self.num_nt, direction) - - def get_sequence(self): - """ return 5-to-3 """ - # TODOTODO test - seg = self.segment - if self.is_fwd: - return [seg.sequence[nt] for nt in self._nucleotide_ids()] - else: - return [seqComplement[seg.sequence[nt]] for nt in self._nucleotide_ids()] - - def get_contour_points(self): - c0,c1 = [self.segment.nt_pos_to_contour(p) for p in (self.start,self.end)] - return np.linspace(c0,c1,self.num_nt) - - def get_nucleotide(self, idx): - """ idx expressed as nt coordinate within segment """ - - lo,hi = sorted((self.start,self.end)) - if self.is_fwd: - idx_in_strand = idx - lo - else: - idx_in_strand = hi - idx - assert( np.isclose( idx_in_strand , int(round(idx_in_strand)) ) ) - assert(idx_in_strand >= 0) - return self.children[int(round(idx_in_strand))] - def __repr__(self): - return "<StrandInSegment {}{}[{:.2f}-{:.2f},{:d}]>".format( self.parent.segname, self.segment.name, self.start, self.end, self.is_fwd) - -class Strand(Group): - """ Represents an entire ssDNA strand from 5' to 3' as it routes through segments """ - def __init__(self, segname = None, is_circular = False): - Group.__init__(self) - self.num_nt = 0 - self.children = self.strand_segments = [] - self.segname = segname - self.is_circular = is_circular - self.debug = False + """ Simulation system parameters """ + ## TODO decide whether to move these elsewhere + # self.dimensions = dimensions + # self.temperature = temperature + # self.timestep = timestep + # self.cutoff = cutoff + # self.decomposition_period = decomposition_period + # self.pairlist_distance = pairlist_distance + # self.nonbonded_resolution = nonbonded_resolution + self.polymers = polymers - def __repr__(self): - return "<Strand {}({})>".format( self.segname, self.num_nt ) - - ## TODO disambiguate names of functions - def add_dna(self, segment, start, end, is_fwd): - """ start/end are given as nt """ - if np.abs(start-end) <= 0.9: - if self.debug: - print( "WARNING: segment constructed with a very small number of nts ({})".format(np.abs(start-end)) ) - # import pdb - # pdb.set_trace() - for s in self.strand_segments: - if s.segment == segment and s.is_fwd == is_fwd: - # assert( s.start not in (start,end) ) - # assert( s.end not in (start,end) ) - if s.start in (start,end) or s.end in (start,end): - raise CircularDnaError("Found circular DNA") - - s = StrandInSegment( segment, start, end, is_fwd ) - self.add( s ) - self.num_nt += s.num_nt - - def set_sequence(self,sequence): # , set_complement=True): - ## validate input - assert( len(sequence) >= self.num_nt ) - assert( np.all( [i in ('A','T','C','G') for i in sequence] ) ) - - seq_idx = 0 - ## set sequence on each segment - for s in self.children: - seg = s.segment - if seg.sequence is None: - seg.sequence = [None for i in range(seg.num_nt)] - - if s.is_fwd: - for nt in s._nucleotide_ids(): - seg.sequence[nt] = sequence[seq_idx] - seq_idx += 1 - else: - for nt in s._nucleotide_ids(): - seg.sequence[nt] = seqComplement[sequence[seq_idx]] - seq_idx += 1 - - # def get_sequence(self): - # sequence = [] - # for ss in self.strand_segments: - # sequence.extend( ss.get_sequence() ) - - # assert( len(sequence) >= self.num_nt ) - # ret = ["5"+sequence[0]] +\ - # sequence[1:-1] +\ - # [sequence[-1]+"3"] - # assert( len(ret) == self.num_nt ) - # return ret - - def link_nucleotides(self, nt5, nt3): - parent = nt5.parent if nt5.parent is nt3.parent else self - o3,c3,c4,c2,h3 = [nt5.atoms_by_name[n] - for n in ("O3'","C3'","C4'","C2'","H3'")] - p,o5,o1,o2,c5 = [nt3.atoms_by_name[n] - for n in ("P","O5'","O1P","O2P","C5'")] - parent.add_bond( o3, p, None ) - parent.add_angle( c3, o3, p, None ) - for x in (o5,o1,o2): - parent.add_angle( o3, p, x, None ) - parent.add_dihedral(c3, o3, p, x, None ) - for x in (c4,c2,h3): - parent.add_dihedral(x, c3, o3, p, None ) - parent.add_dihedral(o3, p, o5, c5, None) - - def generate_atomic_model(self, scale, first_atomic_index): - last = None - resid = 1 - ## TODO relabel "strand_segment" - strand_segment_count = 0 - for s in self.strand_segments: - strand_segment_count += 1 - seg = s.segment - contour = s.get_contour_points() - # if s.end == s.start: - # pdb.set_trace() - # assert(s.end != s.start) - assert( s.num_nt == 1 or (np.linalg.norm( seg.contour_to_position(contour[-1]) - seg.contour_to_position(contour[0]) ) > 0.1) ) - nucleotide_count = 0 - for c,seq in zip(contour,s.get_sequence()): - nucleotide_count += 1 - if last is None and not self.is_circular: - seq = "5"+seq - if strand_segment_count == len(s.strand_segments) and nucleotide_count == s.num_nt and not self.is_circular: - seq = seq+"3" - - nt = seg._generate_atomic_nucleotide( c, s.is_fwd, seq, scale, s ) - - ## Join last basepairs - if last is not None: - self.link_nucleotides(last,nt) - - nt.__dict__['resid'] = resid - resid += 1 - last = nt - nt._first_atomic_index = first_atomic_index - first_atomic_index += len(nt.children) - - if self.is_circular: - self.link_nucleotides(last,self.strand_segments[0].children[0]) - - return first_atomic_index - - def update_atomic_orientations(self,default_orientation): - last = None - resid = 1 - for s in self.strand_segments: - seg = s.segment - contour = s.get_contour_points() - for c,seq,nt in zip(contour,s.get_sequence(),s.children): - - orientation = seg.contour_to_orientation(c) - ## TODO: move this code (?) - if orientation is None: - axis = seg.contour_to_tangent(c) - angleVec = np.array([1,0,0]) - if axis.dot(angleVec) > 0.9: angleVec = np.array([0,1,0]) - angleVec = angleVec - angleVec.dot(axis)*axis - angleVec = angleVec/np.linalg.norm(angleVec) - y = np.cross(axis,angleVec) - orientation = np.array([angleVec,y,axis]).T - - nt.orientation = orientation.dot(default_orientation) # this one should be correct - -class SegmentModel(ArbdModel): - def __init__(self, segments=[], local_twist=True, escapable_twist=True, - max_basepairs_per_bead=7, - max_nucleotides_per_bead=4, - dimensions=(1000,1000,1000), temperature=291, - timestep=50e-6, cutoff=50, - decompPeriod=10000, pairlistDistance=None, - nonbondedResolution=0,DEBUG=0): - self.DEBUG = DEBUG - if DEBUG > 0: print("Building ARBD Model") - ArbdModel.__init__(self,segments, - dimensions, temperature, timestep, cutoff, - decompPeriod, pairlistDistance=None, - nonbondedResolution=0) - - # self.max_basepairs_per_bead = max_basepairs_per_bead # dsDNA - # self.max_nucleotides_per_bead = max_nucleotides_per_bead # ssDNA - self.children = self.segments = segments - - self._bonded_potential = dict() # cache for bonded potentials - self._generate_strands() self.grid_potentials = [] - self._generate_bead_model( max_basepairs_per_bead, max_nucleotides_per_bead, local_twist, escapable_twist) - self.useNonbondedScheme( nbDnaScheme ) - self.useTclForces = False + # self.useNonbondedScheme( nbDnaScheme ) def get_connections(self,type_=None,exclude=()): """ Find all connections in model, without double-counting """ added=set() ret=[] - for s in self.segments: + for s in self.polymers: items = [e for e in s.get_connections_and_locations(type_,exclude=exclude) if e[0] not in added] added.update([e[0] for e in items]) - ret.extend( list(sorted(items,key=lambda x: x[1].address)) ) + ret.extend( list(sorted(items,key=lambda x: x[1].contour_position)) ) return ret - def _recursively_get_beads_within_bonds(self,b1,bonds,done=()): - ret = [] - done = list(done) - done.append(b1) - if bonds == 0: - return [[]] - - for b2 in b1.intrahelical_neighbors: - if b2 in done: continue - for tmp in self._recursively_get_beads_within_bonds(b2, bonds-1, done): - ret.append( [b2]+tmp ) - return ret - - def _get_intrahelical_beads(self,num=2): - ## TODO: add check that this is not called before adding intrahelical_neighbors in _generate_bead_model - - assert(num >= 2) - - ret = [] - for s in self.segments: - for b1 in s.beads: - for bead_list in self._recursively_get_beads_within_bonds(b1, num-1): - assert(len(bead_list) == num-1) - if b1.idx < bead_list[-1].idx: # avoid double-counting - ret.append([b1]+bead_list) - return ret - - - def _get_intrahelical_angle_beads(self): - return self._get_intrahelical_beads(num=3) - - def _get_potential(self, type_, kSpring, d, max_potential = None): - key = (type_, kSpring, d, max_potential) - if key not in self._bonded_potential: - assert( kSpring >= 0 ) - if type_ == "bond": - self._bonded_potential[key] = HarmonicBond(kSpring,d, rRange=(0,1200), max_potential=max_potential) - elif type_ == "angle": - self._bonded_potential[key] = HarmonicAngle(kSpring,d, max_potential=max_potential) - # , resolution = 1, maxForce=0.1) - elif type_ == "dihedral": - self._bonded_potential[key] = HarmonicDihedral(kSpring,d, max_potential=max_potential) - else: - raise Exception("Unhandled potential type '%s'" % type_) - return self._bonded_potential[key] - def get_bond_potential(self, kSpring, d): - assert( d > 0.2 ) - return self._get_potential("bond", kSpring, d) - def get_angle_potential(self, kSpring, d): - return self._get_potential("angle", kSpring, d) - def get_dihedral_potential(self, kSpring, d, max_potential=None): - while d > 180: d-=360 - while d < -180: d+=360 - return self._get_potential("dihedral", kSpring, d, max_potential) - - - def _getParent(self, *beads ): - if np.all( [b1.parent == b2.parent - for b1,b2 in zip(beads[:-1],beads[1:])] ): - return beads[0].parent - else: - return self - - def _get_twist_spring_constant(self, sep): - """ sep in nt """ - kT = 0.58622522 # kcal/mol - twist_persistence_length = 90 # set semi-arbitrarily as there is a large spread in literature - ## <cos(q)> = exp(-s/Lp) = integrate( cos[x] exp(-A x^2), {x, 0, pi} ) / integrate( exp(-A x^2), {x, 0, pi} ) - ## Assume A is small - ## int[B_] := Normal[Integrate[ Series[Cos[x] Exp[-B x^2], {B, 0, 1}], {x, 0, \[Pi]}]/ - ## Integrate[Series[Exp[-B x^2], {B, 0, 1}], {x, 0, \[Pi]}]] - - ## Actually, without assumptions I get fitFun below - ## From http://www.annualreviews.org/doi/pdf/10.1146/annurev.bb.17.060188.001405 - ## units "3e-19 erg cm/ 295 k K" "nm" =~ 73 - Lp = twist_persistence_length/0.34 - - fitFun = lambda x: np.real(erf( (4*np.pi*x + 1j)/(2*np.sqrt(x)) )) * np.exp(-1/(4*x)) / erf(2*np.sqrt(x)*np.pi) - np.exp(-sep/Lp) - k = opt.leastsq( fitFun, x0=np.exp(-sep/Lp) ) - return k[0][0] * 2*kT*0.00030461742 - def extend(self, other, copy=True, include_strands=False): - assert( isinstance(other, SegmentModel) ) + assert( isinstance(other, PolymerSectionModel) ) if copy: - for s in other.segments: - self.segments.append(deepcopy(s)) + for s in other.polymers: + self.polymers.append(deepcopy(s)) if include_strands: for s in other.strands: self.strands.append(deepcopy(s)) else: - for s in other.segments: - self.segments.append(s) + for s in other.polymers: + self.polymers.append(s) if include_strands: for s in other.strands: self.strands.append(s) self._clear_beads() - def update(self, segment , copy=False): - assert( isinstance(segment, Segment) ) + def update(self, polymer , copy=False): + assert( isinstance(polymer, PolymerSection) ) if copy: - segment = deepcopy(segment) - self.segments.append(segment) - self._clear_beads() - - """ Mapping between different resolution models """ - def clear_atomic(self): - for strand in self.strands: - for s in strand.children: - s.clear_all() - for seg in self.segments: - for d in ('fwd','rev'): - seg.strand_pieces[d] = [] - self._generate_strands() - ## Clear sequence if needed - for seg in self.segments: - if seg.sequence is not None and len(seg.sequence) != seg.num_nt: - seg.sequence = None - - def clear_beads(self): - return self._clear_beads() - - def _clear_beads(self): - ## TODO: deprecate - for s in self.segments: - try: - s.clear_all() - except: - ... - self.clear_all(keep_children=True) - - try: - if len(self.strands[0].children[0].children) > 0: - self.clear_atomic() - except: - ... - - ## Check that it worked - assert( len([b for b in self]) == 0 ) - locParticles = [] - for s in self.segments: - for c,A,B in s.get_connections_and_locations(): - for l in (A,B): - if l.particle is not None: - locParticles.append(l.particle) - assert( len(locParticles) == 0 ) - assert( len([b for s in self.segments for b in s.beads]) == 0 ) - - def _update_segment_positions(self, bead_coordinates): - print("WARNING: called deprecated command '_update_segment_positions; use 'update_splines' instead") - return self.update_splines(bead_coordinates) - - ## Operations on spline coordinates + polymer = deepcopy(polymer) + self.polymers.append(polymer) + + """ Operations on spline coordinates """ def translate(self, translation_vector, position_filter=None): - for s in self.segments: + for s in self.polymers: s.translate(translation_vector, position_filter=position_filter) def rotate(self, rotation_matrix, about=None, position_filter=None): - for s in self.segments: + for s in self.polymers: s.rotate(rotation_matrix, about=about, position_filter=position_filter) def get_center(self, include_ssdna=False): if include_ssdna: - segments = self.segments + polymers = self.polymers else: - segments = list(filter(lambda s: isinstance(s,DoubleStrandedSegment), - self.segments)) - centers = [s.get_center() for s in segments] - weights = [s.num_nt*2 if isinstance(s,DoubleStrandedSegment) else s.num_nt for s in segments] + polymers = list(filter(lambda s: isinstance(s,DoubleStrandedPolymerSection), + self.polymers)) + centers = [s.get_center() for s in polymers] + weights = [s.num_monomers*2 if isinstance(s,DoubleStrandedPolymerSection) else s.num_monomers for s in polymers] # centers,weights = [np.array(a) for a in (centers,weights)] return np.average( centers, axis=0, weights=weights) - def update_splines(self, bead_coordinates): - """ Set new function for each segments functions - contour_to_position and contour_to_orientation """ - - for s in self.segments: - # if s.name == "61-1": - # pdb.set_trace() - - cabs = s.get_connections_and_locations("intrahelical") - if isinstance(s,SingleStrandedSegment): - cabs = cabs + [[c,A,B] for c,A,B in s.get_connections_and_locations("sscrossover") if A.address == 0 or A.address == 1] - if np.any( [B.particle is None for c,A,B in cabs] ): - print( "WARNING: none type found in connection, skipping" ) - cabs = [e for e in cabs if e[2].particle is not None] - - 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_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,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 = 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)) - - 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: - pdb.set_trace() - - - """ Get positions """ - positions = bead_coordinates[ids,:].T - - # print("BEADS NOT IN {}:".format(s)) - # for b,c in filter(lambda z: z[0] not in s.beads, zip(beads,contours)): - # print(" {}[{:03f}]: {:0.3f}".format(b.parent.name, b.contour_position, c) ) - - - tck, u = interpolate.splprep( positions, u=contours, s=0, k=1 ) - - # if len(beads) < 8: - # ret = interpolate.splprep( positions, u=contours, s=0, k=1, full_output=1 ) - # tck = ret[0][0] - # if ret[2] > 0: - # pdb.set_trace() - - # else: - # try: - # ret = interpolate.splprep( positions, u=contours, s=0, k=3, full_output=1 ) - # tck = ret[0][0] - # if ret[2] > 0: - # pdb.set_trace() - # except: - # ret = interpolate.splprep( positions, u=contours, s=0, k=1, full_output=1 ) - # tck = ret[0][0] - # if ret[2] > 0: - # pdb.set_trace() - - s.position_spline_params = (tck,u) - - """ Get orientation """ - def get_orientation_vector(bead,tangent): - if 'orientation_bead' in bead.__dict__: - o = bead.orientation_bead - oVec = bead_coordinates[o.idx,:] - bead_coordinates[bead.idx,:] - oVec = oVec - oVec.dot(tangent)*tangent - oVec = oVec/np.linalg.norm(oVec) - else: - oVec = None - return oVec - - def remove_tangential_projection(vector, tangent): - """ Assume tangent is normalized """ - v = vector - vector.dot(tangent)*tangent - return v/np.linalg.norm(v) - - def get_orientation_vector(bead,tangent): - if 'orientation_bead' in bead.__dict__: - o = bead.orientation_bead - oVec = bead_coordinates[o.idx,:] - bead_coordinates[bead.idx,:] - oVec = remove_tangential_projection(oVec,tangent) - else: - oVec = None - return oVec - - - def get_previous_idx_if_none(list_): - previous = None - result = [] - i = 0 - for e in list_: - if e is None: - result.append(previous) - else: - previous = i - i+=1 - return result - def get_next_idx_if_none(list_): - tmp = get_previous_idx_if_none(list_[::-1])[::-1] - return [ len(list_)-1-idx if idx is not None else idx for idx in tmp ] - - def fill_in_orientation_vectors(contours,orientation_vectors,tangents): - result = [] - last_idx = get_previous_idx_if_none( orientation_vectors ) - next_idx = get_next_idx_if_none( orientation_vectors ) - none_idx = 0 - for c,ov,t in zip(contours,orientation_vectors,tangents): - if ov is not None: - result.append(ov) - else: - p = last_idx[none_idx] - n = next_idx[none_idx] - none_idx += 1 - if p is None: - if n is None: - ## Should be quite rare; give something random if it happens - print("WARNING: unable to interpolate orientation") - o = np.array((1,0,0)) - result.append( remove_tangential_projection(o,t) ) - else: - o = orientation_vectors[n] - result.append( remove_tangential_projection(o,t) ) - else: - if n is None: - o = orientation_vectors[p] - result.append( remove_tangential_projection(o,t) ) - else: - cp,cn = [contours[i] for i in (p,n)] - op,on = [orientation_vectors[i] for i in (p,n)] - if (cn-cp) > 1e-6: - o = ((cn-c)*op+(c-cp)*on)/(cn-cp) - else: - o = op+on - result.append( remove_tangential_projection(o,t) ) - return result - - tangents = s.contour_to_tangent(contours) - orientation_vectors = [get_orientation_vector(b,t) for b,t in zip(beads,tangents)] - if len(beads) > 3 and any([e is not None for e in orientation_vectors] ): - orientation_vectors = fill_in_orientation_vectors(contours, orientation_vectors, tangents) - - quats = [] - lastq = None - for b,t,oVec in zip(beads,tangents,orientation_vectors): - y = np.cross(t,oVec) - assert( np.abs(np.linalg.norm(y) - 1) < 1e-2 ) - q = quaternion_from_matrix( np.array([oVec,y,t]).T) - - if lastq is not None: - if q.dot(lastq) < 0: - q = -q - quats.append( q ) - lastq = q - - # pdb.set_trace() - quats = np.array(quats) - # tck, u = interpolate.splprep( quats.T, u=contours, s=3, k=3 ) ;# cubic spline not as good - tck, u = interpolate.splprep( quats.T, u=contours, s=0, k=1 ) - s.quaternion_spline_params = (tck,u) - - def _generate_bead_model(self, - max_basepairs_per_bead = 7, - max_nucleotides_per_bead = 4, - local_twist=False, - escapable_twist=True): - ## TODO: deprecate - self.generate_bead_model( max_basepairs_per_bead = max_basepairs_per_bead, - max_nucleotides_per_bead = max_nucleotides_per_bead, - local_twist=local_twist, - escapable_twist=escapable_twist) - - def generate_bead_model(self, - max_basepairs_per_bead = 7, - max_nucleotides_per_bead = 4, - local_twist=False, - escapable_twist=True): - - self.children = self.segments # is this okay? - self.clear_beads() - - segments = self.segments - for s in segments: - s.local_twist = local_twist - - """ Simplify connections """ - # d_nt = dict() # - # for s in segments: - # d_nt[s] = 1.5/(s.num_nt-1) - # for s in segments: - # ## replace consecutive crossovers with - # cl = sorted( s.get_connections_and_locations("crossover"), key=lambda x: x[1].address ) - # last = None - # for entry in cl: - # c,A,B = entry - # if last is not None and \ - # (A.address - last[1].address) < d_nt[s]: - # same_type = c.type_ == last[0].type_ - # same_dest_seg = B.container == last[2].container - # if same_type and same_dest_seg: - # if np.abs(B.address - last[2].address) < d_nt[B.container]: - # ## combine - # A.combine = last[1] - # B.combine = last[2] - - # ... - # # if last is not None: - # # s.bead_locations.append(last) - # ... - # last = entry - # del d_nt - - """ Generate beads at intrahelical junctions """ - if self.DEBUG: print( "Adding intrahelical beads at junctions" ) - - ## Loop through all connections, generating beads at appropriate locations - for c,A,B in self.get_connections("intrahelical"): - s1,s2 = [l.container for l in (A,B)] - - assert( A.particle is None ) - assert( B.particle is None ) - - ## TODO: offload the work here to s1 - # TODOTODO - a1,a2 = [l.address for l in (A,B)] - for a in (a1,a2): - 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) - 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): - b = s2.get_nearest_bead(a2) - if b is not None: - if np.abs(b.get_nt_position(s2) - s2.contour_to_nt_pos(a2)) > 0.5: - b = None - - - if b is not None and b.parent not in (s1,s2): - b = None - - if b is None: - ## need to generate a bead - if isinstance(s2,DoubleStrandedSegment): - b = s2._generate_one_bead(a2,0) - else: - b = s1._generate_one_bead(a1,0) - A.particle = B.particle = b - b.is_intrahelical = True - b.locations.extend([A,B]) - - # pdb.set_trace() - - """ Generate beads at other junctions """ - for c,A,B in self.get_connections(exclude="intrahelical"): - s1,s2 = [l.container for l in (A,B)] - if A.particle is not None and B.particle is not None: - continue - # assert( A.particle is None ) - # assert( B.particle is None ) - - ## TODO: offload the work here to s1/s2 (?) - a1,a2 = [l.address for l in (A,B)] - - def maybe_add_bead(location, seg, address, ): - if location.particle is None: - b = seg.get_nearest_bead(address) - try: - distance = seg.contour_to_nt_pos(np.abs(b.contour_position-address)) - max_distance = min(max_basepairs_per_bead, max_nucleotides_per_bead)*0.5 - if "is_intrahelical" in b.__dict__: - max_distance = 0.5 - if distance >= max_distance: - raise Exception("except") - - ## combine beads - b.update_position( 0.5*(b.contour_position + address) ) # avg position - except: - b = seg._generate_one_bead(address,0) - location.particle = b - b.locations.append(location) - - maybe_add_bead(A,s1,a1) - maybe_add_bead(B,s2,a2) - - """ Some tests """ - for c,A,B in self.get_connections("intrahelical"): - for l in (A,B): - if l.particle is None: continue - assert( l.particle.parent is not None ) - - """ Generate beads in between """ - if self.DEBUG: print("Generating beads") - for s in segments: - s._generate_beads( self, max_basepairs_per_bead, max_nucleotides_per_bead ) - - # """ Combine beads at junctions as needed """ - # for c,A,B in self.get_connections(): - # ... - - # ## Debug - # all_beads = [b for s in segments for b in s.beads] - # positions = np.array([b.position for b in all_beads]) - # dists = positions[:,np.newaxis,:] - positions[np.newaxis,:,:] - # ids = np.where( np.sum(dists**2,axis=-1) + 0.02**2*np.eye(len(dists)) < 0.02**2 ) - # print( ids ) - # 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: - ## already handled by Segment._generate_beads - pass - else: - for b in (b1,b2): assert( b is not None ) - b1.make_intrahelical_neighbor(b2) - - """ Reassign bead types """ - if self.DEBUG: print("Assigning bead types") - beadtype_s = dict() - beadtype_count = dict(D=0,O=0,S=0) - - def _assign_bead_type(bead, num_nt, decimals): - num_nt0 = bead.num_nt - bead.num_nt = np.around( np.float32(num_nt), decimals=decimals ) - char = bead.type_.name[0].upper() - key = (char, bead.num_nt) - if key in beadtype_s: - bead.type_ = beadtype_s[key] - else: - t = deepcopy(bead.type_) - t.__dict__["nts"] = bead.num_nt*2 if char in ("D","O") else bead.num_nt - # t.name = t.name + "%03d" % (t.nts*10**decimals) - t.name = char + "%03d" % (beadtype_count[char]) - t.mass = t.nts * 150 - t.diffusivity = 150 if t.nts == 0 else min( 50 / np.sqrt(t.nts/5), 150) - beadtype_count[char] += 1 - if self.DEBUG: print( "{} --> {} ({})".format(num_nt0, bead.num_nt, t.name) ) - beadtype_s[key] = bead.type_ = t - - # (cluster_size[c-1]) - - - import scipy.cluster.hierarchy as hcluster - beads = [b for s in segments for b in s if b.type_.name[0].upper() in ("D","O")] - data = np.array([b.num_nt for b in beads])[:,np.newaxis] - order = int(2-np.log10(2*max_basepairs_per_bead)//1) - try: - clusters = hcluster.fclusterdata(data, float(max_basepairs_per_bead)/500, criterion="distance") - cluster_size = [np.mean(data[clusters == i]) for i in np.unique(clusters)] - except: - clusters = np.arange(len(data))+1 - cluster_size = data.flatten() - for b,c in zip(beads,clusters): - _assign_bead_type(b, cluster_size[c-1], decimals=order) - - beads = [b for s in segments for b in s if b.type_.name[0].upper() in ("S")] - data = np.array([b.num_nt for b in beads])[:,np.newaxis] - order = int(2-np.log10(max_nucleotides_per_bead)//1) - try: - clusters = hcluster.fclusterdata(data, float(max_nucleotides_per_bead)/500, criterion="distance") - cluster_size = [np.mean(data[clusters == i]) for i in np.unique(clusters)] - except: - clusters = np.arange(len(data))+1 - cluster_size = data.flatten() - for b,c in zip(beads,clusters): - _assign_bead_type(b, cluster_size[c-1], decimals=order) - - self._apply_grid_potentials_to_beads(beadtype_s) - - # for bead in [b for s in segments for b in s]: - # num_nt0 = bead.num_nt - # # bead.num_nt = np.around( np.float32(num_nt), decimals=decimals ) - # key = (bead.type_.name[0].upper(), bead.num_nt) - # if key in beadtype_s: - # bead.type_ = beadtype_s[key] - # else: - # t = deepcopy(bead.type_) - # t.__dict__["nts"] = bead.num_nt*2 if t.name[0].upper() in ("D","O") else bead.num_nt - # # t.name = t.name + "%03d" % (t.nts*10**decimals) - # t.name = t.name + "%.16f" % (t.nts) - # print( "{} --> {} ({})".format(num_nt0, bead.num_nt, t.name) ) - # beadtype_s[key] = bead.type_ = t - - - """ Update bead indices """ - self._countParticleTypes() # probably not needed here - self._updateParticleOrder() - - """ Add intrahelical bond potentials """ - if self.DEBUG: print("Adding intrahelical bond potentials") - dists = dict() # intrahelical distances built for later use - intra_beads = self._get_intrahelical_beads() - if self.DEBUG: print(" Adding %d bonds" % len(intra_beads)) - for b1,b2 in intra_beads: - # assert( not np.isclose( np.linalg.norm(b1.collapsedPosition() - b2.collapsedPosition()), 0 ) ) - if np.linalg.norm(b1.collapsedPosition() - b2.collapsedPosition()) < 1: - # print("WARNING: some beads are very close") - ... - - parent = self._getParent(b1,b2) - - ## TODO: could be sligtly smarter about sep - sep = 0.5*(b1.num_nt+b2.num_nt) - - conversion = 0.014393265 # units "pN/AA" kcal_mol/AA^2 - if b1.type_.name[0] == "D" and b2.type_.name[0] == "D": - elastic_modulus_times_area = 1000 # pN http://markolab.bmbcb.northwestern.edu/marko/Cocco.CRP.02.pdf - d = 3.4*sep - k = conversion*elastic_modulus_times_area/d - else: - ## TODO: get better numbers our ssDNA model - elastic_modulus_times_area = 800 # pN http://markolab.bmbcb.northwestern.edu/marko/Cocco.CRP.02.pdf - d = 5*sep - k = conversion*elastic_modulus_times_area/d - # print(sep,d,k) - - if b1 not in dists: - dists[b1] = dict() - if b2 not in dists: - dists[b2] = dict() - # dists[b1].append([b2,sep]) - # dists[b2].append([b1,sep]) - dists[b1][b2] = sep - dists[b2][b1] = sep - - if b1 is b2: continue - - # dists[[b1,b2]] = dists[[b2,b1]] = sep - bond = self.get_bond_potential(k,d) - parent.add_bond( b1, b2, bond, exclude=True ) - - # for s in self.segments: - # sepsum = 0 - # beadsum = 0 - # for b1 in s.beads: - # beadsum += b1.num_nt - # for bead_list in self._recursively_get_beads_within_bonds(b1, 1): - # assert(len(bead_list) == 1) - # if b1.idx < bead_list[-1].idx: # avoid double-counting - # for b2 in bead_list: - # if b2.parent == b1.parent: - # sepsum += dists[b1][b2] - # sepsum += sep - # print("Helix {}: bps {}, beads {}, separation {}".format(s.name, s.num_nt, beadsum, sepsum)) - - """ Add intrahelical angle potentials """ - def get_effective_dsDNA_Lp(sep): - - """ The persistence length of our model was found to be a - little off (probably due to NB interactions). This - attempts to compensate """ - - ## For 1 bp, Lp=559, for 25 Lp = 524 - beads_per_bp = sep/2 - Lp0 = 147 - # return 0.93457944*Lp0 ;# factor1 - return 0.97*Lp0 ;# factor2 - # factor = bead_per_bp * (0.954-0.8944 - # return Lp0 * bead_per_bp - - empirical_compensation_factor = max_basepairs_per_bead - - if self.DEBUG: print("Adding intrahelical angle potentials") - for b1,b2,b3 in self._get_intrahelical_angle_beads(): - ## TODO: could be slightly smarter about sep - sep = 0.5*(0.5*b1.num_nt+b2.num_nt+0.5*b3.num_nt) - parent = self._getParent(b1,b2,b3) - - kT = 0.58622522 # kcal/mol - if b1.type_.name[0] == "D" and b2.type_.name[0] == "D" and b3.type_.name[0] == "D": - Lp = get_effective_dsDNA_Lp(sep) - k = angle_spring_from_lp(sep,Lp) - if local_twist: - k_dihed = 0.25*k - k *= 0.75 # reduce because orientation beads impose similar springs - dihed = self.get_dihedral_potential(k_dihed,180) - parent.add_dihedral(b1,b2,b2.orientation_bead,b3, dihed) - - - else: - ## TODO: get correct number from ssDNA model - k = angle_spring_from_lp(sep,2) - - angle = self.get_angle_potential(k,180) - parent.add_angle( b1, b2, b3, angle ) - - """ Add intrahelical exclusions """ - if self.DEBUG: print("Adding intrahelical exclusions") - beads = dists.keys() - def _recursively_get_beads_within(b1,d,done=()): - ret = [] - for b2,sep in dists[b1].items(): - if b2 in done: continue - if sep < d: - ret.append( b2 ) - done.append( b2 ) - tmp = _recursively_get_beads_within(b2, d-sep, done) - if len(tmp) > 0: ret.extend(tmp) - return ret - - exclusions = set() - for b1 in beads: - exclusions.update( [(b1,b) for b in _recursively_get_beads_within(b1, 20, done=[b1])] ) - - if self.DEBUG: print("Adding %d exclusions" % len(exclusions)) - for b1,b2 in exclusions: - parent = self._getParent(b1,b2) - parent.add_exclusion( b1, b2 ) - - """ Twist potentials """ - if local_twist: - if self.DEBUG: print("Adding twist potentials") - - for b1 in beads: - if "orientation_bead" not in b1.__dict__: continue - for b2,sep in dists[b1].items(): - if "orientation_bead" not in b2.__dict__: continue - if b2.idx < b1.idx: continue # Don't double-count - - p1,p2 = [b.parent for b in (b1,b2)] - o1,o2 = [b.orientation_bead for b in (b1,b2)] - - parent = self._getParent( b1, b2 ) - - """ Add heuristic 90 degree potential to keep orientation bead orthogonal """ - Lp = get_effective_dsDNA_Lp(sep) - k = 0.5*angle_spring_from_lp(sep,Lp) - pot = self.get_angle_potential(k,90) - parent.add_angle(o1,b1,b2, pot) - parent.add_angle(b1,b2,o2, pot) - - ## TODO: improve this - twist_per_nt = 0.5 * (p1.twist_per_nt + p2.twist_per_nt) - angle = sep*twist_per_nt - if angle > 360 or angle < -360: - print("WARNING: twist angle out of normal range... proceeding anyway") - # raise Exception("The twist between beads is too large") - - k = self._get_twist_spring_constant(sep) - if escapable_twist: - pot = self.get_dihedral_potential(k,angle,max_potential=1) - else: - pot = self.get_dihedral_potential(k,angle) - parent.add_dihedral(o1,b1,b2,o2, pot) - - def k_angle(sep): - return 0.5*angle_spring_from_lp(sep,147) - - def k_xover_angle(sep): - return 0.5 * k_angle(sep) - - def add_local_crossover_orientation_potential(b1,b2,is_parallel=True): - u1,u2 = [b.get_intrahelical_above() for b in (b1,b2)] - d1,d2 = [b.get_intrahelical_below() for b in (b1,b2)] - - k = k_xover_angle(sep=1) - pot = self.get_angle_potential(k,120) - - if 'orientation_bead' in b1.__dict__: - o = b1.orientation_bead - self.add_angle( o,b1,b2, pot ) - if 'orientation_bead' in b2.__dict__: - o = b2.orientation_bead - self.add_angle( b1,b2,o, pot ) - - if 'orientation_bead' in b1.__dict__: - t0 = -90 if A.on_fwd_strand else 90 - if not is_parallel: t0 *= -1 - - o1 = b1.orientation_bead - if u2 is not None and isinstance(u2.parent,DoubleStrandedSegment): - k = k_xover_angle( dists[b2][u2] ) - pot = self.get_dihedral_potential(k,t0) - self.add_dihedral( o1,b1,b2,u2, pot ) - elif d2 is not None and isinstance(d2.parent,DoubleStrandedSegment): - k = k_xover_angle( dists[b2][d2] ) - pot = self.get_dihedral_potential(k,-t0) - self.add_dihedral( o1,b1,b2,d2, pot ) - if 'orientation_bead' in b2.__dict__: - t0 = -90 if B.on_fwd_strand else 90 - if not is_parallel: t0 *= -1 - - o2 = b2.orientation_bead - if u1 is not None and isinstance(u1.parent,DoubleStrandedSegment): - k = k_xover_angle( dists[b1][u1] ) - pot = self.get_dihedral_potential(k,t0) - self.add_dihedral( o2,b2,b1,u1, pot ) - elif d1 is not None and isinstance(d1.parent,DoubleStrandedSegment): - k = k_xover_angle( dists[b1][d1] ) - pot = self.get_dihedral_potential(k,-t0) - self.add_dihedral( o2,b2,b1,d1, pot ) - - - """ Add connection potentials """ - for c,A,B in self.get_connections("terminal_crossover"): - ## TODO: use a better description here - b1,b2 = [loc.particle for loc in (c.A,c.B)] - # pot = self.get_bond_potential(4,18.5) - pot = self.get_bond_potential(4,12) - self.add_bond(b1,b2, pot) - - dotProduct = b1.parent.contour_to_tangent(b1.contour_position).dot( - b2.parent.contour_to_tangent(b2.contour_position) ) - - ## Add potential to provide a particular orinetation - if local_twist: - add_local_crossover_orientation_potential(b1,b2, is_parallel=dotProduct > 0) - - - """ Add connection potentials """ - for c,A,B in self.get_connections("sscrossover"): - b1,b2 = [loc.particle for loc in (c.A,c.B)] - ## TODO: improve parameters - pot = self.get_bond_potential(4,6) - self.add_bond(b1,b2, pot) - - ## Add potentials to provide a sensible orientation - ## TODO refine potentials against all-atom simulation data - """ - u1,u2 = [b.get_intrahelical_above() for b in (b1,b2)] - d1,d2 = [b.get_intrahelical_below() for b in (b1,b2)] - k_fn = k_angle - if isinstance(b1.parent,DoubleStrandedSegment): - a,b,c = (d1,b1,b2) if d1.parent is b1.parent else (u1,b1,b2) - else: - a,b,c = (d2,b2,b1) if d2.parent is b2.parent else (u2,b2,b1) - """ - - # pot = self.get_angle_potential( k_fn(dists[a][b]), 120 ) # a little taller than 90 degrees - # self.add_angle( a,b,c, pot ) - # o = b.orientation_bead - # angle=3 - # pot = self.get_angle_potential( 1, 0 ) # a little taller than 90 degrees - # self.add_angle( a,b,c, pot ) - - dotProduct = b1.parent.contour_to_tangent(b1.contour_position).dot( - b2.parent.contour_to_tangent(b2.contour_position) ) - - if local_twist: - add_local_crossover_orientation_potential(b1,b2, is_parallel=dotProduct > 0) - - - crossover_bead_pots = set() - for c,A,B in self.get_connections("crossover"): - b1,b2 = [loc.particle for loc in (A,B)] - - ## Avoid double-counting - if (b1,b2,A.on_fwd_strand,B.on_fwd_strand) in crossover_bead_pots: continue - crossover_bead_pots.add((b1,b2,A.on_fwd_strand,B.on_fwd_strand)) - crossover_bead_pots.add((b2,b1,B.on_fwd_strand,A.on_fwd_strand)) - - pot = self.get_bond_potential(4,18.5) - self.add_bond(b1,b2, pot) - - ## Get beads above and below - u1,u2 = [b.get_intrahelical_above() for b in (b1,b2)] - d1,d2 = [b.get_intrahelical_below() for b in (b1,b2)] - dotProduct = b1.parent.contour_to_tangent(b1.contour_position).dot( - b2.parent.contour_to_tangent(b2.contour_position) ) - if dotProduct < 0: - tmp = d2 - d2 = u2 - u2 = tmp - - a = None - if u1 is not None and u2 is not None: - t0 = 0 - a,b,c,d = (u1,b1,b2,u2) - elif d1 is not None and d2 is not None: - t0 = 0 - a,b,c,d = (d1,b1,b2,d2 ) - elif d1 is not None and u2 is not None: - t0 = 180 - a,b,c,d = (d1,b1,b2,u2) - elif u1 is not None and d2 is not None: - t0 = 180 - a,b,c,d = (u1,b1,b2,d2) - - if a is not None: - k = k_xover_angle( dists[b][a]+dists[c][d] ) - pot = self.get_dihedral_potential(k,t0) - self.add_dihedral( a,b,c,d, pot ) - - if local_twist: - add_local_crossover_orientation_potential(b1,b2, is_parallel=dotProduct > 0) - - - ## TODOTODO check that this works - for crossovers in self.get_consecutive_crossovers(): - if local_twist: break - ## filter crossovers - for i in range(len(crossovers)-2): - 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,near_address=A1.address) - A2.particle.get_nt_position(s2,near_address=A2.address) - sep = np.abs(sep) - - assert(sep >= 0) - - n1,n2,n3,n4 = (B1.particle, A1.particle, A2.particle, B2.particle) - - """ - <cos(q)> = exp(-s/Lp) = integrate( cos[x] exp(-A x^2), {x, 0, pi} ) / integrate( exp(-A x^2), {x, 0, pi} ) - """ - - ## From http://www.annualreviews.org/doi/pdf/10.1146/annurev.bb.17.060188.001405 - ## units "3e-19 erg cm/ 295 k K" "nm" =~ 73 - Lp = s1.twist_persistence_length/0.34 # set semi-arbitrarily as there is a large spread in literature - - def get_spring(sep): - fitFun = lambda x: np.real(erf( (4*np.pi*x + 1j)/(2*np.sqrt(x)) )) * np.exp(-1/(4*x)) / erf(2*np.sqrt(x)*np.pi) - np.exp(-sep/Lp) - k = opt.leastsq( fitFun, x0=np.exp(-sep/Lp) ) - return k[0][0] * 2*kT*0.00030461742 - - k = get_spring( max(sep,2) ) - t0 = sep*s1.twist_per_nt # TODO weighted avg between s1 and s2 - - # pdb.set_trace() - if A1.on_fwd_strand: t0 -= 120 - if dir1 != dir2: - A2_on_fwd = not A2.on_fwd_strand - else: - A2_on_fwd = A2.on_fwd_strand - if A2_on_fwd: t0 += 120 - - # t0 = (t0 % 360 - - # if n2.idx == 0: - # print( n1.idx,n2.idx,n3.idx,n4.idx,k,t0,sep ) - if sep == 0 and n1 is not n4: - # pot = self.get_angle_potential(k,t0) - # self.add_angle( n1,n2,n4, pot ) - pass - else: - pot = self.get_dihedral_potential(k,t0) - self.add_dihedral( n1,n2,n3,n4, pot ) - - # ## remove duplicate potentials; ## TODO ensure that they aren't added twice in the first place? - # self.remove_duplicate_terms() - - def walk_through_helices(segment, direction=1, processed_segments=None): - """ - - First and last segment should be same for circular helices - """ - - assert( direction in (1,-1) ) - if processed_segments == None: - processed_segments = set() - - def segment_is_new_helix(s): - return isinstance(s,DoubleStrandedSegment) and s not in processed_segments - - new_s = None - s = segment - ## iterate intrahelically connected dsDNA segments - while segment_is_new_helix(s): - conn_locs = s.get_contour_sorted_connections_and_locations("intrahelical")[::direction] - processed_segments.add(new_s) - new_s = None - new_dir = None - for i in range(len(conn_locs)): - c,A,B = conn_locs[i] - ## TODO: handle change of direction - # TODOTODO - address = 1*(direction==-1) - if A.address == address and segment_is_new_helix(B.container): - new_s = B.container - assert(B.address in (0,1)) - new_dir = 2*(B.address == 0) - 1 - break - - yield s,direction - s = new_s # will break if None - direction = new_dir - - # if new_s is None: - # break - # else: - # s = new_s - # yield s - ## return s - - - def get_consecutive_crossovers(self): - ## TODOTODO TEST - crossovers = [] - processed_segments = set() - for s1 in self.segments: - if not isinstance(s1,DoubleStrandedSegment): - continue - if s1 in processed_segments: continue - - s0,d0 = list(SegmentModel.walk_through_helices(s1,direction=-1))[-1] - - # s,direction = get_start_of_helices() - tmp = [] - for s,d in SegmentModel.walk_through_helices(s0,-d0): - if s == s0 and len(tmp) > 0: - ## end of circular helix, only add first crossover - cl_list = s.get_contour_sorted_connections_and_locations("crossover") - if len(cl_list) > 0: - tmp.append( cl_list[::d][0] + [d] ) - else: - tmp.extend( [cl + [d] for cl in s.get_contour_sorted_connections_and_locations("crossover")[::d]] ) - processed_segments.add(s) - crossovers.append(tmp) - return crossovers - - def set_sequence(self, sequence, force=True): - if force: - self.strands[0].set_sequence(sequence) - else: - try: - self.strands[0].set_sequence(sequence) - except: - ... - for s in self.segments: - s.randomize_unset_sequence() - - def _generate_strands(self): - ## clear strands - try: - for s in self.strands: - self.children.remove(s) - for seg in self.segments: - for d in ('fwd','rev'): - seg.strand_pieces[d] = [] - except: - pass - self.strands = strands = [] - - """ Ensure unconnected ends have 5prime Location objects """ - for seg in self.segments: - ## TODO move into Segment calls - five_prime_locs = sum([seg.get_locations(s) for s in ("5prime","crossover","terminal_crossover")],[]) - three_prime_locs = sum([seg.get_locations(s) for s in ("3prime","crossover","terminal_crossover")],[]) - - def is_start_5prime(l): - return l.get_nt_pos() < 1 and l.on_fwd_strand - def is_end_5prime(l): - return l.get_nt_pos() > seg.num_nt-2 and not l.on_fwd_strand - def is_start_3prime(l): - return l.get_nt_pos() < 1 and not l.on_fwd_strand - def is_end_3prime(l): - return l.get_nt_pos() > seg.num_nt-2 and l.on_fwd_strand - - if seg.start5.connection is None: - if len(list(filter( is_start_5prime, five_prime_locs ))) == 0: - seg.add_5prime(0) # TODO ensure this is the same place - - if 'end5' in seg.__dict__ and seg.end5.connection is None: - if len(list(filter( is_end_5prime, five_prime_locs ))) == 0: - seg.add_5prime(seg.num_nt-1,on_fwd_strand=False) - - if 'start3' in seg.__dict__ and seg.start3.connection is None: - if len(list(filter( is_start_3prime, three_prime_locs ))) == 0: - seg.add_3prime(0,on_fwd_strand=False) - - if seg.end3.connection is None: - if len(list(filter( is_end_3prime, three_prime_locs ))) == 0: - seg.add_3prime(seg.num_nt-1) - - # print( [(l,l.get_connected_location()) for l in seg.locations] ) - # addresses = np.array([l.address for l in seg.get_locations("5prime")]) - # if not np.any( addresses == 0 ): - # ## check if end is connected - # for c,l,B in self.get_connections_and_locations(): - # if c[0] - - """ Build strands from connectivity of helices """ - def _recursively_build_strand(strand, segment, pos, is_fwd, mycounter=0, move_at_least=0.5): - seg = segment - history = [] - while True: - mycounter+=1 - if mycounter > 10000: - raise Exception("Too many iterations") - - #if seg.name == "22-1" and pos > 140: - # if seg.name == "22-2": - # import pdb - # pdb.set_trace() - - end_pos, next_seg, next_pos, next_dir, move_at_least = seg.get_strand_segment(pos, is_fwd, move_at_least) - history.append((seg,pos,end_pos,is_fwd)) - try: - strand.add_dna(seg, pos, end_pos, is_fwd) - except CircularDnaError: - ## Circular DNA was found - break - except: - print("Unexpected error:", sys.exc_info()[0]) - # import pdb - # pdb.set_trace() - # seg.get_strand_segment(pos, is_fwd, move_at_least) - # strand.add_dna(seg, pos, end_pos, is_fwd) - raise - - if next_seg is None: - break - else: - seg,pos,is_fwd = (next_seg, next_pos, next_dir) - strand.history = list(history) - return history - - strand_counter = 0 - history = [] - for seg in self.segments: - locs = filter(lambda l: l.connection is None, seg.get_5prime_locations()) - if locs is None: continue - # for pos, is_fwd in locs: - for l in locs: - # print("Tracing",l) - # TODOTODO - pos = seg.contour_to_nt_pos(l.address, round_nt=True) - is_fwd = l.on_fwd_strand - s = Strand(segname="S{:03d}".format(len(strands))) - strand_history = _recursively_build_strand(s, seg, pos, is_fwd) - history.append((l,strand_history)) - # print("{} {}".format(seg.name,s.num_nt)) - strands.append(s) - - ## Trace circular DNA - def strands_cover_segment(segment, is_fwd=True): - direction = 'fwd' if is_fwd else 'rev' - nt = 0 - for sp in segment.strand_pieces[direction]: - nt += sp.num_nt - return nt == segment.num_nt - - def find_nt_not_in_strand(segment, is_fwd=True): - fwd_str = 'fwd' if is_fwd else 'rev' - - def check(val): - assert(val >= 0 and val < segment.num_nt) - # print("find_nt_not_in_strand({},{}) returning {}".format( - # segment, is_fwd, val)) - return val - - if is_fwd: - last = -1 - for sp in segment.strand_pieces[fwd_str]: - if sp.start-last > 1: - return check(last+1) - last = sp.end - return check(last+1) - else: - last = segment.num_nt - for sp in segment.strand_pieces[fwd_str]: - if last-sp.end > 1: - return check(last-1) - last = sp.start - return check(last-1) - - def add_strand_if_needed(seg,is_fwd): - history = [] - if not strands_cover_segment(seg, is_fwd): - pos = nt = find_nt_not_in_strand(seg, is_fwd) - s = Strand(is_circular = True) - history = _recursively_build_strand(s, seg, pos, is_fwd) - strands.append(s) - return history - - for seg in self.segments: - add_strand_if_needed(seg,True) - if isinstance(seg, DoubleStrandedSegment): - add_strand_if_needed(seg,False) - - self.strands = sorted(strands, key=lambda s:s.num_nt)[::-1] - def check_strands(): - dsdna = filter(lambda s: isinstance(s,DoubleStrandedSegment), self.segments) - for s in dsdna: - nt_fwd = nt_rev = 0 - for sp in s.strand_pieces['fwd']: - nt_fwd += sp.num_nt - for sp in s.strand_pieces['rev']: - nt_rev += sp.num_nt - assert( nt_fwd == s.num_nt and nt_rev == s.num_nt ) - # print("{}: {},{} (fwd,rev)".format(s.name, nt_fwd/s.num_nt,nt_rev/s.num_nt)) - check_strands() - - ## relabel segname - counter = 0 - for s in self.strands: - if s.segname is None: - s.segname = "D%03d" % counter - counter += 1 - - def _assign_basepairs(self): - ## Assign basepairs - for seg in self.segments: - if isinstance(seg, DoubleStrandedSegment): - strands1 = seg.strand_pieces['fwd'] # already sorted - strands2 = seg.strand_pieces['rev'] - - nts1 = [nt for s in strands1 for nt in s.children] - nts2 = [nt for s in strands2 for nt in s.children[::-1]] - assert(len(nts1) == len(nts2)) - for nt1,nt2 in zip(nts1,nts2): - ## TODO weakref - nt1.basepair = nt2 - nt2.basepair = nt1 - - def _update_orientations(self,orientation): - for s in self.strands: - s.update_atomic_orientations(orientation) - - def write_atomic_ENM(self, output_name, lattice_type=None): - ## TODO: ensure atomic model was generated already - if lattice_type is None: - try: - lattice_type = self.lattice_type - except: - lattice_type = "square" - else: - try: - if lattice_type != self.lattice_type: - print("WARNING: printing ENM with a lattice type ({}) that differs from model's lattice type ({})".format(lattice_type,self.lattice_type)) - except: - pass - - if lattice_type == "square": - enmTemplate = enmTemplateSQ - elif lattice_type == "honeycomb": - enmTemplate = enmTemplateHC - else: - raise Exception("Lattice type '%s' not supported" % self.latticeType) - - ## TODO: allow ENM to be created without first building atomic model - noStackPrime = 0 - noBasepair = 0 - with open("%s.exb" % output_name,'w') as fh: - # natoms=0 - - for seg in self.segments: - ## Continue unless dsDNA - if not isinstance(seg,DoubleStrandedSegment): continue - for strand_piece in seg.strand_pieces['fwd'] + seg.strand_pieces['rev']: - for nt1 in strand_piece.children: - other = [] - nt2 = nt1.basepair - if strand_piece.is_fwd: - other.append((nt2,'pair')) - - nt2 = nt2.get_intrahelical_above() - if nt2 is not None and strand_piece.is_fwd: - ## TODO: check if this already exists - other.append((nt2,'paircross')) - - nt2 = nt1.get_intrahelical_above() - if nt2 is not None: - other.append((nt2,'stack')) - nt2 = nt2.basepair - if nt2 is not None and strand_piece.is_fwd: - other.append((nt2,'cross')) - - for nt2,key in other: - """ - if np.linalg.norm(nt2.position-nt1.position) > 7: - import pdb - pdb.set_trace() - """ - key = ','.join((key,nt1.sequence[0],nt2.sequence[0])) - for n1, n2, d in enmTemplate[key]: - d = float(d) - k = 0.1 - if lattice_type == 'honeycomb': - correctionKey = ','.join((key,n1,n2)) - assert(correctionKey in enmCorrectionsHC) - dk,dr = enmCorrectionsHC[correctionKey] - k = float(dk) - d += float(dr) - - i = nt1._get_atomic_index(name=n1) - j = nt2._get_atomic_index(name=n2) - fh.write("bond %d %d %f %.2f\n" % (i,j,k,d)) - - # print("NO STACKS found for:", noStackPrime) - # print("NO BASEPAIRS found for:", noBasepair) - - ## Loop dsDNA regions - push_bonds = [] - processed_segs = set() - ## TODO possibly merge some of this code with SegmentModel.get_consecutive_crossovers() - for segI in self.segments: # TODOTODO: generalize through some abstract intrahelical interface that effectively joins "segments", for now interhelical bonds that cross intrahelically-connected segments are ignored - if not isinstance(segI,DoubleStrandedSegment): continue - - ## Loop over dsDNA regions connected by crossovers - conn_locs = segI.get_contour_sorted_connections_and_locations("crossover") - other_segs = list(set([B.container for c,A,B in conn_locs])) - for segJ in other_segs: - if (segI,segJ) in processed_segs: - continue - processed_segs.add((segI,segJ)) - processed_segs.add((segJ,segI)) - - ## TODO perhaps handle ends that are not between crossovers - - ## Loop over ordered pairs of crossovers between the two - cls = filter(lambda x: x[-1].container == segJ, conn_locs) - cls = sorted( cls, key=lambda x: x[1].get_nt_pos() ) - for cl1,cl2 in zip(cls[:-1],cls[1:]): - c1,A1,B1 = cl1 - c2,A2,B2 = cl2 - - ntsI1,ntsI2 = [segI.contour_to_nt_pos(A.address) for A in (A1,A2)] - ntsJ1,ntsJ2 = [segJ.contour_to_nt_pos(B.address) for B in (B1,B2)] - ntsI = ntsI2-ntsI1+1 - ntsJ = ntsJ2-ntsJ1+1 - assert( np.isclose( ntsI, int(round(ntsI)) ) ) - assert( np.isclose( ntsJ, int(round(ntsJ)) ) ) - ntsI,ntsJ = [int(round(i)) for i in (ntsI,ntsJ)] - - ## Find if dsDNA "segments" are pointing in same direction - ## could move this block out of the loop - tangentA = segI.contour_to_tangent(A1.address) - tangentB = segJ.contour_to_tangent(B1.address) - dot1 = tangentA.dot(tangentB) - tangentA = segI.contour_to_tangent(A2.address) - tangentB = segJ.contour_to_tangent(B2.address) - dot2 = tangentA.dot(tangentB) - - if dot1 > 0.5 and dot2 > 0.5: - ... - elif dot1 < -0.5 and dot2 < -0.5: - ## TODO, reverse - ... - # print("Warning: {} and {} are on antiparallel helices (not yet implemented)... skipping".format(A1,B1)) - continue - else: - # print("Warning: {} and {} are on helices that do not point in similar direction... skipping".format(A1,B1)) - continue - - ## Go through each nucleotide between the two - for ijmin in range(min(ntsI,ntsJ)): - i=j=ijmin - if ntsI < ntsJ: - j = int(round(float(ntsJ*i)/ntsI)) - elif ntsJ < ntsI: - i = int(round(float(ntsI*j)/ntsJ)) - ntI_idx = int(round(ntsI1+i)) - ntJ_idx = int(round(ntsJ1+j)) - - ## Skip nucleotides that are too close to crossovers - if i < 11 or j < 11: continue - if ntsI2-ntI_idx < 11 or ntsJ2-ntJ_idx < 11: continue - - ## Find phosphates at ntI/ntJ - for direction in [True,False]: - try: - i = segI._get_atomic_nucleotide(ntI_idx, direction)._get_atomic_index(name="P") - j = segJ._get_atomic_nucleotide(ntJ_idx, direction)._get_atomic_index(name="P") - push_bonds.append((i,j)) - except: - # print("WARNING: could not find 'P' atom in {}:{} or {}:{}".format( segI, ntI_idx, segJ, ntJ_idx )) - ... - - # print("PUSH BONDS:", len(push_bonds)) - - if not self.useTclForces: - with open("%s.exb" % output_name, 'a') as fh: - for i,j in push_bonds: - fh.write("bond %d %d %f %.2f\n" % (i,j,1.0,31)) - else: - flat_push_bonds = list(sum(push_bonds)) - atomList = list(set( flat_push_bonds )) - with open("%s.forces.tcl" % output_name,'w') as fh: - fh.write("set atomList {%s}\n\n" % - " ".join([str(x-1) for x in atomList]) ) - fh.write("set bonds {%s}\n" % - " ".join([str(x-1) for x in flat_push_bonds]) ) - fh.write(""" -foreach atom $atomList { - addatom $atom -} - -proc calcforces {} { - global atomList bonds - loadcoords rv - - foreach i $atomList { - set force($i) {0 0 0} - } - - foreach {i j} $bonds { - set rvec [vecsub $rv($j) $rv($i)] - # lassign $rvec x y z - # set r [expr {sqrt($x*$x+$y*$y+$z*$z)}] - set r [getbond $rv($j) $rv($i)] - set f [expr {2*($r-31.0)/$r}] - vecadd $force($i) [vecscale $f $rvec] - vecadd $force($j) [vecscale [expr {-1.0*$f}] $rvec] - } - - foreach i $atomList { - addforce $i $force($i) - } - -} -""") - def dimensions_from_structure( self, padding_factor=1.5, isotropic=False ): positions = [] - for s in self.segments: + for s in self.polymers: positions.append(s.contour_to_position(0)) positions.append(s.contour_to_position(0.5)) positions.append(s.contour_to_position(1)) @@ -2965,68 +482,23 @@ proc calcforces {} { dx = dy = dz = max((dx,dy,dz)) return [dx,dy,dz] - def add_grid_potential(self, grid_file, scale=1, per_nucleotide=True): + def add_grid_potential(self, grid_file, scale=1, per_monomer=True): grid_file = Path(grid_file) if not grid_file.is_file(): raise ValueError("Grid file {} does not exist".format(grid_file)) if not grid_file.is_absolute(): grid_file = Path.cwd() / grid_file - self.grid_potentials.append((grid_file,scale,per_nucleotide)) - - def _apply_grid_potentials_to_beads(self, bead_type_dict): - if len(self.grid_potentials) > 1: - raise NotImplementedError("Multiple grid potentials are not yet supported") - - for grid_file, scale, per_nucleotide in self.grid_potentials: - for key,particle_type in bead_type_dict.items(): - if particle_type.name[0] == "O": continue - s = scale*particle_type.nts if per_nucleotide else scale - try: - particle_type.grid = particle_type.grid + (grid_file, s) - except: - particle_type.grid = tuple((grid_file, s)) - - def _generate_atomic_model(self, scale=1): - ## TODO: deprecate - self.generate_atomic_model(scale=scale) - - def generate_atomic_model(self, scale=1): - self.clear_beads() - self.children = self.strands # TODO: is this going to be okay? probably - first_atomic_index = 0 - for s in self.strands: - first_atomic_index = s.generate_atomic_model(scale,first_atomic_index) - self._assign_basepairs() - return - - ## Angle optimization - angles = np.linspace(-180,180,180) - score = [] - for a in angles: - o = rotationAboutAxis([0,0,1], a) - sum2 = count = 0 - for s in self.strands: - s.update_atomic_orientations(o) - for s1,s2 in zip(s.strand_segments[:-1],s.strand_segments[1:]): - nt1 = s1.children[-1] - nt2 = s2.children[0] - o3 = nt1.atoms_by_name["O3'"] - p = nt2.atoms_by_name["P"] - sum2 += np.sum((p.collapsedPosition()-o3.collapsedPosition())**2) - count += 1 - score.append(sum2/count) - print(angles[np.argmin(score)]) - print(score) - - def vmd_tube_tcl(self, file_name="drawTubes.tcl"): + self.grid_potentials.append((grid_file,scale,per_,pmp,er)) + + def vmd_tube_tcl(self, file_name="drawTubes.tcl", radius=5): with open(file_name, 'w') as tclFile: tclFile.write("## beginning TCL script \n") - def draw_tube(segment,radius_value=10, color="cyan", resolution=5): + def draw_tube(polymer,radius_value=10, color="cyan", resolution=5): tclFile.write("## Tube being drawn... \n") - contours = np.linspace(0,1, max(2,1+segment.num_nt//resolution) ) - rs = [segment.contour_to_position(c) for c in contours] + contours = np.linspace(0,1, max(2,1+polymer.num_monomers//resolution) ) + rs = [polymer.contour_to_position(c) for c in contours] radius_value = str(radius_value) tclFile.write("graphics top color {} \n".format(str(color))) @@ -3044,27 +516,19 @@ proc calcforces {} { tclFile.write("graphics top materials on \n") tclFile.write("graphics top material AOEdgy \n") - ## iterate through the model segments - for s in self.segments: - if isinstance(s,DoubleStrandedSegment): - tclFile.write("## dsDNA! \n") - draw_tube(s,10,"cyan") - elif isinstance(s,SingleStrandedSegment): - tclFile.write("## ssDNA! \n") - draw_tube(s,3,"orange",resolution=1.5) - else: - raise Exception ("your model includes beads that are neither ssDNA nor dsDNA") - ## tclFile complete - tclFile.close() + ## iterate through the model polymers + for s in self.polymers: + draw_tube(s,radius,"cyan") + + + def vmd_cylinder_tcl(self, file_name="drawCylinders.tcl", radius=5): - def vmd_cylinder_tcl(self, file_name="drawCylinders.tcl"): - #raise NotImplementedError with open(file_name, 'w') as tclFile: tclFile.write("## beginning TCL script \n") - def draw_cylinder(segment,radius_value=10,color="cyan"): + def draw_cylinder(polymer,radius_value=10,color="cyan"): tclFile.write("## cylinder being drawn... \n") - r0 = segment.contour_to_position(0) - r1 = segment.contour_to_position(1) + r0 = polymer.contour_to_position(0) + r1 = polymer.contour_to_position(1) radius_value = str(radius_value) color = str(color) @@ -3076,15 +540,6 @@ proc calcforces {} { tclFile.write("graphics top materials on \n") tclFile.write("graphics top material AOEdgy \n") - ## iterate through the model segments - for s in self.segments: - if isinstance(s,DoubleStrandedSegment): - tclFile.write("## dsDNA! \n") - draw_cylinder(s,10,"cyan") - elif isinstance(s,SingleStrandedSegment): - tclFile.write("## ssDNA! \n") - draw_cylinder(s,3,"orange") - else: - raise Exception ("your model includes beads that are neither ssDNA nor dsDNA") - ## tclFile complete - tclFile.close() + ## iterate through the model polymers + for s in self.polymers: + draw_cylinder(s,radius,"cyan") diff --git a/arbdmodel/nonbonded.py b/arbdmodel/interactions.py similarity index 100% rename from arbdmodel/nonbonded.py rename to arbdmodel/interactions.py