pyLoopSage 1.1.3__tar.gz → 1.1.5__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.
Files changed (26) hide show
  1. pyloopsage-1.1.5/MANIFEST.in +1 -0
  2. {pyloopsage-1.1.3/pyLoopSage.egg-info → pyloopsage-1.1.5}/PKG-INFO +47 -1
  3. {pyloopsage-1.1.3 → pyloopsage-1.1.5}/README.md +45 -0
  4. {pyloopsage-1.1.3 → pyloopsage-1.1.5}/loopsage/__init__.py +1 -1
  5. {pyloopsage-1.1.3 → pyloopsage-1.1.5}/loopsage/args_definition.py +1 -0
  6. pyloopsage-1.1.5/loopsage/knots.py +210 -0
  7. {pyloopsage-1.1.3 → pyloopsage-1.1.5}/loopsage/plots.py +62 -52
  8. {pyloopsage-1.1.3 → pyloopsage-1.1.5}/loopsage/run.py +5 -2
  9. {pyloopsage-1.1.3 → pyloopsage-1.1.5}/loopsage/stochastic_simulation.py +10 -1
  10. {pyloopsage-1.1.3 → pyloopsage-1.1.5}/loopsage/utils.py +11 -2
  11. {pyloopsage-1.1.3 → pyloopsage-1.1.5/pyLoopSage.egg-info}/PKG-INFO +47 -1
  12. {pyloopsage-1.1.3 → pyloopsage-1.1.5}/pyLoopSage.egg-info/SOURCES.txt +1 -0
  13. {pyloopsage-1.1.3 → pyloopsage-1.1.5}/pyLoopSage.egg-info/requires.txt +1 -0
  14. {pyloopsage-1.1.3 → pyloopsage-1.1.5}/setup.py +2 -1
  15. pyloopsage-1.1.3/MANIFEST.in +0 -1
  16. {pyloopsage-1.1.3 → pyloopsage-1.1.5}/LICENSE +0 -0
  17. {pyloopsage-1.1.3 → pyloopsage-1.1.5}/loopsage/em.py +0 -0
  18. {pyloopsage-1.1.3 → pyloopsage-1.1.5}/loopsage/forcefields/classic_sm_ff.xml +0 -0
  19. {pyloopsage-1.1.3 → pyloopsage-1.1.5}/loopsage/initial_structures.py +0 -0
  20. {pyloopsage-1.1.3 → pyloopsage-1.1.5}/loopsage/md.py +0 -0
  21. {pyloopsage-1.1.3 → pyloopsage-1.1.5}/loopsage/preproc.py +0 -0
  22. {pyloopsage-1.1.3 → pyloopsage-1.1.5}/loopsage/vizualization_tools.py +0 -0
  23. {pyloopsage-1.1.3 → pyloopsage-1.1.5}/pyLoopSage.egg-info/dependency_links.txt +0 -0
  24. {pyloopsage-1.1.3 → pyloopsage-1.1.5}/pyLoopSage.egg-info/entry_points.txt +0 -0
  25. {pyloopsage-1.1.3 → pyloopsage-1.1.5}/pyLoopSage.egg-info/top_level.txt +0 -0
  26. {pyloopsage-1.1.3 → pyloopsage-1.1.5}/setup.cfg +0 -0
@@ -0,0 +1 @@
1
+ exclude Dockerfile
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyLoopSage
3
- Version: 1.1.3
3
+ Version: 1.1.5
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
@@ -32,6 +32,7 @@ Requires-Dist: imageio
32
32
  Requires-Dist: imageio[ffmpeg]
33
33
  Requires-Dist: pillow
34
34
  Requires-Dist: pyBigWig
35
+ Requires-Dist: topoly
35
36
  Dynamic: author
36
37
  Dynamic: author-email
37
38
  Dynamic: classifier
@@ -126,6 +127,51 @@ In general the user can run simulation in two different ways:
126
127
 
127
128
  Can be easily installed with `pip install pyLoopSage`. To have CUDA acceleration, it is needed to have cuda-toolkit installed in case that you use nvidia drivers (otherwise you can use OpenCL or parallelization across CPU cores).
128
129
 
