pyLoopSage 0.1.1__tar.gz → 1.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyLoopSage
3
- Version: 0.1.1
3
+ Version: 1.0.1
4
4
  Summary: An energy-based stochastic model of loop extrusion in chromatin.
5
5
  Home-page: https://github.com/SFGLab/pyLoopSage
6
6
  Author: Sebastian Korsak
@@ -0,0 +1,4 @@
1
+ # Importing specific functions or classes from submodules
2
+ from .run import main
3
+
4
+ __version__ = "1.0.1"
@@ -0,0 +1,160 @@
1
+ import datetime
2
+ import re
3
+ from dataclasses import dataclass
4
+ from math import pi
5
+ from typing import Union
6
+ import argparse
7
+ import openmm as mm
8
+ from openmm.unit import Quantity
9
+
10
+ @dataclass
11
+ class Arg(object):
12
+ name: str
13
+ help: str
14
+ type: type
15
+ default: Union[str, float, int, bool, Quantity, None]
16
+ val: Union[str, float, int, bool, Quantity, None]
17
+
18
+ # Define custom type to parse list from string
19
+ def parse_list(s):
20
+ try:
21
+ return [int(x.strip()) for x in s.strip('[]').split(',')]
22
+ except ValueError:
23
+ raise argparse.ArgumentTypeError("Invalid list format. Must be a comma-separated list of integers.")
24
+
25
+ class ListOfArgs(list):
26
+ quantity_regexp = re.compile(r'(?P<value>[-+]?\d+(?:\.\d+)?) ?(?P<unit>\w+)')
27
+
28
+ def get_arg(self, name: str) -> Arg:
29
+ """Stupid arg search in list of args"""
30
+ name = name.upper()
31
+ for i in self:
32
+ if i.name == name:
33
+ return i
34
+ raise ValueError(f"No such arg: {name}")
35
+
36
+ def __getattr__(self, item):
37
+ return self.get_arg(item).val
38
+
39
+ def parse_args(self):
40
+ parser = argparse.ArgumentParser()
41
+ for arg in self.arg_list:
42
+ parser.add_argument(arg['name'], help=arg['help'], type=arg.get('type', str), default=arg.get('default', ''), val=arg.get('val', ''))
43
+
44
+ args = parser.parse_args()
45
+ parsed_args = {arg['name']: getattr(args, arg['name']) for arg in self.arg_list}
46
+ return parsed_args
47
+
48
+ def parse_quantity(self, val: str) -> Union[Quantity, None]:
49
+ if val == '':
50
+ return None
51
+ match_obj = self.quantity_regexp.match(val)
52
+ value, unit = match_obj.groups()
53
+ try:
54
+ unit = getattr(mm.unit, unit)
55
+ except AttributeError:
56
+ raise ValueError(f"I Can't recognise unit {unit} in expression {val}. Example of valid quantity: 12.3 femtosecond.")
57
+ return Quantity(value=float(value), unit=unit)
58
+
59
+ def to_python(self):
60
+ """Casts string args to ints, floats, bool..."""
61
+ for i in self:
62
+ if i.val == '':
63
+ i.val = None
64
+ elif i.name == "HR_K_PARAM": # Workaround for complex unit
65
+ i.val = Quantity(float(i.val), mm.unit.kilojoule_per_mole / mm.unit.nanometer ** 2)
66
+ elif i.type == str:
67
+ continue
68
+ elif i.type == int:
69
+ i.val = int(i.val)
70
+ elif i.type == float:
71
+ i.val = float(i.val)
72
+ elif i.type == bool:
73
+ if i.val.lower() in ['true', '1', 'y', 'yes']:
74
+ i.val = True
75
+ elif i.val.lower() in ['false', '0', 'n', 'no']:
76
+ i.val = False
77
+ else:
78
+ raise ValueError(f"Can't convert {i.val} into bool type.")
79
+ elif i.type == Quantity:
80
+ try:
81
+ i.val = self.parse_quantity(i.val)
82
+ except AttributeError:
83
+ raise ValueError(f"Can't parse: {i.name} = {i.val}")
84
+ else:
85
+ raise ValueError(f"Can't parse: {i.name} = {i.val}")
86
+
87
+ def get_complete_config(self) -> str:
88
+ w = "####################\n"
89
+ w += "# LoopSage Model #\n"
90
+ w += "####################\n\n"
91
+ w += "# This is automatically generated config file.\n"
92
+ w += f"# Generated at: {datetime.datetime.now().isoformat()}\n\n"
93
+ w += "# Notes:\n"
94
+ w += "# Some fields require units. Units are represented as objects from mm.units module.\n"
95
+ w += "# Simple units are parsed directly. For example: \n"
96
+ w += "# HR_R0_PARAM = 0.2 nanometer\n"
97
+ w += "# But more complex units does not have any more sophisticated parser written, and will fail.'\n"
98
+ w += "# In such cases the unit is fixed (and noted in comment), so please convert complex units manually if needed.\n"
99
+ w += "# <float> and <int> types does not require any unit. Quantity require unit.\n\n"
100
+ w += "# Default values does not mean valid value. In many places it's only a empty field that need to be filled.\n\n"
101
+
102
+ w += '[Main]'
103
+ for i in self:
104
+ w += f'; {i.help}, type: {i.type.__name__}, default: {i.default}\n'
105
+ if i.val is None:
106
+ w += f'{i.name} = \n\n'
107
+ else:
108
+ if i.type == Quantity:
109
+ # noinspection PyProtectedMember
110
+ w += f'{i.name} = {i.val._value} {i.val.unit.get_name()}\n\n'
111
+ else:
112
+ w += f'{i.name} = {i.val}\n\n'
113
+ w = w[:-2]
114
+ return w
115
+
116
+ def write_config_file(self):
117
+ auto_config_filename = 'config_auto.ini'
118
+ with open(auto_config_filename, 'w') as f:
119
+ f.write(self.get_complete_config())
120
+ print(f"Automatically generated config file saved in {auto_config_filename}")
121
+
122
+ available_platforms = [mm.Platform.getPlatform(i).getName() for i in range(mm.Platform.getNumPlatforms())]
123
+
124
+ args = ListOfArgs([
125
+ # Platform settings
126
+ Arg('PLATFORM', help=f"name of the platform. Available choices: {' '.join(available_platforms)}", type=str, default='CPU', val='CPU'),
127
+ Arg('DEVICE', help="device index for CUDA or OpenCL (count from 0)", type=str, default='', val=''),
128
+
129
+ # Input data
130
+ Arg('N_BEADS', help="Number of Simulation Beads.", type=int, default='', val=''),
131
+ Arg('BEDPE_PATH', help="A .bedpe file path with loops. It is required.", type=str, default='', val=''),
132
+ Arg('OUT_PATH', help="Output folder name.", type=str, default='../results', val='../results'),
133
+ Arg('REGION_START', help="Starting region coordinate.", type=int, default='', val=''),
134
+ Arg('REGION_END', help="Ending region coordinate.", type=int, default='', val=''),
135
+ Arg('CHROM', help="Chromosome that corresponds the the modelling region of interest (in case that you do not want to model the whole genome).", type=str, default='', val=''),
136
+
137
+ # Stochastic Simulation parameters
138
+ Arg('LEF_RW', help="True in case that you would like to make cohesins slide as random walk, instead of sliding only in one direction.", type=bool, default='True', val='True'),
139
+ Arg('N_STEPS', help="Number of Monte Carlo steps.", type=int, default='', val=''),
140
+ Arg('N_LEF', help="Number of loop extrusion factors (condensins and cohesins).", type=int, default='', val=''),
141
+ Arg('MC_STEP', help="Monte Carlo frequency.", type=int, default='200', val='200'),
142
+ Arg('BURNIN', help="Burnin-period (steps that are considered before equillibrium).", type=int, default='1000', val='1000'),
143
+ Arg('T_INIT', help="Initial Temperature of the Stochastic Model.", type=float, default='1.8', val='1.8'),
144
+ Arg('T_FINAL', help="Final Temperature of the Stochastic Model.", type=float, default='0.01', val='0.01'),
145
+ Arg('METHOD', help="Stochastic modelling method. It can be Metropolis or Simulated Annealing.", type=str, default='Annealing', val='Annealing'),
146
+ Arg('FOLDING_COEFF', help="Folding coefficient.", type=float, default='1.0', val='1.0'),
147
+ Arg('CROSS_COEFF', help="LEF crossing coefficient.", type=float, default='1.0', val='1.0'),
148
+ Arg('BIND_COEFF', help="CTCF binding coefficient.", type=float, default='1.0', val='1.0'),
149
+
150
+ # Molecular Dynamic Properties
151
+ Arg('INITIAL_STRUCTURE_TYPE', help="you can choose between: rw, confined_rw, self_avoiding_rw, helix, circle, spiral, sphere.", type=str, default='rw', val='rw'),
152
+ Arg('SIMULATION_TYPE', help="It can be either EM (multiple energy minimizations) or MD (one energy minimization and then run molecular dynamics).", type=str, default='', val=''),
153
+ Arg('FORCEFIELD_PATH', help="Path to XML file with forcefield.", type=str, default='forcefield/classic_sm_ff.xml', val='forcefield/classic_sm_ff.xml'),
154
+ Arg('ANGLE_FF_STRENGTH', help="Angle force strength.", type=float, default='200.0', val='200.0'),
155
+ Arg('LE_FF_LENGTH', help="Equillibrium distance of loop forces.", type=float, default='0.0', val='0.0'),
156
+ Arg('LE_FF_STRENGTH', help="Equillibrium distance of loop forces.", type=float, default='300000.0', val='300000.0'),
157
+ Arg('EV_FF_STRENGTH', help="Excluded-volume strength.", type=float, default='10.0', val='10.0'),
158
+ Arg('TOLERANCE', help="Tolerance that works as stopping condition for energy minimization.", type=float, default='0.001', val='0.001'),
159
+ Arg('SIM_STEP', help="This is the amount of simulation steps that are perform each time that we change the loop forces. If this number is too high, the simulation is slow, if is too low it may not have enough time to adapt the structure to the new constraints.", type=int, default='100', val='100'),
160
+ ])
@@ -0,0 +1,131 @@
1
+ import copy
2
+ import time
3
+ import numpy as np
4
+ import openmm as mm
5
+ import openmm.unit as u
6
+ from tqdm import tqdm
7
+ from sys import stdout
8
+ from mdtraj.reporters import HDF5Reporter
9
+ from scipy import ndimage
10
+ from openmm.app import PDBFile, PDBxFile, ForceField, Simulation, PDBReporter, PDBxReporter, DCDReporter, StateDataReporter, CharmmPsfFile
11
+ from .utils import *
12
+ from .initial_structures import *
13
+
14
+ class EM_LE:
15
+ def __init__(self,M,N,N_beads,burnin,MC_step,path,platform,angle_ff_strength=200,le_distance=0.1,le_ff_strength=300000.0,ev_ff_strength=10.0,tolerance=0.001):
16
+ '''
17
+ M, N (np arrays): Position matrix of two legs of cohesin m,n.
18
+ Rows represent loops/cohesins and columns represent time
19
+ N_beads (int): The number of beads of initial structure.
20
+ step (int): sampling rate
21
+ path (int): the path where the simulation will save structures etc.
22
+ '''
23
+ self.M, self.N = M, N
24
+ self.N_coh, self.N_steps = M.shape
25
+ self.N_beads, self.step, self.burnin = N_beads, MC_step, burnin//MC_step
26
+ self.path = path
27
+ self.platform = platform
28
+ self.angle_ff_strength = angle_ff_strength
29
+ self.le_distance = le_distance
30
+ self.le_ff_strength = le_ff_strength
31
+ self.ev_ff_strength = ev_ff_strength
32
+ self.tolerance = tolerance
33
+
34
+ def run_pipeline(self,write_files=False,plots=False):
35
+ '''
36
+ This is the basic function that runs the molecular simulation pipeline.
37
+
38
+ Input parameters:
39
+ run_MD (bool): True if user wants to run molecular simulation (not only energy minimization).
40
+ sim_step (int): the simulation step of Langevin integrator.
41
+ write_files (bool): True if the user wants to save the structures that determine the simulation ensemble.
42
+ plots (bool): True if the user wants to see the output average heatmaps.
43
+ '''
44
+ # Define initial structure
45
+ print('Building initial structure...')
46
+ points = compute_init_struct(self.N_beads,mode='rw')
47
+ write_mmcif(points,self.path+'/LE_init_struct.cif')
48
+ generate_psf(self.N_beads,self.path+'/other/LE_init_struct.psf')
49
+ print('Done brother ;D\n')
50
+
51
+ counter = 0
52
+ sum_heat = np.zeros((self.N_beads,self.N_beads))
53
+ print('Running energy minimizations...')
54
+ for i in tqdm(range(self.burnin,self.N_steps)):
55
+ # Define System
56
+ pdb = PDBxFile(self.path+'/LE_init_struct.cif')
57
+ forcefield = ForceField('forcefields/classic_sm_ff.xml')
58
+ self.system = forcefield.createSystem(pdb.topology, nonbondedCutoff=1*u.nanometer)
59
+ integrator = mm.LangevinIntegrator(310, 0.05, 100 * mm.unit.femtosecond)
60
+
61
+ # Add forces
62
+ ms,ns = self.M[:,i], self.N[:,i]
63
+ self.add_forcefield(ms,ns)
64
+
65
+ # Minimize energy
66
+ platform = mm.Platform.getPlatformByName(self.platform)
67
+ simulation = Simulation(pdb.topology, self.system, integrator, platform)
68
+ simulation.context.setPositions(pdb.positions)
69
+ simulation.minimizeEnergy(tolerance=self.tolerance)
70
+ self.state = simulation.context.getState(getPositions=True)
71
+ PDBxFile.writeFile(pdb.topology, self.state.getPositions(), open(self.path+f'/ensemble/EMLE_{i-self.burnin+1}.cif', 'w'))
72
+ save_path = self.path+f'/heatmaps/heat_{i-self.burnin+1}.svg' if write_files else None
73
+ sum_heat+=get_heatmap(self.state.getPositions(),save_path=save_path,save=write_files)
74
+ counter+=1
75
+ print('Energy minimizations done :D\n')
76
+
77
+ self.avg_heat = sum_heat/counter
78
+ np.save(self.path+f'/other/avg_heatmap.npy',self.avg_heat)
79
+ if plots:
80
+ figure(figsize=(10, 10))
81
+ plt.imshow(self.avg_heat,cmap="Reds",vmax=1)
82
+ plt.colorbar()
83
+ plt.savefig(self.path+f'/plots/avg_heatmap.svg',format='svg',dpi=500)
84
+ plt.savefig(self.path+f'/plots/avg_heatmap.pdf',format='pdf',dpi=500)
85
+ # plt.colorbar()
86
+ plt.close()
87
+
88
+ return self.avg_heat
89
+
90
+ def add_forcefield(self,ms,ns):
91
+ '''
92
+ Here is the definition of the forcefield.
93
+
94
+ There are the following energies:
95
+ - ev force: repelling LJ-like forcefield
96
+ - harmonic bond force: to connect adjacent beads.
97
+ - angle force: for polymer stiffness.
98
+ - LE forces: this is a list of force objects. Each object corresponds to a different cohesin. It is needed to define a force for each time step.
99
+ '''
100
+ # Leonard-Jones potential for excluded volume
101
+ self.ev_force = mm.CustomNonbondedForce('epsilon*((sigma1+sigma2)/r)^6')
102
+ self.ev_force.addGlobalParameter('epsilon', defaultValue=self.ev_ff_strength)
103
+ self.ev_force.addPerParticleParameter('sigma')
104
+ self.system.addForce(self.ev_force)
105
+ for i in range(self.system.getNumParticles()):
106
+ self.ev_force.addParticle([0.1])
107
+
108
+ # Harmonic bond borce between succesive beads
109
+ self.bond_force = mm.HarmonicBondForce()
110
+ self.system.addForce(self.bond_force)
111
+ for i in range(self.system.getNumParticles() - 1):
112
+ self.bond_force.addBond(i, i + 1, 0.1, 300000.0)
113
+
114
+ # Harmonic angle force between successive beads so as to make chromatin rigid
115
+ self.angle_force = mm.HarmonicAngleForce()
116
+ self.system.addForce(self.angle_force)
117
+ for i in range(self.system.getNumParticles() - 2):
118
+ self.angle_force.addAngle(i, i + 1, i + 2, np.pi, self.angle_ff_strength)
119
+
120
+ # LE force that connects cohesin restraints
121
+ self.LE_force = mm.HarmonicBondForce()
122
+ self.system.addForce(self.LE_force)
123
+ for nn in range(self.N_coh):
124
+ self.LE_force.addBond(ms[nn], ns[nn], self.le_distance, self.le_ff_strength)
125
+
126
+ def main():
127
+ # A potential example
128
+ M = np.load('/home/skorsak/Dropbox/LoopSage/files/region_[48100000,48700000]_chr3/Annealing_Nbeads500_ncoh50/Ms.npy')
129
+ N = np.load('/home/skorsak/Dropbox/LoopSage/files/region_[48100000,48700000]_chr3/Annealing_Nbeads500_ncoh50/Ns.npy')
130
+ md = EM_LE(4*M,4*N,2000,5,1)
131
+ md.run_pipeline(write_files=False,plots=True,sim_step=100)
@@ -0,0 +1,137 @@
1
+ import numpy as np
2
+ from tqdm import tqdm
3
+
4
+ def dist(p1: np.ndarray, p2: np.ndarray) -> float:
5
+ """Mierzy dystans w przestrzeni R^3"""
6
+ x1, y1, z1 = p1
7
+ x2, y2, z2 = p2
8
+ return ((x1 - x2) ** 2 + (y1 - y2) ** 2 + (z1 - z2) ** 2) ** 0.5 # faster than np.linalg.norm
9
+
10
+ def random_versor() -> np.ndarray:
11
+ """Losuje wersor"""
12
+ x = np.random.uniform(-1, 1)
13
+ y = np.random.uniform(-1, 1)
14
+ z = np.random.uniform(-1, 1)
15
+ d = (x ** 2 + y ** 2 + z ** 2) ** 0.5
16
+ return np.array([x / d, y / d, z / d])
17
+
18
+ def self_avoiding_random_walk(n: int, step: float = 1.0, bead_radius: float = 0.5, epsilon: float = 0.001, two_dimensions=False) -> np.ndarray:
19
+ potential_new_step = [0, 0, 0]
20
+ while True:
21
+ points = [np.array([0, 0, 0])]
22
+ for _ in tqdm(range(n - 1)):
23
+ step_is_ok = False
24
+ trials = 0
25
+ while not step_is_ok and trials < 1000:
26
+ potential_new_step = points[-1] + step * random_versor()
27
+ if two_dimensions:
28
+ potential_new_step[2] = 0
29
+ for j in points:
30
+ d = dist(j, potential_new_step)
31
+ if d < 2 * bead_radius - epsilon:
32
+ trials += 1
33
+ break
34
+ else:
35
+ step_is_ok = True
36
+ points.append(potential_new_step)
37
+ points = np.array(points)
38
+ return points
39
+
40
+ def polymer_circle(n: int, z_stretch: float = 1.0, radius: float = 5.0) -> np.ndarray:
41
+ points = []
42
+ angle_increment = 360 / float(n)
43
+ radius = 1 / (2 * np.sin(np.radians(angle_increment) / 2.)) if radius==None else radius
44
+ z_stretch = z_stretch / n
45
+ z = 0
46
+ for i in range(n):
47
+ x = radius * np.cos(angle_increment * i * np.pi / 180)
48
+ y = radius * np.sin(angle_increment * i * np.pi / 180)
49
+ if z_stretch != 0:
50
+ z += z_stretch
51
+ points.append((x, y, z))
52
+ points = np.array(points)
53
+ return points
54
+
55
+ def helix_structure(N_beads, radius=1, pitch=2):
56
+ theta = np.linspace(0, 4 * np.pi, N_beads) # 2 full turns
57
+ x = radius * np.cos(theta)
58
+ y = radius * np.sin(theta)
59
+ z = np.linspace(0, pitch * N_beads, N_beads)
60
+ V = np.column_stack((x, y, z))
61
+ return V
62
+
63
+ def spiral_structure(N_beads, initial_radius=1, pitch=1, growth_factor=0.05):
64
+ theta = np.linspace(0, 4 * np.pi, N_beads)
65
+ radius = initial_radius + growth_factor * np.arange(N_beads)
66
+ x = radius * np.cos(theta)
67
+ y = radius * np.sin(theta)
68
+ z = np.linspace(0, pitch * N_beads, N_beads)
69
+ V = np.column_stack((x, y, z))
70
+ return V
71
+
72
+ def sphere_surface_structure(N_beads, radius=1):
73
+ phi = np.random.uniform(0, 2 * np.pi, N_beads)
74
+ costheta = np.random.uniform(-1, 1, N_beads)
75
+ u = np.random.uniform(0, 1, N_beads)
76
+
77
+ theta = np.arccos(costheta)
78
+ r = radius * u ** (1/3)
79
+
80
+ x = r * np.sin(theta) * np.cos(phi)
81
+ y = r * np.sin(theta) * np.sin(phi)
82
+ z = r * np.cos(theta)
83
+
84
+ V = np.column_stack((x, y, z))
85
+ return V
86
+
87
+ def confined_random_walk(N_beads, box_size=5):
88
+ V = np.zeros((N_beads, 3))
89
+ for i in range(1, N_beads):
90
+ step = np.random.choice([-1, 1], size=3) # Random step in x, y, z
91
+ V[i] = V[i-1] + step
92
+ # Keep the points within a confined box (folding in)
93
+ V[i] = np.clip(V[i], -box_size, box_size)
94
+ return V
95
+
96
+ def trefoil_knot_structure(N_beads, scale=5):
97
+ t = np.linspace(0, 2 * np.pi, N_beads)
98
+ x = scale * (np.sin(t) + 2 * np.sin(2 * t))
99
+ y = scale * (np.cos(t) - 2 * np.cos(2 * t))
100
+ z = -scale * np.sin(3 * t)
101
+
102
+ V = np.column_stack((x, y, z))
103
+ return V
104
+
105
+ def random_walk_structure(N_beads, step_size=1):
106
+ # Initialize the structure array
107
+ V = np.zeros((N_beads, 3))
108
+
109
+ # Loop over each bead, starting from the second one
110
+ for i in range(1, N_beads):
111
+ # Randomly pick a direction for each step
112
+ step_direction = np.random.normal(size=3)
113
+ step_direction /= np.linalg.norm(step_direction) # Normalize to make it unit length
114
+
115
+ # Move the current bead from the last bead by a fixed step size
116
+ V[i] = V[i-1] + step_size * step_direction
117
+
118
+ return V
119
+
120
+ def compute_init_struct(N_beads,mode='rw'):
121
+ match mode:
122
+ case 'rw':
123
+ return random_walk_structure(N_beads)
124
+ case 'confined_rw':
125
+ return confined_random_walk(N_beads)
126
+ case 'self_avoiding_rw':
127
+ return self_avoiding_random_walk(N_beads)
128
+ case 'circle':
129
+ return polymer_circle(N_beads)
130
+ case 'helix':
131
+ return helix_structure(N_beads)
132
+ case 'spiral':
133
+ return spiral_structure(N_beads)
134
+ case 'sphere':
135
+ return sphere_surface_structure(N_beads)
136
+ case _:
137
+ return IndentationError('Invalid option for initial structure.')
@@ -0,0 +1,175 @@
1
+ #########################################################################
2
+ ########### CREATOR: SEBASTIAN KORSAK, WARSAW 2022 ######################
3
+ #########################################################################
4
+
5
+ import copy
6
+ import time
7
+ import numpy as np
8
+ import openmm as mm
9
+ import openmm.unit as u
10
+ from tqdm import tqdm
11
+ from sys import stdout
12
+ from mdtraj.reporters import HDF5Reporter
13
+ from scipy import ndimage
14
+ from openmm.app import PDBFile, PDBxFile, ForceField, Simulation, PDBReporter, PDBxReporter, DCDReporter, StateDataReporter, CharmmPsfFile
15
+ from .utils import *
16
+ from .initial_structures import *
17
+
18
+ class MD_LE:
19
+ def __init__(self,M,N,N_beads,burnin,MC_step,path,platform,angle_ff_strength=200,le_distance=0.1,le_ff_strength=300000.0,ev_ff_strength=10.0,tolerance=0.001):
20
+ '''
21
+ M, N (np arrays): Position matrix of two legs of cohesin m,n.
22
+ Rows represent loops/cohesins and columns represent time
23
+ N_beads (int): The number of beads of initial structure.
24
+ step (int): sampling rate
25
+ path (int): the path where the simulation will save structures etc.
26
+ '''
27
+ self.M, self.N = M, N
28
+ self.N_coh, self.N_steps = M.shape
29
+ self.N_beads, self.step, self.burnin = N_beads, MC_step, burnin//MC_step
30
+ self.path = path
31
+ self.platform = platform
32
+ self.angle_ff_strength = angle_ff_strength
33
+ self.le_distance = le_distance
34
+ self.le_ff_strength = le_ff_strength
35
+ self.ev_ff_strength = ev_ff_strength
36
+ self.tolerance = tolerance
37
+
38
+ def run_pipeline(self,run_MD=True,sim_step=100,write_files=False,plots=False):
39
+ '''
40
+ This is the basic function that runs the molecular simulation pipeline.
41
+
42
+ Input parameters:
43
+ run_MD (bool): True if user wants to run molecular simulation (not only energy minimization).
44
+ sim_step (int): the simulation step of Langevin integrator.
45
+ write_files (bool): True if the user wants to save the structures that determine the simulation ensemble.
46
+ plots (bool): True if the user wants to see the output average heatmaps.
47
+ '''
48
+ # Parameters
49
+ self.angle_ff_strength=200
50
+ self.le_distance=0.1
51
+ self.le_ff_strength=300000.0
52
+ self.tolerance=0.001
53
+
54
+ # Define initial structure
55
+ print('Building initial structure...')
56
+ points = compute_init_struct(self.N_beads,mode='rw')
57
+ write_mmcif(points,self.path+'/LE_init_struct.cif')
58
+ generate_psf(self.N_beads,self.path+'/other/LE_init_struct.psf')
59
+ print('Done brother ;D\n')
60
+
61
+ # Define System
62
+ pdb = PDBxFile(self.path+'/LE_init_struct.cif')
63
+ forcefield = ForceField('forcefields/classic_sm_ff.xml')
64
+ self.system = forcefield.createSystem(pdb.topology, nonbondedCutoff=1*u.nanometer)
65
+ integrator = mm.LangevinIntegrator(310, 0.05, 100 * mm.unit.femtosecond)
66
+
67
+ # Add forces
68
+ print('Adding forces...')
69
+ self.add_forcefield()
70
+ print('Forces added ;)\n')
71
+
72
+ # Minimize energy
73
+ print('Minimizing energy...')
74
+ platform = mm.Platform.getPlatformByName(self.platform)
75
+ self.simulation = Simulation(pdb.topology, self.system, integrator, platform)
76
+ self.simulation.reporters.append(StateDataReporter(stdout, (self.N_steps*sim_step)//100, step=True, totalEnergy=True, potentialEnergy=True, temperature=True))
77
+ self.simulation.reporters.append(DCDReporter(self.path+'/other/stochastic_LE.dcd', 5))
78
+ self.simulation.context.setPositions(pdb.positions)
79
+ current_platform = self.simulation.context.getPlatform()
80
+ print(f"Simulation will run on platform: {current_platform.getName()}")
81
+ self.simulation.minimizeEnergy(tolerance=self.tolerance)
82
+ print('Energy minimization done :D\n')
83
+
84
+ # Run molecular dynamics simulation
85
+ if run_MD:
86
+ print('Running molecular dynamics (wait for 100 steps)...')
87
+ start = time.time()
88
+ heats = list()
89
+ for i in range(1,self.N_steps):
90
+ self.change_loop(i)
91
+ self.simulation.step(sim_step)
92
+ if i>=self.burnin:
93
+ self.state = self.simulation.context.getState(getPositions=True)
94
+ if write_files: PDBxFile.writeFile(pdb.topology, self.state.getPositions(), open(self.path+f'/ensemble/MDLE_{i-self.burnin+1}.cif', 'w'))
95
+ save_path = self.path+f'/heatmaps/heat_{i-self.burnin+1}.svg' if write_files else None
96
+ heats.append(get_heatmap(self.state.getPositions(),save_path=save_path,save=write_files))
97
+ end = time.time()
98
+ elapsed = end - start
99
+
100
+ print(f'Everything is done! Simulation finished succesfully!\nMD finished in {elapsed/60:.2f} minutes.\n')
101
+
102
+ self.avg_heat = np.average(heats,axis=0)
103
+ self.std_heat = np.std(heats,axis=0)
104
+ np.save(self.path+f'/other/avg_heatmap.npy',self.avg_heat)
105
+ np.save(self.path+f'/other/std_heatmap.npy',self.std_heat)
106
+ if plots:
107
+ self.plot_heat(self.avg_heat,f'/plots/avg_heatmap.svg')
108
+ self.plot_heat(self.std_heat,f'/plots/std_heatmap.svg')
109
+ return self.avg_heat
110
+
111
+ def change_loop(self,i):
112
+ force_idx = self.system.getNumForces()-1
113
+ self.system.removeForce(force_idx)
114
+ self.add_loops(i)
115
+ self.simulation.context.reinitialize(preserveState=True)
116
+ self.LE_force.updateParametersInContext(self.simulation.context)
117
+
118
+ def add_evforce(self):
119
+ 'Leonard-Jones potential for excluded volume'
120
+ self.ev_force = mm.CustomNonbondedForce('epsilon*((sigma1+sigma2)/(r+r_small))^6')
121
+ self.ev_force.addGlobalParameter('epsilon', defaultValue=self.ev_ff_strength)
122
+ self.ev_force.addGlobalParameter('r_small', defaultValue=0.01)
123
+ self.ev_force.addPerParticleParameter('sigma')
124
+ for i in range(self.N_beads):
125
+ self.ev_force.addParticle([0.1])
126
+ self.system.addForce(self.ev_force)
127
+
128
+ def add_bonds(self):
129
+ 'Harmonic bond borce between succesive beads'
130
+ self.bond_force = mm.HarmonicBondForce()
131
+ for i in range(self.N_beads - 1):
132
+ self.bond_force.addBond(i, i + 1, 0.1, 3e5)
133
+ self.system.addForce(self.bond_force)
134
+
135
+ def add_stiffness(self):
136
+ 'Harmonic angle force between successive beads so as to make chromatin rigid'
137
+ self.angle_force = mm.HarmonicAngleForce()
138
+ for i in range(self.N_beads - 2):
139
+ self.angle_force.addAngle(i, i + 1, i + 2, np.pi, self.angle_ff_strength)
140
+ self.system.addForce(self.angle_force)
141
+
142
+ def add_loops(self,i=0):
143
+ 'LE force that connects cohesin restraints'
144
+ self.LE_force = mm.HarmonicBondForce()
145
+ for nn in range(self.N_coh):
146
+ self.LE_force.addBond(self.M[nn,i], self.N[nn,i], self.le_distance, self.le_ff_strength)
147
+ self.system.addForce(self.LE_force)
148
+
149
+ def add_forcefield(self):
150
+ '''
151
+ Here is the definition of the forcefield.
152
+
153
+ There are the following energies:
154
+ - ev force: repelling LJ-like forcefield
155
+ - harmonic bond force: to connect adjacent beads.
156
+ - angle force: for polymer stiffness.
157
+ - loop forces: this is a list of force objects. Each object corresponds to a different cohesin. It is needed to define a force for each time step.
158
+ '''
159
+ self.add_evforce()
160
+ self.add_bonds()
161
+ self.add_stiffness()
162
+ self.add_loops()
163
+
164
+ def plot_heat(self,img,file_name):
165
+ figure(figsize=(10, 10))
166
+ plt.imshow(img,cmap="Reds",vmax=1)
167
+ plt.savefig(self.path+file_name,format='svg',dpi=500)
168
+ plt.close()
169
+
170
+ def main():
171
+ # A potential example
172
+ M = np.load('/home/skorsak/Dropbox/LoopSage/files/region_[48100000,48700000]_chr3/Annealing_Nbeads500_ncoh50/Ms.npy')
173
+ N = np.load('/home/skorsak/Dropbox/LoopSage/files/region_[48100000,48700000]_chr3/Annealing_Nbeads500_ncoh50/Ns.npy')
174
+ md = MD_LE(4*M,4*N,2000,5,1)
175
+ md.run_pipeline(write_files=False,plots=True,sim_step=100)