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.
- {pyloopsage-0.1.1 → pyloopsage-1.0.1}/PKG-INFO +1 -1
- pyloopsage-1.0.1/loopsage/__init__.py +4 -0
- pyloopsage-1.0.1/loopsage/args_definition.py +160 -0
- pyloopsage-1.0.1/loopsage/em.py +131 -0
- pyloopsage-1.0.1/loopsage/initial_structures.py +137 -0
- pyloopsage-1.0.1/loopsage/md.py +175 -0
- pyloopsage-1.0.1/loopsage/plots.py +273 -0
- pyloopsage-1.0.1/loopsage/preproc.py +156 -0
- pyloopsage-1.0.1/loopsage/run.py +84 -0
- pyloopsage-1.0.1/loopsage/stochastic_simulation.py +305 -0
- pyloopsage-1.0.1/loopsage/utils.py +468 -0
- pyloopsage-1.0.1/loopsage/vizualization_tools.py +153 -0
- {pyloopsage-0.1.1 → pyloopsage-1.0.1}/pyLoopSage.egg-info/PKG-INFO +1 -1
- pyloopsage-1.0.1/pyLoopSage.egg-info/SOURCES.txt +20 -0
- pyloopsage-1.0.1/pyLoopSage.egg-info/top_level.txt +1 -0
- {pyloopsage-0.1.1 → pyloopsage-1.0.1}/setup.py +2 -2
- pyloopsage-0.1.1/pyLoopSage.egg-info/SOURCES.txt +0 -9
- pyloopsage-0.1.1/pyLoopSage.egg-info/top_level.txt +0 -1
- {pyloopsage-0.1.1 → pyloopsage-1.0.1}/LICENSE +0 -0
- {pyloopsage-0.1.1 → pyloopsage-1.0.1}/README.md +0 -0
- {pyloopsage-0.1.1 → pyloopsage-1.0.1}/pyLoopSage.egg-info/dependency_links.txt +0 -0
- {pyloopsage-0.1.1 → pyloopsage-1.0.1}/pyLoopSage.egg-info/entry_points.txt +0 -0
- {pyloopsage-0.1.1 → pyloopsage-1.0.1}/pyLoopSage.egg-info/requires.txt +0 -0
- {pyloopsage-0.1.1 → pyloopsage-1.0.1}/setup.cfg +0 -0
|
@@ -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)
|