130
+ ## 🐳 Running LoopSage with Docker
131
+
132
+ To use LoopSage in a fully containerized and reproducible way, you can build and run it using Docker. This is a very efficient way when you want to use CUDA.
133
+
134
+ ### Step 1: Build the Docker Image
135
+
136
+ Clone the repository and build the image:
137
+
138
+ ```bash
139
+ docker build -t pyloopsage-cuda .
140
+ ```
141
+
142
+ The `Dockerfile` can be found in the GitHub repo of pyLoopSage.
143
+
144
+ ### Step 2: Run the Simulation
145
+
146
+ Use the following command to run your simulation:
147
+
148
+ ```bash
149
+ docker run --rm -it --gpus all \
150
+ -v "$PWD/config.ini:/app/config.ini:ro" \
151
+ -v "$PWD/tmp:/app/output" \
152
+ -v "$HOME/Data:/home/blackpianocat/Data:ro" \
153
+ pyloopsage-cuda \
154
+ python -m loopsage.run -c /app/config.ini
155
+ ```
156
+
157
+ **What this does:**
158
+
159
+ * `--rm`: Automatically removes the container after it finishes.
160
+ * `--gpus all`: It detects the gpus of the system.
161
+ * `-it`: Runs with an interactive terminal.
162
+ * `-v "$PWD/config.ini:/app/config.ini:ro"`: Mounts your local `config.ini` as read-only inside the container.
163
+ * `-v "$PWD/tmp:/app/output"`: Maps the `tmp/` directory for outputs.
164
+ * `-v "$HOME/Data:/home/blackpianocat/Data:ro"`: Mounts your full data directory so LoopSage can access input files.
165
+ * The final command runs LoopSage with your config file.
166
+
167
+ You do **not** need to manually stop or clean up anything—the container is temporary and self-destructs after it completes. The image (`pyloopsage-cuda`) remains available on your system and can be deleted anytime using:
168
+
169
+ ```bash
170
+ docker rmi pyloopsage-cuda
171
+ ```
172
+
173
+ **Note:** Install `nvidia-container-toolkit` in your system if you want to use the container with CUDA: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html
174
+
129
175
  ## How to use?
130
176
 
131
177
  ### Python Implementation
@@ -80,6 +80,51 @@ In general the user can run simulation in two different ways:
80
80
 
81
81
  Can be easily installed with `pip install pyLoopSage`. To have CUDA acceleration, it is needed to have cuda-toolkit installed in case that you use nvidia drivers (otherwise you can use OpenCL or parallelization across CPU cores).
82
82
 
83
+ ## 🐳 Running LoopSage with Docker
84
+
85
+ To use LoopSage in a fully containerized and reproducible way, you can build and run it using Docker. This is a very efficient way when you want to use CUDA.
86
+
87
+ ### Step 1: Build the Docker Image
88
+
89
+ Clone the repository and build the image:
90
+
91
+ ```bash
92
+ docker build -t pyloopsage-cuda .
93
+ ```
94
+
95
+ The `Dockerfile` can be found in the GitHub repo of pyLoopSage.
96
+
97
+ ### Step 2: Run the Simulation
98
+
99
+ Use the following command to run your simulation:
100
+
101
+ ```bash
102
+ docker run --rm -it --gpus all \
103
+ -v "$PWD/config.ini:/app/config.ini:ro" \
104
+ -v "$PWD/tmp:/app/output" \
105
+ -v "$HOME/Data:/home/blackpianocat/Data:ro" \
106
+ pyloopsage-cuda \
107
+ python -m loopsage.run -c /app/config.ini
108
+ ```
109
+
110
+ **What this does:**
111
+
112
+ * `--rm`: Automatically removes the container after it finishes.
113
+ * `--gpus all`: It detects the gpus of the system.
114
+ * `-it`: Runs with an interactive terminal.
115
+ * `-v "$PWD/config.ini:/app/config.ini:ro"`: Mounts your local `config.ini` as read-only inside the container.
116
+ * `-v "$PWD/tmp:/app/output"`: Maps the `tmp/` directory for outputs.
117
+ * `-v "$HOME/Data:/home/blackpianocat/Data:ro"`: Mounts your full data directory so LoopSage can access input files.
118
+ * The final command runs LoopSage with your config file.
119
+
120
+ You do **not** need to manually stop or clean up anything—the container is temporary and self-destructs after it completes. The image (`pyloopsage-cuda`) remains available on your system and can be deleted anytime using:
121
+
122
+ ```bash
123
+ docker rmi pyloopsage-cuda
124
+ ```
125
+
126
+ **Note:** Install `nvidia-container-toolkit` in your system if you want to use the container with CUDA: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html
127
+
83
128
  ## How to use?
84
129
 
85
130
  ### Python Implementation
@@ -1,4 +1,4 @@
1
1
  # Importing specific functions or classes from submodules
2
2
  from .run import main
3
3
 
4
- __version__ = "1.1.3"
4
+ __version__ = "1.1.5"
@@ -196,6 +196,7 @@ args = ListOfArgs([
196
196
  Arg('BIND_COEFF', help="CTCF binding coefficient.", type=float, default='1.0', val='1.0'),
197
197
  Arg('SAVE_PLOTS', help="It should be true in case that you would like to save diagnostic plots. In case that you use small MC_STEP or large N_STEPS is better to mark it as False.", type=bool, default='True', val='True'),
198
198
  Arg('SAVE_MDT', help="In case that you would liketo save metadata of the stochastic simulation.", type=bool, default='True', val='True'),
199
+ Arg('DETECT_KNOTS', help="In case that you would like to find out if there are knots in the structure.", type=bool, default='False', val='False'),
199
200
 
200
201
  # Molecular Dynamic Properties
201
202
  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'),
@@ -0,0 +1,210 @@
1
+ from scipy.interpolate import CubicSpline, interp1d
2
+ from scipy.spatial.distance import pdist, squareform
3
+ from scipy.integrate import dblquad
4
+ import topoly as tp
5
+ import numpy as np
6
+ from tqdm import tqdm
7
+ from .utils import *
8
+ import os
9
+
10
+ ################ Initial Knotted Structure Creation ######################
11
+
12
+ def resample_curve(points, N):
13
+ deltas = np.diff(points, axis=0)
14
+ dists = np.linalg.norm(deltas, axis=1)
15
+ cumulative = np.insert(np.cumsum(dists), 0, 0)
16
+ uniform_samples = np.linspace(0, cumulative[-1], N)
17
+ interp = interp1d(cumulative, points, axis=0)
18
+ return interp(uniform_samples)
19
+
20
+ def trefoil_knot(N=200, scale=0.5, offset=(0, 0, 0), rotation=None):
21
+ dense_N = 5000
22
+ t = np.linspace(0, 2 * np.pi, dense_N)
23
+ x = scale * (np.sin(t) + 2 * np.sin(2 * t)) + offset[0]
24
+ y = scale * (np.cos(t) - 2 * np.cos(2 * t)) + offset[1]
25
+ z = scale * (-np.sin(3 * t)) + offset[2]
26
+
27
+ V = np.vstack((x, y, z)).T
28
+ if rotation is not None:
29
+ V = np.dot(V, rotation)
30
+ return resample_curve(V, N)
31
+
32
+ def cinquefoil_knot(N=200, scale=0.5, offset=(0, 0, 0), rotation=None):
33
+ dense_N = 5000
34
+ t = np.linspace(0, 2 * np.pi, dense_N)
35
+ x = scale * (np.sin(2*t) + 2*np.sin(3*t))
36
+ y = scale * (np.cos(2*t) - 2*np.cos(3*t))
37
+ z = scale * (-np.sin(5*t))
38
+
39
+ V = np.vstack((x, y, z)).T
40
+ if rotation is not None:
41
+ V = np.dot(V, rotation)
42
+ return resample_curve(V + np.array(offset), N)
43
+
44
+ def random_rotation_matrix():
45
+ """Generate a random 3D rotation matrix."""
46
+ theta, phi, z = np.random.uniform(0, 2*np.pi-np.pi/36, 3)
47
+ r = np.sqrt(z)
48
+ V = np.array([
49
+ [np.cos(theta) * np.cos(phi), -np.sin(theta), np.cos(theta) * np.sin(phi)],
50
+ [np.sin(theta) * np.cos(phi), np.cos(theta), np.sin(theta) * np.sin(phi)],
51
+ [-np.sin(phi), 0, np.cos(phi)]
52
+ ])
53
+ return V
54
+
55
+ def smooth_linkage(start, end, N=50):
56
+ """Generate a smooth linking curve between two knots."""
57
+ t = np.linspace(0, 1, N)
58
+ x = (1 - t) * start[0] + t * end[0]
59
+ y = (1 - t) * start[1] + t * end[1]
60
+ z = (1 - t) * start[2] + t * end[2]
61
+ return np.vstack((x, y, z)).T
62
+
63
+ def smooth_spline_linkage(start, start_dir, end, end_dir, N=100):
64
+ """Generate a smooth cubic spline from `start` to `end` with given directional derivatives."""
65
+ t = np.array([0, 1])
66
+ x = np.array([start[0], end[0]])
67
+ y = np.array([start[1], end[1]])
68
+ z = np.array([start[2], end[2]])
69
+
70
+ # Use directional derivative constraints
71
+ cs_x = CubicSpline(t, x, bc_type=((1, start_dir[0]), (1, end_dir[0])))
72
+ cs_y = CubicSpline(t, y, bc_type=((1, start_dir[1]), (1, end_dir[1])))
73
+ cs_z = CubicSpline(t, z, bc_type=((1, start_dir[2]), (1, end_dir[2])))
74
+
75
+ tt = np.linspace(0, 1, N)
76
+ return np.vstack((cs_x(tt), cs_y(tt), cs_z(tt))).T
77
+
78
+ def random_unit_vector():
79
+ """Uniformly sample a unit vector on the 3D sphere."""
80
+ phi = np.random.uniform(0, 2*np.pi)
81
+ costheta = np.random.uniform(-0.1, 0.1)
82
+ sintheta = np.sqrt(1 - costheta**2)
83
+ return np.array([sintheta * np.cos(phi), sintheta * np.sin(phi), costheta])
84
+
85
+ def generate_knotted_structure(N=1000, num_knots=5):
86
+ """Generate a structure with `num_knots` along a random walk, total `N` points."""
87
+ if num_knots >= N:
88
+ raise ValueError("N must be much larger than num_knots")
89
+
90
+ points_per_knot = N // num_knots
91
+ structure = []
92
+ position = np.zeros(3)
93
+ radius_buffer = 2.0 # Step size for knot placement
94
+
95
+ for _ in range(num_knots):
96
+ # Move to next knot position
97
+ direction = random_unit_vector()
98
+ position = position + direction * radius_buffer
99
+
100
+ # Random rotation and scale
101
+ rotation = random_rotation_matrix()
102
+ scale = np.random.uniform(0.6, 1.0)
103
+
104
+ # Generate knot
105
+ knot_type = np.random.choice(["trefoil", "figure_eight"])
106
+ if knot_type == "trefoil":
107
+ knot = trefoil_knot(N=points_per_knot, scale=scale, offset=position, rotation=rotation)
108
+ else:
109
+ knot = cinquefoil_knot(N=points_per_knot, scale=scale, offset=position, rotation=rotation)
110
+
111
+ structure.append(knot)
112
+
113
+ curve = np.vstack(structure)
114
+ return resample_curve(curve, N) if len(curve) != N else curve
115
+
116
+ ############## Identification of Knots and Links ######################
117
+
118
+ def smooth_knot_spline(V, num_interp=200, closed=True):
119
+ """
120
+ Smoothly interpolates a 3D knot using a cubic spline.
121
+
122
+ Parameters:
123
+ V (numpy.ndarray): Nx3 array representing the 3D knot.
124
+ num_interp (int): Number of points in the interpolated knot.
125
+ closed (bool): Whether to enforce periodic boundary conditions for closed knots.
126
+
127
+ Returns:
128
+ numpy.ndarray: Smoothed Nx3 array of the interpolated knot.
129
+ """
130
+ if V.shape[1] != 3:
131
+ raise ValueError("Input array V must have shape (N, 3)")
132
+
133
+ if len(V) < 4:
134
+ raise ValueError("At least 4 points are required for spline fitting.")
135
+
136
+ # Define the parameter t along the curve
137
+ t = np.linspace(0, 1, len(V))
138
+
139
+ # Interpolation points
140
+ t_new = np.linspace(0, 1, num_interp)
141
+
142
+ # Fit cubic splines separately for each coordinate
143
+ cs_x = CubicSpline(t, V[:, 0], bc_type='periodic' if closed else 'not-a-knot')
144
+ cs_y = CubicSpline(t, V[:, 1], bc_type='periodic' if closed else 'not-a-knot')
145
+ cs_z = CubicSpline(t, V[:, 2], bc_type='periodic' if closed else 'not-a-knot')
146
+
147
+ # Evaluate the splines
148
+ x_new = cs_x(t_new)
149
+ y_new = cs_y(t_new)
150
+ z_new = cs_z(t_new)
151
+
152
+ return np.vstack((x_new, y_new, z_new)).T
153
+
154
+ def calculate_linking_number(V,ms,ns):
155
+ links = list()
156
+ for i in range(len(ms)):
157
+ for j in range(i+1,len(ms)):
158
+ if (ns[i]-ms[i])>5 and (ns[j]-ms[j])>5 and (((ms[i]<ns[i]) and (ns[i]<ms[j])) or ((ms[j]<ns[j]) and (ns[j]<ms[i]))):
159
+ loop1 = V[ms[i]:ns[i]]
160
+ loop1 = np.vstack((loop1,loop1[0,:]))
161
+ loop1 = smooth_knot_spline(loop1,2*len(loop1))
162
+ l1 = [list(loop1[i]) for i in range(len(loop1))]
163
+ loop2 = V[ms[j]:ns[j]]
164
+ loop2 = np.vstack((loop2,loop2[0,:]))
165
+ loop2 = smooth_knot_spline(loop2,2*len(loop2))
166
+ l2 = [list(loop2[i]) for i in range(len(loop2))]
167
+ links.append(tp.gln(l1,l2))
168
+ links = np.array(links)
169
+ links = links[links>0.5]
170
+ N_links = len(links)
171
+
172
+ return N_links, links
173
+
174
+ def link_number_ensemble(path, step=1, mode='MD', viz=False):
175
+ nlinks = list()
176
+ Ms = np.load(path+'/other/Ms.npy')
177
+ Ns = np.load(path+'/other/Ns.npy')
178
+
179
+ # Automatically determine the number of ensembles
180
+ ensemble_files = [f for f in os.listdir(path+'/ensemble') if f.startswith('MDLE_') and f.endswith('.cif')]
181
+ max_index = max(int(f.split('_')[1].split('.')[0]) for f in ensemble_files)
182
+
183
+ print('\nCalculating linking number of structures....')
184
+ for i in tqdm(range(step, max_index + 1, step)):
185
+ V = get_coordinates_cif(path+f'/ensemble/{mode}LE_{i}.cif')
186
+ ms, ns = Ms[:, i-1], Ns[:, i-1]
187
+ N_links, links = calculate_linking_number(V, ms, ns)
188
+ nlinks.append(N_links)
189
+ print('Done!')
190
+
191
+ avg_link_number = np.mean(nlinks)
192
+ std_link_number = np.std(nlinks)
193
+ if viz:
194
+ plt.figure()
195
+ plt.plot(np.arange(0, len(nlinks)*step, step), nlinks)
196
+ plt.xlabel('Step')
197
+ plt.ylabel('Number of links')
198
+ plt.savefig(path+'/plots/links.png')
199
+ plt.close()
200
+
201
+ plt.figure()
202
+ plt.hist(nlinks, bins=20, alpha=0.75, color='blue', edgecolor='black')
203
+ plt.xlabel('Number of links')
204
+ plt.ylabel('Frequency')
205
+ plt.title('Histogram of Number of Links')
206
+ plt.savefig(path+'/plots/links_histogram.png')
207
+ plt.close()
208
+ np.save(path+'/other/links.npy', nlinks)
209
+ print(f'Average number of links: {avg_link_number:.2f} ± {std_link_number:.2f}')
210
+ return avg_link_number, std_link_number, nlinks
@@ -101,60 +101,70 @@ def average_pooling(mat,dim_new):
101
101
  im_resized = np.array(im.resize(size))
102
102
  return im_resized
103
103
 
104
- def correlation_plot(given_heatmap,T_range,path):
105
- pearsons, spearmans, kendals = np.zeros(len(T_range)), np.zeros(len(T_range)), np.zeros(len(T_range))
106
- exp_heat_dim = len(given_heatmap)
107
- for i, T in enumerate(T_range):
108
- N_beads,N_coh,kappa,f,b = 500,30,20000,-2000,-2000
109
- N_steps, MC_step, burnin = int(1e4), int(1e2), 20
110
- L, R = binding_vectors_from_bedpe_with_peaks("/mnt/raid/data/Zofia_Trios/bedpe/hg00731_CTCF_pulled_2.bedpe",N_beads,[178421513,179491193],'chr1',False)
111
- sim = LoopSage(N_beads,N_coh,kappa,f,b,L,R)
112
- Es, Ms, Ns, Bs, Ks, Fs, ufs = sim.run_energy_minimization(N_steps,MC_step,burnin,T,mode='Metropolis',viz=True,vid=False)
113
- md = MD_LE(Ms,Ns,N_beads,burnin,MC_step)
114
- heat = md.run_pipeline(write_files=False,plots=False)
115
- if N_beads>exp_heat_dim:
116
- heat = average_pooling(heat,exp_heat_dim)
117
- L = exp_heat_dim
118
- else:
119
- given_heatmap = average_pooling(given_heatmap,N_beads)
120
- L = N_beads
121
- a, b = np.reshape(heat, (L**2, )), np.reshape(given_heatmap, (L**2, ))
122
- pearsons[i] = scipy.stats.pearsonr(a,b)[0]
123
- spearmans[i] = scipy.stats.spearmanr(a, b).correlation
124
- kendals[i] = scipy.stats.kendalltau(a, b).correlation
125
- print(f'\nTemperature:{T}, Pearson Correlation coefficient:{pearsons[i]}, Spearman:{spearmans[i]}, Kendal:{kendals[i]}\n\n')
126
-
127
- figure(figsize=(10, 8), dpi=600)
128
- plt.plot(T_range,pearsons,'bo-')
129
- plt.plot(T_range,spearmans,'ro-')
130
- plt.plot(T_range,kendals,'go-')
131
- # plt.plot(T_range,Cross,'go-')
132
- plt.ylabel('Correlation with Experimental Heatmap', fontsize=16)
133
- plt.xlabel('Temperature', fontsize=16)
134
- # plt.yscale('symlog')
135
- plt.legend(['Pearson','Spearman','Kendall Tau'])
136
- plt.grid()
137
- save_path = path+'/plots/pearson_plot.pdf' if path!=None else 'pearson_plot.pdf'
138
- plt.savefig(save_path,dpi=600)
139
- plt.close()
140
-
141
- def coh_traj_plot(ms,ns,N_beads,path):
104
+ def coh_traj_plot(ms, ns, N_beads, path, jump_threshold=200, min_stable_time=10):
105
+ """
106
+ Plots the trajectories of cohesins as filled regions between their two ends over time.
107
+
108
+ Parameters:
109
+ ms (list of arrays): List where each element is an array of left-end positions of a cohesin over time.
110
+ ns (list of arrays): List where each element is an array of right-end positions of a cohesin over time.
111
+ N_beads (int): Total number of beads (simulation sites) in the system.
112
+ path (str): Directory path where the plots will be saved.
113
+ jump_threshold (int, optional): Maximum allowed jump (in bead units) between consecutive time points for both ends.
114
+ If the jump between two consecutive positions exceeds this threshold for either end, that segment is considered a jump and is masked out.
115
+ Lower values make the criterion for erasing (masking) trajectories more strict (more segments are erased), higher values make it less strict.
116
+ min_stable_time (int, optional): Minimum number of consecutive time points required for a region to be considered stable and shown.
117
+ Shorter stable regions (less than this value) are erased (masked out).
118
+ Higher values make the criterion more strict (only longer stable regions are shown), lower values make it less strict.
119
+
120
+ The function highlights only stable, non-jumping regions of cohesin trajectories.
121
+ """
122
+ print('\nPlotting trajectories of cohesins...')
142
123
  N_coh = len(ms)
143
- figure(figsize=(18, 25))
144
- color = ["#"+''.join([rd.choice('0123456789ABCDEF') for j in range(6)]) for i in range(N_coh)]
145
- size = 0.01 if (N_beads > 500 or N_coh > 20) else 0.1
146
-
147
- ls = 'None'
148
- for nn in range(N_coh):
149
- tr_m, tr_n = ms[nn], ns[nn]
150
- plt.fill_between(np.arange(len(tr_m)), tr_m, tr_n, color=color[nn], alpha=0.4, interpolate=False, linewidth=0)
151
- plt.xlabel('Simulation Step', fontsize=24)
152
- plt.ylabel('Position of Cohesin', fontsize=24)
124
+ figure(figsize=(10, 10), dpi=200)
125
+ cmap = plt.get_cmap('prism')
126
+ colors = [cmap(i / N_coh) for i in range(N_coh)]
127
+
128
+ for nn in tqdm(range(N_coh)):
129
+ tr_m, tr_n = np.array(ms[nn]), np.array(ns[nn])
130
+ steps = np.arange(len(tr_m))
131
+
132
+ # Calculate jump size for tr_m and tr_n independently
133
+ jumps_m = np.abs(np.diff(tr_m))
134
+ jumps_n = np.abs(np.diff(tr_n))
135
+
136
+ # Create mask: True = good point, False = jump
137
+ jump_mask = np.ones_like(tr_m, dtype=bool)
138
+ jump_mask[1:] = (jumps_m < jump_threshold) & (jumps_n < jump_threshold) # both must be below threshold
139
+
140
+ # Now we want to detect stable regions
141
+ stable_mask = np.copy(jump_mask)
142
+
143
+ # Find connected regions
144
+ current_length = 0
145
+ for i in range(len(stable_mask)):
146
+ if jump_mask[i]:
147
+ current_length += 1
148
+ else:
149
+ if current_length < min_stable_time:
150
+ stable_mask[i-current_length:i] = False
151
+ current_length = 0
152
+ # Handle last region
153
+ if current_length < min_stable_time:
154
+ stable_mask[len(stable_mask)-current_length:] = False
155
+
156
+ # Apply mask
157
+ tr_m_masked = np.ma.masked_array(tr_m, mask=~stable_mask)
158
+ tr_n_masked = np.ma.masked_array(tr_n, mask=~stable_mask)
159
+
160
+ plt.fill_between(steps, tr_m_masked, tr_n_masked,
161
+ color=colors[nn], alpha=0.6, interpolate=False, linewidth=0)
162
+ plt.xlabel('MC Step', fontsize=16)
163
+ plt.ylabel('Simulation Beads', fontsize=16)
153
164
  plt.gca().invert_yaxis()
154
- save_path = path+'/plots/coh_trajectories.png' if path!=None else 'coh_trajectories.png'
155
- plt.savefig(save_path, format='png', dpi=200)
156
- save_path = path+'/plots/coh_trajectories.svg' if path!=None else 'coh_trajectories.svg'
157
- plt.savefig(save_path, format='svg', dpi=200)
165
+ plt.ylim((0, N_beads))
166
+ save_path = path + '/plots/LEFs.png'
167
+ plt.savefig(save_path, format='png',dpi=200)
158
168
  plt.close()
159
169
 
160
170
  def coh_probdist_plot(ms,ns,N_beads,path):
@@ -1,7 +1,6 @@
1
1
  from .stochastic_simulation import *
2
2
  from .args_definition import *
3
- import os
4
- import time
3
+ from .knots import *
5
4
  import argparse
6
5
  import configparser
7
6
  from typing import List
@@ -95,5 +94,9 @@ def main():
95
94
  else:
96
95
  IndentationError('Uknown simulation type. It can be either MD or EM.')
97
96
 
97
+ # Knoting
98
+ if args.DETECT_KNOTS:
99
+ link_number_ensemble(path=args.OUT_PATH, viz=args.SAVE_PLOTS, mode=args.SIMULATION_TYPE)
100
+
98
101
  if __name__=='__main__':
99
102
  main()
@@ -198,7 +198,16 @@ def run_simulation(N_beads, N_steps, MC_step, burnin, T, T_min, fold_norm, fold_
198
198
  Es, Ks, Fs, Bs, ufs = np.zeros(N_steps // MC_step, dtype=np.float64), np.zeros(N_steps // MC_step, dtype=np.float64), np.zeros(N_steps // MC_step, dtype=np.float64), np.zeros(N_steps // MC_step, dtype=np.float64), np.zeros(N_steps // MC_step, dtype=np.float64)
199
199
  Ms, Ns = np.zeros((N_lef + N_lef2, N_steps // MC_step), dtype=np.int64), np.zeros((N_lef + N_lef2, N_steps // MC_step), dtype=np.int64)
200
200
 
201
+ last_percent = -1
202
+
201
203
  for i in range(N_steps):
204
+ # Print progress every 5%
205
+ percent = int(100 * i / N_steps)
206
+ if percent % 5 == 0 and percent != last_percent:
207
+ # Numba can't use print with flush, so just print
208
+ print(f"Progress: {percent} % completed.")
209
+ last_percent = percent
210
+
202
211
  Ti = T - (T - T_min) * (i + 1) / N_steps if mode == 'Annealing' else T
203
212
  for j in range(N_lef + N_lef2):
204
213
  # Randomly choose a move (sliding or rebinding)
@@ -275,7 +284,7 @@ class StochasticSimulation:
275
284
  r = np.full(self.N_bws, -self.N_beads / 10) if not r and self.N_bws > 0 else (None if not r else r)
276
285
 
277
286
  # Run simulation
278
- print('\nRunning simulation (with parallelization across CPU cores)...')
287
+ print('\nRunning simulation (with numba acceleration)...')
279
288
  start = time.time()
280
289
  self.burnin = burnin
281
290
  self.Ms, self.Ns, self.Es, self.Ks, self.Fs, self.Bs, self.ufs = run_simulation(self.N_beads, N_steps, MC_step, burnin, T, T_min, fold_norm, fold_norm2, bind_norm, k_norm, self.N_lef, self.N_lef2, self.L, self.R, mode, lef_rw, lef_drift, cross_loop, r, self.N_bws, self.BWs, self.lef_track, between_families_penalty)
@@ -13,14 +13,23 @@ from scipy.stats.stats import pearsonr, spearmanr, kendalltau
13
13
  from tqdm import tqdm
14
14
 
15
15
  def make_folder(folder_name):
16
+ created = False
16
17
  if not os.path.exists(folder_name):
17
18
  os.mkdir(folder_name)
19
+ created = True
18
20
  elif os.path.isdir(folder_name):
19
- print(f'Directory with name "{folder_name}" already exists! No problem lets continue!')
21
+ print(f'\033[94mDirectory with name "{folder_name}" already exists! No problem, let\'s continue!\033[0m')
20
22
  else:
21
23
  raise IOError(f'File with name "{folder_name}" already exists! Please change the name of the folder!')
24
+
22
25
  for subfolder in ['plots', 'other', 'ensemble']:
23
- os.makedirs(os.path.join(folder_name, subfolder), exist_ok=True)
26
+ subfolder_path = os.path.join(folder_name, subfolder)
27
+ if not os.path.exists(subfolder_path):
28
+ os.makedirs(subfolder_path, exist_ok=True)
29
+ created = True
30
+
31
+ if created:
32
+ print(f'\033[92mAt least one directory was created!\033[0m')
24
33
  return folder_name
25
34
 
26
35
  ############# Creation of mmcif and psf files #############
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyLoopSage
3
- Version: 1.1.3
3
+ Version: 1.1.5
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
@@ -32,6 +32,7 @@ Requires-Dist: imageio
32
32
  Requires-Dist: imageio[ffmpeg]
33
33
  Requires-Dist: pillow
34
34
  Requires-Dist: pyBigWig
35
+ Requires-Dist: topoly
35
36
  Dynamic: author
36
37
  Dynamic: author-email
37
38
  Dynamic: classifier
@@ -126,6 +127,51 @@ In general the user can run simulation in two different ways:
126
127
 
127
128
  Can be easily installed with `pip install pyLoopSage`. To have CUDA acceleration, it is needed to have cuda-toolkit installed in case that you use nvidia drivers (otherwise you can use OpenCL or parallelization across CPU cores).
128
129
 
130
+ ## 🐳 Running LoopSage with Docker
131
+
132
+ To use LoopSage in a fully containerized and reproducible way, you can build and run it using Docker. This is a very efficient way when you want to use CUDA.
133
+
134
+ ### Step 1: Build the Docker Image
135
+
136
+ Clone the repository and build the image:
137
+
138
+ ```bash
139
+ docker build -t pyloopsage-cuda .
140
+ ```
141
+
142
+ The `Dockerfile` can be found in the GitHub repo of pyLoopSage.
143
+
144
+ ### Step 2: Run the Simulation
145
+
146
+ Use the following command to run your simulation:
147
+
148
+ ```bash
149
+ docker run --rm -it --gpus all \
150
+ -v "$PWD/config.ini:/app/config.ini:ro" \
151
+ -v "$PWD/tmp:/app/output" \
152
+ -v "$HOME/Data:/home/blackpianocat/Data:ro" \
153
+ pyloopsage-cuda \
154
+ python -m loopsage.run -c /app/config.ini
155
+ ```
156
+
157
+ **What this does:**
158
+
159
+ * `--rm`: Automatically removes the container after it finishes.
160
+ * `--gpus all`: It detects the gpus of the system.
161
+ * `-it`: Runs with an interactive terminal.
162
+ * `-v "$PWD/config.ini:/app/config.ini:ro"`: Mounts your local `config.ini` as read-only inside the container.
163
+ * `-v "$PWD/tmp:/app/output"`: Maps the `tmp/` directory for outputs.
164
+ * `-v "$HOME/Data:/home/blackpianocat/Data:ro"`: Mounts your full data directory so LoopSage can access input files.
165
+ * The final command runs LoopSage with your config file.
166
+
167
+ You do **not** need to manually stop or clean up anything—the container is temporary and self-destructs after it completes. The image (`pyloopsage-cuda`) remains available on your system and can be deleted anytime using:
168
+
169
+ ```bash
170
+ docker rmi pyloopsage-cuda
171
+ ```
172
+
173
+ **Note:** Install `nvidia-container-toolkit` in your system if you want to use the container with CUDA: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html
174
+
129
175
  ## How to use?
130
176
 
131
177
  ### Python Implementation
@@ -6,6 +6,7 @@ loopsage/__init__.py
6
6
  loopsage/args_definition.py
7
7
  loopsage/em.py
8
8
  loopsage/initial_structures.py
9
+ loopsage/knots.py
9
10
  loopsage/md.py
10
11
  loopsage/plots.py
11
12
  loopsage/preproc.py
@@ -18,3 +18,4 @@ imageio
18
18
  imageio[ffmpeg]
19
19
  pillow
20
20
  pyBigWig
21
+ topoly
@@ -6,7 +6,7 @@ with open('README.md', 'r', encoding='utf-8') as f:
6
6
 
7
7
  setup(
8
8
  name='pyLoopSage', # Package name
9
- version='1.1.3', # Version of the software
9
+ version='1.1.5', # Version of the software
10
10
  description='An energy-based stochastic model of loop extrusion in chromatin.',
11
11
  long_description=long_description,
12
12
  long_description_content_type='text/markdown',
@@ -40,6 +40,7 @@ setup(
40
40
  'imageio[ffmpeg]',
41
41
  'pillow',
42
42
  'pyBigWig',
43
+ 'topoly',
43
44
  ],
44
45
  entry_points={
45
46
  'console_scripts': [
@@ -1 +0,0 @@
1
- exclude loopsage/knots.py
File without changes
File without changes
File without changes
File without changes